Migrate Diun to published Helm chart
This commit is contained in:
1 parent
0bcd74aa71
commit
b9e3bd8fb0
21 files changed
+519
-341
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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 -}}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "diun.fullname" . }}
|
||||
labels:
|
||||
{{- include "diun.labels" . | nindent 4 }}
|
||||
automountServiceAccountToken: true
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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: {}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: diun
|
||||
labels:
|
||||
app.kubernetes.io/name: diun
|
||||
Executable
+12
@@ -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"
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user