diff --git a/charts/clickhouse/.helmignore b/charts/clickhouse/.helmignore
new file mode 100644
index 00000000..31c27db2
--- /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 00000000..833da512
--- /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 00000000..d0edb7ba
--- /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 00000000..7386ece9
--- /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 00000000..e6bc99a2
--- /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 00000000..f1f1a7e1
--- /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 00000000..a11cf17a
--- /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 00000000..b2798c2a
--- /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 00000000..4866e0dc
--- /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 00000000..bf147aa6
--- /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 00000000..6de2b604
--- /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 00000000..1e542585
--- /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 00000000..24146283
--- /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 00000000..f8ae8a78
--- /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 00000000..f71524b5
--- /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 00000000..5c2834af
--- /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 00000000..dace75b7
--- /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 00000000..f3c09e8e
--- /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 00000000..96a5793d
--- /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 00000000..9c8d83a4
--- /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 00000000..acbfe311
--- /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 00000000..6784690b
--- /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 00000000..483fafed
--- /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 00000000..930833f3
--- /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 00000000..9a353138
--- /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 }}
+ - podSelector: {}
+ {{- 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 }}
+ - podSelector: {}
+ {{- 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 00000000..03d11e6f
--- /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 00000000..9988ec3d
--- /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 00000000..a6cfc2c5
--- /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 00000000..83726b62
--- /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 00000000..86f04db1
--- /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 00000000..22000e4a
--- /dev/null
+++ b/charts/clickhouse/templates/statefulset.yaml
@@ -0,0 +1,183 @@
+{{/* 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 }}
+ checksum/secret: {{ include (print $.Template.BasePath "/secret.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 00000000..fbda997a
--- /dev/null
+++ b/charts/clickhouse/templates/tests/test-connection.yaml
@@ -0,0 +1,40 @@
+{{/* 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
+ activeDeadlineSeconds: 60
+ containers:
+ - name: curl
+ image: docker.io/curlimages/curl:8.17.0
+ imagePullPolicy: IfNotPresent
+ command:
+ - sh
+ - -ec
+ - curl -fsS --connect-timeout 5 --max-time 15 "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 00000000..8d770580
--- /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 00000000..d131d88a
--- /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 00000000..ddc503c7
--- /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 00000000..7a9e3185
--- /dev/null
+++ b/charts/clickhouse/tests/networkpolicy_test.yaml
@@ -0,0 +1,50 @@
+# 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
+ - contains:
+ path: spec.ingress[0].from
+ content:
+ podSelector: {}
+ - 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
+ - contains:
+ path: spec.ingress[1].from
+ content:
+ podSelector: {}
+ - 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 00000000..bd56fe45
--- /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 00000000..63213ec3
--- /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 00000000..0dcc3197
--- /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 00000000..023cffe5
--- /dev/null
+++ b/charts/clickhouse/tests/statefulset_test.yaml
@@ -0,0 +1,65 @@
+# SPDX-License-Identifier: Apache-2.0
+suite: clickhouse statefulset
+templates:
+ - configmap.yaml
+ - secret.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
+ - exists:
+ path: spec.template.metadata.annotations["checksum/secret"]
+ - 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 00000000..3fb90d0f
--- /dev/null
+++ b/charts/clickhouse/tests/test_connection_test.yaml
@@ -0,0 +1,18 @@
+# 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: --max-time 15.*/ping
+ - equal:
+ path: spec.activeDeadlineSeconds
+ value: 60
diff --git a/charts/clickhouse/tests/validation_test.yaml b/charts/clickhouse/tests/validation_test.yaml
new file mode 100644
index 00000000..b79cb92c
--- /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 00000000..8d296383
--- /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 00000000..394893ad
--- /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: []
diff --git a/charts/matomo/.helmignore b/charts/matomo/.helmignore
new file mode 100644
index 00000000..31c27db2
--- /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 00000000..c00356e7
--- /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 00000000..72da57d8
--- /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 00000000..a3983351
--- /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 00000000..9d703550
--- /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 00000000..a11cf17a
--- /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 00000000..18d076f7
--- /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 00000000..bb82ef78
--- /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 00000000..82c8f425
--- /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 00000000..eab3781b
--- /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 00000000..a2c9811c
--- /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 00000000..43e80f1e
--- /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 00000000..f834285f
--- /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 00000000..5f257675
--- /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 00000000..0d993e45
--- /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 00000000..ebf5e1f9
--- /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 00000000..d8e93445
--- /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 00000000..cd2b208f
--- /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 00000000..a23be6a0
--- /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 00000000..0889cad2
--- /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 00000000..975edfc1
--- /dev/null
+++ b/charts/matomo/templates/_helpers.tpl
@@ -0,0 +1,197 @@
+{{/* 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.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) (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 -}}
+{{- 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 00000000..fed9273e
--- /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 00000000..f60848bf
--- /dev/null
+++ b/charts/matomo/templates/cronjob-archiver.yaml
@@ -0,0 +1,83 @@
+{{/* 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 }}
+ {{- with .Values.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 16 }}
+ {{- end }}
+ 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 00000000..56e4886b
--- /dev/null
+++ b/charts/matomo/templates/deployment.yaml
@@ -0,0 +1,180 @@
+{{/* 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 }}
+ checksum/secret: {{ include (print $.Template.BasePath "/secret.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 00000000..6563e1c0
--- /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 00000000..930833f3
--- /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 00000000..c5121140
--- /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 00000000..b38caa64
--- /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 00000000..8070c27f
--- /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 00000000..e4a3cb84
--- /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 00000000..01821544
--- /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 00000000..e939d351
--- /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 00000000..58d47edd
--- /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 00000000..e1405b53
--- /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 00000000..7c39b6a5
--- /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 00000000..30331a02
--- /dev/null
+++ b/charts/matomo/templates/tests/test-connection.yaml
@@ -0,0 +1,40 @@
+{{/* 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
+ activeDeadlineSeconds: 60
+ containers:
+ - name: curl
+ image: docker.io/curlimages/curl:8.17.0
+ imagePullPolicy: IfNotPresent
+ command:
+ - sh
+ - -ec
+ - curl -fsS --connect-timeout 5 --max-time 20 "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 00000000..47a6f955
--- /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 00000000..5a59a013
--- /dev/null
+++ b/charts/matomo/tests/archiver_test.yaml
@@ -0,0 +1,41 @@
+# 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
+ - 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
new file mode 100644
index 00000000..d28b9b3d
--- /dev/null
+++ b/charts/matomo/tests/deployment_test.yaml
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+suite: matomo deployment
+templates:
+ - configmap.yaml
+ - secret.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:
+ - exists:
+ path: spec.template.metadata.annotations["checksum/secret"]
+ - 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 00000000..c0bef92b
--- /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 00000000..aa102260
--- /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 00000000..b6309e94
--- /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 00000000..11b7ddef
--- /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 00000000..30458f0b
--- /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 00000000..6ac3e7cc
--- /dev/null
+++ b/charts/matomo/tests/test_connection_test.yaml
@@ -0,0 +1,18 @@
+# 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: --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
new file mode 100644
index 00000000..e99f7962
--- /dev/null
+++ b/charts/matomo/tests/validation_test.yaml
@@ -0,0 +1,84 @@
+# 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
+ - 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
+ - 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: {}
diff --git a/charts/matomo/values.schema.json b/charts/matomo/values.schema.json
new file mode 100644
index 00000000..9bf017a3
--- /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 00000000..53a93d6c
--- /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: []