From 68d31e9692f5faae7e5b53776b2de4f55f9b08c4 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Mon, 19 Oct 2026 08:43:31 +0200 Subject: [PATCH] Add Argo CD Helm chart release --- .gitea/workflows/container.yml | 137 ----------------------------- .gitea/workflows/publish.yml | 149 ++++++++++++++++++++++++++++++++ README.md | 50 +++++------ chart/Chart.yaml | 6 ++ chart/README.md | 25 ++++++ chart/install.sh | 16 ++++ chart/templates/_helpers.tpl | 7 ++ chart/templates/deployment.yaml | 86 ++++++++++++++++++ chart/templates/ingress.yaml | 58 +++++++++++++ chart/templates/pvc.yaml | 18 ++++ chart/templates/service.yaml | 16 ++++ chart/test.sh | 23 +++++ chart/values.schema.json | 36 ++++++++ chart/values.yaml | 46 ++++++++++ install.sh | 3 +- 15 files changed, 511 insertions(+), 165 deletions(-) delete mode 100644 .gitea/workflows/container.yml create mode 100644 .gitea/workflows/publish.yml create mode 100644 chart/Chart.yaml create mode 100644 chart/README.md create mode 100755 chart/install.sh create mode 100644 chart/templates/_helpers.tpl create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/ingress.yaml create mode 100644 chart/templates/pvc.yaml create mode 100644 chart/templates/service.yaml create mode 100755 chart/test.sh create mode 100644 chart/values.schema.json create mode 100644 chart/values.yaml diff --git a/.gitea/workflows/container.yml b/.gitea/workflows/container.yml deleted file mode 100644 index c438641..0000000 --- a/.gitea/workflows/container.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Publish and deploy birthday invitation - -on: - push: - -env: - IMAGE: registry.brunner.ninja/feedc0de/30-birthday - KUBERNETES_API: https://host.containers.internal:6443 - KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 - -jobs: - test: - name: Check JavaScript - runs-on: linux_amd64 - steps: - - name: Check out source - uses: actions/checkout@v4 - - name: Check server and browser scripts - run: | - authfile=$(mktemp) - trap 'rm -f "$authfile"' EXIT - printf '{"auths":{}}' > "$authfile" - export REGISTRY_AUTH_FILE="$authfile" - podman run --rm --interactive docker.io/library/node:24-alpine node --check - < server.js - podman run --rm --interactive docker.io/library/node:24-alpine node --check - < database.js - podman run --rm --interactive docker.io/library/node:24-alpine node --check - < public/app.js - podman run --rm --interactive docker.io/library/node:24-alpine node --check - < public/admin.js - podman run --rm \ - --volume "$PWD:/app:ro" \ - --workdir /app \ - docker.io/library/node:24-alpine npm test - - publish: - name: Build and push - if: gitea.ref == 'refs/heads/main' - needs: test - runs-on: linux_amd64 - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Configure isolated registry access - env: - QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} - QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} - run: | - set -euo pipefail - authfile="${RUNNER_TEMP}/quay-auth.json" - printf '{"auths":{}}' > "$authfile" - chmod 0600 "$authfile" - printf '%s' "$QUAY_TOKEN" | podman login registry.brunner.ninja \ - --authfile "$authfile" --username "$QUAY_USERNAME" --password-stdin - echo "REGISTRY_AUTH_FILE=$authfile" >> "$GITHUB_ENV" - - - name: Build image - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - podman build \ - --label "org.opencontainers.image.revision=${COMMIT_SHA}" \ - --label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/30-birthday" \ - --tag "${IMAGE}:${COMMIT_SHA}" \ - . - - - name: Push commit tag - env: - COMMIT_SHA: ${{ gitea.sha }} - run: podman push "${IMAGE}:${COMMIT_SHA}" - - - name: Push latest tag - if: gitea.ref == 'refs/heads/main' - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - podman tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:latest" - podman push "${IMAGE}:latest" - - - name: Remove registry credentials - if: always() - run: rm -f "${RUNNER_TEMP}/quay-auth.json" - - deploy: - name: Deploy to Kubernetes - if: gitea.ref == 'refs/heads/main' - needs: publish - runs-on: ubuntu-latest - steps: - - name: Install kubectl - env: - KUBECTL_VERSION: v1.36.4 - 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" - - - 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" - "${RUNNER_TEMP}/kubectl" config set-cluster cluster \ - --server="${KUBERNETES_API}" \ - --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" - - - name: Deploy commit image - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - "${RUNNER_TEMP}/kubectl" --namespace default set image \ - deployment/30-birthday \ - "30-birthday=${IMAGE}:${COMMIT_SHA}" - - for attempt in {1..60}; do - IFS='|' read -r generation observed desired updated available unavailable <<< "$( - "${RUNNER_TEMP}/kubectl" --namespace default get deployment/30-birthday \ - --output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}' - )" - - echo "Rollout ${attempt}/60: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${available}/${desired} available" - if [[ "${observed}" == "${generation}" \ - && "${updated}" == "${desired}" \ - && "${available}" == "${desired}" \ - && ( -z "${unavailable}" || "${unavailable}" == "0" ) ]]; then - echo "Deployment successfully rolled out" - exit 0 - fi - - sleep 5 - done - - "${RUNNER_TEMP}/kubectl" --namespace default describe deployment/30-birthday - exit 1 diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..ae9bb16 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,149 @@ +name: Test, publish birthday app and chart + +on: + push: + pull_request: + +env: + IMAGE: registry.brunner.ninja/feedc0de/30-birthday + +jobs: + validate: + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Helm + run: | + curl --fail --silent --show-error --location \ + --output "${RUNNER_TEMP}/helm.tar.gz" \ + https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz + tar -xzf "${RUNNER_TEMP}/helm.tar.gz" -C "${RUNNER_TEMP}" linux-amd64/helm + mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm" + chmod 0700 "${RUNNER_TEMP}/helm" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + - name: Validate application and chart + run: | + set -euo pipefail + podman run --rm --interactive docker.io/library/node:24-alpine node --check - < server.js + podman run --rm --interactive docker.io/library/node:24-alpine node --check - < database.js + podman run --rm --interactive docker.io/library/node:24-alpine node --check - < public/app.js + podman run --rm --interactive docker.io/library/node:24-alpine node --check - < public/admin.js + podman run --rm --volume "$PWD:/app:ro" --workdir /app \ + docker.io/library/node:24-alpine npm test + chart/test.sh + + publish-image: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: validate + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Configure isolated registry access + env: + QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} + QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} + run: | + set -euo pipefail + authfile="${RUNNER_TEMP}/quay-auth.json" + printf '{"auths":{}}' > "$authfile" + chmod 0600 "$authfile" + printf '%s' "$QUAY_TOKEN" | podman login registry.brunner.ninja \ + --authfile "$authfile" --username "$QUAY_USERNAME" --password-stdin + echo "REGISTRY_AUTH_FILE=$authfile" >> "$GITHUB_ENV" + - name: Build and push immutable image + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + podman build \ + --label "org.opencontainers.image.revision=${COMMIT_SHA}" \ + --label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/30-birthday" \ + --tag "${IMAGE}:${COMMIT_SHA}" . + podman push "${IMAGE}:${COMMIT_SHA}" + podman tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:latest" + podman push "${IMAGE}:latest" + - if: always() + run: rm -f "${RUNNER_TEMP}/quay-auth.json" + + publish-chart: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: + - validate + - publish-image + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Helm + run: | + curl --fail --silent --show-error --location \ + --output "${RUNNER_TEMP}/helm.tar.gz" \ + https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz + tar -xzf "${RUNNER_TEMP}/helm.tar.gz" -C "${RUNNER_TEMP}" linux-amd64/helm + mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm" + chmod 0700 "${RUNNER_TEMP}/helm" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + - id: chart + name: Derive chart version + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + git fetch --tags origin main + test "$(git rev-parse origin/main)" = "$COMMIT_SHA" + description=$(git describe --tags --long --match 'v[0-9]*' --dirty) + case "$description" in *-dirty) echo 'Refusing dirty chart tree' >&2; exit 1 ;; esac + version=${description#v} + case "$version" in *-0-g*) version=${version%%-0-g*} ;; esac + echo "version=${version}" >> "$GITHUB_OUTPUT" + - name: Package and publish chart + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + CHART_VERSION: ${{ steps.chart.outputs.version }} + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + test -n "$PACKAGE_USERNAME" && test -n "$PACKAGE_TOKEN" + helm lint chart + helm template 30-birthday chart --namespace default \ + --set persistence.existingClaim=30-birthday-data \ + --set ingress.enabled=true --set ingress.host=30.brunner.ninja >/dev/null + helm package chart --version "$CHART_VERSION" --app-version "$COMMIT_SHA" --destination "$RUNNER_TEMP" + curl --fail --silent --show-error \ + --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST --upload-file "${RUNNER_TEMP}/30-birthday-${CHART_VERSION}.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + - name: Promote GitOps chart version + env: + GITOPS_TOKEN: ${{ secrets.GITOPS_TOKEN }} + CHART_VERSION: ${{ steps.chart.outputs.version }} + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + test -n "$GITOPS_TOKEN" + test "$(git rev-parse origin/main)" = "$COMMIT_SHA" + auth=$(printf 'feedc0de:%s' "$GITOPS_TOKEN" | base64 | tr -d '\n') + git -c "http.extraheader=AUTHORIZATION: basic ${auth}" clone --depth=1 \ + https://code.brunner.ninja/feedc0de/homelab-gitops.git "$RUNNER_TEMP/gitops" + unset auth + cd "$RUNNER_TEMP/gitops" + gitops_auth=$(printf 'feedc0de:%s' "$GITOPS_TOKEN" | base64 | tr -d '\n') + unset GITOPS_TOKEN + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + sed -i -E "0,/targetRevision: [^ ]+/s//targetRevision: ${CHART_VERSION}/" bootstrap/30-birthday.yaml + if git diff --quiet; then exit 0; fi + git add bootstrap/30-birthday.yaml + git commit -m "Deploy birthday invitation chart ${CHART_VERSION}" + for attempt in 1 2 3; do + if git -c "http.extraheader=AUTHORIZATION: basic ${gitops_auth}" push origin HEAD:main; then exit 0; fi + git -c "http.extraheader=AUTHORIZATION: basic ${gitops_auth}" fetch origin main + git rebase origin/main + done + exit 1 diff --git a/README.md b/README.md index d44d866..8e11f86 100644 --- a/README.md +++ b/README.md @@ -89,36 +89,34 @@ Die Einladungsgrafik und PDF sind bewusst nicht im Git-Repository oder Container ## Kubernetes, Traefik und Authentik -Ein vollständiges Beispiel liegt in `k8s/invitation.yaml`. Es verwendet: +Das veröffentlichte Helm Chart befindet sich in `chart/`. Es owns the +Deployment, Service, probes, SQLite persistence contract, and the public/admin +Ingresses. SQLite must stay at one replica and uses a `Recreate` strategy. -- genau einen Pod (SQLite ist hier bewusst nicht horizontal skaliert) -- einen `128Mi`-`rook-ceph-block`-PVC für `/data/invites.db` -- die Domain `30.brunner.ninja` -- ausschließlich Traefiks EntryPoint `websecure` -- das bestehende `default-authentik@kubernetescrd`-Middleware nur für `/admin*` und `/api/admin/*` -- öffentliche Gast- und Asset-Routen ohne Authentik -- `Recreate`-Updates mit einer kurzen Unterbrechung, damit nur ein SQLite-Prozess den Block-PVC verwendet +For this homelab, Argo CD is the sole workload writer. Its cluster values and +existing-claim reference are in the separate +[homelab-gitops application](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/30-birthday). +The live `30-birthday-data` PVC is referenced as `existingClaim`; the chart +does not render or replace it. Invitation assets stay in the existing +`30-birthday-invitation` Secret, outside public Git. Authentik protects only +admin paths; guest and invitation asset routes remain public. -Zuerst das Image bauen und in die eigene Registry pushen: +For another cluster, create the invitation Secret first (the local helper +expects private assets excluded from Git), then install the chart: ```bash -docker build -t registry.brunner.ninja/feedc0de/30-birthday:latest . -docker push registry.brunner.ninja/feedc0de/30-birthday:latest +./k8s/create-invitation-secret.sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +helm upgrade --install 30-birthday brunner.ninja/30-birthday \ + --namespace default --wait --timeout 5m ``` -Die Gitea Action in `.gitea/workflows/container.yml` übernimmt Build, Push und Deployment automatisch. Sie benötigt diese Repository-Secrets: +Main-branch CI tests the app, publishes an immutable image, packages a chart +with that exact image as `appVersion`, and promotes only the chart version in +homelab-gitops. CI uses `QUAY_USERNAME`, `QUAY_TOKEN`, `PACKAGE_USERNAME`, +`PACKAGE_TOKEN`, and `GITOPS_TOKEN`; it has no Kubernetes credentials. -- `QUAY_USERNAME`: `feedc0de+birthday30` -- `QUAY_TOKEN`: Token dieses Robot-Accounts -- `KUBE_CONFIG_BASE64`: Kubeconfig des eingeschränkten `30-birthday-deployer`-ServiceAccounts - -Workload und CI-Berechtigungen werden einmalig mit einem Cluster-Administrator angelegt: - -```bash -./install.sh -./k8s/create-ci-kubeconfig.sh -``` - -Die Action baut `registry.brunner.ninja/feedc0de/30-birthday`, versieht jedes Image mit dem unveränderlichen Commit-SHA und aktualisiert auf `main` ausschließlich das bestehende `default/30-birthday`-Deployment. Ihre Kubernetes-Rolle kann keine Secrets oder anderen Deployments lesen oder verändern. - -Der Cluster stellt Authentiks `/outpost.goauthentik.io/*` bereits zentral bereit. Der App-Service ist nur als `ClusterIP` erreichbar; daher kann ein externer Client Authentiks Identitätsheader nicht direkt am App-Port einschleusen. +The cluster exposes Authentik's `/outpost.goauthentik.io/*` centrally. The app +Service remains `ClusterIP`, so external clients cannot inject Authentik +identity headers through the app port. diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..93b0aa4 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: 30-birthday +description: Helm chart for the 30th birthday RSVP application +type: application +version: 0.1.0 +appVersion: "bfac12a5bb24142c0c2f38b2a1c7bb0f5212df91" diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 0000000..9c2966f --- /dev/null +++ b/chart/README.md @@ -0,0 +1,25 @@ +# 30-birthday Helm chart + +Portable Helm chart for the 30th birthday invitation and RSVP application. The +chart owns its Deployment, Service, probes and public/admin Ingresses. SQLite +requires one replica and a persistent claim. Set `persistence.existingClaim` +to adopt an existing PVC; the chart then does not render a PVC object. + +Invitation PDF/JPEG assets and optional admin credentials are referenced by +existing Secret name only. Never commit their values. + +The homelab keeps its desired values in +[homelab-gitops](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/30-birthday). +For a new cluster, create the invitation Secret and run: + +```sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +helm upgrade --install 30-birthday brunner.ninja/30-birthday \ + --namespace default --wait --timeout 5m \ + --set origin=https://example.invalid \ + --set ingress.enabled=true --set ingress.host=example.invalid +``` + +Chart tags version the Helm contract. `appVersion` is the exact immutable image +tag published by the same source CI run. diff --git a/chart/install.sh b/chart/install.sh new file mode 100755 index 0000000..c79dcb9 --- /dev/null +++ b/chart/install.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +release=${RELEASE_NAME:-30-birthday} +namespace=${NAMESPACE:-default} + +helm repo add brunner.ninja https://brunner.ninja/charts --force-update +helm repo update brunner.ninja +if [ -n "${VALUES_FILE:-}" ]; then + helm upgrade --install "$release" brunner.ninja/30-birthday \ + --namespace "$namespace" --create-namespace \ + --values "$VALUES_FILE" --wait --timeout 5m +else + helm upgrade --install "$release" brunner.ninja/30-birthday \ + --namespace "$namespace" --create-namespace --wait --timeout 5m +fi diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..d07b1ca --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{- define "birthday.name" -}} +30-birthday +{{- end -}} + +{{- define "birthday.dataClaim" -}} +{{- default "30-birthday-data" .Values.persistence.existingClaim -}} +{{- end -}} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..a6d3372 --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: 30-birthday + labels: + app: 30-birthday + app.kubernetes.io/name: 30-birthday + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + app: 30-birthday + template: + metadata: + labels: + app: 30-birthday + app.kubernetes.io/name: 30-birthday + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + automountServiceAccountToken: false + securityContext: + fsGroup: 65532 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: 30-birthday + image: {{ printf "%s:%s" .Values.image.repository .Chart.AppVersion | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: ORIGIN + value: {{ .Values.origin | quote }} + - name: DATABASE_FILE + value: /data/invites.db + - name: TRUST_AUTHENTIK + value: {{ .Values.trustAuthentik | quote }} + - name: INVITATION_PDF + value: {{ .Values.invitationPdf | quote }} + - name: INVITATION_IMAGE + value: {{ .Values.invitationImage | quote }} + {{- if .Values.adminKey.secretName }} + - name: ADMIN_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.adminKey.secretName | quote }} + key: {{ .Values.adminKey.secretKey | quote }} + {{- end }} + ports: + - name: http + containerPort: 3000 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 2 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + volumeMounts: + - name: data + mountPath: /data + - name: invitation + mountPath: /private + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "birthday.dataClaim" . | quote }} + - name: invitation + secret: + secretName: {{ .Values.secrets.invitation | quote }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000..026e6e0 --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,58 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: 30-birthday-public + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- if .Values.ingress.tlsSecretName }} + tls: + - hosts: [{{ .Values.ingress.host | quote }}] + secretName: {{ .Values.ingress.tlsSecretName | quote }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: 30-birthday + port: + name: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: 30-birthday-admin + annotations: + {{- toYaml .Values.ingress.adminAnnotations | nindent 4 }} +spec: + ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- if .Values.ingress.tlsSecretName }} + tls: + - hosts: [{{ .Values.ingress.host | quote }}] + secretName: {{ .Values.ingress.tlsSecretName | quote }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: /admin + pathType: Prefix + backend: + service: + name: 30-birthday + port: + name: http + - path: /api/admin/ + pathType: Prefix + backend: + service: + name: 30-birthday + port: + name: http +{{- end }} diff --git a/chart/templates/pvc.yaml b/chart/templates/pvc.yaml new file mode 100644 index 0000000..f67cfe6 --- /dev/null +++ b/chart/templates/pvc.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "birthday.dataClaim" . }} + labels: + app.kubernetes.io/name: 30-birthday + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- with .Values.persistence.storageClassName }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..73a7774 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: 30-birthday + labels: + app: 30-birthday + app.kubernetes.io/name: 30-birthday + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + selector: + app: 30-birthday + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http diff --git a/chart/test.sh b/chart/test.sh new file mode 100755 index 0000000..e6eb143 --- /dev/null +++ b/chart/test.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -eu + +chart_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT + +helm lint "$chart_dir" +helm template 30-birthday "$chart_dir" --namespace default \ + --set persistence.existingClaim=existing-data \ + --set ingress.enabled=true --set ingress.host=test.example \ + >"$workdir/rendered.yaml" +if grep -q '^kind: PersistentVolumeClaim$' "$workdir/rendered.yaml"; then + echo 'existingClaim must not render a PVC' >&2 + exit 1 +fi +grep -q 'claimName: "existing-data"' "$workdir/rendered.yaml" +grep -q 'name: 30-birthday-public' "$workdir/rendered.yaml" +grep -q 'name: 30-birthday-admin' "$workdir/rendered.yaml" +grep -q 'path: /api/admin/' "$workdir/rendered.yaml" +app_version=$(sed -n 's/^appVersion: *"\{0,1\}\([^" ]*\).*/\1/p' "$chart_dir/Chart.yaml") +image_repository=$(sed -n 's/^ repository: *//p' "$chart_dir/values.yaml") +grep -Fq "${image_repository}:${app_version}" "$workdir/rendered.yaml" diff --git a/chart/values.schema.json b/chart/values.schema.json new file mode 100644 index 0000000..43d9aa9 --- /dev/null +++ b/chart/values.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": {"type": "integer", "const": 1}, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": {"type": "string", "minLength": 1}, + "pullPolicy": {"type": "string", "enum": ["Always", "IfNotPresent", "Never"]} + }, + "additionalProperties": false + }, + "origin": {"type": "string", "minLength": 1}, + "trustAuthentik": {"type": "boolean"}, + "invitationPdf": {"type": "string"}, + "invitationImage": {"type": "string"}, + "secrets": {"type": "object"}, + "persistence": { + "type": "object", + "required": ["enabled", "existingClaim"], + "properties": { + "enabled": {"type": "boolean"}, + "existingClaim": {"type": "string"}, + "size": {"type": "string"}, + "storageClassName": {"type": "string"} + }, + "additionalProperties": true + }, + "adminKey": {"type": "object"}, + "ingress": {"type": "object"}, + "resources": {"type": "object"} + }, + "additionalProperties": true +} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..91c0f1a --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,46 @@ +replicaCount: 1 + +image: + repository: registry.brunner.ninja/feedc0de/30-birthday + pullPolicy: Always + +origin: http://localhost:3000 +trustAuthentik: false +invitationPdf: /private/Einladung.pdf +invitationImage: /private/einladung.jpg + +secrets: + invitation: 30-birthday-invitation + +persistence: + enabled: true + existingClaim: "" + size: 128Mi + storageClassName: "" + +adminKey: + secretName: "" + secretKey: ADMIN_KEY + +ingress: + enabled: false + host: "" + ingressClassName: traefik + tlsSecretName: "" + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.priority: "10" + adminAnnotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd + traefik.ingress.kubernetes.io/router.priority: "100" + +resources: + requests: + cpu: 10m + memory: 32Mi + ephemeral-storage: 32Mi + limits: + cpu: 250m + memory: 192Mi + ephemeral-storage: 128Mi diff --git a/install.sh b/install.sh index 4bd1efe..9748df0 100755 --- a/install.sh +++ b/install.sh @@ -5,5 +5,4 @@ set -euo pipefail repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) "${repo_root}/k8s/create-invitation-secret.sh" -kubectl apply --filename="${repo_root}/k8s/ci-deployer.yaml" -kubectl apply --filename="${repo_root}/k8s/invitation.yaml" +exec "${repo_root}/chart/install.sh"