Skip to content

Commit

Permalink
feat: save params to config file on interactive deploy
Browse files Browse the repository at this point in the history
  • Loading branch information
sanathkr authored and sriram-mv committed Nov 20, 2019
1 parent 2b8399d commit b793184
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 66 deletions.
2 changes: 1 addition & 1 deletion requirements/base.txt
Expand Up @@ -14,4 +14,4 @@ serverlessrepo==0.1.9
aws_lambda_builders==0.5.0
# https://github.com/mhammond/pywin32/issues/1439
pywin32 < 226; sys_platform == 'win32'
toml==0.10.0
tomlkit==0.5.8
2 changes: 1 addition & 1 deletion requirements/isolated.txt
Expand Up @@ -32,7 +32,7 @@ requests==2.22.0
s3transfer==0.2.1
serverlessrepo==0.1.9
six==1.11.0
toml==0.10.0
tomlkit==0.5.8
tzlocal==2.0.0
urllib3==1.25.3
websocket-client==0.56.0
Expand Down
93 changes: 34 additions & 59 deletions samcli/cli/cli_config_file.py
Expand Up @@ -6,17 +6,15 @@
## SPDX-License-Identifier: MIT

import functools
import os
import logging

import click
import toml
from samcli.cli.context import get_cmd_names
from samcli.lib.config.samconfig import SamConfig, DEFAULT_ENV

__all__ = ("TomlProvider", "configuration_option", "get_ctx_defaults")

LOG = logging.getLogger("samcli")
DEFAULT_CONFIG_FILE_NAME = "samconfig.toml"
DEFAULT_IDENTIFER = "default"
LOG = logging.getLogger(__name__)


class TomlProvider:
Expand All @@ -29,44 +27,43 @@ class TomlProvider:
def __init__(self, section=None):
self.section = section

def __call__(self, file_path, config_env, cmd_name):
def __call__(self, config_dir, config_env, cmd_names):
"""
Get resolved config based on the `file_path` for the configuration file,
`config_env` targeted inside the config file and corresponding `cmd_name`
as denoted by `click`.
:param file_path: The path to the configuration file
:param config_env: The name of the sectional config_env within configuration file.
:param cmd_name: sam command name as defined by click
:param cmd_names list(str): sam command name as defined by click
:returns dictionary containing the configuration parameters under specified config_env
"""

resolved_config = {}
try:
config = toml.load(file_path)
except Exception as ex:
LOG.error("Error reading configuration file :%s %s", file_path, str(ex))

samconfig = SamConfig(config_dir)
LOG.debug("Config file location: %s", samconfig.path())

if not samconfig.exists():
LOG.debug("Config file does not exist")
return resolved_config
if self.section:
try:
resolved_config = self._get_config_env(config, config_env)[cmd_name][self.section]
except KeyError:
LOG.debug(
"Error reading configuration file at %s with config_env %s, command %s, section %s",
file_path,
config_env,
cmd_name,
self.section,
)
return resolved_config

def _get_config_env(self, config, config_env):
"""
try:
LOG.debug("Getting configuration value for %s %s %s", cmd_names, self.section, config_env)
resolved_config = samconfig.get_all(cmd_names, self.section, env=config_env)
except KeyError:
LOG.debug(
"Error reading configuration file at %s with config_env=%s, command=%s, section=%s",
samconfig.path(),
config_dir,
config_env,
cmd_names,
self.section,
)
except Exception as ex:
LOG.error("Error reading configuration file: %s %s", samconfig.path(), str(ex))
raise ex

:param config: loaded TOML configuration file into dictionary representation
:param config_env: top level section defined within TOML configuration file
:return:
"""
return config.get(config_env, config.get(DEFAULT_IDENTIFER, {}))
return resolved_config


def configuration_callback(cmd_name, option_name, config_env_name, saved_callback, provider, ctx, param, value):
Expand All @@ -91,15 +88,15 @@ def configuration_callback(cmd_name, option_name, config_env_name, saved_callbac
# ctx, param and value are default arguments for click specified callbacks.
ctx.default_map = ctx.default_map or {}
cmd_name = cmd_name or ctx.info_name
param.default = DEFAULT_IDENTIFER
param.default = None
config_env_name = value or config_env_name
config = get_ctx_defaults(cmd_name, provider, ctx, config_env_name=config_env_name)
ctx.default_map.update(config)

return saved_callback(ctx, param, value) if saved_callback else value


def get_ctx_defaults(cmd_name, provider, ctx, config_env_name=DEFAULT_IDENTIFER):
def get_ctx_defaults(cmd_name, provider, ctx, config_env_name):
"""
Get the set of the parameters that are needed to be set into the click command.
This function also figures out the command name by looking up current click context's parent
Expand All @@ -114,31 +111,9 @@ def get_ctx_defaults(cmd_name, provider, ctx, config_env_name=DEFAULT_IDENTIFER)
:return: dictionary of defaults for parameters
"""

cwd = getattr(ctx, "config_path", None)
config_file = os.path.join(cwd if cwd else os.getcwd(), DEFAULT_CONFIG_FILE_NAME)
config = {}
if os.path.isfile(config_file):
LOG.debug("Config file location: %s", os.path.abspath(config_file))

# Find parent of current context
_parent = ctx.parent
_cmd_names = []
# Need to find the total set of commands that current command is part of.
if cmd_name != ctx.info_name:
_cmd_names = [cmd_name]
_cmd_names.append(ctx.info_name)
# Go through all parents till a parent of a context exists.
while _parent.parent:
info_name = _parent.info_name
_cmd_names.append(info_name)
_parent = _parent.parent

# construct a parsed name that is of the format: a_b_c_d
parsed_cmd_name = "_".join(reversed([cmd.replace("-", "_").replace(" ", "_") for cmd in _cmd_names]))

config = provider(config_file, config_env_name, parsed_cmd_name)

return config
# `config_dir` will be a directory relative to SAM template, if it is available. If not it's relative to cwd
config_dir = getattr(ctx, "samconfig_dir", None) or SamConfig.config_dir()
return provider(config_dir, config_env_name, get_cmd_names(cmd_name, ctx))


def configuration_option(*param_decls, **attrs):
Expand Down Expand Up @@ -172,7 +147,7 @@ def decorator(f):
# --config-env is hidden and can potentially be opened up in the future.
attrs.setdefault("hidden", True)
# explicitly ignore values passed to --config-env, can be opened up in the future.
config_env_name = DEFAULT_IDENTIFER
config_env_name = DEFAULT_ENV
provider = attrs.pop("provider")
attrs["type"] = click.STRING
saved_callback = attrs.pop("callback", None)
Expand Down
17 changes: 17 additions & 0 deletions samcli/cli/context.py
Expand Up @@ -146,3 +146,20 @@ def _refresh_session(self):
boto3.setup_default_session(region_name=self._aws_region, profile_name=self._aws_profile)
except botocore.exceptions.ProfileNotFound as ex:
raise CredentialsError(str(ex))


def get_cmd_names(cmd_name, ctx):
# Find parent of current context
_parent = ctx.parent
_cmd_names = []
# Need to find the total set of commands that current command is part of.
if cmd_name != ctx.info_name:
_cmd_names = [cmd_name]
_cmd_names.append(ctx.info_name)
# Go through all parents till a parent of a context exists.
while _parent.parent:
info_name = _parent.info_name
_cmd_names.append(info_name)
_parent = _parent.parent

return _cmd_names
3 changes: 2 additions & 1 deletion samcli/commands/_utils/options.py
Expand Up @@ -10,6 +10,7 @@
from click.types import FuncParamType
from samcli.cli.types import CfnParameterOverridesType, CfnMetadataType, CfnTags
from samcli.commands._utils.custom_options.option_nargs import OptionNargs
from samcli.lib.config.samconfig import SamConfig


_TEMPLATE_OPTION_DEFAULT_VALUE = "template.[yaml|yml]"
Expand Down Expand Up @@ -47,7 +48,7 @@ def get_or_default_template_file_name(ctx, param, provided_value, include_build)
result = os.path.abspath(provided_value)

if ctx:
setattr(ctx, "config_path", os.path.dirname(result))
setattr(ctx, "samconfig_dir", SamConfig.config_dir(result))
LOG.debug("Using SAM Template at %s", result)
return result

Expand Down
42 changes: 38 additions & 4 deletions samcli/commands/deploy/command.py
Expand Up @@ -19,6 +19,8 @@
from samcli.lib.telemetry.metrics import track_command
from samcli.lib.utils.colors import Colored
from samcli.lib.bootstrap.bootstrap import manage_stack
from samcli.lib.config.samconfig import SamConfig
from samcli.cli.context import get_cmd_names


SHORT_HELP = "Deploy an AWS SAM application."
Expand All @@ -32,14 +34,16 @@
\b
"""

CONFIG_SECTION = "parameters"


@click.command(
"deploy",
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="parameters"))
@configuration_option(provider=TomlProvider(section=CONFIG_SECTION))
@template_click_option(include_build=True)
@click.option(
"--stack-name",
Expand Down Expand Up @@ -194,10 +198,20 @@ def do_cli(

confirm_changeset = False
if interactive:
stack_name, s3_bucket, region, profile, confirm_changeset = guided_deploy(
stack_name, s3_bucket, region, profile, confirm_changeset, save_to_config = guided_deploy(
stack_name, s3_bucket, region, profile
)

if save_to_config:
save_config(
template_file,
stack_name=stack_name,
s3_bucket=s3_bucket,
region=region,
profile=profile,
confirm_changeset=confirm_changeset,
)

# We print deploy args only on interactive.
# Should we print this always?
print_deploy_args(stack_name, s3_bucket, region, profile, capabilities, parameter_overrides, confirm_changeset)
Expand Down Expand Up @@ -254,15 +268,15 @@ def guided_deploy(stack_name, s3_bucket, region, profile):
region = click.prompt(f"{tick} AWS Region", default=default_region, type=click.STRING)
profile = click.prompt(f"{tick} AWS Profile", default=default_profile, type=click.STRING)

_ = click.confirm(f"{tick} Save values to samconfig.toml", default=True)
save_to_config = click.confirm(f"{tick} Save values to samconfig.toml", default=True)

if not s3_bucket:
click.echo(color.yellow("\nConfiguring Deployment S3 Bucket\n================================"))
s3_bucket = manage_stack(profile, region)
click.echo(f"{tick} Using Deployment Bucket: {s3_bucket}")
click.echo("You may specify a different default deployment bucket in samconfig.toml")

return stack_name, s3_bucket, region, profile, confirm_changeset
return stack_name, s3_bucket, region, profile, confirm_changeset, save_to_config


def print_deploy_args(stack_name, s3_bucket, region, profile, capabilities, parameter_overrides, confirm_changeset):
Expand All @@ -280,3 +294,23 @@ def print_deploy_args(stack_name, s3_bucket, region, profile, capabilities, para
click.echo(f"Confirm Changeset : {confirm_changeset}")

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


def save_config(template_file, **kwargs):
color = Colored()
tick = color.yellow("✓")

click.echo(f"\n{tick} Saving arguments to config file")

section = CONFIG_SECTION
config_dir = SamConfig.config_dir(template_file)

ctx = click.get_current_context()
cmd_names = get_cmd_names(ctx.info_name, ctx)

samconfig = SamConfig(config_dir)

for key, value in kwargs.items():
samconfig.put(cmd_names, section, key, value)

samconfig.flush()
Empty file added samcli/lib/config/__init__.py
Empty file.

0 comments on commit b793184

Please sign in to comment.