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
13 changes: 9 additions & 4 deletions samcli/cli/cli_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ class TomlProvider:
A parser for toml configuration files
"""

def __init__(self, section=None):
def __init__(self, section=None, cmd_names=None):
"""
The constructor for TomlProvider class
:param section: section defined in the configuration file nested within `cmd`
:param cmd_names: cmd_name defined in the configuration file
"""
self.section = section
self.cmd_names = cmd_names

def __call__(self, config_path, config_env, cmd_names):
"""
Expand Down Expand Up @@ -67,18 +69,21 @@ def __call__(self, config_path, config_env, cmd_names):
LOG.debug("Config file '%s' does not exist", samconfig.path())
return resolved_config

if not self.cmd_names:
self.cmd_names = cmd_names

try:
LOG.debug(
"Loading configuration values from [%s.%s.%s] (env.command_name.section) in config file at '%s'...",
config_env,
cmd_names,
self.cmd_names,
self.section,
samconfig.path(),
)

# NOTE(TheSriram): change from tomlkit table type to normal dictionary,
# so that click defaults work out of the box.
resolved_config = dict(samconfig.get_all(cmd_names, self.section, env=config_env).items())
resolved_config = dict(samconfig.get_all(self.cmd_names, self.section, env=config_env).items())
LOG.debug("Configuration values successfully loaded.")
LOG.debug("Configuration values are: %s", resolved_config)

Expand All @@ -87,7 +92,7 @@ def __call__(self, config_path, config_env, cmd_names):
"Error reading configuration from [%s.%s.%s] (env.command_name.section) "
"in configuration file at '%s' with : %s",
config_env,
cmd_names,
self.cmd_names,
self.section,
samconfig.path(),
str(ex),
Expand Down
1 change: 1 addition & 0 deletions samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"samcli.commands.local.local",
"samcli.commands.package",
"samcli.commands.deploy",
"samcli.commands.delete",
"samcli.commands.logs",
"samcli.commands.publish",
# We intentionally do not expose the `bootstrap` command for now. We might open it up later
Expand Down
6 changes: 6 additions & 0 deletions samcli/commands/delete/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
`sam delete` command
"""

# Expose the cli object here
from .command import cli # noqa
71 changes: 71 additions & 0 deletions samcli/commands/delete/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
CLI command for "delete" command
"""

import logging

import click
from samcli.cli.cli_config_file import TomlProvider, configuration_option
from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args

from samcli.lib.utils.version_checker import check_newer_version

SHORT_HELP = "Delete an AWS SAM application."

HELP_TEXT = """The sam delete command deletes a Cloudformation Stack and deletes all your resources which were created.

\b
e.g. sam delete --stack-name sam-app --region us-east-1

\b
"""

CONFIG_SECTION = "parameters"
CONFIG_COMMAND = "deploy"
LOG = logging.getLogger(__name__)


@click.command(
"delete",
short_help=SHORT_HELP,
context_settings={"ignore_unknown_options": False, "allow_interspersed_args": True, "allow_extra_args": True},
help=HELP_TEXT,
)
@configuration_option(provider=TomlProvider(section=CONFIG_SECTION, cmd_names=[CONFIG_COMMAND]))
@click.option(
"--stack-name",
required=False,
help="The name of the AWS CloudFormation stack you want to delete. ",
)
@aws_creds_options
@common_options
@pass_context
@check_newer_version
@print_cmdline_args
def cli(
ctx,
stack_name,
config_file,
config_env,
):
"""
`sam delete` command entry point
"""

# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing
do_cli(stack_name, ctx.region, ctx.profile) # pragma: no cover


def do_cli(stack_name, region, profile):
"""
Implementation of the ``cli`` method
"""
from samcli.commands.delete.delete_context import DeleteContext

ctx = click.get_current_context()
s3_bucket = ctx.default_map.get("s3_bucket", None)
s3_prefix = ctx.default_map.get("s3_prefix", None)
with DeleteContext(
stack_name=stack_name, region=region, s3_bucket=s3_bucket, s3_prefix=s3_prefix, profile=profile
) as delete_context:
delete_context.run()
131 changes: 131 additions & 0 deletions samcli/commands/delete/delete_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Delete a SAM stack
"""

import boto3

# import docker
import click
from click import confirm
from click import prompt

from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.lib.delete.cf_utils import CfUtils
from samcli.lib.delete.utils import get_cf_template_name
from samcli.lib.package.s3_uploader import S3Uploader

# from samcli.yamlhelper import yaml_parse

# Intentionally commented
# from samcli.lib.package.artifact_exporter import Template
# from samcli.lib.package.ecr_uploader import ECRUploader
# from samcli.lib.package.uploaders import Uploaders


class DeleteContext:
def __init__(self, stack_name, region, s3_bucket, s3_prefix, profile):
self.stack_name = stack_name
self.region = region
self.profile = profile
self.s3_bucket = s3_bucket
self.s3_prefix = s3_prefix
self.cf_utils = None
self.start_bold = "\033[1m"
self.end_bold = "\033[0m"
self.s3_uploader = None
# self.uploaders = None
self.cf_template_file_name = None
self.delete_artifacts_folder = None
self.delete_cf_template_file = None

def __enter__(self):
return self

def __exit__(self, *args):
pass

def run(self):
"""
Delete the stack based on the argument provided by customers and samconfig.toml.
"""
if not self.stack_name:
self.stack_name = prompt(
f"\t{self.start_bold}Enter stack name you want to delete{self.end_bold}", type=click.STRING
)

if not self.region:
self.region = prompt(
f"\t{self.start_bold}Enter region you want to delete from{self.end_bold}", type=click.STRING
)
delete_stack = confirm(
f"\t{self.start_bold}Are you sure you want to delete the stack {self.stack_name}?{self.end_bold}",
default=False,
)
# Fetch the template using the stack-name
if delete_stack and self.region:
boto_config = get_boto_config_with_user_agent()

# Define cf_client based on the region as different regions can have same stack-names
cloudformation_client = boto3.client(
"cloudformation", region_name=self.region if self.region else None, config=boto_config
)

s3_client = boto3.client("s3", region_name=self.region if self.region else None, config=boto_config)
# ecr_client = boto3.client("ecr", region_name=self.region if self.region else None, config=boto_config)

self.s3_uploader = S3Uploader(s3_client=s3_client, bucket_name=self.s3_bucket, prefix=self.s3_prefix)

# docker_client = docker.from_env()
# ecr_uploader = ECRUploader(docker_client, ecr_client, None, None)

self.cf_utils = CfUtils(cloudformation_client)

is_deployed = self.cf_utils.has_stack(self.stack_name)

if is_deployed:
template_str = self.cf_utils.get_stack_template(self.stack_name, "Original")

# template_dict = yaml_parse(template_str)

if self.s3_bucket and self.s3_prefix:
self.delete_artifacts_folder = confirm(
f"\t{self.start_bold}Are you sure you want to delete the folder"
+ f"{self.s3_prefix} in S3 which contains the artifacts?{self.end_bold}",
default=False,
)
if not self.delete_artifacts_folder:
self.cf_template_file_name = get_cf_template_name(template_str, "template")
self.delete_cf_template_file = confirm(
f"\t{self.start_bold}Do you want to delete the template file"
+ f" {self.cf_template_file_name} in S3?{self.end_bold}",
default=False,
)

click.echo("\n")
# Delete the primary stack
self.cf_utils.delete_stack(self.stack_name)

click.echo("- deleting Cloudformation stack {0}".format(self.stack_name))

# Delete the artifacts
# Intentionally commented
# self.uploaders = Uploaders(self.s3_uploader, ecr_uploader)
# template = Template(None, None, self.uploaders, None)
# template.delete(template_dict)

# Delete the CF template file in S3
if self.delete_cf_template_file:
self.s3_uploader.delete_artifact(self.cf_template_file_name)

# Delete the folder of artifacts if s3_bucket and s3_prefix provided
elif self.delete_artifacts_folder:
self.s3_uploader.delete_prefix_artifacts()

# Delete the ECR companion stack

if self.cf_template_file_name:
click.echo(f"- deleting template file {self.cf_template_file_name}")
click.echo("\n")
click.echo("delete complete")
else:
click.echo("Error: The input stack {0} does not exist on Cloudformation".format(self.stack_name))
14 changes: 14 additions & 0 deletions samcli/commands/delete/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Exceptions that are raised by sam delete
"""
from samcli.commands.exceptions import UserException


class DeleteFailedError(UserException):
def __init__(self, stack_name, msg):
self.stack_name = stack_name
self.msg = msg

message_fmt = "Failed to delete the stack: {stack_name}, {msg}"

super().__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg))
Empty file added samcli/lib/delete/__init__.py
Empty file.
101 changes: 101 additions & 0 deletions samcli/lib/delete/cf_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Delete Cloudformation stacks and s3 files
"""

import logging

from botocore.exceptions import ClientError, BotoCoreError
from samcli.commands.delete.exceptions import DeleteFailedError

LOG = logging.getLogger(__name__)


class CfUtils:
def __init__(self, cloudformation_client):
self._client = cloudformation_client

def has_stack(self, stack_name):
"""
Checks if a CloudFormation stack with given name exists

:param stack_name: Name or ID of the stack
:return: True if stack exists. False otherwise
"""
try:
resp = self._client.describe_stacks(StackName=stack_name)
if not resp["Stacks"]:
return False

stack = resp["Stacks"][0]
return stack["StackStatus"] != "REVIEW_IN_PROGRESS"

except ClientError as e:
# If a stack does not exist, describe_stacks will throw an
# exception. Unfortunately we don't have a better way than parsing
# the exception msg to understand the nature of this exception.

if "Stack with id {0} does not exist".format(stack_name) in str(e):
LOG.debug("Stack with id %s does not exist", stack_name)
return False
raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e
except BotoCoreError as e:
# If there are credentials, environment errors,
# catch that and throw a delete failed error.

LOG.debug("Botocore Exception : %s", str(e))
raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e

except Exception as e:
# We don't know anything about this exception. Don't handle
LOG.debug("Unable to get stack details.", exc_info=e)
raise e

def get_stack_template(self, stack_name, stage):
"""
Return the Cloudformation template of the given stack_name

:param stack_name: Name or ID of the stack
:param stage: The Stage of the template Original or Processed
:return: Template body of the stack
"""
try:
resp = self._client.get_template(StackName=stack_name, TemplateStage=stage)
if not resp["TemplateBody"]:
return None

return resp["TemplateBody"]

except (ClientError, BotoCoreError) as e:
# If there are credentials, environment errors,
# catch that and throw a delete failed error.

LOG.debug("Failed to delete stack : %s", str(e))
raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e

except Exception as e:
# We don't know anything about this exception. Don't handle
LOG.debug("Unable to get stack details.", exc_info=e)
raise e

def delete_stack(self, stack_name):
"""
Delete the Cloudformation stack with the given stack_name

:param stack_name: Name or ID of the stack
:return: Status of deletion
"""
try:
resp = self._client.delete_stack(StackName=stack_name)
return resp

except (ClientError, BotoCoreError) as e:
# If there are credentials, environment errors,
# catch that and throw a delete failed error.

LOG.debug("Failed to delete stack : %s", str(e))
raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e

except Exception as e:
# We don't know anything about this exception. Don't handle
LOG.debug("Unable to get stack details.", exc_info=e)
raise e
Loading