Rename roles that are removed from PostgresTeam CRD (#1457)
* rename db roles that are removed from manifests * extend PostgresTeam e2e test * make suffix configurable and add deprecated field to pgUser struct * deny LOGIN from deprecated roles * update feature documentation
This commit is contained in:
		
							parent
							
								
									7a8dc6084d
								
							
						
					
					
						commit
						eeb59c5bfd
					
				|  | @ -443,6 +443,9 @@ spec: | ||||||
|                   enable_postgres_team_crd_superusers: |                   enable_postgres_team_crd_superusers: | ||||||
|                     type: boolean |                     type: boolean | ||||||
|                     default: false |                     default: false | ||||||
|  |                   enable_team_member_deprecation: | ||||||
|  |                     type: boolean | ||||||
|  |                     default: false | ||||||
|                   enable_team_superuser: |                   enable_team_superuser: | ||||||
|                     type: boolean |                     type: boolean | ||||||
|                     default: false |                     default: false | ||||||
|  | @ -465,6 +468,9 @@ spec: | ||||||
|                       type: string |                       type: string | ||||||
|                     default: |                     default: | ||||||
|                     - admin |                     - admin | ||||||
|  |                   role_deletion_suffix: | ||||||
|  |                     type: string | ||||||
|  |                     default: "_deleted" | ||||||
|                   team_admin_role: |                   team_admin_role: | ||||||
|                     type: string |                     type: string | ||||||
|                     default: "admin" |                     default: "admin" | ||||||
|  |  | ||||||
|  | @ -289,13 +289,13 @@ configLogicalBackup: | ||||||
| # automate creation of human users with teams API service | # automate creation of human users with teams API service | ||||||
| configTeamsApi: | configTeamsApi: | ||||||
|   # team_admin_role will have the rights to grant roles coming from PG manifests |   # team_admin_role will have the rights to grant roles coming from PG manifests | ||||||
|   # enable_admin_role_for_users: true |   enable_admin_role_for_users: true | ||||||
| 
 |  | ||||||
|   # operator watches for PostgresTeam CRs to assign additional teams and members to clusters |   # operator watches for PostgresTeam CRs to assign additional teams and members to clusters | ||||||
|   enable_postgres_team_crd: false |   enable_postgres_team_crd: false | ||||||
|   # toogle to create additional superuser teams from PostgresTeam CRs |   # toogle to create additional superuser teams from PostgresTeam CRs | ||||||
|   # enable_postgres_team_crd_superusers: false |   enable_postgres_team_crd_superusers: false | ||||||
| 
 |   # toggle to automatically rename roles of former team members and deny LOGIN | ||||||
|  |   enable_team_member_deprecation: false | ||||||
|   # toggle to grant superuser to team members created from the Teams API |   # toggle to grant superuser to team members created from the Teams API | ||||||
|   enable_team_superuser: false |   enable_team_superuser: false | ||||||
|   # toggles usage of the Teams API by the operator |   # toggles usage of the Teams API by the operator | ||||||
|  | @ -306,12 +306,13 @@ configTeamsApi: | ||||||
|   # operator will add all team member roles to this group and add a pg_hba line |   # operator will add all team member roles to this group and add a pg_hba line | ||||||
|   pam_role_name: zalandos |   pam_role_name: zalandos | ||||||
|   # List of teams which members need the superuser role in each Postgres cluster |   # List of teams which members need the superuser role in each Postgres cluster | ||||||
|   # postgres_superuser_teams: |   postgres_superuser_teams: | ||||||
|   # - postgres_superusers |   - postgres_superusers | ||||||
| 
 |  | ||||||
|   # List of roles that cannot be overwritten by an application, team or infrastructure role |   # List of roles that cannot be overwritten by an application, team or infrastructure role | ||||||
|   protected_role_names: |   protected_role_names: | ||||||
|   - admin |   - admin | ||||||
|  |   # Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD | ||||||
|  |   role_deletion_suffix: "_deleted" | ||||||
|   # role name to grant to team members created from the Teams API |   # role name to grant to team members created from the Teams API | ||||||
|   team_admin_role: admin |   team_admin_role: admin | ||||||
|   # postgres config parameters to apply to each team member role |   # postgres config parameters to apply to each team member role | ||||||
|  |  | ||||||
|  | @ -280,36 +280,32 @@ configLogicalBackup: | ||||||
| # automate creation of human users with teams API service | # automate creation of human users with teams API service | ||||||
| configTeamsApi: | configTeamsApi: | ||||||
|   # team_admin_role will have the rights to grant roles coming from PG manifests |   # team_admin_role will have the rights to grant roles coming from PG manifests | ||||||
|   # enable_admin_role_for_users: "true" |   enable_admin_role_for_users: "true" | ||||||
| 
 |  | ||||||
|   # operator watches for PostgresTeam CRs to assign additional teams and members to clusters |   # operator watches for PostgresTeam CRs to assign additional teams and members to clusters | ||||||
|   enable_postgres_team_crd: "false" |   enable_postgres_team_crd: "false" | ||||||
|   # toogle to create additional superuser teams from PostgresTeam CRs |   # toogle to create additional superuser teams from PostgresTeam CRs | ||||||
|   # enable_postgres_team_crd_superusers: "false" |   enable_postgres_team_crd_superusers: "false" | ||||||
| 
 |   # toggle to automatically rename roles of former team members and deny LOGIN | ||||||
|  |   enable_team_member_deprecation: "false" | ||||||
|   # toggle to grant superuser to team members created from the Teams API |   # toggle to grant superuser to team members created from the Teams API | ||||||
|   # enable_team_superuser: "false" |   enable_team_superuser: "false" | ||||||
| 
 |  | ||||||
|   # toggles usage of the Teams API by the operator |   # toggles usage of the Teams API by the operator | ||||||
|   enable_teams_api: "false" |   enable_teams_api: "false" | ||||||
|   # should contain a URL to use for authentication (username and token) |   # should contain a URL to use for authentication (username and token) | ||||||
|   # pam_configuration: https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees |   # pam_configuration: https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees | ||||||
| 
 | 
 | ||||||
|   # operator will add all team member roles to this group and add a pg_hba line |   # operator will add all team member roles to this group and add a pg_hba line | ||||||
|   # pam_role_name: zalandos |   pam_role_name: "zalandos" | ||||||
| 
 |  | ||||||
|   # List of teams which members need the superuser role in each Postgres cluster |   # List of teams which members need the superuser role in each Postgres cluster | ||||||
|   # postgres_superuser_teams: "postgres_superusers" |   postgres_superuser_teams: "postgres_superusers" | ||||||
| 
 |  | ||||||
|   # List of roles that cannot be overwritten by an application, team or infrastructure role |   # List of roles that cannot be overwritten by an application, team or infrastructure role | ||||||
|   # protected_role_names: "admin" |   protected_role_names: "admin" | ||||||
| 
 |   # Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD | ||||||
|  |   role_deletion_suffix: "_deleted" | ||||||
|   # role name to grant to team members created from the Teams API |   # role name to grant to team members created from the Teams API | ||||||
|   # team_admin_role: "admin" |   team_admin_role: "admin" | ||||||
| 
 |  | ||||||
|   # postgres config parameters to apply to each team member role |   # postgres config parameters to apply to each team member role | ||||||
|   # team_api_role_configuration: "log_statement:all" |   team_api_role_configuration: "log_statement:all" | ||||||
| 
 |  | ||||||
|   # URL of the Teams API service |   # URL of the Teams API service | ||||||
|   # teams_api_url: http://fake-teams-api.default.svc.cluster.local |   # teams_api_url: http://fake-teams-api.default.svc.cluster.local | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -704,6 +704,19 @@ key. | ||||||
|   cluster to administer Postgres and maintain infrastructure built around it. |   cluster to administer Postgres and maintain infrastructure built around it. | ||||||
|   The default is empty. |   The default is empty. | ||||||
| 
 | 
 | ||||||
|  | * **role_deletion_suffix** | ||||||
|  |   defines a suffix that - when `enable_team_member_deprecation` is set to | ||||||
|  |   `true` - will be appended to database role names of team members that were | ||||||
|  |   removed from either the team in the Teams API or a `PostgresTeam` custom | ||||||
|  |   resource (additionalMembers). When re-added, the operator will rename roles | ||||||
|  |   with the defined suffix back to the original role name. | ||||||
|  |   The default is `_deleted`. | ||||||
|  | 
 | ||||||
|  | * **enable_team_member_deprecation** | ||||||
|  |   if `true` database roles of former team members will be renamed by appending | ||||||
|  |   the configured `role_deletion_suffix` and `LOGIN` privilege will be revoked. | ||||||
|  |   The default is `false`. | ||||||
|  | 
 | ||||||
| * **enable_postgres_team_crd** | * **enable_postgres_team_crd** | ||||||
|   toggle to make the operator watch for created or updated `PostgresTeam` CRDs |   toggle to make the operator watch for created or updated `PostgresTeam` CRDs | ||||||
|   and create roles for specified additional teams and members. |   and create roles for specified additional teams and members. | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								docs/user.md
								
								
								
								
							
							
						
						
									
										17
									
								
								docs/user.md
								
								
								
								
							|  | @ -407,6 +407,23 @@ spec: | ||||||
|     - "briggs" |     - "briggs" | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | #### Removed members | ||||||
|  | 
 | ||||||
|  | The Postgres Operator does not delete database roles when users are removed | ||||||
|  | from manifests. But, using the `PostgresTeam` custom resource or Teams API it | ||||||
|  | is very easy to add roles to many clusters. Manually reverting such a change | ||||||
|  | is cumbersome. Therefore, if members are removed from a `PostgresTeam` or the | ||||||
|  | Teams API the operator can rename roles appending a configured suffix to the | ||||||
|  | name (see `role_deletion_suffix` option) and revoke the `LOGIN` privilege. | ||||||
|  | The suffix makes it easy then for a cleanup script to remove those deprecated | ||||||
|  | roles completely. Switch `enable_team_member_deprecation` to `true` to enable | ||||||
|  | this behavior. | ||||||
|  | 
 | ||||||
|  | When a role is re-added to a `PostgresTeam` manifest (or to the source behind | ||||||
|  | the Teams API) the operator will check for roles with the configured suffix | ||||||
|  | and if found, rename the role back to the original name and grant `LOGIN` | ||||||
|  | again. | ||||||
|  | 
 | ||||||
| ## Prepared databases with roles and default privileges | ## Prepared databases with roles and default privileges | ||||||
| 
 | 
 | ||||||
| The `users` section in the manifests only allows for creating database roles | The `users` section in the manifests only allows for creating database roles | ||||||
|  |  | ||||||
|  | @ -197,13 +197,15 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|         enable_postgres_team_crd = { |         enable_postgres_team_crd = { | ||||||
|             "data": { |             "data": { | ||||||
|                 "enable_postgres_team_crd": "true", |                 "enable_postgres_team_crd": "true", | ||||||
|                 "resync_period": "15s", |                 "enable_team_member_deprecation": "true", | ||||||
|  |                 "role_deletion_suffix": "_delete_me", | ||||||
|  |                 "resync_period": "15s" | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|         self.k8s.update_config(enable_postgres_team_crd) |         self.k8s.update_config(enable_postgres_team_crd) | ||||||
|         self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, |         self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, | ||||||
|                              "Operator does not get in sync") |                              "Operator does not get in sync") | ||||||
|          | 
 | ||||||
|         self.k8s.api.custom_objects_api.patch_namespaced_custom_object( |         self.k8s.api.custom_objects_api.patch_namespaced_custom_object( | ||||||
|         'acid.zalan.do', 'v1', 'default', |         'acid.zalan.do', 'v1', 'default', | ||||||
|         'postgresteams', 'custom-team-membership', |         'postgresteams', 'custom-team-membership', | ||||||
|  | @ -222,18 +224,60 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         # make sure we let one sync pass and the new user being added |  | ||||||
|         time.sleep(15) |  | ||||||
| 
 |  | ||||||
|         leader = self.k8s.get_cluster_leader_pod() |         leader = self.k8s.get_cluster_leader_pod() | ||||||
|         user_query = """ |         user_query = """ | ||||||
|             SELECT usename |             SELECT rolname | ||||||
|               FROM pg_catalog.pg_user |               FROM pg_catalog.pg_roles | ||||||
|              WHERE usename IN ('elephant', 'kind'); |              WHERE rolname IN ('elephant', 'kind'); | ||||||
|         """ |         """ | ||||||
|         users = self.query_database(leader.metadata.name, "postgres", user_query) |         self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,  | ||||||
|         self.eventuallyEqual(lambda: len(users), 2,  |             "Not all additional users found in database", 10, 5) | ||||||
|             "Not all additional users found in database: {}".format(users)) | 
 | ||||||
|  |         # replace additional member and check if the removed member's role is renamed | ||||||
|  |         self.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 | ||||||
|  |         self.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 config change | ||||||
|         revert_resync = { |         revert_resync = { | ||||||
|  | @ -407,9 +451,9 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
| 
 | 
 | ||||||
|         leader = k8s.get_cluster_leader_pod() |         leader = k8s.get_cluster_leader_pod() | ||||||
|         schemas_query = """ |         schemas_query = """ | ||||||
|             select schema_name |             SELECT schema_name | ||||||
|             from information_schema.schemata |               FROM information_schema.schemata | ||||||
|             where schema_name = 'pooler' |              WHERE schema_name = 'pooler' | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         db_list = self.list_databases(leader.metadata.name) |         db_list = self.list_databases(leader.metadata.name) | ||||||
|  | @ -529,6 +573,7 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|                             "Parameters": None, |                             "Parameters": None, | ||||||
|                             "AdminRole": "", |                             "AdminRole": "", | ||||||
|                             "Origin": 2, |                             "Origin": 2, | ||||||
|  |                             "Deleted": False | ||||||
|                         }) |                         }) | ||||||
|                         return True |                         return True | ||||||
|                 except: |                 except: | ||||||
|  | @ -1417,7 +1462,7 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|         k8s = self.k8s |         k8s = self.k8s | ||||||
|         result_set = [] |         result_set = [] | ||||||
|         db_list = [] |         db_list = [] | ||||||
|         db_list_query = "select datname from pg_database" |         db_list_query = "SELECT datname FROM pg_database" | ||||||
|         exec_query = r"psql -tAq -c \"{}\" -d {}" |         exec_query = r"psql -tAq -c \"{}\" -d {}" | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|  |  | ||||||
|  | @ -51,6 +51,7 @@ data: | ||||||
|   # enable_shm_volume: "true" |   # enable_shm_volume: "true" | ||||||
|   # enable_sidecars: "true" |   # enable_sidecars: "true" | ||||||
|   enable_spilo_wal_path_compat: "true" |   enable_spilo_wal_path_compat: "true" | ||||||
|  |   enable_team_member_deprecation: "false" | ||||||
|   # enable_team_superuser: "false" |   # enable_team_superuser: "false" | ||||||
|   enable_teams_api: "false" |   enable_teams_api: "false" | ||||||
|   # etcd_host: "" |   # etcd_host: "" | ||||||
|  | @ -111,6 +112,7 @@ data: | ||||||
|   resource_check_timeout: 10m |   resource_check_timeout: 10m | ||||||
|   resync_period: 30m |   resync_period: 30m | ||||||
|   ring_log_lines: "100" |   ring_log_lines: "100" | ||||||
|  |   role_deletion_suffix: "_deleted" | ||||||
|   secret_name_template: "{username}.{cluster}.credentials" |   secret_name_template: "{username}.{cluster}.credentials" | ||||||
|   # sidecar_docker_images: "" |   # sidecar_docker_images: "" | ||||||
|   # set_memory_request_to_limit: "false" |   # set_memory_request_to_limit: "false" | ||||||
|  |  | ||||||
|  | @ -439,6 +439,9 @@ spec: | ||||||
|                   enable_postgres_team_crd_superusers: |                   enable_postgres_team_crd_superusers: | ||||||
|                     type: boolean |                     type: boolean | ||||||
|                     default: false |                     default: false | ||||||
|  |                   enable_team_member_deprecation: | ||||||
|  |                     type: boolean | ||||||
|  |                     default: false | ||||||
|                   enable_team_superuser: |                   enable_team_superuser: | ||||||
|                     type: boolean |                     type: boolean | ||||||
|                     default: false |                     default: false | ||||||
|  | @ -461,6 +464,9 @@ spec: | ||||||
|                       type: string |                       type: string | ||||||
|                     default: |                     default: | ||||||
|                     - admin |                     - admin | ||||||
|  |                   role_deletion_suffix: | ||||||
|  |                     type: string | ||||||
|  |                     default: "_deleted" | ||||||
|                   team_admin_role: |                   team_admin_role: | ||||||
|                     type: string |                     type: string | ||||||
|                     default: "admin" |                     default: "admin" | ||||||
|  |  | ||||||
|  | @ -141,6 +141,7 @@ configuration: | ||||||
|     # enable_admin_role_for_users: true |     # enable_admin_role_for_users: true | ||||||
|     # enable_postgres_team_crd: false |     # enable_postgres_team_crd: false | ||||||
|     # enable_postgres_team_crd_superusers: false |     # enable_postgres_team_crd_superusers: false | ||||||
|  |     enable_team_member_deprecation: false | ||||||
|     enable_team_superuser: false |     enable_team_superuser: false | ||||||
|     enable_teams_api: false |     enable_teams_api: false | ||||||
|     # pam_configuration: "" |     # pam_configuration: "" | ||||||
|  | @ -149,6 +150,7 @@ configuration: | ||||||
|     # - postgres_superusers |     # - postgres_superusers | ||||||
|     protected_role_names: |     protected_role_names: | ||||||
|     - admin |     - admin | ||||||
|  |     role_deletion_suffix: "_deleted" | ||||||
|     team_admin_role: admin |     team_admin_role: admin | ||||||
|     team_api_role_configuration: |     team_api_role_configuration: | ||||||
|       log_statement: all |       log_statement: all | ||||||
|  |  | ||||||
|  | @ -1377,6 +1377,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ | ||||||
| 							"enable_postgres_team_crd_superusers": { | 							"enable_postgres_team_crd_superusers": { | ||||||
| 								Type: "boolean", | 								Type: "boolean", | ||||||
| 							}, | 							}, | ||||||
|  | 							"enable_team_member_deprecation": { | ||||||
|  | 								Type: "boolean", | ||||||
|  | 							}, | ||||||
| 							"enable_team_superuser": { | 							"enable_team_superuser": { | ||||||
| 								Type: "boolean", | 								Type: "boolean", | ||||||
| 							}, | 							}, | ||||||
|  | @ -1405,6 +1408,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ | ||||||
| 									}, | 									}, | ||||||
| 								}, | 								}, | ||||||
| 							}, | 							}, | ||||||
|  | 							"role_deletion_suffix": { | ||||||
|  | 								Type: "string", | ||||||
|  | 							}, | ||||||
| 							"team_admin_role": { | 							"team_admin_role": { | ||||||
| 								Type: "string", | 								Type: "string", | ||||||
| 							}, | 							}, | ||||||
|  |  | ||||||
|  | @ -159,6 +159,8 @@ type TeamsAPIConfiguration struct { | ||||||
| 	PostgresSuperuserTeams          []string          `json:"postgres_superuser_teams,omitempty"` | 	PostgresSuperuserTeams          []string          `json:"postgres_superuser_teams,omitempty"` | ||||||
| 	EnablePostgresTeamCRD           bool              `json:"enable_postgres_team_crd,omitempty"` | 	EnablePostgresTeamCRD           bool              `json:"enable_postgres_team_crd,omitempty"` | ||||||
| 	EnablePostgresTeamCRDSuperusers bool              `json:"enable_postgres_team_crd_superusers,omitempty"` | 	EnablePostgresTeamCRDSuperusers bool              `json:"enable_postgres_team_crd_superusers,omitempty"` | ||||||
|  | 	EnableTeamMemberDeprecation     bool              `json:"enable_team_member_deprecation,omitempty"` | ||||||
|  | 	RoleDeletionSuffix              string            `json:"role_deletion_suffix,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // LoggingRESTAPIConfiguration defines Logging API conf
 | // LoggingRESTAPIConfiguration defines Logging API conf
 | ||||||
|  |  | ||||||
|  | @ -74,6 +74,7 @@ type Cluster struct { | ||||||
| 	eventRecorder    record.EventRecorder | 	eventRecorder    record.EventRecorder | ||||||
| 	patroni          patroni.Interface | 	patroni          patroni.Interface | ||||||
| 	pgUsers          map[string]spec.PgUser | 	pgUsers          map[string]spec.PgUser | ||||||
|  | 	pgUsersCache     map[string]spec.PgUser | ||||||
| 	systemUsers      map[string]spec.PgUser | 	systemUsers      map[string]spec.PgUser | ||||||
| 	podSubscribers   map[spec.NamespacedName]chan PodEvent | 	podSubscribers   map[spec.NamespacedName]chan PodEvent | ||||||
| 	podSubscribersMu sync.RWMutex | 	podSubscribersMu sync.RWMutex | ||||||
|  | @ -129,7 +130,9 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres | ||||||
| 			Secrets:   make(map[types.UID]*v1.Secret), | 			Secrets:   make(map[types.UID]*v1.Secret), | ||||||
| 			Services:  make(map[PostgresRole]*v1.Service), | 			Services:  make(map[PostgresRole]*v1.Service), | ||||||
| 			Endpoints: make(map[PostgresRole]*v1.Endpoints)}, | 			Endpoints: make(map[PostgresRole]*v1.Endpoints)}, | ||||||
| 		userSyncStrategy:    users.DefaultUserSyncStrategy{PasswordEncryption: passwordEncryption}, | 		userSyncStrategy: users.DefaultUserSyncStrategy{ | ||||||
|  | 			PasswordEncryption: passwordEncryption, | ||||||
|  | 			RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix}, | ||||||
| 		deleteOptions:       metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy}, | 		deleteOptions:       metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy}, | ||||||
| 		podEventsQueue:      podEventsQueue, | 		podEventsQueue:      podEventsQueue, | ||||||
| 		KubeClient:          kubeClient, | 		KubeClient:          kubeClient, | ||||||
|  | @ -190,6 +193,17 @@ func (c *Cluster) isNewCluster() bool { | ||||||
| func (c *Cluster) initUsers() error { | func (c *Cluster) initUsers() error { | ||||||
| 	c.setProcessName("initializing users") | 	c.setProcessName("initializing users") | ||||||
| 
 | 
 | ||||||
|  | 	// if team member deprecation is enabled save current state of pgUsers
 | ||||||
|  | 	// to check for deleted roles
 | ||||||
|  | 	c.pgUsersCache = map[string]spec.PgUser{} | ||||||
|  | 	if c.OpConfig.EnableTeamMemberDeprecation { | ||||||
|  | 		for k, v := range c.pgUsers { | ||||||
|  | 			if v.Origin == spec.RoleOriginTeamsAPI { | ||||||
|  | 				c.pgUsersCache[k] = v | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// clear our the previous state of the cluster users (in case we are
 | 	// clear our the previous state of the cluster users (in case we are
 | ||||||
| 	// running a sync).
 | 	// running a sync).
 | ||||||
| 	c.systemUsers = map[string]spec.PgUser{} | 	c.systemUsers = map[string]spec.PgUser{} | ||||||
|  | @ -650,7 +664,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { | ||||||
| 	needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) || | 	needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) || | ||||||
| 		needReplicaConnectionPoolerWorker(&newSpec.Spec) | 		needReplicaConnectionPoolerWorker(&newSpec.Spec) | ||||||
| 	if !sameUsers || needConnectionPooler { | 	if !sameUsers || needConnectionPooler { | ||||||
| 		c.logger.Debugf("syncing secrets") | 		c.logger.Debugf("initialize users") | ||||||
| 		if err := c.initUsers(); err != nil { | 		if err := c.initUsers(); err != nil { | ||||||
| 			c.logger.Errorf("could not init users: %v", err) | 			c.logger.Errorf("could not init users: %v", err) | ||||||
| 			updateFailed = true | 			updateFailed = true | ||||||
|  |  | ||||||
|  | @ -198,6 +198,7 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser | ||||||
| 			rolname, rolpassword                                          string | 			rolname, rolpassword                                          string | ||||||
| 			rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool | 			rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool | ||||||
| 			roloptions, memberof                                          []string | 			roloptions, memberof                                          []string | ||||||
|  | 			roldeleted                                                    bool | ||||||
| 		) | 		) | ||||||
| 		err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit, | 		err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit, | ||||||
| 			&rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof)) | 			&rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof)) | ||||||
|  | @ -216,7 +217,11 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser | ||||||
| 			parameters[fields[0]] = fields[1] | 			parameters[fields[0]] = fields[1] | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters} | 		if strings.HasSuffix(rolname, c.OpConfig.RoleDeletionSuffix) { | ||||||
|  | 			roldeleted = true | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters, Deleted: roldeleted} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return users, nil | 	return users, nil | ||||||
|  |  | ||||||
|  | @ -551,10 +551,29 @@ func (c *Cluster) syncRoles() (err error) { | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
| 
 | 
 | ||||||
|  | 	// mapping between original role name and with deletion suffix
 | ||||||
|  | 	deletedUsers := map[string]string{} | ||||||
|  | 
 | ||||||
|  | 	// create list of database roles to query
 | ||||||
| 	for _, u := range c.pgUsers { | 	for _, u := range c.pgUsers { | ||||||
| 		userNames = append(userNames, u.Name) | 		userNames = append(userNames, u.Name) | ||||||
|  | 		// add team member role name with rename suffix in case we need to rename it back
 | ||||||
|  | 		if u.Origin == spec.RoleOriginTeamsAPI && c.OpConfig.EnableTeamMemberDeprecation { | ||||||
|  | 			deletedUsers[u.Name+c.OpConfig.RoleDeletionSuffix] = u.Name | ||||||
|  | 			userNames = append(userNames, u.Name+c.OpConfig.RoleDeletionSuffix) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// add team members that exist only in cache
 | ||||||
|  | 	// to trigger a rename of the role in ProduceSyncRequests
 | ||||||
|  | 	for _, cachedUser := range c.pgUsersCache { | ||||||
|  | 		if _, exists := c.pgUsers[cachedUser.Name]; !exists { | ||||||
|  | 			userNames = append(userNames, cachedUser.Name) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// add pooler user to list of pgUsers, too
 | ||||||
|  | 	// to check if the pooler user exists or has to be created
 | ||||||
| 	if needMasterConnectionPooler(&c.Spec) || needReplicaConnectionPooler(&c.Spec) { | 	if needMasterConnectionPooler(&c.Spec) || needReplicaConnectionPooler(&c.Spec) { | ||||||
| 		connectionPoolerUser := c.systemUsers[constants.ConnectionPoolerUserKeyName] | 		connectionPoolerUser := c.systemUsers[constants.ConnectionPoolerUserKeyName] | ||||||
| 		userNames = append(userNames, connectionPoolerUser.Name) | 		userNames = append(userNames, connectionPoolerUser.Name) | ||||||
|  | @ -569,6 +588,16 @@ func (c *Cluster) syncRoles() (err error) { | ||||||
| 		return fmt.Errorf("error getting users from the database: %v", err) | 		return fmt.Errorf("error getting users from the database: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// update pgUsers where a deleted role was found
 | ||||||
|  | 	// so that they are skipped in ProduceSyncRequests
 | ||||||
|  | 	for _, dbUser := range dbUsers { | ||||||
|  | 		if originalUser, exists := deletedUsers[dbUser.Name]; exists { | ||||||
|  | 			recreatedUser := c.pgUsers[originalUser] | ||||||
|  | 			recreatedUser.Deleted = true | ||||||
|  | 			c.pgUsers[originalUser] = recreatedUser | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(dbUsers, c.pgUsers) | 	pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(dbUsers, c.pgUsers) | ||||||
| 	if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil { | 	if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil { | ||||||
| 		return fmt.Errorf("error executing sync statements: %v", err) | 		return fmt.Errorf("error executing sync statements: %v", err) | ||||||
|  |  | ||||||
|  | @ -242,7 +242,7 @@ func (c *Cluster) getTeamMembers(teamID string) ([]string, error) { | ||||||
| 		for team, membership := range *c.Config.PgTeamMap { | 		for team, membership := range *c.Config.PgTeamMap { | ||||||
| 			if team == teamID { | 			if team == teamID { | ||||||
| 				additionalMembers = membership.AdditionalMembers | 				additionalMembers = membership.AdditionalMembers | ||||||
| 				c.logger.Debugf("found %d additional members for team %q", len(members), teamID) | 				c.logger.Debugf("found %d additional members for team %q", len(additionalMembers), teamID) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -256,14 +256,12 @@ func (c *Cluster) getTeamMembers(teamID string) ([]string, error) { | ||||||
| 
 | 
 | ||||||
| 	token, err := c.oauthTokenGetter.getOAuthToken() | 	token, err := c.oauthTokenGetter.getOAuthToken() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.logger.Warnf("could not get oauth token to authenticate to team service API, only returning %d members for team %q: %v", len(members), teamID, err) | 		return nil, fmt.Errorf("could not get oauth token to authenticate to team service API: %v", err) | ||||||
| 		return members, nil |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token) | 	teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.logger.Warnf("could not get team info for team %q, only returning %d members: %v", teamID, len(members), err) | 		return nil, fmt.Errorf("could not get team info for team %q: %v", teamID, err) | ||||||
| 		return members, nil |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, member := range teamInfo.Members { | 	for _, member := range teamInfo.Members { | ||||||
|  |  | ||||||
|  | @ -180,6 +180,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur | ||||||
| 	result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams | 	result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams | ||||||
| 	result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD | 	result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD | ||||||
| 	result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers | 	result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers | ||||||
|  | 	result.EnableTeamMemberDeprecation = fromCRD.TeamsAPI.EnableTeamMemberDeprecation | ||||||
|  | 	result.RoleDeletionSuffix = util.Coalesce(fromCRD.TeamsAPI.RoleDeletionSuffix, "_deleted") | ||||||
| 
 | 
 | ||||||
| 	// logging REST API config
 | 	// logging REST API config
 | ||||||
| 	result.APIPort = util.CoalesceInt(fromCRD.LoggingRESTAPI.APIPort, 8080) | 	result.APIPort = util.CoalesceInt(fromCRD.LoggingRESTAPI.APIPort, 8080) | ||||||
|  |  | ||||||
|  | @ -42,6 +42,7 @@ const ( | ||||||
| 	PGSyncUserAdd = iota | 	PGSyncUserAdd = iota | ||||||
| 	PGsyncUserAlter | 	PGsyncUserAlter | ||||||
| 	PGSyncAlterSet // handle ALTER ROLE SET parameter = value
 | 	PGSyncAlterSet // handle ALTER ROLE SET parameter = value
 | ||||||
|  | 	PGSyncUserRename | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // PgUser contains information about a single user.
 | // PgUser contains information about a single user.
 | ||||||
|  | @ -53,6 +54,7 @@ type PgUser struct { | ||||||
| 	MemberOf   []string          `yaml:"inrole"` | 	MemberOf   []string          `yaml:"inrole"` | ||||||
| 	Parameters map[string]string `yaml:"db_parameters"` | 	Parameters map[string]string `yaml:"db_parameters"` | ||||||
| 	AdminRole  string            `yaml:"admin_role"` | 	AdminRole  string            `yaml:"admin_role"` | ||||||
|  | 	Deleted    bool              `yaml:"deleted"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (user *PgUser) Valid() bool { | func (user *PgUser) Valid() bool { | ||||||
|  |  | ||||||
|  | @ -9,8 +9,6 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	True       = true |  | ||||||
| 	False      = false |  | ||||||
| 	pgTeamList = acidv1.PostgresTeamList{ | 	pgTeamList = acidv1.PostgresTeamList{ | ||||||
| 		TypeMeta: metav1.TypeMeta{ | 		TypeMeta: metav1.TypeMeta{ | ||||||
| 			Kind:       "List", | 			Kind:       "List", | ||||||
|  |  | ||||||
|  | @ -176,6 +176,8 @@ type Config struct { | ||||||
| 	EnableTeamsAPI                         bool              `name:"enable_teams_api" default:"true"` | 	EnableTeamsAPI                         bool              `name:"enable_teams_api" default:"true"` | ||||||
| 	EnableTeamSuperuser                    bool              `name:"enable_team_superuser" default:"false"` | 	EnableTeamSuperuser                    bool              `name:"enable_team_superuser" default:"false"` | ||||||
| 	TeamAdminRole                          string            `name:"team_admin_role" default:"admin"` | 	TeamAdminRole                          string            `name:"team_admin_role" default:"admin"` | ||||||
|  | 	RoleDeletionSuffix                     string            `name:"role_deletion_suffix" default:"_deleted"` | ||||||
|  | 	EnableTeamMemberDeprecation            bool              `name:"enable_team_member_deprecation" default:"false"` | ||||||
| 	EnableAdminRoleForUsers                bool              `name:"enable_admin_role_for_users" default:"true"` | 	EnableAdminRoleForUsers                bool              `name:"enable_admin_role_for_users" default:"true"` | ||||||
| 	EnablePostgresTeamCRD                  bool              `name:"enable_postgres_team_crd" default:"false"` | 	EnablePostgresTeamCRD                  bool              `name:"enable_postgres_team_crd" default:"false"` | ||||||
| 	EnablePostgresTeamCRDSuperusers        bool              `name:"enable_postgres_team_crd_superusers" default:"false"` | 	EnablePostgresTeamCRDSuperusers        bool              `name:"enable_postgres_team_crd_superusers" default:"false"` | ||||||
|  |  | ||||||
|  | @ -9,11 +9,13 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/zalando/postgres-operator/pkg/spec" | 	"github.com/zalando/postgres-operator/pkg/spec" | ||||||
| 	"github.com/zalando/postgres-operator/pkg/util" | 	"github.com/zalando/postgres-operator/pkg/util" | ||||||
|  | 	"github.com/zalando/postgres-operator/pkg/util/constants" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	createUserSQL        = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;` | 	createUserSQL        = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;` | ||||||
| 	alterUserSQL         = `ALTER ROLE "%s" %s` | 	alterUserSQL         = `ALTER ROLE "%s" %s` | ||||||
|  | 	alterUserRenameSQL   = `ALTER ROLE "%s" RENAME TO "%s%s"` | ||||||
| 	alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL` | 	alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL` | ||||||
| 	alterRoleSetSQL      = `ALTER ROLE "%s" SET %s TO %s` | 	alterRoleSetSQL      = `ALTER ROLE "%s" SET %s TO %s` | ||||||
| 	grantToUserSQL       = `GRANT %s TO "%s"` | 	grantToUserSQL       = `GRANT %s TO "%s"` | ||||||
|  | @ -29,6 +31,7 @@ const ( | ||||||
| // (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
 | // (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
 | ||||||
| type DefaultUserSyncStrategy struct { | type DefaultUserSyncStrategy struct { | ||||||
| 	PasswordEncryption string | 	PasswordEncryption string | ||||||
|  | 	RoleDeletionSuffix string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ProduceSyncRequests figures out the types of changes that need to happen with the given users.
 | // ProduceSyncRequests figures out the types of changes that need to happen with the given users.
 | ||||||
|  | @ -36,8 +39,11 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM | ||||||
| 	newUsers spec.PgUserMap) []spec.PgSyncUserRequest { | 	newUsers spec.PgUserMap) []spec.PgSyncUserRequest { | ||||||
| 
 | 
 | ||||||
| 	var reqs []spec.PgSyncUserRequest | 	var reqs []spec.PgSyncUserRequest | ||||||
| 	// No existing roles are deleted or stripped of role memebership/flags
 |  | ||||||
| 	for name, newUser := range newUsers { | 	for name, newUser := range newUsers { | ||||||
|  | 		// do not create user that exists in DB with deletion suffix
 | ||||||
|  | 		if newUser.Deleted { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
| 		dbUser, exists := dbUsers[name] | 		dbUser, exists := dbUsers[name] | ||||||
| 		if !exists { | 		if !exists { | ||||||
| 			reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserAdd, User: newUser}) | 			reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserAdd, User: newUser}) | ||||||
|  | @ -70,6 +76,25 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// No existing roles are deleted or stripped of role membership/flags
 | ||||||
|  | 	// but team roles will be renamed and denied from LOGIN
 | ||||||
|  | 	for name, dbUser := range dbUsers { | ||||||
|  | 		if _, exists := newUsers[name]; !exists { | ||||||
|  | 			// toggle LOGIN flag based on role deletion
 | ||||||
|  | 			userFlags := make([]string, len(dbUser.Flags)) | ||||||
|  | 			userFlags = append(userFlags, dbUser.Flags...) | ||||||
|  | 			if dbUser.Deleted { | ||||||
|  | 				dbUser.Flags = util.StringSliceReplaceElement(dbUser.Flags, constants.RoleFlagNoLogin, constants.RoleFlagLogin) | ||||||
|  | 			} else { | ||||||
|  | 				dbUser.Flags = util.StringSliceReplaceElement(dbUser.Flags, constants.RoleFlagLogin, constants.RoleFlagNoLogin) | ||||||
|  | 			} | ||||||
|  | 			if !util.IsEqualIgnoreOrder(userFlags, dbUser.Flags) { | ||||||
|  | 				reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGsyncUserAlter, User: dbUser}) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserRename, User: dbUser}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return reqs | 	return reqs | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -94,6 +119,11 @@ func (strategy DefaultUserSyncStrategy) ExecuteSyncRequests(requests []spec.PgSy | ||||||
| 				reqretries = append(reqretries, request) | 				reqretries = append(reqretries, request) | ||||||
| 				errors = append(errors, fmt.Sprintf("could not set custom user %q parameters: %v", request.User.Name, err)) | 				errors = append(errors, fmt.Sprintf("could not set custom user %q parameters: %v", request.User.Name, err)) | ||||||
| 			} | 			} | ||||||
|  | 		case spec.PGSyncUserRename: | ||||||
|  | 			if err := strategy.alterPgUserRename(request.User, db); err != nil { | ||||||
|  | 				reqretries = append(reqretries, request) | ||||||
|  | 				errors = append(errors, fmt.Sprintf("could not rename custom user %q: %v", request.User.Name, err)) | ||||||
|  | 			} | ||||||
| 		default: | 		default: | ||||||
| 			return fmt.Errorf("unrecognized operation: %v", request.Kind) | 			return fmt.Errorf("unrecognized operation: %v", request.Kind) | ||||||
| 		} | 		} | ||||||
|  | @ -124,6 +154,23 @@ func (strategy DefaultUserSyncStrategy) alterPgUserSet(user spec.PgUser, db *sql | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (strategy DefaultUserSyncStrategy) alterPgUserRename(user spec.PgUser, db *sql.DB) error { | ||||||
|  | 	var query string | ||||||
|  | 
 | ||||||
|  | 	// append or trim deletion suffix depending if the user has the suffix or not
 | ||||||
|  | 	if user.Deleted { | ||||||
|  | 		newName := strings.TrimSuffix(user.Name, strategy.RoleDeletionSuffix) | ||||||
|  | 		query = fmt.Sprintf(alterUserRenameSQL, user.Name, newName, "") | ||||||
|  | 	} else { | ||||||
|  | 		query = fmt.Sprintf(alterUserRenameSQL, user.Name, user.Name, strategy.RoleDeletionSuffix) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := db.Exec(query); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.DB) error { | func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.DB) error { | ||||||
| 	var userFlags []string | 	var userFlags []string | ||||||
| 	var userPassword string | 	var userPassword string | ||||||
|  |  | ||||||
|  | @ -151,6 +151,18 @@ func IsEqualIgnoreOrder(a, b []string) bool { | ||||||
| 	return reflect.DeepEqual(a_copy, b_copy) | 	return reflect.DeepEqual(a_copy, b_copy) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // SliceReplaceElement
 | ||||||
|  | func StringSliceReplaceElement(s []string, a, b string) (result []string) { | ||||||
|  | 	tmp := make([]string, 0, len(s)) | ||||||
|  | 	for _, str := range s { | ||||||
|  | 		if str == a { | ||||||
|  | 			str = b | ||||||
|  | 		} | ||||||
|  | 		tmp = append(tmp, str) | ||||||
|  | 	} | ||||||
|  | 	return tmp | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // SubstractStringSlices finds elements in a that are not in b and return them as a result slice.
 | // SubstractStringSlices finds elements in a that are not in b and return them as a result slice.
 | ||||||
| func SubstractStringSlices(a []string, b []string) (result []string, equal bool) { | func SubstractStringSlices(a []string, b []string) (result []string, equal bool) { | ||||||
| 	// Slices are assumed to contain unique elements only
 | 	// Slices are assumed to contain unique elements only
 | ||||||
|  |  | ||||||
|  | @ -166,6 +166,14 @@ func TestIsEqualIgnoreOrder(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestStringSliceReplaceElement(t *testing.T) { | ||||||
|  | 	testSlice := []string{"a", "b", "c"} | ||||||
|  | 	testSlice = StringSliceReplaceElement(testSlice, "b", "d") | ||||||
|  | 	if !SliceContains(testSlice, "d") { | ||||||
|  | 		t.Errorf("testSlide item not replaced: %v", testSlice) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestSubstractSlices(t *testing.T) { | func TestSubstractSlices(t *testing.T) { | ||||||
| 	for _, tt := range substractTest { | 	for _, tt := range substractTest { | ||||||
| 		actualRes, actualEqual := SubstractStringSlices(tt.inA, tt.inB) | 		actualRes, actualEqual := SubstractStringSlices(tt.inA, tt.inB) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue