{% if tls_backup_enabled %}--- apiVersion: v1 kind: ServiceAccount metadata: name: tls-backup namespace: {{ taskserv.namespace }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: tls-backup-reader rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list"] - apiGroups: ["cert-manager.io"] resources: ["certificates", "clusterissuers"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: tls-backup-reader roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: tls-backup-reader subjects: - kind: ServiceAccount name: tls-backup namespace: {{ taskserv.namespace }} --- apiVersion: v1 kind: Secret metadata: name: restic-tls namespace: {{ taskserv.namespace }} type: Opaque stringData: RESTIC_REPOSITORY: "{{ restic_repository }}" RESTIC_PASSWORD: "{{ restic_password }}" AWS_ACCESS_KEY_ID: "{{ aws_access_key_id }}" AWS_SECRET_ACCESS_KEY: "{{ aws_secret_access_key }}" AWS_DEFAULT_REGION: "{{ aws_default_region }}" --- apiVersion: v1 kind: ConfigMap metadata: name: tls-backup-watcher namespace: {{ taskserv.namespace }} data: watch.nu: | #!/usr/bin/env nu $env.PATH = ($env.PATH | append "/kubectl-bin") const CERT_NS = "cert-manager" def dump_all [] { print "[tls-backup] dumping TLS secrets" let r = (do { ^kubectl get secrets -A -l cert-manager.io/certificate-name -o yaml } | complete) if $r.exit_code == 0 { $r.stdout | save --force /dump/tls-secrets.yaml } else { print $"[tls-backup] warn: secrets dump failed: ($r.stderr | str trim)" } print "[tls-backup] dumping ACME keys" let ri = (do { ^kubectl get clusterissuers -o json } | complete) if $ri.exit_code == 0 { $ri.stdout | from json | get -o items | default [] | each {|i| $i | get -o spec.acme.privateKeySecretRef.name | default ""} | where {|k| $k | is-not-empty} | each {|key| let rk = (do { ^kubectl get secret -n $CERT_NS $key -o yaml } | complete) if $rk.exit_code == 0 { $rk.stdout | save --force $"/dump/acme-key--($key).yaml" } } | ignore } "" | save --force /trigger/ready print "[tls-backup] backup trigger written" } def process_event [line: string] { let parts = ($line | split row ' ') if ($parts | length) < 3 { return } let evtype = ($parts | get 0? | default "") let ns = ($parts | get 1? | default "") let name = ($parts | get 2? | default "") if $evtype != "MODIFIED" { return } let fmt = 'jsonpath={.status.conditions[?(@.type=="Ready")].status}' let r = (do { ^kubectl get certificate $name -n $ns -o $fmt } | complete) if ($r.stdout | str trim) == "True" { print $"[tls-backup] ($ns)/($name) Ready" dump_all } } print "[tls-backup] watcher started" loop { print "[tls-backup] watching certificates" let watch_fmt = 'jsonpath={.type} {.object.metadata.namespace} {.object.metadata.name}{"\n"}' try { ^kubectl get certificates -A -w --output-watch-events=true -o $watch_fmt | lines | where {|l| $l | str trim | is-not-empty} | each {|line| process_event $line; null} | ignore } catch {|e| print $"[tls-backup] watch ended: ($e.msg)" } sleep 10sec } --- apiVersion: apps/v1 kind: Deployment metadata: name: tls-backup-watcher namespace: {{ taskserv.namespace }} spec: replicas: 1 selector: matchLabels: app: tls-backup-watcher template: metadata: labels: app: tls-backup-watcher spec: serviceAccountName: tls-backup initContainers: - name: install-kubectl image: alpine:3.21 command: ["/bin/sh", "-c"] args: - | set -euo pipefail apk add --no-cache curl ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; esac curl -fsSL https://dl.k8s.io/release/{{ tls_backup_kubectl_version }}/bin/linux/${ARCH}/kubectl \ -o /kubectl-bin/kubectl chmod +x /kubectl-bin/kubectl echo "[tls-backup] kubectl installed (arch=${ARCH})" volumeMounts: - name: kubectl-bin mountPath: /kubectl-bin volumes: - name: kubectl-bin emptyDir: {} - name: dump emptyDir: {} - name: trigger emptyDir: {} - name: scripts configMap: name: tls-backup-watcher defaultMode: 0755 containers: - name: watcher image: {{ tls_backup_nushell_image }} command: ["nu", "/scripts/watch.nu"] volumeMounts: - name: kubectl-bin mountPath: /kubectl-bin - name: dump mountPath: /dump - name: trigger mountPath: /trigger - name: scripts mountPath: /scripts - name: backup image: restic/restic:0.17.3 env: - name: RESTIC_REPOSITORY valueFrom: secretKeyRef: name: restic-tls key: RESTIC_REPOSITORY - name: RESTIC_PASSWORD valueFrom: secretKeyRef: name: restic-tls key: RESTIC_PASSWORD - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: name: restic-tls key: AWS_ACCESS_KEY_ID - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: restic-tls key: AWS_SECRET_ACCESS_KEY - name: AWS_DEFAULT_REGION valueFrom: secretKeyRef: name: restic-tls key: AWS_DEFAULT_REGION volumeMounts: - name: dump mountPath: /dump readOnly: true - name: trigger mountPath: /trigger command: ["/bin/sh", "-c"] args: - | set -euo pipefail restic snapshots >/dev/null 2>&1 || restic init echo "[tls-backup] backup container ready, watching for trigger" while true; do if [ -f /trigger/ready ]; then echo "[tls-backup] trigger detected, running backup" restic backup /dump \ --tag tls --tag workspace=libre-wuji \ --host libre-wuji-cluster restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune rm -f /trigger/ready echo "[tls-backup] backup complete" fi sleep 5 done {% else %}# tls-backup: not enabled (set tls_backup.enabled = true in component config){% endif %}