Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions charts/postgres-operator/crds/postgresqls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,8 @@ spec:
type: integer
useLoadBalancer: # deprecated
type: boolean
enableNamespacedSecret:
type: boolean
users:
type: object
additionalProperties:
Expand Down
Empty file.
5 changes: 4 additions & 1 deletion charts/postgres-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ configKubernetes:

# Postgres pods are terminated forcefully after this timeout
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}"
# set user and group for the spilo container (required to run Spilo as non-root process)
# spilo_runasuser: 101
Expand Down
17 changes: 10 additions & 7 deletions docs/reference/operator_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ under the `users` key.

## Major version upgrades

Parameters configuring automatic major version upgrades. In a
Parameters configuring automatic major version upgrades. In a
CRD-configuration, they are grouped under the `major_version_upgrade` key.

* **major_version_upgrade_mode**
Postgres Operator supports [in-place major version upgrade](../administrator.md#in-place-major-version-upgrade)
Postgres Operator supports [in-place major version upgrade](../administrator.md#in-place-major-version-upgrade)
with three different modes:
`"off"` = no upgrade by the operator,
`"manual"` = manifest triggers action,
Expand Down Expand Up @@ -275,11 +275,14 @@ configuration they are grouped under the `kubernetes` key.

* **secret_name_template**
a template for the name of the database user secrets generated by the
operator. `{username}` is replaced with name of the secret, `{cluster}` with
the 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
`{username}.{cluster}.credentials.{tprkind}.{tprgroup}`.
operator. `{namespace}` is replaced with name of the namespace (if cross
namespace secrets are enabled via EnableNamespacedSecret flag, otherwise the
secret is in cluster's namespace and in that case it is not present in secret
name), `{username}` is replaced with name of the secret, `{cluster}` with the
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**
defines the default DNS domain for the kubernetes cluster the operator is
Expand Down
21 changes: 20 additions & 1 deletion docs/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

An infrastructure role is a role that should be present on every PostgreSQL
Expand Down Expand Up @@ -330,7 +349,7 @@ spec:

This creates roles for members of the `c-team` team not only in all clusters
owned by `a-team`, but as well in cluster owned by `b-team`, as `a-team` is
an `additionalTeam` to `b-team`
an `additionalTeam` to `b-team`

Not, you can also define `additionalSuperuserTeams` in the `PostgresTeam`
manifest. By default, this option is disabled and must be configured with
Expand Down
10 changes: 10 additions & 0 deletions e2e/tests/k8s_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@ def wait_for_pod_failover(self, failover_targets, labels, namespace='default'):
pod_phase = pods[0].status.phase
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'):
return self.api.batch_v1_beta1.list_namespaced_cron_job(namespace, label_selector="application=spilo")

Expand Down
41 changes: 38 additions & 3 deletions e2e/tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def test_additional_teams_and_members(self):
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,
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
Expand All @@ -276,7 +276,7 @@ def test_additional_teams_and_members(self):
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,
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
Expand Down Expand Up @@ -322,7 +322,6 @@ def test_overwrite_pooler_deployment(self):
self.eventuallyEqual(lambda: self.k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"),
0, "Pooler pods not scaled down")


@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_enable_disable_connection_pooler(self):
'''
Expand Down Expand Up @@ -568,6 +567,7 @@ def verify_role():
role.pop("Password", None)
self.assertDictEqual(role, {
"Name": "robot_zmon_acid_monitoring_new",
"Namespace":"",
"Flags": None,
"MemberOf": ["robot_zmon"],
"Parameters": None,
Expand All @@ -587,6 +587,41 @@ def verify_role():
print('Operator log: {}'.format(k8s.get_operator_log()))
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)
def test_lazy_spilo_upgrade(self):
'''
Expand Down
1 change: 1 addition & 0 deletions manifests/complete-postgres-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ spec:
dockerImage: registry.opensource.zalan.do/acid/spilo-13:2.0-p7
teamId: "acid"
numberOfInstances: 2
enableNamespacedSecret: False
users: # Application/Robot users
zalando:
- superuser
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/acid.zalan.do/v1/crds.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,9 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
Type: "boolean",
Description: "Deprecated",
},
"enableNamespacedSecret": {
Type: "boolean",
},
"users": {
Type: "object",
AdditionalProperties: &apiextv1.JSONSchemaPropsOrBool{
Expand Down
43 changes: 22 additions & 21 deletions pkg/apis/acid.zalan.do/v1/postgresql_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,28 @@ type PostgresSpec struct {
// load balancers' source ranges are the same for master and replica services
AllowedSourceRanges []string `json:"allowedSourceRanges"`

NumberOfInstances int32 `json:"numberOfInstances"`
Users map[string]UserFlags `json:"users,omitempty"`
MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"`
Clone *CloneDescription `json:"clone,omitempty"`
ClusterName string `json:"-"`
Databases map[string]string `json:"databases,omitempty"`
PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"`
SchedulerName *string `json:"schedulerName,omitempty"`
NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"`
Tolerations []v1.Toleration `json:"tolerations,omitempty"`
Sidecars []Sidecar `json:"sidecars,omitempty"`
InitContainers []v1.Container `json:"initContainers,omitempty"`
PodPriorityClassName string `json:"podPriorityClassName,omitempty"`
ShmVolume *bool `json:"enableShmVolume,omitempty"`
EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"`
LogicalBackupSchedule string `json:"logicalBackupSchedule,omitempty"`
StandbyCluster *StandbyDescription `json:"standby,omitempty"`
PodAnnotations map[string]string `json:"podAnnotations,omitempty"`
ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"`
TLS *TLSDescription `json:"tls,omitempty"`
AdditionalVolumes []AdditionalVolume `json:"additionalVolumes,omitempty"`
NumberOfInstances int32 `json:"numberOfInstances"`
EnableNamespacedSecret *bool `json:"enableNamespacedSecret,omitempty"`
Users map[string]UserFlags `json:"users,omitempty"`
MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"`
Clone *CloneDescription `json:"clone,omitempty"`
ClusterName string `json:"-"`
Databases map[string]string `json:"databases,omitempty"`
PreparedDatabases map[string]PreparedDatabase `json:"preparedDatabases,omitempty"`
SchedulerName *string `json:"schedulerName,omitempty"`
NodeAffinity *v1.NodeAffinity `json:"nodeAffinity,omitempty"`
Tolerations []v1.Toleration `json:"tolerations,omitempty"`
Sidecars []Sidecar `json:"sidecars,omitempty"`
InitContainers []v1.Container `json:"initContainers,omitempty"`
PodPriorityClassName string `json:"podPriorityClassName,omitempty"`
ShmVolume *bool `json:"enableShmVolume,omitempty"`
EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"`
LogicalBackupSchedule string `json:"logicalBackupSchedule,omitempty"`
StandbyCluster *StandbyDescription `json:"standby,omitempty"`
PodAnnotations map[string]string `json:"podAnnotations,omitempty"`
ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"`
TLS *TLSDescription `json:"tls,omitempty"`
AdditionalVolumes []AdditionalVolume `json:"additionalVolumes,omitempty"`

// deprecated json tags
InitContainersOld []v1.Container `json:"init_containers,omitempty"`
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 27 additions & 10 deletions pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -940,14 +940,16 @@ func (c *Cluster) initSystemUsers() {
// secrets, therefore, setting flags like SUPERUSER or REPLICATION
// is not necessary here
c.systemUsers[constants.SuperuserKeyName] = spec.PgUser{
Origin: spec.RoleOriginSystem,
Name: c.OpConfig.SuperUsername,
Password: util.RandomPassword(constants.PasswordLength),
Origin: spec.RoleOriginSystem,
Name: c.OpConfig.SuperUsername,
Namespace: c.Namespace,
Password: util.RandomPassword(constants.PasswordLength),
}
c.systemUsers[constants.ReplicationUserKeyName] = spec.PgUser{
Origin: spec.RoleOriginSystem,
Name: c.OpConfig.ReplicationUsername,
Password: util.RandomPassword(constants.PasswordLength),
Origin: spec.RoleOriginSystem,
Name: c.OpConfig.ReplicationUsername,
Namespace: c.Namespace,
Password: util.RandomPassword(constants.PasswordLength),
}

// Connection pooler user is an exception, if requested it's going to be
Expand Down Expand Up @@ -975,10 +977,11 @@ func (c *Cluster) initSystemUsers() {

// connection pooler application should be able to login with this role
connectionPoolerUser := spec.PgUser{
Origin: spec.RoleConnectionPooler,
Name: username,
Flags: []string{constants.RoleFlagLogin},
Password: util.RandomPassword(constants.PasswordLength),
Origin: spec.RoleConnectionPooler,
Name: username,
Namespace: c.Namespace,
Flags: []string{constants.RoleFlagLogin},
Password: util.RandomPassword(constants.PasswordLength),
}

if _, exists := c.pgUsers[username]; !exists {
Expand Down Expand Up @@ -1081,6 +1084,7 @@ func (c *Cluster) initDefaultRoles(defaultRoles map[string]string, admin, prefix
newRole := spec.PgUser{
Origin: spec.RoleOriginBootstrap,
Name: roleName,
Namespace: c.Namespace,
Password: util.RandomPassword(constants.PasswordLength),
Flags: flags,
MemberOf: memberOf,
Expand All @@ -1105,6 +1109,17 @@ func (c *Cluster) initRobotUsers() error {
if c.shouldAvoidProtectedOrSystemRole(username, "manifest robot role") {
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)
if err != nil {
return fmt.Errorf("invalid flags for user %q: %v", username, err)
Expand All @@ -1116,6 +1131,7 @@ func (c *Cluster) initRobotUsers() error {
newRole := spec.PgUser{
Origin: spec.RoleOriginManifest,
Name: username,
Namespace: namespace,
Password: util.RandomPassword(constants.PasswordLength),
Flags: flags,
AdminRole: adminRole,
Expand Down Expand Up @@ -1233,6 +1249,7 @@ func (c *Cluster) initInfrastructureRoles() error {
return fmt.Errorf("invalid flags for user '%v': %v", username, err)
}
newRole.Flags = flags
newRole.Namespace = c.Namespace

if currentRole, present := c.pgUsers[username]; present {
c.pgUsers[username] = c.resolveNameConflict(&currentRole, &newRole)
Expand Down
Loading