From 455e6ded4c49a2b462d03b98b245b7596dd5f9d5 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 11:49:29 +0100 Subject: [PATCH 01/66] fix: PDB now protects pods based on effective replica count and allows eviction of unhealthy pods - Add unhealthyPodEvictionPolicy: AlwaysAllow to prevent unhealthy pods from blocking node drains during cluster upgrades - Fix forceReplicas > 1 getting minAvailable 0% (all pods evictable) - Fix replicas=1 with HPA getting minAvailable 0% despite 2+ pods running - PDB logic now checks effective replicas: forceReplicas, HPA min replicas, or configured replicas Co-Authored-By: Claude Opus 4.6 (1M context) --- charts/common/templates/pdb.yaml | 23 ++++- charts/common/tests/pdb_test.yaml | 137 ++++++++++++++++++++++++------ 2 files changed, 133 insertions(+), 27 deletions(-) diff --git a/charts/common/templates/pdb.yaml b/charts/common/templates/pdb.yaml index f5f74f7..ab5fe22 100644 --- a/charts/common/templates/pdb.yaml +++ b/charts/common/templates/pdb.yaml @@ -8,6 +8,23 @@ {{- $replicas := .Values.deployment.replicas | default .Values.container.replicas -}} {{- $hpa := .Values.hpa | default dict -}} {{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas -}} +{{- /* + Determine effective replica count to decide if PDB should protect pods. + - forceReplicas: exact count, no HPA + - HPA enabled: always >= 2 replicas (hpa.yaml enforces minReplicas: max(2, replicas)) + - Otherwise: use configured replicas + PDB is set to 0% only when effective replicas <= 1. +*/}} +{{- $protected := false -}} +{{- if $forceReplicas -}} + {{- if gt (int $forceReplicas) 1 -}} + {{- $protected = true -}} + {{- end -}} +{{- else if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" -}} + {{- $protected = true -}} +{{- else if and $replicas (gt (int $replicas) 1) -}} + {{- $protected = true -}} +{{- end -}} {{- /* YAML Spec */}} apiVersion: policy/v1 kind: PodDisruptionBudget @@ -19,8 +36,10 @@ metadata: annotations: {{- include "annotations" . |trim| nindent 4 }} spec: - {{- if (or (eq (int $replicas) 1) (eq "false" (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)))) }} - {{- /* We set PDB even if forceReplicas or replicas = 1 or if hpa is disabled */}} + {{- /* Allow eviction of unhealthy pods regardless of budget to prevent blocking node drains */}} + unhealthyPodEvictionPolicy: AlwaysAllow + {{- if not $protected }} + {{- /* We set PDB even if only 1 effective replica or if hpa is disabled */}} {{- /* This is because helm is not able to delete unknown-previous config. */}} {{- /* In this case we set the minAvailable to 0% so it behaves the same way as a PDB does not exist. */}} minAvailable: 0% diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index a66577d..2b56a0b 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -34,6 +34,20 @@ tests: - equal: path: metadata.annotations["meta.helm.sh/release-namespace"] value: NAMESPACE + - it: must always have unhealthyPodEvictionPolicy AlwaysAllow + set: + env: prd + asserts: + - equal: + path: spec.unhealthyPodEvictionPolicy + value: AlwaysAllow + - it: must have unhealthyPodEvictionPolicy even when minAvailable is 0% + set: + env: dev + asserts: + - equal: + path: spec.unhealthyPodEvictionPolicy + value: AlwaysAllow - it: must use default with 2 replicas or more set: env: prd @@ -88,7 +102,7 @@ tests: - equal: path: spec.minAvailable value: "30%" - - it: use minAvailable from pdb if not set on pdb or container + - it: pdb.minAvailable takes precedence over container.minAvailable set: env: prd pdb: @@ -101,7 +115,8 @@ tests: - equal: path: spec.minAvailable value: "25%" - - it: if container Replicas is set to 1, minAvailable must be 0% + # replicas=1 with HPA enabled: HPA forces min 2 pods, PDB should protect + - it: if container replicas is 1 in prd, HPA is active so minAvailable must be 50% set: env: prd container: @@ -110,18 +125,41 @@ tests: asserts: - equal: path: spec.minAvailable - value: "0%" - - it: if deployment Replicas is set to 1, minAvailable must be 0% + value: "50%" + - it: if deployment replicas is 1 in prd, HPA is active so minAvailable must be 50% set: env: prd deployment: replicas: 1 container: image: some + asserts: + - equal: + path: spec.minAvailable + value: "50%" + # replicas=1 without HPA: truly single pod, PDB should be 0% + - it: if container replicas is 1 in dev without HPA, minAvailable must be 0% + set: + env: dev + container: + image: some + replicas: 1 asserts: - equal: path: spec.minAvailable value: "0%" + - it: if deployment replicas is 1 in dev without HPA, minAvailable must be 0% + set: + env: dev + deployment: + replicas: 1 + container: + image: some + asserts: + - equal: + path: spec.minAvailable + value: "0%" + # forceReplicas=1: single pod, PDB should be 0% - it: if container forceReplicas is set to 1, minAvailable must be 0% set: env: prd @@ -143,37 +181,53 @@ tests: - equal: path: spec.minAvailable value: "0%" - - it: must use pdb if forceReplicas is set to more than 1 + # forceReplicas > 1: multiple pods, PDB should protect + - it: if container forceReplicas is 2, minAvailable must be 50% set: env: prd container: image: some forceReplicas: 2 - replicas: 2 asserts: - - hasDocuments: - count: 1 - - it: must use pdb if forceReplicas is set to more than 1 + - equal: + path: spec.minAvailable + value: "50%" + - it: if deployment forceReplicas is 2, minAvailable must be 50% + set: + env: prd + deployment: + forceReplicas: 2 + containers: + - image: some + asserts: + - equal: + path: spec.minAvailable + value: "50%" + - it: if container forceReplicas is 3, minAvailable must be 50% set: env: prd container: image: some - forceReplicas: 2 - replicas: 2 + forceReplicas: 3 asserts: - - hasDocuments: - count: 1 - - it: must use pdb if forceReplicas is set to more than 1 on deployment + - equal: + path: spec.minAvailable + value: "50%" + - it: if deployment forceReplicas is 3 with custom minAvailable, must use custom value set: env: prd + pdb: + minAvailable: 33% deployment: - forceReplicas: 2 + forceReplicas: 3 containers: - image: some asserts: - - hasDocuments: - count: 1 - - it: must use pdb if forceReplicas is set to 1 on deployment + - equal: + path: spec.minAvailable + value: "33%" + # PDB document always exists (helm can't delete previous config) + - it: must have pdb document when forceReplicas is 1 on deployment set: env: prd deployment: @@ -233,33 +287,43 @@ tests: - equal: path: spec.minAvailable value: "27%" - - it: minAvailable equals zero even if minAvailable is set on container if only one pod in tst + - it: minAvailable equals zero if no HPA and no replicas set in tst set: env: tst container: - minAvailable: 24% + replicas: null asserts: - equal: path: spec.minAvailable value: "0%" - - it: minAvailable equals zero even if minAvailable is set on container if only one pod in tst + - it: minAvailable equals zero if no HPA and no replicas set on deployment in tst set: env: tst - deployment: - minAvailable: 24% + container: + replicas: null asserts: - equal: path: spec.minAvailable value: "0%" - - it: minAvailable equals zero even if minAvailable is set on container if only one pod in dev + - it: minAvailable equals zero if no HPA and no replicas set in dev set: env: dev container: - minAvailable: 24% + replicas: null asserts: - equal: path: spec.minAvailable value: "0%" + - it: minAvailable from container is used when 2 replicas in dev without HPA + set: + env: dev + container: + replicas: 2 + minAvailable: 24% + asserts: + - equal: + path: spec.minAvailable + value: "24%" - it: use minAvailable from container if not set on pdb in dev set: env: dev @@ -270,3 +334,26 @@ tests: - equal: path: spec.minAvailable value: "29%" + # replicas=1 with HPA via maxReplicas in non-prd: HPA active, PDB should protect + - it: if replicas is 1 but maxReplicas set in dev, HPA is active so minAvailable must be 50% + set: + env: dev + container: + image: some + replicas: 1 + maxReplicas: 4 + asserts: + - equal: + path: spec.minAvailable + value: "50%" + # replicas > 1 without HPA in non-prd + - it: if replicas is 2 without HPA in dev, minAvailable must be 50% + set: + env: dev + container: + image: some + replicas: 2 + asserts: + - equal: + path: spec.minAvailable + value: "50%" From e41fd68a4a4dac0984b74b86e1d43b156dee06b8 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 11:49:37 +0100 Subject: [PATCH 02/66] fix: cron seccompProfile, postgres proxy placement, and per-ingress annotations - Add missing seccompProfile to cron pod securityContext (matches deployment) - Move postgres proxy outside container loop in cron to prevent duplicate sidecars - Support per-ingress annotations when using ingresses list Co-Authored-By: Claude Opus 4.6 (1M context) --- charts/common/templates/cron.yaml | 8 +++++--- charts/common/templates/ingress.yaml | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/charts/common/templates/cron.yaml b/charts/common/templates/cron.yaml index def2e8f..5d7cf97 100644 --- a/charts/common/templates/cron.yaml +++ b/charts/common/templates/cron.yaml @@ -52,6 +52,9 @@ spec: spec: serviceAccountName: {{ .Values.cron.serviceAccountName | default "application" }} containers: + {{- if $postgres.enabled }} + {{- include "gcloud_sql_proxy" (dict "postgres" $postgres "app" $app "releaseName" $releaseName) | indent 12 }} + {{- end }} {{ range $containers }} {{- $image := .image | required ".Values.common.container.image is required." -}} {{- printf "\n " -}} @@ -75,9 +78,6 @@ spec: lifecycle: {{- toYaml .lifecycle | nindent 16 }} {{- end }} - {{- if $postgres.enabled }} - {{- include "gcloud_sql_proxy" (dict "postgres" $postgres "app" $app) | indent 12 }} - {{- end }} {{- end }} {{- if $cronjob.volumes }} volumes: @@ -92,4 +92,6 @@ spec: runAsNonRoot: true runAsUser: {{ .Values.container.uid }} fsGroup: {{ .Values.container.uid }} + seccompProfile: + type: RuntimeDefault {{- end -}} \ No newline at end of file diff --git a/charts/common/templates/ingress.yaml b/charts/common/templates/ingress.yaml index 173600a..7964746 100644 --- a/charts/common/templates/ingress.yaml +++ b/charts/common/templates/ingress.yaml @@ -20,7 +20,9 @@ metadata: annotations: {{- include "annotations" $chart |trim| nindent 4 }} kubernetes.io/ingress.class: traefik - {{- if $.Values.ingress.annotations }} + {{- if .annotations }} + {{- toYaml .annotations | nindent 4 }} + {{- else if $.Values.ingress.annotations }} {{- toYaml $.Values.ingress.annotations | nindent 4 }} {{- end }} spec: From 1ae8c84fd26b882aac269d3a617a8d30b87d67dc Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 11:50:51 +0100 Subject: [PATCH 03/66] feat!: upgrade Cloud SQL proxy to v2 and source instances via External Secrets - Upgrade cloud-sql-proxy from v1 (1.33.16) to v2 (2.21.2) - Update image, executable name, and CLI flags for v2 - Fix memoryLimt typo in helpers, deprecate memoryLimit (limit = request) - Replace configmap-based connection config with External Secrets - Add postgres.instances to configure Secret Manager keys for instance connection names - Support multiple SQL databases via indexed CSQL_PROXY_INSTANCE_CONNECTION_NAME_N env vars - Deprecate postgres.connectionConfig with fail message - Fix deployment.prometheus.path not falling back to default (#225) BREAKING CHANGE: postgres.connectionConfig is removed in favor of postgres.instances. Users must migrate their connection config from Kubernetes ConfigMaps to Secret Manager keys via External Secrets (e.g. postgres.instances: [PGINSTANCES]). --- charts/common/templates/_helpers.tpl | 28 ++++++------- charts/common/templates/deployment.yaml | 4 +- charts/common/templates/sql-proxy-secret.yaml | 40 +++++++++++++++++++ charts/common/tests/deployment_test.yaml | 29 ++++++++++---- .../deployment_v1_compatibility_test.yaml | 12 +++++- charts/common/values.yaml | 12 ++++-- fixture/helm/values-postgres.yaml | 2 + 7 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 charts/common/templates/sql-proxy-secret.yaml diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 81e2133..fa7fec4 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -169,21 +169,18 @@ livenessProbe: {{- end }} {{- define "gcloud_sql_proxy" }} +{{- if .postgres.connectionConfig }} + {{- fail "postgres.connectionConfig is deprecated. Use postgres.instances instead. See migration guide for Cloud SQL Proxy v2." }} +{{- end }} - name: "{{ .app }}-sql-proxy" - image: eu.gcr.io/cloudsql-docker/gce-proxy:1.33.16 + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.21.2 command: - - "/cloud_sql_proxy" - - "-verbose=false" - - "-log_debug_stdout=true" - - "-structured_logs=true" - - "-term_timeout=30s" + - "/cloud-sql-proxy" + - "--structured-logs" + - "--max-sigterm-delay=30s" envFrom: - - configMapRef: - {{- if .postgres.connectionConfig }} - name: {{ .postgres.connectionConfig }} - {{- else }} - name: {{ .app }}-psql-connection - {{- end }} + - secretRef: + name: {{ .releaseName }}-sql-proxy securityContext: runAsNonRoot: true allowPrivilegeEscalation: false @@ -191,16 +188,15 @@ livenessProbe: drop: ["ALL"] seccompProfile: type: RuntimeDefault + {{- if .postgres.memoryLimit }} + {{- fail "postgres.memoryLimit is deprecated. Memory limit is now always equal to memory request. Remove memoryLimit and use postgres.memory instead." }} + {{- end }} resources: limits: {{- if .postgres.cpuLimit }} cpu: "{{ .postgres.cpuLimit }}" {{- end }} - {{- if .postgres.memoryLimt }} - memory: "{{ .postgres.memoryLimit }}Mi" - {{- else }} memory: "{{ .postgres.memory }}Mi" - {{- end }} requests: cpu: "{{ .postgres.cpu }}" memory: "{{ .postgres.memory }}Mi" diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index b4ea0f1..382d289 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -15,7 +15,7 @@ {{- $volumes := .Values.deployment.volumes | default .Values.container.volumes }} {{- $enabled := .Values.deployment.enabled | default .Values.container.enabled }} {{- $prometheus := .Values.deployment.prometheus | default .Values.container.prometheus }} -{{- $prometheusPath := (.Values.deployment.prometheus).path | default .Values.container.prometheus.path }} +{{- $prometheusPath := (.Values.deployment.prometheus).path | default .Values.container.prometheus.path | default "/actuator/prometheus" }} {{- $replicas := .Values.deployment.replicas | default .Values.container.replicas }} {{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas }} {{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas }} @@ -75,7 +75,7 @@ spec: containers: {{- if $postgres.enabled }} - {{- include "gcloud_sql_proxy" (dict "postgres" $postgres "app" $app) | indent 8 }} + {{- include "gcloud_sql_proxy" (dict "postgres" $postgres "app" $app "releaseName" $releaseName) | indent 8 }} {{- end }} {{ range $containers }} {{- $image := .image | required ".Values.common.container.image is required." -}} diff --git a/charts/common/templates/sql-proxy-secret.yaml b/charts/common/templates/sql-proxy-secret.yaml new file mode 100644 index 0000000..5a2ad52 --- /dev/null +++ b/charts/common/templates/sql-proxy-secret.yaml @@ -0,0 +1,40 @@ +{{- /* Rules */}} +{{- $chart := . -}} +{{- $releaseNamespace := .Release.Namespace -}} +{{- $releaseName := include "name" . -}} +{{- $postgres := .Values.postgres -}} +{{- if $postgres.enabled }} +{{- $instances := $postgres.instances | default list -}} +{{- if eq (len $instances) 0 }} + {{- fail "postgres.instances is required when postgres.enabled is true. Provide a list of Secret Manager keys containing instance connection names (e.g. [PGINSTANCES])." }} +{{- end }} +{{- /* YAML Spec */}} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $releaseName }}-sql-proxy + namespace: {{ $releaseNamespace }} + labels: + {{- include "labels" $chart |trim| nindent 4 }} + annotations: + timestamp: {{ now | date "2006-01-02T15:04:05" }} + {{- include "annotations" $chart |trim| nindent 4 }} +spec: + data: + {{- range $i, $secretKey := $instances }} + - remoteRef: + conversionStrategy: Default + decodingStrategy: None + key: {{ $secretKey }} + version: latest + secretKey: CSQL_PROXY_INSTANCE_CONNECTION_NAME_{{ $i }} + {{- end }} + refreshInterval: 1h + secretStoreRef: + kind: SecretStore + name: {{ $releaseNamespace }} + target: + creationPolicy: Owner + deletionPolicy: Delete + name: {{ $releaseName }}-sql-proxy +{{- end }} diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index 47fa305..d269a42 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -245,20 +245,23 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES credentialsSecret: my-secret asserts: - equal: path: spec.template.spec.containers[1].envFrom[0].secretRef.name value: my-secret - - it: override connectionConfig + - it: connectionConfig is deprecated set: postgres: enabled: true + instances: + - PGINSTANCES connectionConfig: my-config asserts: - - equal: - path: spec.template.spec.containers[0].envFrom[0].configMapRef.name - value: my-config + - failedTemplate: + errorMessage: "postgres.connectionConfig is deprecated. Use postgres.instances instead. See migration guide for Cloud SQL Proxy v2." - it: command use correct value set: containers: @@ -431,6 +434,8 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES cpu: 0.1 asserts: - notExists: @@ -502,6 +507,8 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES asserts: - equal: path: spec.template.spec.containers[1].envFrom[0].secretRef.name @@ -510,12 +517,14 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES asserts: - isNotEmpty: path: spec.template.spec.containers[1] - equal: - path: spec.template.spec.containers[0].envFrom[0].configMapRef.name - value: rudder-test-psql-connection + path: spec.template.spec.containers[0].envFrom[0].secretRef.name + value: rudder-test-sql-proxy - it: must enable only one sidecar if postgres enabled for multiple containers set: container: {} @@ -530,12 +539,14 @@ tests: enabled: false postgres: enabled: true + instances: + - PGINSTANCES asserts: - isNotEmpty: path: spec.template.spec.containers[2] - equal: - path: spec.template.spec.containers[0].envFrom[0].configMapRef.name - value: rudder-test-psql-connection + path: spec.template.spec.containers[0].envFrom[0].secretRef.name + value: rudder-test-sql-proxy - notExists: path: spec.template.spec.containers[3] - it: postgres cpu limit can be overridden @@ -543,6 +554,8 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES cpu: 0.1 cpuLimit: 1 asserts: diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml index 2af80ba..cb80503 100644 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ b/charts/common/tests/deployment_v1_compatibility_test.yaml @@ -119,6 +119,8 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES asserts: - equal: path: spec.template.spec.containers[1].envFrom[0].secretRef.name @@ -127,16 +129,20 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES asserts: - isNotEmpty: path: spec.template.spec.containers[1] - equal: - path: spec.template.spec.containers[0].envFrom[0].configMapRef.name - value: testsuite-psql-connection + path: spec.template.spec.containers[0].envFrom[0].secretRef.name + value: RELEASE-NAME-sql-proxy - it: postgres cpu limit can be overridden set: postgres: enabled: true + instances: + - PGINSTANCES cpu: "0.1" cpuLimit: "1" asserts: @@ -150,6 +156,8 @@ tests: set: postgres: enabled: true + instances: + - PGINSTANCES cpu: 0.1 asserts: - notExists: diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 5e5f39a..d779f2e 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -268,10 +268,14 @@ postgres: # -- Configure memory request for proxy without units, `Mi` inferred # @default -- 16 memory: 16 - # -- Configure memoryLimit for proxy without units, `Mi` inferred - # @default -- 16 - memoryLimit: 16 - # -- Override name for connection configmap. This must at least contain `INSTANCES`. + # -- (float) @deprecated memoryLimit is deprecated and will cause a deploy failure if set. Memory limit is now always equal to memory request. Use `postgres.memory` instead. + memoryLimit: + # -- List of Secret Manager keys containing Cloud SQL instance connection names. Supports multiple databases. Each key is mapped to `CSQL_PROXY_INSTANCE_CONNECTION_NAME_N` for the v2 proxy. The secret keys match those created by the `entur/terraform-google-sql-db` module (e.g. `PGINSTANCES`). + # @default -- [] + instances: [] + # - PGINSTANCES + # - ANALYTICS_PGINSTANCES + # -- @deprecated connectionConfig is deprecated. Use `postgres.instances` instead to source connection names from Secret Manager via External Secrets. connectionConfig: # -- Override name for credentials secret. This must at least contain `PGUSER` and `PGPASSWORD`. credentialsSecret: diff --git a/fixture/helm/values-postgres.yaml b/fixture/helm/values-postgres.yaml index 9d17b31..2b8c6db 100644 --- a/fixture/helm/values-postgres.yaml +++ b/fixture/helm/values-postgres.yaml @@ -13,3 +13,5 @@ container: postgres: enabled: true + instances: + - PGINSTANCES From f3b050b17ef40fb3622182cfb77ed343cc3e8318 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 11:51:02 +0100 Subject: [PATCH 04/66] docs: add AGENTS.md and symlink CLAUDE.md --- AGENTS.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 84 insertions(+) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..690d924 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,83 @@ +# AGENTS.md — Entur Helm Charts + +## Project Overview + +This repository contains Entur's opinionated Helm charts for deploying applications to Kubernetes. The primary chart is `common` (in `charts/common/`), which provides a convention-over-configuration approach for Spring Boot apps and general workloads. Example charts in `examples/common/` demonstrate usage patterns. + +## Repository Structure + +``` +charts/common/ # The main Helm chart (source of truth) + templates/ # Kubernetes resource templates + tests/ # helm-unittest test suites + tests/values/ # Shared test values + values.yaml # Default values (heavily documented) +examples/common/ # 7 example charts showing usage patterns +fixture/helm/ # Fixture values for template rendering validation +.github/workflows/ # CI/CD (PR checks, release, docs generation) +``` + +## Development Commands + +```bash +# Run unit tests (primary validation step) +helm unittest ./charts/common + +# Render templates with fixture values to verify output +helm template charts/common -f fixture/helm/values-minimal.yaml +helm template charts/common -f fixture/helm/values-cron.yaml +helm template charts/common -f fixture/helm/values-secrets.yaml +helm template charts/common -f fixture/helm/values-postgres.yaml + +# Regenerate chart documentation (README.md files) +helm-docs + +# Update dependencies for example charts after version bump +helm dependency update examples/common/ +``` + +## Testing + +- **Framework**: [helm-unittest](https://github.com/helm-unittest/helm-unittest) — YAML-based declarative assertions +- **Test location**: `charts/common/tests/*_test.yaml` +- **Shared test values**: `charts/common/tests/values/common-test-values.yaml` +- **Snapshots**: `charts/common/tests/__snapshot__/` +- **Always run `helm unittest ./charts/common` after modifying any template or values** +- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, and v1 backward compatibility + +## Key Conventions + +### Commit Messages +Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): +- `feat!:` — breaking change (major version bump) +- `feat:` — new feature (minor version bump) +- `fix:` — bug fix (patch version bump) +- `docs:` / `ci:` / `chore:` — no version bump + +### Chart Design Principles +- Environment-aware defaults: `prd` automatically enables HA (HPA, PDB) +- Resource convention: CPU limit = 5x request, memory limit = 1.2x request +- Security: non-root, no privilege escalation, drop all capabilities +- Traffic types must be explicit: `api`, `public`, or `http2` +- Template helpers are in `charts/common/templates/_helpers.tpl` + +### Values Patterns +- Required fields: `app`, `team`, `env`, `container.image` +- Environment values: `dev`, `tst`, `prd` +- Single container: use `container:` key +- Multiple containers: use `containers:` list +- Environment-specific overrides go in `env/values-kub-ent-{dev,tst,prd}.yaml` + +## CI/CD + +- **PR checks**: lint, unit tests, kind cluster install tests across all examples and environments +- **Release**: automated via release-please with semantic versioning; tags like `common-v1.21.1` +- **Docs**: auto-generated on release branches via helm-docs workflow +- **Ownership**: `@entur/team-plattform` (see CODEOWNERS) + +## Important Notes + +- `README.md` files in charts are **auto-generated** by helm-docs — do not edit them manually; edit `values.yaml` comments or `Chart.yaml` description instead +- Example charts pin their dependency on `common` — update both `Chart.yaml` version and run `helm dependency update` when changing +- The chart supports both Deployment and CronJob workloads (mutually exclusive via `deployment.enabled` / `cron.enabled`) +- Fixture values in `fixture/helm/` are used for CI template rendering validation diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From af60a30273bf0fd7019e3225ec037fb7408ad9b3 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:03:15 +0100 Subject: [PATCH 05/66] feat: move cpuUtilization under deployment (#221) Add deployment.cpuUtilization as the preferred location for HPA CPU target utilization. Falls back to top-level cpuUtilization for backwards compatibility, then to default 100%. --- charts/common/templates/hpa.yaml | 2 +- charts/common/tests/hpa_test.yaml | 10 ++++++++++ charts/common/values.yaml | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index 02c27ae..5eb6469 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -5,7 +5,7 @@ {{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas }} {{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas }} {{- $hpa := .Values.hpa | default dict }} -{{- $cpuUtilization := .Values.cpuUtilization | default 100 }} +{{- $cpuUtilization := .Values.deployment.cpuUtilization | default .Values.cpuUtilization | default 100 }} {{- if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" }} {{- /* Rules */}} diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index 50299f6..f451fd1 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -75,6 +75,16 @@ tests: - equal: path: spec.metrics[0].resource.target.averageUtilization value: 100 + - it: deployment.cpuUtilization overrides default + set: + container: + maxReplicas: 5 + deployment: + cpuUtilization: 60 + asserts: + - equal: + path: spec.metrics[0].resource.target.averageUtilization + value: 60 - it: Must use minimum two replicas in prod, max 10 set: env: prd diff --git a/charts/common/values.yaml b/charts/common/values.yaml index d779f2e..1ee1d02 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -61,6 +61,9 @@ deployment: # -- Override pod serviceAccountName (default application). # @default -- application serviceAccountName: + # -- Set the target CPU average utilization (%) for HPA scaling. Default is 100% because Java apps are resource heavy during startup. Lower to 50-60% if you have good startup/readiness probes. + # @default -- 100 + cpuUtilization: # -- See https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#progress-deadline-seconds # @default -- 0 minReadySeconds: 0 From 1d1df2e007fe1a6779651f7c6828943c2e722487 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:17:02 +0100 Subject: [PATCH 06/66] feat: add GKE Startup CPU Boost and lower default cpuUtilization to 70% - Add StartupCPUBoost CRD resource (enabled by default, 50% increase) - Boost targets pods by app label, reverts when pod becomes Ready - Lower default HPA cpuUtilization from 100% to 70% (best practice when startup CPU spikes are handled by the boost operator) - Requires kube-startup-cpu-boost operator installed in the cluster --- charts/common/templates/hpa.yaml | 2 +- .../common/templates/startup-cpu-boost.yaml | 28 +++++++++++++++++++ charts/common/tests/hpa_test.yaml | 4 +-- charts/common/values.yaml | 10 +++++-- 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 charts/common/templates/startup-cpu-boost.yaml diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index 5eb6469..832e66d 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -5,7 +5,7 @@ {{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas }} {{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas }} {{- $hpa := .Values.hpa | default dict }} -{{- $cpuUtilization := .Values.deployment.cpuUtilization | default .Values.cpuUtilization | default 100 }} +{{- $cpuUtilization := .Values.deployment.cpuUtilization | default .Values.cpuUtilization | default 70 }} {{- if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" }} {{- /* Rules */}} diff --git a/charts/common/templates/startup-cpu-boost.yaml b/charts/common/templates/startup-cpu-boost.yaml new file mode 100644 index 0000000..b54c3fd --- /dev/null +++ b/charts/common/templates/startup-cpu-boost.yaml @@ -0,0 +1,28 @@ +{{- $releaseName := include "name" . -}} +{{- $startupCPUBoost := .Values.deployment.startupCPUBoost | default dict -}} +{{- if and .Values.deployment.enabled $startupCPUBoost.enabled }} +apiVersion: autoscaling.x-k8s.io/v1alpha1 +kind: StartupCPUBoost +metadata: + name: {{ $releaseName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "labels" . |trim| nindent 4 }} + annotations: + {{- include "annotations" . |trim| nindent 4 }} +selector: + matchExpressions: + - key: app + operator: In + values: ["{{ $releaseName }}"] +spec: + resourcePolicy: + containerPolicies: + - containerName: "*" + percentageIncrease: + value: {{ $startupCPUBoost.percentageIncrease | default 50 }} + durationPolicy: + podCondition: + type: Ready + status: "True" +{{- end }} diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index f451fd1..458fd87 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -67,14 +67,14 @@ tests: path: spec.minReplicas value: 4 # #TODO 100% cpu target is set as default? - - it: uses 100 % as target cpu + - it: uses 70 % as default target cpu set: container: maxReplicas: 5 asserts: - equal: path: spec.metrics[0].resource.target.averageUtilization - value: 100 + value: 70 - it: deployment.cpuUtilization overrides default set: container: diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 1ee1d02..ce45e04 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -61,9 +61,15 @@ deployment: # -- Override pod serviceAccountName (default application). # @default -- application serviceAccountName: - # -- Set the target CPU average utilization (%) for HPA scaling. Default is 100% because Java apps are resource heavy during startup. Lower to 50-60% if you have good startup/readiness probes. - # @default -- 100 + # -- Set the target CPU average utilization (%) for HPA scaling. With startupCPUBoost enabled, 70% is a good default. Without it, 100% may be needed for Java apps with heavy startup CPU usage. + # @default -- 70 cpuUtilization: + startupCPUBoost: + # -- Enable GKE Startup CPU Boost to temporarily increase CPU during pod startup. Requires the kube-startup-cpu-boost operator installed in the cluster. Boost is reverted when the pod becomes Ready. + enabled: true + # -- (int) Percentage to increase CPU requests during startup + # @default -- 50 + percentageIncrease: 50 # -- See https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#progress-deadline-seconds # @default -- 0 minReadySeconds: 0 From 684abcfef34767303aa43007ac37dee5b274d9b6 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:18:46 +0100 Subject: [PATCH 07/66] feat: allow path for startup probe (#237) When container.probes.startup.path is set, the startup probe uses httpGet instead of tcpSocket. This enables custom startup health checks for apps with long-running startup tasks like cache warming. --- charts/common/templates/_helpers.tpl | 6 ++++++ charts/common/values.yaml | 2 ++ 2 files changed, 8 insertions(+) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index fa7fec4..2f256b6 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -121,8 +121,14 @@ readinessProbe: failureThreshold: {{ .probes.readiness.failureThreshold | default 6 }} periodSeconds: {{ .probes.readiness.periodSeconds | default 5 }} startupProbe: + {{- if .probes.startup.path }} + httpGet: + path: {{ .probes.startup.path }} + port: {{ .probes.startup.port | default .internalPort }} + {{- else }} tcpSocket: port: {{ .probes.startup.port | default .internalPort }} + {{- end }} failureThreshold: {{ .probes.startup.failureThreshold | default 300 }} periodSeconds: {{ .probes.startup.periodSeconds | default 1 }} {{- end }} diff --git a/charts/common/values.yaml b/charts/common/values.yaml index ce45e04..aab3bd1 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -243,6 +243,8 @@ container: grpc: # port: 8080 startup: + # -- Set the path for startup probe. If set, uses httpGet instead of tcpSocket. Useful when startup includes long-running tasks like cache warming. + path: # -- Set the failure threshold # @default -- 300 failureThreshold: 300 From 473e5022c3ced7e64f2d0a8bb01060d77a11d95c Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:22:21 +0100 Subject: [PATCH 08/66] docs: add downtime warning for ingress trafficType changes (#235) --- charts/common/README.md | 2 +- charts/common/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/common/README.md b/charts/common/README.md index 2f6ba23..71f6cef 100644 --- a/charts/common/README.md +++ b/charts/common/README.md @@ -142,7 +142,7 @@ common: | ingress.annotations | object | `{}` | Optionally set annotations for the ingress | | ingress.enabled | bool | `true` | Enable or disable the ingress | | ingress.host | string | `nil` | Set the host name, do this in your `values-kub-ent-$env.yaml` files | -| ingress.trafficType | string | `nil` | Set the traffic type (`api`,`public` or `http2` for gRPC) | +| ingress.trafficType | string | `nil` | Set the traffic type (`api`,`public` or `http2` for gRPC). Note: changing this value will cause a couple of minutes of downtime while the ingress controller reconciles. | | ingresses | list | `[]` | Specify a list of `ingress` specs | | initContainers | list | `[]` | See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | | labels | object | `{ app shortname team common:version environment }` | Specify additional labels for every resource | diff --git a/charts/common/values.yaml b/charts/common/values.yaml index aab3bd1..7ec5689 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -18,7 +18,7 @@ ingress: enabled: true # -- Set the host name, do this in your `values-kub-ent-$env.yaml` files host: - # -- Set the traffic type (`api`,`public` or `http2` for gRPC) + # -- Set the traffic type (`api`,`public` or `http2` for gRPC). Note: changing this value will cause a couple of minutes of downtime while the ingress controller reconciles. trafficType: # -- Optionally set annotations for the ingress annotations: {} From 52a0e7dfb0e030671ffa9c48bd9b94cd4d75303f Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:24:53 +0100 Subject: [PATCH 09/66] fix: use native K8s gRPC probes by default when grpc is enabled (#101) Setting grpc: true now uses native K8s gRPC probes with internalPort by default. No need to manually set probe ports for each probe. Removes fallback to exec-based grpc_health_probe which required the binary in the container image. --- charts/common/templates/_helpers.tpl | 6 +++--- charts/common/templates/deployment.yaml | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 2f256b6..58d4ddc 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -136,19 +136,19 @@ startupProbe: {{- define "grpcprobes" }} startupProbe: grpc: - port: {{ .probes.startup.grpc.port | default .internalPort }} + port: {{ ((.probes.startup).grpc).port | default .internalPort }} initialDelaySeconds: 10 failureThreshold: 30 periodSeconds: 10 readinessProbe: grpc: - port: {{ .probes.readiness.grpc.port | default .internalPort }} + port: {{ ((.probes.readiness).grpc).port | default .internalPort }} initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 livenessProbe: grpc: - port: {{ .probes.liveness.grpc.port | default .internalPort }} + port: {{ ((.probes.liveness).grpc).port | default .internalPort }} initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index 382d289..a9553a7 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -100,11 +100,7 @@ spec: {{- include "resources" . | nindent 10 }} {{- include "securitycontext" . | nindent 10 }} {{- if or $grpc .grpc }} - {{- if .probes.liveness.grpc }} - {{- include "grpcprobes" (dict "internalPort" $internalPort "probes" .probes) | nindent 10 -}} - {{- else }} - {{- include "grpcexecprobes" (dict "internalPort" $internalPort) | nindent 10 -}} - {{- end }} + {{- include "grpcprobes" (dict "internalPort" $internalPort "probes" .probes) | nindent 10 -}} {{- else if (and (ne .probes.enabled false) .probes.spec) }} {{ toYaml .probes.spec | nindent 10 }} {{- else if ne .probes.enabled false }} From 5dea1c8273477f16fedad01b564f8e59c90e5501 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:27:18 +0100 Subject: [PATCH 10/66] fix: replace deprecated ingress.class annotation with spec.ingressClassName (#126) Replace kubernetes.io/ingress.class annotation (deprecated since K8s 1.18) with spec.ingressClassName. Defaults to "traefik", configurable via ingress.ingressClassName or per-ingress ingressClassName field. --- charts/common/templates/ingress.yaml | 2 +- charts/common/tests/ingress_test.yaml | 2 +- charts/common/values.yaml | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/charts/common/templates/ingress.yaml b/charts/common/templates/ingress.yaml index 7964746..b9cc2fb 100644 --- a/charts/common/templates/ingress.yaml +++ b/charts/common/templates/ingress.yaml @@ -19,13 +19,13 @@ metadata: traffic-type: {{.trafficType }} annotations: {{- include "annotations" $chart |trim| nindent 4 }} - kubernetes.io/ingress.class: traefik {{- if .annotations }} {{- toYaml .annotations | nindent 4 }} {{- else if $.Values.ingress.annotations }} {{- toYaml $.Values.ingress.annotations | nindent 4 }} {{- end }} spec: + ingressClassName: {{ .ingressClassName | default $.Values.ingress.ingressClassName | default "traefik" }} rules: {{- if .rules -}} {{ toYaml .rules | nindent 4 }} diff --git a/charts/common/tests/ingress_test.yaml b/charts/common/tests/ingress_test.yaml index 44c8b30..77d6eb9 100644 --- a/charts/common/tests/ingress_test.yaml +++ b/charts/common/tests/ingress_test.yaml @@ -45,7 +45,7 @@ tests: path: spec.rules[0].host value: test.dev.entur.io - equal: - path: metadata.annotations["kubernetes.io/ingress.class"] + path: spec.ingressClassName value: "traefik" - equal: path: metadata.labels.traffic-type diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 7ec5689..fb08dfa 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -20,6 +20,9 @@ ingress: host: # -- Set the traffic type (`api`,`public` or `http2` for gRPC). Note: changing this value will cause a couple of minutes of downtime while the ingress controller reconciles. trafficType: + # -- Set the IngressClass name. Uses `spec.ingressClassName` (replaces the deprecated `kubernetes.io/ingress.class` annotation). + # @default -- traefik + ingressClassName: # -- Optionally set annotations for the ingress annotations: {} # rules: # k8s spec for ingress rules From 8ca4d06b0df76dd6eb2ac07ec2fc0b9e4e46a49a Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:34:32 +0100 Subject: [PATCH 11/66] feat: add appId as preferred input, deprecate shortname Add appId field matching GoogleCloudApplication metadata.id. Falls back to shortname for backwards compatibility. Adds new "appId" label to all resources alongside existing "shortname" label. --- charts/common/templates/_helpers.tpl | 3 ++- charts/common/templates/cron.yaml | 2 +- charts/common/templates/deployment.yaml | 2 +- charts/common/values.yaml | 4 +++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 58d4ddc..60901ba 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -4,7 +4,8 @@ {{- define "labels" }} app: {{ empty .Values.releaseName | ternary .Release.Name .Values.releaseName }} -shortname: {{ .Values.shortname }} +appId: {{ .Values.appId | default .Values.shortname }} +shortname: {{ .Values.appId | default .Values.shortname }} team: {{ .Values.team }} common: {{ .Chart.Version }} environment: {{ .Values.env }} diff --git a/charts/common/templates/cron.yaml b/charts/common/templates/cron.yaml index 5d7cf97..ec19d99 100644 --- a/charts/common/templates/cron.yaml +++ b/charts/common/templates/cron.yaml @@ -1,7 +1,7 @@ {{- /* Rules */}} {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $app := .Values.app | required ".Values.common.app is required." -}} -{{- $shortname := .Values.shortname | required ".Values.common.shortname is required." -}} +{{- $appId := (.Values.appId | default .Values.shortname) | required ".Values.common.appId is required." -}} {{- $team := .Values.team | required ".Values.common.team is required." -}} {{- $releaseName := include "name" . -}} diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index a9553a7..20098ff 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -1,7 +1,7 @@ {{- /* Rules */}} {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $app := .Values.app | required ".Values.common.app is required." -}} -{{- $shortname := .Values.shortname | required ".Values.common.shortname is required." -}} +{{- $appId := (.Values.appId | default .Values.shortname) | required ".Values.common.appId is required." -}} {{- $team := .Values.team | required ".Values.common.team is required." -}} {{- $releaseName := include "name" . -}} diff --git a/charts/common/values.yaml b/charts/common/values.yaml index fb08dfa..1d9412a 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -1,6 +1,8 @@ # -- Application name, typically on the form `the-application` app: -# -- `id` for GCP 2.0, typically on the form `theapp`. Max 10 characters +# -- App ID from GoogleCloudApplication `metadata.id`. Max 10 alphanumeric characters. See https://github.com/entur/tf-gcp-apps/blob/main/docs/manifests/GoogleCloudApplication.md +appId: +# -- @deprecated Use `appId` instead. shortname: # -- Your team name, without a `team-` prefix team: From a9f0915d9ece4060bb1764c18a76f05176eeda2d Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:35:56 +0100 Subject: [PATCH 12/66] docs: update AGENTS.md with tools and expanded commands --- AGENTS.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 690d924..38f7d6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,23 +17,44 @@ fixture/helm/ # Fixture values for template rendering validation .github/workflows/ # CI/CD (PR checks, release, docs generation) ``` +## Tools + +- **`helm`** — render templates, manage dependencies, package charts +- **`helm unittest`** — run YAML-based unit tests (plugin: helm-unittest) +- **`helm template`** — render and inspect template output with fixture values +- **`helm-docs`** — auto-generate README.md from values.yaml comments +- **`gh`** — GitHub CLI for issues, PRs, releases, and CI status + ## Development Commands ```bash -# Run unit tests (primary validation step) +# Run unit tests (primary validation step — always run after any template/values change) helm unittest ./charts/common +# Run a single test file +helm unittest ./charts/common -f tests/pdb_test.yaml + # Render templates with fixture values to verify output helm template charts/common -f fixture/helm/values-minimal.yaml helm template charts/common -f fixture/helm/values-cron.yaml helm template charts/common -f fixture/helm/values-secrets.yaml helm template charts/common -f fixture/helm/values-postgres.yaml +# Render a single template +helm template test charts/common -f fixture/helm/values-minimal.yaml --show-only templates/pdb.yaml + +# Render with value overrides (useful for testing specific scenarios) +helm template test charts/common -f fixture/helm/values-minimal.yaml --set env=prd --set container.replicas=2 + # Regenerate chart documentation (README.md files) helm-docs # Update dependencies for example charts after version bump helm dependency update examples/common/ + +# View GitHub issues +gh issue view --repo entur/helm-charts +gh issue view --repo entur/helm-charts --comments ``` ## Testing @@ -43,7 +64,7 @@ helm dependency update examples/common/ - **Shared test values**: `charts/common/tests/values/common-test-values.yaml` - **Snapshots**: `charts/common/tests/__snapshot__/` - **Always run `helm unittest ./charts/common` after modifying any template or values** -- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, and v1 backward compatibility +- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, startup-cpu-boost, sql-proxy, and v1 backward compatibility ## Key Conventions @@ -57,16 +78,18 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): ### Chart Design Principles - Environment-aware defaults: `prd` automatically enables HA (HPA, PDB) - Resource convention: CPU limit = 5x request, memory limit = 1.2x request -- Security: non-root, no privilege escalation, drop all capabilities +- Security: non-root, no privilege escalation, drop all capabilities, seccompProfile RuntimeDefault - Traffic types must be explicit: `api`, `public`, or `http2` - Template helpers are in `charts/common/templates/_helpers.tpl` +- Deprecated values use `fail` to give clear migration messages ### Values Patterns -- Required fields: `app`, `team`, `env`, `container.image` +- Required fields: `app`, `appId` (or `shortname`), `team`, `env`, `container.image` - Environment values: `dev`, `tst`, `prd` - Single container: use `container:` key - Multiple containers: use `containers:` list - Environment-specific overrides go in `env/values-kub-ent-{dev,tst,prd}.yaml` +- Postgres/Cloud SQL: use `postgres.instances` with Secret Manager keys via External Secrets ## CI/CD From 77a1a4fe02695e6662faf75e98a7f1b0fa9222de Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:49:29 +0100 Subject: [PATCH 13/66] feat: support custom HPA metrics alongside default CPU scaling Add hpa.metrics list for appending custom metrics (Pods, External, Object) alongside the default CPU utilization metric. Supports Prometheus/GMP gauges, Pub/Sub queue depth, and any Cloud Monitoring metric. The existing hpa.spec override for full control is preserved. --- charts/common/templates/hpa.yaml | 3 +++ charts/common/tests/hpa_test.yaml | 25 ++++++++++++++++++ charts/common/values.yaml | 42 ++++++++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index 832e66d..75ca088 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -34,6 +34,9 @@ spec: target: type: Utilization averageUtilization: {{ $cpuUtilization }} + {{- range ((.Values.hpa).metrics) }} + - {{ toYaml . | nindent 4 }} + {{- end }} maxReplicas: {{ $maxReplicas | default 10 }} minReplicas: {{ max 2 $replicas }} {{- end }} diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index 458fd87..effe3be 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -85,6 +85,31 @@ tests: - equal: path: spec.metrics[0].resource.target.averageUtilization value: 60 + - it: custom metrics are appended alongside CPU metric + set: + env: prd + hpa: + metrics: + - type: Pods + pods: + metric: + name: prometheus.googleapis.com|http_requests|gauge + target: + type: AverageValue + averageValue: 100 + asserts: + - equal: + path: spec.metrics[0].type + value: Resource + - equal: + path: spec.metrics[1].type + value: Pods + - equal: + path: spec.metrics[1].pods.metric.name + value: "prometheus.googleapis.com|http_requests|gauge" + - equal: + path: spec.metrics[1].pods.target.averageValue + value: 100 - it: Must use minimum two replicas in prod, max 10 set: env: prd diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 1d9412a..ae13a57 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -112,10 +112,44 @@ cron: # -- (int) Active deadline seconds for the job, default 24 hours (86300s) activeDeadlineSeconds: hpa: - # -- Custom spec for HPA, inherits `scaleTargetRef` and min/max replicas. - # ps: Reason why we have set 100% cpu as default is because the java applications are resource hogs during startup. - # If you have good startupProbe/readinessProbes in place you can lower the cpu average utilization to ie 50/60%. - # - Or scale on other (custom) metrics. + # -- Additional HPA metrics appended alongside the default CPU metric. Accepts standard `autoscaling/v2` metric entries (Pods, Object, External). + # Use for scaling on custom metrics from Cloud Monitoring, Prometheus (GMP), or Pub/Sub. When multiple metrics are specified, HPA picks the one demanding the most replicas. + # @default -- [] + metrics: [] + # Pods type (per-pod custom metric, averaged across pods — supports AverageValue only): + # - type: Pods + # pods: + # metric: + # name: prometheus.googleapis.com|http_requests_total|gauge + # target: + # type: AverageValue + # averageValue: 100 + # + # External type (e.g. Pub/Sub queue depth — supports Value and AverageValue): + # - type: External + # external: + # metric: + # name: pubsub.googleapis.com|subscription|num_undelivered_messages + # selector: + # matchLabels: + # resource.labels.subscription_id: my-subscription + # target: + # type: AverageValue + # averageValue: 5 + # + # Object type (metric from another K8s object — supports Value and AverageValue): + # - type: Object + # object: + # metric: + # name: requests-per-second + # describedObject: + # apiVersion: networking.k8s.io/v1 + # kind: Ingress + # name: main-route + # target: + # type: Value + # value: 10k + # -- Full custom spec for HPA, replaces default metrics and min/max replicas. Inherits `scaleTargetRef`. spec: {} # Example for custom spec where cpu scaling is set to 60%: From 050554196674209a8efafe49b83c589021bd764d Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 12:55:08 +0100 Subject: [PATCH 14/66] =?UTF-8?q?feat!:=20v2=20cleanup=20=E2=80=94=20remov?= =?UTF-8?q?e=20dead=20code,=20require=20appId,=20simplify=20HPA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused grpcexecprobes helper (native K8s gRPC probes are now default) - Remove shortname value entirely — appId is now required with fail message for migration - Remove top-level cpuUtilization fallback, use only deployment.cpuUtilization - Fix readiness probe comment typo (said "liveness") - Rename shortname to appId in all fixtures and test values BREAKING CHANGE: shortname is removed. Use appId instead. --- charts/common/templates/_helpers.tpl | 28 ++++--------------- charts/common/templates/cron.yaml | 2 +- charts/common/templates/deployment.yaml | 2 +- charts/common/templates/hpa.yaml | 2 +- .../tests/values/common-test-values.yaml | 2 +- .../tests/values/deployment-v1-values.yaml | 2 +- charts/common/values.yaml | 4 +-- fixture/helm/ci/values-ci-cronjob-tests.yaml | 2 +- fixture/helm/ci/values-ci-tests.yaml | 2 +- fixture/helm/values-cron.yaml | 2 +- fixture/helm/values-minimal.yaml | 2 +- fixture/helm/values-postgres.yaml | 2 +- fixture/helm/values-secrets.yaml | 2 +- 13 files changed, 17 insertions(+), 37 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 60901ba..7c0283c 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -4,8 +4,11 @@ {{- define "labels" }} app: {{ empty .Values.releaseName | ternary .Release.Name .Values.releaseName }} -appId: {{ .Values.appId | default .Values.shortname }} -shortname: {{ .Values.appId | default .Values.shortname }} +{{- if and (not .Values.appId) .Values.shortname }} + {{- fail "shortname is deprecated. Use appId instead." }} +{{- end }} +appId: {{ .Values.appId }} +shortname: {{ .Values.appId }} team: {{ .Values.team }} common: {{ .Chart.Version }} environment: {{ .Values.env }} @@ -154,27 +157,6 @@ livenessProbe: periodSeconds: 10 timeoutSeconds: 5 {{- end }} -{{- define "grpcexecprobes" }} -startupProbe: - exec: - command: ["/bin/grpc_health_probe", "-addr=:{{ .internalPort }}", "-service=ready"] - initialDelaySeconds: 10 - failureThreshold: 30 - periodSeconds: 10 -readinessProbe: - exec: - command: ["/bin/grpc_health_probe", "-addr=:{{ .internalPort }}", "-service=ready"] - initialDelaySeconds: 10 - periodSeconds: 10 - timeoutSeconds: 5 -livenessProbe: - exec: - command: ["/bin/grpc_health_probe", "-addr=:{{ .internalPort }}", "-service=health"] - initialDelaySeconds: 10 - periodSeconds: 10 - timeoutSeconds: 5 -{{- end }} - {{- define "gcloud_sql_proxy" }} {{- if .postgres.connectionConfig }} {{- fail "postgres.connectionConfig is deprecated. Use postgres.instances instead. See migration guide for Cloud SQL Proxy v2." }} diff --git a/charts/common/templates/cron.yaml b/charts/common/templates/cron.yaml index ec19d99..5b46276 100644 --- a/charts/common/templates/cron.yaml +++ b/charts/common/templates/cron.yaml @@ -1,7 +1,7 @@ {{- /* Rules */}} {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $app := .Values.app | required ".Values.common.app is required." -}} -{{- $appId := (.Values.appId | default .Values.shortname) | required ".Values.common.appId is required." -}} +{{- $appId := .Values.appId | required ".Values.common.appId is required." -}} {{- $team := .Values.team | required ".Values.common.team is required." -}} {{- $releaseName := include "name" . -}} diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index 20098ff..6626d1b 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -1,7 +1,7 @@ {{- /* Rules */}} {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $app := .Values.app | required ".Values.common.app is required." -}} -{{- $appId := (.Values.appId | default .Values.shortname) | required ".Values.common.appId is required." -}} +{{- $appId := .Values.appId | required ".Values.common.appId is required." -}} {{- $team := .Values.team | required ".Values.common.team is required." -}} {{- $releaseName := include "name" . -}} diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index 75ca088..f76a433 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -5,7 +5,7 @@ {{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas }} {{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas }} {{- $hpa := .Values.hpa | default dict }} -{{- $cpuUtilization := .Values.deployment.cpuUtilization | default .Values.cpuUtilization | default 70 }} +{{- $cpuUtilization := .Values.deployment.cpuUtilization | default 70 }} {{- if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" }} {{- /* Rules */}} diff --git a/charts/common/tests/values/common-test-values.yaml b/charts/common/tests/values/common-test-values.yaml index 0f4ed10..945008d 100644 --- a/charts/common/tests/values/common-test-values.yaml +++ b/charts/common/tests/values/common-test-values.yaml @@ -24,4 +24,4 @@ container: periodSeconds: 1 prometheus: enabled: true -shortname: rudder +appId: rudder diff --git a/charts/common/tests/values/deployment-v1-values.yaml b/charts/common/tests/values/deployment-v1-values.yaml index 9548814..cc7a078 100644 --- a/charts/common/tests/values/deployment-v1-values.yaml +++ b/charts/common/tests/values/deployment-v1-values.yaml @@ -1,6 +1,6 @@ env: dev app: testsuite -shortname: tstsut +appId: tstsut team: common ingress: host: test.dev.entur.io diff --git a/charts/common/values.yaml b/charts/common/values.yaml index ae13a57..5b39b6b 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -2,8 +2,6 @@ app: # -- App ID from GoogleCloudApplication `metadata.id`. Max 10 alphanumeric characters. See https://github.com/entur/tf-gcp-apps/blob/main/docs/manifests/GoogleCloudApplication.md appId: -# -- @deprecated Use `appId` instead. -shortname: # -- Your team name, without a `team-` prefix team: # -- The current env, override in your `values-kub-ent-$env.yaml` files to `dev`, `tst` or `prd` @@ -264,7 +262,7 @@ container: grpc: # port: 8080 readiness: - # -- Set the path for liveness probe + # -- Set the path for readiness probe # @default -- /actuator/health/readiness path: "/actuator/health/readiness" # -- Set the initial delay for the probe diff --git a/fixture/helm/ci/values-ci-cronjob-tests.yaml b/fixture/helm/ci/values-ci-cronjob-tests.yaml index 9c3e661..0291af2 100644 --- a/fixture/helm/ci/values-ci-cronjob-tests.yaml +++ b/fixture/helm/ci/values-ci-cronjob-tests.yaml @@ -1,5 +1,5 @@ app: mycronjobtest -shortname: mycrntst +appId: mycrntst team: team env: dev diff --git a/fixture/helm/ci/values-ci-tests.yaml b/fixture/helm/ci/values-ci-tests.yaml index 7cf1750..b0063bf 100644 --- a/fixture/helm/ci/values-ci-tests.yaml +++ b/fixture/helm/ci/values-ci-tests.yaml @@ -1,5 +1,5 @@ app: mytest -shortname: mytst +appId: mytst team: team env: dev diff --git a/fixture/helm/values-cron.yaml b/fixture/helm/values-cron.yaml index c7d8b38..798d86c 100644 --- a/fixture/helm/values-cron.yaml +++ b/fixture/helm/values-cron.yaml @@ -1,5 +1,5 @@ app: my-app -shortname: myapp +appId: myapp team: plattform env: dev diff --git a/fixture/helm/values-minimal.yaml b/fixture/helm/values-minimal.yaml index 79c5975..fa700eb 100644 --- a/fixture/helm/values-minimal.yaml +++ b/fixture/helm/values-minimal.yaml @@ -1,5 +1,5 @@ app: my-app -shortname: myapp +appId: myapp team: mat env: dev diff --git a/fixture/helm/values-postgres.yaml b/fixture/helm/values-postgres.yaml index 2b8c6db..0773cc1 100644 --- a/fixture/helm/values-postgres.yaml +++ b/fixture/helm/values-postgres.yaml @@ -1,5 +1,5 @@ app: my-app -shortname: myapp +appId: myapp team: mat env: dev diff --git a/fixture/helm/values-secrets.yaml b/fixture/helm/values-secrets.yaml index 3d07f7b..5c3af75 100644 --- a/fixture/helm/values-secrets.yaml +++ b/fixture/helm/values-secrets.yaml @@ -1,5 +1,5 @@ app: my-app-with-secrets -shortname: myapps +appId: myapps team: platform env: dev From 72fba4dac00a2dda75e725622d3a1a2c23344963 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 13:04:01 +0100 Subject: [PATCH 15/66] feat!: remove container/deployment duality and enable SQL proxy prometheus metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove container.replicas, container.maxReplicas, container.forceReplicas, container.minAvailable, container.terminationGracePeriodSeconds — these are now only under deployment.* where they belong - Remove container.* fallbacks from deployment.yaml, hpa.yaml, pdb.yaml - Enable prometheus metrics on Cloud SQL proxy v2 (--http-port=9801 --prometheus) exposing metrics at :9801/metrics for monitoring proxy health and connections - Update all tests and fixtures to use deployment.* for scaling fields BREAKING CHANGE: container.replicas, container.maxReplicas, container.forceReplicas, container.minAvailable, and container.terminationGracePeriodSeconds are removed. Use deployment.replicas, deployment.maxReplicas, etc. instead. --- charts/common/templates/_helpers.tpl | 6 +++ charts/common/templates/deployment.yaml | 14 +++--- charts/common/templates/hpa.yaml | 8 +-- charts/common/templates/pdb.yaml | 10 ++-- charts/common/tests/deployment_test.yaml | 14 +++--- .../deployment_v1_compatibility_test.yaml | 12 +++-- charts/common/tests/hpa_test.yaml | 18 ++++--- charts/common/tests/pdb_test.yaml | 50 ++++++++++--------- .../tests/values/common-test-values.yaml | 3 +- charts/common/values.yaml | 11 ---- 10 files changed, 75 insertions(+), 71 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 7c0283c..6634f86 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -167,6 +167,12 @@ livenessProbe: - "/cloud-sql-proxy" - "--structured-logs" - "--max-sigterm-delay=30s" + - "--http-port=9801" + - "--prometheus" + ports: + - name: metrics + containerPort: 9801 + protocol: TCP envFrom: - secretRef: name: {{ .releaseName }}-sql-proxy diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index 6626d1b..770cc03 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -11,15 +11,15 @@ {{- $secrets := .Values.secrets -}} {{- $internalPort := .Values.service.internalPort -}} {{- $grpc := .Values.grpc -}} -{{- $labels := .Values.deployment.labels | default .Values.container.labels }} -{{- $volumes := .Values.deployment.volumes | default .Values.container.volumes }} -{{- $enabled := .Values.deployment.enabled | default .Values.container.enabled }} +{{- $labels := .Values.deployment.labels }} +{{- $volumes := .Values.deployment.volumes }} +{{- $enabled := .Values.deployment.enabled }} {{- $prometheus := .Values.deployment.prometheus | default .Values.container.prometheus }} {{- $prometheusPath := (.Values.deployment.prometheus).path | default .Values.container.prometheus.path | default "/actuator/prometheus" }} -{{- $replicas := .Values.deployment.replicas | default .Values.container.replicas }} -{{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas }} -{{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas }} -{{- $terminationGracePeriodSeconds := .Values.deployment.terminationGracePeriodSeconds | default .Values.container.terminationGracePeriodSeconds }} +{{- $replicas := .Values.deployment.replicas }} +{{- $forceReplicas := .Values.deployment.forceReplicas }} +{{- $maxReplicas := .Values.deployment.maxReplicas }} +{{- $terminationGracePeriodSeconds := .Values.deployment.terminationGracePeriodSeconds }} {{- $maxSurge := .Values.deployment.maxSurge | default "25%" }} {{- $maxUnavailable := .Values.deployment.maxUnavailable | default "25%" }} {{- $hpa := .Values.hpa | default dict }} diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index f76a433..72c865f 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -1,16 +1,16 @@ {{- /* Rules */}} {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $releaseName := include "name" . -}} -{{- $replicas := .Values.deployment.replicas | default .Values.container.replicas }} -{{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas }} -{{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas }} +{{- $replicas := .Values.deployment.replicas }} +{{- $forceReplicas := .Values.deployment.forceReplicas }} +{{- $maxReplicas := .Values.deployment.maxReplicas }} {{- $hpa := .Values.hpa | default dict }} {{- $cpuUtilization := .Values.deployment.cpuUtilization | default 70 }} {{- if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" }} {{- /* Rules */}} {{- if (eq 1 (int $maxReplicas)) }} - {{- required ".Values.common.container.maxReplicas must be more than 1." .Values.error -}} + {{- required ".Values.common.deployment.maxReplicas must be more than 1." .Values.error -}} {{- else }} {{- /* YAML Spec */}} diff --git a/charts/common/templates/pdb.yaml b/charts/common/templates/pdb.yaml index ab5fe22..ba1021a 100644 --- a/charts/common/templates/pdb.yaml +++ b/charts/common/templates/pdb.yaml @@ -2,12 +2,12 @@ {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $releaseName := include "name" . -}} {{- $releaseNamespace := .Release.Namespace -}} -{{- $forceReplicas := .Values.deployment.forceReplicas | default .Values.container.forceReplicas -}} -{{- $minAvailable := .Values.deployment.minAvailable | default .Values.container.minAvailable -}} +{{- $forceReplicas := .Values.deployment.forceReplicas -}} +{{- $minAvailable := .Values.deployment.minAvailable -}} {{- $minAvailablePDB := .Values.pdb.minAvailable -}} -{{- $replicas := .Values.deployment.replicas | default .Values.container.replicas -}} +{{- $replicas := .Values.deployment.replicas -}} {{- $hpa := .Values.hpa | default dict -}} -{{- $maxReplicas := .Values.deployment.maxReplicas | default .Values.container.maxReplicas -}} +{{- $maxReplicas := .Values.deployment.maxReplicas -}} {{- /* Determine effective replica count to decide if PDB should protect pods. - forceReplicas: exact count, no HPA @@ -44,7 +44,7 @@ spec: {{- /* In this case we set the minAvailable to 0% so it behaves the same way as a PDB does not exist. */}} minAvailable: 0% {{- else if ($minAvailablePDB) }} - {{- /* PDB.minAvailable takes precedence over deployment/container.minAvailable */}} + {{- /* PDB.minAvailable takes precedence over deployment.minAvailable */}} minAvailable: {{ $minAvailablePDB }} {{- else }} minAvailable: {{ $minAvailable | default "50%" }} diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index d269a42..86ac1f2 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -420,10 +420,9 @@ tests: asserts: - isNotEmpty: path: spec.template.spec.containers[0].livenessProbe.exec.command - - it: terminationGracePeriodSeconds use correct value + - it: terminationGracePeriodSeconds use correct value from container set: - container: - image: img + deployment: terminationGracePeriodSeconds: 60 asserts: - equal: @@ -443,9 +442,10 @@ tests: - it: must use 3 replicas if replicas is 3 set: env: dev + deployment: + replicas: 3 container: image: testimg - replicas: 3 asserts: - equal: path: spec.replicas @@ -456,9 +456,10 @@ tests: - it: must use 1 replica if forceReplicas is 1 and recreate set: env: prd + deployment: + forceReplicas: 1 container: image: some - forceReplicas: 1 asserts: - equal: path: spec.replicas @@ -469,9 +470,10 @@ tests: - it: must use 3 replica if forceReplicas is 3 set: env: prd + deployment: + forceReplicas: 3 container: image: some - forceReplicas: 3 asserts: - equal: path: spec.replicas diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml index cb80503..f9128e8 100644 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ b/charts/common/tests/deployment_v1_compatibility_test.yaml @@ -165,9 +165,10 @@ tests: - it: must use 3 replicas if replicas is 3 set: env: dev + deployment: + replicas: 3 container: image: testimg - replicas: 3 asserts: - equal: path: spec.replicas @@ -178,9 +179,10 @@ tests: - it: must use 1 replica if forceReplicas is 1 and recreate set: env: prd + deployment: + forceReplicas: 1 container: image: some - forceReplicas: 1 asserts: - equal: path: spec.replicas @@ -191,9 +193,10 @@ tests: - it: must use 3 replica if forceReplicas is 3 set: env: prd + deployment: + forceReplicas: 3 container: image: some - forceReplicas: 3 asserts: - equal: path: spec.replicas @@ -211,8 +214,7 @@ tests: path: spec.template.spec.containers[0].livenessProbe.exec.command - it: terminationGracePeriodSeconds use correct value set: - container: - image: img + deployment: terminationGracePeriodSeconds: 60 asserts: - equal: diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index effe3be..7cf9c97 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -40,7 +40,7 @@ tests: count: 0 - it: hpa must be generated if maxReplicas > 1 and must have labels set: - container: + deployment: maxReplicas: 2 asserts: - isNotEmpty: @@ -48,9 +48,10 @@ tests: - it: must have minReplicas 2 if not set set: env: prd + deployment: + maxReplicas: 5 container: image: img - maxReplicas: 5 asserts: - equal: path: spec.minReplicas @@ -69,7 +70,7 @@ tests: # #TODO 100% cpu target is set as default? - it: uses 70 % as default target cpu set: - container: + deployment: maxReplicas: 5 asserts: - equal: @@ -77,9 +78,8 @@ tests: value: 70 - it: deployment.cpuUtilization overrides default set: - container: - maxReplicas: 5 deployment: + maxReplicas: 5 cpuUtilization: 60 asserts: - equal: @@ -113,8 +113,9 @@ tests: - it: Must use minimum two replicas in prod, max 10 set: env: prd - container: + deployment: replicas: 1 # even if 1, this must be 2 in prod + container: image: img asserts: - equal: @@ -126,10 +127,11 @@ tests: - it: Must not use hpa if forceReplicas is set set: env: prd - container: - image: some + deployment: maxReplicas: 5 # should not matter forceReplicas: 1 + container: + image: some hpa: spec: maxReplicas: 199 # should not matter diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index 2b56a0b..66e6dfe 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -51,7 +51,7 @@ tests: - it: must use default with 2 replicas or more set: env: prd - container: + deployment: replicas: 2 asserts: - equal: @@ -68,7 +68,7 @@ tests: - it: use minAvailable from container if not set on pdb set: env: prd - container: + deployment: replicas: 2 minAvailable: 27% asserts: @@ -93,9 +93,6 @@ tests: deployment: replicas: 2 minAvailable: 30% - container: - replicas: 2 - minAvailable: 50% containers: - image: app asserts: @@ -107,7 +104,7 @@ tests: env: prd pdb: minAvailable: 25% - container: + deployment: replicas: 2 containers: - image: app @@ -119,9 +116,10 @@ tests: - it: if container replicas is 1 in prd, HPA is active so minAvailable must be 50% set: env: prd + deployment: + replicas: 1 container: image: some - replicas: 1 asserts: - equal: path: spec.minAvailable @@ -141,9 +139,10 @@ tests: - it: if container replicas is 1 in dev without HPA, minAvailable must be 0% set: env: dev + deployment: + replicas: 1 container: image: some - replicas: 1 asserts: - equal: path: spec.minAvailable @@ -163,9 +162,10 @@ tests: - it: if container forceReplicas is set to 1, minAvailable must be 0% set: env: prd + deployment: + forceReplicas: 1 container: image: some - forceReplicas: 1 asserts: - equal: path: spec.minAvailable @@ -185,9 +185,10 @@ tests: - it: if container forceReplicas is 2, minAvailable must be 50% set: env: prd + deployment: + forceReplicas: 2 container: image: some - forceReplicas: 2 asserts: - equal: path: spec.minAvailable @@ -206,9 +207,10 @@ tests: - it: if container forceReplicas is 3, minAvailable must be 50% set: env: prd + deployment: + forceReplicas: 3 container: image: some - forceReplicas: 3 asserts: - equal: path: spec.minAvailable @@ -242,8 +244,6 @@ tests: env: prd deployment: minAvailable: 4 - container: - image: some asserts: - hasDocuments: count: 1 @@ -262,7 +262,7 @@ tests: - it: must use default with 2 maxReplicas or more set: env: tst - container: + deployment: maxReplicas: 5 asserts: - equal: @@ -271,7 +271,7 @@ tests: - it: must use default with 2 maxreplicas or more in tst set: env: tst - container: + deployment: maxReplicas: 3 asserts: - equal: @@ -280,7 +280,7 @@ tests: - it: use minAvailable from container if not set on pdb in tst set: env: tst - container: + deployment: maxReplicas: 2 minAvailable: 27% asserts: @@ -290,7 +290,7 @@ tests: - it: minAvailable equals zero if no HPA and no replicas set in tst set: env: tst - container: + deployment: replicas: null asserts: - equal: @@ -299,7 +299,7 @@ tests: - it: minAvailable equals zero if no HPA and no replicas set on deployment in tst set: env: tst - container: + deployment: replicas: null asserts: - equal: @@ -308,7 +308,7 @@ tests: - it: minAvailable equals zero if no HPA and no replicas set in dev set: env: dev - container: + deployment: replicas: null asserts: - equal: @@ -317,7 +317,7 @@ tests: - it: minAvailable from container is used when 2 replicas in dev without HPA set: env: dev - container: + deployment: replicas: 2 minAvailable: 24% asserts: @@ -327,7 +327,7 @@ tests: - it: use minAvailable from container if not set on pdb in dev set: env: dev - container: + deployment: maxReplicas: 2 minAvailable: 29% asserts: @@ -338,10 +338,11 @@ tests: - it: if replicas is 1 but maxReplicas set in dev, HPA is active so minAvailable must be 50% set: env: dev - container: - image: some + deployment: replicas: 1 maxReplicas: 4 + container: + image: some asserts: - equal: path: spec.minAvailable @@ -350,9 +351,10 @@ tests: - it: if replicas is 2 without HPA in dev, minAvailable must be 50% set: env: dev + deployment: + replicas: 2 container: image: some - replicas: 2 asserts: - equal: path: spec.minAvailable diff --git a/charts/common/tests/values/common-test-values.yaml b/charts/common/tests/values/common-test-values.yaml index 945008d..c9373f0 100644 --- a/charts/common/tests/values/common-test-values.yaml +++ b/charts/common/tests/values/common-test-values.yaml @@ -9,11 +9,12 @@ ingress: service: externalPort: 8080 internalPort: 8080 +deployment: + replicas: 2 container: image: img memory: 768 cpu: 0.1 - replicas: 2 probes: liveness: path: /actuator/health diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 5b39b6b..9289fd0 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -206,15 +206,6 @@ container: # -- Set the uid that your user runs with # @default -- 1000 uid: 1000 - # -- (int) Set the target replica count, if equal to 1 the PDB minAvailable will be set to 100% - replicas: - # -- (int) Force replicas disables autoscaling and PDB, if set to 1 it will use Recreate strategy - forceReplicas: - # -- (string) Set the minimal available replicas, used by PDB - # @default -- 50% - minAvailable: - # -- (int) Set the maxReplicas for your HPA - maxReplicas: # -- Attach secrets and configmaps to your `env` envFrom: [] @@ -295,8 +286,6 @@ container: volumeMounts: [] # -- Configure volume, accepts kubernetes syntax volumes: [] - # -- (int) Override pod terminationGracePeriodSeconds (default 30s). - terminationGracePeriodSeconds: # -- Set pod lifecycle handlers lifecycle: {} From 57c0059abe693577a8b2a49e1a69f515e3894019 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 13:08:39 +0100 Subject: [PATCH 16/66] feat: add prometheus annotations for SQL proxy metrics scraping When postgres is enabled, adds prometheus.io/scrape-sql-proxy, prometheus.io/sql-proxy-port (9801), and prometheus.io/sql-proxy-path (/metrics) annotations to pods. Allows configuring Prometheus to scrape the SQL proxy sidecar alongside the main application. --- charts/common/templates/deployment.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index 770cc03..73adb78 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -65,6 +65,11 @@ spec: prometheus.io/path: "{{ $prometheusPath }}" prometheus.io/port: "{{ $prometheus.port | default .Values.service.internalPort | required "Must set deployment.prometheus.port" }}" {{- end }} + {{- if $postgres.enabled }} + prometheus.io/scrape-sql-proxy: "true" + prometheus.io/sql-proxy-port: "9801" + prometheus.io/sql-proxy-path: "/metrics" + {{- end }} labels: {{- include "labels" . |trim| nindent 8 }} {{- if $labels }} From b69e510fe4f194e02e21a5b785e4c12402c25774 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:29:13 +0100 Subject: [PATCH 17/66] fix: update gRPC tests to assert native probes instead of exec-based Tests were still asserting livenessProbe.exec.command from the removed grpcexecprobes helper. Updated to assert livenessProbe.grpc which is the native K8s gRPC probe now used by default. --- charts/common/tests/deployment_test.yaml | 8 ++++---- charts/common/tests/deployment_v1_compatibility_test.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index 86ac1f2..e448b0e 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -156,7 +156,7 @@ tests: - equal: path: spec.strategy.type value: RollingUpdate - - it: must adopt for gRPC + - it: must adopt for gRPC with native probes set: containers: - name: test @@ -172,7 +172,7 @@ tests: periodSeconds: 1 asserts: - isNotEmpty: - path: spec.template.spec.containers[0].livenessProbe.exec.command + path: spec.template.spec.containers[0].livenessProbe.grpc - it: must adopt for new gRPC probes 1.24 set: containers: @@ -412,14 +412,14 @@ tests: asserts: - notExists: path: spec.template.spec.terminationGracePeriodSeconds - - it: must adopt for gRPC + - it: must adopt for gRPC with native probes using internalPort set: grpc: true container: image: img asserts: - isNotEmpty: - path: spec.template.spec.containers[0].livenessProbe.exec.command + path: spec.template.spec.containers[0].livenessProbe.grpc - it: terminationGracePeriodSeconds use correct value from container set: deployment: diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml index f9128e8..b946401 100644 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ b/charts/common/tests/deployment_v1_compatibility_test.yaml @@ -204,14 +204,14 @@ tests: - equal: path: spec.strategy.type value: RollingUpdate - - it: must adopt for gRPC + - it: must adopt for gRPC with native probes set: grpc: true container: image: img asserts: - isNotEmpty: - path: spec.template.spec.containers[0].livenessProbe.exec.command + path: spec.template.spec.containers[0].livenessProbe.grpc - it: terminationGracePeriodSeconds use correct value set: deployment: From f16fb63bed2bce93aa39bfe16797316c325e7545 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:33:16 +0100 Subject: [PATCH 18/66] ci: install kube-startup-cpu-boost operator in kind cluster Add the StartupCPUBoost Helm chart to the CI kind cluster setup so the StartupCPUBoost CRD is available during helm install tests. --- .github/workflows/pull-request.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e8ce590..7cea692 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -32,13 +32,15 @@ jobs: with: node_image: kindest/node:v1.32.3 - - name: Configure metrics and VPA + - name: Configure metrics, VPA and StartupCPUBoost run: | helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/ + helm repo add cowboysysop https://cowboysysop.github.io/charts/ + helm repo add kube-startup-cpu-boost https://google.github.io/kube-startup-cpu-boost helm repo update helm install --set args={--kubelet-insecure-tls} metrics-server metrics-server/metrics-server --namespace kube-system - helm repo add cowboysysop https://cowboysysop.github.io/charts/ helm -n kube-system install vertical-pod-autoscaler cowboysysop/vertical-pod-autoscaler + helm install kube-startup-cpu-boost kube-startup-cpu-boost/kube-startup-cpu-boost --namespace kube-startup-cpu-boost-system --create-namespace - name: Install helm chart run: | From 8234390f82306a0a797e587be08d2600227f4046 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:35:47 +0100 Subject: [PATCH 19/66] ci: wait for startup-cpu-boost webhook to be ready before install tests --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7cea692..4410017 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -40,7 +40,7 @@ jobs: helm repo update helm install --set args={--kubelet-insecure-tls} metrics-server metrics-server/metrics-server --namespace kube-system helm -n kube-system install vertical-pod-autoscaler cowboysysop/vertical-pod-autoscaler - helm install kube-startup-cpu-boost kube-startup-cpu-boost/kube-startup-cpu-boost --namespace kube-startup-cpu-boost-system --create-namespace + helm install kube-startup-cpu-boost kube-startup-cpu-boost/kube-startup-cpu-boost --namespace kube-startup-cpu-boost-system --create-namespace --wait --timeout 2m0s - name: Install helm chart run: | From dd4d9853054bd3a1f9a6988ee695b5081c3ac77a Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:42:12 +0100 Subject: [PATCH 20/66] fix: increase startup-cpu-boost timeout to 5m and update examples to use appId --- .github/workflows/pull-request.yml | 8 ++++---- examples/common/cronjob/values.yaml | 2 +- examples/common/grpc-app/values.yaml | 2 +- examples/common/multi-container/values.yaml | 2 +- examples/common/multi-deploy/values.yaml | 4 ++-- examples/common/simple-app/values.yaml | 2 +- examples/common/typical-backend/values.yaml | 2 +- examples/common/typical-frontend/values.yaml | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4410017..7075389 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -25,12 +25,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Helm - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 - name: Create kind cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: - node_image: kindest/node:v1.32.3 + node_image: kindest/node:v1.35.1 - name: Configure metrics, VPA and StartupCPUBoost run: | @@ -40,7 +40,7 @@ jobs: helm repo update helm install --set args={--kubelet-insecure-tls} metrics-server metrics-server/metrics-server --namespace kube-system helm -n kube-system install vertical-pod-autoscaler cowboysysop/vertical-pod-autoscaler - helm install kube-startup-cpu-boost kube-startup-cpu-boost/kube-startup-cpu-boost --namespace kube-startup-cpu-boost-system --create-namespace --wait --timeout 2m0s + helm install kube-startup-cpu-boost kube-startup-cpu-boost/kube-startup-cpu-boost --namespace kube-startup-cpu-boost-system --create-namespace --wait --timeout 5m0s - name: Install helm chart run: | @@ -68,7 +68,7 @@ jobs: fetch-depth: 0 - name: Set up Helm - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 - name: Helm verify examples working-directory: examples/common/${{ matrix.example }} diff --git a/examples/common/cronjob/values.yaml b/examples/common/cronjob/values.yaml index d62ac6d..42f8c14 100644 --- a/examples/common/cronjob/values.yaml +++ b/examples/common/cronjob/values.yaml @@ -1,6 +1,6 @@ common: app: my-cronjob - shortname: mycron + appId: mycron team: example deployment: enabled: false diff --git a/examples/common/grpc-app/values.yaml b/examples/common/grpc-app/values.yaml index 63499bd..c632582 100644 --- a/examples/common/grpc-app/values.yaml +++ b/examples/common/grpc-app/values.yaml @@ -1,6 +1,6 @@ common: app: grpc-app - shortname: grpcapp + appId: grpcapp team: example grpc: true # -- Enable gRPC which will add an annotation and use grpc probes ingress: diff --git a/examples/common/multi-container/values.yaml b/examples/common/multi-container/values.yaml index cc04395..cd5c8d7 100644 --- a/examples/common/multi-container/values.yaml +++ b/examples/common/multi-container/values.yaml @@ -1,6 +1,6 @@ common: app: multi-container - shortname: multcont + appId: multcont team: example env: dev diff --git a/examples/common/multi-deploy/values.yaml b/examples/common/multi-deploy/values.yaml index bde24ce..4bbdbbb 100644 --- a/examples/common/multi-deploy/values.yaml +++ b/examples/common/multi-deploy/values.yaml @@ -2,7 +2,7 @@ multi-1: # must match the multi-1 alias in Chart.yaml releaseName: my-rest-api # the secret sauce to divide the two apps into separate releases app: my-rest-api - shortname: myappid1 # appID, let this be the same for both multi-1 and multi-2. + appId: myappid1 # appID, let this be the same for both multi-1 and multi-2. team: team-excellence ingress: @@ -21,7 +21,7 @@ multi-2: # must match the multi-2 alias in Chart.yaml releaseName: my-kafka-consumer # the secret sauce to divide the two apps into separate releases app: my-kafka-consumer - shortname: myappid1 # appID, let this be the same for both multi-1 and multi-2. + appId: myappid1 # appID, let this be the same for both multi-1 and multi-2. team: team-excellence ingress: diff --git a/examples/common/simple-app/values.yaml b/examples/common/simple-app/values.yaml index 1ffa35a..b2e5fc9 100644 --- a/examples/common/simple-app/values.yaml +++ b/examples/common/simple-app/values.yaml @@ -1,6 +1,6 @@ common: app: simple-app - shortname: simapp + appId: simapp team: example ingress: trafficType: public diff --git a/examples/common/typical-backend/values.yaml b/examples/common/typical-backend/values.yaml index f761c5a..bf763e7 100644 --- a/examples/common/typical-backend/values.yaml +++ b/examples/common/typical-backend/values.yaml @@ -1,6 +1,6 @@ common: app: typical-backend - shortname: typbak + appId: typbak team: team-example ingress: enabled: true diff --git a/examples/common/typical-frontend/values.yaml b/examples/common/typical-frontend/values.yaml index fc3b7f6..12e0fe0 100644 --- a/examples/common/typical-frontend/values.yaml +++ b/examples/common/typical-frontend/values.yaml @@ -1,6 +1,6 @@ common: app: typical-frontend - shortname: typfro + appId: typfro team: example ingress: enabled: true From 847914fd034ba62f677fe75982215e631734bf5f Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:43:48 +0100 Subject: [PATCH 21/66] fix: update agents and ignore asdf --- .gitignore | 1 + AGENTS.md | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f494b1b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.tool-versions diff --git a/AGENTS.md b/AGENTS.md index 38f7d6a..8aa0962 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ This repository contains Entur's opinionated Helm charts for deploying applications to Kubernetes. The primary chart is `common` (in `charts/common/`), which provides a convention-over-configuration approach for Spring Boot apps and general workloads. Example charts in `examples/common/` demonstrate usage patterns. +Compatible with Helm 3 and Helm 4. + ## Repository Structure ``` @@ -44,7 +46,7 @@ helm template charts/common -f fixture/helm/values-postgres.yaml helm template test charts/common -f fixture/helm/values-minimal.yaml --show-only templates/pdb.yaml # Render with value overrides (useful for testing specific scenarios) -helm template test charts/common -f fixture/helm/values-minimal.yaml --set env=prd --set container.replicas=2 +helm template test charts/common -f fixture/helm/values-minimal.yaml --set env=prd --set deployment.replicas=2 # Regenerate chart documentation (README.md files) helm-docs @@ -64,7 +66,7 @@ gh issue view --repo entur/helm-charts --comments - **Shared test values**: `charts/common/tests/values/common-test-values.yaml` - **Snapshots**: `charts/common/tests/__snapshot__/` - **Always run `helm unittest ./charts/common` after modifying any template or values** -- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, startup-cpu-boost, sql-proxy, and v1 backward compatibility +- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, and v1 backward compatibility ## Key Conventions @@ -82,14 +84,18 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - Traffic types must be explicit: `api`, `public`, or `http2` - Template helpers are in `charts/common/templates/_helpers.tpl` - Deprecated values use `fail` to give clear migration messages +- Scaling fields (replicas, maxReplicas, forceReplicas, minAvailable) belong under `deployment.*` only — not `container.*` +- Container-specific fields (cpu, memory, image, probes, env, ports, lifecycle) belong under `container.*` ### Values Patterns -- Required fields: `app`, `appId` (or `shortname`), `team`, `env`, `container.image` +- Required fields: `app`, `appId`, `team`, `env`, `container.image` - Environment values: `dev`, `tst`, `prd` - Single container: use `container:` key - Multiple containers: use `containers:` list - Environment-specific overrides go in `env/values-kub-ent-{dev,tst,prd}.yaml` - Postgres/Cloud SQL: use `postgres.instances` with Secret Manager keys via External Secrets +- gRPC: set `grpc: true` — native K8s gRPC probes are used automatically with `service.internalPort` +- Custom HPA metrics: use `hpa.metrics` list to add Pods/External/Object metrics alongside default CPU ## CI/CD @@ -104,3 +110,5 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - Example charts pin their dependency on `common` — update both `Chart.yaml` version and run `helm dependency update` when changing - The chart supports both Deployment and CronJob workloads (mutually exclusive via `deployment.enabled` / `cron.enabled`) - Fixture values in `fixture/helm/` are used for CI template rendering validation +- `shortname` is removed — use `appId` (matches GoogleCloudApplication `metadata.id`) +- `postgres.connectionConfig` is removed — use `postgres.instances` with Secret Manager keys From a6ac7c48fe2fd8f3528fbc533a2b4ecd32497531 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:49:17 +0100 Subject: [PATCH 22/66] ci: use local charts/common for example validation instead of remote repo Copy the local common chart into each example's charts/ directory before running helm template. This tests examples against the current branch's chart instead of the published version. --- .github/workflows/pull-request.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7075389..90a79f2 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -58,6 +58,8 @@ jobs: typical-frontend, multi-container, multi-deploy, + cronjob, + grpc-app, ] env: [dev, tst, prd] steps: @@ -73,4 +75,6 @@ jobs: - name: Helm verify examples working-directory: examples/common/${{ matrix.example }} run: | - helm template --dependency-update . -f env/values-kub-ent-${{ matrix.env }}.yaml + mkdir -p charts + cp -r ../../../charts/common charts/common + helm template . -f env/values-kub-ent-${{ matrix.env }}.yaml From d38bf5cb36a9ae536014cafcf8a89bd20ec329ae Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 14:53:18 +0100 Subject: [PATCH 23/66] fix: add missing grpc-app env files and postgres.instances to typical-backend - Add values-kub-ent-tst.yaml and values-kub-ent-prd.yaml for grpc-app example so CI can validate all environments - Add postgres.instances to typical-backend example (required by v2 proxy) --- examples/common/grpc-app/env/values-kub-ent-prd.yaml | 4 ++++ examples/common/grpc-app/env/values-kub-ent-tst.yaml | 4 ++++ examples/common/typical-backend/values.yaml | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 examples/common/grpc-app/env/values-kub-ent-prd.yaml create mode 100644 examples/common/grpc-app/env/values-kub-ent-tst.yaml diff --git a/examples/common/grpc-app/env/values-kub-ent-prd.yaml b/examples/common/grpc-app/env/values-kub-ent-prd.yaml new file mode 100644 index 0000000..157fa4f --- /dev/null +++ b/examples/common/grpc-app/env/values-kub-ent-prd.yaml @@ -0,0 +1,4 @@ +common: + env: prd + ingress: + host: grpc-app.entur.io diff --git a/examples/common/grpc-app/env/values-kub-ent-tst.yaml b/examples/common/grpc-app/env/values-kub-ent-tst.yaml new file mode 100644 index 0000000..af00018 --- /dev/null +++ b/examples/common/grpc-app/env/values-kub-ent-tst.yaml @@ -0,0 +1,4 @@ +common: + env: tst + ingress: + host: grpc-app.tst.entur.io diff --git a/examples/common/typical-backend/values.yaml b/examples/common/typical-backend/values.yaml index bf763e7..bcbdcf9 100644 --- a/examples/common/typical-backend/values.yaml +++ b/examples/common/typical-backend/values.yaml @@ -16,5 +16,7 @@ common: memory: 512 # Adjust this to your application needs, a java application might need more memory to start like 1024 or 2048 or +++ postgres: # sets up a postgres proxy so you can connect to your postgresql instance via localhost on the pod enabled: true + instances: + - PGINSTANCES # Secret Manager key from entur/terraform-google-sql-db containing the instance connection name cpu: 0.1 # PostgreSQL Proxy setting, this usually is enough for most applications memory: 32 # PostgreSQL Proxy setting, this usually is enough for most applications From c6cd395d934dc8c5334a81c2bdb9a6c07c0e8e2a Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:00:06 +0100 Subject: [PATCH 24/66] docs: add UPGRADE.md with v1 to v2 migration guide --- UPGRADE.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 UPGRADE.md diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..ea772f3 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,137 @@ +# Upgrading to common chart v2 + +This guide covers all breaking changes and required migration steps when upgrading from v1 to v2 of the `common` Helm chart. + +## Prerequisites + +- Helm 3.x or Helm 4.x +- The [kube-startup-cpu-boost](https://github.com/google/kube-startup-cpu-boost) operator installed in your cluster (enabled by default in v2). If not available, set `deployment.startupCPUBoost.enabled: false`. +- External Secrets Operator installed (required if using `postgres` or `secrets`) + +## Breaking Changes + +### 1. `shortname` renamed to `appId` + +The `shortname` field is removed. Use `appId` instead. This aligns with the GoogleCloudApplication [`metadata.id`](https://github.com/entur/tf-gcp-apps/blob/main/docs/manifests/GoogleCloudApplication.md) field. + +```yaml +# v1 +common: + shortname: myapp + +# v2 +common: + appId: myapp +``` + +The Kubernetes label `shortname` is still emitted for backwards compatibility alongside the new `appId` label. + +### 2. Scaling fields moved from `container.*` to `deployment.*` + +The following fields are removed from `container` and must be set under `deployment`: + +| Removed (v1) | Replacement (v2) | +|---|---| +| `container.replicas` | `deployment.replicas` | +| `container.maxReplicas` | `deployment.maxReplicas` | +| `container.forceReplicas` | `deployment.forceReplicas` | +| `container.minAvailable` | `deployment.minAvailable` | +| `container.terminationGracePeriodSeconds` | `deployment.terminationGracePeriodSeconds` | + +```yaml +# v1 +common: + container: + replicas: 2 + maxReplicas: 5 + +# v2 +common: + deployment: + replicas: 2 + maxReplicas: 5 +``` + +### 3. Cloud SQL Proxy upgraded to v2 + +The Cloud SQL Auth Proxy has been upgraded from v1 (1.33.16) to v2 (2.21.2). This changes how database connections are configured. + +**`postgres.connectionConfig` is removed.** Use `postgres.instances` instead, which sources instance connection names from Google Secret Manager via External Secrets. + +```yaml +# v1 +common: + postgres: + enabled: true + connectionConfig: my-app-psql-connection # Kubernetes ConfigMap with INSTANCES env var + +# v2 +common: + postgres: + enabled: true + instances: + - PGINSTANCES # Secret Manager key from entur/terraform-google-sql-db +``` + +**Migration steps:** + +1. The `entur/terraform-google-sql-db` Terraform module already stores `PGINSTANCES` in Secret Manager. Verify it exists. +2. Replace `connectionConfig` with `instances: [PGINSTANCES]` in your values. +3. For multiple databases, list all instance keys: `instances: [PGINSTANCES, ANALYTICS_PGINSTANCES]`. +4. Remove any manual Kubernetes ConfigMaps that were providing the `INSTANCES` env var. +5. Optionally set `create_kubernetes_resources: false` in your Terraform module to stop creating the now-unused ConfigMap. + +**`postgres.memoryLimit` is removed.** Memory limit is now always equal to memory request. Use `postgres.memory` to set both. + +### 4. `ingress.class` annotation replaced with `spec.ingressClassName` + +The deprecated `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). + +If you need a different ingress class: + +```yaml +common: + ingress: + ingressClassName: nginx +``` + +## New Features (no action required) + +### PDB improvements +- `unhealthyPodEvictionPolicy: AlwaysAllow` prevents unhealthy pods from blocking cluster upgrades. +- PDB now correctly protects pods when `forceReplicas > 1` (was incorrectly set to 0%). + +### GKE Startup CPU Boost +- Enabled by default. Temporarily increases CPU by 50% during startup, reverts when pod becomes Ready. +- HPA default `cpuUtilization` lowered from 100% to 70% since startup spikes are handled by the boost. +- Disable with `deployment.startupCPUBoost.enabled: false` if the operator is not installed. + +### gRPC native probes +- Setting `grpc: true` now uses native Kubernetes gRPC probes by default, using `service.internalPort`. +- No longer requires the `/bin/grpc_health_probe` binary in your container image. +- No need to set `probes.liveness.grpc.port` etc. — ports default to `service.internalPort`. + +### Startup probe path +- `container.probes.startup.path` — when set, the startup probe switches from `tcpSocket` to `httpGet`. + +### Custom HPA metrics +- `hpa.metrics` — append custom metrics (Pods, External, Object) alongside default CPU scaling. +- `deployment.cpuUtilization` — set HPA CPU target (default 70%). + +### Per-ingress annotations and ingressClassName +- Each entry in `ingresses` list can now have its own `annotations` and `ingressClassName`. + +### Cloud SQL Proxy v2 features +- Prometheus metrics exposed on port 9801 (`/metrics`). +- Support for multiple databases via `postgres.instances` list. + +## Quick Migration Checklist + +- [ ] Rename `shortname` to `appId` in all values files +- [ ] Move `container.replicas` → `deployment.replicas` (and maxReplicas, forceReplicas, minAvailable, terminationGracePeriodSeconds) +- [ ] Replace `postgres.connectionConfig` with `postgres.instances: [PGINSTANCES]` (if using postgres) +- [ ] Remove `postgres.memoryLimit` (if set) — use `postgres.memory` instead +- [ ] Verify `kube-startup-cpu-boost` operator is installed, or set `deployment.startupCPUBoost.enabled: false` +- [ ] If using gRPC: remove explicit `probes.*.grpc.port` settings (now defaults to `service.internalPort`) +- [ ] Update `Chart.yaml` dependency version to v2 +- [ ] Run `helm dependency update` and `helm template` to verify From 293cb970a73d28e3d51bb84e44eff9e726a5ec30 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:05:55 +0100 Subject: [PATCH 25/66] feat: add JSON Schema for values validation (v2) Add values.schema.json matching v2 values structure. Validates values on helm install/upgrade/template/lint to catch typos and unknown properties early. Updated from PR #222 for v2 changes: appId instead of shortname, scaling fields under deployment only, postgres.instances, hpa.metrics, startupCPUBoost, ingressClassName, startup probe path. --- charts/common/tests/hpa_test.yaml | 4 +- charts/common/tests/ingress_test.yaml | 2 +- charts/common/tests/pdb_test.yaml | 3 +- .../tests/values/common-test-values.yaml | 1 - charts/common/values.schema.json | 508 ++++++++++++++++++ 5 files changed, 512 insertions(+), 6 deletions(-) create mode 100644 charts/common/values.schema.json diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index 7cf9c97..ee7bed0 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -139,12 +139,10 @@ tests: asserts: - hasDocuments: count: 0 - - it: Uses maxReplicas from deployment before container settings + - it: Uses maxReplicas from deployment set: deployment: maxReplicas: 99 - containers: - - maxReplicas: 2 asserts: - equal: path: spec.maxReplicas diff --git a/charts/common/tests/ingress_test.yaml b/charts/common/tests/ingress_test.yaml index 77d6eb9..9159719 100644 --- a/charts/common/tests/ingress_test.yaml +++ b/charts/common/tests/ingress_test.yaml @@ -98,7 +98,7 @@ tests: set: env: dev app: testsuite - shortname: tstsut + appId: tstsut team: common service: externalPort: 8080 diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index 66e6dfe..f7c202d 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -60,7 +60,8 @@ tests: - it: must use default with 2 maxreplicas or more in prod set: env: prd - maxReplicas: 3 + deployment: + maxReplicas: 3 asserts: - equal: path: spec.minAvailable diff --git a/charts/common/tests/values/common-test-values.yaml b/charts/common/tests/values/common-test-values.yaml index c9373f0..58502de 100644 --- a/charts/common/tests/values/common-test-values.yaml +++ b/charts/common/tests/values/common-test-values.yaml @@ -1,7 +1,6 @@ app: rudder-test releaseName: rudder-test env: dev -appname: rudder-test team: platform ingress: host: test.dev.entur.io diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json new file mode 100644 index 0000000..d0dceeb --- /dev/null +++ b/charts/common/values.schema.json @@ -0,0 +1,508 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "global": { + "description": "Global values accessible from any chart or subchart.", + "type": "object" + }, + "app": { + "description": "Application name, typically on the form `the-application`", + "type": ["string", "null"] + }, + "appId": { + "description": "App ID from GoogleCloudApplication metadata.id. Max 10 alphanumeric characters.", + "type": ["string", "null"], + "maxLength": 10 + }, + "team": { + "description": "Your team name, without a `team-` prefix", + "type": ["string", "null"] + }, + "env": { + "description": "The current env: `dev`, `tst` or `prd`", + "type": "string", + "enum": ["dev", "tst", "prd"] + }, + "releaseName": { + "description": "Override release name, useful for multiple deployments", + "type": ["string", "null"] + }, + "labels": { + "description": "Specify additional labels for every resource", + "type": "object" + }, + "ingress": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable the ingress", + "type": "boolean" + }, + "host": { + "description": "Set the host name", + "type": ["string", "null"] + }, + "trafficType": { + "description": "Set the traffic type (`api`, `public` or `http2` for gRPC)", + "type": ["string", "null"], + "enum": ["api", "public", "http2", null] + }, + "ingressClassName": { + "description": "Set the IngressClass name (default: traefik)", + "type": ["string", "null"] + }, + "annotations": { + "description": "Optionally set annotations for the ingress", + "type": "object" + }, + "rules": { + "description": "K8s spec for ingress rules", + "items": { "type": "object" }, + "type": "array" + } + }, + "type": "object" + }, + "ingresses": { + "description": "Specify a list of ingress specs", + "items": { "$ref": "#/properties/ingress" }, + "type": "array" + }, + "grpc": { + "description": "Enable gRPC which will use native K8s gRPC probes with service.internalPort", + "type": "boolean" + }, + "deployment": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable the deployment", + "type": "boolean" + }, + "labels": { + "description": "Add labels to your pods", + "type": "object" + }, + "volumes": { + "description": "Configure volume, accepts kubernetes syntax", + "items": { "type": "object" }, + "type": "array" + }, + "prometheus": { + "$ref": "#/properties/container/properties/prometheus" + }, + "replicas": { + "description": "Set the target replica count", + "type": ["integer", "null"] + }, + "maxReplicas": { + "description": "Set the max replica count for HPA", + "type": ["integer", "null"] + }, + "forceReplicas": { + "description": "Force replicas disables autoscaling and PDB, if set to 1 it will use Recreate strategy", + "type": ["integer", "null"] + }, + "terminationGracePeriodSeconds": { + "description": "Override pod terminationGracePeriodSeconds (default 30s)", + "type": ["integer", "null"] + }, + "minAvailable": { + "description": "Set minimum available % for PDB", + "type": ["string", "integer", "null"] + }, + "maxSurge": { + "description": "Limit max surge for rolling updates (default 25%)", + "type": ["integer", "string", "null"] + }, + "maxUnavailable": { + "description": "Limit max unavailable for rolling updates (default 25%)", + "type": ["string", "null"] + }, + "serviceAccountName": { + "description": "Override pod serviceAccountName (default application)", + "type": ["string", "null"] + }, + "cpuUtilization": { + "description": "Set the target CPU average utilization (%) for HPA scaling (default 70)", + "type": ["integer", "null"] + }, + "startupCPUBoost": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable GKE Startup CPU Boost. Requires the kube-startup-cpu-boost operator.", + "type": "boolean" + }, + "percentageIncrease": { + "description": "Percentage to increase CPU requests during startup", + "type": "integer" + } + }, + "type": "object" + }, + "minReadySeconds": { + "description": "Minimum number of seconds a pod should be ready before considered available", + "type": "integer" + } + }, + "type": "object" + }, + "cron": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable the cron job", + "type": "boolean" + }, + "concurrencyPolicy": { + "description": "Concurrency policy", + "type": ["string", "null"] + }, + "failedJobsHistoryLimit": { + "description": "Failed jobs history limit", + "type": ["integer", "null"] + }, + "schedule": { + "description": "Required crontab schedule", + "type": ["string", "null"] + }, + "successfulJobsHistoryLimit": { + "description": "Successful jobs history limit", + "type": ["integer", "null"] + }, + "suspend": { + "description": "Suspend flag", + "type": ["boolean", "null"] + }, + "labels": { + "description": "Add labels to your pods", + "type": "object" + }, + "volumes": { + "description": "Configure volume, accepts kubernetes syntax", + "items": { "type": "object" }, + "type": "array" + }, + "terminationGracePeriodSeconds": { + "description": "Override pod terminationGracePeriodSeconds (default 30s)", + "type": ["integer", "null"] + }, + "restartPolicy": { + "description": "Override pod restartPolicy (default OnFailure)", + "type": ["string", "null"] + }, + "serviceAccountName": { + "description": "Override pod serviceAccountName (default application)", + "type": ["string", "null"] + }, + "activeDeadlineSeconds": { + "description": "Active deadline seconds for the job, default 24 hours (86300s)", + "type": ["integer", "null"] + } + }, + "type": "object" + }, + "hpa": { + "additionalProperties": false, + "properties": { + "metrics": { + "description": "Additional HPA metrics appended alongside the default CPU metric (Pods, Object, External)", + "items": { "type": "object" }, + "type": "array" + }, + "spec": { + "description": "Full custom spec for HPA, replaces default metrics and min/max replicas. Inherits scaleTargetRef.", + "type": "object" + } + }, + "type": "object" + }, + "pdb": { + "additionalProperties": false, + "properties": { + "minAvailable": { + "description": "Set minimum available %, overrides deployment.minAvailable", + "type": ["string", "integer", "null"] + } + }, + "type": "object" + }, + "service": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable the service", + "type": "boolean" + }, + "externalPort": { + "description": "Set the external port for your service", + "type": "integer" + }, + "internalPort": { + "description": "Set the internal port for your service", + "type": "integer" + }, + "annotations": { + "description": "Optionally set annotations for the service", + "type": "object" + }, + "ports": { + "description": "Custom ports spec, overrides default port config", + "items": { "type": "object" }, + "type": "array" + } + }, + "type": "object" + }, + "container": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of container", + "type": ["string", "null"] + }, + "labels": { + "description": "Add labels to your pods", + "type": "object" + }, + "command": { + "description": "Optionally set the command that will run in the pod", + "type": ["array", "null"], + "items": { "type": "string" } + }, + "args": { + "description": "Optionally set the arguments that will be passed to the command", + "type": ["array", "null"], + "items": { "type": "string" } + }, + "cpu": { + "description": "Set CPU without any unit. 100m is 0.1", + "type": ["string", "number"] + }, + "cpuLimit": { + "description": "Set CPU limit without any unit. 100m is 0.1", + "type": ["string", "number", "null"] + }, + "memory": { + "description": "Set memory without any unit, Mi is inferred", + "type": "integer" + }, + "memoryLimit": { + "description": "Set memory limit without any unit, Mi is inferred", + "type": ["integer", "null"] + }, + "uid": { + "description": "Set the uid that your user runs with", + "type": "integer" + }, + "image": { + "description": "Set the image for your container", + "type": ["string", "null"] + }, + "envFrom": { + "description": "Attach secrets and configmaps to your env", + "items": { "type": "object" }, + "type": "array" + }, + "env": { + "description": "Specify env entries for your container", + "items": { "type": "object" }, + "type": "array" + }, + "ephemeralStorage": { + "description": "Set ephemeral storage request", + "type": ["string", "null"] + }, + "ephemeralStorageLimit": { + "description": "Set ephemeral storage limit", + "type": ["string", "null"] + }, + "grpc": { + "description": "Enable gRPC for this specific container", + "type": "boolean" + }, + "ports": { + "description": "Custom container ports, overrides default port config", + "items": { "type": "object" }, + "type": "array" + }, + "prometheus": { + "additionalProperties": false, + "description": "Prometheus scraping configuration", + "properties": { + "enabled": { + "description": "Enable or disable Prometheus", + "type": "boolean" + }, + "path": { + "description": "Set the path for scraping metrics", + "type": "string" + }, + "port": { + "description": "Set the port for prometheus scraping", + "type": ["integer", "null"] + } + }, + "type": "object" + }, + "probes": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable probes", + "type": "boolean" + }, + "spec": { + "description": "Override with k8s spec for custom probes", + "type": ["object", "null"] + }, + "liveness": { + "additionalProperties": false, + "properties": { + "path": { "description": "Set the path for liveness probe", "type": "string" }, + "initialDelaySeconds": { "type": "integer" }, + "successThreshold": { "type": "integer" }, + "failureThreshold": { "type": "integer" }, + "periodSeconds": { "type": "integer" }, + "port": { "type": ["integer", "null"] }, + "grpc": { "type": ["object", "null"] } + }, + "type": "object" + }, + "readiness": { + "additionalProperties": false, + "properties": { + "path": { "description": "Set the path for readiness probe", "type": "string" }, + "initialDelaySeconds": { "type": "integer" }, + "successThreshold": { "type": "integer" }, + "failureThreshold": { "type": "integer" }, + "periodSeconds": { "type": "integer" }, + "port": { "type": ["integer", "null"] }, + "grpc": { "type": ["object", "null"] } + }, + "type": "object" + }, + "startup": { + "additionalProperties": false, + "properties": { + "path": { "description": "Set the path for startup probe. If set, uses httpGet instead of tcpSocket.", "type": ["string", "null"] }, + "failureThreshold": { "type": "integer" }, + "periodSeconds": { "type": "integer" }, + "port": { "type": ["integer", "null"] }, + "grpc": { "type": ["object", "null"] } + }, + "type": "object" + } + }, + "type": "object" + }, + "volumeMounts": { + "description": "Configure volume mounts, accepts kubernetes syntax", + "items": { "type": "object" }, + "type": "array" + }, + "volumes": { + "description": "Configure volume, accepts kubernetes syntax", + "items": { "type": "object" }, + "type": "array" + }, + "lifecycle": { + "description": "Set pod lifecycle handlers", + "type": "object" + } + }, + "type": "object" + }, + "containers": { + "description": "Takes a list of container entries, you must add a `name` field for each entry", + "items": { "$ref": "#/properties/container" }, + "type": "array" + }, + "initContainers": { + "description": "Takes a list of initContainers", + "items": { "type": "object" }, + "type": "array" + }, + "postgres": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable the Cloud SQL proxy v2 sidecar", + "type": "boolean" + }, + "cpu": { + "description": "Configure cpu request for proxy", + "type": ["string", "number"] + }, + "cpuLimit": { + "description": "Configure optional cpu limit for proxy", + "type": ["string", "number", "null"] + }, + "memory": { + "description": "Configure memory request for proxy without units, Mi inferred", + "type": "integer" + }, + "memoryLimit": { + "description": "Deprecated. Will cause a deploy failure if set.", + "type": ["integer", "null"] + }, + "instances": { + "description": "List of Secret Manager keys containing Cloud SQL instance connection names", + "items": { "type": "string" }, + "type": "array" + }, + "connectionConfig": { + "description": "Deprecated. Use postgres.instances instead.", + "type": ["string", "null"] + }, + "credentialsSecret": { + "description": "Override name for credentials secret. Must contain PGUSER and PGPASSWORD.", + "type": ["string", "null"] + } + }, + "type": "object" + }, + "configmap": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable or disable the configmap", + "type": "boolean" + }, + "data": { + "description": "Set data for configmap", + "type": "object" + } + }, + "type": "object" + }, + "secrets": { + "description": "Add externalSecret to sync secrets from secret manager", + "type": "object" + }, + "serviceAccount": { + "additionalProperties": false, + "properties": { + "create": { + "description": "Create a service account for this application", + "type": "boolean" + } + }, + "type": "object" + }, + "vpa": { + "additionalProperties": false, + "properties": { + "enabled": { + "description": "Enable Vertical Pod Autoscaler", + "type": "boolean" + } + }, + "type": "object" + } + }, + "required": ["app", "appId", "env", "team"], + "type": "object" +} From 181c982e6359464fdc28e273a9153e8caf530e40 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:07:52 +0100 Subject: [PATCH 26/66] ci: add helm lint to example validation --- .github/workflows/pull-request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 90a79f2..42ac8b5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -77,4 +77,5 @@ jobs: run: | mkdir -p charts cp -r ../../../charts/common charts/common + helm lint . -f env/values-kub-ent-${{ matrix.env }}.yaml helm template . -f env/values-kub-ent-${{ matrix.env }}.yaml From 3c93d958ffc344108938dd78fabcf9cb35ff3a1c Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:09:35 +0100 Subject: [PATCH 27/66] docs: add IDE setup instructions for values schema and link to UPGRADE.md --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index 1e410ce..09fb574 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,43 @@ Browse our [examples/common](./examples/common) folder for quick examples to get Full documentation on each chart you can find in the [charts/](./charts/) folders. +## IDE Support + +The chart includes a [JSON Schema](./charts/common/values.schema.json) for `values.yaml` validation. This gives you autocompletion, inline documentation, and error highlighting in your IDE. + +### VS Code + +Install the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) and add this to your `values.yaml`: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/entur/helm-charts/main/charts/common/values.schema.json +common: + app: my-app + ... +``` + +Or configure it in `.vscode/settings.json`: + +```json +{ + "yaml.schemas": { + "https://raw.githubusercontent.com/entur/helm-charts/main/charts/common/values.schema.json": ["values*.yaml", "env/values*.yaml"] + } +} +``` + +### JetBrains (IntelliJ / GoLand) + +Go to **Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings**, add a new mapping: +- **Schema URL**: `https://raw.githubusercontent.com/entur/helm-charts/main/charts/common/values.schema.json` +- **File path pattern**: `values*.yaml` + +Note: Since the chart is used as a dependency, your values are nested under `common:`. The schema validates the properties under that key. + +## Upgrading + +See [UPGRADE.md](UPGRADE.md) for migration guides between major versions. + ## Contributing For guidance on how to contribute, see our [contribution documentation](CONTRIBUTING.md). From f961f36af8a0b273c3aa443779113cd55dfab36d Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:11:40 +0100 Subject: [PATCH 28/66] docs: add helm lint step to UPGRADE.md migration checklist --- UPGRADE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/UPGRADE.md b/UPGRADE.md index ea772f3..1a5d17b 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -134,4 +134,6 @@ common: - [ ] Verify `kube-startup-cpu-boost` operator is installed, or set `deployment.startupCPUBoost.enabled: false` - [ ] If using gRPC: remove explicit `probes.*.grpc.port` settings (now defaults to `service.internalPort`) - [ ] Update `Chart.yaml` dependency version to v2 -- [ ] Run `helm dependency update` and `helm template` to verify +- [ ] Run `helm dependency update` +- [ ] Run `helm lint . -f env/values-kub-ent-dev.yaml` to catch unknown properties and schema errors +- [ ] Run `helm template . -f env/values-kub-ent-dev.yaml` to verify rendered output From 83a3995f04fa9f7255f01c0387e7f5b0cee7eaf4 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:17:45 +0100 Subject: [PATCH 29/66] ci: test with both Helm v3.20.0 and v4.1.3 Run unit tests, kind cluster install tests, and example validation against both Helm 3 and Helm 4 using matrix strategy. --- .github/workflows/pull-request.yml | 32 ++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 42ac8b5..a38aaa8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -13,19 +13,39 @@ jobs: uses: entur/gha-meta/.github/workflows/verify-pr.yml@v1 unittest-common-chart: - uses: entur/gha-helm/.github/workflows/unittest.yml@v1 - with: - chart: charts/common + name: unittest (helm ${{ matrix.helm-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + helm-version: [v3.20.0, v4.1.3] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Helm + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + with: + version: ${{ matrix.helm-version }} + + - name: Install helm-unittest plugin + run: helm plugin install https://github.com/helm-unittest/helm-unittest.git + + - name: Run unit tests + run: helm unittest ./charts/common helm-install-test: - name: helm install + name: helm install (helm ${{ matrix.helm-version }}) runs-on: ubuntu-24.04 needs: unittest-common-chart + strategy: + matrix: + helm-version: [v3.20.0, v4.1.3] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Helm uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + with: + version: ${{ matrix.helm-version }} - name: Create kind cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 @@ -48,6 +68,7 @@ jobs: helm install --generate-name --dependency-update --wait --timeout 5m0s charts/common --values fixture/helm/ci/values-ci-cronjob-tests.yaml validate-examples: + name: examples (${{ matrix.example }}/${{ matrix.env }}/helm ${{ matrix.helm-version }}) runs-on: ubuntu-24.04 strategy: matrix: @@ -62,6 +83,7 @@ jobs: grpc-app, ] env: [dev, tst, prd] + helm-version: [v3.20.0, v4.1.3] steps: - name: Check out the repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -71,6 +93,8 @@ jobs: - name: Set up Helm uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 + with: + version: ${{ matrix.helm-version }} - name: Helm verify examples working-directory: examples/common/${{ matrix.example }} From 32372a50d52621a236dfc9936af21fc9173f46dd Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:20:15 +0100 Subject: [PATCH 30/66] fix: helm-unittest --verify=false for Helm 4 and move container scaling fields in examples --- .github/workflows/pull-request.yml | 2 +- examples/common/simple-app/env/values-kub-ent-prd.yaml | 2 +- examples/common/typical-frontend/env/values-kub-ent-prd.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a38aaa8..24b0e06 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -27,7 +27,7 @@ jobs: version: ${{ matrix.helm-version }} - name: Install helm-unittest plugin - run: helm plugin install https://github.com/helm-unittest/helm-unittest.git + run: helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest.git - name: Run unit tests run: helm unittest ./charts/common diff --git a/examples/common/simple-app/env/values-kub-ent-prd.yaml b/examples/common/simple-app/env/values-kub-ent-prd.yaml index 8d25a63..c4201c2 100644 --- a/examples/common/simple-app/env/values-kub-ent-prd.yaml +++ b/examples/common/simple-app/env/values-kub-ent-prd.yaml @@ -2,7 +2,7 @@ common: env: prd ingress: host: simple-app.entur.io - container: + deployment: replicas: 2 maxReplicas: 10 configmap: diff --git a/examples/common/typical-frontend/env/values-kub-ent-prd.yaml b/examples/common/typical-frontend/env/values-kub-ent-prd.yaml index 3bb8ff6..7464fe4 100644 --- a/examples/common/typical-frontend/env/values-kub-ent-prd.yaml +++ b/examples/common/typical-frontend/env/values-kub-ent-prd.yaml @@ -3,7 +3,7 @@ common: ingress: host: typical-frontend.entur.no - container: + deployment: replicas: 2 maxReplicas: 10 configmap: From 3a7ddc3015e8176c2d15dca3d6101a445b29fa49 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:22:17 +0100 Subject: [PATCH 31/66] ci: fix helm-unittest plugin install for both Helm 3 and 4 --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 24b0e06..43948f5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -27,7 +27,7 @@ jobs: version: ${{ matrix.helm-version }} - name: Install helm-unittest plugin - run: helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest.git + run: helm plugin install https://github.com/helm-unittest/helm-unittest.git || helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest.git - name: Run unit tests run: helm unittest ./charts/common From 1d52741239a414506c3d024b96736d6aa8ac98c5 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:24:20 +0100 Subject: [PATCH 32/66] fix: fix command typo, move scaling fields in fixtures, fix cronjob command type --- examples/common/cronjob/values.yaml | 2 +- fixture/helm/ci/values-ci-cronjob-tests.yaml | 4 +--- fixture/helm/ci/values-ci-tests.yaml | 6 ++++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/common/cronjob/values.yaml b/examples/common/cronjob/values.yaml index 42f8c14..166ff75 100644 --- a/examples/common/cronjob/values.yaml +++ b/examples/common/cronjob/values.yaml @@ -13,7 +13,7 @@ common: schedule: "0 */6 * * *" container: image: <+artifacts.primary.image> - command: "['/bin/sh']" + command: ["/bin/sh"] args: - "-c" - "echo 'Hello from CronJob'" diff --git a/fixture/helm/ci/values-ci-cronjob-tests.yaml b/fixture/helm/ci/values-ci-cronjob-tests.yaml index 0291af2..c93d275 100644 --- a/fixture/helm/ci/values-ci-cronjob-tests.yaml +++ b/fixture/helm/ci/values-ci-cronjob-tests.yaml @@ -16,13 +16,11 @@ service: container: image: nginxinc/nginx-unprivileged:latest - commmand: ["/bin/sh", "-c", "echo hello world"] + command: ["/bin/sh", "-c", "echo hello world"] labels: version: v1.2.3 cpu: 0.2 memory: 64 - replicas: 1 - maxReplicas: 2 memoryLimit: 64 envFrom: [] probes: diff --git a/fixture/helm/ci/values-ci-tests.yaml b/fixture/helm/ci/values-ci-tests.yaml index b0063bf..ec7047b 100644 --- a/fixture/helm/ci/values-ci-tests.yaml +++ b/fixture/helm/ci/values-ci-tests.yaml @@ -17,14 +17,16 @@ service: externalPort: 8080 internalPort: 8080 +deployment: + replicas: 1 + maxReplicas: 2 + container: image: nginxinc/nginx-unprivileged:latest labels: version: v1.2.3 cpu: 0.2 memory: 64 - replicas: 1 - maxReplicas: 2 memoryLimit: 64 envFrom: [] probes: From bd38997b40956192d489277ffd3341710d0eec5b Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:33:01 +0100 Subject: [PATCH 33/66] chore: add Entur icon to Chart.yaml --- charts/common/Chart.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/common/Chart.yaml b/charts/common/Chart.yaml index 0141f1c..ffe4ac9 100644 --- a/charts/common/Chart.yaml +++ b/charts/common/Chart.yaml @@ -1,5 +1,6 @@ apiVersion: v2 name: common +icon: https://avatars.githubusercontent.com/u/23213604?s=96&v=4 description: > A Helm chart for Entur's Kubernetes workloads From 7ffb83a9c4f3cf0cf3b37c8fb117e15f7f76d517 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 15:43:03 +0100 Subject: [PATCH 34/66] chore: thanks @majori for the JSON Schema values validation idea (#222) From 08d0443c93c57c9f8f762b6885a623baf4e2da14 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 16:09:25 +0100 Subject: [PATCH 35/66] chore: bump chart version to 2.0.0 --- charts/common/Chart.yaml | 2 +- examples/common/cronjob/Chart.yaml | 2 +- examples/common/grpc-app/Chart.yaml | 2 +- examples/common/multi-container/Chart.yaml | 2 +- examples/common/multi-deploy/Chart.yaml | 4 ++-- examples/common/simple-app/Chart.yaml | 2 +- examples/common/typical-backend/Chart.yaml | 2 +- examples/common/typical-frontend/Chart.yaml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/charts/common/Chart.yaml b/charts/common/Chart.yaml index ffe4ac9..b39de0c 100644 --- a/charts/common/Chart.yaml +++ b/charts/common/Chart.yaml @@ -81,5 +81,5 @@ description: > type: application -version: 1.21.1 +version: 2.0.0 appVersion: 0.0.1 diff --git a/examples/common/cronjob/Chart.yaml b/examples/common/cronjob/Chart.yaml index 9845149..6d42631 100644 --- a/examples/common/cronjob/Chart.yaml +++ b/examples/common/cronjob/Chart.yaml @@ -5,5 +5,5 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" diff --git a/examples/common/grpc-app/Chart.yaml b/examples/common/grpc-app/Chart.yaml index 43ca974..1917bda 100644 --- a/examples/common/grpc-app/Chart.yaml +++ b/examples/common/grpc-app/Chart.yaml @@ -5,5 +5,5 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" diff --git a/examples/common/multi-container/Chart.yaml b/examples/common/multi-container/Chart.yaml index 5c71e01..0554af7 100644 --- a/examples/common/multi-container/Chart.yaml +++ b/examples/common/multi-container/Chart.yaml @@ -5,5 +5,5 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" diff --git a/examples/common/multi-deploy/Chart.yaml b/examples/common/multi-deploy/Chart.yaml index 120a1d9..f20e3db 100644 --- a/examples/common/multi-deploy/Chart.yaml +++ b/examples/common/multi-deploy/Chart.yaml @@ -5,10 +5,10 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" alias: multi-1 - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" alias: multi-2 diff --git a/examples/common/simple-app/Chart.yaml b/examples/common/simple-app/Chart.yaml index 2bd0575..f740e9f 100644 --- a/examples/common/simple-app/Chart.yaml +++ b/examples/common/simple-app/Chart.yaml @@ -5,5 +5,5 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" diff --git a/examples/common/typical-backend/Chart.yaml b/examples/common/typical-backend/Chart.yaml index 9c7d5be..5ae6b45 100644 --- a/examples/common/typical-backend/Chart.yaml +++ b/examples/common/typical-backend/Chart.yaml @@ -6,5 +6,5 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" diff --git a/examples/common/typical-frontend/Chart.yaml b/examples/common/typical-frontend/Chart.yaml index b2cb2da..bda5589 100644 --- a/examples/common/typical-frontend/Chart.yaml +++ b/examples/common/typical-frontend/Chart.yaml @@ -6,5 +6,5 @@ version: 0.0.3 appVersion: "0.0.1" dependencies: - name: common - version: 1.21.1 + version: 2.0.0 repository: "https://entur.github.io/helm-charts" From 997cc446aa0cfcf6a5faae91d16ea28b1ed05087 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 20:40:24 +0100 Subject: [PATCH 36/66] docs: add configmap.toEnv migration note to UPGRADE.md --- UPGRADE.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 1a5d17b..133437d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -95,6 +95,33 @@ common: ingressClassName: nginx ``` +### 5. `configmap.toEnv` is removed + +If you get a schema error like `configmap.toEnv is no longer valid in v2`, switch to `container.envFrom` to mount the configmap as environment variables: + +```yaml +# v1 +common: + configmap: + enabled: true + toEnv: true + data: + MY_CONFIG: "value" + +# v2 +common: + configmap: + enabled: true + data: + MY_CONFIG: "value" + container: + envFrom: + - configMapRef: + name: my-app # matches your release name +``` + +Note: The configmap is automatically mounted via `envFrom` when `configmap.enabled: true`. You only need explicit `envFrom` if you renamed the configmap or need additional control. + ## New Features (no action required) ### PDB improvements From 414111d5b345262b4f691a0f06f964380907fe7f Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 20:43:13 +0100 Subject: [PATCH 37/66] feat: add sbx as valid environment in schema --- charts/common/values.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json index d0dceeb..920c949 100644 --- a/charts/common/values.schema.json +++ b/charts/common/values.schema.json @@ -22,7 +22,7 @@ "env": { "description": "The current env: `dev`, `tst` or `prd`", "type": "string", - "enum": ["dev", "tst", "prd"] + "enum": ["sbx", "dev", "tst", "prd"] }, "releaseName": { "description": "Override release name, useful for multiple deployments", From 70f8684bf602e5b567c34ab02cba21c70eef2c9c Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 21:00:57 +0100 Subject: [PATCH 38/66] docs: add AI agent upgrade prompt to UPGRADE.md --- UPGRADE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 133437d..a9b7fdb 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -164,3 +164,17 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl - [ ] Run `helm dependency update` - [ ] Run `helm lint . -f env/values-kub-ent-dev.yaml` to catch unknown properties and schema errors - [ ] Run `helm template . -f env/values-kub-ent-dev.yaml` to verify rendered output + +## Using an AI agent to upgrade + +If you use an AI coding agent (Claude Code, Copilot, Cursor, etc.), you can paste the following prompt to have it perform the migration for you: + +``` +Upgrade the Entur common Helm chart dependency from v1 to v2. + +First, read the migration guide: + https://raw.githubusercontent.com/entur/helm-charts/main/UPGRADE.md + +Then apply the "Quick Migration Checklist" to all values files in this repository. +Run `helm dependency update` and `helm lint` to verify. +``` From dff475b0c571d364a5d50c7edbe5948a2cddf45a Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 21:21:05 +0100 Subject: [PATCH 39/66] fix: default startupCPUBoost to disabled --- charts/common/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 9289fd0..c0d5fce 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -69,7 +69,7 @@ deployment: cpuUtilization: startupCPUBoost: # -- Enable GKE Startup CPU Boost to temporarily increase CPU during pod startup. Requires the kube-startup-cpu-boost operator installed in the cluster. Boost is reverted when the pod becomes Ready. - enabled: true + enabled: false # -- (int) Percentage to increase CPU requests during startup # @default -- 50 percentageIncrease: 50 From 6aae4744f0db6034b888721439c27c589103bd0c Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 21:24:40 +0100 Subject: [PATCH 40/66] feat: add 120s scaleUp stabilization window when startupCPUBoost is disabled When CPU boost is off, Java startup CPU spikes can trigger unnecessary HPA scale-ups. A 120s stabilization window gives pods time to finish startup before HPA acts on the elevated CPU. When CPU boost is enabled the window is not needed since startup spikes are handled by the boost. --- charts/common/templates/hpa.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index 72c865f..a66e8e2 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -37,6 +37,11 @@ spec: {{- range ((.Values.hpa).metrics) }} - {{ toYaml . | nindent 4 }} {{- end }} + {{- if not ((.Values.deployment).startupCPUBoost).enabled }} + behavior: + scaleUp: + stabilizationWindowSeconds: 120 + {{- end }} maxReplicas: {{ $maxReplicas | default 10 }} minReplicas: {{ max 2 $replicas }} {{- end }} From 33e786b1b0f1e0b803dfe1153e505f1b60cad73e Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Wed, 25 Mar 2026 21:29:30 +0100 Subject: [PATCH 41/66] feat: make stabilizationWindowSeconds configurable via hpa.stabilizationWindowSeconds Defaults to 120s when startupCPUBoost is disabled. Tune to match your application's typical startup time (e.g. 60s for a fast app, 300s for a heavy Spring Boot app with cache warming). --- charts/common/templates/hpa.yaml | 2 +- charts/common/tests/hpa_test.yaml | 25 +++++++++++++++++++++++++ charts/common/values.schema.json | 4 ++++ charts/common/values.yaml | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index a66e8e2..bb62e44 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -40,7 +40,7 @@ spec: {{- if not ((.Values.deployment).startupCPUBoost).enabled }} behavior: scaleUp: - stabilizationWindowSeconds: 120 + stabilizationWindowSeconds: {{ ((.Values.hpa).stabilizationWindowSeconds) | default 120 }} {{- end }} maxReplicas: {{ $maxReplicas | default 10 }} minReplicas: {{ max 2 $replicas }} diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index ee7bed0..92e24d3 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -85,6 +85,31 @@ tests: - equal: path: spec.metrics[0].resource.target.averageUtilization value: 60 + - it: adds 120s stabilization window when startupCPUBoost is disabled + set: + env: prd + asserts: + - equal: + path: spec.behavior.scaleUp.stabilizationWindowSeconds + value: 120 + - it: uses custom stabilizationWindowSeconds when set + set: + env: prd + hpa: + stabilizationWindowSeconds: 300 + asserts: + - equal: + path: spec.behavior.scaleUp.stabilizationWindowSeconds + value: 300 + - it: no stabilization window when startupCPUBoost is enabled + set: + env: prd + deployment: + startupCPUBoost: + enabled: true + asserts: + - notExists: + path: spec.behavior - it: custom metrics are appended alongside CPU metric set: env: prd diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json index 920c949..30cca85 100644 --- a/charts/common/values.schema.json +++ b/charts/common/values.schema.json @@ -212,6 +212,10 @@ "items": { "type": "object" }, "type": "array" }, + "stabilizationWindowSeconds": { + "description": "Seconds to wait before scaling up after a metric spike. Only applied when startupCPUBoost is disabled. Tune this to match your application's typical startup time (e.g. 60s for a fast app, 300s for a heavy Spring Boot app with cache warming).", + "type": ["integer", "null"] + }, "spec": { "description": "Full custom spec for HPA, replaces default metrics and min/max replicas. Inherits scaleTargetRef.", "type": "object" diff --git a/charts/common/values.yaml b/charts/common/values.yaml index c0d5fce..2a31366 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -147,6 +147,9 @@ hpa: # target: # type: Value # value: 10k + # -- (int) Seconds to wait before scaling up after a metric spike. Only applied when startupCPUBoost is disabled, to avoid scaling on startup CPU spikes. Tune this to match your application's typical startup time (e.g. 60s for a fast app, 300s for a heavy Spring Boot app with cache warming). + # @default -- 120 + stabilizationWindowSeconds: # -- Full custom spec for HPA, replaces default metrics and min/max replicas. Inherits `scaleTargetRef`. spec: {} From 6d0c216fee7b9af4fa83fa799b5cf5db9233d7c0 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 08:45:46 +0100 Subject: [PATCH 42/66] feat: auto-set CPU limit to 1.3x request when startupCPUBoost is enabled When CPU boost is enabled and no explicit cpuLimit is set, the CPU limit is automatically set to 130% of the CPU request. This gives the boost operator a ceiling to work within. Explicit cpuLimit always takes precedence. --- charts/common/templates/_helpers.tpl | 2 ++ charts/common/templates/deployment.yaml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 6634f86..9b80c48 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -57,6 +57,8 @@ resources: limits: {{- if .cpuLimit }} cpu: "{{ .cpuLimit| float64 }}" + {{- else if .startupCPUBoostEnabled }} + cpu: "{{ divf (mulf .cpu 13) 10 }}" {{- end }} {{- if .memoryLimit }} memory: "{{ .memoryLimit }}Mi" diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index 73adb78..64b134c 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -23,6 +23,7 @@ {{- $maxSurge := .Values.deployment.maxSurge | default "25%" }} {{- $maxUnavailable := .Values.deployment.maxUnavailable | default "25%" }} {{- $hpa := .Values.hpa | default dict }} +{{- $startupCPUBoostEnabled := ((.Values.deployment).startupCPUBoost).enabled }} {{- if $enabled }} {{- /* YAML Spec */}} apiVersion: apps/v1 @@ -102,7 +103,7 @@ spec: - containerPort: {{ $internalPort }} protocol: TCP {{- end }} - {{- include "resources" . | nindent 10 }} + {{- include "resources" (merge (dict "startupCPUBoostEnabled" $startupCPUBoostEnabled) .) | nindent 10 }} {{- include "securitycontext" . | nindent 10 }} {{- if or $grpc .grpc }} {{- include "grpcprobes" (dict "internalPort" $internalPort "probes" .probes) | nindent 10 -}} From 2909b6dc3b8a85f5abdad472c70bb11169b62b46 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 08:46:52 +0100 Subject: [PATCH 43/66] docs: mention auto CPU limit in startupCPUBoost comment --- charts/common/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 2a31366..ccc0f7d 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -68,7 +68,7 @@ deployment: # @default -- 70 cpuUtilization: startupCPUBoost: - # -- Enable GKE Startup CPU Boost to temporarily increase CPU during pod startup. Requires the kube-startup-cpu-boost operator installed in the cluster. Boost is reverted when the pod becomes Ready. + # -- Enable GKE Startup CPU Boost to temporarily increase CPU during pod startup. Requires the kube-startup-cpu-boost operator installed in the cluster. Boost is reverted when the pod becomes Ready. When enabled, a CPU limit of 1.3x the CPU request is automatically set (unless `container.cpuLimit` is explicitly configured). enabled: false # -- (int) Percentage to increase CPU requests during startup # @default -- 50 From 2026884e860acabaeb82bf3df96a52c1e311607d Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 08:53:12 +0100 Subject: [PATCH 44/66] fix: round CPU boost limit to 2 decimals and pass boost flag to cron resources --- charts/common/templates/_helpers.tpl | 2 +- charts/common/templates/cron.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 9b80c48..1bd2b03 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -58,7 +58,7 @@ resources: {{- if .cpuLimit }} cpu: "{{ .cpuLimit| float64 }}" {{- else if .startupCPUBoostEnabled }} - cpu: "{{ divf (mulf .cpu 13) 10 }}" + cpu: "{{ printf "%.2f" (divf (mulf .cpu 13) 10) }}" {{- end }} {{- if .memoryLimit }} memory: "{{ .memoryLimit }}Mi" diff --git a/charts/common/templates/cron.yaml b/charts/common/templates/cron.yaml index 5b46276..b031fcd 100644 --- a/charts/common/templates/cron.yaml +++ b/charts/common/templates/cron.yaml @@ -9,6 +9,7 @@ {{- $configmap := .Values.configmap -}} {{- $postgres := .Values.postgres -}} {{- $secrets := .Values.secrets -}} +{{- $startupCPUBoostEnabled := ((.Values.deployment).startupCPUBoost).enabled -}} {{- $cronjob := .Values.cron -}} {{- if $cronjob.enabled -}} {{- /* YAML Spec */}} @@ -68,7 +69,7 @@ spec: args: {{ toYaml .args | nindent 14 }} {{- end }} {{- include "environment" (dict "releaseName" $releaseName "app" $app "envLabel" $env "env" .env "envFrom" .envFrom "configmap" $configmap "postgres" $postgres "secrets" $secrets) | nindent 14 -}} - {{- include "resources" . | nindent 14 }} + {{- include "resources" (merge (dict "startupCPUBoostEnabled" $startupCPUBoostEnabled) .) | nindent 14 }} {{- include "securitycontext" . | nindent 14 }} {{- if .volumeMounts }} volumeMounts: From ed1219854b1be4c8881e33534dfda658f98956d2 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 08:54:10 +0100 Subject: [PATCH 45/66] docs: add comment explaining CPU boost limit calculation --- charts/common/templates/_helpers.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 1bd2b03..fc89e98 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -58,6 +58,7 @@ resources: {{- if .cpuLimit }} cpu: "{{ .cpuLimit| float64 }}" {{- else if .startupCPUBoostEnabled }} + {{- /* When CPU boost is enabled, set limit to 1.3x request so the boost operator has a ceiling to work within */}} cpu: "{{ printf "%.2f" (divf (mulf .cpu 13) 10) }}" {{- end }} {{- if .memoryLimit }} From 5df408d00b4dee4b43a7b2ca371f3ff89025d38a Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 09:41:53 +0100 Subject: [PATCH 46/66] test: add tests for CPU boost auto-limit, explicit override, and cron boost --- charts/common/tests/cron_test.yaml | 11 +++++++++ charts/common/tests/deployment_test.yaml | 30 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/charts/common/tests/cron_test.yaml b/charts/common/tests/cron_test.yaml index b2ce8b3..eeeac79 100644 --- a/charts/common/tests/cron_test.yaml +++ b/charts/common/tests/cron_test.yaml @@ -68,6 +68,17 @@ tests: - equal: path: spec.jobTemplate.spec.template.spec.containers[0].resources.requests.cpu value: "0.1" + - it: CPU limit is auto-set to 1.3x request when startupCPUBoost is enabled + set: + deployment: + startupCPUBoost: + enabled: true + container: + cpu: 0.2 + asserts: + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].resources.limits.cpu + value: "0.26" # #TODO mem req/limit should be equal - it: memory limit is 120 percent of request release: {} diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index e448b0e..fe003bf 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -48,6 +48,36 @@ tests: - equal: path: spec.template.spec.containers[0].resources.limits.cpu value: "1" + - it: CPU limit is auto-set to 1.3x request when startupCPUBoost is enabled + set: + deployment: + startupCPUBoost: + enabled: true + container: + cpu: 0.5 + asserts: + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: "0.65" + - equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: "0.5" + - it: explicit cpuLimit takes precedence over startupCPUBoost auto-limit + set: + deployment: + startupCPUBoost: + enabled: true + container: + cpu: 0.5 + cpuLimit: 2 + asserts: + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: "2" + - it: no CPU limit when startupCPUBoost is disabled and no cpuLimit set + asserts: + - notExists: + path: spec.template.spec.containers[0].resources.limits.cpu # #TODO mem req/limit should be equal - it: memory limit is 120 percent of request set: From dee00baf98bddbeeaf8d005613724f423d7e28d7 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 09:59:30 +0100 Subject: [PATCH 47/66] feat!: set memory limit equal to memory request, remove 120% multiplier Memory limit is now always equal to memory request. The previous 1.2x multiplier and memoryLimit override are removed. container.memoryLimit is deprecated with a note to use container.memory instead. BREAKING CHANGE: container.memoryLimit is removed. Memory limit now always equals memory request. Set container.memory to the value you need. --- charts/common/templates/_helpers.tpl | 6 +----- charts/common/tests/cron_test.yaml | 16 +--------------- charts/common/tests/deployment_test.yaml | 18 +----------------- .../deployment_v1_compatibility_test.yaml | 15 +-------------- charts/common/values.schema.json | 2 +- charts/common/values.yaml | 3 +-- fixture/helm/ci/values-ci-cronjob-tests.yaml | 1 - fixture/helm/ci/values-ci-tests.yaml | 1 - 8 files changed, 6 insertions(+), 56 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index fc89e98..fe3e32e 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -61,11 +61,7 @@ resources: {{- /* When CPU boost is enabled, set limit to 1.3x request so the boost operator has a ceiling to work within */}} cpu: "{{ printf "%.2f" (divf (mulf .cpu 13) 10) }}" {{- end }} - {{- if .memoryLimit }} - memory: "{{ .memoryLimit }}Mi" - {{- else }} - memory: "{{ (div (mul .memory 6) 5) }}Mi" - {{- end }} + memory: "{{ .memory }}Mi" {{- if .ephemeralStorageLimit }} ephemeral-storage: "{{ .ephemeralStorageLimit }}" {{- end }} diff --git a/charts/common/tests/cron_test.yaml b/charts/common/tests/cron_test.yaml index eeeac79..6866ca4 100644 --- a/charts/common/tests/cron_test.yaml +++ b/charts/common/tests/cron_test.yaml @@ -80,7 +80,7 @@ tests: path: spec.jobTemplate.spec.template.spec.containers[0].resources.limits.cpu value: "0.26" # #TODO mem req/limit should be equal - - it: memory limit is 120 percent of request + - it: memory limit equals memory request release: {} set: containers: @@ -89,21 +89,7 @@ tests: asserts: - equal: path: spec.jobTemplate.spec.template.spec.containers[0].resources.limits.memory - value: 307Mi - - equal: - path: spec.jobTemplate.spec.template.spec.containers[0].resources.requests.memory value: 256Mi - - it: memory limit can be overridden - release: {} - set: - containers: - - image: img - memory: 256 - memoryLimit: 1024 - asserts: - - equal: - path: spec.jobTemplate.spec.template.spec.containers[0].resources.limits.memory - value: 1024Mi - equal: path: spec.jobTemplate.spec.template.spec.containers[0].resources.requests.memory value: 256Mi diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index fe003bf..07f7f0a 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -79,7 +79,7 @@ tests: - notExists: path: spec.template.spec.containers[0].resources.limits.cpu # #TODO mem req/limit should be equal - - it: memory limit is 120 percent of request + - it: memory limit equals memory request set: containers: - name: test @@ -90,23 +90,7 @@ tests: asserts: - equal: path: spec.template.spec.containers[0].resources.limits.memory - value: 307Mi - - equal: - path: spec.template.spec.containers[0].resources.requests.memory value: 256Mi - - it: memory limit can be overridden - set: - containers: - - name: test - image: img - memory: 256 - memoryLimit: 1024 - probes: - enabled: false - asserts: - - equal: - path: spec.template.spec.containers[0].resources.limits.memory - value: 1024Mi - equal: path: spec.template.spec.containers[0].resources.requests.memory value: 256Mi diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml index b946401..0f11313 100644 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ b/charts/common/tests/deployment_v1_compatibility_test.yaml @@ -74,7 +74,7 @@ tests: - equal: path: spec.template.spec.containers[0].resources.requests.cpu value: "0.1" - - it: memory limit is 120 percent of request + - it: memory limit equals memory request set: container: image: img @@ -82,20 +82,7 @@ tests: asserts: - equal: path: spec.template.spec.containers[0].resources.limits.memory - value: 307Mi - - equal: - path: spec.template.spec.containers[0].resources.requests.memory value: 256Mi - - it: memory limit can be overridden - set: - container: - image: img - memory: 256 - memoryLimit: 1024 - asserts: - - equal: - path: spec.template.spec.containers[0].resources.limits.memory - value: 1024Mi - equal: path: spec.template.spec.containers[0].resources.requests.memory value: 256Mi diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json index 30cca85..ba3ef69 100644 --- a/charts/common/values.schema.json +++ b/charts/common/values.schema.json @@ -294,7 +294,7 @@ "type": "integer" }, "memoryLimit": { - "description": "Set memory limit without any unit, Mi is inferred", + "description": "Deprecated. Memory limit is now always equal to memory request. Use container.memory instead.", "type": ["integer", "null"] }, "uid": { diff --git a/charts/common/values.yaml b/charts/common/values.yaml index ccc0f7d..98d35ff 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -203,8 +203,7 @@ container: # -- Set memory without any unit, `Mi` is inferred # @default -- 16 memory: 16 - # -- Set memory limit without any unit, `Mi` is inferred - # @default -- `1.2 * memory` + # -- @deprecated memoryLimit is removed. Memory limit is now always equal to memory request. Use `container.memory` instead. memoryLimit: # -- Set the uid that your user runs with # @default -- 1000 diff --git a/fixture/helm/ci/values-ci-cronjob-tests.yaml b/fixture/helm/ci/values-ci-cronjob-tests.yaml index c93d275..8f842d1 100644 --- a/fixture/helm/ci/values-ci-cronjob-tests.yaml +++ b/fixture/helm/ci/values-ci-cronjob-tests.yaml @@ -21,7 +21,6 @@ container: version: v1.2.3 cpu: 0.2 memory: 64 - memoryLimit: 64 envFrom: [] probes: liveness: diff --git a/fixture/helm/ci/values-ci-tests.yaml b/fixture/helm/ci/values-ci-tests.yaml index ec7047b..02a85fb 100644 --- a/fixture/helm/ci/values-ci-tests.yaml +++ b/fixture/helm/ci/values-ci-tests.yaml @@ -27,7 +27,6 @@ container: version: v1.2.3 cpu: 0.2 memory: 64 - memoryLimit: 64 envFrom: [] probes: liveness: From ff954911f31cdc2f03f3cea051479000e09b4db5 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:17:14 +0100 Subject: [PATCH 48/66] feat!: enable HPA in all environments with env-aware minReplicas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HPA is now always enabled (unless forceReplicas is set). The Deployment spec never emits replicas — HPA controls pod count in all environments. Default minReplicas by environment: - sbx/dev/tst: 1 (scales down to single pod in low traffic) - prd: 2 (HA by default) deployment.replicas overrides the default minReplicas for any env. PDB protection follows minReplicas: 0% when minReplicas=1, 50% when >=2. This prevents the v1 bug where helm upgrade would reset HPA-managed replica counts back to the configured value. BREAKING CHANGE: HPA is now enabled by default in all environments. Use deployment.forceReplicas to opt out of HPA. --- charts/common/templates/_helpers.tpl | 14 ++++++++++++-- charts/common/templates/hpa.yaml | 2 +- charts/common/templates/pdb.yaml | 9 +++++---- charts/common/tests/deployment_test.yaml | 10 ++++------ .../deployment_v1_compatibility_test.yaml | 5 ++--- charts/common/tests/hpa_test.yaml | 18 ++++++++++++------ charts/common/tests/pdb_test.yaml | 15 ++++++--------- 7 files changed, 42 insertions(+), 31 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index fe3e32e..d6fec00 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -197,10 +197,20 @@ livenessProbe: {{- end }} {{- define "hpa.enabled" -}} - {{- if and (not .forceReplicas) (or (eq "prd" .env) .maxReplicas .hpa.spec.minReplicas) -}} + {{- if .forceReplicas -}} + {{- printf "false" -}} + {{- else -}} {{- printf "true" -}} + {{- end -}} +{{- end -}} + +{{- define "hpa.minReplicas" -}} + {{- if .replicas -}} + {{- .replicas -}} + {{- else if eq "prd" .env -}} + {{- 2 -}} {{- else -}} - {{- printf "false" -}} + {{- 1 -}} {{- end -}} {{- end -}} diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index bb62e44..766143f 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -43,7 +43,7 @@ spec: stabilizationWindowSeconds: {{ ((.Values.hpa).stabilizationWindowSeconds) | default 120 }} {{- end }} maxReplicas: {{ $maxReplicas | default 10 }} - minReplicas: {{ max 2 $replicas }} + minReplicas: {{ include "hpa.minReplicas" (dict "env" $env "replicas" $replicas) }} {{- end }} scaleTargetRef: apiVersion: apps/v1 diff --git a/charts/common/templates/pdb.yaml b/charts/common/templates/pdb.yaml index ba1021a..8833494 100644 --- a/charts/common/templates/pdb.yaml +++ b/charts/common/templates/pdb.yaml @@ -20,10 +20,11 @@ {{- if gt (int $forceReplicas) 1 -}} {{- $protected = true -}} {{- end -}} -{{- else if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" -}} - {{- $protected = true -}} -{{- else if and $replicas (gt (int $replicas) 1) -}} - {{- $protected = true -}} +{{- else -}} + {{- $minReplicas := int (include "hpa.minReplicas" (dict "env" $env "replicas" $replicas)) -}} + {{- if gt $minReplicas 1 -}} + {{- $protected = true -}} + {{- end -}} {{- end -}} {{- /* YAML Spec */}} apiVersion: policy/v1 diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index 07f7f0a..be44507 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -125,15 +125,14 @@ tests: - equal: path: spec.template.metadata.annotations["prometheus.io/port"] value: "8080" - - it: must use 3 replicas if replicas is 3 + - it: replicas field is skipped when HPA is enabled set: env: dev deployment: replicas: 3 asserts: - - equal: + - notExists: path: spec.replicas - value: 3 - equal: path: spec.strategy.type value: RollingUpdate @@ -453,7 +452,7 @@ tests: asserts: - notExists: path: spec.template.spec.containers[1].resources.limits.cpu - - it: must use 3 replicas if replicas is 3 + - it: replicas field is skipped when HPA is enabled (v1 compat) set: env: dev deployment: @@ -461,9 +460,8 @@ tests: container: image: testimg asserts: - - equal: + - notExists: path: spec.replicas - value: 3 - equal: path: spec.strategy.type value: RollingUpdate diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml index 0f11313..3f8314a 100644 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ b/charts/common/tests/deployment_v1_compatibility_test.yaml @@ -149,7 +149,7 @@ tests: asserts: - notExists: path: spec.template.spec.containers[1].resources.limits.cpu - - it: must use 3 replicas if replicas is 3 + - it: replicas field is skipped when HPA is enabled set: env: dev deployment: @@ -157,9 +157,8 @@ tests: container: image: testimg asserts: - - equal: + - notExists: path: spec.replicas - value: 3 - equal: path: spec.strategy.type value: RollingUpdate diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index 92e24d3..148a48e 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -34,11 +34,19 @@ tests: - equal: path: metadata.annotations["meta.helm.sh/release-namespace"] value: NAMESPACE - - it: Should not generate hpa by default + - it: HPA is always generated by default asserts: - hasDocuments: - count: 0 - - it: hpa must be generated if maxReplicas > 1 and must have labels + count: 1 + - it: HPA minReplicas defaults to 1 in dev + set: + deployment: + replicas: null + asserts: + - equal: + path: spec.minReplicas + value: 1 + - it: hpa must have labels set: deployment: maxReplicas: 2 @@ -135,11 +143,9 @@ tests: - equal: path: spec.metrics[1].pods.target.averageValue value: 100 - - it: Must use minimum two replicas in prod, max 10 + - it: Must default to 2 minReplicas in prod, max 10 set: env: prd - deployment: - replicas: 1 # even if 1, this must be 2 in prod container: image: img asserts: diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index f7c202d..6ae60c4 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -114,7 +114,7 @@ tests: path: spec.minAvailable value: "25%" # replicas=1 with HPA enabled: HPA forces min 2 pods, PDB should protect - - it: if container replicas is 1 in prd, HPA is active so minAvailable must be 50% + - it: if replicas is 1 in prd, minAvailable must be 0% (single pod) set: env: prd deployment: @@ -124,12 +124,10 @@ tests: asserts: - equal: path: spec.minAvailable - value: "50%" - - it: if deployment replicas is 1 in prd, HPA is active so minAvailable must be 50% + value: "0%" + - it: prd defaults to minAvailable 50% when replicas not set (minReplicas=2) set: env: prd - deployment: - replicas: 1 container: image: some asserts: @@ -336,19 +334,18 @@ tests: path: spec.minAvailable value: "29%" # replicas=1 with HPA via maxReplicas in non-prd: HPA active, PDB should protect - - it: if replicas is 1 but maxReplicas set in dev, HPA is active so minAvailable must be 50% + - it: if replicas is 1 in dev, minAvailable must be 0% (single pod) set: env: dev deployment: replicas: 1 - maxReplicas: 4 container: image: some asserts: - equal: path: spec.minAvailable - value: "50%" - # replicas > 1 without HPA in non-prd + value: "0%" + # replicas > 1 in non-prd - it: if replicas is 2 without HPA in dev, minAvailable must be 50% set: env: dev From 4a15431b0a25fb18b27b00db5b7c23d813b65bf6 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:25:11 +0100 Subject: [PATCH 49/66] feat!: replace deployment.replicas with deployment.minReplicas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deployment.replicas is removed. Use deployment.minReplicas to set the HPA minimum replica count, or deployment.forceReplicas to disable HPA. Since HPA is always enabled, the Deployment spec never emits replicas — HPA controls the pod count. This prevents helm upgrade from resetting HPA-managed replica counts. BREAKING CHANGE: deployment.replicas is removed. Use deployment.minReplicas (sets HPA minimum) or deployment.forceReplicas (disables HPA). --- charts/common/templates/deployment.yaml | 8 +----- charts/common/templates/hpa.yaml | 6 ++-- charts/common/templates/pdb.yaml | 14 ++++------ charts/common/tests/deployment_test.yaml | 6 ++-- .../deployment_v1_compatibility_test.yaml | 2 +- charts/common/tests/hpa_test.yaml | 4 +-- charts/common/tests/pdb_test.yaml | 28 +++++++++---------- .../tests/values/common-test-values.yaml | 2 +- charts/common/values.schema.json | 4 +-- charts/common/values.yaml | 9 +++--- .../simple-app/env/values-kub-ent-prd.yaml | 2 +- .../env/values-kub-ent-prd.yaml | 2 +- examples/common/typical-frontend/values.yaml | 2 +- fixture/helm/ci/values-ci-tests.yaml | 2 +- 14 files changed, 41 insertions(+), 50 deletions(-) diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index 64b134c..c848b35 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -16,13 +16,10 @@ {{- $enabled := .Values.deployment.enabled }} {{- $prometheus := .Values.deployment.prometheus | default .Values.container.prometheus }} {{- $prometheusPath := (.Values.deployment.prometheus).path | default .Values.container.prometheus.path | default "/actuator/prometheus" }} -{{- $replicas := .Values.deployment.replicas }} {{- $forceReplicas := .Values.deployment.forceReplicas }} -{{- $maxReplicas := .Values.deployment.maxReplicas }} {{- $terminationGracePeriodSeconds := .Values.deployment.terminationGracePeriodSeconds }} {{- $maxSurge := .Values.deployment.maxSurge | default "25%" }} {{- $maxUnavailable := .Values.deployment.maxUnavailable | default "25%" }} -{{- $hpa := .Values.hpa | default dict }} {{- $startupCPUBoostEnabled := ((.Values.deployment).startupCPUBoost).enabled }} {{- if $enabled }} {{- /* YAML Spec */}} @@ -42,11 +39,8 @@ spec: minReadySeconds: {{ .Values.deployment.minReadySeconds }} {{- if $forceReplicas }} replicas: {{ $forceReplicas }} - {{- else if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" }} - # intentionally skip replicas - {{- else }} - replicas: {{ $replicas }} {{- end }} + {{- /* When HPA is enabled (no forceReplicas), replicas is omitted so HPA controls the pod count */}} strategy: {{- if (eq (int $forceReplicas) 1) }} type: Recreate diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index 766143f..d2db021 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -1,13 +1,13 @@ {{- /* Rules */}} {{- $env := .Values.env | required ".Values.common.env is required." -}} {{- $releaseName := include "name" . -}} -{{- $replicas := .Values.deployment.replicas }} +{{- $minReplicas := .Values.deployment.minReplicas }} {{- $forceReplicas := .Values.deployment.forceReplicas }} {{- $maxReplicas := .Values.deployment.maxReplicas }} {{- $hpa := .Values.hpa | default dict }} {{- $cpuUtilization := .Values.deployment.cpuUtilization | default 70 }} -{{- if eq (include "hpa.enabled" (dict "env" $env "forceReplicas" $forceReplicas "maxReplicas" $maxReplicas "hpa" $hpa)) "true" }} +{{- if eq (include "hpa.enabled" (dict "forceReplicas" $forceReplicas)) "true" }} {{- /* Rules */}} {{- if (eq 1 (int $maxReplicas)) }} {{- required ".Values.common.deployment.maxReplicas must be more than 1." .Values.error -}} @@ -43,7 +43,7 @@ spec: stabilizationWindowSeconds: {{ ((.Values.hpa).stabilizationWindowSeconds) | default 120 }} {{- end }} maxReplicas: {{ $maxReplicas | default 10 }} - minReplicas: {{ include "hpa.minReplicas" (dict "env" $env "replicas" $replicas) }} + minReplicas: {{ include "hpa.minReplicas" (dict "env" $env "replicas" $minReplicas) }} {{- end }} scaleTargetRef: apiVersion: apps/v1 diff --git a/charts/common/templates/pdb.yaml b/charts/common/templates/pdb.yaml index 8833494..7f84593 100644 --- a/charts/common/templates/pdb.yaml +++ b/charts/common/templates/pdb.yaml @@ -5,14 +5,12 @@ {{- $forceReplicas := .Values.deployment.forceReplicas -}} {{- $minAvailable := .Values.deployment.minAvailable -}} {{- $minAvailablePDB := .Values.pdb.minAvailable -}} -{{- $replicas := .Values.deployment.replicas -}} -{{- $hpa := .Values.hpa | default dict -}} +{{- $minReplicas := .Values.deployment.minReplicas -}} {{- $maxReplicas := .Values.deployment.maxReplicas -}} {{- /* - Determine effective replica count to decide if PDB should protect pods. - - forceReplicas: exact count, no HPA - - HPA enabled: always >= 2 replicas (hpa.yaml enforces minReplicas: max(2, replicas)) - - Otherwise: use configured replicas + Determine if PDB should protect pods. + - forceReplicas: exact count, no HPA — protect if > 1 + - HPA: protect if minReplicas > 1 (prd defaults to 2, dev/tst to 1) PDB is set to 0% only when effective replicas <= 1. */}} {{- $protected := false -}} @@ -21,8 +19,8 @@ {{- $protected = true -}} {{- end -}} {{- else -}} - {{- $minReplicas := int (include "hpa.minReplicas" (dict "env" $env "replicas" $replicas)) -}} - {{- if gt $minReplicas 1 -}} + {{- $effectiveMinReplicas := int (include "hpa.minReplicas" (dict "env" $env "replicas" $minReplicas)) -}} + {{- if gt $effectiveMinReplicas 1 -}} {{- $protected = true -}} {{- end -}} {{- end -}} diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index be44507..70496ce 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -129,7 +129,7 @@ tests: set: env: dev deployment: - replicas: 3 + minReplicas: 3 asserts: - notExists: path: spec.replicas @@ -140,7 +140,7 @@ tests: set: env: prd deployment: - replicas: 3 + minReplicas: 3 maxReplicas: 8 asserts: - notExists: @@ -456,7 +456,7 @@ tests: set: env: dev deployment: - replicas: 3 + minReplicas: 3 container: image: testimg asserts: diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml index 3f8314a..39bbf83 100644 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ b/charts/common/tests/deployment_v1_compatibility_test.yaml @@ -153,7 +153,7 @@ tests: set: env: dev deployment: - replicas: 3 + minReplicas: 3 container: image: testimg asserts: diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index 148a48e..d07af44 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -41,7 +41,7 @@ tests: - it: HPA minReplicas defaults to 1 in dev set: deployment: - replicas: null + minReplicas: null asserts: - equal: path: spec.minReplicas @@ -67,7 +67,7 @@ tests: - it: use minReplicas from deployment if not set on container set: deployment: - replicas: 4 + minReplicas: 4 maxReplicas: 5 containers: - image: img diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index 6ae60c4..a0618ea 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -52,7 +52,7 @@ tests: set: env: prd deployment: - replicas: 2 + minReplicas: 2 asserts: - equal: path: spec.minAvailable @@ -70,7 +70,7 @@ tests: set: env: prd deployment: - replicas: 2 + minReplicas: 2 minAvailable: 27% asserts: - equal: @@ -80,7 +80,7 @@ tests: set: env: prd deployment: - replicas: 2 + minReplicas: 2 minAvailable: 26% containers: - image: app @@ -92,7 +92,7 @@ tests: set: env: prd deployment: - replicas: 2 + minReplicas: 2 minAvailable: 30% containers: - image: app @@ -106,7 +106,7 @@ tests: pdb: minAvailable: 25% deployment: - replicas: 2 + minReplicas: 2 containers: - image: app asserts: @@ -118,7 +118,7 @@ tests: set: env: prd deployment: - replicas: 1 + minReplicas: 1 container: image: some asserts: @@ -139,7 +139,7 @@ tests: set: env: dev deployment: - replicas: 1 + minReplicas: 1 container: image: some asserts: @@ -150,7 +150,7 @@ tests: set: env: dev deployment: - replicas: 1 + minReplicas: 1 container: image: some asserts: @@ -290,7 +290,7 @@ tests: set: env: tst deployment: - replicas: null + minReplicas: null asserts: - equal: path: spec.minAvailable @@ -299,7 +299,7 @@ tests: set: env: tst deployment: - replicas: null + minReplicas: null asserts: - equal: path: spec.minAvailable @@ -308,7 +308,7 @@ tests: set: env: dev deployment: - replicas: null + minReplicas: null asserts: - equal: path: spec.minAvailable @@ -317,7 +317,7 @@ tests: set: env: dev deployment: - replicas: 2 + minReplicas: 2 minAvailable: 24% asserts: - equal: @@ -338,7 +338,7 @@ tests: set: env: dev deployment: - replicas: 1 + minReplicas: 1 container: image: some asserts: @@ -350,7 +350,7 @@ tests: set: env: dev deployment: - replicas: 2 + minReplicas: 2 container: image: some asserts: diff --git a/charts/common/tests/values/common-test-values.yaml b/charts/common/tests/values/common-test-values.yaml index 58502de..84fbecf 100644 --- a/charts/common/tests/values/common-test-values.yaml +++ b/charts/common/tests/values/common-test-values.yaml @@ -9,7 +9,7 @@ service: externalPort: 8080 internalPort: 8080 deployment: - replicas: 2 + minReplicas: 2 container: image: img memory: 768 diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json index ba3ef69..04bf1d2 100644 --- a/charts/common/values.schema.json +++ b/charts/common/values.schema.json @@ -92,8 +92,8 @@ "prometheus": { "$ref": "#/properties/container/properties/prometheus" }, - "replicas": { - "description": "Set the target replica count", + "minReplicas": { + "description": "Set the minimum replica count for HPA. Defaults to 1 for sbx/dev/tst and 2 for prd.", "type": ["integer", "null"] }, "maxReplicas": { diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 98d35ff..1c69e8f 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -42,13 +42,12 @@ deployment: volumes: [] # -- Prometheus #prometheus: same as container.prometheus stanza - # -- Set the target replica count - # @default -- container.replicas - replicas: - # -- Set the max replica count + # -- (int) Set the minimum replica count for HPA. Defaults to 1 for sbx/dev/tst and 2 for prd. + minReplicas: + # -- (int) Set the max replica count for HPA # @default -- 10 maxReplicas: - # -- (int) Force replicas disables autoscaling and PDB, if set to 1 it will use Recreate strategy + # -- (int) Force a fixed replica count, disables HPA and PDB. If set to 1 it will use Recreate strategy. forceReplicas: # -- (int) Override pod terminationGracePeriodSeconds (default 30s). terminationGracePeriodSeconds: diff --git a/examples/common/simple-app/env/values-kub-ent-prd.yaml b/examples/common/simple-app/env/values-kub-ent-prd.yaml index c4201c2..421c942 100644 --- a/examples/common/simple-app/env/values-kub-ent-prd.yaml +++ b/examples/common/simple-app/env/values-kub-ent-prd.yaml @@ -3,7 +3,7 @@ common: ingress: host: simple-app.entur.io deployment: - replicas: 2 + minReplicas: 2 maxReplicas: 10 configmap: enabled: true diff --git a/examples/common/typical-frontend/env/values-kub-ent-prd.yaml b/examples/common/typical-frontend/env/values-kub-ent-prd.yaml index 7464fe4..5c9bf27 100644 --- a/examples/common/typical-frontend/env/values-kub-ent-prd.yaml +++ b/examples/common/typical-frontend/env/values-kub-ent-prd.yaml @@ -4,7 +4,7 @@ common: host: typical-frontend.entur.no deployment: - replicas: 2 + minReplicas: 2 maxReplicas: 10 configmap: enabled: true diff --git a/examples/common/typical-frontend/values.yaml b/examples/common/typical-frontend/values.yaml index 12e0fe0..9419734 100644 --- a/examples/common/typical-frontend/values.yaml +++ b/examples/common/typical-frontend/values.yaml @@ -10,7 +10,7 @@ common: deployment: prometheus: enabled: true - replicas: 2 + minReplicas: 2 container: image: <+artifacts.primary.image> cpu: 0.3 diff --git a/fixture/helm/ci/values-ci-tests.yaml b/fixture/helm/ci/values-ci-tests.yaml index 02a85fb..eaf6175 100644 --- a/fixture/helm/ci/values-ci-tests.yaml +++ b/fixture/helm/ci/values-ci-tests.yaml @@ -18,7 +18,7 @@ service: internalPort: 8080 deployment: - replicas: 1 + minReplicas: 1 maxReplicas: 2 container: From e089531e35aa5c947b136715e2fe451f77759eec Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:31:40 +0100 Subject: [PATCH 50/66] docs: update UPGRADE.md for minReplicas, HPA-always-on, and memoryLimit removal --- UPGRADE.md | 71 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index a9b7fdb..e781f0e 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -5,8 +5,8 @@ This guide covers all breaking changes and required migration steps when upgradi ## Prerequisites - Helm 3.x or Helm 4.x -- The [kube-startup-cpu-boost](https://github.com/google/kube-startup-cpu-boost) operator installed in your cluster (enabled by default in v2). If not available, set `deployment.startupCPUBoost.enabled: false`. - External Secrets Operator installed (required if using `postgres` or `secrets`) +- Optionally: [kube-startup-cpu-boost](https://github.com/google/kube-startup-cpu-boost) operator for CPU boost feature ## Breaking Changes @@ -26,18 +26,25 @@ common: The Kubernetes label `shortname` is still emitted for backwards compatibility alongside the new `appId` label. -### 2. Scaling fields moved from `container.*` to `deployment.*` +### 2. Scaling fields replaced -The following fields are removed from `container` and must be set under `deployment`: +Scaling fields have been removed from `container.*` and consolidated under `deployment.*`. Additionally, `deployment.replicas` is replaced by `deployment.minReplicas`. + +**HPA is now always enabled** (unless `forceReplicas` is set). The Deployment spec never emits `replicas` — HPA controls the pod count in all environments. This prevents the v1 bug where `helm upgrade` would reset HPA-managed replica counts. | Removed (v1) | Replacement (v2) | |---|---| -| `container.replicas` | `deployment.replicas` | +| `container.replicas` | `deployment.minReplicas` | +| `deployment.replicas` | `deployment.minReplicas` | | `container.maxReplicas` | `deployment.maxReplicas` | | `container.forceReplicas` | `deployment.forceReplicas` | | `container.minAvailable` | `deployment.minAvailable` | | `container.terminationGracePeriodSeconds` | `deployment.terminationGracePeriodSeconds` | +Default `minReplicas` by environment: +- `sbx`/`dev`/`tst`: **1** (scales down to single pod in low traffic) +- `prd`: **2** (HA by default) + ```yaml # v1 common: @@ -48,11 +55,36 @@ common: # v2 common: deployment: - replicas: 2 - maxReplicas: 5 + minReplicas: 2 # HPA minimum + maxReplicas: 5 # HPA maximum +``` + +To disable HPA and fix a specific replica count, use `forceReplicas`: + +```yaml +common: + deployment: + forceReplicas: 3 # disables HPA, fixed at 3 pods ``` -### 3. Cloud SQL Proxy upgraded to v2 +### 3. `container.memoryLimit` removed + +Memory limit is now always equal to memory request. The 1.2x multiplier and `memoryLimit` override are removed. Set `container.memory` to the value you need for both request and limit. + +```yaml +# v1 +common: + container: + memory: 512 + memoryLimit: 1024 + +# v2 +common: + container: + memory: 1024 # sets both request and limit +``` + +### 4. Cloud SQL Proxy upgraded to v2 The Cloud SQL Auth Proxy has been upgraded from v1 (1.33.16) to v2 (2.21.2). This changes how database connections are configured. @@ -83,7 +115,7 @@ common: **`postgres.memoryLimit` is removed.** Memory limit is now always equal to memory request. Use `postgres.memory` to set both. -### 4. `ingress.class` annotation replaced with `spec.ingressClassName` +### 5. `ingress.class` annotation replaced with `spec.ingressClassName` The deprecated `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). @@ -95,7 +127,7 @@ common: ingressClassName: nginx ``` -### 5. `configmap.toEnv` is removed +### 6. `configmap.toEnv` is removed If you get a schema error like `configmap.toEnv is no longer valid in v2`, switch to `container.envFrom` to mount the configmap as environment variables: @@ -124,14 +156,19 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl ## New Features (no action required) +### HPA always enabled +- HPA is now enabled in all environments, not just `prd`. Default `minReplicas` is 1 for sbx/dev/tst and 2 for prd. +- When `startupCPUBoost` is disabled, a 120s scaleUp stabilization window prevents startup CPU spikes from triggering unnecessary scale-ups. Tune via `hpa.stabilizationWindowSeconds` to match your app's startup time. + ### PDB improvements - `unhealthyPodEvictionPolicy: AlwaysAllow` prevents unhealthy pods from blocking cluster upgrades. - PDB now correctly protects pods when `forceReplicas > 1` (was incorrectly set to 0%). ### GKE Startup CPU Boost -- Enabled by default. Temporarily increases CPU by 50% during startup, reverts when pod becomes Ready. -- HPA default `cpuUtilization` lowered from 100% to 70% since startup spikes are handled by the boost. -- Disable with `deployment.startupCPUBoost.enabled: false` if the operator is not installed. +- Disabled by default. Enable with `deployment.startupCPUBoost.enabled: true` (requires the operator installed in the cluster). +- Temporarily increases CPU by 50% during startup, reverts when pod becomes Ready. +- When enabled, a CPU limit of 1.3x the CPU request is automatically set. +- HPA default `cpuUtilization` lowered from 100% to 70%. ### gRPC native probes - Setting `grpc: true` now uses native Kubernetes gRPC probes by default, using `service.internalPort`. @@ -144,6 +181,7 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl ### Custom HPA metrics - `hpa.metrics` — append custom metrics (Pods, External, Object) alongside default CPU scaling. - `deployment.cpuUtilization` — set HPA CPU target (default 70%). +- `hpa.stabilizationWindowSeconds` — tune scaleUp delay (default 120s when CPU boost is disabled). ### Per-ingress annotations and ingressClassName - Each entry in `ingresses` list can now have its own `annotations` and `ingressClassName`. @@ -155,10 +193,13 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl ## Quick Migration Checklist - [ ] Rename `shortname` to `appId` in all values files -- [ ] Move `container.replicas` → `deployment.replicas` (and maxReplicas, forceReplicas, minAvailable, terminationGracePeriodSeconds) +- [ ] Replace `container.replicas` / `deployment.replicas` → `deployment.minReplicas` +- [ ] Replace `container.maxReplicas` → `deployment.maxReplicas` +- [ ] Replace `container.forceReplicas` → `deployment.forceReplicas` +- [ ] Replace `container.minAvailable` → `deployment.minAvailable` +- [ ] Replace `container.terminationGracePeriodSeconds` → `deployment.terminationGracePeriodSeconds` +- [ ] Remove `container.memoryLimit` / `postgres.memoryLimit` — set `memory` to the value you need - [ ] Replace `postgres.connectionConfig` with `postgres.instances: [PGINSTANCES]` (if using postgres) -- [ ] Remove `postgres.memoryLimit` (if set) — use `postgres.memory` instead -- [ ] Verify `kube-startup-cpu-boost` operator is installed, or set `deployment.startupCPUBoost.enabled: false` - [ ] If using gRPC: remove explicit `probes.*.grpc.port` settings (now defaults to `service.internalPort`) - [ ] Update `Chart.yaml` dependency version to v2 - [ ] Run `helm dependency update` From 6b687f227abcf87cb3948867a79041e38e2ab357 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:45:08 +0100 Subject: [PATCH 51/66] =?UTF-8?q?feat!:=20simplify=20scaling=20defaults=20?= =?UTF-8?q?=E2=80=94=20minReplicas=202=20everywhere,=20remove=20pdb.minAva?= =?UTF-8?q?ilable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default minReplicas to 2 in all environments (no more env-aware branching) - Remove pdb.minAvailable — use deployment.minAvailable instead (one place to configure) - Change maxSurge and maxUnavailable defaults from 25% to 1 (works correctly with 2 replicas) - PDB automatically 50% when minReplicas >= 2, 0% when minReplicas = 1 BREAKING CHANGE: pdb.minAvailable is removed. Use deployment.minAvailable instead. Default minReplicas is now 2 in all environments (was 1 in dev/tst). Default maxSurge and maxUnavailable changed from 25% to 1. --- charts/common/templates/_helpers.tpl | 8 +------- charts/common/templates/deployment.yaml | 4 ++-- charts/common/templates/hpa.yaml | 2 +- charts/common/templates/pdb.yaml | 9 ++------- charts/common/tests/hpa_test.yaml | 4 ++-- charts/common/tests/pdb_test.yaml | 20 +++++++++----------- charts/common/values.schema.json | 10 ++-------- charts/common/values.yaml | 18 ++++++++---------- 8 files changed, 27 insertions(+), 48 deletions(-) diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index d6fec00..4429d28 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -205,12 +205,6 @@ livenessProbe: {{- end -}} {{- define "hpa.minReplicas" -}} - {{- if .replicas -}} - {{- .replicas -}} - {{- else if eq "prd" .env -}} - {{- 2 -}} - {{- else -}} - {{- 1 -}} - {{- end -}} + {{- .replicas | default 2 -}} {{- end -}} diff --git a/charts/common/templates/deployment.yaml b/charts/common/templates/deployment.yaml index c848b35..019c853 100644 --- a/charts/common/templates/deployment.yaml +++ b/charts/common/templates/deployment.yaml @@ -18,8 +18,8 @@ {{- $prometheusPath := (.Values.deployment.prometheus).path | default .Values.container.prometheus.path | default "/actuator/prometheus" }} {{- $forceReplicas := .Values.deployment.forceReplicas }} {{- $terminationGracePeriodSeconds := .Values.deployment.terminationGracePeriodSeconds }} -{{- $maxSurge := .Values.deployment.maxSurge | default "25%" }} -{{- $maxUnavailable := .Values.deployment.maxUnavailable | default "25%" }} +{{- $maxSurge := .Values.deployment.maxSurge | default 1 }} +{{- $maxUnavailable := .Values.deployment.maxUnavailable | default 1 }} {{- $startupCPUBoostEnabled := ((.Values.deployment).startupCPUBoost).enabled }} {{- if $enabled }} {{- /* YAML Spec */}} diff --git a/charts/common/templates/hpa.yaml b/charts/common/templates/hpa.yaml index d2db021..49fadce 100644 --- a/charts/common/templates/hpa.yaml +++ b/charts/common/templates/hpa.yaml @@ -43,7 +43,7 @@ spec: stabilizationWindowSeconds: {{ ((.Values.hpa).stabilizationWindowSeconds) | default 120 }} {{- end }} maxReplicas: {{ $maxReplicas | default 10 }} - minReplicas: {{ include "hpa.minReplicas" (dict "env" $env "replicas" $minReplicas) }} + minReplicas: {{ include "hpa.minReplicas" (dict "replicas" $minReplicas) }} {{- end }} scaleTargetRef: apiVersion: apps/v1 diff --git a/charts/common/templates/pdb.yaml b/charts/common/templates/pdb.yaml index 7f84593..ef01b1e 100644 --- a/charts/common/templates/pdb.yaml +++ b/charts/common/templates/pdb.yaml @@ -4,13 +4,11 @@ {{- $releaseNamespace := .Release.Namespace -}} {{- $forceReplicas := .Values.deployment.forceReplicas -}} {{- $minAvailable := .Values.deployment.minAvailable -}} -{{- $minAvailablePDB := .Values.pdb.minAvailable -}} {{- $minReplicas := .Values.deployment.minReplicas -}} -{{- $maxReplicas := .Values.deployment.maxReplicas -}} {{- /* Determine if PDB should protect pods. - forceReplicas: exact count, no HPA — protect if > 1 - - HPA: protect if minReplicas > 1 (prd defaults to 2, dev/tst to 1) + - HPA: protect if minReplicas > 1 (default 2) PDB is set to 0% only when effective replicas <= 1. */}} {{- $protected := false -}} @@ -19,7 +17,7 @@ {{- $protected = true -}} {{- end -}} {{- else -}} - {{- $effectiveMinReplicas := int (include "hpa.minReplicas" (dict "env" $env "replicas" $minReplicas)) -}} + {{- $effectiveMinReplicas := int (include "hpa.minReplicas" (dict "replicas" $minReplicas)) -}} {{- if gt $effectiveMinReplicas 1 -}} {{- $protected = true -}} {{- end -}} @@ -42,9 +40,6 @@ spec: {{- /* This is because helm is not able to delete unknown-previous config. */}} {{- /* In this case we set the minAvailable to 0% so it behaves the same way as a PDB does not exist. */}} minAvailable: 0% - {{- else if ($minAvailablePDB) }} - {{- /* PDB.minAvailable takes precedence over deployment.minAvailable */}} - minAvailable: {{ $minAvailablePDB }} {{- else }} minAvailable: {{ $minAvailable | default "50%" }} {{- end }} diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index d07af44..bfda36d 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -38,14 +38,14 @@ tests: asserts: - hasDocuments: count: 1 - - it: HPA minReplicas defaults to 1 in dev + - it: HPA minReplicas defaults to 2 set: deployment: minReplicas: null asserts: - equal: path: spec.minReplicas - value: 1 + value: 2 - it: hpa must have labels set: deployment: diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index a0618ea..0cac37e 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -100,13 +100,12 @@ tests: - equal: path: spec.minAvailable value: "30%" - - it: pdb.minAvailable takes precedence over container.minAvailable + - it: deployment.minAvailable overrides default 50% set: env: prd - pdb: - minAvailable: 25% deployment: minReplicas: 2 + minAvailable: 25% containers: - image: app asserts: @@ -217,10 +216,9 @@ tests: - it: if deployment forceReplicas is 3 with custom minAvailable, must use custom value set: env: prd - pdb: - minAvailable: 33% deployment: forceReplicas: 3 + minAvailable: 33% containers: - image: some asserts: @@ -286,7 +284,7 @@ tests: - equal: path: spec.minAvailable value: "27%" - - it: minAvailable equals zero if no HPA and no replicas set in tst + - it: default minReplicas 2 means PDB is 50% even without explicit config set: env: tst deployment: @@ -294,21 +292,21 @@ tests: asserts: - equal: path: spec.minAvailable - value: "0%" - - it: minAvailable equals zero if no HPA and no replicas set on deployment in tst + value: "50%" + - it: minAvailable equals zero if minReplicas is 1 in tst set: env: tst deployment: - minReplicas: null + minReplicas: 1 asserts: - equal: path: spec.minAvailable value: "0%" - - it: minAvailable equals zero if no HPA and no replicas set in dev + - it: minAvailable equals zero if minReplicas is 1 in dev set: env: dev deployment: - minReplicas: null + minReplicas: 1 asserts: - equal: path: spec.minAvailable diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json index 04bf1d2..8b8d7f6 100644 --- a/charts/common/values.schema.json +++ b/charts/common/values.schema.json @@ -93,7 +93,7 @@ "$ref": "#/properties/container/properties/prometheus" }, "minReplicas": { - "description": "Set the minimum replica count for HPA. Defaults to 1 for sbx/dev/tst and 2 for prd.", + "description": "Set the minimum replica count for HPA. Default: 2.", "type": ["integer", "null"] }, "maxReplicas": { @@ -224,13 +224,7 @@ "type": "object" }, "pdb": { - "additionalProperties": false, - "properties": { - "minAvailable": { - "description": "Set minimum available %, overrides deployment.minAvailable", - "type": ["string", "integer", "null"] - } - }, + "description": "PDB is automatically configured based on minReplicas. Use deployment.minAvailable to override the default 50%.", "type": "object" }, "service": { diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 1c69e8f..2dc650a 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -42,7 +42,8 @@ deployment: volumes: [] # -- Prometheus #prometheus: same as container.prometheus stanza - # -- (int) Set the minimum replica count for HPA. Defaults to 1 for sbx/dev/tst and 2 for prd. + # -- (int) Set the minimum replica count for HPA. + # @default -- 2 minReplicas: # -- (int) Set the max replica count for HPA # @default -- 10 @@ -51,14 +52,14 @@ deployment: forceReplicas: # -- (int) Override pod terminationGracePeriodSeconds (default 30s). terminationGracePeriodSeconds: - # -- (string) Set minimum available % + # -- (string) Set minimum available % for PDB # @default -- 50% minAvailable: - # -- Limit max surge for rolling updates (default 25%). Not in use when using forceReplicas. - # @default -- 25% + # -- Limit max surge for rolling updates. Accepts an integer (pod count) or a string percentage (e.g. "25%"). Not in use when using forceReplicas. + # @default -- 1 maxSurge: - # -- Limit max unavailable for rolling updates (default 25%). Not in use when using forceReplicas. - # @default -- 25% + # -- Limit max unavailable for rolling updates. Accepts an integer (pod count) or a string percentage (e.g. "25%"). Not in use when using forceReplicas. + # @default -- 1 maxUnavailable: # -- Override pod serviceAccountName (default application). # @default -- application @@ -163,10 +164,7 @@ hpa: # maxReplicas: 10 # minReplicas: 2 -pdb: - # -- (string) Set minimum available %, this overrides pdb setting minAvailable in deployment/container - # @default -- 50% - minAvailable: +pdb: {} service: # -- Enable or disable the service From a86fd9f1955e0b348c46205b257c7f48a8e4ccee Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:46:50 +0100 Subject: [PATCH 52/66] fix: update test names and comments to match v2 behavior --- charts/common/tests/hpa_test.yaml | 6 +- charts/common/tests/pdb_test.yaml | 127 ++++++++++-------------------- 2 files changed, 44 insertions(+), 89 deletions(-) diff --git a/charts/common/tests/hpa_test.yaml b/charts/common/tests/hpa_test.yaml index bfda36d..387c42d 100644 --- a/charts/common/tests/hpa_test.yaml +++ b/charts/common/tests/hpa_test.yaml @@ -53,7 +53,7 @@ tests: asserts: - isNotEmpty: path: metadata.labels - - it: must have minReplicas 2 if not set + - it: defaults to minReplicas 2 when not set set: env: prd deployment: @@ -64,7 +64,7 @@ tests: - equal: path: spec.minReplicas value: 2 - - it: use minReplicas from deployment if not set on container + - it: deployment.minReplicas overrides default set: deployment: minReplicas: 4 @@ -143,7 +143,7 @@ tests: - equal: path: spec.metrics[1].pods.target.averageValue value: 100 - - it: Must default to 2 minReplicas in prod, max 10 + - it: defaults to minReplicas 2 and maxReplicas 10 set: env: prd container: diff --git a/charts/common/tests/pdb_test.yaml b/charts/common/tests/pdb_test.yaml index 0cac37e..3f22d19 100644 --- a/charts/common/tests/pdb_test.yaml +++ b/charts/common/tests/pdb_test.yaml @@ -41,14 +41,15 @@ tests: - equal: path: spec.unhealthyPodEvictionPolicy value: AlwaysAllow - - it: must have unhealthyPodEvictionPolicy even when minAvailable is 0% + - it: must have unhealthyPodEvictionPolicy in all environments set: env: dev asserts: - equal: path: spec.unhealthyPodEvictionPolicy value: AlwaysAllow - - it: must use default with 2 replicas or more + # default minReplicas=2 means PDB is always 50% by default + - it: default minAvailable is 50% with default minReplicas 2 set: env: prd deployment: @@ -57,16 +58,22 @@ tests: - equal: path: spec.minAvailable value: "50%" - - it: must use default with 2 maxreplicas or more in prod + - it: default minAvailable is 50% in dev (minReplicas defaults to 2) set: - env: prd - deployment: - maxReplicas: 3 + env: dev + asserts: + - equal: + path: spec.minAvailable + value: "50%" + - it: default minAvailable is 50% in tst (minReplicas defaults to 2) + set: + env: tst asserts: - equal: path: spec.minAvailable value: "50%" - - it: use minAvailable from container if not set on pdb + # deployment.minAvailable overrides the default + - it: deployment.minAvailable overrides default 50% set: env: prd deployment: @@ -76,7 +83,7 @@ tests: - equal: path: spec.minAvailable value: "27%" - - it: use minAvailable from deployment if not set on pdb or container + - it: deployment.minAvailable works with containers list set: env: prd deployment: @@ -88,32 +95,32 @@ tests: - equal: path: spec.minAvailable value: "26%" - - it: check for minAvailable on deployment before container + - it: deployment.minAvailable 25% with containers list set: env: prd deployment: minReplicas: 2 - minAvailable: 30% + minAvailable: 25% containers: - image: app asserts: - equal: path: spec.minAvailable - value: "30%" - - it: deployment.minAvailable overrides default 50% + value: "25%" + - it: deployment.minAvailable 30% with containers list set: env: prd deployment: minReplicas: 2 - minAvailable: 25% + minAvailable: 30% containers: - image: app asserts: - equal: path: spec.minAvailable - value: "25%" - # replicas=1 with HPA enabled: HPA forces min 2 pods, PDB should protect - - it: if replicas is 1 in prd, minAvailable must be 0% (single pod) + value: "30%" + # minReplicas=1: single pod, PDB should be 0% + - it: minAvailable is 0% when minReplicas is 1 in prd set: env: prd deployment: @@ -124,17 +131,7 @@ tests: - equal: path: spec.minAvailable value: "0%" - - it: prd defaults to minAvailable 50% when replicas not set (minReplicas=2) - set: - env: prd - container: - image: some - asserts: - - equal: - path: spec.minAvailable - value: "50%" - # replicas=1 without HPA: truly single pod, PDB should be 0% - - it: if container replicas is 1 in dev without HPA, minAvailable must be 0% + - it: minAvailable is 0% when minReplicas is 1 in dev set: env: dev deployment: @@ -145,19 +142,17 @@ tests: - equal: path: spec.minAvailable value: "0%" - - it: if deployment replicas is 1 in dev without HPA, minAvailable must be 0% + - it: minAvailable is 0% when minReplicas is 1 in tst set: - env: dev + env: tst deployment: minReplicas: 1 - container: - image: some asserts: - equal: path: spec.minAvailable value: "0%" # forceReplicas=1: single pod, PDB should be 0% - - it: if container forceReplicas is set to 1, minAvailable must be 0% + - it: forceReplicas 1 means minAvailable 0% set: env: prd deployment: @@ -168,7 +163,7 @@ tests: - equal: path: spec.minAvailable value: "0%" - - it: if deployment forceReplicas is set to 1, minAvailable must be 0% + - it: forceReplicas 1 with containers list means minAvailable 0% set: env: prd deployment: @@ -180,7 +175,7 @@ tests: path: spec.minAvailable value: "0%" # forceReplicas > 1: multiple pods, PDB should protect - - it: if container forceReplicas is 2, minAvailable must be 50% + - it: forceReplicas 2 means minAvailable 50% set: env: prd deployment: @@ -191,7 +186,7 @@ tests: - equal: path: spec.minAvailable value: "50%" - - it: if deployment forceReplicas is 2, minAvailable must be 50% + - it: forceReplicas 2 with containers list means minAvailable 50% set: env: prd deployment: @@ -202,7 +197,7 @@ tests: - equal: path: spec.minAvailable value: "50%" - - it: if container forceReplicas is 3, minAvailable must be 50% + - it: forceReplicas 3 means minAvailable 50% set: env: prd deployment: @@ -213,7 +208,7 @@ tests: - equal: path: spec.minAvailable value: "50%" - - it: if deployment forceReplicas is 3 with custom minAvailable, must use custom value + - it: forceReplicas 3 with custom deployment.minAvailable set: env: prd deployment: @@ -226,7 +221,7 @@ tests: path: spec.minAvailable value: "33%" # PDB document always exists (helm can't delete previous config) - - it: must have pdb document when forceReplicas is 1 on deployment + - it: PDB document exists even when forceReplicas is 1 set: env: prd deployment: @@ -236,7 +231,7 @@ tests: asserts: - hasDocuments: count: 1 - - it: must use pdb when replicas not set + - it: PDB document exists with custom deployment.minAvailable set: env: prd deployment: @@ -255,8 +250,8 @@ tests: - equal: path: metadata.name value: override - # test env - - it: must use default with 2 maxReplicas or more + # deployment.minAvailable works across environments + - it: deployment.minAvailable overrides default in tst set: env: tst deployment: @@ -265,16 +260,7 @@ tests: - equal: path: spec.minAvailable value: "50%" - - it: must use default with 2 maxreplicas or more in tst - set: - env: tst - deployment: - maxReplicas: 3 - asserts: - - equal: - path: spec.minAvailable - value: "50%" - - it: use minAvailable from container if not set on pdb in tst + - it: deployment.minAvailable custom value in tst set: env: tst deployment: @@ -284,7 +270,7 @@ tests: - equal: path: spec.minAvailable value: "27%" - - it: default minReplicas 2 means PDB is 50% even without explicit config + - it: default minReplicas 2 means PDB is 50% without explicit config in tst set: env: tst deployment: @@ -293,25 +279,7 @@ tests: - equal: path: spec.minAvailable value: "50%" - - it: minAvailable equals zero if minReplicas is 1 in tst - set: - env: tst - deployment: - minReplicas: 1 - asserts: - - equal: - path: spec.minAvailable - value: "0%" - - it: minAvailable equals zero if minReplicas is 1 in dev - set: - env: dev - deployment: - minReplicas: 1 - asserts: - - equal: - path: spec.minAvailable - value: "0%" - - it: minAvailable from container is used when 2 replicas in dev without HPA + - it: deployment.minAvailable custom value in dev set: env: dev deployment: @@ -321,7 +289,7 @@ tests: - equal: path: spec.minAvailable value: "24%" - - it: use minAvailable from container if not set on pdb in dev + - it: deployment.minAvailable custom value in dev with maxReplicas set: env: dev deployment: @@ -331,20 +299,7 @@ tests: - equal: path: spec.minAvailable value: "29%" - # replicas=1 with HPA via maxReplicas in non-prd: HPA active, PDB should protect - - it: if replicas is 1 in dev, minAvailable must be 0% (single pod) - set: - env: dev - deployment: - minReplicas: 1 - container: - image: some - asserts: - - equal: - path: spec.minAvailable - value: "0%" - # replicas > 1 in non-prd - - it: if replicas is 2 without HPA in dev, minAvailable must be 50% + - it: minReplicas 2 in dev means minAvailable 50% set: env: dev deployment: From 5127b8fcecfe9aa932d688d08ebc587328dbb6a2 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:51:40 +0100 Subject: [PATCH 53/66] docs: regenerate README.md files with helm-docs for v2 --- charts/common/Chart.yaml | 12 ++++++--- charts/common/README.md | 53 +++++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/charts/common/Chart.yaml b/charts/common/Chart.yaml index b39de0c..7613c9e 100644 --- a/charts/common/Chart.yaml +++ b/charts/common/Chart.yaml @@ -10,22 +10,26 @@ description: > * Defaults typically match a properly configured Spring Boot project - * Automatic HA with HPA and PDB in `prd` + * Automatic HA with HPA and PDB in all environments (minReplicas: 2 by default) - * Enforces explicit setting of important aspecs such as traffic type + * Enforces explicit setting of important aspects such as traffic type * Rule based safety net, a chart that breaks business rules will fail with a helpful message - * Convention based automatic limit configuration. Cpu is 5x request, and memory is +20%. + * JSON Schema validation catches typos and unknown properties on `helm lint` + + * Compatible with Helm 3 and Helm 4 ## Take full control - * Most properties can be overriden to your specific needs. + * Most properties can be overridden to your specific needs. * Read the values.yaml file to get template documentation. + * See [UPGRADE.md](../../UPGRADE.md) for migration guides between major versions. + ### Fully customize `container.probes.spec` and `hpa.spec` with literal Kubernetes configuration
diff --git a/charts/common/README.md b/charts/common/README.md index 71f6cef..ac067ca 100644 --- a/charts/common/README.md +++ b/charts/common/README.md @@ -1,21 +1,23 @@ # common -![Version: 1.21.1](https://img.shields.io/badge/Version-1.21.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 2.0.0](https://img.shields.io/badge/Version-2.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) A Helm chart for Entur's Kubernetes workloads ## Highlighted features * Defaults typically match a properly configured Spring Boot project -* Automatic HA with HPA and PDB in `prd` +* Automatic HA with HPA and PDB in all environments (minReplicas: 2 by default) * Enforces explicit setting of important aspects such as traffic type * Rule based safety net, a chart that breaks business rules will fail with a helpful message -* Convention based automatic limit configuration. Cpu is 5x request, and memory is +20%. +* JSON Schema validation catches typos and unknown properties on `helm lint` +* Compatible with Helm 3 and Helm 4 ## Take full control * Most properties can be overridden to your specific needs. * Read the values.yaml file to get template documentation. +* See [UPGRADE.md](../../UPGRADE.md) for migration guides between major versions. ### Fully customize `container.probes.spec` and `hpa.spec` with literal Kubernetes configuration
@@ -70,6 +72,7 @@ common: | Key | Type | Default | Description | |-----|------|---------|-------------| | app | string | `nil` | Application name, typically on the form `the-application` | +| appId | string | `nil` | App ID from GoogleCloudApplication `metadata.id`. Max 10 alphanumeric characters. See https://github.com/entur/tf-gcp-apps/blob/main/docs/manifests/GoogleCloudApplication.md | | configmap.data | object | `{}` | Set data for configmap | | configmap.enabled | bool | false | Enable or disable the configmap | | container.args | string | `nil` | Optionally set the arguments that will be passed to the command, e.g. ["arg1","arg2"]. | @@ -78,13 +81,10 @@ common: | container.cpuLimit | float | `5 x cpu` | Set CPU limit without any unit. 100m is 0.1 | | container.env | list | `[]` | Specify `env` entries for your container | | container.envFrom | list | `[]` | Attach secrets and configmaps to your `env` | -| container.forceReplicas | int | `nil` | Force replicas disables autoscaling and PDB, if set to 1 it will use Recreate strategy | | container.labels | object | `{}` | Add labels to your pods | | container.lifecycle | object | `{}` | Set pod lifecycle handlers | -| container.maxReplicas | int | `nil` | Set the maxReplicas for your HPA | | container.memory | int | 16 | Set memory without any unit, `Mi` is inferred | -| container.memoryLimit | string | `1.2 * memory` | Set memory limit without any unit, `Mi` is inferred | -| container.minAvailable | string | 50% | Set the minimal available replicas, used by PDB | +| container.memoryLimit | string | `nil` | @deprecated memoryLimit is removed. Memory limit is now always equal to memory request. Use `container.memory` instead. | | container.name | string | .app | Name of container | | container.probes.enabled | bool | `true` | Enable or disable probes | | container.probes.liveness.failureThreshold | int | 6 | Set the failure threshold | @@ -102,65 +102,72 @@ common: | container.probes.spec | string | `nil` | Override with k8s spec for custom probes | | container.probes.startup.failureThreshold | int | 300 | Set the failure threshold | | container.probes.startup.grpc | string | `nil` | Specify grpc probes for a port. Needs `port` child stanza | +| container.probes.startup.path | string | `nil` | Set the path for startup probe. If set, uses httpGet instead of tcpSocket. Useful when startup includes long-running tasks like cache warming. | | container.probes.startup.periodSeconds | int | 1 | Set the period of checking | | container.prometheus.enabled | bool | `false` | Enable or disable Prometheus | | container.prometheus.path | string | /actuator/prometheus | Set the path for scraping metrics | | container.prometheus.port | int | service.internalPort | Set the port for prometheus scraping | -| container.replicas | int | `nil` | Set the target replica count, if equal to 1 the PDB minAvailable will be set to 100% | -| container.terminationGracePeriodSeconds | int | `nil` | Override pod terminationGracePeriodSeconds (default 30s). | | container.uid | int | 1000 | Set the uid that your user runs with | | container.volumeMounts | list | `[]` | Configure volume mounts, accepts kubernetes syntax | | container.volumes | list | `[]` | Configure volume, accepts kubernetes syntax | | containers | list | `[]` | Takes a list of `container` entries, you must add a `name` field for each entry | | cron.activeDeadlineSeconds | int | `nil` | Active deadline seconds for the job, default 24 hours (86300s) | | cron.concurrencyPolicy | string | Forbid | Concurrency policy | -| cron.enabled | bool | `false` | Generate a CronJob resource. Requires `cron.schedule` to be set. Set `deployment.enabled: false` if you only want a CronJob. | +| cron.enabled | bool | `false` | Enable or disable the cron job | | cron.failedJobsHistoryLimit | int | 1 | Failed jobs history limit | | cron.labels | object | `{}` | Add labels to your pods | | cron.restartPolicy | string | OnFailure | Override pod restartPolicy (default OnFailure). | -| cron.schedule | string | `nil` | Required crontab schedule `* * * * *` | +| cron.schedule | string | `nil` | Required crontab schedule `* * * * * *` | | cron.serviceAccountName | string | application | Override pod serviceAccountName (default application). | | cron.successfulJobsHistoryLimit | int | 1 | Successful jobs history limit | | cron.suspend | string | false | Suspend flag | | cron.terminationGracePeriodSeconds | int | false | Override pod terminationGracePeriodSeconds (default 30s). | | cron.volumes | list | `[]` | Configure volume, accepts kubernetes syntax | -| deployment.enabled | bool | `true` | Generate a Deployment resource | -| deployment.forceReplicas | int | `nil` | Force replicas disables autoscaling and PDB, if set to 1 it will use Recreate strategy | +| deployment.cpuUtilization | string | 70 | Set the target CPU average utilization (%) for HPA scaling. With startupCPUBoost enabled, 70% is a good default. Without it, 100% may be needed for Java apps with heavy startup CPU usage. | +| deployment.enabled | bool | `true` | Enable or disable the deployment | +| deployment.forceReplicas | int | `nil` | Force a fixed replica count, disables HPA and PDB. If set to 1 it will use Recreate strategy. | | deployment.labels | object | `{}` | Add labels to your pods | -| deployment.maxReplicas | string | 10 | Set the max replica count | -| deployment.maxSurge | string | 25% | Limit max surge for rolling updates (default 25%). Not in use when using forceReplicas. | -| deployment.maxUnavailable | string | 25% | Limit max unavailable for rolling updates (default 25%). Not in use when using forceReplicas. | -| deployment.minAvailable | string | 50% | Set minimum available % | +| deployment.maxReplicas | int | 10 | Set the max replica count for HPA | +| deployment.maxSurge | string | 1 | Limit max surge for rolling updates. Accepts an integer (pod count) or a string percentage (e.g. "25%"). Not in use when using forceReplicas. | +| deployment.maxUnavailable | string | 1 | Limit max unavailable for rolling updates. Accepts an integer (pod count) or a string percentage (e.g. "25%"). Not in use when using forceReplicas. | +| deployment.minAvailable | string | 50% | Set minimum available % for PDB | | deployment.minReadySeconds | int | 0 | See https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#progress-deadline-seconds | -| deployment.replicas | string | container.replicas | Set the target replica count | +| deployment.minReplicas | int | 2 | Set the minimum replica count for HPA. | | deployment.serviceAccountName | string | application | Override pod serviceAccountName (default application). | +| deployment.startupCPUBoost.enabled | bool | `false` | Enable GKE Startup CPU Boost to temporarily increase CPU during pod startup. Requires the kube-startup-cpu-boost operator installed in the cluster. Boost is reverted when the pod becomes Ready. When enabled, a CPU limit of 1.3x the CPU request is automatically set (unless `container.cpuLimit` is explicitly configured). | +| deployment.startupCPUBoost.percentageIncrease | int | 50 | Percentage to increase CPU requests during startup | | deployment.terminationGracePeriodSeconds | int | `nil` | Override pod terminationGracePeriodSeconds (default 30s). | | deployment.volumes | list | `[]` | Configure volume, accepts kubernetes syntax | | env | string | `nil` | The current env, override in your `values-kub-ent-$env.yaml` files to `dev`, `tst` or `prd` | | grpc | bool | `false` | Enable gRPC which will add an annotation and use grpc probes | -| hpa.spec | object | `{}` | Custom spec for HPA, inherits `scaleTargetRef` and min/max replicas. ps: Reason why we have set 100% cpu as default is because the java applications are resource hogs during startup. If you have good startupProbe/readinessProbes in place you can lower the cpu average utilization to ie 50/60%. - Or scale on other (custom) metrics. | +| hpa.metrics | list | [] | Additional HPA metrics appended alongside the default CPU metric. Accepts standard `autoscaling/v2` metric entries (Pods, Object, External). Use for scaling on custom metrics from Cloud Monitoring, Prometheus (GMP), or Pub/Sub. When multiple metrics are specified, HPA picks the one demanding the most replicas. | +| hpa.spec | object | `{}` | Full custom spec for HPA, replaces default metrics and min/max replicas. Inherits `scaleTargetRef`. | +| hpa.stabilizationWindowSeconds | int | 120 | Seconds to wait before scaling up after a metric spike. Only applied when startupCPUBoost is disabled, to avoid scaling on startup CPU spikes. Tune this to match your application's typical startup time (e.g. 60s for a fast app, 300s for a heavy Spring Boot app with cache warming). | | ingress.annotations | object | `{}` | Optionally set annotations for the ingress | | ingress.enabled | bool | `true` | Enable or disable the ingress | | ingress.host | string | `nil` | Set the host name, do this in your `values-kub-ent-$env.yaml` files | +| ingress.ingressClassName | string | traefik | Set the IngressClass name. Uses `spec.ingressClassName` (replaces the deprecated `kubernetes.io/ingress.class` annotation). | | ingress.trafficType | string | `nil` | Set the traffic type (`api`,`public` or `http2` for gRPC). Note: changing this value will cause a couple of minutes of downtime while the ingress controller reconciles. | | ingresses | list | `[]` | Specify a list of `ingress` specs | | initContainers | list | `[]` | See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | | labels | object | `{ app shortname team common:version environment }` | Specify additional labels for every resource | -| pdb.minAvailable | string | 50% | Set minimum available %, this overrides pdb setting minAvailable in deployment/container | -| postgres.connectionConfig | string | `nil` | Override name for connection configmap. This must at least contain `INSTANCES`. | +| pdb | object | `{}` | | +| postgres.connectionConfig | string | `nil` | @deprecated connectionConfig is deprecated. Use `postgres.instances` instead to source connection names from Secret Manager via External Secrets. | | postgres.cpu | float | 0.05 | Configure cpu request for proxy | | postgres.cpuLimit | float | `nil` | Configure optional cpu limit for proxy | | postgres.credentialsSecret | string | `nil` | Override name for credentials secret. This must at least contain `PGUSER` and `PGPASSWORD`. | | postgres.enabled | bool | false | Enable or disable the proxy | +| postgres.instances | list | [] | List of Secret Manager keys containing Cloud SQL instance connection names. Supports multiple databases. Each key is mapped to `CSQL_PROXY_INSTANCE_CONNECTION_NAME_N` for the v2 proxy. The secret keys match those created by the `entur/terraform-google-sql-db` module (e.g. `PGINSTANCES`). | | postgres.memory | int | 16 | Configure memory request for proxy without units, `Mi` inferred | -| postgres.memoryLimit | int | 16 | Configure memoryLimit for proxy without units, `Mi` inferred | +| postgres.memoryLimit | float | `nil` | @deprecated memoryLimit is deprecated and will cause a deploy failure if set. Memory limit is now always equal to memory request. Use `postgres.memory` instead. | | releaseName | string | `nil` | Override release name, useful for multiple deployments | | secrets | object | `{}` | Add externalSecret to sync secrets from secret manager | | service.annotations | object | `{}` | Optionally set annotations for the service | | service.enabled | bool | `true` | Enable or disable the service | | service.externalPort | int | 80 | Set the external port for your service | | service.internalPort | int | 8080 | Set the internal port for your service | -| shortname | string | `nil` | `id` for GCP 2.0, typically on the form `theapp`. Max 10 characters | | team | string | `nil` | Your team name, without a `team-` prefix | | vpa.enabled | bool | `true` | Enable Vertical Pod Autoscaler to get resource requirement and limit recommendations | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) From 0edbdaeacb47544af9a0b439b63f688459c9a730 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 10:52:34 +0100 Subject: [PATCH 54/66] docs: update AGENTS.md with helm-docs workflow and v2 conventions --- AGENTS.md | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8aa0962..668f394 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ charts/common/ # The main Helm chart (source of truth) tests/ # helm-unittest test suites tests/values/ # Shared test values values.yaml # Default values (heavily documented) + values.schema.json # JSON Schema for values validation examples/common/ # 7 example charts showing usage patterns fixture/helm/ # Fixture values for template rendering validation .github/workflows/ # CI/CD (PR checks, release, docs generation) @@ -24,7 +25,8 @@ fixture/helm/ # Fixture values for template rendering validation - **`helm`** — render templates, manage dependencies, package charts - **`helm unittest`** — run YAML-based unit tests (plugin: helm-unittest) - **`helm template`** — render and inspect template output with fixture values -- **`helm-docs`** — auto-generate README.md from values.yaml comments +- **`helm lint`** — validate chart structure and values against JSON Schema +- **`helm-docs`** — auto-generate README.md from values.yaml comments and Chart.yaml description - **`gh`** — GitHub CLI for issues, PRs, releases, and CI status ## Development Commands @@ -36,6 +38,9 @@ helm unittest ./charts/common # Run a single test file helm unittest ./charts/common -f tests/pdb_test.yaml +# Lint chart with schema validation +helm lint charts/common -f fixture/helm/values-minimal.yaml + # Render templates with fixture values to verify output helm template charts/common -f fixture/helm/values-minimal.yaml helm template charts/common -f fixture/helm/values-cron.yaml @@ -46,9 +51,9 @@ helm template charts/common -f fixture/helm/values-postgres.yaml helm template test charts/common -f fixture/helm/values-minimal.yaml --show-only templates/pdb.yaml # Render with value overrides (useful for testing specific scenarios) -helm template test charts/common -f fixture/helm/values-minimal.yaml --set env=prd --set deployment.replicas=2 +helm template test charts/common -f fixture/helm/values-minimal.yaml --set env=prd --set deployment.minReplicas=3 -# Regenerate chart documentation (README.md files) +# Regenerate chart documentation (README.md files) — run after changing values.yaml or Chart.yaml helm-docs # Update dependencies for example charts after version bump @@ -78,18 +83,18 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - `docs:` / `ci:` / `chore:` — no version bump ### Chart Design Principles -- Environment-aware defaults: `prd` automatically enables HA (HPA, PDB) -- Resource convention: CPU limit = 5x request, memory limit = 1.2x request +- HPA is always enabled with `minReplicas: 2` by default (use `forceReplicas` to opt out) +- Memory limit always equals memory request - Security: non-root, no privilege escalation, drop all capabilities, seccompProfile RuntimeDefault - Traffic types must be explicit: `api`, `public`, or `http2` - Template helpers are in `charts/common/templates/_helpers.tpl` - Deprecated values use `fail` to give clear migration messages -- Scaling fields (replicas, maxReplicas, forceReplicas, minAvailable) belong under `deployment.*` only — not `container.*` +- Scaling fields (minReplicas, maxReplicas, forceReplicas, minAvailable) belong under `deployment.*` only — not `container.*` - Container-specific fields (cpu, memory, image, probes, env, ports, lifecycle) belong under `container.*` ### Values Patterns - Required fields: `app`, `appId`, `team`, `env`, `container.image` -- Environment values: `dev`, `tst`, `prd` +- Environment values: `sbx`, `dev`, `tst`, `prd` - Single container: use `container:` key - Multiple containers: use `containers:` list - Environment-specific overrides go in `env/values-kub-ent-{dev,tst,prd}.yaml` @@ -97,10 +102,16 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - gRPC: set `grpc: true` — native K8s gRPC probes are used automatically with `service.internalPort` - Custom HPA metrics: use `hpa.metrics` list to add Pods/External/Object metrics alongside default CPU +### Documentation +- `README.md` files in `charts/` and `examples/` are **auto-generated** by `helm-docs` — never edit them manually +- To update documentation: edit `values.yaml` comments (use `# --` prefix for helm-docs) or `Chart.yaml` description, then run `helm-docs` +- After any change to `values.yaml`, `Chart.yaml`, or `values.schema.json`: run `helm-docs` to regenerate README.md files +- `values.schema.json` must be updated manually when adding/removing/renaming values fields + ## CI/CD -- **PR checks**: lint, unit tests, kind cluster install tests across all examples and environments -- **Release**: automated via release-please with semantic versioning; tags like `common-v1.21.1` +- **PR checks**: lint, unit tests (Helm 3 + 4), kind cluster install tests (Helm 3 + 4), example validation (Helm 3 + 4) +- **Release**: automated via release-please with semantic versioning; tags like `common-v2.0.0` - **Docs**: auto-generated on release branches via helm-docs workflow - **Ownership**: `@entur/team-plattform` (see CODEOWNERS) @@ -112,3 +123,6 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - Fixture values in `fixture/helm/` are used for CI template rendering validation - `shortname` is removed — use `appId` (matches GoogleCloudApplication `metadata.id`) - `postgres.connectionConfig` is removed — use `postgres.instances` with Secret Manager keys +- `deployment.replicas` is removed — use `deployment.minReplicas` (HPA controls pod count) +- `container.memoryLimit` is removed — memory limit always equals memory request +- `pdb.minAvailable` is removed — use `deployment.minAvailable` From 5041f2094c02139c76b434f072fb5bb3b15bdf5b Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 11:00:54 +0100 Subject: [PATCH 55/66] ci: use explicit release names to avoid name collision in install tests --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 43948f5..8465afd 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -64,8 +64,8 @@ jobs: - name: Install helm chart run: | - helm install --generate-name --dependency-update --wait --timeout 5m0s charts/common --values fixture/helm/ci/values-ci-tests.yaml - helm install --generate-name --dependency-update --wait --timeout 5m0s charts/common --values fixture/helm/ci/values-ci-cronjob-tests.yaml + helm install ci-deployment --dependency-update --wait --timeout 5m0s charts/common --values fixture/helm/ci/values-ci-tests.yaml + helm install ci-cronjob --dependency-update --wait --timeout 5m0s charts/common --values fixture/helm/ci/values-ci-cronjob-tests.yaml validate-examples: name: examples (${{ matrix.example }}/${{ matrix.env }}/helm ${{ matrix.helm-version }}) From 809ecaa3249fa2fb7ce8c667eaacf71cb39f81e2 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 13:12:09 +0100 Subject: [PATCH 56/66] docs: simplify ingress.class section in UPGRADE.md --- UPGRADE.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index e781f0e..392b1d5 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -117,15 +117,7 @@ common: ### 5. `ingress.class` annotation replaced with `spec.ingressClassName` -The deprecated `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). - -If you need a different ingress class: - -```yaml -common: - ingress: - ingressClassName: nginx -``` +The deprecated `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). No action needed if you use traefik. ### 6. `configmap.toEnv` is removed From c39014fd268983876c79c25e9048d70ac17f6ad7 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Thu, 26 Mar 2026 13:13:18 +0100 Subject: [PATCH 57/66] docs: trim ingress.class section --- UPGRADE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.md b/UPGRADE.md index 392b1d5..4a8ff99 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -117,7 +117,7 @@ common: ### 5. `ingress.class` annotation replaced with `spec.ingressClassName` -The deprecated `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). No action needed if you use traefik. +The deprecated `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). ### 6. `configmap.toEnv` is removed From fd38d4043a25005d7401e267e8535c54c8cf914f Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Mon, 4 May 2026 12:28:17 +0200 Subject: [PATCH 58/66] feat!: postgres secretKeyPrefix integration with Terraform module Replace the split credential model (ExternalSecrets for proxy connection names + Terraform-created K8s secrets for credentials) with a unified approach where secretKeyPrefix is the single contract between Terraform and Helm. Given a prefix (default PG), the chart derives all Secret Manager keys ({prefix}INSTANCES, {prefix}USER, {prefix}PASSWORD) and fetches everything via ExternalSecrets. The simplest case is just `postgres.enabled: true`. Changes: - postgres.instances now takes objects with secretKeyPrefix instead of raw Secret Manager key names - New sql-credentials ExternalSecret fetches {prefix}USER and {prefix}PASSWORD from Secret Manager - Chart generates {prefix}HOST=localhost and {prefix}PORT=5432+index as env vars (no longer fetched from Secret Manager) - Proxy command adds --port=5432 for deterministic port assignment - postgres.termTimeout renamed to postgres.maxSigtermDelay to match the Cloud SQL Proxy v2 flag - Removed v1 compatibility tests and deprecated fields (connectionConfig, memoryLimit) --- UPGRADE.md | 50 ++-- charts/common/README.md | 9 +- charts/common/templates/_helpers.tpl | 24 +- charts/common/templates/sql-proxy-secret.yaml | 6 +- charts/common/tests/cron_test.yaml | 48 +++- charts/common/tests/deployment_test.yaml | 119 ++++++++-- .../deployment_v1_compatibility_test.yaml | 215 ------------------ .../tests/values/deployment-v1-values.yaml | 9 - charts/common/values.schema.json | 30 ++- charts/common/values.yaml | 21 +- examples/common/cronjob/README.md | 14 +- examples/common/grpc-app/README.md | 6 +- examples/common/multi-container/README.md | 6 +- examples/common/multi-deploy/README.md | 79 +++---- examples/common/simple-app/README.md | 6 +- examples/common/typical-backend/README.md | 6 +- examples/common/typical-backend/values.yaml | 6 +- examples/common/typical-frontend/README.md | 8 +- fixture/helm/values-postgres.yaml | 2 +- 19 files changed, 298 insertions(+), 366 deletions(-) delete mode 100644 charts/common/tests/deployment_v1_compatibility_test.yaml delete mode 100644 charts/common/tests/values/deployment-v1-values.yaml diff --git a/UPGRADE.md b/UPGRADE.md index 4a8ff99..90099c0 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -84,36 +84,56 @@ common: memory: 1024 # sets both request and limit ``` -### 4. Cloud SQL Proxy upgraded to v2 +### 4. Cloud SQL Proxy — `secretKeyPrefix` integration -The Cloud SQL Auth Proxy has been upgraded from v1 (1.33.16) to v2 (2.21.2). This changes how database connections are configured. +The postgres integration now uses `secretKeyPrefix` as the single contract with the `entur/terraform-google-sql-db` Terraform module. Given a prefix, the chart derives all Secret Manager key names and fetches everything via External Secrets. Terraform-created Kubernetes secrets are no longer needed. -**`postgres.connectionConfig` is removed.** Use `postgres.instances` instead, which sources instance connection names from Google Secret Manager via External Secrets. +**`postgres.instances` format changed.** Items are now objects with `secretKeyPrefix` instead of raw Secret Manager key names. When `enabled: true` with no `instances`, the chart defaults to `[{secretKeyPrefix: PG}]`. + +**`postgres.connectionConfig`, `postgres.memoryLimit`, and `postgres.termTimeout` are removed.** The `termTimeout` field is replaced by `postgres.maxSigtermDelay` to match the Cloud SQL Proxy v2 flag name. ```yaml # v1 common: postgres: enabled: true - connectionConfig: my-app-psql-connection # Kubernetes ConfigMap with INSTANCES env var + connectionConfig: my-app-psql-connection -# v2 +# v2 (simplest — uses default PG prefix) +common: + postgres: + enabled: true + +# v2 (explicit prefix) +common: + postgres: + enabled: true + instances: + - secretKeyPrefix: PG + +# v2 (multiple instances) common: postgres: enabled: true instances: - - PGINSTANCES # Secret Manager key from entur/terraform-google-sql-db + - secretKeyPrefix: PG + - secretKeyPrefix: ANALYTICS_PG ``` -**Migration steps:** +**What changed:** + +- Credentials (`{prefix}USER`, `{prefix}PASSWORD`) are now fetched from Secret Manager via External Secrets, not from a Terraform-created Kubernetes secret. +- The chart generates `{prefix}HOST=localhost` and `{prefix}PORT=5432+index` as environment variables. +- A new `sql-credentials` ExternalSecret is created alongside the existing `sql-proxy` ExternalSecret. +- `credentialsSecret` still works as an escape hatch for custom credential sources. +- `postgres.maxSigtermDelay` replaces `postgres.termTimeout` — controls the delay before the proxy begins shutdown after SIGTERM (default `30s`). -1. The `entur/terraform-google-sql-db` Terraform module already stores `PGINSTANCES` in Secret Manager. Verify it exists. -2. Replace `connectionConfig` with `instances: [PGINSTANCES]` in your values. -3. For multiple databases, list all instance keys: `instances: [PGINSTANCES, ANALYTICS_PGINSTANCES]`. -4. Remove any manual Kubernetes ConfigMaps that were providing the `INSTANCES` env var. -5. Optionally set `create_kubernetes_resources: false` in your Terraform module to stop creating the now-unused ConfigMap. +**Migration steps:** -**`postgres.memoryLimit` is removed.** Memory limit is now always equal to memory request. Use `postgres.memory` to set both. +1. Replace `instances: [PGINSTANCES]` with `instances: [{secretKeyPrefix: PG}]`, or simply use `enabled: true` for the default `PG` prefix. +2. Ensure `{prefix}USER`, `{prefix}PASSWORD`, and `{prefix}INSTANCES` exist in Secret Manager (the `entur/terraform-google-sql-db` module creates these). +3. Optionally set `create_kubernetes_resources: false` in your Terraform module — the chart no longer uses Terraform-created Kubernetes secrets. +4. For multiple databases, list each Terraform module's `secret_key_prefix` as a separate entry in `instances`. ### 5. `ingress.class` annotation replaced with `spec.ingressClassName` @@ -181,6 +201,7 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl ### Cloud SQL Proxy v2 features - Prometheus metrics exposed on port 9801 (`/metrics`). - Support for multiple databases via `postgres.instances` list. +- `postgres.maxSigtermDelay` — configurable shutdown delay (default `30s`). ## Quick Migration Checklist @@ -191,7 +212,8 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl - [ ] Replace `container.minAvailable` → `deployment.minAvailable` - [ ] Replace `container.terminationGracePeriodSeconds` → `deployment.terminationGracePeriodSeconds` - [ ] Remove `container.memoryLimit` / `postgres.memoryLimit` — set `memory` to the value you need -- [ ] Replace `postgres.connectionConfig` with `postgres.instances: [PGINSTANCES]` (if using postgres) +- [ ] Replace `postgres.connectionConfig` with `postgres.enabled: true` or `postgres.instances: [{secretKeyPrefix: PG}]` +- [ ] Replace `postgres.termTimeout` with `postgres.maxSigtermDelay` (if set) - [ ] If using gRPC: remove explicit `probes.*.grpc.port` settings (now defaults to `service.internalPort`) - [ ] Update `Chart.yaml` dependency version to v2 - [ ] Run `helm dependency update` diff --git a/charts/common/README.md b/charts/common/README.md index ac067ca..c8b6ee6 100644 --- a/charts/common/README.md +++ b/charts/common/README.md @@ -152,14 +152,13 @@ common: | initContainers | list | `[]` | See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | | labels | object | `{ app shortname team common:version environment }` | Specify additional labels for every resource | | pdb | object | `{}` | | -| postgres.connectionConfig | string | `nil` | @deprecated connectionConfig is deprecated. Use `postgres.instances` instead to source connection names from Secret Manager via External Secrets. | | postgres.cpu | float | 0.05 | Configure cpu request for proxy | | postgres.cpuLimit | float | `nil` | Configure optional cpu limit for proxy | -| postgres.credentialsSecret | string | `nil` | Override name for credentials secret. This must at least contain `PGUSER` and `PGPASSWORD`. | -| postgres.enabled | bool | false | Enable or disable the proxy | -| postgres.instances | list | [] | List of Secret Manager keys containing Cloud SQL instance connection names. Supports multiple databases. Each key is mapped to `CSQL_PROXY_INSTANCE_CONNECTION_NAME_N` for the v2 proxy. The secret keys match those created by the `entur/terraform-google-sql-db` module (e.g. `PGINSTANCES`). | +| postgres.credentialsSecret | string | `nil` | Override the Kubernetes secret name for credentials. Bypasses the ExternalSecret for credentials; the proxy ExternalSecret is still created. The secret must contain the expected env vars (e.g. `PGUSER`, `PGPASSWORD`). | +| postgres.enabled | bool | false | Enable or disable the Cloud SQL proxy v2 sidecar | +| postgres.instances | list | [] | List of database connections keyed by Terraform `secret_key_prefix`. Each entry derives Secret Manager keys: `{prefix}INSTANCES`, `{prefix}USER`, `{prefix}PASSWORD`. The chart generates `{prefix}HOST=localhost` and `{prefix}PORT=5432+index`. When empty and `enabled: true`, defaults to `[{secretKeyPrefix: PG}]`. | +| postgres.maxSigtermDelay | string | 30s | Override the max-sigterm-delay for the Cloud SQL Proxy. Adds a delay before the proxy begins shutdown after receiving SIGTERM, useful for allowing load balancers to deregister the pod. | | postgres.memory | int | 16 | Configure memory request for proxy without units, `Mi` inferred | -| postgres.memoryLimit | float | `nil` | @deprecated memoryLimit is deprecated and will cause a deploy failure if set. Memory limit is now always equal to memory request. Use `postgres.memory` instead. | | releaseName | string | `nil` | Override release name, useful for multiple deployments | | secrets | object | `{}` | Add externalSecret to sync secrets from secret manager | | service.annotations | object | `{}` | Optionally set annotations for the service | diff --git a/charts/common/templates/_helpers.tpl b/charts/common/templates/_helpers.tpl index 3f7c282..5084b2d 100644 --- a/charts/common/templates/_helpers.tpl +++ b/charts/common/templates/_helpers.tpl @@ -74,13 +74,26 @@ resources: {{- end }} {{- define "environment" }} +{{- $postgresInstances := list }} +{{- if .postgres.enabled }} + {{- $postgresInstances = .postgres.instances | default list }} + {{- if eq (len $postgresInstances) 0 }} + {{- $postgresInstances = list (dict "secretKeyPrefix" "PG") }} + {{- end }} +{{- end }} env: - name: COMMON_ENV value: {{ .envLabel }} + {{- range $i, $inst := $postgresInstances }} + - name: {{ $inst.secretKeyPrefix }}HOST + value: "localhost" + - name: {{ $inst.secretKeyPrefix }}PORT + value: "{{ $inst.port | default (add 5432 $i) }}" + {{- end }} {{- if .env }} {{- toYaml .env | nindent 2 }} {{ end }} -{{- if or .envFrom .configmap.enabled .postgres.enabled .secrets}} +{{- if or .envFrom .configmap.enabled (gt (len $postgresInstances) 0) .secrets}} envFrom: {{- if .envFrom }} {{- toYaml .envFrom | nindent 2 }} @@ -89,12 +102,12 @@ envFrom: - configMapRef: name: {{ .releaseName }} {{- end }} - {{- if .postgres.enabled }} + {{- if gt (len $postgresInstances) 0 }} - secretRef: {{- if .postgres.credentialsSecret }} name: {{ .postgres.credentialsSecret }} {{- else }} - name: {{ .app }}-psql-credentials + name: {{ .releaseName }}-sql-credentials {{- end }} {{- end }} {{- if .secrets }} @@ -162,7 +175,7 @@ livenessProbe: command: - "/cloud-sql-proxy" - "--structured-logs" - - "--max-sigterm-delay={{ .postgres.termTimeout | default "30s" }}" + - "--max-sigterm-delay={{ .postgres.maxSigtermDelay | default "30s" }}" - "--http-port=9801" - "--prometheus" - "--port=5432" @@ -180,9 +193,6 @@ livenessProbe: drop: ["ALL"] seccompProfile: type: RuntimeDefault - {{- if .postgres.memoryLimit }} - {{- fail "postgres.memoryLimit is deprecated. Memory limit is now always equal to memory request. Remove memoryLimit and use postgres.memory instead." }} - {{- end }} resources: limits: {{- if .postgres.cpuLimit }} diff --git a/charts/common/templates/sql-proxy-secret.yaml b/charts/common/templates/sql-proxy-secret.yaml index 5a2ad52..8968023 100644 --- a/charts/common/templates/sql-proxy-secret.yaml +++ b/charts/common/templates/sql-proxy-secret.yaml @@ -6,7 +6,7 @@ {{- if $postgres.enabled }} {{- $instances := $postgres.instances | default list -}} {{- if eq (len $instances) 0 }} - {{- fail "postgres.instances is required when postgres.enabled is true. Provide a list of Secret Manager keys containing instance connection names (e.g. [PGINSTANCES])." }} + {{- $instances = list (dict "secretKeyPrefix" "PG") }} {{- end }} {{- /* YAML Spec */}} apiVersion: external-secrets.io/v1 @@ -21,11 +21,11 @@ metadata: {{- include "annotations" $chart |trim| nindent 4 }} spec: data: - {{- range $i, $secretKey := $instances }} + {{- range $i, $inst := $instances }} - remoteRef: conversionStrategy: Default decodingStrategy: None - key: {{ $secretKey }} + key: {{ $inst.secretKeyPrefix }}INSTANCES version: latest secretKey: CSQL_PROXY_INSTANCE_CONNECTION_NAME_{{ $i }} {{- end }} diff --git a/charts/common/tests/cron_test.yaml b/charts/common/tests/cron_test.yaml index 6866ca4..92b6956 100644 --- a/charts/common/tests/cron_test.yaml +++ b/charts/common/tests/cron_test.yaml @@ -236,4 +236,50 @@ tests: asserts: - equal: path: spec.jobTemplate.spec.activeDeadlineSeconds - value: 1200 \ No newline at end of file + value: 1200 + - it: must enable sidecar if postgres enabled + set: + postgres: + enabled: true + asserts: + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].name + value: rudder-test-sql-proxy + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].envFrom[0].secretRef.name + value: rudder-test-sql-proxy + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].command + content: "--port=5432" + - it: must mount sql-credentials envFrom if postgres enabled + set: + postgres: + enabled: true + asserts: + - equal: + path: spec.jobTemplate.spec.template.spec.containers[1].envFrom[0].secretRef.name + value: rudder-test-sql-credentials + - it: generates PGHOST and PGPORT env vars in cronjob + set: + postgres: + enabled: true + asserts: + - contains: + path: spec.jobTemplate.spec.template.spec.containers[1].env + content: + name: PGHOST + value: "localhost" + - contains: + path: spec.jobTemplate.spec.template.spec.containers[1].env + content: + name: PGPORT + value: "5432" + - it: credentialsSecret override works in cronjob + set: + postgres: + enabled: true + credentialsSecret: my-cron-creds + asserts: + - equal: + path: spec.jobTemplate.spec.template.spec.containers[1].envFrom[0].secretRef.name + value: my-cron-creds \ No newline at end of file diff --git a/charts/common/tests/deployment_test.yaml b/charts/common/tests/deployment_test.yaml index 70496ce..de06dca 100644 --- a/charts/common/tests/deployment_test.yaml +++ b/charts/common/tests/deployment_test.yaml @@ -259,22 +259,12 @@ tests: postgres: enabled: true instances: - - PGINSTANCES + - secretKeyPrefix: PG credentialsSecret: my-secret asserts: - equal: path: spec.template.spec.containers[1].envFrom[0].secretRef.name value: my-secret - - it: connectionConfig is deprecated - set: - postgres: - enabled: true - instances: - - PGINSTANCES - connectionConfig: my-config - asserts: - - failedTemplate: - errorMessage: "postgres.connectionConfig is deprecated. Use postgres.instances instead. See migration guide for Cloud SQL Proxy v2." - it: command use correct value set: containers: @@ -447,7 +437,7 @@ tests: postgres: enabled: true instances: - - PGINSTANCES + - secretKeyPrefix: PG cpu: 0.1 asserts: - notExists: @@ -521,18 +511,14 @@ tests: set: postgres: enabled: true - instances: - - PGINSTANCES asserts: - equal: path: spec.template.spec.containers[1].envFrom[0].secretRef.name - value: rudder-test-psql-credentials + value: rudder-test-sql-credentials - it: must enable sidecar if postgres enabled set: postgres: enabled: true - instances: - - PGINSTANCES asserts: - isNotEmpty: path: spec.template.spec.containers[1] @@ -553,8 +539,6 @@ tests: enabled: false postgres: enabled: true - instances: - - PGINSTANCES asserts: - isNotEmpty: path: spec.template.spec.containers[2] @@ -569,7 +553,7 @@ tests: postgres: enabled: true instances: - - PGINSTANCES + - secretKeyPrefix: PG cpu: 0.1 cpuLimit: 1 asserts: @@ -579,6 +563,101 @@ tests: - equal: path: spec.template.spec.containers[0].resources.requests.cpu value: "0.1" + - it: generates PGHOST and PGPORT env vars for default instance + set: + postgres: + enabled: true + asserts: + - contains: + path: spec.template.spec.containers[1].env + content: + name: PGHOST + value: "localhost" + - contains: + path: spec.template.spec.containers[1].env + content: + name: PGPORT + value: "5432" + - it: generates sequential ports for multiple instances + set: + postgres: + enabled: true + instances: + - secretKeyPrefix: PG + - secretKeyPrefix: ANALYTICS_PG + asserts: + - contains: + path: spec.template.spec.containers[1].env + content: + name: PGPORT + value: "5432" + - contains: + path: spec.template.spec.containers[1].env + content: + name: ANALYTICS_PGPORT + value: "5433" + - contains: + path: spec.template.spec.containers[1].env + content: + name: ANALYTICS_PGHOST + value: "localhost" + - it: port override works for same-instance additional user + set: + postgres: + enabled: true + instances: + - secretKeyPrefix: PG + - secretKeyPrefix: READONLY_PG + port: 5432 + asserts: + - contains: + path: spec.template.spec.containers[1].env + content: + name: PGPORT + value: "5432" + - contains: + path: spec.template.spec.containers[1].env + content: + name: READONLY_PGPORT + value: "5432" + - it: proxy command includes --port=5432 + set: + postgres: + enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].command + content: "--port=5432" + - it: prometheus sql-proxy annotations when postgres enabled + set: + postgres: + enabled: true + asserts: + - equal: + path: spec.template.metadata.annotations["prometheus.io/scrape-sql-proxy"] + value: "true" + - equal: + path: spec.template.metadata.annotations["prometheus.io/sql-proxy-port"] + value: "9801" + - equal: + path: spec.template.metadata.annotations["prometheus.io/sql-proxy-path"] + value: "/metrics" + - it: no prometheus sql-proxy annotations when postgres disabled + asserts: + - notExists: + path: spec.template.metadata.annotations["prometheus.io/scrape-sql-proxy"] + - it: no PGHOST or PGPORT env vars when postgres disabled + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: PGHOST + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: PGPORT + any: true - it: has common runtime env set from env label asserts: - equal: diff --git a/charts/common/tests/deployment_v1_compatibility_test.yaml b/charts/common/tests/deployment_v1_compatibility_test.yaml deleted file mode 100644 index 39bbf83..0000000 --- a/charts/common/tests/deployment_v1_compatibility_test.yaml +++ /dev/null @@ -1,215 +0,0 @@ -suite: test v1 compatibility -values: - - ./values/deployment-v1-values.yaml -templates: - - deployment.yaml -tests: - - it: must have labels - set: - env: dev - labels: - custom: label - container: - image: img - labels: - version: 1 - asserts: - - isNotEmpty: - path: metadata.labels - - isNotEmpty: - path: metadata.labels.environment - - isNotEmpty: - path: metadata.labels.custom - - isNotEmpty: - path: spec.template.metadata.labels.common - - it: has common runtime env set from env label - asserts: - - equal: - path: spec.template.spec.containers[0].env[0].value - value: dev - - it: must add to env if listed - set: - container: - image: img - env: - - name: FOO - value: bar - asserts: - - equal: - path: spec.template.spec.containers[0].env[1].value - value: bar - - it: must mount envFrom if configmap is enabled - release: - name: testsuite - set: - configmap: - enabled: true - data: - FOO: bar - asserts: - - equal: - path: spec.template.spec.containers[0].envFrom[0].configMapRef.name - value: testsuite - - it: cpu limit can be set - set: - container: - image: img - cpu: "0.1" - cpuLimit: 1 - asserts: - - equal: - path: spec.template.spec.containers[0].resources.limits.cpu - value: "1" - - equal: - path: spec.template.spec.containers[0].resources.requests.cpu - value: "0.1" - - it: cpu limit can be skipped - set: - container: - image: img - cpu: 0.1 - asserts: - - notExists: - path: spec.template.spec.containers[0].resources.limits.cpu - - equal: - path: spec.template.spec.containers[0].resources.requests.cpu - value: "0.1" - - it: memory limit equals memory request - set: - container: - image: img - memory: 256 - asserts: - - equal: - path: spec.template.spec.containers[0].resources.limits.memory - value: 256Mi - - equal: - path: spec.template.spec.containers[0].resources.requests.memory - value: 256Mi - - it: must enable prometheus if enabled - set: - container: - image: img - prometheus: - enabled: true - asserts: - - equal: - path: spec.template.metadata.annotations["prometheus.io/scrape"] - value: "true" - - equal: - path: spec.template.metadata.annotations["prometheus.io/path"] - value: "/actuator/prometheus" - - equal: - path: spec.template.metadata.annotations["prometheus.io/port"] - value: "8080" - - it: must mount envFrom if postgres enabled - set: - postgres: - enabled: true - instances: - - PGINSTANCES - asserts: - - equal: - path: spec.template.spec.containers[1].envFrom[0].secretRef.name - value: testsuite-psql-credentials - - it: must enable sidecar if postgres enabled - set: - postgres: - enabled: true - instances: - - PGINSTANCES - asserts: - - isNotEmpty: - path: spec.template.spec.containers[1] - - equal: - path: spec.template.spec.containers[0].envFrom[0].secretRef.name - value: RELEASE-NAME-sql-proxy - - it: postgres cpu limit can be overridden - set: - postgres: - enabled: true - instances: - - PGINSTANCES - cpu: "0.1" - cpuLimit: "1" - asserts: - - equal: - path: spec.template.spec.containers[0].resources.limits.cpu - value: "1" - - equal: - path: spec.template.spec.containers[0].resources.requests.cpu - value: "0.1" - - it: postgres cpu limit can be cleared - set: - postgres: - enabled: true - instances: - - PGINSTANCES - cpu: 0.1 - asserts: - - notExists: - path: spec.template.spec.containers[1].resources.limits.cpu - - it: replicas field is skipped when HPA is enabled - set: - env: dev - deployment: - minReplicas: 3 - container: - image: testimg - asserts: - - notExists: - path: spec.replicas - - equal: - path: spec.strategy.type - value: RollingUpdate - - it: must use 1 replica if forceReplicas is 1 and recreate - set: - env: prd - deployment: - forceReplicas: 1 - container: - image: some - asserts: - - equal: - path: spec.replicas - value: 1 - - equal: - path: spec.strategy.type - value: Recreate - - it: must use 3 replica if forceReplicas is 3 - set: - env: prd - deployment: - forceReplicas: 3 - container: - image: some - asserts: - - equal: - path: spec.replicas - value: 3 - - equal: - path: spec.strategy.type - value: RollingUpdate - - it: must adopt for gRPC with native probes - set: - grpc: true - container: - image: img - asserts: - - isNotEmpty: - path: spec.template.spec.containers[0].livenessProbe.grpc - - it: terminationGracePeriodSeconds use correct value - set: - deployment: - terminationGracePeriodSeconds: 60 - asserts: - - equal: - path: spec.template.spec.terminationGracePeriodSeconds - value: 60 - - it: spec does not contain terminationGracePeriodSeconds if missing from values - set: - container: - image: img - asserts: - - notExists: - path: spec.template.spec.terminationGracePeriodSeconds diff --git a/charts/common/tests/values/deployment-v1-values.yaml b/charts/common/tests/values/deployment-v1-values.yaml deleted file mode 100644 index cc7a078..0000000 --- a/charts/common/tests/values/deployment-v1-values.yaml +++ /dev/null @@ -1,9 +0,0 @@ -env: dev -app: testsuite -appId: tstsut -team: common -ingress: - host: test.dev.entur.io - trafficType: public -container: - image: img diff --git a/charts/common/values.schema.json b/charts/common/values.schema.json index 8b8d7f6..0d9fe8d 100644 --- a/charts/common/values.schema.json +++ b/charts/common/values.schema.json @@ -442,21 +442,31 @@ "description": "Configure memory request for proxy without units, Mi inferred", "type": "integer" }, - "memoryLimit": { - "description": "Deprecated. Will cause a deploy failure if set.", - "type": ["integer", "null"] - }, "instances": { - "description": "List of Secret Manager keys containing Cloud SQL instance connection names", - "items": { "type": "string" }, + "description": "List of database connections keyed by Terraform secret_key_prefix", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["secretKeyPrefix"], + "properties": { + "secretKeyPrefix": { + "description": "Terraform secret_key_prefix. Derives keys: {prefix}INSTANCES, {prefix}USER, {prefix}PASSWORD", + "type": "string" + }, + "port": { + "description": "Override auto-assigned proxy listen port (default: 5432 + index)", + "type": "integer" + } + } + }, "type": "array" }, - "connectionConfig": { - "description": "Deprecated. Use postgres.instances instead.", + "credentialsSecret": { + "description": "Override K8s secret for credentials. Bypasses ExternalSecret for credentials.", "type": ["string", "null"] }, - "credentialsSecret": { - "description": "Override name for credentials secret. Must contain PGUSER and PGPASSWORD.", + "maxSigtermDelay": { + "description": "Override the max-sigterm-delay for the Cloud SQL Proxy (e.g. 30s, 5m).", "type": ["string", "null"] } }, diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 06a800a..6025226 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -293,7 +293,7 @@ container: initContainers: [] postgres: - # -- Enable or disable the proxy + # -- Enable or disable the Cloud SQL proxy v2 sidecar # @default -- false enabled: false # -- Configure cpu request for proxy @@ -304,22 +304,17 @@ postgres: # -- Configure memory request for proxy without units, `Mi` inferred # @default -- 16 memory: 16 - # -- (float) @deprecated memoryLimit is deprecated and will cause a deploy failure if set. Memory limit is now always equal to memory request. Use `postgres.memory` instead. - memoryLimit: - # -- List of Secret Manager keys containing Cloud SQL instance connection names. Supports multiple databases. Each key is mapped to `CSQL_PROXY_INSTANCE_CONNECTION_NAME_N` for the v2 proxy. The secret keys match those created by the `entur/terraform-google-sql-db` module (e.g. `PGINSTANCES`). + # -- List of database connections keyed by Terraform `secret_key_prefix`. Each entry derives Secret Manager keys: `{prefix}INSTANCES`, `{prefix}USER`, `{prefix}PASSWORD`. The chart generates `{prefix}HOST=localhost` and `{prefix}PORT=5432+index`. When empty and `enabled: true`, defaults to `[{secretKeyPrefix: PG}]`. # @default -- [] instances: [] - # - PGINSTANCES - # - ANALYTICS_PGINSTANCES - # -- @deprecated connectionConfig is deprecated. Use `postgres.instances` instead to source connection names from Secret Manager via External Secrets. - connectionConfig: - # -- Override name for credentials secret. This must at least contain `PGUSER` and `PGPASSWORD`. + # - secretKeyPrefix: PG + # - secretKeyPrefix: ANALYTICS_PG + # port: 6000 + # -- Override the Kubernetes secret name for credentials. Bypasses the ExternalSecret for credentials; the proxy ExternalSecret is still created. The secret must contain the expected env vars (e.g. `PGUSER`, `PGPASSWORD`). credentialsSecret: - # -- Override the term_timeout for the Cloud SQL Proxy. Controls how long the proxy waits for - # existing connections to close after receiving SIGTERM before force-closing them. - # Increase this if your app runs long-lived jobs that need the database during pod termination. + # -- Override the max-sigterm-delay for the Cloud SQL Proxy. Adds a delay before the proxy begins shutdown after receiving SIGTERM, useful for allowing load balancers to deregister the pod. # @default -- 30s - termTimeout: + maxSigtermDelay: configmap: # -- Enable or disable the configmap diff --git a/examples/common/cronjob/README.md b/examples/common/cronjob/README.md index d21ca8c..c76ece6 100644 --- a/examples/common/cronjob/README.md +++ b/examples/common/cronjob/README.md @@ -2,24 +2,30 @@ ![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for Entur CronJob workloads (no Deployment, Service, or Ingress). +A Helm chart for Entur CronJob workloads ## Requirements | Repository | Name | Version | |------------|------|---------| -| https://entur.github.io/helm-charts | common | 1.21.1 | +| https://entur.github.io/helm-charts | common | 2.0.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | common.app | string | `"my-cronjob"` | | +| common.appId | string | `"mycron"` | | +| common.container.args[0] | string | `"-c"` | | +| common.container.args[1] | string | `"echo 'Hello from CronJob'"` | | +| common.container.command[0] | string | `"/bin/sh"` | | | common.container.image | string | `"<+artifacts.primary.image>"` | | | common.cron.enabled | bool | `true` | | -| common.cron.schedule | string | `"0 */6 * * *"` | Runs every 6 hours | +| common.cron.schedule | string | `"0 */6 * * *"` | | | common.deployment.enabled | bool | `false` | | | common.ingress.enabled | bool | `false` | | | common.service.enabled | bool | `false` | | -| common.shortname | string | `"mycron"` | | | common.team | string | `"example"` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/examples/common/grpc-app/README.md b/examples/common/grpc-app/README.md index 7e836e7..9e23655 100644 --- a/examples/common/grpc-app/README.md +++ b/examples/common/grpc-app/README.md @@ -8,18 +8,20 @@ A Helm chart for basic Entur deployments using gRPC | Repository | Name | Version | |------------|------|---------| -| https://entur.github.io/helm-charts | common | 1.21.1 | +| https://entur.github.io/helm-charts | common | 2.0.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | common.app | string | `"grpc-app"` | | +| common.appId | string | `"grpcapp"` | | | common.container.image | string | `"<+artifacts.primary.image>"` | | | common.grpc | bool | `true` | | | common.ingress.enabled | bool | `true` | | | common.ingress.trafficType | string | `"http2"` | | | common.service.annotations."entur.no/internal-http2" | string | `"true"` | | -| common.shortname | string | `"grpcapp"` | | | common.team | string | `"example"` | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/examples/common/multi-container/README.md b/examples/common/multi-container/README.md index ad10689..2ce599e 100644 --- a/examples/common/multi-container/README.md +++ b/examples/common/multi-container/README.md @@ -8,13 +8,14 @@ A Helm chart for multiple containers | Repository | Name | Version | |------------|------|---------| -| https://entur.github.io/helm-charts | common | 1.21.1 | +| https://entur.github.io/helm-charts | common | 2.0.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | common.app | string | `"multi-container"` | | +| common.appId | string | `"multcont"` | | | common.configmap.data.APP1CONF | string | `"yes"` | | | common.configmap.enabled | bool | `true` | | | common.containers[0].cpu | float | `1.1` | | @@ -58,6 +59,7 @@ A Helm chart for multiple containers | common.service.ports[1].port | int | `5001` | | | common.service.ports[1].protocol | string | `"TCP"` | | | common.service.ports[1].targetPort | int | `6001` | | -| common.shortname | string | `"multcont"` | | | common.team | string | `"example"` | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/examples/common/multi-deploy/README.md b/examples/common/multi-deploy/README.md index de71a67..6cee818 100644 --- a/examples/common/multi-deploy/README.md +++ b/examples/common/multi-deploy/README.md @@ -2,58 +2,39 @@ ![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm Common Chart example where we deploy the same container image to two different deployments with different environment variables. - -This way the application code can pick up the environment variable (CONTAINER_ROLE) and act accordingly. - -In the example - one is a basic rest api, and the other is a kafka queue consumer. - -## GitHub Actions CD - -When using Enturs shared [gha-helm](https://github.com/entur/gha-helm/blob/main/README-deploy.md) reusable workflow we also need define the `image:` path being replaced during deploy. - -```yaml -helm-deploy: - uses: entur/gha-helm/.github/workflows/deploy.yml@v1 - with: - environment: dev - image: amazing-app:2.3.1 - image_set_path: "multi-1.container.image,multi-2.container.image" - secrets: inherit -``` +A Helm chart for basic Entur deployments ## Requirements -| Repository | Name | Version | -| ----------------------------------- | --------------- | ------- | -| https://entur.github.io/helm-charts | multi-1(common) | 1.21.1 | -| https://entur.github.io/helm-charts | multi-2(common) | 1.21.1 | +| Repository | Name | Version | +|------------|------|---------| +| https://entur.github.io/helm-charts | multi-1(common) | 2.0.0 | +| https://entur.github.io/helm-charts | multi-2(common) | 2.0.0 | ## Values -| Key | Type | Default | Description | -| ----------------------------------- | ------ | ------------------------------- | ----------- | -| multi-1.app | string | `"multi-1"` | | -| multi-1.configmap.data.APP1CONF | string | `"yes"` | | -| multi-1.configmap.enabled | bool | `true` | | -| multi-1.container.image | string | `"<+artifacts.primary.image>"` | | -| multi-1.env | string | `"dev"` | | -| multi-1.ingress.trafficType | string | `"public"` | | -| multi-1.releaseName | string | `"multi1"` | | -| multi-1.secrets.auth-credentials[0] | string | `"MNG_AUTH0_INT_CLIENT_ID"` | | -| multi-1.secrets.auth-credentials[1] | string | `"MNG_AUTH0_INT_CLIENT_SECRET"` | | -| multi-1.service.internalPort | int | `9000` | | -| multi-1.shortname | string | `"mult1"` | | -| multi-1.team | string | `"example"` | | -| multi-2.app | string | `"multi-2"` | | -| multi-2.configmap.data.APP2CONF | string | `"yes"` | | -| multi-2.configmap.enabled | bool | `true` | | -| multi-2.container.image | string | `"<+artifacts.primary.image>"` | | -| multi-2.env | string | `"dev"` | | -| multi-2.ingress.trafficType | string | `"public"` | | -| multi-2.releaseName | string | `"multi2"` | | -| multi-2.secrets.auth-credentials[0] | string | `"MNG_AUTH0_INT_CLIENT_ID"` | | -| multi-2.secrets.auth-credentials[1] | string | `"MNG_AUTH0_INT_CLIENT_SECRET"` | | -| multi-2.service.internalPort | int | `9000` | | -| multi-2.shortname | string | `"mult2"` | | -| multi-2.team | string | `"example"` | | +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| multi-1.app | string | `"my-rest-api"` | | +| multi-1.appId | string | `"myappid1"` | | +| multi-1.container.env[0].name | string | `"CONTAINER_ROLE"` | | +| multi-1.container.env[0].value | string | `"rest-api"` | | +| multi-1.container.image | string | `"<+artifacts.primary.image>"` | | +| multi-1.ingress.trafficType | string | `"api"` | | +| multi-1.releaseName | string | `"my-rest-api"` | | +| multi-1.secrets.auth-credentials[0] | string | `"MNG_AUTH0_INT_CLIENT_ID"` | | +| multi-1.secrets.auth-credentials[1] | string | `"MNG_AUTH0_INT_CLIENT_SECRET"` | | +| multi-1.team | string | `"team-excellence"` | | +| multi-2.app | string | `"my-kafka-consumer"` | | +| multi-2.appId | string | `"myappid1"` | | +| multi-2.container.env[0].name | string | `"CONTAINER_ROLE"` | | +| multi-2.container.env[0].value | string | `"kafka-consumer"` | | +| multi-2.container.image | string | `"<+artifacts.primary.image>"` | | +| multi-2.ingress.enabled | bool | `false` | | +| multi-2.releaseName | string | `"my-kafka-consumer"` | | +| multi-2.secrets.auth-credentials[0] | string | `"MNG_AUTH0_INT_CLIENT_ID"` | | +| multi-2.secrets.auth-credentials[1] | string | `"MNG_AUTH0_INT_CLIENT_SECRET"` | | +| multi-2.team | string | `"team-excellence"` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/examples/common/simple-app/README.md b/examples/common/simple-app/README.md index b4ef663..8e2edab 100644 --- a/examples/common/simple-app/README.md +++ b/examples/common/simple-app/README.md @@ -8,15 +8,17 @@ A Helm chart for basic Entur deployments | Repository | Name | Version | |------------|------|---------| -| https://entur.github.io/helm-charts | common | 1.21.1 | +| https://entur.github.io/helm-charts | common | 2.0.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | common.app | string | `"simple-app"` | | +| common.appId | string | `"simapp"` | | | common.container.image | string | `"<+artifacts.primary.image>"` | | | common.ingress.trafficType | string | `"public"` | | -| common.shortname | string | `"simapp"` | | | common.team | string | `"example"` | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/examples/common/typical-backend/README.md b/examples/common/typical-backend/README.md index f90631b..6361f7c 100644 --- a/examples/common/typical-backend/README.md +++ b/examples/common/typical-backend/README.md @@ -8,13 +8,14 @@ A Helm chart for basic Entur deployments | Repository | Name | Version | |------------|------|---------| -| https://entur.github.io/helm-charts | common | 1.21.1 | +| https://entur.github.io/helm-charts | common | 2.0.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | common.app | string | `"typical-backend"` | | +| common.appId | string | `"typbak"` | | | common.container.cpu | float | `0.3` | | | common.container.image | string | `"<+artifacts.primary.image>"` | | | common.container.memory | int | `512` | | @@ -25,6 +26,7 @@ A Helm chart for basic Entur deployments | common.postgres.enabled | bool | `true` | | | common.postgres.memory | int | `32` | | | common.service.internalPort | int | `9000` | | -| common.shortname | string | `"typbak"` | | | common.team | string | `"team-example"` | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/examples/common/typical-backend/values.yaml b/examples/common/typical-backend/values.yaml index bcbdcf9..a5dabf9 100644 --- a/examples/common/typical-backend/values.yaml +++ b/examples/common/typical-backend/values.yaml @@ -14,9 +14,7 @@ common: image: <+artifacts.primary.image> # The CD tool will replace this value with the actual image cpu: 0.3 # Adjust this to your application needs memory: 512 # Adjust this to your application needs, a java application might need more memory to start like 1024 or 2048 or +++ - postgres: # sets up a postgres proxy so you can connect to your postgresql instance via localhost on the pod - enabled: true - instances: - - PGINSTANCES # Secret Manager key from entur/terraform-google-sql-db containing the instance connection name + postgres: # sets up a Cloud SQL proxy sidecar so you can connect to your postgresql instance via localhost on the pod + enabled: true # defaults to secretKeyPrefix: PG, matching entur/terraform-google-sql-db's default secret_key_prefix cpu: 0.1 # PostgreSQL Proxy setting, this usually is enough for most applications memory: 32 # PostgreSQL Proxy setting, this usually is enough for most applications diff --git a/examples/common/typical-frontend/README.md b/examples/common/typical-frontend/README.md index b8f65d7..96aa0f7 100644 --- a/examples/common/typical-frontend/README.md +++ b/examples/common/typical-frontend/README.md @@ -8,25 +8,27 @@ A Helm chart for basic Entur deployments | Repository | Name | Version | |------------|------|---------| -| https://entur.github.io/helm-charts | common | 1.21.1 | +| https://entur.github.io/helm-charts | common | 2.0.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | common.app | string | `"typical-frontend"` | | +| common.appId | string | `"typfro"` | | | common.configmap.data.TZx | string | `"Europe/Oslo"` | | | common.configmap.enabled | bool | `true` | | | common.container.cpu | float | `0.3` | | | common.container.image | string | `"<+artifacts.primary.image>"` | | | common.container.memory | int | `512` | | +| common.deployment.minReplicas | int | `2` | | | common.deployment.prometheus.enabled | bool | `true` | | -| common.deployment.replicas | int | `2` | | | common.ingress.enabled | bool | `true` | | | common.ingress.trafficType | string | `"public"` | | | common.secrets.auth-credentials[0] | string | `"MNG_AUTH0_INT_CLIENT_ID"` | | | common.secrets.auth-credentials[1] | string | `"MNG_AUTH0_INT_CLIENT_SECRET"` | | | common.service.internalPort | int | `9000` | | -| common.shortname | string | `"typfro"` | | | common.team | string | `"example"` | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/fixture/helm/values-postgres.yaml b/fixture/helm/values-postgres.yaml index 0773cc1..e343b7c 100644 --- a/fixture/helm/values-postgres.yaml +++ b/fixture/helm/values-postgres.yaml @@ -14,4 +14,4 @@ container: postgres: enabled: true instances: - - PGINSTANCES + - secretKeyPrefix: PG From 13c51c4d85ce8c12385782a23f5530ff57d09ca0 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Mon, 4 May 2026 12:31:28 +0200 Subject: [PATCH 59/66] docs: update AGENTS.md for postgres secretKeyPrefix changes --- AGENTS.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 668f394..f394d00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,7 @@ helm template charts/common -f fixture/helm/values-minimal.yaml helm template charts/common -f fixture/helm/values-cron.yaml helm template charts/common -f fixture/helm/values-secrets.yaml helm template charts/common -f fixture/helm/values-postgres.yaml +helm template charts/common -f fixture/helm/values-postgres-multi.yaml # Render a single template helm template test charts/common -f fixture/helm/values-minimal.yaml --show-only templates/pdb.yaml @@ -71,7 +72,7 @@ gh issue view --repo entur/helm-charts --comments - **Shared test values**: `charts/common/tests/values/common-test-values.yaml` - **Snapshots**: `charts/common/tests/__snapshot__/` - **Always run `helm unittest ./charts/common` after modifying any template or values** -- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, and v1 backward compatibility +- Tests cover: deployment, service, ingress, HPA, PDB, VPA, configmap, secrets, cron, sql-proxy, sql-credentials ## Key Conventions @@ -98,7 +99,7 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - Single container: use `container:` key - Multiple containers: use `containers:` list - Environment-specific overrides go in `env/values-kub-ent-{dev,tst,prd}.yaml` -- Postgres/Cloud SQL: use `postgres.instances` with Secret Manager keys via External Secrets +- Postgres/Cloud SQL: `postgres.enabled: true` defaults to `secretKeyPrefix: PG`. For multiple instances use `postgres.instances: [{secretKeyPrefix: PG}, {secretKeyPrefix: ANALYTICS_PG}]`. The `secretKeyPrefix` is the contract with the `entur/terraform-google-sql-db` Terraform module — it derives Secret Manager keys `{prefix}INSTANCES`, `{prefix}USER`, `{prefix}PASSWORD` - gRPC: set `grpc: true` — native K8s gRPC probes are used automatically with `service.internalPort` - Custom HPA metrics: use `hpa.metrics` list to add Pods/External/Object metrics alongside default CPU @@ -122,7 +123,8 @@ Uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - The chart supports both Deployment and CronJob workloads (mutually exclusive via `deployment.enabled` / `cron.enabled`) - Fixture values in `fixture/helm/` are used for CI template rendering validation - `shortname` is removed — use `appId` (matches GoogleCloudApplication `metadata.id`) -- `postgres.connectionConfig` is removed — use `postgres.instances` with Secret Manager keys +- `postgres.connectionConfig` is removed — use `postgres.enabled: true` (defaults to `secretKeyPrefix: PG`) +- `postgres.termTimeout` is removed — use `postgres.maxSigtermDelay` - `deployment.replicas` is removed — use `deployment.minReplicas` (HPA controls pod count) - `container.memoryLimit` is removed — memory limit always equals memory request - `pdb.minAvailable` is removed — use `deployment.minAvailable` From 7b67300f2e9186cedb974ac0ca15a6addeefefca Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Mon, 4 May 2026 12:36:27 +0200 Subject: [PATCH 60/66] feat: add upgrade-common-chart skill for v1 to v2 migration Claude Code skill that automates migrating Helm values files from common chart v1 to v2. Covers all breaking changes: shortname to appId, scaling field moves, postgres secretKeyPrefix integration, memoryLimit removal, and configmap.toEnv removal. --- .claude/skills/upgrade-common-chart/SKILL.md | 146 +++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 .claude/skills/upgrade-common-chart/SKILL.md diff --git a/.claude/skills/upgrade-common-chart/SKILL.md b/.claude/skills/upgrade-common-chart/SKILL.md new file mode 100644 index 0000000..6490cba --- /dev/null +++ b/.claude/skills/upgrade-common-chart/SKILL.md @@ -0,0 +1,146 @@ +--- +name: upgrade-common-chart +description: > + Upgrade Entur common Helm chart dependency from v1 to v2. Use this skill when + the user wants to migrate their Helm values files to the v2 common chart, asks + about upgrading to common chart v2, mentions "common chart upgrade", or has + schema validation errors after bumping the common chart version. Also trigger + when the user mentions deprecated fields like shortname, container.replicas, + connectionConfig, memoryLimit, or postgres.instances with raw string values. +--- + +# Upgrade Entur Common Helm Chart (v1 to v2) + +You are upgrading a Helm chart that depends on `entur/common` from v1 to v2. This is a breaking change that requires migrating values files and updating the chart dependency. + +## Step 1: Understand the project + +Find all relevant files: +1. `Chart.yaml` — contains the `common` dependency version to update +2. All `values*.yaml` files — contain the values to migrate (check `env/` subdirectories too) +3. Any `values-kub-ent-*.yaml` files — environment-specific overrides + +Read each file before making changes. The common chart is typically referenced as a dependency under the `common:` key in values files. + +## Step 2: Update Chart.yaml + +Bump the common chart dependency version to `2.0.0`: + +```yaml +dependencies: + - name: common + version: 2.0.0 + repository: "https://entur.github.io/helm-charts" +``` + +## Step 3: Apply migrations to every values file + +Work through each migration in order. Skip any that don't apply to the file. + +### 3.1 Rename `shortname` to `appId` + +```yaml +# Before +common: + shortname: myapp + +# After +common: + appId: myapp +``` + +### 3.2 Move scaling fields from `container.*` to `deployment.*` + +| Removed (v1) | Replacement (v2) | +|---|---| +| `container.replicas` | `deployment.minReplicas` | +| `deployment.replicas` | `deployment.minReplicas` | +| `container.maxReplicas` | `deployment.maxReplicas` | +| `container.forceReplicas` | `deployment.forceReplicas` | +| `container.minAvailable` | `deployment.minAvailable` | +| `container.terminationGracePeriodSeconds` | `deployment.terminationGracePeriodSeconds` | + +HPA is now always enabled (unless `forceReplicas` is set). The Deployment never emits a `replicas` field — HPA controls pod count. To pin replicas, use `deployment.forceReplicas`. + +### 3.3 Remove `container.memoryLimit` and `postgres.memoryLimit` + +Memory limit now always equals memory request. Remove `memoryLimit` and set `memory` to the value you need for both. + +### 3.4 Migrate postgres configuration + +This is the most significant change. The postgres integration now uses `secretKeyPrefix` as the contract with the `entur/terraform-google-sql-db` Terraform module. + +**Remove deprecated fields:** `postgres.connectionConfig`, `postgres.memoryLimit`, `postgres.termTimeout` + +**Migrate `postgres.instances`:** Items changed from raw Secret Manager key names (strings) to objects with `secretKeyPrefix`. When `enabled: true` with no `instances`, the chart defaults to `[{secretKeyPrefix: PG}]`. + +```yaml +# v1 +common: + postgres: + enabled: true + connectionConfig: my-app-psql-connection + +# v2 (simplest — default PG prefix) +common: + postgres: + enabled: true + +# v2 (explicit prefix) +common: + postgres: + enabled: true + instances: + - secretKeyPrefix: PG + +# v2 (multiple instances) +common: + postgres: + enabled: true + instances: + - secretKeyPrefix: PG + - secretKeyPrefix: ANALYTICS_PG +``` + +If `postgres.termTimeout` was set, rename it to `postgres.maxSigtermDelay` (maps to the Cloud SQL Proxy v2 `--max-sigterm-delay` flag). + +### 3.5 Remove `configmap.toEnv` + +The configmap is automatically mounted via `envFrom` when `configmap.enabled: true`. + +```yaml +# v1 +common: + configmap: + enabled: true + toEnv: true + +# v2 +common: + configmap: + enabled: true +``` + +### 3.6 Update ingress if using `ingress.class` + +The `kubernetes.io/ingress.class` annotation is removed. Ingress now uses `spec.ingressClassName` (defaults to `traefik`). If you had a custom `ingress.class` annotation, use `ingress.ingressClassName` instead. + +### 3.7 Update gRPC probe configuration + +If using gRPC, explicit `probes.*.grpc.port` settings are no longer needed — they default to `service.internalPort`. Remove them unless you need a non-default port. + +## Step 4: Verify + +Run these commands and fix any issues: + +```bash +helm dependency update +helm lint . -f env/values-kub-ent-dev.yaml +helm template . -f env/values-kub-ent-dev.yaml +``` + +If lint reports unknown properties, you likely missed a renamed or removed field. Check the migration steps above. + +## Step 5: Summary + +After completing all changes, provide the user with a summary of what was changed, organized by file. Mention any fields that were removed or renamed. From 5446f14ffb55064244420a9dca174b14ca760770 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Mon, 4 May 2026 12:37:34 +0200 Subject: [PATCH 61/66] docs: reference upgrade-common-chart skill in UPGRADE.md --- UPGRADE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 90099c0..44722ae 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -220,9 +220,17 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl - [ ] Run `helm lint . -f env/values-kub-ent-dev.yaml` to catch unknown properties and schema errors - [ ] Run `helm template . -f env/values-kub-ent-dev.yaml` to verify rendered output -## Using an AI agent to upgrade +## Using Claude Code to upgrade -If you use an AI coding agent (Claude Code, Copilot, Cursor, etc.), you can paste the following prompt to have it perform the migration for you: +This repo includes a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that automates the migration. Add `entur/helm-charts` as a dependency source and run: + +``` +/upgrade-common-chart +``` + +Or simply ask Claude Code to upgrade your chart — the skill triggers automatically when it detects common chart v1 values or deprecated fields. + +If you use a different AI coding agent (Copilot, Cursor, etc.), you can paste the following prompt instead: ``` Upgrade the Entur common Helm chart dependency from v1 to v2. From 13264735892d5a15935df5a7b1461186007482ef Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Mon, 4 May 2026 12:41:10 +0200 Subject: [PATCH 62/66] docs: clarify how to use upgrade skill from other repos --- UPGRADE.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 44722ae..be939c6 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -220,17 +220,30 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl - [ ] Run `helm lint . -f env/values-kub-ent-dev.yaml` to catch unknown properties and schema errors - [ ] Run `helm template . -f env/values-kub-ent-dev.yaml` to verify rendered output -## Using Claude Code to upgrade +## Automated upgrade with Claude Code -This repo includes a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that automates the migration. Add `entur/helm-charts` as a dependency source and run: +We provide a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that automates the migration. Run this in **your application's repo** (not in `helm-charts`): +### Option 1: Copy the skill into your repo + +```bash +# From your application repo +mkdir -p .claude/skills +curl -sL https://raw.githubusercontent.com/entur/helm-charts/main/.claude/skills/upgrade-common-chart/SKILL.md \ + -o .claude/skills/upgrade-common-chart.md ``` -/upgrade-common-chart + +Then ask Claude Code to upgrade: + ``` +Upgrade my common chart dependency to v2 +``` + +The skill triggers automatically and walks through all migration steps. You can delete the skill file after the upgrade is complete. -Or simply ask Claude Code to upgrade your chart — the skill triggers automatically when it detects common chart v1 values or deprecated fields. +### Option 2: Paste the migration prompt -If you use a different AI coding agent (Copilot, Cursor, etc.), you can paste the following prompt instead: +If you prefer not to install the skill, paste this into Claude Code (or any AI coding agent): ``` Upgrade the Entur common Helm chart dependency from v1 to v2. From 1899955389041d185d80d57e5b61b606507baea3 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Mon, 4 May 2026 12:43:18 +0200 Subject: [PATCH 63/66] docs: simplify upgrade instructions to reference public skill URL --- UPGRADE.md | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index be939c6..359c9ad 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -220,37 +220,16 @@ Note: The configmap is automatically mounted via `envFrom` when `configmap.enabl - [ ] Run `helm lint . -f env/values-kub-ent-dev.yaml` to catch unknown properties and schema errors - [ ] Run `helm template . -f env/values-kub-ent-dev.yaml` to verify rendered output -## Automated upgrade with Claude Code +## Automated upgrade with an AI coding agent -We provide a [Claude Code skill](https://docs.anthropic.com/en/docs/claude-code/skills) that automates the migration. Run this in **your application's repo** (not in `helm-charts`): - -### Option 1: Copy the skill into your repo - -```bash -# From your application repo -mkdir -p .claude/skills -curl -sL https://raw.githubusercontent.com/entur/helm-charts/main/.claude/skills/upgrade-common-chart/SKILL.md \ - -o .claude/skills/upgrade-common-chart.md -``` - -Then ask Claude Code to upgrade: - -``` -Upgrade my common chart dependency to v2 -``` - -The skill triggers automatically and walks through all migration steps. You can delete the skill file after the upgrade is complete. - -### Option 2: Paste the migration prompt - -If you prefer not to install the skill, paste this into Claude Code (or any AI coding agent): +Paste this prompt into Claude Code, Copilot, Cursor, or any AI coding agent from **your application's repo**: ``` Upgrade the Entur common Helm chart dependency from v1 to v2. -First, read the migration guide: - https://raw.githubusercontent.com/entur/helm-charts/main/UPGRADE.md +Read the upgrade skill and follow its instructions: + https://raw.githubusercontent.com/entur/helm-charts/main/.claude/skills/upgrade-common-chart/SKILL.md -Then apply the "Quick Migration Checklist" to all values files in this repository. +Apply all migration steps to every values file in this repository. Run `helm dependency update` and `helm lint` to verify. ``` From d0e3dfe4dbd09a22c63776ee5ca4eebaef786f3b Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Tue, 5 May 2026 16:32:49 +0200 Subject: [PATCH 64/66] feat!: raise container.cpu and container.memory defaults for JVM apps Default container.cpu raised from 0.1 to 0.3 and container.memory from 16 to 512 (Mi). The previous defaults were stub values that would OOMKill any JVM app before it finished booting; the JVM alone needs ~150-250 MiB before app code runs, and ~90% of Entur services are Spring Boot. Existing values that omit container.cpu / container.memory will see 3x CPU and 32x memory requests on next deploy. Override down for non-JVM workloads (sidecars, small Go services, static frontends). Co-Authored-By: Claude Opus 4.7 (1M context) --- charts/common/README.md | 4 ++-- charts/common/values.yaml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/common/README.md b/charts/common/README.md index c8b6ee6..8969e0d 100644 --- a/charts/common/README.md +++ b/charts/common/README.md @@ -77,13 +77,13 @@ common: | configmap.enabled | bool | false | Enable or disable the configmap | | container.args | string | `nil` | Optionally set the arguments that will be passed to the command, e.g. ["arg1","arg2"]. | | container.command | string | `nil` | Optionally set the command that will run in the pod. If not set, the entrypoint for the container-image is used (recommended for most Java-apps). | -| container.cpu | float | 0.1 | Set CPU without any unit. 100m is 0.1 | +| container.cpu | float | 0.3 | Set CPU request without any unit. 100m is 0.1. Default is sized for JVM/Spring Boot apps; lighter workloads (sidecars, small Go services, static frontends) should override down. | | container.cpuLimit | float | `5 x cpu` | Set CPU limit without any unit. 100m is 0.1 | | container.env | list | `[]` | Specify `env` entries for your container | | container.envFrom | list | `[]` | Attach secrets and configmaps to your `env` | | container.labels | object | `{}` | Add labels to your pods | | container.lifecycle | object | `{}` | Set pod lifecycle handlers | -| container.memory | int | 16 | Set memory without any unit, `Mi` is inferred | +| container.memory | int | 512 | Set memory request without any unit, `Mi` is inferred. Memory limit always equals request. Default is sized for JVM/Spring Boot apps (the JVM alone needs ~150–250 MiB before app code runs); lighter workloads should override down. | | container.memoryLimit | string | `nil` | @deprecated memoryLimit is removed. Memory limit is now always equal to memory request. Use `container.memory` instead. | | container.name | string | .app | Name of container | | container.probes.enabled | bool | `true` | Enable or disable probes | diff --git a/charts/common/values.yaml b/charts/common/values.yaml index 6025226..40e4b35 100644 --- a/charts/common/values.yaml +++ b/charts/common/values.yaml @@ -191,15 +191,15 @@ container: command: # -- Optionally set the arguments that will be passed to the command, e.g. ["arg1","arg2"]. args: - # -- Set CPU without any unit. 100m is 0.1 - # @default -- 0.1 - cpu: 0.1 + # -- Set CPU request without any unit. 100m is 0.1. Default is sized for JVM/Spring Boot apps; lighter workloads (sidecars, small Go services, static frontends) should override down. + # @default -- 0.3 + cpu: 0.3 # -- (float) Set CPU limit without any unit. 100m is 0.1 # @default -- `5 x cpu` cpuLimit: - # -- Set memory without any unit, `Mi` is inferred - # @default -- 16 - memory: 16 + # -- Set memory request without any unit, `Mi` is inferred. Memory limit always equals request. Default is sized for JVM/Spring Boot apps (the JVM alone needs ~150–250 MiB before app code runs); lighter workloads should override down. + # @default -- 512 + memory: 512 # -- @deprecated memoryLimit is removed. Memory limit is now always equal to memory request. Use `container.memory` instead. memoryLimit: # -- Set the uid that your user runs with From 5db3267f3aa0d2308b29fa523bb332026ef95f5b Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Tue, 5 May 2026 16:33:03 +0200 Subject: [PATCH 65/66] ci: fix helm-docs workflow to run on release-please PRs The pull_request branches filter was matching the wrong direction (base instead of head), so the workflow never triggered on release-please PRs. Switch to filtering base on main and gating the job with an if: condition on github.head_ref. Also fix: - checkout uses github.head_ref instead of github.ref (which is the ephemeral merge ref on pull_request events) - drop redundant git switch (checkout already lands on the branch) - move the printf below the VERSION export - replace inline shell substitution in jq and yq filters with --arg / strenv for safer interpolation - quote $CUR_CHART in find - add a concurrency group to avoid races with release-please regenerations Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/helm-docs.yml | 36 ++++++++++++++------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/helm-docs.yml b/.github/workflows/helm-docs.yml index d21e243..57bbf78 100644 --- a/.github/workflows/helm-docs.yml +++ b/.github/workflows/helm-docs.yml @@ -2,13 +2,17 @@ name: helm-docs-and-examples-update on: pull_request: - branches: - - "release-please--branches--**" + branches: [main] workflow_dispatch: +concurrency: + group: helm-docs-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + jobs: helm-doc-example-update: name: Update helm chart versions in examples and docs + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please--branches--') runs-on: ubuntu-24.04 permissions: contents: write @@ -16,40 +20,30 @@ jobs: - name: Checkout source code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.ref }} + ref: ${{ github.head_ref }} fetch-depth: 0 - - name: Add helm-docs common changes to release branch - env: - RELEASE_BRANCH: ${{ github.ref_name }} + - name: Update examples and regenerate docs run: | CUR_CHART="common" # TODO get from release-please manifest output - - printf "Updating Helm chart %s documentation for version %s\n" $CUR_CHART $VERSION git config --global user.email "actions@github.com" git config --global user.name "GitHub Actions" - git switch $RELEASE_BRANCH - # get version from release-please-manifest.json - export VERSION=$(jq -r '.["charts/'"$CUR_CHART"'"]' .github/release-please-manifest.json) - printf "Version: %s\n" $VERSION + export VERSION=$(jq -r --arg c "$CUR_CHART" '.["charts/" + $c]' .github/release-please-manifest.json) + printf "Updating Helm chart %s documentation for version %s\n" "$CUR_CHART" "$VERSION" - # Update the version in examples directory - all_charts=$(find ./examples/$CUR_CHART -name Chart.yaml) + all_charts=$(find "./examples/$CUR_CHART" -name Chart.yaml) for chart in $all_charts; do - yq -e -i '(.dependencies[] | select(.name == "'$CUR_CHART'") | .version) = env(VERSION)' "${chart}" + CUR_CHART="$CUR_CHART" yq -e -i '(.dependencies[] | select(.name == strenv(CUR_CHART)) | .version) = env(VERSION)' "${chart}" done - # Install and run helm-docs go install github.com/norwoodj/helm-docs/cmd/helm-docs@37d3055fece566105cf8cff7c17b7b2355a01677 # 1.14.2 - export PATH=${PATH}:`go env GOPATH`/bin + export PATH=${PATH}:$(go env GOPATH)/bin helm-docs - if [ -n "$(git status --porcelain '*.md')" ]; then - git add \*README.md - git add \*Chart.yaml + if [ -n "$(git status --porcelain '*.md' '*Chart.yaml')" ]; then + git add \*README.md \*Chart.yaml git commit -m "docs: update Helm chart documentation" git push else echo "Helm versions are up to date" - exit 0 fi From 39f0c980dc3296200362fe5c9b8feb17b1675a21 Mon Sep 17 00:00:00 2001 From: Glenn Terjesen Date: Tue, 5 May 2026 16:33:16 +0200 Subject: [PATCH 66/66] docs: add ELI5 README templates for example charts Add README.md.gotmpl per example so the narrative survives helm-docs regeneration. Each template renders the chart header and badges, then custom what / when / key-values sections, then the auto-generated requirements + values tables. The narrative explains each example's purpose in plain language, with cross-references between them (e.g. cronjob points to multi-deploy for event-driven work; multi-container points to multi-deploy as the preferred alternative when processes can run independently). A short gloss of "ingress" is included where relevant. Also fix a TZx -> TZ typo in typical-frontend/values.yaml so the configmap example matches the standard timezone env var. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/common/cronjob/README.md | 21 +++++++++- examples/common/cronjob/README.md.gotmpl | 28 +++++++++++++ examples/common/grpc-app/README.md | 20 ++++++++- examples/common/grpc-app/README.md.gotmpl | 27 ++++++++++++ examples/common/multi-container/README.md | 21 +++++++++- .../common/multi-container/README.md.gotmpl | 28 +++++++++++++ examples/common/multi-deploy/README.md | 35 +++++++++++++++- examples/common/multi-deploy/README.md.gotmpl | 42 +++++++++++++++++++ examples/common/simple-app/README.md | 20 ++++++++- examples/common/simple-app/README.md.gotmpl | 27 ++++++++++++ examples/common/typical-backend/README.md | 25 ++++++++++- .../common/typical-backend/README.md.gotmpl | 32 ++++++++++++++ examples/common/typical-frontend/README.md | 28 +++++++++++-- .../common/typical-frontend/README.md.gotmpl | 33 +++++++++++++++ examples/common/typical-frontend/values.yaml | 2 +- 15 files changed, 373 insertions(+), 16 deletions(-) create mode 100644 examples/common/cronjob/README.md.gotmpl create mode 100644 examples/common/grpc-app/README.md.gotmpl create mode 100644 examples/common/multi-container/README.md.gotmpl create mode 100644 examples/common/multi-deploy/README.md.gotmpl create mode 100644 examples/common/simple-app/README.md.gotmpl create mode 100644 examples/common/typical-backend/README.md.gotmpl create mode 100644 examples/common/typical-frontend/README.md.gotmpl diff --git a/examples/common/cronjob/README.md b/examples/common/cronjob/README.md index c76ece6..b1052db 100644 --- a/examples/common/cronjob/README.md +++ b/examples/common/cronjob/README.md @@ -1,8 +1,25 @@ # cronjob -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for Entur CronJob workloads +A scheduled job (Kubernetes `CronJob`) instead of a long-running `Deployment`. No service, no ingress, no HPA — just a container that runs on a schedule. + +## What this example shows + +- `deployment.enabled: false` — turn off the long-running deployment. +- `cron.enabled: true` — turn on the CronJob workload (mutually exclusive with `deployment.enabled`). +- `cron.schedule` — standard 5-field cron expression (this example: every 6 hours, in cluster timezone / UTC). +- `container.command` and `container.args` — what the job actually runs. + +## When to use this + +Use this for time-driven work: nightly imports, periodic cleanups, batch processing. If the work is event-driven instead, use a regular `Deployment` with a queue consumer (see the `multi-deploy` example). + +## Key values to know + +- `cron.schedule` — standard cron syntax. +- `container.image` — the same `<+artifacts.primary.image>` placeholder as deployments. The CronJob pulls this image each invocation. +- `container.command` / `container.args` — what to run. If your image already has the right entrypoint, you can omit both. ## Requirements diff --git a/examples/common/cronjob/README.md.gotmpl b/examples/common/cronjob/README.md.gotmpl new file mode 100644 index 0000000..a5909d3 --- /dev/null +++ b/examples/common/cronjob/README.md.gotmpl @@ -0,0 +1,28 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +A scheduled job (Kubernetes `CronJob`) instead of a long-running `Deployment`. No service, no ingress, no HPA — just a container that runs on a schedule. + +## What this example shows + +- `deployment.enabled: false` — turn off the long-running deployment. +- `cron.enabled: true` — turn on the CronJob workload (mutually exclusive with `deployment.enabled`). +- `cron.schedule` — standard 5-field cron expression (this example: every 6 hours, in cluster timezone / UTC). +- `container.command` and `container.args` — what the job actually runs. + +## When to use this + +Use this for time-driven work: nightly imports, periodic cleanups, batch processing. If the work is event-driven instead, use a regular `Deployment` with a queue consumer (see the `multi-deploy` example). + +## Key values to know + +- `cron.schedule` — standard cron syntax. +- `container.image` — the same `<+artifacts.primary.image>` placeholder as deployments. The CronJob pulls this image each invocation. +- `container.command` / `container.args` — what to run. If your image already has the right entrypoint, you can omit both. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/grpc-app/README.md b/examples/common/grpc-app/README.md index 9e23655..e2fe63b 100644 --- a/examples/common/grpc-app/README.md +++ b/examples/common/grpc-app/README.md @@ -1,8 +1,24 @@ # grpc-app -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for basic Entur deployments using gRPC +A gRPC service. The chart switches HTTP probes to native Kubernetes gRPC probes and configures the **ingress** (the Kubernetes resource that routes external traffic into your service) and the service itself for end-to-end HTTP/2 (h2c — HTTP/2 cleartext) traffic. + +## What this example shows + +- `grpc: true` — tells the chart to use native gRPC probes (Kubernetes ≥1.24) instead of HTTP probes against `/actuator/health/*`. +- `ingress.trafficType: http2` — configures the ingress for end-to-end HTTP/2 instead of HTTP/1. +- `service.annotations.entur.no/internal-http2: "true"` — enables HTTP/2 for in-cluster service-to-service calls too. + +## When to use this + +Use this for pure gRPC services. If your service exposes both gRPC and REST, leave `grpc: false` and configure the HTTP/2 annotation manually on the parts that need it — see the comment in `values.yaml`. + +## Key values to know + +- `grpc: true` — the headline switch. Also wires up the `traefik.ingress.kubernetes.io/service.serversscheme: h2c` annotation automatically. +- `service.internalPort` — the port your gRPC server listens on. The chart wires this into the gRPC probes. +- `ingress.trafficType: http2` — anything else (`api`, `public`) terminates HTTP/2 at the ingress. ## Requirements diff --git a/examples/common/grpc-app/README.md.gotmpl b/examples/common/grpc-app/README.md.gotmpl new file mode 100644 index 0000000..e9fc7f9 --- /dev/null +++ b/examples/common/grpc-app/README.md.gotmpl @@ -0,0 +1,27 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +A gRPC service. The chart switches HTTP probes to native Kubernetes gRPC probes and configures the **ingress** (the Kubernetes resource that routes external traffic into your service) and the service itself for end-to-end HTTP/2 (h2c — HTTP/2 cleartext) traffic. + +## What this example shows + +- `grpc: true` — tells the chart to use native gRPC probes (Kubernetes ≥1.24) instead of HTTP probes against `/actuator/health/*`. +- `ingress.trafficType: http2` — configures the ingress for end-to-end HTTP/2 instead of HTTP/1. +- `service.annotations.entur.no/internal-http2: "true"` — enables HTTP/2 for in-cluster service-to-service calls too. + +## When to use this + +Use this for pure gRPC services. If your service exposes both gRPC and REST, leave `grpc: false` and configure the HTTP/2 annotation manually on the parts that need it — see the comment in `values.yaml`. + +## Key values to know + +- `grpc: true` — the headline switch. Also wires up the `traefik.ingress.kubernetes.io/service.serversscheme: h2c` annotation automatically. +- `service.internalPort` — the port your gRPC server listens on. The chart wires this into the gRPC probes. +- `ingress.trafficType: http2` — anything else (`api`, `public`) terminates HTTP/2 at the ingress. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/multi-container/README.md b/examples/common/multi-container/README.md index 2ce599e..dab5bd1 100644 --- a/examples/common/multi-container/README.md +++ b/examples/common/multi-container/README.md @@ -1,8 +1,25 @@ # multi-container -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for multiple containers +A pod with multiple containers running side by side. Same pod, same lifecycle, shared network namespace. + +## What this example shows + +- `containers:` (plural list) instead of `container:` (single map). Each entry in the list becomes one container in the pod. +- Custom probe paths and a long startup timeout (`startup.failureThreshold: 300`). +- Multiple service ports targeting different containers (`service.ports`). +- Prometheus scraping on a non-default path and port. + +## When to use this + +Only put two containers in one pod when they genuinely need to share a lifecycle — typical sidecars are logging shippers, config reloaders, or proxies (e.g. Envoy). If the two processes can run as separate Deployments, prefer that instead (see the `multi-deploy` example) — separate Deployments scale and fail independently. + +## Key values to know + +- `containers:` (list) — each entry follows the same shape as the single `container:` map. +- `service.ports` — list of TCP ports the service exposes. Each port's `targetPort` can route to a different container's `containerPort`. +- `deployment.prometheus.path` / `port` — override scrape config when your metrics endpoint isn't the default `/actuator/prometheus`. ## Requirements diff --git a/examples/common/multi-container/README.md.gotmpl b/examples/common/multi-container/README.md.gotmpl new file mode 100644 index 0000000..061e01d --- /dev/null +++ b/examples/common/multi-container/README.md.gotmpl @@ -0,0 +1,28 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +A pod with multiple containers running side by side. Same pod, same lifecycle, shared network namespace. + +## What this example shows + +- `containers:` (plural list) instead of `container:` (single map). Each entry in the list becomes one container in the pod. +- Custom probe paths and a long startup timeout (`startup.failureThreshold: 300`). +- Multiple service ports targeting different containers (`service.ports`). +- Prometheus scraping on a non-default path and port. + +## When to use this + +Only put two containers in one pod when they genuinely need to share a lifecycle — typical sidecars are logging shippers, config reloaders, or proxies (e.g. Envoy). If the two processes can run as separate Deployments, prefer that instead (see the `multi-deploy` example) — separate Deployments scale and fail independently. + +## Key values to know + +- `containers:` (list) — each entry follows the same shape as the single `container:` map. +- `service.ports` — list of TCP ports the service exposes. Each port's `targetPort` can route to a different container's `containerPort`. +- `deployment.prometheus.path` / `port` — override scrape config when your metrics endpoint isn't the default `/actuator/prometheus`. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/multi-deploy/README.md b/examples/common/multi-deploy/README.md index 6cee818..eb84dba 100644 --- a/examples/common/multi-deploy/README.md +++ b/examples/common/multi-deploy/README.md @@ -1,8 +1,39 @@ # multi-deploy -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for basic Entur deployments +A single chart that produces two separate Deployments (and Services, ingresses, etc.) from one container image. The image is the same; what differs is which `CONTAINER_ROLE` env var each Deployment gets. + +## What this example shows + +- `multi-1` is a REST API — gets `CONTAINER_ROLE=rest-api` and an **ingress** (the Kubernetes resource that routes external HTTP traffic into your service). +- `multi-2` is a Kafka consumer — gets `CONTAINER_ROLE=kafka-consumer` and **no** ingress (workers don't accept inbound traffic). + +The application code reads `CONTAINER_ROLE` at startup and decides which mode to run in. The two deployments scale independently. + +## When to use this + +Use this when one codebase legitimately serves two roles (typically: web API + async worker) and you want to deploy them as separate, independently-scaling workloads — without maintaining two repos or two charts. + +## GitHub Actions CD + +When using Entur's shared [gha-helm](https://github.com/entur/gha-helm/blob/main/README-deploy.md) reusable workflow we also need to define the `image:` path being replaced during deploy. + +```yaml +helm-deploy: + uses: entur/gha-helm/.github/workflows/deploy.yml@v1 + with: + environment: dev + image: amazing-app:2.3.1 + image_set_path: "multi-1.container.image,multi-2.container.image" + secrets: inherit +``` + +## Key values to know + +- `multi-1` / `multi-2` — top-level keys must match the `alias` fields in `Chart.yaml` dependencies. Each key configures one deployment. +- `releaseName` (per alias) — gives each deployment its own Helm release-style name. This is the secret sauce that splits them apart. +- `appId` — typically the **same** for both halves, since they belong to the same logical app. ## Requirements diff --git a/examples/common/multi-deploy/README.md.gotmpl b/examples/common/multi-deploy/README.md.gotmpl new file mode 100644 index 0000000..d2b379d --- /dev/null +++ b/examples/common/multi-deploy/README.md.gotmpl @@ -0,0 +1,42 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +A single chart that produces two separate Deployments (and Services, ingresses, etc.) from one container image. The image is the same; what differs is which `CONTAINER_ROLE` env var each Deployment gets. + +## What this example shows + +- `multi-1` is a REST API — gets `CONTAINER_ROLE=rest-api` and an **ingress** (the Kubernetes resource that routes external HTTP traffic into your service). +- `multi-2` is a Kafka consumer — gets `CONTAINER_ROLE=kafka-consumer` and **no** ingress (workers don't accept inbound traffic). + +The application code reads `CONTAINER_ROLE` at startup and decides which mode to run in. The two deployments scale independently. + +## When to use this + +Use this when one codebase legitimately serves two roles (typically: web API + async worker) and you want to deploy them as separate, independently-scaling workloads — without maintaining two repos or two charts. + +## GitHub Actions CD + +When using Entur's shared [gha-helm](https://github.com/entur/gha-helm/blob/main/README-deploy.md) reusable workflow we also need to define the `image:` path being replaced during deploy. + +```yaml +helm-deploy: + uses: entur/gha-helm/.github/workflows/deploy.yml@v1 + with: + environment: dev + image: amazing-app:2.3.1 + image_set_path: "multi-1.container.image,multi-2.container.image" + secrets: inherit +``` + +## Key values to know + +- `multi-1` / `multi-2` — top-level keys must match the `alias` fields in `Chart.yaml` dependencies. Each key configures one deployment. +- `releaseName` (per alias) — gives each deployment its own Helm release-style name. This is the secret sauce that splits them apart. +- `appId` — typically the **same** for both halves, since they belong to the same logical app. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/simple-app/README.md b/examples/common/simple-app/README.md index 8e2edab..479b408 100644 --- a/examples/common/simple-app/README.md +++ b/examples/common/simple-app/README.md @@ -1,8 +1,24 @@ # simple-app -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for basic Entur deployments +The smallest possible deployment using the Entur common chart. Just the required values, nothing else. + +## What this example shows + +A bare-minimum Spring Boot-style deployment with public ingress. The chart fills in everything else with sensible defaults: an HPA with 2–10 replicas, a PodDisruptionBudget, security contexts, and resource requests/limits. + +## When to use this + +Use this as your starting point when introducing a brand-new service. Copy `values.yaml`, swap the names, and you have a working deployment. + +## Key values to know + +- `app` — the name of your service (becomes the Deployment name). +- `appId` — must match the `metadata.id` of your `GoogleCloudApplication` resource. +- `team` — the owning team slug. +- `container.image` — `<+artifacts.primary.image>` is the placeholder Harness CD substitutes with the actual built image at deploy time. +- `ingress.trafficType` — controls the **ingress** (the Kubernetes resource that routes external HTTP traffic into your service). Use `public` for end-user traffic, `api` for traffic behind the Apigee API Gateway. ## Requirements diff --git a/examples/common/simple-app/README.md.gotmpl b/examples/common/simple-app/README.md.gotmpl new file mode 100644 index 0000000..395b68a --- /dev/null +++ b/examples/common/simple-app/README.md.gotmpl @@ -0,0 +1,27 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +The smallest possible deployment using the Entur common chart. Just the required values, nothing else. + +## What this example shows + +A bare-minimum Spring Boot-style deployment with public ingress. The chart fills in everything else with sensible defaults: an HPA with 2–10 replicas, a PodDisruptionBudget, security contexts, and resource requests/limits. + +## When to use this + +Use this as your starting point when introducing a brand-new service. Copy `values.yaml`, swap the names, and you have a working deployment. + +## Key values to know + +- `app` — the name of your service (becomes the Deployment name). +- `appId` — must match the `metadata.id` of your `GoogleCloudApplication` resource. +- `team` — the owning team slug. +- `container.image` — `<+artifacts.primary.image>` is the placeholder Harness CD substitutes with the actual built image at deploy time. +- `ingress.trafficType` — controls the **ingress** (the Kubernetes resource that routes external HTTP traffic into your service). Use `public` for end-user traffic, `api` for traffic behind the Apigee API Gateway. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/typical-backend/README.md b/examples/common/typical-backend/README.md index 6361f7c..6a51c91 100644 --- a/examples/common/typical-backend/README.md +++ b/examples/common/typical-backend/README.md @@ -1,8 +1,29 @@ # typical-backend -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for basic Entur deployments +The most common shape of a service at Entur: a Spring Boot backend that talks to a Cloud SQL Postgres database via a sidecar proxy, exposed through the Apigee API Gateway. + +## What this example shows + +- One container running your app. +- A `cloud-sql-proxy` sidecar so your app reaches Postgres on `localhost:5432` instead of going over the public internet. +- An **ingress** (the Kubernetes resource that routes external HTTP traffic into your service) with `trafficType: api` — the URL is only reachable behind the Apigee API Gateway at `https://api.entur.io/yourAPI`. + +## When to use this + +Use this when your service: + +- Is a Spring Boot or other JVM app. +- Needs a Postgres database. +- Exposes an HTTP API that goes through Apigee. + +## Key values to know + +- `container.cpu` / `container.memory` — sized for a JVM. The JVM alone needs ~150–250 MiB before app code runs, so 512 is a reasonable floor. Bump higher if your app needs more heap. +- `postgres.enabled: true` — turns on the Cloud SQL proxy sidecar. The chart auto-derives Secret Manager keys (`PGINSTANCES`, `PGUSER`, `PGPASSWORD`) and exports `PGHOST=localhost`, `PGPORT=5432` to your app container, matching the defaults in [`entur/terraform-google-sql-db`](https://github.com/entur/terraform-google-sql-db). +- `service.internalPort` — the port your app listens on inside the pod. Must match what your Dockerfile exposes. +- `deployment.prometheus.enabled` — flip on once you have a `/actuator/prometheus` endpoint. ## Requirements diff --git a/examples/common/typical-backend/README.md.gotmpl b/examples/common/typical-backend/README.md.gotmpl new file mode 100644 index 0000000..6a28842 --- /dev/null +++ b/examples/common/typical-backend/README.md.gotmpl @@ -0,0 +1,32 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +The most common shape of a service at Entur: a Spring Boot backend that talks to a Cloud SQL Postgres database via a sidecar proxy, exposed through the Apigee API Gateway. + +## What this example shows + +- One container running your app. +- A `cloud-sql-proxy` sidecar so your app reaches Postgres on `localhost:5432` instead of going over the public internet. +- An **ingress** (the Kubernetes resource that routes external HTTP traffic into your service) with `trafficType: api` — the URL is only reachable behind the Apigee API Gateway at `https://api.entur.io/yourAPI`. + +## When to use this + +Use this when your service: + +- Is a Spring Boot or other JVM app. +- Needs a Postgres database. +- Exposes an HTTP API that goes through Apigee. + +## Key values to know + +- `container.cpu` / `container.memory` — sized for a JVM. The JVM alone needs ~150–250 MiB before app code runs, so 512 is a reasonable floor. Bump higher if your app needs more heap. +- `postgres.enabled: true` — turns on the Cloud SQL proxy sidecar. The chart auto-derives Secret Manager keys (`PGINSTANCES`, `PGUSER`, `PGPASSWORD`) and exports `PGHOST=localhost`, `PGPORT=5432` to your app container, matching the defaults in [`entur/terraform-google-sql-db`](https://github.com/entur/terraform-google-sql-db). +- `service.internalPort` — the port your app listens on inside the pod. Must match what your Dockerfile exposes. +- `deployment.prometheus.enabled` — flip on once you have a `/actuator/prometheus` endpoint. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/typical-frontend/README.md b/examples/common/typical-frontend/README.md index 96aa0f7..a378880 100644 --- a/examples/common/typical-frontend/README.md +++ b/examples/common/typical-frontend/README.md @@ -1,8 +1,30 @@ # typical-frontend -![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) +![Version: 0.0.3](https://img.shields.io/badge/Version-0.0.3-informational?style=flat-square) ![AppVersion: 0.0.1](https://img.shields.io/badge/AppVersion-0.0.1-informational?style=flat-square) -A Helm chart for basic Entur deployments +A typical frontend or BFF (backend-for-frontend) web app: public ingress, a ConfigMap for non-secret runtime config, and Secret Manager-backed secrets. + +## What this example shows + +- `ingress.trafficType: public` — accepts end-user traffic directly (not behind Apigee). An **ingress** is the Kubernetes resource that routes external HTTP traffic into your service. +- `configmap` — non-sensitive runtime config (e.g. a timezone) baked into env vars. +- `secrets.auth-credentials` — Secret Manager keys pulled into a Kubernetes Secret named `-auth-credentials` via External Secrets. +- `deployment.prometheus.enabled: true` — scrapes `/actuator/prometheus` by default. + +## When to use this + +Use this for any frontend or BFF that: + +- Serves end users directly over HTTPS. +- Needs both runtime config (ConfigMap) and secrets (External Secrets). +- Is not a JVM app — otherwise bump `container.memory` higher. + +## Key values to know + +- `ingress.trafficType: public` — exposes the service to the public internet via the Entur ingress controller. +- `configmap.data` — keys here become env vars in the container. +- `secrets.auth-credentials` — list of Secret Manager keys; the chart turns them into Kubernetes Secret entries with the same names. +- `deployment.minReplicas: 2` — minimum pod count when HPA scales down. ## Requirements @@ -16,7 +38,7 @@ A Helm chart for basic Entur deployments |-----|------|---------|-------------| | common.app | string | `"typical-frontend"` | | | common.appId | string | `"typfro"` | | -| common.configmap.data.TZx | string | `"Europe/Oslo"` | | +| common.configmap.data.TZ | string | `"Europe/Oslo"` | | | common.configmap.enabled | bool | `true` | | | common.container.cpu | float | `0.3` | | | common.container.image | string | `"<+artifacts.primary.image>"` | | diff --git a/examples/common/typical-frontend/README.md.gotmpl b/examples/common/typical-frontend/README.md.gotmpl new file mode 100644 index 0000000..37853d0 --- /dev/null +++ b/examples/common/typical-frontend/README.md.gotmpl @@ -0,0 +1,33 @@ +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }} {{ template "chart.appVersionBadge" . }} + +A typical frontend or BFF (backend-for-frontend) web app: public ingress, a ConfigMap for non-secret runtime config, and Secret Manager-backed secrets. + +## What this example shows + +- `ingress.trafficType: public` — accepts end-user traffic directly (not behind Apigee). An **ingress** is the Kubernetes resource that routes external HTTP traffic into your service. +- `configmap` — non-sensitive runtime config (e.g. a timezone) baked into env vars. +- `secrets.auth-credentials` — Secret Manager keys pulled into a Kubernetes Secret named `-auth-credentials` via External Secrets. +- `deployment.prometheus.enabled: true` — scrapes `/actuator/prometheus` by default. + +## When to use this + +Use this for any frontend or BFF that: + +- Serves end users directly over HTTPS. +- Needs both runtime config (ConfigMap) and secrets (External Secrets). +- Is not a JVM app — otherwise bump `container.memory` higher. + +## Key values to know + +- `ingress.trafficType: public` — exposes the service to the public internet via the Entur ingress controller. +- `configmap.data` — keys here become env vars in the container. +- `secrets.auth-credentials` — list of Secret Manager keys; the chart turns them into Kubernetes Secret entries with the same names. +- `deployment.minReplicas: 2` — minimum pod count when HPA scales down. + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/examples/common/typical-frontend/values.yaml b/examples/common/typical-frontend/values.yaml index 9419734..6a95b3f 100644 --- a/examples/common/typical-frontend/values.yaml +++ b/examples/common/typical-frontend/values.yaml @@ -18,7 +18,7 @@ common: configmap: enabled: true data: - TZx: "Europe/Oslo" + TZ: "Europe/Oslo" secrets: auth-credentials: # k8s secret name "typical-frontend-auth-credentials" - MNG_AUTH0_INT_CLIENT_ID