-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Expand file tree
/
Copy pathconnector.py
More file actions
251 lines (193 loc) · 9.54 KB
/
Copy pathconnector.py
File metadata and controls
251 lines (193 loc) · 9.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Optional
from typing_extensions import TypeGuard
from samtranslator.model import ResourceResolver
from samtranslator.model.apigateway import ApiGatewayRestApi
from samtranslator.model.apigatewayv2 import ApiGatewayV2HttpApi
from samtranslator.model.connector_profiles.profile import replace_cfn_resource_properties
from samtranslator.model.dynamodb import DynamoDBTable
from samtranslator.model.intrinsics import get_logical_id_from_intrinsic, ref
from samtranslator.model.lambda_ import (
LambdaFunction,
)
from samtranslator.model.stepfunctions import StepFunctionsStateMachine
from samtranslator.public.sdk.resource import SamResourceType
from samtranslator.utils.utils import as_array, insert_unique
@dataclass(frozen=True)
class ConnectorResourceReference:
"""Reference to a connector resource with all its identifying properties."""
logical_id: Optional[str]
resource_type: str
arn: Optional[str] = None
role_name: Optional[str] = None
queue_url: Optional[str] = None
resource_id: Optional[str] = None
name: Optional[str] = None
qualifier: Optional[str] = None
_SAM_TO_CFN_RESOURCE_TYPE = {
SamResourceType.Function.value: LambdaFunction.resource_type,
SamResourceType.StateMachine.value: StepFunctionsStateMachine.resource_type,
SamResourceType.Api.value: ApiGatewayRestApi.resource_type,
SamResourceType.HttpApi.value: ApiGatewayV2HttpApi.resource_type,
SamResourceType.SimpleTable.value: DynamoDBTable.resource_type,
}
UNSUPPORTED_CONNECTOR_PROFILE_TYPE = "UNSUPPORTED_CONNECTOR_PROFILE_TYPE"
class ConnectorResourceError(Exception):
"""
Indicates a template error making a resource unusable for connectors.
"""
def _is_nonblank_str(s: Any) -> TypeGuard[str]:
return s and isinstance(s, str)
def add_depends_on(logical_id: str, depends_on: str, resource_resolver: ResourceResolver) -> None:
"""
Add DependsOn attribute to resource.
"""
resource = resource_resolver.get_resource_by_logical_id(logical_id)
if not resource:
return
old_deps = resource.get("DependsOn", [])
deps = insert_unique(old_deps, depends_on)
resource["DependsOn"] = deps
def replace_depends_on_logical_id(logical_id: str, replacement: List[str], resource_resolver: ResourceResolver) -> None:
"""
For every resource's `DependsOn`, replace `logical_id` by `replacement`.
"""
for resource in resource_resolver.get_all_resources().values():
depends_on = as_array(resource.get("DependsOn", []))
if logical_id in depends_on:
depends_on.remove(logical_id)
resource["DependsOn"] = insert_unique(depends_on, replacement)
def get_event_source_mappings(
event_source_id: str, function_id: str, resource_resolver: ResourceResolver
) -> Iterable[str]:
"""
Get logical IDs of `AWS::Lambda::EventSourceMapping`s between resource logical IDs.
"""
resources = resource_resolver.get_all_resources()
for logical_id, resource in resources.items():
if resource.get("Type") == "AWS::Lambda::EventSourceMapping":
properties = resource.get("Properties", {})
# Not taking intrinsics as input to function as FunctionName could be a number of
# formats, which would require parsing it anyway
resource_function_id = get_logical_id_from_intrinsic(properties.get("FunctionName"))
resource_event_source_id = get_logical_id_from_intrinsic(properties.get("EventSourceArn"))
if (
resource_function_id
and resource_event_source_id
and function_id == resource_function_id
and event_source_id == resource_event_source_id
):
yield logical_id
def _is_valid_resource_reference(obj: Dict[str, Any]) -> bool:
id_provided = "Id" in obj
# Every property in ResourceReference can be implied using 'Id', except for 'Qualifier', so users should be able to combine 'Id' and 'Qualifier'
non_id_provided = len([k for k in obj if k not in ["Id", "Qualifier"]]) > 0
# Must provide Id (with optional Qualifier) or a supported combination of other properties.
return id_provided != non_id_provided
def get_resource_reference(
obj: Dict[str, Any], resource_resolver: ResourceResolver, connecting_obj: Dict[str, Any]
) -> ConnectorResourceReference:
if not _is_valid_resource_reference(obj):
raise ConnectorResourceError(
"Must provide 'Id' (with optional 'Qualifier') or a supported combination of other properties."
)
logical_id = obj.get("Id")
# Must provide Id (with optional Qualifier) or a supported combination of other properties
# If Id is not provided, all values must come from overrides.
if not logical_id:
resource_type = obj.get("Type")
if not _is_nonblank_str(resource_type):
raise ConnectorResourceError("'Type' is missing or not a string.")
# profiles.json only support CFN resource type.
# We need to convert SAM resource types to corresponding CFN resource type
resource_type = _SAM_TO_CFN_RESOURCE_TYPE.get(resource_type, resource_type)
return ConnectorResourceReference(
logical_id=None,
resource_type=resource_type,
arn=obj.get("Arn"),
role_name=obj.get("RoleName"),
queue_url=obj.get("QueueUrl"),
resource_id=obj.get("ResourceId"),
name=obj.get("Name"),
qualifier=obj.get("Qualifier"),
)
if not _is_nonblank_str(logical_id):
raise ConnectorResourceError("'Id' is missing or not a string.")
resource = resource_resolver.get_resource_by_logical_id(logical_id)
if not resource:
raise ConnectorResourceError(f"Unable to find resource with logical ID '{logical_id}'.")
resource_type = resource.get("Type")
if not _is_nonblank_str(resource_type):
raise ConnectorResourceError("'Type' is missing or not a string.")
properties = resource.get("Properties", {})
cfn_resource_properties = replace_cfn_resource_properties(resource_type, logical_id)
cfn_resource_properties_output = cfn_resource_properties.get("Outputs", {})
arn = _get_resource_arn(cfn_resource_properties_output)
role_name = _get_resource_role_name(
connecting_obj.get("Id"), connecting_obj.get("Arn"), cfn_resource_properties, properties
)
queue_url = _get_resource_queue_url(cfn_resource_properties_output)
resource_id = _get_resource_id(cfn_resource_properties_output)
name = _get_resource_name(cfn_resource_properties_output)
qualifier = obj.get("Qualifier") if "Qualifier" in obj else _get_resource_qualifier(cfn_resource_properties_output)
return ConnectorResourceReference(
logical_id=logical_id,
resource_type=resource_type,
arn=arn,
role_name=role_name,
queue_url=queue_url,
resource_id=resource_id,
name=name,
qualifier=qualifier,
)
def _get_events_rule_role(
connecting_obj_id: Optional[str], connecting_obj_arn: Optional[Any], properties: Dict[str, Any]
) -> Optional[Any]:
for target in properties.get("Targets", []):
target_arn = target.get("Arn")
target_logical_id = get_logical_id_from_intrinsic(target_arn)
if (target_logical_id and target_logical_id == connecting_obj_id) or (
connecting_obj_arn and target_arn == connecting_obj_arn
):
return target.get("RoleArn")
return None
def _get_resource_role_property(
connecting_obj_id: Optional[str],
connecting_obj_arn: Optional[Any],
cfn_resource_properties: Dict[str, Any],
properties: Dict[str, Any],
) -> Any:
role_property = cfn_resource_properties.get("Inputs", {}).get("Role")
if isinstance(role_property, str):
return properties.get(role_property)
if isinstance(role_property, dict) and role_property.get("Function") == "GetEventsRuleRole":
return _get_events_rule_role(connecting_obj_id, connecting_obj_arn, properties)
return None
def _get_resource_role_name(
connecting_obj_id: Optional[str],
connecting_obj_arn: Optional[Any],
cfn_resource_properties: Dict[str, Any],
properties: Dict[str, Any],
) -> Any:
role = _get_resource_role_property(connecting_obj_id, connecting_obj_arn, cfn_resource_properties, properties)
if not role:
return None
logical_id = get_logical_id_from_intrinsic(role)
if not logical_id:
return None
return ref(logical_id)
def _get_resource_queue_url(properties: Dict[str, Any]) -> Optional[Any]:
return properties.get("Url")
def _get_resource_id(properties: Dict[str, Any]) -> Optional[Any]:
return properties.get("Id")
def _get_resource_name(properties: Dict[str, Any]) -> Optional[Any]:
return properties.get("Name")
def _get_resource_qualifier(properties: Dict[str, Any]) -> Optional[Any]:
# Qualifier is used as the execute-api ARN suffix; by default allow whole API
return properties.get("Qualifier")
def _get_resource_arn(properties: Dict[str, Any]) -> Any:
# according to documentation, Ref returns ARNs for these two resource types
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#aws-resource-stepfunctions-statemachine-return-values
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html#aws-resource-sns-topic-return-values
# For all other supported resources, we can typically use Fn::GetAtt LogicalId.Arn to obtain ARNs
return properties.get("Arn")