Skip to content

Commit 08d0c87

Browse files
authored
refactor DB instance e2e tests (#44)
Pulls out the waiter and getter functions for DB instance resources into a test/e2e/db_instance.py file, modeled after the same set of functions for the DB cluster resource. Signed-off-by: Jay Pipes <[email protected]> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent f87a283 commit 08d0c87

File tree

4 files changed

+146
-66
lines changed

4 files changed

+146
-66
lines changed

test/e2e/db_cluster.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
import boto3
2121
import pytest
2222

23-
DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS = 600
23+
DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS = 60*10
2424
DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS = 15
25-
DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS = 600
25+
DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS = 60*10
2626
DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS = 15
2727

2828
ClusterMatchFunc = typing.NewType(

test/e2e/db_instance.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 instance 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 = 60*30
24+
DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS = 15
25+
DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS = 60*10
26+
DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS = 15
27+
28+
InstanceMatchFunc = typing.NewType(
29+
'InstanceMatchFunc',
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 ('DBInstanceStatus' in record
39+
and record['DBInstanceStatus'] == self.match_on)
40+
41+
42+
def status_matches(status: str) -> InstanceMatchFunc:
43+
return StatusMatcher(status)
44+
45+
46+
def wait_until(
47+
db_instance_id: str,
48+
match_fn: InstanceMatchFunc,
49+
timeout_seconds: int = DEFAULT_WAIT_UNTIL_TIMEOUT_SECONDS,
50+
interval_seconds: int = DEFAULT_WAIT_UNTIL_INTERVAL_SECONDS,
51+
) -> None:
52+
"""Waits until a DB instance with a supplied ID is returned from the RDS API
53+
and the matching functor returns True.
54+
55+
Usage:
56+
from e2e.db_instance import wait_until, status_matches
57+
58+
wait_until(
59+
instance_id,
60+
status_matches("available"),
61+
)
62+
63+
Raises:
64+
pytest.fail upon timeout
65+
"""
66+
now = datetime.datetime.now()
67+
timeout = now + datetime.timedelta(seconds=timeout_seconds)
68+
69+
while not match_fn(get(db_instance_id)):
70+
if datetime.datetime.now() >= timeout:
71+
pytest.fail("failed to match DBInstance before timeout")
72+
time.sleep(interval_seconds)
73+
74+
75+
def wait_until_deleted(
76+
db_instance_id: str,
77+
timeout_seconds: int = DEFAULT_WAIT_UNTIL_DELETED_TIMEOUT_SECONDS,
78+
interval_seconds: int = DEFAULT_WAIT_UNTIL_DELETED_INTERVAL_SECONDS,
79+
) -> None:
80+
"""Waits until a DB instance with a supplied ID is no longer returned from
81+
the RDS API.
82+
83+
Usage:
84+
from e2e.db_instance import wait_until_deleted
85+
86+
wait_until_deleted(instance_id)
87+
88+
Raises:
89+
pytest.fail upon timeout or if the DB instance goes to any other status
90+
other than 'deleting'
91+
"""
92+
now = datetime.datetime.now()
93+
timeout = now + datetime.timedelta(seconds=timeout_seconds)
94+
95+
while True:
96+
if datetime.datetime.now() >= timeout:
97+
pytest.fail(
98+
"Timed out waiting for DB instance to be "
99+
"deleted in RDS API"
100+
)
101+
time.sleep(interval_seconds)
102+
103+
latest = get(db_instance_id)
104+
if latest is None:
105+
break
106+
107+
if latest['DBInstanceStatus'] != "deleting":
108+
pytest.fail(
109+
"Status is not 'deleting' for DB instance that was "
110+
"deleted. Status is " + latest['DBInstanceStatus']
111+
)
112+
113+
114+
def get(db_instance_id):
115+
"""Returns a dict containing the DB instance record from the RDS API.
116+
117+
If no such DB instance exists, returns None.
118+
"""
119+
c = boto3.client('rds')
120+
try:
121+
resp = c.describe_db_instances(DBInstanceIdentifier=db_instance_id)
122+
assert len(resp['DBInstances']) == 1
123+
return resp['DBInstances'][0]
124+
except c.exceptions.DBInstanceNotFoundFault:
125+
return None

test/e2e/tests/test_db_cluster.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class TestDBCluster:
4646
MUP_SEC_KEY = "master_user_password"
4747
MUP_SEC_VAL = "secretpass123456"
4848

49-
def test_create_delete_mysql_serverless(
49+
def test_crud_mysql_serverless(
5050
self,
5151
k8s_secret,
5252
):
@@ -121,7 +121,6 @@ def test_create_delete_mysql_serverless(
121121
assert latest is not None
122122
assert latest['CopyTagsToSnapshot'] == True
123123

124-
# Delete the k8s resource on teardown of the module
125124
k8s.delete_custom_resource(ref)
126125

127126
time.sleep(DELETE_WAIT_AFTER_SECONDS)

test/e2e/tests/test_db_instance.py

Lines changed: 18 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,24 @@
1414
"""Integration tests for the RDS API DBInstance resource
1515
"""
1616

17-
import datetime
18-
import logging
1917
import time
20-
from typing import Dict
2118

2219
import pytest
2320

2421
from acktest.k8s import resource as k8s
2522
from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_rds_resource
2623
from e2e.replacement_values import REPLACEMENT_VALUES
2724
from e2e.bootstrap_resources import get_bootstrap_resources
28-
from e2e.fixtures import rds_client, k8s_secret
25+
from e2e.fixtures import k8s_secret
26+
from e2e import db_instance
2927

3028
RESOURCE_PLURAL = 'dbinstances'
3129

32-
DELETE_WAIT_INTERVAL_SLEEP_SECONDS = 15
3330
DELETE_WAIT_AFTER_SECONDS = 120
34-
# NOTE(jaypipes): I've seen this take upwards of 5 minutes or more to fully see
35-
# the DB instance not appear in the DescribeDBInstances call once
36-
# DeleteDBInstance has been called (even with SkipFinalSnapshot=true)
37-
DELETE_TIMEOUT_SECONDS = 60*10
38-
39-
CREATE_INTERVAL_SLEEP_SECONDS = 15
40-
# Time to wait before we get to an expected `available` state.
41-
# NOTE(jaypipes): I have seen creation of t3-micro PG instances take more than
42-
# 20 minutes to get to `available`...
43-
CREATE_TIMEOUT_SECONDS = 60*30
31+
4432
# Time we wait after resource becoming available in RDS and checking the CR's
4533
# Status has been updated
46-
CHECK_STATUS_WAIT_SECONDS = 10
34+
CHECK_STATUS_WAIT_SECONDS = 20
4735

4836
MODIFY_WAIT_AFTER_SECONDS = 20
4937

@@ -60,10 +48,9 @@ class TestDBInstance:
6048

6149
def test_crud_postgres13_t3_micro(
6250
self,
63-
rds_client,
6451
k8s_secret,
6552
):
66-
db_id = "pg13-t3-micro"
53+
db_instance_id = "pg13-t3-micro"
6754
secret = k8s_secret(
6855
self.MUP_NS,
6956
self.MUP_SEC_NAME,
@@ -73,7 +60,7 @@ def test_crud_postgres13_t3_micro(
7360

7461
replacements = REPLACEMENT_VALUES.copy()
7562
replacements['COPY_TAGS_TO_SNAPSHOT'] = "False"
76-
replacements["DB_INSTANCE_ID"] = db_id
63+
replacements["DB_INSTANCE_ID"] = db_instance_id
7764
replacements["MASTER_USER_PASS_SECRET_NAMESPACE"] = secret.ns
7865
replacements["MASTER_USER_PASS_SECRET_NAME"] = secret.name
7966
replacements["MASTER_USER_PASS_SECRET_KEY"] = secret.key
@@ -82,12 +69,11 @@ def test_crud_postgres13_t3_micro(
8269
"db_instance_postgres13_t3_micro",
8370
additional_replacements=replacements,
8471
)
85-
logging.debug(resource_data)
8672

8773
# Create the k8s resource
8874
ref = k8s.CustomResourceReference(
8975
CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL,
90-
db_id, namespace="default",
76+
db_instance_id, namespace="default",
9177
)
9278
k8s.create_custom_resource(ref, resource_data)
9379
cr = k8s.wait_resource_consumed_by_controller(ref)
@@ -97,23 +83,10 @@ def test_crud_postgres13_t3_micro(
9783
assert 'dbInstanceStatus' in cr['status']
9884
assert cr['status']['dbInstanceStatus'] == 'creating'
9985

100-
# Let's check that the DB instance appears in RDS
101-
aws_res = rds_client.describe_db_instances(DBInstanceIdentifier=db_id)
102-
assert aws_res is not None
103-
assert len(aws_res['DBInstances']) == 1
104-
dbi_rec = aws_res['DBInstances'][0]
105-
106-
now = datetime.datetime.now()
107-
timeout = now + datetime.timedelta(seconds=CREATE_TIMEOUT_SECONDS)
108-
109-
# TODO(jaypipes): Move this into generic AWS-side waiter
110-
while dbi_rec['DBInstanceStatus'] != "available":
111-
if datetime.datetime.now() >= timeout:
112-
pytest.fail("failed to find available DBInstance before timeout")
113-
time.sleep(CREATE_INTERVAL_SLEEP_SECONDS)
114-
aws_res = rds_client.describe_db_instances(DBInstanceIdentifier=db_id)
115-
assert len(aws_res['DBInstances']) == 1
116-
dbi_rec = aws_res['DBInstances'][0]
86+
db_instance.wait_until(
87+
db_instance_id,
88+
db_instance.status_matches('available'),
89+
)
11790

11891
time.sleep(CHECK_STATUS_WAIT_SECONDS)
11992

@@ -134,38 +107,21 @@ def test_crud_postgres13_t3_micro(
134107
# We're now going to modify the CopyTagsToSnapshot field of the DB
135108
# instance, wait some time and verify that the RDS server-side resource
136109
# shows the new value of the field.
137-
assert dbi_rec['CopyTagsToSnapshot'] == False
110+
latest = db_instance.get(db_instance_id)
111+
assert latest is not None
112+
assert latest['CopyTagsToSnapshot'] == False
138113
updates = {
139114
"spec": {"copyTagsToSnapshot": True},
140115
}
141116
k8s.patch_custom_resource(ref, updates)
142117
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
143118

144-
aws_res = rds_client.describe_db_instances(DBInstanceIdentifier=db_id)
145-
assert aws_res is not None
146-
assert len(aws_res['DBInstances']) == 1
147-
dbi_rec = aws_res['DBInstances'][0]
148-
assert dbi_rec['CopyTagsToSnapshot'] == True
119+
latest = db_instance.get(db_instance_id)
120+
assert latest is not None
121+
assert latest['CopyTagsToSnapshot'] == True
149122

150-
# Delete the k8s resource on teardown of the module
151123
k8s.delete_custom_resource(ref)
152124

153125
time.sleep(DELETE_WAIT_AFTER_SECONDS)
154126

155-
now = datetime.datetime.now()
156-
timeout = now + datetime.timedelta(seconds=DELETE_TIMEOUT_SECONDS)
157-
158-
# DB instance should no longer appear in RDS
159-
while True:
160-
if datetime.datetime.now() >= timeout:
161-
pytest.fail("Timed out waiting for DB instance to being deleted in RDS API")
162-
time.sleep(DELETE_WAIT_INTERVAL_SLEEP_SECONDS)
163-
164-
try:
165-
aws_res = rds_client.describe_db_instances(DBInstanceIdentifier=db_id)
166-
assert len(aws_res['DBInstances']) == 1
167-
dbi_rec = aws_res['DBInstances'][0]
168-
if dbi_rec['DBInstanceStatus'] != "deleting":
169-
pytest.fail("DBInstanceStatus is not 'deleting' for DB instance that was deleted. DBInstanceStatus is "+dbi_rec['DBInstanceStatus'])
170-
except rds_client.exceptions.DBInstanceNotFoundFault:
171-
break
127+
db_instance.wait_until_deleted(db_instance_id)

0 commit comments

Comments
 (0)