cloudformula 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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