add e2e test
This commit is contained in:
		
							parent
							
								
									08542711e0
								
							
						
					
					
						commit
						58280809d8
					
				|  | @ -551,6 +551,16 @@ spec: | |||
|                     - SUPERUSER | ||||
|                     - nosuperuser | ||||
|                     - NOSUPERUSER | ||||
|               usersWithPasswordRotation: | ||||
|                 type: array | ||||
|                 nullable: true | ||||
|                 items: | ||||
|                   type: string | ||||
|               usersWithInPlacePasswordRotation: | ||||
|                 type: array | ||||
|                 nullable: true | ||||
|                 items: | ||||
|                   type: string | ||||
|               volume: | ||||
|                 type: object | ||||
|                 required: | ||||
|  |  | |||
|  | @ -329,7 +329,9 @@ manifest. The rotation and retention intervals can only be configured globally. | |||
| 
 | ||||
| ``` | ||||
| spec: | ||||
|   usersWithSecretRotation: "foo_user,bar_reader_user" | ||||
|   usersWithSecretRotation: | ||||
|   - foo_user | ||||
|   - bar_reader_user | ||||
| ``` | ||||
| 
 | ||||
| ### Password replacement without extra roles | ||||
|  | @ -342,7 +344,9 @@ manifest: | |||
| 
 | ||||
| ``` | ||||
| spec: | ||||
|   usersWithInPlaceSecretRotation: "flyway,bar_owner_user" | ||||
|   usersWithInPlaceSecretRotation: | ||||
|   - flyway | ||||
|   - bar_owner_user | ||||
| ``` | ||||
| 
 | ||||
| This would be the recommended option to enable rotation in secrets of database | ||||
|  |  | |||
|  | @ -321,6 +321,9 @@ class K8s: | |||
|     def get_cluster_replica_pod(self, labels='application=spilo,cluster-name=acid-minimal-cluster', namespace='default'): | ||||
|         return self.get_cluster_pod('replica', labels, namespace) | ||||
| 
 | ||||
|     def get_secret_data(self, username, clustername='acid-minimal-cluster', namespace='default'): | ||||
|         return self.api.core_v1.read_namespaced_secret( | ||||
|                 "{}.{}.credentials.postgresql.acid.zalan.do".format(username.replace("_","-"), clustername), namespace).data | ||||
| 
 | ||||
| class K8sBase: | ||||
|     ''' | ||||
|  |  | |||
|  | @ -4,8 +4,9 @@ import time | |||
| import timeout_decorator | ||||
| import os | ||||
| import yaml | ||||
| import base64 | ||||
| 
 | ||||
| from datetime import datetime | ||||
| from datetime import datetime, date, timedelta | ||||
| from kubernetes import client | ||||
| 
 | ||||
| from tests.k8s_api import K8s | ||||
|  | @ -600,7 +601,6 @@ class EndToEndTestCase(unittest.TestCase): | |||
|         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 | ||||
|  | @ -695,7 +695,6 @@ class EndToEndTestCase(unittest.TestCase): | |||
|         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 | ||||
|  | @ -1056,6 +1055,90 @@ class EndToEndTestCase(unittest.TestCase): | |||
|         self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), | ||||
|                              0, "Pooler pods not scaled down") | ||||
| 
 | ||||
|     @timeout_decorator.timeout(TEST_TIMEOUT_SEC) | ||||
|     def test_password_rotation(self): | ||||
|         ''' | ||||
|            Test password rotation and removal of users due to retention policy | ||||
|         ''' | ||||
|         k8s = self.k8s | ||||
|         leader = k8s.get_cluster_leader_pod() | ||||
|         today = date.today() | ||||
| 
 | ||||
|         # enable password rotation for owner of foo database | ||||
|         pg_patch_inplace_rotation_for_owner = { | ||||
|             "spec": { | ||||
|                 "usersWithInPlaceSecretRotation": [ | ||||
|                     "zalando" | ||||
|                 ] | ||||
|             } | ||||
|         } | ||||
|         k8s.api.custom_objects_api.patch_namespaced_custom_object( | ||||
|             "acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_inplace_rotation_for_owner) | ||||
|         self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") | ||||
| 
 | ||||
|         # check if next rotation date was set in secret | ||||
|         secret_data = k8s.get_secret_data("zalando") | ||||
|         next_rotation_timestamp = datetime.fromisoformat(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8')) | ||||
|         today90days = today+timedelta(days=90) | ||||
|         self.assertEqual(today90days, next_rotation_timestamp.date(), | ||||
|                         "Unexpected rotation date in secret of zalando user: expected {}, got {}".format(today90days, next_rotation_timestamp.date())) | ||||
| 
 | ||||
|         # create fake rotation users that should be removed by operator | ||||
|         create_fake_rotation_user = """ | ||||
|             CREATE ROLE foo_user201031 IN ROLE foo_user; | ||||
|             CREATE ROLE foo_user211031 IN ROLE foo_user; | ||||
|         """ | ||||
|         self.query_database(leader.metadata.name, "postgres", create_fake_rotation_user) | ||||
| 
 | ||||
|         # patch foo_user secret with outdated rotation date | ||||
|         fake_rotation_date = today.isoformat() + ' 00:00:00' | ||||
|         fake_rotation_date_encoded = base64.b64encode(fake_rotation_date.encode('utf-8')) | ||||
|         secret_fake_rotation = { | ||||
|             "data": { | ||||
|                 "nextRotation": str(fake_rotation_date_encoded, 'utf-8'), | ||||
|             }, | ||||
|         } | ||||
|         k8s.api.core_v1.patch_namespaced_secret( | ||||
|             name="foo-user.acid-minimal-cluster.credentials.postgresql.acid.zalan.do",  | ||||
|             namespace="default", | ||||
|             body=secret_fake_rotation) | ||||
| 
 | ||||
|         # enable password rotation for all other users (foo_user) | ||||
|         # this will force a sync of secrets for further assertions | ||||
|         enable_password_rotation = { | ||||
|             "data": { | ||||
|                 "enable_password_rotation": "true", | ||||
|                 "password_rotation_interval": "30", | ||||
|                 "password_rotation_user_retention": "30",  # should be set to 60  | ||||
|             }, | ||||
|         } | ||||
|         k8s.update_config(enable_password_rotation) | ||||
|         self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, | ||||
|                              "Operator does not get in sync") | ||||
| 
 | ||||
|         # check if next rotation date and username have been replaced | ||||
|         secret_data = k8s.get_secret_data("foo_user") | ||||
|         secret_username = str(base64.b64decode(secret_data["username"]), 'utf-8') | ||||
|         next_rotation_timestamp = datetime.fromisoformat(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8')) | ||||
|         rotation_user = "foo_user"+today.strftime("%y%m%d") | ||||
|         today30days = today+timedelta(days=30) | ||||
| 
 | ||||
|         self.assertEqual(rotation_user, secret_username, | ||||
|                         "Unexpected username in secret of foo_user: expected {}, got {}".format(rotation_user, secret_username)) | ||||
|         self.assertEqual(today30days, next_rotation_timestamp.date(), | ||||
|                         "Unexpected rotation date in secret of foo_user: expected {}, got {}".format(today30days, next_rotation_timestamp.date())) | ||||
| 
 | ||||
|         # check if oldest fake rotation users were deleted | ||||
|         # there should only be foo_user and foo_user+today.strftime("%y%m%d") | ||||
|         user_query = """ | ||||
|             SELECT rolname | ||||
|               FROM pg_catalog.pg_roles | ||||
|              WHERE rolname LIKE 'foo_user%'; | ||||
|         """ | ||||
|         self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2, | ||||
|             "Found incorrect number of rotation users", 10, 5) | ||||
| 
 | ||||
| 
 | ||||
|     @timeout_decorator.timeout(TEST_TIMEOUT_SEC) | ||||
|     def test_patroni_config_update(self): | ||||
|         ''' | ||||
|  |  | |||
|  | @ -549,6 +549,16 @@ spec: | |||
|                     - SUPERUSER | ||||
|                     - nosuperuser | ||||
|                     - NOSUPERUSER | ||||
|               usersWithPasswordRotation: | ||||
|                 type: array | ||||
|                 nullable: true | ||||
|                 items: | ||||
|                   type: string | ||||
|               usersWithInPlacePasswordRotation: | ||||
|                 type: array | ||||
|                 nullable: true | ||||
|                 items: | ||||
|                   type: string | ||||
|               volume: | ||||
|                 type: object | ||||
|                 required: | ||||
|  |  | |||
|  | @ -833,6 +833,24 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ | |||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					"usersWithSecretRotation": { | ||||
| 						Type:     "array", | ||||
| 						Nullable: true, | ||||
| 						Items: &apiextv1.JSONSchemaPropsOrArray{ | ||||
| 							Schema: &apiextv1.JSONSchemaProps{ | ||||
| 								Type: "string", | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					"usersWithInPlaceSecretRotation": { | ||||
| 						Type:     "array", | ||||
| 						Nullable: true, | ||||
| 						Items: &apiextv1.JSONSchemaPropsOrArray{ | ||||
| 							Schema: &apiextv1.JSONSchemaProps{ | ||||
| 								Type: "string", | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					"volume": { | ||||
| 						Type:     "object", | ||||
| 						Required: []string{"size"}, | ||||
|  |  | |||
|  | @ -710,13 +710,18 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// connection pooler needs one system user created, which is done in
 | ||||
| 	// initUsers. Check if it needs to be called.
 | ||||
| 	// check if users need to be synced
 | ||||
| 	sameUsers := reflect.DeepEqual(oldSpec.Spec.Users, newSpec.Spec.Users) && | ||||
| 		reflect.DeepEqual(oldSpec.Spec.PreparedDatabases, newSpec.Spec.PreparedDatabases) | ||||
| 	sameRotatedUsers := reflect.DeepEqual(oldSpec.Spec.UsersWithSecretRotation, newSpec.Spec.UsersWithSecretRotation) && | ||||
| 		reflect.DeepEqual(oldSpec.Spec.UsersWithInPlaceSecretRotation, newSpec.Spec.UsersWithInPlaceSecretRotation) | ||||
| 
 | ||||
| 	// connection pooler needs one system user created, which is done in
 | ||||
| 	// initUsers. Check if it needs to be called.
 | ||||
| 	needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) || | ||||
| 		needReplicaConnectionPoolerWorker(&newSpec.Spec) | ||||
| 	if !sameUsers || needConnectionPooler { | ||||
| 
 | ||||
| 	if !sameUsers || !sameRotatedUsers || needConnectionPooler { | ||||
| 		c.logger.Debugf("initialize users") | ||||
| 		if err := c.initUsers(); err != nil { | ||||
| 			c.logger.Errorf("could not init users: %v", err) | ||||
|  |  | |||
|  | @ -267,14 +267,15 @@ func (c *Cluster) cleanupRotatedUsers(rotatedUsers []string, db *sql.DB) error { | |||
| 		return fmt.Errorf("error when querying for deprecated users from password rotation: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// make sure user retention policy aligns with rotation interval
 | ||||
| 	retenionDays := c.OpConfig.PasswordRotationUserRetention | ||||
| 	if retenionDays < 2*c.OpConfig.PasswordRotationInterval { | ||||
| 		retenionDays = 2 * c.OpConfig.PasswordRotationInterval | ||||
| 		c.logger.Warnf("user retention days too few compared to rotation interval %d - setting it to %d", c.OpConfig.PasswordRotationInterval, retenionDays) | ||||
| 	} | ||||
| 	retentionDate := time.Now().AddDate(0, 0, int(retenionDays)*-1) | ||||
| 
 | ||||
| 	for rotatedUser, dateSuffix := range extraUsers { | ||||
| 		// make sure user retention policy aligns with rotation interval
 | ||||
| 		retenionDays := c.OpConfig.PasswordRotationUserRetention | ||||
| 		if retenionDays < 2*c.OpConfig.PasswordRotationInterval { | ||||
| 			retenionDays = 2 * c.OpConfig.PasswordRotationInterval | ||||
| 			c.logger.Warnf("user retention days too few compared to rotation interval %d - setting it to %d", c.OpConfig.PasswordRotationInterval, retenionDays) | ||||
| 		} | ||||
| 		retentionDate := time.Now().AddDate(0, 0, int(retenionDays)*-1) | ||||
| 		userCreationDate, err := time.Parse("060102", dateSuffix) | ||||
| 		if err != nil { | ||||
| 			c.logger.Errorf("could not parse creation date suffix of user %q: %v", rotatedUser, err) | ||||
|  |  | |||
|  | @ -674,7 +674,7 @@ func (c *Cluster) syncSecrets() error { | |||
| 
 | ||||
| 			// if password rotation is enabled update password and username if rotation interval has been passed
 | ||||
| 			if (c.OpConfig.EnablePasswordRotation && pwdUser.Origin != spec.RoleOriginInfrastructure && !pwdUser.IsDbOwner) || | ||||
| 				util.SliceContains(c.Spec.UsersWithSecretRotation, secretUsername) || util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) { | ||||
| 				util.SliceContains(c.Spec.UsersWithSecretRotation, pwdUser.Name) || util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, pwdUser.Name) { | ||||
| 				currentTime := time.Now() | ||||
| 
 | ||||
| 				// initialize password rotation setting first rotation date
 | ||||
|  | @ -690,7 +690,7 @@ func (c *Cluster) syncSecrets() error { | |||
| 				} | ||||
| 
 | ||||
| 				if currentTime.After(nextRotationDate) { | ||||
| 					if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) { | ||||
| 					if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, pwdUser.Name) { | ||||
| 						retentionUsers = append(retentionUsers, pwdUser.Name) | ||||
| 						newRotationUsername := pwdUser.Name + currentTime.Format("060102") | ||||
| 						pwdUser.MemberOf = []string{pwdUser.Name} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue