cfer 0.2.0 → 0.3.0

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.
@@ -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