Skip to content

Commit ff81437

Browse files
authored
Improve rolling upgrades and rolling upgrade continue (zalando#1341)
* add TODOs for moving rooling update label on pods * steer rolling update via pod annotation * rename patch method and fix reading flag on sync * pass only pods to recreatePods function * do not take address of iterator if you use it later * add e2e test and pass switchover targets to recreatePods * add wait_for_pod_failover for e2e test * add one more e2e test case * helm chart remove 1.6.0 archive from 1.6.0 archive * reflect code review feedback
1 parent e837751 commit ff81437

File tree

8 files changed

+330
-169
lines changed

8 files changed

+330
-169
lines changed

e2e/scripts/watch_objects.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ watch -c "
44
kubectl get postgresql --all-namespaces
55
echo
66
echo -n 'Rolling upgrade pending: '
7-
kubectl get statefulset -o jsonpath='{.items..metadata.annotations.zalando-postgres-operator-rolling-update-required}'
7+
kubectl get pods -o jsonpath='{.items[].metadata.annotations.zalando-postgres-operator-rolling-update-required}'
88
echo
99
echo
1010
echo 'Pods'

e2e/tests/k8s_api.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,16 @@ def wait_for_logical_backup_job_creation(self):
211211
self.wait_for_logical_backup_job(expected_num_of_jobs=1)
212212

213213
def delete_operator_pod(self, step="Delete operator pod"):
214-
# patching the pod template in the deployment restarts the operator pod
214+
# patching the pod template in the deployment restarts the operator pod
215215
self.api.apps_v1.patch_namespaced_deployment("postgres-operator", "default", {"spec": {"template": {"metadata": {"annotations": {"step": "{}-{}".format(step, time.time())}}}}})
216216
self.wait_for_operator_pod_start()
217217

218218
def update_config(self, config_map_patch, step="Updating operator deployment"):
219219
self.api.core_v1.patch_namespaced_config_map("postgres-operator", "default", config_map_patch)
220220
self.delete_operator_pod(step=step)
221221

222-
def patch_statefulset(self, data, name="acid-minimal-cluster", namespace="default"):
223-
self.api.apps_v1.patch_namespaced_stateful_set(name, namespace, data)
222+
def patch_pod(self, data, pod_name, namespace="default"):
223+
self.api.core_v1.patch_namespaced_pod(pod_name, namespace, data)
224224

225225
def create_with_kubectl(self, path):
226226
return subprocess.run(
@@ -280,19 +280,21 @@ def get_effective_pod_image(self, pod_name, namespace='default'):
280280
return None
281281
return pod.items[0].spec.containers[0].image
282282

283-
def get_cluster_leader_pod(self, pg_cluster_name, namespace='default'):
284-
labels = {
285-
'application': 'spilo',
286-
'cluster-name': pg_cluster_name,
287-
'spilo-role': 'master',
288-
}
283+
def get_cluster_pod(self, role, labels='application=spilo,cluster-name=acid-minimal-cluster', namespace='default'):
284+
labels = labels + ',spilo-role=' + role
289285

290286
pods = self.api.core_v1.list_namespaced_pod(
291-
namespace, label_selector=to_selector(labels)).items
287+
namespace, label_selector=labels).items
292288

293289
if pods:
294290
return pods[0]
295291

292+
def get_cluster_leader_pod(self, labels='application=spilo,cluster-name=acid-minimal-cluster', namespace='default'):
293+
return self.get_cluster_pod('master', labels, namespace)
294+
295+
def get_cluster_replica_pod(self, labels='application=spilo,cluster-name=acid-minimal-cluster', namespace='default'):
296+
return self.get_cluster_pod('replica', labels, namespace)
297+
296298

297299
class K8sBase:
298300
'''

e2e/tests/test_e2e.py

Lines changed: 155 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,25 @@ def test_additional_pod_capabilities(self):
168168
"additional_pod_capabilities": ','.join(capabilities),
169169
},
170170
}
171-
self.k8s.update_config(patch_capabilities)
172-
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"},
173-
"Operator does not get in sync")
174-
175-
self.eventuallyEqual(lambda: self.k8s.count_pods_with_container_capabilities(capabilities, cluster_label),
176-
2, "Container capabilities not updated")
171+
172+
# get node and replica (expected target of new master)
173+
_, replica_nodes = self.k8s.get_pg_nodes(cluster_label)
174+
175+
try:
176+
self.k8s.update_config(patch_capabilities)
177+
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"},
178+
"Operator does not get in sync")
179+
180+
# changed security context of postrges container should trigger a rolling update
181+
self.k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
182+
self.k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
183+
184+
self.eventuallyEqual(lambda: self.k8s.count_pods_with_container_capabilities(capabilities, cluster_label),
185+
2, "Container capabilities not updated")
186+
187+
except timeout_decorator.TimeoutError:
188+
print('Operator log: {}'.format(k8s.get_operator_log()))
189+
raise
177190

178191
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
179192
def test_additional_teams_and_members(self):
@@ -212,7 +225,7 @@ def test_additional_teams_and_members(self):
212225
# make sure we let one sync pass and the new user being added
213226
time.sleep(15)
214227

215-
leader = self.k8s.get_cluster_leader_pod('acid-minimal-cluster')
228+
leader = self.k8s.get_cluster_leader_pod()
216229
user_query = """
217230
SELECT usename
218231
FROM pg_catalog.pg_user
@@ -392,7 +405,7 @@ def test_enable_disable_connection_pooler(self):
392405
# credentials.
393406
db_list = []
394407

395-
leader = k8s.get_cluster_leader_pod('acid-minimal-cluster')
408+
leader = k8s.get_cluster_leader_pod()
396409
schemas_query = """
397410
select schema_name
398411
from information_schema.schemata
@@ -611,7 +624,7 @@ def test_lazy_spilo_upgrade(self):
611624
k8s.update_config(unpatch_lazy_spilo_upgrade, step="patch lazy upgrade")
612625

613626
# at this point operator will complete the normal rolling upgrade
614-
# so we additonally test if disabling the lazy upgrade - forcing the normal rolling upgrade - works
627+
# so we additionally test if disabling the lazy upgrade - forcing the normal rolling upgrade - works
615628
self.eventuallyEqual(lambda: k8s.get_effective_pod_image(pod0),
616629
conf_image, "Rolling upgrade was not executed",
617630
50, 3)
@@ -750,12 +763,6 @@ def verify_pod_limits():
750763

751764
self.eventuallyTrue(verify_pod_limits, "Pod limits where not adjusted")
752765

753-
@classmethod
754-
def setUp(cls):
755-
# cls.k8s.update_config({}, step="Setup")
756-
cls.k8s.patch_statefulset({"meta": {"annotations": {"zalando-postgres-operator-rolling-update-required": False}}})
757-
pass
758-
759766
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
760767
def test_multi_namespace_support(self):
761768
'''
@@ -784,6 +791,139 @@ def test_multi_namespace_support(self):
784791
"acid.zalan.do", "v1", self.test_namespace, "postgresqls", "acid-test-cluster")
785792
time.sleep(5)
786793

794+
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
795+
def test_rolling_update_flag(self):
796+
'''
797+
Add rolling update flag to only the master and see it failing over
798+
'''
799+
k8s = self.k8s
800+
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
801+
802+
# verify we are in good state from potential previous tests
803+
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
804+
805+
# get node and replica (expected target of new master)
806+
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
807+
808+
# rolling update annotation
809+
flag = {
810+
"metadata": {
811+
"annotations": {
812+
"zalando-postgres-operator-rolling-update-required": "true",
813+
}
814+
}
815+
}
816+
817+
try:
818+
podsList = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_label)
819+
for pod in podsList.items:
820+
# add flag only to the master to make it appear to the operator as a leftover from a rolling update
821+
if pod.metadata.labels.get('spilo-role') == 'master':
822+
old_creation_timestamp = pod.metadata.creation_timestamp
823+
k8s.patch_pod(flag, pod.metadata.name, pod.metadata.namespace)
824+
else:
825+
# remember replica name to check if operator does a switchover
826+
switchover_target = pod.metadata.name
827+
828+
# do not wait until the next sync
829+
k8s.delete_operator_pod()
830+
831+
# operator should now recreate the master pod and do a switchover before
832+
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
833+
834+
# check if the former replica is now the new master
835+
leader = k8s.get_cluster_leader_pod()
836+
self.eventuallyEqual(lambda: leader.metadata.name, switchover_target, "Rolling update flag did not trigger switchover")
837+
838+
# check that the old master has been recreated
839+
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
840+
replica = k8s.get_cluster_replica_pod()
841+
self.assertTrue(replica.metadata.creation_timestamp > old_creation_timestamp, "Old master pod was not recreated")
842+
843+
844+
except timeout_decorator.TimeoutError:
845+
print('Operator log: {}'.format(k8s.get_operator_log()))
846+
raise
847+
848+
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
849+
def test_rolling_update_label_timeout(self):
850+
'''
851+
Simulate case when replica does not receive label in time and rolling update does not finish
852+
'''
853+
k8s = self.k8s
854+
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
855+
flag = "zalando-postgres-operator-rolling-update-required"
856+
857+
# verify we are in good state from potential previous tests
858+
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2, "No 2 pods running")
859+
860+
# get node and replica (expected target of new master)
861+
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
862+
863+
# rolling update annotation
864+
rolling_update_patch = {
865+
"metadata": {
866+
"annotations": {
867+
flag: "true",
868+
}
869+
}
870+
}
871+
872+
# make pod_label_wait_timeout so short that rolling update fails on first try
873+
# temporarily lower resync interval to reduce waiting for further tests
874+
# pods should get healthy in the meantime
875+
patch_resync_config = {
876+
"data": {
877+
"pod_label_wait_timeout": "2s",
878+
"resync_period": "20s",
879+
}
880+
}
881+
882+
try:
883+
# patch both pods for rolling update
884+
podList = k8s.api.core_v1.list_namespaced_pod('default', label_selector=cluster_label)
885+
for pod in podList.items:
886+
k8s.patch_pod(rolling_update_patch, pod.metadata.name, pod.metadata.namespace)
887+
if pod.metadata.labels.get('spilo-role') == 'replica':
888+
switchover_target = pod.metadata.name
889+
890+
# update config and restart operator
891+
k8s.update_config(patch_resync_config, "update resync interval and pod_label_wait_timeout")
892+
893+
# operator should now recreate the replica pod first and do a switchover after
894+
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
895+
896+
# pod_label_wait_timeout should have been exceeded hence the rolling update is continued on next sync
897+
# check if the cluster state is "SyncFailed"
898+
self.eventuallyEqual(lambda: k8s.pg_get_status(), "SyncFailed", "Expected SYNC event to fail")
899+
900+
# wait for next sync, replica should be running normally by now and be ready for switchover
901+
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
902+
903+
# check if the former replica is now the new master
904+
leader = k8s.get_cluster_leader_pod()
905+
self.eventuallyEqual(lambda: leader.metadata.name, switchover_target, "Rolling update flag did not trigger switchover")
906+
907+
# wait for the old master to get restarted
908+
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
909+
910+
# status should again be "SyncFailed" but turn into "Running" on the next sync
911+
time.sleep(10)
912+
self.eventuallyEqual(lambda: k8s.pg_get_status(), "Running", "Expected running cluster after two syncs")
913+
914+
# revert config changes
915+
patch_resync_config = {
916+
"data": {
917+
"pod_label_wait_timeout": "10m",
918+
"resync_period": "30m",
919+
}
920+
}
921+
k8s.update_config(patch_resync_config, "revert resync interval and pod_label_wait_timeout")
922+
923+
924+
except timeout_decorator.TimeoutError:
925+
print('Operator log: {}'.format(k8s.get_operator_log()))
926+
raise
787927

788928
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
789929
def test_zz_node_readiness_label(self):

pkg/cluster/cluster.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,7 @@ func (c *Cluster) Switchover(curMaster *v1.Pod, candidate spec.NamespacedName) e
13091309
err = fmt.Errorf("could not get master pod label: %v", err)
13101310
}
13111311
} else {
1312-
err = fmt.Errorf("could not switch over: %v", err)
1312+
err = fmt.Errorf("could not switch over from %q to %q: %v", curMaster.Name, candidate, err)
13131313
}
13141314

13151315
// signal the role label waiting goroutine to close the shop and go home

pkg/cluster/k8sres.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"path"
88
"sort"
9-
"strconv"
109
"strings"
1110

1211
"github.com/sirupsen/logrus"
@@ -1279,7 +1278,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
12791278
}
12801279

12811280
stsAnnotations := make(map[string]string)
1282-
stsAnnotations[rollingUpdateStatefulsetAnnotationKey] = strconv.FormatBool(false)
12831281
stsAnnotations = c.AnnotationsToPropagate(c.annotationsSet(nil))
12841282

12851283
statefulSet := &appsv1.StatefulSet{

0 commit comments

Comments
 (0)