add e2e test
This commit is contained in:
parent
08542711e0
commit
58280809d8
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue