diff --git a/api/v1/backend.go b/api/v1/backend.go index f0488f515..63d9eaea7 100644 --- a/api/v1/backend.go +++ b/api/v1/backend.go @@ -1,11 +1,13 @@ package v1 import ( + "context" "fmt" "reflect" "strings" corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/k8up-io/k8up/v2/operator/cfg" ) @@ -71,6 +73,14 @@ func (in *Backend) String() string { return "" } +// ResolveSecretRefs resolves Secret-backed repository fields in place. +func (in *Backend) ResolveSecretRefs(ctx context.Context, reader client.Reader, namespace, secretName string) error { + if in == nil || in.S3 == nil { + return nil + } + return in.S3.ResolveSecretRefs(ctx, reader, namespace, secretName) +} + // IsBackendEqualTo returns true if the restic repository string is equal to the other's string. // If other is nil, it returns false. func (in *Backend) IsBackendEqualTo(other *Backend) bool { @@ -98,6 +108,14 @@ func addEnvVarFromSecret(vars map[string]*corev1.EnvVarSource, key string, ref * } } +func secretValue(secret *corev1.Secret, key, field string) (string, error) { + value, ok := secret.Data[key] + if !ok { + return "", fmt.Errorf("secret %s/%s has no key %q for %s", secret.Namespace, secret.Name, key, field) + } + return string(value), nil +} + type LocalSpec struct { MountPath string `json:"mountPath,omitempty"` } @@ -114,11 +132,47 @@ func (in *LocalSpec) String() string { type S3Spec struct { Endpoint string `json:"endpoint,omitempty"` + EndpointSecretKey string `json:"endpointSecretKey,omitempty"` Bucket string `json:"bucket,omitempty"` + BucketSecretKey string `json:"bucketSecretKey,omitempty"` AccessKeyIDSecretRef *corev1.SecretKeySelector `json:"accessKeyIDSecretRef,omitempty"` SecretAccessKeySecretRef *corev1.SecretKeySelector `json:"secretAccessKeySecretRef,omitempty"` } +// ResolveSecretRefs replaces configured endpoint and bucket Secret references +// with their values before the repository string is used to create a Job. +func (in *S3Spec) ResolveSecretRefs(ctx context.Context, reader client.Reader, namespace, secretName string) error { + if in == nil || (in.EndpointSecretKey == "" && in.BucketSecretKey == "") { + return nil + } + + secret := &corev1.Secret{} + if err := reader.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretName}, secret); err != nil { + return fmt.Errorf("get repository Secret %s/%s: %w", namespace, secretName, err) + } + + endpoint := in.Endpoint + if in.EndpointSecretKey != "" { + var err error + endpoint, err = secretValue(secret, in.EndpointSecretKey, "S3 endpoint") + if err != nil { + return err + } + } + bucket := in.Bucket + if in.BucketSecretKey != "" { + var err error + bucket, err = secretValue(secret, in.BucketSecretKey, "S3 bucket") + if err != nil { + return err + } + } + + in.Endpoint = endpoint + in.Bucket = bucket + return nil +} + // EnvVars returns the env vars for this backend. func (in *S3Spec) EnvVars(vars map[string]*corev1.EnvVarSource) map[string]*corev1.EnvVarSource { addEnvVarFromSecret(vars, cfg.AwsAccessKeyIDEnvName, in.AccessKeyIDSecretRef) diff --git a/api/v1/backend_test.go b/api/v1/backend_test.go index b4bc1f815..28e7d1bd5 100644 --- a/api/v1/backend_test.go +++ b/api/v1/backend_test.go @@ -1,10 +1,15 @@ package v1 import ( + "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/k8up-io/k8up/v2/operator/cfg" ) @@ -155,6 +160,32 @@ func Test_Backend_GetCredentialEnv(t *testing.T) { } } +func TestBackend_ResolveSecretRefs(t *testing.T) { + const ( + namespace = "backup" + secretName = "k8up-repo" + ) + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + reader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: secretName}, + Data: map[string][]byte{ + "endpoint": []byte("https://endpoint"), + "bucket": []byte("bucket"), + }, + }).Build() + + backend := &Backend{S3: &S3Spec{ + EndpointSecretKey: "endpoint", + BucketSecretKey: "bucket", + }} + + err := backend.ResolveSecretRefs(context.Background(), reader, namespace, secretName) + + require.NoError(t, err) + assert.Equal(t, "s3:https://endpoint/bucket", backend.String()) +} + func newSecretRef(name string) *corev1.SecretKeySelector { return &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ diff --git a/charts/k8up/Makefile b/charts/k8up/Makefile index d3e1f5ee2..92850d1a2 100644 --- a/charts/k8up/Makefile +++ b/charts/k8up/Makefile @@ -19,6 +19,7 @@ $(rbac_gen_tgt): @yq -i e '.metadata.name="{{ include \"k8up.fullname\" . }}-manager", del(.metadata.creationTimestamp)' $@ @yq -i e '.metadata.labels.replace="LABELS"' $@ @$(sed) -e 's/replace: LABELS/{{- include "k8up.labels" . | nindent 4 }}/g' $@ + @$(sed) -e 's/- k8up-repo/- {{ .Values.k8up.repositorySecretName | default "k8up-repo" }}/g' $@ @$(sed) -e '1s/^/{{- if .Values.rbac.create -}}\n/' $@ @$(sed) -e '$$a{{- end -}}' $@ diff --git a/charts/k8up/README.md b/charts/k8up/README.md index 1397ea1c6..db2772a30 100644 --- a/charts/k8up/README.md +++ b/charts/k8up/README.md @@ -61,6 +61,7 @@ Document your changes in values.yaml and let `make docs:helm` generate this sect | k8up.globalResources.requests.cpu | string | `""` | Global CPU resource requests applied to jobs. See [supported units][resource-units]. | | k8up.globalResources.requests.memory | string | `""` | Global Memory resource requests applied to jobs. See [supported units][resource-units]. | | k8up.operatorNamespace | string | `""` | Specifies the namespace in which K8up's `EffectiveSchedules` are stored. Defaults to release namespace if left empty. | +| k8up.repositorySecretName | string | `""` | Specifies the name of the Secret that contains repository configuration values (e.g. S3 endpoint and bucket). Defaults to `k8up-repo` if left empty. | | k8up.skipSnapshotSync | bool | `false` | Specifies whether K8up should skip synchronizing Snapshot custom resources to the cluster after backup or prune operations. When enabled, the operator passes `BACKUP_SKIP_SNAPSHOT_SYNC=true` to backup and prune job pods. Webhook notifications are still sent. | | k8up.skipWithoutAnnotation | bool | `false` | Specifies whether K8up should ignore PVCs without the backup annotation (by default, `k8up.io/backup`) | | k8up.timezone | string | `""` | Specifies the timezone K8up is using for scheduling. Empty value defaults to the timezone in which Kubernetes is deployed. Accepts `tz database` compatible entries, e.g. `Europe/Zurich` | diff --git a/charts/k8up/crds/k8up.io_archives.yaml b/charts/k8up/crds/k8up.io_archives.yaml index 52357d123..2644f1d88 100644 --- a/charts/k8up/crds/k8up.io_archives.yaml +++ b/charts/k8up/crds/k8up.io_archives.yaml @@ -383,8 +383,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -873,8 +877,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/charts/k8up/crds/k8up.io_backups.yaml b/charts/k8up/crds/k8up.io_backups.yaml index 3c6098328..9a03def0e 100644 --- a/charts/k8up/crds/k8up.io_backups.yaml +++ b/charts/k8up/crds/k8up.io_backups.yaml @@ -390,8 +390,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/charts/k8up/crds/k8up.io_checks.yaml b/charts/k8up/crds/k8up.io_checks.yaml index ab41ecc89..f6efe0112 100644 --- a/charts/k8up/crds/k8up.io_checks.yaml +++ b/charts/k8up/crds/k8up.io_checks.yaml @@ -385,8 +385,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/charts/k8up/crds/k8up.io_prunes.yaml b/charts/k8up/crds/k8up.io_prunes.yaml index 754c63d04..6eabf307e 100644 --- a/charts/k8up/crds/k8up.io_prunes.yaml +++ b/charts/k8up/crds/k8up.io_prunes.yaml @@ -385,8 +385,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/charts/k8up/crds/k8up.io_restores.yaml b/charts/k8up/crds/k8up.io_restores.yaml index 4950c0d04..7320c073e 100644 --- a/charts/k8up/crds/k8up.io_restores.yaml +++ b/charts/k8up/crds/k8up.io_restores.yaml @@ -385,8 +385,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -875,8 +879,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/charts/k8up/crds/k8up.io_schedules.yaml b/charts/k8up/crds/k8up.io_schedules.yaml index 25dfbae12..476f9e9f7 100644 --- a/charts/k8up/crds/k8up.io_schedules.yaml +++ b/charts/k8up/crds/k8up.io_schedules.yaml @@ -376,8 +376,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -868,8 +872,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -1486,8 +1494,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -1930,8 +1942,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -2950,8 +2966,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -4164,8 +4184,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -5204,8 +5228,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -5696,8 +5724,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/charts/k8up/templates/deployment.yaml b/charts/k8up/templates/deployment.yaml index 365818355..2a0dbbdcb 100644 --- a/charts/k8up/templates/deployment.yaml +++ b/charts/k8up/templates/deployment.yaml @@ -43,6 +43,8 @@ spec: value: "{{ .Values.k8up.skipWithoutAnnotation }}" - name: BACKUP_GLOBAL_SKIP_SNAPSHOT_SYNC value: "{{ .Values.k8up.skipSnapshotSync }}" + - name: BACKUP_REPOSITORY_SECRET_NAME + value: "{{ .Values.k8up.repositorySecretName | default "k8up-repo" }}" - name: BACKUP_OPERATOR_NAMESPACE {{- if .Values.k8up.operatorNamespace }} value: "{{ .Values.k8up.operatorNamespace }}" diff --git a/charts/k8up/templates/operator-clusterrole.yaml b/charts/k8up/templates/operator-clusterrole.yaml index 054fc67d6..966baa328 100644 --- a/charts/k8up/templates/operator-clusterrole.yaml +++ b/charts/k8up/templates/operator-clusterrole.yaml @@ -24,6 +24,14 @@ rules: - get - list - watch + - apiGroups: + - "" + resourceNames: + - {{ .Values.k8up.repositorySecretName | default "k8up-repo" }} + resources: + - secrets + verbs: + - get - apiGroups: - "" resources: diff --git a/charts/k8up/test/deployment_test.go b/charts/k8up/test/deployment_test.go index 929c38f3d..b9117e6b4 100644 --- a/charts/k8up/test/deployment_test.go +++ b/charts/k8up/test/deployment_test.go @@ -39,12 +39,14 @@ func Test_Deployment_ShouldRender_EnvironmentVariables(t *testing.T) { assert.Equal(t, wantTimezone, envs[1].Value) assert.Equal(t, "BACKUP_SKIP_WITHOUT_ANNOTATION", envs[3].Name) assert.Equal(t, "BACKUP_GLOBAL_SKIP_SNAPSHOT_SYNC", envs[4].Name) - assert.Equal(t, "BACKUP_OPERATOR_NAMESPACE", envs[5].Name) - assert.Equal(t, "metadata.namespace", envs[5].ValueFrom.FieldRef.FieldPath) - assert.Equal(t, "BACKUP_GLOBAL_CPU_REQUEST", envs[6].Name, "Deployment does not use configured Env Name") - assert.Equal(t, wantCpuRequest, envs[6].Value, "Deployment does not use configured Env Value") - assert.Equal(t, "VARIABLE", envs[7].Name, "Deployment does not use configured Env Name") - assert.Equal(t, "VALUE", envs[7].Value, "Deployment does not use configured Env Value") + assert.Equal(t, "BACKUP_REPOSITORY_SECRET_NAME", envs[5].Name) + assert.Equal(t, "k8up-repo", envs[5].Value) + assert.Equal(t, "BACKUP_OPERATOR_NAMESPACE", envs[6].Name) + assert.Equal(t, "metadata.namespace", envs[6].ValueFrom.FieldRef.FieldPath) + assert.Equal(t, "BACKUP_GLOBAL_CPU_REQUEST", envs[7].Name, "Deployment does not use configured Env Name") + assert.Equal(t, wantCpuRequest, envs[7].Value, "Deployment does not use configured Env Value") + assert.Equal(t, "VARIABLE", envs[8].Name, "Deployment does not use configured Env Name") + assert.Equal(t, "VALUE", envs[8].Value, "Deployment does not use configured Env Value") } func Test_Deployment_ShouldRender_Affinity(t *testing.T) { @@ -93,8 +95,8 @@ func Test_Deployment_ShouldRender_OverrideNamespace(t *testing.T) { got := renderDeployment(t, options, false) assert.Equal(t, overrideNamespace, got.Namespace, "Deployment should use the overridden namespace") - assert.Equal(t, "BACKUP_OPERATOR_NAMESPACE", got.Spec.Template.Spec.Containers[0].Env[4].Name) - assert.Equal(t, "metadata.namespace", got.Spec.Template.Spec.Containers[0].Env[4].ValueFrom.FieldRef.FieldPath) + assert.Equal(t, "BACKUP_OPERATOR_NAMESPACE", got.Spec.Template.Spec.Containers[0].Env[5].Name) + assert.Equal(t, "metadata.namespace", got.Spec.Template.Spec.Containers[0].Env[5].ValueFrom.FieldRef.FieldPath) } func Test_Deployment_ShouldRender_Resources(t *testing.T) { diff --git a/charts/k8up/values.yaml b/charts/k8up/values.yaml index bf468ac29..eefd330b2 100644 --- a/charts/k8up/values.yaml +++ b/charts/k8up/values.yaml @@ -60,6 +60,10 @@ k8up: # When enabled, the operator passes `BACKUP_SKIP_SNAPSHOT_SYNC=true` to backup and prune job pods. Webhook notifications are still sent. skipSnapshotSync: false + # -- Specifies the name of the Secret that contains repository configuration values (e.g. S3 endpoint and bucket). + # Defaults to `k8up-repo` if left empty. + repositorySecretName: "" + # -- Specifies the namespace in which K8up's `EffectiveSchedules` are stored. # Defaults to release namespace if left empty. operatorNamespace: "" diff --git a/cmd/operator/main.go b/cmd/operator/main.go index f18ba2e0c..17b8b096f 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -57,6 +57,7 @@ var ( &cli.StringFlag{Destination: &cfg.Config.GlobalRestoreS3SecretAccessKey, Name: "globalrestores3secretaccesskey", EnvVars: []string{"BACKUP_GLOBALRESTORES3SECRETACCESSKEY"}, Usage: "set the global restore S3 SecretAccessKey for restores"}, &cli.StringFlag{Destination: &cfg.Config.GlobalRepoPassword, Name: "globalrepopassword", EnvVars: []string{"BACKUP_GLOBALREPOPASSWORD"}, Usage: "set the restic repository password to be used globally"}, + &cli.StringFlag{Destination: &cfg.Config.RepositorySecretName, Name: "repository-secret-name", EnvVars: []string{"BACKUP_REPOSITORY_SECRET_NAME"}, Value: "k8up-repo", Usage: "set the name of the Secret that contains repository configuration values (e.g. S3 endpoint and bucket)"}, &cli.StringFlag{Destination: &cfg.Config.GlobalAccessKey, Name: "globalaccesskeyid", EnvVars: []string{"BACKUP_GLOBALACCESSKEYID"}, Usage: "set the S3 access key id to be used globally"}, &cli.StringFlag{Destination: &cfg.Config.GlobalSecretAccessKey, Name: "globalsecretaccesskey", EnvVars: []string{"BACKUP_GLOBALSECRETACCESSKEY"}, Usage: "set the S3 secret access key to be used globally"}, &cli.StringFlag{Destination: &cfg.Config.GlobalS3Bucket, Name: "globals3bucket", EnvVars: []string{"BACKUP_GLOBALS3BUCKET"}, Usage: "set the S3 bucket to be used globally"}, diff --git a/config/crd/apiextensions.k8s.io/v1/k8up.io_archives.yaml b/config/crd/apiextensions.k8s.io/v1/k8up.io_archives.yaml index 52357d123..2644f1d88 100644 --- a/config/crd/apiextensions.k8s.io/v1/k8up.io_archives.yaml +++ b/config/crd/apiextensions.k8s.io/v1/k8up.io_archives.yaml @@ -383,8 +383,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -873,8 +877,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/config/crd/apiextensions.k8s.io/v1/k8up.io_backups.yaml b/config/crd/apiextensions.k8s.io/v1/k8up.io_backups.yaml index 3c6098328..9a03def0e 100644 --- a/config/crd/apiextensions.k8s.io/v1/k8up.io_backups.yaml +++ b/config/crd/apiextensions.k8s.io/v1/k8up.io_backups.yaml @@ -390,8 +390,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/config/crd/apiextensions.k8s.io/v1/k8up.io_checks.yaml b/config/crd/apiextensions.k8s.io/v1/k8up.io_checks.yaml index ab41ecc89..f6efe0112 100644 --- a/config/crd/apiextensions.k8s.io/v1/k8up.io_checks.yaml +++ b/config/crd/apiextensions.k8s.io/v1/k8up.io_checks.yaml @@ -385,8 +385,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/config/crd/apiextensions.k8s.io/v1/k8up.io_prunes.yaml b/config/crd/apiextensions.k8s.io/v1/k8up.io_prunes.yaml index 754c63d04..6eabf307e 100644 --- a/config/crd/apiextensions.k8s.io/v1/k8up.io_prunes.yaml +++ b/config/crd/apiextensions.k8s.io/v1/k8up.io_prunes.yaml @@ -385,8 +385,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/config/crd/apiextensions.k8s.io/v1/k8up.io_restores.yaml b/config/crd/apiextensions.k8s.io/v1/k8up.io_restores.yaml index 4950c0d04..7320c073e 100644 --- a/config/crd/apiextensions.k8s.io/v1/k8up.io_restores.yaml +++ b/config/crd/apiextensions.k8s.io/v1/k8up.io_restores.yaml @@ -385,8 +385,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -875,8 +879,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/config/crd/apiextensions.k8s.io/v1/k8up.io_schedules.yaml b/config/crd/apiextensions.k8s.io/v1/k8up.io_schedules.yaml index 25dfbae12..476f9e9f7 100644 --- a/config/crd/apiextensions.k8s.io/v1/k8up.io_schedules.yaml +++ b/config/crd/apiextensions.k8s.io/v1/k8up.io_schedules.yaml @@ -376,8 +376,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -868,8 +872,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -1486,8 +1494,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -1930,8 +1942,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -2950,8 +2966,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -4164,8 +4184,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -5204,8 +5228,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: @@ -5696,8 +5724,12 @@ spec: x-kubernetes-map-type: atomic bucket: type: string + bucketSecretKey: + type: string endpoint: type: string + endpointSecretKey: + type: string secretAccessKeySecretRef: description: SecretKeySelector selects a key of a Secret. properties: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a7eeee568..9e4097695 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -21,6 +21,14 @@ rules: - get - list - watch +- apiGroups: + - "" + resourceNames: + - k8up-repo + resources: + - secrets + verbs: + - get - apiGroups: - "" resources: diff --git a/operator/archivecontroller/controller.go b/operator/archivecontroller/controller.go index 608f3d720..5c1c32d42 100644 --- a/operator/archivecontroller/controller.go +++ b/operator/archivecontroller/controller.go @@ -2,6 +2,7 @@ package archivecontroller import ( "context" + "fmt" "time" k8upv1 "github.com/k8up-io/k8up/v2/api/v1" @@ -16,6 +17,8 @@ import ( // ArchiveReconciler reconciles Archive objects type ArchiveReconciler struct { Kube client.Client + // KubeReader performs direct API reads so Secrets only require named GET permissions, not cache list/watch permissions. + KubeReader client.Reader } func (r *ArchiveReconciler) NewObject() *k8upv1.Archive { @@ -29,6 +32,17 @@ func (r *ArchiveReconciler) NewObjectList() *k8upv1.ArchiveList { func (r *ArchiveReconciler) Provision(ctx context.Context, obj *k8upv1.Archive) (controllerruntime.Result, error) { log := controllerruntime.LoggerFrom(ctx) + if obj.Spec.Backend != nil { + if err := obj.Spec.Backend.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return controllerruntime.Result{}, fmt.Errorf("resolve backend Secret references: %w", err) + } + } + if obj.Spec.RestoreMethod != nil { + if err := obj.Spec.RestoreMethod.S3.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return controllerruntime.Result{}, fmt.Errorf("resolve restore S3 Secret references: %w", err) + } + } + repository := cfg.Config.GetGlobalRepository() if obj.Spec.Backend != nil { repository = obj.Spec.Backend.String() diff --git a/operator/archivecontroller/executor.go b/operator/archivecontroller/executor.go index 32f8a7621..bede15485 100644 --- a/operator/archivecontroller/executor.go +++ b/operator/archivecontroller/executor.go @@ -116,8 +116,8 @@ func (a *ArchiveExecutor) setupEnvVars(ctx context.Context, archive *k8upv1.Arch for key, value := range archive.Spec.Backend.GetCredentialEnv() { vars.SetEnvVarSource(key, value) } - vars.SetString(cfg.ResticRepositoryEnvName, archive.Spec.Backend.String()) } + vars.SetString(cfg.ResticRepositoryEnvName, a.Repository) err := vars.Merge(executor.DefaultEnv(a.Obj.GetNamespace())) if err != nil { diff --git a/operator/archivecontroller/setup.go b/operator/archivecontroller/setup.go index 48271024d..e426043d4 100644 --- a/operator/archivecontroller/setup.go +++ b/operator/archivecontroller/setup.go @@ -16,7 +16,8 @@ import ( func SetupWithManager(mgr controllerruntime.Manager) error { name := "archive.k8up.io" r := reconciler.NewReconciler[*k8upv1.Archive, *k8upv1.ArchiveList](mgr.GetClient(), &ArchiveReconciler{ - Kube: mgr.GetClient(), + Kube: mgr.GetClient(), + KubeReader: mgr.GetAPIReader(), }) return controllerruntime.NewControllerManagedBy(mgr). For(&k8upv1.Archive{}). diff --git a/operator/backupcontroller/backup_utils.go b/operator/backupcontroller/backup_utils.go index 0eaa6a4cb..acff8af8c 100644 --- a/operator/backupcontroller/backup_utils.go +++ b/operator/backupcontroller/backup_utils.go @@ -183,8 +183,8 @@ func (b *BackupExecutor) setupEnvVars() ([]corev1.EnvVar, error) { for key, value := range b.backup.Spec.Backend.GetCredentialEnv() { vars.SetEnvVarSource(key, value) } - vars.SetString(cfg.ResticRepositoryEnvName, b.backup.Spec.Backend.String()) } + vars.SetString(cfg.ResticRepositoryEnvName, b.Repository) } vars.SetStringOrDefault("STATS_URL", b.backup.Spec.StatsURL, cfg.Config.GlobalStatsURL) diff --git a/operator/backupcontroller/controller.go b/operator/backupcontroller/controller.go index bf4a39c3e..346163b3e 100644 --- a/operator/backupcontroller/controller.go +++ b/operator/backupcontroller/controller.go @@ -19,6 +19,8 @@ import ( // BackupReconciler reconciles a Backup object type BackupReconciler struct { Kube client.Client + // KubeReader performs direct API reads so Secrets only require named GET permissions, not cache list/watch permissions. + KubeReader client.Reader } func (r *BackupReconciler) NewObject() *k8upv1.Backup { @@ -32,6 +34,12 @@ func (r *BackupReconciler) NewObjectList() *k8upv1.BackupList { func (r *BackupReconciler) Provision(ctx context.Context, obj *k8upv1.Backup) (reconcile.Result, error) { log := controllerruntime.LoggerFrom(ctx) + if obj.Spec.Backend != nil { + if err := obj.Spec.Backend.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return reconcile.Result{}, fmt.Errorf("resolve backend Secret references: %w", err) + } + } + repository := cfg.Config.GetGlobalRepository() if obj.Spec.Backend != nil { repository = obj.Spec.Backend.String() diff --git a/operator/backupcontroller/setup.go b/operator/backupcontroller/setup.go index 8bfef1873..857d60d9f 100644 --- a/operator/backupcontroller/setup.go +++ b/operator/backupcontroller/setup.go @@ -25,7 +25,8 @@ import ( func SetupWithManager(mgr controllerruntime.Manager) error { name := "backup.k8up.io" r := reconciler.NewReconciler[*k8upv1.Backup, *k8upv1.BackupList](mgr.GetClient(), &BackupReconciler{ - Kube: mgr.GetClient(), + Kube: mgr.GetClient(), + KubeReader: mgr.GetAPIReader(), }) return controllerruntime.NewControllerManagedBy(mgr). Named(name). diff --git a/operator/cfg/config.go b/operator/cfg/config.go index d9e9186bb..3741f9e05 100644 --- a/operator/cfg/config.go +++ b/operator/cfg/config.go @@ -83,6 +83,7 @@ type Configuration struct { PromURL string ClusterName string RestartPolicy string + RepositorySecretName string SkipWithoutAnnotation bool SkipSnapshotSync bool EnableRelaxedScheduling bool diff --git a/operator/checkcontroller/controller.go b/operator/checkcontroller/controller.go index 7e4242e20..fcf81c119 100644 --- a/operator/checkcontroller/controller.go +++ b/operator/checkcontroller/controller.go @@ -2,6 +2,7 @@ package checkcontroller import ( "context" + "fmt" "time" k8upv1 "github.com/k8up-io/k8up/v2/api/v1" @@ -16,6 +17,8 @@ import ( // CheckReconciler reconciles a Check object type CheckReconciler struct { Kube client.Client + // KubeReader performs direct API reads so Secrets only require named GET permissions, not cache list/watch permissions. + KubeReader client.Reader } func (r *CheckReconciler) NewObject() *k8upv1.Check { @@ -29,6 +32,12 @@ func (r *CheckReconciler) NewObjectList() *k8upv1.CheckList { func (r *CheckReconciler) Provision(ctx context.Context, obj *k8upv1.Check) (controllerruntime.Result, error) { log := controllerruntime.LoggerFrom(ctx) + if obj.Spec.Backend != nil { + if err := obj.Spec.Backend.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return controllerruntime.Result{}, fmt.Errorf("resolve backend Secret references: %w", err) + } + } + repository := cfg.Config.GetGlobalRepository() if obj.Spec.Backend != nil { repository = obj.Spec.Backend.String() diff --git a/operator/checkcontroller/executor.go b/operator/checkcontroller/executor.go index f0f30ea2a..db0429f83 100644 --- a/operator/checkcontroller/executor.go +++ b/operator/checkcontroller/executor.go @@ -91,8 +91,8 @@ func (c *CheckExecutor) setupEnvVars(ctx context.Context) []corev1.EnvVar { for key, value := range c.check.Spec.Backend.GetCredentialEnv() { vars.SetEnvVarSource(key, value) } - vars.SetString(cfg.ResticRepositoryEnvName, c.check.Spec.Backend.String()) } + vars.SetString(cfg.ResticRepositoryEnvName, c.Repository) } vars.SetString("PROM_URL", cfg.Config.PromURL) diff --git a/operator/checkcontroller/setup.go b/operator/checkcontroller/setup.go index 43f91ca4f..8549b7d28 100644 --- a/operator/checkcontroller/setup.go +++ b/operator/checkcontroller/setup.go @@ -16,7 +16,8 @@ import ( func SetupWithManager(mgr ctrl.Manager) error { name := "check.k8up.io" r := reconciler.NewReconciler[*k8upv1.Check, *k8upv1.CheckList](mgr.GetClient(), &CheckReconciler{ - Kube: mgr.GetClient(), + Kube: mgr.GetClient(), + KubeReader: mgr.GetAPIReader(), }) return ctrl.NewControllerManagedBy(mgr). For(&k8upv1.Check{}). diff --git a/operator/controllers.go b/operator/controllers.go index 1213368fe..22895c61e 100644 --- a/operator/controllers.go +++ b/operator/controllers.go @@ -2,3 +2,4 @@ package operator // +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get,resourceNames=k8up-repo diff --git a/operator/prunecontroller/controller.go b/operator/prunecontroller/controller.go index a2a7f813c..cc923048d 100644 --- a/operator/prunecontroller/controller.go +++ b/operator/prunecontroller/controller.go @@ -2,6 +2,7 @@ package prunecontroller import ( "context" + "fmt" "time" k8upv1 "github.com/k8up-io/k8up/v2/api/v1" @@ -16,6 +17,8 @@ import ( // PruneReconciler reconciles a Prune object type PruneReconciler struct { Kube client.Client + // KubeReader performs direct API reads so Secrets only require named GET permissions, not cache list/watch permissions. + KubeReader client.Reader } func (r *PruneReconciler) NewObject() *k8upv1.Prune { @@ -29,6 +32,12 @@ func (r *PruneReconciler) NewObjectList() *k8upv1.PruneList { func (r *PruneReconciler) Provision(ctx context.Context, obj *k8upv1.Prune) (controllerruntime.Result, error) { log := controllerruntime.LoggerFrom(ctx) + if obj.Spec.Backend != nil { + if err := obj.Spec.Backend.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return controllerruntime.Result{}, fmt.Errorf("resolve backend Secret references: %w", err) + } + } + repository := cfg.Config.GetGlobalRepository() if obj.Spec.Backend != nil { repository = obj.Spec.Backend.String() diff --git a/operator/prunecontroller/executor.go b/operator/prunecontroller/executor.go index e80e5e9a1..00955eeed 100644 --- a/operator/prunecontroller/executor.go +++ b/operator/prunecontroller/executor.go @@ -131,8 +131,8 @@ func (p *PruneExecutor) setupEnvVars(ctx context.Context, prune *k8upv1.Prune) [ for key, value := range prune.Spec.Backend.GetCredentialEnv() { vars.SetEnvVarSource(key, value) } - vars.SetString(cfg.ResticRepositoryEnvName, prune.Spec.Backend.String()) } + vars.SetString(cfg.ResticRepositoryEnvName, p.Repository) vars.SetString("PROM_URL", cfg.Config.PromURL) diff --git a/operator/prunecontroller/setup.go b/operator/prunecontroller/setup.go index e69529b3a..677b12423 100644 --- a/operator/prunecontroller/setup.go +++ b/operator/prunecontroller/setup.go @@ -16,7 +16,8 @@ import ( func SetupWithManager(mgr ctrl.Manager) error { name := "prune.k8up.io" r := reconciler.NewReconciler[*k8upv1.Prune, *k8upv1.PruneList](mgr.GetClient(), &PruneReconciler{ - Kube: mgr.GetClient(), + Kube: mgr.GetClient(), + KubeReader: mgr.GetAPIReader(), }) return ctrl.NewControllerManagedBy(mgr). For(&k8upv1.Prune{}). diff --git a/operator/restorecontroller/controller.go b/operator/restorecontroller/controller.go index c298d99d1..39b82f715 100644 --- a/operator/restorecontroller/controller.go +++ b/operator/restorecontroller/controller.go @@ -2,6 +2,7 @@ package restorecontroller import ( "context" + "fmt" "time" k8upv1 "github.com/k8up-io/k8up/v2/api/v1" @@ -16,6 +17,8 @@ import ( // RestoreReconciler reconciles a Restore object type RestoreReconciler struct { Kube client.Client + // KubeReader performs direct API reads so Secrets only require named GET permissions, not cache list/watch permissions. + KubeReader client.Reader } func (r *RestoreReconciler) NewObject() *k8upv1.Restore { @@ -29,6 +32,17 @@ func (r *RestoreReconciler) NewObjectList() *k8upv1.RestoreList { func (r *RestoreReconciler) Provision(ctx context.Context, obj *k8upv1.Restore) (controllerruntime.Result, error) { log := controllerruntime.LoggerFrom(ctx) + if obj.Spec.Backend != nil { + if err := obj.Spec.Backend.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return controllerruntime.Result{}, fmt.Errorf("resolve backend Secret references: %w", err) + } + } + if obj.Spec.RestoreMethod != nil { + if err := obj.Spec.RestoreMethod.S3.ResolveSecretRefs(ctx, r.KubeReader, obj.GetNamespace(), cfg.Config.RepositorySecretName); err != nil { + return controllerruntime.Result{}, fmt.Errorf("resolve restore S3 Secret references: %w", err) + } + } + repository := cfg.Config.GetGlobalRepository() if obj.Spec.Backend != nil { repository = obj.Spec.Backend.String() diff --git a/operator/restorecontroller/executor.go b/operator/restorecontroller/executor.go index afc7535a9..099ee7bfa 100644 --- a/operator/restorecontroller/executor.go +++ b/operator/restorecontroller/executor.go @@ -185,8 +185,8 @@ func (r *RestoreExecutor) setupEnvVars(ctx context.Context, restore *k8upv1.Rest for key, value := range restore.Spec.Backend.GetCredentialEnv() { vars.SetEnvVarSource(key, value) } - vars.SetString(cfg.ResticRepositoryEnvName, restore.Spec.Backend.String()) } + vars.SetString(cfg.ResticRepositoryEnvName, r.Repository) err := vars.Merge(executor.DefaultEnv(r.Obj.GetNamespace())) if err != nil { diff --git a/operator/restorecontroller/executor_test.go b/operator/restorecontroller/executor_test.go index 075c9b9fe..cf0109463 100644 --- a/operator/restorecontroller/executor_test.go +++ b/operator/restorecontroller/executor_test.go @@ -7,6 +7,7 @@ import ( "testing" k8upv1 "github.com/k8up-io/k8up/v2/api/v1" + "github.com/k8up-io/k8up/v2/operator/cfg" "github.com/k8up-io/k8up/v2/operator/job" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -134,7 +135,11 @@ func TestRestore_setupEnvVars(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - e := NewRestoreExecutor(*newConfig()) + repository := cfg.Config.GetGlobalRepository() + if tt.GivenResource.Spec.Backend != nil { + repository = tt.GivenResource.Spec.Backend.String() + } + e := NewRestoreExecutor(job.NewConfig(nil, tt.GivenResource, repository)) envVars := e.setupEnvVars(context.TODO(), tt.GivenResource) actualEnvVars, actualSecretKeyRefs := extractVarsAndSecretRefs(envVars) diff --git a/operator/restorecontroller/setup.go b/operator/restorecontroller/setup.go index 0dd0dd725..de5147e9e 100644 --- a/operator/restorecontroller/setup.go +++ b/operator/restorecontroller/setup.go @@ -4,7 +4,7 @@ import ( k8upv1 "github.com/k8up-io/k8up/v2/api/v1" "github.com/k8up-io/k8up/v2/operator/reconciler" batchv1 "k8s.io/api/batch/v1" - "sigs.k8s.io/controller-runtime" + controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/predicate" ) @@ -16,7 +16,8 @@ import ( func SetupWithManager(mgr controllerruntime.Manager) error { name := "restore.k8up.io" r := reconciler.NewReconciler[*k8upv1.Restore, *k8upv1.RestoreList](mgr.GetClient(), &RestoreReconciler{ - Kube: mgr.GetClient(), + Kube: mgr.GetClient(), + KubeReader: mgr.GetAPIReader(), }) return controllerruntime.NewControllerManagedBy(mgr). For(&k8upv1.Restore{}).