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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## v0.1.6
2
+ * Adds inspect action
3
+ * Updates to commons
4
+ * Allow multiple stack destroys at once
5
+ * Updates to options to make consistent
6
+
1
7
  ## v0.1.4
2
8
  * Support outputs on stack creation
3
9
  * Poll on destroy by default
@@ -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.12.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? && !args.include?(:ignore_empty_output))
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-]polling',
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][:polling] = val }
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
- stack_name = name_args.last
13
- ui.warn "Destroying Cloud Formation: #{ui.color(stack_name, :bold)}"
14
- ui.confirm 'Destroy this formation'
15
- destroy_formation!(stack_name)
16
- poll_stack(stack_name)
17
- ui.info " -> Destroyed Cloud Formation: #{ui.color(stack_name, :bold, :red)}"
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][:polling] = v }
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
- load_stack
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).describe_stacks('StackName' => name).body['Stacks'].first
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).describe_stack_resources('StackName' => name).body['StackResources']
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
- :template => template,
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
- def parameters
116
- unless(@memo[:parameters])
117
- @memo[:parameters] = Hash[*(
118
- @raw_stack['Parameters'].map do |ary|
119
- [ary['ParameterKey'], ary['ParameterValue']]
120
- end.flatten
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
- res.delete_if{|e| @memo[:events].include?(e['EventId'])}
152
- @memo[:events] += res.map{|e| e['EventId']}
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 = common.snake(kind)
263
+ kind_snake = snake(kind)
205
264
  aws = common.aws(kind_snake)
206
- aws.send("#{common.snake(resource['ResourceType'].split('::').last).to_s.split('_').last}s").get(resource['PhysicalResourceId'])
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
- as_resources = resources.find_all{|r|r['ResourceType'] == 'AWS::AutoScaling::AutoScalingGroup'}
211
- as_resources.map do |as_resource|
212
- as_group = expand_resource(as_resource)
213
- as_group.instances.map do |inst|
214
- common.aws(:ec2).servers.get(inst.id)
215
- end
216
- end.flatten
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
- if(type == :compute)
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 stack(name)
73
- unless(@memo[:stacks][name])
74
- @memo[:stacks][name] = Stack.new(name, self)
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)
@@ -2,5 +2,5 @@ module KnifeCloudformation
2
2
  class Version < Gem::Version
3
3
  end
4
4
 
5
- VERSION = Version.new('0.1.4')
5
+ VERSION = Version.new('0.1.6')
6
6
  end
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
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 00:00:00.000000000 Z
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.12.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.12.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