diff --git a/config/crd/bases/postgres-operator.crunchydata.com_pgadmins.yaml b/config/crd/bases/postgres-operator.crunchydata.com_pgadmins.yaml index accc248ec9..92627608fc 100644 --- a/config/crd/bases/postgres-operator.crunchydata.com_pgadmins.yaml +++ b/config/crd/bases/postgres-operator.crunchydata.com_pgadmins.yaml @@ -2621,6 +2621,57 @@ spec: x-kubernetes-list-map-keys: - username x-kubernetes-list-type: map + volumes: + description: PGAdminVolumesSpec defines the configuration for pgAdmin + additional volumes + properties: + additional: + description: Additional pre-existing volumes to add to the pod. + items: + properties: + claimName: + description: Name of an existing PersistentVolumeClaim. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + containers: + description: |- + The names of containers in which to mount this volume. + The default mounts the volume in *all* containers. An empty list does not mount the volume to any containers. + items: + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + maxItems: 10 + type: array + x-kubernetes-list-type: set + name: + allOf: + - maxLength: 63 + - maxLength: 55 + description: |- + The name of the directory in which to mount this volume. + Volumes are mounted in containers at `/volumes/{name}`. + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + readOnly: + description: When true, mount the volume read-only, otherwise + read-write. Defaults to false. + type: boolean + required: + - claimName + - name + type: object + x-kubernetes-map-type: atomic + maxItems: 10 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object required: - dataVolumeClaimSpec type: object diff --git a/internal/controller/postgrescluster/instance.go b/internal/controller/postgrescluster/instance.go index 97b035c04d..68838f5c40 100644 --- a/internal/controller/postgrescluster/instance.go +++ b/internal/controller/postgrescluster/instance.go @@ -1196,7 +1196,7 @@ func (r *Reconciler) reconcileInstance( // mount additional volumes to the Postgres instance containers if err == nil && spec.Volumes != nil && len(spec.Volumes.Additional) > 0 { - missingContainers := addAdditionalVolumesToSpecifiedContainers(&instance.Spec.Template, spec.Volumes.Additional) + missingContainers := AddAdditionalVolumesToSpecifiedContainers(&instance.Spec.Template, spec.Volumes.Additional) if len(missingContainers) > 0 { r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "SpecifiedContainerNotFound", diff --git a/internal/controller/postgrescluster/pgbackrest.go b/internal/controller/postgrescluster/pgbackrest.go index 77fbd716b3..6c6a182008 100644 --- a/internal/controller/postgrescluster/pgbackrest.go +++ b/internal/controller/postgrescluster/pgbackrest.go @@ -721,7 +721,7 @@ func (r *Reconciler) generateRepoHostIntent(ctx context.Context, postgresCluster // mount additional volumes to the repo host containers if repoHost != nil && repoHost.Volumes != nil && len(repoHost.Volumes.Additional) > 0 { - missingContainers := addAdditionalVolumesToSpecifiedContainers(&repo.Spec.Template, repoHost.Volumes.Additional) + missingContainers := AddAdditionalVolumesToSpecifiedContainers(&repo.Spec.Template, repoHost.Volumes.Additional) if len(missingContainers) > 0 { r.Recorder.Eventf(postgresCluster, corev1.EventTypeWarning, "SpecifiedContainerNotFound", @@ -908,7 +908,7 @@ func (r *Reconciler) generateBackupJobSpecIntent(ctx context.Context, postgresCl // mount additional volumes to the job containers if jobs != nil && jobs.Volumes != nil && len(jobs.Volumes.Additional) > 0 { - missingContainers := addAdditionalVolumesToSpecifiedContainers(&jobSpec.Template, jobs.Volumes.Additional) + missingContainers := AddAdditionalVolumesToSpecifiedContainers(&jobSpec.Template, jobs.Volumes.Additional) if len(missingContainers) > 0 { r.Recorder.Eventf(postgresCluster, corev1.EventTypeWarning, "SpecifiedContainerNotFound", @@ -1408,7 +1408,7 @@ func (r *Reconciler) generateRestoreJobIntent(cluster *v1beta1.PostgresCluster, job.Spec.Template.Spec.PriorityClassName = initialize.FromPointer(dataSource.PriorityClassName) if dataSource.Volumes != nil && len(dataSource.Volumes.Additional) > 0 { - missingContainers := addAdditionalVolumesToSpecifiedContainers(&job.Spec.Template, dataSource.Volumes.Additional) + missingContainers := AddAdditionalVolumesToSpecifiedContainers(&job.Spec.Template, dataSource.Volumes.Additional) if len(missingContainers) > 0 { r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "SpecifiedContainerNotFound", diff --git a/internal/controller/postgrescluster/pgbouncer.go b/internal/controller/postgrescluster/pgbouncer.go index 822f69f57d..46b66cdbf5 100644 --- a/internal/controller/postgrescluster/pgbouncer.go +++ b/internal/controller/postgrescluster/pgbouncer.go @@ -477,7 +477,7 @@ func (r *Reconciler) generatePGBouncerDeployment( // mount additional volumes to the pgbouncer containers if err == nil && cluster.Spec.Proxy.PGBouncer.Volumes != nil && len(cluster.Spec.Proxy.PGBouncer.Volumes.Additional) > 0 { - missingContainers := addAdditionalVolumesToSpecifiedContainers(&deploy.Spec.Template, cluster.Spec.Proxy.PGBouncer.Volumes.Additional) + missingContainers := AddAdditionalVolumesToSpecifiedContainers(&deploy.Spec.Template, cluster.Spec.Proxy.PGBouncer.Volumes.Additional) if len(missingContainers) > 0 { r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "SpecifiedContainerNotFound", diff --git a/internal/controller/postgrescluster/util.go b/internal/controller/postgrescluster/util.go index 2fc849956c..fa021f4626 100644 --- a/internal/controller/postgrescluster/util.go +++ b/internal/controller/postgrescluster/util.go @@ -297,12 +297,12 @@ func AdditionalVolumeMount(name string, readOnly bool) corev1.VolumeMount { } } -// addAdditionalVolumesToSpecifiedContainers adds additional volumes to the specified +// AddAdditionalVolumesToSpecifiedContainers adds additional volumes to the specified // containers in the specified pod -// addAdditionalVolumesToSpecifiedContainers adds the volumes to the pod +// AddAdditionalVolumesToSpecifiedContainers adds the volumes to the pod // as `volumes-` // and adds the directory to the path `/volumes/` -func addAdditionalVolumesToSpecifiedContainers(template *corev1.PodTemplateSpec, +func AddAdditionalVolumesToSpecifiedContainers(template *corev1.PodTemplateSpec, additionalVolumes []v1beta1.AdditionalVolume) []string { missingContainers := []string{} diff --git a/internal/controller/postgrescluster/util_test.go b/internal/controller/postgrescluster/util_test.go index 0dde296aef..2912cd489c 100644 --- a/internal/controller/postgrescluster/util_test.go +++ b/internal/controller/postgrescluster/util_test.go @@ -619,7 +619,7 @@ func TestAddAdditionalVolumesToSpecifiedContainers(t *testing.T) { copyPodTemplate := podTemplate.DeepCopy() - missingContainers := addAdditionalVolumesToSpecifiedContainers( + missingContainers := AddAdditionalVolumesToSpecifiedContainers( copyPodTemplate, tc.additionalVolumes, ) diff --git a/internal/controller/standalone_pgadmin/statefulset.go b/internal/controller/standalone_pgadmin/statefulset.go index b8730b7112..e0a807d47e 100644 --- a/internal/controller/standalone_pgadmin/statefulset.go +++ b/internal/controller/standalone_pgadmin/statefulset.go @@ -26,7 +26,7 @@ func (r *PGAdminReconciler) reconcilePGAdminStatefulSet( ctx context.Context, pgadmin *v1beta1.PGAdmin, configmap *corev1.ConfigMap, dataVolume *corev1.PersistentVolumeClaim, ) error { - sts := statefulset(ctx, pgadmin, configmap, dataVolume) + sts := r.statefulset(ctx, pgadmin, configmap, dataVolume) // Previous versions of PGO used a StatefulSet Pod Management Policy that could leave the Pod // in a failed state. When we see that it has the wrong policy, we will delete the StatefulSet @@ -58,7 +58,7 @@ func (r *PGAdminReconciler) reconcilePGAdminStatefulSet( } // statefulset defines the StatefulSet needed to run pgAdmin. -func statefulset( +func (r *PGAdminReconciler) statefulset( ctx context.Context, pgadmin *v1beta1.PGAdmin, configmap *corev1.ConfigMap, @@ -138,5 +138,15 @@ func statefulset( postgrescluster.AddTMPEmptyDir(&sts.Spec.Template) + // mount additional volumes to the Postgres instance containers + if pgadmin.Spec.Volumes != nil && len(pgadmin.Spec.Volumes.Additional) > 0 { + missingContainers := postgrescluster.AddAdditionalVolumesToSpecifiedContainers(&sts.Spec.Template, pgadmin.Spec.Volumes.Additional) + + if len(missingContainers) > 0 { + r.Recorder.Eventf(pgadmin, corev1.EventTypeWarning, "SpecifiedContainerNotFound", + "The following pgAdmin pod containers were specified for additional volumes but cannot be found: %s.", missingContainers) + } + } + return sts } diff --git a/internal/controller/standalone_pgadmin/statefulset_test.go b/internal/controller/standalone_pgadmin/statefulset_test.go index 9d6b804476..fb401e2603 100644 --- a/internal/controller/standalone_pgadmin/statefulset_test.go +++ b/internal/controller/standalone_pgadmin/statefulset_test.go @@ -14,9 +14,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/crunchydata/postgres-operator/internal/controller/runtime" "github.com/crunchydata/postgres-operator/internal/initialize" "github.com/crunchydata/postgres-operator/internal/naming" "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" ) @@ -219,4 +221,78 @@ tolerations: assert.Assert(t, cmp.MarshalMatches(template.Spec, compare)) }) + + t.Run("verify additional volumes", func(t *testing.T) { + recorder := events.NewRecorder(t, runtime.Scheme) + reconciler.Recorder = recorder + + custompgadmin := new(v1beta1.PGAdmin) + + // add pod level customizations + custompgadmin.Name = "custom-volumes" + custompgadmin.Namespace = ns.Name + + require.UnmarshalInto(t, &custompgadmin.Spec, `{ + dataVolumeClaimSpec: { + accessModes: [ReadWriteOnce], + resources: { requests: { storage: 1Gi } }, + }, + }`) + require.UnmarshalInto(t, &custompgadmin.Spec, `{ + volumes: { + additional: [ + { + "name": "required", + "claimName": "required-1" + } + ], + }, + }`) + + assert.NilError(t, cc.Create(ctx, custompgadmin)) + t.Cleanup(func() { assert.Check(t, cc.Delete(ctx, custompgadmin)) }) + + err := reconciler.reconcilePGAdminStatefulSet(ctx, custompgadmin, configmap, pvc) + assert.NilError(t, err) + + selector, err := naming.AsSelector(metav1.LabelSelector{ + MatchLabels: map[string]string{ + naming.LabelStandalonePGAdmin: custompgadmin.Name, + }, + }) + assert.NilError(t, err) + + list := appsv1.StatefulSetList{} + assert.NilError(t, cc.List(ctx, &list, client.InNamespace(custompgadmin.Namespace), + client.MatchingLabelsSelector{Selector: selector})) + assert.Equal(t, len(list.Items), 1) + + template := list.Items[0].Spec.Template.DeepCopy() + + for _, container := range template.Spec.Containers { + assert.Assert(t, cmp.MarshalContains(container.VolumeMounts, + `- mountPath: /etc/pgadmin/conf.d + name: pgadmin-config + readOnly: true +- mountPath: /var/lib/pgadmin + name: pgadmin-data +- mountPath: /etc/pgadmin + name: pgadmin-config-system + readOnly: true +- mountPath: /tmp + name: tmp +- mountPath: /volumes/required + name: volumes-required`)) + } + + assert.Assert(t, cmp.MarshalContains(template.Spec.Volumes, + ` +- name: volumes-required + persistentVolumeClaim: + claimName: required-1`)) + + // No events created + assert.Equal(t, len(recorder.Events), 0) + + }) } diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/standalone_pgadmin_types.go b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/standalone_pgadmin_types.go index aa5ac90b46..eacf54e365 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/standalone_pgadmin_types.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/standalone_pgadmin_types.go @@ -167,6 +167,19 @@ type PGAdminSpec struct { // https://kubernetes.io/docs/concepts/services-networking/service/ // +optional ServiceName string `json:"serviceName,omitempty"` + + Volumes *PGAdminVolumesSpec `json:"volumes,omitempty"` +} + +// PGAdminVolumesSpec defines the configuration for pgAdmin additional volumes +type PGAdminVolumesSpec struct { + // Additional pre-existing volumes to add to the pod. + // --- + // +optional + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=10 + Additional []AdditionalVolume `json:"additional,omitempty"` } // +kubebuilder:validation:XValidation:rule=`[has(self.postgresClusterName),has(self.postgresClusterSelector)].exists_one(x,x)`,message=`exactly one of "postgresClusterName" or "postgresClusterSelector" is required` diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go index dbb22d8f38..8670e751a6 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/zz_generated.deepcopy.go @@ -1038,6 +1038,11 @@ func (in *PGAdminSpec) DeepCopyInto(out *PGAdminSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = new(PGAdminVolumesSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PGAdminSpec. @@ -1092,6 +1097,28 @@ func (in *PGAdminUser) DeepCopy() *PGAdminUser { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PGAdminVolumesSpec) DeepCopyInto(out *PGAdminVolumesSpec) { + *out = *in + if in.Additional != nil { + in, out := &in.Additional, &out.Additional + *out = make([]AdditionalVolume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PGAdminVolumesSpec. +func (in *PGAdminVolumesSpec) DeepCopy() *PGAdminVolumesSpec { + if in == nil { + return nil + } + out := new(PGAdminVolumesSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PGBackRestArchive) DeepCopyInto(out *PGBackRestArchive) { *out = *in