knife-cloudformation 0.1.2 → 0.1.4

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.
@@ -1,3 +1,9 @@
1
+ ## v0.1.4
2
+ * Support outputs on stack creation
3
+ * Poll on destroy by default
4
+ * Add inspection helper for failed node inspection
5
+ * Refactor AWS interactions to common library
6
+
1
7
  ## v0.1.2
2
8
  * Update dependency restriction to get later version
3
9
 
@@ -1,85 +1,15 @@
1
1
  require 'chef/knife'
2
+ require 'knife-cloudformation/utils'
3
+ require 'knife-cloudformation/aws_commons'
2
4
 
3
- class Chef
4
- class Knife
5
+ module KnifeCloudformation
5
6
 
6
- # Populate up the hashes so they are available for knife config
7
- # with issues of nils
8
- ['knife.cloudformation.credentials', 'knife.cloudformation.options'].each do |stack|
9
- stack.split('.').inject(Chef::Config) do |memo, item|
10
- memo[item.to_sym] = Mash.new unless memo[item.to_sym]
11
- memo[item.to_sym]
12
- end
13
- end
14
-
15
- Chef::Config[:knife][:cloudformation] ||= Mash.new
16
-
17
- module CloudformationDefault
18
- class << self
19
- def included(klass)
20
- klass.instance_eval do
21
- deps do
22
- require 'fog'
23
- Chef::Config[:knife][:cloudformation] ||= Mash.new
24
- Chef::Config[:knife][:cloudformation][:credentials] ||= Mash.new
25
- Chef::Config[:knife][:cloudformation][:options] ||= Mash.new
26
- end
27
-
28
- option(:key,
29
- :short => '-K KEY',
30
- :long => '--key KEY',
31
- :description => 'AWS access key id',
32
- :proc => lambda {|val|
33
- Chef::Config[:knife][:cloudformation][:credentials][:key] = val
34
- }
35
- )
36
- option(:secret,
37
- :short => '-S SECRET',
38
- :long => '--secret SECRET',
39
- :description => 'AWS secret access key',
40
- :proc => lambda {|val|
41
- Chef::Config[:knife][:cloudformation][:credentials][:secret] = val
42
- }
43
- )
44
- option(:region,
45
- :short => '-r REGION',
46
- :long => '--region REGION',
47
- :description => 'AWS region',
48
- :proc => lambda {|val|
49
- Chef::Config[:knife][:cloudformation][:credentials][:region] = val
50
- }
51
- )
52
- end
53
- end
54
- end
55
- end
56
-
57
- class CloudformationBase < Knife
58
-
59
- class << self
60
- def aws_con
61
- @connection ||= Fog::AWS::CloudFormation.new(
62
- :aws_access_key_id => _key,
63
- :aws_secret_access_key => _secret,
64
- :region => _region
65
- )
66
- end
67
-
68
- def _key
69
- Chef::Config[:knife][:cloudformation][:credentials][:key] ||
70
- Chef::Config[:knife][:aws_access_key_id]
71
- end
72
-
73
- def _secret
74
- Chef::Config[:knife][:cloudformation][:credentials][:secret] ||
75
- Chef::Config[:knife][:aws_secret_access_key]
76
- end
7
+ module KnifeBase
77
8
 
78
- def _region
79
- Chef::Config[:knife][:cloudformation][:credentials][:region] ||
80
- Chef::Config[:knife][:region]
81
- end
9
+ module InstanceMethods
82
10
 
11
+ def aws
12
+ self.class.con(ui)
83
13
  end
84
14
 
85
15
  def _debug(e, *args)
@@ -91,43 +21,8 @@ class Chef
91
21
  end
92
22
  end
93
23
 
94
- def aws_con
95
- self.class.aws_con
96
- end
97
-
98
- def stack_status(name)
99
- aws_con.describe_stacks('StackName' => name).body['Stacks'].first['StackStatus']
100
- end
101
-
102
- def get_titles(thing, format=false)
103
- unless(@titles)
104
- attrs = allowed_attributes
105
- if(attrs.empty?)
106
- hash = thing.is_a?(Array) ? thing.first : thing
107
- hash ||= {}
108
- attrs = hash.keys
109
- end
110
- @titles = attrs.map do |key|
111
- next unless attribute_allowed?(key)
112
- key.gsub(/([a-z])([A-Z])/, '\1 \2')
113
- end.compact
114
- end
115
- if(format)
116
- @titles.map{|s| ui.color(s, :bold)}
117
- else
118
- @titles
119
- end
120
- end
121
-
122
- def process(things)
123
- @event_ids ||= []
124
- things.reverse.map do |thing|
125
- next if @event_ids.include?(thing['EventId'])
126
- @event_ids.push(thing['EventId']).compact!
127
- get_titles(thing).map do |key|
128
- thing[key.gsub(' ', '')].to_s
129
- end
130
- end.flatten.compact
24
+ def stack(name)
25
+ self.class.con(ui).stack(name)
131
26
  end
132
27
 
133
28
  def allowed_attributes
@@ -142,15 +37,27 @@ class Chef
142
37
  config[:all_attributes] || allowed_attributes.include?(attr)
143
38
  end
144
39
 
145
- def things_output(stack, things, what)
146
- output = get_titles(things, :format)
147
- output += process(things)
40
+ def poll_stack(name)
41
+ knife_events = Chef::Knife::CloudformationEvents.new
42
+ knife_events.name_args.push(name)
43
+ Chef::Config[:knife][:cloudformation][:poll] = true
44
+ knife_events.run
45
+ end
46
+
47
+ def things_output(stack, things, what, *args)
48
+ unless(args.include?(:no_title))
49
+ output = aws.get_titles(things, :format => true, :attributes => allowed_attributes)
50
+ else
51
+ output = []
52
+ end
53
+ columns = allowed_attributes.size
54
+ output += aws.process(things, :flat => true, :attributes => allowed_attributes)
148
55
  output.compact.flatten
149
- if(output.empty?)
56
+ if(output.empty? && !args.include?(:ignore_empty_output))
150
57
  ui.warn 'No information found'
151
58
  else
152
59
  ui.info "#{what.to_s.capitalize} for stack: #{ui.color(stack, :bold)}" if stack
153
- ui.info "#{ui.list(output, :uneven_columns_across, get_titles(things).size)}\n"
60
+ ui.info "#{ui.list(output, :uneven_columns_across, columns)}"
154
61
  end
155
62
  end
156
63
 
@@ -165,38 +72,97 @@ class Chef
165
72
  end
166
73
  end
167
74
 
168
- def try_json_compat
169
- begin
170
- require 'chef/json_compat'
171
- rescue
75
+ end
76
+
77
+ module ClassMethods
78
+
79
+ def con(ui=nil)
80
+ unless(@common)
81
+ @common = KnifeCloudformation::AwsCommons.new(
82
+ :ui => ui,
83
+ :fog => {
84
+ :aws_access_key_id => _key,
85
+ :aws_secret_access_key => _secret,
86
+ :region => _region
87
+ }
88
+ )
172
89
  end
173
- defined?(Chef::JSONCompat)
90
+ @common
174
91
  end
175
-
176
- def _to_json(thing)
177
- if(try_json_compat)
178
- Chef::JSONCompat.to_json(thing)
179
- else
180
- JSON.dump(thing)
181
- end
92
+
93
+ def _key
94
+ Chef::Config[:knife][:cloudformation][:credentials][:key] ||
95
+ Chef::Config[:knife][:aws_access_key_id]
182
96
  end
183
97
 
184
- def _from_json(thing)
185
- if(try_json_compat)
186
- Chef::JSONCompat.from_json(thing)
187
- else
188
- JSON.read(thing)
189
- end
98
+ def _secret
99
+ Chef::Config[:knife][:cloudformation][:credentials][:secret] ||
100
+ Chef::Config[:knife][:aws_secret_access_key]
190
101
  end
191
102
 
192
- def _format_json(thing)
193
- thing = _from_json(thing) if thing.is_a?(String)
194
- if(try_json_compat)
195
- Chef::JSONCompat.to_json_pretty(thing)
196
- else
197
- JSON.pretty_generate(thing)
103
+ def _region
104
+ Chef::Config[:knife][:cloudformation][:credentials][:region] ||
105
+ Chef::Config[:knife][:region]
106
+ end
107
+
108
+ end
109
+
110
+ class << self
111
+ def included(klass)
112
+ klass.instance_eval do
113
+
114
+ extend KnifeCloudformation::KnifeBase::ClassMethods
115
+ include KnifeCloudformation::KnifeBase::InstanceMethods
116
+ include KnifeCloudformation::Utils::JSON
117
+ include KnifeCloudformation::Utils::AnimalStrings
118
+
119
+ deps do
120
+ require 'fog'
121
+ Chef::Config[:knife][:cloudformation] ||= Mash.new
122
+ Chef::Config[:knife][:cloudformation][:credentials] ||= Mash.new
123
+ Chef::Config[:knife][:cloudformation][:options] ||= Mash.new
124
+ end
125
+
126
+ option(:key,
127
+ :short => '-K KEY',
128
+ :long => '--key KEY',
129
+ :description => 'AWS access key id',
130
+ :proc => lambda {|val|
131
+ Chef::Config[:knife][:cloudformation][:credentials][:key] = val
132
+ }
133
+ )
134
+ option(:secret,
135
+ :short => '-S SECRET',
136
+ :long => '--secret SECRET',
137
+ :description => 'AWS secret access key',
138
+ :proc => lambda {|val|
139
+ Chef::Config[:knife][:cloudformation][:credentials][:secret] = val
140
+ }
141
+ )
142
+ option(:region,
143
+ :short => '-r REGION',
144
+ :long => '--region REGION',
145
+ :description => 'AWS region',
146
+ :proc => lambda {|val|
147
+ Chef::Config[:knife][:cloudformation][:credentials][:region] = val
148
+ }
149
+ )
150
+
151
+
152
+ # Populate up the hashes so they are available for knife config
153
+ # with issues of nils
154
+ ['knife.cloudformation.credentials', 'knife.cloudformation.options'].each do |stack|
155
+ stack.split('.').inject(Chef::Config) do |memo, item|
156
+ memo[item.to_sym] = Mash.new unless memo[item.to_sym]
157
+ memo[item.to_sym]
158
+ end
159
+ end
160
+
161
+ Chef::Config[:knife][:cloudformation] ||= Mash.new
162
+
198
163
  end
199
164
  end
200
165
  end
201
166
  end
167
+
202
168
  end
@@ -3,7 +3,9 @@ require 'chef/knife/cloudformation_base'
3
3
 
4
4
  class Chef
5
5
  class Knife
6
- class CloudformationCreate < CloudformationBase
6
+ class CloudformationCreate < Knife
7
+
8
+ include KnifeCloudformation::KnifeBase
7
9
 
8
10
  banner 'knife cloudformation create NAME'
9
11
 
@@ -13,7 +15,7 @@ class Chef
13
15
  klass.class_eval do
14
16
 
15
17
  attr_accessor :action_type
16
-
18
+
17
19
  option(:parameter,
18
20
  :short => '-p KEY:VALUE',
19
21
  :long => '--parameter KEY:VALUE',
@@ -32,11 +34,13 @@ class Chef
32
34
  Chef::Config[:knife][:cloudformation][:options][:timeout_in_minutes] = val
33
35
  }
34
36
  )
35
- option(:disable_rollback,
37
+ option(:rollback,
36
38
  :short => '-R',
37
- :long => '--disable-rollback',
38
- :description => 'Disable rollback on stack creation failure',
39
- :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:options][:disable_rollback] = true }
39
+ :long => '--[no]-rollback',
40
+ :description => 'Rollback on stack creation failure',
41
+ :boolean => true,
42
+ :default => true,
43
+ :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:options][:disable_rollback] = !val }
40
44
  )
41
45
  option(:capability,
42
46
  :short => '-C CAPABILITY',
@@ -47,15 +51,19 @@ class Chef
47
51
  Chef::Config[:knife][:cloudformation][:options][:capabilities].push(val).uniq!
48
52
  }
49
53
  )
50
- option(:enable_processing,
51
- :long => '--enable-processing',
52
- :description => 'Call the unicorns.',
53
- :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:enable_processing] = true }
54
+ option(:processing,
55
+ :long => '--[no-]processing',
56
+ :description => 'Call the unicorns and explode the glitter bombs',
57
+ :boolean => true,
58
+ :default => false,
59
+ :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:processing] = val }
54
60
  )
55
- option(:disable_polling,
56
- :long => '--disable-polling',
57
- :description => 'Disable stack even polling.',
58
- :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:disable_polling] = true }
61
+ option(:polling,
62
+ :long => '--[no-]polling',
63
+ :description => 'Enable stack event polling.',
64
+ :boolean => true,
65
+ :default => true,
66
+ :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:polling] = val }
59
67
  )
60
68
  option(:notifications,
61
69
  :long => '--notification ARN',
@@ -73,23 +81,30 @@ class Chef
73
81
  Chef::Config[:knife][:cloudformation][:file] = val
74
82
  }
75
83
  )
76
- option(:disable_interactive_parameters,
77
- :long => '--no-parameter-prompts',
78
- :description => 'Do not prompt for input on dynamic parameters'
84
+ option(:interactive_parameters,
85
+ :long => '--[no-]parameter-prompts',
86
+ :boolean => true,
87
+ :default => true,
88
+ :description => 'Do not prompt for input on dynamic parameters',
89
+ :default => true
79
90
  )
80
-
81
91
  option(:print_only,
82
92
  :long => '--print-only',
83
93
  :description => 'Print template and exit'
84
94
  )
95
+
96
+ %w(rollback polling interactive_parameters).each do |key|
97
+ if(Chef::Config[:knife][:cloudformation][key].nil?)
98
+ Chef::Config[:knife][:cloudformation][key] = true
99
+ end
100
+ end
85
101
  end
86
102
  end
87
103
  end
88
104
  end
89
105
 
90
- include CloudformationDefault
91
106
  include Options
92
-
107
+
93
108
  def run
94
109
  @action_type = self.class.name.split('::').last.sub('Cloudformation', '').upcase
95
110
  unless(File.exists?(Chef::Config[:knife][:cloudformation][:file].to_s))
@@ -97,69 +112,46 @@ class Chef
97
112
  exit 1
98
113
  end
99
114
  name = name_args.first
100
- if(Chef::Config[:knife][:cloudformation][:enable_processing])
115
+ if(Chef::Config[:knife][:cloudformation][:processing])
101
116
  file = KnifeCloudformation::SparkleFormation.compile(Chef::Config[:knife][:cloudformation][:file])
102
117
  else
103
118
  file = _from_json(File.read(Chef::Config[:knife][:cloudformation][:file]))
104
119
  end
105
- ui.info "#{ui.color('Cloud Formation: ', :bold)} #{ui.color(action_type, :green)}"
106
- ui.info " -> #{ui.color('Name:', :bold)} #{name} #{ui.color('Path:', :bold)} #{Chef::Config[:knife][:cloudformation][:file]} #{ui.color('(not pre-processed)', :yellow) if Chef::Config[:knife][:cloudformation][:disable_processing]}"
107
- stack = build_stack(file)
108
120
  if(config[:print_only])
109
121
  ui.warn 'Print only requested'
110
- ui.info _format_json(stack['TemplateBody'])
122
+ ui.info _format_json(file)
111
123
  exit 1
112
124
  end
113
- create_stack(name, stack)
114
- unless(Chef::Config[:knife][:cloudformation][:disable_polling])
125
+ ui.info "#{ui.color('Cloud Formation: ', :bold)} #{ui.color(action_type, :green)}"
126
+ ui.info " -> #{ui.color('Name:', :bold)} #{name} #{ui.color('Path:', :bold)} #{Chef::Config[:knife][:cloudformation][:file]} #{ui.color('(not pre-processed)', :yellow) if Chef::Config[:knife][:cloudformation][:disable_processing]}"
127
+ populate_parameters!(file)
128
+ stack_def = KnifeCloudformation::AwsCommons::Stack.build_stack_definition(file, Chef::Config[:knife][:cloudformation][:options])
129
+ aws.create_stack(name, stack_def)
130
+ if(Chef::Config[:knife][:cloudformation][:polling])
115
131
  poll_stack(name)
116
- else
117
- ui.warn 'Stack state polling has been disabled.'
118
- end
119
- ui.info "Stack #{action_type} complete: #{ui.color('SUCCESS', :green)}"
120
- end
121
-
122
- def build_stack(template)
123
- stack = Mash.new
124
- populate_parameters!(template)
125
- Chef::Config[:knife][:cloudformation][:options].each do |key, value|
126
- format_key = key.split('_').map(&:capitalize).join
127
- stack[format_key] = value
128
- =begin
129
- case value
130
- when Hash && key.to_sym != :parameters
131
- i = 1
132
- value.each do |k, v|
133
- stack["#{format_key}.member.#{i}.#{format_key[0, (format_key.length - 1)]}Key"] = k
134
- stack["#{format_key}.member.#{i}.#{format_key[0, (format_key.length - 1)]}Value"] = v
135
- end
136
- when Array
137
- value.each_with_index do |v, i|
138
- stack["#{format_key}.member.#{i+1}"] = v
139
- end
132
+ if(stack(name).success?)
133
+ ui.info "Stack #{action_type} complete: #{ui.color('SUCCESS', :green)}"
134
+ knife_output = Chef::Knife::CloudformationDescribe.new
135
+ knife_output.name_args.push(name)
136
+ knife_output.config[:outputs] = true
137
+ knife_output.run
140
138
  else
141
-
139
+ ui.fatal "#{action_type} of new stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
140
+ ui.info ""
141
+ knife_inspect = Chef::Knife::CloudformationInspect.new
142
+ knife_inspect.name_args.push(name)
143
+ knife_inspect.config[:instance_failure] = true
144
+ knife_inspect.run
145
+ exit 1
142
146
  end
143
- =end
147
+ else
148
+ ui.warn 'Stack state polling has been disabled.'
149
+ ui.info "Stack creation initialized for #{ui.color(name, :green)}"
144
150
  end
145
- enable_capabilities!(stack, template)
146
- stack['TemplateBody'] = Chef::JSONCompat.to_json(template)
147
- stack
148
151
  end
149
152
 
150
- # Currently only checking for IAM resources since that's all
151
- # that is supported for creation
152
- def enable_capabilities!(stack, template)
153
- found = Array(template['Resources']).detect do |resource_name, resource|
154
- resource['Type'].start_with?('AWS::IAM')
155
- end
156
- if(found)
157
- stack['Capabilities'] = ['CAPABILITY_IAM']
158
- end
159
- end
160
-
161
153
  def populate_parameters!(stack)
162
- unless(config[:disable_interactive_parameters])
154
+ if(Chef::Config[:knife][:cloudformation][:interactive_parameters])
163
155
  if(stack['Parameters'])
164
156
  Chef::Config[:knife][:cloudformation][:options][:parameters] ||= Mash.new
165
157
  stack['Parameters'].each do |k,v|
@@ -167,50 +159,21 @@ class Chef
167
159
  until(valid)
168
160
  default = Chef::Config[:knife][:cloudformation][:options][:parameters][k] || v['Default']
169
161
  answer = ui.ask_question("#{k.split(/([A-Z]+[^A-Z]*)/).find_all{|s|!s.empty?}.join(' ')} ", :default => default)
170
- if(v['AllowedValues'])
171
- valid = v['AllowedValues'].include?(answer)
172
- else
173
- valid = true
174
- end
175
- if(valid)
162
+ validation = KnifeCloudformation::AwsCommons::Stack::ParameterValidator.validate(answer, v)
163
+ if(validation == true)
176
164
  Chef::Config[:knife][:cloudformation][:options][:parameters][k] = answer
165
+ valid = true
177
166
  else
178
- ui.error "Not an allowed value: #{v['AllowedValues'].join(', ')}"
167
+ validation.each do |validation_error|
168
+ ui.error validation_error.last
169
+ end
179
170
  end
180
171
  end
181
172
  end
182
173
  end
183
174
  end
184
175
  end
185
-
186
- def create_stack(name, stack)
187
- begin
188
- res = aws_con.create_stack(name, stack)
189
- rescue => e
190
- ui.fatal "Failed to #{action_type} stack #{name}. Reason: #{e}"
191
- _debug(e, "Generated template used:\n#{_format_json(stack['TemplateBody'])}")
192
- exit 1
193
- end
194
- end
195
176
 
196
- def poll_stack(name)
197
- knife_events = Chef::Knife::CloudformationEvents.new
198
- knife_events.name_args.push(name)
199
- Chef::Config[:knife][:cloudformation][:poll] = true
200
- knife_events.run
201
- unless(action_successful?(name))
202
- ui.fatal "#{action_type} of new stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
203
- exit 1
204
- end
205
- end
206
-
207
- def action_in_progress?(name)
208
- stack_status(name) == 'CREATE_IN_PROGRESS'
209
- end
210
-
211
- def action_successful?(name)
212
- stack_status(name) == 'CREATE_COMPLETE'
213
- end
214
177
  end
215
178
  end
216
179
  end