Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions samcli/commands/bootstrap/exceptions.py

This file was deleted.

119 changes: 10 additions & 109 deletions samcli/lib/bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,129 +4,30 @@

import json
import logging

import boto3

import click

from botocore.config import Config
from botocore.exceptions import ClientError, BotoCoreError, NoRegionError, NoCredentialsError

from samcli.commands.bootstrap.exceptions import ManagedStackError
from samcli import __version__
from samcli.cli.global_config import GlobalConfig
from samcli.commands.exceptions import UserException, CredentialsError, RegionError

from samcli.commands.exceptions import UserException
from samcli.lib.utils.managed_cloudformation_stack import manage_stack as manage_cloudformation_stack

SAM_CLI_STACK_NAME = "aws-sam-cli-managed-default"
LOG = logging.getLogger(__name__)


def manage_stack(profile, region):
try:
cloudformation_client = boto3.client("cloudformation", config=Config(region_name=region if region else None))
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)


def _create_or_get_stack(cloudformation_client):
try:
stack = None
try:
ds_resp = cloudformation_client.describe_stacks(StackName=SAM_CLI_STACK_NAME)
stacks = ds_resp["Stacks"]
stack = stacks[0]
click.echo("\n\tLooking for resources needed for deployment: Found!")
except ClientError:
click.echo("\n\tLooking for resources needed for deployment: Not found.")
stack = _create_stack(cloudformation_client) # exceptions are not captured from subcommands

_check_sanity_of_stack(stack)

outputs = stack["Outputs"]
try:
bucket_name = next(o for o in outputs if o["OutputKey"] == "SourceBucket")["OutputValue"]
except StopIteration as ex:
msg = (
"Stack " + SAM_CLI_STACK_NAME + " exists, but is missing the managed source bucket key. "
"Failing as this stack was likely not created by the AWS SAM CLI."
)
raise UserException(msg) from ex
# This bucket name is what we would write to a config file
return bucket_name
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):
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("StackName", None)
msg = (
f"Stack {SAM_CLI_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)
outputs = manage_cloudformation_stack(
profile=None, region=region, stack_name=SAM_CLI_STACK_NAME, template_body=_get_stack_template()
Copy link
Contributor Author

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

)

# 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 "
+ SAM_CLI_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)
bucket_name = next(o for o in outputs if o["OutputKey"] == "SourceBucket")["OutputValue"]
except StopIteration as ex:
msg = (
"Stack " + SAM_CLI_STACK_NAME + " exists, but the ManagedStackSource tag is missing. "
"Failing as the stack was likely not created by the AWS SAM CLI."
"Stack " + SAM_CLI_STACK_NAME + " exists, but is missing the managed source bucket key. "
"Failing as this stack was likely not created by the AWS SAM CLI."
)
raise UserException(msg) from ex


def _create_stack(cloudformation_client):
click.echo("\tCreating the required resources...")
change_set_name = "InitialCreation"
change_set_resp = cloudformation_client.create_change_set(
StackName=SAM_CLI_STACK_NAME,
TemplateBody=_get_stack_template(),
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=SAM_CLI_STACK_NAME, WaiterConfig={"Delay": 15, "MaxAttempts": 60}
)
cloudformation_client.execute_change_set(ChangeSetName=change_set_name, StackName=SAM_CLI_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=SAM_CLI_STACK_NAME)
stacks = ds_resp["Stacks"]
click.echo("\tSuccessfully created!")
return stacks[0]
# This bucket name is what we would write to a config file
return bucket_name


def _get_stack_template():
Expand Down
136 changes: 136 additions & 0 deletions samcli/lib/utils/managed_cloudformation_stack.py
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)

Copy link
Contributor

Choose a reason for hiding this comment

The 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

def _refresh_session(self):
"""
Update boto3's default session by creating a new session based on values set in the context. Some properties of
the Boto3's session object are read-only. Therefore when Click parses new AWS session related properties (like
region & profile), it will call this method to create a new session with latest values for these properties.
"""
try:
botocore_session = botocore.session.get_session()
boto3.setup_default_session(
botocore_session=botocore_session, region_name=self._aws_region, profile_name=self._aws_profile
)
# get botocore session and setup caching for MFA based credentials
botocore_session.get_component("credential_provider").get_provider(
"assume-role"
).cache = credentials.JSONFileCache()
except botocore.exceptions.ProfileNotFound as ex:
raise CredentialsError(str(ex)) from ex

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

Copy link
Contributor Author

@elbayaaa elbayaaa Mar 22, 2021

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 ProfileNotFoundexception will never be raised

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]
Loading