Prevent empty syncs (#922)
There is a possibility to pass nil as one of the specs and an empty spec into syncConnectionPooler. In this case it will perfom a syncronization because nil != empty struct. Avoid such cases and make it testable by returning list of syncronization reasons on top together with the final error.
This commit is contained in:
		
							parent
							
								
									7e8f6687eb
								
							
						
					
					
						commit
						6a689cdc1c
					
				|  | @ -741,7 +741,8 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { | |||
| 	} | ||||
| 
 | ||||
| 	// sync connection pooler
 | ||||
| 	if err := c.syncConnectionPooler(oldSpec, newSpec, c.installLookupFunction); err != nil { | ||||
| 	if _, err := c.syncConnectionPooler(oldSpec, newSpec, | ||||
| 		c.installLookupFunction); err != nil { | ||||
| 		return fmt.Errorf("could not sync connection pooler: %v", err) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -111,7 +111,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { | |||
| 	} | ||||
| 
 | ||||
| 	// sync connection pooler
 | ||||
| 	if err = c.syncConnectionPooler(&oldSpec, newSpec, c.installLookupFunction); err != nil { | ||||
| 	if _, err = c.syncConnectionPooler(&oldSpec, newSpec, c.installLookupFunction); err != nil { | ||||
| 		return fmt.Errorf("could not sync connection pooler: %v", err) | ||||
| 	} | ||||
| 
 | ||||
|  | @ -620,7 +620,13 @@ func (c *Cluster) syncLogicalBackupJob() error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, lookup InstallFunction) error { | ||||
| func (c *Cluster) syncConnectionPooler(oldSpec, | ||||
| 	newSpec *acidv1.Postgresql, | ||||
| 	lookup InstallFunction) (SyncReason, error) { | ||||
| 
 | ||||
| 	var reason SyncReason | ||||
| 	var err error | ||||
| 
 | ||||
| 	if c.ConnectionPooler == nil { | ||||
| 		c.ConnectionPooler = &ConnectionPoolerObjects{} | ||||
| 	} | ||||
|  | @ -657,20 +663,20 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, look | |||
| 				specUser, | ||||
| 				c.OpConfig.ConnectionPooler.User) | ||||
| 
 | ||||
| 			if err := lookup(schema, user); err != nil { | ||||
| 				return err | ||||
| 			if err = lookup(schema, user); err != nil { | ||||
| 				return NoSync, err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if err := c.syncConnectionPoolerWorker(oldSpec, newSpec); err != nil { | ||||
| 		if reason, err = c.syncConnectionPoolerWorker(oldSpec, newSpec); err != nil { | ||||
| 			c.logger.Errorf("could not sync connection pooler: %v", err) | ||||
| 			return err | ||||
| 			return reason, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if oldNeedConnectionPooler && !newNeedConnectionPooler { | ||||
| 		// delete and cleanup resources
 | ||||
| 		if err := c.deleteConnectionPooler(); err != nil { | ||||
| 		if err = c.deleteConnectionPooler(); err != nil { | ||||
| 			c.logger.Warningf("could not remove connection pooler: %v", err) | ||||
| 		} | ||||
| 	} | ||||
|  | @ -681,20 +687,22 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, look | |||
| 			(c.ConnectionPooler.Deployment != nil || | ||||
| 				c.ConnectionPooler.Service != nil) { | ||||
| 
 | ||||
| 			if err := c.deleteConnectionPooler(); err != nil { | ||||
| 			if err = c.deleteConnectionPooler(); err != nil { | ||||
| 				c.logger.Warningf("could not remove connection pooler: %v", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| 	return reason, nil | ||||
| } | ||||
| 
 | ||||
| // Synchronize connection pooler resources. Effectively we're interested only in
 | ||||
| // synchronizing the corresponding deployment, but in case of deployment or
 | ||||
| // service is missing, create it. After checking, also remember an object for
 | ||||
| // the future references.
 | ||||
| func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql) error { | ||||
| func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql) ( | ||||
| 	SyncReason, error) { | ||||
| 
 | ||||
| 	deployment, err := c.KubeClient. | ||||
| 		Deployments(c.Namespace). | ||||
| 		Get(context.TODO(), c.connectionPoolerName(), metav1.GetOptions{}) | ||||
|  | @ -706,7 +714,7 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql | |||
| 		deploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec) | ||||
| 		if err != nil { | ||||
| 			msg = "could not generate deployment for connection pooler: %v" | ||||
| 			return fmt.Errorf(msg, err) | ||||
| 			return NoSync, fmt.Errorf(msg, err) | ||||
| 		} | ||||
| 
 | ||||
| 		deployment, err := c.KubeClient. | ||||
|  | @ -714,18 +722,35 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql | |||
| 			Create(context.TODO(), deploymentSpec, metav1.CreateOptions{}) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return NoSync, err | ||||
| 		} | ||||
| 
 | ||||
| 		c.ConnectionPooler.Deployment = deployment | ||||
| 	} else if err != nil { | ||||
| 		return fmt.Errorf("could not get connection pooler deployment to sync: %v", err) | ||||
| 		msg := "could not get connection pooler deployment to sync: %v" | ||||
| 		return NoSync, fmt.Errorf(msg, err) | ||||
| 	} else { | ||||
| 		c.ConnectionPooler.Deployment = deployment | ||||
| 
 | ||||
| 		// actual synchronization
 | ||||
| 		oldConnectionPooler := oldSpec.Spec.ConnectionPooler | ||||
| 		newConnectionPooler := newSpec.Spec.ConnectionPooler | ||||
| 
 | ||||
| 		// sync implementation below assumes that both old and new specs are
 | ||||
| 		// not nil, but it can happen. To avoid any confusion like updating a
 | ||||
| 		// deployment because the specification changed from nil to an empty
 | ||||
| 		// struct (that was initialized somewhere before) replace any nil with
 | ||||
| 		// an empty spec.
 | ||||
| 		if oldConnectionPooler == nil { | ||||
| 			oldConnectionPooler = &acidv1.ConnectionPooler{} | ||||
| 		} | ||||
| 
 | ||||
| 		if newConnectionPooler == nil { | ||||
| 			newConnectionPooler = &acidv1.ConnectionPooler{} | ||||
| 		} | ||||
| 
 | ||||
| 		c.logger.Infof("Old: %+v, New %+v", oldConnectionPooler, newConnectionPooler) | ||||
| 
 | ||||
| 		specSync, specReason := c.needSyncConnectionPoolerSpecs(oldConnectionPooler, newConnectionPooler) | ||||
| 		defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(newConnectionPooler, deployment) | ||||
| 		reason := append(specReason, defaultsReason...) | ||||
|  | @ -736,7 +761,7 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql | |||
| 			newDeploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec) | ||||
| 			if err != nil { | ||||
| 				msg := "could not generate deployment for connection pooler: %v" | ||||
| 				return fmt.Errorf(msg, err) | ||||
| 				return reason, fmt.Errorf(msg, err) | ||||
| 			} | ||||
| 
 | ||||
| 			oldDeploymentSpec := c.ConnectionPooler.Deployment | ||||
|  | @ -746,11 +771,11 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql | |||
| 				newDeploymentSpec) | ||||
| 
 | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 				return reason, err | ||||
| 			} | ||||
| 
 | ||||
| 			c.ConnectionPooler.Deployment = deployment | ||||
| 			return nil | ||||
| 			return reason, nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -768,16 +793,17 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql | |||
| 			Create(context.TODO(), serviceSpec, metav1.CreateOptions{}) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return NoSync, err | ||||
| 		} | ||||
| 
 | ||||
| 		c.ConnectionPooler.Service = service | ||||
| 	} else if err != nil { | ||||
| 		return fmt.Errorf("could not get connection pooler service to sync: %v", err) | ||||
| 		msg := "could not get connection pooler service to sync: %v" | ||||
| 		return NoSync, fmt.Errorf(msg, err) | ||||
| 	} else { | ||||
| 		// Service updates are not supported and probably not that useful anyway
 | ||||
| 		c.ConnectionPooler.Service = service | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| 	return NoSync, nil | ||||
| } | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package cluster | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" | ||||
|  | @ -17,7 +18,7 @@ func int32ToPointer(value int32) *int32 { | |||
| 	return &value | ||||
| } | ||||
| 
 | ||||
| func deploymentUpdated(cluster *Cluster, err error) error { | ||||
| func deploymentUpdated(cluster *Cluster, err error, reason SyncReason) error { | ||||
| 	if cluster.ConnectionPooler.Deployment.Spec.Replicas == nil || | ||||
| 		*cluster.ConnectionPooler.Deployment.Spec.Replicas != 2 { | ||||
| 		return fmt.Errorf("Wrong nubmer of instances") | ||||
|  | @ -26,7 +27,7 @@ func deploymentUpdated(cluster *Cluster, err error) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func objectsAreSaved(cluster *Cluster, err error) error { | ||||
| func objectsAreSaved(cluster *Cluster, err error, reason SyncReason) error { | ||||
| 	if cluster.ConnectionPooler == nil { | ||||
| 		return fmt.Errorf("Connection pooler resources are empty") | ||||
| 	} | ||||
|  | @ -42,7 +43,7 @@ func objectsAreSaved(cluster *Cluster, err error) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func objectsAreDeleted(cluster *Cluster, err error) error { | ||||
| func objectsAreDeleted(cluster *Cluster, err error, reason SyncReason) error { | ||||
| 	if cluster.ConnectionPooler != nil { | ||||
| 		return fmt.Errorf("Connection pooler was not deleted") | ||||
| 	} | ||||
|  | @ -50,6 +51,16 @@ func objectsAreDeleted(cluster *Cluster, err error) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func noEmptySync(cluster *Cluster, err error, reason SyncReason) error { | ||||
| 	for _, msg := range reason { | ||||
| 		if strings.HasPrefix(msg, "update [] from '<nil>' to '") { | ||||
| 			return fmt.Errorf("There is an empty reason, %s", msg) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func TestConnectionPoolerSynchronization(t *testing.T) { | ||||
| 	testName := "Test connection pooler synchronization" | ||||
| 	var cluster = New( | ||||
|  | @ -91,15 +102,15 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 
 | ||||
| 	clusterNewDefaultsMock := *cluster | ||||
| 	clusterNewDefaultsMock.KubeClient = k8sutil.NewMockKubernetesClient() | ||||
| 	cluster.OpConfig.ConnectionPooler.Image = "pooler:2.0" | ||||
| 	cluster.OpConfig.ConnectionPooler.NumberOfInstances = int32ToPointer(2) | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		subTest string | ||||
| 		oldSpec *acidv1.Postgresql | ||||
| 		newSpec *acidv1.Postgresql | ||||
| 		cluster *Cluster | ||||
| 		check   func(cluster *Cluster, err error) error | ||||
| 		subTest          string | ||||
| 		oldSpec          *acidv1.Postgresql | ||||
| 		newSpec          *acidv1.Postgresql | ||||
| 		cluster          *Cluster | ||||
| 		defaultImage     string | ||||
| 		defaultInstances int32 | ||||
| 		check            func(cluster *Cluster, err error, reason SyncReason) error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			subTest: "create if doesn't exist", | ||||
|  | @ -113,8 +124,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 					ConnectionPooler: &acidv1.ConnectionPooler{}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			cluster: &clusterMissingObjects, | ||||
| 			check:   objectsAreSaved, | ||||
| 			cluster:          &clusterMissingObjects, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            objectsAreSaved, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "create if doesn't exist with a flag", | ||||
|  | @ -126,8 +139,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 					EnableConnectionPooler: boolToPointer(true), | ||||
| 				}, | ||||
| 			}, | ||||
| 			cluster: &clusterMissingObjects, | ||||
| 			check:   objectsAreSaved, | ||||
| 			cluster:          &clusterMissingObjects, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            objectsAreSaved, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "create from scratch", | ||||
|  | @ -139,8 +154,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 					ConnectionPooler: &acidv1.ConnectionPooler{}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			cluster: &clusterMissingObjects, | ||||
| 			check:   objectsAreSaved, | ||||
| 			cluster:          &clusterMissingObjects, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            objectsAreSaved, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "delete if not needed", | ||||
|  | @ -152,8 +169,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 			newSpec: &acidv1.Postgresql{ | ||||
| 				Spec: acidv1.PostgresSpec{}, | ||||
| 			}, | ||||
| 			cluster: &clusterMock, | ||||
| 			check:   objectsAreDeleted, | ||||
| 			cluster:          &clusterMock, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            objectsAreDeleted, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "cleanup if still there", | ||||
|  | @ -163,8 +182,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 			newSpec: &acidv1.Postgresql{ | ||||
| 				Spec: acidv1.PostgresSpec{}, | ||||
| 			}, | ||||
| 			cluster: &clusterDirtyMock, | ||||
| 			check:   objectsAreDeleted, | ||||
| 			cluster:          &clusterDirtyMock, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            objectsAreDeleted, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "update deployment", | ||||
|  | @ -182,8 +203,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			cluster: &clusterMock, | ||||
| 			check:   deploymentUpdated, | ||||
| 			cluster:          &clusterMock, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            deploymentUpdated, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "update image from changed defaults", | ||||
|  | @ -197,14 +220,40 @@ func TestConnectionPoolerSynchronization(t *testing.T) { | |||
| 					ConnectionPooler: &acidv1.ConnectionPooler{}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			cluster: &clusterNewDefaultsMock, | ||||
| 			check:   deploymentUpdated, | ||||
| 			cluster:          &clusterNewDefaultsMock, | ||||
| 			defaultImage:     "pooler:2.0", | ||||
| 			defaultInstances: 2, | ||||
| 			check:            deploymentUpdated, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subTest: "there is no sync from nil to an empty spec", | ||||
| 			oldSpec: &acidv1.Postgresql{ | ||||
| 				Spec: acidv1.PostgresSpec{ | ||||
| 					EnableConnectionPooler: boolToPointer(true), | ||||
| 					ConnectionPooler:       nil, | ||||
| 				}, | ||||
| 			}, | ||||
| 			newSpec: &acidv1.Postgresql{ | ||||
| 				Spec: acidv1.PostgresSpec{ | ||||
| 					EnableConnectionPooler: boolToPointer(true), | ||||
| 					ConnectionPooler:       &acidv1.ConnectionPooler{}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			cluster:          &clusterMock, | ||||
| 			defaultImage:     "pooler:1.0", | ||||
| 			defaultInstances: 1, | ||||
| 			check:            noEmptySync, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		err := tt.cluster.syncConnectionPooler(tt.oldSpec, tt.newSpec, mockInstallLookupFunction) | ||||
| 		tt.cluster.OpConfig.ConnectionPooler.Image = tt.defaultImage | ||||
| 		tt.cluster.OpConfig.ConnectionPooler.NumberOfInstances = | ||||
| 			int32ToPointer(tt.defaultInstances) | ||||
| 
 | ||||
| 		if err := tt.check(tt.cluster, err); err != nil { | ||||
| 		reason, err := tt.cluster.syncConnectionPooler(tt.oldSpec, | ||||
| 			tt.newSpec, mockInstallLookupFunction) | ||||
| 
 | ||||
| 		if err := tt.check(tt.cluster, err, reason); err != nil { | ||||
| 			t.Errorf("%s [%s]: Could not synchronize, %+v", | ||||
| 				testName, tt.subTest, err) | ||||
| 		} | ||||
|  |  | |||
|  | @ -73,3 +73,8 @@ type ClusterStatus struct { | |||
| type TemplateParams map[string]interface{} | ||||
| 
 | ||||
| type InstallFunction func(schema string, user string) error | ||||
| 
 | ||||
| type SyncReason []string | ||||
| 
 | ||||
| // no sync happened, empty value
 | ||||
| var NoSync SyncReason = []string{} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue