From b407f6039ce16059936607f086eb4339e6138451 Mon Sep 17 00:00:00 2001 From: Drew Sessler Date: Wed, 23 Jul 2025 17:47:18 -0700 Subject: [PATCH] Allow user to set an annotation that will specify an existing PVC to be mounted to cloud backup jobs so that the backup logs can be persisted. --- .../controller/postgrescluster/pgbackrest.go | 55 ++++- .../postgrescluster/pgbackrest_test.go | 192 ++++++++++++++++-- internal/naming/annotations.go | 4 + internal/naming/annotations_test.go | 1 + internal/pgbackrest/config.go | 13 +- internal/pgbackrest/config_test.go | 63 +++++- internal/util/volumes.go | 42 ++++ internal/util/volumes_test.go | 78 +++++++ 8 files changed, 416 insertions(+), 32 deletions(-) create mode 100644 internal/util/volumes.go create mode 100644 internal/util/volumes_test.go diff --git a/internal/controller/postgrescluster/pgbackrest.go b/internal/controller/postgrescluster/pgbackrest.go index aada99ec57..49bbde0f45 100644 --- a/internal/controller/postgrescluster/pgbackrest.go +++ b/internal/controller/postgrescluster/pgbackrest.go @@ -38,6 +38,7 @@ import ( "github.com/crunchydata/postgres-operator/internal/pgbackrest" "github.com/crunchydata/postgres-operator/internal/pki" "github.com/crunchydata/postgres-operator/internal/postgres" + "github.com/crunchydata/postgres-operator/internal/util" "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) @@ -771,7 +772,7 @@ func (r *Reconciler) generateRepoVolumeIntent(postgresCluster *v1beta1.PostgresC } // generateBackupJobSpecIntent generates a JobSpec for a pgBackRest backup job -func generateBackupJobSpecIntent(ctx context.Context, postgresCluster *v1beta1.PostgresCluster, +func (r *Reconciler) generateBackupJobSpecIntent(ctx context.Context, postgresCluster *v1beta1.PostgresCluster, repo v1beta1.PGBackRestRepo, serviceAccountName string, labels, annotations map[string]string, opts ...string) *batchv1.JobSpec { @@ -873,6 +874,27 @@ func generateBackupJobSpecIntent(ctx context.Context, postgresCluster *v1beta1.P // to read certificate files jobSpec.Template.Spec.SecurityContext = postgres.PodSecurityContext(postgresCluster) pgbackrest.AddConfigToCloudBackupJob(postgresCluster, &jobSpec.Template) + + // If the user has specified a PVC to use as a log volume via the PGBackRestCloudLogVolume + // annotation, check for the PVC. If we find it, mount it to the backup job. + // Otherwise, create a warning event. + if logVolumeName := postgresCluster.Annotations[naming.PGBackRestCloudLogVolume]; logVolumeName != "" { + logVolume := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: logVolumeName, + Namespace: postgresCluster.GetNamespace(), + }, + } + err := errors.WithStack(r.Client.Get(ctx, + client.ObjectKeyFromObject(logVolume), logVolume)) + if err != nil { + // PVC not retrieved, create warning event + r.Recorder.Event(postgresCluster, corev1.EventTypeWarning, "PGBackRestCloudLogVolumeNotFound", err.Error()) + } else { + // We successfully found the specified PVC, so we will add it to the backup job + util.AddVolumeAndMountsToPod(&jobSpec.Template.Spec, logVolume) + } + } } return jobSpec @@ -2040,8 +2062,31 @@ func (r *Reconciler) reconcilePGBackRestConfig(ctx context.Context, repoHostName, configHash, serviceName, serviceNamespace string, instanceNames []string) error { + // If the user has specified a PVC to use as a log volume for cloud backups via the + // PGBackRestCloudLogVolume annotation, check for the PVC. If we find it, set the cloud + // log path. If the user has specified a PVC, but we can't find it, create a warning event. + cloudLogPath := "" + if logVolumeName := postgresCluster.Annotations[naming.PGBackRestCloudLogVolume]; logVolumeName != "" { + logVolume := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: logVolumeName, + Namespace: postgresCluster.GetNamespace(), + }, + } + err := errors.WithStack(r.Client.Get(ctx, + client.ObjectKeyFromObject(logVolume), logVolume)) + if err != nil { + // PVC not retrieved, create warning event + r.Recorder.Event(postgresCluster, corev1.EventTypeWarning, + "PGBackRestCloudLogVolumeNotFound", err.Error()) + } else { + // We successfully found the specified PVC, so we will set the log path + cloudLogPath = "/volumes/" + logVolumeName + } + } + backrestConfig, err := pgbackrest.CreatePGBackRestConfigMapIntent(ctx, postgresCluster, repoHostName, - configHash, serviceName, serviceNamespace, instanceNames) + configHash, serviceName, serviceNamespace, cloudLogPath, instanceNames) if err != nil { return err } @@ -2454,7 +2499,7 @@ func (r *Reconciler) reconcileManualBackup(ctx context.Context, backupJob.Labels = labels backupJob.Annotations = annotations - spec := generateBackupJobSpecIntent(ctx, postgresCluster, repo, + spec := r.generateBackupJobSpecIntent(ctx, postgresCluster, repo, serviceAccount.GetName(), labels, annotations, backupOpts...) backupJob.Spec = *spec @@ -2631,7 +2676,7 @@ func (r *Reconciler) reconcileReplicaCreateBackup(ctx context.Context, backupJob.Labels = labels backupJob.Annotations = annotations - spec := generateBackupJobSpecIntent(ctx, postgresCluster, replicaCreateRepo, + spec := r.generateBackupJobSpecIntent(ctx, postgresCluster, replicaCreateRepo, serviceAccount.GetName(), labels, annotations) backupJob.Spec = *spec @@ -3058,7 +3103,7 @@ func (r *Reconciler) reconcilePGBackRestCronJob( // set backup type (i.e. "full", "diff", "incr") backupOpts := []string{"--type=" + backupType} - jobSpec := generateBackupJobSpecIntent(ctx, cluster, repo, + jobSpec := r.generateBackupJobSpecIntent(ctx, cluster, repo, serviceAccount.GetName(), labels, annotations, backupOpts...) // Suspend cronjobs when shutdown or read-only. Any jobs that have already diff --git a/internal/controller/postgrescluster/pgbackrest_test.go b/internal/controller/postgrescluster/pgbackrest_test.go index 6c57479274..6dc4e05e76 100644 --- a/internal/controller/postgrescluster/pgbackrest_test.go +++ b/internal/controller/postgrescluster/pgbackrest_test.go @@ -40,6 +40,7 @@ import ( "github.com/crunchydata/postgres-operator/internal/pgbackrest" "github.com/crunchydata/postgres-operator/internal/pki" "github.com/crunchydata/postgres-operator/internal/testing/cmp" + "github.com/crunchydata/postgres-operator/internal/testing/events" "github.com/crunchydata/postgres-operator/internal/testing/require" "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) @@ -2601,6 +2602,15 @@ func TestCopyConfigurationResources(t *testing.T) { } func TestGenerateBackupJobIntent(t *testing.T) { + _, cc := setupKubernetes(t) + require.ParallelCapacity(t, 0) + ns := setupNamespace(t, cc) + + r := &Reconciler{ + Client: cc, + Owner: ControllerName, + } + ctx := context.Background() cluster := v1beta1.PostgresCluster{} cluster.Name = "hippo-test" @@ -2609,7 +2619,7 @@ func TestGenerateBackupJobIntent(t *testing.T) { // If repo.Volume is nil, the code interprets this as a cloud repo backup, // therefore, an "empty" input results in a job spec for a cloud repo backup t.Run("empty", func(t *testing.T) { - spec := generateBackupJobSpecIntent(ctx, + spec := r.generateBackupJobSpecIntent(ctx, &cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2670,7 +2680,7 @@ volumes: }) t.Run("volumeRepo", func(t *testing.T) { - spec := generateBackupJobSpecIntent(ctx, + spec := r.generateBackupJobSpecIntent(ctx, &cluster, v1beta1.PGBackRestRepo{ Volume: &v1beta1.RepoPVC{ VolumeClaimSpec: v1beta1.VolumeClaimSpec{}, @@ -2747,7 +2757,7 @@ volumes: ImagePullPolicy: corev1.PullAlways, }, } - job := generateBackupJobSpecIntent(ctx, + job := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2762,7 +2772,7 @@ volumes: cluster.Spec.Backups = v1beta1.Backups{ PGBackRest: v1beta1.PGBackRestArchive{}, } - job := generateBackupJobSpecIntent(ctx, + job := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2779,7 +2789,7 @@ volumes: }, }, } - job := generateBackupJobSpecIntent(ctx, + job := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2818,7 +2828,7 @@ volumes: }, }, } - job := generateBackupJobSpecIntent(ctx, + job := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2831,7 +2841,7 @@ volumes: cluster.Spec.Backups.PGBackRest.Jobs = &v1beta1.BackupJobs{ PriorityClassName: initialize.String("some-priority-class"), } - job := generateBackupJobSpecIntent(ctx, + job := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2849,7 +2859,7 @@ volumes: cluster.Spec.Backups.PGBackRest.Jobs = &v1beta1.BackupJobs{ Tolerations: tolerations, } - job := generateBackupJobSpecIntent(ctx, + job := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, @@ -2863,14 +2873,14 @@ volumes: t.Run("Undefined", func(t *testing.T) { cluster.Spec.Backups.PGBackRest.Jobs = nil - spec := generateBackupJobSpecIntent(ctx, + spec := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, ) assert.Assert(t, spec.TTLSecondsAfterFinished == nil) cluster.Spec.Backups.PGBackRest.Jobs = &v1beta1.BackupJobs{} - spec = generateBackupJobSpecIntent(ctx, + spec = r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, ) assert.Assert(t, spec.TTLSecondsAfterFinished == nil) @@ -2881,7 +2891,7 @@ volumes: TTLSecondsAfterFinished: initialize.Int32(0), } - spec := generateBackupJobSpecIntent(ctx, + spec := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, ) if assert.Check(t, spec.TTLSecondsAfterFinished != nil) { @@ -2894,7 +2904,7 @@ volumes: TTLSecondsAfterFinished: initialize.Int32(100), } - spec := generateBackupJobSpecIntent(ctx, + spec := r.generateBackupJobSpecIntent(ctx, cluster, v1beta1.PGBackRestRepo{}, "", nil, nil, ) if assert.Check(t, spec.TTLSecondsAfterFinished != nil) { @@ -2902,6 +2912,164 @@ volumes: } }) }) + + t.Run("CloudLogVolumeAnnotationNoPvc", func(t *testing.T) { + recorder := events.NewRecorder(t, runtime.Scheme) + r.Recorder = recorder + + cluster.Namespace = ns.Name + cluster.Annotations = map[string]string{} + cluster.Annotations[naming.PGBackRestCloudLogVolume] = "some-pvc" + spec := r.generateBackupJobSpecIntent(ctx, + &cluster, v1beta1.PGBackRestRepo{}, + "", + nil, nil, + ) + assert.Assert(t, cmp.MarshalMatches(spec.Template.Spec, ` +containers: +- command: + - /bin/pgbackrest + - backup + - --stanza=db + - --repo= + name: pgbackrest + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /etc/pgbackrest/conf.d + name: pgbackrest-config + readOnly: true + - mountPath: /tmp + name: tmp +enableServiceLinks: false +restartPolicy: Never +securityContext: + fsGroup: 26 + fsGroupChangePolicy: OnRootMismatch +volumes: +- name: pgbackrest-config + projected: + sources: + - configMap: + items: + - key: pgbackrest_cloud.conf + path: pgbackrest_cloud.conf + name: hippo-test-pgbackrest-config + - secret: + items: + - key: pgbackrest.ca-roots + path: ~postgres-operator/tls-ca.crt + - key: pgbackrest-client.crt + path: ~postgres-operator/client-tls.crt + - key: pgbackrest-client.key + mode: 384 + path: ~postgres-operator/client-tls.key + name: hippo-test-pgbackrest +- emptyDir: + sizeLimit: 16Mi + name: tmp + `)) + + assert.Equal(t, len(recorder.Events), 1) + assert.Equal(t, recorder.Events[0].Regarding.Name, cluster.Name) + assert.Equal(t, recorder.Events[0].Reason, "PGBackRestCloudLogVolumeNotFound") + assert.Equal(t, recorder.Events[0].Note, "persistentvolumeclaims \"some-pvc\" not found") + }) + + t.Run("CloudLogVolumeAnnotationPvcInPlace", func(t *testing.T) { + recorder := events.NewRecorder(t, runtime.Scheme) + r.Recorder = recorder + + cluster.Namespace = ns.Name + cluster.Annotations = map[string]string{} + cluster.Annotations[naming.PGBackRestCloudLogVolume] = "another-pvc" + + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-pvc", + Namespace: ns.Name, + }, + Spec: corev1.PersistentVolumeClaimSpec(testVolumeClaimSpec()), + } + err := r.Client.Create(ctx, pvc) + assert.NilError(t, err) + + spec := r.generateBackupJobSpecIntent(ctx, + &cluster, v1beta1.PGBackRestRepo{}, + "", + nil, nil, + ) + assert.Assert(t, cmp.MarshalMatches(spec.Template.Spec, ` +containers: +- command: + - /bin/pgbackrest + - backup + - --stanza=db + - --repo= + name: pgbackrest + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /etc/pgbackrest/conf.d + name: pgbackrest-config + readOnly: true + - mountPath: /tmp + name: tmp + - mountPath: /volumes/another-pvc + name: another-pvc +enableServiceLinks: false +restartPolicy: Never +securityContext: + fsGroup: 26 + fsGroupChangePolicy: OnRootMismatch +volumes: +- name: pgbackrest-config + projected: + sources: + - configMap: + items: + - key: pgbackrest_cloud.conf + path: pgbackrest_cloud.conf + name: hippo-test-pgbackrest-config + - secret: + items: + - key: pgbackrest.ca-roots + path: ~postgres-operator/tls-ca.crt + - key: pgbackrest-client.crt + path: ~postgres-operator/client-tls.crt + - key: pgbackrest-client.key + mode: 384 + path: ~postgres-operator/client-tls.key + name: hippo-test-pgbackrest +- emptyDir: + sizeLimit: 16Mi + name: tmp +- name: another-pvc + persistentVolumeClaim: + claimName: another-pvc + `)) + + // No events created + assert.Equal(t, len(recorder.Events), 0) + }) } func TestGenerateRepoHostIntent(t *testing.T) { diff --git a/internal/naming/annotations.go b/internal/naming/annotations.go index 38d30926d9..61a5438908 100644 --- a/internal/naming/annotations.go +++ b/internal/naming/annotations.go @@ -54,6 +54,10 @@ const ( // bind all addresses does not work in certain IPv6 environments. PGBackRestIPVersion = annotationPrefix + "pgbackrest-ip-version" + // PGBackRestCloudLogVolume is an annotation used to indicate which persistent volume claim + // should be mounted to cloud repo backup jobs so that the backup logs can be persisted. + PGBackRestCloudLogVolume = annotationPrefix + "pgbackrest-cloud-log-volume" + // PostgresExporterCollectorsAnnotation is an annotation used to allow users to control whether or // not postgres_exporter default metrics, settings, and collectors are enabled. The value "None" // disables all postgres_exporter defaults. Disabling the defaults may cause errors in dashboards. diff --git a/internal/naming/annotations_test.go b/internal/naming/annotations_test.go index 593d000984..9553e5e72a 100644 --- a/internal/naming/annotations_test.go +++ b/internal/naming/annotations_test.go @@ -22,6 +22,7 @@ func TestAnnotationsValid(t *testing.T) { assert.Assert(t, nil == validation.IsQualifiedName(PGBackRestConfigHash)) assert.Assert(t, nil == validation.IsQualifiedName(PGBackRestCurrentConfig)) assert.Assert(t, nil == validation.IsQualifiedName(PGBackRestIPVersion)) + assert.Assert(t, nil == validation.IsQualifiedName(PGBackRestCloudLogVolume)) assert.Assert(t, nil == validation.IsQualifiedName(PGBackRestRestore)) assert.Assert(t, nil == validation.IsQualifiedName(PostgresExporterCollectorsAnnotation)) } diff --git a/internal/pgbackrest/config.go b/internal/pgbackrest/config.go index 0fdb407ffc..3899c33339 100644 --- a/internal/pgbackrest/config.go +++ b/internal/pgbackrest/config.go @@ -75,7 +75,7 @@ const ( // pgbackrest_repo.conf is used by the pgBackRest repository pod // pgbackrest_cloud.conf is used by cloud repo backup jobs func CreatePGBackRestConfigMapIntent(ctx context.Context, postgresCluster *v1beta1.PostgresCluster, - repoHostName, configHash, serviceName, serviceNamespace string, + repoHostName, configHash, serviceName, serviceNamespace, cloudLogPath string, instanceNames []string) (*corev1.ConfigMap, error) { var err error @@ -163,7 +163,7 @@ func CreatePGBackRestConfigMapIntent(ctx context.Context, postgresCluster *v1bet serviceName, serviceNamespace, pgdataDir, config.FetchKeyCommand(&postgresCluster.Spec), strconv.Itoa(postgresCluster.Spec.PostgresVersion), - pgPort, instanceNames, + cloudLogPath, pgPort, instanceNames, postgresCluster.Spec.Backups.PGBackRest.Repos, postgresCluster.Spec.Backups.PGBackRest.Global, ).String() @@ -519,7 +519,7 @@ func populateRepoHostConfigurationMap( func populateCloudRepoConfigurationMap( serviceName, serviceNamespace, pgdataDir, - fetchKeyCommand, postgresVersion string, + fetchKeyCommand, postgresVersion, logPath string, pgPort int32, pgHosts []string, repos []v1beta1.PGBackRestRepo, globalConfig map[string]string, ) iniSectionSet { @@ -539,7 +539,12 @@ func populateCloudRepoConfigurationMap( } } - global.Set("log-level-file", "off") + // If we are given a log path, set it in the config. Otherwise, turn off logging to file. + if logPath != "" { + global.Set("log-path", logPath) + } else { + global.Set("log-level-file", "off") + } for option, val := range globalConfig { global.Set(option, val) diff --git a/internal/pgbackrest/config_test.go b/internal/pgbackrest/config_test.go index 110a0928c4..c1b4e0b155 100644 --- a/internal/pgbackrest/config_test.go +++ b/internal/pgbackrest/config_test.go @@ -40,7 +40,7 @@ func TestCreatePGBackRestConfigMapIntent(t *testing.T) { cluster.UID = "piano" configmap, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "", "number", "pod-service-name", "test-ns", + "", "number", "pod-service-name", "test-ns", "", []string{"some-instance"}) assert.NilError(t, err) @@ -96,19 +96,33 @@ pg1-socket-path = /tmp/postgres } configmap, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "", "anumber", "pod-service-name", "test-ns", + "", "anumber", "pod-service-name", "test-ns", "", + []string{"some-instance"}) + assert.NilError(t, err) + + configmapWithCloudLogging, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, + "", "anumber", "pod-service-name", "test-ns", "/a/log/path", []string{"some-instance"}) assert.NilError(t, err) assert.DeepEqual(t, configmap.Annotations, map[string]string{}) + assert.DeepEqual(t, configmapWithCloudLogging.Annotations, map[string]string{}) + assert.DeepEqual(t, configmap.Labels, map[string]string{ "postgres-operator.crunchydata.com/cluster": "hippo-dance", "postgres-operator.crunchydata.com/pgbackrest": "", "postgres-operator.crunchydata.com/pgbackrest-config": "", }) + assert.DeepEqual(t, configmapWithCloudLogging.Labels, map[string]string{ + "postgres-operator.crunchydata.com/cluster": "hippo-dance", + "postgres-operator.crunchydata.com/pgbackrest": "", + "postgres-operator.crunchydata.com/pgbackrest-config": "", + }) assert.Equal(t, configmap.Data["config-hash"], "anumber") - assert.Equal(t, configmap.Data["pgbackrest-server.conf"], strings.Trim(` + assert.Equal(t, configmapWithCloudLogging.Data["config-hash"], "anumber") + + serverConfigExpectation := strings.Trim(` # Generated by postgres-operator. DO NOT EDIT. # Your changes will not be saved. @@ -124,9 +138,11 @@ log-level-console = detail log-level-file = off log-level-stderr = error log-timestamp = n - `, "\t\n")+"\n") + `, "\t\n") + assert.Equal(t, configmap.Data["pgbackrest-server.conf"], serverConfigExpectation+"\n") + assert.Equal(t, configmapWithCloudLogging.Data["pgbackrest-server.conf"], serverConfigExpectation+"\n") - assert.Equal(t, configmap.Data["pgbackrest_instance.conf"], strings.Trim(` + instanceConfigExpectation := strings.Trim(` # Generated by postgres-operator. DO NOT EDIT. # Your changes will not be saved. @@ -143,7 +159,9 @@ spool-path = /pgdata/pgbackrest-spool pg1-path = /pgdata/pg12 pg1-port = 2345 pg1-socket-path = /tmp/postgres - `, "\t\n")+"\n") + `, "\t\n") + assert.Equal(t, configmap.Data["pgbackrest_instance.conf"], instanceConfigExpectation+"\n") + assert.Equal(t, configmapWithCloudLogging.Data["pgbackrest_instance.conf"], instanceConfigExpectation+"\n") assert.Equal(t, configmap.Data["pgbackrest_cloud.conf"], strings.Trim(` # Generated by postgres-operator. DO NOT EDIT. @@ -156,6 +174,28 @@ repo1-path = /pgbackrest/repo1 repo1-test = something repo1-type = gcs +[db] +pg1-host = some-instance-0.pod-service-name.test-ns.svc.`+domain+` +pg1-host-ca-file = /etc/pgbackrest/conf.d/~postgres-operator/tls-ca.crt +pg1-host-cert-file = /etc/pgbackrest/conf.d/~postgres-operator/client-tls.crt +pg1-host-key-file = /etc/pgbackrest/conf.d/~postgres-operator/client-tls.key +pg1-host-type = tls +pg1-path = /pgdata/pg12 +pg1-port = 2345 +pg1-socket-path = /tmp/postgres + `, "\t\n")+"\n") + + assert.Equal(t, configmapWithCloudLogging.Data["pgbackrest_cloud.conf"], strings.Trim(` +# Generated by postgres-operator. DO NOT EDIT. +# Your changes will not be saved. + +[global] +log-path = /a/log/path +repo1-gcs-bucket = g-bucket +repo1-path = /pgbackrest/repo1 +repo1-test = something +repo1-type = gcs + [db] pg1-host = some-instance-0.pod-service-name.test-ns.svc.`+domain+` pg1-host-ca-file = /etc/pgbackrest/conf.d/~postgres-operator/tls-ca.crt @@ -168,6 +208,7 @@ pg1-socket-path = /tmp/postgres `, "\t\n")+"\n") assert.Equal(t, configmap.Data["pgbackrest_repo.conf"], "") + assert.Equal(t, configmapWithCloudLogging.Data["pgbackrest_repo.conf"], "") }) t.Run("VolumeRepoPresentNoCloudRepo", func(t *testing.T) { @@ -181,7 +222,7 @@ pg1-socket-path = /tmp/postgres } configmap, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "repo-hostname", "anumber", "pod-service-name", "test-ns", + "repo-hostname", "anumber", "pod-service-name", "test-ns", "", []string{"some-instance"}) assert.NilError(t, err) @@ -283,7 +324,7 @@ pg1-socket-path = /tmp/postgres } configmap, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "repo-hostname", "abcde12345", "pod-service-name", "test-ns", + "repo-hostname", "abcde12345", "pod-service-name", "test-ns", "", []string{"some-instance"}) assert.NilError(t, err) @@ -438,7 +479,7 @@ pg1-socket-path = /tmp/postgres } configmap, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "any", "any", "any", "any", nil) + "any", "any", "any", "any", "any", nil) assert.NilError(t, err) assert.DeepEqual(t, configmap.Annotations, map[string]string{ @@ -470,7 +511,7 @@ pg1-socket-path = /tmp/postgres } configmap, err := CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "", "number", "pod-service-name", "test-ns", + "", "number", "pod-service-name", "test-ns", "", []string{"some-instance"}) assert.NilError(t, err) @@ -492,7 +533,7 @@ pg1-socket-path = /tmp/postgres } configmap, err = CreatePGBackRestConfigMapIntent(context.Background(), cluster, - "repo1", "number", "pod-service-name", "test-ns", + "repo1", "number", "pod-service-name", "test-ns", "", []string{"some-instance"}) assert.NilError(t, err) diff --git a/internal/util/volumes.go b/internal/util/volumes.go new file mode 100644 index 0000000000..34e2699b54 --- /dev/null +++ b/internal/util/volumes.go @@ -0,0 +1,42 @@ +// Copyright 2017 - 2025 Crunchy Data Solutions, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +package util + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" +) + +// AddVolumeAndMountsToPod takes a Pod spec and a PVC and adds a Volume to the Pod spec with +// the PVC as the VolumeSource and mounts the volume to all containers and init containers +// in the Pod spec. +func AddVolumeAndMountsToPod(podSpec *corev1.PodSpec, volume *corev1.PersistentVolumeClaim) { + + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ + Name: volume.Name, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: volume.Name, + }, + }, + }) + + for i := range podSpec.Containers { + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, + corev1.VolumeMount{ + Name: volume.Name, + MountPath: fmt.Sprintf("/volumes/%s", volume.Name), + }) + } + + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, + corev1.VolumeMount{ + Name: volume.Name, + MountPath: fmt.Sprintf("/volumes/%s", volume.Name), + }) + } +} diff --git a/internal/util/volumes_test.go b/internal/util/volumes_test.go new file mode 100644 index 0000000000..b438943e3a --- /dev/null +++ b/internal/util/volumes_test.go @@ -0,0 +1,78 @@ +// Copyright 2021 - 2025 Crunchy Data Solutions, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +package util + +import ( + "testing" + + "github.com/google/go-cmp/cmp/cmpopts" + "gotest.tools/v3/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/crunchydata/postgres-operator/internal/testing/cmp" +) + +func TestAddVolumeAndMountsToPod(t *testing.T) { + pod := &corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "database"}, + {Name: "other"}, + {Name: "pgbackrest"}, + }, + InitContainers: []corev1.Container{ + {Name: "initializer"}, + {Name: "another"}, + }, + } + + volume := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "volume-name", + }, + } + + alwaysExpect := func(t testing.TB, result *corev1.PodSpec) { + // Only Containers, InitContainers, and Volumes fields have changed. + assert.DeepEqual(t, *pod, *result, cmpopts.IgnoreFields(*pod, "Containers", "InitContainers", "Volumes")) + + // Volume is mounted to all containers + assert.Assert(t, cmp.MarshalMatches(result.Containers, ` +- name: database + resources: {} + volumeMounts: + - mountPath: /volumes/volume-name + name: volume-name +- name: other + resources: {} + volumeMounts: + - mountPath: /volumes/volume-name + name: volume-name +- name: pgbackrest + resources: {} + volumeMounts: + - mountPath: /volumes/volume-name + name: volume-name + `)) + + // Volume is mounted to all init containers + assert.Assert(t, cmp.MarshalMatches(result.InitContainers, ` +- name: initializer + resources: {} + volumeMounts: + - mountPath: /volumes/volume-name + name: volume-name +- name: another + resources: {} + volumeMounts: + - mountPath: /volumes/volume-name + name: volume-name + `)) + } + + out := pod.DeepCopy() + AddVolumeAndMountsToPod(out, volume) + alwaysExpect(t, out) +}