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