Skip to content

Commit

Permalink
refactor: sam deploy (#1681)
Browse files Browse the repository at this point in the history
* refactor: `sam deploy`

- Move guided deploy prompts to its own file
- New class for dealing with configuration during deploy
- Utilities for printing deploy arguments and sanitizing parameter
  overrides

* fix: save parameter values with spaces into config during guided deploy

* fix: add user agent during package and deploy

* fix: separate method for adding botoconfig with user-agent
  • Loading branch information
sriram-mv committed Jan 2, 2020
1 parent db6fcd6 commit ff04d75
Show file tree
Hide file tree
Showing 9 changed files with 638 additions and 473 deletions.
278 changes: 40 additions & 238 deletions samcli/commands/deploy/command.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
"""
CLI command for "deploy" command
"""
import json
import logging

import click
from click.types import FuncParamType

from samcli.lib.utils import osutils
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
from samcli.cli.cli_config_file import TomlProvider, configuration_option
from samcli.cli.main import aws_creds_options, common_options, pass_context
from samcli.commands._utils.options import (
parameter_override_option,
capabilities_override_option,
tags_override_option,
guided_deploy_stack_name,
metadata_override_option,
notification_arns_override_option,
parameter_override_option,
tags_override_option,
template_click_option,
metadata_override_option,
_space_separated_list_func_type,
guided_deploy_stack_name,
)
from samcli.commands._utils.template import get_template_parameters
from samcli.commands.deploy.exceptions import GuidedDeployFailedError
from samcli.lib.bootstrap.bootstrap import manage_stack
from samcli.lib.config.samconfig import SamConfig
from samcli.commands.deploy.utils import print_deploy_args, sanitize_parameter_overrides
from samcli.lib.telemetry.metrics import track_command
from samcli.lib.utils.colors import Colored
from samcli.lib.utils import osutils

SHORT_HELP = "Deploy an AWS SAM application."

Expand Down Expand Up @@ -211,258 +203,68 @@ def do_cli(
):
from samcli.commands.package.package_context import PackageContext
from samcli.commands.deploy.deploy_context import DeployContext

# set capabilities and changeset decision to None, before guided gets input from the user
changeset_decision = None
_capabilities = None
_parameter_overrides = None
guided_stack_name = None
guided_s3_bucket = None
guided_s3_prefix = None
guided_region = None
from samcli.commands.deploy.guided_context import GuidedContext

if guided:

try:
_parameter_override_keys = get_template_parameters(template_file=template_file)
except ValueError as ex:
LOG.debug("Failed to parse SAM template", exc_info=ex)
raise GuidedDeployFailedError(str(ex))

read_config_showcase(template_file=template_file)

(
guided_stack_name,
guided_s3_bucket,
guided_s3_prefix,
guided_region,
guided_profile,
changeset_decision,
_capabilities,
_parameter_overrides,
save_to_config,
) = guided_deploy(
stack_name, s3_bucket, region, profile, confirm_changeset, _parameter_override_keys, parameter_overrides
# Allow for a guided deploy to prompt and save those details.
guided_context = GuidedContext(
template_file=template_file,
stack_name=stack_name,
s3_bucket=s3_bucket,
s3_prefix=s3_prefix,
region=region,
profile=profile,
confirm_changeset=confirm_changeset,
capabilities=capabilities,
parameter_overrides=parameter_overrides,
config_section=CONFIG_SECTION,
)

if save_to_config:
save_config(
template_file,
stack_name=guided_stack_name,
s3_bucket=guided_s3_bucket,
s3_prefix=guided_s3_prefix,
region=guided_region,
profile=guided_profile,
confirm_changeset=changeset_decision,
capabilities=_capabilities,
parameter_overrides=_parameter_overrides,
)
guided_context.run()

print_deploy_args(
stack_name=guided_stack_name if guided else stack_name,
s3_bucket=guided_s3_bucket if guided else s3_bucket,
region=guided_region if guided else region,
capabilities=_capabilities if guided else capabilities,
parameter_overrides=_parameter_overrides if guided else parameter_overrides,
confirm_changeset=changeset_decision if guided else confirm_changeset,
stack_name=guided_context.guided_stack_name if guided else stack_name,
s3_bucket=guided_context.guided_s3_bucket if guided else s3_bucket,
region=guided_context.guided_region if guided else region,
capabilities=guided_context.guided_capabilities if guided else capabilities,
parameter_overrides=guided_context.guided_parameter_overrides if guided else parameter_overrides,
confirm_changeset=guided_context.confirm_changeset if guided else confirm_changeset,
)

with osutils.tempfile_platform_independent() as output_template_file:

with PackageContext(
template_file=template_file,
s3_bucket=guided_s3_bucket if guided else s3_bucket,
s3_prefix=guided_s3_prefix if guided else s3_prefix,
s3_bucket=guided_context.guided_s3_bucket if guided else s3_bucket,
s3_prefix=guided_context.guided_s3_prefix if guided else s3_prefix,
output_template_file=output_template_file.name,
kms_key_id=kms_key_id,
use_json=use_json,
force_upload=force_upload,
metadata=metadata,
on_deploy=True,
region=guided_region if guided else region,
region=guided_context.guided_region if guided else region,
profile=profile,
) as package_context:
package_context.run()

with DeployContext(
template_file=output_template_file.name,
stack_name=guided_stack_name if guided else stack_name,
s3_bucket=guided_s3_bucket if guided else s3_bucket,
stack_name=guided_context.guided_stack_name if guided else stack_name,
s3_bucket=guided_context.guided_s3_bucket if guided else s3_bucket,
force_upload=force_upload,
s3_prefix=guided_s3_prefix if guided else s3_prefix,
s3_prefix=guided_context.guided_s3_prefix if guided else s3_prefix,
kms_key_id=kms_key_id,
parameter_overrides=sanitize_parameter_overrides(_parameter_overrides) if guided else parameter_overrides,
capabilities=_capabilities if guided else capabilities,
parameter_overrides=sanitize_parameter_overrides(guided_context.guided_parameter_overrides)
if guided
else parameter_overrides,
capabilities=guided_context.guided_capabilities if guided else capabilities,
no_execute_changeset=no_execute_changeset,
role_arn=role_arn,
notification_arns=notification_arns,
fail_on_empty_changeset=fail_on_empty_changeset,
tags=tags,
region=guided_region if guided else region,
region=guided_context.guided_region if guided else region,
profile=profile,
confirm_changeset=changeset_decision if guided else confirm_changeset,
confirm_changeset=guided_context.confirm_changeset if guided else confirm_changeset,
) as deploy_context:
deploy_context.run()


def guided_deploy(
stack_name, s3_bucket, region, profile, confirm_changeset, parameter_override_keys, parameter_overrides
):
default_stack_name = stack_name or "sam-app"
default_region = region or "us-east-1"
default_capabilities = ("CAPABILITY_IAM",)
input_capabilities = None

color = Colored()
start_bold = "\033[1m"
end_bold = "\033[0m"

click.echo(
color.yellow("\n\tSetting default arguments for 'sam deploy'\n\t=========================================")
)

stack_name = click.prompt(f"\t{start_bold}Stack Name{end_bold}", default=default_stack_name, type=click.STRING)
s3_prefix = stack_name
region = click.prompt(f"\t{start_bold}AWS Region{end_bold}", default=default_region, type=click.STRING)
input_parameter_overrides = prompt_parameters(parameter_override_keys, start_bold, end_bold)

click.secho("\t#Shows you resources changes to be deployed and require a 'Y' to initiate deploy")
confirm_changeset = click.confirm(
f"\t{start_bold}Confirm changes before deploy{end_bold}", default=confirm_changeset
)
click.secho("\t#SAM needs permission to be able to create roles to connect to the resources in your template")
capabilities_confirm = click.confirm(f"\t{start_bold}Allow SAM CLI IAM role creation{end_bold}", default=True)

if not capabilities_confirm:
input_capabilities = click.prompt(
f"\t{start_bold}Capabilities{end_bold}",
default=default_capabilities[0],
type=FuncParamType(func=_space_separated_list_func_type),
)

save_to_config = click.confirm(f"\t{start_bold}Save arguments to samconfig.toml{end_bold}", default=True)

s3_bucket = manage_stack(profile=profile, region=region)
click.echo(f"\n\t\tManaged S3 bucket: {s3_bucket}")
click.echo("\t\tA different default S3 bucket can be set in samconfig.toml")

return (
stack_name,
s3_bucket,
s3_prefix,
region,
profile,
confirm_changeset,
input_capabilities if input_capabilities else default_capabilities,
input_parameter_overrides if input_parameter_overrides else parameter_overrides,
save_to_config,
)


def prompt_parameters(parameter_override_keys, start_bold, end_bold):
_prompted_param_overrides = {}
if parameter_override_keys:
for parameter_key, parameter_properties in parameter_override_keys.items():
no_echo = parameter_properties.get("NoEcho", False)
if no_echo:
parameter = click.prompt(
f"\t{start_bold}Parameter {parameter_key}{end_bold}", type=click.STRING, hide_input=True
)
_prompted_param_overrides[parameter_key] = {"Value": parameter, "Hidden": True}
else:
# Make sure the default is casted to a string.
parameter = click.prompt(
f"\t{start_bold}Parameter {parameter_key}{end_bold}",
default=_prompted_param_overrides.get(parameter_key, str(parameter_properties.get("Default", ""))),
type=click.STRING,
)
_prompted_param_overrides[parameter_key] = {"Value": parameter, "Hidden": False}
return _prompted_param_overrides


def print_deploy_args(stack_name, s3_bucket, region, capabilities, parameter_overrides, confirm_changeset):

_parameters = parameter_overrides.copy()
for key, value in _parameters.items():
if isinstance(value, dict):
_parameters[key] = value.get("Value", value) if not value.get("Hidden") else "*" * len(value.get("Value"))

capabilities_string = json.dumps(capabilities)

click.secho("\n\tDeploying with following values\n\t===============================", fg="yellow")
click.echo(f"\tStack name : {stack_name}")
click.echo(f"\tRegion : {region}")
click.echo(f"\tConfirm changeset : {confirm_changeset}")
click.echo(f"\tDeployment s3 bucket : {s3_bucket}")
click.echo(f"\tCapabilities : {capabilities_string}")
click.echo(f"\tParameter overrides : {_parameters}")

click.secho("\nInitiating deployment\n=====================", fg="yellow")


def read_config_showcase(template_file):
_, samconfig = get_config_ctx(template_file)

status = "Found" if samconfig.exists() else "Not found"
msg = (
"Syntax invalid in samconfig.toml; save values "
"through sam deploy --guided to overwrite file with a valid set of values."
)
config_sanity = samconfig.sanity_check()
click.secho("\nConfiguring SAM deploy\n======================", fg="yellow")
click.echo(f"\n\tLooking for samconfig.toml : {status}")
if samconfig.exists():
click.echo("\tReading default arguments : {}".format("Success" if config_sanity else "Failure"))

if not config_sanity and samconfig.exists():
raise GuidedDeployFailedError(msg)


def save_config(template_file, parameter_overrides, **kwargs):

section = CONFIG_SECTION
ctx, samconfig = get_config_ctx(template_file)

cmd_names = get_cmd_names(ctx.info_name, ctx)

for key, value in kwargs.items():
if isinstance(value, (list, tuple)):
value = " ".join(val for val in value)
if value:
samconfig.put(cmd_names, section, key, value)

if parameter_overrides:
_params = []
for key, value in parameter_overrides.items():
if isinstance(value, dict):
if not value.get("Hidden"):
_params.append(f"{key}={value.get('Value')}")
else:
_params.append(f"{key}={value}")
if _params:
samconfig.put(cmd_names, section, "parameter_overrides", " ".join(_params))

samconfig.flush()

click.echo(f"\n\tSaved arguments to config file")
click.echo("\tRunning 'sam deploy' for future deployments will use the parameters saved above.")
click.echo("\tThe above parameters can be changed by modifying samconfig.toml")
click.echo(
"\tLearn more about samconfig.toml syntax at "
"\n\thttps://docs.aws.amazon.com/serverless-application-model/latest/"
"developerguide/serverless-sam-cli-config.html"
)


def get_config_ctx(template_file):
ctx = click.get_current_context()

samconfig_dir = getattr(ctx, "samconfig_dir", None)
samconfig = SamConfig(
config_dir=samconfig_dir if samconfig_dir else SamConfig.config_dir(template_file_path=template_file)
)
return ctx, samconfig


def sanitize_parameter_overrides(parameter_overrides):
return {key: value.get("Value") if isinstance(value, dict) else value for key, value in parameter_overrides.items()}
10 changes: 7 additions & 3 deletions samcli/commands/deploy/deploy_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import os
import logging
import os

import boto3
import click

from samcli.commands.deploy import exceptions as deploy_exceptions
from samcli.lib.deploy.deployer import Deployer
from samcli.lib.package.s3_uploader import S3Uploader
from samcli.lib.utils.botoconfig import get_boto_config_with_user_agent
from samcli.yamlhelper import yaml_parse
from samcli.lib.utils.colors import Colored

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,7 +103,10 @@ def run(self):
raise deploy_exceptions.DeployBucketRequiredError()

session = boto3.Session(profile_name=self.profile if self.profile else None)
cloudformation_client = session.client("cloudformation", region_name=self.region if self.region else None)
config = get_boto_config_with_user_agent()
cloudformation_client = session.client(
"cloudformation", region_name=self.region if self.region else None, config=config
)

s3_client = None
if self.s3_bucket:
Expand Down
Loading

0 comments on commit ff04d75

Please sign in to comment.