Extend infrastructure roles handling

Postgres Operator uses infrastructure roles to provide access to a
database for external users e.g. for monitoring purposes. Such
infrastructure roles are expected to be present in the form of k8s
secrets with the following content:

    inrole1: some_encrypted_role
    password1: some_encrypted_password
    user1: some_entrypted_name

    inrole2: some_encrypted_role
    password2: some_encrypted_password
    user2: some_entrypted_name

The format of this content is implied implicitely and not flexible
enough. In case if we do not have possibility to change the format of a
secret we want to use in the Operator, we need to recreate it in this
format.

To address this lets make the format of secret content explicitely. The
idea is to introduce a new configuration option for the Operator.

    infrastructure_roles_secrets:
    - secret: k8s_secret_name
      name: some_encrypted_name
      password: some_encrypted_password
      role: some_encrypted_role

    - secret: k8s_secret_name
      name: some_encrypted_name
      password: some_encrypted_password
      role: some_encrypted_role

This would allow Operator to use any avalable secrets to prepare
infrastructure roles. To make it backward compatible simulate the old
behaviour if the new option is not present.

The new configuration option is intended be used mainly from CRD, but
it's also available via Operator ConfigMap in a limited fashion. For
ConfigMap one can put there only a string with one secret definition in
the following format (as a string):

    infrastructure_roles_secret_name: |
        secret: k8s_secret_name,
        name: some_encrypted_name,
        password: some_encrypted_password,
        role: some_encrypted_role
This commit is contained in:
Dmitrii Dolgov 2020-07-16 11:03:09 +02:00
parent c10d30903e
commit bd576942f2
7 changed files with 637 additions and 111 deletions

View File

@ -45,28 +45,29 @@ type PostgresUsersConfiguration struct {
type KubernetesMetaConfiguration struct {
PodServiceAccountName string `json:"pod_service_account_name,omitempty"`
// TODO: change it to the proper json
PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"`
PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"`
PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"`
SpiloPrivileged bool `json:"spilo_privileged,omitempty"`
SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"`
WatchedNamespace string `json:"watched_namespace,omitempty"`
PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"`
EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"`
StorageResizeMode string `json:"storage_resize_mode,omitempty"`
EnableInitContainers *bool `json:"enable_init_containers,omitempty"`
EnableSidecars *bool `json:"enable_sidecars,omitempty"`
SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"`
ClusterDomain string `json:"cluster_domain,omitempty"`
OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"`
InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"`
PodRoleLabel string `json:"pod_role_label,omitempty"`
ClusterLabels map[string]string `json:"cluster_labels,omitempty"`
InheritedLabels []string `json:"inherited_labels,omitempty"`
DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"`
ClusterNameLabel string `json:"cluster_name_label,omitempty"`
NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"`
CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"`
PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"`
PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"`
PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"`
SpiloPrivileged bool `json:"spilo_privileged,omitempty"`
SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"`
WatchedNamespace string `json:"watched_namespace,omitempty"`
PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"`
EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"`
StorageResizeMode string `json:"storage_resize_mode,omitempty"`
EnableInitContainers *bool `json:"enable_init_containers,omitempty"`
EnableSidecars *bool `json:"enable_sidecars,omitempty"`
SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"`
ClusterDomain string `json:"cluster_domain,omitempty"`
OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"`
InfrastructureRolesSecretName spec.NamespacedName `json:"infrastructure_roles_secret_name,omitempty"`
InfrastructureRolesDefs []*config.InfrastructureRole `json:"infrastructure_roles_secrets,omitempty"`
PodRoleLabel string `json:"pod_role_label,omitempty"`
ClusterLabels map[string]string `json:"cluster_labels,omitempty"`
InheritedLabels []string `json:"inherited_labels,omitempty"`
DownscalerAnnotations []string `json:"downscaler_annotations,omitempty"`
ClusterNameLabel string `json:"cluster_name_label,omitempty"`
NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"`
CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"`
// TODO: use a proper toleration structure?
PodToleration map[string]string `json:"toleration,omitempty"`
PodEnvironmentConfigMap spec.NamespacedName `json:"pod_environment_configmap,omitempty"`

View File

@ -300,7 +300,8 @@ func (c *Controller) initController() {
c.logger.Infof("config: %s", c.opConfig.MustMarshal())
if infraRoles, err := c.getInfrastructureRoles(&c.opConfig.InfrastructureRolesSecretName); err != nil {
roleDefs := c.getInfrastructureRoleDefinitions()
if infraRoles, err := c.getInfrastructureRoles(roleDefs); err != nil {
c.logger.Warningf("could not get infrastructure roles: %v", err)
} else {
c.config.InfrastructureRoles = infraRoles

View File

@ -70,7 +70,22 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.EnableSidecars = util.CoalesceBool(fromCRD.Kubernetes.EnableSidecars, util.True())
result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate
result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName
result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName
if fromCRD.Kubernetes.InfrastructureRolesDefs != nil {
result.InfrastructureRoles = []*config.InfrastructureRole{}
for _, secret := range fromCRD.Kubernetes.InfrastructureRolesDefs {
result.InfrastructureRoles = append(
result.InfrastructureRoles,
&config.InfrastructureRole{
Secret: secret.Secret,
Name: secret.Name,
Role: secret.Role,
Password: secret.Password,
})
}
}
result.PodRoleLabel = util.Coalesce(fromCRD.Kubernetes.PodRoleLabel, "spilo-role")
result.ClusterLabels = util.CoalesceStrMap(fromCRD.Kubernetes.ClusterLabels, map[string]string{"application": "spilo"})
result.InheritedLabels = fromCRD.Kubernetes.InheritedLabels

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
v1 "k8s.io/api/core/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
@ -109,8 +110,158 @@ func readDecodedRole(s string) (*spec.PgUser, error) {
return &result, nil
}
func (c *Controller) getInfrastructureRoles(rolesSecret *spec.NamespacedName) (map[string]spec.PgUser, error) {
if *rolesSecret == (spec.NamespacedName{}) {
var emptyNamespacedName = (spec.NamespacedName{})
// Return information about what secrets we need to use to create
// infrastructure roles and in which format are they. This is done in
// compatible way, so that the previous logic is not changed, and handles both
// configuration in ConfigMap & CRD.
func (c *Controller) getInfrastructureRoleDefinitions() []*config.InfrastructureRole {
var roleDef config.InfrastructureRole
rolesDefs := c.opConfig.InfrastructureRoles
if c.opConfig.InfrastructureRolesSecretName == emptyNamespacedName {
// All the other possibilities require secret name to be present, so if
// it is not, then nothing else to be done here.
return rolesDefs
}
// check if we can extract something from the configmap config option
if c.opConfig.InfrastructureRolesDefs != "" {
// The configmap option could contain either a role description (in the
// form key1: value1, key2: value2), which has to be used together with
// an old secret name.
propertySep := ","
valueSep := ":"
// The field contains the format in which secret is written, let's
// convert it to a proper definition
properties := strings.Split(c.opConfig.InfrastructureRolesDefs, propertySep)
roleDef = config.InfrastructureRole{
Secret: c.opConfig.InfrastructureRolesSecretName,
Template: false,
}
for _, property := range properties {
values := strings.Split(property, valueSep)
if len(values) < 2 {
continue
}
name := strings.TrimSpace(values[0])
value := strings.TrimSpace(values[1])
switch name {
case "name":
roleDef.Name = value
case "password":
roleDef.Password = value
case "role":
roleDef.Role = value
default:
c.logger.Warningf("Role description is not known: %s",
c.opConfig.InfrastructureRolesSecretName)
}
}
} else {
// At this point we deal with the old format, let's replicate it
// via existing definition structure and remember that it's just a
// template, the real values are in user1,password1,inrole1 etc.
roleDef = config.InfrastructureRole{
Secret: c.opConfig.InfrastructureRolesSecretName,
Name: "user",
Password: "password",
Role: "inrole",
Template: true,
}
}
if roleDef.Name != "" &&
roleDef.Password != "" &&
roleDef.Role != "" {
rolesDefs = append(rolesDefs, &roleDef)
}
return rolesDefs
}
func (c *Controller) getInfrastructureRoles(
rolesSecrets []*config.InfrastructureRole) (
map[string]spec.PgUser, []error) {
var errors []error
var noRolesProvided = true
roles := []spec.PgUser{}
uniqRoles := map[string]spec.PgUser{}
// To be compatible with the legacy implementation we need to return nil if
// the provided secret name is empty. The equivalent situation in the
// current implementation is an empty rolesSecrets slice or all its items
// are empty.
for _, role := range rolesSecrets {
if role.Secret != emptyNamespacedName {
noRolesProvided = false
}
}
if noRolesProvided {
return nil, nil
}
for _, secret := range rolesSecrets {
infraRoles, err := c.getInfrastructureRole(secret)
if err != nil || infraRoles == nil {
c.logger.Debugf("Cannot get infrastructure role: %+v", *secret)
if err != nil {
errors = append(errors, err)
}
continue
}
for _, r := range infraRoles {
roles = append(roles, r)
}
}
for _, r := range roles {
if _, exists := uniqRoles[r.Name]; exists {
msg := "Conflicting infrastructure roles: roles[%s] = (%q, %q)"
c.logger.Debugf(msg, r.Name, uniqRoles[r.Name], r)
}
uniqRoles[r.Name] = r
}
return uniqRoles, errors
}
// Generate list of users representing one infrastructure role based on its
// description in various K8S objects. An infrastructure role could be
// described by a secret and optionally a config map. The former should contain
// the secret information, i.e. username, password, role. The latter could
// contain an extensive description of the role and even override an
// information obtained from the secret (except a password).
//
// This function returns a list of users to be compatible with the previous
// behaviour, since we don't know how many users are actually encoded in the
// secret if it's a "template" role. If the provided role is not a template
// one, the result would be a list with just one user in it.
//
// FIXME: This dependency on two different objects is rather unnecessary
// complicated, so let's get rid of it via deprecation process.
func (c *Controller) getInfrastructureRole(
infraRole *config.InfrastructureRole) (
[]spec.PgUser, error) {
rolesSecret := infraRole.Secret
roles := []spec.PgUser{}
if rolesSecret == (spec.NamespacedName{}) {
// we don't have infrastructure roles defined, bail out
return nil, nil
}
@ -119,52 +270,84 @@ func (c *Controller) getInfrastructureRoles(rolesSecret *spec.NamespacedName) (m
Secrets(rolesSecret.Namespace).
Get(context.TODO(), rolesSecret.Name, metav1.GetOptions{})
if err != nil {
c.logger.Debugf("infrastructure roles secret name: %q", *rolesSecret)
return nil, fmt.Errorf("could not get infrastructure roles secret: %v", err)
msg := "could not get infrastructure roles secret %s/%s: %v"
return nil, fmt.Errorf(msg, rolesSecret.Namespace, rolesSecret.Name, err)
}
secretData := infraRolesSecret.Data
result := make(map[string]spec.PgUser)
Users:
// in worst case we would have one line per user
for i := 1; i <= len(secretData); i++ {
properties := []string{"user", "password", "inrole"}
t := spec.PgUser{Origin: spec.RoleOriginInfrastructure}
for _, p := range properties {
key := fmt.Sprintf("%s%d", p, i)
if val, present := secretData[key]; !present {
if p == "user" {
// exit when the user name with the next sequence id is absent
break Users
}
} else {
s := string(val)
switch p {
case "user":
t.Name = s
case "password":
t.Password = s
case "inrole":
t.MemberOf = append(t.MemberOf, s)
default:
c.logger.Warningf("unknown key %q", p)
}
if infraRole.Template {
Users:
for i := 1; i <= len(secretData); i++ {
properties := []string{
infraRole.Name,
infraRole.Password,
infraRole.Role,
}
delete(secretData, key)
t := spec.PgUser{Origin: spec.RoleOriginInfrastructure}
for _, p := range properties {
key := fmt.Sprintf("%s%d", p, i)
if val, present := secretData[key]; !present {
if p == "user" {
// exit when the user name with the next sequence id is
// absent
break Users
}
} else {
s := string(val)
switch p {
case "user":
t.Name = s
case "password":
t.Password = s
case "inrole":
t.MemberOf = append(t.MemberOf, s)
default:
c.logger.Warningf("unknown key %q", p)
}
}
// XXX: This is a part of the original implementation, which is
// rather obscure. Why do we delete this key? Wouldn't it be
// used later in comparison for configmap?
delete(secretData, key)
}
roles = append(roles, t)
}
} else {
roleDescr := &spec.PgUser{Origin: spec.RoleOriginInfrastructure}
if details, exists := secretData[infraRole.Details]; exists {
if err := yaml.Unmarshal(details, &roleDescr); err != nil {
return nil, fmt.Errorf("could not decode yaml role: %v", err)
}
} else {
roleDescr.Name = string(secretData[infraRole.Name])
roleDescr.Password = string(secretData[infraRole.Password])
roleDescr.MemberOf = append(roleDescr.MemberOf, string(secretData[infraRole.Role]))
}
if t.Name != "" {
if t.Password == "" {
c.logger.Warningf("infrastructure role %q has no password defined and is ignored", t.Name)
continue
}
result[t.Name] = t
if roleDescr.Name == "" {
msg := "infrastructure role %q has no name defined and is ignored"
c.logger.Warningf(msg, roleDescr.Name)
return nil, nil
}
if roleDescr.Password == "" {
msg := "infrastructure role %q has no password defined and is ignored"
c.logger.Warningf(msg, roleDescr.Name)
return nil, nil
}
roles = append(roles, *roleDescr)
}
// perhaps we have some map entries with usernames, passwords, let's check if we have those users in the configmap
if infraRolesMap, err := c.KubeClient.ConfigMaps(rolesSecret.Namespace).Get(
context.TODO(), rolesSecret.Name, metav1.GetOptions{}); err == nil {
// Now plot twist. We need to check if there is a configmap with the same
// name and extract a role description if it exists.
infraRolesMap, err := c.KubeClient.
ConfigMaps(rolesSecret.Namespace).
Get(context.TODO(), rolesSecret.Name, metav1.GetOptions{})
if err == nil {
// we have a configmap with username - json description, let's read and decode it
for role, s := range infraRolesMap.Data {
roleDescr, err := readDecodedRole(s)
@ -182,20 +365,12 @@ Users:
}
roleDescr.Name = role
roleDescr.Origin = spec.RoleOriginInfrastructure
result[role] = *roleDescr
roles = append(roles, *roleDescr)
}
}
if len(secretData) > 0 {
c.logger.Warningf("%d unprocessed entries in the infrastructure roles secret,"+
" checking configmap %v", len(secretData), rolesSecret.Name)
c.logger.Info(`infrastructure role entries should be in the {key}{id} format,` +
` where {key} can be either of "user", "password", "inrole" and the {id}` +
` a monotonically increasing integer starting with 1`)
c.logger.Debugf("unprocessed entries: %#v", secretData)
}
return result, nil
// TODO: check for role collisions
return roles, nil
}
func (c *Controller) podClusterName(pod *v1.Pod) spec.NamespacedName {

View File

@ -8,20 +8,25 @@ import (
b64 "encoding/base64"
"github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util/config"
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
testInfrastructureRolesSecretName = "infrastructureroles-test"
testInfrastructureRolesOldSecretName = "infrastructureroles-old-test"
testInfrastructureRolesNewSecretName = "infrastructureroles-new-test"
)
func newUtilTestController() *Controller {
controller := NewController(&spec.ControllerConfig{}, "util-test")
controller.opConfig.ClusterNameLabel = "cluster-name"
controller.opConfig.InfrastructureRolesSecretName =
spec.NamespacedName{Namespace: v1.NamespaceDefault, Name: testInfrastructureRolesSecretName}
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
}
controller.opConfig.Workers = 4
controller.KubeClient = k8sutil.NewMockKubernetesClient()
return controller
@ -80,24 +85,32 @@ func TestClusterWorkerID(t *testing.T) {
}
}
func TestGetInfrastructureRoles(t *testing.T) {
// Test functionality of getting infrastructure roles from their description in
// corresponding secrets. Here we test only common stuff (e.g. when a secret do
// not exist, or empty) and the old format.
func TestOldInfrastructureRoleFormat(t *testing.T) {
var testTable = []struct {
secretName spec.NamespacedName
expectedRoles map[string]spec.PgUser
expectedError error
secretName spec.NamespacedName
expectedRoles map[string]spec.PgUser
expectedErrors []error
}{
{
// empty secret name
spec.NamespacedName{},
nil,
nil,
},
{
// secret does not exist
spec.NamespacedName{Namespace: v1.NamespaceDefault, Name: "null"},
nil,
fmt.Errorf(`could not get infrastructure roles secret: NotFound`),
map[string]spec.PgUser{},
[]error{fmt.Errorf(`could not get infrastructure roles secret default/null: NotFound`)},
},
{
spec.NamespacedName{Namespace: v1.NamespaceDefault, Name: testInfrastructureRolesSecretName},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
map[string]spec.PgUser{
"testrole": {
Name: "testrole",
@ -116,15 +129,268 @@ func TestGetInfrastructureRoles(t *testing.T) {
},
}
for _, test := range testTable {
roles, err := utilTestController.getInfrastructureRoles(&test.secretName)
if err != test.expectedError {
if err != nil && test.expectedError != nil && err.Error() == test.expectedError.Error() {
continue
}
t.Errorf("expected error '%v' does not match the actual error '%v'", test.expectedError, err)
roles, errors := utilTestController.getInfrastructureRoles(
[]*config.InfrastructureRole{
&config.InfrastructureRole{
Secret: test.secretName,
Name: "user",
Password: "password",
Role: "inrole",
Template: true,
},
})
if len(errors) != len(test.expectedErrors) {
t.Errorf("expected error '%v' does not match the actual error '%v'",
test.expectedErrors, errors)
}
for idx := range errors {
err := errors[idx]
expectedErr := test.expectedErrors[idx]
if err != expectedErr {
if err != nil && expectedErr != nil && err.Error() == expectedErr.Error() {
continue
}
t.Errorf("expected error '%v' does not match the actual error '%v'",
expectedErr, err)
}
}
if !reflect.DeepEqual(roles, test.expectedRoles) {
t.Errorf("expected roles output %v does not match the actual %v", test.expectedRoles, roles)
t.Errorf("expected roles output %#v does not match the actual %#v",
test.expectedRoles, roles)
}
}
}
// Test functionality of getting infrastructure roles from their description in
// corresponding secrets. Here we test the new format.
func TestNewInfrastructureRoleFormat(t *testing.T) {
var testTable = []struct {
secrets []spec.NamespacedName
expectedRoles map[string]spec.PgUser
expectedErrors []error
}{
// one secret with one configmap
{
[]spec.NamespacedName{
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesNewSecretName,
},
},
map[string]spec.PgUser{
"new-test-role": {
Name: "new-test-role",
Origin: spec.RoleOriginInfrastructure,
Password: "new-test-password",
MemberOf: []string{"new-test-inrole"},
},
"new-foobar": {
Name: "new-foobar",
Origin: spec.RoleOriginInfrastructure,
Password: b64.StdEncoding.EncodeToString([]byte("password")),
MemberOf: nil,
},
},
nil,
},
// multiple standalone secrets
{
[]spec.NamespacedName{
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: "infrastructureroles-new-test1",
},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: "infrastructureroles-new-test2",
},
},
map[string]spec.PgUser{
"new-test-role1": {
Name: "new-test-role1",
Origin: spec.RoleOriginInfrastructure,
Password: "new-test-password1",
MemberOf: []string{"new-test-inrole1"},
},
"new-test-role2": {
Name: "new-test-role2",
Origin: spec.RoleOriginInfrastructure,
Password: "new-test-password2",
MemberOf: []string{"new-test-inrole2"},
},
},
nil,
},
}
for _, test := range testTable {
definitions := []*config.InfrastructureRole{}
for _, secret := range test.secrets {
definitions = append(definitions, &config.InfrastructureRole{
Secret: secret,
Name: "user",
Password: "password",
Role: "inrole",
Template: false,
})
}
roles, errors := utilTestController.getInfrastructureRoles(definitions)
if len(errors) != len(test.expectedErrors) {
t.Errorf("expected error does not match the actual error:\n%+v\n%+v",
test.expectedErrors, errors)
// Stop and do not do any further checks
return
}
for idx := range errors {
err := errors[idx]
expectedErr := test.expectedErrors[idx]
if err != expectedErr {
if err != nil && expectedErr != nil && err.Error() == expectedErr.Error() {
continue
}
t.Errorf("expected error '%v' does not match the actual error '%v'",
expectedErr, err)
}
}
if !reflect.DeepEqual(roles, test.expectedRoles) {
t.Errorf("expected roles output/the actual:\n%#v\n%#v",
test.expectedRoles, roles)
}
}
}
// Tests for getting correct infrastructure roles definitions from present
// configuration. E.g. in which secrets for which roles too look. The biggest
// point here is compatibility of old and new formats of defining
// infrastructure roles.
func TestInfrastructureRoleDefinitions(t *testing.T) {
var testTable = []struct {
rolesDefs []*config.InfrastructureRole
roleSecretName spec.NamespacedName
roleSecrets string
expectedDefs []*config.InfrastructureRole
}{
// only new format
{
[]*config.InfrastructureRole{
&config.InfrastructureRole{
Secret: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesNewSecretName,
},
Name: "user",
Password: "password",
Role: "inrole",
Template: false,
},
},
spec.NamespacedName{},
"",
[]*config.InfrastructureRole{
&config.InfrastructureRole{
Secret: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesNewSecretName,
},
Name: "user",
Password: "password",
Role: "inrole",
Template: false,
},
},
},
// only old format
{
[]*config.InfrastructureRole{},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
"",
[]*config.InfrastructureRole{
&config.InfrastructureRole{
Secret: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
Name: "user",
Password: "password",
Role: "inrole",
Template: true,
},
},
},
// only configmap format
{
[]*config.InfrastructureRole{},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
"name: test-user, password: test-password, role: test-role",
[]*config.InfrastructureRole{
&config.InfrastructureRole{
Secret: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
Name: "test-user",
Password: "test-password",
Role: "test-role",
Template: false,
},
},
},
// incorrect configmap format
{
[]*config.InfrastructureRole{},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
"wrong-format",
[]*config.InfrastructureRole{},
},
// configmap without a secret
{
[]*config.InfrastructureRole{},
spec.NamespacedName{},
"name: test-user, password: test-password, role: test-role",
[]*config.InfrastructureRole{},
},
}
for _, test := range testTable {
t.Logf("Test: %+v", test)
utilTestController.opConfig.InfrastructureRoles = test.rolesDefs
utilTestController.opConfig.InfrastructureRolesSecretName = test.roleSecretName
utilTestController.opConfig.InfrastructureRolesDefs = test.roleSecrets
defs := utilTestController.getInfrastructureRoleDefinitions()
if len(defs) != len(test.expectedDefs) {
t.Errorf("expected definitions does not match the actual:\n%#v\n%#v",
test.expectedDefs, defs)
// Stop and do not do any further checks
return
}
for idx := range defs {
def := defs[idx]
expectedDef := test.expectedDefs[idx]
if !reflect.DeepEqual(def, expectedDef) {
t.Errorf("expected definition/the actual:\n%#v\n%#v",
expectedDef, def)
}
}
}
}

View File

@ -51,16 +51,42 @@ type Resources struct {
ShmVolume *bool `name:"enable_shm_volume" default:"true"`
}
type InfrastructureRole struct {
// Name of a secret which describes the role, and optionally name of a
// configmap with an extra information
Secret spec.NamespacedName
Name string
Password string
Role string
// This field point out the detailed yaml definition of the role, if exists
Details string
// Specify if a secret contains multiple fields in the following format:
//
// %(name)idx: ...
// %(password)idx: ...
// %(role)idx: ...
//
// If it does, Name/Password/Role are interpreted not as unique field
// names, but as a template.
Template bool
}
// Auth describes authentication specific configuration parameters
type Auth struct {
SecretNameTemplate StringTemplate `name:"secret_name_template" default:"{username}.{cluster}.credentials.{tprkind}.{tprgroup}"`
PamRoleName string `name:"pam_role_name" default:"zalandos"`
PamConfiguration string `name:"pam_configuration" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"`
TeamsAPIUrl string `name:"teams_api_url" default:"https://teams.example.com/api/"`
OAuthTokenSecretName spec.NamespacedName `name:"oauth_token_secret_name" default:"postgresql-operator"`
InfrastructureRolesSecretName spec.NamespacedName `name:"infrastructure_roles_secret_name"`
SuperUsername string `name:"super_username" default:"postgres"`
ReplicationUsername string `name:"replication_username" default:"standby"`
SecretNameTemplate StringTemplate `name:"secret_name_template" default:"{username}.{cluster}.credentials.{tprkind}.{tprgroup}"`
PamRoleName string `name:"pam_role_name" default:"zalandos"`
PamConfiguration string `name:"pam_configuration" default:"https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees"`
TeamsAPIUrl string `name:"teams_api_url" default:"https://teams.example.com/api/"`
OAuthTokenSecretName spec.NamespacedName `name:"oauth_token_secret_name" default:"postgresql-operator"`
InfrastructureRolesSecretName spec.NamespacedName `name:"infrastructure_roles_secret_name"`
InfrastructureRoles []*InfrastructureRole `name:"-"`
InfrastructureRolesDefs string `name:"infrastructure_roles_secrets"`
SuperUsername string `name:"super_username" default:"postgres"`
ReplicationUsername string `name:"replication_username" default:"standby"`
}
// Scalyr holds the configuration for the Scalyr Agent sidecar for log shipping:

View File

@ -271,31 +271,73 @@ func SameLogicalBackupJob(cur, new *batchv1beta1.CronJob) (match bool, reason st
}
func (c *mockSecret) Get(ctx context.Context, name string, options metav1.GetOptions) (*v1.Secret, error) {
if name != "infrastructureroles-test" {
return nil, fmt.Errorf("NotFound")
}
secret := &v1.Secret{}
secret.Name = "testcluster"
secret.Data = map[string][]byte{
oldFormatSecret := &v1.Secret{}
oldFormatSecret.Name = "testcluster"
oldFormatSecret.Data = map[string][]byte{
"user1": []byte("testrole"),
"password1": []byte("testpassword"),
"inrole1": []byte("testinrole"),
"foobar": []byte(b64.StdEncoding.EncodeToString([]byte("password"))),
}
return secret, nil
newFormatSecret := &v1.Secret{}
newFormatSecret.Name = "test-secret-new-format"
newFormatSecret.Data = map[string][]byte{
"user": []byte("new-test-role"),
"password": []byte("new-test-password"),
"inrole": []byte("new-test-inrole"),
"new-foobar": []byte(b64.StdEncoding.EncodeToString([]byte("password"))),
}
secrets := map[string]*v1.Secret{
"infrastructureroles-old-test": oldFormatSecret,
"infrastructureroles-new-test": newFormatSecret,
}
for idx := 1; idx <= 2; idx++ {
newFormatStandaloneSecret := &v1.Secret{}
newFormatStandaloneSecret.Name = fmt.Sprintf("test-secret-new-format%d", idx)
newFormatStandaloneSecret.Data = map[string][]byte{
"user": []byte(fmt.Sprintf("new-test-role%d", idx)),
"password": []byte(fmt.Sprintf("new-test-password%d", idx)),
"inrole": []byte(fmt.Sprintf("new-test-inrole%d", idx)),
}
secrets[fmt.Sprintf("infrastructureroles-new-test%d", idx)] =
newFormatStandaloneSecret
}
if secret, exists := secrets[name]; exists {
return secret, nil
}
return nil, fmt.Errorf("NotFound")
}
func (c *mockConfigMap) Get(ctx context.Context, name string, options metav1.GetOptions) (*v1.ConfigMap, error) {
if name != "infrastructureroles-test" {
return nil, fmt.Errorf("NotFound")
}
configmap := &v1.ConfigMap{}
configmap.Name = "testcluster"
configmap.Data = map[string]string{
oldFormatConfigmap := &v1.ConfigMap{}
oldFormatConfigmap.Name = "testcluster"
oldFormatConfigmap.Data = map[string]string{
"foobar": "{}",
}
return configmap, nil
newFormatConfigmap := &v1.ConfigMap{}
newFormatConfigmap.Name = "testcluster"
newFormatConfigmap.Data = map[string]string{
"new-foobar": "{}",
}
configmaps := map[string]*v1.ConfigMap{
"infrastructureroles-old-test": oldFormatConfigmap,
"infrastructureroles-new-test": newFormatConfigmap,
}
if configmap, exists := configmaps[name]; exists {
return configmap, nil
}
return nil, fmt.Errorf("NotFound")
}
// Secrets to be mocked