44"""
55Common 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+
716import re
817import secrets
918from copy import deepcopy
2029from 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
163154def 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
211199def 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