Migrate Diun to published Helm chart
Validate, publish and promote Diun Helm chart / Validate chart (push) Failing after 15s
Validate, publish and promote Diun Helm chart / Publish and promote chart (push) Skipped

This commit is contained in:
feedc0de committed 2026-10-19 14:50:13 +02:00
1 parent 0bcd74aa71
commit b9e3bd8fb0
21 files changed
+519 -341

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
name: Validate, publish and promote Diun Helm chart
on:
push:
pull_request:
env:
HELM_VERSION: v4.3.0
GITOPS_REPOSITORY: feedc0de/homelab-gitops
jobs:
validate:
name: Validate chart
runs-on: ubuntu-latest
steps:
- name: Check out source and tags
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: ${{ env.HELM_VERSION }}
- name: Install chart test dependency
run: python3 -m pip install --user PyYAML
- name: Lint, render and test chart behavior
run: ./test.sh
publish:
name: Publish and promote chart
if: gitea.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
steps:
- name: Check out source and tags
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: ${{ env.HELM_VERSION }}
- name: Select immutable chart version
run: printf 'CHART_VERSION=%s\n' "$(./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}"
install -d .chart-packages
helm package chart/diun --destination .chart-packages --version "${CHART_VERSION}"
chart_file=".chart-packages/diun-${CHART_VERSION}.tgz"
test -f "${chart_file}"
curl --fail --silent --show-error \
--user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \
--request POST \
--upload-file "${chart_file}" \
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" >> "${GITEA_OUTPUT}"
else
echo "This build is no longer current main; skipping promotion"
echo "promote=false" >> "${GITEA_OUTPUT}"
fi
- name: Check out GitOps repository
if: steps.current-main.outputs.promote == 'true'
uses: actions/checkout@v4
with:
repository: ${{ env.GITOPS_REPOSITORY }}
ref: main
token: ${{ secrets.GITOPS_TOKEN }}
path: homelab-gitops
- name: Commit chart promotion
if: steps.current-main.outputs.promote == 'true'
working-directory: homelab-gitops
run: |
set -euo pipefail
./scripts/update-chart-version.sh diun "${CHART_VERSION}"
git config user.name "feedc0de"
git config user.email "feedc0de@users.noreply.code.brunner.ninja"
git add bootstrap/diun.yaml
if git diff --cached --quiet; then
echo "GitOps already selects ${CHART_VERSION}"
exit 0
fi
git commit -m "Deploy diun chart ${CHART_VERSION}"
for attempt in 1 2 3; do
if git push origin HEAD:main; then
exit 0
fi
if [ "${attempt}" = 3 ]; then
echo "GitOps promotion failed after three attempts" >&2
exit 1
fi
git fetch origin main
git rebase origin/main
done
-55
View File
@@ -1,55 +0,0 @@
name: Validate and deploy Diun
on:
push:
pull_request:
env:
KUBECTL_VERSION: v1.36.3
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
NAMESPACE: diun
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 -m pip install --user PyYAML
- run: bash test.sh
deploy:
if: gitea.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubectl
run: |
curl --fail --silent --show-error --location \
--output "${RUNNER_TEMP}/kubectl" \
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod 0700 "${RUNNER_TEMP}/kubectl"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Configure Kubernetes access
env:
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
run: |
test -n "${KUBE_CONFIG_BASE64}"
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig"
chmod 0600 "${RUNNER_TEMP}/kubeconfig"
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
kubectl config set-cluster cluster --server="${KUBERNETES_API}" \
--tls-server-name="${KUBERNETES_TLS_SERVER_NAME}"
- name: Apply and verify
run: |
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
kubectl apply --dry-run=server --validate=false -f app.yaml
kubectl apply --validate=false -f app.yaml
kubectl -n "${NAMESPACE}" rollout restart deployment/diun
kubectl -n "${NAMESPACE}" rollout status deployment/diun --timeout=5m || {
kubectl -n "${NAMESPACE}" describe deployment/diun
kubectl -n "${NAMESPACE}" get pods -o wide
kubectl -n "${NAMESPACE}" logs deployment/diun --all-containers=true --tail=100 || true
exit 1
}
+42 -45
View File
@@ -1,11 +1,38 @@
# Diun
# Diun Helm chart
Diun checks container images used by Kubernetes Pods and sends
notifications when registry images change. It runs in its own `diun` namespace
using the upstream `crazymax/diun:4.33.0` image. The Kubernetes provider reads
Pods across namespaces by default.
This repository maintains a small adapter chart for the upstream Diun image.
It owns the Deployment, service account and read-only Kubernetes Pod RBAC,
configuration, notification wiring, and optional persistent data claim. The
cluster chooses the chart release and supplies only its Secret references,
registry/Matrix endpoints, and storage settings in
[`homelab-gitops`](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/diun).
Add this annotation to a Pod template to exclude an application:
The chart uses upstream `crazymax/diun` release tags. Its Chart `appVersion`
is the Diun release; the chart package version is derived from Git tags with
`git describe`. The Matrix integration downloads Shoutrrr 0.14.0 at startup
because later 0.14.1 sent duplicate Matrix authentication data rejected by the
configured Synapse. The init image uses a readable curl image tag. No image
digests or hard-coded download checksums are used.
## Install
Add the public chart repository and install the published chart (or use the
same-release convenience script in this checkout):
```sh
helm repo add brunner.ninja https://brunner.ninja/charts
helm repo update brunner.ninja
./install.sh
```
For a new cluster, create a values file that references existing Kubernetes
Secrets for `registry.credentialsSecretName` and
`matrix.tokenSecretName`; do not put their values in Helm values or Git.
See `values.example.yaml` for the chart interface. The default chart supports a
portable PVC; the homelab GitOps values preserve the existing `diun` claim.
Add this annotation to a Pod template to exclude it from Diun's Kubernetes
provider:
```yaml
spec:
@@ -15,44 +42,14 @@ spec:
diun.enable: "false"
```
Diun sends Matrix notices to `!ckpLXMvXlZJtuvQBIJ:brunner.ninja` as
`@diun:brunner.ninja`. It calls the Shoutrrr CLI through Diun's script notifier.
An init container downloads the Shoutrrr v0.14.0 release and makes the binary
available to Diun. Version 0.14.0 is pinned because
0.14.1 sends duplicate Matrix authentication data that this Synapse rejects.
The Matrix access token is stored in the `diun-matrix-token` Secret. A separate
`diun-registry-credentials` Secret grants read access to private Quay images.
Synapse password login remains disabled.
Container images here use readable version tags, with `latest` only when no
version tag is available. Image digests and hard-coded download checksums are
not used.
## Install
Run `./install.sh` to create the namespace and Diun resources, then wait for
the Deployment and data PVC. Both Secrets must already exist in the
namespace. The persistent volume stores Diun's database.
`./test.sh` performs local manifest and script checks without contacting the
cluster.
## Gitea deployment
Bootstrap the CI identity once using an administrator kubeconfig:
```sh
./create-ci-kubeconfig.sh
```
Create the `feedc0de/diun` Gitea repository and push this directory's `main`
branch, then save the script output as the repository Actions secret
`KUBE_CONFIG_BASE64`. CI may
only read and update the named Diun ConfigMap and Deployment in the `diun`
namespace; it cannot read Secrets or modify RBAC, the PVC, or other resources.
The Diun runtime ServiceAccount separately receives read-only access to Pods
across namespaces as required by the upstream Kubernetes provider.
Diun has no web UI. It notices newly seen image references and changed image
digests on scheduled scans; a first sighting is not proof that an image is
outdated. For Uptime Kuma later, create a separate Matrix account and token
and configure its built-in Matrix notification with the same room ID.
outdated.
## Development and publication
Run `./test.sh` to lint and render the chart and check resource relationships.
Gitea Actions publishes immutable chart versions to the public `brunner.ninja`
Helm repository and promotes the published version to this Argo Application.
The repository Actions secrets are `PACKAGE_USERNAME`, `PACKAGE_TOKEN`, and
`GITOPS_TOKEN`; chart CI has no Kubernetes credentials.
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: diun
description: A Helm chart for running Diun with Kubernetes provider support
type: application
version: 0.0.0
appVersion: "4.33.0"
+23
View File
@@ -0,0 +1,23 @@
{{- define "diun.name" -}}
{{- .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "diun.fullname" -}}
{{- if contains .Chart.Name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "diun.labels" -}}
app.kubernetes.io/name: {{ include "diun.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end -}}
{{- define "diun.image" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- printf "%s:%s" .Values.image.repository $tag -}}
{{- end -}}
+10
View File
@@ -0,0 +1,10 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "diun.fullname" . }}
labels:
{{- include "diun.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
@@ -0,0 +1,14 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "diun.fullname" . }}
labels:
{{- include "diun.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "diun.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "diun.fullname" . }}
namespace: {{ .Release.Namespace }}
+45
View File
@@ -0,0 +1,45 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "diun.fullname" . }}
labels:
{{- include "diun.labels" . | nindent 4 }}
data:
LOG_LEVEL: {{ .Values.config.logLevel | quote }}
LOG_JSON: {{ .Values.config.logJson | quote }}
TZ: {{ .Values.config.timeZone | quote }}
DIUN_WATCH_SCHEDULE: {{ .Values.config.watchSchedule | quote }}
DIUN_WATCH_JITTER: {{ .Values.config.watchJitter | quote }}
DIUN_PROVIDERS_KUBERNETES: "true"
DIUN_PROVIDERS_KUBERNETES_WATCHBYDEFAULT: {{ .Values.config.watchByDefault | quote }}
{{- if .Values.registry.enabled }}
DIUN_REGOPTS_0_NAME: {{ .Values.registry.name | quote }}
DIUN_REGOPTS_0_SELECTOR: image
DIUN_REGOPTS_0_USERNAMEFILE: /run/secrets/diun-registry-credentials/username
DIUN_REGOPTS_0_PASSWORDFILE: /run/secrets/diun-registry-credentials/password
{{- end }}
{{- if .Values.matrix.enabled }}
DIUN_NOTIF_SCRIPT_CMD: /bin/sh
DIUN_NOTIF_SCRIPT_ARGS: /etc/diun/notifier/send-matrix.sh
MATRIX_HOMESERVER: {{ .Values.matrix.homeserver | quote }}
MATRIX_ROOM_ID: {{ .Values.matrix.roomId | quote }}
{{- end }}
{{- if .Values.matrix.enabled }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "diun.fullname" . }}-matrix-script
labels:
{{- include "diun.labels" . | nindent 4 }}
data:
send-matrix.sh: |
set -eu
token=$(cat /run/secrets/diun-matrix-token/{{ .Values.matrix.tokenSecretKey }})
url="matrix://:${token}@${MATRIX_HOMESERVER}/?rooms=${MATRIX_ROOM_ID}"
message="Diun: ${DIUN_ENTRY_IMAGE} ${DIUN_ENTRY_STATUS} (${DIUN_ENTRY_PROVIDER})"
if [ -n "${DIUN_ENTRY_METADATA_POD_NAMESPACE:-}" ]; then
message="${message} in namespace ${DIUN_ENTRY_METADATA_POD_NAMESPACE}"
fi
SHOUTRRR_URL="${url}" /opt/shoutrrr/shoutrrr send --message "${message}"
{{- end }}
@@ -1,87 +1,52 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: diun
namespace: diun
data:
LOG_LEVEL: info
LOG_JSON: "false"
TZ: Europe/Vienna
DIUN_WATCH_SCHEDULE: "0 */6 * * *"
DIUN_WATCH_JITTER: 30s
DIUN_PROVIDERS_KUBERNETES: "true"
DIUN_PROVIDERS_KUBERNETES_WATCHBYDEFAULT: "true"
DIUN_REGOPTS_0_NAME: registry.brunner.ninja
DIUN_REGOPTS_0_SELECTOR: image
DIUN_REGOPTS_0_USERNAMEFILE: /run/secrets/diun-registry-credentials/username
DIUN_REGOPTS_0_PASSWORDFILE: /run/secrets/diun-registry-credentials/password
DIUN_NOTIF_SCRIPT_CMD: /bin/sh
DIUN_NOTIF_SCRIPT_ARGS: /etc/diun/notifier/send-matrix.sh
---
apiVersion: v1
kind: ConfigMap
metadata:
name: diun-matrix-script
namespace: diun
data:
send-matrix.sh: |
set -eu
token=$(cat /run/secrets/diun-matrix-token/token)
url="matrix://:${token}@matrix.brunner.ninja:443/?rooms=!ckpLXMvXlZJtuvQBIJ:brunner.ninja"
message="Diun: ${DIUN_ENTRY_IMAGE} ${DIUN_ENTRY_STATUS} (${DIUN_ENTRY_PROVIDER})"
if [ -n "${DIUN_ENTRY_METADATA_POD_NAMESPACE:-}" ]; then
message="${message} in namespace ${DIUN_ENTRY_METADATA_POD_NAMESPACE}"
fi
SHOUTRRR_URL="${url}" /opt/shoutrrr/shoutrrr send --message "${message}"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: diun
namespace: diun
name: {{ include "diun.fullname" . }}
labels:
app.kubernetes.io/name: diun
{{- include "diun.labels" . | nindent 4 }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: diun
app.kubernetes.io/name: {{ include "diun.name" . }}
template:
metadata:
labels:
app.kubernetes.io/name: diun
app.kubernetes.io/name: {{ include "diun.name" . }}
annotations:
diun.enable: "false"
spec:
serviceAccountName: diun
serviceAccountName: {{ include "diun.fullname" . }}
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
{{- if .Values.matrix.enabled }}
initContainers:
- name: install-shoutrrr
image: docker.io/curlimages/curl:8.16.0
image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
command: ["/bin/sh", "-ec"]
args:
- |
curl -fsSL -o /tmp/shoutrrr.tar.gz \
https://github.com/nicholas-fedor/shoutrrr/releases/download/v0.14.0/shoutrrr_linux_amd64_0.14.0.tar.gz
https://github.com/nicholas-fedor/shoutrrr/releases/download/v{{ .Values.shoutrrr.version }}/shoutrrr_linux_amd64_{{ .Values.shoutrrr.version }}.tar.gz
tar -xzf /tmp/shoutrrr.tar.gz -C /tools shoutrrr
chmod 0555 /tools/shoutrrr
volumeMounts:
- name: shoutrrr
mountPath: /tools
{{- end }}
containers:
- name: diun
image: crazymax/diun:4.33.0
imagePullPolicy: IfNotPresent
image: {{ include "diun.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["serve"]
envFrom:
- configMapRef:
name: diun
name: {{ include "diun.fullname" . }}
startupProbe:
exec:
command: ["diun", "healthcheck"]
@@ -98,18 +63,22 @@ spec:
volumeMounts:
- name: data
mountPath: /data
- name: matrix-token
mountPath: /run/secrets/diun-matrix-token
readOnly: true
{{- if .Values.registry.enabled }}
- name: registry-credentials
mountPath: /run/secrets/diun-registry-credentials
readOnly: true
{{- end }}
{{- if .Values.matrix.enabled }}
- name: matrix-token
mountPath: /run/secrets/diun-matrix-token
readOnly: true
- name: matrix-script
mountPath: /etc/diun/notifier
readOnly: true
- name: shoutrrr
mountPath: /opt/shoutrrr
readOnly: true
{{- end }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
@@ -117,16 +86,24 @@ spec:
drop: [ALL]
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: diun
- name: matrix-token
secret:
secretName: diun-matrix-token
claimName: {{ .Values.persistence.existingClaim | default (include "diun.fullname" .) }}
{{- else }}
emptyDir: {}
{{- end }}
{{- if .Values.registry.enabled }}
- name: registry-credentials
secret:
secretName: diun-registry-credentials
secretName: {{ required "registry.credentialsSecretName is required when registry auth is enabled" .Values.registry.credentialsSecretName }}
{{- end }}
{{- if .Values.matrix.enabled }}
- name: matrix-token
secret:
secretName: {{ required "matrix.tokenSecretName is required when Matrix notifications are enabled" .Values.matrix.tokenSecretName }}
- name: matrix-script
configMap:
name: diun-matrix-script
name: {{ include "diun.fullname" . }}-matrix-script
- name: shoutrrr
emptyDir: {}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "diun.fullname" . }}
labels:
{{- include "diun.labels" . | nindent 4 }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
{{- if .Values.persistence.storageClassName }}
storageClassName: {{ .Values.persistence.storageClassName | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "diun.fullname" . }}
labels:
{{- include "diun.labels" . | nindent 4 }}
automountServiceAccountToken: true
+76
View File
@@ -0,0 +1,76 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": ["image", "initImage", "shoutrrr", "config", "registry", "matrix", "persistence", "resources"],
"properties": {
"global": { "type": "object", "additionalProperties": true },
"image": {
"type": "object", "additionalProperties": false,
"required": ["repository", "pullPolicy", "tag"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] },
"tag": { "type": "string" }
}
},
"initImage": {
"type": "object", "additionalProperties": false,
"required": ["repository", "tag"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string", "minLength": 1 }
}
},
"shoutrrr": {
"type": "object", "additionalProperties": false,
"required": ["version"],
"properties": { "version": { "type": "string", "minLength": 1 } }
},
"config": {
"type": "object", "additionalProperties": false,
"required": ["logLevel", "logJson", "timeZone", "watchSchedule", "watchJitter", "watchByDefault"],
"properties": {
"logLevel": { "type": "string", "minLength": 1 },
"logJson": { "type": "string", "enum": ["true", "false"] },
"timeZone": { "type": "string", "minLength": 1 },
"watchSchedule": { "type": "string", "minLength": 1 },
"watchJitter": { "type": "string", "minLength": 1 },
"watchByDefault": { "type": "boolean" }
}
},
"registry": {
"type": "object", "additionalProperties": false,
"required": ["enabled", "name", "credentialsSecretName"],
"properties": {
"enabled": { "type": "boolean" },
"name": { "type": "string", "minLength": 1 },
"credentialsSecretName": { "type": "string" }
}
},
"matrix": {
"type": "object", "additionalProperties": false,
"required": ["enabled", "homeserver", "roomId", "tokenSecretName", "tokenSecretKey"],
"properties": {
"enabled": { "type": "boolean" },
"homeserver": { "type": "string", "minLength": 1 },
"roomId": { "type": "string" },
"tokenSecretName": { "type": "string" },
"tokenSecretKey": { "type": "string", "minLength": 1 }
}
},
"persistence": {
"type": "object", "additionalProperties": false,
"required": ["enabled", "existingClaim", "storageClassName", "accessModes", "size", "annotations"],
"properties": {
"enabled": { "type": "boolean" },
"existingClaim": { "type": "string" },
"storageClassName": { "type": "string" },
"accessModes": { "type": "array", "minItems": 1, "items": { "type": "string" } },
"size": { "type": "string", "minLength": 1 },
"annotations": { "type": "object", "additionalProperties": { "type": "string" } }
}
},
"resources": { "type": "object" }
}
}
+42
View File
@@ -0,0 +1,42 @@
image:
repository: crazymax/diun
pullPolicy: IfNotPresent
tag: ""
initImage:
repository: docker.io/curlimages/curl
tag: 8.23.0
shoutrrr:
version: 0.14.0
config:
logLevel: info
logJson: "false"
timeZone: Europe/Vienna
watchSchedule: "0 */6 * * *"
watchJitter: 30s
watchByDefault: true
registry:
enabled: false
name: registry.example.net
credentialsSecretName: ""
matrix:
enabled: false
homeserver: matrix.example.net:443
roomId: ""
tokenSecretName: ""
tokenSecretKey: token
persistence:
enabled: true
existingClaim: ""
storageClassName: ""
accessModes:
- ReadWriteOnce
size: 1Gi
annotations: {}
resources: {}
-44
View File
@@ -1,44 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: diun-deployer
namespace: diun
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: diun-deployer
namespace: diun
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["diun", "diun-matrix-script"]
verbs: ["get", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["diun"]
verbs: ["get", "patch", "update", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: diun-deployer
namespace: diun
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: diun-deployer
subjects:
- kind: ServiceAccount
name: diun-deployer
namespace: diun
---
apiVersion: v1
kind: Secret
metadata:
name: diun-deployer-token
namespace: diun
annotations:
kubernetes.io/service-account.name: diun-deployer
type: kubernetes.io/service-account-token
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=diun
service_account=diun-deployer
secret=diun-deployer-token
expected_context=kubernetes-admin@kubernetes
current_context=$(kubectl config current-context)
if [[ "${current_context}" != "${expected_context}" ]]; then
echo "Refusing cluster mutation: expected context ${expected_context}, got ${current_context}" >&2
exit 1
fi
kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
for attempt in {1..30}; do
if kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then
break
fi
if [[ "${attempt}" == 30 ]]; then
echo "Timed out waiting for the service-account token" >&2
exit 1
fi
sleep 1
done
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT
server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
token=$(kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' | base64 --decode)
export KUBECONFIG="${workdir}/config"
kubectl config set-cluster cluster --server="${server}" \
--certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null
kubectl config set-context diun --cluster=cluster --user="${service_account}" \
--namespace="${namespace}" >/dev/null
kubectl config use-context diun >/dev/null
echo "Store the following single line as the Gitea Actions secret KUBE_CONFIG_BASE64." >&2
echo "Treat it as a password; do not commit it." >&2
base64 --wrap=0 "${KUBECONFIG}"
printf '\n'
-40
View File
@@ -1,40 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: diun
namespace: diun
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: diun
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: diun
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: diun
subjects:
- kind: ServiceAccount
name: diun
namespace: diun
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: diun
namespace: diun
spec:
accessModes: [ReadWriteOnce]
storageClassName: rook-ceph-block
resources:
requests:
storage: 1Gi
+13 -21
View File
@@ -1,23 +1,15 @@
#!/usr/bin/env bash
#!/bin/sh
set -eu
set -euo pipefail
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
chart_version=$(cd "${project_dir}" && ./scripts/chart-version.sh)
values_file=${1:-${project_dir}/values.example.yaml}
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
expected_context=kubernetes-admin@kubernetes
current_context=$(kubectl config current-context)
if [[ "${current_context}" != "${expected_context}" ]]; then
echo "Refusing cluster mutation: expected context ${expected_context}, got ${current_context}" >&2
exit 1
fi
kubectl apply --dry-run=server --filename "${project_dir}/namespace.yaml"
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl apply --dry-run=server --filename "${project_dir}/diun.yaml"
kubectl apply --filename "${project_dir}/diun.yaml"
kubectl apply --dry-run=server --filename "${project_dir}/app.yaml"
kubectl apply --filename "${project_dir}/app.yaml"
kubectl --namespace diun rollout status deployment/diun --timeout=5m
kubectl --namespace diun wait --for=jsonpath='{.status.phase}'=Bound \
pvc/diun --timeout=5m
printf 'Installed Diun in namespace diun\n'
helm repo add brunner.ninja https://brunner.ninja/charts --force-update
helm repo update brunner.ninja
helm upgrade --install diun brunner.ninja/diun \
--namespace diun \
--create-namespace \
--version "${chart_version}" \
--values "${values_file}" \
--wait --timeout 10m
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: diun
labels:
app.kubernetes.io/name: diun
+12
View File
@@ -0,0 +1,12 @@
#!/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"
+52 -26
View File
@@ -3,34 +3,60 @@
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
command -v kubectl >/dev/null
chart_dir="${project_dir}/chart/diun"
rendered_file="${TMPDIR:-/tmp}/diun-chart-rendered.yaml"
example_values="${project_dir}/values.example.yaml"
command -v helm >/dev/null
command -v python3 >/dev/null
bash -n "${project_dir}/install.sh"
bash -n "${project_dir}/create-ci-kubeconfig.sh"
bash -n "${project_dir}/test.sh"
python3 - "${project_dir}" <<'PY'
import pathlib
helm lint --strict "${chart_dir}"
helm template diun "${chart_dir}" \
--namespace diun \
--values "${example_values}" \
> "${rendered_file}"
python3 - "${rendered_file}" "${project_dir}" "${example_values}" <<'PY'
import sys
import yaml
from pathlib import Path
root = pathlib.Path(sys.argv[1])
expected = {
"namespace.yaml": {"Namespace"},
"diun.yaml": {"ServiceAccount", "ClusterRole", "ClusterRoleBinding", "PersistentVolumeClaim"},
"app.yaml": {"ConfigMap", "Deployment"},
"ci-deployer.yaml": {"ServiceAccount", "Role", "RoleBinding", "Secret"},
}
for filename, kinds in expected.items():
with (root / filename).open(encoding="utf-8") as stream:
documents = list(yaml.safe_load_all(stream))
assert documents and all(isinstance(document, dict) for document in documents), filename
assert kinds <= {document.get("kind") for document in documents}, filename
project_dir = Path(sys.argv[2])
with open(sys.argv[1], encoding="utf-8") as stream:
objects = [item for item in yaml.safe_load_all(stream) if item]
with (project_dir / "chart/diun/values.yaml").open(encoding="utf-8") as stream:
chart_values = yaml.safe_load(stream)
with (project_dir / "chart/diun/Chart.yaml").open(encoding="utf-8") as stream:
chart_metadata = yaml.safe_load(stream)
with open(sys.argv[3], encoding="utf-8") as stream:
example_values = yaml.safe_load(stream)
def one(kind, name):
matches = [obj for obj in objects if obj.get("kind") == kind and obj.get("metadata", {}).get("name") == name]
assert len(matches) == 1, f"expected one {kind}/{name}, found {len(matches)}"
return matches[0]
deployment = one("Deployment", "diun")
pod = deployment["spec"]["template"]
pod_spec = pod["spec"]
container = next(item for item in pod_spec["containers"] if item["name"] == "diun")
selector = deployment["spec"]["selector"]["matchLabels"]
assert all(pod["metadata"]["labels"].get(key) == value for key, value in selector.items())
assert deployment["spec"]["replicas"] == 1
expected_tag = chart_values["image"].get("tag") or str(chart_metadata["appVersion"])
assert container["image"] == f'{chart_values["image"]["repository"]}:{expected_tag}'
assert all(name in container for name in ("startupProbe", "readinessProbe", "livenessProbe"))
assert all(container[name]["exec"]["command"] == ["diun", "healthcheck"] for name in ("startupProbe", "readinessProbe", "livenessProbe"))
assert container["envFrom"][0]["configMapRef"]["name"] == "diun"
assert pod_spec["serviceAccountName"] == "diun"
assert any(item["name"] == "install-shoutrrr" for item in pod_spec["initContainers"])
assert one("ConfigMap", "diun-matrix-script")["data"].get("send-matrix.sh")
assert one("ClusterRole", "diun")["rules"] == [{"apiGroups": [""], "resources": ["pods"], "verbs": ["get", "list", "watch"]}]
assert one("ClusterRoleBinding", "diun")["roleRef"]["name"] == "diun"
pvc = one("PersistentVolumeClaim", "diun")
assert pvc["spec"]["storageClassName"] == example_values["persistence"]["storageClassName"]
assert pod_spec["volumes"][0]["persistentVolumeClaim"]["claimName"] == pvc["metadata"]["name"]
assert not any(item.get("kind") == "Secret" for item in objects), "chart must reference secrets, never render their data"
volume = next(item for item in pod_spec["volumes"] if item["name"] == "matrix-token")
assert volume["secret"]["secretName"] == example_values["matrix"]["tokenSecretName"]
mount = next(item for item in container["volumeMounts"] if item["name"] == "matrix-token")
assert mount["readOnly"] is True
PY
grep -Fq 'resourceNames: ["diun", "diun-matrix-script"]' "${project_dir}/ci-deployer.yaml"
if grep -Eq 'resources: \["secrets"\]|resources:[[:space:]]*\[[^]]*secrets' \
"${project_dir}/ci-deployer.yaml"; then
echo 'CI deployer must not access Kubernetes Secrets' >&2
exit 1
fi
echo 'Diun manifests and scripts validated'
+17
View File
@@ -0,0 +1,17 @@
config:
timeZone: UTC
matrix:
enabled: true
homeserver: matrix.example.net:443
roomId: "!room-id:example.net"
tokenSecretName: diun-matrix-token
registry:
enabled: false
persistence:
enabled: true
storageClassName: example-storage-class
size: 1Gi