Add volume selector (#1385)
* Add volume selector * Add slightly better documentation and gofmt changes * Update generated deepcopy * Add test for PV selector Co-authored-by: John Rood <j.rood@picturae.com>
This commit is contained in:
		
							parent
							
								
									1b3366e9f4
								
							
						
					
					
						commit
						2d2ce6197b
					
				|  | @ -561,6 +561,24 @@ spec: | ||||||
|                 properties: |                 properties: | ||||||
|                   iops: |                   iops: | ||||||
|                     type: integer |                     type: integer | ||||||
|  |                   selector: | ||||||
|  |                     type: object | ||||||
|  |                     properties: | ||||||
|  |                       matchExpressions: | ||||||
|  |                         type: array | ||||||
|  |                         items: | ||||||
|  |                           type: object | ||||||
|  |                           properties: | ||||||
|  |                             key: | ||||||
|  |                               type: string | ||||||
|  |                             operator: | ||||||
|  |                               type: string | ||||||
|  |                             values: | ||||||
|  |                               type: array | ||||||
|  |                               items: | ||||||
|  |                                 type: string | ||||||
|  |                       matchLabels: | ||||||
|  |                         type: object | ||||||
|                   size: |                   size: | ||||||
|                     type: string |                     type: string | ||||||
|                     pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' |                     pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' | ||||||
|  |  | ||||||
|  | @ -399,6 +399,11 @@ properties of the persistent storage that stores Postgres data. | ||||||
|   When running the operator on AWS the latest generation of EBS volumes (`gp3`) |   When running the operator on AWS the latest generation of EBS volumes (`gp3`) | ||||||
|   allows for configuring the throughput in MB/s. Maximum is 1000. Optional. |   allows for configuring the throughput in MB/s. Maximum is 1000. Optional. | ||||||
| 
 | 
 | ||||||
|  | * **selector** | ||||||
|  |   A label query over PVs to consider for binding. See the [Kubernetes  | ||||||
|  |   documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) | ||||||
|  |   for details on using `matchLabels` and `matchExpressions`. Optional | ||||||
|  | 
 | ||||||
| ## Sidecar definitions | ## Sidecar definitions | ||||||
| 
 | 
 | ||||||
| Those parameters are defined under the `sidecars` key. They consist of a list | Those parameters are defined under the `sidecars` key. They consist of a list | ||||||
|  |  | ||||||
|  | @ -46,6 +46,12 @@ spec: | ||||||
| #    storageClass: my-sc | #    storageClass: my-sc | ||||||
| #    iops: 1000  # for EBS gp3 | #    iops: 1000  # for EBS gp3 | ||||||
| #    throughput: 250  # in MB/s for EBS gp3 | #    throughput: 250  # in MB/s for EBS gp3 | ||||||
|  | #    selector: | ||||||
|  | #      matchExpressions: | ||||||
|  | #        - { key: flavour, operator: In, values: [ "banana", "chocolate" ] } | ||||||
|  | #      matchLabels: | ||||||
|  | #        environment: dev | ||||||
|  | #        service: postgres | ||||||
|   additionalVolumes: |   additionalVolumes: | ||||||
|     - name: empty |     - name: empty | ||||||
|       mountPath: /opt/empty |       mountPath: /opt/empty | ||||||
|  |  | ||||||
|  | @ -557,6 +557,24 @@ spec: | ||||||
|                 properties: |                 properties: | ||||||
|                   iops: |                   iops: | ||||||
|                     type: integer |                     type: integer | ||||||
|  |                   selector: | ||||||
|  |                     type: object | ||||||
|  |                     properties: | ||||||
|  |                       matchExpressions: | ||||||
|  |                         type: array | ||||||
|  |                         items: | ||||||
|  |                           type: object | ||||||
|  |                           properties: | ||||||
|  |                             key: | ||||||
|  |                               type: string | ||||||
|  |                             operator: | ||||||
|  |                               type: string | ||||||
|  |                             values: | ||||||
|  |                               type: array | ||||||
|  |                               items: | ||||||
|  |                                 type: string | ||||||
|  |                       matchLabels: | ||||||
|  |                         type: object | ||||||
|                   size: |                   size: | ||||||
|                     type: string |                     type: string | ||||||
|                     pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' |                     pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' | ||||||
|  |  | ||||||
|  | @ -841,6 +841,54 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ | ||||||
| 							"iops": { | 							"iops": { | ||||||
| 								Type: "integer", | 								Type: "integer", | ||||||
| 							}, | 							}, | ||||||
|  | 							"selector": { | ||||||
|  | 								Type: "object", | ||||||
|  | 								Properties: map[string]apiextv1.JSONSchemaProps{ | ||||||
|  | 									"matchExpressions": { | ||||||
|  | 										Type: "array", | ||||||
|  | 										Items: &apiextv1.JSONSchemaPropsOrArray{ | ||||||
|  | 											Schema: &apiextv1.JSONSchemaProps{ | ||||||
|  | 												Type:     "object", | ||||||
|  | 												Required: []string{"key", "operator", "values"}, | ||||||
|  | 												Properties: map[string]apiextv1.JSONSchemaProps{ | ||||||
|  | 													"key": { | ||||||
|  | 														Type: "string", | ||||||
|  | 													}, | ||||||
|  | 													"operator": { | ||||||
|  | 														Type: "string", | ||||||
|  | 														Enum: []apiextv1.JSON{ | ||||||
|  | 															{ | ||||||
|  | 																Raw: []byte(`"In"`), | ||||||
|  | 															}, | ||||||
|  | 															{ | ||||||
|  | 																Raw: []byte(`"NotIn"`), | ||||||
|  | 															}, | ||||||
|  | 															{ | ||||||
|  | 																Raw: []byte(`"Exists"`), | ||||||
|  | 															}, | ||||||
|  | 															{ | ||||||
|  | 																Raw: []byte(`"DoesNotExist"`), | ||||||
|  | 															}, | ||||||
|  | 														}, | ||||||
|  | 													}, | ||||||
|  | 													"values": { | ||||||
|  | 														Type: "array", | ||||||
|  | 														Items: &apiextv1.JSONSchemaPropsOrArray{ | ||||||
|  | 															Schema: &apiextv1.JSONSchemaProps{ | ||||||
|  | 																Type: "string", | ||||||
|  | 															}, | ||||||
|  | 														}, | ||||||
|  | 													}, | ||||||
|  | 												}, | ||||||
|  | 											}, | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 									"matchLabels": { | ||||||
|  | 										Type:                   "object", | ||||||
|  | 										XPreserveUnknownFields: util.True(), | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
| 							"size": { | 							"size": { | ||||||
| 								Type:        "string", | 								Type:        "string", | ||||||
| 								Description: "Value must not be zero", | 								Description: "Value must not be zero", | ||||||
|  |  | ||||||
|  | @ -114,6 +114,7 @@ type MaintenanceWindow struct { | ||||||
| 
 | 
 | ||||||
| // Volume describes a single volume in the manifest.
 | // Volume describes a single volume in the manifest.
 | ||||||
| type Volume struct { | type Volume struct { | ||||||
|  | 	Selector     *metav1.LabelSelector `json:"selector,omitempty"` | ||||||
| 	Size         string                `json:"size"` | 	Size         string                `json:"size"` | ||||||
| 	StorageClass string                `json:"storageClass,omitempty"` | 	StorageClass string                `json:"storageClass,omitempty"` | ||||||
| 	SubPath      string                `json:"subPath,omitempty"` | 	SubPath      string                `json:"subPath,omitempty"` | ||||||
|  |  | ||||||
|  | @ -29,6 +29,7 @@ package v1 | ||||||
| import ( | import ( | ||||||
| 	config "github.com/zalando/postgres-operator/pkg/util/config" | 	config "github.com/zalando/postgres-operator/pkg/util/config" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	runtime "k8s.io/apimachinery/pkg/runtime" | 	runtime "k8s.io/apimachinery/pkg/runtime" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -314,22 +315,6 @@ func (in *MaintenanceWindow) DeepCopy() *MaintenanceWindow { | ||||||
| 	return out | 	return out | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 |  | ||||||
| func (in *MajorVersionUpgradeConfiguration) DeepCopyInto(out *MajorVersionUpgradeConfiguration) { |  | ||||||
| 	*out = *in |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MajorVersionUpgradeConfiguration.
 |  | ||||||
| func (in *MajorVersionUpgradeConfiguration) DeepCopy() *MajorVersionUpgradeConfiguration { |  | ||||||
| 	if in == nil { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	out := new(MajorVersionUpgradeConfiguration) |  | ||||||
| 	in.DeepCopyInto(out) |  | ||||||
| 	return out |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 | ||||||
| func (in *OperatorConfiguration) DeepCopyInto(out *OperatorConfiguration) { | func (in *OperatorConfiguration) DeepCopyInto(out *OperatorConfiguration) { | ||||||
| 	*out = *in | 	*out = *in | ||||||
|  | @ -385,7 +370,6 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	out.PostgresUsersConfiguration = in.PostgresUsersConfiguration | 	out.PostgresUsersConfiguration = in.PostgresUsersConfiguration | ||||||
| 	out.MajorVersionUpgrade = in.MajorVersionUpgrade |  | ||||||
| 	in.Kubernetes.DeepCopyInto(&out.Kubernetes) | 	in.Kubernetes.DeepCopyInto(&out.Kubernetes) | ||||||
| 	out.PostgresPodResources = in.PostgresPodResources | 	out.PostgresPodResources = in.PostgresPodResources | ||||||
| 	out.Timeouts = in.Timeouts | 	out.Timeouts = in.Timeouts | ||||||
|  | @ -1197,6 +1181,11 @@ func (in UserFlags) DeepCopy() UserFlags { | ||||||
| // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 | ||||||
| func (in *Volume) DeepCopyInto(out *Volume) { | func (in *Volume) DeepCopyInto(out *Volume) { | ||||||
| 	*out = *in | 	*out = *in | ||||||
|  | 	if in.Selector != nil { | ||||||
|  | 		in, out := &in.Selector, &out.Selector | ||||||
|  | 		*out = new(metav1.LabelSelector) | ||||||
|  | 		(*in).DeepCopyInto(*out) | ||||||
|  | 	} | ||||||
| 	if in.Iops != nil { | 	if in.Iops != nil { | ||||||
| 		in, out := &in.Iops, &out.Iops | 		in, out := &in.Iops, &out.Iops | ||||||
| 		*out = new(int64) | 		*out = new(int64) | ||||||
|  |  | ||||||
|  | @ -1272,7 +1272,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if volumeClaimTemplate, err = generatePersistentVolumeClaimTemplate(spec.Volume.Size, | 	if volumeClaimTemplate, err = generatePersistentVolumeClaimTemplate(spec.Volume.Size, | ||||||
| 		spec.Volume.StorageClass); err != nil { | 		spec.Volume.StorageClass, spec.Volume.Selector); err != nil { | ||||||
| 		return nil, fmt.Errorf("could not generate volume claim template: %v", err) | 		return nil, fmt.Errorf("could not generate volume claim template: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -1520,7 +1520,8 @@ func (c *Cluster) addAdditionalVolumes(podSpec *v1.PodSpec, | ||||||
| 	podSpec.Volumes = volumes | 	podSpec.Volumes = volumes | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) (*v1.PersistentVolumeClaim, error) { | func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string, | ||||||
|  | 	volumeSelector *metav1.LabelSelector) (*v1.PersistentVolumeClaim, error) { | ||||||
| 
 | 
 | ||||||
| 	var storageClassName *string | 	var storageClassName *string | ||||||
| 
 | 
 | ||||||
|  | @ -1553,6 +1554,7 @@ func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string | ||||||
| 			}, | 			}, | ||||||
| 			StorageClassName: storageClassName, | 			StorageClassName: storageClassName, | ||||||
| 			VolumeMode:       &volumeMode, | 			VolumeMode:       &volumeMode, | ||||||
|  | 			Selector:         volumeSelector, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1509,3 +1509,106 @@ func TestGenerateCapabilities(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestVolumeSelector(t *testing.T) { | ||||||
|  | 	testName := "TestVolumeSelector" | ||||||
|  | 	makeSpec := func(volume acidv1.Volume) acidv1.PostgresSpec { | ||||||
|  | 		return acidv1.PostgresSpec{ | ||||||
|  | 			TeamID:            "myapp", | ||||||
|  | 			NumberOfInstances: 0, | ||||||
|  | 			Resources: acidv1.Resources{ | ||||||
|  | 				ResourceRequests: acidv1.ResourceDescription{CPU: "1", Memory: "10"}, | ||||||
|  | 				ResourceLimits:   acidv1.ResourceDescription{CPU: "1", Memory: "10"}, | ||||||
|  | 			}, | ||||||
|  | 			Volume: volume, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		subTest      string | ||||||
|  | 		volume       acidv1.Volume | ||||||
|  | 		wantSelector *metav1.LabelSelector | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			subTest: "PVC template has no selector", | ||||||
|  | 			volume: acidv1.Volume{ | ||||||
|  | 				Size: "1G", | ||||||
|  | 			}, | ||||||
|  | 			wantSelector: nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subTest: "PVC template has simple label selector", | ||||||
|  | 			volume: acidv1.Volume{ | ||||||
|  | 				Size: "1G", | ||||||
|  | 				Selector: &metav1.LabelSelector{ | ||||||
|  | 					MatchLabels: map[string]string{"environment": "unittest"}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantSelector: &metav1.LabelSelector{ | ||||||
|  | 				MatchLabels: map[string]string{"environment": "unittest"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subTest: "PVC template has full selector", | ||||||
|  | 			volume: acidv1.Volume{ | ||||||
|  | 				Size: "1G", | ||||||
|  | 				Selector: &metav1.LabelSelector{ | ||||||
|  | 					MatchLabels: map[string]string{"environment": "unittest"}, | ||||||
|  | 					MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 						{ | ||||||
|  | 							Key:      "flavour", | ||||||
|  | 							Operator: metav1.LabelSelectorOpIn, | ||||||
|  | 							Values:   []string{"banana", "chocolate"}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantSelector: &metav1.LabelSelector{ | ||||||
|  | 				MatchLabels: map[string]string{"environment": "unittest"}, | ||||||
|  | 				MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 					{ | ||||||
|  | 						Key:      "flavour", | ||||||
|  | 						Operator: metav1.LabelSelectorOpIn, | ||||||
|  | 						Values:   []string{"banana", "chocolate"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	cluster := New( | ||||||
|  | 		Config{ | ||||||
|  | 			OpConfig: config.Config{ | ||||||
|  | 				PodManagementPolicy: "ordered_ready", | ||||||
|  | 				ProtectedRoles:      []string{"admin"}, | ||||||
|  | 				Auth: config.Auth{ | ||||||
|  | 					SuperUsername:       superUserName, | ||||||
|  | 					ReplicationUsername: replicationUserName, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		pgSpec := makeSpec(tt.volume) | ||||||
|  | 		sts, err := cluster.generateStatefulSet(&pgSpec) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("%s %s: no statefulset created %v", testName, tt.subTest, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		volIdx := len(sts.Spec.VolumeClaimTemplates) | ||||||
|  | 		for i, ct := range sts.Spec.VolumeClaimTemplates { | ||||||
|  | 			if ct.ObjectMeta.Name == constants.DataVolumeName { | ||||||
|  | 				volIdx = i | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if volIdx == len(sts.Spec.VolumeClaimTemplates) { | ||||||
|  | 			t.Errorf("%s %s: no datavolume found in sts", testName, tt.subTest) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		selector := sts.Spec.VolumeClaimTemplates[volIdx].Spec.Selector | ||||||
|  | 		if !reflect.DeepEqual(selector, tt.wantSelector) { | ||||||
|  | 			t.Errorf("%s %s: expected: %#v but got: %#v", testName, tt.subTest, tt.wantSelector, selector) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue