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.
- checksums.yaml +5 -13
- data/.codeclimate.yml +25 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +1168 -0
- data/.travis.yml +14 -1
- data/Gemfile +2 -0
- data/README.md +48 -10
- data/Rakefile +6 -7
- data/cfer.gemspec +2 -1
- data/examples/chef_instance.rb +56 -0
- data/examples/common/instance_deps.rb +34 -0
- data/examples/instance.rb +3 -36
- data/examples/vpc.rb +1 -1
- data/lib/cfer.rb +42 -23
- data/lib/cfer/cfn/client.rb +124 -50
- data/lib/cfer/cli.rb +13 -4
- data/lib/cfer/core/client.rb +0 -4
- data/lib/cfer/core/fn.rb +7 -7
- data/lib/cfer/core/resource.rb +1 -1
- data/lib/cfer/core/stack.rb +24 -36
- data/lib/cfer/util/error.rb +3 -0
- data/lib/cfer/version.rb +1 -1
- data/lib/cferext/aws/auto_scaling/launch_configuration.rb +15 -0
- data/lib/cferext/aws/ec2/instance.rb +3 -0
- data/lib/cferext/provisioning.rb +12 -2
- data/lib/cferext/provisioning/cfn-bootstrap.rb +186 -0
- data/lib/cferext/provisioning/chef.rb +75 -0
- metadata +58 -36
- data/cfer-demo.gif +0 -0
data/lib/cfer/cfn/client.rb
CHANGED
@@ -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
|
-
|
49
|
-
|
50
|
-
cfn_param = resolve(cfn_param)
|
37
|
+
create_params = []
|
38
|
+
update_params = []
|
51
39
|
|
52
|
-
|
53
|
-
|
40
|
+
previous_parameters =
|
41
|
+
begin
|
42
|
+
fetch_parameters
|
43
|
+
rescue Cfer::Util::StackDoesNotExistError
|
44
|
+
nil
|
45
|
+
end
|
54
46
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
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 = {}
|
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 |
|
91
|
-
q.unshift
|
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 |
|
109
|
-
if event_id_highwater ==
|
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
|
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
|
-
|
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
|
144
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
data/lib/cfer/cli.rb
CHANGED
@@ -26,7 +26,7 @@ module Cfer
|
|
26
26
|
default: 'table'
|
27
27
|
end
|
28
28
|
|
29
|
-
desc 'converge [OPTIONS] <stack-name>', '
|
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
|
-
|
data/lib/cfer/core/client.rb
CHANGED
data/lib/cfer/core/fn.rb
CHANGED
@@ -28,8 +28,12 @@ module Cfer::Core::Fn
|
|
28
28
|
{"Condition" => cond}
|
29
29
|
end
|
30
30
|
|
31
|
-
def and(conds)
|
32
|
-
{"Fn::And" =>
|
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)
|
data/lib/cfer/core/resource.rb
CHANGED
data/lib/cfer/core/stack.rb
CHANGED
@@ -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] =
|
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] ||=
|
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
|
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
|
-
|
174
|
-
|
175
|
-
|
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
|
|