Skip to content

Commit 6a689cd

Browse files
authored
Prevent empty syncs (zalando#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.
1 parent 7e8f668 commit 6a689cd

File tree

4 files changed

+127
-46
lines changed

4 files changed

+127
-46
lines changed

pkg/cluster/cluster.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,8 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
741741
}
742742

743743
// sync connection pooler
744-
if err := c.syncConnectionPooler(oldSpec, newSpec, c.installLookupFunction); err != nil {
744+
if _, err := c.syncConnectionPooler(oldSpec, newSpec,
745+
c.installLookupFunction); err != nil {
745746
return fmt.Errorf("could not sync connection pooler: %v", err)
746747
}
747748

pkg/cluster/sync.go

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
111111
}
112112

113113
// sync connection pooler
114-
if err = c.syncConnectionPooler(&oldSpec, newSpec, c.installLookupFunction); err != nil {
114+
if _, err = c.syncConnectionPooler(&oldSpec, newSpec, c.installLookupFunction); err != nil {
115115
return fmt.Errorf("could not sync connection pooler: %v", err)
116116
}
117117

@@ -620,7 +620,13 @@ func (c *Cluster) syncLogicalBackupJob() error {
620620
return nil
621621
}
622622

623-
func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, lookup InstallFunction) error {
623+
func (c *Cluster) syncConnectionPooler(oldSpec,
624+
newSpec *acidv1.Postgresql,
625+
lookup InstallFunction) (SyncReason, error) {
626+
627+
var reason SyncReason
628+
var err error
629+
624630
if c.ConnectionPooler == nil {
625631
c.ConnectionPooler = &ConnectionPoolerObjects{}
626632
}
@@ -657,20 +663,20 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, look
657663
specUser,
658664
c.OpConfig.ConnectionPooler.User)
659665

660-
if err := lookup(schema, user); err != nil {
661-
return err
666+
if err = lookup(schema, user); err != nil {
667+
return NoSync, err
662668
}
663669
}
664670

665-
if err := c.syncConnectionPoolerWorker(oldSpec, newSpec); err != nil {
671+
if reason, err = c.syncConnectionPoolerWorker(oldSpec, newSpec); err != nil {
666672
c.logger.Errorf("could not sync connection pooler: %v", err)
667-
return err
673+
return reason, err
668674
}
669675
}
670676

671677
if oldNeedConnectionPooler && !newNeedConnectionPooler {
672678
// delete and cleanup resources
673-
if err := c.deleteConnectionPooler(); err != nil {
679+
if err = c.deleteConnectionPooler(); err != nil {
674680
c.logger.Warningf("could not remove connection pooler: %v", err)
675681
}
676682
}
@@ -681,20 +687,22 @@ func (c *Cluster) syncConnectionPooler(oldSpec, newSpec *acidv1.Postgresql, look
681687
(c.ConnectionPooler.Deployment != nil ||
682688
c.ConnectionPooler.Service != nil) {
683689

684-
if err := c.deleteConnectionPooler(); err != nil {
690+
if err = c.deleteConnectionPooler(); err != nil {
685691
c.logger.Warningf("could not remove connection pooler: %v", err)
686692
}
687693
}
688694
}
689695

690-
return nil
696+
return reason, nil
691697
}
692698

693699
// Synchronize connection pooler resources. Effectively we're interested only in
694700
// synchronizing the corresponding deployment, but in case of deployment or
695701
// service is missing, create it. After checking, also remember an object for
696702
// the future references.
697-
func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql) error {
703+
func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql) (
704+
SyncReason, error) {
705+
698706
deployment, err := c.KubeClient.
699707
Deployments(c.Namespace).
700708
Get(context.TODO(), c.connectionPoolerName(), metav1.GetOptions{})
@@ -706,26 +714,43 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
706714
deploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec)
707715
if err != nil {
708716
msg = "could not generate deployment for connection pooler: %v"
709-
return fmt.Errorf(msg, err)
717+
return NoSync, fmt.Errorf(msg, err)
710718
}
711719

712720
deployment, err := c.KubeClient.
713721
Deployments(deploymentSpec.Namespace).
714722
Create(context.TODO(), deploymentSpec, metav1.CreateOptions{})
715723

716724
if err != nil {
717-
return err
725+
return NoSync, err
718726
}
719727

720728
c.ConnectionPooler.Deployment = deployment
721729
} else if err != nil {
722-
return fmt.Errorf("could not get connection pooler deployment to sync: %v", err)
730+
msg := "could not get connection pooler deployment to sync: %v"
731+
return NoSync, fmt.Errorf(msg, err)
723732
} else {
724733
c.ConnectionPooler.Deployment = deployment
725734

726735
// actual synchronization
727736
oldConnectionPooler := oldSpec.Spec.ConnectionPooler
728737
newConnectionPooler := newSpec.Spec.ConnectionPooler
738+
739+
// sync implementation below assumes that both old and new specs are
740+
// not nil, but it can happen. To avoid any confusion like updating a
741+
// deployment because the specification changed from nil to an empty
742+
// struct (that was initialized somewhere before) replace any nil with
743+
// an empty spec.
744+
if oldConnectionPooler == nil {
745+
oldConnectionPooler = &acidv1.ConnectionPooler{}
746+
}
747+
748+
if newConnectionPooler == nil {
749+
newConnectionPooler = &acidv1.ConnectionPooler{}
750+
}
751+
752+
c.logger.Infof("Old: %+v, New %+v", oldConnectionPooler, newConnectionPooler)
753+
729754
specSync, specReason := c.needSyncConnectionPoolerSpecs(oldConnectionPooler, newConnectionPooler)
730755
defaultsSync, defaultsReason := c.needSyncConnectionPoolerDefaults(newConnectionPooler, deployment)
731756
reason := append(specReason, defaultsReason...)
@@ -736,7 +761,7 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
736761
newDeploymentSpec, err := c.generateConnectionPoolerDeployment(&newSpec.Spec)
737762
if err != nil {
738763
msg := "could not generate deployment for connection pooler: %v"
739-
return fmt.Errorf(msg, err)
764+
return reason, fmt.Errorf(msg, err)
740765
}
741766

742767
oldDeploymentSpec := c.ConnectionPooler.Deployment
@@ -746,11 +771,11 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
746771
newDeploymentSpec)
747772

748773
if err != nil {
749-
return err
774+
return reason, err
750775
}
751776

752777
c.ConnectionPooler.Deployment = deployment
753-
return nil
778+
return reason, nil
754779
}
755780
}
756781

@@ -768,16 +793,17 @@ func (c *Cluster) syncConnectionPoolerWorker(oldSpec, newSpec *acidv1.Postgresql
768793
Create(context.TODO(), serviceSpec, metav1.CreateOptions{})
769794

770795
if err != nil {
771-
return err
796+
return NoSync, err
772797
}
773798

774799
c.ConnectionPooler.Service = service
775800
} else if err != nil {
776-
return fmt.Errorf("could not get connection pooler service to sync: %v", err)
801+
msg := "could not get connection pooler service to sync: %v"
802+
return NoSync, fmt.Errorf(msg, err)
777803
} else {
778804
// Service updates are not supported and probably not that useful anyway
779805
c.ConnectionPooler.Service = service
780806
}
781807

782-
return nil
808+
return NoSync, nil
783809
}

pkg/cluster/sync_test.go

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cluster
22

33
import (
44
"fmt"
5+
"strings"
56
"testing"
67

78
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
@@ -17,7 +18,7 @@ func int32ToPointer(value int32) *int32 {
1718
return &value
1819
}
1920

20-
func deploymentUpdated(cluster *Cluster, err error) error {
21+
func deploymentUpdated(cluster *Cluster, err error, reason SyncReason) error {
2122
if cluster.ConnectionPooler.Deployment.Spec.Replicas == nil ||
2223
*cluster.ConnectionPooler.Deployment.Spec.Replicas != 2 {
2324
return fmt.Errorf("Wrong nubmer of instances")
@@ -26,7 +27,7 @@ func deploymentUpdated(cluster *Cluster, err error) error {
2627
return nil
2728
}
2829

29-
func objectsAreSaved(cluster *Cluster, err error) error {
30+
func objectsAreSaved(cluster *Cluster, err error, reason SyncReason) error {
3031
if cluster.ConnectionPooler == nil {
3132
return fmt.Errorf("Connection pooler resources are empty")
3233
}
@@ -42,14 +43,24 @@ func objectsAreSaved(cluster *Cluster, err error) error {
4243
return nil
4344
}
4445

45-
func objectsAreDeleted(cluster *Cluster, err error) error {
46+
func objectsAreDeleted(cluster *Cluster, err error, reason SyncReason) error {
4647
if cluster.ConnectionPooler != nil {
4748
return fmt.Errorf("Connection pooler was not deleted")
4849
}
4950

5051
return nil
5152
}
5253

54+
func noEmptySync(cluster *Cluster, err error, reason SyncReason) error {
55+
for _, msg := range reason {
56+
if strings.HasPrefix(msg, "update [] from '<nil>' to '") {
57+
return fmt.Errorf("There is an empty reason, %s", msg)
58+
}
59+
}
60+
61+
return nil
62+
}
63+
5364
func TestConnectionPoolerSynchronization(t *testing.T) {
5465
testName := "Test connection pooler synchronization"
5566
var cluster = New(
@@ -91,15 +102,15 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
91102

92103
clusterNewDefaultsMock := *cluster
93104
clusterNewDefaultsMock.KubeClient = k8sutil.NewMockKubernetesClient()
94-
cluster.OpConfig.ConnectionPooler.Image = "pooler:2.0"
95-
cluster.OpConfig.ConnectionPooler.NumberOfInstances = int32ToPointer(2)
96105

97106
tests := []struct {
98-
subTest string
99-
oldSpec *acidv1.Postgresql
100-
newSpec *acidv1.Postgresql
101-
cluster *Cluster
102-
check func(cluster *Cluster, err error) error
107+
subTest string
108+
oldSpec *acidv1.Postgresql
109+
newSpec *acidv1.Postgresql
110+
cluster *Cluster
111+
defaultImage string
112+
defaultInstances int32
113+
check func(cluster *Cluster, err error, reason SyncReason) error
103114
}{
104115
{
105116
subTest: "create if doesn't exist",
@@ -113,8 +124,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
113124
ConnectionPooler: &acidv1.ConnectionPooler{},
114125
},
115126
},
116-
cluster: &clusterMissingObjects,
117-
check: objectsAreSaved,
127+
cluster: &clusterMissingObjects,
128+
defaultImage: "pooler:1.0",
129+
defaultInstances: 1,
130+
check: objectsAreSaved,
118131
},
119132
{
120133
subTest: "create if doesn't exist with a flag",
@@ -126,8 +139,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
126139
EnableConnectionPooler: boolToPointer(true),
127140
},
128141
},
129-
cluster: &clusterMissingObjects,
130-
check: objectsAreSaved,
142+
cluster: &clusterMissingObjects,
143+
defaultImage: "pooler:1.0",
144+
defaultInstances: 1,
145+
check: objectsAreSaved,
131146
},
132147
{
133148
subTest: "create from scratch",
@@ -139,8 +154,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
139154
ConnectionPooler: &acidv1.ConnectionPooler{},
140155
},
141156
},
142-
cluster: &clusterMissingObjects,
143-
check: objectsAreSaved,
157+
cluster: &clusterMissingObjects,
158+
defaultImage: "pooler:1.0",
159+
defaultInstances: 1,
160+
check: objectsAreSaved,
144161
},
145162
{
146163
subTest: "delete if not needed",
@@ -152,8 +169,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
152169
newSpec: &acidv1.Postgresql{
153170
Spec: acidv1.PostgresSpec{},
154171
},
155-
cluster: &clusterMock,
156-
check: objectsAreDeleted,
172+
cluster: &clusterMock,
173+
defaultImage: "pooler:1.0",
174+
defaultInstances: 1,
175+
check: objectsAreDeleted,
157176
},
158177
{
159178
subTest: "cleanup if still there",
@@ -163,8 +182,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
163182
newSpec: &acidv1.Postgresql{
164183
Spec: acidv1.PostgresSpec{},
165184
},
166-
cluster: &clusterDirtyMock,
167-
check: objectsAreDeleted,
185+
cluster: &clusterDirtyMock,
186+
defaultImage: "pooler:1.0",
187+
defaultInstances: 1,
188+
check: objectsAreDeleted,
168189
},
169190
{
170191
subTest: "update deployment",
@@ -182,8 +203,10 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
182203
},
183204
},
184205
},
185-
cluster: &clusterMock,
186-
check: deploymentUpdated,
206+
cluster: &clusterMock,
207+
defaultImage: "pooler:1.0",
208+
defaultInstances: 1,
209+
check: deploymentUpdated,
187210
},
188211
{
189212
subTest: "update image from changed defaults",
@@ -197,14 +220,40 @@ func TestConnectionPoolerSynchronization(t *testing.T) {
197220
ConnectionPooler: &acidv1.ConnectionPooler{},
198221
},
199222
},
200-
cluster: &clusterNewDefaultsMock,
201-
check: deploymentUpdated,
223+
cluster: &clusterNewDefaultsMock,
224+
defaultImage: "pooler:2.0",
225+
defaultInstances: 2,
226+
check: deploymentUpdated,
227+
},
228+
{
229+
subTest: "there is no sync from nil to an empty spec",
230+
oldSpec: &acidv1.Postgresql{
231+
Spec: acidv1.PostgresSpec{
232+
EnableConnectionPooler: boolToPointer(true),
233+
ConnectionPooler: nil,
234+
},
235+
},
236+
newSpec: &acidv1.Postgresql{
237+
Spec: acidv1.PostgresSpec{
238+
EnableConnectionPooler: boolToPointer(true),
239+
ConnectionPooler: &acidv1.ConnectionPooler{},
240+
},
241+
},
242+
cluster: &clusterMock,
243+
defaultImage: "pooler:1.0",
244+
defaultInstances: 1,
245+
check: noEmptySync,
202246
},
203247
}
204248
for _, tt := range tests {
205-
err := tt.cluster.syncConnectionPooler(tt.oldSpec, tt.newSpec, mockInstallLookupFunction)
249+
tt.cluster.OpConfig.ConnectionPooler.Image = tt.defaultImage
250+
tt.cluster.OpConfig.ConnectionPooler.NumberOfInstances =
251+
int32ToPointer(tt.defaultInstances)
252+
253+
reason, err := tt.cluster.syncConnectionPooler(tt.oldSpec,
254+
tt.newSpec, mockInstallLookupFunction)
206255

207-
if err := tt.check(tt.cluster, err); err != nil {
256+
if err := tt.check(tt.cluster, err, reason); err != nil {
208257
t.Errorf("%s [%s]: Could not synchronize, %+v",
209258
testName, tt.subTest, err)
210259
}

pkg/cluster/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,8 @@ type ClusterStatus struct {
7373
type TemplateParams map[string]interface{}
7474

7575
type InstallFunction func(schema string, user string) error
76+
77+
type SyncReason []string
78+
79+
// no sync happened, empty value
80+
var NoSync SyncReason = []string{}

0 commit comments

Comments
 (0)