knife-cloudformation 0.1.2 → 0.1.4

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