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: add version to samconfig.toml file #1581

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
9 changes: 9 additions & 0 deletions samcli/cli/cli_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import logging

import click

from samcli.commands.exceptions import ConfigException
from samcli.lib.config.exceptions import SamConfigVersionException
from samcli.cli.context import get_cmd_names
from samcli.lib.config.samconfig import SamConfig, DEFAULT_ENV

Expand Down Expand Up @@ -53,6 +56,7 @@ def __call__(self, config_dir, config_env, cmd_names):

# NOTE(TheSriram): change from tomlkit table type to normal dictionary,
# so that click defaults work out of the box.
samconfig.sanity_check()
resolved_config = {k: v for k, v in samconfig.get_all(cmd_names, self.section, env=config_env).items()}
LOG.debug("Configuration values read from the file: %s", resolved_config)

Expand All @@ -65,6 +69,11 @@ def __call__(self, config_dir, config_env, cmd_names):
self.section,
str(ex),
)

except SamConfigVersionException as ex:
LOG.debug("%s %s", samconfig.path(), str(ex))
raise ConfigException(f"Syntax invalid in samconfig.toml: {str(ex)}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Why change Exception type? What is difference between SamConfigVersionException and ConfigException?

Also probably include in exception message that this is due to Invalid Version instead of Invalid Syntax.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One is a click exception, and the other isnt. str(ex) gives us version based information.


except Exception as ex:
LOG.debug("Error reading configuration file: %s %s", samconfig.path(), str(ex))

Expand Down
6 changes: 6 additions & 0 deletions samcli/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
import click


class ConfigException(click.ClickException):
"""
Exception class when configuration file fails checks.
"""


class UserException(click.ClickException):
"""
Base class for all exceptions that need to be surfaced to the user. Typically, we will display the exception
Expand Down
7 changes: 7 additions & 0 deletions samcli/lib/config/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Exceptions to be used by samconfig.py
"""


class SamConfigVersionException(Exception):
pass
38 changes: 27 additions & 11 deletions samcli/lib/config/samconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

import tomlkit

from samcli.lib.config.version import SAM_CONFIG_VERSION, VERSION_KEY
from samcli.lib.config.exceptions import SamConfigVersionException

LOG = logging.getLogger(__name__)

DEFAULT_CONFIG_FILE_NAME = "samconfig.toml"
Expand All @@ -21,7 +24,6 @@ class SamConfig:
"""

document = None
VERSION = "0.1"

def __init__(self, config_dir, filename=None):
"""
Expand Down Expand Up @@ -101,8 +103,8 @@ def put(self, cmd_names, section, key, value, env=DEFAULT_ENV):
If the data is invalid
"""

self._read()
if not self.document:
self._read()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

bugfix: do not attempt to read everytime a put is made, read once and load.

# Empty document prepare the initial structure.
self.document.update({env: {self._to_key(cmd_names): {section: {key: value}}}})
# Only update appropriate key value pairs within a section
Expand Down Expand Up @@ -149,15 +151,16 @@ def config_dir(template_file_path=None):
return os.getcwd()

def _read(self):
if self.document:
return self.document

try:
txt = self.filepath.read_text()
self.document = tomlkit.loads(txt)
except OSError:
self.document = tomlkit.document()

if not self.document:
try:
txt = self.filepath.read_text()
self.document = tomlkit.loads(txt)
self._version_sanity_check(self._version())
except OSError:
self.document = tomlkit.document()
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: Probably add a debug log before default to tomlkit.document().

Copy link
Contributor Author

@sriram-mv sriram-mv Nov 22, 2019

Choose a reason for hiding this comment

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

Sure. Edit: actually lets think about that a bit more, if there is no config file we say we couldn't find it every-time under debug, I'm not sure if thats the behavior we want.


if self.document.body:
self._version_sanity_check(self._version())
return self.document

def _write(self):
Expand All @@ -166,8 +169,21 @@ def _write(self):
if not self.exists():
open(self.filepath, "a+").close()

current_version = self._version() if self._version() else SAM_CONFIG_VERSION
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious: any reason why we are not checking for key to be present and add only if it isn't?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we do.

self.document.add(VERSION_KEY, current_version)
except tomlkit.exceptions.KeyAlreadyPresent:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We check it here.

# NOTE(TheSriram): Do not attempt to re-write an existing version
pass
self.filepath.write_text(tomlkit.dumps(self.document))

def _version(self):
return self.document.get(VERSION_KEY, None)

def _version_sanity_check(self, version):
if not isinstance(version, float):
raise SamConfigVersionException(f"'{VERSION_KEY}' key is not present or is in unrecognized format. ")

@staticmethod
def _to_key(cmd_names):
# construct a parsed name that is of the format: a_b_c_d
Expand Down
6 changes: 6 additions & 0 deletions samcli/lib/config/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Constants and helper functions for samconfig.toml's versioning.
"""

SAM_CONFIG_VERSION = 0.1
VERSION_KEY = "version"
20 changes: 17 additions & 3 deletions tests/unit/cli/test_cli_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from pathlib import Path
from unittest import TestCase
from unittest.mock import MagicMock, patch

from unittest.mock import MagicMock

from samcli.commands.exceptions import ConfigException
from samcli.cli.cli_config_file import TomlProvider, configuration_option, configuration_callback, get_ctx_defaults
from samcli.lib.config.samconfig import SamConfig

Expand All @@ -27,11 +27,25 @@ def setUp(self):
def test_toml_valid_with_section(self):
config_dir = tempfile.gettempdir()
configpath = Path(config_dir, "samconfig.toml")
configpath.write_text("[config_env.topic.parameters]\nword='clarity'\n")
configpath.write_text("version=0.1\n[config_env.topic.parameters]\nword='clarity'\n")
self.assertEqual(
TomlProvider(section=self.parameters)(config_dir, self.config_env, [self.cmd_name]), {"word": "clarity"}
)

def test_toml_valid_with_no_version(self):
config_dir = tempfile.gettempdir()
configpath = Path(config_dir, "samconfig.toml")
configpath.write_text("[config_env.topic.parameters]\nword='clarity'\n")
with self.assertRaises(ConfigException):
TomlProvider(section=self.parameters)(config_dir, self.config_env, [self.cmd_name])

def test_toml_valid_with_invalid_version(self):
config_dir = tempfile.gettempdir()
configpath = Path(config_dir, "samconfig.toml")
configpath.write_text("version='abc'\n[config_env.topic.parameters]\nword='clarity'\n")
with self.assertRaises(ConfigException):
TomlProvider(section=self.parameters)(config_dir, self.config_env, [self.cmd_name])

def test_toml_invalid_empty_dict(self):
config_dir = tempfile.gettempdir()
configpath = Path(config_dir, "samconfig.toml")
Expand Down
77 changes: 77 additions & 0 deletions tests/unit/commands/deploy/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,80 @@ def test_all_args_guided_no_params_no_save_config(
self.assertEqual(mock_save_config.call_count, 0)
mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1")
self.assertEqual(context_mock.run.call_count, 1)

@patch("samcli.commands.package.command.click")
@patch("samcli.commands.package.package_context.PackageContext")
@patch("samcli.commands.deploy.command.click")
@patch("samcli.commands.deploy.deploy_context.DeployContext")
@patch("samcli.commands.deploy.command.save_config")
@patch("samcli.commands.deploy.command.manage_stack")
@patch("samcli.commands.deploy.command.get_template_parameters")
@patch("samcli.commands.deploy.command.get_config_ctx")
def test_all_args_guided_no_params_no_save_config(
self,
mock_get_config_ctx,
mock_get_template_parameters,
mock_managed_stack,
mock_save_config,
mock_deploy_context,
mock_deploy_click,
mock_package_context,
mock_package_click,
):

context_mock = Mock()
mock_sam_config = MagicMock()
mock_sam_config.exists = MagicMock(return_value=True)
mock_get_config_ctx.return_value = (None, mock_sam_config)
mock_get_template_parameters.return_value = {}
mock_deploy_context.return_value.__enter__.return_value = context_mock
mock_deploy_click.prompt = MagicMock(side_effect=["sam-app", "us-east-1", ("CAPABILITY_IAM",)])
mock_deploy_click.confirm = MagicMock(side_effect=[True, False, False])

mock_managed_stack.return_value = "managed-s3-bucket"

do_cli(
template_file=self.template_file,
stack_name=self.stack_name,
s3_bucket=None,
force_upload=self.force_upload,
s3_prefix=self.s3_prefix,
kms_key_id=self.kms_key_id,
parameter_overrides=self.parameter_overrides,
capabilities=self.capabilities,
no_execute_changeset=self.no_execute_changeset,
role_arn=self.role_arn,
notification_arns=self.notification_arns,
fail_on_empty_changeset=self.fail_on_empty_changset,
tags=self.tags,
region=self.region,
profile=self.profile,
use_json=self.use_json,
metadata=self.metadata,
guided=True,
confirm_changeset=True,
)

mock_deploy_context.assert_called_with(
template_file=ANY,
stack_name="sam-app",
s3_bucket="managed-s3-bucket",
force_upload=self.force_upload,
s3_prefix="sam-app",
kms_key_id=self.kms_key_id,
parameter_overrides=self.parameter_overrides,
capabilities=self.capabilities,
no_execute_changeset=self.no_execute_changeset,
role_arn=self.role_arn,
notification_arns=self.notification_arns,
fail_on_empty_changeset=self.fail_on_empty_changset,
tags=self.tags,
region="us-east-1",
profile=self.profile,
confirm_changeset=True,
)

context_mock.run.assert_called_with()
self.assertEqual(mock_save_config.call_count, 0)
mock_managed_stack.assert_called_with(profile=self.profile, region="us-east-1")
self.assertEqual(context_mock.run.call_count, 1)
30 changes: 30 additions & 0 deletions tests/unit/lib/samconfig/test_samconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from unittest import TestCase

from samcli.lib.config.exceptions import SamConfigVersionException
from samcli.lib.config.version import VERSION_KEY, SAM_CONFIG_VERSION
from samcli.lib.config.samconfig import SamConfig, DEFAULT_CONFIG_FILE_NAME


Expand All @@ -20,6 +22,7 @@ def _setup_config(self):
self.samconfig.flush()
self.assertTrue(self.samconfig.exists())
self.assertTrue(self.samconfig.sanity_check())
self.assertEqual(SAM_CONFIG_VERSION, self.samconfig.document.get(VERSION_KEY))

def test_init(self):
self.assertEqual(self.samconfig.filepath, Path(self.config_dir, DEFAULT_CONFIG_FILE_NAME))
Expand All @@ -36,3 +39,30 @@ def test_check_config_exists(self):

def test_check_sanity(self):
self.assertTrue(self.samconfig.sanity_check())

def test_check_version_non_supported_type(self):
self._setup_config()
self.samconfig.document.remove(VERSION_KEY)
self.samconfig.document.add(VERSION_KEY, "aadeff")
with self.assertRaises(SamConfigVersionException):
self.samconfig.sanity_check()

def test_check_version_no_version_exists(self):
self._setup_config()
self.samconfig.document.remove(VERSION_KEY)
with self.assertRaises(SamConfigVersionException):
self.samconfig.sanity_check()

def test_check_version_float(self):
self._setup_config()
self.samconfig.document.remove(VERSION_KEY)
self.samconfig.document.add(VERSION_KEY, 0.2)
self.samconfig.sanity_check()

def test_write_config_file_non_standard_version(self):
self._setup_config()
self.samconfig.document.remove(VERSION_KEY)
self.samconfig.document.add(VERSION_KEY, 0.2)
self.samconfig.put(cmd_names=["local", "start", "api"], section="parameters", key="skip_pull_image", value=True)
self.samconfig.sanity_check()
self.assertEqual(self.samconfig.document.get(VERSION_KEY), 0.2)