Create cross namespace secrets (#1490)
* Create cross namespace secrets * add test cases * fixes * Fixes - include namespace in secret name only when namespace is provided - use username.namespace as key to pgUsers only when namespace is provided - avoid conflict in the role creation in db by checking namespace alongwith the username * Update unit tests * Fix test case * Fixes - update regular expression for usernames - add test to allow check for valid usernames - create pg roles with namespace (if any) appended in rolename * add more test cases for valid usernames * update docs * fixes as per review comments * update e2e * fixes * Add toggle to allow namespaced secrets * update docs * comment update * Update e2e/tests/test_e2e.py * few minor fixes * fix unit tests * fix e2e * fix e2e attempt 2 * fix e2e Co-authored-by: Rafia Sabih <rafia.sabih@zalando.de> Co-authored-by: Felix Kunde <felix-kunde@gmx.de>
This commit is contained in:
		
							parent
							
								
									9668ac21a3
								
							
						
					
					
						commit
						75a9e2be38
					
				|  | @ -515,6 +515,8 @@ spec: | ||||||
|                       type: integer |                       type: integer | ||||||
|               useLoadBalancer:  # deprecated |               useLoadBalancer:  # deprecated | ||||||
|                 type: boolean |                 type: boolean | ||||||
|  |               enableNamespacedSecret: | ||||||
|  |                 type: boolean | ||||||
|               users: |               users: | ||||||
|                 type: object |                 type: object | ||||||
|                 additionalProperties: |                 additionalProperties: | ||||||
|  |  | ||||||
|  | @ -148,7 +148,10 @@ configKubernetes: | ||||||
| 
 | 
 | ||||||
|   # Postgres pods are terminated forcefully after this timeout |   # Postgres pods are terminated forcefully after this timeout | ||||||
|   pod_terminate_grace_period: 5m |   pod_terminate_grace_period: 5m | ||||||
|   # template for database user secrets generated by the operator |   # template for database user secrets generated by the operator, | ||||||
|  |   # here username contains the namespace in the format namespace.username | ||||||
|  |   # if the user is in different namespace than cluster and cross namespace secrets | ||||||
|  |   # are enabled via EnableNamespacedSecret flag. | ||||||
|   secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" |   secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" | ||||||
|   # set user and group for the spilo container (required to run Spilo as non-root process) |   # set user and group for the spilo container (required to run Spilo as non-root process) | ||||||
|   # spilo_runasuser: 101 |   # spilo_runasuser: 101 | ||||||
|  |  | ||||||
|  | @ -275,11 +275,14 @@ configuration they are grouped under the `kubernetes` key. | ||||||
| 
 | 
 | ||||||
| * **secret_name_template** | * **secret_name_template** | ||||||
|   a template for the name of the database user secrets generated by the |   a template for the name of the database user secrets generated by the | ||||||
|   operator. `{username}` is replaced with name of the secret, `{cluster}` with |   operator. `{namespace}` is replaced with name of the namespace (if cross | ||||||
|   the name of the cluster, `{tprkind}` with the kind of CRD (formerly known as |   namespace secrets are enabled via EnableNamespacedSecret flag, otherwise the | ||||||
|   TPR) and `{tprgroup}` with the group of the CRD. No other placeholders are |   secret is in cluster's namespace and in that case it is not present in secret | ||||||
|   allowed. The default is |   name), `{username}` is replaced with name of the secret, `{cluster}` with the | ||||||
|   `{username}.{cluster}.credentials.{tprkind}.{tprgroup}`. |   name of the cluster, `{tprkind}` with the kind of CRD (formerly known as TPR) | ||||||
|  |   and `{tprgroup}` with the group of the CRD. No other placeholders are allowed. | ||||||
|  |   The default is | ||||||
|  |   `{namespace}.{username}.{cluster}.credentials.{tprkind}.{tprgroup}`. | ||||||
| 
 | 
 | ||||||
| * **cluster_domain** | * **cluster_domain** | ||||||
|   defines the default DNS domain for the kubernetes cluster the operator is |   defines the default DNS domain for the kubernetes cluster the operator is | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								docs/user.md
								
								
								
								
							
							
						
						
									
										19
									
								
								docs/user.md
								
								
								
								
							|  | @ -139,6 +139,25 @@ secret, without ever sharing it outside of the cluster. | ||||||
| At the moment it is not possible to define membership of the manifest role in | At the moment it is not possible to define membership of the manifest role in | ||||||
| other roles. | other roles. | ||||||
| 
 | 
 | ||||||
|  | To define the secrets for the users in a different namespace than that of the cluster, | ||||||
|  | one can use the flag `EnableNamespacedSecret` and declare the namespace for the | ||||||
|  | secrets in the manifest in the following manner, | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | spec: | ||||||
|  |   users: | ||||||
|  |   #users with secret in dfferent namespace | ||||||
|  |    appspace.db_user: | ||||||
|  |     - createdb | ||||||
|  | ``` | ||||||
|  | Here, anything before the first dot is taken as the namespace and the text after | ||||||
|  | the first dot is the username. Also, the postgres roles of these usernames would | ||||||
|  | be in the form of `namespace.username`. | ||||||
|  | 
 | ||||||
|  | For such usernames, the secret is created in the given namespace and its name is | ||||||
|  | of the following form, | ||||||
|  | `{namespace}.{username}.{team}-{clustername}.credentials.postgresql.acid.zalan.do` | ||||||
|  | 
 | ||||||
| ### Infrastructure roles | ### Infrastructure roles | ||||||
| 
 | 
 | ||||||
| An infrastructure role is a role that should be present on every PostgreSQL | An infrastructure role is a role that should be present on every PostgreSQL | ||||||
|  |  | ||||||
|  | @ -197,6 +197,16 @@ class K8s: | ||||||
|                 pod_phase = pods[0].status.phase |                 pod_phase = pods[0].status.phase | ||||||
|             time.sleep(self.RETRY_TIMEOUT_SEC) |             time.sleep(self.RETRY_TIMEOUT_SEC) | ||||||
| 
 | 
 | ||||||
|  |     def wait_for_namespace_creation(self, namespace='default'): | ||||||
|  |         ns_found = False | ||||||
|  |         while ns_found != True: | ||||||
|  |             ns = self.api.core_v1.list_namespace().items | ||||||
|  |             for n in ns: | ||||||
|  |                 if n.metadata.name == namespace: | ||||||
|  |                     ns_found = True | ||||||
|  |                     break | ||||||
|  |             time.sleep(self.RETRY_TIMEOUT_SEC) | ||||||
|  | 
 | ||||||
|     def get_logical_backup_job(self, namespace='default'): |     def get_logical_backup_job(self, namespace='default'): | ||||||
|         return self.api.batch_v1_beta1.list_namespaced_cron_job(namespace, label_selector="application=spilo") |         return self.api.batch_v1_beta1.list_namespaced_cron_job(namespace, label_selector="application=spilo") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -322,7 +322,6 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|         self.eventuallyEqual(lambda: self.k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), |         self.eventuallyEqual(lambda: self.k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"), | ||||||
|                              0, "Pooler pods not scaled down") |                              0, "Pooler pods not scaled down") | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     @timeout_decorator.timeout(TEST_TIMEOUT_SEC) |     @timeout_decorator.timeout(TEST_TIMEOUT_SEC) | ||||||
|     def test_enable_disable_connection_pooler(self): |     def test_enable_disable_connection_pooler(self): | ||||||
|         ''' |         ''' | ||||||
|  | @ -568,6 +567,7 @@ class EndToEndTestCase(unittest.TestCase): | ||||||
|                         role.pop("Password", None) |                         role.pop("Password", None) | ||||||
|                         self.assertDictEqual(role, { |                         self.assertDictEqual(role, { | ||||||
|                             "Name": "robot_zmon_acid_monitoring_new", |                             "Name": "robot_zmon_acid_monitoring_new", | ||||||
|  |                             "Namespace":"", | ||||||
|                             "Flags": None, |                             "Flags": None, | ||||||
|                             "MemberOf": ["robot_zmon"], |                             "MemberOf": ["robot_zmon"], | ||||||
|                             "Parameters": None, |                             "Parameters": None, | ||||||
|  | @ -587,6 +587,41 @@ 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_zz_cross_namespace_secrets(self): | ||||||
|  |         ''' | ||||||
|  |             Test secrets in different namespace | ||||||
|  |         ''' | ||||||
|  |         app_namespace = "appspace" | ||||||
|  | 
 | ||||||
|  |         v1_appnamespace = client.V1Namespace(metadata=client.V1ObjectMeta(name=app_namespace)) | ||||||
|  |         self.k8s.api.core_v1.create_namespace(v1_appnamespace) | ||||||
|  |         self.k8s.wait_for_namespace_creation(app_namespace) | ||||||
|  | 
 | ||||||
|  |         self.k8s.api.custom_objects_api.patch_namespaced_custom_object( | ||||||
|  |             'acid.zalan.do', 'v1', 'default', | ||||||
|  |             'postgresqls', 'acid-minimal-cluster', | ||||||
|  |             { | ||||||
|  |                 'spec': { | ||||||
|  |                     'enableNamespacedSecret': True, | ||||||
|  |                     'users':{ | ||||||
|  |                         'appspace.db_user': [], | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         self.eventuallyEqual(lambda: self.k8s.count_secrets_with_label("cluster-name=acid-minimal-cluster,application=spilo", app_namespace), | ||||||
|  |                              1, "Secret not created for user in namespace") | ||||||
|  | 
 | ||||||
|  |         #reset the flag | ||||||
|  |         self.k8s.api.custom_objects_api.patch_namespaced_custom_object( | ||||||
|  |             'acid.zalan.do', 'v1', 'default', | ||||||
|  |             'postgresqls', 'acid-minimal-cluster', | ||||||
|  |             { | ||||||
|  |                 'spec': { | ||||||
|  |                     'enableNamespacedSecret': False, | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|     @timeout_decorator.timeout(TEST_TIMEOUT_SEC) |     @timeout_decorator.timeout(TEST_TIMEOUT_SEC) | ||||||
|     def test_lazy_spilo_upgrade(self): |     def test_lazy_spilo_upgrade(self): | ||||||
|         ''' |         ''' | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ spec: | ||||||
|   dockerImage: registry.opensource.zalan.do/acid/spilo-13:2.0-p7 |   dockerImage: registry.opensource.zalan.do/acid/spilo-13:2.0-p7 | ||||||
|   teamId: "acid" |   teamId: "acid" | ||||||
|   numberOfInstances: 2 |   numberOfInstances: 2 | ||||||
|  |   enableNamespacedSecret: False | ||||||
|   users:  # Application/Robot users |   users:  # Application/Robot users | ||||||
|     zalando: |     zalando: | ||||||
|     - superuser |     - superuser | ||||||
|  |  | ||||||
|  | @ -730,6 +730,9 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ | ||||||
| 						Type:        "boolean", | 						Type:        "boolean", | ||||||
| 						Description: "Deprecated", | 						Description: "Deprecated", | ||||||
| 					}, | 					}, | ||||||
|  | 					"enableNamespacedSecret": { | ||||||
|  | 						Type: "boolean", | ||||||
|  | 					}, | ||||||
| 					"users": { | 					"users": { | ||||||
| 						Type: "object", | 						Type: "object", | ||||||
| 						AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ | 						AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{ | ||||||
|  |  | ||||||
|  | @ -53,27 +53,28 @@ type PostgresSpec struct { | ||||||
| 	// load balancers' source ranges are the same for master and replica services
 | 	// load balancers' source ranges are the same for master and replica services
 | ||||||
| 	AllowedSourceRanges []string `json:"allowedSourceRanges"` | 	AllowedSourceRanges []string `json:"allowedSourceRanges"` | ||||||
| 
 | 
 | ||||||
| 	NumberOfInstances     int32                       `json:"numberOfInstances"` | 	NumberOfInstances      int32                       `json:"numberOfInstances"` | ||||||
| 	Users                 map[string]UserFlags        `json:"users,omitempty"` | 	EnableNamespacedSecret *bool                       `json:"enableNamespacedSecret,omitempty"` | ||||||
| 	MaintenanceWindows    []MaintenanceWindow         `json:"maintenanceWindows,omitempty"` | 	Users                  map[string]UserFlags        `json:"users,omitempty"` | ||||||
| 	Clone                 *CloneDescription           `json:"clone,omitempty"` | 	MaintenanceWindows     []MaintenanceWindow         `json:"maintenanceWindows,omitempty"` | ||||||
| 	ClusterName           string                      `json:"-"` | 	Clone                  *CloneDescription           `json:"clone,omitempty"` | ||||||
| 	Databases             map[string]string           `json:"databases,omitempty"` | 	ClusterName            string                      `json:"-"` | ||||||
| 	PreparedDatabases     map[string]PreparedDatabase `json:"preparedDatabases,omitempty"` | 	Databases              map[string]string           `json:"databases,omitempty"` | ||||||
| 	SchedulerName         *string                     `json:"schedulerName,omitempty"` | 	PreparedDatabases      map[string]PreparedDatabase `json:"preparedDatabases,omitempty"` | ||||||
| 	NodeAffinity          *v1.NodeAffinity            `json:"nodeAffinity,omitempty"` | 	SchedulerName          *string                     `json:"schedulerName,omitempty"` | ||||||
| 	Tolerations           []v1.Toleration             `json:"tolerations,omitempty"` | 	NodeAffinity           *v1.NodeAffinity            `json:"nodeAffinity,omitempty"` | ||||||
| 	Sidecars              []Sidecar                   `json:"sidecars,omitempty"` | 	Tolerations            []v1.Toleration             `json:"tolerations,omitempty"` | ||||||
| 	InitContainers        []v1.Container              `json:"initContainers,omitempty"` | 	Sidecars               []Sidecar                   `json:"sidecars,omitempty"` | ||||||
| 	PodPriorityClassName  string                      `json:"podPriorityClassName,omitempty"` | 	InitContainers         []v1.Container              `json:"initContainers,omitempty"` | ||||||
| 	ShmVolume             *bool                       `json:"enableShmVolume,omitempty"` | 	PodPriorityClassName   string                      `json:"podPriorityClassName,omitempty"` | ||||||
| 	EnableLogicalBackup   bool                        `json:"enableLogicalBackup,omitempty"` | 	ShmVolume              *bool                       `json:"enableShmVolume,omitempty"` | ||||||
| 	LogicalBackupSchedule string                      `json:"logicalBackupSchedule,omitempty"` | 	EnableLogicalBackup    bool                        `json:"enableLogicalBackup,omitempty"` | ||||||
| 	StandbyCluster        *StandbyDescription         `json:"standby,omitempty"` | 	LogicalBackupSchedule  string                      `json:"logicalBackupSchedule,omitempty"` | ||||||
| 	PodAnnotations        map[string]string           `json:"podAnnotations,omitempty"` | 	StandbyCluster         *StandbyDescription         `json:"standby,omitempty"` | ||||||
| 	ServiceAnnotations    map[string]string           `json:"serviceAnnotations,omitempty"` | 	PodAnnotations         map[string]string           `json:"podAnnotations,omitempty"` | ||||||
| 	TLS                   *TLSDescription             `json:"tls,omitempty"` | 	ServiceAnnotations     map[string]string           `json:"serviceAnnotations,omitempty"` | ||||||
| 	AdditionalVolumes     []AdditionalVolume          `json:"additionalVolumes,omitempty"` | 	TLS                    *TLSDescription             `json:"tls,omitempty"` | ||||||
|  | 	AdditionalVolumes      []AdditionalVolume          `json:"additionalVolumes,omitempty"` | ||||||
| 
 | 
 | ||||||
| 	// deprecated json tags
 | 	// deprecated json tags
 | ||||||
| 	InitContainersOld       []v1.Container `json:"init_containers,omitempty"` | 	InitContainersOld       []v1.Container `json:"init_containers,omitempty"` | ||||||
|  |  | ||||||
|  | @ -614,6 +614,11 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { | ||||||
| 		*out = make([]string, len(*in)) | 		*out = make([]string, len(*in)) | ||||||
| 		copy(*out, *in) | 		copy(*out, *in) | ||||||
| 	} | 	} | ||||||
|  | 	if in.EnableNamespacedSecret != nil { | ||||||
|  | 		in, out := &in.EnableNamespacedSecret, &out.EnableNamespacedSecret | ||||||
|  | 		*out = new(bool) | ||||||
|  | 		**out = **in | ||||||
|  | 	} | ||||||
| 	if in.Users != nil { | 	if in.Users != nil { | ||||||
| 		in, out := &in.Users, &out.Users | 		in, out := &in.Users, &out.Users | ||||||
| 		*out = make(map[string]UserFlags, len(*in)) | 		*out = make(map[string]UserFlags, len(*in)) | ||||||
|  |  | ||||||
|  | @ -940,14 +940,16 @@ func (c *Cluster) initSystemUsers() { | ||||||
| 	// secrets, therefore, setting flags like SUPERUSER or REPLICATION
 | 	// secrets, therefore, setting flags like SUPERUSER or REPLICATION
 | ||||||
| 	// is not necessary here
 | 	// is not necessary here
 | ||||||
| 	c.systemUsers[constants.SuperuserKeyName] = spec.PgUser{ | 	c.systemUsers[constants.SuperuserKeyName] = spec.PgUser{ | ||||||
| 		Origin:   spec.RoleOriginSystem, | 		Origin:    spec.RoleOriginSystem, | ||||||
| 		Name:     c.OpConfig.SuperUsername, | 		Name:      c.OpConfig.SuperUsername, | ||||||
| 		Password: util.RandomPassword(constants.PasswordLength), | 		Namespace: c.Namespace, | ||||||
|  | 		Password:  util.RandomPassword(constants.PasswordLength), | ||||||
| 	} | 	} | ||||||
| 	c.systemUsers[constants.ReplicationUserKeyName] = spec.PgUser{ | 	c.systemUsers[constants.ReplicationUserKeyName] = spec.PgUser{ | ||||||
| 		Origin:   spec.RoleOriginSystem, | 		Origin:    spec.RoleOriginSystem, | ||||||
| 		Name:     c.OpConfig.ReplicationUsername, | 		Name:      c.OpConfig.ReplicationUsername, | ||||||
| 		Password: util.RandomPassword(constants.PasswordLength), | 		Namespace: c.Namespace, | ||||||
|  | 		Password:  util.RandomPassword(constants.PasswordLength), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Connection pooler user is an exception, if requested it's going to be
 | 	// Connection pooler user is an exception, if requested it's going to be
 | ||||||
|  | @ -975,10 +977,11 @@ func (c *Cluster) initSystemUsers() { | ||||||
| 
 | 
 | ||||||
| 		// connection pooler application should be able to login with this role
 | 		// connection pooler application should be able to login with this role
 | ||||||
| 		connectionPoolerUser := spec.PgUser{ | 		connectionPoolerUser := spec.PgUser{ | ||||||
| 			Origin:   spec.RoleConnectionPooler, | 			Origin:    spec.RoleConnectionPooler, | ||||||
| 			Name:     username, | 			Name:      username, | ||||||
| 			Flags:    []string{constants.RoleFlagLogin}, | 			Namespace: c.Namespace, | ||||||
| 			Password: util.RandomPassword(constants.PasswordLength), | 			Flags:     []string{constants.RoleFlagLogin}, | ||||||
|  | 			Password:  util.RandomPassword(constants.PasswordLength), | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if _, exists := c.pgUsers[username]; !exists { | 		if _, exists := c.pgUsers[username]; !exists { | ||||||
|  | @ -1081,6 +1084,7 @@ func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix | ||||||
| 		newRole := spec.PgUser{ | 		newRole := spec.PgUser{ | ||||||
| 			Origin:     spec.RoleOriginBootstrap, | 			Origin:     spec.RoleOriginBootstrap, | ||||||
| 			Name:       roleName, | 			Name:       roleName, | ||||||
|  | 			Namespace:  c.Namespace, | ||||||
| 			Password:   util.RandomPassword(constants.PasswordLength), | 			Password:   util.RandomPassword(constants.PasswordLength), | ||||||
| 			Flags:      flags, | 			Flags:      flags, | ||||||
| 			MemberOf:   memberOf, | 			MemberOf:   memberOf, | ||||||
|  | @ -1105,6 +1109,17 @@ func (c *Cluster) initRobotUsers() error { | ||||||
| 		if c.shouldAvoidProtectedOrSystemRole(username, "manifest robot role") { | 		if c.shouldAvoidProtectedOrSystemRole(username, "manifest robot role") { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | 		namespace := c.Namespace | ||||||
|  | 
 | ||||||
|  | 		//if namespaced secrets are allowed
 | ||||||
|  | 		if c.Postgresql.Spec.EnableNamespacedSecret != nil && | ||||||
|  | 			*c.Postgresql.Spec.EnableNamespacedSecret { | ||||||
|  | 			if strings.Contains(username, ".") { | ||||||
|  | 				splits := strings.Split(username, ".") | ||||||
|  | 				namespace = splits[0] | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		flags, err := normalizeUserFlags(userFlags) | 		flags, err := normalizeUserFlags(userFlags) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("invalid flags for user %q: %v", username, err) | 			return fmt.Errorf("invalid flags for user %q: %v", username, err) | ||||||
|  | @ -1116,6 +1131,7 @@ func (c *Cluster) initRobotUsers() error { | ||||||
| 		newRole := spec.PgUser{ | 		newRole := spec.PgUser{ | ||||||
| 			Origin:    spec.RoleOriginManifest, | 			Origin:    spec.RoleOriginManifest, | ||||||
| 			Name:      username, | 			Name:      username, | ||||||
|  | 			Namespace: namespace, | ||||||
| 			Password:  util.RandomPassword(constants.PasswordLength), | 			Password:  util.RandomPassword(constants.PasswordLength), | ||||||
| 			Flags:     flags, | 			Flags:     flags, | ||||||
| 			AdminRole: adminRole, | 			AdminRole: adminRole, | ||||||
|  | @ -1233,6 +1249,7 @@ func (c *Cluster) initInfrastructureRoles() error { | ||||||
| 			return fmt.Errorf("invalid flags for user '%v': %v", username, err) | 			return fmt.Errorf("invalid flags for user '%v': %v", username, err) | ||||||
| 		} | 		} | ||||||
| 		newRole.Flags = flags | 		newRole.Flags = flags | ||||||
|  | 		newRole.Namespace = c.Namespace | ||||||
| 
 | 
 | ||||||
| 		if currentRole, present := c.pgUsers[username]; present { | 		if currentRole, present := c.pgUsers[username]; present { | ||||||
| 			c.pgUsers[username] = c.resolveNameConflict(¤tRole, &newRole) | 			c.pgUsers[username] = c.resolveNameConflict(¤tRole, &newRole) | ||||||
|  |  | ||||||
|  | @ -7,12 +7,14 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" | 	acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" | ||||||
|  | 	fakeacidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/fake" | ||||||
| 	"github.com/zalando/postgres-operator/pkg/spec" | 	"github.com/zalando/postgres-operator/pkg/spec" | ||||||
| 	"github.com/zalando/postgres-operator/pkg/util/config" | 	"github.com/zalando/postgres-operator/pkg/util/config" | ||||||
| 	"github.com/zalando/postgres-operator/pkg/util/constants" | 	"github.com/zalando/postgres-operator/pkg/util/constants" | ||||||
| 	"github.com/zalando/postgres-operator/pkg/util/k8sutil" | 	"github.com/zalando/postgres-operator/pkg/util/k8sutil" | ||||||
| 	"github.com/zalando/postgres-operator/pkg/util/teams" | 	"github.com/zalando/postgres-operator/pkg/util/teams" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/client-go/kubernetes/fake" | ||||||
| 	"k8s.io/client-go/tools/record" | 	"k8s.io/client-go/tools/record" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -79,8 +81,8 @@ func TestInitRobotUsers(t *testing.T) { | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			manifestUsers: map[string]acidv1.UserFlags{"foo": {"superuser", "createdb"}}, | 			manifestUsers: map[string]acidv1.UserFlags{"foo": {"superuser", "createdb"}}, | ||||||
| 			infraRoles:    map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}}, | 			infraRoles:    map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Namespace: cl.Namespace, Password: "bar"}}, | ||||||
| 			result:        map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Password: "bar"}}, | 			result:        map[string]spec.PgUser{"foo": {Origin: spec.RoleOriginInfrastructure, Name: "foo", Namespace: cl.Namespace, Password: "bar"}}, | ||||||
| 			err:           nil, | 			err:           nil, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -845,3 +847,90 @@ func TestPreparedDatabases(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestCrossNamespacedSecrets(t *testing.T) { | ||||||
|  | 	testName := "test secrets in different namespace" | ||||||
|  | 	clientSet := fake.NewSimpleClientset() | ||||||
|  | 	acidClientSet := fakeacidv1.NewSimpleClientset() | ||||||
|  | 	namespace := "default" | ||||||
|  | 
 | ||||||
|  | 	client := k8sutil.KubernetesClient{ | ||||||
|  | 		StatefulSetsGetter: clientSet.AppsV1(), | ||||||
|  | 		ServicesGetter:     clientSet.CoreV1(), | ||||||
|  | 		DeploymentsGetter:  clientSet.AppsV1(), | ||||||
|  | 		PostgresqlsGetter:  acidClientSet.AcidV1(), | ||||||
|  | 		SecretsGetter:      clientSet.CoreV1(), | ||||||
|  | 	} | ||||||
|  | 	pg := acidv1.Postgresql{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:      "acid-fake-cluster", | ||||||
|  | 			Namespace: namespace, | ||||||
|  | 		}, | ||||||
|  | 		Spec: acidv1.PostgresSpec{ | ||||||
|  | 			Volume: acidv1.Volume{ | ||||||
|  | 				Size: "1Gi", | ||||||
|  | 			}, | ||||||
|  | 			EnableNamespacedSecret: boolToPointer(true), | ||||||
|  | 			Users: map[string]acidv1.UserFlags{ | ||||||
|  | 				"appspace.db_user": {}, | ||||||
|  | 				"db_user":          {}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var cluster = New( | ||||||
|  | 		Config{ | ||||||
|  | 			OpConfig: config.Config{ | ||||||
|  | 				ConnectionPooler: config.ConnectionPooler{ | ||||||
|  | 					ConnectionPoolerDefaultCPURequest:    "100m", | ||||||
|  | 					ConnectionPoolerDefaultCPULimit:      "100m", | ||||||
|  | 					ConnectionPoolerDefaultMemoryRequest: "100Mi", | ||||||
|  | 					ConnectionPoolerDefaultMemoryLimit:   "100Mi", | ||||||
|  | 					NumberOfInstances:                    int32ToPointer(1), | ||||||
|  | 				}, | ||||||
|  | 				PodManagementPolicy: "ordered_ready", | ||||||
|  | 				Resources: config.Resources{ | ||||||
|  | 					ClusterLabels:        map[string]string{"application": "spilo"}, | ||||||
|  | 					ClusterNameLabel:     "cluster-name", | ||||||
|  | 					DefaultCPURequest:    "300m", | ||||||
|  | 					DefaultCPULimit:      "300m", | ||||||
|  | 					DefaultMemoryRequest: "300Mi", | ||||||
|  | 					DefaultMemoryLimit:   "300Mi", | ||||||
|  | 					PodRoleLabel:         "spilo-role", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, client, pg, logger, eventRecorder) | ||||||
|  | 
 | ||||||
|  | 	userNamespaceMap := map[string]string{ | ||||||
|  | 		cluster.Namespace: "db_user", | ||||||
|  | 		"appspace":        "appspace.db_user", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := cluster.initRobotUsers() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Could not create secret for namespaced users with error: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, u := range cluster.pgUsers { | ||||||
|  | 		if u.Name != userNamespaceMap[u.Namespace] { | ||||||
|  | 			t.Errorf("%s: Could not create namespaced user in its correct namespaces for user %s in namespace %s", testName, u.Name, u.Namespace) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestValidUsernames(t *testing.T) { | ||||||
|  | 	testName := "test username validity" | ||||||
|  | 
 | ||||||
|  | 	invalidUsernames := []string{"_", ".", ".user", "appspace.", "user_", "_user", "-user", "user-", ",", "-", ",user", "user,", "namespace,user"} | ||||||
|  | 	validUsernames := []string{"user", "appspace.user", "appspace.dot.user", "user_name", "app_space.user_name"} | ||||||
|  | 	for _, username := range invalidUsernames { | ||||||
|  | 		if isValidUsername(username) { | ||||||
|  | 			t.Errorf("%s Invalid username is allowed: %s", testName, username) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for _, username := range validUsernames { | ||||||
|  | 		if !isValidUsername(username) { | ||||||
|  | 			t.Errorf("%s Valid username is not allowed: %s", testName, username) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1547,10 +1547,11 @@ func (c *Cluster) generateUserSecrets() map[string]*v1.Secret { | ||||||
| 	namespace := c.Namespace | 	namespace := c.Namespace | ||||||
| 	for username, pgUser := range c.pgUsers { | 	for username, pgUser := range c.pgUsers { | ||||||
| 		//Skip users with no password i.e. human users (they'll be authenticated using pam)
 | 		//Skip users with no password i.e. human users (they'll be authenticated using pam)
 | ||||||
| 		secret := c.generateSingleUserSecret(namespace, pgUser) | 		secret := c.generateSingleUserSecret(pgUser.Namespace, pgUser) | ||||||
| 		if secret != nil { | 		if secret != nil { | ||||||
| 			secrets[username] = secret | 			secrets[username] = secret | ||||||
| 		} | 		} | ||||||
|  | 		namespace = pgUser.Namespace | ||||||
| 	} | 	} | ||||||
| 	/* special case for the system user */ | 	/* special case for the system user */ | ||||||
| 	for _, systemUser := range c.systemUsers { | 	for _, systemUser := range c.systemUsers { | ||||||
|  | @ -1590,7 +1591,7 @@ func (c *Cluster) generateSingleUserSecret(namespace string, pgUser spec.PgUser) | ||||||
| 	secret := v1.Secret{ | 	secret := v1.Secret{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name:        c.credentialSecretName(username), | 			Name:        c.credentialSecretName(username), | ||||||
| 			Namespace:   namespace, | 			Namespace:   pgUser.Namespace, | ||||||
| 			Labels:      lbls, | 			Labels:      lbls, | ||||||
| 			Annotations: c.annotationsSet(nil), | 			Annotations: c.annotationsSet(nil), | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ func (c *Cluster) listResources() error { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, obj := range c.Secrets { | 	for _, obj := range c.Secrets { | ||||||
| 		c.logger.Infof("found secret: %q (uid: %q)", util.NameFromMeta(obj.ObjectMeta), obj.UID) | 		c.logger.Infof("found secret: %q (uid: %q) namesapce: %s", util.NameFromMeta(obj.ObjectMeta), obj.UID, obj.ObjectMeta.Namespace) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for role, endpoint := range c.Endpoints { | 	for role, endpoint := range c.Endpoints { | ||||||
|  |  | ||||||
|  | @ -483,7 +483,7 @@ func (c *Cluster) syncSecrets() error { | ||||||
| 	for secretUsername, secretSpec := range secrets { | 	for secretUsername, secretSpec := range secrets { | ||||||
| 		if secret, err = c.KubeClient.Secrets(secretSpec.Namespace).Create(context.TODO(), secretSpec, metav1.CreateOptions{}); err == nil { | 		if secret, err = c.KubeClient.Secrets(secretSpec.Namespace).Create(context.TODO(), secretSpec, metav1.CreateOptions{}); err == nil { | ||||||
| 			c.Secrets[secret.UID] = secret | 			c.Secrets[secret.UID] = secret | ||||||
| 			c.logger.Debugf("created new secret %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), secret.UID) | 			c.logger.Debugf("created new secret %s, namespace: %s, uid: %s", util.NameFromMeta(secret.ObjectMeta), secretSpec.Namespace, secret.UID) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		if k8sutil.ResourceAlreadyExists(err) { | 		if k8sutil.ResourceAlreadyExists(err) { | ||||||
|  | @ -521,7 +521,7 @@ func (c *Cluster) syncSecrets() error { | ||||||
| 				userMap[secretUsername] = pwdUser | 				userMap[secretUsername] = pwdUser | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			return fmt.Errorf("could not create secret for user %s: %v", secretUsername, err) | 			return fmt.Errorf("could not create secret for user %s: in namespace %s: %v", secretUsername, secretSpec.Namespace, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -556,11 +556,17 @@ func (c *Cluster) syncRoles() (err error) { | ||||||
| 
 | 
 | ||||||
| 	// create list of database roles to query
 | 	// create list of database roles to query
 | ||||||
| 	for _, u := range c.pgUsers { | 	for _, u := range c.pgUsers { | ||||||
| 		userNames = append(userNames, u.Name) | 		pgRole := u.Name | ||||||
|  | 		if u.Namespace != c.Namespace && u.Namespace != "" { | ||||||
|  | 			// to avoid the conflict of having multiple users of same name
 | ||||||
|  | 			// but each in different namespace.
 | ||||||
|  | 			pgRole = fmt.Sprintf("%s.%s", u.Name, u.Namespace) | ||||||
|  | 		} | ||||||
|  | 		userNames = append(userNames, pgRole) | ||||||
| 		// add team member role name with rename suffix in case we need to rename it back
 | 		// add team member role name with rename suffix in case we need to rename it back
 | ||||||
| 		if u.Origin == spec.RoleOriginTeamsAPI && c.OpConfig.EnableTeamMemberDeprecation { | 		if u.Origin == spec.RoleOriginTeamsAPI && c.OpConfig.EnableTeamMemberDeprecation { | ||||||
| 			deletedUsers[u.Name+c.OpConfig.RoleDeletionSuffix] = u.Name | 			deletedUsers[pgRole+c.OpConfig.RoleDeletionSuffix] = pgRole | ||||||
| 			userNames = append(userNames, u.Name+c.OpConfig.RoleDeletionSuffix) | 			userNames = append(userNames, pgRole+c.OpConfig.RoleDeletionSuffix) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -49,6 +49,7 @@ const ( | ||||||
| type PgUser struct { | type PgUser struct { | ||||||
| 	Origin     RoleOrigin        `yaml:"-"` | 	Origin     RoleOrigin        `yaml:"-"` | ||||||
| 	Name       string            `yaml:"-"` | 	Name       string            `yaml:"-"` | ||||||
|  | 	Namespace  string            `yaml:"-"` | ||||||
| 	Password   string            `yaml:"-"` | 	Password   string            `yaml:"-"` | ||||||
| 	Flags      []string          `yaml:"user_flags"` | 	Flags      []string          `yaml:"user_flags"` | ||||||
| 	MemberOf   []string          `yaml:"inrole"` | 	MemberOf   []string          `yaml:"inrole"` | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue