├── .github ├── FUNDING.yml ├── dependabot.yaml ├── workflows │ ├── cve-scan.yml │ ├── test.yml │ └── e2e.yml └── actions │ ├── runner-cleanup │ └── action.yml │ └── kubeconform │ └── action.yml ├── .gitattributes ├── timoni ├── podinfo │ ├── cue.mod │ │ ├── module.cue │ │ ├── gen │ │ │ ├── k8s.io │ │ │ │ ├── api │ │ │ │ │ ├── core │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ ├── doc_go_gen.cue │ │ │ │ │ │ │ └── well_known_taints_go_gen.cue │ │ │ │ │ ├── apps │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── batch │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── policy │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ └── doc_go_gen.cue │ │ │ │ │ ├── node │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── events │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── storage │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── autoscaling │ │ │ │ │ │ ├── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ │ └── v2 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── rbac │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── admission │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── discovery │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ └── well_known_labels_go_gen.cue │ │ │ │ │ ├── networking │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ └── well_known_annotations_go_gen.cue │ │ │ │ │ ├── scheduling │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ └── types_go_gen.cue │ │ │ │ │ ├── certificates │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── coordination │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ └── types_go_gen.cue │ │ │ │ │ ├── authorization │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ ├── authentication │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ └── register_go_gen.cue │ │ │ │ │ └── admissionregistration │ │ │ │ │ │ └── v1 │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ └── doc_go_gen.cue │ │ │ │ ├── apimachinery │ │ │ │ │ └── pkg │ │ │ │ │ │ ├── runtime │ │ │ │ │ │ ├── embedded_go_gen.cue │ │ │ │ │ │ ├── types_proto_go_gen.cue │ │ │ │ │ │ ├── conversion_go_gen.cue │ │ │ │ │ │ ├── allocator_go_gen.cue │ │ │ │ │ │ ├── converter_go_gen.cue │ │ │ │ │ │ ├── negotiate_go_gen.cue │ │ │ │ │ │ ├── swagger_doc_generator_go_gen.cue │ │ │ │ │ │ ├── splice_go_gen.cue │ │ │ │ │ │ ├── helper_go_gen.cue │ │ │ │ │ │ ├── codec_go_gen.cue │ │ │ │ │ │ └── doc_go_gen.cue │ │ │ │ │ │ ├── types │ │ │ │ │ │ ├── doc_go_gen.cue │ │ │ │ │ │ ├── namespacedname_go_gen.cue │ │ │ │ │ │ ├── uid_go_gen.cue │ │ │ │ │ │ ├── patch_go_gen.cue │ │ │ │ │ │ └── nodename_go_gen.cue │ │ │ │ │ │ ├── apis │ │ │ │ │ │ └── meta │ │ │ │ │ │ │ └── v1 │ │ │ │ │ │ │ ├── register_go_gen.cue │ │ │ │ │ │ │ ├── duration_go_gen.cue │ │ │ │ │ │ │ ├── micro_time_go_gen.cue │ │ │ │ │ │ │ ├── time_go_gen.cue │ │ │ │ │ │ │ ├── time_proto_go_gen.cue │ │ │ │ │ │ │ ├── watch_go_gen.cue │ │ │ │ │ │ │ ├── meta_go_gen.cue │ │ │ │ │ │ │ └── group_version_go_gen.cue │ │ │ │ │ │ ├── watch │ │ │ │ │ │ ├── doc_go_gen.cue │ │ │ │ │ │ ├── filter_go_gen.cue │ │ │ │ │ │ ├── streamwatcher_go_gen.cue │ │ │ │ │ │ ├── mux_go_gen.cue │ │ │ │ │ │ └── watch_go_gen.cue │ │ │ │ │ │ ├── api │ │ │ │ │ │ └── resource │ │ │ │ │ │ │ ├── suffix_go_gen.cue │ │ │ │ │ │ │ ├── math_go_gen.cue │ │ │ │ │ │ │ └── amount_go_gen.cue │ │ │ │ │ │ └── util │ │ │ │ │ │ └── intstr │ │ │ │ │ │ └── intstr_go_gen.cue │ │ │ │ └── apiextensions-apiserver │ │ │ │ │ └── pkg │ │ │ │ │ └── apis │ │ │ │ │ └── apiextensions │ │ │ │ │ └── v1 │ │ │ │ │ ├── doc_go_gen.cue │ │ │ │ │ └── register_go_gen.cue │ │ │ └── monitoring.coreos.com │ │ │ │ └── prometheusrule │ │ │ │ └── v1 │ │ │ │ └── types_gen.cue │ │ └── pkg │ │ │ └── timoni.sh │ │ │ └── core │ │ │ └── v1alpha1 │ │ │ ├── action.cue │ │ │ ├── selector.cue │ │ │ ├── semver.cue │ │ │ ├── instance.cue │ │ │ └── requirements.cue │ ├── templates │ │ ├── serviceaccount.cue │ │ ├── servicemonitor.cue │ │ ├── service.cue │ │ ├── ingress.cue │ │ ├── hpa.cue │ │ └── job.cue │ ├── values.cue │ ├── debug_tool.cue │ ├── timoni.cue │ └── debug_values.cue └── bundles │ └── test.podinfo.cue ├── deploy ├── overlays │ ├── dev │ │ ├── namespace.yaml │ │ ├── labels.yaml │ │ └── kustomization.yaml │ ├── staging │ │ ├── namespace.yaml │ │ ├── labels.yaml │ │ └── kustomization.yaml │ └── production │ │ ├── namespace.yaml │ │ ├── labels.yaml │ │ └── kustomization.yaml ├── secure │ ├── common │ │ ├── namespace.yaml │ │ ├── service-account.yaml │ │ ├── cluster-issuer.yaml │ │ └── reconciler-rbac.yaml │ ├── frontend │ │ ├── service.yaml │ │ ├── certificate.yaml │ │ ├── hpa.yaml │ │ └── deployment.yaml │ └── backend │ │ ├── service.yaml │ │ ├── hpa.yaml │ │ └── deployment.yaml ├── webapp │ ├── common │ │ ├── namespace.yaml │ │ ├── service-account.yaml │ │ └── reconciler-rbac.yaml │ ├── frontend │ │ ├── service.yaml │ │ ├── hpa.yaml │ │ └── deployment.yaml │ └── backend │ │ ├── service.yaml │ │ ├── hpa.yaml │ │ └── deployment.yaml ├── bases │ ├── cache │ │ ├── redis.conf │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ └── deployment.yaml │ ├── backend │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ ├── hpa.yaml │ │ └── deployment.yaml │ └── frontend │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ ├── hpa.yaml │ │ └── deployment.yaml ├── README.md └── kind.sh ├── pkg ├── version │ └── version.go ├── signals │ ├── signal_windows.go │ ├── signal_posix.go │ ├── signal.go │ └── shutdown.go ├── api │ ├── grpc │ │ ├── echo │ │ │ └── echo.proto │ │ ├── env │ │ │ └── env.proto │ │ ├── headers │ │ │ └── headers.proto │ │ ├── delay │ │ │ └── delay.proto │ │ ├── panic │ │ │ └── panic.proto │ │ ├── version │ │ │ └── version.proto │ │ ├── status │ │ │ └── status.proto │ │ ├── env.go │ │ ├── token │ │ │ └── token.proto │ │ ├── panic.go │ │ ├── echo.go │ │ ├── delay.go │ │ ├── version.go │ │ ├── info │ │ │ └── info.proto │ │ ├── mock_grpc.go │ │ ├── headers.go │ │ ├── env_test.go │ │ ├── info_test.go │ │ ├── echo_test.go │ │ ├── token_test.go │ │ ├── version_test.go │ │ ├── status.go │ │ ├── headers_test.go │ │ ├── delay_test.go │ │ └── info.go │ └── http │ │ ├── panic.go │ │ ├── configs.go │ │ ├── env.go │ │ ├── headers.go │ │ ├── version.go │ │ ├── status_test.go │ │ ├── status.go │ │ ├── mock.go │ │ ├── token_test.go │ │ ├── env_test.go │ │ ├── info_test.go │ │ ├── delay_test.go │ │ ├── chunked_test.go │ │ ├── version_test.go │ │ ├── headers_test.go │ │ ├── index.go │ │ ├── logging.go │ │ ├── health_test.go │ │ ├── chunked.go │ │ ├── echo_test.go │ │ ├── info.go │ │ ├── delay.go │ │ ├── health.go │ │ ├── store.go │ │ ├── tracer.go │ │ └── echows.go └── fscache │ └── fscache.go ├── kustomize ├── kustomization.yaml ├── service.yaml ├── hpa.yaml └── deployment.yaml ├── test ├── test.sh ├── build.sh ├── e2e.sh └── deploy.sh ├── Dockerfile.base ├── .cosign ├── cosign.pub └── README.md ├── cloudbuild.yaml ├── charts └── podinfo │ ├── templates │ ├── redis │ │ ├── config.yaml │ │ ├── service.yaml │ │ └── deployment.yaml │ ├── serviceaccount.yaml │ ├── pdb.yaml │ ├── certificate.yaml │ ├── tests │ │ ├── fail.yaml │ │ ├── timeout.yaml │ │ ├── grpc.yaml │ │ ├── service.yaml │ │ ├── tls.yaml │ │ ├── jwt.yaml │ │ └── cache.yaml │ ├── servicemonitor.yaml │ ├── hpa.yaml │ ├── httproute.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── NOTES.txt │ ├── _helpers.tpl │ └── linkerd.yaml │ ├── Chart.yaml │ └── .helmignore ├── .notation ├── signingkeys.json ├── README.md ├── codesign.cnf ├── trustpolicy.json └── notation.crt ├── cmd └── podcli │ ├── version.go │ └── main.go ├── otel ├── otel-config.yaml ├── Makefile ├── docker-compose.yaml └── README.md ├── .gitignore ├── .goreleaser.yml ├── Dockerfile └── Dockerfile.xx /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: stefanprodan 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | timoni/podinfo/cue.mod/** linguist-vendored 2 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/module.cue: -------------------------------------------------------------------------------- 1 | module: "timoni.sh/podinfo" 2 | language: version: "v0.9.0" 3 | -------------------------------------------------------------------------------- /deploy/overlays/dev/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: dev 5 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var VERSION = "6.9.4" 4 | var REVISION = "unknown" 5 | -------------------------------------------------------------------------------- /deploy/secure/common/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: secure 5 | -------------------------------------------------------------------------------- /deploy/webapp/common/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: webapp 5 | -------------------------------------------------------------------------------- /deploy/bases/cache/redis.conf: -------------------------------------------------------------------------------- 1 | maxmemory 64mb 2 | maxmemory-policy allkeys-lru 3 | save "" 4 | appendonly no 5 | -------------------------------------------------------------------------------- /deploy/overlays/staging/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: staging 5 | -------------------------------------------------------------------------------- /deploy/overlays/production/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: production 5 | -------------------------------------------------------------------------------- /deploy/secure/common/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: secure 5 | namespace: secure 6 | -------------------------------------------------------------------------------- /deploy/webapp/common/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: webapp 5 | namespace: webapp 6 | -------------------------------------------------------------------------------- /pkg/signals/signal_windows.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var shutdownSignals = []os.Signal{os.Interrupt} 8 | -------------------------------------------------------------------------------- /deploy/secure/common/cluster-issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: self-signed 5 | spec: 6 | selfSigned: {} -------------------------------------------------------------------------------- /kustomize/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - hpa.yaml 5 | - deployment.yaml 6 | - service.yaml 7 | -------------------------------------------------------------------------------- /deploy/bases/backend/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - service.yaml 5 | - deployment.yaml 6 | - hpa.yaml 7 | 8 | -------------------------------------------------------------------------------- /deploy/bases/frontend/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - service.yaml 5 | - deployment.yaml 6 | - hpa.yaml 7 | 8 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #1 /usr/bin/env sh 2 | 3 | set -e 4 | 5 | # wait for podinfo 6 | kubectl rollout status deployment/podinfo --timeout=3m 7 | 8 | # test podinfo 9 | helm test podinfo 10 | -------------------------------------------------------------------------------- /Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM golang:1.25 2 | 3 | WORKDIR /workspace 4 | 5 | # copy modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | 9 | # cache modules 10 | RUN go mod download 11 | -------------------------------------------------------------------------------- /.cosign/cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEST+BqQ1XZhhVYx0YWQjdUJYIG5Lt 3 | iz2+UxRIqmKBqNmce2T+l45qyqOs99qfD7gLNGmkVZ4vtJ9bM7FxChFczg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/core/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/core/v1 4 | 5 | package v1 6 | 7 | #GroupName: "" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/apps/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/apps/v1 4 | 5 | package v1 6 | 7 | #GroupName: "apps" 8 | -------------------------------------------------------------------------------- /test/build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | set -e 4 | 5 | # build the docker file 6 | GIT_COMMIT=$(git rev-list -1 HEAD) && \ 7 | DOCKER_BUILDKIT=1 docker build --tag test/podinfo --build-arg "REVISION=${GIT_COMMIT}" . 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/batch/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/batch/v1 4 | 5 | package v1 6 | 7 | #GroupName: "batch" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/policy/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/policy/v1 4 | 5 | package v1 6 | 7 | #GroupName: "policy" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/node/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/node/v1 4 | 5 | package v1 6 | 7 | #GroupName: "node.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/events/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/events/v1 4 | 5 | package v1 6 | 7 | #GroupName: "events.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/storage/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/storage/v1 4 | 5 | package v1 6 | 7 | #GroupName: "storage.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/autoscaling/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/autoscaling/v1 4 | 5 | package v1 6 | 7 | #GroupName: "autoscaling" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/autoscaling/v2/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/autoscaling/v2 4 | 5 | package v2 6 | 7 | #GroupName: "autoscaling" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/core/v1/doc_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/core/v1 4 | 5 | // Package v1 is the v1 version of the core API. 6 | package v1 7 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/rbac/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/rbac/v1 4 | 5 | package v1 6 | 7 | #GroupName: "rbac.authorization.k8s.io" 8 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | args: ['build','-f' , 'Dockerfile', '-t', 'gcr.io/$PROJECT_ID/podinfo:$BRANCH_NAME-$SHORT_SHA', '.'] 4 | images: ['gcr.io/$PROJECT_ID/podinfo:$BRANCH_NAME-$SHORT_SHA'] 5 | -------------------------------------------------------------------------------- /deploy/overlays/dev/labels.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: builtin 2 | kind: LabelTransformer 3 | metadata: 4 | name: labels 5 | labels: 6 | env: dev 7 | instance: webapp 8 | fieldSpecs: 9 | - path: metadata/labels 10 | create: true 11 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/admission/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/admission/v1 4 | 5 | package v1 6 | 7 | #GroupName: "admission.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/discovery/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/discovery/v1 4 | 5 | package v1 6 | 7 | #GroupName: "discovery.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/networking/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/networking/v1 4 | 5 | package v1 6 | 7 | #GroupName: "networking.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/scheduling/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/scheduling/v1 4 | 5 | package v1 6 | 7 | #GroupName: "scheduling.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/certificates/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/certificates/v1 4 | 5 | package v1 6 | 7 | #GroupName: "certificates.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/coordination/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/coordination/v1 4 | 5 | package v1 6 | 7 | #GroupName: "coordination.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/embedded_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | _#encodable: _ 8 | -------------------------------------------------------------------------------- /deploy/overlays/staging/labels.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: builtin 2 | kind: LabelTransformer 3 | metadata: 4 | name: labels 5 | labels: 6 | env: staging 7 | instance: webapp 8 | fieldSpecs: 9 | - path: metadata/labels 10 | create: true 11 | -------------------------------------------------------------------------------- /pkg/signals/signal_posix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package signals 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGINT} 12 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/authorization/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/authorization/v1 4 | 5 | package v1 6 | 7 | #GroupName: "authorization.k8s.io" 8 | -------------------------------------------------------------------------------- /deploy/overlays/production/labels.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: builtin 2 | kind: LabelTransformer 3 | metadata: 4 | name: labels 5 | labels: 6 | env: production 7 | instance: webapp 8 | fieldSpecs: 9 | - path: metadata/labels 10 | create: true 11 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/authentication/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/authentication/v1 4 | 5 | package v1 6 | 7 | #GroupName: "authentication.k8s.io" 8 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | groups: 7 | actions: 8 | patterns: 9 | - "*" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /deploy/bases/cache/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - service.yaml 5 | - deployment.yaml 6 | configMapGenerator: 7 | - name: redis-config 8 | files: 9 | - redis.conf 10 | -------------------------------------------------------------------------------- /pkg/api/grpc/echo/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./echo"; 4 | 5 | package echo; 6 | 7 | message Message { 8 | string body = 1; 9 | } 10 | 11 | 12 | service EchoService { 13 | rpc Echo(Message) returns (Message) {} 14 | } -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/admissionregistration/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/admissionregistration/v1 4 | 5 | package v1 6 | 7 | #GroupName: "admissionregistration.k8s.io" 8 | -------------------------------------------------------------------------------- /deploy/bases/cache/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: cache 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: cache 9 | ports: 10 | - name: redis 11 | port: 6379 12 | protocol: TCP 13 | targetPort: redis 14 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/types/doc_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/types 4 | 5 | // Package types implements various generic types used throughout kubernetes. 6 | package types 7 | -------------------------------------------------------------------------------- /deploy/bases/frontend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: frontend 9 | ports: 10 | - name: http 11 | port: 80 12 | protocol: TCP 13 | targetPort: http 14 | -------------------------------------------------------------------------------- /deploy/overlays/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: dev 4 | resources: 5 | - ../../bases/backend 6 | - ../../bases/frontend 7 | - ../../bases/cache 8 | - namespace.yaml 9 | transformers: 10 | - labels.yaml 11 | -------------------------------------------------------------------------------- /deploy/overlays/staging/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: staging 4 | resources: 5 | - ../../bases/backend 6 | - ../../bases/frontend 7 | - ../../bases/cache 8 | - namespace.yaml 9 | transformers: 10 | - labels.yaml 11 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/doc_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 4 | 5 | // Package v1 is the v1 version of the API. 6 | package v1 7 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/types_proto_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | #ProtobufMarshaller: _ 8 | 9 | #ProtobufReverseMarshaller: _ 10 | -------------------------------------------------------------------------------- /deploy/overlays/production/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: production 4 | resources: 5 | - ../../bases/backend 6 | - ../../bases/frontend 7 | - ../../bases/cache 8 | - namespace.yaml 9 | transformers: 10 | - labels.yaml 11 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 4 | 5 | package v1 6 | 7 | #GroupName: "apiextensions.k8s.io" 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/register_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | #GroupName: "meta.k8s.io" 8 | 9 | #WatchEventKind: "WatchEvent" 10 | -------------------------------------------------------------------------------- /timoni/podinfo/templates/serviceaccount.cue: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | ) 6 | 7 | #ServiceAccount: corev1.#ServiceAccount & { 8 | _config: #Config 9 | apiVersion: "v1" 10 | kind: "ServiceAccount" 11 | metadata: _config.metadata 12 | } 13 | -------------------------------------------------------------------------------- /pkg/api/grpc/env/env.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./env"; 4 | 5 | package env; 6 | 7 | message EnvRequest {} 8 | 9 | message EnvResponse { 10 | repeated string envVars = 1; 11 | } 12 | 13 | service EnvService { 14 | rpc Env (EnvRequest) returns (EnvResponse) {} 15 | } -------------------------------------------------------------------------------- /deploy/webapp/frontend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend 5 | namespace: webapp 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: frontend 10 | ports: 11 | - name: http 12 | port: 80 13 | protocol: TCP 14 | targetPort: http 15 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/watch/doc_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/watch 4 | 5 | // Package watch contains a generic watchable interface, and a fake for 6 | // testing code that uses the watch interface. 7 | package watch 8 | -------------------------------------------------------------------------------- /charts/podinfo/templates/redis/config.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.redis.enabled -}} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-redis 6 | data: 7 | redis.conf: | 8 | maxmemory 64mb 9 | maxmemory-policy allkeys-lru 10 | save "" 11 | appendonly no 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/api/resource/suffix_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/api/resource 4 | 5 | package resource 6 | 7 | _#suffix: string 8 | 9 | // suffixer can interpret and construct suffixes. 10 | _#suffixer: _ 11 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/conversion_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | // Package runtime defines conversions between generic types and structs to map query strings 6 | // to struct objects. 7 | package runtime 8 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/types/namespacedname_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/types 4 | 5 | package types 6 | 7 | #NamespacedName: { 8 | Namespace: string 9 | Name: string 10 | } 11 | 12 | #Separator: 47 // '/' 13 | -------------------------------------------------------------------------------- /pkg/api/grpc/headers/headers.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./header"; 4 | 5 | package header; 6 | 7 | message HeaderRequest {} 8 | 9 | message HeaderResponse { 10 | repeated string headers = 1; 11 | } 12 | 13 | service HeaderService { 14 | rpc Header(HeaderRequest) returns (HeaderResponse) {} 15 | } 16 | -------------------------------------------------------------------------------- /.notation/signingkeys.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "stefanprodan.com", 3 | "keys": [ 4 | { 5 | "name": "stefanprodan.com", 6 | "keyPath": "/home/runner/.config/notation/localkeys/notation.key", 7 | "certPath": "/home/runner/.config/notation/localkeys/notation.crt" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /pkg/api/grpc/delay/delay.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./delay"; 4 | 5 | package delay; 6 | 7 | message DelayRequest { 8 | int64 seconds = 1; 9 | } 10 | 11 | message DelayResponse { 12 | int64 message = 1; 13 | } 14 | 15 | service DelayService { 16 | rpc Delay (DelayRequest) returns (DelayResponse) {} 17 | } -------------------------------------------------------------------------------- /pkg/api/grpc/panic/panic.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./panic"; 4 | 5 | package panic; 6 | 7 | // The greeting service definition. 8 | service PanicService { 9 | rpc Panic (PanicRequest) returns (PanicResponse) {} 10 | } 11 | 12 | message PanicRequest { 13 | } 14 | 15 | message PanicResponse { 16 | } 17 | 18 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/watch/filter_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/watch 4 | 5 | package watch 6 | 7 | // Recorder records all events that are sent from the watch until it is closed. 8 | #Recorder: { 9 | Interface: #Interface 10 | } 11 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/policy/v1/doc_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/policy/v1 4 | 5 | // Package policy is for any kind of policy object. Suitable examples, even if 6 | // they aren't all here, are PodDisruptionBudget, PodSecurityPolicy, 7 | // NetworkPolicy, etc. 8 | package v1 9 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/allocator_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // SimpleAllocator a wrapper around make([]byte) 8 | // conforms to the MemoryAllocator interface 9 | #SimpleAllocator: { 10 | } 11 | -------------------------------------------------------------------------------- /kustomize/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: podinfo 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: podinfo 9 | ports: 10 | - name: http 11 | port: 9898 12 | protocol: TCP 13 | targetPort: http 14 | - port: 9999 15 | targetPort: grpc 16 | protocol: TCP 17 | name: grpc 18 | -------------------------------------------------------------------------------- /pkg/api/grpc/version/version.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./version"; 4 | 5 | package version; 6 | 7 | 8 | service VersionService { 9 | rpc Version (VersionRequest) returns (VersionResponse) {} 10 | } 11 | 12 | 13 | message VersionRequest {} 14 | 15 | message VersionResponse { 16 | string version = 1; 17 | string commit = 2; 18 | } 19 | -------------------------------------------------------------------------------- /deploy/bases/backend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backend 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: backend 9 | ports: 10 | - name: http 11 | port: 9898 12 | protocol: TCP 13 | targetPort: http 14 | - port: 9999 15 | targetPort: grpc 16 | protocol: TCP 17 | name: grpc 18 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/converter_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // UnstructuredConverter is an interface for converting between interface{} 8 | // and map[string]interface representation. 9 | #UnstructuredConverter: _ 10 | -------------------------------------------------------------------------------- /pkg/api/http/panic.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | ) 7 | 8 | // Panic godoc 9 | // @Summary Panic 10 | // @Description crashes the process with exit code 255 11 | // @Tags HTTP API 12 | // @Router /panic [get] 13 | func (s *Server) panicHandler(w http.ResponseWriter, r *http.Request) { 14 | s.logger.Info("Panic command received") 15 | os.Exit(255) 16 | } 17 | -------------------------------------------------------------------------------- /deploy/secure/frontend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend 5 | namespace: secure 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: frontend 10 | ports: 11 | - name: http 12 | port: 80 13 | protocol: TCP 14 | targetPort: http 15 | - name: https 16 | port: 443 17 | protocol: TCP 18 | targetPort: https -------------------------------------------------------------------------------- /pkg/api/grpc/status/status.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./status"; 4 | 5 | package status; 6 | 7 | // The greeting service definition. 8 | service StatusService { 9 | rpc Status (StatusRequest) returns (StatusResponse) {} 10 | } 11 | 12 | message StatusRequest { 13 | string code = 1; 14 | } 15 | 16 | message StatusResponse { 17 | string status = 1; 18 | } 19 | -------------------------------------------------------------------------------- /charts/podinfo/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | version: 6.9.4 3 | appVersion: 6.9.4 4 | name: podinfo 5 | engine: gotpl 6 | description: Podinfo Helm chart for Kubernetes 7 | home: https://github.com/stefanprodan/podinfo 8 | maintainers: 9 | - email: stefanprodan@users.noreply.github.com 10 | name: stefanprodan 11 | sources: 12 | - https://github.com/stefanprodan/podinfo 13 | kubeVersion: ">=1.23.0-0" 14 | -------------------------------------------------------------------------------- /deploy/secure/backend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backend 5 | namespace: secure 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: backend 10 | ports: 11 | - name: http 12 | port: 9898 13 | protocol: TCP 14 | targetPort: http 15 | - port: 9999 16 | targetPort: grpc 17 | protocol: TCP 18 | name: grpc 19 | -------------------------------------------------------------------------------- /deploy/webapp/backend/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: backend 5 | namespace: webapp 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: backend 10 | ports: 11 | - name: http 12 | port: 9898 13 | protocol: TCP 14 | targetPort: http 15 | - port: 9999 16 | targetPort: grpc 17 | protocol: TCP 18 | name: grpc 19 | -------------------------------------------------------------------------------- /deploy/secure/frontend/certificate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: podinfo-frontend 5 | namespace: secure 6 | spec: 7 | dnsNames: 8 | - frontend 9 | - frontend.secure 10 | - frontend.secure.cluster.local 11 | - localhost 12 | secretName: podinfo-frontend-tls 13 | issuerRef: 14 | name: self-signed 15 | kind: ClusterIssuer 16 | -------------------------------------------------------------------------------- /charts/podinfo/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.enabled -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ template "podinfo.serviceAccountName" . }} 6 | labels: 7 | {{- include "podinfo.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.imagePullSecrets }} 9 | imagePullSecrets: 10 | {{- toYaml . | nindent 2 }} 11 | {{- end -}} 12 | {{- end -}} -------------------------------------------------------------------------------- /test/e2e.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | set -e 4 | 5 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) 6 | 7 | # run the build 8 | $SCRIPT_DIR/build.sh 9 | 10 | # create the kind cluster 11 | kind create cluster || true 12 | 13 | # load the docker image 14 | kind load docker-image test/podinfo:latest 15 | 16 | # run the deploy 17 | $SCRIPT_DIR/deploy.sh 18 | 19 | # run the tests 20 | $SCRIPT_DIR/test.sh 21 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/negotiate_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // NegotiateError is returned when a ClientNegotiator is unable to locate 8 | // a serializer for the requested operation. 9 | #NegotiateError: { 10 | ContentType: string 11 | Stream: bool 12 | } 13 | -------------------------------------------------------------------------------- /deploy/bases/backend/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: backend 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: backend 10 | minReplicas: 1 11 | maxReplicas: 2 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 99 19 | -------------------------------------------------------------------------------- /deploy/bases/frontend/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: frontend 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: frontend 10 | minReplicas: 1 11 | maxReplicas: 4 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 99 19 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/duration_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | // Duration is a wrapper around time.Duration which supports correct 8 | // marshaling to YAML and JSON. In particular, it marshals into strings, which 9 | // can be used as map keys in json. 10 | #Duration: _ 11 | -------------------------------------------------------------------------------- /charts/podinfo/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /cmd/podcli/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/stefanprodan/podinfo/pkg/version" 8 | ) 9 | 10 | func init() { 11 | rootCmd.AddCommand(versionCmd) 12 | } 13 | 14 | var versionCmd = &cobra.Command{ 15 | Use: `version`, 16 | Short: "Prints podcli version", 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | fmt.Println(version.VERSION) 19 | return nil 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/types/uid_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/types 4 | 5 | package types 6 | 7 | // UID is a type that holds unique ID values, including UUIDs. Because we 8 | // don't ONLY use UUIDs, this is an alias to string. Being a type captures 9 | // intent and helps make sure that UIDs and names do not get conflated. 10 | #UID: string 11 | -------------------------------------------------------------------------------- /deploy/secure/backend/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: backend 5 | namespace: secure 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: backend 11 | minReplicas: 1 12 | maxReplicas: 2 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 99 20 | -------------------------------------------------------------------------------- /deploy/webapp/backend/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: backend 5 | namespace: webapp 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: backend 11 | minReplicas: 1 12 | maxReplicas: 2 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 99 20 | -------------------------------------------------------------------------------- /deploy/webapp/frontend/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: frontend 5 | namespace: webapp 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: backend 11 | minReplicas: 1 12 | maxReplicas: 4 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 99 20 | -------------------------------------------------------------------------------- /pkg/api/grpc/env.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | "os" 7 | 8 | pb "github.com/stefanprodan/podinfo/pkg/api/grpc/env" 9 | ) 10 | 11 | type EnvServer struct { 12 | pb.UnimplementedEnvServiceServer 13 | config *Config 14 | logger *zap.Logger 15 | } 16 | 17 | func (s *EnvServer) Env(ctx context.Context, envInput *pb.EnvRequest) (*pb.EnvResponse, error) { 18 | return &pb.EnvResponse{EnvVars: os.Environ()}, nil 19 | } 20 | -------------------------------------------------------------------------------- /deploy/secure/frontend/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: frontend 5 | namespace: secure 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: frontend 11 | minReplicas: 1 12 | maxReplicas: 4 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 99 20 | -------------------------------------------------------------------------------- /otel/otel-config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | http: 6 | 7 | processors: 8 | 9 | exporters: 10 | jaeger: 11 | endpoint: jaeger:14250 12 | tls: 13 | insecure: true 14 | 15 | extensions: 16 | health_check: 17 | pprof: 18 | zpages: 19 | 20 | service: 21 | extensions: [health_check,pprof,zpages] 22 | pipelines: 23 | traces: 24 | receivers: [otlp] 25 | processors: [] 26 | exporters: [jaeger] 27 | -------------------------------------------------------------------------------- /timoni/podinfo/values.cue: -------------------------------------------------------------------------------- 1 | // Code generated by timoni. 2 | // Note that this file must have no imports and all values must be concrete. 3 | 4 | @if(!debug) 5 | 6 | package main 7 | 8 | // Defaults 9 | values: { 10 | image: { 11 | repository: "ghcr.io/stefanprodan/podinfo" 12 | tag: "6.9.4" 13 | digest: "" 14 | } 15 | test: image: { 16 | repository: "ghcr.io/curl/curl-container/curl-multi" 17 | tag: "master" 18 | digest: "" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/api/grpc/token/token.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./token"; 4 | 5 | package token; 6 | 7 | 8 | // The greeting service definition. 9 | service TokenService { 10 | rpc TokenGenerate (TokenRequest) returns (TokenResponse) {} 11 | rpc TokenValidate (TokenRequest) returns (TokenResponse) {} 12 | } 13 | 14 | message TokenRequest {} 15 | 16 | message TokenResponse { 17 | string token = 1; 18 | string expiresAt = 2; 19 | string message = 3; 20 | } -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/swagger_doc_generator_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // Pair of strings. We keed the name of fields and the doc 8 | #Pair: { 9 | Name: string 10 | Doc: string 11 | } 12 | 13 | // KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself 14 | #KubeTypes: [...#Pair] 15 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/splice_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // Splice is the interface that wraps the Splice method. 8 | // 9 | // Splice moves data from given slice without copying the underlying data for 10 | // efficiency purpose. Therefore, the caller should make sure the underlying 11 | // data is not changed later. 12 | #Splice: _ 13 | -------------------------------------------------------------------------------- /charts/podinfo/templates/redis/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.redis.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-redis 6 | labels: 7 | app: {{ template "podinfo.fullname" . }}-redis 8 | spec: 9 | type: ClusterIP 10 | selector: 11 | app: {{ template "podinfo.fullname" . }}-redis 12 | ports: 13 | - name: redis 14 | port: 6379 15 | protocol: TCP 16 | targetPort: redis 17 | appProtocol: redis 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /pkg/api/http/configs.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "net/http" 4 | 5 | func (s *Server) configReadHandler(w http.ResponseWriter, r *http.Request) { 6 | _, span := s.tracer.Start(r.Context(), "configReadHandler") 7 | defer span.End() 8 | 9 | files := make(map[string]string) 10 | if watcher != nil { 11 | watcher.Cache.Range(func(key interface{}, value interface{}) bool { 12 | files[key.(string)] = value.(string) 13 | return true 14 | }) 15 | } 16 | 17 | s.JSONResponse(w, r, files) 18 | } 19 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/admissionregistration/v1/doc_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/admissionregistration/v1 4 | 5 | // Package v1 is the v1 version of the API. 6 | // AdmissionConfiguration and AdmissionPluginConfiguration are legacy static admission plugin configuration 7 | // MutatingWebhookConfiguration and ValidatingWebhookConfiguration are for the 8 | // new dynamic admission controller configuration. 9 | package v1 10 | -------------------------------------------------------------------------------- /kustomize/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: podinfo 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: podinfo 10 | minReplicas: 2 11 | maxReplicas: 4 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | # scale up if usage is above 19 | # 99% of the requested CPU (100m) 20 | averageUtilization: 99 21 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/micro_time_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | #RFC3339Micro: "2006-01-02T15:04:05.000000Z07:00" 8 | 9 | // MicroTime is version of Time with microsecond level precision. 10 | // 11 | // +protobuf.options.marshal=false 12 | // +protobuf.as=Timestamp 13 | // +protobuf.options.(gogoproto.goproto_stringer)=false 14 | #MicroTime: _ 15 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/watch/streamwatcher_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/watch 4 | 5 | package watch 6 | 7 | // Decoder allows StreamWatcher to watch any stream for which a Decoder can be written. 8 | #Decoder: _ 9 | 10 | // Reporter hides the details of how an error is turned into a runtime.Object for 11 | // reporting on a watch stream since this package may not import a higher level report. 12 | #Reporter: _ 13 | -------------------------------------------------------------------------------- /charts/podinfo/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.podDisruptionBudget (gt (int .Values.replicaCount) 1) }} 2 | apiVersion: policy/v1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ include "podinfo.fullname" . }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | spec: 10 | selector: 11 | matchLabels: 12 | {{- include "podinfo.selectorLabels" . | nindent 6 }} 13 | {{- toYaml .Values.podDisruptionBudget | nindent 2 }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /pkg/api/grpc/panic.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | // "log" 6 | "os" 7 | 8 | pb "github.com/stefanprodan/podinfo/pkg/api/grpc/panic" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type PanicServer struct { 13 | pb.UnimplementedPanicServiceServer 14 | config *Config 15 | logger *zap.Logger 16 | } 17 | 18 | func (s *PanicServer) Panic(ctx context.Context, req *pb.PanicRequest) (*pb.PanicResponse, error) { 19 | s.logger.Info("Panic command received") 20 | os.Exit(225) 21 | return &pb.PanicResponse{}, nil 22 | } 23 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/api/resource/math_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/api/resource 4 | 5 | package resource 6 | 7 | // maxInt64Factors is the highest value that will be checked when removing factors of 10 from an int64. 8 | // It is also the maximum decimal digits that can be represented with an int64. 9 | _#maxInt64Factors: 18 10 | 11 | _#mostNegative: -9223372036854775808 12 | 13 | _#mostPositive: 9223372036854775807 14 | -------------------------------------------------------------------------------- /timoni/podinfo/templates/servicemonitor.cue: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | promv1 "monitoring.coreos.com/servicemonitor/v1" 5 | ) 6 | 7 | #ServiceMonitor: promv1.#ServiceMonitor & { 8 | _config: #Config 9 | metadata: _config.metadata 10 | spec: { 11 | endpoints: [{ 12 | path: "/metrics" 13 | port: "http-metrics" 14 | interval: "\(_config.monitoring.interval)s" 15 | }] 16 | namespaceSelector: matchNames: [_config.metadata.namespace] 17 | selector: matchLabels: _config.selector.labels 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | .DS_Store 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | .idea/ 18 | release/ 19 | build/ 20 | gcloud/ 21 | dist/ 22 | bin/ 23 | cue/cue.mod/gen/ 24 | cue/go.mod 25 | cue/go.sum 26 | 27 | .notation/podinfo.csr 28 | .notation/podinfo.key 29 | -------------------------------------------------------------------------------- /pkg/api/grpc/echo.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stefanprodan/podinfo/pkg/api/grpc/echo" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type echoServer struct { 11 | echo.UnimplementedEchoServiceServer 12 | config *Config 13 | logger *zap.Logger 14 | } 15 | 16 | func (s *echoServer) Echo(ctx context.Context, message *echo.Message) (*echo.Message, error) { 17 | 18 | s.logger.Info("Received message body from client:", zap.String("input body", message.Body)) 19 | return &echo.Message{Body: message.Body}, nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/api/http/env.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "os" 7 | ) 8 | 9 | // Env godoc 10 | // @Summary Environment 11 | // @Description returns the environment variables as a JSON array 12 | // @Tags HTTP API 13 | // @Accept json 14 | // @Produce json 15 | // @Router /env [get] 16 | // @Success 200 {object} http.ArrayResponse 17 | func (s *Server) envHandler(w http.ResponseWriter, r *http.Request) { 18 | _, span := s.tracer.Start(r.Context(), "envHandler") 19 | defer span.End() 20 | s.JSONResponse(w, r, os.Environ()) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/http/headers.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Headers godoc 8 | // @Summary Headers 9 | // @Description returns a JSON array with the request HTTP headers 10 | // @Tags HTTP API 11 | // @Accept json 12 | // @Produce json 13 | // @Router /headers [get] 14 | // @Success 200 {object} http.ArrayResponse 15 | func (s *Server) echoHeadersHandler(w http.ResponseWriter, r *http.Request) { 16 | _, span := s.tracer.Start(r.Context(), "echoHeadersHandler") 17 | defer span.End() 18 | s.JSONResponse(w, r, r.Header) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/grpc/delay.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | pb "github.com/stefanprodan/podinfo/pkg/api/grpc/delay" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type DelayServer struct { 12 | pb.UnimplementedDelayServiceServer 13 | config *Config 14 | logger *zap.Logger 15 | } 16 | 17 | func (s *DelayServer) Delay(ctx context.Context, delayInput *pb.DelayRequest) (*pb.DelayResponse, error) { 18 | 19 | time.Sleep(time.Duration(delayInput.Seconds) * time.Second) 20 | return &pb.DelayResponse{Message: delayInput.Seconds}, nil 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/grpc/version.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | pb "github.com/stefanprodan/podinfo/pkg/api/grpc/version" 7 | "github.com/stefanprodan/podinfo/pkg/version" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type VersionServer struct { 12 | pb.UnimplementedVersionServiceServer 13 | config *Config 14 | logger *zap.Logger 15 | } 16 | 17 | func (s *VersionServer) Version(ctx context.Context, req *pb.VersionRequest) (*pb.VersionResponse, error) { 18 | return &pb.VersionResponse{Version: version.VERSION, Commit: version.REVISION}, nil 19 | } 20 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/time_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | // Time is a wrapper around time.Time which supports correct 8 | // marshaling to YAML and JSON. Wrappers are provided for many 9 | // of the factory methods that the time package offers. 10 | // 11 | // +protobuf.options.marshal=false 12 | // +protobuf.as=Timestamp 13 | // +protobuf.options.(gogoproto.goproto_stringer)=false 14 | #Time: _ 15 | -------------------------------------------------------------------------------- /pkg/api/grpc/info/info.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./info"; 4 | 5 | package info; 6 | 7 | message InfoRequest {} 8 | 9 | message InfoResponse { 10 | string hostname = 1; 11 | string version = 2; 12 | string revision = 3; 13 | string color = 4 ; 14 | string logo = 5 ; 15 | string message = 6 ; 16 | string goos = 7; 17 | string goarch = 8; 18 | string runtime = 9; 19 | string numgoroutine = 10; 20 | string numcpu = 11; 21 | } 22 | 23 | service InfoService { 24 | rpc Info (InfoRequest) returns (InfoResponse) {} 25 | } -------------------------------------------------------------------------------- /.github/workflows/cve-scan.yml: -------------------------------------------------------------------------------- 1 | name: cve-scan 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "master" 8 | pull_request: 9 | branches: 10 | - "master" 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | govulncheck: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: ./.github/actions/runner-cleanup 21 | - name: Vulnerability scan 22 | id: govulncheck 23 | uses: golang/govulncheck-action@v1 24 | with: 25 | repo-checkout: false 26 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/networking/v1/well_known_annotations_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/networking/v1 4 | 5 | package v1 6 | 7 | // AnnotationIsDefaultIngressClass can be used to indicate that an 8 | // IngressClass should be considered default. When a single IngressClass 9 | // resource has this annotation set to true, new Ingress resources without a 10 | // class specified will be assigned this default class. 11 | #AnnotationIsDefaultIngressClass: "ingressclass.kubernetes.io/is-default-class" 12 | -------------------------------------------------------------------------------- /.notation/README.md: -------------------------------------------------------------------------------- 1 | # Podinfo signed releases 2 | 3 | Podinfo release assets such as the Helm chart and the Flux artifact 4 | are published to GitHub Container Registry and are signed with 5 | [Notation](https://github.com/notaryproject/notation). 6 | 7 | ## Generate signing keys 8 | 9 | Generate a new signing key pair: 10 | 11 | ```sh 12 | openssl genrsa -out podinfo.key 2048 13 | openssl req -new -key podinfo.key -out podinfo.csr -config codesign.cnf 14 | openssl x509 -req -days 1826 -in podinfo.csr -signkey podinfo.key -out notation.crt -extensions v3_req -extfile codesign.cnf 15 | ``` 16 | -------------------------------------------------------------------------------- /pkg/api/http/version.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/stefanprodan/podinfo/pkg/version" 7 | ) 8 | 9 | // Version godoc 10 | // @Summary Version 11 | // @Description returns podinfo version and git commit hash 12 | // @Tags HTTP API 13 | // @Produce json 14 | // @Router /version [get] 15 | // @Success 200 {object} http.MapResponse 16 | func (s *Server) versionHandler(w http.ResponseWriter, r *http.Request) { 17 | result := map[string]string{ 18 | "version": version.VERSION, 19 | "commit": version.REVISION, 20 | } 21 | s.JSONResponse(w, r, result) 22 | } 23 | -------------------------------------------------------------------------------- /.notation/codesign.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | default_keyfile = privatekey.pem 4 | distinguished_name = req_distinguished_name 5 | req_extensions = v3_req 6 | prompt = no 7 | 8 | [ req_distinguished_name ] 9 | C = RO 10 | ST = BU 11 | L = Bucharest 12 | O = Notary 13 | CN = stefanprodan.com 14 | 15 | [ v3_req ] 16 | keyUsage = critical,digitalSignature 17 | extendedKeyUsage = critical,codeSigning 18 | #subjectKeyIdentifier = hash 19 | -------------------------------------------------------------------------------- /charts/podinfo/templates/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.certificate.create -}} 2 | apiVersion: cert-manager.io/v1 3 | kind: Certificate 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | spec: 10 | dnsNames: 11 | {{- range .Values.certificate.dnsNames }} 12 | - {{ . | quote }} 13 | {{- end }} 14 | secretName: {{ template "podinfo.tlsSecretName" . }} 15 | issuerRef: 16 | {{- .Values.certificate.issuerRef | toYaml | trimSuffix "\n" | nindent 4 }} 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /otel/Makefile: -------------------------------------------------------------------------------- 1 | DC=docker-compose -f docker-compose.yaml 2 | 3 | .PHONY: help 4 | .DEFAULT_GOAL := help 5 | 6 | help: 7 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 8 | 9 | stop: ## Stop all Docker Containers run in Compose 10 | $(DC) stop 11 | 12 | clean: stop ## Clean all Docker Containers and Volumes 13 | $(DC) down --rmi local --remove-orphans -v 14 | $(DC) rm -f -v 15 | 16 | build: clean ## Rebuild the Docker Image for use by Compose 17 | $(DC) build 18 | 19 | run: stop ## Run the Application 20 | $(DC) up 21 | -------------------------------------------------------------------------------- /.notation/trustpolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "trustPolicies": [ 4 | { 5 | "name": "stefanprodan.com", 6 | "registryScopes": [ 7 | "ghcr.io/stefanprodan/podinfo-deploy", 8 | "ghcr.io/stefanprodan/charts/podinfo" 9 | ], 10 | "signatureVerification": { 11 | "level" : "strict" 12 | }, 13 | "trustStores": [ "ca:stefanprodan.com" ], 14 | "trustedIdentities": [ 15 | "x509.subject: C=RO, ST=BU, L=Bucharest, O=Notary, CN=stefanprodan.com" 16 | ] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/http/status_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestStatusHandler(t *testing.T) { 10 | req, err := http.NewRequest("GET", "/status/404", nil) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | rr := httptest.NewRecorder() 16 | srv := NewMockServer() 17 | 18 | srv.router.HandleFunc("/status/{code}", srv.statusHandler) 19 | srv.router.ServeHTTP(rr, req) 20 | 21 | // Check the status code is what we expect. 22 | if status := rr.Code; status != http.StatusNotFound { 23 | t.Errorf("handler returned wrong status code: got %v want %v", 24 | status, http.StatusNotFound) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /deploy/secure/common/reconciler-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: reconciler 5 | namespace: secure 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: reconciler 11 | namespace: secure 12 | rules: 13 | - apiGroups: ['*'] 14 | resources: ['*'] 15 | verbs: ['*'] 16 | --- 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: RoleBinding 19 | metadata: 20 | name: reconciler 21 | namespace: secure 22 | roleRef: 23 | apiGroup: rbac.authorization.k8s.io 24 | kind: Role 25 | name: reconciler 26 | subjects: 27 | - kind: ServiceAccount 28 | name: reconciler 29 | namespace: secure 30 | -------------------------------------------------------------------------------- /deploy/webapp/common/reconciler-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: reconciler 5 | namespace: webapp 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: Role 9 | metadata: 10 | name: reconciler 11 | namespace: webapp 12 | rules: 13 | - apiGroups: ['*'] 14 | resources: ['*'] 15 | verbs: ['*'] 16 | --- 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: RoleBinding 19 | metadata: 20 | name: reconciler 21 | namespace: webapp 22 | roleRef: 23 | apiGroup: rbac.authorization.k8s.io 24 | kind: Role 25 | name: reconciler 26 | subjects: 27 | - kind: ServiceAccount 28 | name: reconciler 29 | namespace: webapp 30 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/helper_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // MultiObjectTyper returns the types of objects across multiple schemes in order. 8 | #MultiObjectTyper: [...#ObjectTyper] 9 | 10 | _#defaultFramer: { 11 | } 12 | 13 | // WithVersionEncoder serializes an object and ensures the GVK is set. 14 | #WithVersionEncoder: { 15 | Version: #GroupVersioner 16 | Encoder: #Encoder 17 | ObjectTyper: #ObjectTyper 18 | } 19 | 20 | // WithoutVersionDecoder clears the group version kind of a deserialized object. 21 | #WithoutVersionDecoder: { 22 | Decoder: #Decoder 23 | } 24 | -------------------------------------------------------------------------------- /pkg/signals/signal.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | ) 7 | 8 | var onlyOneSignalHandler = make(chan struct{}) 9 | 10 | // SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned 11 | // which is closed on one of these signals. If a second signal is caught, the program 12 | // is terminated with exit code 1. 13 | func SetupSignalHandler() (stopCh <-chan struct{}) { 14 | close(onlyOneSignalHandler) // panics when called twice 15 | 16 | stop := make(chan struct{}) 17 | c := make(chan os.Signal, 2) 18 | signal.Notify(c, shutdownSignals...) 19 | go func() { 20 | <-c 21 | close(stop) 22 | <-c 23 | os.Exit(1) // second signal. Exit directly. 24 | }() 25 | 26 | return stop 27 | } 28 | -------------------------------------------------------------------------------- /pkg/api/grpc/mock_grpc.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | func NewMockGrpcServer() *Server { 8 | config := &Config{ 9 | Port: 9999, 10 | // ServerShutdownTimeout: 5 * time.Second, 11 | // HttpServerTimeout: 30 * time.Second, 12 | BackendURL: []string{}, 13 | ConfigPath: "/config", 14 | DataPath: "/data", 15 | // HttpClientTimeout: 30 * time.Second, 16 | UIColor: "blue", 17 | UIPath: ".ui", 18 | UIMessage: "Greetings", 19 | Hostname: "localhost", 20 | } 21 | 22 | logger, _ := zap.NewDevelopment() 23 | 24 | return &Server{ 25 | //router: mux.NewRouter(), 26 | logger: logger, 27 | config: config, 28 | //tracer: trace.NewNoopTracerProvider().Tracer("mock"), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/podcli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var rootCmd = &cobra.Command{ 14 | Use: "podcli", 15 | Short: "podinfo command line", 16 | Long: ` 17 | podinfo command line utilities`, 18 | } 19 | 20 | var ( 21 | logger *zap.Logger 22 | ) 23 | 24 | func main() { 25 | 26 | var err error 27 | logger, err = zap.NewDevelopment() 28 | if err != nil { 29 | log.Fatalf("can't initialize zap logger: %v", err) 30 | } 31 | defer logger.Sync() 32 | 33 | rootCmd.SetArgs(os.Args[1:]) 34 | if err := rootCmd.Execute(); err != nil { 35 | e := err.Error() 36 | fmt.Println(strings.ToUpper(e[:1]) + e[1:]) 37 | os.Exit(1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/actions/runner-cleanup/action.yml: -------------------------------------------------------------------------------- 1 | name: Runner Cleanup 2 | description: A GitHub Action for removing bloat from Ubuntu GitHub Actions runner. 3 | author: Stefan Prodan 4 | branding: 5 | color: blue 6 | icon: command 7 | runs: 8 | using: composite 9 | steps: 10 | - name: "Disk Usage Before Cleanup" 11 | shell: bash 12 | run: | 13 | df -h 14 | - name: "Remove .NET, Android and Haskell" 15 | shell: bash 16 | run: | 17 | sudo rm -rf /usr/share/dotnet || true 18 | sudo rm -rf /usr/local/lib/android || true 19 | sudo rm -rf /opt/ghc || true 20 | sudo rm -rf /usr/local/.ghcup || true 21 | - name: "Disk Usage After Cleanup" 22 | shell: bash 23 | run: | 24 | df -h 25 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/fail.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.faults.testFail }} 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-fault-test-{{ randAlphaNum 5 | lower }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | annotations: 10 | "helm.sh/hook": test-success 11 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 12 | sidecar.istio.io/inject: "false" 13 | linkerd.io/inject: disabled 14 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 15 | spec: 16 | containers: 17 | - name: fault 18 | image: alpine:3.11 19 | command: ['/bin/sh'] 20 | args: ['-c', 'exit 1'] 21 | restartPolicy: Never 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/podinfo/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled -}} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | {{- with .Values.serviceMonitor.additionalLabels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | endpoints: 14 | - path: /metrics 15 | port: http 16 | interval: {{ .Values.serviceMonitor.interval }} 17 | namespaceSelector: 18 | matchNames: 19 | - {{ include "podinfo.namespace" . }} 20 | selector: 21 | matchLabels: 22 | {{- include "podinfo.selectorLabels" . | nindent 6 }} 23 | {{- end }} 24 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/watch/mux_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/watch 4 | 5 | package watch 6 | 7 | // FullChannelBehavior controls how the Broadcaster reacts if a watcher's watch 8 | // channel is full. 9 | #FullChannelBehavior: int // #enumFullChannelBehavior 10 | 11 | #enumFullChannelBehavior: 12 | #WaitIfChannelFull | 13 | #DropIfChannelFull 14 | 15 | #values_FullChannelBehavior: { 16 | WaitIfChannelFull: #WaitIfChannelFull 17 | DropIfChannelFull: #DropIfChannelFull 18 | } 19 | 20 | #WaitIfChannelFull: #FullChannelBehavior & 0 21 | #DropIfChannelFull: #FullChannelBehavior & 1 22 | 23 | _#incomingQueueLength: 25 24 | 25 | _#internalRunFunctionMarker: "internal-do-function" 26 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/timeout.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.faults.testTimeout }} 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-fault-test-{{ randAlphaNum 5 | lower }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | annotations: 10 | "helm.sh/hook": test-success 11 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 12 | sidecar.istio.io/inject: "false" 13 | linkerd.io/inject: disabled 14 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 15 | spec: 16 | containers: 17 | - name: fault 18 | image: alpine:3.11 19 | command: ['/bin/sh'] 20 | args: ['-c', 'while sleep 3600; do :; done'] 21 | restartPolicy: Never 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/grpc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: {{ template "podinfo.fullname" . }}-grpc-test-{{ randAlphaNum 5 | lower }} 5 | namespace: {{ include "podinfo.namespace" . }} 6 | labels: 7 | {{- include "podinfo.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/hook": test-success 10 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 11 | sidecar.istio.io/inject: "false" 12 | linkerd.io/inject: disabled 13 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 14 | spec: 15 | containers: 16 | - name: grpc-health-probe 17 | image: stefanprodan/grpc_health_probe:v0.3.0 18 | command: ['grpc_health_probe'] 19 | args: ['-addr={{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.service.grpcPort }}'] 20 | restartPolicy: Never 21 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/types/patch_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/types 4 | 5 | package types 6 | 7 | // Similarly to above, these are constants to support HTTP PATCH utilized by 8 | // both the client and server that didn't make sense for a whole package to be 9 | // dedicated to. 10 | #PatchType: string // #enumPatchType 11 | 12 | #enumPatchType: 13 | #JSONPatchType | 14 | #MergePatchType | 15 | #StrategicMergePatchType | 16 | #ApplyPatchType 17 | 18 | #JSONPatchType: #PatchType & "application/json-patch+json" 19 | #MergePatchType: #PatchType & "application/merge-patch+json" 20 | #StrategicMergePatchType: #PatchType & "application/strategic-merge-patch+json" 21 | #ApplyPatchType: #PatchType & "application/apply-patch+yaml" 22 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/action.cue: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Stefan Prodan 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | // Action holds the list of annotations for controlling 7 | // Timoni's apply behaviour of Kubernetes resources. 8 | Action: { 9 | // Force annotation for recreating immutable resources such as Kubernetes Jobs. 10 | Force: { 11 | "action.timoni.sh/force": ActionStatus.Enabled 12 | } 13 | // One-off annotation for appling resources only if they don't exist on the cluster. 14 | Oneoff: { 15 | "action.timoni.sh/one-off": ActionStatus.Enabled 16 | } 17 | // Keep annotation for preventing Timoni's garbage collector from deleting resources. 18 | Keep: { 19 | "action.timoni.sh/prune": ActionStatus.Disabled 20 | } 21 | } 22 | 23 | ActionStatus: { 24 | Enabled: "enabled" 25 | Disabled: "disabled" 26 | } 27 | -------------------------------------------------------------------------------- /pkg/api/http/status.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "strconv" 7 | 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // Status godoc 12 | // @Summary Status code 13 | // @Description sets the response status code to the specified code 14 | // @Tags HTTP API 15 | // @Accept json 16 | // @Produce json 17 | // @Param code path int true "status code to return" 18 | // @Router /status/{code} [get] 19 | // @Success 200 {object} http.MapResponse 20 | func (s *Server) statusHandler(w http.ResponseWriter, r *http.Request) { 21 | _, span := s.tracer.Start(r.Context(), "statusHandler") 22 | defer span.End() 23 | 24 | vars := mux.Vars(r) 25 | 26 | code, err := strconv.Atoi(vars["code"]) 27 | if err != nil { 28 | s.ErrorResponse(w, r, span, err.Error(), http.StatusBadRequest) 29 | return 30 | } 31 | 32 | s.JSONResponseCode(w, r, map[string]int{"status": code}, code) 33 | } 34 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Stefan Prodan 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | // Selector defines the schema for Kubernetes Pod label selector used in Deployments, Services, Jobs, etc. 7 | #Selector: { 8 | // Name must be unique within a namespace. Is required when creating resources. 9 | // Name is primarily intended for creation idempotence and configuration definition. 10 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names 11 | #Name!: #InstanceName 12 | 13 | // Map of string keys and values that can be used to organize and categorize (scope and select) objects. 14 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels 15 | labels: #Labels 16 | 17 | // Standard Kubernetes label: app name. 18 | labels: "\(#StdLabelName)": #Name 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/http/mock.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "time" 5 | 6 | "go.opentelemetry.io/otel/trace" 7 | 8 | "github.com/gorilla/mux" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func NewMockServer() *Server { 13 | config := &Config{ 14 | Port: "9898", 15 | ServerShutdownTimeout: 5 * time.Second, 16 | HttpServerTimeout: 30 * time.Second, 17 | BackendURL: []string{}, 18 | ConfigPath: "/config", 19 | DataPath: "/data", 20 | HttpClientTimeout: 30 * time.Second, 21 | UIColor: "blue", 22 | UIPath: ".ui", 23 | UIMessage: "Greetings", 24 | Hostname: "localhost", 25 | } 26 | 27 | logger, _ := zap.NewDevelopment() 28 | 29 | return &Server{ 30 | router: mux.NewRouter(), 31 | logger: logger, 32 | config: config, 33 | tracer: trace.NewNoopTracerProvider().Tracer("mock"), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/http/token_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestTokenHandler(t *testing.T) { 12 | req, err := http.NewRequest("POST", "/token", strings.NewReader("test-user")) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | rr := httptest.NewRecorder() 18 | srv := NewMockServer() 19 | handler := http.HandlerFunc(srv.tokenGenerateHandler) 20 | 21 | handler.ServeHTTP(rr, req) 22 | 23 | // Check the status code is what we expect. 24 | if status := rr.Code; status != http.StatusOK { 25 | t.Errorf("handler returned wrong status code: got %v want %v", 26 | status, http.StatusOK) 27 | } 28 | 29 | var token TokenResponse 30 | if err := json.Unmarshal(rr.Body.Bytes(), &token); err != nil { 31 | t.Fatal(err) 32 | } 33 | if token.Token == "" { 34 | t.Error("handler returned no token") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/util/intstr/intstr_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/util/intstr 4 | 5 | package intstr 6 | 7 | // IntOrString is a type that can hold an int32 or a string. When used in 8 | // JSON or YAML marshalling and unmarshalling, it produces or consumes the 9 | // inner type. This allows you to have, for example, a JSON field that can 10 | // accept a name or number. 11 | // TODO: Rename to Int32OrString 12 | // 13 | // +protobuf=true 14 | // +protobuf.options.(gogoproto.goproto_stringer)=false 15 | // +k8s:openapi-gen=true 16 | #IntOrString: _ 17 | 18 | // Type represents the stored type of IntOrString. 19 | #Type: int64 // #enumType 20 | 21 | #enumType: 22 | #Int | 23 | #String 24 | 25 | #values_Type: { 26 | Int: #Int 27 | String: #String 28 | } 29 | 30 | #Int: #Type & 0 31 | #String: #Type & 1 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # xref: https://goreleaser.com/customization/project/ 4 | project_name: podinfo 5 | 6 | # xref: https://goreleaser.com/customization/hooks/ 7 | before: 8 | hooks: 9 | - go mod download 10 | 11 | # xref: https://goreleaser.com/customization/env/ 12 | env: 13 | - CGO_ENABLED=0 14 | 15 | # xref: https://goreleaser.com/customization/build/ 16 | builds: 17 | - main: ./cmd/podcli 18 | binary: podcli 19 | ldflags: -s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION={{.Commit}} 20 | goos: 21 | - windows 22 | - darwin 23 | - linux 24 | goarch: 25 | - amd64 26 | 27 | # xref: https://goreleaser.com/customization/archive/ 28 | archives: 29 | - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 30 | files: 31 | - LICENSE 32 | 33 | # xref: https://goreleaser.com/customization/changelog/ 34 | changelog: 35 | use: github-native 36 | -------------------------------------------------------------------------------- /pkg/api/http/env_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestEnvHandler(t *testing.T) { 11 | req, err := http.NewRequest("GET", "/api/env", nil) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | rr := httptest.NewRecorder() 17 | srv := NewMockServer() 18 | handler := http.HandlerFunc(srv.infoHandler) 19 | 20 | handler.ServeHTTP(rr, req) 21 | 22 | // Check the status code is what we expect. 23 | if status := rr.Code; status != http.StatusOK { 24 | t.Errorf("handler returned wrong status code: got %v want %v", 25 | status, http.StatusOK) 26 | } 27 | 28 | // Check the response body is what we expect. 29 | expected := ".*hostname.*" 30 | r := regexp.MustCompile(expected) 31 | if !r.MatchString(rr.Body.String()) { 32 | t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", 33 | rr.Body.String(), expected) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/http/info_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestInfoHandler(t *testing.T) { 11 | req, err := http.NewRequest("GET", "/api/info", nil) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | rr := httptest.NewRecorder() 17 | srv := NewMockServer() 18 | handler := http.HandlerFunc(srv.infoHandler) 19 | 20 | handler.ServeHTTP(rr, req) 21 | 22 | // Check the status code is what we expect. 23 | if status := rr.Code; status != http.StatusOK { 24 | t.Errorf("handler returned wrong status code: got %v want %v", 25 | status, http.StatusOK) 26 | } 27 | 28 | // Check the response body is what we expect. 29 | expected := ".*color.*blue.*" 30 | r := regexp.MustCompile(expected) 31 | if !r.MatchString(rr.Body.String()) { 32 | t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", 33 | rr.Body.String(), expected) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/http/delay_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestDelayHandler(t *testing.T) { 11 | req, err := http.NewRequest("GET", "/delay/0", nil) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | rr := httptest.NewRecorder() 17 | srv := NewMockServer() 18 | 19 | srv.router.HandleFunc("/delay/{wait}", srv.delayHandler) 20 | srv.router.ServeHTTP(rr, req) 21 | 22 | // Check the status code is what we expect. 23 | if status := rr.Code; status != http.StatusOK { 24 | t.Errorf("handler returned wrong status code: got %v want %v", 25 | status, http.StatusOK) 26 | } 27 | 28 | // Check the response body is what we expect. 29 | expected := ".*delay.*0.*" 30 | r := regexp.MustCompile(expected) 31 | if !r.MatchString(rr.Body.String()) { 32 | t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", 33 | rr.Body.String(), expected) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/http/chunked_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestChunkedHandler(t *testing.T) { 11 | req, err := http.NewRequest("GET", "/chunked/0", nil) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | rr := httptest.NewRecorder() 17 | srv := NewMockServer() 18 | 19 | srv.router.HandleFunc("/chunked/{wait}", srv.chunkedHandler) 20 | srv.router.ServeHTTP(rr, req) 21 | 22 | // Check the status code is what we expect. 23 | if status := rr.Code; status != http.StatusOK { 24 | t.Errorf("handler returned wrong status code: got %v want %v", 25 | status, http.StatusOK) 26 | } 27 | 28 | // Check the response body is what we expect. 29 | expected := ".*delay.*0.*" 30 | r := regexp.MustCompile(expected) 31 | if !r.MatchString(rr.Body.String()) { 32 | t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", 33 | rr.Body.String(), expected) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/api/grpc/headers.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | pb "github.com/stefanprodan/podinfo/pkg/api/grpc/headers" 8 | "go.uber.org/zap" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/metadata" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | type HeaderServer struct { 15 | pb.UnimplementedHeaderServiceServer 16 | config *Config 17 | logger *zap.Logger 18 | } 19 | 20 | func (s *HeaderServer) Header(ctx context.Context, in *pb.HeaderRequest) (*pb.HeaderResponse, error) { 21 | md, ok := metadata.FromIncomingContext(ctx) 22 | if !ok { 23 | return nil, status.Errorf(codes.DataLoss, "UnaryEcho: failed to get metadata") 24 | } 25 | 26 | // Creating slices beacause echoing the header metadata can't be predetermined by the proto contract 27 | res := []string{} 28 | for i, e := range md { 29 | res = append(res, i+"="+strings.Join(e, ",")) 30 | } 31 | 32 | return &pb.HeaderResponse{Headers: res}, nil 33 | 34 | } 35 | -------------------------------------------------------------------------------- /timoni/podinfo/templates/service.cue: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | ) 6 | 7 | #Service: corev1.#Service & { 8 | _config: #Config 9 | apiVersion: "v1" 10 | kind: "Service" 11 | metadata: _config.metadata 12 | metadata: { 13 | if _config.service.labels != _|_ { 14 | labels: _config.service.labels 15 | } 16 | if _config.service.annotations != _|_ { 17 | annotations: _config.service.annotations 18 | } 19 | } 20 | spec: corev1.#ServiceSpec & { 21 | type: corev1.#ServiceTypeClusterIP 22 | selector: _config.selector.labels 23 | ports: [ 24 | { 25 | name: "http" 26 | port: _config.service.port 27 | targetPort: "\(name)" 28 | protocol: "TCP" 29 | }, 30 | if _config.monitoring.enabled { 31 | { 32 | name: "http-metrics" 33 | port: 9797 34 | targetPort: "http-metrics" 35 | protocol: "TCP" 36 | } 37 | }, 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: {{ template "podinfo.fullname" . }}-service-test-{{ randAlphaNum 5 | lower }} 5 | namespace: {{ include "podinfo.namespace" . }} 6 | labels: 7 | {{- include "podinfo.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/hook": test-success 10 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 11 | sidecar.istio.io/inject: "false" 12 | linkerd.io/inject: disabled 13 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 14 | spec: 15 | containers: 16 | - name: curl 17 | image: curlimages/curl:7.69.0 18 | command: 19 | - sh 20 | - -c 21 | - | 22 | curl -s ${PODINFO_SVC}/api/info | grep version 23 | env: 24 | - name: PODINFO_SVC 25 | value: "{{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.service.externalPort }}" 26 | restartPolicy: Never 27 | -------------------------------------------------------------------------------- /pkg/api/http/version_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | func TestVersionHandler(t *testing.T) { 12 | req, err := http.NewRequest("GET", "/version", nil) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | rr := httptest.NewRecorder() 18 | srv := NewMockServer() 19 | handler := http.HandlerFunc(srv.versionHandler) 20 | 21 | handler.ServeHTTP(rr, req) 22 | 23 | // Check the status code is what we expect. 24 | if status := rr.Code; status != http.StatusOK { 25 | t.Errorf("handler returned wrong status code: got %v want %v", 26 | status, http.StatusOK) 27 | } 28 | 29 | // Check the response body is what we expect. 30 | expected := "unknown" 31 | r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected)) 32 | if !r.MatchString(rr.Body.String()) { 33 | t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", 34 | rr.Body.String(), expected) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/discovery/v1/well_known_labels_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/discovery/v1 4 | 5 | package v1 6 | 7 | // LabelServiceName is used to indicate the name of a Kubernetes service. 8 | #LabelServiceName: "kubernetes.io/service-name" 9 | 10 | // LabelManagedBy is used to indicate the controller or entity that manages 11 | // an EndpointSlice. This label aims to enable different EndpointSlice 12 | // objects to be managed by different controllers or entities within the 13 | // same cluster. It is highly recommended to configure this label for all 14 | // EndpointSlices. 15 | #LabelManagedBy: "endpointslice.kubernetes.io/managed-by" 16 | 17 | // LabelSkipMirror can be set to true on an Endpoints resource to indicate 18 | // that the EndpointSliceMirroring controller should not mirror this 19 | // resource with EndpointSlices. 20 | #LabelSkipMirror: "endpointslice.kubernetes.io/skip-mirror" 21 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/runtime/codec_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/runtime 4 | 5 | package runtime 6 | 7 | // codec binds an encoder and decoder. 8 | _#codec: { 9 | Encoder: #Encoder 10 | Decoder: #Decoder 11 | } 12 | 13 | // NoopEncoder converts an Decoder to a Serializer or Codec for code that expects them but only uses decoding. 14 | #NoopEncoder: { 15 | Decoder: #Decoder 16 | } 17 | 18 | _#noopEncoderIdentifier: #Identifier & "noop" 19 | 20 | // NoopDecoder converts an Encoder to a Serializer or Codec for code that expects them but only uses encoding. 21 | #NoopDecoder: { 22 | Encoder: #Encoder 23 | } 24 | 25 | _#base64Serializer: { 26 | Encoder: #Encoder 27 | Decoder: #Decoder 28 | } 29 | 30 | _#internalGroupVersionerIdentifier: "internal" 31 | _#disabledGroupVersionerIdentifier: "disabled" 32 | 33 | _#internalGroupVersioner: { 34 | } 35 | 36 | _#disabledGroupVersioner: { 37 | } 38 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/tls.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tls.enabled -}} 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-tls-test-{{ randAlphaNum 5 | lower }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | annotations: 10 | "helm.sh/hook": test-success 11 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 12 | sidecar.istio.io/inject: "false" 13 | linkerd.io/inject: disabled 14 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 15 | spec: 16 | containers: 17 | - name: curl 18 | image: curlimages/curl:7.69.0 19 | command: 20 | - sh 21 | - -c 22 | - | 23 | curl -sk ${PODINFO_SVC}/api/info | grep version 24 | env: 25 | - name: PODINFO_SVC 26 | value: "https://{{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.tls.port }}" 27 | restartPolicy: Never 28 | {{- end }} -------------------------------------------------------------------------------- /test/deploy.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | # install cert-manager 4 | kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.3/cert-manager.yaml 5 | 6 | # wait for cert manager 7 | kubectl -n cert-manager rollout status deployment/cert-manager --timeout=2m 8 | kubectl -n cert-manager rollout status deployment/cert-manager-webhook --timeout=2m 9 | kubectl -n cert-manager rollout status deployment/cert-manager-cainjector --timeout=2m 10 | 11 | # install self-signed certificate 12 | cat << 'EOF' | kubectl apply -f - 13 | apiVersion: cert-manager.io/v1 14 | kind: ClusterIssuer 15 | metadata: 16 | name: self-signed 17 | spec: 18 | selfSigned: {} 19 | EOF 20 | 21 | # install podinfo with tls enabled 22 | helm upgrade --install podinfo ./charts/podinfo \ 23 | --set image.repository=test/podinfo \ 24 | --set image.tag=latest \ 25 | --set tls.enabled=true \ 26 | --set certificate.create=true \ 27 | --set hpa.enabled=true \ 28 | --set hpa.cpu=95 \ 29 | --namespace=default 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25-alpine AS builder 2 | 3 | ARG REVISION 4 | 5 | RUN mkdir -p /podinfo/ 6 | 7 | WORKDIR /podinfo 8 | 9 | COPY . . 10 | 11 | RUN go mod download 12 | 13 | RUN CGO_ENABLED=0 go build -ldflags "-s -w \ 14 | -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ 15 | -a -o bin/podinfo cmd/podinfo/* 16 | 17 | RUN CGO_ENABLED=0 go build -ldflags "-s -w \ 18 | -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ 19 | -a -o bin/podcli cmd/podcli/* 20 | 21 | FROM alpine:3.23 22 | 23 | ARG BUILD_DATE 24 | ARG VERSION 25 | ARG REVISION 26 | 27 | LABEL maintainer="stefanprodan" 28 | 29 | RUN addgroup -S app \ 30 | && adduser -S -G app app \ 31 | && apk --no-cache add \ 32 | ca-certificates curl netcat-openbsd 33 | 34 | WORKDIR /home/app 35 | 36 | COPY --from=builder /podinfo/bin/podinfo . 37 | COPY --from=builder /podinfo/bin/podcli /usr/local/bin/podcli 38 | COPY ./ui ./ui 39 | RUN chown -R app:app ./ 40 | 41 | USER app 42 | 43 | CMD ["./podinfo"] 44 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/time_proto_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | // Timestamp is a struct that is equivalent to Time, but intended for 8 | // protobuf marshalling/unmarshalling. It is generated into a serialization 9 | // that matches Time. Do not use in Go structs. 10 | #Timestamp: { 11 | // Represents seconds of UTC time since Unix epoch 12 | // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 13 | // 9999-12-31T23:59:59Z inclusive. 14 | seconds: int64 @go(Seconds) @protobuf(1,varint,opt) 15 | 16 | // Non-negative fractions of a second at nanosecond resolution. Negative 17 | // second values with fractions must still have non-negative nanos values 18 | // that count forward in time. Must be from 0 to 999,999,999 19 | // inclusive. This field may be limited in precision depending on context. 20 | nanos: int32 @go(Nanos) @protobuf(2,varint,opt) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/http/headers_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | func TestEchoHeadersHandler(t *testing.T) { 12 | req, err := http.NewRequest("GET", "/api/headers", nil) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | req.Header.Set("X-Test", "testing") 17 | rr := httptest.NewRecorder() 18 | srv := NewMockServer() 19 | handler := http.HandlerFunc(srv.echoHeadersHandler) 20 | 21 | handler.ServeHTTP(rr, req) 22 | 23 | // Check the status code is what we expect. 24 | if status := rr.Code; status != http.StatusOK { 25 | t.Errorf("handler returned wrong status code: got %v want %v", 26 | status, http.StatusOK) 27 | } 28 | 29 | // Check the response body is what we expect. 30 | expected := "testing" 31 | r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected)) 32 | if !r.MatchString(rr.Body.String()) { 33 | t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", 34 | rr.Body.String(), expected) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/api/http/index.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "path" 7 | ) 8 | 9 | // Index godoc 10 | // @Summary Index 11 | // @Description renders podinfo UI 12 | // @Tags HTTP API 13 | // @Produce html 14 | // @Router / [get] 15 | // @Success 200 {string} string "OK" 16 | func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) { 17 | _, span := s.tracer.Start(r.Context(), "indexHandler") 18 | defer span.End() 19 | 20 | tmpl, err := template.New("vue.html").ParseFiles(path.Join(s.config.UIPath, "vue.html")) 21 | if err != nil { 22 | w.WriteHeader(http.StatusInternalServerError) 23 | w.Write([]byte(path.Join(s.config.UIPath, "vue.html") + err.Error())) 24 | return 25 | } 26 | 27 | data := struct { 28 | Title string 29 | Logo string 30 | }{ 31 | Title: s.config.Hostname, 32 | Logo: s.config.UILogo, 33 | } 34 | 35 | if err := tmpl.Execute(w, data); err != nil { 36 | http.Error(w, path.Join(s.config.UIPath, "vue.html")+err.Error(), http.StatusInternalServerError) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/watch_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | import ( 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/watch" 10 | ) 11 | 12 | // Event represents a single event to a watched resource. 13 | // 14 | // +protobuf=true 15 | // +k8s:deepcopy-gen=true 16 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 17 | #WatchEvent: { 18 | type: string @go(Type) @protobuf(1,bytes,opt) 19 | 20 | // Object is: 21 | // * If Type is Added or Modified: the new state of the object. 22 | // * If Type is Deleted: the state of the object immediately before deletion. 23 | // * If Type is Error: *Status is recommended; other types may make sense 24 | // depending on context. 25 | object: runtime.#RawExtension @go(Object) @protobuf(2,bytes,opt) 26 | } 27 | 28 | // InternalEvent makes watch.Event versioned 29 | // +protobuf=false 30 | #InternalEvent: watch.#Event 31 | -------------------------------------------------------------------------------- /pkg/api/http/logging.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.opentelemetry.io/otel/trace" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type LoggingMiddleware struct { 11 | logger *zap.Logger 12 | } 13 | 14 | func NewLoggingMiddleware(logger *zap.Logger) *LoggingMiddleware { 15 | return &LoggingMiddleware{ 16 | logger: logger, 17 | } 18 | } 19 | 20 | func (m *LoggingMiddleware) Handler(next http.Handler) http.Handler { 21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | fields := []zap.Field{ 23 | zap.String("proto", r.Proto), 24 | zap.String("uri", r.RequestURI), 25 | zap.String("method", r.Method), 26 | zap.String("remote", r.RemoteAddr), 27 | zap.String("user-agent", r.UserAgent()), 28 | } 29 | 30 | spanCtx := trace.SpanContextFromContext(r.Context()) 31 | if spanCtx.HasTraceID() { 32 | fields = append(fields, zap.String("trace_id", spanCtx.TraceID().String())) 33 | } 34 | 35 | m.logger.Debug( 36 | "request started", 37 | fields..., 38 | ) 39 | next.ServeHTTP(w, r) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /timoni/podinfo/templates/ingress.cue: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | netv1 "k8s.io/api/networking/v1" 5 | ) 6 | 7 | #Ingress: netv1.#Ingress & { 8 | _config: #Config 9 | apiVersion: "networking.k8s.io/v1" 10 | kind: "Ingress" 11 | metadata: _config.metadata 12 | metadata: { 13 | if _config.ingress.labels != _|_ { 14 | labels: _config.ingress.labels 15 | } 16 | if _config.ingress.annotations != _|_ { 17 | annotations: _config.ingress.annotations 18 | } 19 | } 20 | spec: netv1.#IngressSpec & { 21 | rules: [{ 22 | host: _config.ingress.host 23 | http: { 24 | paths: [{ 25 | pathType: "Prefix" 26 | path: "/" 27 | backend: service: { 28 | name: _config.metadata.name 29 | port: name: "http" 30 | } 31 | }] 32 | } 33 | }] 34 | if _config.ingress.tls { 35 | tls: [{ 36 | hosts: [_config.ingress.host] 37 | secretName: "\(_config.metadata.name)-cert" 38 | }] 39 | } 40 | if _config.ingress.className != _|_ { 41 | ingressClassName: _config.ingress.className 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/jwt.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: {{ template "podinfo.fullname" . }}-jwt-test-{{ randAlphaNum 5 | lower }} 5 | namespace: {{ include "podinfo.namespace" . }} 6 | labels: 7 | {{- include "podinfo.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/hook": test-success 10 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 11 | sidecar.istio.io/inject: "false" 12 | linkerd.io/inject: disabled 13 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 14 | spec: 15 | containers: 16 | - name: tools 17 | image: giantswarm/tiny-tools 18 | command: 19 | - sh 20 | - -c 21 | - | 22 | TOKEN=$(curl -sd 'test' ${PODINFO_SVC}/token | jq -r .token) && 23 | curl -sH "Authorization: Bearer ${TOKEN}" ${PODINFO_SVC}/token/validate | grep test 24 | env: 25 | - name: PODINFO_SVC 26 | value: "{{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.service.externalPort }}" 27 | restartPolicy: Never 28 | -------------------------------------------------------------------------------- /otel/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | podinfo_frontend: 5 | build: .. 6 | command: ./podinfo --backend-url http://podinfo_backend:9899/status/200 --otel-service-name=podinfo_frontend 7 | environment: 8 | - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel:4317 9 | ports: 10 | - "9898:9898" 11 | podinfo_backend: 12 | build: .. 13 | command: ./podinfo --port 9899 --otel-service-name=podinfo_backend 14 | environment: 15 | - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel:4317 16 | ports: 17 | - "9899:9899" 18 | otel: 19 | command: --config otel-config.yaml 20 | image: otel/opentelemetry-collector:0.41.0 21 | ports: 22 | - "4317:4317" 23 | volumes: 24 | - ${PWD}/otel-config.yaml:/otel-config.yaml 25 | jaeger: 26 | image: jaegertracing/all-in-one:1.29.0 27 | ports: 28 | - "5775:5775/udp" 29 | - "6831:6831/udp" 30 | - "6832:6832/udp" 31 | - "5778:5778" 32 | - "16686:16686" 33 | - "14268:14268" 34 | - "14250:14250" 35 | - "9411:9411" 36 | -------------------------------------------------------------------------------- /charts/podinfo/templates/tests/cache.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.cache }} 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-cache-test-{{ randAlphaNum 5 | lower }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | annotations: 10 | "helm.sh/hook": test 11 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 12 | sidecar.istio.io/inject: "false" 13 | linkerd.io/inject: disabled 14 | appmesh.k8s.aws/sidecarInjectorWebhook: disabled 15 | spec: 16 | containers: 17 | - name: curl 18 | image: curlimages/curl:7.69.0 19 | command: 20 | - sh 21 | - -c 22 | - | 23 | curl -sd 'data' ${PODINFO_SVC}/cache/test && 24 | curl -s ${PODINFO_SVC}/cache/test | grep data && 25 | curl -s -XDELETE ${PODINFO_SVC}/cache/test 26 | env: 27 | - name: PODINFO_SVC 28 | value: "{{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}:{{ .Values.service.externalPort }}" 29 | restartPolicy: Never 30 | {{- end }} 31 | -------------------------------------------------------------------------------- /timoni/podinfo/debug_tool.cue: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "list" 5 | "tool/cli" 6 | "encoding/yaml" 7 | "text/tabwriter" 8 | ) 9 | 10 | _resources: list.Concat([timoni.apply.app, timoni.apply.test]) 11 | 12 | // The build command generates the Kubernetes manifests and prints the multi-docs YAML to stdout. 13 | // Example 'cue cmd -t debug -t name=podinfo -t namespace=test -t mv=1.0.0 -t kv=1.28.0 build'. 14 | command: build: { 15 | task: print: cli.Print & { 16 | text: yaml.MarshalStream(_resources) 17 | } 18 | } 19 | 20 | // The ls command prints a table with the Kubernetes resources kind, namespace, name and version. 21 | // Example 'cue cmd -t debug -t name=podinfo -t namespace=test -t mv=1.0.0 -t kv=1.28.0 ls'. 22 | command: ls: { 23 | task: print: cli.Print & { 24 | text: tabwriter.Write([ 25 | "RESOURCE \tAPI VERSION", 26 | for r in _resources { 27 | if r.metadata.namespace == _|_ { 28 | "\(r.kind)/\(r.metadata.name) \t\(r.apiVersion)" 29 | } 30 | if r.metadata.namespace != _|_ { 31 | "\(r.kind)/\(r.metadata.namespace)/\(r.metadata.name) \t\(r.apiVersion)" 32 | } 33 | }, 34 | ]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/api/resource/amount_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/api/resource 4 | 5 | package resource 6 | 7 | // Scale is used for getting and setting the base-10 scaled value. 8 | // Base-2 scales are omitted for mathematical simplicity. 9 | // See Quantity.ScaledValue for more details. 10 | #Scale: int32 // #enumScale 11 | 12 | #enumScale: 13 | #Nano | 14 | #Micro | 15 | #Milli | 16 | #Kilo | 17 | #Mega | 18 | #Giga | 19 | #Tera | 20 | #Peta | 21 | #Exa 22 | 23 | #values_Scale: { 24 | Nano: #Nano 25 | Micro: #Micro 26 | Milli: #Milli 27 | Kilo: #Kilo 28 | Mega: #Mega 29 | Giga: #Giga 30 | Tera: #Tera 31 | Peta: #Peta 32 | Exa: #Exa 33 | } 34 | 35 | #Nano: #Scale & -9 36 | #Micro: #Scale & -6 37 | #Milli: #Scale & -3 38 | #Kilo: #Scale & 3 39 | #Mega: #Scale & 6 40 | #Giga: #Scale & 9 41 | #Tera: #Scale & 12 42 | #Peta: #Scale & 15 43 | #Exa: #Scale & 18 44 | 45 | // infDecAmount implements common operations over an inf.Dec that are specific to the quantity 46 | // representation. 47 | _#infDecAmount: string 48 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Stefan Prodan 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // SemVer validates the input version string and extracts the major and minor version numbers. 12 | // When Minimum is set, the major and minor parts must be greater or equal to the minimum 13 | // or a validation error is returned. 14 | #SemVer: { 15 | // Input version string in strict semver format. 16 | #Version!: string & =~"^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" 17 | 18 | // Minimum is the minimum allowed MAJOR.MINOR version. 19 | #Minimum: *"0.0.0" | string & =~"^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" 20 | 21 | let minMajor = strconv.Atoi(strings.Split(#Minimum, ".")[0]) 22 | let minMinor = strconv.Atoi(strings.Split(#Minimum, ".")[1]) 23 | 24 | major: int & >=minMajor 25 | major: strconv.Atoi(strings.Split(#Version, ".")[0]) 26 | 27 | minor: int & >=minMinor 28 | minor: strconv.Atoi(strings.Split(#Version, ".")[1]) 29 | } 30 | -------------------------------------------------------------------------------- /timoni/podinfo/templates/hpa.cue: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | autoscaling "k8s.io/api/autoscaling/v2" 5 | ) 6 | 7 | #HorizontalPodAutoscaler: autoscaling.#HorizontalPodAutoscaler & { 8 | _config: #Config 9 | apiVersion: "autoscaling/v2" 10 | kind: "HorizontalPodAutoscaler" 11 | metadata: _config.metadata 12 | spec: { 13 | scaleTargetRef: { 14 | apiVersion: "apps/v1" 15 | kind: "Deployment" 16 | name: _config.metadata.name 17 | } 18 | minReplicas: _config.autoscaling.minReplicas 19 | maxReplicas: _config.autoscaling.maxReplicas 20 | metrics: [ 21 | if _config.autoscaling.cpu > 0 { 22 | { 23 | type: "Resource" 24 | resource: { 25 | name: "cpu" 26 | target: { 27 | type: "Utilization" 28 | averageUtilization: _config.autoscaling.cpu 29 | } 30 | } 31 | } 32 | }, 33 | if _config.autoscaling.memory != "" { 34 | { 35 | type: "Resource" 36 | resource: { 37 | name: "memory" 38 | target: { 39 | type: "AverageValue" 40 | averageValue: _config.autoscaling.memory 41 | } 42 | } 43 | } 44 | }, 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /otel/README.md: -------------------------------------------------------------------------------- 1 | # Tracing Demo 2 | 3 | The directory contains sample [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) 4 | and [Jaeger](https://www.jaegertracing.io) configurations for a tracing demo. 5 | 6 | ## Configuration 7 | 8 | The provided [docker-compose.yaml](docker-compose.yaml) sets up 4 Containers 9 | 10 | 1. PodInfo Frontend on port 9898 11 | 2. PodInfo Backend on port 9899 12 | 3. OpenTelemetry Collector listening on port 4317 for GRPC 13 | 4. Jaeger all-in-one listening on multiple ports 14 | 15 | ## How does it work? 16 | 17 | The frontend pods are configured to call onto the backend pods. Both the podinfo 18 | pods are configured to send traces over to the collector at port 4317 using GRPC. 19 | The collector forwards all received spans to Jaeger over port 14250 and Jaeger 20 | exposes a UI over port `16686`. 21 | 22 | ## Running it locally 23 | 24 | 1. Start all the Containers 25 | ```shell 26 | make run 27 | ``` 28 | 2. Send some sample requests 29 | ```shell 30 | curl -v http://localhost:9898/status/200 31 | curl -X POST -v http://localhost:9898/api/echo 32 | ``` 33 | 3. Visit `http://localhost:16686/` to see the spans 34 | 4. Stop all the containers 35 | ```shell 36 | make stop 37 | ``` 38 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/instance.cue: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Stefan Prodan 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import "strings" 7 | 8 | // InstanceName defines the schema for the name of a Timoni instance. 9 | // The instance name is used as a Kubernetes label value and must be 63 characters or less. 10 | #InstanceName: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(63) 11 | 12 | // InstanceNamespace defines the schema for the namespace of a Timoni instance. 13 | // The instance namespace is used as a Kubernetes label value and must be 63 characters or less. 14 | #InstanceNamespace: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(63) 15 | 16 | // InstanceOwnerReference defines the schema for Kubernetes labels used to denote ownership. 17 | #InstanceOwnerReference: { 18 | #Name: "instance.timoni.sh/name" 19 | #Namespace: "instance.timoni.sh/namespace" 20 | } 21 | 22 | // InstanceModule defines the schema for the Module of a Timoni instance. 23 | #InstanceModule: { 24 | url: string & =~"^((oci|file)://.*)$" 25 | version: *"latest" | string 26 | digest?: string 27 | } 28 | -------------------------------------------------------------------------------- /charts/podinfo/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.hpa.enabled -}} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | spec: 10 | scaleTargetRef: 11 | apiVersion: apps/v1 12 | kind: Deployment 13 | name: {{ template "podinfo.fullname" . }} 14 | minReplicas: {{ .Values.replicaCount }} 15 | maxReplicas: {{ .Values.hpa.maxReplicas }} 16 | metrics: 17 | {{- if .Values.hpa.cpu }} 18 | - type: Resource 19 | resource: 20 | name: cpu 21 | target: 22 | type: Utilization 23 | averageUtilization: {{ .Values.hpa.cpu }} 24 | {{- end }} 25 | {{- if .Values.hpa.memory }} 26 | - type: Resource 27 | resource: 28 | name: memory 29 | target: 30 | type: AverageValue 31 | averageValue: {{ .Values.hpa.memory }} 32 | {{- end }} 33 | {{- if .Values.hpa.requests }} 34 | - type: Pods 35 | pods: 36 | metric: 37 | name: http_requests 38 | target: 39 | type: AverageValue 40 | averageValue: {{ .Values.hpa.requests }} 41 | {{- end }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /pkg/api/http/health_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestHealthzHandler(t *testing.T) { 10 | req, err := http.NewRequest("GET", "/healthz", nil) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | rr := httptest.NewRecorder() 16 | srv := NewMockServer() 17 | handler := http.HandlerFunc(srv.healthzHandler) 18 | 19 | handler.ServeHTTP(rr, req) 20 | 21 | // Check the status code is what we expect. 22 | if status := rr.Code; status != http.StatusServiceUnavailable { 23 | t.Errorf("handler returned wrong status code: got %v want %v", 24 | status, http.StatusServiceUnavailable) 25 | } 26 | } 27 | 28 | func TestReadyzHandler(t *testing.T) { 29 | req, err := http.NewRequest("GET", "/readyz", nil) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | rr := httptest.NewRecorder() 35 | srv := NewMockServer() 36 | handler := http.HandlerFunc(srv.readyzHandler) 37 | 38 | handler.ServeHTTP(rr, req) 39 | 40 | // Check the status code is what we expect. 41 | if status := rr.Code; status != http.StatusServiceUnavailable { 42 | t.Errorf("handler returned wrong status code: got %v want %v", 43 | status, http.StatusServiceUnavailable) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Deploy demo webapp 2 | 3 | Demo webapp manifests: 4 | 5 | - [common](webapp/common) 6 | - [frontend](webapp/frontend) 7 | - [backend](webapp/backend) 8 | 9 | Deploy the demo in `webapp` namespace: 10 | 11 | ```bash 12 | kubectl apply -f ./webapp/common 13 | kubectl apply -f ./webapp/backend 14 | kubectl apply -f ./webapp/frontend 15 | ``` 16 | 17 | Deploy the demo in the `dev` namespace: 18 | 19 | ```bash 20 | kustomize build ./overlays/dev | kubectl apply -f- 21 | ``` 22 | 23 | Deploy the demo in the `staging` namespace: 24 | 25 | ```bash 26 | kustomize build ./overlays/staging | kubectl apply -f- 27 | ``` 28 | 29 | Deploy the demo in the `production` namespace: 30 | 31 | ```bash 32 | kustomize build ./overlays/production | kubectl apply -f- 33 | ``` 34 | 35 | ## Testing Locally Using Kind 36 | 37 | > NOTE: You can install [kind from here](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) 38 | 39 | The following will create a new cluster called "podinfo" and configure host ports on 80 and 443. You can access the 40 | endpoints on localhost. The example also deploys cert-manager within the cluster along with a self-signed cluster issuer 41 | used to generate the certificate to validate the secure port. 42 | 43 | ```sh 44 | ./kind.sh 45 | ``` 46 | -------------------------------------------------------------------------------- /Dockerfile.xx: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.25 2 | ARG XX_VERSION=1.9.0 3 | 4 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx 5 | 6 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine as builder 7 | 8 | # Copy the build utilities. 9 | COPY --from=xx / / 10 | 11 | ARG TARGETPLATFORM 12 | ARG REVISION 13 | 14 | RUN mkdir -p /podinfo/ 15 | 16 | WORKDIR /podinfo 17 | 18 | COPY . . 19 | 20 | RUN go mod download 21 | 22 | ENV CGO_ENABLED=0 23 | RUN xx-go build -ldflags "-s -w \ 24 | -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ 25 | -a -o bin/podinfo cmd/podinfo/* 26 | 27 | RUN xx-go build -ldflags "-s -w \ 28 | -X github.com/stefanprodan/podinfo/pkg/version.REVISION=${REVISION}" \ 29 | -a -o bin/podcli cmd/podcli/* 30 | 31 | FROM alpine:3.23 32 | 33 | ARG BUILD_DATE 34 | ARG VERSION 35 | ARG REVISION 36 | 37 | LABEL maintainer="stefanprodan" 38 | 39 | RUN addgroup -S app \ 40 | && adduser -S -G app app \ 41 | && apk --no-cache add \ 42 | ca-certificates curl netcat-openbsd 43 | 44 | WORKDIR /home/app 45 | 46 | COPY --from=builder /podinfo/bin/podinfo . 47 | COPY --from=builder /podinfo/bin/podcli /usr/local/bin/podcli 48 | COPY ./ui ./ui 49 | RUN chown -R app:app ./ 50 | 51 | USER app 52 | 53 | CMD ["./podinfo"] 54 | -------------------------------------------------------------------------------- /charts/podinfo/templates/httproute.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.httpRoute.enabled -}} 2 | {{- $fullName := include "podinfo.fullname" . -}} 3 | {{- $svcPort := .Values.service.externalPort -}} 4 | apiVersion: gateway.networking.k8s.io/v1 5 | kind: HTTPRoute 6 | metadata: 7 | name: {{ $fullName }} 8 | namespace: {{ include "podinfo.namespace" . }} 9 | labels: 10 | {{- include "podinfo.labels" . | nindent 4 }} 11 | {{- with .Values.httpRoute.additionalLabels }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | {{- with .Values.httpRoute.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | parentRefs: 20 | {{- with .Values.httpRoute.parentRefs }} 21 | {{- toYaml . | nindent 4 }} 22 | {{- end }} 23 | {{- with .Values.httpRoute.hostnames }} 24 | hostnames: 25 | {{- toYaml . | nindent 4 }} 26 | {{- end }} 27 | rules: 28 | {{- range .Values.httpRoute.rules }} 29 | {{- with .matches }} 30 | - matches: 31 | {{- toYaml . | nindent 8 }} 32 | {{- end }} 33 | {{- with .filters }} 34 | filters: 35 | {{- toYaml . | nindent 8 }} 36 | {{- end }} 37 | backendRefs: 38 | - name: {{ $fullName }} 39 | port: {{ $svcPort }} 40 | weight: 1 41 | {{- end }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /.notation/notation.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDbDCCAlSgAwIBAgIUP7zhmTw5XTWLcgBGkBEsErMOkz4wDQYJKoZIhvcNAQEL 3 | BQAwWjELMAkGA1UEBhMCUk8xCzAJBgNVBAgMAkJVMRIwEAYDVQQHDAlCdWNoYXJl 4 | c3QxDzANBgNVBAoMBk5vdGFyeTEZMBcGA1UEAwwQc3RlZmFucHJvZGFuLmNvbTAe 5 | Fw0yNDAyMjUxMDAyMzZaFw0yOTAyMjQxMDAyMzZaMFoxCzAJBgNVBAYTAlJPMQsw 6 | CQYDVQQIDAJCVTESMBAGA1UEBwwJQnVjaGFyZXN0MQ8wDQYDVQQKDAZOb3Rhcnkx 7 | GTAXBgNVBAMMEHN0ZWZhbnByb2Rhbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB 8 | DwAwggEKAoIBAQDtH4oPi3SyX/DGv6NdjIvmApvD9eeSgsmHdwpAly8T9D2me+fx 9 | Z+wRNJmq4aq/A1anX+Sg28iwHzV+1WKpsHnjYzDAJSEYP2S8A5H1nGRKUoibdijw 10 | C3QBh5C75rjF/tmZVSX/Vgbf3HJJEsF4WUxWabLxoV2QLo7UlEsQd9+bSeKNMncx 11 | 1+E6FdbRCrYo90iobvZJ8K/S2zCWq/JTeHfTnmSEDhx6nMJcaSjvMPn3zyauWcQw 12 | dDpkcaGiJ64fEJRT2OFxXv9u+vDmIMKzo/Wjbd+IzFj6YY4VisK88aU7tmDelnk5 13 | gQB9eu62PFoaVsYJp4VOhblFKvGJpQwbWB9BAgMBAAGjKjAoMA4GA1UdDwEB/wQE 14 | AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEA 15 | 6x+C6hAIbLwMvkNx4K5p7Qe/pLQR0VwQFAw10yr/5KSN+YKFpon6pQ0TebL7qll+ 16 | uBGZvtQhN6v+DlnVqB7lvJKd+89isgirkkews5KwuXg7Gv5UPIugH0dXISZU8DMJ 17 | 7J4oKREv5HzdFmfsUfNlQcfyVTjKL6UINXfKGdqNNxXxR9b4a1TY2JcmEhzBTHaq 18 | ZqX6HK784a0dB7aHgeFrFwPCCP4M684Hs7CFbk3jo2Ef4ljnB5AyWpe8pwCLMdRt 19 | UjSjL5xJWVQvRU+STQsPr6SvpokPCG4rLQyjgeYYk4CCj5piSxbSUZFavq8v1y7Y 20 | m91USVqfeUX7ZzjDxPHE2A== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /.github/actions/kubeconform/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup kubeconform 2 | description: A GitHub Action for running kubeconform commands 3 | author: Stefan Prodan 4 | branding: 5 | color: blue 6 | icon: command 7 | inputs: 8 | version: 9 | description: "kubeconform version e.g. 0.5.0 (defaults to latest stable release)" 10 | required: false 11 | arch: 12 | description: "arch can be amd64 or arm64" 13 | required: true 14 | default: "amd64" 15 | runs: 16 | using: composite 17 | steps: 18 | - name: "Download binary to the GH runner cache" 19 | shell: bash 20 | run: | 21 | ARCH=${{ inputs.arch }} 22 | VERSION=${{ inputs.version }} 23 | 24 | if [ -z $VERSION ]; then 25 | VERSION=$(curl https://api.github.com/repos/yannh/kubeconform/releases/latest -sL | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) 26 | fi 27 | 28 | BIN_URL="https://github.com/yannh/kubeconform/releases/download/v${VERSION}/kubeconform-linux-${ARCH}.tar.gz" 29 | BIN_DIR=$RUNNER_TOOL_CACHE/kubeconform/$VERSION/$ARCH 30 | 31 | if [[ ! -x "$BIN_DIR/kind" ]]; then 32 | mkdir -p $BIN_DIR 33 | cd $BIN_DIR 34 | curl -sL $BIN_URL | tar xz 35 | chmod +x kubeconform 36 | fi 37 | 38 | echo "$BIN_DIR" >> "$GITHUB_PATH" 39 | -------------------------------------------------------------------------------- /pkg/api/http/chunked.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | // Chunked godoc 13 | // @Summary Chunked transfer encoding 14 | // @Description uses transfer-encoding type chunked to give a partial response and then waits for the specified period 15 | // @Tags HTTP API 16 | // @Accept json 17 | // @Produce json 18 | // @Param seconds path int true "seconds to wait for" 19 | // @Router /chunked/{seconds} [get] 20 | // @Success 200 {object} http.MapResponse 21 | func (s *Server) chunkedHandler(w http.ResponseWriter, r *http.Request) { 22 | _, span := s.tracer.Start(r.Context(), "chunkedHandler") 23 | defer span.End() 24 | 25 | vars := mux.Vars(r) 26 | 27 | delay, err := strconv.Atoi(vars["wait"]) 28 | if err != nil { 29 | delay = rand.Intn(int(s.config.HttpServerTimeout*time.Second)-10) + 10 30 | } 31 | 32 | flusher, ok := w.(http.Flusher) 33 | if !ok { 34 | s.ErrorResponse(w, r, span, "Streaming unsupported!", http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | w.Header().Set("Connection", "Keep-Alive") 39 | w.Header().Set("Transfer-Encoding", "chunked") 40 | w.Header().Set("X-Content-Type-Options", "nosniff") 41 | 42 | flusher.Flush() 43 | 44 | time.Sleep(time.Duration(delay) * time.Second) 45 | s.JSONResponse(w, r, map[string]int{"delay": delay}) 46 | 47 | flusher.Flush() 48 | } 49 | -------------------------------------------------------------------------------- /charts/podinfo/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "podinfo.fullname" . -}} 3 | {{- $svcPort := .Values.service.externalPort -}} 4 | apiVersion: networking.k8s.io/v1 5 | kind: Ingress 6 | metadata: 7 | name: {{ $fullName }} 8 | namespace: {{ include "podinfo.namespace" . }} 9 | labels: 10 | {{- include "podinfo.labels" . | nindent 4 }} 11 | {{- with .Values.ingress.additionalLabels }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | ingressClassName: {{ .Values.ingress.className }} 20 | {{- if .Values.ingress.tls }} 21 | tls: 22 | {{- range .Values.ingress.tls }} 23 | - hosts: 24 | {{- range .hosts }} 25 | - {{ . | quote }} 26 | {{- end }} 27 | secretName: {{ .secretName }} 28 | {{- end }} 29 | {{- end }} 30 | rules: 31 | {{- range .Values.ingress.hosts }} 32 | - host: {{ .host | quote }} 33 | http: 34 | paths: 35 | {{- range .paths }} 36 | - path: {{ .path }} 37 | pathType: {{ .pathType }} 38 | backend: 39 | service: 40 | name: {{ $fullName }} 41 | port: 42 | number: {{ $svcPort }} 43 | {{- end }} 44 | {{- end }} 45 | {{- end }} 46 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/requirements.cue: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Stefan Prodan 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // CPUQuantity is a string that is validated as a quantity of CPU, such as 100m or 2000m. 12 | #CPUQuantity: string & =~"^[1-9]\\d*m$" 13 | 14 | // MemoryQuantity is a string that is validated as a quantity of memory, such as 128Mi or 2Gi. 15 | #MemoryQuantity: string & =~"^[1-9]\\d*(Mi|Gi)$" 16 | 17 | // ResourceRequirement defines the schema for the CPU and Memory resource requirements. 18 | #ResourceRequirement: { 19 | cpu?: #CPUQuantity 20 | memory?: #MemoryQuantity 21 | } 22 | 23 | // ResourceRequirements defines the schema for the compute resource requirements of a container. 24 | // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/. 25 | #ResourceRequirements: { 26 | // Limits describes the maximum amount of compute resources allowed. 27 | limits?: #ResourceRequirement 28 | 29 | // Requests describes the minimum amount of compute resources required. 30 | // Requests cannot exceed Limits. 31 | requests?: #ResourceRequirement & { 32 | if limits != _|_ { 33 | if limits.cpu != _|_ { 34 | _lc: strconv.Atoi(strings.Split(limits.cpu, "m")[0]) 35 | _rc: strconv.Atoi(strings.Split(requests.cpu, "m")[0]) 36 | #cpu: int & >=_rc & _lc 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /charts/podinfo/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.service.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }} 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | {{- with .Values.service.additionalLabels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.service.annotations }} 13 | annotations: 14 | {{ toYaml . | indent 4 }} 15 | {{- end }} 16 | spec: 17 | type: {{ .Values.service.type }} 18 | ports: 19 | - port: {{ .Values.service.externalPort }} 20 | targetPort: http 21 | protocol: TCP 22 | name: http 23 | {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} 24 | nodePort: {{ .Values.service.nodePort }} 25 | {{- end }} 26 | {{- if .Values.tls.enabled }} 27 | - port: {{ .Values.tls.port | default 9899 }} 28 | targetPort: https 29 | protocol: TCP 30 | name: https 31 | {{- end }} 32 | {{- if .Values.service.grpcPort }} 33 | - port: {{ .Values.service.grpcPort }} 34 | targetPort: grpc 35 | protocol: TCP 36 | name: grpc 37 | {{- end }} 38 | selector: 39 | {{- include "podinfo.selectorLabels" . | nindent 4 }} 40 | {{- if .Values.service.trafficDistribution }} 41 | trafficDistribution: {{ .Values.service.trafficDistribution }} 42 | {{- end }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /charts/podinfo/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ include "podinfo.namespace" . }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "podinfo.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ include "podinfo.namespace" . }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get svc -w {{ template "podinfo.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ include "podinfo.namespace" . }} {{ template "podinfo.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 16 | echo http://$SERVICE_IP:{{ .Values.service.externalPort }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | echo "Visit http://127.0.0.1:8080 to use your application" 19 | kubectl -n {{ include "podinfo.namespace" . }} port-forward deploy/{{ template "podinfo.fullname" . }} 8080:{{ .Values.service.externalPort }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /deploy/kind.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | mkdir -p bin 4 | cat > ./bin/kind.yaml < uses in-memory connection instead of system network I/O 22 | lis := bufconn.Listen(1024 * 1024) 23 | t.Cleanup(func() { 24 | lis.Close() 25 | }) 26 | 27 | srv := grpc.NewServer() 28 | t.Cleanup(func() { 29 | srv.Stop() 30 | }) 31 | 32 | delay.RegisterDelayServiceServer(srv, &DelayServer{}) 33 | 34 | go func() { 35 | if err := srv.Serve(lis); err != nil { 36 | log.Fatalf("srv.Serve %v", err) 37 | } 38 | }() 39 | 40 | // - Test 41 | dialer := func(context.Context, string) (net.Conn, error) { 42 | return lis.Dial() 43 | } 44 | 45 | ctx := context.Background() 46 | 47 | conn, err := grpc.DialContext(ctx, "", grpc.WithContextDialer(dialer), grpc.WithInsecure()) 48 | t.Cleanup(func() { 49 | conn.Close() 50 | }) 51 | 52 | if err != nil { 53 | t.Fatalf("grpc.DialContext %v", err) 54 | } 55 | 56 | client := delay.NewDelayServiceClient(conn) 57 | res, err := client.Delay(context.Background(), &delay.DelayRequest{Seconds: 3}) 58 | 59 | // Check the status code is what we expect. 60 | if _, ok := status.FromError(err); !ok { 61 | t.Errorf("Delay returned type %T, want %T", err, status.Error) 62 | } 63 | 64 | if res != nil { 65 | fmt.Printf("res %v\n", res) 66 | } 67 | 68 | // Check the response body is what we expect. Here we expect the response to be "3" as the delay is set to 3 seconds. 69 | expected := "3" 70 | r := regexp.MustCompile(expected) 71 | if !r.MatchString(strconv.FormatInt(res.Message, 10)) { 72 | t.Fatalf("Returned unexpected body:\ngot \n%v \nwant \n%s", 73 | res.Message, expected) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /deploy/bases/frontend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | spec: 6 | minReadySeconds: 3 7 | revisionHistoryLimit: 5 8 | progressDeadlineSeconds: 60 9 | strategy: 10 | rollingUpdate: 11 | maxUnavailable: 0 12 | type: RollingUpdate 13 | selector: 14 | matchLabels: 15 | app: frontend 16 | template: 17 | metadata: 18 | annotations: 19 | prometheus.io/scrape: "true" 20 | prometheus.io/port: "9797" 21 | labels: 22 | app: frontend 23 | spec: 24 | containers: 25 | - name: frontend 26 | image: ghcr.io/stefanprodan/podinfo:6.9.4 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - name: http 30 | containerPort: 9898 31 | protocol: TCP 32 | - name: http-metrics 33 | containerPort: 9797 34 | protocol: TCP 35 | - name: grpc 36 | containerPort: 9999 37 | protocol: TCP 38 | command: 39 | - ./podinfo 40 | - --port=9898 41 | - --port-metrics=9797 42 | - --level=info 43 | - --backend-url=http://backend:9898/echo 44 | - --cache-server=tcp://cache:6379 45 | env: 46 | - name: PODINFO_UI_COLOR 47 | value: "#34577c" 48 | livenessProbe: 49 | exec: 50 | command: 51 | - podcli 52 | - check 53 | - http 54 | - localhost:9898/healthz 55 | initialDelaySeconds: 5 56 | timeoutSeconds: 5 57 | readinessProbe: 58 | exec: 59 | command: 60 | - podcli 61 | - check 62 | - http 63 | - localhost:9898/readyz 64 | initialDelaySeconds: 5 65 | timeoutSeconds: 5 66 | resources: 67 | limits: 68 | cpu: 1000m 69 | memory: 128Mi 70 | requests: 71 | cpu: 100m 72 | memory: 32Mi 73 | -------------------------------------------------------------------------------- /deploy/bases/backend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | minReadySeconds: 3 7 | revisionHistoryLimit: 5 8 | progressDeadlineSeconds: 60 9 | strategy: 10 | rollingUpdate: 11 | maxUnavailable: 0 12 | type: RollingUpdate 13 | selector: 14 | matchLabels: 15 | app: backend 16 | template: 17 | metadata: 18 | annotations: 19 | prometheus.io/scrape: "true" 20 | prometheus.io/port: "9797" 21 | labels: 22 | app: backend 23 | spec: 24 | containers: 25 | - name: backend 26 | image: ghcr.io/stefanprodan/podinfo:6.9.4 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - name: http 30 | containerPort: 9898 31 | protocol: TCP 32 | - name: http-metrics 33 | containerPort: 9797 34 | protocol: TCP 35 | - name: grpc 36 | containerPort: 9999 37 | protocol: TCP 38 | command: 39 | - ./podinfo 40 | - --port=9898 41 | - --port-metrics=9797 42 | - --grpc-port=9999 43 | - --grpc-service-name=backend 44 | - --level=info 45 | - --cache-server=tcp://cache:6379 46 | env: 47 | - name: PODINFO_UI_COLOR 48 | value: "#34577c" 49 | livenessProbe: 50 | exec: 51 | command: 52 | - podcli 53 | - check 54 | - http 55 | - localhost:9898/healthz 56 | initialDelaySeconds: 5 57 | timeoutSeconds: 5 58 | readinessProbe: 59 | exec: 60 | command: 61 | - podcli 62 | - check 63 | - http 64 | - localhost:9898/readyz 65 | initialDelaySeconds: 5 66 | timeoutSeconds: 5 67 | resources: 68 | limits: 69 | cpu: 2000m 70 | memory: 512Mi 71 | requests: 72 | cpu: 100m 73 | memory: 32Mi 74 | -------------------------------------------------------------------------------- /deploy/webapp/frontend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | namespace: webapp 6 | spec: 7 | minReadySeconds: 3 8 | revisionHistoryLimit: 5 9 | progressDeadlineSeconds: 60 10 | strategy: 11 | rollingUpdate: 12 | maxUnavailable: 0 13 | type: RollingUpdate 14 | selector: 15 | matchLabels: 16 | app: frontend 17 | template: 18 | metadata: 19 | annotations: 20 | prometheus.io/scrape: "true" 21 | prometheus.io/port: "9797" 22 | labels: 23 | app: frontend 24 | spec: 25 | serviceAccountName: webapp 26 | containers: 27 | - name: frontend 28 | image: ghcr.io/stefanprodan/podinfo:6.9.4 29 | imagePullPolicy: IfNotPresent 30 | ports: 31 | - name: http 32 | containerPort: 9898 33 | protocol: TCP 34 | - name: http-metrics 35 | containerPort: 9797 36 | protocol: TCP 37 | - name: grpc 38 | containerPort: 9999 39 | protocol: TCP 40 | command: 41 | - ./podinfo 42 | - --port=9898 43 | - --port-metrics=9797 44 | - --level=info 45 | - --backend-url=http://backend:9898/echo 46 | env: 47 | - name: PODINFO_UI_COLOR 48 | value: "#34577c" 49 | livenessProbe: 50 | exec: 51 | command: 52 | - podcli 53 | - check 54 | - http 55 | - localhost:9898/healthz 56 | initialDelaySeconds: 5 57 | timeoutSeconds: 5 58 | readinessProbe: 59 | exec: 60 | command: 61 | - podcli 62 | - check 63 | - http 64 | - localhost:9898/readyz 65 | initialDelaySeconds: 5 66 | timeoutSeconds: 5 67 | resources: 68 | limits: 69 | cpu: 1000m 70 | memory: 128Mi 71 | requests: 72 | cpu: 100m 73 | memory: 32Mi 74 | -------------------------------------------------------------------------------- /pkg/api/http/health.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "sync/atomic" 6 | ) 7 | 8 | // Healthz godoc 9 | // @Summary Liveness check 10 | // @Description used by Kubernetes liveness probe 11 | // @Tags Kubernetes 12 | // @Accept json 13 | // @Produce json 14 | // @Router /healthz [get] 15 | // @Success 200 {string} string "OK" 16 | func (s *Server) healthzHandler(w http.ResponseWriter, r *http.Request) { 17 | if atomic.LoadInt32(&healthy) == 1 { 18 | s.JSONResponse(w, r, map[string]string{"status": "OK"}) 19 | return 20 | } 21 | w.WriteHeader(http.StatusServiceUnavailable) 22 | } 23 | 24 | // Readyz godoc 25 | // @Summary Readiness check 26 | // @Description used by Kubernetes readiness probe 27 | // @Tags Kubernetes 28 | // @Accept json 29 | // @Produce json 30 | // @Router /readyz [get] 31 | // @Success 200 {string} string "OK" 32 | func (s *Server) readyzHandler(w http.ResponseWriter, r *http.Request) { 33 | if atomic.LoadInt32(&ready) == 1 { 34 | s.JSONResponse(w, r, map[string]string{"status": "OK"}) 35 | return 36 | } 37 | w.WriteHeader(http.StatusServiceUnavailable) 38 | } 39 | 40 | // EnableReady godoc 41 | // @Summary Enable ready state 42 | // @Description signals the Kubernetes LB that this instance is ready to receive traffic 43 | // @Tags Kubernetes 44 | // @Accept json 45 | // @Produce json 46 | // @Router /readyz/enable [post] 47 | // @Success 202 {string} string "OK" 48 | func (s *Server) enableReadyHandler(w http.ResponseWriter, r *http.Request) { 49 | atomic.StoreInt32(&ready, 1) 50 | w.WriteHeader(http.StatusAccepted) 51 | } 52 | 53 | // DisableReady godoc 54 | // @Summary Disable ready state 55 | // @Description signals the Kubernetes LB to stop sending requests to this instance 56 | // @Tags Kubernetes 57 | // @Accept json 58 | // @Produce json 59 | // @Router /readyz/disable [post] 60 | // @Success 202 {string} string "OK" 61 | func (s *Server) disableReadyHandler(w http.ResponseWriter, r *http.Request) { 62 | atomic.StoreInt32(&ready, 0) 63 | w.WriteHeader(http.StatusAccepted) 64 | } 65 | -------------------------------------------------------------------------------- /deploy/secure/backend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | namespace: secure 6 | spec: 7 | minReadySeconds: 3 8 | revisionHistoryLimit: 5 9 | progressDeadlineSeconds: 60 10 | strategy: 11 | rollingUpdate: 12 | maxUnavailable: 0 13 | type: RollingUpdate 14 | selector: 15 | matchLabels: 16 | app: backend 17 | template: 18 | metadata: 19 | annotations: 20 | prometheus.io/scrape: "true" 21 | prometheus.io/port: "9797" 22 | labels: 23 | app: backend 24 | spec: 25 | serviceAccountName: secure 26 | containers: 27 | - name: backend 28 | image: ghcr.io/stefanprodan/podinfo:5.0.3 29 | imagePullPolicy: IfNotPresent 30 | ports: 31 | - name: http 32 | containerPort: 9898 33 | protocol: TCP 34 | - name: http-metrics 35 | containerPort: 9797 36 | protocol: TCP 37 | - name: grpc 38 | containerPort: 9999 39 | protocol: TCP 40 | command: 41 | - ./podinfo 42 | - --port=9898 43 | - --port-metrics=9797 44 | - --grpc-port=9999 45 | - --grpc-service-name=backend 46 | - --level=info 47 | env: 48 | - name: PODINFO_UI_COLOR 49 | value: "#34577c" 50 | livenessProbe: 51 | exec: 52 | command: 53 | - podcli 54 | - check 55 | - http 56 | - localhost:9898/healthz 57 | initialDelaySeconds: 5 58 | timeoutSeconds: 5 59 | readinessProbe: 60 | exec: 61 | command: 62 | - podcli 63 | - check 64 | - http 65 | - localhost:9898/readyz 66 | initialDelaySeconds: 5 67 | timeoutSeconds: 5 68 | resources: 69 | limits: 70 | cpu: 2000m 71 | memory: 512Mi 72 | requests: 73 | cpu: 100m 74 | memory: 32Mi 75 | -------------------------------------------------------------------------------- /deploy/webapp/backend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | namespace: webapp 6 | spec: 7 | minReadySeconds: 3 8 | revisionHistoryLimit: 5 9 | progressDeadlineSeconds: 60 10 | strategy: 11 | rollingUpdate: 12 | maxUnavailable: 0 13 | type: RollingUpdate 14 | selector: 15 | matchLabels: 16 | app: backend 17 | template: 18 | metadata: 19 | annotations: 20 | prometheus.io/scrape: "true" 21 | prometheus.io/port: "9797" 22 | labels: 23 | app: backend 24 | spec: 25 | serviceAccountName: webapp 26 | containers: 27 | - name: backend 28 | image: ghcr.io/stefanprodan/podinfo:6.9.4 29 | imagePullPolicy: IfNotPresent 30 | ports: 31 | - name: http 32 | containerPort: 9898 33 | protocol: TCP 34 | - name: http-metrics 35 | containerPort: 9797 36 | protocol: TCP 37 | - name: grpc 38 | containerPort: 9999 39 | protocol: TCP 40 | command: 41 | - ./podinfo 42 | - --port=9898 43 | - --port-metrics=9797 44 | - --grpc-port=9999 45 | - --grpc-service-name=backend 46 | - --level=info 47 | env: 48 | - name: PODINFO_UI_COLOR 49 | value: "#34577c" 50 | livenessProbe: 51 | exec: 52 | command: 53 | - podcli 54 | - check 55 | - http 56 | - localhost:9898/healthz 57 | initialDelaySeconds: 5 58 | timeoutSeconds: 5 59 | readinessProbe: 60 | exec: 61 | command: 62 | - podcli 63 | - check 64 | - http 65 | - localhost:9898/readyz 66 | initialDelaySeconds: 5 67 | timeoutSeconds: 5 68 | resources: 69 | limits: 70 | cpu: 2000m 71 | memory: 512Mi 72 | requests: 73 | cpu: 100m 74 | memory: 32Mi 75 | -------------------------------------------------------------------------------- /pkg/api/grpc/info.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | "log" 7 | "runtime" 8 | "strconv" 9 | 10 | pb "github.com/stefanprodan/podinfo/pkg/api/grpc/info" 11 | "github.com/stefanprodan/podinfo/pkg/version" 12 | ) 13 | 14 | type infoServer struct { 15 | pb.UnimplementedInfoServiceServer 16 | config *Config 17 | logger *zap.Logger 18 | } 19 | 20 | func (s *infoServer) Info(ctx context.Context, message *pb.InfoRequest) (*pb.InfoResponse, error) { 21 | 22 | defer func() { 23 | if r := recover(); r != nil { 24 | log.Println("Recovered from panic:", r) 25 | } 26 | }() 27 | 28 | data := RuntimeResponse{ 29 | Hostname: s.config.Hostname, 30 | Version: version.VERSION, 31 | Revision: version.REVISION, 32 | Color: s.config.UIColor, 33 | Logo: s.config.UILogo, 34 | Message: s.config.UIMessage, 35 | Goos: runtime.GOOS, 36 | Goarch: runtime.GOARCH, 37 | Runtime: runtime.Version(), 38 | Numgoroutine: strconv.FormatInt(int64(runtime.NumGoroutine()), 10), 39 | Numcpu: strconv.FormatInt(int64(runtime.NumCPU()), 10), 40 | } 41 | 42 | return &pb.InfoResponse{ 43 | Hostname: data.Hostname, 44 | Version: data.Version, 45 | Revision: data.Revision, 46 | Color: data.Color, 47 | Logo: data.Logo, 48 | Message: data.Message, 49 | Goos: data.Goos, 50 | Goarch: data.Goarch, 51 | Runtime: data.Runtime, 52 | Numgoroutine: data.Numgoroutine, 53 | Numcpu: data.Numcpu, 54 | }, nil 55 | 56 | } 57 | 58 | type RuntimeResponse struct { 59 | Hostname string `json:"hostname"` 60 | Version string `json:"version"` 61 | Revision string `json:"revision"` 62 | Color string `json:"color"` 63 | Logo string `json:"logo"` 64 | Message string `json:"message"` 65 | Goos string `json:"goos"` 66 | Goarch string `json:"goarch"` 67 | Runtime string `json:"runtime"` 68 | Numgoroutine string `json:"num_goroutine"` 69 | Numcpu string `json:"num_cpu"` 70 | } 71 | -------------------------------------------------------------------------------- /.cosign/README.md: -------------------------------------------------------------------------------- 1 | # Podinfo signed releases 2 | 3 | Podinfo release assets (container image, Helm chart, Flux artifact, Timoni module) 4 | are published to GitHub Container Registry and are signed with 5 | [Cosign v2](https://github.com/sigstore/cosign) keyless & GitHub Actions OIDC. 6 | 7 | ## Verify podinfo with cosign 8 | 9 | Install the [cosign](https://github.com/sigstore/cosign) CLI: 10 | 11 | ```sh 12 | brew install sigstore/tap/cosign 13 | ``` 14 | 15 | ### Container image 16 | 17 | Verify the podinfo container image hosted on GHCR: 18 | 19 | ```sh 20 | cosign verify ghcr.io/stefanprodan/podinfo:6.5.0 \ 21 | --certificate-identity-regexp="^https://github.com/stefanprodan/podinfo.*$" \ 22 | --certificate-oidc-issuer=https://token.actions.githubusercontent.com 23 | ``` 24 | 25 | Verify the podinfo container image hosted on Docker Hub: 26 | 27 | ```sh 28 | cosign verify docker.io/stefanprodan/podinfo:6.5.0 \ 29 | --certificate-identity-regexp="^https://github.com/stefanprodan/podinfo.*$" \ 30 | --certificate-oidc-issuer=https://token.actions.githubusercontent.com 31 | ``` 32 | 33 | ### Helm chart 34 | 35 | Verify the podinfo [Helm](https://helm.sh) chart hosted on GHCR: 36 | 37 | ```sh 38 | cosign verify ghcr.io/stefanprodan/charts/podinfo:6.5.0 \ 39 | --certificate-identity-regexp="^https://github.com/stefanprodan/podinfo.*$" \ 40 | --certificate-oidc-issuer=https://token.actions.githubusercontent.com 41 | ``` 42 | 43 | ### Flux artifact 44 | 45 | Verify the podinfo [Flux](https://fluxcd.io) artifact hosted on GHCR: 46 | 47 | ```sh 48 | cosign verify ghcr.io/stefanprodan/manifests/podinfo:6.5.0 \ 49 | --certificate-identity-regexp="^https://github.com/stefanprodan/podinfo.*$" \ 50 | --certificate-oidc-issuer=https://token.actions.githubusercontent.com 51 | ``` 52 | 53 | ### Timoni module 54 | 55 | Verify the podinfo [Timoni](https://timoni.sh) module hosted on GHCR: 56 | 57 | ```sh 58 | cosign verify ghcr.io/stefanprodan/modules/podinfo:6.5.0 \ 59 | --certificate-identity-regexp="^https://github.com/stefanprodan/podinfo.*$" \ 60 | --certificate-oidc-issuer=https://token.actions.githubusercontent.com 61 | ``` 62 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/apimachinery/pkg/apis/meta/v1/group_version_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/apimachinery/pkg/apis/meta/v1 4 | 5 | package v1 6 | 7 | // GroupResource specifies a Group and a Resource, but does not force a version. This is useful for identifying 8 | // concepts during lookup stages without having partially valid types 9 | // 10 | // +protobuf.options.(gogoproto.goproto_stringer)=false 11 | #GroupResource: { 12 | group: string @go(Group) @protobuf(1,bytes,opt) 13 | resource: string @go(Resource) @protobuf(2,bytes,opt) 14 | } 15 | 16 | // GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion 17 | // to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling 18 | // 19 | // +protobuf.options.(gogoproto.goproto_stringer)=false 20 | #GroupVersionResource: { 21 | group: string @go(Group) @protobuf(1,bytes,opt) 22 | version: string @go(Version) @protobuf(2,bytes,opt) 23 | resource: string @go(Resource) @protobuf(3,bytes,opt) 24 | } 25 | 26 | // GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying 27 | // concepts during lookup stages without having partially valid types 28 | // 29 | // +protobuf.options.(gogoproto.goproto_stringer)=false 30 | #GroupKind: { 31 | group: string @go(Group) @protobuf(1,bytes,opt) 32 | kind: string @go(Kind) @protobuf(2,bytes,opt) 33 | } 34 | 35 | // GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion 36 | // to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling 37 | // 38 | // +protobuf.options.(gogoproto.goproto_stringer)=false 39 | #GroupVersionKind: { 40 | group: string @go(Group) @protobuf(1,bytes,opt) 41 | version: string @go(Version) @protobuf(2,bytes,opt) 42 | kind: string @go(Kind) @protobuf(3,bytes,opt) 43 | } 44 | 45 | // GroupVersion contains the "group" and the "version", which uniquely identifies the API. 46 | // 47 | // +protobuf.options.(gogoproto.goproto_stringer)=false 48 | #GroupVersion: _ 49 | -------------------------------------------------------------------------------- /kustomize/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: podinfo 5 | spec: 6 | minReadySeconds: 3 7 | revisionHistoryLimit: 5 8 | progressDeadlineSeconds: 60 9 | strategy: 10 | rollingUpdate: 11 | maxUnavailable: 0 12 | type: RollingUpdate 13 | selector: 14 | matchLabels: 15 | app: podinfo 16 | template: 17 | metadata: 18 | annotations: 19 | prometheus.io/scrape: "true" 20 | prometheus.io/port: "9797" 21 | labels: 22 | app: podinfo 23 | spec: 24 | containers: 25 | - name: podinfod 26 | image: ghcr.io/stefanprodan/podinfo:6.9.4 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - name: http 30 | containerPort: 9898 31 | protocol: TCP 32 | - name: http-metrics 33 | containerPort: 9797 34 | protocol: TCP 35 | - name: grpc 36 | containerPort: 9999 37 | protocol: TCP 38 | command: 39 | - ./podinfo 40 | - --port=9898 41 | - --port-metrics=9797 42 | - --grpc-port=9999 43 | - --grpc-service-name=podinfo 44 | - --level=info 45 | - --random-delay=false 46 | - --random-error=false 47 | env: 48 | - name: PODINFO_UI_COLOR 49 | value: "#34577c" 50 | livenessProbe: 51 | exec: 52 | command: 53 | - podcli 54 | - check 55 | - http 56 | - localhost:9898/healthz 57 | initialDelaySeconds: 5 58 | timeoutSeconds: 5 59 | readinessProbe: 60 | exec: 61 | command: 62 | - podcli 63 | - check 64 | - http 65 | - localhost:9898/readyz 66 | initialDelaySeconds: 5 67 | timeoutSeconds: 5 68 | resources: 69 | limits: 70 | cpu: 2000m 71 | memory: 512Mi 72 | requests: 73 | cpu: 100m 74 | memory: 64Mi 75 | volumeMounts: 76 | - name: data 77 | mountPath: /data 78 | volumes: 79 | - name: data 80 | emptyDir: {} 81 | -------------------------------------------------------------------------------- /pkg/api/http/store.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "io" 7 | "net/http" 8 | "os" 9 | "path" 10 | 11 | "github.com/gorilla/mux" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | // Store godoc 16 | // @Summary Upload file 17 | // @Description writes the posted content to disk at /data/hash and returns the SHA1 hash of the content 18 | // @Tags HTTP API 19 | // @Accept json 20 | // @Produce json 21 | // @Router /store [post] 22 | // @Success 200 {object} http.MapResponse 23 | func (s *Server) storeWriteHandler(w http.ResponseWriter, r *http.Request) { 24 | _, span := s.tracer.Start(r.Context(), "storeWriteHandler") 25 | defer span.End() 26 | 27 | body, err := io.ReadAll(r.Body) 28 | if err != nil { 29 | s.ErrorResponse(w, r, span, "reading the request body failed", http.StatusBadRequest) 30 | return 31 | } 32 | 33 | hash := hash(string(body)) 34 | err = os.WriteFile(path.Join(s.config.DataPath, hash), body, 0644) 35 | if err != nil { 36 | s.logger.Warn("writing file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash))) 37 | s.ErrorResponse(w, r, span, "writing file failed", http.StatusInternalServerError) 38 | return 39 | } 40 | s.JSONResponseCode(w, r, map[string]string{"hash": hash}, http.StatusAccepted) 41 | } 42 | 43 | // Store godoc 44 | // @Summary Download file 45 | // @Description returns the content of the file /data/hash if exists 46 | // @Tags HTTP API 47 | // @Accept json 48 | // @Produce plain 49 | // @Param hash path string true "hash value" 50 | // @Router /store/{hash} [get] 51 | // @Success 200 {string} string "file" 52 | func (s *Server) storeReadHandler(w http.ResponseWriter, r *http.Request) { 53 | _, span := s.tracer.Start(r.Context(), "storeReadHandler") 54 | defer span.End() 55 | 56 | hash := mux.Vars(r)["hash"] 57 | content, err := os.ReadFile(path.Join(s.config.DataPath, hash)) 58 | if err != nil { 59 | s.logger.Warn("reading file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash))) 60 | s.ErrorResponse(w, r, span, "reading file failed", http.StatusInternalServerError) 61 | return 62 | } 63 | w.WriteHeader(http.StatusAccepted) 64 | w.Write([]byte(content)) 65 | } 66 | 67 | func hash(input string) string { 68 | h := sha1.New() 69 | h.Write([]byte(input)) 70 | return hex.EncodeToString(h.Sum(nil)) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/api/http/tracer.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/spf13/viper" 8 | "github.com/stefanprodan/podinfo/pkg/version" 9 | "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" 10 | "go.opentelemetry.io/contrib/propagators/aws/xray" 11 | "go.opentelemetry.io/contrib/propagators/b3" 12 | "go.opentelemetry.io/contrib/propagators/jaeger" 13 | "go.opentelemetry.io/contrib/propagators/ot" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 16 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 17 | "go.opentelemetry.io/otel/propagation" 18 | "go.opentelemetry.io/otel/sdk/resource" 19 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 20 | semconv "go.opentelemetry.io/otel/semconv/v1.7.0" 21 | "go.opentelemetry.io/otel/trace" 22 | "go.uber.org/zap" 23 | ) 24 | 25 | const ( 26 | instrumentationName = "github.com/stefanprodan/podinfo/pkg/api" 27 | ) 28 | 29 | func (s *Server) initTracer(ctx context.Context) { 30 | if viper.GetString("otel-service-name") == "" { 31 | nop := trace.NewNoopTracerProvider() 32 | s.tracer = nop.Tracer(viper.GetString("otel-service-name")) 33 | return 34 | } 35 | 36 | client := otlptracegrpc.NewClient() 37 | exporter, err := otlptrace.New(ctx, client) 38 | if err != nil { 39 | s.logger.Error("creating OTLP trace exporter", zap.Error(err)) 40 | } 41 | 42 | s.tracerProvider = sdktrace.NewTracerProvider( 43 | sdktrace.WithBatcher(exporter), 44 | sdktrace.WithResource(resource.NewWithAttributes( 45 | semconv.SchemaURL, 46 | semconv.ServiceNameKey.String(viper.GetString("otel-service-name")), 47 | semconv.ServiceVersionKey.String(version.VERSION), 48 | )), 49 | ) 50 | 51 | otel.SetTracerProvider(s.tracerProvider) 52 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( 53 | propagation.TraceContext{}, 54 | propagation.Baggage{}, 55 | b3.New(), 56 | &jaeger.Jaeger{}, 57 | &ot.OT{}, 58 | &xray.Propagator{}, 59 | )) 60 | 61 | s.tracer = s.tracerProvider.Tracer( 62 | instrumentationName, 63 | trace.WithInstrumentationVersion(version.VERSION), 64 | trace.WithSchemaURL(semconv.SchemaURL), 65 | ) 66 | } 67 | 68 | func NewOpenTelemetryMiddleware() mux.MiddlewareFunc { 69 | return otelmux.Middleware(viper.GetString("otel-service-name")) 70 | } 71 | -------------------------------------------------------------------------------- /charts/podinfo/templates/redis/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.redis.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}-redis 6 | labels: 7 | app: {{ template "podinfo.fullname" . }}-redis 8 | spec: 9 | strategy: 10 | type: Recreate 11 | selector: 12 | matchLabels: 13 | app: {{ template "podinfo.fullname" . }}-redis 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ template "podinfo.fullname" . }}-redis 18 | annotations: 19 | checksum/config: {{ include (print $.Template.BasePath "/redis/config.yaml") . | sha256sum | quote }} 20 | spec: 21 | {{- if .Values.serviceAccount.enabled }} 22 | serviceAccountName: {{ template "podinfo.serviceAccountName" . }} 23 | {{- end }} 24 | {{- if .Values.redis.imagePullSecrets }} 25 | imagePullSecrets: {{ toYaml .Values.redis.imagePullSecrets | nindent 8 }} 26 | {{- end }} 27 | containers: 28 | - name: redis 29 | image: "{{ .Values.redis.repository }}:{{ .Values.redis.tag }}" 30 | imagePullPolicy: IfNotPresent 31 | command: 32 | - redis-server 33 | - "/redis-master/redis.conf" 34 | ports: 35 | - name: redis 36 | containerPort: 6379 37 | protocol: TCP 38 | livenessProbe: 39 | tcpSocket: 40 | port: redis 41 | initialDelaySeconds: 5 42 | timeoutSeconds: 5 43 | readinessProbe: 44 | exec: 45 | command: 46 | - redis-cli 47 | - ping 48 | initialDelaySeconds: 5 49 | timeoutSeconds: 5 50 | resources: 51 | limits: 52 | cpu: 1000m 53 | memory: 128Mi 54 | requests: 55 | cpu: 100m 56 | memory: 32Mi 57 | volumeMounts: 58 | - mountPath: /var/lib/redis 59 | name: data 60 | - mountPath: /redis-master 61 | name: config 62 | volumes: 63 | - name: data 64 | emptyDir: {} 65 | - name: config 66 | configMap: 67 | name: {{ template "podinfo.fullname" . }}-redis 68 | items: 69 | - key: redis.conf 70 | path: redis.conf 71 | {{- end }} 72 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/coordination/v1/types_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/coordination/v1 4 | 5 | package v1 6 | 7 | import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | 9 | // Lease defines a lease concept. 10 | #Lease: { 11 | metav1.#TypeMeta 12 | 13 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata 14 | // +optional 15 | metadata?: metav1.#ObjectMeta @go(ObjectMeta) @protobuf(1,bytes,opt) 16 | 17 | // spec contains the specification of the Lease. 18 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status 19 | // +optional 20 | spec?: #LeaseSpec @go(Spec) @protobuf(2,bytes,opt) 21 | } 22 | 23 | // LeaseSpec is a specification of a Lease. 24 | #LeaseSpec: { 25 | // holderIdentity contains the identity of the holder of a current lease. 26 | // +optional 27 | holderIdentity?: null | string @go(HolderIdentity,*string) @protobuf(1,bytes,opt) 28 | 29 | // leaseDurationSeconds is a duration that candidates for a lease need 30 | // to wait to force acquire it. This is measure against time of last 31 | // observed renewTime. 32 | // +optional 33 | leaseDurationSeconds?: null | int32 @go(LeaseDurationSeconds,*int32) @protobuf(2,varint,opt) 34 | 35 | // acquireTime is a time when the current lease was acquired. 36 | // +optional 37 | acquireTime?: null | metav1.#MicroTime @go(AcquireTime,*metav1.MicroTime) @protobuf(3,bytes,opt) 38 | 39 | // renewTime is a time when the current holder of a lease has last 40 | // updated the lease. 41 | // +optional 42 | renewTime?: null | metav1.#MicroTime @go(RenewTime,*metav1.MicroTime) @protobuf(4,bytes,opt) 43 | 44 | // leaseTransitions is the number of transitions of a lease between 45 | // holders. 46 | // +optional 47 | leaseTransitions?: null | int32 @go(LeaseTransitions,*int32) @protobuf(5,varint,opt) 48 | } 49 | 50 | // LeaseList is a list of Lease objects. 51 | #LeaseList: { 52 | metav1.#TypeMeta 53 | 54 | // Standard list metadata. 55 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata 56 | // +optional 57 | metadata?: metav1.#ListMeta @go(ListMeta) @protobuf(1,bytes,opt) 58 | 59 | // items is a list of schema objects. 60 | items: [...#Lease] @go(Items,[]Lease) @protobuf(2,bytes,rep) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/api/http/echows.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var wsCon = websocket.Upgrader{} 14 | 15 | // EchoWS godoc 16 | // @Summary Echo over websockets 17 | // @Description echos content via websockets 18 | // @Tags HTTP API 19 | // @Accept json 20 | // @Produce json 21 | // @Router /ws/echo [post] 22 | // @Success 202 {object} http.MapResponse 23 | // Test: go run ./cmd/podcli/* ws localhost:9898/ws/echo 24 | func (s *Server) echoWsHandler(w http.ResponseWriter, r *http.Request) { 25 | c, err := wsCon.Upgrade(w, r, nil) 26 | if err != nil { 27 | if err != nil { 28 | s.logger.Warn("websocket upgrade error", zap.Error(err)) 29 | return 30 | } 31 | } 32 | var wg sync.WaitGroup 33 | wg.Add(1) 34 | 35 | defer c.Close() 36 | done := make(chan struct{}) 37 | defer close(done) 38 | in := make(chan interface{}) 39 | go s.writeWs(c, in) 40 | go s.sendHostWs(c, in, done, &wg) 41 | go func() { 42 | defer close(in) 43 | wg.Wait() 44 | }() 45 | for { 46 | _, message, err := c.ReadMessage() 47 | if err != nil { 48 | if !strings.Contains(err.Error(), "close") { 49 | s.logger.Warn("websocket read error", zap.Error(err)) 50 | } 51 | break 52 | } 53 | var response = struct { 54 | Time time.Time `json:"ts"` 55 | Message string `json:"msg"` 56 | }{ 57 | Time: time.Now(), 58 | Message: string(message), 59 | } 60 | in <- response 61 | } 62 | } 63 | 64 | func (s *Server) sendHostWs(ws *websocket.Conn, in chan interface{}, done chan struct{}, wg *sync.WaitGroup) { 65 | ticker := time.NewTicker(5 * time.Second) 66 | defer ticker.Stop() 67 | for { 68 | select { 69 | case <-ticker.C: 70 | var status = struct { 71 | Time time.Time `json:"ts"` 72 | Host string `json:"server"` 73 | }{ 74 | Time: time.Now(), 75 | Host: s.config.Hostname, 76 | } 77 | in <- status 78 | case <-done: 79 | s.logger.Debug("websocket exit") 80 | wg.Done() 81 | return 82 | } 83 | } 84 | } 85 | 86 | func (s *Server) writeWs(ws *websocket.Conn, in chan interface{}) { 87 | for { 88 | select { 89 | case msg := <-in: 90 | if err := ws.WriteJSON(msg); err != nil { 91 | if !strings.Contains(err.Error(), "close") { 92 | s.logger.Warn("websocket write error", zap.Error(err)) 93 | } 94 | return 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'master' 8 | 9 | permissions: 10 | contents: read 11 | 12 | env: 13 | KUBERNETES_VERSION: 1.31.0 14 | HELM_VERSION: 3.17.3 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: ./.github/actions/runner-cleanup 22 | - name: Setup Go 23 | uses: actions/setup-go@v6 24 | with: 25 | go-version: 1.25.x 26 | cache-dependency-path: | 27 | **/go.sum 28 | **/go.mod 29 | - name: Setup kubectl 30 | uses: azure/setup-kubectl@v4 31 | with: 32 | version: v${{ env.KUBERNETES_VERSION }} 33 | - name: Setup kubeconform 34 | uses: ./.github/actions/kubeconform 35 | - name: Setup Helm 36 | uses: azure/setup-helm@v4 37 | with: 38 | version: v${{ env.HELM_VERSION }} 39 | - name: Setup CUE 40 | uses: cue-lang/setup-cue@v1.0.1 41 | - name: Setup Timoni 42 | uses: stefanprodan/timoni/actions/setup@v0.25.2 43 | - name: Run unit tests 44 | run: make test 45 | - name: Validate Helm chart 46 | run: | 47 | helm lint ./charts/podinfo/ 48 | helm template ./charts/podinfo/ | kubeconform -strict -summary -kubernetes-version ${{ env.KUBERNETES_VERSION }} 49 | - name: Validate Kustomize overlay 50 | run: | 51 | kubectl kustomize ./kustomize/ | kubeconform -strict -summary -kubernetes-version ${{ env.KUBERNETES_VERSION }} 52 | - name: Verify CUE formatting 53 | working-directory: ./timoni/podinfo 54 | run: | 55 | cue fmt ./... 56 | status=$(git status . --porcelain) 57 | [[ -z "$status" ]] || { 58 | echo "CUE files are not correctly formatted" 59 | echo "$status" 60 | git diff 61 | exit 1 62 | } 63 | - name: Validate Timoni module 64 | working-directory: ./timoni/podinfo 65 | run: | 66 | timoni mod lint . 67 | timoni build podinfo . -f test_values.cue | kubeconform -strict -summary -skip=ServiceMonitor -kubernetes-version ${{ env.KUBERNETES_VERSION }} 68 | - name: Check if working tree is dirty 69 | run: | 70 | if [[ $(git diff --stat) != '' ]]; then 71 | echo 'run make test and commit changes' 72 | exit 1 73 | fi 74 | -------------------------------------------------------------------------------- /timoni/podinfo/cue.mod/gen/k8s.io/api/scheduling/v1/types_go_gen.cue: -------------------------------------------------------------------------------- 1 | // Code generated by cue get go. DO NOT EDIT. 2 | 3 | //cue:generate cue get go k8s.io/api/scheduling/v1 4 | 5 | package v1 6 | 7 | import ( 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | apiv1 "k8s.io/api/core/v1" 10 | ) 11 | 12 | // PriorityClass defines mapping from a priority class name to the priority 13 | // integer value. The value can be any valid integer. 14 | #PriorityClass: { 15 | metav1.#TypeMeta 16 | 17 | // Standard object's metadata. 18 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata 19 | // +optional 20 | metadata?: metav1.#ObjectMeta @go(ObjectMeta) @protobuf(1,bytes,opt) 21 | 22 | // value represents the integer value of this priority class. This is the actual priority that pods 23 | // receive when they have the name of this class in their pod spec. 24 | value: int32 @go(Value) @protobuf(2,bytes,opt) 25 | 26 | // globalDefault specifies whether this PriorityClass should be considered as 27 | // the default priority for pods that do not have any priority class. 28 | // Only one PriorityClass can be marked as `globalDefault`. However, if more than 29 | // one PriorityClasses exists with their `globalDefault` field set to true, 30 | // the smallest value of such global default PriorityClasses will be used as the default priority. 31 | // +optional 32 | globalDefault?: bool @go(GlobalDefault) @protobuf(3,bytes,opt) 33 | 34 | // description is an arbitrary string that usually provides guidelines on 35 | // when this priority class should be used. 36 | // +optional 37 | description?: string @go(Description) @protobuf(4,bytes,opt) 38 | 39 | // preemptionPolicy is the Policy for preempting pods with lower priority. 40 | // One of Never, PreemptLowerPriority. 41 | // Defaults to PreemptLowerPriority if unset. 42 | // +optional 43 | preemptionPolicy?: null | apiv1.#PreemptionPolicy @go(PreemptionPolicy,*apiv1.PreemptionPolicy) @protobuf(5,bytes,opt) 44 | } 45 | 46 | // PriorityClassList is a collection of priority classes. 47 | #PriorityClassList: { 48 | metav1.#TypeMeta 49 | 50 | // Standard list metadata 51 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata 52 | // +optional 53 | metadata?: metav1.#ListMeta @go(ListMeta) @protobuf(1,bytes,opt) 54 | 55 | // items is the list of PriorityClasses 56 | items: [...#PriorityClass] @go(Items,[]PriorityClass) @protobuf(2,bytes,rep) 57 | } 58 | -------------------------------------------------------------------------------- /charts/podinfo/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "podinfo.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "podinfo.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Allow the release namespace to be overridden for multi-namespace deployments in combined charts. 28 | */}} 29 | {{- define "podinfo.namespace" -}} 30 | {{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} 31 | {{- end -}} 32 | 33 | {{/* 34 | Create chart name and version as used by the chart label. 35 | */}} 36 | {{- define "podinfo.chart" -}} 37 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 38 | {{- end }} 39 | 40 | {{/* 41 | Common labels 42 | */}} 43 | {{- define "podinfo.labels" -}} 44 | helm.sh/chart: {{ include "podinfo.chart" . }} 45 | {{ include "podinfo.selectorLabels" . }} 46 | {{- if .Chart.AppVersion }} 47 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 48 | {{- end }} 49 | app.kubernetes.io/managed-by: {{ .Release.Service }} 50 | {{- end }} 51 | 52 | {{/* 53 | Selector labels 54 | */}} 55 | {{- define "podinfo.selectorLabels" -}} 56 | app.kubernetes.io/name: {{ include "podinfo.fullname" . }} 57 | {{- end }} 58 | 59 | {{/* 60 | Create the name of the service account to use 61 | */}} 62 | {{- define "podinfo.serviceAccountName" -}} 63 | {{- if .Values.serviceAccount.enabled }} 64 | {{- default (include "podinfo.fullname" .) .Values.serviceAccount.name }} 65 | {{- else }} 66 | {{- default "default" .Values.serviceAccount.name }} 67 | {{- end }} 68 | {{- end }} 69 | 70 | {{/* 71 | Create the name of the tls secret for secure port 72 | */}} 73 | {{- define "podinfo.tlsSecretName" -}} 74 | {{- $fullname := include "podinfo.fullname" . -}} 75 | {{- default (printf "%s-tls" $fullname) .Values.tls.secretName }} 76 | {{- end }} -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'master' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | kind-helm: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v6 18 | - name: Disk Cleanup 19 | uses: ./.github/actions/runner-cleanup 20 | - name: Setup Kubernetes 21 | uses: helm/kind-action@v1.13.0 22 | with: 23 | cluster_name: kind 24 | - name: Build container image 25 | run: | 26 | ./test/build.sh 27 | kind load docker-image test/podinfo:latest 28 | - name: Setup Helm 29 | uses: azure/setup-helm@v4 30 | with: 31 | version: v3.17.3 32 | - name: Deploy 33 | run: ./test/deploy.sh 34 | - name: Run integration tests 35 | run: ./test/test.sh 36 | - name: Debug failure 37 | if: failure() 38 | run: | 39 | kubectl logs -l app=podinfo || true 40 | kind-timoni: 41 | runs-on: ubuntu-latest 42 | services: 43 | registry: 44 | image: registry:2 45 | ports: 46 | - 5000:5000 47 | env: 48 | PODINFO_IMAGE_URL: "test/podinfo" 49 | PODINFO_MODULE_URL: "oci://localhost:5000/podinfo" 50 | PODINFO_VERSION: "0.0.0-devel" 51 | steps: 52 | - uses: actions/checkout@v6 53 | - uses: ./.github/actions/runner-cleanup 54 | - name: Setup Timoni 55 | uses: stefanprodan/timoni/actions/setup@main 56 | - name: Setup Kubernetes 57 | uses: helm/kind-action@v1.13.0 58 | with: 59 | cluster_name: kind 60 | - name: Build container 61 | run: | 62 | docker build -t ${PODINFO_IMAGE_URL}:${PODINFO_VERSION} --build-arg "REVISION=${GITHUB_SHA}" -f Dockerfile.xx . 63 | kind load docker-image ${PODINFO_IMAGE_URL}:${PODINFO_VERSION} 64 | - name: Vet module 65 | run: | 66 | timoni mod vet ./timoni/podinfo --debug 67 | - name: Build module 68 | run: | 69 | timoni mod push ./timoni/podinfo ${PODINFO_MODULE_URL} -v ${PODINFO_VERSION} 70 | - name: Apply bundle 71 | run: | 72 | timoni bundle apply -f ./timoni/bundles/test.podinfo.cue --runtime-from-env 73 | - name: Verify status 74 | run: | 75 | timoni -n podinfo status backend 76 | timoni -n podinfo status frontend 77 | - name: Debug failure 78 | if: failure() 79 | run: | 80 | kubectl -n podinfo get all || true 81 | -------------------------------------------------------------------------------- /deploy/secure/frontend/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | namespace: secure 6 | spec: 7 | minReadySeconds: 3 8 | revisionHistoryLimit: 5 9 | progressDeadlineSeconds: 60 10 | strategy: 11 | rollingUpdate: 12 | maxUnavailable: 0 13 | type: RollingUpdate 14 | selector: 15 | matchLabels: 16 | app: frontend 17 | template: 18 | metadata: 19 | annotations: 20 | prometheus.io/scrape: "true" 21 | prometheus.io/port: "9797" 22 | labels: 23 | app: frontend 24 | spec: 25 | serviceAccountName: secure 26 | volumes: 27 | - name: tls 28 | secret: 29 | secretName: podinfo-frontend-tls 30 | containers: 31 | - name: frontend 32 | image: deavon/podinfo:secure-port 33 | imagePullPolicy: IfNotPresent 34 | securityContext: 35 | capabilities: 36 | drop: 37 | - ALL 38 | add: 39 | - NET_BIND_SERVICE 40 | allowPrivilegeEscalation: true 41 | ports: 42 | - name: http 43 | containerPort: 9898 44 | protocol: TCP 45 | hostPort: 80 46 | - name: https 47 | containerPort: 9899 48 | protocol: TCP 49 | hostPort: 443 50 | - name: http-metrics 51 | containerPort: 9797 52 | protocol: TCP 53 | - name: grpc 54 | containerPort: 9999 55 | protocol: TCP 56 | volumeMounts: 57 | - name: tls 58 | mountPath: /data/cert 59 | readOnly: true 60 | command: 61 | - ./podinfo 62 | - --port=9898 63 | - --secure-port=9899 64 | - --port-metrics=9797 65 | - --level=info 66 | - --cert-path=/data/cert 67 | - --backend-url=http://backend:9898/echo 68 | env: 69 | - name: PODINFO_UI_COLOR 70 | value: "#34577c" 71 | livenessProbe: 72 | exec: 73 | command: 74 | - podcli 75 | - check 76 | - http 77 | - localhost:9898/healthz 78 | initialDelaySeconds: 5 79 | timeoutSeconds: 5 80 | readinessProbe: 81 | exec: 82 | command: 83 | - podcli 84 | - check 85 | - http 86 | - localhost:9898/readyz 87 | initialDelaySeconds: 5 88 | timeoutSeconds: 5 89 | resources: 90 | limits: 91 | cpu: 1000m 92 | memory: 128Mi 93 | requests: 94 | cpu: 100m 95 | memory: 32Mi 96 | -------------------------------------------------------------------------------- /pkg/signals/shutdown.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | "github.com/spf13/viper" 11 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 12 | "go.uber.org/zap" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | type Shutdown struct { 17 | logger *zap.Logger 18 | pool *redis.Pool 19 | tracerProvider *sdktrace.TracerProvider 20 | serverShutdownTimeout time.Duration 21 | } 22 | 23 | func NewShutdown(serverShutdownTimeout time.Duration, logger *zap.Logger) (*Shutdown, error) { 24 | srv := &Shutdown{ 25 | logger: logger, 26 | serverShutdownTimeout: serverShutdownTimeout, 27 | } 28 | 29 | return srv, nil 30 | } 31 | 32 | func (s *Shutdown) Graceful(stopCh <-chan struct{}, httpServer *http.Server, httpsServer *http.Server, grpcServer *grpc.Server, healthy *int32, ready *int32) { 33 | ctx := context.Background() 34 | 35 | // wait for SIGTERM or SIGINT 36 | <-stopCh 37 | ctx, cancel := context.WithTimeout(ctx, s.serverShutdownTimeout) 38 | defer cancel() 39 | 40 | // all calls to /healthz and /readyz will fail from now on 41 | atomic.StoreInt32(healthy, 0) 42 | atomic.StoreInt32(ready, 0) 43 | 44 | // close cache pool 45 | if s.pool != nil { 46 | _ = s.pool.Close() 47 | } 48 | 49 | s.logger.Info("Shutting down HTTP/HTTPS server", zap.Duration("timeout", s.serverShutdownTimeout)) 50 | 51 | // There could be a period where a terminating pod may still receive requests. Implementing a brief wait can mitigate this. 52 | // See: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination 53 | // the readiness check interval must be lower than the timeout 54 | if viper.GetString("level") != "debug" { 55 | time.Sleep(3 * time.Second) 56 | } 57 | 58 | // stop OpenTelemetry tracer provider 59 | if s.tracerProvider != nil { 60 | if err := s.tracerProvider.Shutdown(ctx); err != nil { 61 | s.logger.Warn("stopping tracer provider", zap.Error(err)) 62 | } 63 | } 64 | 65 | // determine if the GRPC was started 66 | if grpcServer != nil { 67 | s.logger.Info("Shutting down GRPC server", zap.Duration("timeout", s.serverShutdownTimeout)) 68 | grpcServer.GracefulStop() 69 | } 70 | 71 | // determine if the http server was started 72 | if httpServer != nil { 73 | if err := httpServer.Shutdown(ctx); err != nil { 74 | s.logger.Warn("HTTP server graceful shutdown failed", zap.Error(err)) 75 | } 76 | } 77 | 78 | // determine if the secure server was started 79 | if httpsServer != nil { 80 | if err := httpsServer.Shutdown(ctx); err != nil { 81 | s.logger.Warn("HTTPS server graceful shutdown failed", zap.Error(err)) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /charts/podinfo/templates/linkerd.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.linkerd.profile.enabled -}} 2 | apiVersion: linkerd.io/v1alpha2 3 | kind: ServiceProfile 4 | metadata: 5 | name: {{ template "podinfo.fullname" . }}.{{ include "podinfo.namespace" . }}.svc.cluster.local 6 | namespace: {{ include "podinfo.namespace" . }} 7 | labels: 8 | {{- include "podinfo.labels" . | nindent 4 }} 9 | spec: 10 | routes: 11 | - condition: 12 | method: GET 13 | pathRegex: / 14 | name: GET / 15 | - condition: 16 | method: POST 17 | pathRegex: /api/echo 18 | name: POST /api/echo 19 | - condition: 20 | method: GET 21 | pathRegex: /api/info 22 | name: GET /api/info 23 | - condition: 24 | method: GET 25 | pathRegex: /chunked/[^/]* 26 | name: GET /chunked/{seconds} 27 | - condition: 28 | method: GET 29 | pathRegex: /delay/[^/]* 30 | name: GET /delay/{seconds} 31 | - condition: 32 | method: GET 33 | pathRegex: /env 34 | name: GET /env 35 | - condition: 36 | method: GET 37 | pathRegex: /headers 38 | name: GET /headers 39 | - condition: 40 | method: GET 41 | pathRegex: /healthz 42 | name: GET /healthz 43 | - condition: 44 | method: GET 45 | pathRegex: /metrics 46 | name: GET /metrics 47 | - condition: 48 | method: GET 49 | pathRegex: /panic 50 | name: GET /panic 51 | - condition: 52 | method: GET 53 | pathRegex: /readyz 54 | name: GET /readyz 55 | - condition: 56 | method: POST 57 | pathRegex: /readyz/disable 58 | name: POST /readyz/disable 59 | - condition: 60 | method: POST 61 | pathRegex: /readyz/enable 62 | name: POST /readyz/enable 63 | - condition: 64 | method: GET 65 | pathRegex: /status/[^/]* 66 | name: GET /status/{code} 67 | - condition: 68 | method: POST 69 | pathRegex: /cache 70 | name: POST /cache 71 | - condition: 72 | method: GET 73 | pathRegex: /cache/[^/]* 74 | name: GET /cache/{hash} 75 | - condition: 76 | method: POST 77 | pathRegex: /store 78 | name: POST /store 79 | - condition: 80 | method: GET 81 | pathRegex: /store/[^/]* 82 | name: GET /store/{hash} 83 | - condition: 84 | method: POST 85 | pathRegex: /token 86 | name: POST /token 87 | - condition: 88 | method: POST 89 | pathRegex: /token/validate 90 | name: POST /token/validate 91 | - condition: 92 | method: GET 93 | pathRegex: /version 94 | name: GET /version 95 | - condition: 96 | method: POST 97 | pathRegex: /ws/echo 98 | name: POST /ws/echo 99 | {{- end }} -------------------------------------------------------------------------------- /pkg/fscache/fscache.go: -------------------------------------------------------------------------------- 1 | package fscache 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | ) 13 | 14 | type Watcher struct { 15 | dir string 16 | fswatcher *fsnotify.Watcher 17 | Cache *sync.Map 18 | } 19 | 20 | // NewWatch creates a directory watcher and 21 | // updates the cache when any file changes in that dir 22 | func NewWatch(dir string) (*Watcher, error) { 23 | if len(dir) < 1 { 24 | return nil, errors.New("directory is empty") 25 | } 26 | 27 | fw, err := fsnotify.NewWatcher() 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | w := &Watcher{ 33 | dir: dir, 34 | fswatcher: fw, 35 | Cache: new(sync.Map), 36 | } 37 | 38 | log.Printf("fscache start watcher for %s", w.dir) 39 | err = w.fswatcher.Add(w.dir) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // initial read 45 | err = w.updateCache() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return w, nil 51 | } 52 | 53 | // Watch watches for when kubelet updates the volume mount content 54 | func (w *Watcher) Watch() { 55 | go func() { 56 | for { 57 | select { 58 | // it can take up to a 2 minutes for kubelet to recreate the ..data symlink 59 | case event := <-w.fswatcher.Events: 60 | if event.Op&fsnotify.Create == fsnotify.Create { 61 | if filepath.Base(event.Name) == "..data" { 62 | err := w.updateCache() 63 | if err != nil { 64 | log.Printf("fscache update error %v", err) 65 | } else { 66 | log.Printf("fscache reload %s", w.dir) 67 | } 68 | } 69 | } 70 | case err := <-w.fswatcher.Errors: 71 | log.Printf("fswatcher %s error %v", w.dir, err) 72 | } 73 | } 74 | }() 75 | } 76 | 77 | // updateCache reads files content and loads them into the cache 78 | func (w *Watcher) updateCache() error { 79 | fileMap := make(map[string]string) 80 | files, err := os.ReadDir(w.dir) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | // read files ignoring symlinks and sub directories 86 | for _, file := range files { 87 | name := filepath.Base(file.Name()) 88 | if !file.IsDir() && !strings.Contains(name, "..") { 89 | b, err := os.ReadFile(filepath.Join(w.dir, file.Name())) 90 | if err != nil { 91 | return err 92 | } 93 | fileMap[name] = string(b) 94 | } 95 | } 96 | 97 | // remove deleted files from cache 98 | w.Cache.Range(func(key interface{}, value interface{}) bool { 99 | _, ok := fileMap[key.(string)] 100 | if !ok { 101 | w.Cache.Delete(key) 102 | } 103 | return true 104 | }) 105 | 106 | // sync cache 107 | for k, v := range fileMap { 108 | w.Cache.Store(k, v) 109 | } 110 | 111 | return nil 112 | } 113 | --------------------------------------------------------------------------------