Skip to content

Commit f1a165e

Browse files
authored
Implement canary file generation functionality from contract test inp… (#1069)
1 parent 4a8d9a0 commit f1a165e

File tree

3 files changed

+425
-2
lines changed

3 files changed

+425
-2
lines changed

src/rpdk/core/generate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def generate(args):
2020
args.profile,
2121
)
2222
project.generate_docs()
23-
23+
project.generate_canary_files()
2424
LOG.warning("Generated files for %s", project.type_name)
2525

2626

src/rpdk/core/project.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
import json
33
import logging
44
import os
5+
import re
56
import shutil
67
import sys
78
import zipfile
89
from pathlib import Path
910
from tempfile import TemporaryFile
11+
from typing import Any, Dict
1012
from uuid import uuid4
1113

14+
import yaml
1215
from botocore.exceptions import ClientError, WaiterError
1316
from jinja2 import Environment, PackageLoader, select_autoescape
1417
from jsonschema import Draft7Validator
@@ -56,7 +59,32 @@
5659
ARTIFACT_TYPE_RESOURCE = "RESOURCE"
5760
ARTIFACT_TYPE_MODULE = "MODULE"
5861
ARTIFACT_TYPE_HOOK = "HOOK"
59-
62+
TARGET_CANARY_ROOT_FOLDER = "canary-bundle"
63+
TARGET_CANARY_FOLDER = "canary-bundle/canary"
64+
RPDK_CONFIG_FILE = ".rpdk-config"
65+
CANARY_FILE_PREFIX = "canary"
66+
CONTRACT_TEST_DEPENDENCY_FILE_NAME = "dependencies.yml"
67+
CANARY_DEPENDENCY_FILE_NAME = "bootstrap.yaml"
68+
CANARY_SETTINGS = "canarySettings"
69+
TYPE_NAME = "typeName"
70+
CONTRACT_TEST_FILE_NAMES = "contract_test_file_names"
71+
INPUT1_FILE_NAME = "inputs_1.json"
72+
FILE_GENERATION_ENABLED = "file_generation_enabled"
73+
CONTRACT_TEST_FOLDER = "contract-tests-artifacts"
74+
CONTRACT_TEST_INPUT_PREFIX = "inputs_*"
75+
CONTRACT_TEST_DEPENDENCY_FILE_NAME = "dependencies.yml"
76+
FILE_GENERATION_ENABLED = "file_generation_enabled"
77+
TYPE_NAME = "typeName"
78+
CONTRACT_TEST_FILE_NAMES = "contract_test_file_names"
79+
INPUT1_FILE_NAME = "inputs_1.json"
80+
FN_SUB = "Fn::Sub"
81+
FN_IMPORT_VALUE = "Fn::ImportValue"
82+
UUID = "uuid"
83+
DYNAMIC_VALUES_MAP = {
84+
"region": "${AWS::Region}",
85+
"partition": "${AWS::Partition}",
86+
"account": "${AWS::AccountId}",
87+
}
6088
DEFAULT_ROLE_TIMEOUT_MINUTES = 120 # 2 hours
6189
# min and max are according to CreateRole API restrictions
6290
# https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateRole.html
@@ -145,6 +173,7 @@ def __init__(self, overwrite_enabled=False, root=None):
145173
self.test_entrypoint = None
146174
self.executable_entrypoint = None
147175
self.fragment_dir = None
176+
self.canary_settings = {}
148177
self.target_info = {}
149178

150179
self.env = Environment(
@@ -207,6 +236,30 @@ def target_schemas_path(self):
207236
def target_info_path(self):
208237
return self.root / TARGET_INFO_FILENAME
209238

239+
@property
240+
def target_canary_root_path(self):
241+
return self.root / TARGET_CANARY_ROOT_FOLDER
242+
243+
@property
244+
def target_canary_folder_path(self):
245+
return self.root / TARGET_CANARY_FOLDER
246+
247+
@property
248+
def rpdk_config(self):
249+
return self.root / RPDK_CONFIG_FILE
250+
251+
@property
252+
def file_generation_enabled(self):
253+
return self.canary_settings.get(FILE_GENERATION_ENABLED, False)
254+
255+
@property
256+
def contract_test_file_names(self):
257+
return self.canary_settings.get(CONTRACT_TEST_FILE_NAMES, [INPUT1_FILE_NAME])
258+
259+
@property
260+
def target_contract_test_folder_path(self):
261+
return self.root / CONTRACT_TEST_FOLDER
262+
210263
@staticmethod
211264
def _raise_invalid_project(msg, e):
212265
LOG.debug(msg, exc_info=e)
@@ -277,6 +330,7 @@ def validate_and_load_resource_settings(self, raw_settings):
277330
self.executable_entrypoint = raw_settings.get("executableEntrypoint")
278331
self._plugin = load_plugin(raw_settings["language"])
279332
self.settings = raw_settings.get("settings", {})
333+
self.canary_settings = raw_settings.get("canarySettings", {})
280334

281335
def _write_example_schema(self):
282336
self.schema = resource_json(
@@ -338,6 +392,7 @@ def _write_resource_settings(f):
338392
"testEntrypoint": self.test_entrypoint,
339393
"settings": self.settings,
340394
**executable_entrypoint_dict,
395+
"canarySettings": self.canary_settings,
341396
},
342397
f,
343398
indent=4,
@@ -391,6 +446,10 @@ def init(self, type_name, language, settings=None):
391446
self.language = language
392447
self._plugin = load_plugin(language)
393448
self.settings = settings or {}
449+
self.canary_settings = {
450+
FILE_GENERATION_ENABLED: True,
451+
CONTRACT_TEST_FILE_NAMES: [INPUT1_FILE_NAME],
452+
}
394453
self._write_example_schema()
395454
self._write_example_inputs()
396455
self._plugin.init(self)
@@ -1251,3 +1310,83 @@ def _load_target_info(
12511310
)
12521311

12531312
return type_info
1313+
1314+
def generate_canary_files(self) -> None:
1315+
if (
1316+
not self.file_generation_enabled
1317+
or not Path(self.target_contract_test_folder_path).exists()
1318+
):
1319+
return
1320+
self._setup_stack_template_environment()
1321+
self._generate_stack_template_files()
1322+
1323+
def _setup_stack_template_environment(self) -> None:
1324+
stack_template_root = Path(self.target_canary_root_path)
1325+
stack_template_folder = Path(self.target_canary_folder_path)
1326+
stack_template_folder.mkdir(parents=True, exist_ok=True)
1327+
dependencies_file = (
1328+
Path(self.target_contract_test_folder_path)
1329+
/ CONTRACT_TEST_DEPENDENCY_FILE_NAME
1330+
)
1331+
bootstrap_file = stack_template_root / CANARY_DEPENDENCY_FILE_NAME
1332+
if dependencies_file.exists():
1333+
shutil.copy(str(dependencies_file), str(bootstrap_file))
1334+
1335+
def _generate_stack_template_files(self) -> None:
1336+
stack_template_folder = Path(self.target_canary_folder_path)
1337+
contract_test_folder = Path(self.target_contract_test_folder_path)
1338+
contract_test_files = [
1339+
file
1340+
for file in contract_test_folder.glob(CONTRACT_TEST_INPUT_PREFIX)
1341+
if file.is_file() and file.name in self.contract_test_file_names
1342+
]
1343+
contract_test_files = sorted(contract_test_files)
1344+
for count, ct_file in enumerate(contract_test_files, start=1):
1345+
with ct_file.open("r") as f:
1346+
json_data = json.load(f)
1347+
resource_name = self.type_info[2]
1348+
stack_template_data = {
1349+
"Description": f"Template for {self.type_name}",
1350+
"Resources": {
1351+
f"{resource_name}": {
1352+
"Type": self.type_name,
1353+
"Properties": self._replace_dynamic_values(
1354+
json_data["CreateInputs"]
1355+
),
1356+
}
1357+
},
1358+
}
1359+
stack_template_file_name = f"{CANARY_FILE_PREFIX}{count}_001.yaml"
1360+
stack_template_file_path = stack_template_folder / stack_template_file_name
1361+
with stack_template_file_path.open("w") as stack_template_file:
1362+
yaml.dump(stack_template_data, stack_template_file, indent=2)
1363+
1364+
def _replace_dynamic_values(self, properties: Dict[str, Any]) -> Dict[str, Any]:
1365+
for key, value in properties.items():
1366+
if isinstance(value, dict):
1367+
properties[key] = self._replace_dynamic_values(value)
1368+
elif isinstance(value, list):
1369+
properties[key] = [self._replace_dynamic_value(item) for item in value]
1370+
else:
1371+
return_value = self._replace_dynamic_value(value)
1372+
properties[key] = return_value
1373+
return properties
1374+
1375+
def _replace_dynamic_value(self, original_value: Any) -> Any:
1376+
pattern = r"\{\{(.*?)\}\}"
1377+
1378+
def replace_token(match):
1379+
token = match.group(1)
1380+
if UUID in token:
1381+
return str(uuid4())
1382+
if token in DYNAMIC_VALUES_MAP:
1383+
return DYNAMIC_VALUES_MAP[token]
1384+
return f'{{"{FN_IMPORT_VALUE}": "{token.strip()}"}}'
1385+
1386+
replaced_value = re.sub(pattern, replace_token, str(original_value))
1387+
1388+
if any(value in replaced_value for value in DYNAMIC_VALUES_MAP.values()):
1389+
replaced_value = {FN_SUB: replaced_value}
1390+
if FN_IMPORT_VALUE in replaced_value:
1391+
replaced_value = json.loads(replaced_value)
1392+
return replaced_value

0 commit comments

Comments
 (0)