From 76511f385058ff76c43815e70f05c22118ca0b21 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Tue, 20 Oct 2026 09:31:49 +0200 Subject: [PATCH] Publish portable nextcloud-clamav chart --- .gitea/workflows/chart.yml | 71 ++++++++++ .gitignore | 1 + README.md | 26 ++++ chart/nextcloud-clamav/Chart.yaml | 6 + .../nextcloud-clamav/templates/resources.yaml | 127 ++++++++++++++++++ chart/nextcloud-clamav/values.yaml | 10 ++ chart/test.sh | 28 ++++ install.sh | 14 ++ scripts/chart-version.sh | 35 +++++ 9 files changed, 318 insertions(+) create mode 100644 .gitea/workflows/chart.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 chart/nextcloud-clamav/Chart.yaml create mode 100644 chart/nextcloud-clamav/templates/resources.yaml create mode 100644 chart/nextcloud-clamav/values.yaml create mode 100755 chart/test.sh create mode 100755 install.sh create mode 100755 scripts/chart-version.sh diff --git a/.gitea/workflows/chart.yml b/.gitea/workflows/chart.yml new file mode 100644 index 0000000..62d522f --- /dev/null +++ b/.gitea/workflows/chart.yml @@ -0,0 +1,71 @@ +name: Validate, publish and promote nextcloud-clamav chart +on: + push: + pull_request: +jobs: + chart: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: azure/setup-helm@v4 + with: + version: v4.3.0 + - name: Validate chart + run: | + python3 -m venv "${RUNNER_TEMP}/chart-test" + "${RUNNER_TEMP}/chart-test/bin/pip" install PyYAML + PATH="${RUNNER_TEMP}/chart-test/bin:${PATH}" bash chart/test.sh + - name: Select immutable chart version + if: gitea.ref == 'refs/heads/main' + run: printf 'CHART_VERSION=%s\n' "$(./scripts/chart-version.sh)" >> "${GITHUB_ENV}" + - name: Publish chart + if: gitea.ref == 'refs/heads/main' + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + run: | + set -euo pipefail + test -n "${PACKAGE_USERNAME}" && test -n "${PACKAGE_TOKEN}" + mkdir -p .chart-packages + helm package chart/nextcloud-clamav --destination .chart-packages --version "${CHART_VERSION}" + curl --fail --silent --show-error --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST --upload-file ".chart-packages/nextcloud-clamav-${CHART_VERSION}.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + - name: Check current main + id: current-main + if: gitea.ref == 'refs/heads/main' + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + git fetch --no-tags origin main + if test "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}"; then + echo 'promote=true' >> "${GITEA_OUTPUT}" + else + echo 'promote=false' >> "${GITEA_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 + if: steps.current-main.outputs.promote == 'true' + working-directory: homelab-gitops + run: | + set -euo pipefail + ./scripts/update-chart-version.sh nextcloud-clamav "${CHART_VERSION}" + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + git add bootstrap/nextcloud-clamav.yaml + if git diff --cached --quiet; then exit 0; fi + git commit -m "Deploy nextcloud-clamav chart ${CHART_VERSION}" + for attempt in 1 2 3; do + git push origin HEAD:main && exit 0 + test "${attempt}" = 3 && exit 1 + git fetch origin main + git rebase origin/main + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad14e04 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.chart-packages/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4274f55 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Nextcloud ClamAV daemon + +This repository maintains a portable Helm chart for the Nextcloud ClamAV daemon integration. +It is used with the upstream Nextcloud Helm release. The application image is +`ghcr.io/nextcloud-releases/aio-clamav:20260825_084538`; the chart selects that tested tag through +`Chart.yaml.appVersion`, while GitOps selects only the chart version. + +The chart owns a PVC for virus signatures. Set its storage class and size for your cluster, and preserve the claim during adoption. + +```sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +VALUES_FILE=values.yaml ./install.sh +``` + +The chart and its templates own the Kubernetes workload contract; set hostnames, +Ingress annotations, resources, and Secret names through values. The live +[homelab configuration](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/nextcloud) +is an example, not a portable default. Run `bash chart/test.sh` before +publishing. + +The Git tag follows the actual upstream release `20260825_084538`. Its first chart +package is `20260825.0.0-r084538`; later chart-only commits append the commit count +and Git hash. CI publishes the package to the indexed +`https://brunner.ninja/charts` repository and promotes only this chart's +Argo CD targetRevision. diff --git a/chart/nextcloud-clamav/Chart.yaml b/chart/nextcloud-clamav/Chart.yaml new file mode 100644 index 0000000..a0316ed --- /dev/null +++ b/chart/nextcloud-clamav/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: nextcloud-clamav +description: ClamAV daemon for Nextcloud +type: application +version: 0.0.0 +appVersion: "20260825_084538" diff --git a/chart/nextcloud-clamav/templates/resources.yaml b/chart/nextcloud-clamav/templates/resources.yaml new file mode 100644 index 0000000..f58dc0a --- /dev/null +++ b/chart/nextcloud-clamav/templates/resources.yaml @@ -0,0 +1,127 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Release.Name }} + labels: + app.kubernetes.io/name: nextcloud-clamav + app.kubernetes.io/instance: {{ .Values.nextcloudReleaseName }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + app.kubernetes.io/name: nextcloud-clamav + app.kubernetes.io/instance: {{ .Values.nextcloudReleaseName }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: nextcloud-clamav + app.kubernetes.io/instance: {{ .Values.nextcloudReleaseName }} + template: + metadata: + labels: + app.kubernetes.io/name: nextcloud-clamav + app.kubernetes.io/instance: {{ .Values.nextcloudReleaseName }} + spec: + automountServiceAccountToken: false + securityContext: + fsGroup: 100 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 100 + runAsNonRoot: true + runAsUser: 100 + seccompProfile: + type: RuntimeDefault + initContainers: + - name: clamav-signatures + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -eu + - -c + - test -s /var/lib/clamav/main.cvd -a -s /var/lib/clamav/daily.cvd || freshclam --foreground --stdout + resources: + {{- toYaml .Values.initResources | nindent 10 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - name: signatures + mountPath: /var/lib/clamav + containers: + - name: clamav + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: IfNotPresent + env: + - name: AIO_LOG_LEVEL + value: info + - name: MAX_SIZE + value: {{ .Values.maxSize | quote }} + - name: TZ + value: {{ .Values.timezone | quote }} + ports: + - name: clamd + containerPort: 3310 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + resources: + {{- toYaml .Values.resources | nindent 10 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - name: signatures + mountPath: /var/lib/clamav + volumes: + - name: signatures + persistentVolumeClaim: + claimName: {{ .Release.Name }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + app.kubernetes.io/name: nextcloud-clamav + app.kubernetes.io/instance: {{ .Values.nextcloudReleaseName }} +spec: + selector: + app.kubernetes.io/name: nextcloud-clamav + app.kubernetes.io/instance: {{ .Values.nextcloudReleaseName }} + ports: + - name: clamd + port: 3310 + targetPort: clamd diff --git a/chart/nextcloud-clamav/values.yaml b/chart/nextcloud-clamav/values.yaml new file mode 100644 index 0000000..b2ff546 --- /dev/null +++ b/chart/nextcloud-clamav/values.yaml @@ -0,0 +1,10 @@ +image: + repository: ghcr.io/nextcloud-releases/aio-clamav +nextcloudReleaseName: nextcloud +persistence: + storageClass: "" + size: 2Gi +maxSize: 512M +timezone: UTC +initResources: {} +resources: {} diff --git a/chart/test.sh b/chart/test.sh new file mode 100755 index 0000000..740a736 --- /dev/null +++ b/chart/test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail +chart_name=$(basename "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/nextcloud-clamav" && pwd)") +chart_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/nextcloud-clamav" && pwd) +helm lint "$chart_dir" --strict +python3 - "$chart_dir" "$chart_name" <<'PY' +import subprocess,sys,yaml +chart,name=sys.argv[1:] +with open(chart+'/Chart.yaml') as f: metadata=yaml.safe_load(f) +def render(*args): + output=subprocess.check_output(['helm','template',name,chart,'--namespace','nextcloud',*args],text=True) + return {(x['kind'],x['metadata']['name']):x for x in yaml.safe_load_all(output) if x} +default=render() +enabled=render('--set','ingress.enabled=true','--set','monitoring.enabled=true') +assert ('Service',name) in default +assert ('Ingress',name) not in default +if name!='nextcloud-clamav': + assert ('Ingress',name) in enabled +if name=='nextcloud-notify-push': + assert ('ServiceMonitor',name) in enabled +else: + assert ('Deployment',name) in default + images=[c['image'] for c in default['Deployment',name]['spec']['template']['spec'].get('initContainers',[])+default['Deployment',name]['spec']['template']['spec']['containers']] + assert all(image.endswith(':'+metadata['appVersion']) for image in images) +if name=='nextcloud-clamav': + assert ('PersistentVolumeClaim',name) in default +assert not any(kind=='Secret' for kind,_ in enabled) +PY diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c9b7d69 --- /dev/null +++ b/install.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu +chart_name=$(basename "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)") +namespace=${NAMESPACE:-nextcloud} +values_file=${VALUES_FILE:-values.yaml} +if [ ! -f "$values_file" ]; then + echo "Values file not found: $values_file" >&2 + exit 1 +fi +helm repo add brunner.ninja https://brunner.ninja/charts --force-update +helm repo update brunner.ninja +helm upgrade --install "$chart_name" "brunner.ninja/$chart_name" \ + --namespace "$namespace" --create-namespace --values "$values_file" \ + --wait --timeout 15m diff --git a/scripts/chart-version.sh b/scripts/chart-version.sh new file mode 100755 index 0000000..7868ea7 --- /dev/null +++ b/scripts/chart-version.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +description=$(git -C "$repo_dir" describe --tags --long --dirty) +case "$description" in + *-dirty) echo 'Refusing to package a dirty tree.' >&2; exit 1 ;; +esac +if [[ "$description" =~ ^(.+)-([0-9]+)-g([0-9a-f]+)$ ]]; then + release_tag=${BASH_REMATCH[1]} + count=${BASH_REMATCH[2]} + hash=${BASH_REMATCH[3]} +else + echo "Unexpected git describe result: $description" >&2 + exit 1 +fi + +case "$release_tag" in + v[0-9]*) + version=${release_tag#v} + ;; + [0-9]*_[0-9]*) + date=${release_tag%%_*} + time=${release_tag#*_} + version="${date}.0.0-r${time}" + ;; + *) + echo "Unsupported application release tag: $release_tag" >&2 + exit 1 + ;; +esac +if [ "$count" -ne 0 ]; then + version="$version-$count-g$hash" +fi +printf '%s\n' "$version"