cfer 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,7 @@ module Cfer::Cfn
3
3
 
4
4
  class Client < Cfer::Core::Client
5
5
  attr_reader :name
6
+ attr_reader :stack
6
7
 
7
8
  def initialize(options)
8
9
  @name = options[:stack_name]
@@ -27,51 +28,74 @@ module Cfer::Cfn
27
28
  @cfn.send(method, *args, &block)
28
29
  end
29
30
 
30
- def resolve(param)
31
- # See if the value follows the form @<stack>.<output>
32
- m = /^@(.+?)\.(.+)$/.match(param)
33
-
34
- if m
35
- fetch_output(m[1], m[2])
36
- else
37
- param
38
- end
39
- end
40
-
41
-
42
31
  def converge(stack, options = {})
43
32
  Preconditions.check(@name).is_not_nil
44
33
  Preconditions.check(stack) { is_not_nil and has_type(Cfer::Core::Stack) }
45
34
 
46
35
  response = validate_template(template_body: stack.to_cfn)
47
36
 
48
- parameters = response.parameters.map do |tmpl_param|
49
- cfn_param = stack.parameters[tmpl_param.parameter_key] || raise(Cfer::Util::CferError, "Parameter #{tmpl_param.parameter_key} was required, but not specified")
50
- cfn_param = resolve(cfn_param)
37
+ create_params = []
38
+ update_params = []
51
39
 
52
- output_val = tmpl_param.no_echo ? '*****' : cfn_param
53
- Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key}=#{output_val}"
40
+ previous_parameters =
41
+ begin
42
+ fetch_parameters
43
+ rescue Cfer::Util::StackDoesNotExistError
44
+ nil
45
+ end
54
46
 
55
- {
56
- parameter_key: tmpl_param.parameter_key,
57
- parameter_value: cfn_param,
58
- use_previous_value: false
59
- }
47
+ response.parameters.each do |tmpl_param|
48
+ input_param = stack.input_parameters[tmpl_param.parameter_key]
49
+ old_param = previous_parameters[tmpl_param.parameter_key] if previous_parameters
50
+
51
+ Cfer::LOGGER.debug "== Evaluating Parameter '#{tmpl_param.parameter_key.to_s}':"
52
+ Cfer::LOGGER.debug "Input value: #{input_param.to_s || 'nil'}"
53
+ Cfer::LOGGER.debug "Previous value: #{old_param.to_s || 'nil'}"
54
+
55
+
56
+ if input_param
57
+ output_val = tmpl_param.no_echo ? '*****' : input_param
58
+ Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key}=#{output_val}"
59
+ p = {
60
+ parameter_key: tmpl_param.parameter_key,
61
+ parameter_value: input_param,
62
+ use_previous_value: false
63
+ }
64
+
65
+ create_params << p
66
+ update_params << p
67
+ else
68
+ if old_param
69
+ Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key} is unspecified (unchanged)"
70
+ update_params << {
71
+ parameter_key: tmpl_param.parameter_key,
72
+ use_previous_value: true
73
+ }
74
+ else
75
+ Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key} is unspecified (default)"
76
+ end
77
+ end
60
78
  end
61
79
 
62
- options = {
80
+ Cfer::LOGGER.debug "==================="
81
+
82
+ stack_options = {
63
83
  stack_name: name,
64
84
  template_body: stack.to_cfn,
65
- parameters: parameters,
66
85
  capabilities: response.capabilities
67
86
  }
68
87
 
69
- created = false
70
- cfn_stack = begin
71
- created = true
72
- create_stack options
88
+ stack_options[:on_failure] = options[:on_failure] if options[:on_failure]
89
+ stack_options[:timeout_in_minutes] = options[:timeout] if options[:timeout]
90
+
91
+ stack_options.merge! parse_stack_policy(:stack_policy, options[:stack_policy])
92
+ stack_options.merge! parse_stack_policy(:stack_policy_during_update, options[:stack_policy_during_update])
93
+
94
+ cfn_stack =
95
+ begin
96
+ create_stack stack_options.merge parameters: create_params
73
97
  rescue Cfer::Util::StackExistsError
74
- update_stack options
98
+ update_stack stack_options.merge parameters: update_params
75
99
  end
76
100
 
77
101
  flush_cache
@@ -82,13 +106,13 @@ module Cfer::Cfn
82
106
  # @param options [Hash] The options hash
83
107
  # @option options [Fixnum] :number The maximum number of already-existing CloudFormation events to yield.
84
108
  # @option options [Boolean] :follow Set to true to wait until the stack enters a `COMPLETE` or `FAILED` state, yielding events as they occur.
85
- def tail(options = {}, &block)
109
+ def tail(options = {})
86
110
  q = []
87
111
  event_id_highwater = nil
88
112
  counter = 0
89
113
  number = options[:number] || 0
90
- for_each_event name do |event|
91
- q.unshift event if counter < number
114
+ for_each_event name do |fetched_event|
115
+ q.unshift fetched_event if counter < number
92
116
  counter = counter + 1
93
117
  end
94
118
 
@@ -105,13 +129,13 @@ module Cfer::Cfn
105
129
  running = running && (/.+_(COMPLETE|FAILED)$/.match(stack_status) == nil)
106
130
 
107
131
  yielding = true
108
- for_each_event name do |event|
109
- if event_id_highwater == event.event_id
132
+ for_each_event name do |fetched_event|
133
+ if event_id_highwater == fetched_event.event_id
110
134
  yielding = false
111
135
  end
112
136
 
113
137
  if yielding
114
- q.unshift event
138
+ q.unshift fetched_event
115
139
  end
116
140
  end
117
141
 
@@ -127,7 +151,28 @@ module Cfer::Cfn
127
151
  end
128
152
 
129
153
  def fetch_stack(stack_name = @name)
130
- @stack_cache[stack_name] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
154
+ raise Cfer::Util::StackDoesNotExistError, 'Stack name must be specified' if stack_name == nil
155
+ begin
156
+ @stack_cache[stack_name] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
157
+ rescue Aws::CloudFormation::Errors::ValidationError => e
158
+ raise Cfer::Util::StackDoesNotExistError, e.message
159
+ end
160
+ end
161
+
162
+ def fetch_parameters(stack_name = @name)
163
+ @stack_parameters[stack_name] ||= cfn_list_to_hash('parameter', fetch_stack(stack_name)[:parameters])
164
+ end
165
+
166
+ def fetch_outputs(stack_name = @name)
167
+ @stack_outputs[stack_name] ||= cfn_list_to_hash('output', fetch_stack(stack_name)[:outputs])
168
+ end
169
+
170
+ def fetch_output(stack_name, output_name)
171
+ fetch_outputs(stack_name)[output_name] || raise(Cfer::Util::CferError, "Stack #{stack_name} has no output named `#{output_name}`")
172
+ end
173
+
174
+ def fetch_parameter(stack_name, param_name)
175
+ fetch_parameters(stack_name)[param_name] || raise(Cfer::Util::CferError, "Stack #{stack_name} has no parameter named `#{param_name}`")
131
176
  end
132
177
 
133
178
  def to_h
@@ -136,29 +181,58 @@ module Cfer::Cfn
136
181
 
137
182
  private
138
183
 
184
+ def cfn_list_to_hash(attribute, list)
185
+ key = :"#{attribute}_key"
186
+ value = :"#{attribute}_value"
187
+
188
+ Hash[ *list.map { |kv| [ kv[key].to_s, kv[value].to_s ] }.flatten ]
189
+ end
190
+
139
191
  def flush_cache
192
+ Cfer::LOGGER.debug "*********** FLUSH CACHE ***************"
193
+ Cfer::LOGGER.debug "Stack cache: #{@stack_cache}"
194
+ Cfer::LOGGER.debug "Stack parameters: #{@stack_parameters}"
195
+ Cfer::LOGGER.debug "Stack outputs: #{@stack_outputs}"
196
+ Cfer::LOGGER.debug "***************************************"
140
197
  @stack_cache = {}
198
+ @stack_parameters = {}
199
+ @stack_outputs = {}
141
200
  end
142
201
 
143
- def fetch_output(stack_name, output_name)
144
- stack = fetch_stack(stack_name)
145
-
146
- output = stack[:outputs].find do |o|
147
- o[:output_key] == output_name
202
+ def for_each_event(stack_name)
203
+ describe_stack_events(stack_name: stack_name).stack_events.each do |event|
204
+ yield event
148
205
  end
206
+ end
149
207
 
150
- if output
151
- output[:output_value]
152
- else
153
- raise CferError, "Stack #{stack_name} has no output value named `#{output_name}`"
154
- end
208
+ # Validates a string as json
209
+ #
210
+ # @param string [String]
211
+ def is_json?(string)
212
+ JSON.parse(string)
213
+ true
214
+ rescue JSON::ParserError
215
+ false
155
216
  end
156
217
 
157
- def for_each_event(stack_name)
158
- describe_stack_events(stack_name: stack_name).stack_events.each do |event|
159
- yield event
218
+ # Parses stack-policy-* options as an S3 URL, file to read, or JSON string
219
+ #
220
+ # @param name [String] Name of option: 'stack_policy' or 'stack_policy_during_update'
221
+ # @param value [String] String containing URL, filename or JSON string
222
+ # @return [Hash] Hash suitable for merging into options for create_stack or update_stack
223
+ def parse_stack_policy(name, value)
224
+ Cfer::LOGGER.debug "Using #{name} from: #{value}"
225
+ if value.nil?
226
+ {}
227
+ elsif value.match(/\A#{URI::regexp(%w[http https s3])}\z/) # looks like a URL
228
+ {"#{name}_url".to_sym => value}
229
+ elsif File.exist?(value) # looks like a file to read
230
+ {"#{name}_body".to_sym => File.read(value)}
231
+ elsif is_json?(value) # looks like a JSON string
232
+ {"#{name}_body".to_sym => value}
233
+ else # none of the above
234
+ raise Cfer::Util::CferError, "Stack policy must be an S3 url, a filename, or a valid json string"
160
235
  end
161
236
  end
162
237
  end
163
238
  end
164
-
@@ -26,7 +26,7 @@ module Cfer
26
26
  default: 'table'
27
27
  end
28
28
 
29
- desc 'converge [OPTIONS] <stack-name>', 'Converges a cloudformation stack according to the template'
29
+ desc 'converge [OPTIONS] <stack-name>', 'Create or update a cloudformation stack according to the template'
30
30
  #method_option :git_lock,
31
31
  # type: :boolean,
32
32
  # default: true,
@@ -34,7 +34,6 @@ module Cfer
34
34
 
35
35
  method_option :on_failure,
36
36
  type: :string,
37
- default: 'DELETE',
38
37
  desc: 'The action to take if the stack creation fails'
39
38
  method_option :follow,
40
39
  aliases: :f,
@@ -49,6 +48,17 @@ module Cfer
49
48
  aliases: :t,
50
49
  type: :string,
51
50
  desc: 'Override the stack filename (defaults to <stack-name>.rb)'
51
+ method_option :stack_policy,
52
+ aliases: :s,
53
+ type: :string,
54
+ desc: 'Set a new stack policy on create or update of the stack [file|url|json]'
55
+ method_option :stack_policy_during_update,
56
+ aliases: :u,
57
+ type: :string,
58
+ desc: 'Set a temporary overriding stack policy during an update [file|url|json]'
59
+ method_option :timeout,
60
+ type: :numeric,
61
+ desc: 'The timeout (in minutes) before the stack operation aborts'
52
62
  template_options
53
63
  stack_options
54
64
  def converge(stack_name)
@@ -112,7 +122,7 @@ module Cfer
112
122
  if Cfer::DEBUG
113
123
  Pry::rescued(e)
114
124
  else
115
- Cfer::Util.bug_report(e)
125
+ #Cfer::Util.bug_report(e)
116
126
  end
117
127
  exit 1
118
128
  end
@@ -135,4 +145,3 @@ module Cfer
135
145
 
136
146
 
137
147
  end
138
-
@@ -7,9 +7,5 @@ module Cfer::Core
7
7
  def tail(options = {}, &block)
8
8
  raise Cfer::Util::CferError, 'tail not implemented on this client'
9
9
  end
10
-
11
- def resolve(param)
12
- param
13
- end
14
10
  end
15
11
  end
@@ -28,8 +28,12 @@ module Cfer::Core::Fn
28
28
  {"Condition" => cond}
29
29
  end
30
30
 
31
- def and(conds)
32
- {"Fn::And" => [conds]}
31
+ def and(*conds)
32
+ {"Fn::And" => conds}
33
+ end
34
+
35
+ def or(*conds)
36
+ {"Fn::Or" => conds}
33
37
  end
34
38
 
35
39
  def equals(a, b)
@@ -41,11 +45,7 @@ module Cfer::Core::Fn
41
45
  end
42
46
 
43
47
  def not(cond)
44
- {"Fn::Not" => cond}
45
- end
46
-
47
- def or(conds)
48
- {"Fn::Or" => conds}
48
+ {"Fn::Not" => [cond]}
49
49
  end
50
50
 
51
51
  def get_azs(region)
@@ -1,6 +1,6 @@
1
1
  module Cfer::Cfn
2
2
  class Resource < Cfer::Block
3
- NON_PROXIED_METHODS = [:parameters, :options, :resolve]
3
+ NON_PROXIED_METHODS = [:parameters, :options, :lookup_output]
4
4
 
5
5
  def initialize(name, type, **options, &block)
6
6
  @name = name
@@ -5,7 +5,12 @@ module Cfer::Core
5
5
  include Cfer::Core
6
6
  include Cfer::Cfn
7
7
 
8
+ # The parameters strictly as passed via command line
9
+ attr_reader :input_parameters
10
+
11
+ # The fully resolved parameters, including defaults and parameters fetched from an existing stack during an update
8
12
  attr_reader :parameters
13
+
9
14
  attr_reader :options
10
15
 
11
16
  def converge!
@@ -20,10 +25,6 @@ module Cfer::Core
20
25
  end
21
26
  end
22
27
 
23
- def resolve(val)
24
- @options[:client] ? @options[:client].resolve(val) : val
25
- end
26
-
27
28
  def initialize(options = {})
28
29
  self[:AWSTemplateFormatVersion] = '2010-09-09'
29
30
  self[:Description] = ''
@@ -37,17 +38,23 @@ module Cfer::Core
37
38
  self[:Outputs] = {}
38
39
 
39
40
  @parameters = HashWithIndifferentAccess.new
41
+ @input_parameters = HashWithIndifferentAccess.new
42
+
43
+ if options[:client]
44
+ begin
45
+ @parameters.merge! options[:client].fetch_parameters
46
+ rescue Cfer::Util::StackDoesNotExistError
47
+ Cfer::LOGGER.debug "Can't include current stack parameters because the stack doesn't exist yet."
48
+ end
49
+ end
40
50
 
41
51
  if options[:parameters]
42
52
  options[:parameters].each do |key, val|
43
- @parameters[key] = resolve(val)
53
+ @input_parameters[key] = @parameters[key] = val
44
54
  end
45
55
  end
46
56
  end
47
57
 
48
- def pre_block
49
- end
50
-
51
58
  # Sets the description for this CloudFormation stack
52
59
  def description(desc)
53
60
  self[:Description] = desc
@@ -80,35 +87,12 @@ module Cfer::Core
80
87
  k = key.to_s.camelize.to_sym
81
88
  param[k] =
82
89
  case k
83
- when :AllowedValues
84
- str_list = v.join(',')
85
- verify_param(name, "Parameter #{name} must be one of: #{str_list}") { |input_val| str_list.include?(input_val) }
86
- str_list
87
90
  when :AllowedPattern
88
91
  if v.class == Regexp
89
- verify_param(name, "Parameter #{name} must match /#{v.source}/") { |input_val| v =~ input_val }
90
92
  v.source
91
- else
92
- verify_param(name, "Parameter #{name} must match /#{v}/") { |input_val| Regexp.new(v) =~ input_val }
93
- v
94
93
  end
95
- when :MaxLength
96
- verify_param(name, "Parameter #{name} must have length <= #{v}") { |input_val| input_val.length <= v.to_i }
97
- v
98
- when :MinLength
99
- verify_param(name, "Parameter #{name} must have length >= #{v}") { |input_val| input_val.length >= v.to_i }
100
- v
101
- when :MaxValue
102
- verify_param(name, "Parameter #{name} must be <= #{v}") { |input_val| input_val.to_i <= v.to_i }
103
- v
104
- when :MinValue
105
- verify_param(name, "Parameter #{name} must be >= #{v}") { |input_val| input_val.to_i >= v.to_i }
106
- v
107
- when :Description
108
- Preconditions.check_argument(v.length <= 4000, "#{key} must be <= 4000 characters")
109
- v
110
94
  when :Default
111
- @parameters[name] ||= resolve(v)
95
+ @parameters[name] ||= v
112
96
  end
113
97
  param[k] ||= v
114
98
  end
@@ -148,7 +132,7 @@ module Cfer::Core
148
132
  # @param name [String] The Logical ID of the output parameter
149
133
  # @param value [String] Value to return
150
134
  # @param options [Hash] Extra options for this output parameter
151
- # @option options [String] :Description Informationa bout the value
135
+ # @option options [String] :Description Information about the value
152
136
  def output(name, value, options = {})
153
137
  self[:Outputs][name] = options.merge('Value' => value)
154
138
  end
@@ -159,6 +143,10 @@ module Cfer::Core
159
143
  to_h.to_json
160
144
  end
161
145
 
146
+ def client
147
+ @options[:client] || raise(Cfer::Util::CferError, "Stack has no associated client.")
148
+ end
149
+
162
150
  # Includes template code from one or more files, and evals it in the context of this stack.
163
151
  # Filenames are relative to the file containing the invocation of this method.
164
152
  def include_template(*files)
@@ -170,9 +158,9 @@ module Cfer::Core
170
158
  end
171
159
  end
172
160
 
173
- private
174
- def verify_param(param_name, err_msg)
175
- raise Cfer::Util::CferError, err_msg if (@parameters[param_name] && !yield(@parameters[param_name].to_s))
161
+ def lookup_output(stack, out)
162
+ client = @options[:client] || raise(Cfer::Util::CferError, "Can not fetch stack outputs without a client")
163
+ client.fetch_output(stack, out)
176
164
  end
177
165
  end
178
166