cloudformula 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +1 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +206 -0
  9. data/Rakefile +1 -0
  10. data/bin/cloudformula +12 -0
  11. data/cloudformula.gemspec +26 -0
  12. data/integration_tests/.ruby-gemset +1 -0
  13. data/integration_tests/.ruby-version +1 -0
  14. data/integration_tests/Gemfile +3 -0
  15. data/integration_tests/README.md +33 -0
  16. data/integration_tests/fixtures/minimal_cf_template.json +30 -0
  17. data/integration_tests/fixtures/minimal_cf_template_update.json +54 -0
  18. data/integration_tests/stack_create_update.rb +26 -0
  19. data/lib/cloudformula/cli.rb +113 -0
  20. data/lib/cloudformula/cloud_formation.rb +54 -0
  21. data/lib/cloudformula/help/create.txt +12 -0
  22. data/lib/cloudformula/help/generate.txt +10 -0
  23. data/lib/cloudformula/help/top.txt +20 -0
  24. data/lib/cloudformula/help/update.txt +12 -0
  25. data/lib/cloudformula/json_erb.rb +42 -0
  26. data/lib/cloudformula/string.rb +11 -0
  27. data/lib/cloudformula/template.rb +99 -0
  28. data/lib/cloudformula/validator.rb +172 -0
  29. data/lib/cloudformula/version.rb +3 -0
  30. data/lib/cloudformula.rb +23 -0
  31. data/spec/cloud_formula_spec.rb +18 -0
  32. data/spec/cloudformula/cloud_formation_spec.rb +55 -0
  33. data/spec/cloudformula/template_spec.rb +303 -0
  34. data/spec/fixtures/_partial.json.erb +1 -0
  35. data/spec/fixtures/with_custom_erb_validations.erb +4 -0
  36. data/spec/fixtures/with_erb_parameters.erb +3 -0
  37. data/spec/fixtures/with_erb_parameters.json.erb +4 -0
  38. data/spec/fixtures/with_erb_parameters_and_stack_options.json.erb +5 -0
  39. data/spec/fixtures/with_erb_parameters_answer.json +4 -0
  40. data/spec/fixtures/with_erb_parameters_answer.txt +3 -0
  41. data/spec/fixtures/with_erb_parameters_escaped_answer.json +4 -0
  42. data/spec/fixtures/with_erb_validations.json.erb +53 -0
  43. data/spec/fixtures/with_partial.json.erb +5 -0
  44. data/spec/fixtures/with_partial_answer.json +5 -0
  45. data/spec/fixtures/with_raw.json.erb +3 -0
  46. data/spec/fixtures/with_raw_answer.json +3 -0
  47. data/spec/fixtures/with_stack_options.json.erb +4 -0
  48. data/spec/fixtures/without_erb_parameters.erb +1 -0
  49. data/spec/fixtures/without_erb_parameters.json.erb +3 -0
  50. data/spec/spec_helper.rb +11 -0
  51. metadata +185 -0
@@ -0,0 +1,20 @@
1
+ NAME
2
+ cloudformula
3
+ https://github.com/Kabam/cloudformula
4
+
5
+ DESCRIPTION
6
+ Dynamically generates AWS CloudFormation templates, creates, and updates
7
+ CloudFormation stacks.
8
+
9
+ USAGE
10
+ cloudformula <command> <options>
11
+
12
+ AVAILABLE COMMANDS
13
+ 'cloudformula <command> --help' provides more detailed information for each
14
+ command.
15
+
16
+ generate :: Generate a template
17
+ create :: Generate a template and create a new CloudFormation stack
18
+ update :: Generate a template and update an existing CloudFormation stack
19
+
20
+ OPTIONS
@@ -0,0 +1,12 @@
1
+ NAME
2
+ cloudformula update
3
+
4
+ DESCRIPTION
5
+ Dynamically generates an AWS CloudFormation template and uses it to update an
6
+ existing CloudFormation stack.
7
+
8
+ USAGE
9
+ cloudformula update --stack-name '<stackname>' --template '<template>' \\
10
+ --parameters '<params>' [--region '<region>']
11
+
12
+ OPTIONS
@@ -0,0 +1,42 @@
1
+ require 'erb'
2
+
3
+ module CloudFormula
4
+ # Custom ERB compiler which causes variable interpolation inside <%= %> to be json encoded.
5
+ # Inside regular <% %>, the variables are used without json encoding.
6
+ #
7
+ # CloudFormation stack options defaults can be set directly within a template by setting @stack_options.
8
+ # See README.md "CloudFormation stack options" for details.
9
+ #
10
+ # Basic validation functionality is possible by setting @validations.
11
+ # See README.md "Validations" for details
12
+ #
13
+ class JsonErb < ::ERB
14
+ class CompilerWithBlock < ::ERB::Compiler
15
+ def add_insert_cmd(out, content)
16
+ out.push "#{@insert_cmd}((#{content}).to_s.try_to_json)"
17
+ end
18
+ end
19
+
20
+ def make_compiler(*args)
21
+ CompilerWithBlock.new *args
22
+ end
23
+
24
+ # Returns the @stack_options value set within the template.
25
+ #
26
+ # @param [Binding] b accepts a Binding or Proc object which is used to set the context of code evaluation.
27
+ # @return [Hash] The stack options set by the template or an empty Hash if none are set.
28
+ def stack_options(b = new_toplevel)
29
+ result(b)
30
+ b.eval('@stack_options') || {}
31
+ end
32
+
33
+ # Returns the @validations value set within the template.
34
+ #
35
+ # @param [Binding] b accepts a Binding or Proc object which is used to set the context of code evaluation.
36
+ # @return [Hash] The stack options set by the template or an empty Hash if none are set.
37
+ def validations(b = new_toplevel)
38
+ result(b)
39
+ b.eval('@validations') || {}
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ require 'json'
2
+
3
+ class String
4
+ # Returns the string JSON-escaped.
5
+ # If the cloudformula_raw flag is set, returns the string as-is.
6
+ # @return [String]
7
+ def try_to_json
8
+ return self if instance_variable_get :@cloudformula_raw
9
+ self.to_json
10
+ end
11
+ end
@@ -0,0 +1,99 @@
1
+ require 'json'
2
+
3
+ module CloudFormula
4
+ # Defines a CloudFormula/CloudFormation template
5
+ class Template
6
+ attr :parameters, :source
7
+
8
+ # @param [String] source Path to source template
9
+ # @param [Hash] parameters Parameter names and values to use in the template
10
+ def initialize(source = '', parameters = {})
11
+ @source = source
12
+ @parameters = parameters
13
+ end
14
+
15
+ # Evaluates and returns the @source template populated with values from @parameters
16
+ # @return [String]
17
+ def generate
18
+ init_parameters @parameters
19
+ raise "Parameter validation failed.\n\nErrors:\n #{errors.join("\n")}" if invalid?
20
+ JsonErb.new(File.read(@source)).result(binding)
21
+ end
22
+
23
+ # Evalutes and returns the @source template populated with values from @parameters
24
+ # @return [Hash]
25
+ def generate_hash
26
+ JSON.parse(generate)
27
+ end
28
+
29
+ # Causes the given string value to be output without escape characters
30
+ # @param [String] value
31
+ # @return [String]
32
+ def raw(value)
33
+ value.instance_variable_set :@cloudformula_raw, true
34
+ value
35
+ end
36
+
37
+ # @param [String] source Path to source template
38
+ # @param [Hash] parameters Parameter names and values to use in the template
39
+ # @return [String] The result of generating the template
40
+ def render(source, parameters = {})
41
+ raw CloudFormula::Template.new(source, parameters).generate
42
+ end
43
+
44
+ # Returns the CloudFormation stack options defined by the template.
45
+ def stack_options
46
+ init_parameters @parameters
47
+ JsonErb.new(File.read(@source), 0).stack_options(binding) rescue {}
48
+ end
49
+
50
+ # Returns the validations defined by the template.
51
+ def validations
52
+ init_parameters @parameters
53
+ JsonErb.new(File.read(@source), 0).validations(binding) rescue {}
54
+ end
55
+
56
+ # Returns the merged options Hash for use by AWS::CloudFormation::StackCollection#create and
57
+ # AWS::CloudFormation::Stack#update
58
+ #
59
+ # @param [Hash] override_options Optional. Stack options to override. See README.md "CloudFormation stack options" for details
60
+ # @param [Hash] parameters Parameters to supply to the template. Required if the template contains any required parameters without a default value
61
+ def aws_options(override_options = {}, parameters = {})
62
+ stack_options.merge(override_options.merge({ :parameters => parameters }))
63
+ end
64
+
65
+ # Similar to Active Record Validations errors[] object
66
+ # @see http://guides.rubyonrails.org/v3.2.13/active_record_validations_callbacks.html#validations_overview-errors
67
+ def errors
68
+ validate
69
+ end
70
+
71
+ def valid?
72
+ errors.length == 0
73
+ end
74
+
75
+ def invalid?
76
+ !valid?
77
+ end
78
+
79
+ private
80
+
81
+ # Sets the given Hash as instance variables for use in the ERB template
82
+ # @param [Hash] values
83
+ def init_parameters(values = {})
84
+ values.each { |key, value| instance_variable_set("@#{key}", value) }
85
+ end
86
+
87
+ def validate
88
+ errors = []
89
+ validations.each do |parameter_name, parameter_validations|
90
+ validator = CloudFormula::Validator.new(parameter_name, parameter_validations)
91
+ validate_result = validator.validate(@parameters[parameter_name.to_sym])
92
+ errors += validate_result
93
+ end
94
+ #puts errors.inspect
95
+ errors
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,172 @@
1
+ require 'json'
2
+
3
+ module CloudFormula
4
+ # Contains logic to perform basic validation of parameters defined in a JsonErb template.
5
+ #
6
+ # Available validations:
7
+ # :exclusion => ['val1'] Ensures a value is not in the exclusion list
8
+ # :inclusion => ['val1', 'val2'] Ensures a value is in the inclusion list
9
+ # :length => { Ensures value length
10
+ # :minimum => n,
11
+ # :maximum => n,
12
+ # :in|:within => x..y,
13
+ # :is => n
14
+ # }
15
+ # :format => /regex/ Ensures a value matches the regex
16
+ # :presence => true|false Ensures a value exists and has a length greater than 0
17
+ #
18
+ class Validator
19
+ class Rule
20
+ attr_accessor :name, :value
21
+ def initialize(name, value)
22
+ @name = name
23
+ @value = value
24
+ end
25
+ end
26
+
27
+ attr_accessor :parameter_name, :validations
28
+
29
+ # @param [Hash] validations The rules defined for the given parameter_name
30
+ def initialize(parameter_name, validations)
31
+ @parameter_name = parameter_name
32
+ @validations = validations
33
+ end
34
+
35
+ # @param [Object] value The value to check against
36
+ # @return [Array] An Array of Strings describing the failure(s) if any checks fail, or an empty Array otherwise
37
+ def validate(value)
38
+ errors = []
39
+ @validations.each do |rule_name, rule_value|
40
+ rule_result = check_rule Rule.new(rule_name, rule_value), value
41
+ errors += rule_result
42
+ end
43
+ errors
44
+ end
45
+
46
+ private
47
+
48
+ # @param [Rule] rule The rule to check
49
+ # @param [Object] parameter_value The value to check against
50
+ # @return [Array] A string describing the failure will be returned if the check fails, or an empty Array otherwise
51
+ def check_rule(rule, parameter_value)
52
+ case rule.name
53
+ when :exclusion, :inclusion, :length, :numericality, :format, :presence
54
+ self.send("check_#{rule.name}", rule.value, @parameter_name, parameter_value)
55
+ else
56
+ raise "Unknown rule name #{rule.name} found in ERB template."
57
+ end || []
58
+ end
59
+
60
+ def check_exclusion(constraint, parameter_name, value)
61
+ unless value.nil?
62
+ if constraint.include? value
63
+ ["Value '#{value}' for parameter '#{parameter_name}' must not be one of [#{constraint.join(', ')}]"]
64
+ end
65
+ end
66
+ end
67
+
68
+ def check_inclusion(constraint, parameter_name, value)
69
+ unless value.nil?
70
+ unless constraint.include? value
71
+ ["Value '#{value}' for parameter '#{parameter_name}' must be one of [#{constraint.join(', ')}]"]
72
+ end
73
+ end
74
+ end
75
+
76
+ def check_length(constraint, parameter_name, value)
77
+ errors = []
78
+ unless value.nil?
79
+ constraint.each do |length_rule, length_value|
80
+ case length_rule
81
+ when :maximum
82
+ if value.length > length_value
83
+ errors << "Parameter '#{parameter_name}' must have length less than or equal to #{length_value}"
84
+ end
85
+ when :minimum
86
+ if value.length < length_value
87
+ errors << "Parameter '#{parameter_name}' must have length greater than or equal to #{length_value}"
88
+ end
89
+ when :in, :within
90
+ unless length_value.cover? value.length
91
+ errors << "Parameter '#{parameter_name}' must have length in the range of #{length_value}"
92
+ end
93
+ when :is
94
+ if value.length != length_value
95
+ errors << "Parameter '#{parameter_name}' must have length #{length_value}"
96
+ end
97
+ else
98
+ raise "Unknown length rule attribute #{length_rule} found in ERB template."
99
+ end
100
+ end
101
+ end
102
+ errors
103
+ end
104
+
105
+ def check_numericality(constraint, parameter_name, value)
106
+ errors = []
107
+ unless value.nil?
108
+ constraint.each do |numericality_rule, numericality_constraint|
109
+ numeric_value = (/\./ =~ value.to_s ? value.to_f : value.to_i)
110
+ case numericality_rule
111
+ when :only_integer
112
+ if numericality_constraint
113
+ unless value.to_s =~ /\A[+-]?\d+\Z/
114
+ errors << "Parameter '#{parameter_name}' must be an integer"
115
+ end
116
+ end
117
+ when :equal_to
118
+ unless numeric_value == numericality_constraint
119
+ errors << "Parameter '#{parameter_name}' must be equal to #{numericality_constraint}"
120
+ end
121
+ when :greater_than
122
+ unless numeric_value > numericality_constraint
123
+ errors << "Parameter '#{parameter_name}' must be greater than #{numericality_constraint}"
124
+ end
125
+ when :less_than
126
+ unless numeric_value < numericality_constraint
127
+ errors << "Parameter '#{parameter_name}' must be less than #{numericality_constraint}"
128
+ end
129
+ when :greater_than_or_equal_to
130
+ unless numeric_value >= numericality_constraint
131
+ errors << "Parameter '#{parameter_name}' must be greater than or equal to #{numericality_constraint}"
132
+ end
133
+ when :less_than_or_equal_to
134
+ unless numeric_value <= numericality_constraint
135
+ errors << "Parameter '#{parameter_name}' must be less than or equal to #{numericality_constraint}"
136
+ end
137
+ when :even
138
+ if numericality_constraint
139
+ unless numeric_value.respond_to?('even?') && numeric_value.even?
140
+ errors << "Parameter '#{parameter_name}' must be even"
141
+ end
142
+ end
143
+ when :odd
144
+ if numericality_constraint
145
+ unless numeric_value.respond_to?('odd?') && numeric_value.odd?
146
+ errors << "Parameter '#{parameter_name}' must be odd"
147
+ end
148
+ end
149
+ else
150
+ raise "Unknown numericality rule attribute #{numericality_rule} found in ERB template."
151
+ end
152
+ end
153
+ end
154
+ errors
155
+ end
156
+
157
+ def check_format(constraint, parameter_name, value)
158
+ unless value.nil?
159
+ unless constraint =~ value
160
+ ["Value '#{value}' for parameter '#{parameter_name}' must match regexp #{constraint.source}"]
161
+ end
162
+ end
163
+ end
164
+
165
+ def check_presence(constraint, parameter_name, value)
166
+ if constraint && (value.nil? || value.to_s == '')
167
+ ["Parameter '#{parameter_name}' must exist and have a length of at least 1"]
168
+ end
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module CloudFormula
2
+ VERSION = '1.1.2'
3
+ end
@@ -0,0 +1,23 @@
1
+ Dir.glob("#{File.dirname(__FILE__)}/cloudformula/*.rb") { |file| require file }
2
+
3
+ module CloudFormula
4
+ class << self # class methods
5
+ # Creates a new CloudFormula::Template object.
6
+ # @see CloudFormula::Template.initialize
7
+ def template(source = '', parameters = {})
8
+ Template.new(source, parameters)
9
+ end
10
+
11
+ # Creates a new AWS CloudFormation stack
12
+ # @see CloudFormula::CloudFormation.create_stack
13
+ def create_stack(region, stack_name, template, override_options = {}, parameters = {}, aws_access_key_id = nil, aws_secret_key = nil)
14
+ CloudFormation.create_stack(region, stack_name, template, override_options, parameters, aws_access_key_id, aws_secret_key)
15
+ end
16
+
17
+ # Updates an AWS CloudFormation stack
18
+ # @see CloudFormula::CloudFormation.update_stack
19
+ def update_stack(region, stack_name, template, parameters = {}, aws_access_key_id = nil, aws_secret_key = nil)
20
+ CloudFormation.update_stack(region, stack_name, template, parameters, aws_access_key_id, aws_secret_key)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ describe CloudFormula do
2
+ it 'should have a version' do
3
+ expect(CloudFormula::VERSION).to_not be_nil
4
+ expect(CloudFormula::VERSION).to_not be_empty
5
+ end
6
+
7
+ it 'template method should return a new Template object' do
8
+ expect(CloudFormula.template).to be_an_instance_of(CloudFormula::Template)
9
+ end
10
+
11
+ it 'create_stack method' do
12
+ expect(CloudFormula).to respond_to(:create_stack)
13
+ end
14
+
15
+ it 'update_stack method' do
16
+ expect(CloudFormula).to respond_to(:update_stack)
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ module CloudFormula
4
+ describe CloudFormation do
5
+ describe 'create_stack method' do
6
+ it 'should exist' do
7
+ expect(CloudFormula::CloudFormation).to respond_to(:create_stack)
8
+ end
9
+ end
10
+
11
+ describe 'update_stack method' do
12
+ it 'should exist' do
13
+ expect(CloudFormula::CloudFormation).to respond_to(:update_stack)
14
+ end
15
+ end
16
+
17
+ describe 'private get_cloud_formation method' do
18
+ it 'should raise an error with invalid credentials explicitly passed to it' do
19
+ stub_aws_env
20
+ CloudFormula::CloudFormation.instance_eval do
21
+ def test_cf
22
+ get_cloud_formation('us-east-1', 'foo', 'bar')
23
+ end
24
+ end
25
+ expect(CloudFormula::CloudFormation.test_cf).to be_a(AWS::CloudFormation)
26
+ expect { CloudFormula::CloudFormation.test_cf.stacks.each }.to raise_error
27
+ end
28
+
29
+ it 'should work with credentials from ENV' do
30
+ # This test requires AWS credentials in ENV. Unfortunately it cannot be faked.
31
+ CloudFormula::CloudFormation.instance_eval do
32
+ def test_cf
33
+ get_cloud_formation('us-east-1')
34
+ end
35
+ end
36
+ expect(CloudFormula::CloudFormation.test_cf).to be_a(AWS::CloudFormation)
37
+ expect { CloudFormula::CloudFormation.test_cf.stacks.each }.to_not raise_error
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+
44
+ def stub_aws_env
45
+ ENV.stub(:[]).with('AWS_ACCESS_KEY_ID').and_return(nil)
46
+ ENV.stub(:[]).with('AWS_SECRET_ACCESS_KEY').and_return(nil)
47
+ ENV.stub(:[]).with('AWS_SESSION_TOKEN').and_return(nil)
48
+ ENV.stub(:[]).with('AWS_CREDENTIAL_FILE').and_return(nil)
49
+ ENV.stub(:[]).with('http_proxy').and_return(nil)
50
+ ENV.stub(:[]).with('HTTP_PROXY').and_return(nil)
51
+ ENV.stub(:[]).with('AMAZON_ACCESS_KEY_ID').and_return(nil)
52
+ ENV.stub(:[]).with('AMAZON_SECRET_ACCESS_KEY').and_return(nil)
53
+ ENV.stub(:[]).with('AMAZON_SESSION_TOKEN').and_return(nil)
54
+ ENV.stub(:[]).with('AMAZON_CREDENTIAL_FILE').and_return(nil)
55
+ end