diff --git a/charts/countly/ci/dual-stack.yaml b/charts/countly/ci/dual-stack.yaml index 8d752e4ba..54b6a2b26 100644 --- a/charts/countly/ci/dual-stack.yaml +++ b/charts/countly/ci/dual-stack.yaml @@ -1,14 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # CI scenario: dual-stack for HTTP service (GR-058) -mongodb: - enabled: false - -externalMongodb: - enabled: true - uri: mongodb://external-mongo:27017/countly - +# Keep this scenario portable across IPv4-only and dual-stack k3d clusters. +# Explicit ipFamilies rendering is covered by unit tests because Kubernetes +# rejects IPv6 entries on clusters that do not advertise IPv6. service: ipFamilyPolicy: PreferDualStack - ipFamilies: - - IPv4 - - IPv6 diff --git a/charts/countly/ci/external-mongodb-values.yaml b/charts/countly/ci/external-mongodb-values.yaml index 21a621c82..b98933d81 100644 --- a/charts/countly/ci/external-mongodb-values.yaml +++ b/charts/countly/ci/external-mongodb-values.yaml @@ -1,8 +1,25 @@ # SPDX-License-Identifier: Apache-2.0 -# CI: external MongoDB +# CI: external MongoDB, backed by the bundled MongoDB service for k3d. mongodb: - enabled: false + enabled: true + auth: + rootPassword: test-password externalMongodb: enabled: true - uri: "mongodb://user:password@external-mongodb:27017/countly?authSource=admin" + uri: "mongodb://root:test-password@countly-external-mongodb:27017/countly?authSource=admin" + +extraManifests: + - apiVersion: v1 + kind: Service + metadata: + name: countly-external-mongodb + spec: + type: ClusterIP + ports: + - name: mongodb + port: 27017 + targetPort: mongodb + selector: + app.kubernetes.io/name: mongodb + app.kubernetes.io/component: mongodb diff --git a/charts/countly/ci/external-secrets.yaml b/charts/countly/ci/external-secrets.yaml index dd8823c2e..ba825fc40 100644 --- a/charts/countly/ci/external-secrets.yaml +++ b/charts/countly/ci/external-secrets.yaml @@ -1,7 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 # CI scenario: ExternalSecrets with drift guard (GR-061) mongodb: - enabled: false + enabled: true + auth: + rootPassword: test-password externalMongodb: enabled: true @@ -11,10 +13,33 @@ externalMongodb: externalSecrets: enabled: true secretStoreRef: - name: my-store + name: helmforge-fake-store kind: ClusterSecretStore + target: + creationPolicy: None data: - secretKey: mongodb-uri remoteRef: - key: countly/db - property: uri + key: test/password + +extraManifests: + - apiVersion: v1 + kind: Secret + metadata: + name: countly-mongo-credentials + type: Opaque + stringData: + mongodb-uri: mongodb://root:test-password@countly-external-mongodb:27017/countly?authSource=admin + - apiVersion: v1 + kind: Service + metadata: + name: countly-external-mongodb + spec: + type: ClusterIP + ports: + - name: mongodb + port: 27017 + targetPort: mongodb + selector: + app.kubernetes.io/name: mongodb + app.kubernetes.io/component: mongodb diff --git a/charts/countly/ci/gateway-api.yaml b/charts/countly/ci/gateway-api.yaml index bc8f49e7e..42f991bcb 100644 --- a/charts/countly/ci/gateway-api.yaml +++ b/charts/countly/ci/gateway-api.yaml @@ -1,12 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 # CI scenario: Gateway API (GR-060) -mongodb: - enabled: false - -externalMongodb: - enabled: true - uri: mongodb://external-mongo:27017/countly - gateway: enabled: true hostnames: diff --git a/charts/countly/templates/_helpers.tpl b/charts/countly/templates/_helpers.tpl index 7c806ef52..031423f30 100644 --- a/charts/countly/templates/_helpers.tpl +++ b/charts/countly/templates/_helpers.tpl @@ -43,14 +43,61 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- printf "%s:%s" .Values.image.repository .Values.image.tag -}} {{- end -}} +{{- define "countly.validate" -}} +{{- if and .Values.externalMongodb.enabled (not .Values.externalMongodb.uri) (not .Values.externalMongodb.existingSecret) -}} +{{- fail "externalMongodb.uri or externalMongodb.existingSecret is required when externalMongodb.enabled=true" -}} +{{- end -}} +{{- if and .Values.ingress.enabled (not .Values.ingress.hosts) -}} +{{- fail "ingress.hosts must contain at least one rule when ingress.enabled=true" -}} +{{- end -}} +{{- if and .Values.gateway.enabled (not .Values.gateway.parentRefs) -}} +{{- fail "gateway.enabled requires gateway.parentRefs to be populated to create a valid HTTPRoute." -}} +{{- end -}} +{{- if .Values.externalSecrets.enabled -}} + {{- if not .Values.externalMongodb.enabled -}} + {{- fail "externalSecrets.enabled requires externalMongodb.enabled=true; the Deployment only mounts the MongoDB secret when external MongoDB is active." -}} + {{- end -}} + {{- if not .Values.externalMongodb.existingSecret -}} + {{- fail "externalSecrets.enabled requires externalMongodb.existingSecret to be set to prevent credential drift between the chart-managed Secret and the ExternalSecret." -}} + {{- end -}} + {{- if not .Values.externalSecrets.secretStoreRef.name -}} + {{- fail "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" -}} + {{- end -}} + {{- if not .Values.externalSecrets.data -}} + {{- fail "externalSecrets.data must not be empty when externalSecrets.enabled=true" -}} + {{- end -}} + {{- $uriKey := .Values.externalMongodb.existingSecretUriKey | default "mongodb-uri" -}} + {{- $hasUri := false -}} + {{- range .Values.externalSecrets.data -}} + {{- if eq .secretKey $uriKey }}{{ $hasUri = true }}{{ end -}} + {{- end -}} + {{- if not $hasUri -}} + {{- fail (printf "externalSecrets.data must include a mapping for key '%s' (MongoDB connection URI)" $uriKey) -}} + {{- end -}} +{{- end -}} +{{- if .Values.backup.enabled -}} + {{- $_ := include "countly.backupEnabled" . -}} + {{- $_ := include "countly.backupMongodbUri" . -}} +{{- end -}} +{{- $podLabels := .Values.podLabels | default dict -}} +{{- $selectorLabels := include "countly.selectorLabels" . | fromYaml -}} +{{- range $key, $_ := $selectorLabels -}} +{{- if hasKey $podLabels $key -}} +{{- fail (printf "podLabels must not override selector label %q" $key) -}} +{{- end -}} +{{- end -}} +{{- end -}} + {{/* MongoDB host */}} {{- define "countly.mongodbHost" -}} -{{- if .Values.externalMongodb.enabled -}} -{{- "" -}} +{{- if .Values.mongodb.enabled -}} +{{- if contains "mongodb" .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} {{- else -}} {{- printf "%s-mongodb" .Release.Name -}} {{- end -}} {{- end -}} +{{- end -}} {{/* MongoDB URI */}} {{- define "countly.mongodbUri" -}} @@ -91,6 +138,9 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- 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 -}} true {{- end -}} {{- end -}} diff --git a/charts/countly/templates/ingress.yaml b/charts/countly/templates/ingress.yaml index 33c7b95b6..206d8833b 100644 --- a/charts/countly/templates/ingress.yaml +++ b/charts/countly/templates/ingress.yaml @@ -26,7 +26,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/countly/templates/validate.yaml b/charts/countly/templates/validate.yaml new file mode 100644 index 000000000..b1c41f101 --- /dev/null +++ b/charts/countly/templates/validate.yaml @@ -0,0 +1,2 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- include "countly.validate" . -}} diff --git a/charts/countly/tests/deployment_test.yaml b/charts/countly/tests/deployment_test.yaml index be4f032cd..f1edc2a18 100644 --- a/charts/countly/tests/deployment_test.yaml +++ b/charts/countly/tests/deployment_test.yaml @@ -108,6 +108,15 @@ tests: - notExists: path: spec.template.spec.initContainers + - it: should not wait for bundled MongoDB when external MongoDB is enabled + set: + mongodb.enabled: true + externalMongodb.enabled: true + externalMongodb.uri: "mongodb://root:test-password@test-mongodb:27017/countly?authSource=admin" + asserts: + - notExists: + path: spec.template.spec.initContainers + - it: should use TCP probes on dashboard port asserts: - equal: diff --git a/charts/countly/tests/ingress_test.yaml b/charts/countly/tests/ingress_test.yaml index b4e41475e..71d8745a6 100644 --- a/charts/countly/tests/ingress_test.yaml +++ b/charts/countly/tests/ingress_test.yaml @@ -22,3 +22,16 @@ tests: asserts: - isKind: of: Ingress + + - it: should support hostless catch-all rules + set: + ingress.enabled: true + ingress.hosts: + - paths: + - path: / + pathType: Prefix + asserts: + - isKind: + of: Ingress + - notExists: + path: spec.rules[0].host diff --git a/charts/countly/tests/service_test.yaml b/charts/countly/tests/service_test.yaml index f9e464a8c..b3a3c2dbb 100644 --- a/charts/countly/tests/service_test.yaml +++ b/charts/countly/tests/service_test.yaml @@ -30,3 +30,20 @@ tests: port: 3001 targetPort: api protocol: TCP + + - it: should render dual-stack service fields when configured + set: + service.ipFamilyPolicy: PreferDualStack + service.ipFamilies: + - IPv4 + - IPv6 + asserts: + - equal: + path: spec.ipFamilyPolicy + value: PreferDualStack + - equal: + path: spec.ipFamilies[0] + value: IPv4 + - equal: + path: spec.ipFamilies[1] + value: IPv6 diff --git a/charts/countly/tests/validation_test.yaml b/charts/countly/tests/validation_test.yaml new file mode 100644 index 000000000..86c37d808 --- /dev/null +++ b/charts/countly/tests/validation_test.yaml @@ -0,0 +1,169 @@ +# 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 MongoDB has no URI or secret + set: + mongodb.enabled: false + externalMongodb.enabled: true + asserts: + - failedTemplate: + errorMessage: "externalMongodb.uri or externalMongodb.existingSecret is required when externalMongodb.enabled=true" + + - it: should fail when ingress has no rules + set: + ingress.enabled: true + ingress.hosts: [] + asserts: + - failedTemplate: + errorMessage: "ingress.hosts must contain at least one rule when ingress.enabled=true" + + - it: should fail when gateway has no parent references + set: + gateway.enabled: true + asserts: + - failedTemplate: + errorMessage: "gateway.enabled requires gateway.parentRefs to be populated to create a valid HTTPRoute." + + - it: should fail when external secrets is enabled without external MongoDB + set: + externalSecrets.enabled: true + externalSecrets.secretStoreRef.name: helmforge-fake-store + externalSecrets.data: + - secretKey: mongodb-uri + remoteRef: + key: countly/mongodb + asserts: + - failedTemplate: + errorMessage: "externalSecrets.enabled requires externalMongodb.enabled=true; the Deployment only mounts the MongoDB secret when external MongoDB is active." + + - it: should fail when external secrets has no target secret + set: + mongodb.enabled: false + externalMongodb.enabled: true + externalMongodb.uri: mongodb://mongodb.example.com:27017/countly + externalSecrets.enabled: true + externalSecrets.secretStoreRef.name: helmforge-fake-store + externalSecrets.data: + - secretKey: mongodb-uri + remoteRef: + key: countly/mongodb + asserts: + - failedTemplate: + errorMessage: "externalSecrets.enabled requires externalMongodb.existingSecret to be set to prevent credential drift between the chart-managed Secret and the ExternalSecret." + + - it: should fail when external secrets has no store reference + set: + mongodb.enabled: false + externalMongodb.enabled: true + externalMongodb.existingSecret: countly-mongodb + externalSecrets.enabled: true + externalSecrets.data: + - secretKey: mongodb-uri + remoteRef: + key: countly/mongodb + asserts: + - failedTemplate: + errorMessage: "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" + + - it: should fail when external secrets has no data mappings + set: + mongodb.enabled: false + externalMongodb.enabled: true + externalMongodb.existingSecret: countly-mongodb + externalSecrets.enabled: true + externalSecrets.secretStoreRef.name: helmforge-fake-store + asserts: + - failedTemplate: + errorMessage: "externalSecrets.data must not be empty when externalSecrets.enabled=true" + + - it: should fail when external secrets misses the MongoDB URI key + set: + mongodb.enabled: false + externalMongodb.enabled: true + externalMongodb.existingSecret: countly-mongodb + externalSecrets.enabled: true + externalSecrets.secretStoreRef.name: helmforge-fake-store + externalSecrets.data: + - secretKey: other + remoteRef: + key: countly/mongodb + asserts: + - failedTemplate: + errorMessage: "externalSecrets.data must include a mapping for key 'mongodb-uri' (MongoDB connection URI)" + + - it: should fail when backup has no endpoint + set: + backup.enabled: true + backup.s3.bucket: backups + backup.s3.accessKey: admin + backup.s3.secretKey: admin123 + asserts: + - failedTemplate: + errorMessage: "backup.s3.endpoint is required when backup.enabled is true" + + - it: should fail when backup has no bucket + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.accessKey: admin + backup.s3.secretKey: admin123 + asserts: + - failedTemplate: + errorMessage: "backup.s3.bucket is required when backup.enabled is true" + + - it: should fail when backup has no access key or existing secret + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.bucket: backups + backup.s3.secretKey: admin123 + asserts: + - failedTemplate: + errorMessage: "backup.s3.accessKey or backup.s3.existingSecret is required when backup.enabled is true" + + - it: should fail when backup has no secret key or existing secret + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.bucket: backups + backup.s3.accessKey: admin + asserts: + - failedTemplate: + errorMessage: "backup.s3.secretKey or backup.s3.existingSecret is required when backup.enabled is true" + + - it: should fail when external secret MongoDB URI cannot be injected into backup + set: + backup.enabled: true + backup.s3.endpoint: http://minio:9000 + backup.s3.bucket: backups + backup.s3.accessKey: admin + backup.s3.secretKey: admin123 + mongodb.enabled: false + externalMongodb.enabled: true + externalMongodb.existingSecret: countly-mongodb + asserts: + - failedTemplate: + errorMessage: "backup requires backup.database.uri when externalMongodb.existingSecret is set and externalMongodb.uri is empty (the URI is in the secret and cannot be injected into mongodump)" + + - it: should fail when podLabels override selector labels + set: + podLabels: + app.kubernetes.io/name: custom + asserts: + - failedTemplate: + errorMessage: 'podLabels must not override selector label "app.kubernetes.io/name"' + + - it: should fail when podLabels override release selector labels + set: + podLabels: + app.kubernetes.io/instance: custom + asserts: + - failedTemplate: + errorMessage: 'podLabels must not override selector label "app.kubernetes.io/instance"'