Skip to content

Commit 66556c4

Browse files
committed
Add tablespace functionality
1 parent a9b7912 commit 66556c4

File tree

9 files changed

+332
-7
lines changed

9 files changed

+332
-7
lines changed

internal/controller/postgrescluster/instance.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,7 @@ func (r *Reconciler) reconcileInstance(
10711071
instanceCertificates *corev1.Secret
10721072
postgresDataVolume *corev1.PersistentVolumeClaim
10731073
postgresWALVolume *corev1.PersistentVolumeClaim
1074+
tablespaceVolumes []*corev1.PersistentVolumeClaim
10741075
)
10751076

10761077
if err == nil {
@@ -1086,11 +1087,14 @@ func (r *Reconciler) reconcileInstance(
10861087
if err == nil {
10871088
postgresWALVolume, err = r.reconcilePostgresWALVolume(ctx, cluster, spec, instance, observed, clusterVolumes)
10881089
}
1090+
if err == nil {
1091+
tablespaceVolumes, err = r.reconcileTablespaceVolumes(ctx, cluster, spec, instance, clusterVolumes)
1092+
}
10891093
if err == nil {
10901094
postgres.InstancePod(
10911095
ctx, cluster, spec,
10921096
primaryCertificate, replicationCertSecretProjection(clusterReplicationSecret),
1093-
postgresDataVolume, postgresWALVolume,
1097+
postgresDataVolume, postgresWALVolume, tablespaceVolumes,
10941098
&instance.Spec.Template.Spec)
10951099

10961100
addPGBackRestToInstancePodSpec(

internal/controller/postgrescluster/postgres.go

+74
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,80 @@ func (r *Reconciler) reconcilePostgresDataVolume(
561561
return pvc, err
562562
}
563563

564+
// TODO think about multiple tablespace volumes
565+
566+
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=create;patch
567+
568+
// reconcileTablespaceVolumes writes the PersistentVolumeClaims for instance's
569+
// tablespace data volumes.
570+
func (r *Reconciler) reconcileTablespaceVolumes(
571+
ctx context.Context, cluster *v1beta1.PostgresCluster,
572+
instanceSpec *v1beta1.PostgresInstanceSetSpec, instance *appsv1.StatefulSet,
573+
clusterVolumes []corev1.PersistentVolumeClaim,
574+
) (tablespaceVolumes []*corev1.PersistentVolumeClaim, err error) {
575+
576+
if !util.DefaultMutableFeatureGate.Enabled(util.TablespaceVolumes) {
577+
return
578+
}
579+
580+
if instanceSpec.TablespaceVolumes == nil {
581+
return
582+
}
583+
584+
for _, vol := range instanceSpec.TablespaceVolumes {
585+
labelMap := map[string]string{
586+
naming.LabelCluster: cluster.Name,
587+
naming.LabelInstanceSet: instanceSpec.Name,
588+
naming.LabelInstance: instance.Name,
589+
naming.LabelRole: "tablespace",
590+
naming.LabelData: vol.Name,
591+
}
592+
593+
var pvc *corev1.PersistentVolumeClaim
594+
existingPVCName, err := getPGPVCName(labelMap, clusterVolumes)
595+
if err != nil {
596+
return nil, errors.WithStack(err)
597+
}
598+
if existingPVCName != "" {
599+
pvc = &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{
600+
Namespace: cluster.GetNamespace(),
601+
Name: existingPVCName,
602+
}}
603+
} else {
604+
pvc = &corev1.PersistentVolumeClaim{ObjectMeta: naming.InstanceTablesapceDataVolume(instance, vol.Name)}
605+
}
606+
607+
pvc.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"))
608+
609+
err = errors.WithStack(r.setControllerReference(cluster, pvc))
610+
611+
pvc.Annotations = naming.Merge(
612+
cluster.Spec.Metadata.GetAnnotationsOrNil(),
613+
instanceSpec.Metadata.GetAnnotationsOrNil())
614+
615+
pvc.Labels = naming.Merge(
616+
cluster.Spec.Metadata.GetLabelsOrNil(),
617+
instanceSpec.Metadata.GetLabelsOrNil(),
618+
labelMap,
619+
)
620+
621+
pvc.Spec = vol.DataVolumeClaimSpec
622+
623+
if err == nil {
624+
err = r.handlePersistentVolumeClaimError(cluster,
625+
errors.WithStack(r.apply(ctx, pvc)))
626+
}
627+
628+
if err != nil {
629+
return nil, err
630+
}
631+
632+
tablespaceVolumes = append(tablespaceVolumes, pvc)
633+
}
634+
635+
return
636+
}
637+
564638
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get
565639
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=create;delete;patch
566640

internal/naming/names.go

+13
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,19 @@ func InstancePostgresDataVolume(instance *appsv1.StatefulSet) metav1.ObjectMeta
325325
}
326326
}
327327

328+
// TODO(benjaminjb): We need to consider shortening the PVC name
329+
// or at least need to document so that people can avoid too-long names
330+
// InstanceTablesapceDataVolume returns the ObjectMeta for the tablespace data
331+
// volume for instance.
332+
func InstanceTablesapceDataVolume(instance *appsv1.StatefulSet, tablespaceName string) metav1.ObjectMeta {
333+
return metav1.ObjectMeta{
334+
Namespace: instance.GetNamespace(),
335+
Name: instance.GetName() +
336+
"-" + tablespaceName +
337+
"-tablespace",
338+
}
339+
}
340+
328341
// InstancePostgresWALVolume returns the ObjectMeta for the PostgreSQL WAL
329342
// volume for instance.
330343
func InstancePostgresWALVolume(instance *appsv1.StatefulSet) metav1.ObjectMeta {

internal/pgbackrest/reconcile.go

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/crunchydata/postgres-operator/internal/naming"
2929
"github.com/crunchydata/postgres-operator/internal/pki"
3030
"github.com/crunchydata/postgres-operator/internal/postgres"
31+
"github.com/crunchydata/postgres-operator/internal/util"
3132
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
3233
)
3334

@@ -302,6 +303,14 @@ func addServerContainerAndVolume(
302303
postgres.DataVolumeMount().Name: postgres.DataVolumeMount(),
303304
postgres.WALVolumeMount().Name: postgres.WALVolumeMount(),
304305
}
306+
if util.DefaultMutableFeatureGate.Enabled(util.TablespaceVolumes) {
307+
for _, instance := range cluster.Spec.InstanceSets {
308+
for _, vol := range instance.TablespaceVolumes {
309+
tablespaceVolumeMount := postgres.TablespaceVolumeMount(vol.Name)
310+
postgresMounts[postgres.TablespaceVolumeMount(vol.Name).Name] = tablespaceVolumeMount
311+
}
312+
}
313+
}
305314
for i := range pod.Volumes {
306315
if mount, ok := postgresMounts[pod.Volumes[i].Name]; ok {
307316
container.VolumeMounts = append(container.VolumeMounts, mount)

internal/pgbackrest/reconcile_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
"github.com/crunchydata/postgres-operator/internal/naming"
3232
"github.com/crunchydata/postgres-operator/internal/pki"
33+
"github.com/crunchydata/postgres-operator/internal/util"
3334
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
3435
)
3536

@@ -522,6 +523,7 @@ func TestAddServerToInstancePod(t *testing.T) {
522523
}
523524

524525
t.Run("CustomResources", func(t *testing.T) {
526+
assert.NilError(t, util.AddAndSetFeatureGates(string(util.TablespaceVolumes+"=false")))
525527
cluster := cluster.DeepCopy()
526528
cluster.Spec.Backups.PGBackRest.Sidecars = &v1beta1.PGBackRestSidecars{
527529
PGBackRest: &v1beta1.Sidecar{
@@ -647,6 +649,105 @@ func TestAddServerToInstancePod(t *testing.T) {
647649
name: instance-secret-name
648650
`))
649651
})
652+
653+
t.Run("AddTablespaces", func(t *testing.T) {
654+
assert.NilError(t, util.AddAndSetFeatureGates(string(util.TablespaceVolumes+"=true")))
655+
clusterWithTablespaces := cluster.DeepCopy()
656+
clusterWithTablespaces.Spec.InstanceSets = []v1beta1.PostgresInstanceSetSpec{
657+
{
658+
TablespaceVolumes: []v1beta1.TablespaceVolume{
659+
{Name: "trial"},
660+
{Name: "castle"},
661+
},
662+
},
663+
}
664+
665+
out := pod.DeepCopy()
666+
out.Volumes = append(out.Volumes, corev1.Volume{Name: "trial"}, corev1.Volume{Name: "castle"})
667+
AddServerToInstancePod(clusterWithTablespaces, out, "instance-secret-name")
668+
669+
// Only Containers and Volumes fields have changed.
670+
assert.DeepEqual(t, pod, *out, cmpopts.IgnoreFields(pod, "Containers", "Volumes"))
671+
assert.Assert(t, marshalMatches(out.Containers, `
672+
- name: database
673+
resources: {}
674+
- name: other
675+
resources: {}
676+
- command:
677+
- pgbackrest
678+
- server
679+
livenessProbe:
680+
exec:
681+
command:
682+
- pgbackrest
683+
- server-ping
684+
name: pgbackrest
685+
resources: {}
686+
securityContext:
687+
allowPrivilegeEscalation: false
688+
capabilities:
689+
drop:
690+
- ALL
691+
privileged: false
692+
readOnlyRootFilesystem: true
693+
runAsNonRoot: true
694+
volumeMounts:
695+
- mountPath: /etc/pgbackrest/server
696+
name: pgbackrest-server
697+
readOnly: true
698+
- mountPath: /pgdata
699+
name: postgres-data
700+
- mountPath: /pgwal
701+
name: postgres-wal
702+
- mountPath: /tablespaces/trial
703+
name: trial
704+
- mountPath: /tablespaces/castle
705+
name: castle
706+
- command:
707+
- bash
708+
- -ceu
709+
- --
710+
- |-
711+
monitor() {
712+
exec {fd}<> <(:)
713+
until read -r -t 5 -u "${fd}"; do
714+
if
715+
[ "${filename}" -nt "/proc/self/fd/${fd}" ] &&
716+
pkill -HUP --exact --parent=0 pgbackrest
717+
then
718+
exec {fd}>&- && exec {fd}<> <(:)
719+
stat --dereference --format='Loaded configuration dated %y' "${filename}"
720+
elif
721+
{ [ "${directory}" -nt "/proc/self/fd/${fd}" ] ||
722+
[ "${authority}" -nt "/proc/self/fd/${fd}" ]
723+
} &&
724+
pkill -HUP --exact --parent=0 pgbackrest
725+
then
726+
exec {fd}>&- && exec {fd}<> <(:)
727+
stat --format='Loaded certificates dated %y' "${directory}"
728+
fi
729+
done
730+
}; export directory="$1" authority="$2" filename="$3"; export -f monitor; exec -a "$0" bash -ceu monitor
731+
- pgbackrest-config
732+
- /etc/pgbackrest/server
733+
- /etc/pgbackrest/conf.d/~postgres-operator/tls-ca.crt
734+
- /etc/pgbackrest/conf.d/~postgres-operator_server.conf
735+
name: pgbackrest-config
736+
resources: {}
737+
securityContext:
738+
allowPrivilegeEscalation: false
739+
capabilities:
740+
drop:
741+
- ALL
742+
privileged: false
743+
readOnlyRootFilesystem: true
744+
runAsNonRoot: true
745+
volumeMounts:
746+
- mountPath: /etc/pgbackrest/server
747+
name: pgbackrest-server
748+
readOnly: true
749+
`))
750+
})
650751
}
651752

652753
func TestAddServerToRepoPod(t *testing.T) {

internal/postgres/config.go

+29
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
corev1 "k8s.io/api/core/v1"
2323

2424
"github.com/crunchydata/postgres-operator/internal/naming"
25+
"github.com/crunchydata/postgres-operator/internal/util"
2526
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
2627
)
2728

@@ -62,6 +63,9 @@ safelink() (
6263
// dataMountPath is where to mount the main data volume.
6364
dataMountPath = "/pgdata"
6465

66+
// dataMountPath is where to mount the main data volume.
67+
tablespaceMountPath = "/tablespaces"
68+
6569
// walMountPath is where to mount the optional WAL volume.
6670
walMountPath = "/pgwal"
6771

@@ -201,6 +205,30 @@ func startupCommand(
201205
version := fmt.Sprint(cluster.Spec.PostgresVersion)
202206
walDir := WALDirectory(cluster, instance)
203207

208+
// If the user requests tablespaces, we want to create and chmod the directories
209+
tablespaceCmd := ""
210+
if util.DefaultMutableFeatureGate.Enabled(util.TablespaceVolumes) && instance.TablespaceVolumes != nil {
211+
// This command checks if a dir exists and if not, creates it;
212+
// if the dir does exist, then we `recreate` it to make sure the owner is correct;
213+
// but if the dir exists with the wrong owner and is not writeable, we error.
214+
// This is the same behavior we use for the main PGDATA directory.
215+
// The path for tablespaces volumes is /tablespaces/NAME/data
216+
// -- the `data` path is added so that we can arrange the permissions.
217+
checkInstallRecreateCmd := strings.Join([]string{
218+
`if [[ ! -e "${tablespace_dir}" || -O "${tablespace_dir}" ]]; then`,
219+
`install --directory --mode=0700 "${tablespace_dir}"`,
220+
`elif [[ -w "${tablespace_dir}" && -g "${tablespace_dir}" ]]; then`,
221+
`recreate "${tablespace_dir}" '0700'`,
222+
`else (halt Permissions!); fi ||`,
223+
`halt "$(permissions "${tablespace_dir}" ||:)"`,
224+
}, "\n")
225+
226+
for _, tablespace := range instance.TablespaceVolumes {
227+
tablespaceCmd = tablespaceCmd + "\ntablespace_dir=/tablespaces/" + tablespace.Name + "/data" + "\n" +
228+
checkInstallRecreateCmd
229+
}
230+
}
231+
204232
args := []string{version, walDir, naming.PGBackRestPGDataLogPath}
205233
script := strings.Join([]string{
206234
`declare -r expected_major_version="$1" pgwal_directory="$2" pgbrLog_directory="$3"`,
@@ -287,6 +315,7 @@ func startupCommand(
287315
naming.ReplicationCert, naming.ReplicationPrivateKey,
288316
naming.ReplicationCACert),
289317

318+
tablespaceCmd,
290319
// When the data directory is empty, there's nothing more to do.
291320
`[ -f "${postgres_data_directory}/PG_VERSION" ] || exit 0`,
292321

internal/postgres/reconcile.go

+27
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func DataVolumeMount() corev1.VolumeMount {
3838
return corev1.VolumeMount{Name: "postgres-data", MountPath: dataMountPath}
3939
}
4040

41+
// TablespaceVolumeMount returns the name and mount path of the PostgreSQL tablespace data volume.
42+
func TablespaceVolumeMount(tablespaceName string) corev1.VolumeMount {
43+
return corev1.VolumeMount{Name: tablespaceName, MountPath: tablespaceMountPath + "/" + tablespaceName}
44+
}
45+
4146
// WALVolumeMount returns the name and mount path of the PostgreSQL WAL volume.
4247
func WALVolumeMount() corev1.VolumeMount {
4348
return corev1.VolumeMount{Name: "postgres-wal", MountPath: walMountPath}
@@ -68,6 +73,7 @@ func InstancePod(ctx context.Context,
6873
inInstanceSpec *v1beta1.PostgresInstanceSetSpec,
6974
inClusterCertificates, inClientCertificates *corev1.SecretProjection,
7075
inDataVolume, inWALVolume *corev1.PersistentVolumeClaim,
76+
inTablespaceVolumes []*corev1.PersistentVolumeClaim,
7177
outInstancePod *corev1.PodSpec,
7278
) {
7379
certVolumeMount := corev1.VolumeMount{
@@ -218,6 +224,27 @@ func InstancePod(ctx context.Context,
218224
downwardAPIVolume,
219225
}
220226

227+
// If `TablespaceVolumes` FeatureGate is enabled, add any tablespace volumes to the pod, and
228+
// add volumeMounts to the database and startup containers
229+
if util.DefaultMutableFeatureGate.Enabled(util.TablespaceVolumes) &&
230+
inTablespaceVolumes != nil {
231+
for _, vol := range inTablespaceVolumes {
232+
tablespaceVolumeMount := TablespaceVolumeMount(vol.Labels["postgres-operator.crunchydata.com/data"])
233+
tablespaceVolume := corev1.Volume{
234+
Name: tablespaceVolumeMount.Name,
235+
VolumeSource: corev1.VolumeSource{
236+
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
237+
ClaimName: vol.Name,
238+
ReadOnly: false,
239+
},
240+
},
241+
}
242+
outInstancePod.Volumes = append(outInstancePod.Volumes, tablespaceVolume)
243+
container.VolumeMounts = append(container.VolumeMounts, tablespaceVolumeMount)
244+
startup.VolumeMounts = append(startup.VolumeMounts, tablespaceVolumeMount)
245+
}
246+
}
247+
221248
if len(inCluster.Spec.Config.Files) != 0 {
222249
additionalConfigVolumeMount := AdditionalConfigVolumeMount()
223250
additionalConfigVolume := corev1.Volume{Name: additionalConfigVolumeMount.Name}

0 commit comments

Comments
 (0)