mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-18 15:56:35 +02:00
add e2e test
This commit is contained in:
@@ -551,6 +551,16 @@ spec:
|
|||||||
- SUPERUSER
|
- SUPERUSER
|
||||||
- nosuperuser
|
- nosuperuser
|
||||||
- NOSUPERUSER
|
- NOSUPERUSER
|
||||||
|
usersWithPasswordRotation:
|
||||||
|
type: array
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
usersWithInPlacePasswordRotation:
|
||||||
|
type: array
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
volume:
|
volume:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -329,7 +329,9 @@ manifest. The rotation and retention intervals can only be configured globally.
|
|||||||
|
|
||||||
```
|
```
|
||||||
spec:
|
spec:
|
||||||
usersWithSecretRotation: "foo_user,bar_reader_user"
|
usersWithSecretRotation:
|
||||||
|
- foo_user
|
||||||
|
- bar_reader_user
|
||||||
```
|
```
|
||||||
|
|
||||||
### Password replacement without extra roles
|
### Password replacement without extra roles
|
||||||
@@ -342,7 +344,9 @@ manifest:
|
|||||||
|
|
||||||
```
|
```
|
||||||
spec:
|
spec:
|
||||||
usersWithInPlaceSecretRotation: "flyway,bar_owner_user"
|
usersWithInPlaceSecretRotation:
|
||||||
|
- flyway
|
||||||
|
- bar_owner_user
|
||||||
```
|
```
|
||||||
|
|
||||||
This would be the recommended option to enable rotation in secrets of database
|
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'):
|
def get_cluster_replica_pod(self, labels='application=spilo,cluster-name=acid-minimal-cluster', namespace='default'):
|
||||||
return self.get_cluster_pod('replica', labels, namespace)
|
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:
|
class K8sBase:
|
||||||
'''
|
'''
|
||||||
|
|||||||
+86
-3
@@ -4,8 +4,9 @@ import time
|
|||||||
import timeout_decorator
|
import timeout_decorator
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
import base64
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, date, timedelta
|
||||||
from kubernetes import client
|
from kubernetes import client
|
||||||
|
|
||||||
from tests.k8s_api import K8s
|
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
|
but lets pods run with the old image until they are recreated for
|
||||||
reasons other than operator's activity. That works because the operator
|
reasons other than operator's activity. That works because the operator
|
||||||
configures stateful sets to use "onDelete" pod update policy.
|
configures stateful sets to use "onDelete" pod update policy.
|
||||||
|
|
||||||
The test covers:
|
The test covers:
|
||||||
1) enabling lazy upgrade in existing operator deployment
|
1) enabling lazy upgrade in existing operator deployment
|
||||||
2) forcing the normal rolling upgrade by changing the operator
|
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
|
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
|
(b) update the cluster-wide image for the logical backup pod
|
||||||
(c) delete the job at user request
|
(c) delete the job at user request
|
||||||
|
|
||||||
Limitations:
|
Limitations:
|
||||||
(a) Does not run the actual batch job because there is no S3 mock to upload backups to
|
(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
|
(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"),
|
self.eventuallyEqual(lambda: k8s.count_running_pods("connection-pooler=acid-minimal-cluster-pooler"),
|
||||||
0, "Pooler pods not scaled down")
|
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)
|
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
|
||||||
def test_patroni_config_update(self):
|
def test_patroni_config_update(self):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -549,6 +549,16 @@ spec:
|
|||||||
- SUPERUSER
|
- SUPERUSER
|
||||||
- nosuperuser
|
- nosuperuser
|
||||||
- NOSUPERUSER
|
- NOSUPERUSER
|
||||||
|
usersWithPasswordRotation:
|
||||||
|
type: array
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
usersWithInPlacePasswordRotation:
|
||||||
|
type: array
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
volume:
|
volume:
|
||||||
type: object
|
type: object
|
||||||
required:
|
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": {
|
"volume": {
|
||||||
Type: "object",
|
Type: "object",
|
||||||
Required: []string{"size"},
|
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
|
// check if users need to be synced
|
||||||
// initUsers. Check if it needs to be called.
|
|
||||||
sameUsers := reflect.DeepEqual(oldSpec.Spec.Users, newSpec.Spec.Users) &&
|
sameUsers := reflect.DeepEqual(oldSpec.Spec.Users, newSpec.Spec.Users) &&
|
||||||
reflect.DeepEqual(oldSpec.Spec.PreparedDatabases, newSpec.Spec.PreparedDatabases)
|
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) ||
|
needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) ||
|
||||||
needReplicaConnectionPoolerWorker(&newSpec.Spec)
|
needReplicaConnectionPoolerWorker(&newSpec.Spec)
|
||||||
if !sameUsers || needConnectionPooler {
|
|
||||||
|
if !sameUsers || !sameRotatedUsers || needConnectionPooler {
|
||||||
c.logger.Debugf("initialize users")
|
c.logger.Debugf("initialize users")
|
||||||
if err := c.initUsers(); err != nil {
|
if err := c.initUsers(); err != nil {
|
||||||
c.logger.Errorf("could not init users: %v", err)
|
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)
|
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 {
|
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)
|
userCreationDate, err := time.Parse("060102", dateSuffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorf("could not parse creation date suffix of user %q: %v", rotatedUser, err)
|
c.logger.Errorf("could not parse creation date suffix of user %q: %v", rotatedUser, err)
|
||||||
|
|||||||
+2
-2
@@ -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 password rotation is enabled update password and username if rotation interval has been passed
|
||||||
if (c.OpConfig.EnablePasswordRotation && pwdUser.Origin != spec.RoleOriginInfrastructure && !pwdUser.IsDbOwner) ||
|
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()
|
currentTime := time.Now()
|
||||||
|
|
||||||
// initialize password rotation setting first rotation date
|
// initialize password rotation setting first rotation date
|
||||||
@@ -690,7 +690,7 @@ func (c *Cluster) syncSecrets() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if currentTime.After(nextRotationDate) {
|
if currentTime.After(nextRotationDate) {
|
||||||
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
|
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, pwdUser.Name) {
|
||||||
retentionUsers = append(retentionUsers, pwdUser.Name)
|
retentionUsers = append(retentionUsers, pwdUser.Name)
|
||||||
newRotationUsername := pwdUser.Name + currentTime.Format("060102")
|
newRotationUsername := pwdUser.Name + currentTime.Format("060102")
|
||||||
pwdUser.MemberOf = []string{pwdUser.Name}
|
pwdUser.MemberOf = []string{pwdUser.Name}
|
||||||
|
|||||||
Reference in New Issue
Block a user