Allow drop slots when it gets deleted from the manifest (#2089)
* Allow drop slots when it gets deleted from the manifest * use leader instead replica to query slots * fix and extend unit tests for config update checks Co-authored-by: Felix Kunde <felix-kunde@gmx.de>
This commit is contained in:
		
							parent
							
								
									819e410959
								
							
						
					
					
						commit
						486d5d66e0
					
				|  | @ -395,12 +395,13 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|             "spec": { |             "spec": { | ||||||
|                 "postgresql": { |                 "postgresql": { | ||||||
|                     "parameters": { |                     "parameters": { | ||||||
|                         "max_connections": new_max_connections_value |                         "max_connections": new_max_connections_value, | ||||||
|  |                         "wal_level": "logical" | ||||||
|                      } |                      } | ||||||
|                  }, |                  }, | ||||||
|                  "patroni": { |                  "patroni": { | ||||||
|                     "slots": { |                     "slots": { | ||||||
|                         "test_slot": { |                         "first_slot": { | ||||||
|                             "type": "physical" |                             "type": "physical" | ||||||
|                         } |                         } | ||||||
|                     }, |                     }, | ||||||
|  | @ -437,6 +438,8 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|                             "synchronous_mode not updated") |                             "synchronous_mode not updated") | ||||||
|                 self.assertEqual(desired_config["failsafe_mode"], effective_config["failsafe_mode"], |                 self.assertEqual(desired_config["failsafe_mode"], effective_config["failsafe_mode"], | ||||||
|                             "failsafe_mode not updated") |                             "failsafe_mode not updated") | ||||||
|  |                 self.assertEqual(desired_config["slots"], effective_config["slots"], | ||||||
|  |                             "slots not updated") | ||||||
|                 return True |                 return True | ||||||
| 
 | 
 | ||||||
|             # check if Patroni config has been updated |             # check if Patroni config has been updated | ||||||
|  | @ -497,6 +500,84 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|             self.eventuallyEqual(lambda: self.query_database(replica.metadata.name, "postgres", setting_query)[0], lower_max_connections_value, |             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) |                 "Previous max_connections setting not applied on replica", 10, 5) | ||||||
| 
 | 
 | ||||||
|  |             # patch new slot via Patroni REST | ||||||
|  |             patroni_slot = "test_patroni_slot" | ||||||
|  |             patch_slot_command = """curl -s -XPATCH -d '{"slots": {"test_patroni_slot": {"type": "physical"}}}' localhost:8008/config""" | ||||||
|  |             pg_patch_config["spec"]["patroni"]["slots"][patroni_slot] = {"type": "physical"} | ||||||
|  | 
 | ||||||
|  |             k8s.exec_with_kubectl(leader.metadata.name, patch_slot_command) | ||||||
|  |             self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") | ||||||
|  |             self.eventuallyTrue(compare_config, "Postgres config not applied") | ||||||
|  | 
 | ||||||
|  |             # test adding new slots | ||||||
|  |             pg_add_new_slots_patch = { | ||||||
|  |                 "spec": { | ||||||
|  |                     "patroni": { | ||||||
|  |                          "slots": { | ||||||
|  |                             "test_slot": { | ||||||
|  |                                 "type": "logical", | ||||||
|  |                                 "database": "foo", | ||||||
|  |                                 "plugin": "pgoutput" | ||||||
|  |                             }, | ||||||
|  |                             "test_slot_2": { | ||||||
|  |                                 "type": "physical" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for slot_name, slot_details in pg_add_new_slots_patch["spec"]["patroni"]["slots"].items(): | ||||||
|  |                 pg_patch_config["spec"]["patroni"]["slots"][slot_name] = slot_details | ||||||
|  | 
 | ||||||
|  |             k8s.api.custom_objects_api.patch_namespaced_custom_object( | ||||||
|  |                 "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_add_new_slots_patch) | ||||||
|  | 
 | ||||||
|  |             self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") | ||||||
|  |             self.eventuallyTrue(compare_config, "Postgres config not applied") | ||||||
|  | 
 | ||||||
|  |             # delete test_slot_2 from config and change the database type for test_slot | ||||||
|  |             slot_to_change = "test_slot" | ||||||
|  |             slot_to_remove = "test_slot_2" | ||||||
|  |             pg_delete_slot_patch = { | ||||||
|  |                 "spec": { | ||||||
|  |                     "patroni": { | ||||||
|  |                         "slots": { | ||||||
|  |                             "test_slot": { | ||||||
|  |                                 "type": "logical", | ||||||
|  |                                 "database": "bar", | ||||||
|  |                                 "plugin": "pgoutput" | ||||||
|  |                             }, | ||||||
|  |                             "test_slot_2": None | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             pg_patch_config["spec"]["patroni"]["slots"][slot_to_change]["database"] = "bar" | ||||||
|  |             del pg_patch_config["spec"]["patroni"]["slots"][slot_to_remove] | ||||||
|  |              | ||||||
|  |             k8s.api.custom_objects_api.patch_namespaced_custom_object( | ||||||
|  |                 "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_delete_slot_patch) | ||||||
|  | 
 | ||||||
|  |             self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") | ||||||
|  |             self.eventuallyTrue(compare_config, "Postgres config not applied") | ||||||
|  | 
 | ||||||
|  |             get_slot_query = """ | ||||||
|  |                 SELECT %s | ||||||
|  |                   FROM pg_replication_slots | ||||||
|  |                  WHERE slot_name = '%s'; | ||||||
|  |                 """ | ||||||
|  |             self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", get_slot_query%("slot_name", slot_to_remove))), 0, | ||||||
|  |                 "The replication slot cannot be deleted", 10, 5) | ||||||
|  | 
 | ||||||
|  |             self.eventuallyEqual(lambda: self.query_database(leader.metadata.name, "postgres", get_slot_query%("database", slot_to_change))[0], "bar", | ||||||
|  |                 "The replication slot cannot be updated", 10, 5) | ||||||
|  |              | ||||||
|  |             # make sure slot from Patroni didn't get deleted | ||||||
|  |             self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", get_slot_query%("slot_name", patroni_slot))), 1, | ||||||
|  |                 "The replication slot from Patroni gets deleted", 10, 5) | ||||||
|  | 
 | ||||||
|         except timeout_decorator.TimeoutError: |         except timeout_decorator.TimeoutError: | ||||||
|             print('Operator log: {}'.format(k8s.get_operator_log())) |             print('Operator log: {}'.format(k8s.get_operator_log())) | ||||||
|             raise |             raise | ||||||
|  |  | ||||||
|  | @ -84,6 +84,7 @@ type Cluster struct { | ||||||
| 	userSyncStrategy spec.UserSyncer | 	userSyncStrategy spec.UserSyncer | ||||||
| 	deleteOptions    metav1.DeleteOptions | 	deleteOptions    metav1.DeleteOptions | ||||||
| 	podEventsQueue   *cache.FIFO | 	podEventsQueue   *cache.FIFO | ||||||
|  | 	replicationSlots map[string]interface{} | ||||||
| 
 | 
 | ||||||
| 	teamsAPIClient      teams.Interface | 	teamsAPIClient      teams.Interface | ||||||
| 	oauthTokenGetter    OAuthTokenGetter | 	oauthTokenGetter    OAuthTokenGetter | ||||||
|  | @ -140,6 +141,7 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres | ||||||
| 		podEventsQueue:      podEventsQueue, | 		podEventsQueue:      podEventsQueue, | ||||||
| 		KubeClient:          kubeClient, | 		KubeClient:          kubeClient, | ||||||
| 		currentMajorVersion: 0, | 		currentMajorVersion: 0, | ||||||
|  | 		replicationSlots:    make(map[string]interface{}), | ||||||
| 	} | 	} | ||||||
| 	cluster.logger = logger.WithField("pkg", "cluster").WithField("cluster-name", cluster.clusterName()) | 	cluster.logger = logger.WithField("pkg", "cluster").WithField("cluster-name", cluster.clusterName()) | ||||||
| 	cluster.teamsAPIClient = teams.NewTeamsAPI(cfg.OpConfig.TeamsAPIUrl, logger) | 	cluster.teamsAPIClient = teams.NewTeamsAPI(cfg.OpConfig.TeamsAPIUrl, logger) | ||||||
|  | @ -374,6 +376,10 @@ func (c *Cluster) Create() error { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	for slotName, desiredSlot := range c.Spec.Patroni.Slots { | ||||||
|  | 		c.replicationSlots[slotName] = desiredSlot | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -579,8 +579,18 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	slotsToSet := make(map[string]interface{}) | ||||||
|  | 	// check if there is any slot deletion
 | ||||||
|  | 	for slotName, effectiveSlot := range c.replicationSlots { | ||||||
|  | 		if desiredSlot, exists := desiredPatroniConfig.Slots[slotName]; exists { | ||||||
|  | 			if reflect.DeepEqual(effectiveSlot, desiredSlot) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		slotsToSet[slotName] = nil | ||||||
|  | 		delete(c.replicationSlots, slotName) | ||||||
|  | 	} | ||||||
| 	// check if specified slots exist in config and if they differ
 | 	// check if specified slots exist in config and if they differ
 | ||||||
| 	slotsToSet := make(map[string]map[string]string) |  | ||||||
| 	for slotName, desiredSlot := range desiredPatroniConfig.Slots { | 	for slotName, desiredSlot := range desiredPatroniConfig.Slots { | ||||||
| 		if effectiveSlot, exists := effectivePatroniConfig.Slots[slotName]; exists { | 		if effectiveSlot, exists := effectivePatroniConfig.Slots[slotName]; exists { | ||||||
| 			if reflect.DeepEqual(desiredSlot, effectiveSlot) { | 			if reflect.DeepEqual(desiredSlot, effectiveSlot) { | ||||||
|  | @ -588,6 +598,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		slotsToSet[slotName] = desiredSlot | 		slotsToSet[slotName] = desiredSlot | ||||||
|  | 		c.replicationSlots[slotName] = desiredSlot | ||||||
| 	} | 	} | ||||||
| 	if len(slotsToSet) > 0 { | 	if len(slotsToSet) > 0 { | ||||||
| 		configToSet["slots"] = slotsToSet | 		configToSet["slots"] = slotsToSet | ||||||
|  | @ -614,7 +625,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check if there exist only config updates that require a restart of the primary
 | 	// check if there exist only config updates that require a restart of the primary
 | ||||||
| 	if !util.SliceContains(restartPrimary, false) && len(configToSet) == 0 { | 	if len(restartPrimary) > 0 && !util.SliceContains(restartPrimary, false) && len(configToSet) == 0 { | ||||||
| 		requiresMasterRestart = true | 		requiresMasterRestart = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -144,6 +144,13 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 	client, _ := newFakeK8sSyncClient() | 	client, _ := newFakeK8sSyncClient() | ||||||
| 	clusterName := "acid-test-cluster" | 	clusterName := "acid-test-cluster" | ||||||
| 	namespace := "default" | 	namespace := "default" | ||||||
|  | 	testSlots := map[string]map[string]string{ | ||||||
|  | 		"slot1": { | ||||||
|  | 			"type":     "logical", | ||||||
|  | 			"plugin":   "wal2json", | ||||||
|  | 			"database": "foo", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	ctrl := gomock.NewController(t) | 	ctrl := gomock.NewController(t) | ||||||
| 	defer ctrl.Finish() | 	defer ctrl.Finish() | ||||||
|  | @ -210,9 +217,24 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		subtest         string | 		subtest         string | ||||||
| 		patroni         acidv1.Patroni | 		patroni         acidv1.Patroni | ||||||
|  | 		desiredSlots    map[string]map[string]string | ||||||
|  | 		removedSlots    map[string]map[string]string | ||||||
| 		pgParams        map[string]string | 		pgParams        map[string]string | ||||||
|  | 		shouldBePatched bool | ||||||
| 		restartPrimary  bool | 		restartPrimary  bool | ||||||
| 	}{ | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			subtest: "Patroni and Postgresql.Parameters do not differ", | ||||||
|  | 			patroni: acidv1.Patroni{ | ||||||
|  | 				TTL: 20, | ||||||
|  | 			}, | ||||||
|  | 			pgParams: map[string]string{ | ||||||
|  | 				"log_min_duration_statement": "200", | ||||||
|  | 				"max_connections":            "50", | ||||||
|  | 			}, | ||||||
|  | 			shouldBePatched: false, | ||||||
|  | 			restartPrimary:  false, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			subtest: "Patroni and Postgresql.Parameters differ - restart replica first", | 			subtest: "Patroni and Postgresql.Parameters differ - restart replica first", | ||||||
| 			patroni: acidv1.Patroni{ | 			patroni: acidv1.Patroni{ | ||||||
|  | @ -222,6 +244,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 				"log_min_duration_statement": "500", // desired 200
 | 				"log_min_duration_statement": "500", // desired 200
 | ||||||
| 				"max_connections":            "100", // desired 50
 | 				"max_connections":            "100", // desired 50
 | ||||||
| 			}, | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
| 			restartPrimary:  false, | 			restartPrimary:  false, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -231,6 +254,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 				"log_min_duration_statement": "500", // desired 200
 | 				"log_min_duration_statement": "500", // desired 200
 | ||||||
| 				"max_connections":            "100", // desired 50
 | 				"max_connections":            "100", // desired 50
 | ||||||
| 			}, | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
| 			restartPrimary:  false, | 			restartPrimary:  false, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -240,6 +264,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 				"log_min_duration_statement": "200", | 				"log_min_duration_statement": "200", | ||||||
| 				"max_connections":            "30", // desired 50
 | 				"max_connections":            "30", // desired 50
 | ||||||
| 			}, | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
| 			restartPrimary:  false, | 			restartPrimary:  false, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -249,19 +274,105 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 				"log_min_duration_statement": "200", | 				"log_min_duration_statement": "200", | ||||||
| 				"max_connections":            "100", // desired 50
 | 				"max_connections":            "100", // desired 50
 | ||||||
| 			}, | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
| 			restartPrimary:  true, | 			restartPrimary:  true, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subtest: "slot does not exist but is desired", | ||||||
|  | 			patroni: acidv1.Patroni{ | ||||||
|  | 				TTL: 20, | ||||||
|  | 			}, | ||||||
|  | 			desiredSlots: testSlots, | ||||||
|  | 			pgParams: map[string]string{ | ||||||
|  | 				"log_min_duration_statement": "200", | ||||||
|  | 				"max_connections":            "50", | ||||||
|  | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
|  | 			restartPrimary:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subtest: "slot exist, nothing specified in manifest", | ||||||
|  | 			patroni: acidv1.Patroni{ | ||||||
|  | 				TTL: 20, | ||||||
|  | 				Slots: map[string]map[string]string{ | ||||||
|  | 					"slot1": { | ||||||
|  | 						"type":     "logical", | ||||||
|  | 						"plugin":   "pgoutput", | ||||||
|  | 						"database": "foo", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			pgParams: map[string]string{ | ||||||
|  | 				"log_min_duration_statement": "200", | ||||||
|  | 				"max_connections":            "50", | ||||||
|  | 			}, | ||||||
|  | 			shouldBePatched: false, | ||||||
|  | 			restartPrimary:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subtest: "slot is removed from manifest", | ||||||
|  | 			patroni: acidv1.Patroni{ | ||||||
|  | 				TTL: 20, | ||||||
|  | 				Slots: map[string]map[string]string{ | ||||||
|  | 					"slot1": { | ||||||
|  | 						"type":     "logical", | ||||||
|  | 						"plugin":   "pgoutput", | ||||||
|  | 						"database": "foo", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			removedSlots: testSlots, | ||||||
|  | 			pgParams: map[string]string{ | ||||||
|  | 				"log_min_duration_statement": "200", | ||||||
|  | 				"max_connections":            "50", | ||||||
|  | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
|  | 			restartPrimary:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subtest: "slot plugin differs", | ||||||
|  | 			patroni: acidv1.Patroni{ | ||||||
|  | 				TTL: 20, | ||||||
|  | 				Slots: map[string]map[string]string{ | ||||||
|  | 					"slot1": { | ||||||
|  | 						"type":     "logical", | ||||||
|  | 						"plugin":   "pgoutput", | ||||||
|  | 						"database": "foo", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			desiredSlots: testSlots, | ||||||
|  | 			pgParams: map[string]string{ | ||||||
|  | 				"log_min_duration_statement": "200", | ||||||
|  | 				"max_connections":            "50", | ||||||
|  | 			}, | ||||||
|  | 			shouldBePatched: true, | ||||||
|  | 			restartPrimary:  false, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
|  | 		if len(tt.desiredSlots) > 0 { | ||||||
|  | 			cluster.Spec.Patroni.Slots = tt.desiredSlots | ||||||
|  | 		} | ||||||
|  | 		if len(tt.removedSlots) > 0 { | ||||||
|  | 			for slotName, removedSlot := range tt.removedSlots { | ||||||
|  | 				cluster.replicationSlots[slotName] = removedSlot | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		configPatched, requirePrimaryRestart, err := cluster.checkAndSetGlobalPostgreSQLConfiguration(mockPod, tt.patroni, cluster.Spec.Patroni, tt.pgParams, cluster.Spec.Parameters) | 		configPatched, requirePrimaryRestart, err := cluster.checkAndSetGlobalPostgreSQLConfiguration(mockPod, tt.patroni, cluster.Spec.Patroni, tt.pgParams, cluster.Spec.Parameters) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		if configPatched != true { | 		if configPatched != tt.shouldBePatched { | ||||||
| 			t.Errorf("%s - %s: expected config update did not happen", testName, tt.subtest) | 			t.Errorf("%s - %s: expected config update did not happen", testName, tt.subtest) | ||||||
| 		} | 		} | ||||||
| 		if requirePrimaryRestart != tt.restartPrimary { | 		if requirePrimaryRestart != tt.restartPrimary { | ||||||
| 			t.Errorf("%s - %s: wrong master restart strategy, got restart %v, expected restart %v", testName, tt.subtest, requirePrimaryRestart, tt.restartPrimary) | 			t.Errorf("%s - %s: wrong master restart strategy, got restart %v, expected restart %v", testName, tt.subtest, requirePrimaryRestart, tt.restartPrimary) | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		// reset slots for next tests
 | ||||||
|  | 		cluster.Spec.Patroni.Slots = nil | ||||||
|  | 		cluster.replicationSlots = make(map[string]interface{}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	testsFailsafe := []struct { | 	testsFailsafe := []struct { | ||||||
|  | @ -342,7 +453,7 @@ func TestCheckAndSetGlobalPostgreSQLConfiguration(t *testing.T) { | ||||||
| 			effectiveVal:    util.True(), | 			effectiveVal:    util.True(), | ||||||
| 			desiredVal:      true, | 			desiredVal:      true, | ||||||
| 			shouldBePatched: false, // should not require patching
 | 			shouldBePatched: false, // should not require patching
 | ||||||
| 			restartPrimary:  true, | 			restartPrimary:  false, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue