bring back all e2e tests
This commit is contained in:
parent
1d2b3f5291
commit
e1f5173daf
|
|
@ -158,6 +158,806 @@ class EndToEndTestCase(unittest.TestCase):
|
||||||
print('Operator log: {}'.format(k8s.get_operator_log()))
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_additional_owner_roles(self):
|
||||||
|
'''
|
||||||
|
Test adding additional member roles to existing database owner roles
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
|
||||||
|
# enable PostgresTeam CRD and lower resync
|
||||||
|
owner_roles = {
|
||||||
|
"data": {
|
||||||
|
"additional_owner_roles": "cron_admin",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
k8s.update_config(owner_roles)
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||||
|
"Operator does not get in sync")
|
||||||
|
|
||||||
|
leader = k8s.get_cluster_leader_pod()
|
||||||
|
owner_query = """
|
||||||
|
SELECT a2.rolname
|
||||||
|
FROM pg_catalog.pg_authid a
|
||||||
|
JOIN pg_catalog.pg_auth_members am
|
||||||
|
ON a.oid = am.member
|
||||||
|
AND a.rolname = 'cron_admin'
|
||||||
|
JOIN pg_catalog.pg_authid a2
|
||||||
|
ON a2.oid = am.roleid
|
||||||
|
WHERE a2.rolname IN ('zalando', 'bar_owner', 'bar_data_owner');
|
||||||
|
"""
|
||||||
|
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", owner_query)), 3,
|
||||||
|
"Not all additional users found in database", 10, 5)
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_additional_pod_capabilities(self):
|
||||||
|
'''
|
||||||
|
Extend postgres container capabilities
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
|
||||||
|
capabilities = ["SYS_NICE","CHOWN"]
|
||||||
|
patch_capabilities = {
|
||||||
|
"data": {
|
||||||
|
"additional_pod_capabilities": ','.join(capabilities),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# get node and replica (expected target of new master)
|
||||||
|
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
|
||||||
|
|
||||||
|
try:
|
||||||
|
k8s.update_config(patch_capabilities)
|
||||||
|
|
||||||
|
# changed security context of postgres container should trigger a rolling update
|
||||||
|
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
|
||||||
|
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_pods_with_container_capabilities(capabilities, cluster_label),
|
||||||
|
2, "Container capabilities not updated")
|
||||||
|
|
||||||
|
except timeout_decorator.TimeoutError:
|
||||||
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
|
raise
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_additional_teams_and_members(self):
|
||||||
|
'''
|
||||||
|
Test PostgresTeam CRD with extra teams and members
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
|
||||||
|
# enable PostgresTeam CRD and lower resync
|
||||||
|
enable_postgres_team_crd = {
|
||||||
|
"data": {
|
||||||
|
"enable_postgres_team_crd": "true",
|
||||||
|
"enable_team_member_deprecation": "true",
|
||||||
|
"role_deletion_suffix": "_delete_me",
|
||||||
|
"resync_period": "15s",
|
||||||
|
"repair_period": "15s",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
k8s.update_config(enable_postgres_team_crd)
|
||||||
|
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresteams', 'custom-team-membership',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'additionalTeams': {
|
||||||
|
'acid': [
|
||||||
|
'e2e'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'additionalMembers': {
|
||||||
|
'e2e': [
|
||||||
|
'kind'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
leader = k8s.get_cluster_leader_pod()
|
||||||
|
user_query = """
|
||||||
|
SELECT rolname
|
||||||
|
FROM pg_catalog.pg_roles
|
||||||
|
WHERE rolname IN ('elephant', 'kind');
|
||||||
|
"""
|
||||||
|
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||||
|
"Not all additional users found in database", 10, 5)
|
||||||
|
|
||||||
|
# replace additional member and check if the removed member's role is renamed
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresteams', 'custom-team-membership',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'additionalMembers': {
|
||||||
|
'e2e': [
|
||||||
|
'tester'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user_query = """
|
||||||
|
SELECT rolname
|
||||||
|
FROM pg_catalog.pg_roles
|
||||||
|
WHERE (rolname = 'tester' AND rolcanlogin)
|
||||||
|
OR (rolname = 'kind_delete_me' AND NOT rolcanlogin);
|
||||||
|
"""
|
||||||
|
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||||
|
"Database role of replaced member in PostgresTeam not renamed", 10, 5)
|
||||||
|
|
||||||
|
# re-add additional member and check if the role is renamed back
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresteams', 'custom-team-membership',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'additionalMembers': {
|
||||||
|
'e2e': [
|
||||||
|
'kind'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user_query = """
|
||||||
|
SELECT rolname
|
||||||
|
FROM pg_catalog.pg_roles
|
||||||
|
WHERE (rolname = 'kind' AND rolcanlogin)
|
||||||
|
OR (rolname = 'tester_delete_me' AND NOT rolcanlogin);
|
||||||
|
"""
|
||||||
|
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
|
||||||
|
"Database role of recreated member in PostgresTeam not renamed back to original name", 10, 5)
|
||||||
|
|
||||||
|
# revert config change
|
||||||
|
revert_resync = {
|
||||||
|
"data": {
|
||||||
|
"resync_period": "4m",
|
||||||
|
"repair_period": "1m",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
k8s.update_config(revert_resync)
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||||
|
"Operator does not get in sync")
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_config_update(self):
|
||||||
|
'''
|
||||||
|
Change Postgres config under Spec.Postgresql.Parameters and Spec.Patroni
|
||||||
|
and query Patroni config endpoint to check if manifest changes got applied
|
||||||
|
via restarting cluster through Patroni's rest api
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
leader = k8s.get_cluster_leader_pod()
|
||||||
|
replica = k8s.get_cluster_replica_pod()
|
||||||
|
masterCreationTimestamp = leader.metadata.creation_timestamp
|
||||||
|
replicaCreationTimestamp = replica.metadata.creation_timestamp
|
||||||
|
new_max_connections_value = "50"
|
||||||
|
|
||||||
|
# adjust Postgres config
|
||||||
|
pg_patch_config = {
|
||||||
|
"spec": {
|
||||||
|
"postgresql": {
|
||||||
|
"parameters": {
|
||||||
|
"max_connections": new_max_connections_value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patroni": {
|
||||||
|
"slots": {
|
||||||
|
"test_slot": {
|
||||||
|
"type": "physical"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ttl": 29,
|
||||||
|
"loop_wait": 9,
|
||||||
|
"retry_timeout": 9,
|
||||||
|
"synchronous_mode": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_config)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
def compare_config():
|
||||||
|
effective_config = k8s.patroni_rest(leader.metadata.name, "config")
|
||||||
|
desired_config = pg_patch_config["spec"]["patroni"]
|
||||||
|
desired_parameters = pg_patch_config["spec"]["postgresql"]["parameters"]
|
||||||
|
effective_parameters = effective_config["postgresql"]["parameters"]
|
||||||
|
self.assertEqual(desired_parameters["max_connections"], effective_parameters["max_connections"],
|
||||||
|
"max_connections not updated")
|
||||||
|
self.assertTrue(effective_config["slots"] is not None, "physical replication slot not added")
|
||||||
|
self.assertEqual(desired_config["ttl"], effective_config["ttl"],
|
||||||
|
"ttl not updated")
|
||||||
|
self.assertEqual(desired_config["loop_wait"], effective_config["loop_wait"],
|
||||||
|
"loop_wait not updated")
|
||||||
|
self.assertEqual(desired_config["retry_timeout"], effective_config["retry_timeout"],
|
||||||
|
"retry_timeout not updated")
|
||||||
|
self.assertEqual(desired_config["synchronous_mode"], effective_config["synchronous_mode"],
|
||||||
|
"synchronous_mode not updated")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check if Patroni config has been updated
|
||||||
|
self.eventuallyTrue(compare_config, "Postgres config not applied")
|
||||||
|
|
||||||
|
# make sure that pods were not recreated
|
||||||
|
leader = k8s.get_cluster_leader_pod()
|
||||||
|
replica = k8s.get_cluster_replica_pod()
|
||||||
|
self.assertEqual(masterCreationTimestamp, leader.metadata.creation_timestamp,
|
||||||
|
"Master pod creation timestamp is updated")
|
||||||
|
self.assertEqual(replicaCreationTimestamp, replica.metadata.creation_timestamp,
|
||||||
|
"Master pod creation timestamp is updated")
|
||||||
|
|
||||||
|
# query max_connections setting
|
||||||
|
setting_query = """
|
||||||
|
SELECT setting
|
||||||
|
FROM pg_settings
|
||||||
|
WHERE name = 'max_connections';
|
||||||
|
"""
|
||||||
|
self.eventuallyEqual(lambda: self.query_database(leader.metadata.name, "postgres", setting_query)[0], new_max_connections_value,
|
||||||
|
"New max_connections setting not applied on master", 10, 5)
|
||||||
|
self.eventuallyNotEqual(lambda: self.query_database(replica.metadata.name, "postgres", setting_query)[0], new_max_connections_value,
|
||||||
|
"Expected max_connections not to be updated on replica since Postgres was restarted there first", 10, 5)
|
||||||
|
|
||||||
|
# the next sync should restart the replica because it has pending_restart flag set
|
||||||
|
# force next sync by deleting the operator pod
|
||||||
|
k8s.delete_operator_pod()
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: self.query_database(replica.metadata.name, "postgres", setting_query)[0], new_max_connections_value,
|
||||||
|
"New max_connections setting not applied on replica", 10, 5)
|
||||||
|
|
||||||
|
# decrease max_connections again
|
||||||
|
# this time restart will be correct and new value should appear on both instances
|
||||||
|
lower_max_connections_value = "30"
|
||||||
|
pg_patch_max_connections = {
|
||||||
|
"spec": {
|
||||||
|
"postgresql": {
|
||||||
|
"parameters": {
|
||||||
|
"max_connections": lower_max_connections_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_max_connections)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
# check Patroni config again
|
||||||
|
pg_patch_config["spec"]["postgresql"]["parameters"]["max_connections"] = lower_max_connections_value
|
||||||
|
self.eventuallyTrue(compare_config, "Postgres config not applied")
|
||||||
|
|
||||||
|
# and query max_connections setting again
|
||||||
|
self.eventuallyEqual(lambda: self.query_database(leader.metadata.name, "postgres", setting_query)[0], lower_max_connections_value,
|
||||||
|
"Previous max_connections setting not applied on master", 10, 5)
|
||||||
|
self.eventuallyEqual(lambda: self.query_database(replica.metadata.name, "postgres", setting_query)[0], lower_max_connections_value,
|
||||||
|
"Previous max_connections setting not applied on replica", 10, 5)
|
||||||
|
|
||||||
|
except timeout_decorator.TimeoutError:
|
||||||
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
|
raise
|
||||||
|
|
||||||
|
# make sure cluster is in a good state for further tests
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2,
|
||||||
|
"No 2 pods running")
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_cross_namespace_secrets(self):
|
||||||
|
'''
|
||||||
|
Test secrets in different namespace
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
|
||||||
|
# enable secret creation in separate namespace
|
||||||
|
patch_cross_namespace_secret = {
|
||||||
|
"data": {
|
||||||
|
"enable_cross_namespace_secret": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.update_config(patch_cross_namespace_secret,
|
||||||
|
step="cross namespace secrets enabled")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||||
|
"Operator does not get in sync")
|
||||||
|
|
||||||
|
# create secret in test namespace
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'users':{
|
||||||
|
'test.db_user': [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||||
|
"Operator does not get in sync")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_secrets_with_label("cluster-name=acid-minimal-cluster,application=spilo", self.test_namespace),
|
||||||
|
1, "Secret not created for user in namespace")
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_enable_disable_connection_pooler(self):
|
||||||
|
'''
|
||||||
|
For a database without connection pooler, then turns it on, scale up,
|
||||||
|
turn off and on again. Test with different ways of doing this (via
|
||||||
|
enableConnectionPooler or connectionPooler configuration section). At
|
||||||
|
the end turn connection pooler off to not interfere with other tests.
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
pooler_label = 'application=db-connection-pooler,cluster-name=acid-minimal-cluster'
|
||||||
|
master_pooler_label = 'connection-pooler=acid-minimal-cluster-pooler'
|
||||||
|
replica_pooler_label = master_pooler_label + '-repl'
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'enableConnectionPooler': True,
|
||||||
|
'enableReplicaConnectionPooler': True,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 2, "Deployment replicas is 2 default")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label), 2, "No pooler pods found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label), 2, "No pooler replica pods found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label), 2, "No pooler service found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), 1, "Pooler secret not created")
|
||||||
|
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'enableMasterPoolerLoadBalancer': True,
|
||||||
|
'enableReplicaPoolerLoadBalancer': True,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(master_pooler_label+","+pooler_label),
|
||||||
|
'LoadBalancer',
|
||||||
|
"Expected LoadBalancer service type for master pooler pod, found {}")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(replica_pooler_label+","+pooler_label),
|
||||||
|
'LoadBalancer',
|
||||||
|
"Expected LoadBalancer service type for replica pooler pod, found {}")
|
||||||
|
|
||||||
|
# Turn off only master connection pooler
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'enableConnectionPooler': False,
|
||||||
|
'enableReplicaConnectionPooler': True,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(name="acid-minimal-cluster-pooler-repl"), 2,
|
||||||
|
"Deployment replicas is 2 default")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
|
||||||
|
0, "Master pooler pods not deleted")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label),
|
||||||
|
2, "Pooler replica pods not found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label),
|
||||||
|
1, "No pooler service found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label),
|
||||||
|
1, "Secret not created")
|
||||||
|
|
||||||
|
# Turn off only replica connection pooler
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'enableConnectionPooler': True,
|
||||||
|
'enableReplicaConnectionPooler': False,
|
||||||
|
'enableMasterPoolerLoadBalancer': False,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 2,
|
||||||
|
"Deployment replicas is 2 default")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
|
||||||
|
2, "Master pooler pods not found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(replica_pooler_label),
|
||||||
|
0, "Pooler replica pods not deleted")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label),
|
||||||
|
1, "No pooler service found")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(master_pooler_label+","+pooler_label),
|
||||||
|
'ClusterIP',
|
||||||
|
"Expected LoadBalancer service type for master, found {}")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label),
|
||||||
|
1, "Secret not created")
|
||||||
|
|
||||||
|
# scale up connection pooler deployment
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'connectionPooler': {
|
||||||
|
'numberOfInstances': 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_deployment_replica_count(), 3,
|
||||||
|
"Deployment replicas is scaled to 3")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
|
||||||
|
3, "Scale up of pooler pods does not work")
|
||||||
|
|
||||||
|
# turn it off, keeping config should be overwritten by false
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'enableConnectionPooler': False,
|
||||||
|
'enableReplicaConnectionPooler': False,
|
||||||
|
'enableReplicaPoolerLoadBalancer': False,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(master_pooler_label),
|
||||||
|
0, "Pooler pods not scaled down")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label),
|
||||||
|
0, "Pooler service not removed")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_secrets_with_label('application=spilo,cluster-name=acid-minimal-cluster'),
|
||||||
|
4, "Secrets not deleted")
|
||||||
|
|
||||||
|
# Verify that all the databases have pooler schema installed.
|
||||||
|
# Do this via psql, since otherwise we need to deal with
|
||||||
|
# credentials.
|
||||||
|
db_list = []
|
||||||
|
|
||||||
|
leader = k8s.get_cluster_leader_pod()
|
||||||
|
schemas_query = """
|
||||||
|
SELECT schema_name
|
||||||
|
FROM information_schema.schemata
|
||||||
|
WHERE schema_name = 'pooler'
|
||||||
|
"""
|
||||||
|
|
||||||
|
db_list = self.list_databases(leader.metadata.name)
|
||||||
|
for db in db_list:
|
||||||
|
self.eventuallyNotEqual(lambda: len(self.query_database(leader.metadata.name, db, schemas_query)), 0,
|
||||||
|
"Pooler schema not found in database {}".format(db))
|
||||||
|
|
||||||
|
# remove config section to make test work next time
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
'acid.zalan.do', 'v1', 'default',
|
||||||
|
'postgresqls', 'acid-minimal-cluster',
|
||||||
|
{
|
||||||
|
'spec': {
|
||||||
|
'connectionPooler': None,
|
||||||
|
'EnableReplicaConnectionPooler': False,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_enable_load_balancer(self):
|
||||||
|
'''
|
||||||
|
Test if services are updated when enabling/disabling load balancers in Postgres manifest
|
||||||
|
'''
|
||||||
|
|
||||||
|
k8s = self.k8s
|
||||||
|
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster,spilo-role={}'
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(cluster_label.format("master")),
|
||||||
|
'ClusterIP',
|
||||||
|
"Expected ClusterIP type initially, found {}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# enable load balancer services
|
||||||
|
pg_patch_enable_lbs = {
|
||||||
|
"spec": {
|
||||||
|
"enableMasterLoadBalancer": True,
|
||||||
|
"enableReplicaLoadBalancer": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_enable_lbs)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(cluster_label.format("master")),
|
||||||
|
'LoadBalancer',
|
||||||
|
"Expected LoadBalancer service type for master, found {}")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(cluster_label.format("replica")),
|
||||||
|
'LoadBalancer',
|
||||||
|
"Expected LoadBalancer service type for master, found {}")
|
||||||
|
|
||||||
|
# disable load balancer services again
|
||||||
|
pg_patch_disable_lbs = {
|
||||||
|
"spec": {
|
||||||
|
"enableMasterLoadBalancer": False,
|
||||||
|
"enableReplicaLoadBalancer": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_disable_lbs)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(cluster_label.format("master")),
|
||||||
|
'ClusterIP',
|
||||||
|
"Expected LoadBalancer service type for master, found {}")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_service_type(cluster_label.format("replica")),
|
||||||
|
'ClusterIP',
|
||||||
|
"Expected LoadBalancer service type for master, found {}")
|
||||||
|
|
||||||
|
except timeout_decorator.TimeoutError:
|
||||||
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
|
raise
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_infrastructure_roles(self):
|
||||||
|
'''
|
||||||
|
Test using external secrets for infrastructure roles
|
||||||
|
'''
|
||||||
|
k8s = self.k8s
|
||||||
|
# update infrastructure roles description
|
||||||
|
secret_name = "postgresql-infrastructure-roles"
|
||||||
|
roles = "secretname: postgresql-infrastructure-roles-new, userkey: user,"\
|
||||||
|
"rolekey: memberof, passwordkey: password, defaultrolevalue: robot_zmon"
|
||||||
|
patch_infrastructure_roles = {
|
||||||
|
"data": {
|
||||||
|
"infrastructure_roles_secret_name": secret_name,
|
||||||
|
"infrastructure_roles_secrets": roles,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
k8s.update_config(patch_infrastructure_roles)
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
|
||||||
|
"Operator does not get in sync")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# check that new roles are represented in the config by requesting the
|
||||||
|
# operator configuration via API
|
||||||
|
|
||||||
|
def verify_role():
|
||||||
|
try:
|
||||||
|
operator_pod = k8s.get_operator_pod()
|
||||||
|
get_config_cmd = "wget --quiet -O - localhost:8080/config"
|
||||||
|
result = k8s.exec_with_kubectl(operator_pod.metadata.name,
|
||||||
|
get_config_cmd)
|
||||||
|
try:
|
||||||
|
roles_dict = (json.loads(result.stdout)
|
||||||
|
.get("controller", {})
|
||||||
|
.get("InfrastructureRoles"))
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "robot_zmon_acid_monitoring_new" in roles_dict:
|
||||||
|
role = roles_dict["robot_zmon_acid_monitoring_new"]
|
||||||
|
role.pop("Password", None)
|
||||||
|
self.assertDictEqual(role, {
|
||||||
|
"Name": "robot_zmon_acid_monitoring_new",
|
||||||
|
"Namespace":"",
|
||||||
|
"Flags": None,
|
||||||
|
"MemberOf": ["robot_zmon"],
|
||||||
|
"Parameters": None,
|
||||||
|
"AdminRole": "",
|
||||||
|
"Origin": 2,
|
||||||
|
"IsDbOwner": False,
|
||||||
|
"Deleted": False
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.eventuallyTrue(verify_role, "infrastructure role setup is not loaded")
|
||||||
|
|
||||||
|
except timeout_decorator.TimeoutError:
|
||||||
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
|
raise
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_lazy_spilo_upgrade(self):
|
||||||
|
'''
|
||||||
|
Test lazy upgrade for the Spilo image: operator changes a stateful set
|
||||||
|
but lets pods run with the old image until they are recreated for
|
||||||
|
reasons other than operator's activity. That works because the operator
|
||||||
|
configures stateful sets to use "onDelete" pod update policy.
|
||||||
|
The test covers:
|
||||||
|
1) enabling lazy upgrade in existing operator deployment
|
||||||
|
2) forcing the normal rolling upgrade by changing the operator
|
||||||
|
configmap and restarting its pod
|
||||||
|
'''
|
||||||
|
|
||||||
|
k8s = self.k8s
|
||||||
|
|
||||||
|
pod0 = 'acid-minimal-cluster-0'
|
||||||
|
pod1 = 'acid-minimal-cluster-1'
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2,
|
||||||
|
"No 2 pods running")
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod0)),
|
||||||
|
2, "Postgres status did not enter running")
|
||||||
|
|
||||||
|
patch_lazy_spilo_upgrade = {
|
||||||
|
"data": {
|
||||||
|
"docker_image": SPILO_CURRENT,
|
||||||
|
"enable_lazy_spilo_upgrade": "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.update_config(patch_lazy_spilo_upgrade,
|
||||||
|
step="Init baseline image version")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_statefulset_image(), SPILO_CURRENT,
|
||||||
|
"Statefulset not updated initially")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2,
|
||||||
|
"No 2 pods running")
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod0)),
|
||||||
|
2, "Postgres status did not enter running")
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_effective_pod_image(pod0),
|
||||||
|
SPILO_CURRENT, "Rolling upgrade was not executed")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_effective_pod_image(pod1),
|
||||||
|
SPILO_CURRENT, "Rolling upgrade was not executed")
|
||||||
|
|
||||||
|
# update docker image in config and enable the lazy upgrade
|
||||||
|
conf_image = SPILO_LAZY
|
||||||
|
patch_lazy_spilo_upgrade = {
|
||||||
|
"data": {
|
||||||
|
"docker_image": conf_image,
|
||||||
|
"enable_lazy_spilo_upgrade": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.update_config(patch_lazy_spilo_upgrade,
|
||||||
|
step="patch image and lazy upgrade")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_statefulset_image(), conf_image,
|
||||||
|
"Statefulset not updated to next Docker image")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# restart the pod to get a container with the new image
|
||||||
|
k8s.api.core_v1.delete_namespaced_pod(pod0, 'default')
|
||||||
|
|
||||||
|
# verify only pod-0 which was deleted got new image from statefulset
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_effective_pod_image(pod0),
|
||||||
|
conf_image, "Delete pod-0 did not get new spilo image")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(), 2,
|
||||||
|
"No two pods running after lazy rolling upgrade")
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod0)),
|
||||||
|
2, "Postgres status did not enter running")
|
||||||
|
self.assertNotEqual(lambda: k8s.get_effective_pod_image(pod1),
|
||||||
|
SPILO_CURRENT,
|
||||||
|
"pod-1 should not have change Docker image to {}".format(SPILO_CURRENT))
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
unpatch_lazy_spilo_upgrade = {
|
||||||
|
"data": {
|
||||||
|
"enable_lazy_spilo_upgrade": "false",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.update_config(unpatch_lazy_spilo_upgrade, step="patch lazy upgrade")
|
||||||
|
|
||||||
|
# at this point operator will complete the normal rolling upgrade
|
||||||
|
# so we additionally test if disabling the lazy upgrade - forcing the normal rolling upgrade - works
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_effective_pod_image(pod0),
|
||||||
|
conf_image, "Rolling upgrade was not executed",
|
||||||
|
50, 3)
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_effective_pod_image(pod1),
|
||||||
|
conf_image, "Rolling upgrade was not executed",
|
||||||
|
50, 3)
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members(pod0)),
|
||||||
|
2, "Postgres status did not enter running")
|
||||||
|
|
||||||
|
except timeout_decorator.TimeoutError:
|
||||||
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
|
raise
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
def test_logical_backup_cron_job(self):
|
||||||
|
'''
|
||||||
|
Ensure we can (a) create the cron job at user request for a specific PG cluster
|
||||||
|
(b) update the cluster-wide image for the logical backup pod
|
||||||
|
(c) delete the job at user request
|
||||||
|
Limitations:
|
||||||
|
(a) Does not run the actual batch job because there is no S3 mock to upload backups to
|
||||||
|
(b) Assumes 'acid-minimal-cluster' exists as defined in setUp
|
||||||
|
'''
|
||||||
|
|
||||||
|
k8s = self.k8s
|
||||||
|
|
||||||
|
# create the cron job
|
||||||
|
schedule = "7 7 7 7 *"
|
||||||
|
pg_patch_enable_backup = {
|
||||||
|
"spec": {
|
||||||
|
"enableLogicalBackup": True,
|
||||||
|
"logicalBackupSchedule": schedule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_enable_backup)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_logical_backup_job().items), 1, "failed to create logical backup job")
|
||||||
|
|
||||||
|
job = k8s.get_logical_backup_job().items[0]
|
||||||
|
self.assertEqual(job.metadata.name, "logical-backup-acid-minimal-cluster",
|
||||||
|
"Expected job name {}, found {}"
|
||||||
|
.format("logical-backup-acid-minimal-cluster", job.metadata.name))
|
||||||
|
self.assertEqual(job.spec.schedule, schedule,
|
||||||
|
"Expected {} schedule, found {}"
|
||||||
|
.format(schedule, job.spec.schedule))
|
||||||
|
|
||||||
|
# update the cluster-wide image of the logical backup pod
|
||||||
|
image = "test-image-name"
|
||||||
|
patch_logical_backup_image = {
|
||||||
|
"data": {
|
||||||
|
"logical_backup_docker_image": image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.update_config(patch_logical_backup_image, step="patch logical backup image")
|
||||||
|
|
||||||
|
def get_docker_image():
|
||||||
|
jobs = k8s.get_logical_backup_job().items
|
||||||
|
return jobs[0].spec.job_template.spec.template.spec.containers[0].image
|
||||||
|
|
||||||
|
self.eventuallyEqual(get_docker_image, image,
|
||||||
|
"Expected job image {}, found {}".format(image, "{}"))
|
||||||
|
|
||||||
|
# delete the logical backup cron job
|
||||||
|
pg_patch_disable_backup = {
|
||||||
|
"spec": {
|
||||||
|
"enableLogicalBackup": False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_disable_backup)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_logical_backup_job().items), 0, "failed to create logical backup job")
|
||||||
|
|
||||||
|
except timeout_decorator.TimeoutError:
|
||||||
|
print('Operator log: {}'.format(k8s.get_operator_log()))
|
||||||
|
raise
|
||||||
|
|
||||||
|
# ensure cluster is healthy after tests
|
||||||
|
self.eventuallyEqual(lambda: len(k8s.get_patroni_running_members("acid-minimal-cluster-0")), 2, "Postgres status did not enter running")
|
||||||
|
|
||||||
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
|
@unittest.skip("Skipping this test until fixed")
|
||||||
|
def test_major_version_upgrade(self):
|
||||||
|
k8s = self.k8s
|
||||||
|
result = k8s.create_with_kubectl("manifests/minimal-postgres-manifest-12.yaml")
|
||||||
|
self.eventuallyEqual(lambda: k8s.count_running_pods(labels="application=spilo,cluster-name=acid-upgrade-test"), 2, "No 2 pods running")
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
pg_patch_version = {
|
||||||
|
"spec": {
|
||||||
|
"postgres": {
|
||||||
|
"version": "14"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-upgrade-test", pg_patch_version)
|
||||||
|
|
||||||
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
|
def check_version_14():
|
||||||
|
p = k8s.get_patroni_state("acid-upgrade-test-0")
|
||||||
|
version = p["server_version"][0:2]
|
||||||
|
return version
|
||||||
|
|
||||||
|
self.evantuallyEqual(check_version_14, "14", "Version was not upgrade to 14")
|
||||||
|
|
||||||
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
def test_min_resource_limits(self):
|
def test_min_resource_limits(self):
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue