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 local module support to the cloudformation package command #9124

Draft
wants to merge 26 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ doc/source/tutorial/services.rst

# Pyenv
.python-version
.env

34 changes: 31 additions & 3 deletions awscli/customizations/cloudformation/artifact_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
from awscli.customizations.cloudformation import exceptions
from awscli.customizations.cloudformation.yamlhelper import yaml_dump, \
yaml_parse
from awscli.customizations.cloudformation import modules
import jmespath


LOG = logging.getLogger(__name__)

MODULES = "Modules"
RESOURCES = "Resources"

def is_path_value_valid(path):
return isinstance(path, str)
Expand Down Expand Up @@ -591,8 +594,8 @@ def __init__(self, template_path, parent_dir, uploader,
raise ValueError("parent_dir parameter must be "
"an absolute path to a folder {0}"
.format(parent_dir))

abs_template_path = make_abs_path(parent_dir, template_path)
self.module_parent_path = abs_template_path
template_dir = os.path.dirname(abs_template_path)

with open(abs_template_path, "r") as handle:
Expand Down Expand Up @@ -651,14 +654,39 @@ def export(self):
:return: The template with references to artifacts that have been
exported to s3.
"""

# Process modules
try:
self.template_dict = modules.process_module_section(
self.template_dict,
self.template_dir,
self.module_parent_path)
except Exception as e:
msg=f"Failed to process Modules section: {e}"
LOG.exception(msg)
raise exceptions.InvalidModuleError(msg=msg)

self.template_dict = self.export_metadata(self.template_dict)

if "Resources" not in self.template_dict:
if RESOURCES not in self.template_dict:
return self.template_dict

# Process modules that are specified as Resources, not in Modules
try:
self.template_dict = modules.process_resources_section(
self.template_dict,
self.template_dir,
self.module_parent_path,
None)
except Exception as e:
msg=f"Failed to process modules in Resources: {e}"
LOG.exception(msg)
raise exceptions.InvalidModuleError(msg=msg)


self.template_dict = self.export_global_artifacts(self.template_dict)

self.export_resources(self.template_dict["Resources"])
self.export_resources(self.template_dict[RESOURCES])

return self.template_dict

Expand Down
6 changes: 6 additions & 0 deletions awscli/customizations/cloudformation/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ class DeployBucketRequiredError(CloudFormationCommandError):

class InvalidForEachIntrinsicFunctionError(CloudFormationCommandError):
fmt = 'The value of {resource_id} has an invalid "Fn::ForEach::" format: Must be a list of three entries'

class InvalidModulePathError(CloudFormationCommandError):
fmt = 'The value of {source} is not a valid path to a local file'

class InvalidModuleError(CloudFormationCommandError):
fmt = 'Invalid module: {msg}'
116 changes: 116 additions & 0 deletions awscli/customizations/cloudformation/module_conditions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

# pylint: disable=fixme

"""
Parse the Conditions section in a module

This section is not emitted into the output.
We have to be able to fully resolve it locally.
"""

from awscli.customizations.cloudformation import exceptions

AND = "Fn::And"
EQUALS = "Fn::Equals"
IF = "Fn::If"
NOT = "Fn::Not"
OR = "Fn::Or"
REF = "Ref"
CONDITION = "Condition"


def parse_conditions(d, find_ref):
"""Parse conditions and return a map of name:boolean"""
retval = {}

for k, v in d.items():
retval[k] = istrue(v, find_ref, retval)

return retval


def resolve_if(v, find_ref, prior):
"Resolve Fn::If"
msg = f"If expression should be a list with 3 elements: {v}"
if not isinstance(v, list):
raise exceptions.InvalidModuleError(msg=msg)
if len(v) != 3:
raise exceptions.InvalidModuleError(msg=msg)
if istrue(v[0], find_ref, prior):
return v[1]
return v[2]


# pylint: disable=too-many-branches,too-many-statements
def istrue(v, find_ref, prior):
"Recursive function to evaluate a Condition"
retval = False
if EQUALS in v:
eq = v[EQUALS]
if len(eq) == 2:
val0 = eq[0]
val1 = eq[1]
if IF in val0:
val0 = resolve_if(val0[IF], find_ref, prior)
if IF in val1:
val1 = resolve_if(val1[IF], find_ref, prior)
if REF in val0:
val0 = find_ref(val0[REF])
if REF in val1:
val1 = find_ref(val1[REF])
retval = val0 == val1
else:
msg = f"Equals expression should be a list with 2 elements: {eq}"
raise exceptions.InvalidModuleError(msg=msg)
if NOT in v:
if not isinstance(v[NOT], list):
msg = f"Not expression should be a list with 1 element: {v[NOT]}"
raise exceptions.InvalidModuleError(msg=msg)
retval = not istrue(v[NOT][0], find_ref, prior)
if AND in v:
vand = v[AND]
msg = f"And expression should be a list with 2 elements: {vand}"
if not isinstance(vand, list):
raise exceptions.InvalidModuleError(msg=msg)
if len(vand) != 2:
raise exceptions.InvalidModuleError(msg=msg)
retval = istrue(vand[0], find_ref, prior) and istrue(
vand[1], find_ref, prior
)
if OR in v:
vor = v[OR]
msg = f"Or expression should be a list with 2 elements: {vor}"
if not isinstance(vor, list):
raise exceptions.InvalidModuleError(msg=msg)
if len(vor) != 2:
raise exceptions.InvalidModuleError(msg=msg)
retval = istrue(vor[0], find_ref, prior) or istrue(
vor[1], find_ref, prior
)
if IF in v:
# Shouldn't ever see an IF here
msg = f"Unexpected If: {v[IF]}"
raise exceptions.InvalidModuleError(msg=msg)
if CONDITION in v:
condition_name = v[CONDITION]
if condition_name in prior:
retval = prior[condition_name]
else:
msg = f"Condition {condition_name} was not evaluated yet"
raise exceptions.InvalidModuleError(msg=msg)
# TODO: Should we re-order the conditions?
# We are depending on the author putting them in order

return retval
Loading
Loading