diff --git a/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml b/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml index 9fe1ccf439..a1e6a6a207 100644 --- a/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml +++ b/config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml @@ -11027,6 +11027,44 @@ spec: type: array volumes: properties: + additional: + description: Additional pre-existing volumes to add to the + pod. + items: + properties: + claimName: + description: A reference to a preexisting PVC. + type: string + containers: + description: |- + The containers to attach this volume to. + A blank/unset `Containers` field matches all containers. + items: + type: string + maxItems: 10 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + The name of the volume used for mounting path. + Volumes are mounted in the pods at `volumes/` + Must be unique. + 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 + readOnly: + description: Sets the write/read mode of the volume + type: boolean + required: + - claimName + - name + type: object + maxItems: 10 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map temp: description: |- An ephemeral volume for temporary files. @@ -29598,6 +29636,44 @@ spec: type: array volumes: properties: + additional: + description: Additional pre-existing volumes to add to the + pod. + items: + properties: + claimName: + description: A reference to a preexisting PVC. + type: string + containers: + description: |- + The containers to attach this volume to. + A blank/unset `Containers` field matches all containers. + items: + type: string + maxItems: 10 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + The name of the volume used for mounting path. + Volumes are mounted in the pods at `volumes/` + Must be unique. + 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 + readOnly: + description: Sets the write/read mode of the volume + type: boolean + required: + - claimName + - name + type: object + maxItems: 10 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map temp: description: |- An ephemeral volume for temporary files. diff --git a/internal/controller/postgrescluster/instance.go b/internal/controller/postgrescluster/instance.go index 0c91ca7157..9aff529318 100644 --- a/internal/controller/postgrescluster/instance.go +++ b/internal/controller/postgrescluster/instance.go @@ -1253,6 +1253,11 @@ func (r *Reconciler) reconcileInstance( addDevSHM(&instance.Spec.Template) } + // mount additional volumes to the Postgres instance containers + if err == nil && spec.Volumes != nil && len(spec.Volumes.Additional) > 0 { + addAdditionalVolumesToSpecifiedContainers(&instance.Spec.Template, spec.Volumes.Additional) + } + if err == nil { err = errors.WithStack(r.apply(ctx, instance)) } diff --git a/internal/controller/postgrescluster/util.go b/internal/controller/postgrescluster/util.go index a1ba6ce087..ea4c187dd7 100644 --- a/internal/controller/postgrescluster/util.go +++ b/internal/controller/postgrescluster/util.go @@ -13,9 +13,11 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/sets" "github.com/crunchydata/postgres-operator/internal/initialize" "github.com/crunchydata/postgres-operator/internal/naming" + "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) var tmpDirSizeLimit = resource.MustParse("16Mi") @@ -285,3 +287,61 @@ func safeHash32(content func(w io.Writer) error) (string, error) { } return rand.SafeEncodeString(fmt.Sprint(hash.Sum32())), nil } + +// AdditionalVolumeMount returns the name and mount path of the additional volume. +func AdditionalVolumeMount(name string, readOnly bool) corev1.VolumeMount { + return corev1.VolumeMount{ + Name: fmt.Sprintf("volumes-%s", name), + MountPath: "/volumes/" + name, + ReadOnly: readOnly, + } +} + +// addAdditionalVolumesToSpecifiedContainers adds additional volumes to the specified +// containers in the specified pod +// addAdditionalVolumesToSpecifiedContainers adds the volumes to the pod +// as `volumes-` +// and adds the directory to the path `/volumes/` +func addAdditionalVolumesToSpecifiedContainers(template *corev1.PodTemplateSpec, + additionalVolumes []v1beta1.AdditionalVolume) { + + for _, additionalVolumeRequest := range additionalVolumes { + + additionalVolumeMount := AdditionalVolumeMount( + additionalVolumeRequest.Name, + additionalVolumeRequest.ReadOnly, + ) + + additionalVolume := corev1.Volume{ + Name: additionalVolumeMount.Name, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: additionalVolumeRequest.ClaimName, + ReadOnly: additionalVolumeMount.ReadOnly, + }, + }, + } + + names := sets.New(additionalVolumeRequest.Containers...) + + for i := range template.Spec.Containers { + if names.Len() == 0 || names.Has(template.Spec.Containers[i].Name) { + template.Spec.Containers[i].VolumeMounts = append( + template.Spec.Containers[i].VolumeMounts, + additionalVolumeMount) + } + } + + for i := range template.Spec.InitContainers { + if names.Len() == 0 || names.Has(template.Spec.InitContainers[i].Name) { + template.Spec.InitContainers[i].VolumeMounts = append( + template.Spec.InitContainers[i].VolumeMounts, + additionalVolumeMount) + } + } + + template.Spec.Volumes = append( + template.Spec.Volumes, + additionalVolume) + } +} diff --git a/internal/controller/postgrescluster/util_test.go b/internal/controller/postgrescluster/util_test.go index 8e7d5c434f..47193c7dba 100644 --- a/internal/controller/postgrescluster/util_test.go +++ b/internal/controller/postgrescluster/util_test.go @@ -16,6 +16,7 @@ import ( "github.com/crunchydata/postgres-operator/internal/naming" "github.com/crunchydata/postgres-operator/internal/testing/cmp" + "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1" ) func TestSafeHash32(t *testing.T) { @@ -378,3 +379,170 @@ func TestJobFailed(t *testing.T) { }) } } + +func TestAddAdditionalVolumesToSpecifiedContainers(t *testing.T) { + + podTemplate := &corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "startup"}, + {Name: "config"}, + }, + Containers: []corev1.Container{ + {Name: "database"}, + {Name: "other"}, + }}} + + testCases := []struct { + tcName string + additionalVolumes []v1beta1.AdditionalVolume + expectedContainers string + expectedInitContainers string + expectedVolumes string + }{{ + tcName: "all", + additionalVolumes: []v1beta1.AdditionalVolume{{ + Containers: []string{}, + ClaimName: "required", + Name: "required", + }}, + expectedContainers: `- name: database + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required +- name: other + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required`, + expectedInitContainers: `- name: startup + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required +- name: config + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required`, + expectedVolumes: `- name: volumes-required + persistentVolumeClaim: + claimName: required`, + }, { + tcName: "multiple additional volumes", + additionalVolumes: []v1beta1.AdditionalVolume{{ + Containers: []string{}, + ClaimName: "required", + Name: "required", + }, { + Containers: []string{}, + ClaimName: "also", + Name: "other", + }}, + expectedContainers: `- name: database + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required + - mountPath: /volumes/other + name: volumes-other +- name: other + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required + - mountPath: /volumes/other + name: volumes-other`, + expectedInitContainers: `- name: startup + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required + - mountPath: /volumes/other + name: volumes-other +- name: config + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required + - mountPath: /volumes/other + name: volumes-other`, + expectedVolumes: `- name: volumes-required + persistentVolumeClaim: + claimName: required +- name: volumes-other + persistentVolumeClaim: + claimName: also`, + }, { + tcName: "database and startup containers only", + additionalVolumes: []v1beta1.AdditionalVolume{{ + Containers: []string{"database", "startup"}, + ClaimName: "required", + Name: "required", + }}, + expectedContainers: `- name: database + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required +- name: other + resources: {}`, + expectedInitContainers: `- name: startup + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required +- name: config + resources: {}`, + expectedVolumes: `- name: volumes-required + persistentVolumeClaim: + claimName: required`, + }, { + tcName: "readonly", + additionalVolumes: []v1beta1.AdditionalVolume{{ + Containers: []string{"database"}, + ClaimName: "required", + Name: "required", + ReadOnly: true, + }}, + expectedContainers: `- name: database + resources: {} + volumeMounts: + - mountPath: /volumes/required + name: volumes-required + readOnly: true +- name: other + resources: {}`, + expectedInitContainers: `- name: startup + resources: {} +- name: config + resources: {}`, + expectedVolumes: `- name: volumes-required + persistentVolumeClaim: + claimName: required + readOnly: true`, + }} + + for _, tc := range testCases { + t.Run(tc.tcName, func(t *testing.T) { + + copyPodTemplate := podTemplate.DeepCopy() + + addAdditionalVolumesToSpecifiedContainers( + copyPodTemplate, + tc.additionalVolumes, + ) + + assert.Assert(t, cmp.MarshalMatches( + copyPodTemplate.Spec.Containers, + tc.expectedContainers)) + assert.Assert(t, cmp.MarshalMatches( + copyPodTemplate.Spec.InitContainers, + tc.expectedInitContainers)) + assert.Assert(t, cmp.MarshalMatches( + copyPodTemplate.Spec.Volumes, + tc.expectedVolumes)) + }) + } +} diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1/postgrescluster_types.go b/pkg/apis/postgres-operator.crunchydata.com/v1/postgrescluster_types.go index abd23670c3..1778fdefaf 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1/postgrescluster_types.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1/postgrescluster_types.go @@ -534,6 +534,14 @@ type PostgresVolumesSpec struct { // --- // +optional Temp *v1beta1.VolumeClaimSpec `json:"temp,omitempty"` + + // Additional pre-existing volumes to add to the pod. + // --- + // +optional + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=10 + Additional []v1beta1.AdditionalVolume `json:"additional,omitempty"` } type TablespaceVolume struct { diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1/zz_generated.deepcopy.go b/pkg/apis/postgres-operator.crunchydata.com/v1/zz_generated.deepcopy.go index 94a6ed3389..b296067c9b 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1/zz_generated.deepcopy.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1/zz_generated.deepcopy.go @@ -656,6 +656,13 @@ func (in *PostgresVolumesSpec) DeepCopyInto(out *PostgresVolumesSpec) { in, out := &in.Temp, &out.Temp *out = (*in).DeepCopy() } + if in.Additional != nil { + in, out := &in.Additional, &out.Additional + *out = make([]v1beta1.AdditionalVolume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgresVolumesSpec. diff --git a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go index 07c6d4c805..e434816cb4 100644 --- a/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go +++ b/pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go @@ -531,6 +531,44 @@ type PostgresVolumesSpec struct { // --- // +optional Temp *VolumeClaimSpec `json:"temp,omitempty"` + + // Additional pre-existing volumes to add to the pod. + // --- + // +optional + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=10 + Additional []AdditionalVolume `json:"additional,omitempty"` +} + +type AdditionalVolume struct { + // The name of the volume used for mounting path. + // Volumes are mounted in the pods at `volumes/` + // Must be unique. + // --- + // The `Name` field is a `DNS1123Subdomain` type to enforce + // the max length and also allow us to more easily transition + // to CPK-provisioned volumes. + // +required + Name DNS1123Subdomain `json:"name"` + + // A reference to a preexisting PVC. + // --- + // +required + ClaimName string `json:"claimName"` + + // The containers to attach this volume to. + // A blank/unset `Containers` field matches all containers. + // --- + // +optional + // +listType=atomic + // +kubebuilder:validation:MaxItems=10 + Containers []string `json:"containers,omitempty"` + + // Sets the write/read mode of the volume + // --- + // +optional + ReadOnly bool `json:"readOnly,omitempty"` } type TablespaceVolume struct { 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 747e363854..b6b32c63b0 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 @@ -33,6 +33,26 @@ func (in *APIResponses) DeepCopy() *APIResponses { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalVolume) DeepCopyInto(out *AdditionalVolume) { + *out = *in + if in.Containers != nil { + in, out := &in.Containers, &out.Containers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalVolume. +func (in *AdditionalVolume) DeepCopy() *AdditionalVolume { + if in == nil { + return nil + } + out := new(AdditionalVolume) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackupJobs) DeepCopyInto(out *BackupJobs) { *out = *in @@ -2514,6 +2534,13 @@ func (in *PostgresVolumesSpec) DeepCopyInto(out *PostgresVolumesSpec) { in, out := &in.Temp, &out.Temp *out = (*in).DeepCopy() } + 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 PostgresVolumesSpec. diff --git a/testing/kuttl/e2e/additional-volumes/00--cluster.yaml b/testing/kuttl/e2e/additional-volumes/00--cluster.yaml new file mode 100644 index 0000000000..801a22d460 --- /dev/null +++ b/testing/kuttl/e2e/additional-volumes/00--cluster.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- files/00-create-cluster.yaml +assert: +- files/00-cluster-created.yaml diff --git a/testing/kuttl/e2e/additional-volumes/00-assert.yaml b/testing/kuttl/e2e/additional-volumes/00-assert.yaml new file mode 100644 index 0000000000..f54a5d7d4e --- /dev/null +++ b/testing/kuttl/e2e/additional-volumes/00-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: +- type: command + command: kubectl -n $NAMESPACE describe pods --selector postgres-operator.crunchydata.com/cluster=additional-vols +- type: command + command: kubectl -n $NAMESPACE describe pvcs --selector created=before +- namespace: $NAMESPACE + selector: postgres-operator.crunchydata.com/cluster=additional-vols diff --git a/testing/kuttl/e2e/additional-volumes/01-assert.yaml b/testing/kuttl/e2e/additional-volumes/01-assert.yaml new file mode 100644 index 0000000000..e30e5897bf --- /dev/null +++ b/testing/kuttl/e2e/additional-volumes/01-assert.yaml @@ -0,0 +1,65 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + retry() { bash -ceu 'printf "$1\nSleeping...\n" && sleep 5' - "$@"; } + check_containers_ready() { bash -ceu 'echo "$1" | jq -e ".[] | select(.type==\"ContainersReady\") | .status==\"True\""' - "$@"; } + + pod=$(kubectl get pods -o name -n "${NAMESPACE}" \ + -l postgres-operator.crunchydata.com/cluster=additional-vols) + [ "$pod" = "" ] && retry "Pod not found" && exit 1 + + condition_json=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.conditions}") + [ "$condition_json" = "" ] && retry "conditions not found" && exit 1 + { check_containers_ready "$condition_json"; } || { + retry "containers not ready" + exit 1 + } + + extra_mounted_in_database=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.containerStatuses[?(@.name=='database')].volumeMounts[?(@.name=='volumes-extra')]}") + + [ "$extra_mounted_in_database" = "" ] && (echo "extra not found in database" && exit 1) + + ubiq_mounted_in_database=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.containerStatuses[?(@.name=='database')].volumeMounts[?(@.name=='volumes-ubiq')]}") + + [ "$ubiq_mounted_in_database" = "" ] && (echo "ubiq not found in database" && exit 1) + + extra_mounted_in_rcc=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.containerStatuses[?(@.name=='replication-cert-copy')].volumeMounts[?(@.name=='volumes-extra')]}") + + [ "$extra_mounted_in_rcc" = "" ] || (echo "extra found in rcc" && exit 1) + + ubiq_mounted_in_rcc=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.containerStatuses[?(@.name=='replication-cert-copy')].volumeMounts[?(@.name=='volumes-ubiq')]}") + + [ "$ubiq_mounted_in_rcc" = "" ] && (echo "ubiq not found in rcc" && exit 1) + + extra_mounted_in_nss=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.initContainerStatuses[?(@.name=='nss-wrapper-init')].volumeMounts[?(@.name=='volumes-extra')]}") + + [ "$extra_mounted_in_nss" = "" ] && (echo "extra not found in nss" && exit 1) + + ubiq_mounted_in_nss=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.initContainerStatuses[?(@.name=='nss-wrapper-init')].volumeMounts[?(@.name=='volumes-ubiq')]}") + + [ "$ubiq_mounted_in_nss" = "" ] && (echo "ubiq not found in nss" && exit 1) + + extra_mounted_in_startup=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.initContainerStatuses[?(@.name=='postgres-startup')].volumeMounts[?(@.name=='volumes-extra')]}") + + [ "$extra_mounted_in_startup" = "" ] || (echo "extra found in startup" && exit 1) + + ubiq_mounted_in_startup=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.status.initContainerStatuses[?(@.name=='postgres-startup')].volumeMounts[?(@.name=='volumes-ubiq')]}") + + [ "$ubiq_mounted_in_startup" = "" ] && (echo "ubiq not found in startup" && exit 1) + + extra_isnt_readonly=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.spec.volumes[?(@.name=='volumes-extra')].persistentVolumeClaim.readOnly}") + + [ "$extra_isnt_readonly" = "" ] || (echo "extra is readonly" && exit 1) + + ubiq_is_readonly=$(kubectl get "${pod}" -n "${NAMESPACE}" -o jsonpath="{.spec.volumes[?(@.name=='volumes-ubiq')].persistentVolumeClaim.readOnly}") + + [ "$ubiq_is_readonly" = "" ] && (echo "ubiq is not readonly" && exit 1) + + echo "clean bill of health" + +collectors: +- type: command + command: kubectl -n $NAMESPACE describe pods --selector postgres-operator.crunchydata.com/cluster=additional-vols +- namespace: $NAMESPACE + selector: postgres-operator.crunchydata.com/cluster=additional-vols diff --git a/testing/kuttl/e2e/additional-volumes/files/00-cluster-created.yaml b/testing/kuttl/e2e/additional-volumes/files/00-cluster-created.yaml new file mode 100644 index 0000000000..8209935728 --- /dev/null +++ b/testing/kuttl/e2e/additional-volumes/files/00-cluster-created.yaml @@ -0,0 +1,29 @@ +apiVersion: postgres-operator.crunchydata.com/v1beta1 +kind: PostgresCluster +metadata: + name: additional-vols +status: + instances: + - name: instance1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: additional-vols-primary +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: additional-vol +status: + phase: Bound +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: additional-vol-2 +status: + phase: Bound \ No newline at end of file diff --git a/testing/kuttl/e2e/additional-volumes/files/00-create-cluster.yaml b/testing/kuttl/e2e/additional-volumes/files/00-create-cluster.yaml new file mode 100644 index 0000000000..bc807757b6 --- /dev/null +++ b/testing/kuttl/e2e/additional-volumes/files/00-create-cluster.yaml @@ -0,0 +1,48 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: additional-vol + label: + created: before +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: additional-vol-2 + label: + created: before +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: postgres-operator.crunchydata.com/v1beta1 +kind: PostgresCluster +metadata: + name: additional-vols +spec: + postgresVersion: ${KUTTL_PG_VERSION} + instances: + - name: instance1 + volumes: + additional: + - name: extra + containers: [database, nss-wrapper-init] + claimName: additional-vol + - name: ubiq + claimName: additional-vol-2 + readOnly: true + dataVolumeClaimSpec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: 1Gi