-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: use --role-arn service role for companion stack create/update #9267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
64dfdf0
4bd8f41
5fdfcc7
75f5f7c
a3353b2
c04273d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| """ | ||
|
|
||
| import logging | ||
| from typing import Dict, List, Optional | ||
| from typing import Any, Dict, List, Optional | ||
|
|
||
| import boto3 | ||
| from botocore.config import Config | ||
|
|
@@ -41,14 +41,15 @@ class CompanionStackManager: | |
| _cfn_client: CloudFormationClient | ||
| _s3_client: S3Client | ||
|
|
||
| def __init__(self, stack_name, region, s3_bucket, s3_prefix): | ||
| def __init__(self, stack_name, region, s3_bucket, s3_prefix, role_arn: Optional[str] = None): | ||
| self._companion_stack = CompanionStack(stack_name) | ||
| self._builder = CompanionStackBuilder(self._companion_stack) | ||
| self._boto_config = Config(region_name=region if region else None) | ||
| self._update_stack_waiter_config = {"Delay": 10, "MaxAttempts": 120} | ||
| self._delete_stack_waiter_config = {"Delay": 10, "MaxAttempts": 120} | ||
| self._s3_bucket = s3_bucket | ||
| self._s3_prefix = s3_prefix | ||
| self._role_arn = role_arn | ||
| try: | ||
| self._cfn_client = boto3.client("cloudformation", config=self._boto_config) | ||
| self._ecr_client = boto3.client("ecr", config=self._boto_config) | ||
|
|
@@ -117,16 +118,21 @@ def update_companion_stack(self) -> None: | |
| template_url = s3_uploader.to_path_style_s3_url(parts["Key"], parts.get("Version", None)) | ||
|
|
||
| exists = self.does_companion_stack_exist() | ||
| # Use the service role passed via --role-arn (if any) so the companion stack is created/updated | ||
| # with the same permissions as the main stack. | ||
| stack_kwargs: Dict[str, Any] = { | ||
| "StackName": stack_name, | ||
| "TemplateURL": template_url, | ||
| "Capabilities": ["CAPABILITY_AUTO_EXPAND"], | ||
| } | ||
| if self._role_arn: | ||
| stack_kwargs["RoleARN"] = self._role_arn | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [BUG] The service role is now wired into def sync_repos(self) -> None:
has_repo = bool(self.get_repository_mapping())
if self.does_companion_stack_exist():
self.delete_unreferenced_repos() # <-- direct ECR call, caller creds
...def delete_unreferenced_repos(self) -> None:
repos = self.get_unreferenced_repos()
for repo in repos:
try:
self._ecr_client.delete_repository(repositoryName=repo.physical_id, force=True)
except self._ecr_client.exceptions.RepositoryNotFoundException:
LOG.debug(...)
Two options, either is fine:
Worth covering with a unit test for the unreferenced-repo path, since the current tests exercise only create/update.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — when role_arn is set, the manager now assumes it via STS and builds self._ecr_client from the assumed-role credentials, so delete_unreferenced_repos and all other direct ECR calls run under one identity. An assume-role failure raises an actionable AWSServiceClientError. Added tests covering the assumed-role delete path and the failure case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [BUG] The service role is applied to if self.does_companion_stack_exist():
self.delete_unreferenced_repos()
if has_repo:
self.update_companion_stack()
else:
self._delete_companion_stack() # no RoleARN
def deletecompanion_stack(self) -> None:
stack_name = self._companion_stack.stack_name
waiter = self._cfn_client.get_waiter("stack_delete_complete")
delete_kwargs: Dict[str, Any] = {"StackName": stack_name}
if self._role_arn:
delete_kwargs["RoleARN"] = self._role_arn
self._cfn_client.delete_stack(**delete_kwargs)
waiter.wait(StackName=stack_name, WaiterConfig=self._delete_stack_waiter_config)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — _delete_companion_stack now passes RoleARN on delete_stack when the role is set, so the whole companion-stack lifecycle (create/update/delete) runs under one identity. Also covered by a new test asserting delete_stack gets the RoleARN kwarg; the no-role case still deletes with just StackName. |
||
| if exists: | ||
| self._cfn_client.update_stack( | ||
| StackName=stack_name, TemplateURL=template_url, Capabilities=["CAPABILITY_AUTO_EXPAND"] | ||
| ) | ||
| self._cfn_client.update_stack(**stack_kwargs) | ||
| update_waiter = self._cfn_client.get_waiter("stack_update_complete") | ||
| update_waiter.wait(StackName=stack_name, WaiterConfig=self._update_stack_waiter_config) | ||
| else: | ||
| self._cfn_client.create_stack( | ||
| StackName=stack_name, TemplateURL=template_url, Capabilities=["CAPABILITY_AUTO_EXPAND"] | ||
| ) | ||
| self._cfn_client.create_stack(**stack_kwargs) | ||
| create_waiter = self._cfn_client.get_waiter("stack_create_complete") | ||
| create_waiter.wait(StackName=stack_name, WaiterConfig=self._update_stack_waiter_config) | ||
|
|
||
|
|
@@ -279,7 +285,13 @@ def is_repo_uri(self, repo_uri: Optional[str], function_logical_id: str) -> bool | |
|
|
||
|
|
||
| def sync_ecr_stack( | ||
| template_file: str, stack_name: str, region: str, s3_bucket: str, s3_prefix: str, image_repositories: Dict[str, str] | ||
| template_file: str, | ||
| stack_name: str, | ||
| region: str, | ||
| s3_bucket: str, | ||
| s3_prefix: str, | ||
| image_repositories: Dict[str, str], | ||
| role_arn: Optional[str] = None, | ||
| ) -> Dict[str, str]: | ||
| """Blocking call to sync local functions with ECR Companion Stack | ||
|
|
||
|
|
@@ -297,6 +309,10 @@ def sync_ecr_stack( | |
| S3 prefix for the bucket | ||
| image_repositories : Dict[str, str] | ||
| Mapping between function logical ID and ECR URI | ||
| role_arn : Optional[str] | ||
| Optional service role ARN used when creating or updating the companion stack. | ||
| When provided, the companion stack is created/updated with the same service | ||
| role as the main stack. | ||
|
|
||
| Returns | ||
| ------- | ||
|
|
@@ -305,7 +321,7 @@ def sync_ecr_stack( | |
| for Functions without a repo specified. | ||
| """ | ||
| image_repositories = image_repositories.copy() if image_repositories else {} | ||
| manager = CompanionStackManager(stack_name, region, s3_bucket, s3_prefix) | ||
| manager = CompanionStackManager(stack_name, region, s3_bucket, s3_prefix, role_arn=role_arn) | ||
|
|
||
| stacks = SamLocalStackProvider.get_stacks(template_file, language_extensions_enabled=False)[0] | ||
| function_provider = SamFunctionProvider(stacks, ignore_code_extraction_warnings=True) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[BUG] The fix only covers the non-guided branch, but
--role-arnapplies to guided deploys too — it is passed toDeployContextunconditionally atcommand.py:416, and it is also persisted intosamconfig.toml. That meanssam deploy --guided --role-arn <role> --resolve-image-reposstill reproduces the exact failure from #5051: the main stack deploys under the service role, while the companion stack is created with the caller's credentials and fails onecr:CreateRepository.Both companion-stack entry points in the guided flow are affected:
samcli/commands/deploy/guided_context.py:191—sync_ecr_stack(...)when--resolve-image-reposis setsamcli/commands/deploy/guided_context.py:362—CompanionStackManager(stack_name, region, s3_bucket, s3_prefix)inprompt_image_repository, which callsmanager.sync_repos()at line 390 and therefore reachesupdate_companion_stack()/create_stackThreading
role_arnthroughGuidedContextwould close the gap, e.g. accept it inGuidedContext.__init__, passrole_arn=role_arnfrom theguidedbranch ofdo_cli, and forward it at both call sites:If leaving guided out is deliberate, it is worth stating why in the PR description, since the reported symptom is identical in that path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — you're right, the guided path had the same bug.
GuidedContextnow acceptsrole_arn,do_clipasses--role-arnthrough on the guided branch, and it's forwarded at both companion-stack call sites (sync_ecr_stackinguided_promptsand theCompanionStackManagerinprompt_image_repository). Added guided tests asserting the role reaches both. Verified: 24/24 guided-context tests and all companion-stack manager tests pass.