Skip to content

Commit f14c766

Browse files
authored
Assert expected spec with kubernetes resources and all fields of aws resources (#65)
Issue #, if available: Current test framework only checks whether ResourceSynced is true or not for Kubernetes resource. It doesn't check spec of Kubernetes resource, and it doesn't check aws resource. Description of changes: 1. Add a comparison between expectations and Spec of Kubernetes resources to assert. 2. Add a comparison between expectations and MemoryDB resources to assert. 3. Modify test files to fit new test framework. 4. Fix issue 1 and 2. 5. Comment issue 3 and will create a new PR to investigate. Note: Add custom logic of comparison to tags, parameterNameValues, clusterName, subnetIDs. New custom logic also shows details if assertError happens, which is easier for debugging. Issues caught by latest test framework. 1. MemoryDB doesn't support parameter group creation with parameterNameValues change. 2. Controller only reset parameters in parameter group to default when desired parameters have empty value. Controller doesn't update values of parameters. 3. Tags for cluster and snapshot cannot be updated. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent b2b458a commit f14c766

29 files changed

+861
-133
lines changed

apis/v1alpha1/ack-generate-metadata.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ack_generate_info:
2-
build_date: "2023-02-09T23:37:56Z"
2+
build_date: "2023-02-23T20:04:23Z"
33
build_hash: d0f3d78cbea8061f822cbceac3786128f091efe6
44
go_version: go1.19
55
version: v0.24.2

pkg/resource/parameter_group/sdk.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/hooks/parametergroup/sdk_update_pre_build_request.go.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
}
1414
}
1515

16-
if !delta.DifferentExcept("Spec.Tags", "Spec.ParameterNameValues") {
16+
if !delta.DifferentExcept("Spec.Tags") {
1717
return desired, nil
1818
}

test/e2e/declarative_test_fwk/helper.py

Lines changed: 149 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from e2e.declarative_test_fwk import model
1818

19+
import boto3
1920
import logging
2021
from typing import Tuple
2122
from time import sleep
@@ -56,6 +57,9 @@ class ResourceHelper:
5657

5758
DEFAULT_WAIT_SECS = 30
5859

60+
def __init__(self):
61+
self.mdb = boto3.client("memorydb")
62+
5963
def create(self, input_data: dict, input_replacements: dict = {}) -> Tuple[k8s.CustomResourceReference, dict]:
6064
"""Creates custom resource inside Kubernetes cluster per the specifications in input data.
6165
@@ -110,21 +114,36 @@ def delete(self, reference: k8s.CustomResourceReference) -> None:
110114
sleep(self.DEFAULT_WAIT_SECS)
111115
self.wait_for_delete(reference) # throws exception if wait fails
112116

113-
def assert_expectations(self, verb: str, input_data: dict, expectations: model.ExpectDict, reference: k8s.CustomResourceReference) -> None:
114-
"""Asserts custom resource reference inside Kubernetes cluster against the supplied expectations
117+
def assert_k8s_resource(self, expectations_k8s: model.ExpectK8SDict, reference: k8s.CustomResourceReference) -> None:
118+
"""Compare the supplied expectations with custom resource reference inside Kubernetes cluster
115119
116-
:param verb: expectations after performing the verb (apply, patch, delete)
117-
:param input_data: input data to verb
118-
:param expectations: expectations to assert
120+
:param expectations_k8s: expectations to assert
119121
:param reference: custom resource reference
122+
120123
:return: None
121124
"""
122-
self._assert_conditions(expectations, reference, wait=False)
123-
# conditions expectations met, now check current resource against expectations
125+
self._assert_conditions(expectations_k8s, reference, wait=False)
126+
# conditions expectations met, now check current resource against expectations_k8s
124127
resource = k8s.get_resource(reference)
125-
self.assert_items(expectations.get("status"), resource.get("status"))
128+
# status and spec assertions with resources from Kubernetes
129+
self.assert_items_k8s(expectations_k8s.get("status"), resource.get("status"))
130+
self.assert_items_k8s(expectations_k8s.get("spec"), resource.get("spec"))
126131

127-
# self._assert_state(expectations.get("spec"), resource) # uncomment to support spec assertions
132+
def assert_aws_resource(self, expectations_aws: dict, reference: k8s.CustomResourceReference) -> None:
133+
"""Compare the supplied expectations with aws resource from api calls by boto3
134+
135+
Args:
136+
expectations_aws: expectations to assert
137+
reference: custom resource reference
138+
139+
Returns:
140+
None
141+
"""
142+
resource = k8s.get_resource(reference)
143+
# assertions with resources from boto3
144+
# spec of k8s resource here is for getting name of resource
145+
# boto3 call APIs using name of resource
146+
self.assert_items_aws(expectations_aws, resource.get("spec"))
128147

129148
def wait_for(self, wait_expectations: dict, reference: k8s.CustomResourceReference) -> None:
130149
"""Waits for custom resource reference details inside Kubernetes cluster to match supplied config,
@@ -157,9 +176,9 @@ def _assert_conditions(self, expectations: dict, reference: k8s.CustomResourceRe
157176
assert k8s.wait_on_condition(reference, condition_name, expected_value,
158177
wait_periods=default_wait_periods, period_length=default_period_length)
159178
else:
160-
actual_condition = k8s.get_resource_condition(reference, condition_name)
161-
assert actual_condition is not None
162-
assert expected_value == actual_condition.get("status"), f"Condition status mismatch. Expected condition: {condition_name} - {expected_value} but found {actual_condition}"
179+
k8s_resource_condition = k8s.get_resource_condition(reference, condition_name)
180+
assert k8s_resource_condition is not None
181+
assert expected_value == k8s_resource_condition.get("status"), f"Condition status mismatch. Expected condition: {condition_name} - {expected_value} but found {k8s_resource_condition}"
163182

164183
elif type(expected_value) is dict:
165184
# Example:
@@ -176,23 +195,84 @@ def _assert_conditions(self, expectations: dict, reference: k8s.CustomResourceRe
176195
assert k8s.wait_on_condition(reference, condition_name, condition_value,
177196
wait_periods=wait_timeout, period_length=default_period_length)
178197

179-
actual_condition = k8s.get_resource_condition(reference,
198+
k8s_resource_condition = k8s.get_resource_condition(reference,
180199
condition_name)
181-
assert actual_condition is not None
182-
assert condition_value == actual_condition.get("status"), f"Condition status mismatch. Expected condition: {condition_name} - {expected_value} but found {actual_condition}"
200+
assert k8s_resource_condition is not None
201+
assert condition_value == k8s_resource_condition.get("status"), f"Condition status mismatch. Expected condition: {condition_name} - {expected_value} but found {k8s_resource_condition}"
183202
if condition_message is not None:
184-
assert condition_message == actual_condition.get("message"), f"Condition message mismatch. Expected condition: {condition_name} - {expected_value} but found {actual_condition}"
203+
assert condition_message == k8s_resource_condition.get("message"), f"Condition message mismatch. Expected condition: {condition_name} - {expected_value} but found {k8s_resource_condition}"
185204

186205
else:
187206
raise Exception(f"Condition {condition_name} is provided with invalid value: {expected_value} ")
188207

189-
def assert_items(self, expectations: dict, state: dict) -> None:
190-
"""Asserts state against supplied expectations
191-
Override it as needed for custom verifications
208+
def assert_items_aws(self, expectations: dict, k8s_resource: dict) -> None:
209+
"""Asserts boto3 response against supplied expectations
192210
193211
Args:
194-
expectations: dictionary with items (expected) to assert in state
195-
state: dictionary with items (actual)
212+
expectations: dictionary with items (expected) to assert in k8s resource
213+
k8s_resource: the current status/spec of the k8s resource
214+
215+
Returns:
216+
None
217+
"""
218+
if not expectations:
219+
# nothing to assert as there are no expectations
220+
return
221+
if not k8s_resource:
222+
# there are expectations but no given k8s resource state to validate
223+
# following assert will fail and assert introspection will provide useful information for debugging
224+
assert expectations == k8s_resource
225+
226+
resource_name = ""
227+
for (key, value) in k8s_resource.items():
228+
if key == "name":
229+
resource_name = value
230+
# get aws resource
231+
aws_resource = self.get_aws_resource(resource_name)
232+
latestTags = self.mdb.list_tags(ResourceArn=aws_resource.get("ARN"))['TagList']
233+
234+
for (key, value) in expectations.items():
235+
if key == "Tags":
236+
self.assert_tags(value, latestTags)
237+
continue
238+
# validation for specific fields
239+
if self.assert_extra_items_aws(key, value, aws_resource):
240+
continue
241+
assert value == aws_resource.get(key)
242+
243+
def get_aws_resource(self, resource_name):
244+
"""get aws resource from boto3
245+
Override it for each resource type
246+
247+
Args:
248+
resource_name: the name of resource to use for calling memorydb apis
249+
250+
Returns:
251+
aws resource
252+
"""
253+
return
254+
255+
def assert_extra_items_aws(self, expected_name, expected_value, aws_resource) -> bool:
256+
"""Asserts specific fields that boto3 response against supplied expectation
257+
Override it as needed for validations of specific field
258+
259+
Args:
260+
expected_name: expected name of a specific field to assert in aws resource
261+
expected_value: expected value of a specific field to assert in aws resource
262+
aws_resource: specific resource from aws memorydb
263+
264+
Returns:
265+
The return value. If resource has expected_name as a field, execute custom logic to validate that specific
266+
field and return true. Otherwise, if resource doesn't have expected_name as a field, return false.
267+
"""
268+
return False
269+
270+
def assert_items_k8s(self, expectations: dict, k8s_resource: dict) -> None:
271+
"""Asserts k8s resource against supplied expectations
272+
273+
Args:
274+
expectations: dictionary with items (expected) to assert in k8s_resource
275+
k8s_resource: the current status/spec of the k8s resource
196276
197277
Returns:
198278
None
@@ -201,16 +281,60 @@ def assert_items(self, expectations: dict, state: dict) -> None:
201281
if not expectations:
202282
# nothing to assert as there are no expectations
203283
return
204-
if not state:
205-
# there are expectations but no given state to validate
284+
if not k8s_resource:
285+
# there are expectations but no given k8s_resource state to validate
206286
# following assert will fail and assert introspection will provide useful information for debugging
207-
assert expectations == state
287+
assert expectations == k8s_resource
208288

209289
for (key, value) in expectations.items():
210290
# conditions are processed separately
211291
if key == "conditions":
212292
continue
213-
assert (key, value) == (key, state.get(key))
293+
# tags are processed by custom logic
294+
if key == "tags":
295+
self.assert_tags(value, k8s_resource.get(key))
296+
continue
297+
# assert any specific extra fields
298+
if self.assert_extra_items_k8s(key, value, k8s_resource):
299+
continue
300+
assert value == k8s_resource.get(key)
301+
302+
def assert_extra_items_k8s(self, expected_name, expected_value, k8s_resource) -> bool:
303+
"""Asserts extra fields from k8s resource against fields from expectations
304+
Override it as needed for custom verifications
305+
306+
Args:
307+
expected_name: expected name of a specific field to assert in k8s resource
308+
expected_value: expected value of a specific field to assert in k8s resource
309+
k8s_resource: the current status/spec of the k8s resource
310+
311+
Returns:
312+
The return value. If resource has expected_name as a field, execute custom logic to validate that specific
313+
field and return true. Otherwise, if resource doesn't have expected_name as a field, return false.
314+
"""
315+
return False
316+
317+
def assert_tags(self, expected_tags, latest_tags):
318+
"""Asserts tags from resource against tags from expectations
319+
If latest_tags contains all tags in expected_tags, no AssertError.
320+
If any tag in expected_tags is not in latest_tags, return an AssertError
321+
322+
Args:
323+
expected_tags: tags from expectations
324+
latest_tags: tags from resource
325+
326+
Returns:
327+
None
328+
"""
329+
for expected_tag in expected_tags:
330+
tag_exist = False
331+
for latest_tag in latest_tags:
332+
if expected_tag == latest_tag:
333+
tag_exist = True
334+
break
335+
if tag_exist:
336+
continue
337+
assert expected_tag == None
214338

215339
def custom_resource_reference(self, input_data: dict, input_replacements: dict = {}) -> k8s.CustomResourceReference:
216340
"""Helper method to provide k8s.CustomResourceReference for supplied input

test/e2e/declarative_test_fwk/model.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,11 @@ class DeleteDict(ResourceDict):
5353
pass
5454

5555

56-
# fields for 'expect' field in a test step
57-
class ExpectDict(TypedDict, total=False):
56+
# fields for 'expect_k8s' field in a test step
57+
class ExpectK8SDict(TypedDict, total=False):
5858
spec: Dict
5959
status: Dict
6060

61-
6261
# fields in a test step
6362
class StepDict(TypedDict, total=False):
6463
id: str
@@ -67,7 +66,8 @@ class StepDict(TypedDict, total=False):
6766
patch: Union[str, PatchDict]
6867
delete: Union[str, DeleteDict]
6968
wait: Union[int, Dict]
70-
expect: ExpectDict
69+
expect_k8s: ExpectK8SDict
70+
expect_aws: Dict
7171

7272

7373
class Step:
@@ -82,7 +82,8 @@ def __init__(self, resource_directory: Path, config: StepDict, custom_resource_d
8282

8383
self.verb = None
8484
self.input_data = {}
85-
self.expectations: ExpectDict = None
85+
self.expectations_k8s: ExpectK8SDict = None
86+
self.expectations_aws: Dict = None
8687

8788
# (k8s.CustomResourceReference, ko) to teardown
8889
self.teardown_list = []
@@ -125,7 +126,8 @@ def __init__(self, resource_directory: Path, config: StepDict, custom_resource_d
125126
self.input_data = {**self.custom_resource_details, **self.input_data}
126127

127128
self.wait = self.config.get("wait")
128-
self.expectations = self.config.get("expect")
129+
self.expectations_k8s = self.config.get("expect_k8s")
130+
self.expectations_aws = self.config.get("expect_aws")
129131

130132
@property
131133
def id(self) -> str:

test/e2e/declarative_test_fwk/runner.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ def run_step(step: model.Step) -> None:
8383
elif step.verb == model.Verb.delete:
8484
delete_resource(step)
8585
wait(step)
86-
assert_expectations(step)
86+
assert_k8s_resource(step)
87+
assert_aws_resource(step)
8788

8889

8990
def create_resource(step: model.Step) -> None:
@@ -182,8 +183,8 @@ def wait(step: model.Step) -> None:
182183
raise e
183184

184185

185-
def assert_expectations(step: model.Step) -> None:
186-
"""Asserts expectations as specified in the Step.
186+
def assert_k8s_resource(step: model.Step) -> None:
187+
"""Asserts k8s resource in the Step.
187188
188189
Args:
189190
step: test step
@@ -192,18 +193,39 @@ def assert_expectations(step: model.Step) -> None:
192193
None
193194
"""
194195

195-
logging.info(f"assert: {step}")
196-
if not step.expectations:
196+
logging.debug(f"assert: {step}")
197+
if not step.expectations_k8s:
197198
return
198199

199200
resource_helper = helper.get_resource_helper(step.resource_kind)
200201
reference = resource_helper.custom_resource_reference(step.input_data, step.replacements)
201202
try:
202-
resource_helper.assert_expectations(step.verb, step.input_data, step.expectations, reference)
203+
resource_helper.assert_k8s_resource(step.expectations_k8s, reference)
203204
except AssertionError as ae:
204205
logging.error(f"AssertionError at {step}")
205206
raise ae
206207

208+
def assert_aws_resource(step: model.Step) -> None:
209+
"""Asserts aws resource in the Step.
210+
211+
Args:
212+
step: test step
213+
214+
Returns:
215+
None
216+
"""
217+
218+
logging.debug(f"assert: {step}")
219+
if not step.expectations_aws:
220+
return
221+
222+
resource_helper = helper.get_resource_helper(step.resource_kind)
223+
reference = resource_helper.custom_resource_reference(step.input_data, step.replacements)
224+
try:
225+
resource_helper.assert_aws_resource(step.expectations_aws, reference)
226+
except AssertionError as ae:
227+
logging.error(f"AssertionError at {step}")
228+
raise ae
207229

208230
def teardown_step(step: model.Step) -> None:
209231
"""Teardown custom resources that were created during step execution (run) inside Kubernetes cluster.

0 commit comments

Comments
 (0)