[WIP] Extend infrastructure roles handling (#1064)

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 implicitly 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 explicitly. The idea is
to introduce a new configuration option for the Operator.

    infrastructure_roles_secrets:
    - secretname: k8s_secret_name
      userkey: some_encrypted_name
      passwordkey: some_encrypted_password
      rolekey: some_encrypted_role

    - secretname: k8s_secret_name
      userkey: some_encrypted_name
      passwordkey: some_encrypted_password
      rolekey: 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_secrets: |
        secretname: k8s_secret_name,
        userkey: some_encrypted_name,
        passwordkey: some_encrypted_password,
        rolekey: some_encrypted_role

Note than only one secret could be specified this way, no multiple secrets are
allowed.

Eventually the resulting list of infrastructure roles would be a total sum of
all supported ways to describe it, namely legacy via
infrastructure_roles_secret_name and infrastructure_roles_secrets from both
ConfigMap and CRD.
This commit is contained in:
Dmitry Dolgov
2020-08-05 14:18:56 +02:00
committed by GitHub
parent f3ddce81d5
commit 7cf2fae6df
20 changed files with 1375 additions and 621 deletions
+283 -16
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,269 @@ 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{
SecretName: test.secretName,
UserKey: "user",
PasswordKey: "password",
RoleKey: "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,
Flags: []string{"createdb"},
},
},
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{
SecretName: secret,
UserKey: "user",
PasswordKey: "password",
RoleKey: "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{
SecretName: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesNewSecretName,
},
UserKey: "user",
PasswordKey: "password",
RoleKey: "inrole",
Template: false,
},
},
spec.NamespacedName{},
"",
[]*config.InfrastructureRole{
&config.InfrastructureRole{
SecretName: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesNewSecretName,
},
UserKey: "user",
PasswordKey: "password",
RoleKey: "inrole",
Template: false,
},
},
},
// only old format
{
[]*config.InfrastructureRole{},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
"",
[]*config.InfrastructureRole{
&config.InfrastructureRole{
SecretName: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
UserKey: "user",
PasswordKey: "password",
RoleKey: "inrole",
Template: true,
},
},
},
// only configmap format
{
[]*config.InfrastructureRole{},
spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
"secretname: infrastructureroles-old-test, userkey: test-user, passwordkey: test-password, rolekey: test-role, template: false",
[]*config.InfrastructureRole{
&config.InfrastructureRole{
SecretName: spec.NamespacedName{
Namespace: v1.NamespaceDefault,
Name: testInfrastructureRolesOldSecretName,
},
UserKey: "test-user",
PasswordKey: "test-password",
RoleKey: "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{},
"userkey: test-user, passwordkey: test-password, rolekey: test-role, template: false",
[]*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)
}
}
}
}