Skip to content

Commit b893855

Browse files
committed
Merge branch 'feature/fix-sdlc-pipeline-polling' into 'develop'
Fix SDLC Pipeline Polling and Security Compliance Issues See merge request genaiic-reusable-assets/engagement-artifacts/genaiic-idp-accelerator!323
2 parents 6881abf + b3efd66 commit b893855

File tree

12 files changed

+182
-186
lines changed

12 files changed

+182
-186
lines changed

iam-roles/cloudformation-management/IDP-Cloudformation-Service-Role.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Resources:
1313
CloudFormationServiceRole:
1414
Type: AWS::IAM::Role
1515
Properties:
16-
RoleName: IDPAcceleratorCloudFormationServiceRole
16+
RoleName: !Sub '${AWS::StackName}-CFServiceRole'
1717
AssumeRolePolicyDocument:
1818
Version: '2012-10-17'
1919
Statement:
@@ -109,7 +109,7 @@ Resources:
109109
PassRolePolicy:
110110
Type: AWS::IAM::ManagedPolicy
111111
Properties:
112-
ManagedPolicyName: IDP-PassRolePolicy
112+
ManagedPolicyName: !Sub '${AWS::StackName}-PassRolePolicy'
113113
Description: Policy to allow passing the IDP CloudFormation service role
114114
PolicyDocument:
115115
Version: '2012-10-17'

patterns/pattern-1/template.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@ Resources:
714714
- id: W92
715715
reason: "Function does not require concurrent execution limits as it is designed to scale based on demand"
716716
Properties:
717+
PermissionsBoundary: !If [HasPermissionsBoundary, !Ref PermissionsBoundaryArn, !Ref AWS::NoValue]
717718
CodeUri: src/bda_discovery_function/
718719
Handler: index.handler
719720
Runtime: python3.12

scripts/sdlc/cfn/codepipeline-s3.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ Resources:
155155
- export IDP_CFN_PREFIX=$(make cfn-prefix) || { echo "CFN prefix generation failed"; exit 1; }
156156
- make install -e IDP_CFN_PREFIX=$IDP_CFN_PREFIX
157157
- make smoketest -e IDP_CFN_PREFIX=$IDP_CFN_PREFIX
158-
- make uninstall -e IDP_CFN_PREFIX=$IDP_CFN_PREFIX
158+
post_build:
159+
commands:
160+
- echo "Running cleanup regardless of build result..."
161+
- make uninstall -e IDP_CFN_PREFIX=$IDP_CFN_PREFIX || echo "Cleanup failed but continuing"
159162
160163
DeploymentPipeline:
161164
Type: 'AWS::CodePipeline::Pipeline'
@@ -177,6 +180,7 @@ Resources:
177180
Configuration:
178181
S3Bucket: !Ref BucketName
179182
S3ObjectKey: !Ref FileKey
183+
PollForSourceChanges: true
180184
OutputArtifacts:
181185
- Name: SourceOutput
182186
RunOrder: 1

scripts/sdlc/idp-cli/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
IDP_ACCOUNT_ID?=715841340161
1+
IDP_ACCOUNT_ID?=020432867916
22
AWS_REGION?=us-east-1
33
IDP_ADMIN_EMAIL?[email protected]
44
IDP_CWD?=../../../

scripts/sdlc/idp-cli/poetry.lock

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/sdlc/idp-cli/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ dependencies = [
1414
"loguru (>=0.7.3,<0.8.0)",
1515
"python-slugify (>=8.0.4,<9.0.0)",
1616
"pytest (>=8.3.4,<9.0.0)",
17-
"urllib3 (>=2.5.0,<3.0.0)"
17+
"urllib3 (>=2.5.0,<3.0.0)",
18+
"setuptools"
1819
]
1920

2021
[tool.poetry]

scripts/sdlc/idp-cli/src/idp_cli/cli/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def uninstall(
7474
def smoketest(
7575
stack_name: str = typer.Option("idp-Stack", "--stack-name", help="Name of the deployed stack to test"),
7676
file_path: str = typer.Option("../../../samples/lending_package.pdf", "--file-path", help="Path to the test file"),
77-
verify_string: str = typer.Option("BIGTOWN, MA, 02801", "--verify-string", help="String to verify in the processed output")
77+
verify_string: str = typer.Option("ANYTOWN, USA 12345", "--verify-string", help="String to verify in the processed output")
7878
):
7979
"""
8080
Run a smoke test on the deployed IDP Accelerator

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

Lines changed: 62 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -136,106 +136,6 @@ def publish(self):
136136
return False
137137

138138

139-
def cleanup_failed_stack(self, stack_name):
140-
"""
141-
Clean up failed stack if it exists in ROLLBACK_COMPLETE state.
142-
143-
Args:
144-
stack_name: Name of the stack to clean up
145-
146-
Returns:
147-
bool: True if cleanup successful or not needed, False if cleanup failed
148-
"""
149-
try:
150-
# Check stack status
151-
cmd = [
152-
'aws', 'cloudformation', 'describe-stacks',
153-
'--region', self.region,
154-
'--stack-name', stack_name,
155-
'--query', 'Stacks[0].StackStatus',
156-
'--output', 'text'
157-
]
158-
159-
process = subprocess.run(
160-
cmd,
161-
check=True,
162-
text=True,
163-
stdout=subprocess.PIPE,
164-
stderr=subprocess.PIPE
165-
)
166-
167-
stack_status = process.stdout.strip()
168-
169-
if stack_status in ['ROLLBACK_COMPLETE', 'CREATE_FAILED', 'DELETE_FAILED']:
170-
logger.info(f"Cleaning up failed stack {stack_name} (status: {stack_status})")
171-
172-
delete_cmd = [
173-
'aws', 'cloudformation', 'delete-stack',
174-
'--region', self.region,
175-
'--stack-name', stack_name
176-
]
177-
178-
subprocess.run(delete_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
179-
180-
# Wait for deletion to complete
181-
wait_cmd = [
182-
'aws', 'cloudformation', 'wait', 'stack-delete-complete',
183-
'--region', self.region,
184-
'--stack-name', stack_name
185-
]
186-
187-
subprocess.run(wait_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
188-
logger.info(f"Successfully cleaned up failed stack {stack_name}")
189-
190-
return True
191-
192-
except subprocess.CalledProcessError:
193-
# Stack doesn't exist - no cleanup needed
194-
return True
195-
except Exception as e:
196-
logger.error(f"Failed to cleanup stack {stack_name}: {e}")
197-
return False
198-
199-
def get_service_role_arn(self):
200-
"""
201-
Check if CloudFormation service role stack exists and return its ARN.
202-
203-
Returns:
204-
str: The ARN of the existing service role, or None if not found
205-
"""
206-
service_role_stack_name = f"{self.cfn_prefix}-cloudformation-service-role"
207-
208-
try:
209-
describe_cmd = [
210-
'aws', 'cloudformation', 'describe-stacks',
211-
'--region', self.region,
212-
'--stack-name', service_role_stack_name,
213-
'--query', 'Stacks[0].Outputs[?OutputKey==`ServiceRoleArn`].OutputValue',
214-
'--output', 'text'
215-
]
216-
217-
process = subprocess.run(
218-
describe_cmd,
219-
check=True,
220-
text=True,
221-
stdout=subprocess.PIPE,
222-
stderr=subprocess.PIPE
223-
)
224-
225-
service_role_arn = process.stdout.strip()
226-
if service_role_arn and service_role_arn != "None":
227-
logger.info(f"Found existing service role: {service_role_arn}")
228-
return service_role_arn
229-
else:
230-
return None
231-
232-
except subprocess.CalledProcessError:
233-
logger.debug(f"Service role stack {service_role_stack_name} not found")
234-
return None
235-
except Exception as e:
236-
logger.error(f"Error checking for existing service role: {e}")
237-
return None
238-
239139
def deploy_service_role(self):
240140
"""
241141
Deploy the CloudFormation service role stack.
@@ -278,8 +178,8 @@ def deploy_service_role(self):
278178
if process.stderr:
279179
logger.debug(f"Service role deploy stderr: {process.stderr}")
280180

281-
# Get the service role ARN from stack outputs
282-
service_role_arn = self.get_service_role_arn()
181+
# Get the service role ARN from the deployed stack
182+
service_role_arn = self._get_service_role_arn_from_stack(service_role_stack_name)
283183
if service_role_arn:
284184
logger.info(f"Successfully deployed service role: {service_role_arn}")
285185
return service_role_arn
@@ -296,62 +196,78 @@ def deploy_service_role(self):
296196
logger.debug(f"Command stdout: {e.stdout}")
297197
if e.stderr:
298198
logger.debug(f"Command stderr: {e.stderr}")
299-
300-
# Cleanup failed service role deployment
301-
logger.info("Cleaning up failed service role deployment...")
302-
self.cleanup_failed_stack(service_role_stack_name)
303199
return None
304200
except Exception as e:
305201
logger.error(f"Unexpected error during service role deployment: {e}")
306202
return None
307203

204+
def _get_service_role_arn_from_stack(self, stack_name):
205+
"""
206+
Get service role ARN from a specific stack.
207+
208+
Returns:
209+
str: The ARN of the service role, or None if not found
210+
"""
211+
try:
212+
describe_cmd = [
213+
'aws', 'cloudformation', 'describe-stacks',
214+
'--region', self.region,
215+
'--stack-name', stack_name,
216+
'--query', 'Stacks[0].Outputs[?OutputKey==`ServiceRoleArn`].OutputValue',
217+
'--output', 'text'
218+
]
219+
220+
process = subprocess.run(
221+
describe_cmd,
222+
check=True,
223+
text=True,
224+
stdout=subprocess.PIPE,
225+
stderr=subprocess.PIPE
226+
)
227+
228+
service_role_arn = process.stdout.strip()
229+
if service_role_arn and service_role_arn != "None":
230+
return service_role_arn
231+
else:
232+
return None
233+
234+
except subprocess.CalledProcessError:
235+
return None
236+
except Exception as e:
237+
logger.error(f"Error getting service role ARN from stack {stack_name}: {e}")
238+
return None
239+
308240
def create_permission_boundary_policy(self):
309-
"""Create an 'allow everything' permission boundary policy if it doesn't exist"""
241+
"""Create an 'allow everything' permission boundary policy"""
310242

311-
policy_name = "IDPPermissionBoundary"
243+
policy_name = f"{self.cfn_prefix}-IDPPermissionBoundary"
312244
iam = boto3.client('iam')
313245

314246
try:
315-
# First, check if the policy already exists
316-
account_id = boto3.client('sts').get_caller_identity()['Account']
317-
policy_arn = f"arn:aws:iam::{account_id}:policy/{policy_name}"
247+
policy_document = {
248+
"Version": "2012-10-17",
249+
"Statement": [
250+
{
251+
"Effect": "Allow",
252+
"Action": "*",
253+
"Resource": "*"
254+
}
255+
]
256+
}
318257

319-
# Try to get the existing policy
320-
iam.get_policy(PolicyArn=policy_arn)
321-
logger.info(f"Permission boundary policy already exists: {policy_arn}")
258+
response = iam.create_policy(
259+
PolicyName=policy_name,
260+
PolicyDocument=json.dumps(policy_document),
261+
Description="Permission boundary for IDP deployment - allows all actions"
262+
)
263+
264+
policy_arn = response['Policy']['Arn']
265+
logger.info(f"Created permission boundary policy: {policy_arn}")
322266
return policy_arn
323267

324-
except ClientError as e:
325-
if e.response['Error']['Code'] == 'NoSuchEntity':
326-
# Policy doesn't exist, create it
327-
policy_document = {
328-
"Version": "2012-10-17",
329-
"Statement": [
330-
{
331-
"Effect": "Allow",
332-
"Action": "*",
333-
"Resource": "*"
334-
}
335-
]
336-
}
337-
338-
try:
339-
response = iam.create_policy(
340-
PolicyName=policy_name,
341-
PolicyDocument=json.dumps(policy_document),
342-
Description="Permission boundary for IDP deployment - allows all actions"
343-
)
344-
345-
policy_arn = response['Policy']['Arn']
346-
logger.info(f"Created permission boundary policy: {policy_arn}")
347-
return policy_arn
348-
349-
except ClientError as create_error:
350-
logger.error(f"Error creating permission boundary policy: {create_error}")
351-
return None
352-
else:
353-
logger.error(f"Error checking for existing permission boundary policy: {e}")
354-
return None
268+
except ClientError as create_error:
269+
logger.error(f"Error creating permission boundary policy: {create_error}")
270+
return None
355271

356272
def validate_permission_boundary(self, stack_name, boundary_arn):
357273
"""Validate that all IAM roles in the stack and nested stacks have the permission boundary"""
@@ -511,6 +427,7 @@ def install(self, admin_email: str, idp_pattern: str):
511427
logger.info("Step 4: Validating permission boundary on all IAM roles...")
512428
if not self.validate_permission_boundary(self.stack_name, permission_boundary_arn):
513429
logger.error("Permission boundary validation failed!")
430+
logger.error("Deployment failed due to security policy violations.")
514431
return False
515432

516433
logger.info("Deployment and validation completed successfully!")
@@ -525,10 +442,6 @@ def install(self, admin_email: str, idp_pattern: str):
525442
logger.debug(f"Command stdout: {e.stdout}")
526443
if e.stderr:
527444
logger.debug(f"Command stderr: {e.stderr}")
528-
529-
# Cleanup failed deployment for next attempt
530-
logger.info("Cleaning up failed deployment for next attempt...")
531-
self.cleanup_failed_stack(self.stack_name)
532445
return False
533446
except Exception as e:
534447
logger.error(f"Unexpected error during stack deployment: {e}")

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def verify_result(self, folder_key):
102102
_, output_bucket_name = self.get_bucket_names()
103103

104104
# Define the path to the expected result.json file
105-
object_path = f"{folder_key}/pages/1/result.json"
105+
object_path = f"{folder_key}/pages/0/result.json"
106106
logger.debug(f"Looking for result file at: s3://{output_bucket_name}/{object_path}")
107107

108108
try:
@@ -122,7 +122,7 @@ def verify_result(self, folder_key):
122122
# Check if the text content contains the expected verification string
123123
if self.verify_string not in result_json["pages"][0]["representation"]["markdown"]:
124124
logger.error(f"Text content does not contain expected string: '{self.verify_string}'")
125-
logger.debug(f"Actual text starts with: '{result_json['text'][:100]}...'")
125+
logger.debug(f"Actual text starts with: '{result_json['pages'][0]['representation']['markdown'][:100]}...'")
126126
raise ValueError("Text content does not contain expected verification string")
127127

128128
logger.debug("Smoke test verification passed!")

0 commit comments

Comments
 (0)