Skip to content

Commit 9d6e2ff

Browse files
committed
Returns non 0 error code if cannot update statck. Linted and typed functions
1 parent 77882c6 commit 9d6e2ff

File tree

2 files changed

+86
-73
lines changed

2 files changed

+86
-73
lines changed

ecs_composex/cli.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,16 @@ def main():
227227
root_stack = generate_full_template(settings)
228228
process_stacks(root_stack, settings)
229229

230-
if settings.deploy:
231-
deploy(settings, root_stack)
232-
elif settings.plan:
233-
plan(settings, root_stack, apply=args.apply, cleanup=args.cleanup)
230+
try:
231+
if settings.deploy:
232+
deploy(settings, root_stack)
233+
return 0
234+
elif settings.plan:
235+
plan(settings, root_stack, apply=args.apply, cleanup=args.cleanup)
236+
except Exception as error:
237+
LOG.error("Failed to execute the command successfully")
238+
LOG.exception(error)
239+
return 2
234240
return 0
235241

236242

ecs_composex/common/aws.py

Lines changed: 76 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
"""
55
Common functions and variables fetched from AWS.
66
"""
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING, Union
10+
11+
if TYPE_CHECKING:
12+
from boto3.session import Session
13+
from ecs_composex.common.settings import ComposeXSettings
14+
from ecs_composex.common.stacks import ComposeXStack
15+
716
import re
817
import secrets
918
from copy import deepcopy
@@ -20,16 +29,11 @@
2029
from ecs_composex.iam import ROLE_ARN_ARG
2130

2231

23-
def get_cross_role_session(session, arn, region_name=None, session_name=None):
32+
def get_cross_role_session(
33+
session: Session, arn: str, region_name: str = None, session_name: str = None
34+
) -> Session:
2435
"""
2536
Function to override ComposeXSettings session to specific session for Lookup
26-
27-
:param boto3.session.Session session: The original session fetching the credentials for X-Role
28-
:param str arn:
29-
:param str region_name: Name of region for session
30-
:param str session_name: Override name of the session
31-
:return: boto3 session from lookup settings
32-
:rtype: boto3.session.Session
3337
"""
3438
if not session_name:
3539
session_name = "ComposeX@Lookup"
@@ -42,14 +46,9 @@ def get_cross_role_session(session, arn, region_name=None, session_name=None):
4246
raise
4347

4448

45-
def define_lookup_role_from_info(info, session):
49+
def define_lookup_role_from_info(info: dict, session: Session) -> Session:
4650
"""
4751
Function to override ComposeXSettings session to specific session for Lookup
48-
49-
:param info:
50-
:param session:
51-
:return: boto3 session from lookup settings
52-
:rtype: boto3.session.Session
5352
"""
5453
if not keyisset(ROLE_ARN_ARG, info):
5554
return session
@@ -77,13 +76,9 @@ def set_filters_from_tags_list(tags: list) -> list:
7776
return filters
7877

7978

80-
def define_tagsgroups_filter_tags(tags) -> list:
79+
def define_tagsgroups_filter_tags(tags: list[dict]) -> list:
8180
"""
8281
Function to create the filters out of tags list
83-
84-
:param list tags: list of Key/Value dict
85-
:return: filters
86-
:rtype: list
8782
"""
8883
if isinstance(tags, list):
8984
return set_filters_from_tags_list(tags)
@@ -100,13 +95,11 @@ def define_tagsgroups_filter_tags(tags) -> list:
10095
raise TypeError("Tags must be one of", [list, dict], "Got", type(tags))
10196

10297

103-
def get_resources_from_tags(session, aws_resource_search, search_tags):
98+
def get_resources_from_tags(
99+
session: Session, aws_resource_search: str, search_tags: list
100+
) -> Union[dict, None]:
104101
"""
105-
106-
:param boto3.session.Session session: The boto3 session for API calls
107-
:param str aws_resource_search: AWS Service short code, ie. rds, ec2
108-
:param list search_tags: The tags to search the resource with.
109-
:return:
102+
Function to retrieve AWS Resources ARNs from the tags using the Resource Groups Tagging API
110103
"""
111104
try:
112105
client = session.client("resourcegroupstaggingapi")
@@ -120,16 +113,14 @@ def get_resources_from_tags(session, aws_resource_search, search_tags):
120113
return None
121114

122115

123-
def handle_multi_results(arns, name, res_type, regexp, allow_multi=False):
116+
def handle_multi_results(
117+
arns: list[str], name: str, res_type: str, regexp: str, allow_multi: bool = False
118+
) -> Union[str, list[str]]:
124119
"""
125-
Function to evaluate more than one result to see if we can match an unique name.
120+
Function to evaluate more than one result to see if we can match a unique name.
126121
127-
:param list arns:
128-
:param str name:
129-
:param str res_type:
130-
:param str regexp:
131122
:raises LookupError:
132-
:return: The ARN of the resource matching the name.
123+
:return: The ARN(s) of the resource matching the name. Supports to return multiple ARNs
133124
"""
134125
found = 0
135126
found_arn = None
@@ -161,16 +152,15 @@ def handle_multi_results(arns, name, res_type, regexp, allow_multi=False):
161152

162153

163154
def handle_search_results(
164-
arns, name, res_types, aws_resource_search, allow_multi=False
165-
):
155+
arns: list[str],
156+
name: str,
157+
res_types,
158+
aws_resource_search: str,
159+
allow_multi: bool = False,
160+
) -> Union[str, list[str]]:
166161
"""
167162
Function to parse tag resource search results
168163
169-
:param list arns:
170-
:param str name:
171-
:param dict res_types:
172-
:param str aws_resource_search:
173-
:return:
174164
"""
175165
if not arns:
176166
raise LookupError(
@@ -190,13 +180,11 @@ def handle_search_results(
190180
return arns[0]
191181

192182

193-
def validate_search_input(res_types, res_type):
183+
def validate_search_input(res_types: dict, res_type: str) -> None:
194184
"""
195185
Function to validate the search query
196186
197-
:param dict res_types:
198-
:param str res_type:
199-
:return:
187+
:raises: KeyError
200188
"""
201189

202190
if not isinstance(res_type, str):
@@ -209,8 +197,12 @@ def validate_search_input(res_types, res_type):
209197

210198

211199
def find_aws_resource_arn_from_tags_api(
212-
info, session, aws_resource_search, types=None, allow_multi=False
213-
):
200+
info: dict,
201+
session: Session,
202+
aws_resource_search: str,
203+
types: dict = None,
204+
allow_multi: bool = False,
205+
) -> Union[str, list[str]]:
214206
"""
215207
Function to find the RDS DB based on info
216208
@@ -231,25 +223,30 @@ def find_aws_resource_arn_from_tags_api(
231223
resources_r = get_resources_from_tags(session, aws_resource_search, search_tags)
232224
LOG.debug(search_tags)
233225
if not resources_r or not keyisset("ResourceTagMappingList", resources_r):
234-
arns = []
226+
resource_arns = []
235227
else:
236-
arns = [i["ResourceARN"] for i in resources_r["ResourceTagMappingList"]]
228+
resource_arns = [
229+
i["ResourceARN"] for i in resources_r["ResourceTagMappingList"]
230+
]
237231
return handle_search_results(
238-
arns, name, res_types, aws_resource_search, allow_multi=allow_multi
232+
resource_arns, name, res_types, aws_resource_search, allow_multi=allow_multi
239233
)
240234

241235

242-
def assert_can_create_stack(client, name):
236+
def assert_can_create_stack(client, name: str) -> bool:
243237
"""
244238
Checks whether a stack already exists or not
239+
240+
:raises: LookupError
241+
:raises: ClientError
245242
"""
246243
try:
247244
stack_r = client.describe_stacks(StackName=name)
248245
if not keyisset("Stacks", stack_r):
249246
return True
250247
stacks = stack_r["Stacks"]
251248
if len(stacks) != 1:
252-
raise LookupError("Too many stacks found with machine name", name)
249+
raise LookupError("Too many stacks found with stack name", name)
253250
stack = stacks[0]
254251
if stack["StackStatus"] == "REVIEW_IN_PROGRESS":
255252
return stack
@@ -263,7 +260,7 @@ def assert_can_create_stack(client, name):
263260
raise error
264261

265262

266-
def assert_can_update_stack(client, name):
263+
def assert_can_update_stack(client, name) -> bool:
267264
"""
268265
Checks whether a stack already exists or not
269266
"""
@@ -283,12 +280,13 @@ def assert_can_update_stack(client, name):
283280
return False
284281

285282

286-
def validate_stack_availability(settings, root_stack):
283+
def validate_can_deploy_stack_from_settings(
284+
settings: ComposeXSettings, root_stack: ComposeXStack
285+
) -> None:
287286
"""
288287
Function to check that the stack can be updated
289-
:param settings:
290-
:param root_stack:
291-
:return:
288+
289+
:raises: ValueError
292290
"""
293291
if not settings.upload:
294292
raise RuntimeError(
@@ -301,14 +299,11 @@ def validate_stack_availability(settings, root_stack):
301299
)
302300

303301

304-
def deploy(settings, root_stack):
302+
def deploy(settings: ComposeXSettings, root_stack: ComposeXStack) -> Union[str, None]:
305303
"""
306304
Function to deploy (create or update) the stack to CFN.
307-
:param ComposeXSettings settings:
308-
:param ComposeXStack root_stack:
309-
:return:
310305
"""
311-
validate_stack_availability(settings, root_stack)
306+
validate_can_deploy_stack_from_settings(settings, root_stack)
312307
client = settings.session.client("cloudformation")
313308
if assert_can_create_stack(client, settings.name):
314309
res = client.create_stack(
@@ -336,16 +331,28 @@ def deploy(settings, root_stack):
336331
return None
337332

338333

339-
def get_change_set_status(client, change_set_name, settings):
334+
def get_change_set_status(
335+
client, change_set_name: str, settings: ComposeXSettings
336+
) -> str:
337+
"""
338+
Function to determine whether we can create a new changeset.
339+
340+
If it already exists in a failed status, we raise an exception to report we cannot go forward until user fixes it
341+
in their AWS account.
342+
If the changeset already exists, in a pending state, we wait for it to get to a ready status.
343+
If the changeset already exists, in a ready status, we dump a display of expected changes and return the status.
344+
345+
"""
340346
pending_statuses = [
341347
"CREATE_PENDING",
342348
"CREATE_IN_PROGRESS",
343349
"DELETE_PENDING",
344350
"DELETE_IN_PROGRESS",
345351
"REVIEW_IN_PROGRESS",
352+
"UPDATE_ROLLBACK_IN_PROGRESS",
346353
]
347354
success_statuses = ["CREATE_COMPLETE", "DELETE_COMPLETE"]
348-
failed_statuses = ["DELETE_FAILED", "FAILED"]
355+
failed_statuses = ["DELETE_FAILED", "FAILED", "UPDATE_ROLLBACK_FAILED"]
349356
ready = False
350357
status = None
351358
while not ready:
@@ -381,16 +388,16 @@ def get_change_set_status(client, change_set_name, settings):
381388
return status
382389

383390

384-
def plan(settings, root_stack, apply=None, cleanup=None):
391+
def plan(
392+
settings: ComposeXSettings,
393+
root_stack: ComposeXStack,
394+
apply: bool = None,
395+
cleanup: bool = None,
396+
) -> None:
385397
"""
386398
Function to create a recursive change-set and return diffs
387-
:param ComposeXSettings settings:
388-
:param ComposeXStack root_stack:
389-
:param apply: Optional[bool] - Whether to apply the change-set (True/False). Default is None (prompt user).
390-
:param cleanup: Optional[bool] - Whether to clean up the change-set (True/False). Default is None (prompt user).
391-
:return:
392399
"""
393-
validate_stack_availability(settings, root_stack)
400+
validate_can_deploy_stack_from_settings(settings, root_stack)
394401
client = settings.session.client("cloudformation")
395402
change_set_name = f"{settings.name}" + "".join(
396403
secrets.choice(ascii_lowercase) for _ in range(10)

0 commit comments

Comments
 (0)