Skip to content

Commit 85c35c8

Browse files
author
Taniya Mathur
committed
feat: add CloudFormation service role deployment with cleanup to install service
- Deploy CloudFormation service role automatically if missing - Use service role for IDP stack deployment with --role-arn - Add cleanup_failed_stack for robust failure handling - Enable deployment with reduced user permissions
1 parent aad1dde commit 85c35c8

File tree

1 file changed

+193
-5
lines changed

1 file changed

+193
-5
lines changed

scripts/sdlc/idp-cli/src/idp_cli/service/install_service.py

Lines changed: 193 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)