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:
|
||||
iops:
|
||||
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:
|
||||
type: string
|
||||
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`)
|
||||
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
|
||||
|
||||
Those parameters are defined under the `sidecars` key. They consist of a list
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ spec:
|
|||
# storageClass: my-sc
|
||||
# iops: 1000 # 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:
|
||||
- name: empty
|
||||
mountPath: /opt/empty
|
||||
|
|
|
|||
|
|
@ -557,6 +557,24 @@ spec:
|
|||
properties:
|
||||
iops:
|
||||
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:
|
||||
type: string
|
||||
pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$'
|
||||
|
|
|
|||
|
|
@ -841,6 +841,54 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
|||
"iops": {
|
||||
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": {
|
||||
Type: "string",
|
||||
Description: "Value must not be zero",
|
||||
|
|
|
|||
|
|
@ -114,12 +114,13 @@ type MaintenanceWindow struct {
|
|||
|
||||
// Volume describes a single volume in the manifest.
|
||||
type Volume struct {
|
||||
Size string `json:"size"`
|
||||
StorageClass string `json:"storageClass,omitempty"`
|
||||
SubPath string `json:"subPath,omitempty"`
|
||||
Iops *int64 `json:"iops,omitempty"`
|
||||
Throughput *int64 `json:"throughput,omitempty"`
|
||||
VolumeType string `json:"type,omitempty"`
|
||||
Selector *metav1.LabelSelector `json:"selector,omitempty"`
|
||||
Size string `json:"size"`
|
||||
StorageClass string `json:"storageClass,omitempty"`
|
||||
SubPath string `json:"subPath,omitempty"`
|
||||
Iops *int64 `json:"iops,omitempty"`
|
||||
Throughput *int64 `json:"throughput,omitempty"`
|
||||
VolumeType string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// AdditionalVolume specs additional optional volumes for statefulset
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ package v1
|
|||
import (
|
||||
config "github.com/zalando/postgres-operator/pkg/util/config"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
|
@ -314,22 +315,6 @@ func (in *MaintenanceWindow) DeepCopy() *MaintenanceWindow {
|
|||
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.
|
||||
func (in *OperatorConfiguration) DeepCopyInto(out *OperatorConfiguration) {
|
||||
*out = *in
|
||||
|
|
@ -385,7 +370,6 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
|
|||
}
|
||||
}
|
||||
out.PostgresUsersConfiguration = in.PostgresUsersConfiguration
|
||||
out.MajorVersionUpgrade = in.MajorVersionUpgrade
|
||||
in.Kubernetes.DeepCopyInto(&out.Kubernetes)
|
||||
out.PostgresPodResources = in.PostgresPodResources
|
||||
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.
|
||||
func (in *Volume) DeepCopyInto(out *Volume) {
|
||||
*out = *in
|
||||
if in.Selector != nil {
|
||||
in, out := &in.Selector, &out.Selector
|
||||
*out = new(metav1.LabelSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Iops != nil {
|
||||
in, out := &in.Iops, &out.Iops
|
||||
*out = new(int64)
|
||||
|
|
|
|||
|
|
@ -1272,7 +1272,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -1520,7 +1520,8 @@ func (c *Cluster) addAdditionalVolumes(podSpec *v1.PodSpec,
|
|||
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
|
||||
|
||||
|
|
@ -1553,6 +1554,7 @@ func generatePersistentVolumeClaimTemplate(volumeSize, volumeStorageClass string
|
|||
},
|
||||
StorageClassName: storageClassName,
|
||||
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