Skip to content

Commit 46dde38

Browse files
authored
[aws][feat] Make the list of called mutator APIs available (#1323)
1 parent 59a00d8 commit 46dde38

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+728
-234
lines changed

plugins/aws/resoto_plugin_aws/collector.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,18 @@ def called_collect_apis() -> List[AwsApiSpec]:
8383
"""
8484
# list all calls here, that are not defined in any resource.
8585
additional_calls = [AwsApiSpec("pricing", "get-products")]
86-
specs = [spec for r in all_resources for spec in r.called_apis()] + additional_calls
87-
unique_specs = {f"{spec.service}::{spec.api_action}": spec for spec in specs}
88-
return sorted(unique_specs.values(), key=lambda s: s.service + "::" + s.api_action)
86+
specs = [spec for r in all_resources for spec in r.called_collect_apis()] + additional_calls
87+
return sorted(specs, key=lambda s: s.service + "::" + s.api_action)
88+
89+
90+
def called_mutator_apis() -> List[AwsApiSpec]:
91+
"""
92+
Return a list of all the APIs that are called to mutate resources.
93+
"""
94+
# explicitly list all calls here, that should be allowed to mutate resources.
95+
additional_calls = [AwsApiSpec("ec2", "start-instances"), AwsApiSpec("ec2", "stop-instances")]
96+
specs = [spec for r in all_resources for spec in r.called_mutator_apis()] + additional_calls
97+
return sorted(specs, key=lambda s: s.service + "::" + s.api_action)
8998

9099

91100
class AwsAccountCollector:

plugins/aws/resoto_plugin_aws/resource/apigateway.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
4444
return False
4545
return False
4646

47+
@classmethod
48+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
49+
return [
50+
AwsApiSpec("apigateway", "tag-resource", override_iam_permission="apigateway:PATCH"),
51+
AwsApiSpec("apigateway", "tag-resource", override_iam_permission="apigateway:POST"),
52+
AwsApiSpec("apigateway", "tag-resource", override_iam_permission="apigateway:PUT"),
53+
AwsApiSpec("apigateway", "untag-resource", override_iam_permission="apigateway:DELETE"),
54+
]
55+
4756

4857
@define(eq=False, slots=False)
4958
class AwsApiGatewayMethodResponse:
@@ -179,6 +188,10 @@ def delete_resource(self, client: AwsClient) -> bool:
179188
)
180189
return True
181190

191+
@classmethod
192+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
193+
return [AwsApiSpec("apigateway", "delete-resource", override_iam_permission="apigateway:DELETE")]
194+
182195

183196
@define(eq=False, slots=False)
184197
class AwsApiGatewayAuthorizer(AwsResource):
@@ -235,6 +248,10 @@ def delete_resource(self, client: AwsClient) -> bool:
235248
)
236249
return True
237250

251+
@classmethod
252+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
253+
return [AwsApiSpec("apigateway", "delete-authorizer", override_iam_permission="apigateway:DELETE")]
254+
238255

239256
@define(eq=False, slots=False)
240257
class AwsApiGatewayCanarySetting:
@@ -300,6 +317,12 @@ def delete_resource(self, client: AwsClient) -> bool:
300317
)
301318
return True
302319

320+
@classmethod
321+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
322+
return super().called_mutator_apis() + [
323+
AwsApiSpec("apigateway", "delete-stage", override_iam_permission="apigateway:DELETE")
324+
]
325+
303326

304327
@define(eq=False, slots=False)
305328
class AwsApiGatewayDeployment(AwsResource):
@@ -328,6 +351,12 @@ def delete_resource(self, client: AwsClient) -> bool:
328351
)
329352
return True
330353

354+
@classmethod
355+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
356+
return super().called_mutator_apis() + [
357+
AwsApiSpec("apigateway", "delete-deployment", override_iam_permission="apigateway:DELETE")
358+
]
359+
331360

332361
@define(eq=False, slots=False)
333362
class AwsApiGatewayEndpointConfiguration:
@@ -343,7 +372,9 @@ class AwsApiGatewayEndpointConfiguration:
343372
@define(eq=False, slots=False)
344373
class AwsApiGatewayRestApi(ApiGatewayTaggable, AwsResource):
345374
kind: ClassVar[str] = "aws_api_gateway_rest_api"
346-
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec("apigateway", "get-rest-apis", "items")
375+
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec(
376+
"apigateway", "get-rest-apis", "items", override_iam_permission="apigateway:GET"
377+
)
347378
reference_kinds: ClassVar[ModelReference] = {
348379
"successors": {
349380
"default": [
@@ -381,13 +412,19 @@ class AwsApiGatewayRestApi(ApiGatewayTaggable, AwsResource):
381412
api_disable_execute_api_endpoint: Optional[bool] = field(default=None)
382413

383414
@classmethod
384-
def called_apis(cls) -> List[AwsApiSpec]:
415+
def called_collect_apis(cls) -> List[AwsApiSpec]:
385416
return [
386417
cls.api_spec,
387-
AwsApiSpec("apigateway", "get-deployments"),
388-
AwsApiSpec("apigateway", "get-stages"),
389-
AwsApiSpec("apigateway", "get-authorizers"),
390-
AwsApiSpec("apigateway", "get-resources"),
418+
AwsApiSpec("apigateway", "get-deployments", override_iam_permission="apigateway:GET"),
419+
AwsApiSpec("apigateway", "get-stages", override_iam_permission="apigateway:GET"),
420+
AwsApiSpec("apigateway", "get-authorizers", override_iam_permission="apigateway:GET"),
421+
AwsApiSpec("apigateway", "get-resources", override_iam_permission="apigateway:GET"),
422+
]
423+
424+
@classmethod
425+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
426+
return super().called_mutator_apis() + [
427+
AwsApiSpec("apigateway", "delete-rest-api", override_iam_permission="apigateway:DELETE")
391428
]
392429

393430
@classmethod
@@ -470,7 +507,9 @@ class AwsApiGatewayMutualTlsAuthentication:
470507
@define(eq=False, slots=False)
471508
class AwsApiGatewayDomainName(ApiGatewayTaggable, AwsResource):
472509
kind: ClassVar[str] = "aws_api_gateway_domain_name"
473-
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec("apigateway", "get-domain-names", "items")
510+
api_spec: ClassVar[AwsApiSpec] = AwsApiSpec(
511+
"apigateway", "get-domain-names", "items", override_iam_permission="apigateway:GET"
512+
)
474513
reference_kinds: ClassVar[ModelReference] = {
475514
"predecessors": {"delete": ["aws_route53_zone"]},
476515
"successors": {"default": ["aws_route53_zone", "aws_vpc_endpoint"], "delete": ["aws_vpc_endpoint"]},

plugins/aws/resoto_plugin_aws/resource/athena.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,17 @@ class AwsAthenaWorkGroup(AwsResource):
8585
}
8686

8787
@classmethod
88-
def called_apis(cls) -> List[AwsApiSpec]:
88+
def called_collect_apis(cls) -> List[AwsApiSpec]:
8989
return [cls.api_spec, AwsApiSpec("athena", "get-work-group"), AwsApiSpec("athena", "list-tags-for-resource")]
9090

91+
@classmethod
92+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
93+
return [
94+
AwsApiSpec("athena", "tag-resource"),
95+
AwsApiSpec("athena", "untag-resource"),
96+
AwsApiSpec("athena", "delete-work-group"),
97+
]
98+
9199
@classmethod
92100
def collect(cls: Type[AwsResource], json: List[Json], builder: GraphBuilder) -> None:
93101
def fetch_workgroup(name: str) -> Optional[AwsAthenaWorkGroup]:
@@ -135,7 +143,7 @@ def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None:
135143
def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
136144
client.call(
137145
aws_service=self.api_spec.service,
138-
action="tag_resource",
146+
action="tag-resource",
139147
result_name=None,
140148
ResourceARN=self.arn,
141149
Tags=[{"Key": key, "Value": value}],
@@ -145,7 +153,7 @@ def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
145153
def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
146154
client.call(
147155
aws_service=self.api_spec.service,
148-
action="untag_resource",
156+
action="untag-resource",
149157
result_name=None,
150158
ResourceARN=self.arn,
151159
TagKeys=[key],
@@ -179,9 +187,17 @@ class AwsAthenaDataCatalog(AwsResource):
179187
datacatalog_parameters: Optional[Dict[str, str]] = None
180188

181189
@classmethod
182-
def called_apis(cls) -> List[AwsApiSpec]:
190+
def called_collect_apis(cls) -> List[AwsApiSpec]:
183191
return [cls.api_spec, AwsApiSpec("athena", "get-data-catalog"), AwsApiSpec("athena", "list-tags-for-resource")]
184192

193+
@classmethod
194+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
195+
return [
196+
AwsApiSpec("athena", "tag-resource"),
197+
AwsApiSpec("athena", "untag-resource"),
198+
AwsApiSpec("athena", "delete-data-catalog"),
199+
]
200+
185201
@classmethod
186202
def collect(cls: Type[AwsResource], json: List[Json], builder: GraphBuilder) -> None:
187203
def fetch_data_catalog(data_catalog_name: str) -> Optional[AwsAthenaDataCatalog]:
@@ -218,7 +234,7 @@ def add_tags(data_catalog: AwsAthenaDataCatalog) -> None:
218234
def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
219235
client.call(
220236
aws_service=self.api_spec.service,
221-
action="tag_resource",
237+
action="tag-resource",
222238
result_name=None,
223239
ResourceARN=self.arn,
224240
Tags=[{"Key": key, "Value": value}],
@@ -228,7 +244,7 @@ def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
228244
def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
229245
client.call(
230246
aws_service=self.api_spec.service,
231-
action="untag_resource",
247+
action="untag-resource",
232248
result_name=None,
233249
ResourceARN=self.arn,
234250
TagKeys=[key],

plugins/aws/resoto_plugin_aws/resource/autoscaling.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None:
282282
def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
283283
client.call(
284284
aws_service="autoscaling",
285-
action="create_or_update_tags",
285+
action="create-or-update-tags",
286286
result_name=None,
287287
Tags=[
288288
{
@@ -299,7 +299,7 @@ def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
299299
def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
300300
client.call(
301301
aws_service="autoscaling",
302-
action="delete_tags",
302+
action="delete-tags",
303303
result_name=None,
304304
Tags=[
305305
{
@@ -314,12 +314,20 @@ def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
314314
def delete_resource(self, client: AwsClient) -> bool:
315315
client.call(
316316
aws_service=self.api_spec.service,
317-
action="delete_auto_scaling_group",
317+
action="delete-auto-scaling-group",
318318
result_name=None,
319319
AutoScalingGroupName=self.name,
320320
ForceDelete=True,
321321
)
322322
return True
323323

324+
@classmethod
325+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
326+
return [
327+
AwsApiSpec("autoscaling", "create-or-update-tags"),
328+
AwsApiSpec("autoscaling", "delete-tags"),
329+
AwsApiSpec("autoscaling", "delete-auto-scaling-group"),
330+
]
331+
324332

325333
resources: List[Type[AwsResource]] = [AwsAutoScalingGroup]

plugins/aws/resoto_plugin_aws/resource/base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ class AwsApiSpec:
5353
result_property: Optional[str] = None
5454
parameter: Optional[Dict[str, Any]] = None
5555
expected_errors: Optional[List[str]] = None
56+
override_iam_permission: Optional[str] = None # only set if the permission can not be derived
5657

57-
def action_string(self) -> str:
58-
action = "".join(word.title() for word in self.api_action.split("-"))
59-
return f"{self.service}:{action}"
58+
def iam_permission(self) -> str:
59+
if self.override_iam_permission:
60+
return self.override_iam_permission
61+
else:
62+
action = "".join(word.title() for word in self.api_action.split("-"))
63+
return f"{self.service}:{action}"
6064

6165

6266
@define(eq=False, slots=False)
@@ -204,14 +208,18 @@ def collect(cls: Type[AwsResource], json: List[Json], builder: GraphBuilder) ->
204208
builder.add_node(instance, js)
205209

206210
@classmethod
207-
def called_apis(cls) -> List[AwsApiSpec]:
211+
def called_collect_apis(cls) -> List[AwsApiSpec]:
208212
# The default implementation will return the defined api_spec if defined, otherwise an empty list.
209213
# In case your resource needs more than this api call, please override this method and return the proper list.
210214
if spec := cls.api_spec:
211215
return [spec]
212216
else:
213217
return []
214218

219+
@classmethod
220+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
221+
return []
222+
215223
def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None:
216224
# Default behavior: add resource to the namespace
217225
pass

plugins/aws/resoto_plugin_aws/resource/cloudformation.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def _modify_tag(self, client: AwsClient, key: str, value: Optional[str], mode: L
123123
try:
124124
client.call(
125125
aws_service="cloudformation",
126-
action="update_stack",
126+
action="update-stack",
127127
result_name=None,
128128
StackName=self.name,
129129
Capabilities=["CAPABILITY_NAMED_IAM"],
@@ -161,9 +161,13 @@ def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
161161
return self._modify_tag(client, key, None, "delete")
162162

163163
def delete_resource(self, client: AwsClient) -> bool:
164-
client.call(aws_service=self.api_spec.service, action="delete_stack", result_name=None, StackName=self.name)
164+
client.call(aws_service=self.api_spec.service, action="delete-stack", result_name=None, StackName=self.name)
165165
return True
166166

167+
@classmethod
168+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
169+
return [AwsApiSpec("cloudformation", "update-stack"), AwsApiSpec("cloudformation", "delete-stack")]
170+
167171

168172
@define(eq=False, slots=False)
169173
class AwsCloudFormationAutoDeployment:
@@ -218,7 +222,7 @@ def _modify_tag(self, client: AwsClient, key: str, value: Optional[str], mode: L
218222
try:
219223
client.call(
220224
aws_service="cloudformation",
221-
action="update_stack_set",
225+
action="update-stack-set",
222226
result_name=None,
223227
StackSetName=self.name,
224228
Capabilities=["CAPABILITY_NAMED_IAM"],
@@ -245,11 +249,15 @@ def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
245249
def delete_resource(self, client: AwsClient) -> bool:
246250
client.call(
247251
aws_service=self.api_spec.service,
248-
action="delete_stack_set",
252+
action="delete-stack-set",
249253
result_name=None,
250254
StackSetName=self.name,
251255
)
252256
return True
253257

258+
@classmethod
259+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
260+
return [AwsApiSpec("cloudformation", "update-stack-set"), AwsApiSpec("cloudformation", "delete-stack-set")]
261+
254262

255263
resources: List[Type[AwsResource]] = [AwsCloudFormationStack, AwsCloudFormationStackSet]

plugins/aws/resoto_plugin_aws/resource/cloudwatch.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool:
2121
if spec := self.api_spec:
2222
client.call(
2323
aws_service=spec.service,
24-
action="tag_resource",
24+
action="tag-resource",
2525
result_name=None,
2626
ResourceARN=self.arn,
2727
Tags=[{"Key": key, "Value": value}],
@@ -35,7 +35,7 @@ def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
3535
if spec := self.api_spec:
3636
client.call(
3737
aws_service=spec.service,
38-
action="untag_resource",
38+
action="untag-resource",
3939
result_name=None,
4040
ResourceARN=self.arn,
4141
TagKeys=[key],
@@ -44,6 +44,10 @@ def delete_resource_tag(self, client: AwsClient, key: str) -> bool:
4444
return False
4545
return False
4646

47+
@classmethod
48+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
49+
return [AwsApiSpec("cloudwatch", "tag-resource"), AwsApiSpec("cloudwatch", "untag-resource")]
50+
4751

4852
@define(eq=False, slots=False)
4953
class AwsCloudwatchDimension:
@@ -185,9 +189,13 @@ def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None:
185189
)
186190

187191
def delete_resource(self, client: AwsClient) -> bool:
188-
client.call(aws_service=self.api_spec.service, action="delete_alarms", result_name=None, AlarmNames=[self.name])
192+
client.call(aws_service=self.api_spec.service, action="delete-alarms", result_name=None, AlarmNames=[self.name])
189193
return True
190194

195+
@classmethod
196+
def called_mutator_apis(cls) -> List[AwsApiSpec]:
197+
return super().called_mutator_apis() + [AwsApiSpec("cloudwatch", "delete-alarms")]
198+
191199

192200
@define(hash=True, frozen=True)
193201
class AwsCloudwatchQuery:

0 commit comments

Comments
 (0)