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

Add package argument to deep-merge in a JSON file during packaging #1195

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 11 additions & 3 deletions chalice/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,20 @@ def generate_sdk(ctx, sdk_type, stage, outdir):
"this argument is specified, a single "
"zip file will be created instead."))
@click.option('--stage', default=DEFAULT_STAGE_NAME)
@click.option('--merge-template',
help=('Specify a JSON template to be merged '
'into the generated template. This is useful '
'for adding resources to a Chalice template or '
'modify values in the template.'))
@click.argument('out')
@click.pass_context
def package(ctx, single_file, stage, out):
# type: (click.Context, bool, str, str) -> None
def package(ctx, single_file, stage, merge_template, out):
# type: (click.Context, bool, str, str, str) -> None
factory = ctx.obj['factory'] # type: CLIFactory
config = factory.create_config_obj(stage)
config = factory.create_config_obj(
chalice_stage_name=stage,
package_merge_template=merge_template,
)
packager = factory.create_app_packager(config)
if single_file:
dirname = tempfile.mkdtemp()
Expand Down
11 changes: 8 additions & 3 deletions chalice/cli/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,15 @@ def create_deployment_reporter(self, ui):

def create_config_obj(self, chalice_stage_name=DEFAULT_STAGE_NAME,
autogen_policy=None,
api_gateway_stage=None):
# type: (str, Optional[bool], str) -> Config
api_gateway_stage=None,
package_merge_template=None):
# type: (str, Optional[bool], str, Optional[str]) -> Config
user_provided_params = {} # type: Dict[str, Any]
default_params = {'project_dir': self.project_dir,
'api_gateway_stage': DEFAULT_APIGATEWAY_STAGE_NAME,
'api_gateway_endpoint_type': DEFAULT_ENDPOINT_TYPE,
'autogen_policy': True}
'autogen_policy': True,
'package_merge_template': None}
try:
config_from_disk = self.load_project_config()
except (OSError, IOError):
Expand All @@ -161,6 +163,9 @@ def create_config_obj(self, chalice_stage_name=DEFAULT_STAGE_NAME,
user_provided_params['profile'] = self.profile
if api_gateway_stage is not None:
user_provided_params['api_gateway_stage'] = api_gateway_stage
if package_merge_template is not None:
user_provided_params[
'package_merge_template'] = package_merge_template
config = Config(chalice_stage=chalice_stage_name,
user_provided_params=user_provided_params,
config_from_disk=config_from_disk,
Expand Down
6 changes: 6 additions & 0 deletions chalice/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ def api_gateway_stage(self):
return self._chain_lookup('api_gateway_stage',
varies_per_chalice_stage=True)

@property
def package_merge_template(self):
stealthycoin marked this conversation as resolved.
Show resolved Hide resolved
# type: () -> str
return self._chain_lookup('package_merge_template',
varies_per_chalice_stage=True)

@property
def api_gateway_endpoint_type(self):
# type: () -> str
Expand Down
91 changes: 83 additions & 8 deletions chalice/package.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import copy
import json

from typing import Any, Dict, List, Set, Union # noqa
from typing import cast
Expand All @@ -25,10 +26,14 @@ def create_app_packager(config):
)
resource_builder = ResourceBuilder(application_builder,
deps_builder, build_stage)
processors = [
ReplaceCodeLocationPostProcessor(osutils=osutils),
TemplateMergePostProcessor(osutils=osutils),
]
return AppPackager(
SAMTemplateGenerator(),
resource_builder,
TemplatePostProcessor(osutils=osutils),
processors,
osutils,
)

Expand Down Expand Up @@ -532,15 +537,15 @@ def _register_cfn_resource_name(self, name):

class AppPackager(object):
def __init__(self,
sam_templater, # type: SAMTemplateGenerator
resource_builder, # type: ResourceBuilder
post_processor, # type: TemplatePostProcessor
osutils, # type: OSUtils
sam_templater, # type: SAMTemplateGenerator
resource_builder, # type: ResourceBuilder
post_processors, # type: List[TemplatePostProcessor]
stealthycoin marked this conversation as resolved.
Show resolved Hide resolved
osutils, # type: OSUtils
):
# type: (...) -> None
self._sam_templater = sam_templater
self._resource_builder = resource_builder
self._template_post_processor = post_processor
self._template_post_processors = post_processors
self._osutils = osutils

def _to_json(self, doc):
Expand All @@ -558,8 +563,8 @@ def package_app(self, config, outdir, chalice_stage_name):
resources)
if not self._osutils.directory_exists(outdir):
self._osutils.makedirs(outdir)
self._template_post_processor.process(
sam_template, config, outdir, chalice_stage_name)
for processor in self._template_post_processors:
processor.process(sam_template, config, outdir, chalice_stage_name)
self._osutils.set_file_contents(
filename=os.path.join(outdir, 'sam.json'),
contents=self._to_json(sam_template),
Expand All @@ -572,6 +577,12 @@ def __init__(self, osutils):
# type: (OSUtils) -> None
self._osutils = osutils

def process(self, template, config, outdir, chalice_stage_name):
# type: (Dict[str, Any], Config, str, str) -> None
raise NotImplementedError('process')


class ReplaceCodeLocationPostProcessor(TemplatePostProcessor):
def process(self, template, config, outdir, chalice_stage_name):
# type: (Dict[str, Any], Config, str, str) -> None
self._fixup_deployment_package(template, outdir)
Expand All @@ -594,3 +605,67 @@ def _fixup_deployment_package(self, template, outdir):
self._osutils.copy(original_location, new_location)
copied = True
resource['Properties']['CodeUri'] = './deployment.zip'


class TemplateMergePostProcessor(TemplatePostProcessor):
def process(self, template, config, outdir, chalice_stage_name):
# type: (Dict[str, Any], Config, str, str) -> None
if not self._should_merge_template(config):
return
loaded_template = self._load_template_to_merge(config)
merger = TemplateDeepMerger()
stealthycoin marked this conversation as resolved.
Show resolved Hide resolved
merged = merger.merge(loaded_template, template)
template.clear()
template.update(merged)

def _should_merge_template(self, config):
# type: (Config) -> bool
return config.package_merge_template is not None

def _load_template_to_merge(self, config):
# type: (Config) -> Dict[str, Any]
filepath = os.path.abspath(config.package_merge_template)
if not self._osutils.file_exists(filepath):
raise RuntimeError('Cannot find template file: %s' % filepath)
template_data = self._osutils.get_file_contents(filepath, binary=False)
try:
loaded_template = json.loads(template_data)
except ValueError:
raise RuntimeError(
'Expected %s to be valid JSON template.' % filepath)
return loaded_template


class TemplateDeepMerger(object):
def merge(self, src, dst):
stealthycoin marked this conversation as resolved.
Show resolved Hide resolved
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
return self._merge_dict(src, dst)
stealthycoin marked this conversation as resolved.
Show resolved Hide resolved

def _merge(self, src, dst):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
src_type = type(src).__name__
dst_type = type(dst).__name__
if src_type != dst_type:
name = '_merge_unknown'
else:
name = '_merge_%s' % src_type
return getattr(self, name, self._merge_replace)(src, dst)

def _merge_dict(self, src, dst):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
result = dst.copy()
for key, value in src.items():
existing_value = dst.get(key)
if existing_value is None:
result[key] = value
else:
result[key] = self._merge(value, existing_value)
if result[key] is None:
stealthycoin marked this conversation as resolved.
Show resolved Hide resolved
del result[key]
return result

def _merge_replace(self, src, dst):
# type: (Any, Any) -> None
# Default merge behavior is to take the source value. This behaivor is
Copy link
Contributor

Choose a reason for hiding this comment

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

speling typo

# also desired when the types do not match.
return src
6 changes: 6 additions & 0 deletions tests/functional/cli/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ def test_can_create_config_obj_with_default_api_gateway_stage(clifactory):
assert config.api_gateway_stage == 'api'


def test_can_create_config_obj_with_package_merge_template(clifactory):
config = clifactory.create_config_obj(
package_merge_template='base.json')
assert config.package_merge_template == 'base.json'


def test_cant_load_config_obj_with_bad_project(clifactory):
clifactory.project_dir = 'nowhere-asdfasdfasdfas'
with pytest.raises(RuntimeError):
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_config_create_method():
# Otherwise attributes default to None meaning 'not set'.
assert c.profile is None
assert c.api_gateway_stage is None
assert c.package_merge_template is None


def test_default_chalice_stage():
Expand Down
Loading