knife-cloudformation 0.1.4 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +6 -0
- data/knife-cloudformation.gemspec +2 -1
- data/lib/chef/knife/cloudformation_base.rb +2 -2
- data/lib/chef/knife/cloudformation_create.rb +3 -3
- data/lib/chef/knife/cloudformation_destroy.rb +23 -7
- data/lib/chef/knife/cloudformation_events.rb +3 -1
- data/lib/chef/knife/cloudformation_inspect.rb +18 -0
- data/lib/knife-cloudformation/aws_commons/stack.rb +90 -26
- data/lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb +1 -0
- data/lib/knife-cloudformation/aws_commons.rb +51 -5
- data/lib/knife-cloudformation/version.rb +1 -1
- metadata +20 -4
data/CHANGELOG.md
CHANGED
@@ -10,7 +10,8 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.description = 'Knife tooling for Cloud Formation'
|
11
11
|
s.require_path = 'lib'
|
12
12
|
s.add_dependency 'chef'
|
13
|
-
s.add_dependency 'fog', '~> 1.
|
13
|
+
s.add_dependency 'fog', '~> 1.15'
|
14
|
+
s.add_dependency 'net-sftp'
|
14
15
|
s.add_dependency 'attribute_struct', '~> 0.1.6'
|
15
16
|
s.files = Dir['**/*']
|
16
17
|
end
|
@@ -53,8 +53,8 @@ module KnifeCloudformation
|
|
53
53
|
columns = allowed_attributes.size
|
54
54
|
output += aws.process(things, :flat => true, :attributes => allowed_attributes)
|
55
55
|
output.compact.flatten
|
56
|
-
if(output.empty?
|
57
|
-
ui.warn 'No information found'
|
56
|
+
if(output.empty?)
|
57
|
+
ui.warn 'No information found' unless args.include?(:ignore_empty_output)
|
58
58
|
else
|
59
59
|
ui.info "#{what.to_s.capitalize} for stack: #{ui.color(stack, :bold)}" if stack
|
60
60
|
ui.info "#{ui.list(output, :uneven_columns_across, columns)}"
|
@@ -59,11 +59,11 @@ class Chef
|
|
59
59
|
:proc => lambda {|val| Chef::Config[:knife][:cloudformation][:processing] = val }
|
60
60
|
)
|
61
61
|
option(:polling,
|
62
|
-
:long => '--[no-]
|
62
|
+
:long => '--[no-]poll',
|
63
63
|
:description => 'Enable stack event polling.',
|
64
64
|
:boolean => true,
|
65
65
|
:default => true,
|
66
|
-
:proc => lambda {|val| Chef::Config[:knife][:cloudformation][:
|
66
|
+
:proc => lambda {|val| Chef::Config[:knife][:cloudformation][:poll] = val }
|
67
67
|
)
|
68
68
|
option(:notifications,
|
69
69
|
:long => '--notification ARN',
|
@@ -158,7 +158,7 @@ class Chef
|
|
158
158
|
valid = false
|
159
159
|
until(valid)
|
160
160
|
default = Chef::Config[:knife][:cloudformation][:options][:parameters][k] || v['Default']
|
161
|
-
answer = ui.ask_question("#{k.split(/([A-Z]+[^A-Z]*)/).find_all{|s|!s.empty?}.join(' ')} ", :default => default)
|
161
|
+
answer = ui.ask_question("#{k.split(/([A-Z]+[^A-Z]*)/).find_all{|s|!s.empty?}.join(' ')}: ", :default => default)
|
162
162
|
validation = KnifeCloudformation::AwsCommons::Stack::ParameterValidator.validate(answer, v)
|
163
163
|
if(validation == true)
|
164
164
|
Chef::Config[:knife][:cloudformation][:options][:parameters][k] = answer
|
@@ -6,15 +6,31 @@ class Chef
|
|
6
6
|
|
7
7
|
include KnifeCloudformation::KnifeBase
|
8
8
|
|
9
|
-
banner 'knife cloudformation destroy NAME'
|
9
|
+
banner 'knife cloudformation destroy NAME [NAME]'
|
10
|
+
|
11
|
+
option(:polling,
|
12
|
+
:long => '--[no-]poll',
|
13
|
+
:description => 'Enable stack event polling.',
|
14
|
+
:boolean => true,
|
15
|
+
:default => true,
|
16
|
+
:proc => lambda {|val| Chef::Config[:knife][:cloudformation][:poll] = val }
|
17
|
+
)
|
10
18
|
|
11
19
|
def run
|
12
|
-
|
13
|
-
|
14
|
-
ui.
|
15
|
-
|
16
|
-
|
17
|
-
|
20
|
+
stacks = name_args.sort
|
21
|
+
plural = 's' if stacks.size > 1
|
22
|
+
ui.warn "Destroying Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold)}"
|
23
|
+
ui.confirm "Destroy formation#{plural}"
|
24
|
+
stacks.each do |stack_name|
|
25
|
+
destroy_formation!(stack_name)
|
26
|
+
ui.info "Destroy request sent for stack: #{ui.color(stack_name, :bold)}"
|
27
|
+
end
|
28
|
+
if(config[:polling])
|
29
|
+
stacks.each do |stack_name|
|
30
|
+
poll_stack(stack_name)
|
31
|
+
end
|
32
|
+
ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
|
33
|
+
end
|
18
34
|
end
|
19
35
|
|
20
36
|
def destroy_formation!(stack_name)
|
@@ -11,8 +11,10 @@ class Chef
|
|
11
11
|
option(:polling,
|
12
12
|
:short => '-p',
|
13
13
|
:long => '--[no-]poll',
|
14
|
+
:boolean => true,
|
15
|
+
:default => false,
|
14
16
|
:description => 'Poll events while stack status is "in progress"',
|
15
|
-
:proc => lambda {|v| Chef::Config[:knife][:cloudformation][:
|
17
|
+
:proc => lambda {|v| Chef::Config[:knife][:cloudformation][:poll] = v }
|
16
18
|
)
|
17
19
|
|
18
20
|
option(:attribute,
|
@@ -26,6 +26,13 @@ class Chef
|
|
26
26
|
}
|
27
27
|
)
|
28
28
|
|
29
|
+
option(:nodes,
|
30
|
+
:short => '-N',
|
31
|
+
:long => '--nodes',
|
32
|
+
:boolean => true,
|
33
|
+
:description => 'Display ec2 nodes of stack'
|
34
|
+
)
|
35
|
+
|
29
36
|
option(:ssh_user,
|
30
37
|
:short => '-x SSH_USER',
|
31
38
|
:long => '--ssh-user SSH_USER',
|
@@ -40,6 +47,17 @@ class Chef
|
|
40
47
|
if(config[:instance_failure])
|
41
48
|
do_instance_failure(stack_name)
|
42
49
|
end
|
50
|
+
if(config[:nodes])
|
51
|
+
do_node_list(stack_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def do_node_list(stack_name)
|
56
|
+
nodes = stack(stack_name).nodes.map do |n|
|
57
|
+
[n.id, n.public_ip_address]
|
58
|
+
end.flatten
|
59
|
+
ui.info "Nodes for stack: #{ui.color(stack_name, :bold)}"
|
60
|
+
ui.info "#{ui.list(nodes, :uneven_columns_across, 2)}"
|
43
61
|
end
|
44
62
|
|
45
63
|
def do_instance_failure(stack_name)
|
@@ -5,11 +5,18 @@ module KnifeCloudformation
|
|
5
5
|
class Stack
|
6
6
|
|
7
7
|
include KnifeCloudformation::Utils::JSON
|
8
|
+
include KnifeCloudformation::Utils::AnimalStrings
|
8
9
|
|
9
10
|
attr_reader :name, :raw_stack, :raw_resources, :common
|
10
11
|
|
11
12
|
class << self
|
12
13
|
|
14
|
+
ALLOWED_PARAMETER_ATTRIBUTES = %w(
|
15
|
+
Type Default NoEcho AllowedValues AllowedPattern
|
16
|
+
MaxLength MinLength MaxValue MinValue Description
|
17
|
+
ConstraintDescription
|
18
|
+
)
|
19
|
+
|
13
20
|
include KnifeCloudformation::Utils::JSON
|
14
21
|
|
15
22
|
def create(name, definition, aws_common)
|
@@ -26,6 +33,7 @@ module KnifeCloudformation
|
|
26
33
|
stack[format_key] = value
|
27
34
|
end
|
28
35
|
enable_capabilities!(stack, template)
|
36
|
+
clean_parameters!(template)
|
29
37
|
stack['TemplateBody'] = _to_json(template)
|
30
38
|
stack
|
31
39
|
end
|
@@ -40,13 +48,25 @@ module KnifeCloudformation
|
|
40
48
|
nil
|
41
49
|
end
|
42
50
|
|
51
|
+
def clean_parameters!(template)
|
52
|
+
template['Parameters'].each do |name, options|
|
53
|
+
options.delete_if do |attribute, value|
|
54
|
+
!ALLOWED_PARAMETER_ATTRIBUTES.include?(attribute)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
43
59
|
end
|
44
60
|
|
45
|
-
def initialize(name, common)
|
61
|
+
def initialize(name, common, raw_stack=nil)
|
46
62
|
@name = name
|
47
63
|
@common = common
|
48
64
|
@memo = {}
|
49
|
-
|
65
|
+
if(raw_stack)
|
66
|
+
@raw_stack = raw_stack
|
67
|
+
else
|
68
|
+
load_stack
|
69
|
+
end
|
50
70
|
@force_refresh = false
|
51
71
|
@force_refresh = in_progress?
|
52
72
|
end
|
@@ -54,11 +74,25 @@ module KnifeCloudformation
|
|
54
74
|
## Actions ##
|
55
75
|
|
56
76
|
def update(definition)
|
77
|
+
if(definition.keys.detect{|k|k.is_a?(Symbol)})
|
78
|
+
definition = format_definition(definition)
|
79
|
+
end
|
57
80
|
res = common.aws(:cloud_formation).update_stack(name, definition)
|
58
81
|
reload!
|
59
82
|
res
|
60
83
|
end
|
61
84
|
|
85
|
+
def format_definition(def_hash)
|
86
|
+
new_hash = {}
|
87
|
+
def_hash.each do |k,v|
|
88
|
+
new_hash[camel(k)] = v
|
89
|
+
end
|
90
|
+
if(new_hash['TemplateBody'].is_a?(Hash))
|
91
|
+
new_hash['TemplateBody'] = _to_json(new_hash['TemplateBody'])
|
92
|
+
end
|
93
|
+
new_hash
|
94
|
+
end
|
95
|
+
|
62
96
|
def destroy
|
63
97
|
res = common.aws(:cloud_formation).delete_stack(name)
|
64
98
|
reload!
|
@@ -66,14 +100,18 @@ module KnifeCloudformation
|
|
66
100
|
end
|
67
101
|
|
68
102
|
def load_stack
|
69
|
-
@raw_stack = common.aws(:cloud_formation)
|
103
|
+
@raw_stack = common.aws(:cloud_formation)
|
104
|
+
.describe_stacks('StackName' => name)
|
105
|
+
.body['Stacks'].first
|
70
106
|
end
|
71
107
|
|
72
108
|
def load_resources
|
73
|
-
@raw_resources = common.aws(:cloud_formation)
|
109
|
+
@raw_resources = common.aws(:cloud_formation)
|
110
|
+
.describe_stack_resources('StackName' => name)
|
111
|
+
.body['StackResources']
|
74
112
|
end
|
75
113
|
|
76
|
-
def refresh?(bool)
|
114
|
+
def refresh?(bool=false)
|
77
115
|
bool || (bool.nil? && @force_refresh)
|
78
116
|
end
|
79
117
|
|
@@ -93,7 +131,7 @@ module KnifeCloudformation
|
|
93
131
|
|
94
132
|
def to_hash(extra_data={})
|
95
133
|
{
|
96
|
-
:
|
134
|
+
:template_body => template,
|
97
135
|
:parameters => parameters,
|
98
136
|
:capabilities => capabilities,
|
99
137
|
:disable_rollback => disable_rollback,
|
@@ -112,15 +150,20 @@ module KnifeCloudformation
|
|
112
150
|
@memo[:template]
|
113
151
|
end
|
114
152
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
153
|
+
## Stack metadata ##
|
154
|
+
def parameters(raw=false)
|
155
|
+
if(raw)
|
156
|
+
@raw_stack['Parameters']
|
157
|
+
else
|
158
|
+
unless(@memo[:parameters])
|
159
|
+
@memo[:parameters] = Hash[*(
|
160
|
+
@raw_stack['Parameters'].map do |ary|
|
161
|
+
[ary['ParameterKey'], ary['ParameterValue']]
|
162
|
+
end.flatten
|
163
|
+
)]
|
164
|
+
end
|
165
|
+
@memo[:parameters]
|
122
166
|
end
|
123
|
-
@memo[:parameters]
|
124
167
|
end
|
125
168
|
|
126
169
|
def capabilities
|
@@ -135,6 +178,21 @@ module KnifeCloudformation
|
|
135
178
|
@raw_stack['NotificationARNs']
|
136
179
|
end
|
137
180
|
|
181
|
+
def timeout_in_minutes
|
182
|
+
@raw_stack['TimeoutInMinutes']
|
183
|
+
end
|
184
|
+
alias_method :timeout_in_minutes, :timeout
|
185
|
+
|
186
|
+
def stack_id
|
187
|
+
@raw_stack['StackId']
|
188
|
+
end
|
189
|
+
alias_method :id, :stack_id
|
190
|
+
|
191
|
+
def creation_time
|
192
|
+
@raw_stack['CreationTime']
|
193
|
+
end
|
194
|
+
alias_method :created_at, :creation_time
|
195
|
+
|
138
196
|
def status(force_refresh=nil)
|
139
197
|
load_stack if refresh?(force_refresh)
|
140
198
|
@raw_stack['StackStatus']
|
@@ -148,10 +206,11 @@ module KnifeCloudformation
|
|
148
206
|
def events(all=false)
|
149
207
|
res = common.aws(:cloud_formation).describe_stack_events(name).body['StackEvents']
|
150
208
|
@memo[:events] ||= []
|
151
|
-
|
152
|
-
|
209
|
+
current = @memo[:events].map{|e| e['EventId']}
|
210
|
+
res.delete_if{|e| current.include?(e['EventId'])}
|
211
|
+
@memo[:events] += res
|
153
212
|
@memo[:events].uniq!
|
154
|
-
res
|
213
|
+
all ? @memo[:events] : res
|
155
214
|
end
|
156
215
|
|
157
216
|
def outputs(style=:unformatted)
|
@@ -201,19 +260,24 @@ module KnifeCloudformation
|
|
201
260
|
|
202
261
|
def expand_resource(resource)
|
203
262
|
kind = resource['ResourceType'].split('::')[1]
|
204
|
-
kind_snake =
|
263
|
+
kind_snake = snake(kind)
|
205
264
|
aws = common.aws(kind_snake)
|
206
|
-
aws.send("#{
|
265
|
+
aws.send("#{snake(resource['ResourceType'].split('::').last).to_s.split('_').last}s").get(resource['PhysicalResourceId'])
|
207
266
|
end
|
208
267
|
|
209
268
|
def nodes
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
269
|
+
reload! if refresh?
|
270
|
+
unless(@memo[:nodes])
|
271
|
+
as_resources = resources.find_all{|r|r['ResourceType'] == 'AWS::AutoScaling::AutoScalingGroup'}
|
272
|
+
@memo[:nodes] =
|
273
|
+
as_resources.map do |as_resource|
|
274
|
+
as_group = expand_resource(as_resource)
|
275
|
+
as_group.instances.map do |inst|
|
276
|
+
common.aws(:ec2).servers.get(inst.id)
|
277
|
+
end
|
278
|
+
end.flatten
|
279
|
+
end
|
280
|
+
@memo[:nodes]
|
217
281
|
end
|
218
282
|
|
219
283
|
end
|
@@ -11,6 +11,7 @@ module KnifeCloudformation
|
|
11
11
|
include KnifeCloudformation::Utils::AnimalStrings
|
12
12
|
|
13
13
|
def validate(value, parameter_definition)
|
14
|
+
return [[:blank, 'Value cannot be blank']] if value.to_s.strip.empty?
|
14
15
|
result = %w(AllowedValues AllowedPattern MaxLength MinLength MaxValue MinValue).map do |key|
|
15
16
|
if(parameter_definition[key])
|
16
17
|
res = self.send(snake(key), value, parameter_definition)
|
@@ -23,12 +23,25 @@ module KnifeCloudformation
|
|
23
23
|
}
|
24
24
|
end
|
25
25
|
|
26
|
+
def clear_cache(*types)
|
27
|
+
keys = types.empty? ? @memo.keys : types.map(&:to_sym)
|
28
|
+
keys.each do |key|
|
29
|
+
@memo[key].clear if @memo[key]
|
30
|
+
end
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
26
34
|
def build_connection(type)
|
27
35
|
type = type.to_sym
|
28
36
|
type = FOG_MAP[type] if FOG_MAP[type]
|
29
37
|
unless(@connections[type])
|
30
|
-
|
38
|
+
case type
|
39
|
+
when :compute
|
31
40
|
@connections[:compute] = Fog::Compute::AWS.new(@creds)
|
41
|
+
when :dns
|
42
|
+
dns_creds = @creds.dup
|
43
|
+
dns_creds.delete(:region) || dns_creds.delete('region')
|
44
|
+
@connections[:dns] = Fog::DNS::AWS.new(dns_creds)
|
32
45
|
else
|
33
46
|
Fog.credentials = Fog.symbolize_credentials(@creds)
|
34
47
|
@connections[type] = Fog::AWS[type]
|
@@ -69,11 +82,44 @@ module KnifeCloudformation
|
|
69
82
|
@memo[:stack_list][key]
|
70
83
|
end
|
71
84
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
85
|
+
def name_from_stack_id(name)
|
86
|
+
found = stacks.detect do |s|
|
87
|
+
s['StackId'] == name
|
88
|
+
end
|
89
|
+
if(found)
|
90
|
+
s['StackName']
|
91
|
+
else
|
92
|
+
raise "Failed to locate stack with ID: #{name}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def stack(*names)
|
97
|
+
names = names.map do |name|
|
98
|
+
if(name.start_with?('arn:'))
|
99
|
+
name_from_stack_id(name)
|
100
|
+
else
|
101
|
+
name
|
102
|
+
end
|
103
|
+
end
|
104
|
+
if(names.size == 1)
|
105
|
+
name = names.first
|
106
|
+
unless(@memo[:stacks][name])
|
107
|
+
@memo[:stacks][name] = Stack.new(name, self)
|
108
|
+
end
|
109
|
+
@memo[:stacks][name]
|
110
|
+
else
|
111
|
+
to_fetch = names - @memo[:stacks].keys
|
112
|
+
slim_stacks = {}
|
113
|
+
unless(to_fetch.empty?)
|
114
|
+
to_fetch.each do |name|
|
115
|
+
slim_stacks[name] = Stack.new(name, self, stacks.detect{|s| s['StackName'] == name})
|
116
|
+
end
|
117
|
+
end
|
118
|
+
result = names.map do |n|
|
119
|
+
@memo[:stacks][n] || slim_stacks[n]
|
120
|
+
end
|
121
|
+
result
|
75
122
|
end
|
76
|
-
@memo[:stacks][name]
|
77
123
|
end
|
78
124
|
|
79
125
|
def create_stack(name, definition)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: knife-cloudformation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-09-
|
12
|
+
date: 2013-09-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: chef
|
@@ -34,7 +34,7 @@ dependencies:
|
|
34
34
|
requirements:
|
35
35
|
- - ~>
|
36
36
|
- !ruby/object:Gem::Version
|
37
|
-
version: 1.
|
37
|
+
version: '1.15'
|
38
38
|
type: :runtime
|
39
39
|
prerelease: false
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -42,7 +42,23 @@ dependencies:
|
|
42
42
|
requirements:
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: 1.
|
45
|
+
version: '1.15'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-sftp
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
name: attribute_struct
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|