Skip to content

Commit 4a0c483

Browse files
authored
add unit test and documentation for finalizers (#2509)
* add unit test and documentation for finalizers * error msg with lower case and cover sync case * try to avoid adding json-patch dependency * use Update to remove finalizer * changing status and finalizer during create * do not call Delete() twice
1 parent 3bad9aa commit 4a0c483

File tree

14 files changed

+245
-118
lines changed

14 files changed

+245
-118
lines changed

docs/reference/operator_parameters.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ configuration they are grouped under the `kubernetes` key.
328328
drained if the node_readiness_label is not used. This option if set to `false`
329329
will not add the `spilo-role=master` selector to the PDB.
330330

331+
* **enable_finalizers**
332+
By default, a deletion of the Postgresql resource will trigger a cleanup of
333+
all child resources. However, if the database cluster is in a broken state
334+
(e.g. failed initialization) and the operator cannot fully sync it, there can
335+
be leftovers from a DELETE event. By enabling finalizers the Operator will
336+
ensure all managed resources are deleted prior to the Postgresql resource.
337+
The default is `false`.
338+
331339
* **enable_pod_disruption_budget**
332340
PDB is enabled by default to protect the cluster from voluntarily disruptions
333341
and hence unwanted DB downtime. However, on some cloud providers it could be

pkg/apis/acid.zalan.do/v1/const.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package v1
22

3-
// ClusterStatusUnknown etc : status of a Postgres cluster known to the operator
3+
// ClusterStatusUnknown etc : status of a Postgres cluster known to the operator
44
const (
55
ClusterStatusUnknown = ""
66
ClusterStatusCreating = "Creating"

pkg/cluster/__debug_bin4170763078

-66.1 MB
Binary file not shown.

pkg/cluster/cluster.go

Lines changed: 45 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"sync"
1414
"time"
1515

16-
jsonpatch "github.com/evanphx/json-patch"
1716
"github.com/sirupsen/logrus"
1817
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
1918

@@ -248,9 +247,10 @@ func (c *Cluster) Create() (err error) {
248247
defer c.mu.Unlock()
249248

250249
var (
251-
service *v1.Service
252-
ep *v1.Endpoints
253-
ss *appsv1.StatefulSet
250+
pgCreateStatus *acidv1.Postgresql
251+
service *v1.Service
252+
ep *v1.Endpoints
253+
ss *appsv1.StatefulSet
254254
)
255255

256256
defer func() {
@@ -261,11 +261,15 @@ func (c *Cluster) Create() (err error) {
261261
}
262262
}()
263263

264-
c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusCreating)
264+
pgCreateStatus, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), acidv1.ClusterStatusCreating)
265+
if err != nil {
266+
return fmt.Errorf("could not set cluster status: %v", err)
267+
}
268+
c.setSpec(pgCreateStatus)
269+
265270
if c.OpConfig.EnableFinalizers != nil && *c.OpConfig.EnableFinalizers {
266-
c.logger.Info("Adding finalizer.")
267-
if err = c.AddFinalizer(); err != nil {
268-
return fmt.Errorf("could not add Finalizer: %v", err)
271+
if err = c.addFinalizer(); err != nil {
272+
return fmt.Errorf("could not add finalizer: %v", err)
269273
}
270274
}
271275
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Create", "Started creation of new cluster resources")
@@ -771,60 +775,45 @@ func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) {
771775
return true, ""
772776
}
773777

774-
// AddFinalizer patches the postgresql CR to add our finalizer.
775-
func (c *Cluster) AddFinalizer() error {
776-
if c.HasFinalizer() {
777-
c.logger.Debugf("Finalizer %s already exists.", finalizerName)
778+
// addFinalizer patches the postgresql CR to add finalizer
779+
func (c *Cluster) addFinalizer() error {
780+
if c.hasFinalizer() {
778781
return nil
779782
}
780783

781-
currentSpec := c.DeepCopy()
782-
newSpec := c.DeepCopy()
783-
newSpec.ObjectMeta.SetFinalizers(append(newSpec.ObjectMeta.Finalizers, finalizerName))
784-
patchBytes, err := getPatchBytes(currentSpec, newSpec)
785-
if err != nil {
786-
return fmt.Errorf("Unable to produce patch to add finalizer: %v", err)
787-
}
788-
789-
updatedSpec, err := c.KubeClient.AcidV1ClientSet.AcidV1().Postgresqls(c.clusterNamespace()).Patch(
790-
context.TODO(), c.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
784+
c.logger.Infof("adding finalizer %s", finalizerName)
785+
finalizers := append(c.ObjectMeta.Finalizers, finalizerName)
786+
newSpec, err := c.KubeClient.SetFinalizer(c.clusterName(), c.DeepCopy(), finalizers)
791787
if err != nil {
792-
return fmt.Errorf("Could not add finalizer: %v", err)
788+
return fmt.Errorf("error adding finalizer: %v", err)
793789
}
794790

795-
// update the spec, maintaining the new resourceVersion.
796-
c.setSpec(updatedSpec)
791+
// update the spec, maintaining the new resourceVersion
792+
c.setSpec(newSpec)
797793
return nil
798794
}
799795

800-
// RemoveFinalizer patches postgresql CR to remove finalizer.
801-
func (c *Cluster) RemoveFinalizer() error {
802-
if !c.HasFinalizer() {
803-
c.logger.Debugf("No finalizer %s exists to remove.", finalizerName)
796+
// removeFinalizer patches postgresql CR to remove finalizer
797+
func (c *Cluster) removeFinalizer() error {
798+
if !c.hasFinalizer() {
804799
return nil
805800
}
806-
currentSpec := c.DeepCopy()
807-
newSpec := c.DeepCopy()
808-
newSpec.ObjectMeta.SetFinalizers(removeString(newSpec.ObjectMeta.Finalizers, finalizerName))
809-
patchBytes, err := getPatchBytes(currentSpec, newSpec)
810-
if err != nil {
811-
return fmt.Errorf("Unable to produce patch to remove finalizer: %v", err)
812-
}
813801

814-
updatedSpec, err := c.KubeClient.AcidV1ClientSet.AcidV1().Postgresqls(c.clusterNamespace()).Patch(
815-
context.TODO(), c.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
802+
c.logger.Infof("removing finalizer %s", finalizerName)
803+
finalizers := util.RemoveString(c.ObjectMeta.Finalizers, finalizerName)
804+
newSpec, err := c.KubeClient.SetFinalizer(c.clusterName(), c.DeepCopy(), finalizers)
816805
if err != nil {
817-
return fmt.Errorf("Could not remove finalizer: %v", err)
806+
return fmt.Errorf("error removing finalizer: %v", err)
818807
}
819808

820809
// update the spec, maintaining the new resourceVersion.
821-
c.setSpec(updatedSpec)
810+
c.setSpec(newSpec)
822811

823812
return nil
824813
}
825814

826-
// HasFinalizer checks if our finalizer is currently set or not
827-
func (c *Cluster) HasFinalizer() bool {
815+
// hasFinalizer checks if finalizer is currently set or not
816+
func (c *Cluster) hasFinalizer() bool {
828817
for _, finalizer := range c.ObjectMeta.Finalizers {
829818
if finalizer == finalizerName {
830819
return true
@@ -833,36 +822,6 @@ func (c *Cluster) HasFinalizer() bool {
833822
return false
834823
}
835824

836-
// Iterate through slice and remove certain string, then return cleaned slice
837-
func removeString(slice []string, s string) (result []string) {
838-
for _, item := range slice {
839-
if item == s {
840-
continue
841-
}
842-
result = append(result, item)
843-
}
844-
return result
845-
}
846-
847-
// getPatchBytes will produce a JSONpatch between the two parameters of type acidv1.Postgresql
848-
func getPatchBytes(oldSpec, newSpec *acidv1.Postgresql) ([]byte, error) {
849-
oldData, err := json.Marshal(oldSpec)
850-
if err != nil {
851-
return nil, fmt.Errorf("failed to Marshal oldSpec for postgresql %s/%s: %v", oldSpec.Namespace, oldSpec.Name, err)
852-
}
853-
854-
newData, err := json.Marshal(newSpec)
855-
if err != nil {
856-
return nil, fmt.Errorf("failed to Marshal newSpec for postgresql %s/%s: %v", newSpec.Namespace, newSpec.Name, err)
857-
}
858-
859-
patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
860-
if err != nil {
861-
return nil, fmt.Errorf("failed to CreateMergePatch for postgresl %s/%s: %v", oldSpec.Namespace, oldSpec.Name, err)
862-
}
863-
return patchBytes, nil
864-
}
865-
866825
// Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object
867826
// (i.e. service) is treated as an error
868827
// logical backup cron jobs are an exception: a user-initiated Update can enable a logical backup job
@@ -1106,40 +1065,41 @@ func syncResources(a, b *v1.ResourceRequirements) bool {
11061065
// before the pods, it will be re-created by the current master pod and will remain, obstructing the
11071066
// creation of the new cluster with the same name. Therefore, the endpoints should be deleted last.
11081067
func (c *Cluster) Delete() error {
1068+
var anyErrors = false
11091069
c.mu.Lock()
11101070
defer c.mu.Unlock()
11111071
c.eventRecorder.Event(c.GetReference(), v1.EventTypeNormal, "Delete", "Started deletion of cluster resources")
11121072

11131073
if err := c.deleteStreams(); err != nil {
1074+
anyErrors = true
11141075
c.logger.Warningf("could not delete event streams: %v", err)
1115-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not delete event streams: %v", err)
1076+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete event streams: %v", err)
11161077
}
1117-
var anyErrors = false
11181078

11191079
// delete the backup job before the stateful set of the cluster to prevent connections to non-existing pods
11201080
// deleting the cron job also removes pods and batch jobs it created
11211081
if err := c.deleteLogicalBackupJob(); err != nil {
11221082
anyErrors = true
11231083
c.logger.Warningf("could not remove the logical backup k8s cron job; %v", err)
1124-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not remove the logical backup k8s cron job; %v", err)
1084+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not remove the logical backup k8s cron job; %v", err)
11251085
}
11261086

11271087
if err := c.deleteStatefulSet(); err != nil {
11281088
anyErrors = true
11291089
c.logger.Warningf("could not delete statefulset: %v", err)
1130-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not delete statefulset: %v", err)
1090+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete statefulset: %v", err)
11311091
}
11321092

11331093
if err := c.deleteSecrets(); err != nil {
11341094
anyErrors = true
11351095
c.logger.Warningf("could not delete secrets: %v", err)
1136-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not delete secrets: %v", err)
1096+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete secrets: %v", err)
11371097
}
11381098

11391099
if err := c.deletePodDisruptionBudget(); err != nil {
11401100
anyErrors = true
11411101
c.logger.Warningf("could not delete pod disruption budget: %v", err)
1142-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not delete pod disruption budget: %v", err)
1102+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete pod disruption budget: %v", err)
11431103
}
11441104

11451105
for _, role := range []PostgresRole{Master, Replica} {
@@ -1148,21 +1108,21 @@ func (c *Cluster) Delete() error {
11481108
if err := c.deleteEndpoint(role); err != nil {
11491109
anyErrors = true
11501110
c.logger.Warningf("could not delete %s endpoint: %v", role, err)
1151-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not delete %s endpoint: %v", role, err)
1111+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete %s endpoint: %v", role, err)
11521112
}
11531113
}
11541114

11551115
if err := c.deleteService(role); err != nil {
11561116
anyErrors = true
11571117
c.logger.Warningf("could not delete %s service: %v", role, err)
1158-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not delete %s service: %v", role, err)
1118+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not delete %s service: %v", role, err)
11591119
}
11601120
}
11611121

11621122
if err := c.deletePatroniClusterObjects(); err != nil {
11631123
anyErrors = true
11641124
c.logger.Warningf("could not remove leftover patroni objects; %v", err)
1165-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not remove leftover patroni objects; %v", err)
1125+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not remove leftover patroni objects; %v", err)
11661126
}
11671127

11681128
// Delete connection pooler objects anyway, even if it's not mentioned in the
@@ -1172,20 +1132,19 @@ func (c *Cluster) Delete() error {
11721132
if err := c.deleteConnectionPooler(role); err != nil {
11731133
anyErrors = true
11741134
c.logger.Warningf("could not remove connection pooler: %v", err)
1175-
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "Could not remove connection pooler: %v", err)
1135+
c.eventRecorder.Eventf(c.GetReference(), v1.EventTypeWarning, "Delete", "could not remove connection pooler: %v", err)
11761136
}
11771137
}
11781138

11791139
// If we are done deleting our various resources we remove the finalizer to let K8S finally delete the Postgres CR
11801140
if anyErrors {
1181-
c.eventRecorder.Event(c.GetReference(), v1.EventTypeWarning, "Delete", "Some resources could be successfully deleted yet")
1141+
c.eventRecorder.Event(c.GetReference(), v1.EventTypeWarning, "Delete", "some resources could be successfully deleted yet")
11821142
return fmt.Errorf("some error(s) occured when deleting resources, NOT removing finalizer yet")
11831143
}
1184-
if err := c.RemoveFinalizer(); err != nil {
1185-
return fmt.Errorf("Done cleaning up, but error when trying to remove our finalizer: %v", err)
1144+
if err := c.removeFinalizer(); err != nil {
1145+
return fmt.Errorf("done cleaning up, but error when removing finalizer: %v", err)
11861146
}
11871147

1188-
c.logger.Info("Done cleaning up our resources, removed finalizer.")
11891148
return nil
11901149
}
11911150

pkg/cluster/cluster_test.go

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cluster
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
67
"reflect"
78
"strings"
89
"testing"
10+
"time"
911

1012
"github.com/sirupsen/logrus"
1113
"github.com/stretchr/testify/assert"
@@ -33,7 +35,10 @@ const (
3335
)
3436

3537
var logger = logrus.New().WithField("test", "cluster")
36-
var eventRecorder = record.NewFakeRecorder(1)
38+
39+
// eventRecorder needs buffer for TestCreate which emit events for
40+
// 1 cluster, primary endpoint, 2 services, the secrets, the statefulset and pods being ready
41+
var eventRecorder = record.NewFakeRecorder(7)
3742

3843
var cl = New(
3944
Config{
@@ -79,6 +84,79 @@ var cl = New(
7984
eventRecorder,
8085
)
8186

87+
func TestCreate(t *testing.T) {
88+
clientSet := fake.NewSimpleClientset()
89+
acidClientSet := fakeacidv1.NewSimpleClientset()
90+
clusterName := "cluster-with-finalizer"
91+
clusterNamespace := "test"
92+
93+
client := k8sutil.KubernetesClient{
94+
DeploymentsGetter: clientSet.AppsV1(),
95+
EndpointsGetter: clientSet.CoreV1(),
96+
PersistentVolumeClaimsGetter: clientSet.CoreV1(),
97+
PodDisruptionBudgetsGetter: clientSet.PolicyV1(),
98+
PodsGetter: clientSet.CoreV1(),
99+
PostgresqlsGetter: acidClientSet.AcidV1(),
100+
ServicesGetter: clientSet.CoreV1(),
101+
SecretsGetter: clientSet.CoreV1(),
102+
StatefulSetsGetter: clientSet.AppsV1(),
103+
}
104+
105+
pg := acidv1.Postgresql{
106+
ObjectMeta: metav1.ObjectMeta{
107+
Name: clusterName,
108+
Namespace: clusterNamespace,
109+
},
110+
Spec: acidv1.PostgresSpec{
111+
Volume: acidv1.Volume{
112+
Size: "1Gi",
113+
},
114+
},
115+
}
116+
117+
pod := v1.Pod{
118+
ObjectMeta: metav1.ObjectMeta{
119+
Name: fmt.Sprintf("%s-0", clusterName),
120+
Namespace: clusterNamespace,
121+
Labels: map[string]string{
122+
"application": "spilo",
123+
"cluster-name": clusterName,
124+
"spilo-role": "master",
125+
},
126+
},
127+
}
128+
129+
// manually create resources which must be found by further API calls and are not created by cluster.Create()
130+
client.Postgresqls(clusterNamespace).Create(context.TODO(), &pg, metav1.CreateOptions{})
131+
client.Pods(clusterNamespace).Create(context.TODO(), &pod, metav1.CreateOptions{})
132+
133+
var cluster = New(
134+
Config{
135+
OpConfig: config.Config{
136+
PodManagementPolicy: "ordered_ready",
137+
Resources: config.Resources{
138+
ClusterLabels: map[string]string{"application": "spilo"},
139+
ClusterNameLabel: "cluster-name",
140+
DefaultCPURequest: "300m",
141+
DefaultCPULimit: "300m",
142+
DefaultMemoryRequest: "300Mi",
143+
DefaultMemoryLimit: "300Mi",
144+
PodRoleLabel: "spilo-role",
145+
ResourceCheckInterval: time.Duration(3),
146+
ResourceCheckTimeout: time.Duration(10),
147+
},
148+
EnableFinalizers: util.True(),
149+
},
150+
}, client, pg, logger, eventRecorder)
151+
152+
err := cluster.Create()
153+
assert.NoError(t, err)
154+
155+
if !cluster.hasFinalizer() {
156+
t.Errorf("%s - expected finalizer not found on cluster", t.Name())
157+
}
158+
}
159+
82160
func TestStatefulSetAnnotations(t *testing.T) {
83161
spec := acidv1.PostgresSpec{
84162
TeamID: "myapp", NumberOfInstances: 1,

0 commit comments

Comments
 (0)