Skip to content

Commit f87a283

Browse files
authored
refinement of e2e tests (#43)
This PR includes two patches that provide enhancements and simplifications to the e2e testing for the RDS controller. The first enhancement is to separate the Kubernetes secret creation and management into a pytest fixture that is importable from the test modules. The second enhancement pulls the waiter and getter functionality for DB clusters out of the test module and into a separate importable module. This will be important for later patches where I need to test *both* DB cluster and DB instances in the same file and don't want to copy/paste a bunch of boilerplate waiter/getter code between test modules. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent dc3271f commit f87a283

File tree

4 files changed

+236
-94
lines changed

4 files changed

+236
-94
lines changed

test/e2e/db_cluster.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
# not use this file except in compliance with the License. A copy of the
5+
# License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is distributed
10+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
# express or implied. See the License for the specific language governing
12+
# permissions and limitations under the License.
13+
14+
"""Utilities for working with DB cluster resources"""
15+
16+
import datetime
17+
import time
18+
import typing
19+
20+
import boto3
21+
import pytest
22+
23+
DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS = 600
24+
DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS = 15
25+
DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS = 600
26+
DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS = 15
27+
28+
ClusterMatchFunc = typing.NewType(
29+
'ClusterMatchFunc',
30+
typing.Callable[[dict], bool],
31+
)
32+
33+
class StatusMatcher:
34+
def __init__(self, status):
35+
self.match_on = status
36+
37+
def __call__(self, record: dict) -> bool:
38+
return 'Status' in record and record['Status'] == self.match_on
39+
40+
41+
def status_matches(status: str) -> ClusterMatchFunc:
42+
return StatusMatcher(status)
43+
44+
45+
def wait_until(
46+
db_cluster_id: str,
47+
match_fn: ClusterMatchFunc,
48+
timeout_seconds: int = DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS,
49+
interval_seconds: int = DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS,
50+
) -> None:
51+
"""Waits until a DB cluster with a supplied ID is returned from the RDS API
52+
and the matching functor returns True.
53+
54+
Usage:
55+
from e2e.db_cluster import wait_until, status_matches
56+
57+
wait_until(
58+
cluster_id,
59+
status_matches("available"),
60+
)
61+
62+
Raises:
63+
pytest.fail upon timeout
64+
"""
65+
now = datetime.datetime.now()
66+
timeout = now + datetime.timedelta(seconds=timeout_seconds)
67+
68+
while not match_fn(get(db_cluster_id)):
69+
if datetime.datetime.now() >= timeout:
70+
pytest.fail("failed to match DBCluster before timeout")
71+
time.sleep(interval_seconds)
72+
73+
74+
def wait_until_deleted(
75+
db_cluster_id: str,
76+
timeout_seconds: int = DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS,
77+
interval_seconds: int = DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS,
78+
) -> None:
79+
"""Waits until a DB cluster with a supplied ID is no longer returned from
80+
the RDS API.
81+
82+
Usage:
83+
from e2e.db_cluster import wait_until_deleted
84+
85+
wait_until_deleted(cluster_id)
86+
87+
Raises:
88+
pytest.fail upon timeout or if the DB cluster goes to any other status
89+
other than 'deleting'
90+
"""
91+
now = datetime.datetime.now()
92+
timeout = now + datetime.timedelta(seconds=timeout_seconds)
93+
94+
while True:
95+
if datetime.datetime.now() >= timeout:
96+
pytest.fail(
97+
"Timed out waiting for DB cluster to be "
98+
"deleted in RDS API"
99+
)
100+
time.sleep(interval_seconds)
101+
102+
latest = get(db_cluster_id)
103+
if latest is None:
104+
break
105+
106+
if latest['Status'] != "deleting":
107+
pytest.fail(
108+
"Status is not 'deleting' for DB cluster that was "
109+
"deleted. Status is " + latest['Status']
110+
)
111+
112+
113+
def get(db_cluster_id):
114+
"""Returns a dict containing the DB cluster record from the RDS API.
115+
116+
If no such DB cluster exists, returns None.
117+
"""
118+
c = boto3.client('rds')
119+
try:
120+
resp = c.describe_db_clusters(DBClusterIdentifier=db_cluster_id)
121+
assert len(resp['DBClusters']) == 1
122+
return resp['DBClusters'][0]
123+
except c.exceptions.DBClusterNotFoundFault:
124+
return None

test/e2e/fixtures.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
# not use this file except in compliance with the License. A copy of the
5+
# License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is distributed
10+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
# express or implied. See the License for the specific language governing
12+
# permissions and limitations under the License.
13+
14+
"""Fixtures common to all RDS controller tests"""
15+
16+
import dataclasses
17+
18+
from acktest.k8s import resource as k8s
19+
import boto3
20+
import pytest
21+
22+
23+
@pytest.fixture(scope="module")
24+
def rds_client():
25+
return boto3.client('rds')
26+
27+
28+
@dataclasses.dataclass
29+
class SecretKeyReference:
30+
ns: str
31+
name: str
32+
key: str
33+
val: str
34+
35+
36+
@pytest.fixture(scope="module")
37+
def k8s_secret():
38+
"""Manages the lifecycle of a Kubernetes Secret for use in tests.
39+
40+
Usage:
41+
from e2e.fixtures import k8s_secret
42+
43+
class TestThing:
44+
def test_thing(self, k8s_secret):
45+
secret = k8s_secret(
46+
"default", "mysecret", "mykey", "myval",
47+
)
48+
"""
49+
created = []
50+
def _k8s_secret(ns, name, key, val):
51+
k8s.create_opaque_secret(ns, name, key, val)
52+
secret_ref = SecretKeyReference(ns, name, key, val)
53+
created.append(secret_ref)
54+
return secret_ref
55+
56+
yield _k8s_secret
57+
58+
for secret_ref in created:
59+
k8s.delete_secret(secret_ref.ns, secret_ref.name)

test/e2e/tests/test_db_cluster.py

Lines changed: 31 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,69 +14,58 @@
1414
"""Integration tests for the RDS API DBCluster resource
1515
"""
1616

17-
import boto3
18-
import datetime
19-
import logging
2017
import time
21-
from typing import Dict
2218

2319
import pytest
2420

2521
from acktest.k8s import resource as k8s
2622
from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_rds_resource
2723
from e2e.replacement_values import REPLACEMENT_VALUES
2824
from e2e.bootstrap_resources import get_bootstrap_resources
25+
from e2e.fixtures import k8s_secret
26+
from e2e import db_cluster
2927

3028
RESOURCE_PLURAL = 'dbclusters'
3129

32-
DELETE_WAIT_INTERVAL_SLEEP_SECONDS = 15
3330
DELETE_WAIT_AFTER_SECONDS = 120
34-
DELETE_TIMEOUT_SECONDS = 600
3531

36-
CREATE_INTERVAL_SLEEP_SECONDS = 15
37-
CREATE_TIMEOUT_SECONDS = 600
3832
# Time we wait after resource becoming available in RDS and checking the CR's
3933
# Status has been updated
40-
CHECK_STATUS_WAIT_SECONDS = 10
34+
CHECK_STATUS_WAIT_SECONDS = 20
4135

4236
MODIFY_WAIT_AFTER_SECONDS = 20
4337

4438

45-
@pytest.fixture(scope="module")
46-
def rds_client():
47-
return boto3.client('rds')
48-
49-
50-
@pytest.fixture(scope="module")
51-
def master_user_pass_secret():
52-
ns = "default"
53-
name = "dbclustersecrets"
54-
key = "master_user_password"
55-
secret_val = "secretpass123456"
56-
k8s.create_opaque_secret(ns, name, key, secret_val)
57-
yield ns, name, key
58-
k8s.delete_secret(ns, name)
59-
60-
6139
@service_marker
6240
@pytest.mark.canary
6341
class TestDBCluster:
42+
43+
# MUP == Master user password...
44+
MUP_NS = "default"
45+
MUP_SEC_NAME = "dbclustersecrets"
46+
MUP_SEC_KEY = "master_user_password"
47+
MUP_SEC_VAL = "secretpass123456"
48+
6449
def test_create_delete_mysql_serverless(
6550
self,
66-
rds_client,
67-
master_user_pass_secret,
51+
k8s_secret,
6852
):
6953
db_cluster_id = "my-aurora-mysql"
7054
db_name = "mydb"
71-
mup_sec_ns, mup_sec_name, mup_sec_key = master_user_pass_secret
55+
secret = k8s_secret(
56+
self.MUP_NS,
57+
self.MUP_SEC_NAME,
58+
self.MUP_SEC_KEY,
59+
self.MUP_SEC_VAL,
60+
)
7261

7362
replacements = REPLACEMENT_VALUES.copy()
7463
replacements['COPY_TAGS_TO_SNAPSHOT'] = "False"
7564
replacements["DB_CLUSTER_ID"] = db_cluster_id
7665
replacements["DB_NAME"] = db_name
77-
replacements["MASTER_USER_PASS_SECRET_NAMESPACE"] = mup_sec_ns
78-
replacements["MASTER_USER_PASS_SECRET_NAME"] = mup_sec_name
79-
replacements["MASTER_USER_PASS_SECRET_KEY"] = mup_sec_key
66+
replacements["MASTER_USER_PASS_SECRET_NAMESPACE"] = secret.ns
67+
replacements["MASTER_USER_PASS_SECRET_NAME"] = secret.name
68+
replacements["MASTER_USER_PASS_SECRET_KEY"] = secret.key
8069

8170
resource_data = load_rds_resource(
8271
"db_cluster_mysql_serverless",
@@ -95,23 +84,10 @@ def test_create_delete_mysql_serverless(
9584
assert 'status' in cr['status']
9685
assert cr['status']['status'] == 'creating'
9786

98-
# Let's check that the DB cluster appears in RDS
99-
aws_res = rds_client.describe_db_clusters(DBClusterIdentifier=db_cluster_id)
100-
assert aws_res is not None
101-
assert len(aws_res['DBClusters']) == 1
102-
dbc_rec = aws_res['DBClusters'][0]
103-
104-
now = datetime.datetime.now()
105-
timeout = now + datetime.timedelta(seconds=CREATE_TIMEOUT_SECONDS)
106-
107-
# TODO(jaypipes): Move this into generic AWS-side waiter
108-
while dbc_rec['Status'] != "available":
109-
if datetime.datetime.now() >= timeout:
110-
pytest.fail("failed to find available DBCluster before timeout")
111-
time.sleep(CREATE_INTERVAL_SLEEP_SECONDS)
112-
aws_res = rds_client.describe_db_clusters(DBClusterIdentifier=db_cluster_id)
113-
assert len(aws_res['DBClusters']) == 1
114-
dbc_rec = aws_res['DBClusters'][0]
87+
db_cluster.wait_until(
88+
db_cluster_id,
89+
db_cluster.status_matches('available'),
90+
)
11591

11692
time.sleep(CHECK_STATUS_WAIT_SECONDS)
11793

@@ -132,38 +108,22 @@ def test_create_delete_mysql_serverless(
132108
# We're now going to modify the CopyTagsToSnapshot field of the DB
133109
# instance, wait some time and verify that the RDS server-side resource
134110
# shows the new value of the field.
135-
assert dbc_rec['CopyTagsToSnapshot'] == False
111+
latest = db_cluster.get(db_cluster_id)
112+
assert latest is not None
113+
assert latest['CopyTagsToSnapshot'] == False
136114
updates = {
137115
"spec": {"copyTagsToSnapshot": True},
138116
}
139117
k8s.patch_custom_resource(ref, updates)
140118
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
141119

142-
aws_res = rds_client.describe_db_clusters(DBClusterIdentifier=db_cluster_id)
143-
assert aws_res is not None
144-
assert len(aws_res['DBClusters']) == 1
145-
dbc_rec = aws_res['DBClusters'][0]
146-
assert dbc_rec['CopyTagsToSnapshot'] == True
120+
latest = db_cluster.get(db_cluster_id)
121+
assert latest is not None
122+
assert latest['CopyTagsToSnapshot'] == True
147123

148124
# Delete the k8s resource on teardown of the module
149125
k8s.delete_custom_resource(ref)
150126

151127
time.sleep(DELETE_WAIT_AFTER_SECONDS)
152128

153-
now = datetime.datetime.now()
154-
timeout = now + datetime.timedelta(seconds=DELETE_TIMEOUT_SECONDS)
155-
156-
# DB instance should no longer appear in RDS
157-
while True:
158-
if datetime.datetime.now() >= timeout:
159-
pytest.fail("Timed out waiting for DB cluster to being deleted in RDS API")
160-
time.sleep(DELETE_WAIT_INTERVAL_SLEEP_SECONDS)
161-
162-
try:
163-
aws_res = rds_client.describe_db_clusters(DBClusterIdentifier=db_cluster_id)
164-
assert len(aws_res['DBClusters']) == 1
165-
dbc_rec = aws_res['DBClusters'][0]
166-
if dbc_rec['Status'] != "deleting":
167-
pytest.fail("Status is not 'deleting' for DB cluster that was deleted. Status is "+dbc_rec['Status'])
168-
except rds_client.exceptions.DBClusterNotFoundFault:
169-
break
129+
db_cluster.wait_until_deleted(db_cluster_id)

0 commit comments

Comments
 (0)