knife-cloudformation 0.0.1 → 0.1.0

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,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: []