Skip to content

Commit 1e8e363

Browse files
authored
feat(cli) - allow override for typeConfiguration.json file (#958)
* Update comment and documentation with new option * Add in typeconfig option override and unittests * Updated README with more examples
1 parent ef6990c commit 1e8e363

File tree

8 files changed

+66
-26
lines changed

8 files changed

+66
-26
lines changed

.pylintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ good-names=e,ex,f,fp,i,j,k,n,_
2222

2323
indent-string=' '
2424
max-line-length=160
25+
26+
[DESIGN]
27+
max-locals=16

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ cfn test --log-group-name cw_log_group --log-role-arn log_delivery_role_arn # Ha
6060
```
6161

6262
Note:
63-
* To use your type configuration in contract tests, you will need to save your type configuration json file in `~/.cfn-cli/typeConfiguration.json`.
63+
* To use your type configuration in contract tests, you will need to save your type configuration json file in `~/.cfn-cli/typeConfiguration.json` or specify the file you would like to use
64+
* `--typeconfig ./myResourceTypeConfig.json`
65+
* `--typeconfig /test/myresource/config1.json`
66+
* `--typeconfig C:\MyResource\typeconf.json`
6467

6568
* To use `propertyTransform` in schema, you will need to install [PYJQ](https://pypi.org/project/pyjq/). This feature will not be available to use with contract tests on Windows OS
6669

src/rpdk/core/contract/hook_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(
5757
log_group_name=None,
5858
log_role_arn=None,
5959
docker_image=None,
60+
typeconfig=None,
6061
executable_entrypoint=None,
6162
target_info=None,
6263
profile=None,
@@ -101,6 +102,7 @@ def __init__(
101102
self._executable_entrypoint = executable_entrypoint
102103
self._target_info = self._setup_target_info(target_info)
103104
self._resolved_targets = {}
105+
self._typeconfig = typeconfig
104106

105107
@staticmethod
106108
def _properties_to_paths(schema, key):
@@ -491,7 +493,7 @@ def call(
491493
invocation_point,
492494
target,
493495
target_model,
494-
TypeConfiguration.get_hook_configuration(),
496+
TypeConfiguration.get_hook_configuration(self._typeconfig),
495497
**kwargs,
496498
)
497499
start_time = time.time()

src/rpdk/core/contract/resource_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def __init__(
171171
log_group_name=None,
172172
log_role_arn=None,
173173
docker_image=None,
174+
typeconfig=None,
174175
executable_entrypoint=None,
175176
profile=None,
176177
): # pylint: disable=too-many-arguments
@@ -213,6 +214,7 @@ def __init__(
213214
self._docker_image = docker_image
214215
self._docker_client = docker.from_env() if self._docker_image else None
215216
self._executable_entrypoint = executable_entrypoint
217+
self._typeconfig = typeconfig
216218

217219
def _properties_to_paths(self, key):
218220
return {fragment_decode(prop, prefix="") for prop in self._schema.get(key, [])}
@@ -736,7 +738,7 @@ def call(self, action, current_model, previous_model=None, **kwargs):
736738
action,
737739
current_model,
738740
previous_model,
739-
TypeConfiguration.get_type_configuration(),
741+
TypeConfiguration.get_type_configuration(self._typeconfig),
740742
**kwargs,
741743
)
742744
start_time = time.time()

src/rpdk/core/contract/type_configuration.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,50 @@
66

77
LOG = logging.getLogger(__name__)
88

9-
TYPE_CONFIGURATION_FILE_PATH = "~/.cfn-cli/typeConfiguration.json"
10-
119

1210
class TypeConfiguration:
1311
TYPE_CONFIGURATION = None
1412

1513
@staticmethod
16-
def get_type_configuration():
14+
def get_type_configuration(typeconfigloc):
15+
if typeconfigloc:
16+
type_config_file_path = typeconfigloc
17+
else:
18+
type_config_file_path = "~/.cfn-cli/typeConfiguration.json"
19+
1720
LOG.debug(
18-
"Loading type configuration setting file at '~/.cfn-cli/typeConfiguration.json'"
21+
"Loading type configuration setting file at %s",
22+
type_config_file_path,
1923
)
2024
if TypeConfiguration.TYPE_CONFIGURATION is None:
2125
try:
2226
with open(
23-
os.path.expanduser(TYPE_CONFIGURATION_FILE_PATH), encoding="utf-8"
27+
os.path.expanduser(type_config_file_path), encoding="utf-8"
2428
) as f:
2529
TypeConfiguration.TYPE_CONFIGURATION = json.load(f)
2630
except json.JSONDecodeError as json_decode_error:
2731
LOG.debug(
2832
"Type configuration file '%s' is invalid",
29-
TYPE_CONFIGURATION_FILE_PATH,
33+
type_config_file_path,
3034
)
3135
raise InvalidProjectError(
32-
"Type configuration file '%s' is invalid"
33-
% TYPE_CONFIGURATION_FILE_PATH
36+
"Type configuration file '%s' is invalid" % type_config_file_path
3437
) from json_decode_error
3538
except FileNotFoundError:
3639
LOG.debug(
3740
"Type configuration file '%s' not Found, do nothing",
38-
TYPE_CONFIGURATION_FILE_PATH,
41+
type_config_file_path,
3942
)
4043
return TypeConfiguration.TYPE_CONFIGURATION
4144

4245
@staticmethod
43-
def get_hook_configuration():
44-
# pylint: disable=unsubscriptable-object
45-
type_configuration = TypeConfiguration.get_type_configuration()
46+
def get_hook_configuration(typeconfigloc):
47+
type_configuration = TypeConfiguration.get_type_configuration(typeconfigloc)
4648
if type_configuration:
4749
try:
48-
return type_configuration["CloudFormationConfiguration"][
50+
return type_configuration.get("CloudFormationConfiguration", {})[
4951
"HookConfiguration"
50-
].get("Properties")
52+
]["Properties"]
5153
except KeyError as e:
5254
LOG.warning("Hook configuration is invalid")
5355
raise InvalidProjectError("Hook configuration is invalid") from e

src/rpdk/core/test.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ def get_contract_plugin_client(args, project, overrides, inputs):
342342
args.log_role_arn,
343343
executable_entrypoint=project.executable_entrypoint,
344344
docker_image=args.docker_image,
345+
typeconfig=args.typeconfig,
345346
target_info=project._load_target_info( # pylint: disable=protected-access
346347
args.cloudformation_endpoint_url, args.region
347348
),
@@ -362,6 +363,7 @@ def get_contract_plugin_client(args, project, overrides, inputs):
362363
project.type_name,
363364
args.log_group_name,
364365
args.log_role_arn,
366+
typeconfig=args.typeconfig,
365367
executable_entrypoint=project.executable_entrypoint,
366368
docker_image=args.docker_image,
367369
profile=args.profile,
@@ -441,7 +443,7 @@ def setup_subparser(subparsers, parents):
441443

442444
_sam_arguments(parser)
443445
# this parameter can be used to pass additional arguments to pytest after `--`
444-
# for example,
446+
# for example, cfn test -- -k contract_delete_update # to have pytest run a single test
445447

446448
parser.add_argument(
447449
"--role-arn", help="Role used when performing handler operations."
@@ -477,6 +479,11 @@ def setup_subparser(subparsers, parents):
477479
"of SAM",
478480
)
479481

482+
parser.add_argument(
483+
"--typeconfig",
484+
help="typeConfiguration file to use. Default: '~/.cfn-cli/typeConfiguration.json.'",
485+
)
486+
480487

481488
def _sam_arguments(parser):
482489
parser.add_argument(

tests/contract/test_type_configuration.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from unittest.mock import mock_open, patch
23

34
import pytest
@@ -29,50 +30,68 @@ def teardown_function():
2930
def test_get_type_configuration_with_not_exist_file():
3031
with patch("builtins.open", mock_open()) as f:
3132
f.side_effect = FileNotFoundError()
32-
assert TypeConfiguration.get_type_configuration() is None
33+
assert TypeConfiguration.get_type_configuration(None) is None
34+
35+
36+
def test_get_type_configuration_with_default_typeconfig_location():
37+
with patch(
38+
"builtins.open", mock_open(read_data=TYPE_CONFIGURATION_TEST_SETTING)
39+
) as f:
40+
TypeConfiguration.get_type_configuration(None)
41+
f.assert_called_with(
42+
os.path.expanduser("~/.cfn-cli/typeConfiguration.json"), encoding="utf-8"
43+
)
44+
45+
46+
def test_get_type_configuration_with_set_typeconfig_location():
47+
with patch(
48+
"builtins.open", mock_open(read_data=TYPE_CONFIGURATION_TEST_SETTING)
49+
) as f:
50+
TypeConfiguration.get_type_configuration("./test.json")
51+
f.assert_called_with("./test.json", encoding="utf-8")
3352

3453

3554
@patch("builtins.open", mock_open(read_data=TYPE_CONFIGURATION_TEST_SETTING))
3655
def test_get_type_configuration():
37-
type_configuration = TypeConfiguration.get_type_configuration()
56+
type_configuration = TypeConfiguration.get_type_configuration(None)
3857
assert type_configuration["Credentials"]["ApiKey"] == "123"
3958
assert type_configuration["Credentials"]["ApplicationKey"] == "123"
4059

4160
# get type config again, should be the same config
42-
type_configuration = TypeConfiguration.get_type_configuration()
61+
type_configuration = TypeConfiguration.get_type_configuration(None)
4362
assert type_configuration["Credentials"]["ApiKey"] == "123"
4463
assert type_configuration["Credentials"]["ApplicationKey"] == "123"
4564

4665

4766
@patch("builtins.open", mock_open(read_data=TYPE_CONFIGURATION_INVALID))
4867
def test_get_type_configuration_with_invalid_json():
4968
try:
50-
TypeConfiguration.get_type_configuration()
69+
TypeConfiguration.get_type_configuration(None)
5170
except InvalidProjectError:
5271
pass
5372

5473

5574
@patch("builtins.open", mock_open(read_data=HOOK_CONFIGURATION_TEST_SETTING))
5675
def test_get_hook_configuration():
57-
hook_configuration = TypeConfiguration.get_hook_configuration()
76+
hook_configuration = TypeConfiguration.get_hook_configuration(None)
5877
assert hook_configuration["Credentials"]["ApiKey"] == "123"
5978
assert hook_configuration["Credentials"]["ApplicationKey"] == "123"
6079

6180
# get type config again, should be the same config
62-
hook_configuration = TypeConfiguration.get_hook_configuration()
81+
hook_configuration = TypeConfiguration.get_hook_configuration(None)
6382
assert hook_configuration["Credentials"]["ApiKey"] == "123"
6483
assert hook_configuration["Credentials"]["ApplicationKey"] == "123"
6584

6685

6786
@patch("builtins.open", mock_open(read_data=HOOK_CONFIGURATION_INVALID))
6887
def test_get_hook_configuration_with_invalid_json():
6988
with pytest.raises(InvalidProjectError) as execinfo:
70-
TypeConfiguration.get_hook_configuration()
89+
TypeConfiguration.get_hook_configuration(None)
7190

7291
assert "Hook configuration is invalid" in str(execinfo.value)
7392

7493

7594
def test_get_hook_configuration_with_not_exist_file():
7695
with patch("builtins.open", mock_open()) as f:
7796
f.side_effect = FileNotFoundError()
78-
assert TypeConfiguration.get_hook_configuration() is None
97+
assert TypeConfiguration.get_hook_configuration(None) is None

tests/test_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def test_test_command_happy_path_resource(
227227
mock_project.type_name,
228228
None,
229229
None,
230+
typeconfig=None,
230231
executable_entrypoint=None,
231232
docker_image=None,
232233
profile=profile,
@@ -334,6 +335,7 @@ def test_test_command_happy_path_hook(
334335
mock_project.type_name,
335336
None,
336337
None,
338+
typeconfig=None,
337339
executable_entrypoint=None,
338340
docker_image=None,
339341
target_info=HOOK_TARGET_INFO,

0 commit comments

Comments
 (0)