add e2e test
This commit is contained in:
parent
33ec2202e6
commit
0183c7971f
|
|
@ -8,7 +8,6 @@ import yaml
|
|||
|
||||
from kubernetes import client, config
|
||||
|
||||
|
||||
class EndToEndTestCase(unittest.TestCase):
|
||||
'''
|
||||
Test interaction of the operator with multiple K8s components.
|
||||
|
|
@ -380,6 +379,74 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
# toggle pod anti affinity to move replica away from master node
|
||||
self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label)
|
||||
|
||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||
def test_pvc_deletion(self):
|
||||
'''
|
||||
Operator must remove unused volumes safely:
|
||||
1) Volumes attached to running pods are never removed
|
||||
2) The last volume is kept even if a cluster has 0 pods
|
||||
|
||||
Operator employs 'Delete' reclaim policy for volumes,
|
||||
so it is sufficient to delete persistent volume claims (pvc) to remove a volume.
|
||||
'''
|
||||
|
||||
k8s = self.k8s
|
||||
self.assert_running_pods_have_volumes()
|
||||
|
||||
# get extra unused pvcs to test Sync
|
||||
k8s.wait_for_pg_to_scale(4)
|
||||
k8s.wait_for_pg_to_scale(2)
|
||||
|
||||
# enable pvc deletion
|
||||
patch = {
|
||||
"data": {
|
||||
"should_delete_unused_pvc": "true"
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch)
|
||||
|
||||
# Sync() at operator start-up deletes unused pvcs that had existed before
|
||||
unused_pvcs = ["pgdata-acid-minimal-cluster-2", "pgdata-acid-minimal-cluster-3"]
|
||||
for pvc in unused_pvcs:
|
||||
k8s.wait_for_pvc_deletion(pvc)
|
||||
|
||||
self.assert_running_pods_have_volumes()
|
||||
|
||||
# Update() deletes pvc on scale down
|
||||
# we do not use wait_for_pg_to_scale here because it waits until a pod is completely gone
|
||||
# we want to capture a potential situation where a pod is in Terminating state
|
||||
# but its pvc is already being deleted
|
||||
# TODO that needs a more thourough test at the DB level
|
||||
k8s.change_number_of_instances(1)
|
||||
k8s.wait_for_pvc_deletion("pgdata-acid-minimal-cluster-1")
|
||||
|
||||
self.assert_running_pods_have_volumes()
|
||||
|
||||
# pvc with index 0 must stay around when cluster has 0 pods
|
||||
last_pvc_name = "pgdata-acid-minimal-cluster-0"
|
||||
volume_before_scaledown = k8s.get_volume_name(last_pvc_name)
|
||||
k8s.wait_for_pg_to_scale(0)
|
||||
self.assertTrue(k8s.pvc_exist(last_pvc_name), "The last pvc was deleted")
|
||||
|
||||
# sanity check
|
||||
k8s.wait_for_pg_to_scale(3)
|
||||
volume_after_scaleup = k8s.get_volume_name(last_pvc_name)
|
||||
self.assertEqual(volume_before_scaledown, volume_after_scaleup, "the surviving pvc must have the same volume before scale down to 0 and after scale up")
|
||||
|
||||
# clean up
|
||||
patch = {
|
||||
"data": {
|
||||
"should_delete_unused_pvc": "false"
|
||||
}
|
||||
}
|
||||
k8s.update_config(patch)
|
||||
|
||||
# disablement of the feature actually stops volume deletion
|
||||
k8s.wait_for_pg_to_scale(2)
|
||||
self.assert_running_pods_have_volumes()
|
||||
self.assertTrue(k8s.pvc_exist("pgdata-acid-minimal-cluster-2"), "The pvc of a shut down pod was deleted despite the feature is disabled")
|
||||
|
||||
|
||||
def get_failover_targets(self, master_node, replica_nodes):
|
||||
'''
|
||||
If all pods live on the same node, failover will happen to other worker(s)
|
||||
|
|
@ -451,6 +518,20 @@ class EndToEndTestCase(unittest.TestCase):
|
|||
k8s.wait_for_pod_start('spilo-role=master')
|
||||
k8s.wait_for_pod_start('spilo-role=replica')
|
||||
|
||||
def assert_running_pods_have_volumes(self):
|
||||
'''
|
||||
Operator must never delete a pvc and hence volume of a running pod
|
||||
'''
|
||||
|
||||
k8s = self.k8s
|
||||
labels = 'cluster-name=' + 'acid-minimal-cluster'
|
||||
|
||||
pods = k8s.list_pods(labels)
|
||||
for pod in pods:
|
||||
pgdata = [v for v in pod.spec.volumes if v.name == 'pgdata'][0]
|
||||
pvc = pgdata.persistent_volume_claim
|
||||
self.assertTrue(k8s.pvc_exist(pvc.claim_name))
|
||||
|
||||
|
||||
class K8sApi:
|
||||
|
||||
|
|
@ -521,7 +602,7 @@ class K8s:
|
|||
return False
|
||||
return True
|
||||
|
||||
def wait_for_pg_to_scale(self, number_of_instances, namespace='default'):
|
||||
def change_number_of_instances(self, number_of_instances, namespace='default'):
|
||||
|
||||
body = {
|
||||
"spec": {
|
||||
|
|
@ -531,12 +612,19 @@ class K8s:
|
|||
_ = self.api.custom_objects_api.patch_namespaced_custom_object(
|
||||
"acid.zalan.do", "v1", namespace, "postgresqls", "acid-minimal-cluster", body)
|
||||
|
||||
|
||||
def wait_for_pg_to_scale(self, number_of_instances, namespace='default'):
|
||||
|
||||
self.change_number_of_instances(number_of_instances, namespace='default')
|
||||
labels = 'cluster-name=acid-minimal-cluster'
|
||||
while self.count_pods_with_label(labels) != number_of_instances:
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
def list_pods(self, labels, namespace='default'):
|
||||
return self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
|
||||
|
||||
def count_pods_with_label(self, labels, namespace='default'):
|
||||
return len(self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items)
|
||||
return len(self.list_pods(labels, namespace))
|
||||
|
||||
def wait_for_pod_failover(self, failover_targets, labels, namespace='default'):
|
||||
pod_phase = 'Failing over'
|
||||
|
|
@ -567,12 +655,30 @@ class K8s:
|
|||
|
||||
operator_pod = self.api.core_v1.list_namespaced_pod(
|
||||
'default', label_selector="name=postgres-operator").items[0].metadata.name
|
||||
self.api.core_v1.delete_namespaced_pod(operator_pod, "default") # restart reloads the conf
|
||||
self.api.core_v1.delete_namespaced_pod(operator_pod, "default") # restart reloads the conf and issues Sync()
|
||||
self.wait_for_operator_pod_start()
|
||||
|
||||
def create_with_kubectl(self, path):
|
||||
subprocess.run(["kubectl", "create", "-f", path])
|
||||
|
||||
def wait_for_pvc_deletion(self, pvc_name):
|
||||
|
||||
while self.pvc_exist(pvc_name):
|
||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
||||
|
||||
def pvc_exist(self, pvc_name):
|
||||
exists = True
|
||||
|
||||
try:
|
||||
pvc = self.api.core_v1.read_namespaced_persistent_volume_claim(pvc_name, "default")
|
||||
except: # TODO catch not found exception
|
||||
exists = False
|
||||
|
||||
return exists
|
||||
|
||||
def get_volume_name(self, pvc_name):
|
||||
pvc = self.api.core_v1.read_namespaced_persistent_volume_claim(pvc_name, "default")
|
||||
return pvc.spec.volume_name
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Reference in New Issue