check resize mode on update events (#1194)
* check resize mode on update events * add unit test for PVC resizing * set resize mode to pvc in charts and manifests * add test for quantityToGigabyte * just one debug line for syncing volumes * extend test and update log msg
This commit is contained in:
		
							parent
							
								
									e779eab22f
								
							
						
					
					
						commit
						3fed565328
					
				|  | @ -105,6 +105,10 @@ rules: | ||||||
|   - delete |   - delete | ||||||
|   - get |   - get | ||||||
|   - list |   - list | ||||||
|  | {{- if toString .Values.configKubernetes.storage_resize_mode | eq "pvc" }} | ||||||
|  |   - patch | ||||||
|  |   - update | ||||||
|  | {{- end }} | ||||||
|  # to read existing PVs. Creation should be done via dynamic provisioning |  # to read existing PVs. Creation should be done via dynamic provisioning | ||||||
| - apiGroups: | - apiGroups: | ||||||
|   - "" |   - "" | ||||||
|  | @ -113,7 +117,9 @@ rules: | ||||||
|   verbs: |   verbs: | ||||||
|   - get |   - get | ||||||
|   - list |   - list | ||||||
|  | {{- if toString .Values.configKubernetes.storage_resize_mode | eq "ebs" }} | ||||||
|   - update  # only for resizing AWS volumes |   - update  # only for resizing AWS volumes | ||||||
|  | {{- end }} | ||||||
| # to watch Spilo pods and do rolling updates. Creation via StatefulSet | # to watch Spilo pods and do rolling updates. Creation via StatefulSet | ||||||
| - apiGroups: | - apiGroups: | ||||||
|   - "" |   - "" | ||||||
|  |  | ||||||
|  | @ -136,7 +136,7 @@ configKubernetes: | ||||||
|   # whether the Spilo container should run in privileged mode |   # whether the Spilo container should run in privileged mode | ||||||
|   spilo_privileged: false |   spilo_privileged: false | ||||||
|   # storage resize strategy, available options are: ebs, pvc, off |   # storage resize strategy, available options are: ebs, pvc, off | ||||||
|   storage_resize_mode: ebs |   storage_resize_mode: pvc | ||||||
|   # operator watches for postgres objects in the given namespace |   # operator watches for postgres objects in the given namespace | ||||||
|   watched_namespace: "*"  # listen to all namespaces |   watched_namespace: "*"  # listen to all namespaces | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -130,7 +130,7 @@ configKubernetes: | ||||||
|   # whether the Spilo container should run in privileged mode |   # whether the Spilo container should run in privileged mode | ||||||
|   spilo_privileged: "false" |   spilo_privileged: "false" | ||||||
|   # storage resize strategy, available options are: ebs, pvc, off |   # storage resize strategy, available options are: ebs, pvc, off | ||||||
|   storage_resize_mode: ebs |   storage_resize_mode: pvc | ||||||
|   # operator watches for postgres objects in the given namespace |   # operator watches for postgres objects in the given namespace | ||||||
|   watched_namespace: "*"  # listen to all namespaces |   watched_namespace: "*"  # listen to all namespaces | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -107,7 +107,7 @@ data: | ||||||
|   # spilo_runasgroup: 103 |   # spilo_runasgroup: 103 | ||||||
|   # spilo_fsgroup: 103 |   # spilo_fsgroup: 103 | ||||||
|   spilo_privileged: "false" |   spilo_privileged: "false" | ||||||
|   # storage_resize_mode: "off" |   storage_resize_mode: "pvc" | ||||||
|   super_username: postgres |   super_username: postgres | ||||||
|   # team_admin_role: "admin" |   # team_admin_role: "admin" | ||||||
|   # team_api_role_configuration: "log_statement:all" |   # team_api_role_configuration: "log_statement:all" | ||||||
|  |  | ||||||
|  | @ -106,6 +106,8 @@ rules: | ||||||
|   - delete |   - delete | ||||||
|   - get |   - get | ||||||
|   - list |   - list | ||||||
|  |   - patch | ||||||
|  |   - update | ||||||
|  # to read existing PVs. Creation should be done via dynamic provisioning |  # to read existing PVs. Creation should be done via dynamic provisioning | ||||||
| - apiGroups: | - apiGroups: | ||||||
|   - "" |   - "" | ||||||
|  |  | ||||||
|  | @ -72,7 +72,7 @@ configuration: | ||||||
|     # spilo_runasgroup: 103 |     # spilo_runasgroup: 103 | ||||||
|     # spilo_fsgroup: 103 |     # spilo_fsgroup: 103 | ||||||
|     spilo_privileged: false |     spilo_privileged: false | ||||||
|     storage_resize_mode: ebs |     storage_resize_mode: pvc | ||||||
|     # toleration: {} |     # toleration: {} | ||||||
|     # watched_namespace: "" |     # watched_namespace: "" | ||||||
|   postgres_pod_resources: |   postgres_pod_resources: | ||||||
|  |  | ||||||
|  | @ -140,7 +140,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres | ||||||
| 			Secrets:   make(map[types.UID]*v1.Secret), | 			Secrets:   make(map[types.UID]*v1.Secret), | ||||||
| 			Services:  make(map[PostgresRole]*v1.Service), | 			Services:  make(map[PostgresRole]*v1.Service), | ||||||
| 			Endpoints: make(map[PostgresRole]*v1.Endpoints)}, | 			Endpoints: make(map[PostgresRole]*v1.Endpoints)}, | ||||||
| 		userSyncStrategy: users.DefaultUserSyncStrategy{password_encryption}, | 		userSyncStrategy: users.DefaultUserSyncStrategy{PasswordEncryption: password_encryption}, | ||||||
| 		deleteOptions:    metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy}, | 		deleteOptions:    metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy}, | ||||||
| 		podEventsQueue:   podEventsQueue, | 		podEventsQueue:   podEventsQueue, | ||||||
| 		KubeClient:       kubeClient, | 		KubeClient:       kubeClient, | ||||||
|  | @ -671,13 +671,21 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { | ||||||
| 
 | 
 | ||||||
| 	// Volume
 | 	// Volume
 | ||||||
| 	if oldSpec.Spec.Size != newSpec.Spec.Size { | 	if oldSpec.Spec.Size != newSpec.Spec.Size { | ||||||
| 		c.logger.Debugf("syncing persistent volumes") |  | ||||||
| 		c.logVolumeChanges(oldSpec.Spec.Volume, newSpec.Spec.Volume) | 		c.logVolumeChanges(oldSpec.Spec.Volume, newSpec.Spec.Volume) | ||||||
| 
 | 		c.logger.Debugf("syncing volumes using %q storage resize mode", c.OpConfig.StorageResizeMode) | ||||||
| 		if err := c.syncVolumes(); err != nil { | 		if c.OpConfig.StorageResizeMode == "pvc" { | ||||||
| 			c.logger.Errorf("could not sync persistent volumes: %v", err) | 			if err := c.syncVolumeClaims(); err != nil { | ||||||
| 			updateFailed = true | 				c.logger.Errorf("could not sync persistent volume claims: %v", err) | ||||||
|  | 				updateFailed = true | ||||||
|  | 			} | ||||||
|  | 		} else if c.OpConfig.StorageResizeMode == "ebs" { | ||||||
|  | 			if err := c.syncVolumes(); err != nil { | ||||||
|  | 				c.logger.Errorf("could not sync persistent volumes: %v", err) | ||||||
|  | 				updateFailed = true | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  | 	} else { | ||||||
|  | 		c.logger.Infof("Storage resize is disabled (storage_resize_mode is off). Skipping volume sync.") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Statefulset
 | 	// Statefulset
 | ||||||
|  |  | ||||||
|  | @ -57,8 +57,8 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	c.logger.Debugf("syncing volumes using %q storage resize mode", c.OpConfig.StorageResizeMode) | ||||||
| 	if c.OpConfig.StorageResizeMode == "pvc" { | 	if c.OpConfig.StorageResizeMode == "pvc" { | ||||||
| 		c.logger.Debugf("syncing persistent volume claims") |  | ||||||
| 		if err = c.syncVolumeClaims(); err != nil { | 		if err = c.syncVolumeClaims(); err != nil { | ||||||
| 			err = fmt.Errorf("could not sync persistent volume claims: %v", err) | 			err = fmt.Errorf("could not sync persistent volume claims: %v", err) | ||||||
| 			return err | 			return err | ||||||
|  | @ -70,7 +70,6 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { | ||||||
| 		// TODO: handle the case of the cluster that is downsized and enlarged again
 | 		// TODO: handle the case of the cluster that is downsized and enlarged again
 | ||||||
| 		// (there will be a volume from the old pod for which we can't act before the
 | 		// (there will be a volume from the old pod for which we can't act before the
 | ||||||
| 		//  the statefulset modification is concluded)
 | 		//  the statefulset modification is concluded)
 | ||||||
| 		c.logger.Debugf("syncing persistent volumes") |  | ||||||
| 		if err = c.syncVolumes(); err != nil { | 		if err = c.syncVolumes(); err != nil { | ||||||
| 			err = fmt.Errorf("could not sync persistent volumes: %v", err) | 			err = fmt.Errorf("could not sync persistent volumes: %v", err) | ||||||
| 			return err | 			return err | ||||||
|  |  | ||||||
|  | @ -62,7 +62,7 @@ func (c *Cluster) resizeVolumeClaims(newVolume acidv1.Volume) error { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("could not parse volume size: %v", err) | 		return fmt.Errorf("could not parse volume size: %v", err) | ||||||
| 	} | 	} | ||||||
| 	_, newSize, err := c.listVolumesWithManifestSize(newVolume) | 	newSize := quantityToGigabyte(newQuantity) | ||||||
| 	for _, pvc := range pvcs { | 	for _, pvc := range pvcs { | ||||||
| 		volumeSize := quantityToGigabyte(pvc.Spec.Resources.Requests[v1.ResourceStorage]) | 		volumeSize := quantityToGigabyte(pvc.Spec.Resources.Requests[v1.ResourceStorage]) | ||||||
| 		if volumeSize >= newSize { | 		if volumeSize >= newSize { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,171 @@ | ||||||
|  | package cluster | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	v1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/api/resource" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/labels" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" | ||||||
|  | 	"github.com/zalando/postgres-operator/pkg/util/config" | ||||||
|  | 	"github.com/zalando/postgres-operator/pkg/util/constants" | ||||||
|  | 	"github.com/zalando/postgres-operator/pkg/util/k8sutil" | ||||||
|  | 	"k8s.io/client-go/kubernetes/fake" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewFakeKubernetesClient() (k8sutil.KubernetesClient, *fake.Clientset) { | ||||||
|  | 	clientSet := fake.NewSimpleClientset() | ||||||
|  | 
 | ||||||
|  | 	return k8sutil.KubernetesClient{ | ||||||
|  | 		PersistentVolumeClaimsGetter: clientSet.CoreV1(), | ||||||
|  | 	}, clientSet | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestResizeVolumeClaim(t *testing.T) { | ||||||
|  | 	testName := "test resizing of persistent volume claims" | ||||||
|  | 	client, _ := NewFakeKubernetesClient() | ||||||
|  | 	clusterName := "acid-test-cluster" | ||||||
|  | 	namespace := "default" | ||||||
|  | 	newVolumeSize := "2Gi" | ||||||
|  | 
 | ||||||
|  | 	// new cluster with pvc storage resize mode and configured labels
 | ||||||
|  | 	var cluster = New( | ||||||
|  | 		Config{ | ||||||
|  | 			OpConfig: config.Config{ | ||||||
|  | 				Resources: config.Resources{ | ||||||
|  | 					ClusterLabels:    map[string]string{"application": "spilo"}, | ||||||
|  | 					ClusterNameLabel: "cluster-name", | ||||||
|  | 				}, | ||||||
|  | 				StorageResizeMode: "pvc", | ||||||
|  | 			}, | ||||||
|  | 		}, client, acidv1.Postgresql{}, logger, eventRecorder) | ||||||
|  | 
 | ||||||
|  | 	// set metadata, so that labels will get correct values
 | ||||||
|  | 	cluster.Name = clusterName | ||||||
|  | 	cluster.Namespace = namespace | ||||||
|  | 	filterLabels := cluster.labelsSet(false) | ||||||
|  | 
 | ||||||
|  | 	// define and create PVCs for 1Gi volumes
 | ||||||
|  | 	storage1Gi, err := resource.ParseQuantity("1Gi") | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	pvcList := &v1.PersistentVolumeClaimList{ | ||||||
|  | 		Items: []v1.PersistentVolumeClaim{ | ||||||
|  | 			{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      constants.DataVolumeName + "-" + clusterName + "-0", | ||||||
|  | 					Namespace: namespace, | ||||||
|  | 					Labels:    filterLabels, | ||||||
|  | 				}, | ||||||
|  | 				Spec: v1.PersistentVolumeClaimSpec{ | ||||||
|  | 					Resources: v1.ResourceRequirements{ | ||||||
|  | 						Requests: v1.ResourceList{ | ||||||
|  | 							v1.ResourceStorage: storage1Gi, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      constants.DataVolumeName + "-" + clusterName + "-1", | ||||||
|  | 					Namespace: namespace, | ||||||
|  | 					Labels:    filterLabels, | ||||||
|  | 				}, | ||||||
|  | 				Spec: v1.PersistentVolumeClaimSpec{ | ||||||
|  | 					Resources: v1.ResourceRequirements{ | ||||||
|  | 						Requests: v1.ResourceList{ | ||||||
|  | 							v1.ResourceStorage: storage1Gi, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      constants.DataVolumeName + "-" + clusterName + "-2-0", | ||||||
|  | 					Namespace: namespace, | ||||||
|  | 					Labels:    labels.Set{}, | ||||||
|  | 				}, | ||||||
|  | 				Spec: v1.PersistentVolumeClaimSpec{ | ||||||
|  | 					Resources: v1.ResourceRequirements{ | ||||||
|  | 						Requests: v1.ResourceList{ | ||||||
|  | 							v1.ResourceStorage: storage1Gi, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, pvc := range pvcList.Items { | ||||||
|  | 		cluster.KubeClient.PersistentVolumeClaims(namespace).Create(context.TODO(), &pvc, metav1.CreateOptions{}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// test resizing
 | ||||||
|  | 	cluster.resizeVolumeClaims(acidv1.Volume{Size: newVolumeSize}) | ||||||
|  | 
 | ||||||
|  | 	pvcs, err := cluster.listPersistentVolumeClaims() | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	// check if listPersistentVolumeClaims returns only the PVCs matching the filter
 | ||||||
|  | 	if len(pvcs) != len(pvcList.Items)-1 { | ||||||
|  | 		t.Errorf("%s: could not find all PVCs, got %v, expected %v", testName, len(pvcs), len(pvcList.Items)-1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if PVCs were correctly resized
 | ||||||
|  | 	for _, pvc := range pvcs { | ||||||
|  | 		newStorageSize := quantityToGigabyte(pvc.Spec.Resources.Requests[v1.ResourceStorage]) | ||||||
|  | 		expectedQuantity, err := resource.ParseQuantity(newVolumeSize) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		expectedSize := quantityToGigabyte(expectedQuantity) | ||||||
|  | 		if newStorageSize != expectedSize { | ||||||
|  | 			t.Errorf("%s: resizing failed, got %v, expected %v", testName, newStorageSize, expectedSize) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check if other PVC was not resized
 | ||||||
|  | 	pvc2, err := cluster.KubeClient.PersistentVolumeClaims(namespace).Get(context.TODO(), constants.DataVolumeName+"-"+clusterName+"-2-0", metav1.GetOptions{}) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	unchangedSize := quantityToGigabyte(pvc2.Spec.Resources.Requests[v1.ResourceStorage]) | ||||||
|  | 	expectedSize := quantityToGigabyte(storage1Gi) | ||||||
|  | 	if unchangedSize != expectedSize { | ||||||
|  | 		t.Errorf("%s: volume size changed, got %v, expected %v", testName, unchangedSize, expectedSize) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestQuantityToGigabyte(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		quantityStr string | ||||||
|  | 		expected    int64 | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			"test with 1Gi", | ||||||
|  | 			"1Gi", | ||||||
|  | 			1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"test with float", | ||||||
|  | 			"1.5Gi", | ||||||
|  | 			int64(1), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"test with 1000Mi", | ||||||
|  | 			"1000Mi", | ||||||
|  | 			int64(0), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		quantity, err := resource.ParseQuantity(tt.quantityStr) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		gigabyte := quantityToGigabyte(quantity) | ||||||
|  | 		if gigabyte != tt.expected { | ||||||
|  | 			t.Errorf("%s: got %v, expected %v", tt.name, gigabyte, tt.expected) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue