From 89e8e20a5339b40a922e2efa3afdaef639e4ba5d Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Fri, 3 Jul 2026 07:13:21 -0300 Subject: [PATCH 1/2] fix(ckan): add template validation gate --- charts/ckan/templates/NOTES.txt | 98 ++++++++++++++++++++++---- charts/ckan/templates/_helpers.tpl | 54 +++++++++++--- charts/ckan/templates/ingress.yaml | 4 +- charts/ckan/templates/validate.yaml | 2 + charts/ckan/tests/backup_test.yaml | 11 --- charts/ckan/tests/ingress_test.yaml | 14 ++++ charts/ckan/tests/validation_test.yaml | 92 ++++++++++++++++++++++++ 7 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 charts/ckan/templates/validate.yaml create mode 100644 charts/ckan/tests/validation_test.yaml diff --git a/charts/ckan/templates/NOTES.txt b/charts/ckan/templates/NOTES.txt index e47692a8c..a8505a866 100644 --- a/charts/ckan/templates/NOTES.txt +++ b/charts/ckan/templates/NOTES.txt @@ -1,37 +1,107 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} CKAN {{ .Chart.AppVersion }} has been installed. +1. Installation summary +----------------------- +Release: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} +Chart version: {{ .Chart.Version }} +Application version: {{ .Chart.AppVersion }} +Image: {{ include "ckan.image" . }} +Service name: {{ include "ckan.fullname" . }} +Service type: {{ .Values.service.type }} +Service port: {{ .Values.service.port }} + +2. Access +--------- {{- if .Values.ingress.enabled }} +Ingress routes: {{- range .Values.ingress.hosts }} - Web UI: http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- if .host }} + - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}/ +{{- else }} + - catch-all host on the configured ingress controller +{{- end }} {{- end }} {{- else }} - kubectl port-forward svc/{{ include "ckan.fullname" . }} 5000:{{ .Values.service.port }} - Web UI: http://localhost:5000 +Port-forward the Service: + + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "ckan.fullname" . }} 5000:{{ .Values.service.port }} + +Then open: + + http://localhost:5000/ {{- end }} -Default credentials: {{ .Values.ckan.sysadminName }} / (check secret {{ include "ckan.secretName" . }}) +Cluster-local endpoint: + + http://{{ include "ckan.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}/ + +3. Credentials +-------------- +Sysadmin username: {{ .Values.ckan.sysadminName }} +Sysadmin password secret: {{ include "ckan.secretName" . }} +Sysadmin password key: {{ .Values.ckan.existingSecretPasswordKey | default "sysadmin-password" }} + +Inspect generated credentials: + + kubectl -n {{ .Release.Namespace }} get secret {{ include "ckan.secretName" . }} + +4. Runtime components +--------------------- +PostgreSQL subchart: {{ ternary "enabled" "disabled" .Values.postgresql.enabled }} +Redis subchart: {{ ternary "enabled" "disabled" .Values.redis.enabled }} +Solr: {{ ternary "enabled" "external" .Values.solr.enabled }} +DataPusher: {{ ternary "enabled" "disabled" .Values.datapusher.enabled }} +Backup CronJob: {{ ternary "enabled" "disabled" .Values.backup.enabled }} -Runtime: - Image: {{ include "ckan.image" . }} - PostgreSQL subchart: {{ ternary "enabled" "disabled" .Values.postgresql.enabled }} - Redis subchart: {{ ternary "enabled" "disabled" .Values.redis.enabled }} - Solr: {{ ternary "enabled" "external" .Values.solr.enabled }} - DataPusher: {{ ternary "enabled" "disabled" .Values.datapusher.enabled }} - Backup CronJob: {{ ternary "enabled" "disabled" .Values.backup.enabled }} +5. Operational checks +--------------------- +Wait for CKAN: + + kubectl -n {{ .Release.Namespace }} rollout status deploy/{{ include "ckan.fullname" . }} --timeout=300s + +Inspect workload status: -Checks: kubectl -n {{ .Release.Namespace }} get pods -l app.kubernetes.io/instance={{ .Release.Name }} kubectl -n {{ .Release.Namespace }} logs deploy/{{ include "ckan.fullname" . }} --tail=100 kubectl -n {{ .Release.Namespace }} get events --sort-by=.lastTimestamp + +6. Storage and backup +--------------------- +{{- if .Values.persistence.enabled }} +CKAN file storage persistence is enabled. +PVC: {{ .Values.persistence.existingClaim | default (printf "%s-storage" (include "ckan.fullname" .)) }} +{{- else }} +CKAN file storage persistence is disabled and uses emptyDir. +{{- end }} + +{{- if .Values.backup.enabled }} +Scheduled PostgreSQL backups are enabled. +Backup CronJob: {{ include "ckan.fullname" . }}-backup +{{- else }} +Scheduled PostgreSQL backups are disabled. +{{- end }} + +7. Troubleshooting +------------------ +Inspect CKAN and dependencies: + + kubectl -n {{ .Release.Namespace }} describe deploy/{{ include "ckan.fullname" . }} {{- if .Values.postgresql.enabled }} kubectl -n {{ .Release.Namespace }} logs statefulset/{{ .Release.Name }}-postgresql --tail=80 {{- end }} {{- if .Values.redis.enabled }} kubectl -n {{ .Release.Namespace }} logs statefulset/{{ .Release.Name }}-redis --tail=80 {{- end }} +{{- if .Values.solr.enabled }} + kubectl -n {{ .Release.Namespace }} logs statefulset/{{ include "ckan.fullname" . }}-solr --tail=80 +{{- end }} -Upgrade reminder: - Take a PostgreSQL backup and confirm CKAN plugins before production upgrades. +8. Resources +------------ +Chart docs: https://helmforge.dev/docs/charts/ckan +Chart source: https://github.com/helmforgedev/charts/tree/main/charts/ckan +Upstream project: https://ckan.org Happy Helmforging :) diff --git a/charts/ckan/templates/_helpers.tpl b/charts/ckan/templates/_helpers.tpl index 4ab75f80e..02e160786 100644 --- a/charts/ckan/templates/_helpers.tpl +++ b/charts/ckan/templates/_helpers.tpl @@ -43,6 +43,50 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{- end -}} +{{- define "ckan.validate" -}} +{{- if and (eq .Values.database.mode "external") (not .Values.database.external.host) -}} +{{- fail "database.external.host is required when database.mode is external" -}} +{{- end -}} +{{- if and (eq .Values.database.mode "external") (not .Values.database.external.existingSecret) (not .Values.database.external.password) -}} +{{- fail "database.external.password or database.external.existingSecret is required when database.mode is external" -}} +{{- end -}} +{{- if and (eq .Values.redisConfig.mode "external") (not .Values.redisConfig.external.url) -}} +{{- fail "redisConfig.external.url is required when redisConfig.mode is external" -}} +{{- end -}} +{{- if and (not .Values.solr.enabled) (not .Values.solr.externalUrl) -}} +{{- fail "solr.externalUrl is required when solr.enabled is false" -}} +{{- end -}} +{{- if and .Values.ingress.enabled (not .Values.ingress.hosts) -}} +{{- fail "ingress.enabled requires ingress.hosts to contain at least one rule" -}} +{{- end -}} +{{- if .Values.backup.enabled -}} +{{- if not .Values.backup.s3.endpoint -}} +{{- fail "backup.s3.endpoint is required when backup.enabled is true" -}} +{{- end -}} +{{- if not .Values.backup.s3.bucket -}} +{{- fail "backup.s3.bucket is required when backup.enabled is true" -}} +{{- end -}} +{{- if and (not .Values.backup.s3.existingSecret) (not .Values.backup.s3.accessKey) -}} +{{- fail "backup.s3.accessKey or backup.s3.existingSecret is required when backup.enabled is true" -}} +{{- end -}} +{{- if and (not .Values.backup.s3.existingSecret) (not .Values.backup.s3.secretKey) -}} +{{- fail "backup.s3.secretKey or backup.s3.existingSecret is required when backup.enabled is true" -}} +{{- end -}} +{{- end -}} +{{- range $key, $_ := .Values.podLabels -}} +{{- if or (eq $key "app.kubernetes.io/name") (eq $key "app.kubernetes.io/instance") (eq $key "app.kubernetes.io/component") -}} +{{- fail (printf "podLabels must not override selector label %q" $key) -}} +{{- end -}} +{{- end -}} +{{- if .Values.datapusher.enabled -}} +{{- range $key, $_ := .Values.datapusher.podLabels -}} +{{- if or (eq $key "app.kubernetes.io/name") (eq $key "app.kubernetes.io/instance") (eq $key "app.kubernetes.io/component") -}} +{{- fail (printf "datapusher.podLabels must not override selector label %q" $key) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} + # ============================================================================= # Secret # ============================================================================= @@ -286,18 +330,8 @@ exec /srv/app/start_ckan.sh {{- end -}} {{- end -}} -{{/* Backup — validate required fields */}} {{- define "ckan.backupEnabled" -}} {{- if .Values.backup.enabled -}} - {{- if not .Values.backup.s3.endpoint -}} - {{- fail "backup.s3.endpoint is required when backup.enabled is true" -}} - {{- end -}} - {{- if not .Values.backup.s3.bucket -}} - {{- fail "backup.s3.bucket is required when backup.enabled is true" -}} - {{- end -}} - {{- if and (not .Values.backup.s3.existingSecret) (not .Values.backup.s3.accessKey) -}} - {{- fail "backup.s3.accessKey or backup.s3.existingSecret is required when backup.enabled is true" -}} - {{- end -}} true {{- end -}} {{- end -}} diff --git a/charts/ckan/templates/ingress.yaml b/charts/ckan/templates/ingress.yaml index e25703cc9..b535bf403 100644 --- a/charts/ckan/templates/ingress.yaml +++ b/charts/ckan/templates/ingress.yaml @@ -20,7 +20,9 @@ spec: {{- end }} rules: {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} + - {{- with .host }} + host: {{ . | quote }} + {{- end }} http: paths: {{- range (.paths | default (list (dict "path" "/" "pathType" "Prefix"))) }} diff --git a/charts/ckan/templates/validate.yaml b/charts/ckan/templates/validate.yaml new file mode 100644 index 000000000..eb20bb0fd --- /dev/null +++ b/charts/ckan/templates/validate.yaml @@ -0,0 +1,2 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- include "ckan.validate" . -}} diff --git a/charts/ckan/tests/backup_test.yaml b/charts/ckan/tests/backup_test.yaml index b011ea974..5a4a86de1 100644 --- a/charts/ckan/tests/backup_test.yaml +++ b/charts/ckan/tests/backup_test.yaml @@ -64,14 +64,3 @@ tests: content: name: DB_HOST value: test-postgresql - - - it: should fail without s3 endpoint - set: - backup.enabled: true - backup.s3.bucket: backups - backup.s3.accessKey: admin - backup.s3.secretKey: admin123 - template: backup-cronjob.yaml - asserts: - - failedTemplate: - errorMessage: "backup.s3.endpoint is required when backup.enabled is true" diff --git a/charts/ckan/tests/ingress_test.yaml b/charts/ckan/tests/ingress_test.yaml index 247cb05e5..3149b1e7d 100644 --- a/charts/ckan/tests/ingress_test.yaml +++ b/charts/ckan/tests/ingress_test.yaml @@ -26,3 +26,17 @@ tests: - equal: path: spec.rules[0].host value: ckan.example.com + + - it: should allow catch-all ingress rules without host + set: + ingress: + enabled: true + hosts: + - paths: + - path: / + pathType: Prefix + asserts: + - isKind: + of: Ingress + - notExists: + path: spec.rules[0].host diff --git a/charts/ckan/tests/validation_test.yaml b/charts/ckan/tests/validation_test.yaml new file mode 100644 index 000000000..8970e4e0b --- /dev/null +++ b/charts/ckan/tests/validation_test.yaml @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: Validation +templates: + - templates/validate.yaml +tests: + - it: should render no validation manifest by default + asserts: + - hasDocuments: + count: 0 + + - it: should fail when external database host is missing + set: + database.mode: external + database.external.password: secret + asserts: + - failedTemplate: + errorMessage: "database.external.host is required when database.mode is external" + + - it: should fail when external database credentials are missing + set: + database.mode: external + database.external.host: postgresql.example.com + asserts: + - failedTemplate: + errorMessage: "database.external.password or database.external.existingSecret is required when database.mode is external" + + - it: should fail when external redis url is missing + set: + redisConfig.mode: external + asserts: + - failedTemplate: + errorMessage: "redisConfig.external.url is required when redisConfig.mode is external" + + - it: should fail when external solr url is missing + set: + solr.enabled: false + asserts: + - failedTemplate: + errorMessage: "solr.externalUrl is required when solr.enabled is false" + + - it: should fail when ingress is enabled without rules + set: + ingress.enabled: true + ingress.hosts: [] + asserts: + - failedTemplate: + errorMessage: "ingress.enabled requires ingress.hosts to contain at least one rule" + + - it: should fail when backup endpoint is missing + set: + backup.enabled: true + backup.s3.bucket: backups + backup.s3.accessKey: access + backup.s3.secretKey: secret + asserts: + - failedTemplate: + errorMessage: "backup.s3.endpoint is required when backup.enabled is true" + + - it: should fail when backup secret key is missing + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.bucket: backups + backup.s3.accessKey: access + asserts: + - failedTemplate: + errorMessage: "backup.s3.secretKey or backup.s3.existingSecret is required when backup.enabled is true" + + - it: should fail when podLabels override selector labels + set: + podLabels: + app.kubernetes.io/component: custom + asserts: + - failedTemplate: + errorMessage: 'podLabels must not override selector label "app.kubernetes.io/component"' + + - it: should fail when datapusher podLabels override selector labels + set: + datapusher.podLabels: + app.kubernetes.io/name: custom + asserts: + - failedTemplate: + errorMessage: 'datapusher.podLabels must not override selector label "app.kubernetes.io/name"' + + - it: should allow datapusher podLabels when datapusher is disabled + set: + datapusher.enabled: false + datapusher.podLabels: + app.kubernetes.io/name: custom + asserts: + - hasDocuments: + count: 0 From c2f05339e5ca2b7352977c838df1b695566557c1 Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Sun, 5 Jul 2026 19:21:09 -0300 Subject: [PATCH 2/2] test(ckan): cover backup validation branches --- charts/ckan/tests/validation_test.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/charts/ckan/tests/validation_test.yaml b/charts/ckan/tests/validation_test.yaml index 8970e4e0b..5329c7acc 100644 --- a/charts/ckan/tests/validation_test.yaml +++ b/charts/ckan/tests/validation_test.yaml @@ -56,6 +56,26 @@ tests: - failedTemplate: errorMessage: "backup.s3.endpoint is required when backup.enabled is true" + - it: should fail when backup bucket is missing + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.accessKey: access + backup.s3.secretKey: secret + asserts: + - failedTemplate: + errorMessage: "backup.s3.bucket is required when backup.enabled is true" + + - it: should fail when backup access key is missing + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.bucket: backups + backup.s3.secretKey: secret + asserts: + - failedTemplate: + errorMessage: "backup.s3.accessKey or backup.s3.existingSecret is required when backup.enabled is true" + - it: should fail when backup secret key is missing set: backup.enabled: true