From bc187e01facb5fcee9ffdcaa934c817638ebe8a1 Mon Sep 17 00:00:00 2001 From: Lucius Bachmann Date: Wed, 29 Jul 2026 15:19:45 +0000 Subject: [PATCH 1/3] Update from template Template version: main (0beeff3) --- .cruft.json | 2 +- .github/workflows/test.yaml | 2 ++ Makefile.vars.mk | 2 +- .../solution-backup/apps/solution-backup.yaml | 0 tests/override-schedules.yml | 3 +++ 5 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 tests/golden/override-schedules/solution-backup/apps/solution-backup.yaml create mode 100644 tests/override-schedules.yml diff --git a/.cruft.json b/.cruft.json index d3bca23..394dfac 100644 --- a/.cruft.json +++ b/.cruft.json @@ -7,7 +7,7 @@ "name": "solution-backup", "slug": "solution-backup", "parameter_key": "solution_backup", - "test_cases": "defaults", + "test_cases": "defaults override-schedules", "add_lib": "n", "add_pp": "n", "add_golden": "y", diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2cd0c47..0bc78ca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -33,6 +33,7 @@ jobs: matrix: instance: - defaults + - override-schedules defaults: run: working-directory: ${{ env.COMPONENT_NAME }} @@ -48,6 +49,7 @@ jobs: matrix: instance: - defaults + - override-schedules defaults: run: working-directory: ${{ env.COMPONENT_NAME }} diff --git a/Makefile.vars.mk b/Makefile.vars.mk index c5796aa..28af913 100644 --- a/Makefile.vars.mk +++ b/Makefile.vars.mk @@ -50,4 +50,4 @@ KUBENT_IMAGE ?= ghcr.io/doitintl/kube-no-trouble:latest KUBENT_DOCKER ?= $(DOCKER_CMD) $(DOCKER_ARGS) $(root_volume) --entrypoint=/app/kubent $(KUBENT_IMAGE) instance ?= defaults -test_instances = tests/defaults.yml +test_instances = tests/defaults.yml tests/override-schedules.yml diff --git a/tests/golden/override-schedules/solution-backup/apps/solution-backup.yaml b/tests/golden/override-schedules/solution-backup/apps/solution-backup.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/override-schedules.yml b/tests/override-schedules.yml new file mode 100644 index 0000000..a4da5b7 --- /dev/null +++ b/tests/override-schedules.yml @@ -0,0 +1,3 @@ +# Overwrite parameters here + +# parameters: {...} From 46c471a0d8b66b48978e70008fe3c4c083698758 Mon Sep 17 00:00:00 2001 From: Lucius Bachmann Date: Wed, 29 Jul 2026 15:19:46 +0000 Subject: [PATCH 2/3] Update from template Template version: main (0beeff3) --- .cruft.json | 2 +- .github/workflows/test.yaml | 2 ++ Makefile.vars.mk | 2 +- .../override-labels/solution-backup/apps/solution-backup.yaml | 0 tests/override-labels.yml | 3 +++ 5 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 tests/golden/override-labels/solution-backup/apps/solution-backup.yaml create mode 100644 tests/override-labels.yml diff --git a/.cruft.json b/.cruft.json index 394dfac..64eb967 100644 --- a/.cruft.json +++ b/.cruft.json @@ -7,7 +7,7 @@ "name": "solution-backup", "slug": "solution-backup", "parameter_key": "solution_backup", - "test_cases": "defaults override-schedules", + "test_cases": "defaults override-schedules override-labels", "add_lib": "n", "add_pp": "n", "add_golden": "y", diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0bc78ca..e14822b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,6 +34,7 @@ jobs: instance: - defaults - override-schedules + - override-labels defaults: run: working-directory: ${{ env.COMPONENT_NAME }} @@ -50,6 +51,7 @@ jobs: instance: - defaults - override-schedules + - override-labels defaults: run: working-directory: ${{ env.COMPONENT_NAME }} diff --git a/Makefile.vars.mk b/Makefile.vars.mk index 28af913..6fb934e 100644 --- a/Makefile.vars.mk +++ b/Makefile.vars.mk @@ -50,4 +50,4 @@ KUBENT_IMAGE ?= ghcr.io/doitintl/kube-no-trouble:latest KUBENT_DOCKER ?= $(DOCKER_CMD) $(DOCKER_ARGS) $(root_volume) --entrypoint=/app/kubent $(KUBENT_IMAGE) instance ?= defaults -test_instances = tests/defaults.yml tests/override-schedules.yml +test_instances = tests/defaults.yml tests/override-schedules.yml tests/override-labels.yml diff --git a/tests/golden/override-labels/solution-backup/apps/solution-backup.yaml b/tests/golden/override-labels/solution-backup/apps/solution-backup.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/override-labels.yml b/tests/override-labels.yml new file mode 100644 index 0000000..a4da5b7 --- /dev/null +++ b/tests/override-labels.yml @@ -0,0 +1,3 @@ +# Overwrite parameters here + +# parameters: {...} From 6ce348b93873c8420a94feb4734952f62be893a5 Mon Sep 17 00:00:00 2001 From: Lucius Bachmann Date: Mon, 3 Aug 2026 09:46:23 +0200 Subject: [PATCH 3/3] implement first version We currently use the bucketname that we don't have to extract the bucketname from the secret. This can be removed once the bucketname can be referenced from the generated secret. --- .github/changelog-configuration.json | 1 - .github/workflows/espejote.yaml | 20 ++ Makefile | 1 + Makefile.custom.mk | 11 + class/defaults.yml | 31 ++- component/app.jsonnet | 13 +- component/main.jsonnet | 208 +++++++++++++++++- renovate.json | 17 +- .../defaults/defaults/apps/defaults.yaml | 4 + .../defaults/solution-backup/clusterRole.yaml | 25 +++ .../solution-backup/clusterRoleBinding.yaml | 12 + .../solution-backup/managedResource.yaml | 108 +++++++++ .../defaults/solution-backup/namespace.yaml | 7 + .../solution-backup/serviceAccount.yaml | 8 + .../solution-backup/apps/solution-backup.yaml | 0 .../override-labels/apps/override-labels.yaml | 4 + .../solution-backup/clusterRole.yaml | 25 +++ .../solution-backup/clusterRoleBinding.yaml | 12 + .../solution-backup/managedResource.yaml | 108 +++++++++ .../solution-backup/namespace.yaml | 7 + .../solution-backup/serviceAccount.yaml | 8 + .../solution-backup/apps/solution-backup.yaml | 0 .../apps/override-schedules.yaml | 4 + .../solution-backup/clusterRole.yaml | 25 +++ .../solution-backup/clusterRoleBinding.yaml | 12 + .../solution-backup/managedResource.yaml | 109 +++++++++ .../solution-backup/namespace.yaml | 7 + .../solution-backup/serviceAccount.yaml | 8 + .../solution-backup/apps/solution-backup.yaml | 0 tests/override-labels.yml | 13 +- tests/override-schedules.yml | 12 +- .../expected/labeled-general.rendered.yaml | 45 ++++ .../labeled-trigger-namespace.rendered.yaml | 46 ++++ ...with-encryption-secret-label.rendered.yaml | 45 ++++ ...led-without-encryption-label.rendered.yaml | 2 + .../trigger-namespace-unlabeled.rendered.yaml | 3 + tests/render/expected/unlabeled.rendered.yaml | 2 + tests/render/inputs/README.md | 33 +++ tests/render/inputs/labeled-general.yaml | 20 ++ .../inputs/labeled-trigger-namespace.yaml | 29 +++ .../labeled-with-encryption-secret-label.yaml | 19 ++ .../labeled-without-encryption-label.yaml | 18 ++ .../inputs/trigger-namespace-unlabeled.yaml | 17 ++ tests/render/inputs/unlabeled.yaml | 8 + tests/render/run.sh | 35 +++ 45 files changed, 1128 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/espejote.yaml create mode 100644 Makefile.custom.mk create mode 100644 tests/golden/defaults/defaults/apps/defaults.yaml create mode 100644 tests/golden/defaults/defaults/solution-backup/clusterRole.yaml create mode 100644 tests/golden/defaults/defaults/solution-backup/clusterRoleBinding.yaml create mode 100644 tests/golden/defaults/defaults/solution-backup/managedResource.yaml create mode 100644 tests/golden/defaults/defaults/solution-backup/namespace.yaml create mode 100644 tests/golden/defaults/defaults/solution-backup/serviceAccount.yaml delete mode 100644 tests/golden/defaults/solution-backup/apps/solution-backup.yaml create mode 100644 tests/golden/override-labels/override-labels/apps/override-labels.yaml create mode 100644 tests/golden/override-labels/override-labels/solution-backup/clusterRole.yaml create mode 100644 tests/golden/override-labels/override-labels/solution-backup/clusterRoleBinding.yaml create mode 100644 tests/golden/override-labels/override-labels/solution-backup/managedResource.yaml create mode 100644 tests/golden/override-labels/override-labels/solution-backup/namespace.yaml create mode 100644 tests/golden/override-labels/override-labels/solution-backup/serviceAccount.yaml delete mode 100644 tests/golden/override-labels/solution-backup/apps/solution-backup.yaml create mode 100644 tests/golden/override-schedules/override-schedules/apps/override-schedules.yaml create mode 100644 tests/golden/override-schedules/override-schedules/solution-backup/clusterRole.yaml create mode 100644 tests/golden/override-schedules/override-schedules/solution-backup/clusterRoleBinding.yaml create mode 100644 tests/golden/override-schedules/override-schedules/solution-backup/managedResource.yaml create mode 100644 tests/golden/override-schedules/override-schedules/solution-backup/namespace.yaml create mode 100644 tests/golden/override-schedules/override-schedules/solution-backup/serviceAccount.yaml delete mode 100644 tests/golden/override-schedules/solution-backup/apps/solution-backup.yaml create mode 100644 tests/render/expected/labeled-general.rendered.yaml create mode 100644 tests/render/expected/labeled-trigger-namespace.rendered.yaml create mode 100644 tests/render/expected/labeled-with-encryption-secret-label.rendered.yaml create mode 100644 tests/render/expected/labeled-without-encryption-label.rendered.yaml create mode 100644 tests/render/expected/trigger-namespace-unlabeled.rendered.yaml create mode 100644 tests/render/expected/unlabeled.rendered.yaml create mode 100644 tests/render/inputs/README.md create mode 100644 tests/render/inputs/labeled-general.yaml create mode 100644 tests/render/inputs/labeled-trigger-namespace.yaml create mode 100644 tests/render/inputs/labeled-with-encryption-secret-label.yaml create mode 100644 tests/render/inputs/labeled-without-encryption-label.yaml create mode 100644 tests/render/inputs/trigger-namespace-unlabeled.yaml create mode 100644 tests/render/inputs/unlabeled.yaml create mode 100644 tests/render/run.sh diff --git a/.github/changelog-configuration.json b/.github/changelog-configuration.json index 40dc985..2d9bd3d 100644 --- a/.github/changelog-configuration.json +++ b/.github/changelog-configuration.json @@ -29,4 +29,3 @@ ], "template": "${{CATEGORIZED_COUNT}} changes since ${{FROM_TAG}}\n\n${{CHANGELOG}}" } - diff --git a/.github/workflows/espejote.yaml b/.github/workflows/espejote.yaml new file mode 100644 index 0000000..85df869 --- /dev/null +++ b/.github/workflows/espejote.yaml @@ -0,0 +1,20 @@ +name: Espejote render +on: + pull_request: + push: + +env: + ESPEJOTE_VERSION: 0.19.0 + +jobs: + espejote: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install espejote ${{ env.ESPEJOTE_VERSION }} + run: | + set -euo pipefail + curl -sL "https://github.com/vshn/espejote/releases/download/v${ESPEJOTE_VERSION}/espejote_${ESPEJOTE_VERSION}_linux_amd64.tar.gz" | tar xz espejote + sudo install -m 0755 espejote /usr/local/bin/espejote + - name: Render and diff expected files + run: make test-espejote diff --git a/Makefile b/Makefile index 02f0524..9ce5ea8 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ SHELL := bash .SUFFIXES: include Makefile.vars.mk +-include Makefile.custom.mk .PHONY: help help: ## Show this help diff --git a/Makefile.custom.mk b/Makefile.custom.mk new file mode 100644 index 0000000..9bef4ec --- /dev/null +++ b/Makefile.custom.mk @@ -0,0 +1,11 @@ +# Configure instance alias for commodore component compile +commodore_args += --alias $(instance) + +.PHONY: test-espejote +test-espejote: + bash tests/render/run.sh + git diff --exit-code -- tests/render/expected/ + +.PHONY: gen-espejote +gen-espejote: + bash tests/render/run.sh diff --git a/class/defaults.yml b/class/defaults.yml index 91a5a50..12a0afc 100644 --- a/class/defaults.yml +++ b/class/defaults.yml @@ -2,4 +2,33 @@ parameters: solution_backup: =_metadata: multi_tenant: true - namespace: syn-solution-backup + multi_version: true + multi_instance: true + namespace: ${_instance} + + createNamespace: true + namespaceAnnotations: {} + namespaceLabels: {} + + backup_namespaces: + label: vshn.ch/solution-base-backup + value: "true" + + backup_encryption_secret: + label: vshn.ch/solution-base-backup-encryption-secret + + s3: + endpoint: https://objects.%(region)s.cloudscale.ch + region: rma + + objectbucket: + name_prefix: pvc-backup- + + schedule: + backup: "0 1 * * *" + check: "0 3 * * *" + prune: "0 4 * * *" + retention: + keepDaily: 3 + keepWeekly: 2 + keepMonthly: 1 diff --git a/component/app.jsonnet b/component/app.jsonnet index 9ed913d..a22d149 100644 --- a/component/app.jsonnet +++ b/component/app.jsonnet @@ -3,12 +3,21 @@ local inv = kap.inventory(); local params = inv.parameters.solution_backup; local argocd = import 'lib/argocd.libjsonnet'; -local app = argocd.App('solution-backup', params.namespace); +local instance = inv.parameters._instance; +local app = argocd.App(instance, params.namespace, secrets=true, base='solution_backup') { + spec+: { + syncPolicy+: { + syncOptions+: [ + 'ServerSideApply=true', + ], + }, + }, +}; local appPath = local project = std.get(std.get(app, 'spec', {}), 'project', 'syn'); if project == 'syn' then 'apps' else 'apps-%s' % project; { - ['%s/solution-backup' % appPath]: app, + ['%s/%s' % [ appPath, instance ]]: app, } diff --git a/component/main.jsonnet b/component/main.jsonnet index 1570c28..d2e92e7 100644 --- a/component/main.jsonnet +++ b/component/main.jsonnet @@ -1,10 +1,212 @@ -// main template for solution-backup local kap = import 'lib/kapitan.libjsonnet'; local kube = import 'lib/kube.libjsonnet'; + local inv = kap.inventory(); -// The hiera parameters for the component local params = inv.parameters.solution_backup; +local instance = inv.parameters._instance; +local slug = std.substr(std.sha256(instance), 0, 8); + +local nsName = params.namespace; +local objectPrefix = 'pvc-backup-' + slug; +local saName = 'solution-backup-manager'; + +local backupNs = params.backup_namespaces; +local encryption = params.backup_encryption_secret; + +local s3 = params.s3; +local ob = params.objectbucket; +local sched = params.schedule; +local endpointResolved = s3.endpoint % { region: s3.region }; + +local namespace = (if params.createNamespace then kube.Namespace(nsName) else null) + { + metadata+: { + labels+: params.namespaceLabels, + annotations+: params.namespaceAnnotations, + }, +}; + +local serviceAccount = kube.ServiceAccount(saName) { + metadata+: { namespace: nsName }, +}; +local clusterRole = { + apiVersion: 'rbac.authorization.k8s.io/v1', + kind: 'ClusterRole', + metadata: { + name: nsName + ':' + saName, + }, + rules: [ + { + apiGroups: [ '' ], + resources: [ 'namespaces' ], + verbs: [ 'get', 'list', 'watch' ], + }, + { + apiGroups: [ 'appcat.vshn.io' ], + resources: [ 'objectbuckets' ], + verbs: [ '*' ], + }, + { + apiGroups: [ 'k8up.io' ], + resources: [ 'schedules' ], + verbs: [ '*' ], + }, + ], +}; +local clusterRoleBinding = { + apiVersion: 'rbac.authorization.k8s.io/v1', + kind: 'ClusterRoleBinding', + metadata: { + name: nsName + ':' + saName, + }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: clusterRole.metadata.name, + }, + subjects: [ + { + kind: 'ServiceAccount', + name: saName, + namespace: nsName, + }, + ], +}; + +local rawTemplate = ||| + local esp = import 'espejote.libsonnet'; + + local inDelete(obj) = std.get(obj.metadata, 'deletionTimestamp', '') != ''; + local wantsBackup(ns) = + local labels = std.get(ns.metadata, 'labels', {}); + !inDelete(ns) + && std.get(labels, '%(optInLabel)s', 'false') == '%(optInValue)s' + && std.objectHas(labels, '%(encryptionSecretNameLabel)s'); + + local managedBy = { 'espejote.io/managed-by': '%(managedBy)s' }; + local credsSecretName = '%(credsSecretName)s'; + local bucketNamePrefix = '%(bucketNamePrefix)s'; + + local encryptionSecretName(ns) = + std.get(ns.metadata, 'labels', {})['%(encryptionSecretNameLabel)s']; + + local nsSlug(ns) = std.substr(std.sha256(ns.metadata.name), 0, 8); + + local makeObjectBucket(ns) = { + apiVersion: 'appcat.vshn.io/v1', + kind: 'ObjectBucket', + metadata: { + name: '%(objectPrefix)s', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + parameters: { + bucketName: bucketNamePrefix + nsSlug(ns), + region: '%(region)s' + }, + writeConnectionSecretToRef: { + name: credsSecretName + }, + }, + }; + + local makeSchedule(ns) = { + apiVersion: 'k8up.io/v1', + kind: 'Schedule', + metadata: { + name: '%(objectPrefix)s', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + backend: { + repoPasswordSecretRef: { + name: encryptionSecretName(ns), + key: 'password' + }, + s3: { + endpoint: '%(endpoint)s', + bucket: bucketNamePrefix + nsSlug(ns), + accessKeyIDSecretRef: { + name: credsSecretName, + key: 'AWS_ACCESS_KEY_ID' + }, + secretAccessKeySecretRef: { + name: credsSecretName, + key: 'AWS_SECRET_ACCESS_KEY' + }, + }, + }, + backup: { schedule: '%(backupSchedule)s' }, + check: { schedule: '%(checkSchedule)s' }, + prune: { schedule: '%(pruneSchedule)s', retention: %(retention)s }, + }, + }; + + local manageNs(ns) = [ makeObjectBucket(ns), makeSchedule(ns) ]; + + local tn = esp.triggerName(); + if tn == 'namespace' then ( + local ns = esp.triggerData().resource; + if ns != null && wantsBackup(ns) then manageNs(ns) else [] + ) else [ + obj + for ns in esp.context().backup_namespaces + if wantsBackup(ns) + for obj in manageNs(ns) + ] +|||; +local template = rawTemplate % { + optInLabel: backupNs.label, + optInValue: backupNs.value, + managedBy: nsName + '.' + objectPrefix, + objectPrefix: objectPrefix, + credsSecretName: 'backup-bucket-creds-' + slug, + bucketNamePrefix: ob.name_prefix + slug + '-', + encryptionSecretNameLabel: encryption.label, + region: s3.region, + endpoint: endpointResolved, + backupSchedule: sched.backup, + checkSchedule: sched.check, + pruneSchedule: sched.prune, + retention: std.manifestJsonEx(sched.retention, ' '), +}; + +local managedResource = { + apiVersion: 'espejote.io/v1alpha1', + kind: 'ManagedResource', + metadata: { + name: objectPrefix, + namespace: nsName, + }, + spec: { + serviceAccountRef: { name: saName }, + context: [ + { + name: 'backup_namespaces', + resource: { + apiVersion: 'v1', + kind: 'Namespace', + namespace: '', + labelSelector: { + matchLabels: { + [backupNs.label]: backupNs.value, + }, + }, + }, + }, + ], + triggers: [ + { name: 'namespace', watchContextResource: { name: 'backup_namespaces' } }, + ], + template: template, + }, +}; -// Define outputs below { + [if params.createNamespace then 'namespace']: namespace, + serviceAccount: serviceAccount, + clusterRole: clusterRole, + clusterRoleBinding: clusterRoleBinding, + managedResource: managedResource, } diff --git a/renovate.json b/renovate.json index f2e1d67..50cab48 100644 --- a/renovate.json +++ b/renovate.json @@ -5,7 +5,9 @@ ":disableDependencyDashboard" ], "ignorePaths": [ - ".github/**" + ".github/workflows/auto-release.yaml", + ".github/workflows/release.yaml", + ".github/workflows/test.yaml" ], "labels": [ "dependency" @@ -23,6 +25,19 @@ "suppressNotifications": [ "artifactErrors" ], + "customManagers": [ + { + "customType": "regex", + "fileMatch": [ + "^\\.github/workflows/espejote\\.ya?ml$" + ], + "matchStrings": [ + "ESPEJOTE_VERSION:\\s*(?v?[0-9.]+)" + ], + "datasourceTemplate": "github-releases", + "depNameTemplate": "vshn/espejote" + } + ], "packageRules": [ { "matchUpdateTypes": [ diff --git a/tests/golden/defaults/defaults/apps/defaults.yaml b/tests/golden/defaults/defaults/apps/defaults.yaml new file mode 100644 index 0000000..6825b97 --- /dev/null +++ b/tests/golden/defaults/defaults/apps/defaults.yaml @@ -0,0 +1,4 @@ +spec: + syncPolicy: + syncOptions: + - ServerSideApply=true diff --git a/tests/golden/defaults/defaults/solution-backup/clusterRole.yaml b/tests/golden/defaults/defaults/solution-backup/clusterRole.yaml new file mode 100644 index 0000000..c9cfe61 --- /dev/null +++ b/tests/golden/defaults/defaults/solution-backup/clusterRole.yaml @@ -0,0 +1,25 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: defaults:solution-backup-manager +rules: + - apiGroups: + - '' + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - appcat.vshn.io + resources: + - objectbuckets + verbs: + - '*' + - apiGroups: + - k8up.io + resources: + - schedules + verbs: + - '*' diff --git a/tests/golden/defaults/defaults/solution-backup/clusterRoleBinding.yaml b/tests/golden/defaults/defaults/solution-backup/clusterRoleBinding.yaml new file mode 100644 index 0000000..3c8aa02 --- /dev/null +++ b/tests/golden/defaults/defaults/solution-backup/clusterRoleBinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: defaults:solution-backup-manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: defaults:solution-backup-manager +subjects: + - kind: ServiceAccount + name: solution-backup-manager + namespace: defaults diff --git a/tests/golden/defaults/defaults/solution-backup/managedResource.yaml b/tests/golden/defaults/defaults/solution-backup/managedResource.yaml new file mode 100644 index 0000000..61281dd --- /dev/null +++ b/tests/golden/defaults/defaults/solution-backup/managedResource.yaml @@ -0,0 +1,108 @@ +apiVersion: espejote.io/v1alpha1 +kind: ManagedResource +metadata: + name: pvc-backup-0f37e81d + namespace: defaults +spec: + context: + - name: backup_namespaces + resource: + apiVersion: v1 + kind: Namespace + labelSelector: + matchLabels: + vshn.ch/solution-base-backup: 'true' + namespace: '' + serviceAccountRef: + name: solution-backup-manager + template: | + local esp = import 'espejote.libsonnet'; + + local inDelete(obj) = std.get(obj.metadata, 'deletionTimestamp', '') != ''; + local wantsBackup(ns) = + local labels = std.get(ns.metadata, 'labels', {}); + !inDelete(ns) + && std.get(labels, 'vshn.ch/solution-base-backup', 'false') == 'true' + && std.objectHas(labels, 'vshn.ch/solution-base-backup-encryption-secret'); + + local managedBy = { 'espejote.io/managed-by': 'defaults.pvc-backup-0f37e81d' }; + local credsSecretName = 'backup-bucket-creds-0f37e81d'; + local bucketNamePrefix = 'pvc-backup-0f37e81d-'; + + local encryptionSecretName(ns) = + std.get(ns.metadata, 'labels', {})['vshn.ch/solution-base-backup-encryption-secret']; + + local nsSlug(ns) = std.substr(std.sha256(ns.metadata.name), 0, 8); + + local makeObjectBucket(ns) = { + apiVersion: 'appcat.vshn.io/v1', + kind: 'ObjectBucket', + metadata: { + name: 'pvc-backup-0f37e81d', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + parameters: { + bucketName: bucketNamePrefix + nsSlug(ns), + region: 'rma' + }, + writeConnectionSecretToRef: { + name: credsSecretName + }, + }, + }; + + local makeSchedule(ns) = { + apiVersion: 'k8up.io/v1', + kind: 'Schedule', + metadata: { + name: 'pvc-backup-0f37e81d', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + backend: { + repoPasswordSecretRef: { + name: encryptionSecretName(ns), + key: 'password' + }, + s3: { + endpoint: 'https://objects.rma.cloudscale.ch', + bucket: bucketNamePrefix + nsSlug(ns), + accessKeyIDSecretRef: { + name: credsSecretName, + key: 'AWS_ACCESS_KEY_ID' + }, + secretAccessKeySecretRef: { + name: credsSecretName, + key: 'AWS_SECRET_ACCESS_KEY' + }, + }, + }, + backup: { schedule: '0 1 * * *' }, + check: { schedule: '0 3 * * *' }, + prune: { schedule: '0 4 * * *', retention: { + "keepDaily": 3, + "keepMonthly": 1, + "keepWeekly": 2 + } }, + }, + }; + + local manageNs(ns) = [ makeObjectBucket(ns), makeSchedule(ns) ]; + + local tn = esp.triggerName(); + if tn == 'namespace' then ( + local ns = esp.triggerData().resource; + if ns != null && wantsBackup(ns) then manageNs(ns) else [] + ) else [ + obj + for ns in esp.context().backup_namespaces + if wantsBackup(ns) + for obj in manageNs(ns) + ] + triggers: + - name: namespace + watchContextResource: + name: backup_namespaces diff --git a/tests/golden/defaults/defaults/solution-backup/namespace.yaml b/tests/golden/defaults/defaults/solution-backup/namespace.yaml new file mode 100644 index 0000000..be02cdb --- /dev/null +++ b/tests/golden/defaults/defaults/solution-backup/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: + name: defaults + name: defaults diff --git a/tests/golden/defaults/defaults/solution-backup/serviceAccount.yaml b/tests/golden/defaults/defaults/solution-backup/serviceAccount.yaml new file mode 100644 index 0000000..99b30e9 --- /dev/null +++ b/tests/golden/defaults/defaults/solution-backup/serviceAccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + name: solution-backup-manager + name: solution-backup-manager + namespace: defaults diff --git a/tests/golden/defaults/solution-backup/apps/solution-backup.yaml b/tests/golden/defaults/solution-backup/apps/solution-backup.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/golden/override-labels/override-labels/apps/override-labels.yaml b/tests/golden/override-labels/override-labels/apps/override-labels.yaml new file mode 100644 index 0000000..6825b97 --- /dev/null +++ b/tests/golden/override-labels/override-labels/apps/override-labels.yaml @@ -0,0 +1,4 @@ +spec: + syncPolicy: + syncOptions: + - ServerSideApply=true diff --git a/tests/golden/override-labels/override-labels/solution-backup/clusterRole.yaml b/tests/golden/override-labels/override-labels/solution-backup/clusterRole.yaml new file mode 100644 index 0000000..c6e0b9d --- /dev/null +++ b/tests/golden/override-labels/override-labels/solution-backup/clusterRole.yaml @@ -0,0 +1,25 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: override-labels:solution-backup-manager +rules: + - apiGroups: + - '' + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - appcat.vshn.io + resources: + - objectbuckets + verbs: + - '*' + - apiGroups: + - k8up.io + resources: + - schedules + verbs: + - '*' diff --git a/tests/golden/override-labels/override-labels/solution-backup/clusterRoleBinding.yaml b/tests/golden/override-labels/override-labels/solution-backup/clusterRoleBinding.yaml new file mode 100644 index 0000000..ad57464 --- /dev/null +++ b/tests/golden/override-labels/override-labels/solution-backup/clusterRoleBinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: override-labels:solution-backup-manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: override-labels:solution-backup-manager +subjects: + - kind: ServiceAccount + name: solution-backup-manager + namespace: override-labels diff --git a/tests/golden/override-labels/override-labels/solution-backup/managedResource.yaml b/tests/golden/override-labels/override-labels/solution-backup/managedResource.yaml new file mode 100644 index 0000000..38ce0f8 --- /dev/null +++ b/tests/golden/override-labels/override-labels/solution-backup/managedResource.yaml @@ -0,0 +1,108 @@ +apiVersion: espejote.io/v1alpha1 +kind: ManagedResource +metadata: + name: pvc-backup-e5344851 + namespace: override-labels +spec: + context: + - name: backup_namespaces + resource: + apiVersion: v1 + kind: Namespace + labelSelector: + matchLabels: + acme.io/backup-enabled: 'yes' + namespace: '' + serviceAccountRef: + name: solution-backup-manager + template: | + local esp = import 'espejote.libsonnet'; + + local inDelete(obj) = std.get(obj.metadata, 'deletionTimestamp', '') != ''; + local wantsBackup(ns) = + local labels = std.get(ns.metadata, 'labels', {}); + !inDelete(ns) + && std.get(labels, 'acme.io/backup-enabled', 'false') == 'yes' + && std.objectHas(labels, 'acme.io/backup-encryption-secret'); + + local managedBy = { 'espejote.io/managed-by': 'override-labels.pvc-backup-e5344851' }; + local credsSecretName = 'backup-bucket-creds-e5344851'; + local bucketNamePrefix = 'pvc-backup-e5344851-'; + + local encryptionSecretName(ns) = + std.get(ns.metadata, 'labels', {})['acme.io/backup-encryption-secret']; + + local nsSlug(ns) = std.substr(std.sha256(ns.metadata.name), 0, 8); + + local makeObjectBucket(ns) = { + apiVersion: 'appcat.vshn.io/v1', + kind: 'ObjectBucket', + metadata: { + name: 'pvc-backup-e5344851', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + parameters: { + bucketName: bucketNamePrefix + nsSlug(ns), + region: 'ch-dk-2' + }, + writeConnectionSecretToRef: { + name: credsSecretName + }, + }, + }; + + local makeSchedule(ns) = { + apiVersion: 'k8up.io/v1', + kind: 'Schedule', + metadata: { + name: 'pvc-backup-e5344851', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + backend: { + repoPasswordSecretRef: { + name: encryptionSecretName(ns), + key: 'password' + }, + s3: { + endpoint: 'https://sos-ch-dk-2.exo.io', + bucket: bucketNamePrefix + nsSlug(ns), + accessKeyIDSecretRef: { + name: credsSecretName, + key: 'AWS_ACCESS_KEY_ID' + }, + secretAccessKeySecretRef: { + name: credsSecretName, + key: 'AWS_SECRET_ACCESS_KEY' + }, + }, + }, + backup: { schedule: '0 1 * * *' }, + check: { schedule: '0 3 * * *' }, + prune: { schedule: '0 4 * * *', retention: { + "keepDaily": 3, + "keepMonthly": 1, + "keepWeekly": 2 + } }, + }, + }; + + local manageNs(ns) = [ makeObjectBucket(ns), makeSchedule(ns) ]; + + local tn = esp.triggerName(); + if tn == 'namespace' then ( + local ns = esp.triggerData().resource; + if ns != null && wantsBackup(ns) then manageNs(ns) else [] + ) else [ + obj + for ns in esp.context().backup_namespaces + if wantsBackup(ns) + for obj in manageNs(ns) + ] + triggers: + - name: namespace + watchContextResource: + name: backup_namespaces diff --git a/tests/golden/override-labels/override-labels/solution-backup/namespace.yaml b/tests/golden/override-labels/override-labels/solution-backup/namespace.yaml new file mode 100644 index 0000000..d8147f7 --- /dev/null +++ b/tests/golden/override-labels/override-labels/solution-backup/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: + name: override-labels + name: override-labels diff --git a/tests/golden/override-labels/override-labels/solution-backup/serviceAccount.yaml b/tests/golden/override-labels/override-labels/solution-backup/serviceAccount.yaml new file mode 100644 index 0000000..17f3ce1 --- /dev/null +++ b/tests/golden/override-labels/override-labels/solution-backup/serviceAccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + name: solution-backup-manager + name: solution-backup-manager + namespace: override-labels diff --git a/tests/golden/override-labels/solution-backup/apps/solution-backup.yaml b/tests/golden/override-labels/solution-backup/apps/solution-backup.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/golden/override-schedules/override-schedules/apps/override-schedules.yaml b/tests/golden/override-schedules/override-schedules/apps/override-schedules.yaml new file mode 100644 index 0000000..6825b97 --- /dev/null +++ b/tests/golden/override-schedules/override-schedules/apps/override-schedules.yaml @@ -0,0 +1,4 @@ +spec: + syncPolicy: + syncOptions: + - ServerSideApply=true diff --git a/tests/golden/override-schedules/override-schedules/solution-backup/clusterRole.yaml b/tests/golden/override-schedules/override-schedules/solution-backup/clusterRole.yaml new file mode 100644 index 0000000..3f899d6 --- /dev/null +++ b/tests/golden/override-schedules/override-schedules/solution-backup/clusterRole.yaml @@ -0,0 +1,25 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: override-schedules:solution-backup-manager +rules: + - apiGroups: + - '' + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - appcat.vshn.io + resources: + - objectbuckets + verbs: + - '*' + - apiGroups: + - k8up.io + resources: + - schedules + verbs: + - '*' diff --git a/tests/golden/override-schedules/override-schedules/solution-backup/clusterRoleBinding.yaml b/tests/golden/override-schedules/override-schedules/solution-backup/clusterRoleBinding.yaml new file mode 100644 index 0000000..d786a87 --- /dev/null +++ b/tests/golden/override-schedules/override-schedules/solution-backup/clusterRoleBinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: override-schedules:solution-backup-manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: override-schedules:solution-backup-manager +subjects: + - kind: ServiceAccount + name: solution-backup-manager + namespace: override-schedules diff --git a/tests/golden/override-schedules/override-schedules/solution-backup/managedResource.yaml b/tests/golden/override-schedules/override-schedules/solution-backup/managedResource.yaml new file mode 100644 index 0000000..338810c --- /dev/null +++ b/tests/golden/override-schedules/override-schedules/solution-backup/managedResource.yaml @@ -0,0 +1,109 @@ +apiVersion: espejote.io/v1alpha1 +kind: ManagedResource +metadata: + name: pvc-backup-fcde2823 + namespace: override-schedules +spec: + context: + - name: backup_namespaces + resource: + apiVersion: v1 + kind: Namespace + labelSelector: + matchLabels: + vshn.ch/solution-base-backup: 'true' + namespace: '' + serviceAccountRef: + name: solution-backup-manager + template: | + local esp = import 'espejote.libsonnet'; + + local inDelete(obj) = std.get(obj.metadata, 'deletionTimestamp', '') != ''; + local wantsBackup(ns) = + local labels = std.get(ns.metadata, 'labels', {}); + !inDelete(ns) + && std.get(labels, 'vshn.ch/solution-base-backup', 'false') == 'true' + && std.objectHas(labels, 'vshn.ch/solution-base-backup-encryption-secret'); + + local managedBy = { 'espejote.io/managed-by': 'override-schedules.pvc-backup-fcde2823' }; + local credsSecretName = 'backup-bucket-creds-fcde2823'; + local bucketNamePrefix = 'pvc-backup-fcde2823-'; + + local encryptionSecretName(ns) = + std.get(ns.metadata, 'labels', {})['vshn.ch/solution-base-backup-encryption-secret']; + + local nsSlug(ns) = std.substr(std.sha256(ns.metadata.name), 0, 8); + + local makeObjectBucket(ns) = { + apiVersion: 'appcat.vshn.io/v1', + kind: 'ObjectBucket', + metadata: { + name: 'pvc-backup-fcde2823', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + parameters: { + bucketName: bucketNamePrefix + nsSlug(ns), + region: 'rma' + }, + writeConnectionSecretToRef: { + name: credsSecretName + }, + }, + }; + + local makeSchedule(ns) = { + apiVersion: 'k8up.io/v1', + kind: 'Schedule', + metadata: { + name: 'pvc-backup-fcde2823', + namespace: ns.metadata.name, + labels: managedBy + }, + spec: { + backend: { + repoPasswordSecretRef: { + name: encryptionSecretName(ns), + key: 'password' + }, + s3: { + endpoint: 'https://objects.rma.cloudscale.ch', + bucket: bucketNamePrefix + nsSlug(ns), + accessKeyIDSecretRef: { + name: credsSecretName, + key: 'AWS_ACCESS_KEY_ID' + }, + secretAccessKeySecretRef: { + name: credsSecretName, + key: 'AWS_SECRET_ACCESS_KEY' + }, + }, + }, + backup: { schedule: '0 2 * * *' }, + check: { schedule: '0 4 * * 6' }, + prune: { schedule: '0 6 * * 0', retention: { + "keepDaily": 7, + "keepLast": 14, + "keepMonthly": 1, + "keepWeekly": 2 + } }, + }, + }; + + local manageNs(ns) = [ makeObjectBucket(ns), makeSchedule(ns) ]; + + local tn = esp.triggerName(); + if tn == 'namespace' then ( + local ns = esp.triggerData().resource; + if ns != null && wantsBackup(ns) then manageNs(ns) else [] + ) else [ + obj + for ns in esp.context().backup_namespaces + if wantsBackup(ns) + for obj in manageNs(ns) + ] + triggers: + - name: namespace + watchContextResource: + name: backup_namespaces diff --git a/tests/golden/override-schedules/override-schedules/solution-backup/namespace.yaml b/tests/golden/override-schedules/override-schedules/solution-backup/namespace.yaml new file mode 100644 index 0000000..fc4e151 --- /dev/null +++ b/tests/golden/override-schedules/override-schedules/solution-backup/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: + name: override-schedules + name: override-schedules diff --git a/tests/golden/override-schedules/override-schedules/solution-backup/serviceAccount.yaml b/tests/golden/override-schedules/override-schedules/solution-backup/serviceAccount.yaml new file mode 100644 index 0000000..27f5e74 --- /dev/null +++ b/tests/golden/override-schedules/override-schedules/solution-backup/serviceAccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + name: solution-backup-manager + name: solution-backup-manager + namespace: override-schedules diff --git a/tests/golden/override-schedules/solution-backup/apps/solution-backup.yaml b/tests/golden/override-schedules/solution-backup/apps/solution-backup.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/override-labels.yml b/tests/override-labels.yml index a4da5b7..991c6f2 100644 --- a/tests/override-labels.yml +++ b/tests/override-labels.yml @@ -1,3 +1,10 @@ -# Overwrite parameters here - -# parameters: {...} +parameters: + solution_backup: + backup_namespaces: + label: acme.io/backup-enabled + value: "yes" + backup_encryption_secret: + label: acme.io/backup-encryption-secret + s3: + endpoint: https://sos-%(region)s.exo.io + region: ch-dk-2 diff --git a/tests/override-schedules.yml b/tests/override-schedules.yml index a4da5b7..41008d4 100644 --- a/tests/override-schedules.yml +++ b/tests/override-schedules.yml @@ -1,3 +1,9 @@ -# Overwrite parameters here - -# parameters: {...} +parameters: + solution_backup: + schedule: + backup: "0 2 * * *" + check: "0 4 * * 6" + prune: "0 6 * * 0" + retention: + keepLast: 14 + keepDaily: 7 diff --git a/tests/render/expected/labeled-general.rendered.yaml b/tests/render/expected/labeled-general.rendered.yaml new file mode 100644 index 0000000..9419437 --- /dev/null +++ b/tests/render/expected/labeled-general.rendered.yaml @@ -0,0 +1,45 @@ +--- +- apiVersion: appcat.vshn.io/v1 + kind: ObjectBucket + metadata: + labels: + espejote.io/managed-by: defaults.pvc-backup-0f37e81d + name: pvc-backup-0f37e81d + namespace: test-app + spec: + parameters: + bucketName: pvc-backup-0f37e81d-b58b0cb4 + region: rma + writeConnectionSecretToRef: + name: backup-bucket-creds-0f37e81d +- apiVersion: k8up.io/v1 + kind: Schedule + metadata: + labels: + espejote.io/managed-by: defaults.pvc-backup-0f37e81d + name: pvc-backup-0f37e81d + namespace: test-app + spec: + backend: + repoPasswordSecretRef: + key: password + name: backup-encryption + s3: + accessKeyIDSecretRef: + key: AWS_ACCESS_KEY_ID + name: backup-bucket-creds-0f37e81d + bucket: pvc-backup-0f37e81d-b58b0cb4 + endpoint: https://objects.rma.cloudscale.ch + secretAccessKeySecretRef: + key: AWS_SECRET_ACCESS_KEY + name: backup-bucket-creds-0f37e81d + backup: + schedule: 0 1 * * * + check: + schedule: 0 3 * * * + prune: + retention: + keepDaily: 3 + keepMonthly: 1 + keepWeekly: 2 + schedule: 0 4 * * * diff --git a/tests/render/expected/labeled-trigger-namespace.rendered.yaml b/tests/render/expected/labeled-trigger-namespace.rendered.yaml new file mode 100644 index 0000000..72193af --- /dev/null +++ b/tests/render/expected/labeled-trigger-namespace.rendered.yaml @@ -0,0 +1,46 @@ +--- +# Trigger: {"TriggerName":"namespace","WatchResource":{"apiVersion":"v1","kind":"Namespace","name":"test-app"}} +- apiVersion: appcat.vshn.io/v1 + kind: ObjectBucket + metadata: + labels: + espejote.io/managed-by: defaults.pvc-backup-0f37e81d + name: pvc-backup-0f37e81d + namespace: test-app + spec: + parameters: + bucketName: pvc-backup-0f37e81d-b58b0cb4 + region: rma + writeConnectionSecretToRef: + name: backup-bucket-creds-0f37e81d +- apiVersion: k8up.io/v1 + kind: Schedule + metadata: + labels: + espejote.io/managed-by: defaults.pvc-backup-0f37e81d + name: pvc-backup-0f37e81d + namespace: test-app + spec: + backend: + repoPasswordSecretRef: + key: password + name: backup-encryption + s3: + accessKeyIDSecretRef: + key: AWS_ACCESS_KEY_ID + name: backup-bucket-creds-0f37e81d + bucket: pvc-backup-0f37e81d-b58b0cb4 + endpoint: https://objects.rma.cloudscale.ch + secretAccessKeySecretRef: + key: AWS_SECRET_ACCESS_KEY + name: backup-bucket-creds-0f37e81d + backup: + schedule: 0 1 * * * + check: + schedule: 0 3 * * * + prune: + retention: + keepDaily: 3 + keepMonthly: 1 + keepWeekly: 2 + schedule: 0 4 * * * diff --git a/tests/render/expected/labeled-with-encryption-secret-label.rendered.yaml b/tests/render/expected/labeled-with-encryption-secret-label.rendered.yaml new file mode 100644 index 0000000..b44aee4 --- /dev/null +++ b/tests/render/expected/labeled-with-encryption-secret-label.rendered.yaml @@ -0,0 +1,45 @@ +--- +- apiVersion: appcat.vshn.io/v1 + kind: ObjectBucket + metadata: + labels: + espejote.io/managed-by: defaults.pvc-backup-0f37e81d + name: pvc-backup-0f37e81d + namespace: test-app + spec: + parameters: + bucketName: pvc-backup-0f37e81d-b58b0cb4 + region: rma + writeConnectionSecretToRef: + name: backup-bucket-creds-0f37e81d +- apiVersion: k8up.io/v1 + kind: Schedule + metadata: + labels: + espejote.io/managed-by: defaults.pvc-backup-0f37e81d + name: pvc-backup-0f37e81d + namespace: test-app + spec: + backend: + repoPasswordSecretRef: + key: password + name: my-custom-encryption + s3: + accessKeyIDSecretRef: + key: AWS_ACCESS_KEY_ID + name: backup-bucket-creds-0f37e81d + bucket: pvc-backup-0f37e81d-b58b0cb4 + endpoint: https://objects.rma.cloudscale.ch + secretAccessKeySecretRef: + key: AWS_SECRET_ACCESS_KEY + name: backup-bucket-creds-0f37e81d + backup: + schedule: 0 1 * * * + check: + schedule: 0 3 * * * + prune: + retention: + keepDaily: 3 + keepMonthly: 1 + keepWeekly: 2 + schedule: 0 4 * * * diff --git a/tests/render/expected/labeled-without-encryption-label.rendered.yaml b/tests/render/expected/labeled-without-encryption-label.rendered.yaml new file mode 100644 index 0000000..fe92594 --- /dev/null +++ b/tests/render/expected/labeled-without-encryption-label.rendered.yaml @@ -0,0 +1,2 @@ +--- +[] diff --git a/tests/render/expected/trigger-namespace-unlabeled.rendered.yaml b/tests/render/expected/trigger-namespace-unlabeled.rendered.yaml new file mode 100644 index 0000000..39d9db0 --- /dev/null +++ b/tests/render/expected/trigger-namespace-unlabeled.rendered.yaml @@ -0,0 +1,3 @@ +--- +# Trigger: {"TriggerName":"namespace","WatchResource":{"apiVersion":"v1","kind":"Namespace","name":"test-app"}} +[] diff --git a/tests/render/expected/unlabeled.rendered.yaml b/tests/render/expected/unlabeled.rendered.yaml new file mode 100644 index 0000000..fe92594 --- /dev/null +++ b/tests/render/expected/unlabeled.rendered.yaml @@ -0,0 +1,2 @@ +--- +[] diff --git a/tests/render/inputs/README.md b/tests/render/inputs/README.md new file mode 100644 index 0000000..a0e893a --- /dev/null +++ b/tests/render/inputs/README.md @@ -0,0 +1,33 @@ +# Test inputs + +These YAML files simulate in-cluster state offline and feed `espejote render +--input`. Each file is a single document with `triggers`, `context`, and +`libraries`; the `context.resources` arrays are pre-filtered to match what a +real cluster's selectors would return (offline, `espejote` does not re-apply +the ManagedResource's labelSelector against `context[].resources`). + +Opt-in label (defaults instance): `vshn.ch/solution-base-backup: "true"`. +Encryption-secret label (defaults instance): +`vshn.ch/solution-base-backup-encryption-secret`, value = the name of the Secret +holding the restic repo password under key `password`. There is NO default — +a namespace without this label is not backed up. + +- `labeled-general.yaml` — general reconciliation, one opted-in namespace + `test-app` with both the opt-in + encryption-secret labels. Renders the + AppCat `ObjectBucket` claim and the k8up `Schedule`. +- `labeled-trigger-namespace.yaml` — `namespace` watch trigger fires for the + opted-in namespace (both labels); context also lists it. Renders the pair + for that ns only. +- `labeled-with-encryption-secret-label.yaml` — general reconciliation, opted + in AND carrying the encryption-secret label with a custom Secret name. The + Schedule's `repoPasswordSecretRef.name` resolves to that custom name + (`my-custom-encryption`). +- `labeled-without-encryption-label.yaml` — opted in via the opt-in label but + WITHOUT the encryption-secret label. There is no default, so nothing renders. +- `unlabeled.yaml` — general reconciliation, no namespace opted in (empty + `backup_namespaces` context). Renders nothing. +- `trigger-namespace-unlabeled.yaml` — `namespace` watch trigger fires for a + namespace without the opt-in label; context empty. Renders nothing (the + `wantsBackup(ns)` guard returns false). + +No cluster, no kubectl: only `espejote` (from PATH, not vendored) reads them. diff --git a/tests/render/inputs/labeled-general.yaml b/tests/render/inputs/labeled-general.yaml new file mode 100644 index 0000000..5ea44a7 --- /dev/null +++ b/tests/render/inputs/labeled-general.yaml @@ -0,0 +1,20 @@ +# Scenario labeled-general — general reconciliation, one namespace `test-app` +# opted in via `vshn.ch/solution-base-backup=true` AND carrying the encryption +# secret label `vshn.ch/solution-base-backup-encryption-secret: backup-encryption` +# (the name of the restic repo-password Secret the user created in that ns). +# Renders ObjectBucket + Schedule. +triggers: + - {} +context: + - name: backup_namespaces + resources: + - apiVersion: v1 + kind: Namespace + metadata: + name: test-app + labels: + vshn.ch/solution-base-backup: "true" + vshn.ch/solution-base-backup-encryption-secret: backup-encryption + status: + phase: Active +libraries: {} diff --git a/tests/render/inputs/labeled-trigger-namespace.yaml b/tests/render/inputs/labeled-trigger-namespace.yaml new file mode 100644 index 0000000..3992480 --- /dev/null +++ b/tests/render/inputs/labeled-trigger-namespace.yaml @@ -0,0 +1,29 @@ +# Scenario labeled-trigger-namespace — `namespace` watch trigger fires for the +# opted-in namespace `test-app` (opt-in + encryption-secret labels both present), +# and the `backup_namespaces` context also lists it. Renders ObjectBucket + +# Schedule for that namespace only. +triggers: + - name: namespace + watchResource: + apiVersion: v1 + kind: Namespace + metadata: + name: test-app + labels: + vshn.ch/solution-base-backup: "true" + vshn.ch/solution-base-backup-encryption-secret: backup-encryption + status: + phase: Active +context: + - name: backup_namespaces + resources: + - apiVersion: v1 + kind: Namespace + metadata: + name: test-app + labels: + vshn.ch/solution-base-backup: "true" + vshn.ch/solution-base-backup-encryption-secret: backup-encryption + status: + phase: Active +libraries: {} diff --git a/tests/render/inputs/labeled-with-encryption-secret-label.yaml b/tests/render/inputs/labeled-with-encryption-secret-label.yaml new file mode 100644 index 0000000..a88c7f3 --- /dev/null +++ b/tests/render/inputs/labeled-with-encryption-secret-label.yaml @@ -0,0 +1,19 @@ +# Scenario labeled-with-encryption-secret-label — general reconciliation, +# namespace `test-app` opted in (opt-in label) and carrying the encryption +# secret label pointing at a custom Secret name `my-custom-encryption`. The +# Schedule's repoPasswordSecretRef.name must resolve to `my-custom-encryption`. +triggers: + - {} +context: + - name: backup_namespaces + resources: + - apiVersion: v1 + kind: Namespace + metadata: + name: test-app + labels: + vshn.ch/solution-base-backup: "true" + vshn.ch/solution-base-backup-encryption-secret: my-custom-encryption + status: + phase: Active +libraries: {} diff --git a/tests/render/inputs/labeled-without-encryption-label.yaml b/tests/render/inputs/labeled-without-encryption-label.yaml new file mode 100644 index 0000000..98d80d1 --- /dev/null +++ b/tests/render/inputs/labeled-without-encryption-label.yaml @@ -0,0 +1,18 @@ +# Scenario labeled-without-encryption-label — general reconciliation, namespace +# `test-app` opted in via the opt-in label but WITHOUT the encryption-secret +# label. There is no default encryption-secret name, so `wantsBackup(ns)` +# returns false and nothing is rendered for that namespace. +triggers: + - {} +context: + - name: backup_namespaces + resources: + - apiVersion: v1 + kind: Namespace + metadata: + name: test-app + labels: + vshn.ch/solution-base-backup: "true" + status: + phase: Active +libraries: {} diff --git a/tests/render/inputs/trigger-namespace-unlabeled.yaml b/tests/render/inputs/trigger-namespace-unlabeled.yaml new file mode 100644 index 0000000..2a9e533 --- /dev/null +++ b/tests/render/inputs/trigger-namespace-unlabeled.yaml @@ -0,0 +1,17 @@ +# Scenario trigger-namespace-unlabeled — `namespace` watch trigger fires for a +# namespace WITHOUT the opt-in label; backup_namespaces context is empty. The +# `wantsBackup(ns)` guard returns false, so nothing is rendered. +triggers: + - name: namespace + watchResource: + apiVersion: v1 + kind: Namespace + metadata: + name: test-app + labels: {} + status: + phase: Active +context: + - name: backup_namespaces + resources: [] +libraries: {} diff --git a/tests/render/inputs/unlabeled.yaml b/tests/render/inputs/unlabeled.yaml new file mode 100644 index 0000000..21025d9 --- /dev/null +++ b/tests/render/inputs/unlabeled.yaml @@ -0,0 +1,8 @@ +# Scenario unlabeled — general reconciliation, no namespace opted in +# (empty backup_namespaces context). Renders nothing. +triggers: + - {} +context: + - name: backup_namespaces + resources: [] +libraries: {} diff --git a/tests/render/run.sh b/tests/render/run.sh new file mode 100644 index 0000000..bf7468c --- /dev/null +++ b/tests/render/run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# ponytail: render every input through the compiled defaults ManagedResource and +# write the output over its committed expected file. The caller checks for drift +# with `git diff --exit-code tests/render/expected/` — same golden-file model as +# `make golden-diff`, no hand-rolled assertions. +# +# Run `bash tests/render/run.sh && git diff --exit-code tests/render/expected/` +# locally; CI does the same. Regenerate the expected files after an intentional +# template change with `bash tests/render/run.sh` then commit them. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +MANIFEST="$ROOT/compiled/defaults/solution-backup/managedResource.yaml" +INPUTS="$ROOT/tests/render/inputs" +EXPECTED="$ROOT/tests/render/expected" + +if ! command -v espejote >/dev/null 2>&1; then + echo "espejote not found on PATH — install it from https://github.com/vshn/espejote releases (not vendored in this repo)" >&2 + exit 77 +fi + +if [ ! -f "$MANIFEST" ]; then + make test -e instance=defaults >/dev/null +fi +if [ ! -f "$MANIFEST" ]; then + echo "FAIL: compiled ManagedResource not found at $MANIFEST" >&2 + exit 1 +fi + +mkdir -p "$EXPECTED" +for input in "$INPUTS"/*.yaml; do + name="$(basename "$input" .yaml)" + espejote render "$MANIFEST" --input "$input" >"$EXPECTED/$name.rendered.yaml" +done +echo "rendered $(ls -1 "$INPUTS"/*.yaml | wc -l) scenarios into $EXPECTED"