add e2e test
This commit is contained in:
parent
33ec2202e6
commit
0183c7971f
|
|
@ -8,7 +8,6 @@ import yaml
|
||||||
|
|
||||||
from kubernetes import client, config
|
from kubernetes import client, config
|
||||||
|
|
||||||
|
|
||||||
class EndToEndTestCase(unittest.TestCase):
|
class EndToEndTestCase(unittest.TestCase):
|
||||||
'''
|
'''
|
||||||
Test interaction of the operator with multiple K8s components.
|
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
|
# toggle pod anti affinity to move replica away from master node
|
||||||
self.assert_distributed_pods(new_master_node, new_replica_nodes, cluster_label)
|
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):
|
def get_failover_targets(self, master_node, replica_nodes):
|
||||||
'''
|
'''
|
||||||
If all pods live on the same node, failover will happen to other worker(s)
|
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=master')
|
||||||
k8s.wait_for_pod_start('spilo-role=replica')
|
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:
|
class K8sApi:
|
||||||
|
|
||||||
|
|
@ -521,7 +602,7 @@ class K8s:
|
||||||
return False
|
return False
|
||||||
return True
|
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 = {
|
body = {
|
||||||
"spec": {
|
"spec": {
|
||||||
|
|
@ -531,12 +612,19 @@ class K8s:
|
||||||
_ = self.api.custom_objects_api.patch_namespaced_custom_object(
|
_ = self.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
"acid.zalan.do", "v1", namespace, "postgresqls", "acid-minimal-cluster", body)
|
"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'
|
labels = 'cluster-name=acid-minimal-cluster'
|
||||||
while self.count_pods_with_label(labels) != number_of_instances:
|
while self.count_pods_with_label(labels) != number_of_instances:
|
||||||
time.sleep(self.RETRY_TIMEOUT_SEC)
|
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'):
|
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'):
|
def wait_for_pod_failover(self, failover_targets, labels, namespace='default'):
|
||||||
pod_phase = 'Failing over'
|
pod_phase = 'Failing over'
|
||||||
|
|
@ -567,12 +655,30 @@ class K8s:
|
||||||
|
|
||||||
operator_pod = self.api.core_v1.list_namespaced_pod(
|
operator_pod = self.api.core_v1.list_namespaced_pod(
|
||||||
'default', label_selector="name=postgres-operator").items[0].metadata.name
|
'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()
|
self.wait_for_operator_pod_start()
|
||||||
|
|
||||||
def create_with_kubectl(self, path):
|
def create_with_kubectl(self, path):
|
||||||
subprocess.run(["kubectl", "create", "-f", 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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue