-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Extract the logic of managing SAM cloudformation stack from the the S… #2740
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,136 @@ | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| Bootstrap's user's development environment by creating cloud resources required by SAM CLI | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import boto3 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import click | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| from botocore.config import Config | ||||||||||||||||||||||||||||||||||||||
| from botocore.exceptions import ClientError, BotoCoreError, NoRegionError, NoCredentialsError, ProfileNotFound | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| from samcli.commands.exceptions import UserException, CredentialsError, RegionError | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| SAM_CLI_STACK_PREFIX = "aws-sam-cli-managed-" | ||||||||||||||||||||||||||||||||||||||
| LOG = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class ManagedStackError(UserException): | ||||||||||||||||||||||||||||||||||||||
| def __init__(self, ex): | ||||||||||||||||||||||||||||||||||||||
| self.ex = ex | ||||||||||||||||||||||||||||||||||||||
| message_fmt = f"Failed to create managed resources: {ex}" | ||||||||||||||||||||||||||||||||||||||
| super().__init__(message=message_fmt.format(ex=self.ex)) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def manage_stack(profile, region, stack_name, template_body): | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| if profile: | ||||||||||||||||||||||||||||||||||||||
| session = boto3.Session(profile_name=profile, region_name=region if region else None) | ||||||||||||||||||||||||||||||||||||||
| cloudformation_client = session.client("cloudformation") | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+32
Contributor
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. this is a new logic but doesn't not result in any behavior change as the method caller(see bootstrap.py) always pass profile=None (which how it used to be)
Contributor
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. I think we should not create a new session, the session is handled here aws-sam-cli/samcli/cli/context.py Lines 169 to 186 in f084cf2
so whenever we need to apply any changes to the AWS services session we can do it from this location only, like if we need to use specific services endpoints, so we can change in the default session, and we are sure that it will be applied to all AWS services connections
Contributor
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. This use case is different. The one you are referring to is for the --profile option where we execute a SAM CLI command against one AWS account. But for this use case we want to deploy a CFN template to multiple AWS accounts so we don't manage the AWS account through this --profile option. |
||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| cloudformation_client = boto3.client( | ||||||||||||||||||||||||||||||||||||||
| "cloudformation", config=Config(region_name=region if region else None) | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| except ProfileNotFound as ex: | ||||||||||||||||||||||||||||||||||||||
| raise CredentialsError( | ||||||||||||||||||||||||||||||||||||||
| f"Error Setting Up Managed Stack Client: the provided AWS name profile '{profile}' is not found. " | ||||||||||||||||||||||||||||||||||||||
| "please check the documentation for setting up a named profile: " | ||||||||||||||||||||||||||||||||||||||
| "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html" | ||||||||||||||||||||||||||||||||||||||
| ) from ex | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+42
Contributor
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. this is a new logic but doesn't not result in any behavior change as the method caller(see bootstrap.py) always pass profile=None (which how it used to be) so this |
||||||||||||||||||||||||||||||||||||||
| except NoCredentialsError as ex: | ||||||||||||||||||||||||||||||||||||||
| raise CredentialsError( | ||||||||||||||||||||||||||||||||||||||
| "Error Setting Up Managed Stack Client: Unable to resolve credentials for the AWS SDK for Python client. " | ||||||||||||||||||||||||||||||||||||||
| "Please see their documentation for options to pass in credentials: " | ||||||||||||||||||||||||||||||||||||||
| "https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html" | ||||||||||||||||||||||||||||||||||||||
| ) from ex | ||||||||||||||||||||||||||||||||||||||
| except NoRegionError as ex: | ||||||||||||||||||||||||||||||||||||||
| raise RegionError( | ||||||||||||||||||||||||||||||||||||||
| "Error Setting Up Managed Stack Client: Unable to resolve a region. " | ||||||||||||||||||||||||||||||||||||||
| "Please provide a region via the --region parameter or by the AWS_REGION environment variable." | ||||||||||||||||||||||||||||||||||||||
| ) from ex | ||||||||||||||||||||||||||||||||||||||
| return _create_or_get_stack(cloudformation_client, stack_name, template_body) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _create_or_get_stack(cloudformation_client, stack_name, template_body): | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| ds_resp = cloudformation_client.describe_stacks(StackName=stack_name) | ||||||||||||||||||||||||||||||||||||||
| stacks = ds_resp["Stacks"] | ||||||||||||||||||||||||||||||||||||||
| stack = stacks[0] | ||||||||||||||||||||||||||||||||||||||
| click.echo("\n\tLooking for resources needed for deployment: Found!") | ||||||||||||||||||||||||||||||||||||||
| _check_sanity_of_stack(stack, stack_name) | ||||||||||||||||||||||||||||||||||||||
| return stack["Outputs"] | ||||||||||||||||||||||||||||||||||||||
| except ClientError: | ||||||||||||||||||||||||||||||||||||||
| click.echo("\n\tLooking for resources needed for deployment: Not found.") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| stack = _create_stack( | ||||||||||||||||||||||||||||||||||||||
| cloudformation_client, stack_name, template_body | ||||||||||||||||||||||||||||||||||||||
| ) # exceptions are not captured from subcommands | ||||||||||||||||||||||||||||||||||||||
| _check_sanity_of_stack(stack, stack_name) | ||||||||||||||||||||||||||||||||||||||
| return stack["Outputs"] | ||||||||||||||||||||||||||||||||||||||
| except (ClientError, BotoCoreError) as ex: | ||||||||||||||||||||||||||||||||||||||
| LOG.debug("Failed to create managed resources", exc_info=ex) | ||||||||||||||||||||||||||||||||||||||
| raise ManagedStackError(str(ex)) from ex | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _check_sanity_of_stack(stack, stack_name): | ||||||||||||||||||||||||||||||||||||||
| tags = stack.get("Tags", None) | ||||||||||||||||||||||||||||||||||||||
| outputs = stack.get("Outputs", None) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # For some edge cases, stack could be in invalid state | ||||||||||||||||||||||||||||||||||||||
| # Check if stack information contains the Tags and Outputs as we expected | ||||||||||||||||||||||||||||||||||||||
| if tags is None or outputs is None: | ||||||||||||||||||||||||||||||||||||||
| stack_state = stack.get("StackStatus", None) | ||||||||||||||||||||||||||||||||||||||
| msg = ( | ||||||||||||||||||||||||||||||||||||||
| f"Stack {stack_name} is missing Tags and/or Outputs information and therefore not in a " | ||||||||||||||||||||||||||||||||||||||
| f"healthy state (Current state:{stack_state}). Failing as the stack was likely not created " | ||||||||||||||||||||||||||||||||||||||
| f"by the AWS SAM CLI" | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| raise UserException(msg) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # Sanity check for non-none stack? Sanity check for tag? | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| sam_cli_tag = next(t for t in tags if t["Key"] == "ManagedStackSource") | ||||||||||||||||||||||||||||||||||||||
| if not sam_cli_tag["Value"] == "AwsSamCli": | ||||||||||||||||||||||||||||||||||||||
| msg = ( | ||||||||||||||||||||||||||||||||||||||
| "Stack " | ||||||||||||||||||||||||||||||||||||||
| + stack_name | ||||||||||||||||||||||||||||||||||||||
| + " ManagedStackSource tag shows " | ||||||||||||||||||||||||||||||||||||||
| + sam_cli_tag["Value"] | ||||||||||||||||||||||||||||||||||||||
| + " which does not match the AWS SAM CLI generated tag value of AwsSamCli. " | ||||||||||||||||||||||||||||||||||||||
| "Failing as the stack was likely not created by the AWS SAM CLI." | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| raise UserException(msg) | ||||||||||||||||||||||||||||||||||||||
| except StopIteration as ex: | ||||||||||||||||||||||||||||||||||||||
| msg = ( | ||||||||||||||||||||||||||||||||||||||
| "Stack " + stack_name + " exists, but the ManagedStackSource tag is missing. " | ||||||||||||||||||||||||||||||||||||||
| "Failing as the stack was likely not created by the AWS SAM CLI." | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| raise UserException(msg) from ex | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _create_stack(cloudformation_client, stack_name, template_body): | ||||||||||||||||||||||||||||||||||||||
| click.echo("\tCreating the required resources...") | ||||||||||||||||||||||||||||||||||||||
| change_set_name = "InitialCreation" | ||||||||||||||||||||||||||||||||||||||
| change_set_resp = cloudformation_client.create_change_set( | ||||||||||||||||||||||||||||||||||||||
| StackName=stack_name, | ||||||||||||||||||||||||||||||||||||||
| TemplateBody=template_body, | ||||||||||||||||||||||||||||||||||||||
| Tags=[{"Key": "ManagedStackSource", "Value": "AwsSamCli"}], | ||||||||||||||||||||||||||||||||||||||
| ChangeSetType="CREATE", | ||||||||||||||||||||||||||||||||||||||
| ChangeSetName=change_set_name, # this must be unique for the stack, but we only create so that's fine | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| stack_id = change_set_resp["StackId"] | ||||||||||||||||||||||||||||||||||||||
| change_waiter = cloudformation_client.get_waiter("change_set_create_complete") | ||||||||||||||||||||||||||||||||||||||
| change_waiter.wait( | ||||||||||||||||||||||||||||||||||||||
| ChangeSetName=change_set_name, StackName=stack_name, WaiterConfig={"Delay": 15, "MaxAttempts": 60} | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| cloudformation_client.execute_change_set(ChangeSetName=change_set_name, StackName=stack_name) | ||||||||||||||||||||||||||||||||||||||
| stack_waiter = cloudformation_client.get_waiter("stack_create_complete") | ||||||||||||||||||||||||||||||||||||||
| stack_waiter.wait(StackName=stack_id, WaiterConfig={"Delay": 15, "MaxAttempts": 60}) | ||||||||||||||||||||||||||||||||||||||
| ds_resp = cloudformation_client.describe_stacks(StackName=stack_name) | ||||||||||||||||||||||||||||||||||||||
| stacks = ds_resp["Stacks"] | ||||||||||||||||||||||||||||||||||||||
| click.echo("\tSuccessfully created!") | ||||||||||||||||||||||||||||||||||||||
| return stacks[0] | ||||||||||||||||||||||||||||||||||||||
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.
setting profile to None as the existing code didn't use this param anywhere