Skip to content

Commit 2d2ce61

Browse files
jcroodJohn Rood
andauthored
Add volume selector (zalando#1385)
* Add volume selector * Add slightly better documentation and gofmt changes * Update generated deepcopy * Add test for PV selector Co-authored-by: John Rood <[email protected]>
1 parent 1b3366e commit 2d2ce61

File tree

9 files changed

+215
-25
lines changed

9 files changed

+215
-25
lines changed

charts/postgres-operator/crds/postgresqls.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,24 @@ spec:
561561
properties:
562562
iops:
563563
type: integer
564+
selector:
565+
type: object
566+
properties:
567+
matchExpressions:
568+
type: array
569+
items:
570+
type: object
571+
properties:
572+
key:
573+
type: string
574+
operator:
575+
type: string
576+
values:
577+
type: array
578+
items:
579+
type: string
580+
matchLabels:
581+
type: object
564582
size:
565583
type: string
566584
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'

docs/reference/cluster_manifest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,11 @@ properties of the persistent storage that stores Postgres data.
399399
When running the operator on AWS the latest generation of EBS volumes (`gp3`)
400400
allows for configuring the throughput in MB/s. Maximum is 1000. Optional.
401401

402+
* **selector**
403+
A label query over PVs to consider for binding. See the [Kubernetes
404+
documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
405+
for details on using `matchLabels` and `matchExpressions`. Optional
406+
402407
## Sidecar definitions
403408

404409
Those parameters are defined under the `sidecars` key. They consist of a list

manifests/complete-postgres-manifest.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ spec:
4646
# storageClass: my-sc
4747
# iops: 1000 # for EBS gp3
4848
# throughput: 250 # in MB/s for EBS gp3
49+
# selector:
50+
# matchExpressions:
51+
# - { key: flavour, operator: In, values: [ "banana", "chocolate" ] }
52+
# matchLabels:
53+
# environment: dev
54+
# service: postgres
4955
additionalVolumes:
5056
- name: empty
5157
mountPath: /opt/empty

manifests/postgresql.crd.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,24 @@ spec:
557557
properties:
558558
iops:
559559
type: integer
560+
selector:
561+
type: object
562+
properties:
563+
matchExpressions:
564+
type: array
565+
items:
566+
type: object
567+
properties:
568+
key:
569+
type: string
570+
operator:
571+
type: string
572+
values:
573+
type: array
574+
items:
575+
type: string
576+
matchLabels:
577+
type: object
560578
size:
561579
type: string
562580
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'

pkg/apis/acid.zalan.do/v1/crds.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,54 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
841841
"iops": {
842842
Type: "integer",
843843
},
844+
"selector": {
845+
Type: "object",
846+
Properties: map[string]apiextv1.JSONSchemaProps{
847+
"matchExpressions": {
848+
Type: "array",
849+
Items: &apiextv1.JSONSchemaPropsOrArray{
850+
Schema: &apiextv1.JSONSchemaProps{
851+
Type: "object",
852+
Required: []string{"key", "operator", "values"},
853+
Properties: map[string]apiextv1.JSONSchemaProps{
854+
"key": {
855+
Type: "string",
856+
},
857+
"operator": {
858+
Type: "string",
859+
Enum: []apiextv1.JSON{
860+
{
861+
Raw: []byte(`"In"`),
862+
},
863+
{
864+
Raw: []byte(`"NotIn"`),
865+
},
866+
{
867+
Raw: []byte(`"Exists"`),
868+
},
869+
{
870+
Raw: []byte(`"DoesNotExist"`),
871+
},
872+
},
873+
},
874+
"values": {
875+
Type: "array",
876+
Items: &apiextv1.JSONSchemaPropsOrArray{
877+
Schema: &apiextv1.JSONSchemaProps{
878+
Type: "string",
879+
},
880+
},
881+
},
882+
},
883+
},
884+
},
885+
},
886+
"matchLabels": {
887+
Type: "object",
888+
XPreserveUnknownFields: util.True(),
889+
},
890+
},
891+
},
844892
"size": {
845893
Type: "string",
846894
Description: "Value must not be zero",

pkg/apis/acid.zalan.do/v1/postgresql_type.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,13 @@ type MaintenanceWindow struct {
114114

115115
// Volume describes a single volume in the manifest.
116116
type Volume struct {
117-
Size string `json:"size"`
118-
StorageClass string `json:"storageClass,omitempty"`
119-
SubPath string `json:"subPath,omitempty"`
120-
Iops *int64 `json:"iops,omitempty"`
121-
Throughput *int64 `json:"throughput,omitempty"`
122-
VolumeType string `json:"type,omitempty"`
117+
Selector *metav1.LabelSelector `json:"selector,omitempty"`
118+
Size string `json:"size"`
119+
StorageClass string `json:"storageClass,omitempty"`
120+
SubPath string `json:"subPath,omitempty"`
121+
Iops *int64 `json:"iops,omitempty"`
122+
Throughput *int64 `json:"throughput,omitempty"`
123+
VolumeType string `json:"type,omitempty"`
123124
}
124125

125126
// AdditionalVolume specs additional optional volumes for statefulset

pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

Lines changed: 6 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cluster/k8sres.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
12721272
}
12731273

12741274
if volumeClaimTemplate, err = generatePersistentVolumeClaimTemplate(spec.Volume.Size,
1275-
spec.Volume.StorageClass); err != nil {
1275+
spec.Volume.StorageClass, spec.Volume.Selector); err != nil {
12761276
return nil, fmt.Errorf("could not generate volume claim template: %v", err)
12771277
}
12781278

@@ -1520,7 +1520,8 @@ func (c *Cluster) addAdditionalVolumes(podSpec *v1.PodSpec,
15201520
podSpec.Volumes = volumes
15211521
}
15221522

1523-
func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) (*v1.PersistentVolumeClaim, error) {
1523+
func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string,
1524+
volumeSelector *metav1.LabelSelector) (*v1.PersistentVolumeClaim, error) {
15241525

15251526
var storageClassName *string
15261527

@@ -1553,6 +1554,7 @@ func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string
15531554
},
15541555
StorageClassName: storageClassName,
15551556
VolumeMode: &volumeMode,
1557+
Selector: volumeSelector,
15561558
},
15571559
}
15581560

pkg/cluster/k8sres_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,3 +1509,106 @@ func TestGenerateCapabilities(t *testing.T) {
15091509
}
15101510
}
15111511
}
1512+
1513+
func TestVolumeSelector(t *testing.T) {
1514+
testName := "TestVolumeSelector"
1515+
makeSpec := func(volume acidv1.Volume) acidv1.PostgresSpec {
1516+
return acidv1.PostgresSpec{
1517+
TeamID: "myapp",
1518+
NumberOfInstances: 0,
1519+
Resources: acidv1.Resources{
1520+
ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
1521+
ResourceLimits: acidv1.ResourceDescription{CPU: "1", Memory: "10"},
1522+
},
1523+
Volume: volume,
1524+
}
1525+
}
1526+
1527+
tests := []struct {
1528+
subTest string
1529+
volume acidv1.Volume
1530+
wantSelector *metav1.LabelSelector
1531+
}{
1532+
{
1533+
subTest: "PVC template has no selector",
1534+
volume: acidv1.Volume{
1535+
Size: "1G",
1536+
},
1537+
wantSelector: nil,
1538+
},
1539+
{
1540+
subTest: "PVC template has simple label selector",
1541+
volume: acidv1.Volume{
1542+
Size: "1G",
1543+
Selector: &metav1.LabelSelector{
1544+
MatchLabels: map[string]string{"environment": "unittest"},
1545+
},
1546+
},
1547+
wantSelector: &metav1.LabelSelector{
1548+
MatchLabels: map[string]string{"environment": "unittest"},
1549+
},
1550+
},
1551+
{
1552+
subTest: "PVC template has full selector",
1553+
volume: acidv1.Volume{
1554+
Size: "1G",
1555+
Selector: &metav1.LabelSelector{
1556+
MatchLabels: map[string]string{"environment": "unittest"},
1557+
MatchExpressions: []metav1.LabelSelectorRequirement{
1558+
{
1559+
Key: "flavour",
1560+
Operator: metav1.LabelSelectorOpIn,
1561+
Values: []string{"banana", "chocolate"},
1562+
},
1563+
},
1564+
},
1565+
},
1566+
wantSelector: &metav1.LabelSelector{
1567+
MatchLabels: map[string]string{"environment": "unittest"},
1568+
MatchExpressions: []metav1.LabelSelectorRequirement{
1569+
{
1570+
Key: "flavour",
1571+
Operator: metav1.LabelSelectorOpIn,
1572+
Values: []string{"banana", "chocolate"},
1573+
},
1574+
},
1575+
},
1576+
},
1577+
}
1578+
1579+
cluster := New(
1580+
Config{
1581+
OpConfig: config.Config{
1582+
PodManagementPolicy: "ordered_ready",
1583+
ProtectedRoles: []string{"admin"},
1584+
Auth: config.Auth{
1585+
SuperUsername: superUserName,
1586+
ReplicationUsername: replicationUserName,
1587+
},
1588+
},
1589+
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
1590+
1591+
for _, tt := range tests {
1592+
pgSpec := makeSpec(tt.volume)
1593+
sts, err := cluster.generateStatefulSet(&pgSpec)
1594+
if err != nil {
1595+
t.Fatalf("%s %s: no statefulset created %v", testName, tt.subTest, err)
1596+
}
1597+
1598+
volIdx := len(sts.Spec.VolumeClaimTemplates)
1599+
for i, ct := range sts.Spec.VolumeClaimTemplates {
1600+
if ct.ObjectMeta.Name == constants.DataVolumeName {
1601+
volIdx = i
1602+
break
1603+
}
1604+
}
1605+
if volIdx == len(sts.Spec.VolumeClaimTemplates) {
1606+
t.Errorf("%s %s: no datavolume found in sts", testName, tt.subTest)
1607+
}
1608+
1609+
selector := sts.Spec.VolumeClaimTemplates[volIdx].Spec.Selector
1610+
if !reflect.DeepEqual(selector, tt.wantSelector) {
1611+
t.Errorf("%s %s: expected: %#v but got: %#v", testName, tt.subTest, tt.wantSelector, selector)
1612+
}
1613+
}
1614+
}

0 commit comments

Comments
 (0)