commit 3da15981bdb29358b04dc94fce07d02f2cadf09f Author: 0xFEEDC0DE64 Date: Wed Oct 21 18:54:33 2026 +0200 Add Helm chart for Matrix Alertmanager receiver diff --git a/.gitea/workflows/chart.yml b/.gitea/workflows/chart.yml new file mode 100644 index 0000000..c874cba --- /dev/null +++ b/.gitea/workflows/chart.yml @@ -0,0 +1,86 @@ +name: Validate, publish and promote Matrix Alertmanager receiver chart + +on: + push: + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: azure/setup-helm@v4 + with: + version: v4.3.0 + - name: Install chart-test dependency + run: | + python3 -m venv "${RUNNER_TEMP}/chart-test" + "${RUNNER_TEMP}/chart-test/bin/pip" install PyYAML + echo "${RUNNER_TEMP}/chart-test/bin" >> "${GITHUB_PATH}" + - run: ./test.sh + + publish: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: azure/setup-helm@v4 + with: + version: v4.3.0 + - name: Select immutable chart version + run: echo "CHART_VERSION=$(./scripts/chart-version.sh)" >> "${GITHUB_ENV}" + - name: Package and publish chart + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + run: | + set -euo pipefail + test -n "${PACKAGE_USERNAME}" && test -n "${PACKAGE_TOKEN}" + helm package chart/matrix-alertmanager-receiver \ + --destination "${RUNNER_TEMP}" \ + --version "${CHART_VERSION}" + curl --fail --silent --show-error \ + --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST \ + --upload-file "${RUNNER_TEMP}/matrix-alertmanager-receiver-${CHART_VERSION}.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + - name: Check that this is still current main + id: current-main + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + git fetch --no-tags origin main + if [ "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}" ]; then + echo "promote=true" >> "${GITHUB_OUTPUT}" + else + echo "promote=false" >> "${GITHUB_OUTPUT}" + fi + - uses: actions/checkout@v4 + if: steps.current-main.outputs.promote == 'true' + with: + repository: feedc0de/homelab-gitops + ref: main + token: ${{ secrets.GITOPS_TOKEN }} + path: homelab-gitops + - name: Promote chart version + if: steps.current-main.outputs.promote == 'true' + working-directory: homelab-gitops + run: | + set -euo pipefail + ./scripts/update-chart-version.sh matrix-alertmanager-receiver "${CHART_VERSION}" + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + git add bootstrap/matrix-alertmanager-receiver.yaml + if git diff --cached --quiet; then exit 0; fi + git commit -m "Deploy Matrix receiver chart ${CHART_VERSION}" + for attempt in 1 2 3; do + if git push origin HEAD:main; then exit 0; fi + if [ "${attempt}" = 3 ]; then exit 1; fi + git fetch origin main + git rebase origin/main + done diff --git a/README.md b/README.md new file mode 100644 index 0000000..015b254 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Matrix Alertmanager receiver Helm chart + +This repository packages the upstream +[`metio/matrix-alertmanager-receiver`](https://github.com/metio/matrix-alertmanager-receiver) +container as a reusable Helm chart. It does not rebuild or mirror the upstream +image. The chart owns the ConfigMap, Deployment, Service, probes, and optional +ServiceMonitor. Set the Matrix homeserver, bot ID, room mapping, and existing +Secret reference in values; never put an access token in Helm values. + +The image tag defaults to the chart's `appVersion`. The Git tag follows the +upstream application release; CI derives package versions with +`git describe --tags --long`. A tag release packages without the leading `v`; +later chart-only commits append their distance and commit hash. + +For the public chart repository: + +```sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +helm search repo brunner.ninja/matrix-alertmanager-receiver +``` + +For a local install from this checkout, create the referenced Kubernetes +Secret first, then run `NAMESPACE=prometheus ./chart/install.sh` with the +appropriate values file. Argo CD is the normal deployment path in this cluster. diff --git a/chart/install.sh b/chart/install.sh new file mode 100755 index 0000000..bbd07c0 --- /dev/null +++ b/chart/install.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_dir=$(CDPATH= cd -- "${script_dir}/.." && pwd) +namespace=${NAMESPACE:-default} +version=${CHART_VERSION:-$(${repo_dir}/scripts/chart-version.sh)} + +helm repo add brunner.ninja https://brunner.ninja/charts --force-update +helm repo update brunner.ninja +helm upgrade --install matrix-alertmanager-receiver \ + brunner.ninja/matrix-alertmanager-receiver \ + --version "${version}" \ + --namespace "${namespace}" \ + --create-namespace \ + --wait \ + --timeout 10m \ + "$@" diff --git a/chart/matrix-alertmanager-receiver/Chart.yaml b/chart/matrix-alertmanager-receiver/Chart.yaml new file mode 100644 index 0000000..f3f045a --- /dev/null +++ b/chart/matrix-alertmanager-receiver/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: matrix-alertmanager-receiver +description: Matrix webhook receiver for Prometheus Alertmanager +type: application +version: 0.0.0 +appVersion: "2026.9.23" diff --git a/chart/matrix-alertmanager-receiver/templates/_helpers.tpl b/chart/matrix-alertmanager-receiver/templates/_helpers.tpl new file mode 100644 index 0000000..a7443ff --- /dev/null +++ b/chart/matrix-alertmanager-receiver/templates/_helpers.tpl @@ -0,0 +1,19 @@ +{{- define "matrix-alertmanager-receiver.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "matrix-alertmanager-receiver.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s" (include "matrix-alertmanager-receiver.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "matrix-alertmanager-receiver.labels" -}} +app.kubernetes.io/name: {{ include "matrix-alertmanager-receiver.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | quote }} +{{- end -}} diff --git a/chart/matrix-alertmanager-receiver/templates/configmap.yaml b/chart/matrix-alertmanager-receiver/templates/configmap.yaml new file mode 100644 index 0000000..f201a99 --- /dev/null +++ b/chart/matrix-alertmanager-receiver/templates/configmap.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "matrix-alertmanager-receiver.fullname" . }} + labels: + {{- include "matrix-alertmanager-receiver.labels" . | nindent 4 }} +data: + config.yaml: | + http: + port: {{ .Values.service.port }} + metrics-enabled: true + matrix: + homeserver-url: {{ required "matrix.homeserverUrl is required" .Values.matrix.homeserverUrl | quote }} + user-id: {{ required "matrix.userId is required" .Values.matrix.userId | quote }} + access-token: ${MATRIX_ACCESS_TOKEN} + room-mapping: +{{ toYaml .Values.matrix.roomMapping | indent 8 }} + templating: + firing-template: {{ .Values.templating.firing | quote }} + resolved-template: {{ .Values.templating.resolved | quote }} diff --git a/chart/matrix-alertmanager-receiver/templates/deployment.yaml b/chart/matrix-alertmanager-receiver/templates/deployment.yaml new file mode 100644 index 0000000..d85e1f0 --- /dev/null +++ b/chart/matrix-alertmanager-receiver/templates/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "matrix-alertmanager-receiver.fullname" . }} + labels: + {{- include "matrix-alertmanager-receiver.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "matrix-alertmanager-receiver.name" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "matrix-alertmanager-receiver.name" . }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + automountServiceAccountToken: false + containers: + - name: receiver + image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - --config-path + - /etc/matrix-receiver/config.yaml + env: + - name: MATRIX_ACCESS_TOKEN + valueFrom: + secretKeyRef: + name: {{ default (printf "%s-auth" (include "matrix-alertmanager-receiver.fullname" .)) .Values.matrix.accessTokenSecret.name }} + key: {{ .Values.matrix.accessTokenSecret.key }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + volumeMounts: + - name: config + mountPath: /etc/matrix-receiver + readOnly: true + readinessProbe: + httpGet: + path: /metrics + port: http + periodSeconds: 10 + livenessProbe: + httpGet: + path: /metrics + port: http + periodSeconds: 30 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: config + configMap: + name: {{ include "matrix-alertmanager-receiver.fullname" . }} diff --git a/chart/matrix-alertmanager-receiver/templates/service.yaml b/chart/matrix-alertmanager-receiver/templates/service.yaml new file mode 100644 index 0000000..7e4cfe4 --- /dev/null +++ b/chart/matrix-alertmanager-receiver/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "matrix-alertmanager-receiver.fullname" . }} + labels: + {{- include "matrix-alertmanager-receiver.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + app.kubernetes.io/name: {{ include "matrix-alertmanager-receiver.name" . }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/chart/matrix-alertmanager-receiver/templates/servicemonitor.yaml b/chart/matrix-alertmanager-receiver/templates/servicemonitor.yaml new file mode 100644 index 0000000..8f5135e --- /dev/null +++ b/chart/matrix-alertmanager-receiver/templates/servicemonitor.yaml @@ -0,0 +1,17 @@ +{{- if .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "matrix-alertmanager-receiver.fullname" . }} + labels: + {{- include "matrix-alertmanager-receiver.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ include "matrix-alertmanager-receiver.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + endpoints: + - port: http + path: /metrics + interval: 30s +{{- end }} diff --git a/chart/matrix-alertmanager-receiver/values.yaml b/chart/matrix-alertmanager-receiver/values.yaml new file mode 100644 index 0000000..1c3ca1f --- /dev/null +++ b/chart/matrix-alertmanager-receiver/values.yaml @@ -0,0 +1,28 @@ +replicaCount: 1 + +image: + repository: docker.io/metio/matrix-alertmanager-receiver + tag: "" + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 12345 + +matrix: + homeserverUrl: https://matrix.example.org + userId: "@alertmanager:example.org" + roomMapping: + cluster: "!room-id:example.org" + accessTokenSecret: + name: "" + key: token + +templating: + firing: 'FIRING {{ .Alert.Labels.alertname }}{{ if .Alert.Labels.namespace }} in {{ .Alert.Labels.namespace }}{{ end }}: {{ if .Alert.Annotations.description }}{{ .Alert.Annotations.description }}{{ else }}{{ .Alert.Annotations.summary }}{{ end }}' + resolved: 'RESOLVED {{ .Alert.Labels.alertname }}{{ if .Alert.Labels.namespace }} in {{ .Alert.Labels.namespace }}{{ end }}' + +resources: {} + +serviceMonitor: + enabled: false diff --git a/scripts/chart-version.sh b/scripts/chart-version.sh new file mode 100755 index 0000000..4f7a129 --- /dev/null +++ b/scripts/chart-version.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +description=$(git describe --tags --long --match 'v[0-9]*' --dirty) +case "$description" in + *-dirty) echo 'Refusing to package a dirty tree' >&2; exit 1 ;; +esac + +version=${description#v} +case "$version" in + *-0-g*) version=${version%%-0-g*} ;; +esac +printf '%s\n' "$version" diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..efddff0 --- /dev/null +++ b/test.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +repo_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +chart_dir=${repo_dir}/chart/matrix-alertmanager-receiver +rendered=$(mktemp) +trap 'rm -f "$rendered"' EXIT + +sh -n "${repo_dir}/scripts/chart-version.sh" +sh -n "${repo_dir}/chart/install.sh" +helm lint "${chart_dir}" +helm template matrix-alertmanager-receiver "${chart_dir}" \ + --namespace default \ + --set matrix.homeserverUrl=https://matrix.example.org \ + --set matrix.userId='@alertmanager:example.org' \ + --set matrix.roomMapping.cluster='!room-id:example.org' \ + --set serviceMonitor.enabled=true > "${rendered}" +python3 "${repo_dir}/tests/assert_chart.py" "${chart_dir}" "${rendered}" diff --git a/tests/assert_chart.py b/tests/assert_chart.py new file mode 100644 index 0000000..da08e6f --- /dev/null +++ b/tests/assert_chart.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import sys +from pathlib import Path + +import yaml + +chart_dir = Path(sys.argv[1]) +rendered_path = Path(sys.argv[2]) +chart = yaml.safe_load((chart_dir / "Chart.yaml").read_text()) +values = yaml.safe_load((chart_dir / "values.yaml").read_text()) +documents = [doc for doc in yaml.safe_load_all(rendered_path.read_text()) if doc] +by_kind = {(doc["kind"], doc["metadata"]["name"]): doc for doc in documents} + +name = "matrix-alertmanager-receiver" +deployment = by_kind[("Deployment", name)] +service = by_kind[("Service", name)] +service_monitor = by_kind[("ServiceMonitor", name)] +config = by_kind[("ConfigMap", name)]["data"]["config.yaml"] +container = deployment["spec"]["template"]["spec"]["containers"][0] +service_port = service["spec"]["ports"][0] +app_config = yaml.safe_load(config) + +assert container["image"] == f'{values["image"]["repository"]}:{chart["appVersion"]}' +assert deployment["spec"]["replicas"] == 1 +assert deployment["spec"]["selector"]["matchLabels"] == service["spec"]["selector"] +assert deployment["spec"]["selector"]["matchLabels"]["app.kubernetes.io/name"] == chart["name"] +assert container["ports"][0]["containerPort"] == service_port["port"] +assert service_port["targetPort"] == container["ports"][0]["name"] +assert container["readinessProbe"]["httpGet"]["path"] == "/metrics" +assert container["livenessProbe"]["httpGet"]["path"] == "/metrics" +assert container["env"][0]["valueFrom"]["secretKeyRef"]["key"] == values["matrix"]["accessTokenSecret"]["key"] +assert "access-token: ${MATRIX_ACCESS_TOKEN}" in config +assert app_config["matrix"]["access-token"] == "${MATRIX_ACCESS_TOKEN}" +assert service_monitor["spec"]["endpoints"][0]["port"] == service_port["name"] +assert service_monitor["spec"]["endpoints"][0]["path"] == "/metrics"