knife-cloudformation 0.1.4 → 0.1.6

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