add e2e test

This commit is contained in:
Felix Kunde 2022-01-20 18:17:57 +01:00
parent 08542711e0
commit 58280809d8
9 changed files with 151 additions and 17 deletions

View File

@ -551,6 +551,16 @@ spec:
- SUPERUSER
- nosuperuser
- NOSUPERUSER
usersWithPasswordRotation:
type: array
nullable: true
items:
type: string
usersWithInPlacePasswordRotation:
type: array
nullable: true
items:
type: string
volume:
type: object
required:

View File

@ -329,7 +329,9 @@ manifest. The rotation and retention intervals can only be configured globally.
```
spec:
usersWithSecretRotation: "foo_user,bar_reader_user"
usersWithSecretRotation:
- foo_user
- bar_reader_user
```
### Password replacement without extra roles
@ -342,7 +344,9 @@ manifest:
```
spec:
usersWithInPlaceSecretRotation: "flyway,bar_owner_user"
usersWithInPlaceSecretRotation:
- flyway
- bar_owner_user
```
This would be the recommended option to enable rotation in secrets of database

View File

@ -321,6 +321,9 @@ class K8s:
def get_cluster_replica_pod(self, labels='application=spilo,cluster-name=acid-minimal-cluster', namespace='default'):
return self.get_cluster_pod('replica', labels, namespace)
def get_secret_data(self, username, clustername='acid-minimal-cluster', namespace='default'):
return self.api.core_v1.read_namespaced_secret(
"{}.{}.credentials.postgresql.acid.zalan.do".format(username.replace("_","-"), clustername), namespace).data
class K8sBase:
'''

View File

@ -4,8 +4,9 @@ import time
import timeout_decorator
import os
import yaml
import base64
from datetime import datetime
from datetime import datetime, date, timedelta
from kubernetes import client
from tests.k8s_api import K8s
@ -600,7 +601,6 @@ class EndToEndTestCase(unittest.TestCase):
but lets pods run with the old image until they are recreated for
reasons other than operator's activity. That works because the operator
configures stateful sets to use "onDelete" pod update policy.
The test covers:
1) enabling lazy upgrade in existing operator deployment
2) forcing the normal rolling upgrade by changing the operator
@ -695,7 +695,6 @@ class EndToEndTestCase(unittest.TestCase):
Ensure we can (a) create the cron job at user request for a specific PG cluster
(b) update the cluster-wide image for the logical backup pod
(c) delete the job at user request
Limitations:
(a) Does not run the actual batch job because there is no S3 mock to upload backups to
(b) Assumes 'acid-minimal-cluster' exists as defined in setUp
@ -1056,6 +1055,90 @@ class EndToEndTestCase(unittest.TestCase):
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"),
0, "Pooler pods not scaled down")
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_password_rotation(self):
'''
Test password rotation and removal of users due to retention policy
'''
k8s = self.k8s
leader = k8s.get_cluster_leader_pod()
today = date.today()
# enable password rotation for owner of foo database
pg_patch_inplace_rotation_for_owner = {
"spec": {
"usersWithInPlaceSecretRotation": [
"zalando"
]
}
}
k8s.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_inplace_rotation_for_owner)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
# check if next rotation date was set in secret
secret_data = k8s.get_secret_data("zalando")
next_rotation_timestamp = datetime.fromisoformat(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8'))
today90days = today+timedelta(days=90)
self.assertEqual(today90days, next_rotation_timestamp.date(),
"Unexpected rotation date in secret of zalando user: expected {}, got {}".format(today90days, next_rotation_timestamp.date()))
# create fake rotation users that should be removed by operator
create_fake_rotation_user = """
CREATE ROLE foo_user201031 IN ROLE foo_user;
CREATE ROLE foo_user211031 IN ROLE foo_user;
"""
self.query_database(leader.metadata.name, "postgres", create_fake_rotation_user)
# patch foo_user secret with outdated rotation date
fake_rotation_date = today.isoformat() + ' 00:00:00'
fake_rotation_date_encoded = base64.b64encode(fake_rotation_date.encode('utf-8'))
secret_fake_rotation = {
"data": {
"nextRotation": str(fake_rotation_date_encoded, 'utf-8'),
},
}
k8s.api.core_v1.patch_namespaced_secret(
name="foo-user.acid-minimal-cluster.credentials.postgresql.acid.zalan.do",
namespace="default",
body=secret_fake_rotation)
# enable password rotation for all other users (foo_user)
# this will force a sync of secrets for further assertions
enable_password_rotation = {
"data": {
"enable_password_rotation": "true",
"password_rotation_interval": "30",
"password_rotation_user_retention": "30", # should be set to 60
},
}
k8s.update_config(enable_password_rotation)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")
# check if next rotation date and username have been replaced
secret_data = k8s.get_secret_data("foo_user")
secret_username = str(base64.b64decode(secret_data["username"]), 'utf-8')
next_rotation_timestamp = datetime.fromisoformat(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8'))
rotation_user = "foo_user"+today.strftime("%y%m%d")
today30days = today+timedelta(days=30)
self.assertEqual(rotation_user, secret_username,
"Unexpected username in secret of foo_user: expected {}, got {}".format(rotation_user, secret_username))
self.assertEqual(today30days, next_rotation_timestamp.date(),
"Unexpected rotation date in secret of foo_user: expected {}, got {}".format(today30days, next_rotation_timestamp.date()))
# check if oldest fake rotation users were deleted
# there should only be foo_user and foo_user+today.strftime("%y%m%d")
user_query = """
SELECT rolname
FROM pg_catalog.pg_roles
WHERE rolname LIKE 'foo_user%';
"""
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
"Found incorrect number of rotation users", 10, 5)
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_patroni_config_update(self):
'''

View File

@ -549,6 +549,16 @@ spec:
- SUPERUSER
- nosuperuser
- NOSUPERUSER
usersWithPasswordRotation:
type: array
nullable: true
items:
type: string
usersWithInPlacePasswordRotation:
type: array
nullable: true
items:
type: string
volume:
type: object
required:

View File

@ -833,6 +833,24 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
},
},
},
"usersWithSecretRotation": {
Type: "array",
Nullable: true,
Items: &apiextv1.JSONSchemaPropsOrArray{
Schema: &apiextv1.JSONSchemaProps{
Type: "string",
},
},
},
"usersWithInPlaceSecretRotation": {
Type: "array",
Nullable: true,
Items: &apiextv1.JSONSchemaPropsOrArray{
Schema: &apiextv1.JSONSchemaProps{
Type: "string",
},
},
},
"volume": {
Type: "object",
Required: []string{"size"},

View File

@ -710,13 +710,18 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
}
}
// connection pooler needs one system user created, which is done in
// initUsers. Check if it needs to be called.
// check if users need to be synced
sameUsers := reflect.DeepEqual(oldSpec.Spec.Users, newSpec.Spec.Users) &&
reflect.DeepEqual(oldSpec.Spec.PreparedDatabases, newSpec.Spec.PreparedDatabases)
sameRotatedUsers := reflect.DeepEqual(oldSpec.Spec.UsersWithSecretRotation, newSpec.Spec.UsersWithSecretRotation) &&
reflect.DeepEqual(oldSpec.Spec.UsersWithInPlaceSecretRotation, newSpec.Spec.UsersWithInPlaceSecretRotation)
// connection pooler needs one system user created, which is done in
// initUsers. Check if it needs to be called.
needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) ||
needReplicaConnectionPoolerWorker(&newSpec.Spec)
if !sameUsers || needConnectionPooler {
if !sameUsers || !sameRotatedUsers || needConnectionPooler {
c.logger.Debugf("initialize users")
if err := c.initUsers(); err != nil {
c.logger.Errorf("could not init users: %v", err)

View File

@ -267,14 +267,15 @@ func (c *Cluster) cleanupRotatedUsers(rotatedUsers []string, db *sql.DB) error {
return fmt.Errorf("error when querying for deprecated users from password rotation: %v", err)
}
// make sure user retention policy aligns with rotation interval
retenionDays := c.OpConfig.PasswordRotationUserRetention
if retenionDays < 2*c.OpConfig.PasswordRotationInterval {
retenionDays = 2 * c.OpConfig.PasswordRotationInterval
c.logger.Warnf("user retention days too few compared to rotation interval %d - setting it to %d", c.OpConfig.PasswordRotationInterval, retenionDays)
}
retentionDate := time.Now().AddDate(0, 0, int(retenionDays)*-1)
for rotatedUser, dateSuffix := range extraUsers {
// make sure user retention policy aligns with rotation interval
retenionDays := c.OpConfig.PasswordRotationUserRetention
if retenionDays < 2*c.OpConfig.PasswordRotationInterval {
retenionDays = 2 * c.OpConfig.PasswordRotationInterval
c.logger.Warnf("user retention days too few compared to rotation interval %d - setting it to %d", c.OpConfig.PasswordRotationInterval, retenionDays)
}
retentionDate := time.Now().AddDate(0, 0, int(retenionDays)*-1)
userCreationDate, err := time.Parse("060102", dateSuffix)
if err != nil {
c.logger.Errorf("could not parse creation date suffix of user %q: %v", rotatedUser, err)

View File

@ -674,7 +674,7 @@ func (c *Cluster) syncSecrets() error {
// if password rotation is enabled update password and username if rotation interval has been passed
if (c.OpConfig.EnablePasswordRotation && pwdUser.Origin != spec.RoleOriginInfrastructure && !pwdUser.IsDbOwner) ||
util.SliceContains(c.Spec.UsersWithSecretRotation, secretUsername) || util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
util.SliceContains(c.Spec.UsersWithSecretRotation, pwdUser.Name) || util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, pwdUser.Name) {
currentTime := time.Now()
// initialize password rotation setting first rotation date
@ -690,7 +690,7 @@ func (c *Cluster) syncSecrets() error {
}
if currentTime.After(nextRotationDate) {
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, pwdUser.Name) {
retentionUsers = append(retentionUsers, pwdUser.Name)
newRotationUsername := pwdUser.Name + currentTime.Format("060102")
pwdUser.MemberOf = []string{pwdUser.Name}