From 0dc2a21f5fa647a1e814d61836d3242b3ea77874 Mon Sep 17 00:00:00 2001 From: Marco De Luca Date: Fri, 29 May 2026 09:02:30 +0200 Subject: [PATCH 1/3] Initial implementation --- Makefile.vars.mk | 2 +- class/defaults.yml | 64 +++++- component/app.jsonnet | 10 +- component/main.jsonnet | 62 +++++- docs/modules/ROOT/pages/how-tos/.gitkeep | 0 docs/modules/ROOT/pages/index.adoc | 37 +++- .../ROOT/pages/references/parameters.adoc | 209 +++++++++++++++++- lib/talos-backup.libsonnet | 121 ++++++++++ tests/defaults.yml | 10 +- .../talos-backup/apps/talos-backup.yaml | 4 + .../talos-backup/00_namespace.yaml | 10 + .../talos-backup/10_talos_serviceaccount.yaml | 13 ++ .../talos-backup/talos-backup/20_cronjob.yaml | 86 +++++++ .../talos-backup/apps/talos-backup.yaml | 4 + .../talos-backup/00_namespace.yaml | 10 + .../talos-backup/10_s3_credentials.yaml | 15 ++ .../talos-backup/10_talos_serviceaccount.yaml | 13 ++ .../talos-backup/talos-backup/20_cronjob.yaml | 100 +++++++++ tests/resources.yml | 36 +++ 19 files changed, 792 insertions(+), 14 deletions(-) delete mode 100644 docs/modules/ROOT/pages/how-tos/.gitkeep create mode 100644 lib/talos-backup.libsonnet create mode 100644 tests/golden/defaults/talos-backup/talos-backup/00_namespace.yaml create mode 100644 tests/golden/defaults/talos-backup/talos-backup/10_talos_serviceaccount.yaml create mode 100644 tests/golden/defaults/talos-backup/talos-backup/20_cronjob.yaml create mode 100644 tests/golden/resources/talos-backup/apps/talos-backup.yaml create mode 100644 tests/golden/resources/talos-backup/talos-backup/00_namespace.yaml create mode 100644 tests/golden/resources/talos-backup/talos-backup/10_s3_credentials.yaml create mode 100644 tests/golden/resources/talos-backup/talos-backup/10_talos_serviceaccount.yaml create mode 100644 tests/golden/resources/talos-backup/talos-backup/20_cronjob.yaml create mode 100644 tests/resources.yml diff --git a/Makefile.vars.mk b/Makefile.vars.mk index 3e911bb..dba1054 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/resources.yml diff --git a/class/defaults.yml b/class/defaults.yml index 767ce04..a174aef 100644 --- a/class/defaults.yml +++ b/class/defaults.yml @@ -2,4 +2,66 @@ parameters: talos_backup: =_metadata: multi_tenant: true - namespace: syn-talos-backup + + namespace: + name: syn-talos-backup + labels: {} + annotations: {} + + images: + talos_backup: + registry: ghcr.io + repository: siderolabs/talos-backup + # Pinned to a git-describe tag of main HEAD until upstream cuts a + # stable release. Tagged v0.1.0-beta.* releases lack zstd, + # path-style, and multi-recipient age. Bump manually after watching + # https://github.com/siderolabs/talos-backup/releases — Renovate is + # disabled for this image (see renovate.json). + tag: v0.1.0-beta.3-10-gb9fd478 + pull_policy: IfNotPresent + + schedule: "0 */6 * * *" + successful_jobs_history_limit: 3 + failed_jobs_history_limit: 1 + concurrency_policy: Forbid + + # Talos API ServiceAccount (talos.dev/v1alpha1) granting os:etcd:backup. + # Requires kubernetesTalosAPIAccess machineconfig allow-listing the + # deploy namespace and the os:etcd:backup role. + talos_service_account: + name: talos-backup-secrets + + s3: + bucket: "" + region: us-east-1 + endpoint: "" + use_path_style: false + prefix: "" + credentials: + create: false + name: talos-backup-s3 + access_key_id: "" + secret_access_key: "" + + # Optional. Falls back to the talosconfig context name when empty. + cluster_name: "" + + # Required. List of age public keys to encrypt the snapshot for. + age_recipient_public_keys: [] + + enable_compression: false + + # Extra env vars appended to the container. + extra_env: {} + + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + + node_selector: {} + tolerations: [] + affinity: {} diff --git a/component/app.jsonnet b/component/app.jsonnet index 7c8803d..72616cf 100644 --- a/component/app.jsonnet +++ b/component/app.jsonnet @@ -3,7 +3,15 @@ local inv = kap.inventory(); local params = inv.parameters.talos_backup; local argocd = import 'lib/argocd.libjsonnet'; -local app = argocd.App('talos-backup', params.namespace); +local app = argocd.App('talos-backup', params.namespace.name) { + spec+: { + syncPolicy+: { + syncOptions+: [ + 'ServerSideApply=true', + ], + }, + }, +}; local appPath = local project = std.get(std.get(app, 'spec', {}), 'project', 'syn'); diff --git a/component/main.jsonnet b/component/main.jsonnet index 7ae6105..8e99570 100644 --- a/component/main.jsonnet +++ b/component/main.jsonnet @@ -1,10 +1,68 @@ // main template for talos-backup +local com = import 'lib/commodore.libjsonnet'; local kap = import 'lib/kapitan.libjsonnet'; local kube = import 'lib/kube.libjsonnet'; +local lib = import 'lib/talos-backup.libsonnet'; local inv = kap.inventory(); -// The hiera parameters for the component local params = inv.parameters.talos_backup; -// Define outputs below +local componentName = 'talos-backup'; + +assert std.length(params.age_recipient_public_keys) > 0 : + 'talos_backup: age_recipient_public_keys must contain at least one public key'; +assert params.s3.bucket != '' : 'talos_backup: s3.bucket must be set'; +assert !params.s3.credentials.create + || (params.s3.credentials.access_key_id != '' + && params.s3.credentials.secret_access_key != '') : + 'talos_backup: s3.credentials.create=true requires access_key_id and secret_access_key'; + +local commonLabels = { + 'app.kubernetes.io/component': componentName, + 'app.kubernetes.io/managed-by': 'commodore', + 'app.kubernetes.io/part-of': 'syn', +}; + +local commonMetadata = { + labels+: commonLabels, +}; + +local namespace = kube.Namespace(params.namespace.name) { + metadata: commonMetadata + com.makeMergeable({ + name: params.namespace.name, + annotations: params.namespace.annotations, + labels: params.namespace.labels { + 'app.kubernetes.io/name': params.namespace.name, + }, + }), +}; + +local talosServiceAccount = lib.TalosServiceAccount() { + metadata: commonMetadata + com.makeMergeable({ + name: params.talos_service_account.name, + namespace: params.namespace.name, + labels: { 'app.kubernetes.io/name': params.talos_service_account.name }, + }), +}; + +local cronjob = lib.CronJob(params) { + metadata: commonMetadata + com.makeMergeable({ + name: componentName, + namespace: params.namespace.name, + labels: { 'app.kubernetes.io/name': componentName }, + }), +}; + +local s3Secret = lib.S3CredentialsSecret(params) { + metadata: commonMetadata + com.makeMergeable({ + name: params.s3.credentials.name, + namespace: params.namespace.name, + labels: { 'app.kubernetes.io/name': params.s3.credentials.name }, + }), +}; + { + '00_namespace': namespace, + '10_talos_serviceaccount': talosServiceAccount, + [if params.s3.credentials.create then '10_s3_credentials']: s3Secret, + '20_cronjob': cronjob, } diff --git a/docs/modules/ROOT/pages/how-tos/.gitkeep b/docs/modules/ROOT/pages/how-tos/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 503c8ea..f403058 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -1,5 +1,36 @@ -= Talos Linux aware etcd snapshotter. += talos-backup -talos-backup is a Commodore component to manage Talos Linux aware etcd snapshotter.. +talos-backup is a Commodore component that deploys +https://github.com/siderolabs/talos-backup[siderolabs/talos-backup] +as a Kubernetes `CronJob` on a Talos Linux cluster. +It periodically snapshots etcd through the Talos API, encrypts the snapshot +with https://age-encryption.org[age], and pushes it to an S3-compatible +object store. -See the xref:references/parameters.adoc[parameters] reference for further details. +== Prerequisites + +The Talos machine configuration must allow the Kubernetes-side Talos API +access for the `os:etcd:backup` role in the namespace where the component +is deployed: + +[source,yaml] +---- +machine: + features: + kubernetesTalosAPIAccess: + enabled: true + allowedRoles: + - os:etcd:backup + allowedKubernetesNamespaces: + - syn-talos-backup +---- + +You also need: + +* An age keypair (`age-keygen`). The public key(s) are passed to the + component; the private key is kept by the operator to decrypt backups. +* An S3 bucket and credentials. A lifecycle policy on the bucket is the + recommended way to enforce retention. + +See the xref:references/parameters.adoc[parameters] reference for the full +list of configurable options. diff --git a/docs/modules/ROOT/pages/references/parameters.adoc b/docs/modules/ROOT/pages/references/parameters.adoc index 9cd0bba..673e8c7 100644 --- a/docs/modules/ROOT/pages/references/parameters.adoc +++ b/docs/modules/ROOT/pages/references/parameters.adoc @@ -4,16 +4,219 @@ The parent key for all of the following parameters is `talos_backup`. == `namespace` +[horizontal] +type:: object + +Namespace in which the component is deployed. + +`namespace.name`:: Namespace name. Default: `syn-talos-backup`. +`namespace.labels`:: Additional namespace labels. Default: `{}`. +`namespace.annotations`:: Additional namespace annotations. Default: `{}`. + +NOTE: The Talos machine configuration must include this namespace in +`kubernetesTalosAPIAccess.allowedKubernetesNamespaces`. + + +== `images.talos_backup` + +[horizontal] +type:: object + +Container image for the `talos-backup` binary. + +`registry`:: Default: `ghcr.io`. +`repository`:: Default: `siderolabs/talos-backup`. +`tag`:: Default: `v0.1.0-beta.3-10-gb9fd478` (post-release main build; needed for multi-recipient age, zstd compression, and S3 path-style support). +`pull_policy`:: Default: `IfNotPresent`. + + +== `schedule` + +[horizontal] +type:: string +default:: `0 */6 * * *` + +Cron schedule for the backup job. + + +== `successful_jobs_history_limit` + +[horizontal] +type:: integer +default:: `3` + + +== `failed_jobs_history_limit` + +[horizontal] +type:: integer +default:: `1` + + +== `concurrency_policy` + +[horizontal] +type:: string +default:: `Forbid` + +`CronJob.spec.concurrencyPolicy`. Valid values: `Allow`, `Forbid`, `Replace`. + + +== `talos_service_account.name` + +[horizontal] +type:: string +default:: `talos-backup-secrets` + +Name of the `talos.dev/v1alpha1` `ServiceAccount` object created by the +component and consumed by the pod to authenticate against the Talos API. +The Talos SA controller projects a Secret of the same name into the +namespace, which the CronJob mounts at `/var/run/secrets/talos.dev`. + +No Kubernetes `v1` `ServiceAccount` is created — the pod runs with +`automountServiceAccountToken: false` since it does not call the +Kubernetes API. + + +== `s3.bucket` + +[horizontal] +type:: string +default:: `''` + +S3 bucket that receives the backups. Required. + + +== `s3.region` + +[horizontal] +type:: string +default:: `us-east-1` + + +== `s3.endpoint` + +[horizontal] +type:: string +default:: `''` + +Custom S3 endpoint for S3-compatible providers (MinIO, cloudscale, +exoscale, etc.). Leave empty to use the AWS default endpoints. + + +== `s3.use_path_style` + +[horizontal] +type:: boolean +default:: `false` + +Set to `true` for endpoints that require path-style bucket addressing. + + +== `s3.prefix` + [horizontal] type:: string -default:: `syn-talos-backup` +default:: `''` + +Object key prefix inside the bucket. Falls back to the cluster name when +empty. + + +== `s3.credentials` + +[horizontal] +type:: object + +S3 access credentials. + +`create`:: When `true`, the component renders a `Secret` from + `access_key_id` and `secret_access_key`. When `false`, the `CronJob` + references an existing `Secret` by `name`. Default: `false`. +`name`:: Secret name. Default: `talos-backup-s3`. +`access_key_id`:: Only used when `create: true`. +`secret_access_key`:: Only used when `create: true`. Should be sourced + from a secret backend (Vault, SOPS). + +[IMPORTANT] +==== +When `credentials.create` is `false`, an existing `Secret` named according +to `credentials.name` must already exist in the deploy namespace and must +contain the keys `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. The +`CronJob` reads them via `secretKeyRef`. +==== + + +== `cluster_name` + +[horizontal] +type:: string +default:: `''` + +Passed as `CLUSTER_NAME`. If empty, `talos-backup` falls back to the +talosconfig context name. + + +== `age_recipient_public_keys` + +[horizontal] +type:: list of strings +default:: `[]` + +age public keys used to encrypt the etcd snapshot. At least one entry is +required. Multiple recipients are supported. + + +== `enable_compression` + +[horizontal] +type:: boolean +default:: `false` + +Compress the etcd snapshot with zstd before encryption. + + +== `extra_env` + +[horizontal] +type:: object +default:: `{}` + +Additional environment variables to inject into the container. Keys are +variable names, values are stringified. + + +== `resources` + +[horizontal] +type:: object + +`CronJob` container resource requests and limits. + + +== `node_selector`, `tolerations`, `affinity` -The namespace in which to deploy this component. +Pod scheduling hints. All default to empty. == Example [source,yaml] ---- -namespace: example-namespace +parameters: + talos_backup: + schedule: '0 */4 * * *' + s3: + bucket: my-talos-backups + region: eu-west-1 + endpoint: https://objects.example.com + use_path_style: true + credentials: + create: true + access_key_id: ?{vaultkv:.../s3-access-key} + secret_access_key: ?{vaultkv:.../s3-secret-key} + age_recipient_public_keys: + - age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec + cluster_name: ${cluster:name} + enable_compression: true ---- diff --git a/lib/talos-backup.libsonnet b/lib/talos-backup.libsonnet new file mode 100644 index 0000000..228048f --- /dev/null +++ b/lib/talos-backup.libsonnet @@ -0,0 +1,121 @@ +/** + * Library with public helper methods provided by component talos-backup. + */ + +local kube = import 'lib/kube.libjsonnet'; + +local talosApiGroup = 'talos.dev'; + +local TalosServiceAccount() = { + apiVersion: '%s/v1alpha1' % talosApiGroup, + kind: 'ServiceAccount', + spec: { + roles: [ 'os:etcd:backup' ], + }, +}; + +local S3CredentialsSecret(params) = kube.Secret(params.s3.credentials.name) { + stringData: { + AWS_ACCESS_KEY_ID: params.s3.credentials.access_key_id, + AWS_SECRET_ACCESS_KEY: params.s3.credentials.secret_access_key, + }, +}; + +local image(params) = + local i = params.images.talos_backup; + '%s/%s:%s' % [ i.registry, i.repository, i.tag ]; + +local envFromParams(params) = + local fromSecret(k) = { + name: k, + valueFrom: { + secretKeyRef: { + name: params.s3.credentials.name, + key: k, + }, + }, + }; + local kv(k, v) = { name: k, value: std.toString(v) }; + local optional(k, v) = if v != '' && v != null then [ kv(k, v) ] else []; + [ + fromSecret('AWS_ACCESS_KEY_ID'), + fromSecret('AWS_SECRET_ACCESS_KEY'), + kv('AWS_REGION', params.s3.region), + kv('BUCKET', params.s3.bucket), + kv('USE_PATH_STYLE', params.s3.use_path_style), + kv('ENABLE_COMPRESSION', params.enable_compression), + // Set both env vars to the same value: upstream Split("", ",") returns + // [""] from an unset var, poisoning the recipient list. Duplicates harmless. + kv('AGE_RECIPIENT_PUBLIC_KEY', std.join(',', params.age_recipient_public_keys)), + kv('AGE_X25519_PUBLIC_KEY', std.join(',', params.age_recipient_public_keys)), + ] + + optional('CUSTOM_S3_ENDPOINT', params.s3.endpoint) + + optional('S3_PREFIX', params.s3.prefix) + + optional('CLUSTER_NAME', params.cluster_name) + + [ kv(k, params.extra_env[k]) for k in std.objectFields(params.extra_env) ]; + +local CronJob(params) = { + apiVersion: 'batch/v1', + kind: 'CronJob', + spec: { + schedule: params.schedule, + concurrencyPolicy: params.concurrency_policy, + successfulJobsHistoryLimit: params.successful_jobs_history_limit, + failedJobsHistoryLimit: params.failed_jobs_history_limit, + jobTemplate: { + spec: { + template: { + spec: { + restartPolicy: 'OnFailure', + automountServiceAccountToken: false, + securityContext: { + runAsNonRoot: true, + runAsUser: 1000, + runAsGroup: 1000, + fsGroup: 1000, + seccompProfile: { type: 'RuntimeDefault' }, + }, + [if params.node_selector != {} then 'nodeSelector']: params.node_selector, + [if params.tolerations != [] then 'tolerations']: params.tolerations, + [if params.affinity != {} then 'affinity']: params.affinity, + containers: [ { + name: 'talos-backup', + image: image(params), + imagePullPolicy: params.images.talos_backup.pull_policy, + workingDir: '/tmp', + command: [ '/talos-backup' ], + env: envFromParams(params), + resources: params.resources, + securityContext: { + allowPrivilegeEscalation: false, + readOnlyRootFilesystem: true, + capabilities: { drop: [ 'ALL' ] }, + }, + volumeMounts: [ + { mountPath: '/tmp', name: 'tmp' }, + { mountPath: '/.talos', name: 'talos' }, + { mountPath: '/var/run/secrets/talos.dev', name: 'talos-secrets' }, + ], + } ], + volumes: [ + { name: 'tmp', emptyDir: {} }, + { name: 'talos', emptyDir: {} }, + { + name: 'talos-secrets', + secret: { secretName: params.talos_service_account.name }, + }, + ], + }, + }, + }, + }, + }, +}; + +{ + TalosServiceAccount: TalosServiceAccount, + S3CredentialsSecret: S3CredentialsSecret, + CronJob: CronJob, + + talosApiGroup: talosApiGroup, +} diff --git a/tests/defaults.yml b/tests/defaults.yml index a4da5b7..40b8d3e 100644 --- a/tests/defaults.yml +++ b/tests/defaults.yml @@ -1,3 +1,7 @@ -# Overwrite parameters here - -# parameters: {...} +parameters: + talos_backup: + s3: + bucket: example-talos-backups + region: us-east-1 + age_recipient_public_keys: + - age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec diff --git a/tests/golden/defaults/talos-backup/apps/talos-backup.yaml b/tests/golden/defaults/talos-backup/apps/talos-backup.yaml index e69de29..6825b97 100644 --- a/tests/golden/defaults/talos-backup/apps/talos-backup.yaml +++ b/tests/golden/defaults/talos-backup/apps/talos-backup.yaml @@ -0,0 +1,4 @@ +spec: + syncPolicy: + syncOptions: + - ServerSideApply=true diff --git a/tests/golden/defaults/talos-backup/talos-backup/00_namespace.yaml b/tests/golden/defaults/talos-backup/talos-backup/00_namespace.yaml new file mode 100644 index 0000000..9dc16a2 --- /dev/null +++ b/tests/golden/defaults/talos-backup/talos-backup/00_namespace.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: syn-talos-backup + app.kubernetes.io/part-of: syn + name: syn-talos-backup diff --git a/tests/golden/defaults/talos-backup/talos-backup/10_talos_serviceaccount.yaml b/tests/golden/defaults/talos-backup/talos-backup/10_talos_serviceaccount.yaml new file mode 100644 index 0000000..5ba9e92 --- /dev/null +++ b/tests/golden/defaults/talos-backup/talos-backup/10_talos_serviceaccount.yaml @@ -0,0 +1,13 @@ +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: talos-backup-secrets + app.kubernetes.io/part-of: syn + name: talos-backup-secrets + namespace: syn-talos-backup +spec: + roles: + - os:etcd:backup diff --git a/tests/golden/defaults/talos-backup/talos-backup/20_cronjob.yaml b/tests/golden/defaults/talos-backup/talos-backup/20_cronjob.yaml new file mode 100644 index 0000000..da570a8 --- /dev/null +++ b/tests/golden/defaults/talos-backup/talos-backup/20_cronjob.yaml @@ -0,0 +1,86 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: talos-backup + app.kubernetes.io/part-of: syn + name: talos-backup + namespace: syn-talos-backup +spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + spec: + automountServiceAccountToken: false + containers: + - command: + - /talos-backup + env: + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + key: AWS_ACCESS_KEY_ID + name: talos-backup-s3 + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + key: AWS_SECRET_ACCESS_KEY + name: talos-backup-s3 + - name: AWS_REGION + value: us-east-1 + - name: BUCKET + value: example-talos-backups + - name: USE_PATH_STYLE + value: 'false' + - name: ENABLE_COMPRESSION + value: 'false' + - name: AGE_RECIPIENT_PUBLIC_KEY + value: age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec + - name: AGE_X25519_PUBLIC_KEY + value: age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec + image: ghcr.io/siderolabs/talos-backup:v0.1.0-beta.3-10-gb9fd478 + imagePullPolicy: IfNotPresent + name: talos-backup + resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /tmp + name: tmp + - mountPath: /.talos + name: talos + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + workingDir: /tmp + restartPolicy: OnFailure + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumes: + - emptyDir: {} + name: tmp + - emptyDir: {} + name: talos + - name: talos-secrets + secret: + secretName: talos-backup-secrets + schedule: 0 */6 * * * + successfulJobsHistoryLimit: 3 diff --git a/tests/golden/resources/talos-backup/apps/talos-backup.yaml b/tests/golden/resources/talos-backup/apps/talos-backup.yaml new file mode 100644 index 0000000..6825b97 --- /dev/null +++ b/tests/golden/resources/talos-backup/apps/talos-backup.yaml @@ -0,0 +1,4 @@ +spec: + syncPolicy: + syncOptions: + - ServerSideApply=true diff --git a/tests/golden/resources/talos-backup/talos-backup/00_namespace.yaml b/tests/golden/resources/talos-backup/talos-backup/00_namespace.yaml new file mode 100644 index 0000000..9dc16a2 --- /dev/null +++ b/tests/golden/resources/talos-backup/talos-backup/00_namespace.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Namespace +metadata: + annotations: {} + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: syn-talos-backup + app.kubernetes.io/part-of: syn + name: syn-talos-backup diff --git a/tests/golden/resources/talos-backup/talos-backup/10_s3_credentials.yaml b/tests/golden/resources/talos-backup/talos-backup/10_s3_credentials.yaml new file mode 100644 index 0000000..027907b --- /dev/null +++ b/tests/golden/resources/talos-backup/talos-backup/10_s3_credentials.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +data: {} +kind: Secret +metadata: + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: talos-backup-s3 + app.kubernetes.io/part-of: syn + name: talos-backup-s3 + namespace: syn-talos-backup +stringData: + AWS_ACCESS_KEY_ID: examplekey + AWS_SECRET_ACCESS_KEY: examplesecretvalue +type: Opaque diff --git a/tests/golden/resources/talos-backup/talos-backup/10_talos_serviceaccount.yaml b/tests/golden/resources/talos-backup/talos-backup/10_talos_serviceaccount.yaml new file mode 100644 index 0000000..5ba9e92 --- /dev/null +++ b/tests/golden/resources/talos-backup/talos-backup/10_talos_serviceaccount.yaml @@ -0,0 +1,13 @@ +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: talos-backup-secrets + app.kubernetes.io/part-of: syn + name: talos-backup-secrets + namespace: syn-talos-backup +spec: + roles: + - os:etcd:backup diff --git a/tests/golden/resources/talos-backup/talos-backup/20_cronjob.yaml b/tests/golden/resources/talos-backup/talos-backup/20_cronjob.yaml new file mode 100644 index 0000000..bd2c13d --- /dev/null +++ b/tests/golden/resources/talos-backup/talos-backup/20_cronjob.yaml @@ -0,0 +1,100 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + labels: + app.kubernetes.io/component: talos-backup + app.kubernetes.io/managed-by: commodore + app.kubernetes.io/name: talos-backup + app.kubernetes.io/part-of: syn + name: talos-backup + namespace: syn-talos-backup +spec: + concurrencyPolicy: Replace + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + spec: + automountServiceAccountToken: false + containers: + - command: + - /talos-backup + env: + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + key: AWS_ACCESS_KEY_ID + name: talos-backup-s3 + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + key: AWS_SECRET_ACCESS_KEY + name: talos-backup-s3 + - name: AWS_REGION + value: lpg + - name: BUCKET + value: example-talos-backups + - name: USE_PATH_STYLE + value: 'true' + - name: ENABLE_COMPRESSION + value: 'true' + - name: AGE_RECIPIENT_PUBLIC_KEY + value: age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec,age1majtfe4q0u030xcjg0ent45stsfev28fjwy0saxqw4c3x85gge5s5gtkwx + - name: AGE_X25519_PUBLIC_KEY + value: age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec,age1majtfe4q0u030xcjg0ent45stsfev28fjwy0saxqw4c3x85gge5s5gtkwx + - name: CUSTOM_S3_ENDPOINT + value: https://objects.example.com + - name: S3_PREFIX + value: prod-cluster + - name: CLUSTER_NAME + value: prod-cluster + - name: AWS_EC2_METADATA_DISABLED + value: 'true' + image: ghcr.io/siderolabs/talos-backup:v0.1.0-beta.3-10-gb9fd478 + imagePullPolicy: IfNotPresent + name: talos-backup + resources: + limits: + cpu: '1' + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /tmp + name: tmp + - mountPath: /.talos + name: talos + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + workingDir: /tmp + nodeSelector: + kubernetes.io/os: linux + restartPolicy: OnFailure + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + volumes: + - emptyDir: {} + name: tmp + - emptyDir: {} + name: talos + - name: talos-secrets + secret: + secretName: talos-backup-secrets + schedule: '*/30 * * * *' + successfulJobsHistoryLimit: 5 diff --git a/tests/resources.yml b/tests/resources.yml new file mode 100644 index 0000000..f192e06 --- /dev/null +++ b/tests/resources.yml @@ -0,0 +1,36 @@ +parameters: + talos_backup: + schedule: '*/30 * * * *' + concurrency_policy: Replace + successful_jobs_history_limit: 5 + failed_jobs_history_limit: 3 + s3: + bucket: example-talos-backups + region: lpg + endpoint: https://objects.example.com + use_path_style: true + prefix: prod-cluster + credentials: + create: true + access_key_id: examplekey + secret_access_key: examplesecretvalue + cluster_name: prod-cluster + age_recipient_public_keys: + - age1khpnnl86pzx96ttyjmldptsl5yn2v9jgmmzcjcufvk00ttkph9zs0ytgec + - age1majtfe4q0u030xcjg0ent45stsfev28fjwy0saxqw4c3x85gge5s5gtkwx + enable_compression: true + extra_env: + AWS_EC2_METADATA_DISABLED: 'true' + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: '1' + memory: 512Mi + node_selector: + kubernetes.io/os: linux + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule From 618d2f172b1a065adae9dbc34b6b2ec86215b1b8 Mon Sep 17 00:00:00 2001 From: Marco De Luca Date: Fri, 29 May 2026 10:23:20 +0200 Subject: [PATCH 2/3] Apply review feedback --- component/main.jsonnet | 21 ++++--- {lib => component}/talos-backup.libsonnet | 0 .../ROOT/pages/references/parameters.adoc | 56 +++++++++---------- 3 files changed, 37 insertions(+), 40 deletions(-) rename {lib => component}/talos-backup.libsonnet (100%) diff --git a/component/main.jsonnet b/component/main.jsonnet index 8e99570..1dc0cb4 100644 --- a/component/main.jsonnet +++ b/component/main.jsonnet @@ -2,19 +2,22 @@ local com = import 'lib/commodore.libjsonnet'; local kap = import 'lib/kapitan.libjsonnet'; local kube = import 'lib/kube.libjsonnet'; -local lib = import 'lib/talos-backup.libsonnet'; +local lib = import 'talos-backup.libsonnet'; local inv = kap.inventory(); local params = inv.parameters.talos_backup; -local componentName = 'talos-backup'; +local componentName = inv.parameters._instance; -assert std.length(params.age_recipient_public_keys) > 0 : - 'talos_backup: age_recipient_public_keys must contain at least one public key'; -assert params.s3.bucket != '' : 'talos_backup: s3.bucket must be set'; -assert !params.s3.credentials.create - || (params.s3.credentials.access_key_id != '' - && params.s3.credentials.secret_access_key != '') : - 'talos_backup: s3.credentials.create=true requires access_key_id and secret_access_key'; +assert + std.length(params.age_recipient_public_keys) > 0 + : 'talos_backup: age_recipient_public_keys must contain at least one public key'; +assert + params.s3.bucket != '' + : 'talos_backup: s3.bucket must be set'; +assert + !params.s3.credentials.create + || (params.s3.credentials.access_key_id != '' && params.s3.credentials.secret_access_key != '') + : 'talos_backup: s3.credentials.create=true requires access_key_id and secret_access_key'; local commonLabels = { 'app.kubernetes.io/component': componentName, diff --git a/lib/talos-backup.libsonnet b/component/talos-backup.libsonnet similarity index 100% rename from lib/talos-backup.libsonnet rename to component/talos-backup.libsonnet diff --git a/docs/modules/ROOT/pages/references/parameters.adoc b/docs/modules/ROOT/pages/references/parameters.adoc index 673e8c7..f25e45d 100644 --- a/docs/modules/ROOT/pages/references/parameters.adoc +++ b/docs/modules/ROOT/pages/references/parameters.adoc @@ -13,8 +13,7 @@ Namespace in which the component is deployed. `namespace.labels`:: Additional namespace labels. Default: `{}`. `namespace.annotations`:: Additional namespace annotations. Default: `{}`. -NOTE: The Talos machine configuration must include this namespace in -`kubernetesTalosAPIAccess.allowedKubernetesNamespaces`. +NOTE: The Talos machine configuration must include this namespace in `kubernetesTalosAPIAccess.allowedKubernetesNamespaces`. == `images.talos_backup` @@ -59,7 +58,8 @@ default:: `1` type:: string default:: `Forbid` -`CronJob.spec.concurrencyPolicy`. Valid values: `Allow`, `Forbid`, `Replace`. +`CronJob.spec.concurrencyPolicy`. +Valid values: `Allow`, `Forbid`, `Replace`. == `talos_service_account.name` @@ -68,14 +68,10 @@ default:: `Forbid` type:: string default:: `talos-backup-secrets` -Name of the `talos.dev/v1alpha1` `ServiceAccount` object created by the -component and consumed by the pod to authenticate against the Talos API. -The Talos SA controller projects a Secret of the same name into the -namespace, which the CronJob mounts at `/var/run/secrets/talos.dev`. +Name of the `talos.dev/v1alpha1` `ServiceAccount` object created by the component and consumed by the pod to authenticate against the Talos API. +The Talos SA controller projects a Secret of the same name into the namespace, which the CronJob mounts at `/var/run/secrets/talos.dev`. -No Kubernetes `v1` `ServiceAccount` is created — the pod runs with -`automountServiceAccountToken: false` since it does not call the -Kubernetes API. +No Kubernetes `v1` `ServiceAccount` is created — the pod runs with `automountServiceAccountToken: false` since it does not call the Kubernetes API. == `s3.bucket` @@ -84,7 +80,8 @@ Kubernetes API. type:: string default:: `''` -S3 bucket that receives the backups. Required. +S3 bucket that receives the backups. +Required. == `s3.region` @@ -100,8 +97,8 @@ default:: `us-east-1` type:: string default:: `''` -Custom S3 endpoint for S3-compatible providers (MinIO, cloudscale, -exoscale, etc.). Leave empty to use the AWS default endpoints. +Custom S3 endpoint for S3-compatible providers (MinIO, cloudscale, exoscale, etc.). +Leave empty to use the AWS default endpoints. == `s3.use_path_style` @@ -119,8 +116,8 @@ Set to `true` for endpoints that require path-style bucket addressing. type:: string default:: `''` -Object key prefix inside the bucket. Falls back to the cluster name when -empty. +Object key prefix inside the bucket. +Falls back to the cluster name when empty. == `s3.credentials` @@ -130,20 +127,15 @@ type:: object S3 access credentials. -`create`:: When `true`, the component renders a `Secret` from - `access_key_id` and `secret_access_key`. When `false`, the `CronJob` - references an existing `Secret` by `name`. Default: `false`. +`create`:: When `true`, the component renders a `Secret` from `access_key_id` and `secret_access_key`. When `false`, the `CronJob` references an existing `Secret` by `name`. Default: `false`. `name`:: Secret name. Default: `talos-backup-s3`. `access_key_id`:: Only used when `create: true`. -`secret_access_key`:: Only used when `create: true`. Should be sourced - from a secret backend (Vault, SOPS). +`secret_access_key`:: Only used when `create: true`. Should be sourced from a secret backend (Vault, SOPS). [IMPORTANT] ==== -When `credentials.create` is `false`, an existing `Secret` named according -to `credentials.name` must already exist in the deploy namespace and must -contain the keys `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. The -`CronJob` reads them via `secretKeyRef`. +When `credentials.create` is `false`, an existing `Secret` named according to `credentials.name` must already exist in the deploy namespace and must contain the keys `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. +The `CronJob` reads them via `secretKeyRef`. ==== @@ -153,8 +145,8 @@ contain the keys `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. The type:: string default:: `''` -Passed as `CLUSTER_NAME`. If empty, `talos-backup` falls back to the -talosconfig context name. +Passed as `CLUSTER_NAME`. +If empty, `talos-backup` falls back to the talosconfig context name. == `age_recipient_public_keys` @@ -163,8 +155,9 @@ talosconfig context name. type:: list of strings default:: `[]` -age public keys used to encrypt the etcd snapshot. At least one entry is -required. Multiple recipients are supported. +age public keys used to encrypt the etcd snapshot. +At least one entry is required. +Multiple recipients are supported. == `enable_compression` @@ -182,8 +175,8 @@ Compress the etcd snapshot with zstd before encryption. type:: object default:: `{}` -Additional environment variables to inject into the container. Keys are -variable names, values are stringified. +Additional environment variables to inject into the container. +Keys are variable names, values are stringified. == `resources` @@ -196,7 +189,8 @@ type:: object == `node_selector`, `tolerations`, `affinity` -Pod scheduling hints. All default to empty. +Pod scheduling hints. +All default to empty. == Example From 465168ce5526da6be1deae9abdfd263eda0314e8 Mon Sep 17 00:00:00 2001 From: Marco De Luca Date: Fri, 29 May 2026 11:21:41 +0200 Subject: [PATCH 3/3] Small improvements --- component/talos-backup.libsonnet | 3 +-- .../ROOT/pages/references/parameters.adoc | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/component/talos-backup.libsonnet b/component/talos-backup.libsonnet index 228048f..282ec6c 100644 --- a/component/talos-backup.libsonnet +++ b/component/talos-backup.libsonnet @@ -22,8 +22,7 @@ local S3CredentialsSecret(params) = kube.Secret(params.s3.credentials.name) { }; local image(params) = - local i = params.images.talos_backup; - '%s/%s:%s' % [ i.registry, i.repository, i.tag ]; + '%(registry)s/%(repository)s:%(tag)s' % params.images.talos_backup; local envFromParams(params) = local fromSecret(k) = { diff --git a/docs/modules/ROOT/pages/references/parameters.adoc b/docs/modules/ROOT/pages/references/parameters.adoc index f25e45d..5cc08ca 100644 --- a/docs/modules/ROOT/pages/references/parameters.adoc +++ b/docs/modules/ROOT/pages/references/parameters.adoc @@ -127,10 +127,18 @@ type:: object S3 access credentials. -`create`:: When `true`, the component renders a `Secret` from `access_key_id` and `secret_access_key`. When `false`, the `CronJob` references an existing `Secret` by `name`. Default: `false`. -`name`:: Secret name. Default: `talos-backup-s3`. -`access_key_id`:: Only used when `create: true`. -`secret_access_key`:: Only used when `create: true`. Should be sourced from a secret backend (Vault, SOPS). +`create`:: +When `true`, the component renders a `Secret` from `access_key_id` and `secret_access_key`. +When `false`, the `CronJob` references an existing `Secret` by `name`. +Default: `false`. +`name`:: +Secret name. +Default: `talos-backup-s3`. +`access_key_id`:: +Only used when `create: true`. +`secret_access_key`:: +Only used when `create: true`. +Should be sourced from a secret backend (Vault, SOPS). [IMPORTANT] ====