Skip to content

Commit 654d22d

Browse files
FxKumoshloop
andauthored
Configure annotations to be ignored in comparisons during sync (zalando#1823)
* feat: add ignored annotations when comparing during sync Co-authored-by: Felix Kunde <[email protected]> Co-authored-by: Moshe Immerman <[email protected]>
1 parent 36df1bc commit 654d22d

20 files changed

+479
-370
lines changed

charts/postgres-operator/crds/operatorconfigurations.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ spec:
212212
enable_sidecars:
213213
type: boolean
214214
default: true
215+
ignored_annotations:
216+
type: array
217+
items:
218+
type: string
215219
infrastructure_roles_secret_name:
216220
type: string
217221
infrastructure_roles_secrets:

charts/postgres-operator/values.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ configKubernetes:
124124
enable_pod_disruption_budget: true
125125
# enables sidecar containers to run alongside Spilo in the same pod
126126
enable_sidecars: true
127+
128+
# annotations to be ignored when comparing statefulsets, services etc.
129+
# ignored_annotations:
130+
# - k8s.v1.cni.cncf.io/network-status
131+
127132
# namespaced name of the secret containing infrastructure roles names and passwords
128133
# infrastructure_roles_secret_name: postgresql-infrastructure-roles
129134

cmd/main.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package main
22

33
import (
44
"flag"
5-
log "github.com/sirupsen/logrus"
65
"os"
76
"os/signal"
87
"sync"
98
"syscall"
109
"time"
1110

11+
log "github.com/sirupsen/logrus"
12+
1213
"github.com/zalando/postgres-operator/pkg/controller"
1314
"github.com/zalando/postgres-operator/pkg/spec"
1415
"github.com/zalando/postgres-operator/pkg/util/k8sutil"

docs/reference/operator_parameters.md

+6
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ configuration they are grouped under the `kubernetes` key.
287287
Regular expressions like `downscaler/*` etc. are also accepted. Can be used
288288
with [kube-downscaler](https://github.com/hjacobs/kube-downscaler).
289289

290+
* **ignored_annotations**
291+
Some K8s tools inject and update annotations out of the Postgres Operator
292+
control. This can cause rolling updates on each cluster sync cycle. With
293+
this option you can specify an array of annotation keys that should be
294+
ignored when comparing K8s resources on sync. The default is empty.
295+
290296
* **watched_namespace**
291297
The operator watches for Postgres objects in the given namespace. If not
292298
specified, the value is taken from the operator namespace. A special `*`

e2e/tests/test_e2e.py

+43
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,49 @@ def test_enable_load_balancer(self):
704704
print('Operator log: {}'.format(k8s.get_operator_log()))
705705
raise
706706

707+
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
708+
def test_ignored_annotations(self):
709+
'''
710+
Test if injected annotation does not cause replacement of resources when listed under ignored_annotations
711+
'''
712+
k8s = self.k8s
713+
714+
annotation_patch = {
715+
"metadata": {
716+
"annotations": {
717+
"k8s-status": "healthy"
718+
},
719+
}
720+
}
721+
722+
try:
723+
sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default')
724+
old_sts_creation_timestamp = sts.metadata.creation_timestamp
725+
k8s.api.apps_v1.patch_namespaced_stateful_set(sts.metadata.name, sts.metadata.namespace, annotation_patch)
726+
svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default')
727+
old_svc_creation_timestamp = svc.metadata.creation_timestamp
728+
k8s.api.core_v1.patch_namespaced_service(svc.metadata.name, svc.metadata.namespace, annotation_patch)
729+
730+
patch_config_ignored_annotations = {
731+
"data": {
732+
"ignored_annotations": "k8s-status",
733+
}
734+
}
735+
k8s.update_config(patch_config_ignored_annotations)
736+
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
737+
738+
sts = k8s.api.apps_v1.read_namespaced_stateful_set('acid-minimal-cluster', 'default')
739+
new_sts_creation_timestamp = sts.metadata.creation_timestamp
740+
svc = k8s.api.core_v1.read_namespaced_service('acid-minimal-cluster', 'default')
741+
new_svc_creation_timestamp = svc.metadata.creation_timestamp
742+
743+
self.assertEqual(old_sts_creation_timestamp, new_sts_creation_timestamp, "unexpected replacement of statefulset on sync")
744+
self.assertEqual(old_svc_creation_timestamp, new_svc_creation_timestamp, "unexpected replacement of master service on sync")
745+
746+
except timeout_decorator.TimeoutError:
747+
print('Operator log: {}'.format(k8s.get_operator_log()))
748+
raise
749+
707750
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
708751
def test_infrastructure_roles(self):
709752
'''

manifests/configmap.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ data:
6464
external_traffic_policy: "Cluster"
6565
# gcp_credentials: ""
6666
# kubernetes_use_configmaps: "false"
67+
# ignored_annotations: ""
6768
# infrastructure_roles_secret_name: "postgresql-infrastructure-roles"
6869
# infrastructure_roles_secrets: "secretname:monitoring-roles,userkey:user,passwordkey:password,rolekey:inrole"
6970
# inherited_annotations: owned-by

manifests/operatorconfiguration.crd.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ spec:
210210
enable_sidecars:
211211
type: boolean
212212
default: true
213+
ignored_annotations:
214+
type: array
215+
items:
216+
type: string
213217
infrastructure_roles_secret_name:
214218
type: string
215219
infrastructure_roles_secrets:

manifests/postgresql-operator-default-configuration.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ configuration:
5959
enable_pod_antiaffinity: false
6060
enable_pod_disruption_budget: true
6161
enable_sidecars: true
62+
# ignored_annotations:
63+
# - k8s.v1.cni.cncf.io/network-status
6264
# infrastructure_roles_secret_name: "postgresql-infrastructure-roles"
6365
# infrastructure_roles_secrets:
6466
# - secretname: "monitoring-roles"

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

+8
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,14 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
12511251
"enable_sidecars": {
12521252
Type: "boolean",
12531253
},
1254+
"ignored_annotations": {
1255+
Type: "array",
1256+
Items: &apiextv1.JSONSchemaPropsOrArray{
1257+
Schema: &apiextv1.JSONSchemaProps{
1258+
Type: "string",
1259+
},
1260+
},
1261+
},
12541262
"infrastructure_roles_secret_name": {
12551263
Type: "string",
12561264
},

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

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type KubernetesMetaConfiguration struct {
8282
InheritedLabels []string `json:"inherited_labels,omitempty"`
8383
InheritedAnnotations []string `json:"inherited_annotations,omitempty"`
8484
DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"`
85+
IgnoredAnnotations []string `json:"ignored_annotations,omitempty"`
8586
ClusterNameLabel string `json:"cluster_name_label,omitempty"`
8687
DeleteAnnotationDateKey string `json:"delete_annotation_date_key,omitempty"`
8788
DeleteAnnotationNameKey string `json:"delete_annotation_name_key,omitempty"`

pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cluster/cluster.go

+62-5
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,10 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
378378
match = false
379379
reasons = append(reasons, "new statefulset's number of replicas does not match the current one")
380380
}
381-
if !reflect.DeepEqual(c.Statefulset.Annotations, statefulSet.Annotations) {
381+
if changed, reason := c.compareAnnotations(c.Statefulset.Annotations, statefulSet.Annotations); changed {
382+
match = false
382383
needsReplace = true
383-
reasons = append(reasons, "new statefulset's annotations do not match the current one")
384+
reasons = append(reasons, "new statefulset's annotations do not match: "+reason)
384385
}
385386

386387
needsRollUpdate, reasons = c.compareContainers("initContainers", c.Statefulset.Spec.Template.Spec.InitContainers, statefulSet.Spec.Template.Spec.InitContainers, needsRollUpdate, reasons)
@@ -434,10 +435,11 @@ func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compa
434435
}
435436
}
436437

437-
if !reflect.DeepEqual(c.Statefulset.Spec.Template.Annotations, statefulSet.Spec.Template.Annotations) {
438+
if changed, reason := c.compareAnnotations(c.Statefulset.Spec.Template.Annotations, statefulSet.Spec.Template.Annotations); changed {
439+
match = false
438440
needsReplace = true
439441
needsRollUpdate = true
440-
reasons = append(reasons, "new statefulset's pod template metadata annotations does not match the current one")
442+
reasons = append(reasons, "new statefulset's pod template metadata annotations does not match "+reason)
441443
}
442444
if !reflect.DeepEqual(c.Statefulset.Spec.Template.Spec.SecurityContext, statefulSet.Spec.Template.Spec.SecurityContext) {
443445
needsReplace = true
@@ -672,6 +674,61 @@ func comparePorts(a, b []v1.ContainerPort) bool {
672674
return true
673675
}
674676

677+
func (c *Cluster) compareAnnotations(old, new map[string]string) (bool, string) {
678+
reason := ""
679+
ignoredAnnotations := make(map[string]bool)
680+
for _, ignore := range c.OpConfig.IgnoredAnnotations {
681+
ignoredAnnotations[ignore] = true
682+
}
683+
684+
for key := range old {
685+
if _, ok := ignoredAnnotations[key]; ok {
686+
continue
687+
}
688+
if _, ok := new[key]; !ok {
689+
reason += fmt.Sprintf(" Removed %q.", key)
690+
}
691+
}
692+
693+
for key := range new {
694+
if _, ok := ignoredAnnotations[key]; ok {
695+
continue
696+
}
697+
v, ok := old[key]
698+
if !ok {
699+
reason += fmt.Sprintf(" Added %q with value %q.", key, new[key])
700+
} else if v != new[key] {
701+
reason += fmt.Sprintf(" %q changed from %q to %q.", key, v, new[key])
702+
}
703+
}
704+
705+
return reason != "", reason
706+
707+
}
708+
709+
func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) {
710+
if old.Spec.Type != new.Spec.Type {
711+
return false, fmt.Sprintf("new service's type %q does not match the current one %q",
712+
new.Spec.Type, old.Spec.Type)
713+
}
714+
715+
oldSourceRanges := old.Spec.LoadBalancerSourceRanges
716+
newSourceRanges := new.Spec.LoadBalancerSourceRanges
717+
718+
/* work around Kubernetes 1.6 serializing [] as nil. See https://github.com/kubernetes/kubernetes/issues/43203 */
719+
if (len(oldSourceRanges) != 0) || (len(newSourceRanges) != 0) {
720+
if !util.IsEqualIgnoreOrder(oldSourceRanges, newSourceRanges) {
721+
return false, "new service's LoadBalancerSourceRange does not match the current one"
722+
}
723+
}
724+
725+
if changed, reason := c.compareAnnotations(old.Annotations, new.Annotations); changed {
726+
return !changed, "new service's annotations does not match the current one:" + reason
727+
}
728+
729+
return true, ""
730+
}
731+
675732
// Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object
676733
// (i.e. service) is treated as an error
677734
// logical backup cron jobs are an exception: a user-initiated Update can enable a logical backup job
@@ -764,7 +821,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
764821
updateFailed = true
765822
return
766823
}
767-
if syncStatefulSet || !reflect.DeepEqual(oldSs, newSs) || !reflect.DeepEqual(oldSpec.Annotations, newSpec.Annotations) {
824+
if syncStatefulSet || !reflect.DeepEqual(oldSs, newSs) {
768825
c.logger.Debugf("syncing statefulsets")
769826
syncStatefulSet = false
770827
// TODO: avoid generating the StatefulSet object twice by passing it to syncStatefulSet

0 commit comments

Comments
 (0)