@@ -132,33 +132,217 @@ def publish(self):
132132 return False
133133
134134
135+ def cleanup_failed_stack (self , stack_name ):
136+ """
137+ Clean up failed stack if it exists in ROLLBACK_COMPLETE state.
138+
139+ Args:
140+ stack_name: Name of the stack to clean up
141+
142+ Returns:
143+ bool: True if cleanup successful or not needed, False if cleanup failed
144+ """
145+ try :
146+ # Check stack status
147+ cmd = [
148+ 'aws' , 'cloudformation' , 'describe-stacks' ,
149+ '--region' , self .region ,
150+ '--stack-name' , stack_name ,
151+ '--query' , 'Stacks[0].StackStatus' ,
152+ '--output' , 'text'
153+ ]
154+
155+ process = subprocess .run (
156+ cmd ,
157+ check = True ,
158+ text = True ,
159+ stdout = subprocess .PIPE ,
160+ stderr = subprocess .PIPE
161+ )
162+
163+ stack_status = process .stdout .strip ()
164+
165+ if stack_status in ['ROLLBACK_COMPLETE' , 'CREATE_FAILED' , 'DELETE_FAILED' ]:
166+ logger .info (f"Cleaning up failed stack { stack_name } (status: { stack_status } )" )
167+
168+ delete_cmd = [
169+ 'aws' , 'cloudformation' , 'delete-stack' ,
170+ '--region' , self .region ,
171+ '--stack-name' , stack_name
172+ ]
173+
174+ subprocess .run (delete_cmd , check = True , stdout = subprocess .PIPE , stderr = subprocess .PIPE )
175+
176+ # Wait for deletion to complete
177+ wait_cmd = [
178+ 'aws' , 'cloudformation' , 'wait' , 'stack-delete-complete' ,
179+ '--region' , self .region ,
180+ '--stack-name' , stack_name
181+ ]
182+
183+ subprocess .run (wait_cmd , check = True , stdout = subprocess .PIPE , stderr = subprocess .PIPE )
184+ logger .info (f"Successfully cleaned up failed stack { stack_name } " )
185+
186+ return True
187+
188+ except subprocess .CalledProcessError :
189+ # Stack doesn't exist - no cleanup needed
190+ return True
191+ except Exception as e :
192+ logger .error (f"Failed to cleanup stack { stack_name } : { e } " )
193+ return False
194+
195+ def get_existing_service_role_arn (self ):
196+ """
197+ Check if CloudFormation service role stack exists and return its ARN.
198+
199+ Returns:
200+ str: The ARN of the existing service role, or None if not found
201+ """
202+ service_role_stack_name = f"{ self .cfn_prefix } -cloudformation-service-role"
203+
204+ try :
205+ describe_cmd = [
206+ 'aws' , 'cloudformation' , 'describe-stacks' ,
207+ '--region' , self .region ,
208+ '--stack-name' , service_role_stack_name ,
209+ '--query' , 'Stacks[0].Outputs[?OutputKey==`ServiceRoleArn`].OutputValue' ,
210+ '--output' , 'text'
211+ ]
212+
213+ process = subprocess .run (
214+ describe_cmd ,
215+ check = True ,
216+ text = True ,
217+ stdout = subprocess .PIPE ,
218+ stderr = subprocess .PIPE
219+ )
220+
221+ service_role_arn = process .stdout .strip ()
222+ if service_role_arn and service_role_arn != "None" :
223+ logger .info (f"Found existing service role: { service_role_arn } " )
224+ return service_role_arn
225+ else :
226+ return None
227+
228+ except subprocess .CalledProcessError :
229+ logger .debug (f"Service role stack { service_role_stack_name } not found" )
230+ return None
231+ except Exception as e :
232+ logger .error (f"Error checking for existing service role: { e } " )
233+ return None
234+
235+ def deploy_service_role (self ):
236+ """
237+ Deploy the CloudFormation service role stack only if it doesn't exist.
238+
239+ Returns:
240+ str: The ARN of the service role, or None if deployment failed
241+ """
242+ # First check if service role already exists
243+ existing_arn = self .get_existing_service_role_arn ()
244+ if existing_arn :
245+ logger .info ("CloudFormation service role already exists, skipping deployment" )
246+ return existing_arn
247+
248+ service_role_stack_name = f"{ self .cfn_prefix } -cloudformation-service-role"
249+ service_role_template = 'iam-roles/cloudformation-management/IDP-Cloudformation-Service-Role.yaml'
250+
251+ try :
252+ # Verify template file exists
253+ template_path = os .path .join (self .abs_cwd , service_role_template )
254+ if not os .path .exists (template_path ):
255+ raise FileNotFoundError (f"Service role template not found: { template_path } " )
256+
257+ logger .info (f"Deploying CloudFormation service role stack: { service_role_stack_name } " )
258+
259+ # Deploy the service role stack
260+ cmd = [
261+ 'aws' , 'cloudformation' , 'deploy' ,
262+ '--region' , self .region ,
263+ '--template-file' , service_role_template ,
264+ '--capabilities' , 'CAPABILITY_NAMED_IAM' ,
265+ '--stack-name' , service_role_stack_name
266+ ]
267+
268+ logger .debug (f"Running service role deploy command: { ' ' .join (cmd )} " )
269+
270+ process = subprocess .run (
271+ cmd ,
272+ check = True ,
273+ text = True ,
274+ stdout = subprocess .PIPE ,
275+ stderr = subprocess .PIPE ,
276+ cwd = self .cwd
277+ )
278+
279+ logger .debug (f"Service role deploy stdout: { process .stdout } " )
280+ if process .stderr :
281+ logger .debug (f"Service role deploy stderr: { process .stderr } " )
282+
283+ # Get the service role ARN from stack outputs
284+ service_role_arn = self .get_existing_service_role_arn ()
285+ if service_role_arn :
286+ logger .info (f"Successfully deployed service role: { service_role_arn } " )
287+ return service_role_arn
288+ else :
289+ logger .error ("Failed to retrieve service role ARN after deployment" )
290+ return None
291+
292+ except FileNotFoundError as e :
293+ logger .error (f"Service role template error: { e } " )
294+ return None
295+ except subprocess .CalledProcessError as e :
296+ logger .error (f"Failed to deploy service role: { e } " )
297+ if e .stdout :
298+ logger .debug (f"Command stdout: { e .stdout } " )
299+ if e .stderr :
300+ logger .debug (f"Command stderr: { e .stderr } " )
301+
302+ # Cleanup failed service role deployment
303+ logger .info ("Cleaning up failed service role deployment..." )
304+ self .cleanup_failed_stack (service_role_stack_name )
305+ return None
306+ except Exception as e :
307+ logger .error (f"Unexpected error during service role deployment: { e } " )
308+ return None
309+
135310 def install (self , admin_email : str , idp_pattern : str ):
136311 """
137- Install the IDP stack using CloudFormation.
312+ Install the IDP stack using CloudFormation with service role .
138313
139314 Args:
140315 admin_email: Email address for the admin user
141316 idp_pattern: IDP pattern to deploy
142- stack_name: Optional stack name (defaults to idp-Stack)
143317 """
144318 template_file = '.aws-sam/idp-main.yaml'
145-
146319 s3_prefix = f"{ self .cfn_prefix } /0.2.2" # TODO: Make version configurable
147320
148321 try :
322+ # Step 1: Ensure CloudFormation service role exists
323+ logger .info ("Step 1: Ensuring CloudFormation service role exists..." )
324+ service_role_arn = self .deploy_service_role ()
325+ if not service_role_arn :
326+ logger .error ("Failed to deploy or find service role. Aborting IDP deployment." )
327+ return False
328+
329+ # Step 2: Deploy IDP stack using the service role
330+ logger .info ("Step 2: Deploying IDP stack using service role..." )
331+
149332 # Verify template file exists
150333 template_path = os .path .join (self .abs_cwd , template_file )
151334 if not os .path .exists (template_path ):
152335 raise FileNotFoundError (f"Template file not found: { template_path } " )
153336
154- # Construct the CloudFormation deploy command
337+ # Construct the CloudFormation deploy command with service role
155338 cmd = [
156339 'aws' , 'cloudformation' , 'deploy' ,
157340 '--region' , self .region ,
158341 '--template-file' , template_file ,
159342 '--s3-bucket' , self .s3_bucket ,
160343 '--s3-prefix' , s3_prefix ,
161344 '--capabilities' , 'CAPABILITY_NAMED_IAM' , 'CAPABILITY_AUTO_EXPAND' ,
345+ '--role-arn' , service_role_arn , # Use the service role
162346 '--parameter-overrides' ,
163347 "DocumentKnowledgeBase=DISABLED" ,
164348 f"IDPPattern={ idp_pattern } " ,
@@ -187,7 +371,7 @@ def install(self, admin_email: str, idp_pattern: str):
187371 if process .stderr :
188372 logger .debug (f"CloudFormation deploy stderr: { process .stderr } " )
189373
190- logger .info (f"Successfully deployed stack { stack_name } in { self .region } " )
374+ logger .info (f"Successfully deployed stack { self . stack_name } in { self .region } using service role " )
191375 return True
192376
193377 except FileNotFoundError as e :
@@ -199,6 +383,10 @@ def install(self, admin_email: str, idp_pattern: str):
199383 logger .debug (f"Command stdout: { e .stdout } " )
200384 if e .stderr :
201385 logger .debug (f"Command stderr: { e .stderr } " )
386+
387+ # Cleanup failed deployment for next attempt
388+ logger .info ("Cleaning up failed deployment for next attempt..." )
389+ self .cleanup_failed_stack (self .stack_name )
202390 return False
203391 except Exception as e :
204392 logger .error (f"Unexpected error during stack deployment: { e } " )
0 commit comments