-
Notifications
You must be signed in to change notification settings - Fork 164
add raw json/yaml template support #530
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 raw json/yaml template support #530
Conversation
Codecov Report
@@ Coverage Diff @@
## master #530 +/- ##
==========================================
- Coverage 87.94% 87.72% -0.23%
==========================================
Files 91 93 +2
Lines 5875 6003 +128
==========================================
+ Hits 5167 5266 +99
- Misses 708 737 +29
Continue to review full report at Codecov.
|
|
@phobologic @ejholmes I'm mildly surprised at how easy this ended up being; I've tested it with a couple of yaml & json templates without issue. Could use some help figuring out how to use schematics to model the option to specify exactly one of |
ejholmes
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cool. I think this is a good start, but I wonder if we can implement everything within RawTemplateBlueprint, without changing any stacker internals.
stacker/actions/build.py
Outdated
| required_parameters = stack.required_parameter_definitions.keys() | ||
| parameters = _handle_missing_parameters(resolved, required_parameters, | ||
| provider_stack) | ||
| if hasattr(stack.blueprint, 'raw_template_path') and ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the looks of it, the changes in this file shouldn't really be necessary. I think all you would need to do is implement the get_parameter_definitions() and get_required_parameter_definitions() method on the blueprint.
If we go down this path, the contract between _launch_stack and a "blueprint" starts to get muddled and brittle.
stacker/blueprints/raw.py
Outdated
| description) | ||
| self._raw_template_path = raw_template_path | ||
|
|
||
| def create_template(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not really necessary to override this, since it won't ever actually be called. I think it's actually better to remove it, because then the base class will raise an exception if it does get called.
stacker/util.py
Outdated
| return client._client_config.region_name | ||
|
|
||
|
|
||
| def get_template_default_params(template_path): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the CloudFormation API, there's a ValidateTemplate call that will parse the parameters from the template, and return them.
With that said, idk what's actually better here, since that would require another slow network call, which sucks.
stacker/blueprints/raw.py
Outdated
| """Load template and generate its md5 hash.""" | ||
| if self.description: | ||
| self.set_template_description(self.description) | ||
| with open(self.raw_template_path, 'r') as myfile: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we call this something other than myfile? I'm having flashbacks of php.
stacker/util.py
Outdated
| """ | ||
| params = {} | ||
|
|
||
| template_ext = os.path.splitext(template_path)[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems odd that we read the file in 2 separate locations. Can we read the file once, then call this method with the in memory content? I think that'll be easier of you implement those get_parameter_definitions() and get_required_parameter_definitions() methods on the blueprint.
| The python class path to the Blueprint to be used. | ||
| The python class path to the Blueprint to be used. Specify this or | ||
| ``template_path`` for the stack. | ||
| **template_path:** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda feel like this could all be done without even adding a new config option. The RawTemplateBlueprint could just use a variable to specify the template path.
For example:
stacks:
- name: vpc
class_path: stacker.blueprints.RawTemplateBlueprint
variables:
template_path: templates/vpc.yamlWhether or not that's better is probably debatable, but it keeps stacker config's more generic, which is good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth of the pain of class_path vs template_path to avoid any other config values on where the template is.
|
@ejholmes thanks for the feedback; you're right, it's much nicer fixing the blueprint to do the right thing. I've gone through and tested build/destroy/diff now and think this is in a good place. |
ejholmes
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, another round of review. This is a looking better!
I'll let @phobologic chime in as well.
stacker/actions/diff.py
Outdated
| old_params = {} | ||
|
|
||
| stack.resolve(self.context, self.provider) | ||
| yaml_stack = hasattr(stack.blueprint, 'raw_template_format') and ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda the same comment as before about changes to stacker.actions.build. I don't think actions should have any coupling to different types of blueprints.
Instead, we should just change this so that the action calls a to_json method on the blueprint, which should always return the JSON format. In the case of a yaml raw template, it would just yaml parse -> json dump it.
stacker/blueprints/raw.py
Outdated
| return value | ||
|
|
||
|
|
||
| class RawTemplateBlueprint(Blueprint): # pylint: disable=abstract-method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm starting to feel like maybe inheriting from Blueprint isn't the right thing for this. It might be easier to maintain of it just inherits from object, since there doesn't really seem to be much that's re-usable from Blueprint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, agreed that it doesn't seem to re-use much. I like it having a override/subset relationship, but I could be persuaded otherwise.
| mappings=self.mappings, | ||
| description=self.definition.description, | ||
| ) | ||
| if self.definition.class_path: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could probably be simplified a bit to:
kwargs = {}
blueprint_class = None
if self.definition.class_path:
class_path = self.definition.class_path
blueprint_class = util.load_object_from_string(class_path)
if not hasattr(blueprint_class, "rendered"):
raise AttributeError("Stack class %s does not have a "
"\"rendered\" "
"attribute." % (class_path,))
elif self.definition.template_path:
blueprint_class = RawTemplateBlueprint
kwargs["raw_template_path"] = self.definition.template_path
else:
raise AttributeError("Stack does not have a defined class or "
"template path.")
self._blueprint = blueprint_class(
name=self.name,
context=self.context,
mappings=self.mappings,
description=self.definition.description,
**kwargs
)
stacker/tests/test_config.py
Outdated
| self.assertEquals( | ||
| error.__str__(), | ||
| "This field is required.") | ||
| # TODO: add test for class_path or template_path (and not both) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would check the schematics documentation, but I think you'd likely need to perform this validation in stacker.config.Config#validate.
stacker/util.py
Outdated
| return client._client_config.region_name | ||
|
|
||
|
|
||
| def get_template_file_format(template_path): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move these methods to stacker.blueprints.raw, since they're only used there? At the very least, it'll make reading RawTemplateBlueprint easier.
stacker/blueprints/raw.py
Outdated
|
|
||
| def render_template(self): | ||
| """Load template and generate its md5 hash.""" | ||
| with open(self.raw_template_path, 'r') as template: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're still reading the template twice. Once here, and again in util.get_template_params. We should only need to read the template from disk once into memory here, and then use the in memory contents to get params.
stacker/blueprints/raw.py
Outdated
| version = hashlib.md5(rendered).hexdigest()[:8] | ||
| parsed_template = yaml.load(rendered) | ||
| if 'Transform' in parsed_template: | ||
| self.template.add_transform(parsed_template['Transform']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we do this? What significance does the troposphere Template play in this blueprint?
stacker/util.py
Outdated
| params = {} | ||
|
|
||
| if not os.path.isfile(template_path): | ||
| raise EnvironmentError("Could not find CFN template at path " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a test for this? It seems like this isn't a defined exception:
stacker $ git grep EnvironmentError|
@ejholmes good thoughts, thanks. I think I've addressed each comment. |
|
@troyready I opened a PR against yours with a few small changes, and some more tests: troyready#1. |
* update diff action to support regular & raw blueprints without conditions * fix validation & test of class / template path use in Config * move raw blueprint specific function from util to blueprint module * cleanup class selection in stack.py
afb9bf2 to
9cb66ef
Compare
|
Thanks! Merged and rebased. |
|
I'll let @phobologic give the final 👍. Otherwise, this looks awesome to me. |
russellballestrini
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow great work on this @troyready !
Very cool, looks good to me!
* add raw json/yaml template support Fixes cloudtools#444 * fix diff; fix transform; remove build action hack * add raw template tests * additional updates for raw template support * update diff action to support regular & raw blueprints without conditions * fix validation & test of class / template path use in Config * move raw blueprint specific function from util to blueprint module * cleanup class selection in stack.py * Handle mutual exclusion validation better * Add functional test for template_path * Remove dependency on troposphere template * Make RawTemplateBlueprint inherit from object. * cleanup pylint/pydocstyle messages in raw blueprint
Fixes #444