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

feat: Delete methods for CF stacks and S3 files #2981

Merged
merged 16 commits into from
Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from 12 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
90 changes: 90 additions & 0 deletions samcli/commands/delete/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
CLI command for "delete" command
"""

import logging

import click
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 and the artifacts created by sam deploy."

HELP_TEXT = """The sam delete command deletes the CloudFormation
stack and all the artifacts which were created using sam deploy.
\b
e.g. sam delete
\b
"""

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,
)
@click.option(
"--stack-name",
required=False,
help="The name of the AWS CloudFormation stack you want to delete. ",
)
@click.option(
"--config-file",
help=(
"The path and file name of the configuration file containing default parameter values to use. "
"Its default value is 'samconfig.toml' in project directory. For more information about configuration files, "
"see: "
"https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html."
),
type=click.STRING,
default="samconfig.toml",
show_default=True,
)
@click.option(
"--config-env",
help=(
"The environment name specifying the default parameter values in the configuration file to use. "
"Its default value is 'default'. For more information about configuration files, see: "
"https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html."
),
type=click.STRING,
default="default",
show_default=True,
)
@aws_creds_options
@common_options
@pass_context
@check_newer_version
@print_cmdline_args
def cli(
ctx,
stack_name: str,
config_file: str,
config_env: str,
):
"""
`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=stack_name, region=ctx.region, config_file=config_file, config_env=config_env, profile=ctx.profile
) # pragma: no cover


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

with DeleteContext(
stack_name=stack_name, region=region, profile=profile, config_file=config_file, config_env=config_env
) as delete_context:
delete_context.run()
137 changes: 137 additions & 0 deletions samcli/commands/delete/delete_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
Delete a SAM stack
"""

import boto3

import click
from click import confirm
from click import prompt
from samcli.cli.cli_config_file import TomlProvider
from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.lib.delete.cf_utils import CfUtils
from samcli.lib.package.s3_uploader import S3Uploader
from samcli.lib.package.artifact_exporter import mktempfile, get_cf_template_name

CONFIG_COMMAND = "deploy"
CONFIG_SECTION = "parameters"
TEMPLATE_STAGE = "Original"


class DeleteContext:
def __init__(self, stack_name: str, region: str, profile: str, config_file: str, config_env: str):
self.stack_name = stack_name
self.region = region
self.profile = profile
self.config_file = config_file
self.config_env = config_env
self.s3_bucket = None
self.s3_prefix = None
self.cf_utils = None
self.s3_uploader = None
self.cf_template_file_name = None
self.delete_artifacts_folder = None
self.delete_cf_template_file = None

def __enter__(self):
self.parse_config_file()
if not self.stack_name:
self.stack_name = prompt(
click.style("\tEnter stack name you want to delete:", bold=True), type=click.STRING
)

return self

def __exit__(self, *args):
pass

def parse_config_file(self):
"""
Read the provided config file if it exists and assign the options values.
"""
toml_provider = TomlProvider(CONFIG_SECTION, [CONFIG_COMMAND])
config_options = toml_provider(
config_path=self.config_file, config_env=self.config_env, cmd_names=[CONFIG_COMMAND]
)
if config_options:
if not self.stack_name:
self.stack_name = config_options.get("stack_name", None)
if self.stack_name == config_options["stack_name"]:
if not self.region:
self.region = config_options.get("region", None)
if not self.profile:
self.profile = config_options.get("profile", None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for region && profile also update SAM context, to refresh the default boto session

self.s3_bucket = config_options.get("s3_bucket", None)
self.s3_prefix = config_options.get("s3_prefix", None)

def delete(self):
"""
Delete method calls for Cloudformation stacks and S3 and ECR artifacts
"""
template = self.cf_utils.get_stack_template(self.stack_name, TEMPLATE_STAGE)
template_str = template.get("TemplateBody", None)

if self.s3_bucket and self.s3_prefix and template_str:
self.delete_artifacts_folder = confirm(
click.style(
"\tAre you sure you want to delete the folder"
+ f" {self.s3_prefix} in S3 which contains the artifacts?",
bold=True,
),
default=False,
)
if not self.delete_artifacts_folder:
with mktempfile() as temp_file:
self.cf_template_file_name = get_cf_template_name(temp_file, template_str, "template")
self.delete_cf_template_file = confirm(
click.style(
"\tDo you want to delete the template file" + f" {self.cf_template_file_name} in S3?", bold=True
),
default=False,
)

# Delete the primary stack
self.cf_utils.delete_stack(stack_name=self.stack_name)

click.echo(f"\n\t- Deleting Cloudformation stack {self.stack_name}")

# Delete the CF template file in S3
if self.delete_cf_template_file:
self.s3_uploader.delete_artifact(remote_path=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()

def run(self):
"""
Delete the stack based on the argument provided by customers and samconfig.toml.
"""
delete_stack = confirm(
click.style(
f"\tAre you sure you want to delete the stack {self.stack_name}" + f" in the region {self.region} ?",
bold=True,
),
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)

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

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

if is_deployed:
self.delete()
click.echo("\nDeleted successfully")
else:
click.echo(f"Error: The input stack {self.stack_name} does not exist on Cloudformation")
24 changes: 24 additions & 0 deletions samcli/commands/delete/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
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))


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

message_fmt = "Failed to fetch the template for 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.
Loading