knife-cloudformation 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,2 +1,5 @@
1
+ ## v0.1.0
2
+ * Stable-ish release
3
+
1
4
  ## v0.0.1
2
5
  * Initial release
@@ -2,6 +2,18 @@ require 'chef/knife'
2
2
 
3
3
  class Chef
4
4
  class Knife
5
+
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
+
5
17
  module CloudformationDefault
6
18
  class << self
7
19
  def included(klass)
@@ -89,9 +101,13 @@ class Chef
89
101
 
90
102
  def get_titles(thing, format=false)
91
103
  unless(@titles)
92
- hash = thing.is_a?(Array) ? thing.first : thing
93
- hash ||= {}
94
- @titles = hash.keys.map do |key|
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|
95
111
  next unless attribute_allowed?(key)
96
112
  key.gsub(/([a-z])([A-Z])/, '\1 \2')
97
113
  end.compact
@@ -172,6 +188,15 @@ class Chef
172
188
  JSON.read(thing)
173
189
  end
174
190
  end
191
+
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)
198
+ end
199
+ end
175
200
  end
176
201
  end
177
202
  end
@@ -7,73 +7,91 @@ class Chef
7
7
 
8
8
  banner 'knife cloudformation create NAME'
9
9
 
10
+ module Options
11
+ class << self
12
+ def included(klass)
13
+ klass.class_eval do
14
+
15
+ attr_accessor :action_type
16
+
17
+ option(:parameter,
18
+ :short => '-p KEY:VALUE',
19
+ :long => '--parameter KEY:VALUE',
20
+ :description => 'Set parameter. Can be used multiple times.',
21
+ :proc => lambda {|val|
22
+ key,value = val.split(':')
23
+ Chef::Config[:knife][:cloudformation][:options][:parameters] ||= Mash.new
24
+ Chef::Config[:knife][:cloudformation][:options][:parameters][key] = value
25
+ }
26
+ )
27
+ option(:timeout,
28
+ :short => '-t MIN',
29
+ :long => '--timeout MIN',
30
+ :description => 'Set timeout for stack creation',
31
+ :proc => lambda {|val|
32
+ Chef::Config[:knife][:cloudformation][:options][:timeout_in_minutes] = val
33
+ }
34
+ )
35
+ option(:disable_rollback,
36
+ :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 }
40
+ )
41
+ option(:capability,
42
+ :short => '-C CAPABILITY',
43
+ :long => '--capability CAPABILITY',
44
+ :description => 'Specify allowed capabilities. Can be used multiple times.',
45
+ :proc => lambda {|val|
46
+ Chef::Config[:knife][:cloudformation][:options][:capabilities] ||= []
47
+ Chef::Config[:knife][:cloudformation][:options][:capabilities].push(val).uniq!
48
+ }
49
+ )
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
+ )
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 }
59
+ )
60
+ option(:notifications,
61
+ :long => '--notification ARN',
62
+ :description => 'Add notification ARN. Can be used multiple times.',
63
+ :proc => lambda {|val|
64
+ Chef::Config[:knife][:cloudformation][:options][:notification_ARNs] ||= []
65
+ Chef::Config[:knife][:cloudformation][:options][:notification_ARNs].push(val).uniq!
66
+ }
67
+ )
68
+ option(:file,
69
+ :short => '-f PATH',
70
+ :long => '--file PATH',
71
+ :description => 'Path to Cloud Formation to process',
72
+ :proc => lambda {|val|
73
+ Chef::Config[:knife][:cloudformation][:file] = val
74
+ }
75
+ )
76
+ option(:disable_interactive_parameters,
77
+ :long => '--no-parameter-prompts',
78
+ :description => 'Do not prompt for input on dynamic parameters'
79
+ )
80
+
81
+ option(:print_only,
82
+ :long => '--print-only',
83
+ :description => 'Print template and exit'
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
89
+
10
90
  include CloudformationDefault
11
-
12
- option(:parameter,
13
- :short => '-p KEY:VALUE',
14
- :long => '--parameter KEY:VALUE',
15
- :description => 'Set parameter. Can be used multiple times.',
16
- :proc => lambda {|val|
17
- key,value = val.split(':')
18
- Chef::Config[:knife][:cloudformation][:options][:parameters] ||= Mash.new
19
- Chef::Config[:knife][:cloudformation][:options][:parameters][key] = value
20
- }
21
- )
22
- option(:timeout,
23
- :short => '-t MIN',
24
- :long => '--timeout MIN',
25
- :description => 'Set timeout for stack creation',
26
- :proc => lambda {|val|
27
- Chef::Config[:knife][:cloudformation][:options][:timeout_in_minutes] = val
28
- }
29
- )
30
- option(:disable_rollback,
31
- :short => '-R',
32
- :long => '--disable-rollback',
33
- :description => 'Disable rollback on stack creation failure',
34
- :proc => lambda { Chef::Config[:knife][:cloudformation][:options][:disable_rollback] = true }
35
- )
36
- option(:capability,
37
- :short => '-C CAPABILITY',
38
- :long => '--capability CAPABILITY',
39
- :description => 'Specify allowed capabilities. Can be used multiple times.',
40
- :proc => lambda {|val|
41
- Chef::Config[:knife][:cloudformation][:options][:capabilities] ||= []
42
- Chef::Config[:knife][:cloudformation][:options][:capabilities].push(val).uniq!
43
- }
44
- )
45
- option(:enable_processing,
46
- :long => '--enable-processing',
47
- :description => 'Call the unicorns.',
48
- :proc => lambda { Chef::Config[:knife][:cloudformation][:options][:enable_processing] = true }
49
- )
50
- option(:disable_polling,
51
- :long => '--disable-polling',
52
- :description => 'Disable stack even polling.',
53
- :proc => lambda { Chef::Config[:knife][:cloudformation][:options][:disable_polling] = true }
54
- )
55
- option(:notifications,
56
- :long => '--notification ARN',
57
- :description => 'Add notification ARN. Can be used multiple times.',
58
- :proc => lambda {|val|
59
- Chef::Config[:knife][:cloudformation][:options][:notification_ARNs] ||= []
60
- Chef::Config[:knife][:cloudformation][:options][:notification_ARNs].push(val).uniq!
61
- }
62
- )
63
- option(:file,
64
- :short => '-F PATH',
65
- :long => '--file PATH',
66
- :description => 'Path to Cloud Formation to process',
67
- :proc => lambda {|val|
68
- Chef::Config[:knife][:cloudformation][:file] = val
69
- }
70
- )
71
- option(:disable_interactive_parameters,
72
- :long => '--no-parameter-prompts',
73
- :description => 'Do not prompt for input on dynamic parameters'
74
- )
91
+ include Options
75
92
 
76
93
  def run
94
+ @action_type = self.class.name.split('::').last.sub('Cloudformation', '').upcase
77
95
  unless(File.exists?(Chef::Config[:knife][:cloudformation][:file].to_s))
78
96
  ui.fatal "Invalid formation file path provided: #{Chef::Config[:knife][:cloudformation][:file]}"
79
97
  exit 1
@@ -82,26 +100,34 @@ class Chef
82
100
  if(Chef::Config[:knife][:cloudformation][:enable_processing])
83
101
  file = KnifeCloudformation::SparkleFormation.compile(Chef::Config[:knife][:cloudformation][:file])
84
102
  else
85
- file = File.read(Chef::Config[:knife][:cloudformation][:file])
103
+ file = _from_json(File.read(Chef::Config[:knife][:cloudformation][:file]))
86
104
  end
87
- ui.info "#{ui.color('Cloud Formation: ', :bold)} #{ui.color('CREATE', :green)}"
105
+ ui.info "#{ui.color('Cloud Formation: ', :bold)} #{ui.color(action_type, :green)}"
88
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]}"
89
107
  stack = build_stack(file)
108
+ if(config[:print_only])
109
+ ui.warn 'Print only requested'
110
+ ui.info _format_json(stack['TemplateBody'])
111
+ exit 1
112
+ end
90
113
  create_stack(name, stack)
91
114
  unless(Chef::Config[:knife][:cloudformation][:disable_polling])
92
115
  poll_stack(name)
93
116
  else
94
117
  ui.warn 'Stack state polling has been disabled.'
95
118
  end
96
- ui.info "Stack creation complete: #{ui.color('SUCCESS', :green)}"
119
+ ui.info "Stack #{action_type} complete: #{ui.color('SUCCESS', :green)}"
97
120
  end
98
121
 
99
122
  def build_stack(template)
100
123
  stack = Mash.new
124
+ populate_parameters!(template)
101
125
  Chef::Config[:knife][:cloudformation][:options].each do |key, value|
102
126
  format_key = key.split('_').map(&:capitalize).join
127
+ stack[format_key] = value
128
+ =begin
103
129
  case value
104
- when Hash
130
+ when Hash && key.to_sym != :parameters
105
131
  i = 1
106
132
  value.each do |k, v|
107
133
  stack["#{format_key}.member.#{i}.#{format_key[0, (format_key.length - 1)]}Key"] = k
@@ -112,14 +138,26 @@ class Chef
112
138
  stack["#{format_key}.member.#{i+1}"] = v
113
139
  end
114
140
  else
115
- stack[format_key] = value
141
+
116
142
  end
143
+ =end
117
144
  end
118
- populate_parameters!(template)
145
+ enable_capabilities!(stack, template)
119
146
  stack['TemplateBody'] = Chef::JSONCompat.to_json(template)
120
147
  stack
121
148
  end
122
149
 
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
+
123
161
  def populate_parameters!(stack)
124
162
  unless(config[:disable_interactive_parameters])
125
163
  if(stack['Parameters'])
@@ -149,8 +187,8 @@ class Chef
149
187
  begin
150
188
  res = aws_con.create_stack(name, stack)
151
189
  rescue => e
152
- ui.fatal "Failed to create stack #{name}. Reason: #{e}"
153
- _debug(e, "Generated template used:\n#{stack.inspect}")
190
+ ui.fatal "Failed to #{action_type} stack #{name}. Reason: #{e}"
191
+ _debug(e, "Generated template used:\n#{_format_json(stack['TemplateBody'])}")
154
192
  exit 1
155
193
  end
156
194
  end
@@ -160,17 +198,17 @@ class Chef
160
198
  knife_events.name_args.push(name)
161
199
  Chef::Config[:knife][:cloudformation][:poll] = true
162
200
  knife_events.run
163
- unless(create_successful?(name))
164
- ui.fatal "Creation of new stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
201
+ unless(action_successful?(name))
202
+ ui.fatal "#{action_type} of new stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
165
203
  exit 1
166
204
  end
167
205
  end
168
206
 
169
- def create_in_progress?(name)
207
+ def action_in_progress?(name)
170
208
  stack_status(name) == 'CREATE_IN_PROGRESS'
171
209
  end
172
210
 
173
- def create_successful?(name)
211
+ def action_successful?(name)
174
212
  stack_status(name) == 'CREATE_COMPLETE'
175
213
  end
176
214
  end
@@ -63,7 +63,7 @@ class Chef
63
63
  unless(output.empty?)
64
64
  ui.info ui.list(output, :uneven_columns_across, allowed_attributes.size)
65
65
  end
66
- sleep(1)
66
+ sleep((ENV['CLOUDFORMATION_POLL'] || 15).to_i)
67
67
  end
68
68
  # One more to see completion
69
69
  events = stack_events(name)
@@ -78,7 +78,7 @@ class Chef
78
78
  end
79
79
 
80
80
  def default_attributes
81
- %w(Timestamp LogicalResourceId ResourceType ResourceStatus)
81
+ %w(Timestamp LogicalResourceId ResourceType ResourceStatus ResourceStatusReason)
82
82
  end
83
83
  end
84
84
  end
@@ -0,0 +1,31 @@
1
+ require 'chef/knife/cloudformation_create'
2
+
3
+ class Chef
4
+ class Knife
5
+ class CloudformationUpdate < CloudformationCreate
6
+ banner 'knife cloudformation update NAME'
7
+
8
+ include CloudformationDefault
9
+ include CloudformationCreate::Options
10
+
11
+ def create_stack(name, stack)
12
+ begin
13
+ res = aws_con.update_stack(name, stack)
14
+ rescue => e
15
+ ui.fatal "Failed to update stack #{name}. Reason: #{e}"
16
+ _debug(e, "Generated template used:\n#{_format_json(stack['TemplateBody'])}")
17
+ exit 1
18
+ end
19
+ end
20
+
21
+ def action_in_progress?(name)
22
+ stack_status(name) == 'UPDATE_IN_PROGRESS'
23
+ end
24
+
25
+ def action_successful?(name)
26
+ stack_status(name) == 'UPDATE_COMPLETE'
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ require 'attribute_struct'
2
+
3
+ module SparkleAttribute
4
+
5
+ # TODO: look at the docs for Fn stuff. We can probably just map
6
+ # simple ones with a bit of string manipulations
7
+
8
+ def _cf_join(*args)
9
+ options = args.detect{|i| i.is_a?(Hash) && i[:options]} || {:options => {}}
10
+ args.delete(options)
11
+ unless(args.size == 1)
12
+ args = [args]
13
+ end
14
+ {'Fn::Join' => [options[:options][:delimiter] || '', *args]}
15
+ end
16
+
17
+ def _cf_ref(thing)
18
+ thing = _process_key(thing, :force) if thing.is_a?(Symbol)
19
+ {'Ref' => thing}
20
+ end
21
+
22
+ def _cf_map(thing, key, *suffix)
23
+ suffix = suffix.map do |item|
24
+ if(item.is_a?(Symbol))
25
+ _process_key(item, :force)
26
+ else
27
+ item
28
+ end
29
+ end
30
+ thing = _process_key(thing, :force) if thing.is_a?(Symbol)
31
+ key = _process_key(key, :force) if key.is_a?(Symbol)
32
+ {'Fn::FindInMap' => [_process_key(thing), {'Ref' => _process_key(key)}, *suffix]}
33
+ end
34
+
35
+ def _cf_attr(*args)
36
+ args = args.map do |thing|
37
+ if(thing.is_a?(Symbol))
38
+ _process_key(thing, :force)
39
+ else
40
+ thing
41
+ end
42
+
43
+ end
44
+ {'Fn::GetAtt' => args}
45
+ end
46
+
47
+ def _cf_base64(arg)
48
+ {'Fn::Base64' => arg}
49
+ end
50
+
51
+ def rhel?
52
+ !!@platform[:rhel]
53
+ end
54
+
55
+ def debian?
56
+ !!@platform[:debian]
57
+ end
58
+
59
+ def _platform=(plat)
60
+ @platform || __hashish
61
+ @platform.clear
62
+ @platform[plat.to_sym] = true
63
+ end
64
+
65
+ end
66
+
67
+ AttributeStruct.send(:include, SparkleAttribute)
@@ -1,11 +1,15 @@
1
1
  require 'chef/mash'
2
2
  require 'attribute_struct'
3
+ require 'knife-cloudformation/sparkle_attribute'
3
4
 
4
5
  AttributeStruct.camel_keys = true
5
6
 
6
7
  module KnifeCloudformation
7
8
  class SparkleFormation
8
9
  class << self
10
+
11
+ attr_reader :dynamics
12
+
9
13
  def compile(path)
10
14
  formation = self.instance_eval(IO.read(path), path, 1)
11
15
  formation.compile._dump
@@ -21,6 +25,31 @@ module KnifeCloudformation
21
25
  self.instance_eval(IO.read(path), path, 1)
22
26
  end
23
27
 
28
+ def load_dynamics!(directory)
29
+ @loaded_dynamics ||= []
30
+ Dir.glob(File.join(directory, '*.rb')).each do |dyn|
31
+ dyn = File.expand_path(dyn)
32
+ next if @loaded_dynamics.include?(dyn)
33
+ self.instance_eval(IO.read(dyn), dyn, 1)
34
+ @loaded_dynamics << dyn
35
+ end
36
+ @loaded_dynamics.uniq!
37
+ true
38
+ end
39
+
40
+ def dynamic(name, &block)
41
+ @dynamics ||= Mash.new
42
+ @dynamics[name] = block
43
+ end
44
+
45
+ def insert(dynamic_name, struct, *args)
46
+ if(@dynamics && @dynamics[dynamic_name])
47
+ struct.instance_exec(*args, &@dynamics[dynamic_name])
48
+ struct
49
+ else
50
+ raise "Failed to locate requested dynamic block for insertion: #{dynamic_name} (valid: #{@dynamics.keys.sort.join(', ')})"
51
+ end
52
+ end
24
53
  end
25
54
 
26
55
  attr_reader :name
@@ -31,6 +60,8 @@ module KnifeCloudformation
31
60
  def initialize(name, options={})
32
61
  @name = name
33
62
  @sparkle_path = options[:sparkle_path] || File.join(Dir.pwd, 'cloudformation/components')
63
+ @dynamics_directory = options[:dynamics_directory] || File.join(File.dirname(@sparkle_path), 'dynamics')
64
+ self.class.load_dynamics!(@dynamics_directory)
34
65
  @components = Mash.new
35
66
  @load_order = []
36
67
  end
@@ -2,5 +2,5 @@ module KnifeCloudformation
2
2
  class Version < Gem::Version
3
3
  end
4
4
 
5
- VERSION = Version.new('0.0.1')
5
+ VERSION = Version.new('0.1.0')
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.0.1
4
+ version: 0.1.0
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-07-08 00:00:00.000000000 Z
12
+ date: 2013-08-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
@@ -71,11 +71,14 @@ files:
71
71
  - lib/chef/knife/cloudformation_describe.rb
72
72
  - lib/chef/knife/cloudformation_list.rb
73
73
  - lib/chef/knife/cloudformation_create.rb
74
+ - lib/chef/knife/cloudformation_update.rb
74
75
  - lib/knife-cloudformation.rb
76
+ - lib/knife-cloudformation/sparkle_attribute.rb
75
77
  - lib/knife-cloudformation/version.rb
76
78
  - lib/knife-cloudformation/sparkle_formation.rb
77
79
  - README.md
78
80
  - knife-cloudformation.gemspec
81
+ - knife-cloudformation-0.0.1.gem
79
82
  - CHANGELOG.md
80
83
  homepage: http://github.com/heavywater/knife-cloudformation
81
84
  licenses: []