From a9d3e38519d7a2d0b99aa6349d4587b439bab074 Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Tue, 7 Jul 2026 06:09:31 -0300 Subject: [PATCH 1/6] feat(matomo)!: add chart --- charts/matomo/.helmignore | 14 + charts/matomo/Chart.yaml | 56 ++++ charts/matomo/DESIGN.md | 43 +++ charts/matomo/README.md | 126 ++++++++ charts/matomo/ci/default-values.yaml | 3 + charts/matomo/ci/dual-stack.yaml | 3 + charts/matomo/ci/external-db-values.yaml | 21 ++ charts/matomo/ci/external-secrets-values.yaml | 34 +++ charts/matomo/ci/gateway-api-values.yaml | 10 + charts/matomo/ci/ingress-values.yaml | 15 + charts/matomo/ci/network-policy-values.yaml | 12 + charts/matomo/ci/production-values.yaml | 21 ++ charts/matomo/docs/external-secrets.md | 49 ++++ charts/matomo/docs/networking.md | 38 +++ charts/matomo/docs/production.md | 42 +++ charts/matomo/examples/external-database.yaml | 18 ++ charts/matomo/examples/gateway-api.yaml | 15 + charts/matomo/examples/production.yaml | 31 ++ charts/matomo/examples/simple.yaml | 8 + charts/matomo/templates/NOTES.txt | 102 +++++++ charts/matomo/templates/_helpers.tpl | 181 ++++++++++++ charts/matomo/templates/configmap.yaml | 13 + charts/matomo/templates/cronjob-archiver.yaml | 79 +++++ charts/matomo/templates/deployment.yaml | 179 ++++++++++++ charts/matomo/templates/externalsecret.yaml | 34 +++ charts/matomo/templates/extra-manifests.yaml | 5 + .../matomo/templates/gateway-httproute.yaml | 56 ++++ charts/matomo/templates/ingress.yaml | 38 +++ charts/matomo/templates/networkpolicy.yaml | 49 ++++ charts/matomo/templates/pdb.yaml | 19 ++ charts/matomo/templates/pvc.yaml | 23 ++ charts/matomo/templates/secret.yaml | 13 + charts/matomo/templates/service.yaml | 28 ++ charts/matomo/templates/serviceaccount.yaml | 15 + charts/matomo/templates/servicemonitor.yaml | 26 ++ .../templates/tests/test-connection.yaml | 39 +++ charts/matomo/templates/validate.yaml | 2 + charts/matomo/tests/archiver_test.yaml | 32 +++ charts/matomo/tests/deployment_test.yaml | 33 +++ charts/matomo/tests/externalsecret_test.yaml | 32 +++ charts/matomo/tests/networkpolicy_test.yaml | 32 +++ charts/matomo/tests/pdb_test.yaml | 18 ++ charts/matomo/tests/routing_test.yaml | 41 +++ charts/matomo/tests/service_test.yaml | 35 +++ charts/matomo/tests/test_connection_test.yaml | 15 + charts/matomo/tests/validation_test.yaml | 27 ++ charts/matomo/values.schema.json | 168 +++++++++++ charts/matomo/values.yaml | 271 ++++++++++++++++++ 48 files changed, 2164 insertions(+) create mode 100644 charts/matomo/.helmignore create mode 100644 charts/matomo/Chart.yaml create mode 100644 charts/matomo/DESIGN.md create mode 100644 charts/matomo/README.md create mode 100644 charts/matomo/ci/default-values.yaml create mode 100644 charts/matomo/ci/dual-stack.yaml create mode 100644 charts/matomo/ci/external-db-values.yaml create mode 100644 charts/matomo/ci/external-secrets-values.yaml create mode 100644 charts/matomo/ci/gateway-api-values.yaml create mode 100644 charts/matomo/ci/ingress-values.yaml create mode 100644 charts/matomo/ci/network-policy-values.yaml create mode 100644 charts/matomo/ci/production-values.yaml create mode 100644 charts/matomo/docs/external-secrets.md create mode 100644 charts/matomo/docs/networking.md create mode 100644 charts/matomo/docs/production.md create mode 100644 charts/matomo/examples/external-database.yaml create mode 100644 charts/matomo/examples/gateway-api.yaml create mode 100644 charts/matomo/examples/production.yaml create mode 100644 charts/matomo/examples/simple.yaml create mode 100644 charts/matomo/templates/NOTES.txt create mode 100644 charts/matomo/templates/_helpers.tpl create mode 100644 charts/matomo/templates/configmap.yaml create mode 100644 charts/matomo/templates/cronjob-archiver.yaml create mode 100644 charts/matomo/templates/deployment.yaml create mode 100644 charts/matomo/templates/externalsecret.yaml create mode 100644 charts/matomo/templates/extra-manifests.yaml create mode 100644 charts/matomo/templates/gateway-httproute.yaml create mode 100644 charts/matomo/templates/ingress.yaml create mode 100644 charts/matomo/templates/networkpolicy.yaml create mode 100644 charts/matomo/templates/pdb.yaml create mode 100644 charts/matomo/templates/pvc.yaml create mode 100644 charts/matomo/templates/secret.yaml create mode 100644 charts/matomo/templates/service.yaml create mode 100644 charts/matomo/templates/serviceaccount.yaml create mode 100644 charts/matomo/templates/servicemonitor.yaml create mode 100644 charts/matomo/templates/tests/test-connection.yaml create mode 100644 charts/matomo/templates/validate.yaml create mode 100644 charts/matomo/tests/archiver_test.yaml create mode 100644 charts/matomo/tests/deployment_test.yaml create mode 100644 charts/matomo/tests/externalsecret_test.yaml create mode 100644 charts/matomo/tests/networkpolicy_test.yaml create mode 100644 charts/matomo/tests/pdb_test.yaml create mode 100644 charts/matomo/tests/routing_test.yaml create mode 100644 charts/matomo/tests/service_test.yaml create mode 100644 charts/matomo/tests/test_connection_test.yaml create mode 100644 charts/matomo/tests/validation_test.yaml create mode 100644 charts/matomo/values.schema.json create mode 100644 charts/matomo/values.yaml diff --git a/charts/matomo/.helmignore b/charts/matomo/.helmignore new file mode 100644 index 000000000..31c27db2b --- /dev/null +++ b/charts/matomo/.helmignore @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 +.DS_Store +Thumbs.db +.git/ +.gitignore +.gitattributes +.helmignore +.vscode/ +.idea/ +*.orig +*.rej +*.swp +*.tmp +*.log diff --git a/charts/matomo/Chart.yaml b/charts/matomo/Chart.yaml new file mode 100644 index 000000000..c00356e7f --- /dev/null +++ b/charts/matomo/Chart.yaml @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: Apache-2.0 +apiVersion: v2 +name: matomo +description: Privacy-focused web analytics platform with MySQL and production archiving support +type: application +version: 0.1.0 +appVersion: "5.11.2" +kubeVersion: ">=1.26.0-0" +maintainers: + - name: helmforgedev + - name: mberlofa +keywords: + - matomo + - analytics + - privacy + - mysql + - kubernetes +home: https://helmforge.dev/docs/charts/matomo +sources: + - https://github.com/helmforgedev/charts + - https://github.com/matomo-org/matomo + - https://hub.docker.com/_/matomo +icon: https://helmforge.dev/icons/charts/matomo.png +annotations: + artifacthub.io/changes: | + - kind: added + description: "add production-ready Matomo chart (#654)" + artifacthub.io/maintainers: | + - name: HelmForge + email: berlofa@helmforge.dev + - name: Maicon Berlofa + email: maicon.berloffa@gmail.com + helmforge.dev/maturity: beta + helmforge.dev/signed: gpg+cosign + artifacthub.io/signKey: | + fingerprint: 6748042FBAD8C2E11C65E564170D55148EB88A9D + url: https://repo.helmforge.dev/pgp-public-key.asc + helmforge.dev/repository: https://repo.helmforge.dev + artifacthub.io/license: Apache-2.0 + artifacthub.io/category: monitoring-logging + artifacthub.io/links: | + - name: Official Website + url: https://matomo.org + - name: Documentation + url: https://helmforge.dev/docs/charts/matomo + - name: Source + url: https://github.com/helmforgedev/charts/tree/main/charts/matomo + - name: support + url: https://github.com/helmforgedev/charts/issues + artifacthub.io/recommendations: | + - url: https://artifacthub.io/packages/helm/helmforge/mysql +dependencies: + - name: mysql + version: 2.0.2 + repository: oci://ghcr.io/helmforgedev/helm + condition: mysql.enabled diff --git a/charts/matomo/DESIGN.md b/charts/matomo/DESIGN.md new file mode 100644 index 000000000..72da57d87 --- /dev/null +++ b/charts/matomo/DESIGN.md @@ -0,0 +1,43 @@ +# Matomo Chart Design + +## Scope + +This chart deploys the official Matomo Apache image, a web Service, optional +Ingress/Gateway API exposure, persistent application storage, a MySQL subchart +or external MySQL/MariaDB connection, and a CronJob for Matomo archiving. + +## Differentiation + +- Uses the official `docker.io/library/matomo` image instead of vendor images. +- Makes the production archiver CronJob first-class. +- Keeps external database and External Secrets flows explicit for managed DBs. +- Supports dual-stack Service fields, Gateway API, and NetworkPolicy defaults. + +## Non-goals + +- It does not embed Redis session management by default because Matomo session + topology varies by installation. +- It does not manage Matomo plugins or marketplace lifecycle. +- It does not configure Matomo trusted proxy settings inside the database; the + chart documents the reverse proxy requirement and exposes `extraEnv`. + +## Validation Notes + +The default profile is intentionally small enough for k3d validation while still +using persistent storage and the real Matomo image. Production users should move +database state to an external managed MySQL/MariaDB service and retain both DB +and application volume backups. + + diff --git a/charts/matomo/README.md b/charts/matomo/README.md new file mode 100644 index 000000000..5b1ffea0b --- /dev/null +++ b/charts/matomo/README.md @@ -0,0 +1,126 @@ +# Matomo + +Privacy-focused web analytics platform with MySQL and production archiving +support. + +## Installation + +```bash +helm repo add helmforge https://repo.helmforge.dev +helm install matomo helmforge/matomo +``` + +```bash +helm install matomo oci://ghcr.io/helmforgedev/helm/matomo +``` + +## Features + +- Official Matomo Apache image pinned to `5.11.2-apache`. +- Bundled HelmForge MySQL subchart for simple deployments. +- External MySQL/MariaDB mode for production. +- CronJob-based `core:archive` execution. +- Persistent Matomo application volume. +- Ingress and Gateway API HTTPRoute support. +- NetworkPolicy, ServiceMonitor, dual-stack Service fields. +- External Secrets integration for database passwords. + +## Quick Start + +```yaml +matomo: + siteUrl: http://matomo.local +mysql: + enabled: true +``` + +## Production Example + +```yaml +mysql: + enabled: false +database: + mode: external + external: + host: mysql.prod.svc.cluster.local + name: matomo + username: matomo + existingSecret: matomo-database +matomo: + siteUrl: https://analytics.example.com +archiver: + enabled: true + schedule: "5 * * * *" +ingress: + enabled: true + ingressClassName: nginx + hosts: + - host: analytics.example.com +``` + +## Configuration + +| Parameter | Description | Default | +| --- | --- | --- | +| `replicaCount` | Matomo web replicas | `1` | +| `image.repository` | Official Matomo image repository | `docker.io/library/matomo` | +| `image.tag` | Official Matomo image tag | `5.11.2-apache` | +| `database.mode` | `auto`, `external`, or `mysql` | `auto` | +| `mysql.enabled` | Deploy HelmForge MySQL subchart | `true` | +| `persistence.enabled` | Persist `/var/www/html` | `true` | +| `archiver.enabled` | Create archive CronJob | `true` | +| `ingress.enabled` | Render Ingress | `false` | +| `gatewayAPI.enabled` | Render HTTPRoute resources | `false` | +| `externalSecrets.enabled` | Render ExternalSecret resources | `false` | +| `networkPolicy.enabled` | Render NetworkPolicy | `false` | +| `service.ipFamilyPolicy` | Service dual-stack policy | `""` | + +## Examples + +- [Simple](examples/simple.yaml) +- [External database](examples/external-database.yaml) +- [Production](examples/production.yaml) +- [Gateway API](examples/gateway-api.yaml) + +## Architecture Guides + +- [Production](docs/production.md) +- [Networking](docs/networking.md) +- [External Secrets](docs/external-secrets.md) + +## Security Scan + +### Security Scan: `matomo` + +| Framework | Score | +|---|---| +| MITRE + NSA + SOC2 | **84.78%** | + +Security posture acceptable. + +Local details: + +- Tool: Kubescape v4.0.9 +- Command: `kubescape scan framework mitre,nsa,soc2 .tmp\matomo-render.yaml` +- Result: 0 critical failed resources, resource summary score 84.78%. + +## Research Notes + +Matomo production guidance requires MySQL or MariaDB, persistent application +files, scheduled report archiving, and careful reverse proxy handling. The chart +keeps these operational concerns explicit rather than hiding them behind a +minimal deployment. + + diff --git a/charts/matomo/ci/default-values.yaml b/charts/matomo/ci/default-values.yaml new file mode 100644 index 000000000..9d7035501 --- /dev/null +++ b/charts/matomo/ci/default-values.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +matomo: + siteUrl: http://matomo.local diff --git a/charts/matomo/ci/dual-stack.yaml b/charts/matomo/ci/dual-stack.yaml new file mode 100644 index 000000000..a11cf17a6 --- /dev/null +++ b/charts/matomo/ci/dual-stack.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +service: + ipFamilyPolicy: PreferDualStack diff --git a/charts/matomo/ci/external-db-values.yaml b/charts/matomo/ci/external-db-values.yaml new file mode 100644 index 000000000..18d076f79 --- /dev/null +++ b/charts/matomo/ci/external-db-values.yaml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +mysql: + enabled: false +database: + mode: external + waitForConnection: + enabled: false + external: + host: mysql.example.internal + port: 3306 + name: matomo + username: matomo + password: test-password +matomo: + siteUrl: https://analytics.example.com +startupProbe: + enabled: false +livenessProbe: + enabled: false +readinessProbe: + enabled: false diff --git a/charts/matomo/ci/external-secrets-values.yaml b/charts/matomo/ci/external-secrets-values.yaml new file mode 100644 index 000000000..bb82ef78f --- /dev/null +++ b/charts/matomo/ci/external-secrets-values.yaml @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: Apache-2.0 +mysql: + enabled: false +database: + mode: external + waitForConnection: + enabled: false + external: + host: mysql.example.internal + name: matomo + username: matomo + existingSecret: matomo-database +externalSecrets: + enabled: true + refreshInterval: 30m + items: + - fullnameOverride: matomo-database + spec: + secretStoreRef: + name: helmforge-fake-store + kind: ClusterSecretStore + target: + name: matomo-database + creationPolicy: Owner + data: + - secretKey: database-password + remoteRef: + key: test/password +startupProbe: + enabled: false +livenessProbe: + enabled: false +readinessProbe: + enabled: false diff --git a/charts/matomo/ci/gateway-api-values.yaml b/charts/matomo/ci/gateway-api-values.yaml new file mode 100644 index 000000000..82c8f425c --- /dev/null +++ b/charts/matomo/ci/gateway-api-values.yaml @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +gatewayAPI: + enabled: true + httpRoutes: + - name: public + parentRefs: + - name: shared-gateway + namespace: gateway-system + hostnames: + - matomo.example.com diff --git a/charts/matomo/ci/ingress-values.yaml b/charts/matomo/ci/ingress-values.yaml new file mode 100644 index 000000000..eab3781ba --- /dev/null +++ b/charts/matomo/ci/ingress-values.yaml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +ingress: + enabled: true + ingressClassName: nginx + hosts: + - host: matomo.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: matomo-tls + hosts: + - matomo.example.com +matomo: + trustedHost: matomo.example.com diff --git a/charts/matomo/ci/network-policy-values.yaml b/charts/matomo/ci/network-policy-values.yaml new file mode 100644 index 000000000..a2c9811cb --- /dev/null +++ b/charts/matomo/ci/network-policy-values.yaml @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +networkPolicy: + enabled: true + egress: + enabled: true + extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.0/8 + ports: + - protocol: TCP + port: 443 diff --git a/charts/matomo/ci/production-values.yaml b/charts/matomo/ci/production-values.yaml new file mode 100644 index 000000000..43e80f1ee --- /dev/null +++ b/charts/matomo/ci/production-values.yaml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +replicaCount: 2 +matomo: + siteUrl: https://analytics.example.com +archiver: + schedule: "15 * * * *" +podDisruptionBudget: + enabled: true +resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: matomo diff --git a/charts/matomo/docs/external-secrets.md b/charts/matomo/docs/external-secrets.md new file mode 100644 index 000000000..f834285f2 --- /dev/null +++ b/charts/matomo/docs/external-secrets.md @@ -0,0 +1,49 @@ +# External Secrets + +The chart implements the HelmForge canonical `externalSecrets.items[]` contract. +Use it to project database passwords from Vault, AWS Secrets Manager, Google +Secret Manager, Azure Key Vault, or another external-secrets provider. + +## Database Password + +For an external database, set `database.external.existingSecret` to the target +secret name and render an ExternalSecret item that writes the same secret. + +```yaml +mysql: + enabled: false +database: + mode: external + external: + host: mysql.prod.svc.cluster.local + existingSecret: matomo-database +externalSecrets: + enabled: true + items: + - fullnameOverride: matomo-database + spec: + secretStoreRef: + name: prod-secrets + kind: ClusterSecretStore + target: + name: matomo-database + creationPolicy: Owner + data: + - secretKey: database-password + remoteRef: + key: prod/matomo/database +``` + + diff --git a/charts/matomo/docs/networking.md b/charts/matomo/docs/networking.md new file mode 100644 index 000000000..5f257675f --- /dev/null +++ b/charts/matomo/docs/networking.md @@ -0,0 +1,38 @@ +# Networking + +The Matomo chart supports both Kubernetes Ingress and Gateway API HTTPRoute. +Only the HTTP service is exposed; MySQL remains internal or external to the +chart depending on `database.mode`. + +## Ingress + +Set `ingress.enabled=true`, provide `ingress.hosts`, and optionally set +`ingress.ingressClassName`. The field is omitted when empty so the cluster +default IngressClass can apply. + +## Gateway API + +Set `gatewayAPI.enabled=true` and provide one or more `httpRoutes`. Each route +can supply `parentRefs`, `hostnames`, labels, annotations, and optional rules. +When rules do not specify a backend, the chart points the route to the Matomo +Service automatically. + +## Dual-stack + +Set `service.ipFamilyPolicy=PreferDualStack` to request dual-stack service +allocation without forcing explicit IP families. This keeps the values usable +on single-stack clusters. + + diff --git a/charts/matomo/docs/production.md b/charts/matomo/docs/production.md new file mode 100644 index 000000000..0d993e450 --- /dev/null +++ b/charts/matomo/docs/production.md @@ -0,0 +1,42 @@ +# Production Guide + +Matomo has two production-critical runtime domains: MySQL or MariaDB for +analytics data, and the Matomo application volume for configuration, plugins, +and generated assets. Back up both before upgrades. + +## Database + +For production, prefer `database.mode=external` with a managed MySQL or MariaDB +service. The bundled MySQL subchart is useful for development and controlled +single-cluster installations. + +## Archiving + +Enable the chart-managed `archiver` CronJob so reports are precomputed outside +interactive user traffic. Start with the upstream hourly schedule at minute 5 +and tune resources when archive runs overlap. + +## Scaling + +The chart allows multiple web replicas, but Matomo installations must ensure +safe shared storage and session handling first. Keep `replicaCount=1` until +those concerns are handled explicitly. + +## Reverse Proxy + +When Matomo runs behind Ingress or Gateway API, configure trusted proxy headers +inside Matomo after installation so HTTPS and client IP detection are correct. + + diff --git a/charts/matomo/examples/external-database.yaml b/charts/matomo/examples/external-database.yaml new file mode 100644 index 000000000..ebf5e1f97 --- /dev/null +++ b/charts/matomo/examples/external-database.yaml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Production pattern using an external managed MySQL or MariaDB service. +mysql: + enabled: false +database: + mode: external + external: + host: mysql.prod.svc.cluster.local + port: 3306 + name: matomo + username: matomo + existingSecret: matomo-database + existingSecretPasswordKey: database-password +matomo: + siteUrl: https://analytics.example.com +archiver: + enabled: true + schedule: "5 * * * *" diff --git a/charts/matomo/examples/gateway-api.yaml b/charts/matomo/examples/gateway-api.yaml new file mode 100644 index 000000000..d8e93445d --- /dev/null +++ b/charts/matomo/examples/gateway-api.yaml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Gateway API exposure through a shared HTTP Gateway. +gatewayAPI: + enabled: true + httpRoutes: + - name: public + parentRefs: + - name: shared-gateway + namespace: gateway-system + sectionName: https + hostnames: + - analytics.example.com +matomo: + siteUrl: https://analytics.example.com + trustedHost: analytics.example.com diff --git a/charts/matomo/examples/production.yaml b/charts/matomo/examples/production.yaml new file mode 100644 index 000000000..cd2b208fc --- /dev/null +++ b/charts/matomo/examples/production.yaml @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: Apache-2.0 +# Production-oriented Matomo deployment with ingress, archiving, and network policy. +replicaCount: 2 +matomo: + siteUrl: https://analytics.example.com + trustedHost: analytics.example.com +ingress: + enabled: true + ingressClassName: nginx + hosts: + - host: analytics.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: analytics-example-com-tls + hosts: + - analytics.example.com +archiver: + enabled: true + schedule: "5 * * * *" +networkPolicy: + enabled: true + egress: + enabled: true +resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi diff --git a/charts/matomo/examples/simple.yaml b/charts/matomo/examples/simple.yaml new file mode 100644 index 000000000..a23be6a07 --- /dev/null +++ b/charts/matomo/examples/simple.yaml @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +# Simple Matomo deployment with bundled MySQL for a lab namespace. +matomo: + siteUrl: http://matomo.local +mysql: + enabled: true +persistence: + size: 10Gi diff --git a/charts/matomo/templates/NOTES.txt b/charts/matomo/templates/NOTES.txt new file mode 100644 index 000000000..0889cad29 --- /dev/null +++ b/charts/matomo/templates/NOTES.txt @@ -0,0 +1,102 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +Matomo has been installed. + +1. Installation summary +======================= +- Release: {{ .Release.Name }} +- Namespace: {{ include "matomo.namespace" . }} +- Chart: {{ .Chart.Name }} {{ .Chart.Version }} +- App version: {{ .Chart.AppVersion }} +- Image: {{ include "matomo.image" . }} +- Database mode: {{ include "matomo.databaseMode" . }} +- Web replicas: {{ .Values.replicaCount }} +- Archiver CronJob: {{ ternary "enabled" "disabled" .Values.archiver.enabled }} + +2. Access +========= +Cluster service: + http://{{ include "matomo.fullname" . }}.{{ include "matomo.namespace" . }}.svc.{{ .Values.clusterDomain }}:{{ .Values.service.port }} + +Port-forward: + kubectl -n {{ include "matomo.namespace" . }} port-forward svc/{{ include "matomo.fullname" . }} 8080:{{ .Values.service.port }} + Open http://127.0.0.1:8080 + +{{- if .Values.ingress.enabled }} +Ingress hosts: +{{- range .Values.ingress.hosts }} + - {{ .host }} +{{- end }} +{{- end }} + +{{- if .Values.gatewayAPI.enabled }} +Gateway API HTTPRoutes: +{{- range $index, $route := .Values.gatewayAPI.httpRoutes }} + - {{ include "matomo.httpRouteName" (dict "root" $ "route" $route "index" $index) }} +{{- end }} +{{- end }} + +3. Getting started +================== +Wait for Matomo and MySQL: + kubectl -n {{ include "matomo.namespace" . }} rollout status deploy/{{ include "matomo.fullname" . }} --timeout=5m + kubectl -n {{ include "matomo.namespace" . }} get pods -l app.kubernetes.io/instance={{ .Release.Name | quote }} + +Complete the Matomo web installer in the browser. The chart pre-wires database +connectivity through MATOMO_DATABASE_* environment variables, but Matomo still +expects the first-time application setup flow. + +4. Database +=========== +- Host: {{ include "matomo.databaseHost" . }} +- Port: {{ include "matomo.databasePort" . }} +- Database: {{ include "matomo.databaseName" . }} +- User: {{ include "matomo.databaseUsername" . }} +- Password secret: {{ include "matomo.databaseSecretName" . }} / {{ include "matomo.databaseSecretKey" . }} + +5. Production archiving +======================= +The chart can run Matomo report archiving outside user traffic: + kubectl -n {{ include "matomo.namespace" . }} get cronjob {{ include "matomo.suffixedName" (dict "base" (include "matomo.fullname" .) "suffix" "-archiver") }} + +Trigger one archive run manually: + kubectl -n {{ include "matomo.namespace" . }} create job --from=cronjob/{{ include "matomo.suffixedName" (dict "base" (include "matomo.fullname" .) "suffix" "-archiver") }} {{ include "matomo.fullname" . }}-archive-now + +6. Validation commands +====================== +Service endpoints: + kubectl -n {{ include "matomo.namespace" . }} get endpoints {{ include "matomo.fullname" . }} + +HTTP smoke test: + kubectl -n {{ include "matomo.namespace" . }} run {{ include "matomo.fullname" . }}-curl --rm -i --restart=Never --image=docker.io/curlimages/curl:8.17.0 -- curl -fsS http://{{ include "matomo.fullname" . }}:{{ .Values.service.port }}/matomo.php + +Helm test: + helm test {{ .Release.Name }} -n {{ include "matomo.namespace" . }} + +7. Troubleshooting +================== +Pods: + kubectl -n {{ include "matomo.namespace" . }} get pods -l app.kubernetes.io/instance={{ .Release.Name | quote }} -o wide + +Events: + kubectl -n {{ include "matomo.namespace" . }} get events --sort-by=.lastTimestamp + +Matomo logs: + kubectl -n {{ include "matomo.namespace" . }} logs deploy/{{ include "matomo.fullname" . }} -c matomo --tail=200 + +Archiver logs: + kubectl -n {{ include "matomo.namespace" . }} logs job/{{ include "matomo.fullname" . }}-archive-now --tail=200 + +Common checks: +- If the web installer cannot reach the database, verify the password secret. +- If report pages are slow, enable or tune archiver.schedule. +- If client IPs are wrong behind ingress, configure Matomo trusted proxy headers. +- If multiple replicas are used, make sure storage and PHP sessions are safe for that topology. + +8. Resources +============ +- HelmForge docs: https://helmforge.dev/docs/charts/matomo +- Upstream docs: https://matomo.org/faq/on-premise/ +- Archiving guide: https://developer.matomo.org/guides/archiving +- Issues: https://github.com/helmforgedev/charts/issues + +Happy Helmforging :) diff --git a/charts/matomo/templates/_helpers.tpl b/charts/matomo/templates/_helpers.tpl new file mode 100644 index 000000000..168a47f95 --- /dev/null +++ b/charts/matomo/templates/_helpers.tpl @@ -0,0 +1,181 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- define "matomo.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "matomo.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.namespace" -}} +{{- .Values.namespaceOverride | default .Release.Namespace -}} +{{- end -}} + +{{- define "matomo.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "matomo.selectorLabels" -}} +app.kubernetes.io/name: {{ include "matomo.name" . }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +{{- end -}} + +{{- define "matomo.labels" -}} +helm.sh/chart: {{ include "matomo.chart" . }} +{{ include "matomo.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/part-of: helmforge +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end -}} + +{{- define "matomo.image" -}} +{{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default (printf "%s-apache" .Chart.AppVersion)) -}} +{{- end -}} + +{{- define "matomo.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "matomo.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.suffixedName" -}} +{{- $base := .base -}} +{{- $suffix := .suffix -}} +{{- $baseMax := int (max 1 (sub 63 (len $suffix))) -}} +{{- printf "%s%s" ($base | trunc $baseMax | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "matomo.configMapName" -}} +{{- include "matomo.suffixedName" (dict "base" (include "matomo.fullname" .) "suffix" "-config") -}} +{{- end -}} + +{{- define "matomo.databaseMode" -}} +{{- $mode := .Values.database.mode | default "auto" -}} +{{- if not (has $mode (list "auto" "external" "mysql")) -}} +{{- fail (printf "database.mode must be one of: auto, external, mysql (got %s)" $mode) -}} +{{- end -}} +{{- $hasExternal := or (ne (.Values.database.external.host | default "") "") (ne (.Values.database.external.existingSecret | default "") "") -}} +{{- $hasMysql := .Values.mysql.enabled | default false -}} +{{- if eq $mode "auto" -}} + {{- if and $hasExternal $hasMysql -}} + {{- fail "matomo database selection is ambiguous: configure only one of database.external.host or mysql.enabled" -}} + {{- end -}} + {{- if $hasExternal -}}external{{- else if $hasMysql -}}mysql{{- else -}}{{- fail "matomo requires a database: set database.external.host or mysql.enabled=true" -}}{{- end -}} +{{- else -}} + {{- if and (eq $mode "external") (not $hasExternal) -}}{{- fail "database.mode=external requires database.external.host or database.external.existingSecret" -}}{{- end -}} + {{- if and (eq $mode "external") $hasMysql -}}{{- fail "database.mode=external cannot be combined with mysql.enabled" -}}{{- end -}} + {{- if and (eq $mode "mysql") (not $hasMysql) -}}{{- fail "database.mode=mysql requires mysql.enabled=true" -}}{{- end -}} + {{- if and (eq $mode "mysql") $hasExternal -}}{{- fail "database.mode=mysql cannot be combined with database.external" -}}{{- end -}} + {{- $mode -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.databaseHost" -}} +{{- if eq (include "matomo.databaseMode" .) "external" -}} +{{- .Values.database.external.host -}} +{{- else -}} +{{- printf "%s-mysql" .Release.Name -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.databasePort" -}} +{{- if eq (include "matomo.databaseMode" .) "external" -}} +{{- .Values.database.external.port | default 3306 | toString -}} +{{- else -}} +{{- print "3306" -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.databaseName" -}} +{{- if eq (include "matomo.databaseMode" .) "external" -}}{{- .Values.database.external.name -}}{{- else -}}{{- .Values.mysql.auth.database -}}{{- end -}} +{{- end -}} + +{{- define "matomo.databaseUsername" -}} +{{- if eq (include "matomo.databaseMode" .) "external" -}}{{- .Values.database.external.username -}}{{- else -}}{{- .Values.mysql.auth.username -}}{{- end -}} +{{- end -}} + +{{- define "matomo.databaseSecretName" -}} +{{- if and (eq (include "matomo.databaseMode" .) "external") .Values.database.external.existingSecret -}} +{{- .Values.database.external.existingSecret -}} +{{- else if eq (include "matomo.databaseMode" .) "mysql" -}} +{{- printf "%s-mysql-auth" .Release.Name -}} +{{- else -}} +{{- include "matomo.suffixedName" (dict "base" (include "matomo.fullname" .) "suffix" "-database") -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.databaseSecretKey" -}} +{{- if and (eq (include "matomo.databaseMode" .) "external") .Values.database.external.existingSecret -}} +{{- .Values.database.external.existingSecretPasswordKey -}} +{{- else if eq (include "matomo.databaseMode" .) "mysql" -}} +{{- print "mysql-user-password" -}} +{{- else -}} +{{- print "database-password" -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.siteUrl" -}} +{{- if .Values.matomo.siteUrl -}} +{{- .Values.matomo.siteUrl -}} +{{- else -}} +{{- printf "http://%s.%s.svc.%s:%d" (include "matomo.fullname" .) (include "matomo.namespace" .) (.Values.clusterDomain | default "cluster.local") (.Values.service.port | int) -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.httpRouteName" -}} +{{- $root := .root -}} +{{- $route := .route -}} +{{- $index := int (.index | default 0) -}} +{{- if $route.name -}} +{{- include "matomo.suffixedName" (dict "base" (include "matomo.fullname" $root) "suffix" (printf "-%s" $route.name)) -}} +{{- else if gt $index 0 -}} +{{- include "matomo.suffixedName" (dict "base" (include "matomo.fullname" $root) "suffix" (printf "-%d" $index)) -}} +{{- else -}} +{{- include "matomo.fullname" $root -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.externalSecretName" -}} +{{- $root := .root -}} +{{- $item := .item -}} +{{- $index := int (.index | default 0) -}} +{{- if $item.fullnameOverride -}} +{{- $item.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else if $item.name -}} +{{- include "matomo.suffixedName" (dict "base" (include "matomo.fullname" $root) "suffix" (printf "-%s" $item.name)) -}} +{{- else if gt $index 0 -}} +{{- include "matomo.suffixedName" (dict "base" (include "matomo.fullname" $root) "suffix" (printf "-%d" $index)) -}} +{{- else -}} +{{- include "matomo.databaseSecretName" $root -}} +{{- end -}} +{{- end -}} + +{{- define "matomo.validate" -}} +{{- $databaseMode := include "matomo.databaseMode" . -}} +{{- if and .Values.ingress.enabled (empty .Values.ingress.hosts) -}}{{- fail "ingress.hosts must contain at least one host when ingress.enabled=true" -}}{{- end -}} +{{- if and .Values.gatewayAPI.enabled (empty .Values.gatewayAPI.httpRoutes) -}}{{- fail "gatewayAPI.httpRoutes must contain at least one route when gatewayAPI.enabled=true" -}}{{- end -}} +{{- if and .Values.metrics.serviceMonitor.enabled (not .Values.metrics.serviceMonitor.interval) -}}{{- fail "metrics.serviceMonitor.interval is required when metrics.serviceMonitor.enabled=true" -}}{{- end -}} +{{- if and (eq $databaseMode "external") (not .Values.database.external.existingSecret) (not .Values.database.external.password) .Values.archiver.enabled -}}{{- fail "external database mode with archiver.enabled=true requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret" -}}{{- end -}} +{{- if .Values.podLabels -}} +{{- if hasKey .Values.podLabels "app.kubernetes.io/name" -}}{{- fail "podLabels must not override app.kubernetes.io/name" -}}{{- end -}} +{{- if hasKey .Values.podLabels "app.kubernetes.io/instance" -}}{{- fail "podLabels must not override app.kubernetes.io/instance" -}}{{- end -}} +{{- end -}} +{{- if and .Values.externalSecrets.enabled (empty .Values.externalSecrets.items) -}}{{- fail "externalSecrets.items must contain at least one item when externalSecrets.enabled=true" -}}{{- end -}} +{{- range $i, $item := .Values.externalSecrets.items }} +{{- if and $.Values.externalSecrets.enabled (not $item.spec.secretStoreRef) (not $item.spec.data) (not $item.spec.dataFrom) -}}{{- fail (printf "externalSecrets.items[%d].spec must define secretStoreRef/data or dataFrom" $i) -}}{{- end -}} +{{- end -}} +{{- end -}} diff --git a/charts/matomo/templates/configmap.yaml b/charts/matomo/templates/configmap.yaml new file mode 100644 index 000000000..fed9273e8 --- /dev/null +++ b/charts/matomo/templates/configmap.yaml @@ -0,0 +1,13 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.php.ini }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "matomo.configMapName" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} +data: + custom.ini: | +{{ .Values.php.ini | indent 4 }} +{{- end }} diff --git a/charts/matomo/templates/cronjob-archiver.yaml b/charts/matomo/templates/cronjob-archiver.yaml new file mode 100644 index 000000000..9eea17a4d --- /dev/null +++ b/charts/matomo/templates/cronjob-archiver.yaml @@ -0,0 +1,79 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.archiver.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "matomo.suffixedName" (dict "base" (include "matomo.fullname" .) "suffix" "-archiver") }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + app.kubernetes.io/component: archiver +spec: + schedule: {{ .Values.archiver.schedule | quote }} + concurrencyPolicy: {{ .Values.archiver.concurrencyPolicy }} + successfulJobsHistoryLimit: {{ .Values.archiver.successfulJobsHistoryLimit }} + failedJobsHistoryLimit: {{ .Values.archiver.failedJobsHistoryLimit }} + jobTemplate: + spec: + activeDeadlineSeconds: {{ .Values.archiver.activeDeadlineSeconds }} + template: + metadata: + labels: + {{- include "matomo.selectorLabels" . | nindent 12 }} + app.kubernetes.io/component: archiver + spec: + restartPolicy: OnFailure + serviceAccountName: {{ include "matomo.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + containers: + - name: archive + image: {{ include "matomo.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - sh + - -ec + - | + php /var/www/html/console core:archive --url="${MATOMO_URL}" {{ join " " .Values.archiver.extraArgs }} + env: + - name: MATOMO_URL + value: {{ include "matomo.siteUrl" . | quote }} + - name: MATOMO_DATABASE_HOST + value: {{ printf "%s:%s" (include "matomo.databaseHost" .) (include "matomo.databasePort" .) | quote }} + - name: MATOMO_DATABASE_USERNAME + value: {{ include "matomo.databaseUsername" . | quote }} + - name: MATOMO_DATABASE_DBNAME + value: {{ include "matomo.databaseName" . | quote }} + - name: MATOMO_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "matomo.databaseSecretName" . }} + key: {{ include "matomo.databaseSecretKey" . }} + resources: + {{- toYaml .Values.archiver.resources | nindent 16 }} + volumeMounts: + - name: matomo-data + mountPath: /var/www/html + {{- if .Values.php.ini }} + - name: php-config + mountPath: /usr/local/etc/php/conf.d/helmforge.ini + subPath: custom.ini + readOnly: true + {{- end }} + volumes: + - name: matomo-data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ default (include "matomo.fullname" .) .Values.persistence.existingClaim }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.php.ini }} + - name: php-config + configMap: + name: {{ include "matomo.configMapName" . }} + {{- end }} +{{- end }} diff --git a/charts/matomo/templates/deployment.yaml b/charts/matomo/templates/deployment.yaml new file mode 100644 index 000000000..cd8559e6a --- /dev/null +++ b/charts/matomo/templates/deployment.yaml @@ -0,0 +1,179 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + {{- toYaml .Values.updateStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "matomo.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "matomo.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "matomo.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if or .Values.database.waitForConnection.enabled .Values.extraInitContainers }} + initContainers: + {{- if .Values.database.waitForConnection.enabled }} + - name: wait-for-db + image: docker.io/library/busybox:1.37 + command: + - sh + - -ec + - | + until nc -z -w2 "${DB_HOST}" "${DB_PORT}"; do + echo "waiting for database ${DB_HOST}:${DB_PORT}" + sleep 2 + done + env: + - name: DB_HOST + value: {{ include "matomo.databaseHost" . | quote }} + - name: DB_PORT + value: {{ include "matomo.databasePort" . | quote }} + {{- end }} + {{- with .Values.extraInitContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + containers: + - name: matomo + image: {{ include "matomo.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + - name: MATOMO_DATABASE_HOST + value: {{ printf "%s:%s" (include "matomo.databaseHost" .) (include "matomo.databasePort" .) | quote }} + - name: MATOMO_DATABASE_USERNAME + value: {{ include "matomo.databaseUsername" . | quote }} + - name: MATOMO_DATABASE_DBNAME + value: {{ include "matomo.databaseName" . | quote }} + - name: MATOMO_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "matomo.databaseSecretName" . }} + key: {{ include "matomo.databaseSecretKey" . }} + {{- with .Values.matomo.trustedHost }} + - name: MATOMO_TRUSTED_HOST + value: {{ . | quote }} + {{- end }} + {{- with .Values.matomo.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.matomo.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.startupProbe.enabled }} + startupProbe: + httpGet: + path: {{ .Values.startupProbe.path | quote }} + port: http + initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.startupProbe.periodSeconds }} + timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }} + failureThreshold: {{ .Values.startupProbe.failureThreshold }} + {{- end }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: {{ .Values.livenessProbe.path | quote }} + port: http + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.path | quote }} + port: http + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: matomo-data + mountPath: /var/www/html + {{- if .Values.php.ini }} + - name: php-config + mountPath: /usr/local/etc/php/conf.d/helmforge.ini + subPath: custom.ini + readOnly: true + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: matomo-data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ default (include "matomo.fullname" .) .Values.persistence.existingClaim }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.php.ini }} + - name: php-config + configMap: + name: {{ include "matomo.configMapName" . }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/matomo/templates/externalsecret.yaml b/charts/matomo/templates/externalsecret.yaml new file mode 100644 index 000000000..6563e1c04 --- /dev/null +++ b/charts/matomo/templates/externalsecret.yaml @@ -0,0 +1,34 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.externalSecrets.enabled }} +{{- range $index, $item := .Values.externalSecrets.items }} +{{- if gt $index 0 }} +--- +{{- end }} +{{- $name := include "matomo.externalSecretName" (dict "root" $ "item" $item "index" $index) }} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $name }} + namespace: {{ include "matomo.namespace" $ | quote }} + labels: + {{- include "matomo.labels" $ | nindent 4 }} + {{- with $item.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $item.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- $spec := deepCopy $item.spec }} + {{- if not (hasKey $spec "refreshInterval") }} + {{- $_ := set $spec "refreshInterval" $.Values.externalSecrets.refreshInterval }} + {{- end }} + {{- if not (hasKey $spec "target") }} + {{- $_ := set $spec "target" (dict "name" $name "creationPolicy" "Owner") }} + {{- else if not (hasKey $spec.target "name") }} + {{- $_ := set $spec.target "name" $name }} + {{- end }} + {{- toYaml $spec | nindent 2 }} +{{- end }} +{{- end }} diff --git a/charts/matomo/templates/extra-manifests.yaml b/charts/matomo/templates/extra-manifests.yaml new file mode 100644 index 000000000..930833f33 --- /dev/null +++ b/charts/matomo/templates/extra-manifests.yaml @@ -0,0 +1,5 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- range .Values.extraManifests }} +--- +{{ toYaml . }} +{{- end }} diff --git a/charts/matomo/templates/gateway-httproute.yaml b/charts/matomo/templates/gateway-httproute.yaml new file mode 100644 index 000000000..c51211404 --- /dev/null +++ b/charts/matomo/templates/gateway-httproute.yaml @@ -0,0 +1,56 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.gatewayAPI.enabled }} +{{- range $index, $route := .Values.gatewayAPI.httpRoutes }} +{{- if gt $index 0 }} +--- +{{- end }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "matomo.httpRouteName" (dict "root" $ "route" $route "index" $index) }} + namespace: {{ include "matomo.namespace" $ | quote }} + labels: + {{- include "matomo.labels" $ | nindent 4 }} + {{- with $route.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $route.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with $route.parentRefs }} + parentRefs: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $route.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- if $route.rules }} + {{- range $route.rules }} + - {{- with .matches }} + matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .backendRefs }} + backendRefs: + {{- toYaml .backendRefs | nindent 8 }} + {{- else if not .omitDefaultBackend }} + backendRefs: + - name: {{ include "matomo.fullname" $ }} + port: {{ $.Values.service.port }} + {{- end }} + {{- end }} + {{- else }} + - backendRefs: + - name: {{ include "matomo.fullname" $ }} + port: {{ $.Values.service.port }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/matomo/templates/ingress.yaml b/charts/matomo/templates/ingress.yaml new file mode 100644 index 000000000..b38caa64b --- /dev/null +++ b/charts/matomo/templates/ingress.yaml @@ -0,0 +1,38 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- $paths := default (list (dict "path" "/" "pathType" "Prefix")) .paths }} + {{- range $paths }} + - path: {{ default "/" .path }} + pathType: {{ default "Prefix" .pathType }} + backend: + service: + name: {{ include "matomo.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/matomo/templates/networkpolicy.yaml b/charts/matomo/templates/networkpolicy.yaml new file mode 100644 index 000000000..8070c27f9 --- /dev/null +++ b/charts/matomo/templates/networkpolicy.yaml @@ -0,0 +1,49 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "matomo.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + {{- if .Values.networkPolicy.egress.enabled }} + - Egress + {{- end }} + ingress: + - from: + {{- if .Values.networkPolicy.ingressFrom }} + {{- toYaml .Values.networkPolicy.ingressFrom | nindent 8 }} + {{- else }} + - namespaceSelector: {} + {{- end }} + ports: + - protocol: TCP + port: http + {{- if .Values.networkPolicy.egress.enabled }} + egress: + {{- if .Values.networkPolicy.egress.allowDNS }} + - ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + {{- end }} + - ports: + - protocol: TCP + port: {{ include "matomo.databasePort" . }} + {{- with .Values.networkPolicy.egress.extraTo }} + - to: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.networkPolicy.egress.extraEgress }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/matomo/templates/pdb.yaml b/charts/matomo/templates/pdb.yaml new file mode 100644 index 000000000..e4a3cb848 --- /dev/null +++ b/charts/matomo/templates/pdb.yaml @@ -0,0 +1,19 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if and .Values.podDisruptionBudget.enabled (gt (int .Values.replicaCount) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "matomo.selectorLabels" . | nindent 6 }} + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/charts/matomo/templates/pvc.yaml b/charts/matomo/templates/pvc.yaml new file mode 100644 index 000000000..01821544c --- /dev/null +++ b/charts/matomo/templates/pvc.yaml @@ -0,0 +1,23 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- with .Values.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/matomo/templates/secret.yaml b/charts/matomo/templates/secret.yaml new file mode 100644 index 000000000..e939d3515 --- /dev/null +++ b/charts/matomo/templates/secret.yaml @@ -0,0 +1,13 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if and (eq (include "matomo.databaseMode" .) "external") (not .Values.database.external.existingSecret) (not .Values.externalSecrets.enabled) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "matomo.databaseSecretName" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} +type: Opaque +stringData: + database-password: {{ .Values.database.external.password | quote }} +{{- end }} diff --git a/charts/matomo/templates/service.yaml b/charts/matomo/templates/service.yaml new file mode 100644 index 000000000..58d47edd5 --- /dev/null +++ b/charts/matomo/templates/service.yaml @@ -0,0 +1,28 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- with .Values.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ . }} + {{- end }} + {{- with .Values.service.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "matomo.selectorLabels" . | nindent 4 }} diff --git a/charts/matomo/templates/serviceaccount.yaml b/charts/matomo/templates/serviceaccount.yaml new file mode 100644 index 000000000..e1405b536 --- /dev/null +++ b/charts/matomo/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "matomo.serviceAccountName" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/matomo/templates/servicemonitor.yaml b/charts/matomo/templates/servicemonitor.yaml new file mode 100644 index 000000000..7c39b6a50 --- /dev/null +++ b/charts/matomo/templates/servicemonitor.yaml @@ -0,0 +1,26 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "matomo.fullname" . }} + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "matomo.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: /matomo.php + interval: {{ .Values.metrics.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} + {{- with .Values.metrics.serviceMonitor.namespaceSelector }} + namespaceSelector: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/matomo/templates/tests/test-connection.yaml b/charts/matomo/templates/tests/test-connection.yaml new file mode 100644 index 000000000..f03ec72e1 --- /dev/null +++ b/charts/matomo/templates/tests/test-connection.yaml @@ -0,0 +1,39 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "matomo.fullname" . }}-test-connection" + namespace: {{ include "matomo.namespace" . | quote }} + labels: + {{- include "matomo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: curl + image: docker.io/curlimages/curl:8.17.0 + imagePullPolicy: IfNotPresent + command: + - sh + - -ec + - curl -fsS "http://{{ include "matomo.fullname" . }}:{{ .Values.service.port }}/matomo.php" >/dev/null + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault diff --git a/charts/matomo/templates/validate.yaml b/charts/matomo/templates/validate.yaml new file mode 100644 index 000000000..47a6f955f --- /dev/null +++ b/charts/matomo/templates/validate.yaml @@ -0,0 +1,2 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- include "matomo.validate" . -}} diff --git a/charts/matomo/tests/archiver_test.yaml b/charts/matomo/tests/archiver_test.yaml new file mode 100644 index 000000000..3ee945ed9 --- /dev/null +++ b/charts/matomo/tests/archiver_test.yaml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo archiver +templates: + - cronjob-archiver.yaml +tests: + - it: renders archiver CronJob by default + asserts: + - isKind: + of: CronJob + - equal: + path: spec.schedule + value: "5 * * * *" + - equal: + path: spec.concurrencyPolicy + value: Forbid + - it: disables archiver when requested + set: + archiver: + enabled: false + asserts: + - hasDocuments: + count: 0 + - it: uses configured public URL + set: + matomo: + siteUrl: https://analytics.example.com + asserts: + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: MATOMO_URL + value: https://analytics.example.com diff --git a/charts/matomo/tests/deployment_test.yaml b/charts/matomo/tests/deployment_test.yaml new file mode 100644 index 000000000..18c1425ab --- /dev/null +++ b/charts/matomo/tests/deployment_test.yaml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo deployment +templates: + - configmap.yaml + - deployment.yaml +tests: + - it: renders deployment with official image + template: deployment.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/library/matomo:5.11.2-apache + - it: wires Matomo database environment variables + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MATOMO_DATABASE_DBNAME + value: matomo + - contains: + path: spec.template.spec.containers[0].env + content: + name: MATOMO_DATABASE_USERNAME + value: matomo + - it: mounts persistent application data + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /var/www/html diff --git a/charts/matomo/tests/externalsecret_test.yaml b/charts/matomo/tests/externalsecret_test.yaml new file mode 100644 index 000000000..c0bef92b4 --- /dev/null +++ b/charts/matomo/tests/externalsecret_test.yaml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo external secrets +templates: + - externalsecret.yaml +tests: + - it: does not render by default + asserts: + - hasDocuments: + count: 0 + - it: renders canonical ExternalSecret item + set: + externalSecrets: + enabled: true + items: + - fullnameOverride: matomo-database + spec: + secretStoreRef: + name: fake + kind: ClusterSecretStore + data: + - secretKey: database-password + remoteRef: + key: matomo/password + asserts: + - isKind: + of: ExternalSecret + - equal: + path: apiVersion + value: external-secrets.io/v1 + - equal: + path: spec.target.name + value: matomo-database diff --git a/charts/matomo/tests/networkpolicy_test.yaml b/charts/matomo/tests/networkpolicy_test.yaml new file mode 100644 index 000000000..aa1022607 --- /dev/null +++ b/charts/matomo/tests/networkpolicy_test.yaml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo network policy +templates: + - networkpolicy.yaml +tests: + - it: does not render by default + asserts: + - hasDocuments: + count: 0 + - it: renders ingress policy + set: + networkPolicy: + enabled: true + asserts: + - isKind: + of: NetworkPolicy + - contains: + path: spec.policyTypes + content: Ingress + - it: renders egress policy with DNS + set: + networkPolicy: + enabled: true + egress: + enabled: true + asserts: + - contains: + path: spec.policyTypes + content: Egress + - equal: + path: spec.egress[0].ports[0].port + value: 53 diff --git a/charts/matomo/tests/pdb_test.yaml b/charts/matomo/tests/pdb_test.yaml new file mode 100644 index 000000000..b6309e940 --- /dev/null +++ b/charts/matomo/tests/pdb_test.yaml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo pdb +templates: + - pdb.yaml +tests: + - it: does not render for one replica + asserts: + - hasDocuments: + count: 0 + - it: renders for multiple replicas + set: + replicaCount: 2 + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.maxUnavailable + value: 1 diff --git a/charts/matomo/tests/routing_test.yaml b/charts/matomo/tests/routing_test.yaml new file mode 100644 index 000000000..11b7ddef1 --- /dev/null +++ b/charts/matomo/tests/routing_test.yaml @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo routing +templates: + - ingress.yaml + - gateway-httproute.yaml +tests: + - it: does not render ingress by default + template: ingress.yaml + asserts: + - hasDocuments: + count: 0 + - it: renders ingress with ingressClassName + template: ingress.yaml + set: + ingress: + enabled: true + ingressClassName: nginx + hosts: + - host: matomo.example.com + asserts: + - isKind: + of: Ingress + - equal: + path: spec.ingressClassName + value: nginx + - it: renders HTTPRoute with default backend + template: gateway-httproute.yaml + set: + gatewayAPI: + enabled: true + httpRoutes: + - parentRefs: + - name: gateway + hostnames: + - matomo.example.com + asserts: + - isKind: + of: HTTPRoute + - equal: + path: spec.rules[0].backendRefs[0].port + value: 80 diff --git a/charts/matomo/tests/service_test.yaml b/charts/matomo/tests/service_test.yaml new file mode 100644 index 000000000..30458f0b7 --- /dev/null +++ b/charts/matomo/tests/service_test.yaml @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo service +templates: + - service.yaml +tests: + - it: renders ClusterIP service by default + asserts: + - isKind: + of: Service + - equal: + path: spec.type + value: ClusterIP + - equal: + path: spec.ports[0].name + value: http + - it: omits dual stack fields by default + asserts: + - notExists: + path: spec.ipFamilyPolicy + - notExists: + path: spec.ipFamilies + - it: renders dual stack policy + set: + service: + ipFamilyPolicy: PreferDualStack + ipFamilies: + - IPv4 + - IPv6 + asserts: + - equal: + path: spec.ipFamilyPolicy + value: PreferDualStack + - equal: + path: spec.ipFamilies[1] + value: IPv6 diff --git a/charts/matomo/tests/test_connection_test.yaml b/charts/matomo/tests/test_connection_test.yaml new file mode 100644 index 000000000..7171e4c27 --- /dev/null +++ b/charts/matomo/tests/test_connection_test.yaml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo helm test +templates: + - templates/tests/test-connection.yaml +tests: + - it: renders helm test hook under templates + asserts: + - isKind: + of: Pod + - equal: + path: metadata.annotations["helm.sh/hook"] + value: test + - matchRegex: + path: spec.containers[0].command[2] + pattern: matomo.php diff --git a/charts/matomo/tests/validation_test.yaml b/charts/matomo/tests/validation_test.yaml new file mode 100644 index 000000000..25b4c792d --- /dev/null +++ b/charts/matomo/tests/validation_test.yaml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: matomo validations +templates: + - validate.yaml +tests: + - it: rejects selector label override + set: + podLabels: + app.kubernetes.io/name: bad + asserts: + - failedTemplate: + errorMessage: podLabels must not override app.kubernetes.io/name + - it: fails ingress without hosts + set: + ingress: + enabled: true + asserts: + - failedTemplate: + errorMessage: ingress.hosts must contain at least one host when ingress.enabled=true + - it: fails ExternalSecrets without items + set: + externalSecrets: + enabled: true + items: [] + asserts: + - failedTemplate: + errorMessage: externalSecrets.items must contain at least one item when externalSecrets.enabled=true diff --git a/charts/matomo/values.schema.json b/charts/matomo/values.schema.json new file mode 100644 index 000000000..9bf017a3b --- /dev/null +++ b/charts/matomo/values.schema.json @@ -0,0 +1,168 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "Matomo Helm Chart Values", + "type": "object", + "additionalProperties": true, + "properties": { + "nameOverride": { "type": "string" }, + "fullnameOverride": { "type": "string" }, + "namespaceOverride": { "type": "string" }, + "clusterDomain": { "type": "string" }, + "commonLabels": { "type": "object" }, + "replicaCount": { "type": "integer", "minimum": 1 }, + "image": { + "type": "object", + "properties": { + "repository": { "type": "string" }, + "tag": { "type": "string", "not": { "enum": ["latest"] } }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "matomo": { + "type": "object", + "properties": { + "siteUrl": { "type": "string" }, + "trustedHost": { "type": "string" }, + "extraEnv": { "type": "array" }, + "extraEnvFrom": { "type": "array" } + } + }, + "database": { + "type": "object", + "properties": { + "mode": { "type": "string", "enum": ["auto", "external", "mysql"] }, + "waitForConnection": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" } + } + }, + "external": { + "type": "object", + "properties": { + "host": { "type": "string" }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "name": { "type": "string" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "existingSecret": { "type": "string" }, + "existingSecretPasswordKey": { "type": "string" } + } + } + } + }, + "persistence": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "storageClass": { "type": "string" }, + "accessMode": { "type": "string" }, + "size": { "type": "string" }, + "existingClaim": { "type": "string" }, + "annotations": { "type": "object" } + } + }, + "archiver": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "schedule": { "type": "string" }, + "concurrencyPolicy": { "type": "string", "enum": ["Allow", "Forbid", "Replace"] }, + "successfulJobsHistoryLimit": { "type": "integer", "minimum": 0 }, + "failedJobsHistoryLimit": { "type": "integer", "minimum": 0 }, + "activeDeadlineSeconds": { "type": "integer", "minimum": 1 }, + "extraArgs": { "type": "array", "items": { "type": "string" } }, + "resources": { "type": "object" } + } + }, + "service": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "annotations": { "type": "object" }, + "ipFamilyPolicy": { "type": "string", "enum": ["", "SingleStack", "PreferDualStack", "RequireDualStack"] }, + "ipFamilies": { + "type": "array", + "items": { "type": "string", "enum": ["IPv4", "IPv6"] }, + "maxItems": 2, + "uniqueItems": true + } + } + }, + "ingress": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "ingressClassName": { "type": "string" }, + "annotations": { "type": "object" }, + "hosts": { "type": "array" }, + "tls": { "type": "array" } + } + }, + "gatewayAPI": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "httpRoutes": { "type": "array" } + } + }, + "externalSecrets": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "refreshInterval": { "type": "string" }, + "items": { "type": "array" } + } + }, + "metrics": { + "type": "object", + "properties": { + "serviceMonitor": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "interval": { "type": "string" }, + "scrapeTimeout": { "type": "string" }, + "labels": { "type": "object" }, + "namespaceSelector": { "type": "object" } + } + } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "ingressFrom": { "type": "array" }, + "egress": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "allowDNS": { "type": "boolean" }, + "extraTo": { "type": "array" }, + "extraEgress": { "type": "array" } + } + } + } + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "maxUnavailable": { "oneOf": [{ "type": "integer" }, { "type": "string" }] }, + "minAvailable": { "oneOf": [{ "type": "integer" }, { "type": "string" }] } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { "type": "boolean" }, + "name": { "type": "string" }, + "annotations": { "type": "object" }, + "automountServiceAccountToken": { "type": "boolean" } + } + }, + "extraManifests": { "type": "array" } + } +} diff --git a/charts/matomo/values.yaml b/charts/matomo/values.yaml new file mode 100644 index 000000000..53a93d6c1 --- /dev/null +++ b/charts/matomo/values.yaml @@ -0,0 +1,271 @@ +# SPDX-License-Identifier: Apache-2.0 +# ============================================================================= +# Matomo Helm Chart +# ============================================================================= +# Uses the official matomo Docker image (Apache variant). +# Supports MySQL subchart or an external MySQL/MariaDB database. +# Adds a Kubernetes CronJob for production report archiving. +# ============================================================================= + +# -- Override the chart name used in resource names +nameOverride: "" + +# -- Override the full release name used in resource names +fullnameOverride: "" + +# -- Override namespace for namespaced resources. The namespace must already exist. +namespaceOverride: "" + +# -- Kubernetes cluster DNS domain +clusterDomain: cluster.local + +# -- Labels added to all resources +commonLabels: {} + +# -- Number of Matomo web replicas. Use 1 unless storage/session handling is safe for multiple replicas. +replicaCount: 1 + +image: + # -- Official Matomo image repository + repository: docker.io/library/matomo + # -- Official Matomo image tag. The apache variant includes the web server. + tag: "5.11.2-apache" + # -- Image pull policy + pullPolicy: IfNotPresent + +# -- Image pull secrets +imagePullSecrets: [] + +matomo: + # -- Public URL used by archiver jobs. If empty, the chart derives an in-cluster URL. + siteUrl: "" + # -- Optional trusted host used during initial setup and documentation. + trustedHost: "" + # -- Extra environment variables for the Matomo container. + extraEnv: [] + # -- Additional envFrom sources for the Matomo container. + extraEnvFrom: [] + +database: + # -- Database mode: auto | external | mysql + mode: auto + waitForConnection: + # -- Wait for the configured database socket before starting Matomo. + enabled: true + external: + # -- External MySQL/MariaDB host + host: "" + # -- External MySQL/MariaDB port + port: 3306 + # -- Database name + name: matomo + # -- Database username + username: matomo + # -- Database password. Use existingSecret or externalSecrets for production. + password: "" + # -- Existing Secret containing the external database password + existingSecret: "" + # -- Secret key containing the external database password + existingSecretPasswordKey: database-password + +mysql: + # -- Deploy HelmForge MySQL as a subchart + enabled: true + architecture: standalone + auth: + # -- Database created by the MySQL subchart + database: matomo + # -- Database user created by the MySQL subchart + username: matomo + # -- Database password. Auto-generated by the subchart when empty. + password: "" + # -- Root password. Auto-generated by the subchart when empty. + rootPassword: "" + primary: + persistence: + # -- Enable MySQL persistence + enabled: true + # -- MySQL PVC size + size: 8Gi + +persistence: + # -- Enable persistent storage for /var/www/html + enabled: true + # -- Storage class. Empty uses the cluster default. + storageClass: "" + # -- PVC access mode + accessMode: ReadWriteOnce + # -- PVC size for Matomo application files and plugins + size: 10Gi + # -- Use an existing PVC instead of creating one + existingClaim: "" + # -- PVC annotations + annotations: {} + +archiver: + # -- Create a CronJob that runs Matomo core:archive. + enabled: true + # -- Cron schedule for report archiving. Matomo recommends minute 5 by default. + schedule: "5 * * * *" + # -- CronJob concurrency policy + concurrencyPolicy: Forbid + # -- Successful job history limit + successfulJobsHistoryLimit: 3 + # -- Failed job history limit + failedJobsHistoryLimit: 3 + # -- Active deadline seconds for one archive run + activeDeadlineSeconds: 7200 + # -- Extra arguments appended to core:archive + extraArgs: [] + # -- Resources for the archiver job + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + +php: + # -- Custom PHP ini directives rendered into a ConfigMap + ini: | + memory_limit = 256M + max_execution_time = 180 + upload_max_filesize = 64M + post_max_size = 64M + opcache.enable = 1 + opcache.memory_consumption = 128 + +service: + # -- Service type + type: ClusterIP + # -- Service port + port: 80 + # -- Service annotations + annotations: {} + # -- IP family policy: SingleStack | PreferDualStack | RequireDualStack. Empty uses cluster default. + ipFamilyPolicy: "" + # -- IP families. Empty uses cluster default. + ipFamilies: [] + +ingress: + # -- Create an Ingress resource + enabled: false + # -- Ingress class name. Empty uses the cluster default IngressClass. + ingressClassName: "" + # -- Ingress annotations + annotations: {} + # -- Ingress hosts + hosts: [] + # -- Ingress TLS entries + tls: [] + +gatewayAPI: + # -- Create Gateway API HTTPRoute resources + enabled: false + # -- HTTPRoute definitions + httpRoutes: [] + +externalSecrets: + # -- Render ExternalSecret resources + enabled: false + # -- Default refresh interval + refreshInterval: 1h + # -- ExternalSecret items using the canonical HelmForge items[] contract + items: [] + +metrics: + # -- Enable ServiceMonitor for Prometheus scraping. Matomo has no native Prometheus endpoint; this targets HTTP availability only unless users add an exporter. + serviceMonitor: + enabled: false + interval: 30s + scrapeTimeout: 10s + labels: {} + namespaceSelector: {} + +networkPolicy: + # -- Create NetworkPolicy resources + enabled: false + ingressFrom: [] + egress: + # -- Render explicit egress rules + enabled: false + # -- Allow DNS egress + allowDNS: true + # -- Extra egress peers + extraTo: [] + # -- Extra complete egress rules + extraEgress: [] + +podDisruptionBudget: + # -- Create a PodDisruptionBudget when replicaCount > 1 + enabled: true + # -- Maximum unavailable pods + maxUnavailable: 1 + # -- Minimum available pods. Takes precedence when set. + minAvailable: "" + +serviceAccount: + # -- Create a ServiceAccount + create: true + # -- ServiceAccount name override + name: "" + # -- ServiceAccount annotations + annotations: {} + # -- Automount Kubernetes API credentials + automountServiceAccountToken: false + +podSecurityContext: {} + +securityContext: {} + +startupProbe: + # -- Enable startup probe + enabled: true + path: /matomo.php + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + +livenessProbe: + # -- Enable liveness probe + enabled: true + path: /matomo.php + initialDelaySeconds: 0 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 6 + +readinessProbe: + # -- Enable readiness probe + enabled: true + path: /matomo.php + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + +terminationGracePeriodSeconds: 30 +updateStrategy: + type: RollingUpdate + +podLabels: {} +podAnnotations: {} +priorityClassName: "" +nodeSelector: {} +tolerations: [] +affinity: {} +topologySpreadConstraints: [] + +extraInitContainers: [] +extraContainers: [] +extraVolumes: [] +extraVolumeMounts: [] +extraManifests: [] From dd56a71c716f5fda5a30811f0770117fdd7c2f1d Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Tue, 7 Jul 2026 06:09:32 -0300 Subject: [PATCH 2/6] feat(clickhouse)!: add chart --- charts/clickhouse/.helmignore | 14 ++ charts/clickhouse/Chart.yaml | 49 ++++ charts/clickhouse/DESIGN.md | 37 +++ charts/clickhouse/README.md | 116 ++++++++++ charts/clickhouse/ci/auth-values.yaml | 5 + charts/clickhouse/ci/default-values.yaml | 3 + charts/clickhouse/ci/dual-stack.yaml | 3 + charts/clickhouse/ci/ephemeral-values.yaml | 6 + .../ci/external-secrets-values.yaml | 19 ++ charts/clickhouse/ci/metrics-values.yaml | 7 + .../clickhouse/ci/network-policy-values.yaml | 14 ++ charts/clickhouse/ci/production-values.yaml | 18 ++ charts/clickhouse/docs/external-secrets.md | 42 ++++ charts/clickhouse/docs/observability.md | 31 +++ charts/clickhouse/docs/production.md | 36 +++ .../clickhouse/examples/external-secrets.yaml | 19 ++ charts/clickhouse/examples/metrics.yaml | 8 + charts/clickhouse/examples/production.yaml | 24 ++ charts/clickhouse/examples/standalone.yaml | 6 + charts/clickhouse/templates/NOTES.txt | 88 +++++++ charts/clickhouse/templates/_helpers.tpl | 103 +++++++++ charts/clickhouse/templates/configmap.yaml | 31 +++ .../clickhouse/templates/externalsecret.yaml | 34 +++ .../clickhouse/templates/extra-manifests.yaml | 5 + .../clickhouse/templates/networkpolicy.yaml | 59 +++++ charts/clickhouse/templates/pdb.yaml | 19 ++ charts/clickhouse/templates/secret.yaml | 13 ++ charts/clickhouse/templates/service.yaml | 76 ++++++ .../clickhouse/templates/serviceaccount.yaml | 15 ++ .../clickhouse/templates/servicemonitor.yaml | 26 +++ charts/clickhouse/templates/statefulset.yaml | 182 +++++++++++++++ .../templates/tests/test-connection.yaml | 39 ++++ charts/clickhouse/templates/validate.yaml | 2 + .../clickhouse/tests/externalsecret_test.yaml | 32 +++ charts/clickhouse/tests/metrics_test.yaml | 26 +++ .../clickhouse/tests/networkpolicy_test.yaml | 42 ++++ charts/clickhouse/tests/pdb_test.yaml | 9 + charts/clickhouse/tests/secret_test.yaml | 19 ++ charts/clickhouse/tests/service_test.yaml | 47 ++++ charts/clickhouse/tests/statefulset_test.yaml | 62 +++++ .../tests/test_connection_test.yaml | 15 ++ charts/clickhouse/tests/validation_test.yaml | 42 ++++ charts/clickhouse/values.schema.json | 150 ++++++++++++ charts/clickhouse/values.yaml | 216 ++++++++++++++++++ 44 files changed, 1809 insertions(+) create mode 100644 charts/clickhouse/.helmignore create mode 100644 charts/clickhouse/Chart.yaml create mode 100644 charts/clickhouse/DESIGN.md create mode 100644 charts/clickhouse/README.md create mode 100644 charts/clickhouse/ci/auth-values.yaml create mode 100644 charts/clickhouse/ci/default-values.yaml create mode 100644 charts/clickhouse/ci/dual-stack.yaml create mode 100644 charts/clickhouse/ci/ephemeral-values.yaml create mode 100644 charts/clickhouse/ci/external-secrets-values.yaml create mode 100644 charts/clickhouse/ci/metrics-values.yaml create mode 100644 charts/clickhouse/ci/network-policy-values.yaml create mode 100644 charts/clickhouse/ci/production-values.yaml create mode 100644 charts/clickhouse/docs/external-secrets.md create mode 100644 charts/clickhouse/docs/observability.md create mode 100644 charts/clickhouse/docs/production.md create mode 100644 charts/clickhouse/examples/external-secrets.yaml create mode 100644 charts/clickhouse/examples/metrics.yaml create mode 100644 charts/clickhouse/examples/production.yaml create mode 100644 charts/clickhouse/examples/standalone.yaml create mode 100644 charts/clickhouse/templates/NOTES.txt create mode 100644 charts/clickhouse/templates/_helpers.tpl create mode 100644 charts/clickhouse/templates/configmap.yaml create mode 100644 charts/clickhouse/templates/externalsecret.yaml create mode 100644 charts/clickhouse/templates/extra-manifests.yaml create mode 100644 charts/clickhouse/templates/networkpolicy.yaml create mode 100644 charts/clickhouse/templates/pdb.yaml create mode 100644 charts/clickhouse/templates/secret.yaml create mode 100644 charts/clickhouse/templates/service.yaml create mode 100644 charts/clickhouse/templates/serviceaccount.yaml create mode 100644 charts/clickhouse/templates/servicemonitor.yaml create mode 100644 charts/clickhouse/templates/statefulset.yaml create mode 100644 charts/clickhouse/templates/tests/test-connection.yaml create mode 100644 charts/clickhouse/templates/validate.yaml create mode 100644 charts/clickhouse/tests/externalsecret_test.yaml create mode 100644 charts/clickhouse/tests/metrics_test.yaml create mode 100644 charts/clickhouse/tests/networkpolicy_test.yaml create mode 100644 charts/clickhouse/tests/pdb_test.yaml create mode 100644 charts/clickhouse/tests/secret_test.yaml create mode 100644 charts/clickhouse/tests/service_test.yaml create mode 100644 charts/clickhouse/tests/statefulset_test.yaml create mode 100644 charts/clickhouse/tests/test_connection_test.yaml create mode 100644 charts/clickhouse/tests/validation_test.yaml create mode 100644 charts/clickhouse/values.schema.json create mode 100644 charts/clickhouse/values.yaml diff --git a/charts/clickhouse/.helmignore b/charts/clickhouse/.helmignore new file mode 100644 index 000000000..31c27db2b --- /dev/null +++ b/charts/clickhouse/.helmignore @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 +.DS_Store +Thumbs.db +.git/ +.gitignore +.gitattributes +.helmignore +.vscode/ +.idea/ +*.orig +*.rej +*.swp +*.tmp +*.log diff --git a/charts/clickhouse/Chart.yaml b/charts/clickhouse/Chart.yaml new file mode 100644 index 000000000..833da5126 --- /dev/null +++ b/charts/clickhouse/Chart.yaml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: Apache-2.0 +apiVersion: v2 +name: clickhouse +description: Fast column-oriented OLAP database with production-safe standalone defaults +type: application +version: 0.1.0 +appVersion: "25.8.28.1" +kubeVersion: ">=1.26.0-0" +maintainers: + - name: helmforgedev + - name: mberlofa +keywords: + - clickhouse + - database + - olap + - analytics + - kubernetes +home: https://helmforge.dev/docs/charts/clickhouse +sources: + - https://github.com/helmforgedev/charts + - https://github.com/ClickHouse/ClickHouse + - https://hub.docker.com/r/clickhouse/clickhouse-server +icon: https://helmforge.dev/icons/charts/clickhouse.png +annotations: + artifacthub.io/changes: | + - kind: added + description: "add production-ready ClickHouse chart (#705)" + artifacthub.io/maintainers: | + - name: HelmForge + email: berlofa@helmforge.dev + - name: Maicon Berlofa + email: maicon.berloffa@gmail.com + helmforge.dev/maturity: beta + helmforge.dev/signed: gpg+cosign + artifacthub.io/signKey: | + fingerprint: 6748042FBAD8C2E11C65E564170D55148EB88A9D + url: https://repo.helmforge.dev/pgp-public-key.asc + helmforge.dev/repository: https://repo.helmforge.dev + artifacthub.io/license: Apache-2.0 + artifacthub.io/category: database + artifacthub.io/links: | + - name: Official Website + url: https://clickhouse.com + - name: Documentation + url: https://helmforge.dev/docs/charts/clickhouse + - name: Source + url: https://github.com/helmforgedev/charts/tree/main/charts/clickhouse + - name: support + url: https://github.com/helmforgedev/charts/issues diff --git a/charts/clickhouse/DESIGN.md b/charts/clickhouse/DESIGN.md new file mode 100644 index 000000000..d0edb7baa --- /dev/null +++ b/charts/clickhouse/DESIGN.md @@ -0,0 +1,37 @@ +# ClickHouse Chart Design + +## Scope + +This chart deploys the official ClickHouse server image as a single StatefulSet +with persistent data, headless and client Services, optional Prometheus metrics, +External Secrets support, NetworkPolicy, and Helm test coverage. + +## Differentiation + +- Uses official `clickhouse/clickhouse-server` full-version tags. +- Blocks unsafe `replicaCount > 1` and directs users to the ClickHouse Operator + for replicated topologies. +- Enables built-in Prometheus metrics without sidecar images. +- Keeps data and logs persistence separate. +- Includes ESO, dual-stack Service fields, NetworkPolicy, and k3d validation. + +## Non-goals + +- The chart does not implement shards, replicas, Keeper, or ZooKeeper. +- It does not render ClickHouse Operator CRDs. +- It does not automate cluster backup/restore because full backups require + filesystem-local access and topology-specific restore procedures. + + diff --git a/charts/clickhouse/README.md b/charts/clickhouse/README.md new file mode 100644 index 000000000..f1c31493c --- /dev/null +++ b/charts/clickhouse/README.md @@ -0,0 +1,116 @@ +# ClickHouse + +Fast column-oriented OLAP database with production-safe standalone defaults. + +## Installation + +```bash +helm repo add helmforge https://repo.helmforge.dev +helm install clickhouse helmforge/clickhouse +``` + +```bash +helm install clickhouse oci://ghcr.io/helmforgedev/helm/clickhouse +``` + +## Features + +- Official ClickHouse image pinned to `25.8.28.1`. +- StatefulSet with persistent data volume. +- Client Service exposing HTTP `8123` and native TCP `9000`. +- Headless Service for stable pod DNS. +- Built-in Prometheus metrics endpoint. +- ServiceMonitor support. +- External Secrets integration for the initial password. +- NetworkPolicy and dual-stack Service fields. +- Explicit guardrail blocking unsafe Helm-only replication. + +## Quick Start + +```yaml +persistence: + size: 20Gi +``` + +## Production Example + +```yaml +clickhouse: + database: analytics + user: analytics + existingSecret: clickhouse-auth +persistence: + size: 200Gi +metrics: + enabled: true + serviceMonitor: + enabled: true +networkPolicy: + enabled: true +``` + +## Configuration + +| Parameter | Description | Default | +| --- | --- | --- | +| `replicaCount` | ClickHouse pod count. Must remain `1` | `1` | +| `image.repository` | Official image repository | `docker.io/clickhouse/clickhouse-server` | +| `image.tag` | Official full-version tag | `25.8.28.1` | +| `clickhouse.database` | Initial database | `default` | +| `clickhouse.user` | Initial user | `default` | +| `clickhouse.password` | Initial password | `""` | +| `persistence.enabled` | Persist data volume | `true` | +| `persistence.size` | Data PVC size | `20Gi` | +| `metrics.enabled` | Built-in Prometheus endpoint | `false` | +| `networkPolicy.enabled` | Render NetworkPolicy | `false` | +| `service.ipFamilyPolicy` | Service dual-stack policy | `""` | + +## Examples + +- [Standalone](examples/standalone.yaml) +- [Production](examples/production.yaml) +- [External Secrets](examples/external-secrets.yaml) +- [Metrics](examples/metrics.yaml) + +## Architecture Guides + +- [Production](docs/production.md) +- [Observability](docs/observability.md) +- [External Secrets](docs/external-secrets.md) + +## Security Scan + +### Security Scan: `clickhouse` + +| Framework | Score | +|---|---| +| MITRE + NSA + SOC2 | **89.39%** | + +Security posture acceptable. + +Local details: + +- Tool: Kubescape v4.0.9 +- Command: `kubescape scan framework mitre,nsa,soc2 .tmp\clickhouse-render.yaml` +- Result: 0 critical failed resources, resource summary score 89.39%. + +## Operator Boundary + +ClickHouse replication needs Keeper or ZooKeeper, cluster definitions, and +cluster-aware operations. This chart intentionally blocks `replicaCount > 1`. +Use the ClickHouse Operator or Altinity operator stack for sharded or replicated +clusters. + + diff --git a/charts/clickhouse/ci/auth-values.yaml b/charts/clickhouse/ci/auth-values.yaml new file mode 100644 index 000000000..e6bc99a2b --- /dev/null +++ b/charts/clickhouse/ci/auth-values.yaml @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +clickhouse: + user: analytics + password: test-password + database: analytics diff --git a/charts/clickhouse/ci/default-values.yaml b/charts/clickhouse/ci/default-values.yaml new file mode 100644 index 000000000..f1f1a7e15 --- /dev/null +++ b/charts/clickhouse/ci/default-values.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +clickhouse: + database: default diff --git a/charts/clickhouse/ci/dual-stack.yaml b/charts/clickhouse/ci/dual-stack.yaml new file mode 100644 index 000000000..a11cf17a6 --- /dev/null +++ b/charts/clickhouse/ci/dual-stack.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +service: + ipFamilyPolicy: PreferDualStack diff --git a/charts/clickhouse/ci/ephemeral-values.yaml b/charts/clickhouse/ci/ephemeral-values.yaml new file mode 100644 index 000000000..b2798c2aa --- /dev/null +++ b/charts/clickhouse/ci/ephemeral-values.yaml @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +persistence: + enabled: false +logs: + persistence: + enabled: false diff --git a/charts/clickhouse/ci/external-secrets-values.yaml b/charts/clickhouse/ci/external-secrets-values.yaml new file mode 100644 index 000000000..4866e0dca --- /dev/null +++ b/charts/clickhouse/ci/external-secrets-values.yaml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +clickhouse: + existingSecret: clickhouse-auth +externalSecrets: + enabled: true + refreshInterval: 30m + items: + - fullnameOverride: clickhouse-auth + spec: + secretStoreRef: + name: helmforge-fake-store + kind: ClusterSecretStore + target: + name: clickhouse-auth + creationPolicy: Owner + data: + - secretKey: clickhouse-password + remoteRef: + key: test/password diff --git a/charts/clickhouse/ci/metrics-values.yaml b/charts/clickhouse/ci/metrics-values.yaml new file mode 100644 index 000000000..bf147aa62 --- /dev/null +++ b/charts/clickhouse/ci/metrics-values.yaml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +metrics: + enabled: true + serviceMonitor: + enabled: true + labels: + release: prometheus diff --git a/charts/clickhouse/ci/network-policy-values.yaml b/charts/clickhouse/ci/network-policy-values.yaml new file mode 100644 index 000000000..6de2b6046 --- /dev/null +++ b/charts/clickhouse/ci/network-policy-values.yaml @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 +metrics: + enabled: true +networkPolicy: + enabled: true + egress: + enabled: true + extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.0/8 + ports: + - protocol: TCP + port: 443 diff --git a/charts/clickhouse/ci/production-values.yaml b/charts/clickhouse/ci/production-values.yaml new file mode 100644 index 000000000..1e5425859 --- /dev/null +++ b/charts/clickhouse/ci/production-values.yaml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +clickhouse: + database: analytics + user: analytics + password: test-password +metrics: + enabled: true +persistence: + size: 50Gi +logs: + persistence: + enabled: true +resources: + requests: + cpu: "1" + memory: 2Gi + limits: + memory: 4Gi diff --git a/charts/clickhouse/docs/external-secrets.md b/charts/clickhouse/docs/external-secrets.md new file mode 100644 index 000000000..241462836 --- /dev/null +++ b/charts/clickhouse/docs/external-secrets.md @@ -0,0 +1,42 @@ +# External Secrets + +The chart implements the HelmForge canonical `externalSecrets.items[]` contract. +Use it to source the ClickHouse password from an external secret manager. + +```yaml +clickhouse: + existingSecret: clickhouse-auth +externalSecrets: + enabled: true + items: + - fullnameOverride: clickhouse-auth + spec: + secretStoreRef: + name: prod-secrets + kind: ClusterSecretStore + target: + name: clickhouse-auth + creationPolicy: Owner + data: + - secretKey: clickhouse-password + remoteRef: + key: prod/clickhouse/password +``` + +The official image reads `CLICKHOUSE_PASSWORD` only during first initialization +of an empty data directory. Rotate passwords with SQL once a data volume already +exists. + + diff --git a/charts/clickhouse/docs/observability.md b/charts/clickhouse/docs/observability.md new file mode 100644 index 000000000..f8ae8a785 --- /dev/null +++ b/charts/clickhouse/docs/observability.md @@ -0,0 +1,31 @@ +# Observability + +ClickHouse includes a built-in Prometheus endpoint. Set `metrics.enabled=true` +to render config under `config.d/helmforge.xml` and expose the metrics port on +the Service. + +## ServiceMonitor + +Set `metrics.serviceMonitor.enabled=true` when the Prometheus Operator CRDs are +installed in the cluster. The chart keeps labels and namespace selectors +customizable for common Prometheus deployments. + +## Useful Signals + +Track query latency, background merges, part counts, memory usage, disk usage, +replication queue health when using external replicated topologies, and HTTP or +native TCP availability. + + diff --git a/charts/clickhouse/docs/production.md b/charts/clickhouse/docs/production.md new file mode 100644 index 000000000..f71524b5e --- /dev/null +++ b/charts/clickhouse/docs/production.md @@ -0,0 +1,36 @@ +# Production Guide + +This chart intentionally deploys a standalone ClickHouse StatefulSet. It is a +good fit for single-node analytics stores, development, or applications that +need ClickHouse without a Kubernetes operator. + +## Replication Boundary + +Do not increase `replicaCount`. The chart blocks values greater than `1` +because ClickHouse replication requires Keeper or ZooKeeper, shard definitions, +and cluster-aware DDL. For those topologies, use the ClickHouse Operator or +Altinity operator charts. + +## Storage + +ClickHouse stores data under `/var/lib/clickhouse`. Use fast block storage and +confirm that the StorageClass supports expansion before production. + +## Upgrades + +Pin a full image version. Back up data before changing `image.tag`, run the new +version in staging, and watch logs during merges and mutations after startup. + + diff --git a/charts/clickhouse/examples/external-secrets.yaml b/charts/clickhouse/examples/external-secrets.yaml new file mode 100644 index 000000000..5c2834af4 --- /dev/null +++ b/charts/clickhouse/examples/external-secrets.yaml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Project the ClickHouse password from an external secret store. +clickhouse: + existingSecret: clickhouse-auth +externalSecrets: + enabled: true + items: + - fullnameOverride: clickhouse-auth + spec: + secretStoreRef: + name: prod-secrets + kind: ClusterSecretStore + target: + name: clickhouse-auth + creationPolicy: Owner + data: + - secretKey: clickhouse-password + remoteRef: + key: prod/clickhouse/password diff --git a/charts/clickhouse/examples/metrics.yaml b/charts/clickhouse/examples/metrics.yaml new file mode 100644 index 000000000..dace75b7c --- /dev/null +++ b/charts/clickhouse/examples/metrics.yaml @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +# Enable the built-in Prometheus endpoint and ServiceMonitor. +metrics: + enabled: true + serviceMonitor: + enabled: true + labels: + release: prometheus diff --git a/charts/clickhouse/examples/production.yaml b/charts/clickhouse/examples/production.yaml new file mode 100644 index 000000000..f3c09e8ed --- /dev/null +++ b/charts/clickhouse/examples/production.yaml @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Production standalone deployment. Use ClickHouse Operator for replicated clusters. +clickhouse: + database: analytics + user: analytics + existingSecret: clickhouse-auth +persistence: + size: 200Gi +logs: + persistence: + enabled: true + size: 20Gi +metrics: + enabled: true + serviceMonitor: + enabled: true +networkPolicy: + enabled: true +resources: + requests: + cpu: "2" + memory: 8Gi + limits: + memory: 16Gi diff --git a/charts/clickhouse/examples/standalone.yaml b/charts/clickhouse/examples/standalone.yaml new file mode 100644 index 000000000..96a5793d2 --- /dev/null +++ b/charts/clickhouse/examples/standalone.yaml @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Small standalone deployment for development and CI-like environments. +persistence: + size: 20Gi +clickhouse: + database: default diff --git a/charts/clickhouse/templates/NOTES.txt b/charts/clickhouse/templates/NOTES.txt new file mode 100644 index 000000000..9c8d83a40 --- /dev/null +++ b/charts/clickhouse/templates/NOTES.txt @@ -0,0 +1,88 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +ClickHouse has been installed. + +1. Installation summary +======================= +- Release: {{ .Release.Name }} +- Namespace: {{ include "clickhouse.namespace" . }} +- Chart: {{ .Chart.Name }} {{ .Chart.Version }} +- App version: {{ .Chart.AppVersion }} +- Image: {{ include "clickhouse.image" . }} +- Replicas: {{ .Values.replicaCount }} +- Metrics: {{ ternary "enabled" "disabled" .Values.metrics.enabled }} + +2. Access +========= +HTTP API: + http://{{ include "clickhouse.fullname" . }}.{{ include "clickhouse.namespace" . }}.svc.{{ .Values.clusterDomain }}:{{ .Values.service.ports.http }} + +Native TCP: + {{ include "clickhouse.fullname" . }}.{{ include "clickhouse.namespace" . }}.svc.{{ .Values.clusterDomain }}:{{ .Values.service.ports.tcp }} + +Port-forward HTTP: + kubectl -n {{ include "clickhouse.namespace" . }} port-forward svc/{{ include "clickhouse.fullname" . }} 8123:{{ .Values.service.ports.http }} + curl -fsS http://127.0.0.1:8123/ping + +Port-forward native TCP: + kubectl -n {{ include "clickhouse.namespace" . }} port-forward svc/{{ include "clickhouse.fullname" . }} 9000:{{ .Values.service.ports.tcp }} + +3. Getting started +================== +Wait for readiness: + kubectl -n {{ include "clickhouse.namespace" . }} rollout status statefulset/{{ include "clickhouse.fullname" . }} --timeout=5m + +Run an HTTP query: + kubectl -n {{ include "clickhouse.namespace" . }} run {{ include "clickhouse.fullname" . }}-query --rm -i --restart=Never --image=docker.io/curlimages/curl:8.17.0 -- curl -fsS "http://{{ include "clickhouse.fullname" . }}:{{ .Values.service.ports.http }}/?query=SELECT%201" + +4. Credentials +============== +- User: {{ .Values.clickhouse.user }} +{{- if or .Values.clickhouse.password .Values.clickhouse.existingSecret .Values.externalSecrets.enabled }} +- Password secret: {{ include "clickhouse.secretName" . }} / {{ .Values.clickhouse.existingSecretPasswordKey }} +{{- else }} +- Password: empty by default. Set clickhouse.password, clickhouse.existingSecret, or externalSecrets for production. +{{- end }} + +5. Configuration summary +======================== +- Data PVC: {{ ternary "enabled" "disabled" .Values.persistence.enabled }} +- Data size: {{ .Values.persistence.size }} +- Logs PVC: {{ ternary "enabled" "disabled" .Values.logs.persistence.enabled }} +- Prometheus endpoint: {{ ternary (printf "enabled on %s:%d" .Values.metrics.path (.Values.service.ports.metrics | int)) "disabled" .Values.metrics.enabled }} + +6. Validation commands +====================== +Pods: + kubectl -n {{ include "clickhouse.namespace" . }} get pods -l app.kubernetes.io/instance={{ .Release.Name | quote }} -o wide + +Health: + kubectl -n {{ include "clickhouse.namespace" . }} exec statefulset/{{ include "clickhouse.fullname" . }} -- wget -qO- http://127.0.0.1:{{ .Values.service.ports.http }}/ping + +Helm test: + helm test {{ .Release.Name }} -n {{ include "clickhouse.namespace" . }} + +7. Troubleshooting +================== +Events: + kubectl -n {{ include "clickhouse.namespace" . }} get events --sort-by=.lastTimestamp + +Server logs: + kubectl -n {{ include "clickhouse.namespace" . }} logs statefulset/{{ include "clickhouse.fullname" . }} -c clickhouse --tail=200 + +Configuration: + kubectl -n {{ include "clickhouse.namespace" . }} get configmap {{ include "clickhouse.configMapName" . }} -o yaml + +Common checks: +- If the pod is pending, inspect PVC binding and StorageClass availability. +- If authentication fails, verify the password secret before first startup. +- If you need shards or replicas, use the ClickHouse Operator instead of increasing replicaCount. +- If metrics are missing, set metrics.enabled=true and verify the Service exposes the metrics port. + +8. Resources +============ +- HelmForge docs: https://helmforge.dev/docs/charts/clickhouse +- ClickHouse Docker docs: https://clickhouse.com/docs/install/docker +- ClickHouse Operator docs: https://clickhouse.com/docs/clickhouse-operator/guides/introduction +- Issues: https://github.com/helmforgedev/charts/issues + +Happy Helmforging :) diff --git a/charts/clickhouse/templates/_helpers.tpl b/charts/clickhouse/templates/_helpers.tpl new file mode 100644 index 000000000..acbfe3117 --- /dev/null +++ b/charts/clickhouse/templates/_helpers.tpl @@ -0,0 +1,103 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- define "clickhouse.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "clickhouse.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "clickhouse.namespace" -}} +{{- .Values.namespaceOverride | default .Release.Namespace -}} +{{- end -}} + +{{- define "clickhouse.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "clickhouse.selectorLabels" -}} +app.kubernetes.io/name: {{ include "clickhouse.name" . }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +{{- end -}} + +{{- define "clickhouse.labels" -}} +helm.sh/chart: {{ include "clickhouse.chart" . }} +{{ include "clickhouse.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/part-of: helmforge +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end -}} + +{{- define "clickhouse.image" -}} +{{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) -}} +{{- end -}} + +{{- define "clickhouse.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "clickhouse.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{- define "clickhouse.suffixedName" -}} +{{- $base := .base -}} +{{- $suffix := .suffix -}} +{{- $baseMax := int (max 1 (sub 63 (len $suffix))) -}} +{{- printf "%s%s" ($base | trunc $baseMax | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "clickhouse.headlessServiceName" -}} +{{- include "clickhouse.suffixedName" (dict "base" (include "clickhouse.fullname" .) "suffix" "-headless") -}} +{{- end -}} + +{{- define "clickhouse.configMapName" -}} +{{- include "clickhouse.suffixedName" (dict "base" (include "clickhouse.fullname" .) "suffix" "-config") -}} +{{- end -}} + +{{- define "clickhouse.secretName" -}} +{{- if .Values.clickhouse.existingSecret -}} +{{- .Values.clickhouse.existingSecret -}} +{{- else -}} +{{- include "clickhouse.suffixedName" (dict "base" (include "clickhouse.fullname" .) "suffix" "-auth") -}} +{{- end -}} +{{- end -}} + +{{- define "clickhouse.externalSecretName" -}} +{{- $root := .root -}} +{{- $item := .item -}} +{{- $index := int (.index | default 0) -}} +{{- if $item.fullnameOverride -}} +{{- $item.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else if $item.name -}} +{{- include "clickhouse.suffixedName" (dict "base" (include "clickhouse.fullname" $root) "suffix" (printf "-%s" $item.name)) -}} +{{- else if gt $index 0 -}} +{{- include "clickhouse.suffixedName" (dict "base" (include "clickhouse.fullname" $root) "suffix" (printf "-%d" $index)) -}} +{{- else -}} +{{- include "clickhouse.secretName" $root -}} +{{- end -}} +{{- end -}} + +{{- define "clickhouse.validate" -}} +{{- if lt (.Values.replicaCount | int) 1 -}}{{- fail "replicaCount must be at least 1" -}}{{- end -}} +{{- if gt (.Values.replicaCount | int) 1 -}}{{- fail "replicaCount > 1 is intentionally blocked in this chart; use ClickHouse Operator for replicated clusters" -}}{{- end -}} +{{- if and .Values.metrics.serviceMonitor.enabled (not .Values.metrics.enabled) -}}{{- fail "metrics.serviceMonitor.enabled requires metrics.enabled=true" -}}{{- end -}} +{{- if and .Values.clickhouse.existingSecret .Values.clickhouse.password -}}{{- fail "clickhouse.existingSecret and clickhouse.password are mutually exclusive" -}}{{- end -}} +{{- if .Values.podLabels -}} +{{- if hasKey .Values.podLabels "app.kubernetes.io/name" -}}{{- fail "podLabels must not override app.kubernetes.io/name" -}}{{- end -}} +{{- if hasKey .Values.podLabels "app.kubernetes.io/instance" -}}{{- fail "podLabels must not override app.kubernetes.io/instance" -}}{{- end -}} +{{- end -}} +{{- if and .Values.externalSecrets.enabled (empty .Values.externalSecrets.items) -}}{{- fail "externalSecrets.items must contain at least one item when externalSecrets.enabled=true" -}}{{- end -}} +{{- end -}} diff --git a/charts/clickhouse/templates/configmap.yaml b/charts/clickhouse/templates/configmap.yaml new file mode 100644 index 000000000..6784690b2 --- /dev/null +++ b/charts/clickhouse/templates/configmap.yaml @@ -0,0 +1,31 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "clickhouse.configMapName" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} +data: + helmforge.xml: | + + 0.0.0.0 + {{- if .Values.metrics.enabled }} + + {{ .Values.metrics.path }} + {{ .Values.service.ports.metrics }} + true + true + true + + {{- end }} +{{- with .Values.clickhouse.extraConfig }} +{{ . | indent 6 }} +{{- end }} + + users-helmforge.xml: | + +{{- with .Values.clickhouse.extraUsersConfig }} +{{ . | indent 6 }} +{{- end }} + diff --git a/charts/clickhouse/templates/externalsecret.yaml b/charts/clickhouse/templates/externalsecret.yaml new file mode 100644 index 000000000..483fafed8 --- /dev/null +++ b/charts/clickhouse/templates/externalsecret.yaml @@ -0,0 +1,34 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.externalSecrets.enabled }} +{{- range $index, $item := .Values.externalSecrets.items }} +{{- if gt $index 0 }} +--- +{{- end }} +{{- $name := include "clickhouse.externalSecretName" (dict "root" $ "item" $item "index" $index) }} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $name }} + namespace: {{ include "clickhouse.namespace" $ | quote }} + labels: + {{- include "clickhouse.labels" $ | nindent 4 }} + {{- with $item.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $item.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- $spec := deepCopy $item.spec }} + {{- if not (hasKey $spec "refreshInterval") }} + {{- $_ := set $spec "refreshInterval" $.Values.externalSecrets.refreshInterval }} + {{- end }} + {{- if not (hasKey $spec "target") }} + {{- $_ := set $spec "target" (dict "name" $name "creationPolicy" "Owner") }} + {{- else if not (hasKey $spec.target "name") }} + {{- $_ := set $spec.target "name" $name }} + {{- end }} + {{- toYaml $spec | nindent 2 }} +{{- end }} +{{- end }} diff --git a/charts/clickhouse/templates/extra-manifests.yaml b/charts/clickhouse/templates/extra-manifests.yaml new file mode 100644 index 000000000..930833f33 --- /dev/null +++ b/charts/clickhouse/templates/extra-manifests.yaml @@ -0,0 +1,5 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- range .Values.extraManifests }} +--- +{{ toYaml . }} +{{- end }} diff --git a/charts/clickhouse/templates/networkpolicy.yaml b/charts/clickhouse/templates/networkpolicy.yaml new file mode 100644 index 000000000..540e7e4a6 --- /dev/null +++ b/charts/clickhouse/templates/networkpolicy.yaml @@ -0,0 +1,59 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "clickhouse.fullname" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "clickhouse.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + {{- if .Values.networkPolicy.egress.enabled }} + - Egress + {{- end }} + ingress: + - from: + {{- if .Values.networkPolicy.ingressFrom }} + {{- toYaml .Values.networkPolicy.ingressFrom | nindent 8 }} + {{- else }} + - namespaceSelector: {} + {{- end }} + ports: + - protocol: TCP + port: http + - protocol: TCP + port: tcp + {{- if .Values.metrics.enabled }} + - from: + {{- if .Values.networkPolicy.metricsFrom }} + {{- toYaml .Values.networkPolicy.metricsFrom | nindent 8 }} + {{- else }} + - namespaceSelector: {} + {{- end }} + ports: + - protocol: TCP + port: metrics + {{- end }} + {{- if .Values.networkPolicy.egress.enabled }} + egress: + {{- if .Values.networkPolicy.egress.allowDNS }} + - ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + {{- end }} + {{- with .Values.networkPolicy.egress.extraTo }} + - to: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.networkPolicy.egress.extraEgress }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/clickhouse/templates/pdb.yaml b/charts/clickhouse/templates/pdb.yaml new file mode 100644 index 000000000..03d11e6fd --- /dev/null +++ b/charts/clickhouse/templates/pdb.yaml @@ -0,0 +1,19 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if and .Values.podDisruptionBudget.enabled (gt (int .Values.replicaCount) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "clickhouse.fullname" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "clickhouse.selectorLabels" . | nindent 6 }} + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/charts/clickhouse/templates/secret.yaml b/charts/clickhouse/templates/secret.yaml new file mode 100644 index 000000000..9988ec3de --- /dev/null +++ b/charts/clickhouse/templates/secret.yaml @@ -0,0 +1,13 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if and .Values.clickhouse.password (not .Values.clickhouse.existingSecret) (not .Values.externalSecrets.enabled) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "clickhouse.secretName" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} +type: Opaque +stringData: + clickhouse-password: {{ .Values.clickhouse.password | quote }} +{{- end }} diff --git a/charts/clickhouse/templates/service.yaml b/charts/clickhouse/templates/service.yaml new file mode 100644 index 000000000..a6cfc2c51 --- /dev/null +++ b/charts/clickhouse/templates/service.yaml @@ -0,0 +1,76 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "clickhouse.fullname" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- with .Values.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ . }} + {{- end }} + {{- with .Values.service.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end }} + ports: + - name: http + port: {{ .Values.service.ports.http }} + targetPort: http + protocol: TCP + - name: tcp + port: {{ .Values.service.ports.tcp }} + targetPort: tcp + protocol: TCP + {{- if .Values.metrics.enabled }} + - name: metrics + port: {{ .Values.service.ports.metrics }} + targetPort: metrics + protocol: TCP + {{- end }} + selector: + {{- include "clickhouse.selectorLabels" . | nindent 4 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "clickhouse.headlessServiceName" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} + {{- with .Values.service.headless.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + clusterIP: None + publishNotReadyAddresses: {{ .Values.service.headless.publishNotReadyAddresses }} + {{- with .Values.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ . }} + {{- end }} + {{- with .Values.service.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end }} + ports: + - name: http + port: {{ .Values.service.ports.http }} + targetPort: http + protocol: TCP + - name: tcp + port: {{ .Values.service.ports.tcp }} + targetPort: tcp + protocol: TCP + - name: interserver + port: {{ .Values.service.ports.interserver }} + targetPort: interserver + protocol: TCP + selector: + {{- include "clickhouse.selectorLabels" . | nindent 4 }} diff --git a/charts/clickhouse/templates/serviceaccount.yaml b/charts/clickhouse/templates/serviceaccount.yaml new file mode 100644 index 000000000..83726b620 --- /dev/null +++ b/charts/clickhouse/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "clickhouse.serviceAccountName" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/clickhouse/templates/servicemonitor.yaml b/charts/clickhouse/templates/servicemonitor.yaml new file mode 100644 index 000000000..86f04db13 --- /dev/null +++ b/charts/clickhouse/templates/servicemonitor.yaml @@ -0,0 +1,26 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- if .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "clickhouse.fullname" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "clickhouse.selectorLabels" . | nindent 6 }} + endpoints: + - port: metrics + path: {{ .Values.metrics.path | quote }} + interval: {{ .Values.metrics.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} + {{- with .Values.metrics.serviceMonitor.namespaceSelector }} + namespaceSelector: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/clickhouse/templates/statefulset.yaml b/charts/clickhouse/templates/statefulset.yaml new file mode 100644 index 000000000..c400ce39d --- /dev/null +++ b/charts/clickhouse/templates/statefulset.yaml @@ -0,0 +1,182 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "clickhouse.fullname" . }} + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} +spec: + serviceName: {{ include "clickhouse.headlessServiceName" . }} + replicas: {{ .Values.replicaCount }} + podManagementPolicy: {{ .Values.podManagementPolicy }} + updateStrategy: + {{- toYaml .Values.updateStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "clickhouse.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "clickhouse.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "clickhouse.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + containers: + - name: clickhouse + image: {{ include "clickhouse.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CLICKHOUSE_DB + value: {{ .Values.clickhouse.database | quote }} + - name: CLICKHOUSE_USER + value: {{ .Values.clickhouse.user | quote }} + - name: CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT + value: {{ ternary "1" "0" .Values.clickhouse.defaultAccessManagement | quote }} + {{- if or .Values.clickhouse.password .Values.clickhouse.existingSecret .Values.externalSecrets.enabled }} + - name: CLICKHOUSE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "clickhouse.secretName" . }} + key: {{ .Values.clickhouse.existingSecretPasswordKey }} + {{- end }} + {{- with .Values.clickhouse.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.clickhouse.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.ports.http }} + protocol: TCP + - name: tcp + containerPort: {{ .Values.service.ports.tcp }} + protocol: TCP + - name: interserver + containerPort: {{ .Values.service.ports.interserver }} + protocol: TCP + {{- if .Values.metrics.enabled }} + - name: metrics + containerPort: {{ .Values.service.ports.metrics }} + protocol: TCP + {{- end }} + startupProbe: + httpGet: + path: /ping + port: http + {{- toYaml .Values.startupProbe | nindent 12 }} + livenessProbe: + httpGet: + path: /ping + port: http + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + httpGet: + path: /ping + port: http + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/clickhouse + - name: logs + mountPath: /var/log/clickhouse-server + - name: config + mountPath: /etc/clickhouse-server/config.d/helmforge.xml + subPath: helmforge.xml + readOnly: true + - name: config + mountPath: /etc/clickhouse-server/users.d/users-helmforge.xml + subPath: users-helmforge.xml + readOnly: true + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: config + configMap: + name: {{ include "clickhouse.configMapName" . }} + {{- if not .Values.persistence.enabled }} + - name: data + emptyDir: {} + {{- end }} + {{- if not .Values.logs.persistence.enabled }} + - name: logs + emptyDir: {} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if or .Values.persistence.enabled .Values.logs.persistence.enabled }} + volumeClaimTemplates: + {{- if .Values.persistence.enabled }} + - metadata: + name: data + spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 10 }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- with .Values.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} + {{- end }} + {{- if .Values.logs.persistence.enabled }} + - metadata: + name: logs + spec: + accessModes: + {{- toYaml .Values.logs.persistence.accessModes | nindent 10 }} + resources: + requests: + storage: {{ .Values.logs.persistence.size }} + {{- with .Values.logs.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} diff --git a/charts/clickhouse/templates/tests/test-connection.yaml b/charts/clickhouse/templates/tests/test-connection.yaml new file mode 100644 index 000000000..850d47095 --- /dev/null +++ b/charts/clickhouse/templates/tests/test-connection.yaml @@ -0,0 +1,39 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "clickhouse.fullname" . }}-test-connection" + namespace: {{ include "clickhouse.namespace" . | quote }} + labels: + {{- include "clickhouse.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: curl + image: docker.io/curlimages/curl:8.17.0 + imagePullPolicy: IfNotPresent + command: + - sh + - -ec + - curl -fsS "http://{{ include "clickhouse.fullname" . }}:{{ .Values.service.ports.http }}/ping" | grep -q Ok + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault diff --git a/charts/clickhouse/templates/validate.yaml b/charts/clickhouse/templates/validate.yaml new file mode 100644 index 000000000..8d7705808 --- /dev/null +++ b/charts/clickhouse/templates/validate.yaml @@ -0,0 +1,2 @@ +{{/* SPDX-License-Identifier: Apache-2.0 */}} +{{- include "clickhouse.validate" . -}} diff --git a/charts/clickhouse/tests/externalsecret_test.yaml b/charts/clickhouse/tests/externalsecret_test.yaml new file mode 100644 index 000000000..d131d88a3 --- /dev/null +++ b/charts/clickhouse/tests/externalsecret_test.yaml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse external secrets +templates: + - externalsecret.yaml +tests: + - it: does not render by default + asserts: + - hasDocuments: + count: 0 + - it: renders canonical ExternalSecret + set: + externalSecrets: + enabled: true + items: + - fullnameOverride: clickhouse-auth + spec: + secretStoreRef: + name: fake + kind: ClusterSecretStore + data: + - secretKey: clickhouse-password + remoteRef: + key: clickhouse/password + asserts: + - isKind: + of: ExternalSecret + - equal: + path: apiVersion + value: external-secrets.io/v1 + - equal: + path: spec.target.name + value: clickhouse-auth diff --git a/charts/clickhouse/tests/metrics_test.yaml b/charts/clickhouse/tests/metrics_test.yaml new file mode 100644 index 000000000..ddc503c71 --- /dev/null +++ b/charts/clickhouse/tests/metrics_test.yaml @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse metrics +templates: + - configmap.yaml + - service.yaml + - servicemonitor.yaml +tests: + - it: does not render ServiceMonitor by default + template: servicemonitor.yaml + asserts: + - hasDocuments: + count: 0 + - it: renders metrics port and ServiceMonitor + set: + metrics: + enabled: true + serviceMonitor: + enabled: true + asserts: + - isKind: + of: ServiceMonitor + template: servicemonitor.yaml + - equal: + path: spec.endpoints[0].port + value: metrics + template: servicemonitor.yaml diff --git a/charts/clickhouse/tests/networkpolicy_test.yaml b/charts/clickhouse/tests/networkpolicy_test.yaml new file mode 100644 index 000000000..65d1a1a6d --- /dev/null +++ b/charts/clickhouse/tests/networkpolicy_test.yaml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse network policy +templates: + - networkpolicy.yaml +tests: + - it: does not render by default + asserts: + - hasDocuments: + count: 0 + - it: renders ingress network policy + set: + networkPolicy: + enabled: true + asserts: + - isKind: + of: NetworkPolicy + - contains: + path: spec.policyTypes + content: Ingress + - it: renders metrics ingress when metrics enabled + set: + metrics: + enabled: true + networkPolicy: + enabled: true + asserts: + - equal: + path: spec.ingress[1].ports[0].port + value: metrics + - it: renders DNS egress rule + set: + networkPolicy: + enabled: true + egress: + enabled: true + asserts: + - contains: + path: spec.policyTypes + content: Egress + - equal: + path: spec.egress[0].ports[0].port + value: 53 diff --git a/charts/clickhouse/tests/pdb_test.yaml b/charts/clickhouse/tests/pdb_test.yaml new file mode 100644 index 000000000..bd56fe451 --- /dev/null +++ b/charts/clickhouse/tests/pdb_test.yaml @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse pdb +templates: + - pdb.yaml +tests: + - it: does not render for standalone default + asserts: + - hasDocuments: + count: 0 diff --git a/charts/clickhouse/tests/secret_test.yaml b/charts/clickhouse/tests/secret_test.yaml new file mode 100644 index 000000000..63213ec38 --- /dev/null +++ b/charts/clickhouse/tests/secret_test.yaml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse secret +templates: + - secret.yaml +tests: + - it: does not render without inline password + asserts: + - hasDocuments: + count: 0 + - it: renders inline password secret + set: + clickhouse: + password: test-password + asserts: + - isKind: + of: Secret + - equal: + path: stringData.clickhouse-password + value: test-password diff --git a/charts/clickhouse/tests/service_test.yaml b/charts/clickhouse/tests/service_test.yaml new file mode 100644 index 000000000..0dcc31979 --- /dev/null +++ b/charts/clickhouse/tests/service_test.yaml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse services +templates: + - service.yaml +tests: + - it: renders client and headless services + asserts: + - hasDocuments: + count: 2 + - it: renders client service ports + documentIndex: 0 + asserts: + - isKind: + of: Service + - equal: + path: spec.ports[0].name + value: http + - equal: + path: spec.ports[1].name + value: tcp + - it: renders headless service + documentIndex: 1 + asserts: + - equal: + path: spec.clusterIP + value: None + - equal: + path: spec.publishNotReadyAddresses + value: true + - it: omits dual stack by default + documentIndex: 0 + asserts: + - notExists: + path: spec.ipFamilyPolicy + - it: renders dual stack on both services + set: + service: + ipFamilyPolicy: PreferDualStack + asserts: + - equal: + path: spec.ipFamilyPolicy + value: PreferDualStack + documentIndex: 0 + - equal: + path: spec.ipFamilyPolicy + value: PreferDualStack + documentIndex: 1 diff --git a/charts/clickhouse/tests/statefulset_test.yaml b/charts/clickhouse/tests/statefulset_test.yaml new file mode 100644 index 000000000..c2e4ecc3d --- /dev/null +++ b/charts/clickhouse/tests/statefulset_test.yaml @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse statefulset +templates: + - configmap.yaml + - statefulset.yaml +tests: + - it: renders official image + template: statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/clickhouse/clickhouse-server:25.8.28.1 + - it: exposes HTTP and native TCP ports + template: statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].name + value: http + - equal: + path: spec.template.spec.containers[0].ports[1].name + value: tcp + - it: mounts data persistence path + template: statefulset.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: data + mountPath: /var/lib/clickhouse + - it: renders password env when password configured + template: statefulset.yaml + set: + clickhouse: + password: test-password + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: CLICKHOUSE_PASSWORD + valueFrom: + secretKeyRef: + name: RELEASE-NAME-clickhouse-auth + key: clickhouse-password + - it: uses emptyDir when persistence disabled + template: statefulset.yaml + set: + persistence: + enabled: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: data + emptyDir: {} + - it: renders volumeClaimTemplates when persistence enabled + template: statefulset.yaml + asserts: + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: data diff --git a/charts/clickhouse/tests/test_connection_test.yaml b/charts/clickhouse/tests/test_connection_test.yaml new file mode 100644 index 000000000..d67023ce2 --- /dev/null +++ b/charts/clickhouse/tests/test_connection_test.yaml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse helm test +templates: + - templates/tests/test-connection.yaml +tests: + - it: renders helm test hook under templates + asserts: + - isKind: + of: Pod + - equal: + path: metadata.annotations["helm.sh/hook"] + value: test + - matchRegex: + path: spec.containers[0].command[2] + pattern: /ping diff --git a/charts/clickhouse/tests/validation_test.yaml b/charts/clickhouse/tests/validation_test.yaml new file mode 100644 index 000000000..b79cb92cb --- /dev/null +++ b/charts/clickhouse/tests/validation_test.yaml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +suite: clickhouse validations +templates: + - validate.yaml +tests: + - it: blocks unsafe replica count + set: + replicaCount: 2 + asserts: + - failedTemplate: + errorMessage: "replicaCount > 1 is intentionally blocked in this chart; use ClickHouse Operator for replicated clusters" + - it: blocks selector label override + set: + podLabels: + app.kubernetes.io/instance: bad + asserts: + - failedTemplate: + errorMessage: podLabels must not override app.kubernetes.io/instance + - it: fails ExternalSecrets without items + set: + externalSecrets: + enabled: true + items: [] + asserts: + - failedTemplate: + errorMessage: externalSecrets.items must contain at least one item when externalSecrets.enabled=true + - it: fails ServiceMonitor without metrics endpoint + set: + metrics: + serviceMonitor: + enabled: true + asserts: + - failedTemplate: + errorMessage: metrics.serviceMonitor.enabled requires metrics.enabled=true + - it: blocks inline password with existing secret + set: + clickhouse: + password: test + existingSecret: clickhouse-auth + asserts: + - failedTemplate: + errorMessage: clickhouse.existingSecret and clickhouse.password are mutually exclusive diff --git a/charts/clickhouse/values.schema.json b/charts/clickhouse/values.schema.json new file mode 100644 index 000000000..8d2963834 --- /dev/null +++ b/charts/clickhouse/values.schema.json @@ -0,0 +1,150 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "ClickHouse Helm Chart Values", + "type": "object", + "additionalProperties": true, + "properties": { + "nameOverride": { "type": "string" }, + "fullnameOverride": { "type": "string" }, + "namespaceOverride": { "type": "string" }, + "clusterDomain": { "type": "string" }, + "commonLabels": { "type": "object" }, + "replicaCount": { "type": "integer", "minimum": 1 }, + "image": { + "type": "object", + "properties": { + "repository": { "type": "string" }, + "tag": { "type": "string", "not": { "enum": ["latest"] } }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "clickhouse": { + "type": "object", + "properties": { + "database": { "type": "string" }, + "user": { "type": "string" }, + "password": { "type": "string" }, + "existingSecret": { "type": "string" }, + "existingSecretPasswordKey": { "type": "string" }, + "defaultAccessManagement": { "type": "boolean" }, + "extraConfig": { "type": "string" }, + "extraUsersConfig": { "type": "string" }, + "extraEnv": { "type": "array" }, + "extraEnvFrom": { "type": "array" } + } + }, + "service": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "annotations": { "type": "object" }, + "ports": { + "type": "object", + "properties": { + "http": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "tcp": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "interserver": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "metrics": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }, + "headless": { + "type": "object", + "properties": { + "annotations": { "type": "object" }, + "publishNotReadyAddresses": { "type": "boolean" } + } + }, + "ipFamilyPolicy": { "type": "string", "enum": ["", "SingleStack", "PreferDualStack", "RequireDualStack"] }, + "ipFamilies": { + "type": "array", + "items": { "type": "string", "enum": ["IPv4", "IPv6"] }, + "maxItems": 2, + "uniqueItems": true + } + } + }, + "persistence": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "storageClass": { "type": "string" }, + "accessModes": { "type": "array", "items": { "type": "string" } }, + "size": { "type": "string" } + } + }, + "logs": { + "type": "object", + "properties": { + "persistence": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "storageClass": { "type": "string" }, + "accessModes": { "type": "array", "items": { "type": "string" } }, + "size": { "type": "string" } + } + } + } + }, + "metrics": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "path": { "type": "string" }, + "serviceMonitor": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "interval": { "type": "string" }, + "scrapeTimeout": { "type": "string" }, + "labels": { "type": "object" }, + "namespaceSelector": { "type": "object" } + } + } + } + }, + "externalSecrets": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "refreshInterval": { "type": "string" }, + "items": { "type": "array" } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "ingressFrom": { "type": "array" }, + "metricsFrom": { "type": "array" }, + "egress": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "allowDNS": { "type": "boolean" }, + "extraTo": { "type": "array" }, + "extraEgress": { "type": "array" } + } + } + } + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "maxUnavailable": { "oneOf": [{ "type": "integer" }, { "type": "string" }] }, + "minAvailable": { "oneOf": [{ "type": "integer" }, { "type": "string" }] } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { "type": "boolean" }, + "name": { "type": "string" }, + "annotations": { "type": "object" }, + "automountServiceAccountToken": { "type": "boolean" } + } + }, + "extraManifests": { "type": "array" } + } +} diff --git a/charts/clickhouse/values.yaml b/charts/clickhouse/values.yaml new file mode 100644 index 000000000..394893adc --- /dev/null +++ b/charts/clickhouse/values.yaml @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: Apache-2.0 +# ============================================================================= +# ClickHouse Helm Chart +# ============================================================================= +# Uses the official clickhouse/clickhouse-server image. +# This chart intentionally targets production-safe standalone deployments. +# For sharded or replicated clusters, use the ClickHouse Operator/Altinity CRDs. +# ============================================================================= + +# -- Override chart name +nameOverride: "" + +# -- Override full release name +fullnameOverride: "" + +# -- Override namespace for namespaced resources. The namespace must already exist. +namespaceOverride: "" + +# -- Kubernetes cluster DNS domain +clusterDomain: cluster.local + +# -- Labels added to all resources +commonLabels: {} + +# -- Number of ClickHouse pods. Keep 1 unless you are deliberately managing replication outside this chart. +replicaCount: 1 + +image: + # -- Official ClickHouse image repository + repository: docker.io/clickhouse/clickhouse-server + # -- Official full-version tag + tag: "25.8.28.1" + # -- Image pull policy + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +clickhouse: + # -- Initial database created by the official entrypoint on empty data dir + database: default + # -- Initial user created by the official entrypoint + user: default + # -- Initial user password. Empty leaves default user passwordless; use existingSecret or ExternalSecret for production. + password: "" + # -- Existing Secret containing the ClickHouse password + existingSecret: "" + # -- Secret key containing the ClickHouse password + existingSecretPasswordKey: clickhouse-password + # -- Enable default access management for SQL user/role management + defaultAccessManagement: true + # -- Additional XML under config.d/helmforge.xml + extraConfig: "" + # -- Additional XML under users.d/helmforge-users.xml + extraUsersConfig: "" + # -- Extra environment variables for the ClickHouse container + extraEnv: [] + # -- Additional envFrom sources for the ClickHouse container + extraEnvFrom: [] + +service: + # -- Client Service type + type: ClusterIP + # -- Service annotations + annotations: {} + ports: + # -- HTTP API port + http: 8123 + # -- Native TCP port + tcp: 9000 + # -- Interserver HTTP port + interserver: 9009 + # -- Prometheus metrics port + metrics: 9363 + headless: + # -- Headless Service annotations + annotations: {} + # -- Publish not-ready addresses for stable pod DNS + publishNotReadyAddresses: true + # -- IP family policy: SingleStack | PreferDualStack | RequireDualStack. Empty uses cluster default. + ipFamilyPolicy: "" + # -- IP families. Empty uses cluster default. + ipFamilies: [] + +persistence: + # -- Enable persistent storage for /var/lib/clickhouse + enabled: true + # -- Storage class. Empty uses cluster default. + storageClass: "" + # -- PVC access modes + accessModes: + - ReadWriteOnce + # -- PVC size + size: 20Gi + +logs: + persistence: + # -- Persist /var/log/clickhouse-server separately + enabled: false + # -- Storage class. Empty uses cluster default. + storageClass: "" + # -- PVC access modes + accessModes: + - ReadWriteOnce + # -- PVC size + size: 5Gi + +metrics: + # -- Enable built-in ClickHouse Prometheus endpoint + enabled: false + # -- Prometheus endpoint path + path: /metrics + serviceMonitor: + # -- Create ServiceMonitor + enabled: false + interval: 30s + scrapeTimeout: 10s + labels: {} + namespaceSelector: {} + +externalSecrets: + # -- Render ExternalSecret resources + enabled: false + # -- Default refresh interval + refreshInterval: 1h + # -- ExternalSecret items using the canonical HelmForge items[] contract + items: [] + +networkPolicy: + # -- Create NetworkPolicy resources + enabled: false + ingressFrom: [] + metricsFrom: [] + egress: + # -- Render explicit egress rules + enabled: false + # -- Allow DNS egress + allowDNS: true + # -- Extra peers + extraTo: [] + # -- Extra complete egress rules + extraEgress: [] + +podDisruptionBudget: + # -- Create PodDisruptionBudget when replicaCount > 1 + enabled: true + # -- Maximum unavailable pods + maxUnavailable: 1 + # -- Minimum available pods. Takes precedence when set. + minAvailable: "" + +serviceAccount: + # -- Create a ServiceAccount + create: true + # -- ServiceAccount name override + name: "" + # -- ServiceAccount annotations + annotations: {} + # -- Automount Kubernetes API credentials + automountServiceAccountToken: false + +podSecurityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + +resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi + +startupProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + +livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + +readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + +terminationGracePeriodSeconds: 60 +podManagementPolicy: Parallel +updateStrategy: + type: RollingUpdate + +podLabels: {} +podAnnotations: {} +priorityClassName: "" +nodeSelector: {} +tolerations: [] +affinity: {} +topologySpreadConstraints: [] + +extraInitContainers: [] +extraContainers: [] +extraVolumes: [] +extraVolumeMounts: [] +extraManifests: [] From 5ea773f9184304c365d1e4df77055e374af4a344 Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Tue, 7 Jul 2026 06:30:36 -0300 Subject: [PATCH 3/6] fix(clickhouse): address review feedback --- charts/clickhouse/README.md | 2 +- charts/clickhouse/templates/networkpolicy.yaml | 4 ++-- charts/clickhouse/templates/statefulset.yaml | 1 + charts/clickhouse/tests/networkpolicy_test.yaml | 8 ++++++++ charts/clickhouse/tests/statefulset_test.yaml | 3 +++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/charts/clickhouse/README.md b/charts/clickhouse/README.md index f1c31493c..7386ece99 100644 --- a/charts/clickhouse/README.md +++ b/charts/clickhouse/README.md @@ -91,7 +91,7 @@ Security posture acceptable. Local details: - Tool: Kubescape v4.0.9 -- Command: `kubescape scan framework mitre,nsa,soc2 .tmp\clickhouse-render.yaml` +- Command: `kubescape scan framework mitre,nsa,soc2 .tmp/clickhouse-render.yaml` - Result: 0 critical failed resources, resource summary score 89.39%. ## Operator Boundary diff --git a/charts/clickhouse/templates/networkpolicy.yaml b/charts/clickhouse/templates/networkpolicy.yaml index 540e7e4a6..9a3531382 100644 --- a/charts/clickhouse/templates/networkpolicy.yaml +++ b/charts/clickhouse/templates/networkpolicy.yaml @@ -21,7 +21,7 @@ spec: {{- if .Values.networkPolicy.ingressFrom }} {{- toYaml .Values.networkPolicy.ingressFrom | nindent 8 }} {{- else }} - - namespaceSelector: {} + - podSelector: {} {{- end }} ports: - protocol: TCP @@ -33,7 +33,7 @@ spec: {{- if .Values.networkPolicy.metricsFrom }} {{- toYaml .Values.networkPolicy.metricsFrom | nindent 8 }} {{- else }} - - namespaceSelector: {} + - podSelector: {} {{- end }} ports: - protocol: TCP diff --git a/charts/clickhouse/templates/statefulset.yaml b/charts/clickhouse/templates/statefulset.yaml index c400ce39d..22000e4a8 100644 --- a/charts/clickhouse/templates/statefulset.yaml +++ b/charts/clickhouse/templates/statefulset.yaml @@ -24,6 +24,7 @@ spec: {{- end }} annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/clickhouse/tests/networkpolicy_test.yaml b/charts/clickhouse/tests/networkpolicy_test.yaml index 65d1a1a6d..7a9e31856 100644 --- a/charts/clickhouse/tests/networkpolicy_test.yaml +++ b/charts/clickhouse/tests/networkpolicy_test.yaml @@ -17,6 +17,10 @@ tests: - contains: path: spec.policyTypes content: Ingress + - contains: + path: spec.ingress[0].from + content: + podSelector: {} - it: renders metrics ingress when metrics enabled set: metrics: @@ -27,6 +31,10 @@ tests: - equal: path: spec.ingress[1].ports[0].port value: metrics + - contains: + path: spec.ingress[1].from + content: + podSelector: {} - it: renders DNS egress rule set: networkPolicy: diff --git a/charts/clickhouse/tests/statefulset_test.yaml b/charts/clickhouse/tests/statefulset_test.yaml index c2e4ecc3d..023cffe51 100644 --- a/charts/clickhouse/tests/statefulset_test.yaml +++ b/charts/clickhouse/tests/statefulset_test.yaml @@ -2,6 +2,7 @@ suite: clickhouse statefulset templates: - configmap.yaml + - secret.yaml - statefulset.yaml tests: - it: renders official image @@ -43,6 +44,8 @@ tests: secretKeyRef: name: RELEASE-NAME-clickhouse-auth key: clickhouse-password + - exists: + path: spec.template.metadata.annotations["checksum/secret"] - it: uses emptyDir when persistence disabled template: statefulset.yaml set: From 3fa323bbab93a85cdee52f9adf769e36da253cb6 Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Tue, 7 Jul 2026 06:30:37 -0300 Subject: [PATCH 4/6] fix(matomo): address review feedback --- charts/matomo/README.md | 2 +- charts/matomo/templates/_helpers.tpl | 2 +- charts/matomo/templates/cronjob-archiver.yaml | 4 ++++ charts/matomo/templates/deployment.yaml | 1 + charts/matomo/templates/tests/test-connection.yaml | 3 ++- charts/matomo/tests/archiver_test.yaml | 9 +++++++++ charts/matomo/tests/deployment_test.yaml | 3 +++ charts/matomo/tests/test_connection_test.yaml | 5 ++++- charts/matomo/tests/validation_test.yaml | 13 +++++++++++++ 9 files changed, 38 insertions(+), 4 deletions(-) diff --git a/charts/matomo/README.md b/charts/matomo/README.md index 5b1ffea0b..a3983351e 100644 --- a/charts/matomo/README.md +++ b/charts/matomo/README.md @@ -101,7 +101,7 @@ Security posture acceptable. Local details: - Tool: Kubescape v4.0.9 -- Command: `kubescape scan framework mitre,nsa,soc2 .tmp\matomo-render.yaml` +- Command: `kubescape scan framework mitre,nsa,soc2 .tmp/matomo-render.yaml` - Result: 0 critical failed resources, resource summary score 84.78%. ## Research Notes diff --git a/charts/matomo/templates/_helpers.tpl b/charts/matomo/templates/_helpers.tpl index 168a47f95..d3f7e7af5 100644 --- a/charts/matomo/templates/_helpers.tpl +++ b/charts/matomo/templates/_helpers.tpl @@ -169,7 +169,7 @@ app.kubernetes.io/part-of: helmforge {{- if and .Values.ingress.enabled (empty .Values.ingress.hosts) -}}{{- fail "ingress.hosts must contain at least one host when ingress.enabled=true" -}}{{- end -}} {{- if and .Values.gatewayAPI.enabled (empty .Values.gatewayAPI.httpRoutes) -}}{{- fail "gatewayAPI.httpRoutes must contain at least one route when gatewayAPI.enabled=true" -}}{{- end -}} {{- if and .Values.metrics.serviceMonitor.enabled (not .Values.metrics.serviceMonitor.interval) -}}{{- fail "metrics.serviceMonitor.interval is required when metrics.serviceMonitor.enabled=true" -}}{{- end -}} -{{- if and (eq $databaseMode "external") (not .Values.database.external.existingSecret) (not .Values.database.external.password) .Values.archiver.enabled -}}{{- fail "external database mode with archiver.enabled=true requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret" -}}{{- end -}} +{{- if and (eq $databaseMode "external") (not .Values.database.external.existingSecret) (not .Values.database.external.password) (not .Values.externalSecrets.enabled) -}}{{- fail "external database mode requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret" -}}{{- end -}} {{- if .Values.podLabels -}} {{- if hasKey .Values.podLabels "app.kubernetes.io/name" -}}{{- fail "podLabels must not override app.kubernetes.io/name" -}}{{- end -}} {{- if hasKey .Values.podLabels "app.kubernetes.io/instance" -}}{{- fail "podLabels must not override app.kubernetes.io/instance" -}}{{- end -}} diff --git a/charts/matomo/templates/cronjob-archiver.yaml b/charts/matomo/templates/cronjob-archiver.yaml index 9eea17a4d..f60848bfe 100644 --- a/charts/matomo/templates/cronjob-archiver.yaml +++ b/charts/matomo/templates/cronjob-archiver.yaml @@ -33,6 +33,10 @@ spec: - name: archive image: {{ include "matomo.image" . | quote }} imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 16 }} + {{- end }} command: - sh - -ec diff --git a/charts/matomo/templates/deployment.yaml b/charts/matomo/templates/deployment.yaml index cd8559e6a..56e4886ba 100644 --- a/charts/matomo/templates/deployment.yaml +++ b/charts/matomo/templates/deployment.yaml @@ -22,6 +22,7 @@ spec: {{- end }} annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/matomo/templates/tests/test-connection.yaml b/charts/matomo/templates/tests/test-connection.yaml index f03ec72e1..30331a024 100644 --- a/charts/matomo/templates/tests/test-connection.yaml +++ b/charts/matomo/templates/tests/test-connection.yaml @@ -11,6 +11,7 @@ metadata: "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: restartPolicy: Never + activeDeadlineSeconds: 60 containers: - name: curl image: docker.io/curlimages/curl:8.17.0 @@ -18,7 +19,7 @@ spec: command: - sh - -ec - - curl -fsS "http://{{ include "matomo.fullname" . }}:{{ .Values.service.port }}/matomo.php" >/dev/null + - curl -fsS --connect-timeout 5 --max-time 20 "http://{{ include "matomo.fullname" . }}:{{ .Values.service.port }}/matomo.php" >/dev/null resources: requests: cpu: 10m diff --git a/charts/matomo/tests/archiver_test.yaml b/charts/matomo/tests/archiver_test.yaml index 3ee945ed9..5a59a013d 100644 --- a/charts/matomo/tests/archiver_test.yaml +++ b/charts/matomo/tests/archiver_test.yaml @@ -30,3 +30,12 @@ tests: content: name: MATOMO_URL value: https://analytics.example.com + - it: applies container security context to archiver + set: + securityContext: + runAsNonRoot: true + runAsUser: 33 + asserts: + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].securityContext.runAsUser + value: 33 diff --git a/charts/matomo/tests/deployment_test.yaml b/charts/matomo/tests/deployment_test.yaml index 18c1425ab..d28b9b3dc 100644 --- a/charts/matomo/tests/deployment_test.yaml +++ b/charts/matomo/tests/deployment_test.yaml @@ -2,6 +2,7 @@ suite: matomo deployment templates: - configmap.yaml + - secret.yaml - deployment.yaml tests: - it: renders deployment with official image @@ -15,6 +16,8 @@ tests: - it: wires Matomo database environment variables template: deployment.yaml asserts: + - exists: + path: spec.template.metadata.annotations["checksum/secret"] - contains: path: spec.template.spec.containers[0].env content: diff --git a/charts/matomo/tests/test_connection_test.yaml b/charts/matomo/tests/test_connection_test.yaml index 7171e4c27..6ac3e7cca 100644 --- a/charts/matomo/tests/test_connection_test.yaml +++ b/charts/matomo/tests/test_connection_test.yaml @@ -12,4 +12,7 @@ tests: value: test - matchRegex: path: spec.containers[0].command[2] - pattern: matomo.php + pattern: --max-time 20.*matomo.php + - equal: + path: spec.activeDeadlineSeconds + value: 60 diff --git a/charts/matomo/tests/validation_test.yaml b/charts/matomo/tests/validation_test.yaml index 25b4c792d..c46071790 100644 --- a/charts/matomo/tests/validation_test.yaml +++ b/charts/matomo/tests/validation_test.yaml @@ -25,3 +25,16 @@ tests: asserts: - failedTemplate: errorMessage: externalSecrets.items must contain at least one item when externalSecrets.enabled=true + - it: fails external database without password even when archiver is disabled + set: + mysql: + enabled: false + database: + mode: external + external: + host: mysql.example.com + archiver: + enabled: false + asserts: + - failedTemplate: + errorMessage: external database mode requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret From 6d457d61deb7c8ac39c0b24b3d93312ad978576a Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Tue, 7 Jul 2026 06:38:36 -0300 Subject: [PATCH 5/6] fix(clickhouse): bound helm test runtime --- charts/clickhouse/templates/tests/test-connection.yaml | 3 ++- charts/clickhouse/tests/test_connection_test.yaml | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/charts/clickhouse/templates/tests/test-connection.yaml b/charts/clickhouse/templates/tests/test-connection.yaml index 850d47095..fbda997aa 100644 --- a/charts/clickhouse/templates/tests/test-connection.yaml +++ b/charts/clickhouse/templates/tests/test-connection.yaml @@ -11,6 +11,7 @@ metadata: "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: restartPolicy: Never + activeDeadlineSeconds: 60 containers: - name: curl image: docker.io/curlimages/curl:8.17.0 @@ -18,7 +19,7 @@ spec: command: - sh - -ec - - curl -fsS "http://{{ include "clickhouse.fullname" . }}:{{ .Values.service.ports.http }}/ping" | grep -q Ok + - curl -fsS --connect-timeout 5 --max-time 15 "http://{{ include "clickhouse.fullname" . }}:{{ .Values.service.ports.http }}/ping" | grep -q Ok resources: requests: cpu: 10m diff --git a/charts/clickhouse/tests/test_connection_test.yaml b/charts/clickhouse/tests/test_connection_test.yaml index d67023ce2..3fb90d0fe 100644 --- a/charts/clickhouse/tests/test_connection_test.yaml +++ b/charts/clickhouse/tests/test_connection_test.yaml @@ -12,4 +12,7 @@ tests: value: test - matchRegex: path: spec.containers[0].command[2] - pattern: /ping + pattern: --max-time 15.*/ping + - equal: + path: spec.activeDeadlineSeconds + value: 60 From bf674a366782f1c3999f5e71daef0ca2332fcb3b Mon Sep 17 00:00:00 2001 From: MergeCheck Date: Tue, 7 Jul 2026 06:52:20 -0300 Subject: [PATCH 6/6] fix(matomo): require database ExternalSecret target --- charts/matomo/templates/_helpers.tpl | 18 +++++++++- charts/matomo/tests/validation_test.yaml | 44 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/charts/matomo/templates/_helpers.tpl b/charts/matomo/templates/_helpers.tpl index d3f7e7af5..975edfc10 100644 --- a/charts/matomo/templates/_helpers.tpl +++ b/charts/matomo/templates/_helpers.tpl @@ -164,12 +164,28 @@ app.kubernetes.io/part-of: helmforge {{- end -}} {{- end -}} +{{- define "matomo.hasDatabaseExternalSecret" -}} +{{- $root := . -}} +{{- $databaseSecretName := include "matomo.databaseSecretName" . -}} +{{- $found := dict "value" false -}} +{{- if .Values.externalSecrets.enabled -}} +{{- range $index, $item := .Values.externalSecrets.items }} +{{- $externalSecretName := include "matomo.externalSecretName" (dict "root" $root "item" $item "index" $index) -}} +{{- $targetName := dig "spec" "target" "name" $externalSecretName $item -}} +{{- if eq (toString $targetName) $databaseSecretName -}} +{{- $_ := set $found "value" true -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $found.value -}}true{{- else -}}false{{- end -}} +{{- end -}} + {{- define "matomo.validate" -}} {{- $databaseMode := include "matomo.databaseMode" . -}} {{- if and .Values.ingress.enabled (empty .Values.ingress.hosts) -}}{{- fail "ingress.hosts must contain at least one host when ingress.enabled=true" -}}{{- end -}} {{- if and .Values.gatewayAPI.enabled (empty .Values.gatewayAPI.httpRoutes) -}}{{- fail "gatewayAPI.httpRoutes must contain at least one route when gatewayAPI.enabled=true" -}}{{- end -}} {{- if and .Values.metrics.serviceMonitor.enabled (not .Values.metrics.serviceMonitor.interval) -}}{{- fail "metrics.serviceMonitor.interval is required when metrics.serviceMonitor.enabled=true" -}}{{- end -}} -{{- if and (eq $databaseMode "external") (not .Values.database.external.existingSecret) (not .Values.database.external.password) (not .Values.externalSecrets.enabled) -}}{{- fail "external database mode requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret" -}}{{- end -}} +{{- if and (eq $databaseMode "external") (not .Values.database.external.existingSecret) (not .Values.database.external.password) (ne (include "matomo.hasDatabaseExternalSecret" .) "true") -}}{{- fail "external database mode requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret" -}}{{- end -}} {{- if .Values.podLabels -}} {{- if hasKey .Values.podLabels "app.kubernetes.io/name" -}}{{- fail "podLabels must not override app.kubernetes.io/name" -}}{{- end -}} {{- if hasKey .Values.podLabels "app.kubernetes.io/instance" -}}{{- fail "podLabels must not override app.kubernetes.io/instance" -}}{{- end -}} diff --git a/charts/matomo/tests/validation_test.yaml b/charts/matomo/tests/validation_test.yaml index c46071790..e99f79622 100644 --- a/charts/matomo/tests/validation_test.yaml +++ b/charts/matomo/tests/validation_test.yaml @@ -38,3 +38,47 @@ tests: asserts: - failedTemplate: errorMessage: external database mode requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret + - it: fails external database with unrelated ExternalSecret + set: + mysql: + enabled: false + database: + mode: external + external: + host: mysql.example.com + externalSecrets: + enabled: true + items: + - fullnameOverride: unrelated-secret + spec: + secretStoreRef: + name: helmforge-fake-store + kind: ClusterSecretStore + data: + - secretKey: database-password + remoteRef: + key: matomo/database-password + asserts: + - failedTemplate: + errorMessage: external database mode requires database.external.password, database.external.existingSecret, or ExternalSecret targeting the database secret + - it: accepts external database with database-targeting ExternalSecret + set: + mysql: + enabled: false + database: + mode: external + external: + host: mysql.example.com + externalSecrets: + enabled: true + items: + - spec: + secretStoreRef: + name: helmforge-fake-store + kind: ClusterSecretStore + data: + - secretKey: database-password + remoteRef: + key: matomo/database-password + asserts: + - notFailedTemplate: {}