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 parameter_files and parameters configuration options to stack definition #338

Merged
Show file tree
Hide file tree
Changes from 12 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ The format is based on [Keep a Changelog], and this project adheres to

## [Unreleased]

### Added

- `parameters_dir` is now configurable to match the existing `template_dir`.
- `parameter_files` configures an array of parameter files relative to
`parameters_dir` that will be used instead of automatic parameter file globs
based on region and stack name.
- `parameters` configures stack parameters directly on the stack definition
rather than requiring an external parameter file.

### Fixed

- JSON template bodies with whitespace on leading lines would incorrectly be
Expand Down
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ stack_defaults:
```

Additional files can be configured to be uploaded to S3 alongside the templates:

```yaml
stacks:
production:
Expand All @@ -131,6 +132,7 @@ stacks:
files:
- userdata.sh
```

## Directories

- `templates` - CloudFormation, SparkleFormation or CfnDsl templates.
Expand All @@ -155,7 +157,8 @@ template_compilers:

## Parameters

Parameters are loaded from multiple YAML files, merged from the following lookup paths from bottom to top:
By default, parameters are loaded from multiple YAML files, merged from the
following lookup paths from bottom to top:

- parameters/[stack_name].yaml
- parameters/[stack_name].yml
Expand All @@ -170,6 +173,30 @@ A simple parameter file could look like this:
key_name: myapp-us-east-1
```

Alternatively, a `parameter_files` array can be defined to explicitly list
parameter files that will be loaded. If `parameter_files` are defined, the
automatic search locations will not be used.

```yaml
parameters_dir: parameters # the default
stacks:
us-east-1:
my-app:
parameter_files:
- my-app.yml # parameters/my-app.yml
```

Parameters can also be defined inline with stack definitions:

```yaml
stacks:
us-east-1:
my-app:
parameters:
VpcId:
stack_output: my-vpc/VpcId
```

### Compile Time Parameters

Compile time parameters can be used for [SparkleFormation](http://www.sparkleformation.io) templates. It conforms and
Expand Down
65 changes: 65 additions & 0 deletions features/apply_with_explicit_parameter_files.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Feature: Apply command with explicit parameter files

Background:
Given a file named "stack_master.yml" with:
"""
stack_defaults:
tags:
Application: myapp
stacks:
us-east-1:
myapp-web:
template: myapp.rb
parameter_files:
- myapp-web-parameters.yml
"""
And a file named "parameters/us-east-1/myapp-web.yml" with:
"""
Color: blue
"""
And a file named "parameters/myapp-web-parameters.yml" with:
"""
KeyName: my-key
"""
And a directory named "templates"
And a file named "templates/myapp.rb" with:
"""
SparkleFormation.new(:myapp) do
description "Test template"

parameters.key_name do
description 'Key name'
type 'String'
end

parameters.color do
description 'Color'
type 'String'
default 'red'
end

resources.instance do
type 'AWS::EC2::Instance'
properties do
image_id 'ami-0080e4c5bc078760e'
instance_type 't2.micro'
end
end
end
"""

Scenario: Run apply and create stack with explicit parameter files
Given I stub the following stack events:
| stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp |
| 1 | 1 | myapp-web | myapp-web | CREATE_COMPLETE | AWS::CloudFormation::Stack | 2020-10-29 00:00:00 |
When I run `stack_master apply us-east-1 myapp-web --trace`
Then the output should match /2020-10-29 00:00:00 (\+|\-)[0-9]{4} myapp-web AWS::CloudFormation::Stack CREATE_COMPLETE/
And the output should contain all of these lines:
| Stack diff: |
| + "Instance": { |
| Parameters diff: |
| KeyName: my-key |
| Proposed change set: |
And the output should not contain "Color: blue"
And the output should contain "Color: red"
And the exit status should be 0
2 changes: 1 addition & 1 deletion features/apply_with_s3.feature
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Feature: Apply command
| Parameters diff: |
| KeyName: my-key |
And the output should match /2020-10-29 00:00:00 (\+|\-)[0-9]{4} myapp-vpc AWS::CloudFormation::Stack CREATE_COMPLETE/
And an S3 file in bucket "my-bucket" with key "cfn_templates/my-app/myapp_vpc.json" exists with content:
And an S3 file in bucket "my-bucket" with key "cfn_templates/my-app/myapp_vpc.json" exists with JSON content:
"""
{
"Description": "Test template",
Expand Down
46 changes: 46 additions & 0 deletions features/apply_with_stack_definition_parameters.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Feature: Apply command with stack definition parameters

Background:
Given a file named "stack_master.yml" with:
"""
stacks:
us-east-1:
myapp_web:
template: myapp.rb
parameters:
KeyName: my-key
"""
And a directory named "templates"
And a file named "templates/myapp.rb" with:
"""
SparkleFormation.new(:myapp) do
description "Test template"

parameters.key_name do
description 'Key name'
type 'String'
end

resources.instance do
type 'AWS::EC2::Instance'
properties do
image_id 'ami-0080e4c5bc078760e'
instance_type 't2.micro'
end
end
end
"""

Scenario: Run apply with parameters contained in
Given I stub the following stack events:
| stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp |
| 1 | 1 | myapp-web | myapp-web | CREATE_COMPLETE | AWS::CloudFormation::Stack | 2020-10-29 00:00:00 |
When I run `stack_master apply us-east-1 myapp-web --trace`
Then the output should match /2020-10-29 00:00:00 (\+|\-)[0-9]{4} myapp-web AWS::CloudFormation::Stack CREATE_COMPLETE/
And the output should contain all of these lines:
| Stack diff: |
| + "Instance": { |
| Parameters diff: |
| KeyName: my-key |
| Proposed change set: |
And the exit status should be 0
6 changes: 6 additions & 0 deletions features/step_definitions/stack_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ def extract_hash_from_kv_string(string)
file = StackMaster.s3_driver.find_file(bucket: bucket, object_key: key)
expect(file).to eq body
end

When(/^an S3 file in bucket "([^"]*)" with key "([^"]*)" exists with JSON content:$/) do |bucket, key, body|
stevehodgkiss marked this conversation as resolved.
Show resolved Hide resolved
file = StackMaster.s3_driver.find_file(bucket: bucket, object_key: key)
parsed_file = JSON.parse(file)
expect(parsed_file).to eq JSON.parse(body)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what this test achieves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It checks the contents match by comparing parsed hashes rather than strings. For some reason apply_with_s3.feature started failing, and master and has the same issue - https://travis-ci.org/github/envato/stack_master/jobs/692383878#L2514 Maybe it's related to a sparkleformation upgrade?

2 changes: 1 addition & 1 deletion lib/stack_master/commands/tidy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def perform
parameter_files = Set.new(find_parameter_files())

status = @config.stacks.each do |stack_definition|
parameter_files.subtract(stack_definition.parameter_files)
parameter_files.subtract(stack_definition.parameter_files_from_globs)
template = File.absolute_path(stack_definition.template_file_path)

if template
Expand Down
3 changes: 3 additions & 0 deletions lib/stack_master/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def self.load!(config_file = 'stack_master.yml')
attr_accessor :stacks,
:base_dir,
:template_dir,
:parameters_dir,
:stack_defaults,
:region_defaults,
:region_aliases,
Expand All @@ -39,6 +40,7 @@ def initialize(config, base_dir)
@config = config
@base_dir = base_dir
@template_dir = config.fetch('template_dir', nil)
@parameters_dir = config.fetch('parameters_dir', nil)
@stack_defaults = config.fetch('stack_defaults', {})
@region_aliases = Utils.underscore_keys_to_hyphen(config.fetch('region_aliases', {}))
@region_to_aliases = @region_aliases.inject({}) do |hash, (key, value)|
Expand Down Expand Up @@ -115,6 +117,7 @@ def load_stacks(stacks)
'stack_name' => stack_name,
'base_dir' => @base_dir,
'template_dir' => @template_dir,
'parameters_dir' => @parameters_dir,
'additional_parameter_lookup_dirs' => @region_to_aliases[region])
stack_attributes['allowed_accounts'] = attributes['allowed_accounts'] if attributes['allowed_accounts']
@stacks << StackDefinition.new(stack_attributes)
Expand Down
7 changes: 3 additions & 4 deletions lib/stack_master/parameter_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ class ParameterLoader

COMPILE_TIME_PARAMETERS_KEY = 'compile_time_parameters'

def self.load(parameter_files)
def self.load(parameter_files: [], parameters: {})
StackMaster.debug 'Searching for parameter files...'
parameter_files.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, file_name|
parameters = load_parameters(file_name)
all_parameters = parameter_files.map { |file_name| load_parameters(file_name) } + [parameters]
all_parameters.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, parameters|
template_parameters = create_template_parameters(parameters)
compile_time_parameters = create_compile_time_parameters(parameters)

merge_and_camelize(hash[:template_parameters], template_parameters)
merge_and_camelize(hash[:compile_time_parameters], compile_time_parameters)
hash
end

end

private
Expand Down
4 changes: 2 additions & 2 deletions lib/stack_master/stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def self.find(region, stack_name)
end

def self.generate(stack_definition, config)
parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
parameter_hash = ParameterLoader.load(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters)
template_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:template_parameters])
compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options)
Expand All @@ -76,7 +76,7 @@ def self.generate(stack_definition, config)
end

def self.generate_without_parameters(stack_definition, config)
parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
parameter_hash = ParameterLoader.load(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters)
compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options)
template_format = TemplateUtils.identify_template_format(template_body)
Expand Down
35 changes: 26 additions & 9 deletions lib/stack_master/stack_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ class StackDefinition
:additional_parameter_lookup_dirs,
:s3,
:files,
:compiler_options
:compiler_options,
:parameters_dir,
:parameters,
:parameter_files

attr_reader :compiler

Expand All @@ -32,8 +35,12 @@ def initialize(attributes = {})
@compiler = nil
super
@additional_parameter_lookup_dirs ||= []
@base_dir ||= ""
@template_dir ||= File.join(@base_dir, 'templates')
@parameters_dir ||= File.join(@base_dir, 'parameters')
@allowed_accounts = Array(@allowed_accounts)
@parameters ||= {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why use the conditional assignment operator in the initializer? Is @parameters expected to be set before the initializer is run?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The call to super above sets values from the yaml file, so it would either be set from that point or not, in which case the default is {}.

@parameter_files ||= []
end

def ==(other)
Expand All @@ -56,10 +63,6 @@ def ==(other)
@compiler_options == other.compiler_options
end

def compiler=(compiler)
@compiler = compiler.&to_sym
end

def template_file_path
return unless template
File.expand_path(File.join(template_dir, template))
Expand All @@ -85,7 +88,15 @@ def s3_template_file_name
Utils.change_extension(template, 'json')
end

def parameter_files
def all_parameter_files
if parameter_files.empty?
parameter_files_from_globs
else
parameter_files
end
end

def parameter_files_from_globs
parameter_file_globs.map(&Dir.method(:glob)).flatten
end

Expand All @@ -101,20 +112,26 @@ def s3_configured?
!s3.nil?
end

def parameter_files
Array(@parameter_files).map do |file|
File.expand_path(File.join(parameters_dir, file))
stevehodgkiss marked this conversation as resolved.
Show resolved Hide resolved
end
end

private

def additional_parameter_lookup_globs
additional_parameter_lookup_dirs.map do |a|
File.join(base_dir, 'parameters', a, "#{stack_name_glob}.y*ml")
File.join(parameters_dir, a, "#{stack_name_glob}.y*ml")
end
end

def region_parameter_glob
File.join(base_dir, 'parameters', "#{region}", "#{stack_name_glob}.y*ml")
File.join(parameters_dir, "#{region}", "#{stack_name_glob}.y*ml")
end

def default_parameter_glob
File.join(base_dir, 'parameters', "#{stack_name_glob}.y*ml")
File.join(parameters_dir, "#{stack_name_glob}.y*ml")
end

def stack_name_glob
Expand Down
4 changes: 2 additions & 2 deletions spec/stack_master/parameter_loader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
let(:stack_file_name) { '/base_dir/parameters/stack_name.yml' }
let(:region_file_name) { '/base_dir/parameters/us-east-1/stack_name.yml' }

subject(:parameters) { StackMaster::ParameterLoader.load([stack_file_name, region_file_name]) }
subject(:parameters) { StackMaster::ParameterLoader.load(parameter_files: [stack_file_name, region_file_name]) }

before do
file_mock(stack_file_name, **stack_file_returns)
Expand Down Expand Up @@ -60,7 +60,7 @@
let(:region_yaml_file_returns) { {exists: true, read: "Param1: value1\nParam2: valueX"} }
let(:region_yaml_file_name) { "/base_dir/parameters/us-east-1/stack_name.yaml" }

subject(:parameters) { StackMaster::ParameterLoader.load([stack_file_name, region_yaml_file_name, region_file_name]) }
subject(:parameters) { StackMaster::ParameterLoader.load(parameter_files: [stack_file_name, region_yaml_file_name, region_file_name]) }

before do
file_mock(region_yaml_file_name, **region_yaml_file_returns)
Expand Down
Loading