Skip to content
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

fix: managed stack #1585

Merged
merged 2 commits into from
Nov 23, 2019
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: 11 additions & 0 deletions samcli/commands/bootstrap/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Exceptions that are raised by sam bootstrap
"""
from samcli.commands.exceptions import UserException


class ManagedStackError(UserException):
def __init__(self, ex):
self.ex = ex
message_fmt = f"\nFailed to create managed resources: {ex}"
super(ManagedStackError, self).__init__(message=message_fmt.format(ex=self.ex))
4 changes: 2 additions & 2 deletions samcli/commands/deploy/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
CLI command for "deploy" command
"""
import json
import tempfile
import logging

import click
from click.types import FuncParamType

from samcli.lib.utils import temp_file_utils
from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.cli.context import get_cmd_names
from samcli.cli.main import pass_context, common_options, aws_creds_options
Expand Down Expand Up @@ -256,7 +256,7 @@ def do_cli(
confirm_changeset=changeset_decision if guided else confirm_changeset,
)

with tempfile.NamedTemporaryFile() as output_template_file:
with temp_file_utils.tempfile_platform_independent() as output_template_file:

with PackageContext(
template_file=template_file,
Expand Down
80 changes: 44 additions & 36 deletions samcli/lib/bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
"""

import json
import logging

import boto3

import click

from botocore.config import Config
from botocore.exceptions import ClientError, NoRegionError, NoCredentialsError
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


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


def manage_stack(profile, region):
Expand All @@ -34,46 +38,50 @@ def manage_stack(profile, region):


def _create_or_get_stack(cloudformation_client):
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
# Sanity check for non-none stack? Sanity check for tag?
tags = stack["Tags"]
try:
sam_cli_tag = next(t for t in tags if t["Key"] == "ManagedStackSource")
if not sam_cli_tag["Value"] == "AwsSamCli":
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
# Sanity check for non-none stack? Sanity check for tag?
tags = stack["Tags"]
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)
except StopIteration:
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. "
"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."
)
raise UserException(msg)
except StopIteration:
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."
)
raise UserException(msg)
outputs = stack["Outputs"]
try:
bucket_name = next(o for o in outputs if o["OutputKey"] == "SourceBucket")["OutputValue"]
except StopIteration:
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)
# This bucket name is what we would write to a config file
return bucket_name
outputs = stack["Outputs"]
try:
bucket_name = next(o for o in outputs if o["OutputKey"] == "SourceBucket")["OutputValue"]
except StopIteration:
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)
# 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))


def _create_stack(cloudformation_client):
Expand Down
26 changes: 26 additions & 0 deletions samcli/lib/utils/temp_file_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Helper functions for temporary files
"""
import os
import contextlib
import tempfile


def remove(path):
if path:
try:
os.remove(path)
except OSError:
pass


@contextlib.contextmanager
def tempfile_platform_independent():
# NOTE(TheSriram): Setting delete=False is specific to windows.
# https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile
_tempfile = tempfile.NamedTemporaryFile(delete=False)
try:
yield _tempfile
finally:
_tempfile.close()
remove(_tempfile.name)
5 changes: 3 additions & 2 deletions tests/unit/lib/bootstrap/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from botocore.exceptions import ClientError, NoCredentialsError, NoRegionError
from botocore.stub import Stubber

from samcli.commands.bootstrap.exceptions import ManagedStackError
from samcli.commands.exceptions import UserException, CredentialsError, RegionError
from samcli.lib.bootstrap.bootstrap import manage_stack, _create_or_get_stack, _get_stack_template, SAM_CLI_STACK_NAME

Expand Down Expand Up @@ -171,7 +172,7 @@ def test_change_set_creation_fails(self):
}
stubber.add_client_error("create_change_set", service_error_code="ClientError", expected_params=ccs_params)
stubber.activate()
with self.assertRaises(ClientError):
with self.assertRaises(ManagedStackError):
_create_or_get_stack(stub_cf)
stubber.assert_no_pending_responses()
stubber.deactivate()
Expand Down Expand Up @@ -201,7 +202,7 @@ def test_change_set_execution_fails(self):
"execute_change_set", service_error_code="InsufficientCapabilities", expected_params=ecs_params
)
stubber.activate()
with self.assertRaises(ClientError):
with self.assertRaises(ManagedStackError):
_create_or_get_stack(stub_cf)
stubber.assert_no_pending_responses()
stubber.deactivate()
20 changes: 20 additions & 0 deletions tests/unit/lib/utils/test_file_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os
import tempfile
from unittest import TestCase

from samcli.lib.utils.temp_file_utils import remove, tempfile_platform_independent


class TestFile(TestCase):
def test_file_remove(self):
_file = tempfile.NamedTemporaryFile(delete=False)
remove(_file.name)
self.assertFalse(os.path.exists(_file.name))
# No Exception thrown
remove(os.path.join(os.getcwd(), "random"))

def test_temp_file(self):
_path = None
with tempfile_platform_independent() as _tempf:
_path = _tempf.name
self.assertFalse(os.path.exists(_path))