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 35 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0438bdc
Initial commit with TODOs for implementing modules
ericzbeard Nov 22, 2024
84cb385
Unit test configured
ericzbeard Dec 4, 2024
9eac23a
Start resolving refs
ericzbeard Dec 4, 2024
0b79526
Resolve Refs
ericzbeard Dec 5, 2024
6f24f03
parse_sub
ericzbeard Dec 5, 2024
86a5d29
Sub strings
ericzbeard Dec 6, 2024
7b72e29
Basic tests pass
ericzbeard Dec 6, 2024
df507cd
Fixed pylint issues
ericzbeard Dec 6, 2024
7808f62
Fix path to unit tests
ericzbeard Dec 6, 2024
e2131d2
Add the ability to specify a module as a Resource
ericzbeard Dec 9, 2024
49fb392
Fix issues with nested Subs and lists
ericzbeard Dec 9, 2024
ec32ecf
Recursive modules
ericzbeard Dec 9, 2024
160be7c
Implement module outputs
ericzbeard Dec 13, 2024
e517720
Handle spaces in parse_sub
ericzbeard Dec 13, 2024
e8e6d96
Enum
ericzbeard Dec 13, 2024
5b0ee49
Code review fixes
ericzbeard Dec 13, 2024
4eef2c4
Code review fixes
ericzbeard Dec 13, 2024
72659f5
Fix merging lists
ericzbeard Dec 16, 2024
5cb126c
Vpc module example with a map
ericzbeard Dec 18, 2024
3ac0577
Use Map in the parent template
ericzbeard Dec 18, 2024
e0cfcfc
Moving docs to a readme
ericzbeard Dec 18, 2024
c6851c9
updating readme
ericzbeard Dec 18, 2024
dd16375
Handle extra dots in getatts
ericzbeard Dec 18, 2024
e119576
Fix manifest error
ericzbeard Dec 18, 2024
39ac9a7
Add basic conditional support
ericzbeard Dec 19, 2024
2f0143b
Added more complete unit test for conditionals
ericzbeard Dec 20, 2024
36305ec
Download modules from a URL
ericzbeard Jan 13, 2025
497fb3d
Removed the requirement for the s3-bucket parameter
ericzbeard Jan 13, 2025
6e36de0
Confirm GetAtt with double quotes works as expected
ericzbeard Jan 14, 2025
6212018
Fix unit test self.uploader
ericzbeard Jan 14, 2025
92ad3bd
Constants section
ericzbeard Jan 15, 2025
fa7c208
Constants example
ericzbeard Jan 16, 2025
30250b3
Minor tweaks to comments
ericzbeard Jan 16, 2025
32fa228
Constants
ericzbeard Jan 27, 2025
49615a8
Use a constant for module source url in test
ericzbeard Jan 30, 2025
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:

Choose a reason for hiding this comment

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

Today CloudFormation doesn't support this. The only concern I have with this is it could cause infinite loops.

Conditions:
  One:
    Fn::Equals:
    - !If [Two, "A", "B"]
    - "A"
  Two:
    Fn::Equals:
    - !If [One, "A", "B"]
    - "B"

Later on we call out that Fn::If isn't supported so not sure why it is here.

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

Choose a reason for hiding this comment

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

If I'm reading this correct its possible this becomes val0 = "foo", val1 = {"Ref": "SomeResource"}. This would evaluate to false but could be true if this resource returns the value foo

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