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:
Dmitry Dolgov 2020-04-16 15:14:31 +02:00 committed by GitHub
parent 7e8f6687eb
commit 6a689cdc1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 46 deletions

View File

@ -741,7 +741,8 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
} }
// sync connection pooler // 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) return fmt.Errorf("could not sync connection pooler: %v", err)
} }

View File

@ -111,7 +111,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
} }
// sync connection pooler // 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) return fmt.Errorf("could not sync connection pooler: %v", err)
} }
@ -620,7 +620,13 @@ func (c *Cluster) syncLogicalBackupJob() error {
return nil 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 { if c.ConnectionPooler == nil {
c.ConnectionPooler = &ConnectionPoolerObjects{} c.ConnectionPooler = &ConnectionPoolerObjects{}
} }
@ -657,20 +663,20 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, look
specUser, specUser,
c.OpConfig.ConnectionPooler.User) c.OpConfig.ConnectionPooler.User)
if err := lookup(schema, user); err != nil { if err = lookup(schema, user); err != nil {
return err 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) c.logger.Errorf("could not sync connection pooler: %v", err)
return err return reason, err
} }
} }
if oldNeedConnectionPooler && !newNeedConnectionPooler { if oldNeedConnectionPooler && !newNeedConnectionPooler {
// delete and cleanup resources // 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) 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.Deployment != nil ||
c.ConnectionPooler.Service != 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) 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 // Synchronize connection pooler resources. Effectively we're interested only in
// synchronizing the corresponding deployment, but in case of deployment or // synchronizing the corresponding deployment, but in case of deployment or
// service is missing, create it. After checking, also remember an object for // service is missing, create it. After checking, also remember an object for
// the future references. // 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. deployment, err := c.KubeClient.
Deployments(c.Namespace). Deployments(c.Namespace).
Get(context.TODO(), c.connectionPoolerName(), metav1.GetOptions{}) 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) deploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec)
if err != nil { if err != nil {
msg = "could not generate deployment for connection pooler: %v" msg = "could not generate deployment for connection pooler: %v"
return fmt.Errorf(msg, err) return NoSync, fmt.Errorf(msg, err)
} }
deployment, err := c.KubeClient. deployment, err := c.KubeClient.
@ -714,18 +722,35 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
Create(context.TODO(), deploymentSpec, metav1.CreateOptions{}) Create(context.TODO(), deploymentSpec, metav1.CreateOptions{})
if err != nil { if err != nil {
return err return NoSync, err
} }
c.ConnectionPooler.Deployment = deployment c.ConnectionPooler.Deployment = deployment
} else if err != nil { } 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 { } else {
c.ConnectionPooler.Deployment = deployment c.ConnectionPooler.Deployment = deployment
// actual synchronization // actual synchronization
oldConnectionPooler := oldSpec.Spec.ConnectionPooler oldConnectionPooler := oldSpec.Spec.ConnectionPooler
newConnectionPooler := newSpec.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) specSync, specReason := c.needSyncConnectionPoolerSpecs(oldConnectionPooler, newConnectionPooler)
defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(newConnectionPooler, deployment) defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(newConnectionPooler, deployment)
reason := append(specReason, defaultsReason...) reason := append(specReason, defaultsReason...)
@ -736,7 +761,7 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
newDeploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec) newDeploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec)
if err != nil { if err != nil {
msg := "could not generate deployment for connection pooler: %v" msg := "could not generate deployment for connection pooler: %v"
return fmt.Errorf(msg, err) return reason, fmt.Errorf(msg, err)
} }
oldDeploymentSpec := c.ConnectionPooler.Deployment oldDeploymentSpec := c.ConnectionPooler.Deployment
@ -746,11 +771,11 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
newDeploymentSpec) newDeploymentSpec)
if err != nil { if err != nil {
return err return reason, err
} }
c.ConnectionPooler.Deployment = deployment 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{}) Create(context.TODO(), serviceSpec, metav1.CreateOptions{})
if err != nil { if err != nil {
return err return NoSync, err
} }
c.ConnectionPooler.Service = service c.ConnectionPooler.Service = service
} else if err != nil { } 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 { } else {
// Service updates are not supported and probably not that useful anyway // Service updates are not supported and probably not that useful anyway
c.ConnectionPooler.Service = service c.ConnectionPooler.Service = service
} }
return nil return NoSync, nil
} }

View File

@ -2,6 +2,7 @@ package cluster
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
@ -17,7 +18,7 @@ func int32ToPointer(value int32) *int32 {
return &value 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 || if cluster.ConnectionPooler.Deployment.Spec.Replicas == nil ||
*cluster.ConnectionPooler.Deployment.Spec.Replicas != 2 { *cluster.ConnectionPooler.Deployment.Spec.Replicas != 2 {
return fmt.Errorf("Wrong nubmer of instances") return fmt.Errorf("Wrong nubmer of instances")
@ -26,7 +27,7 @@ func deploymentUpdated(cluster *Cluster, err error) error {
return nil return nil
} }
func objectsAreSaved(cluster *Cluster, err error) error { func objectsAreSaved(cluster *Cluster, err error, reason SyncReason) error {
if cluster.ConnectionPooler == nil { if cluster.ConnectionPooler == nil {
return fmt.Errorf("Connection pooler resources are empty") return fmt.Errorf("Connection pooler resources are empty")
} }
@ -42,7 +43,7 @@ func objectsAreSaved(cluster *Cluster, err error) error {
return nil return nil
} }
func objectsAreDeleted(cluster *Cluster, err error) error { func objectsAreDeleted(cluster *Cluster, err error, reason SyncReason) error {
if cluster.ConnectionPooler != nil { if cluster.ConnectionPooler != nil {
return fmt.Errorf("Connection pooler was not deleted") return fmt.Errorf("Connection pooler was not deleted")
} }
@ -50,6 +51,16 @@ func objectsAreDeleted(cluster *Cluster, err error) error {
return nil 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) { func TestConnectionPoolerSynchronization(t *testing.T) {
testName := "Test connection pooler synchronization" testName := "Test connection pooler synchronization"
var cluster = New( var cluster = New(
@ -91,15 +102,15 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
clusterNewDefaultsMock := *cluster clusterNewDefaultsMock := *cluster
clusterNewDefaultsMock.KubeClient = k8sutil.NewMockKubernetesClient() clusterNewDefaultsMock.KubeClient = k8sutil.NewMockKubernetesClient()
cluster.OpConfig.ConnectionPooler.Image = "pooler:2.0"
cluster.OpConfig.ConnectionPooler.NumberOfInstances = int32ToPointer(2)
tests := []struct { tests := []struct {
subTest string subTest string
oldSpec *acidv1.Postgresql oldSpec *acidv1.Postgresql
newSpec *acidv1.Postgresql newSpec *acidv1.Postgresql
cluster *Cluster cluster *Cluster
check func(cluster *Cluster, err error) error defaultImage string
defaultInstances int32
check func(cluster *Cluster, err error, reason SyncReason) error
}{ }{
{ {
subTest: "create if doesn't exist", subTest: "create if doesn't exist",
@ -114,6 +125,8 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
}, },
}, },
cluster: &clusterMissingObjects, cluster: &clusterMissingObjects,
defaultImage: "pooler:1.0",
defaultInstances: 1,
check: objectsAreSaved, check: objectsAreSaved,
}, },
{ {
@ -127,6 +140,8 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
}, },
}, },
cluster: &clusterMissingObjects, cluster: &clusterMissingObjects,
defaultImage: "pooler:1.0",
defaultInstances: 1,
check: objectsAreSaved, check: objectsAreSaved,
}, },
{ {
@ -140,6 +155,8 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
}, },
}, },
cluster: &clusterMissingObjects, cluster: &clusterMissingObjects,
defaultImage: "pooler:1.0",
defaultInstances: 1,
check: objectsAreSaved, check: objectsAreSaved,
}, },
{ {
@ -153,6 +170,8 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
Spec: acidv1.PostgresSpec{}, Spec: acidv1.PostgresSpec{},
}, },
cluster: &clusterMock, cluster: &clusterMock,
defaultImage: "pooler:1.0",
defaultInstances: 1,
check: objectsAreDeleted, check: objectsAreDeleted,
}, },
{ {
@ -164,6 +183,8 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
Spec: acidv1.PostgresSpec{}, Spec: acidv1.PostgresSpec{},
}, },
cluster: &clusterDirtyMock, cluster: &clusterDirtyMock,
defaultImage: "pooler:1.0",
defaultInstances: 1,
check: objectsAreDeleted, check: objectsAreDeleted,
}, },
{ {
@ -183,6 +204,8 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
}, },
}, },
cluster: &clusterMock, cluster: &clusterMock,
defaultImage: "pooler:1.0",
defaultInstances: 1,
check: deploymentUpdated, check: deploymentUpdated,
}, },
{ {
@ -198,13 +221,39 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
}, },
}, },
cluster: &clusterNewDefaultsMock, cluster: &clusterNewDefaultsMock,
defaultImage: "pooler:2.0",
defaultInstances: 2,
check: deploymentUpdated, 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 { 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", t.Errorf("%s [%s]: Could not synchronize, %+v",
testName, tt.subTest, err) testName, tt.subTest, err)
} }

View File

@ -73,3 +73,8 @@ type ClusterStatus struct {
type TemplateParams map[string]interface{} type TemplateParams map[string]interface{}
type InstallFunction func(schema string, user string) error type InstallFunction func(schema string, user string) error
type SyncReason []string
// no sync happened, empty value
var NoSync SyncReason = []string{}