Skip to content

Commit 97ddeab

Browse files
authored
Adding support for TypeConfiguration (#160)
1 parent f1fc623 commit 97ddeab

File tree

12 files changed

+195
-24
lines changed

12 files changed

+195
-24
lines changed

python/rpdk/python/codegen.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ def generate(self, project):
149149

150150
models = resolve_models(project.schema)
151151

152+
if project.configuration_schema:
153+
configuration_schema_path = (
154+
project.root / project.configuration_schema_filename
155+
)
156+
project.write_configuration_schema(configuration_schema_path)
157+
configuration_models = resolve_models(
158+
project.configuration_schema, "TypeConfigurationModel"
159+
)
160+
else:
161+
configuration_models = {"TypeConfigurationModel": {}}
162+
models.update(configuration_models)
163+
152164
path = self.package_root / self.package_name / "models.py"
153165
LOG.debug("Writing file: %s", path)
154166
template = self.env.get_template("models.py")
@@ -158,7 +170,7 @@ def generate(self, project):
158170
LOG.debug("Generate complete")
159171

160172
def _pre_package(self, build_path):
161-
f = TemporaryFile("w+b") # pylint: disable=consider-using-with
173+
f = TemporaryFile("w+b") # pylint: disable=R1732
162174

163175
with zipfile.ZipFile(f, mode="w") as zip_file:
164176
self._recursive_relative_write(build_path, build_path, zip_file)

python/rpdk/python/templates/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ResourceHandlerRequest(BaseResourceHandlerRequest):
3535
# pylint: disable=invalid-name
3636
desiredResourceState: Optional["ResourceModel"]
3737
previousResourceState: Optional["ResourceModel"]
38+
typeConfiguration: Optional["TypeConfigurationModel"]
3839

3940

4041
{% for model, properties in models.items() %}

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ def find_version(*file_paths):
3838
include_package_data=True,
3939
zip_safe=True,
4040
python_requires=">=3.6",
41-
install_requires=[
42-
"cloudformation-cli>=0.1.14",
43-
],
41+
install_requires=["cloudformation-cli>=0.2.13", "types-dataclasses>=0.1.5"],
4442
entry_points={
4543
"rpdk.v1.languages": [
4644
"python37 = rpdk.python.codegen:Python37LanguagePlugin",

src/cloudformation_cli_python_lib/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,11 @@ class NetworkFailure(_HandlerError):
7474

7575
class InternalFailure(_HandlerError):
7676
pass
77+
78+
79+
class InvalidTypeConfiguration(_HandlerError):
80+
def __init__(self, type_name: str, message: str):
81+
super().__init__(
82+
f"Invalid TypeConfiguration provided for type {type_name}."
83+
f" Reason: {message}"
84+
)

src/cloudformation_cli_python_lib/interface.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class HandlerErrorCode(str, _AutoName):
5656
ServiceInternalError = auto()
5757
NetworkFailure = auto()
5858
InternalFailure = auto()
59+
InvalidTypeConfiguration = auto()
5960

6061

6162
class BaseModel:
@@ -137,6 +138,7 @@ class BaseResourceHandlerRequest:
137138
previousSystemTags: Optional[Mapping[str, Any]]
138139
awsAccountId: Optional[str]
139140
logicalResourceIdentifier: Optional[str]
141+
typeConfiguration: Optional[BaseModel]
140142
nextToken: Optional[str]
141143
region: Optional[str]
142144
awsPartition: Optional[str]

src/cloudformation_cli_python_lib/resource.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,17 @@ def wrapper(self: Any, event: MutableMapping[str, Any], context: Any) -> Any:
5656

5757

5858
class Resource:
59-
def __init__(self, type_name: str, resouce_model_cls: Type[BaseModel]) -> None:
59+
def __init__(
60+
self,
61+
type_name: str,
62+
resouce_model_cls: Type[BaseModel],
63+
type_configuration_model_cls: Optional[BaseModel] = None,
64+
) -> None:
6065
self.type_name = type_name
6166
self._model_cls: Type[BaseModel] = resouce_model_cls
67+
self._type_configuration_model_cls: Optional[
68+
BaseModel
69+
] = type_configuration_model_cls
6270
self._handlers: MutableMapping[Action, HandlerSignature] = {}
6371

6472
def handler(self, action: Action) -> Callable[[HandlerSignature], HandlerSignature]:
@@ -101,7 +109,7 @@ def _parse_test_request(
101109
creds = Credentials(**event.credentials)
102110
request: BaseResourceHandlerRequest = UnmodelledRequest(
103111
**event.request
104-
).to_modelled(self._model_cls)
112+
).to_modelled(self._model_cls, self._type_configuration_model_cls)
105113

106114
session = _get_boto_session(creds, event.region)
107115
action = Action[event.action]
@@ -164,7 +172,8 @@ def _cast_resource_request(
164172
logicalResourceIdentifier=request.requestData.logicalResourceId,
165173
stackId=request.stackId,
166174
region=request.region,
167-
).to_modelled(self._model_cls)
175+
typeConfiguration=request.requestData.typeConfiguration,
176+
).to_modelled(self._model_cls, self._type_configuration_model_cls)
168177
except Exception as e: # pylint: disable=broad-except
169178
LOG.exception("Invalid request")
170179
raise InvalidRequest(f"{e} ({type(e).__name__})") from e

src/cloudformation_cli_python_lib/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class RequestData:
5959
previousResourceProperties: Optional[Mapping[str, Any]] = None
6060
previousStackTags: Optional[Mapping[str, Any]] = None
6161
previousSystemTags: Optional[Mapping[str, Any]] = None
62+
typeConfiguration: Optional[Mapping[str, Any]] = None
6263

6364
def __init__(self, **kwargs: Any) -> None:
6465
dataclass_fields = {f.name for f in fields(self)}
@@ -129,13 +130,18 @@ class UnmodelledRequest:
129130
previousResourceTags: Optional[Mapping[str, Any]] = None
130131
systemTags: Optional[Mapping[str, Any]] = None
131132
previousSystemTags: Optional[Mapping[str, Any]] = None
133+
typeConfiguration: Optional[Mapping[str, Any]] = None
132134
awsAccountId: Optional[str] = None
133135
logicalResourceIdentifier: Optional[str] = None
134136
nextToken: Optional[str] = None
135137
stackId: Optional[str] = None
136138
region: Optional[str] = None
137139

138-
def to_modelled(self, model_cls: Type[BaseModel]) -> BaseResourceHandlerRequest:
140+
def to_modelled(
141+
self,
142+
model_cls: Type[BaseModel],
143+
type_configuration_model_cls: Optional[BaseModel],
144+
) -> BaseResourceHandlerRequest:
139145
# pylint: disable=protected-access
140146
return BaseResourceHandlerRequest(
141147
clientRequestToken=self.clientRequestToken,
@@ -147,6 +153,9 @@ def to_modelled(self, model_cls: Type[BaseModel]) -> BaseResourceHandlerRequest:
147153
previousSystemTags=self.previousSystemTags,
148154
awsAccountId=self.awsAccountId,
149155
logicalResourceIdentifier=self.logicalResourceIdentifier,
156+
typeConfiguration=None
157+
if not type_configuration_model_cls
158+
else type_configuration_model_cls._deserialize(self.typeConfiguration),
150159
nextToken=self.nextToken,
151160
stackId=self.stackId,
152161
region=self.region,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"typeName": "Company::Test::Type",
3+
"description": "Test type",
4+
"typeConfiguration": {
5+
"properties": {
6+
"Credentials": {
7+
"$ref": "#/definitions/Credentials"
8+
}
9+
},
10+
"additionalProperties": false,
11+
"required": [
12+
"Credentials"
13+
]
14+
},
15+
"definitions": {
16+
"Credentials": {
17+
"type": "object",
18+
"properties": {
19+
"ApiKey": {
20+
"description": "API key",
21+
"type": "string"
22+
},
23+
"ApplicationKey": {
24+
"description": "application key",
25+
"type": "string"
26+
},
27+
"CountryCode": {
28+
"type": "string"
29+
}
30+
},
31+
"additionalProperties": false
32+
}
33+
},
34+
"properties": {
35+
"Type": {
36+
"type": "string",
37+
"description": "The type of the monitor",
38+
"enum": [
39+
"composite"
40+
]
41+
}
42+
},
43+
"required": [
44+
"Type"
45+
],
46+
"primaryIdentifier": [
47+
"/properties/Type"
48+
],
49+
"additionalProperties": false
50+
}

tests/lib/interface_test.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,3 @@ def test_operation_status_enum_matches_sdk(client):
127127
sdk = set(client.meta.service_model.shape_for("OperationStatus").enum)
128128
enum = set(OperationStatus.__members__)
129129
assert enum == sdk
130-
131-
132-
def test_handler_error_code_enum_matches_sdk(client):
133-
sdk = set(client.meta.service_model.shape_for("HandlerErrorCode").enum)
134-
enum = set(HandlerErrorCode.__members__)
135-
assert enum == sdk

tests/lib/resource_test.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"previousSystemTags": {},
4646
"stackTags": {"tag1": "abc"},
4747
"previousStackTags": {"tag1": "def"},
48+
"typeConfiguration": sentinel.type_configuration,
4849
},
4950
"stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e"
5051
"722ae60-fe62-11e8-9a0e-0ae8cc519968",
@@ -76,7 +77,7 @@ def test_entrypoint_handler_error(resource):
7677

7778

7879
def test_entrypoint_success():
79-
resource = Resource(TYPE_NAME, Mock())
80+
resource = Resource(TYPE_NAME, Mock(), Mock())
8081
event = ProgressEvent(status=OperationStatus.SUCCESS, message="")
8182
mock_handler = resource.handler(Action.CREATE)(Mock(return_value=event))
8283

@@ -106,7 +107,15 @@ class ResourceModel(BaseModel):
106107
def _deserialize(cls, json_data):
107108
return cls("test")
108109

109-
resource = Resource(Mock(), ResourceModel)
110+
@dataclass
111+
class TypeConfigurationModel(BaseModel):
112+
a_string: str
113+
114+
@classmethod
115+
def _deserialize(cls, json_data):
116+
return cls("test")
117+
118+
resource = Resource(Mock(), ResourceModel, TypeConfigurationModel)
110119

111120
with patch(
112121
"cloudformation_cli_python_lib.resource.ProviderLogHandler.setup"
@@ -132,7 +141,7 @@ def _deserialize(cls, json_data):
132141
def test_entrypoint_non_mutating_action():
133142
payload = ENTRYPOINT_PAYLOAD.copy()
134143
payload["action"] = "READ"
135-
resource = Resource(TYPE_NAME, Mock())
144+
resource = Resource(TYPE_NAME, Mock(), Mock())
136145
event = ProgressEvent(status=OperationStatus.SUCCESS, message="")
137146
resource.handler(Action.CREATE)(Mock(return_value=event))
138147

@@ -148,7 +157,7 @@ def test_entrypoint_non_mutating_action():
148157
def test_entrypoint_with_context():
149158
payload = ENTRYPOINT_PAYLOAD.copy()
150159
payload["callbackContext"] = {"a": "b"}
151-
resource = Resource(TYPE_NAME, Mock())
160+
resource = Resource(TYPE_NAME, Mock(), Mock())
152161
event = ProgressEvent(
153162
status=OperationStatus.SUCCESS, message="", callbackContext={"c": "d"}
154163
)
@@ -163,7 +172,7 @@ def test_entrypoint_with_context():
163172

164173

165174
def test_entrypoint_success_without_caller_provider_creds():
166-
resource = Resource(TYPE_NAME, Mock())
175+
resource = Resource(TYPE_NAME, Mock(), Mock())
167176
event = ProgressEvent(status=OperationStatus.SUCCESS, message="")
168177
resource.handler(Action.CREATE)(Mock(return_value=event))
169178

@@ -208,7 +217,12 @@ def test__parse_request_valid_request_and__cast_resource_request():
208217
mock_model = Mock(spec_set=["_deserialize"])
209218
mock_model._deserialize.side_effect = [sentinel.state_out1, sentinel.state_out2]
210219

211-
resource = Resource(TYPE_NAME, mock_model)
220+
mock_type_configuration_model = Mock(spec_set=["_deserialize"])
221+
mock_type_configuration_model._deserialize.side_effect = [
222+
sentinel.type_configuration
223+
]
224+
225+
resource = Resource(TYPE_NAME, mock_model, mock_type_configuration_model)
212226

213227
with patch(
214228
"cloudformation_cli_python_lib.resource._get_boto_session"
@@ -229,6 +243,7 @@ def test__parse_request_valid_request_and__cast_resource_request():
229243
# credentials are used when rescheduling, so can't zero them out (for now)
230244
assert request.requestData.callerCredentials is not None
231245
assert request.requestData.providerCredentials is not None
246+
assert request.requestData.typeConfiguration is sentinel.type_configuration
232247

233248
caller_sess, provider_sess = sessions
234249
assert caller_sess is mock_session.return_value
@@ -245,6 +260,7 @@ def test__parse_request_valid_request_and__cast_resource_request():
245260
assert modeled_request.clientRequestToken == request.bearerToken
246261
assert modeled_request.desiredResourceState is sentinel.state_out1
247262
assert modeled_request.previousResourceState is sentinel.state_out2
263+
assert modeled_request.typeConfiguration is sentinel.type_configuration
248264
assert modeled_request.logicalResourceIdentifier == "myBucket"
249265
assert modeled_request.nextToken is None
250266

@@ -337,6 +353,11 @@ def test__parse_test_request_valid_request():
337353
mock_model = Mock(spec_set=["_deserialize"])
338354
mock_model._deserialize.side_effect = [sentinel.state_out1, sentinel.state_out2]
339355

356+
mock_type_configuration_model = Mock(spec_set=["_deserialize"])
357+
mock_type_configuration_model._deserialize.side_effect = [
358+
sentinel.type_configuration
359+
]
360+
340361
payload = {
341362
"credentials": {"accessKeyId": "", "secretAccessKey": "", "sessionToken": ""},
342363
"action": "CREATE",
@@ -345,11 +366,12 @@ def test__parse_test_request_valid_request():
345366
"desiredResourceState": sentinel.state_in1,
346367
"previousResourceState": sentinel.state_in2,
347368
"logicalResourceIdentifier": None,
369+
"typeConfiguration": sentinel.type_configuration,
348370
},
349371
"callbackContext": None,
350372
}
351373

352-
resource = Resource(TYPE_NAME, mock_model)
374+
resource = Resource(TYPE_NAME, mock_model, mock_type_configuration_model)
353375

354376
with patch(
355377
"cloudformation_cli_python_lib.resource._get_boto_session"
@@ -366,6 +388,7 @@ def test__parse_test_request_valid_request():
366388
)
367389
assert request.desiredResourceState is sentinel.state_out1
368390
assert request.previousResourceState is sentinel.state_out2
391+
assert request.typeConfiguration is sentinel.type_configuration
369392
assert request.logicalResourceIdentifier is None
370393

371394
assert action == Action.CREATE
@@ -394,7 +417,10 @@ def test_test_entrypoint_success():
394417
mock_model = Mock(spec_set=["_deserialize"])
395418
mock_model._deserialize.side_effect = [None, None]
396419

397-
resource = Resource(TYPE_NAME, mock_model)
420+
mock_type_configuration_model = Mock(spec_set=["_deserialize"])
421+
mock_type_configuration_model._deserialize.side_effect = [None]
422+
423+
resource = Resource(TYPE_NAME, mock_model, mock_type_configuration_model)
398424
progress_event = ProgressEvent(status=OperationStatus.SUCCESS)
399425
mock_handler = resource.handler(Action.CREATE)(Mock(return_value=progress_event))
400426

@@ -415,4 +441,5 @@ def test_test_entrypoint_success():
415441
assert event is progress_event
416442

417443
mock_model._deserialize.assert_has_calls([call(None), call(None)])
444+
mock_type_configuration_model._deserialize.assert_has_calls([call(None)])
418445
mock_handler.assert_called_once()

0 commit comments

Comments
 (0)