knife-cloudformation 0.1.22 → 0.2.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +56 -2
  4. data/knife-cloudformation.gemspec +4 -7
  5. data/lib/chef/knife/cloudformation_create.rb +105 -245
  6. data/lib/chef/knife/cloudformation_describe.rb +50 -26
  7. data/lib/chef/knife/cloudformation_destroy.rb +17 -18
  8. data/lib/chef/knife/cloudformation_events.rb +48 -14
  9. data/lib/chef/knife/cloudformation_export.rb +117 -34
  10. data/lib/chef/knife/cloudformation_import.rb +124 -18
  11. data/lib/chef/knife/cloudformation_inspect.rb +159 -71
  12. data/lib/chef/knife/cloudformation_list.rb +20 -24
  13. data/lib/chef/knife/cloudformation_promote.rb +40 -0
  14. data/lib/chef/knife/cloudformation_update.rb +132 -15
  15. data/lib/chef/knife/cloudformation_validate.rb +35 -0
  16. data/lib/knife-cloudformation.rb +28 -0
  17. data/lib/knife-cloudformation/cache.rb +213 -35
  18. data/lib/knife-cloudformation/knife.rb +9 -0
  19. data/lib/knife-cloudformation/knife/base.rb +179 -0
  20. data/lib/knife-cloudformation/knife/stack.rb +94 -0
  21. data/lib/knife-cloudformation/knife/template.rb +174 -0
  22. data/lib/knife-cloudformation/monkey_patch.rb +8 -0
  23. data/lib/knife-cloudformation/monkey_patch/stack.rb +195 -0
  24. data/lib/knife-cloudformation/provider.rb +225 -0
  25. data/lib/knife-cloudformation/utils.rb +18 -98
  26. data/lib/knife-cloudformation/utils/animal_strings.rb +28 -0
  27. data/lib/knife-cloudformation/utils/debug.rb +31 -0
  28. data/lib/knife-cloudformation/utils/json.rb +64 -0
  29. data/lib/knife-cloudformation/utils/object_storage.rb +28 -0
  30. data/lib/knife-cloudformation/utils/output.rb +79 -0
  31. data/lib/knife-cloudformation/utils/path_selector.rb +99 -0
  32. data/lib/knife-cloudformation/utils/ssher.rb +29 -0
  33. data/lib/knife-cloudformation/utils/stack_exporter.rb +271 -0
  34. data/lib/knife-cloudformation/utils/stack_parameter_scrubber.rb +35 -0
  35. data/lib/knife-cloudformation/utils/stack_parameter_validator.rb +124 -0
  36. data/lib/knife-cloudformation/version.rb +2 -4
  37. metadata +47 -94
  38. data/Gemfile +0 -3
  39. data/Gemfile.lock +0 -90
  40. data/knife-cloudformation-0.1.20.gem +0 -0
  41. data/lib/knife-cloudformation/aws_commons.rb +0 -267
  42. data/lib/knife-cloudformation/aws_commons/stack.rb +0 -435
  43. data/lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb +0 -79
  44. data/lib/knife-cloudformation/cloudformation_base.rb +0 -168
  45. data/lib/knife-cloudformation/export.rb +0 -174
@@ -0,0 +1,28 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Helper methods for string format modification
7
+ module AnimalStrings
8
+
9
+ # Camel case string
10
+ #
11
+ # @param string [String]
12
+ # @return [String]
13
+ def camel(string)
14
+ string.to_s.split('_').map{|k| "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"}.join
15
+ end
16
+
17
+ # Snake case string
18
+ #
19
+ # @param string [String]
20
+ # @return [Symbol]
21
+ def snake(string)
22
+ string.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+ # Debug helpers
6
+ module Debug
7
+ # Output helpers
8
+ module Output
9
+ # Write debug message
10
+ #
11
+ # @param msg [String]
12
+ def debug(msg)
13
+ puts "<KnifeCloudformation>: #{msg}" if ENV['DEBUG']
14
+ end
15
+ end
16
+
17
+ class << self
18
+ # Load module into class
19
+ #
20
+ # @param klass [Class]
21
+ def included(klass)
22
+ klass.class_eval do
23
+ include Output
24
+ extend Output
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # JSON helper methods
7
+ module JSON
8
+
9
+ # Attempt to load chef JSON compat helper
10
+ #
11
+ # @return [TrueClass, FalseClass] chef compat helper available
12
+ def try_json_compat
13
+ unless(@_json_loaded)
14
+ begin
15
+ require 'chef/json_compat'
16
+ rescue
17
+ require "#{ENV['RUBY_JSON_LIB'] || 'json'}"
18
+ end
19
+ @_json_loaded = true
20
+ end
21
+ defined?(Chef::JSONCompat)
22
+ end
23
+
24
+ # Convert to JSON
25
+ #
26
+ # @param thing [Object]
27
+ # @return [String]
28
+ def _to_json(thing)
29
+ if(try_json_compat)
30
+ Chef::JSONCompat.to_json(thing)
31
+ else
32
+ JSON.dump(thing)
33
+ end
34
+ end
35
+
36
+ # Load JSON data
37
+ #
38
+ # @param thing [String]
39
+ # @return [Object]
40
+ def _from_json(thing)
41
+ if(try_json_compat)
42
+ Chef::JSONCompat.from_json(thing)
43
+ else
44
+ JSON.read(thing)
45
+ end
46
+ end
47
+
48
+ # Format object into pretty JSON
49
+ #
50
+ # @param thing [Object]
51
+ # @return [String]
52
+ def _format_json(thing)
53
+ thing = _from_json(thing) if thing.is_a?(String)
54
+ if(try_json_compat)
55
+ Chef::JSONCompat.to_json_pretty(thing)
56
+ else
57
+ JSON.pretty_generate(thing)
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Storage helpers
7
+ module ObjectStorage
8
+
9
+ # Write to file
10
+ #
11
+ # @param object [Object]
12
+ # @param path [String] path to write object
13
+ # @param directory [Miasma::Models::Storage::Directory]
14
+ # @return [String] file path
15
+ def file_store(object, path, directory)
16
+ raise NotImplementedError.new 'Internal updated required! :('
17
+ content = object.is_a?(String) ? object : Utils._format_json(object)
18
+ directory.files.create(
19
+ :identity => path,
20
+ :body => content
21
+ )
22
+ loc = directory.service.service.name.split('::').last.downcase
23
+ "#{loc}://#{directory.identity}/#{path}"
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,79 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+ # Output Helpers
6
+ module Output
7
+
8
+ # Process things and return items
9
+ #
10
+ # @param things [Array] items to process
11
+ # @param args [Hash] options
12
+ # @option args [TrueClass, FalseClass] :flat flatten result array
13
+ # @option args [Array] :attributes attributes to extract
14
+ # @todo this was extracted from events and needs to be cleaned up
15
+ def process(things, args={})
16
+ @event_ids ||= []
17
+ processed = things.reverse.map do |thing|
18
+ next if @event_ids.include?(thing['id'])
19
+ @event_ids.push(thing['id']).compact!
20
+ if(args[:attributes])
21
+ args[:attributes].map do |key|
22
+ thing[key].to_s
23
+ end
24
+ else
25
+ thing.values
26
+ end
27
+ end
28
+ args[:flat] ? processed.flatten : processed
29
+ end
30
+
31
+ # Generate formatted titles
32
+ #
33
+ # @param thing [Object] thing being processed
34
+ # @param args [Hash]
35
+ # @option args [Array] :attributes
36
+ # @return [Array<String>] formatted titles
37
+ def get_titles(thing, args={})
38
+ attrs = args[:attributes] || []
39
+ if(attrs.empty?)
40
+ hash = thing.is_a?(Array) ? thing.first : thing
41
+ hash ||= {}
42
+ attrs = hash.keys
43
+ end
44
+ titles = attrs.map do |key|
45
+ camel(key).gsub(/([a-z])([A-Z])/, '\1 \2')
46
+ end.compact
47
+ if(args[:format])
48
+ titles.map{|s| @ui.color(s, :bold)}
49
+ else
50
+ titles
51
+ end
52
+ end
53
+
54
+ # Output stack related things in nice format
55
+ #
56
+ # @param stack [String] name of stack
57
+ # @param things [Array] things to display
58
+ # @param what [String] description of things for output
59
+ # @param args [Symbol] options (:ignore_empty_output)
60
+ def things_output(stack, things, what, *args)
61
+ unless(args.include?(:no_title))
62
+ output = get_titles(things, :format => true, :attributes => allowed_attributes)
63
+ else
64
+ output = []
65
+ end
66
+ columns = allowed_attributes.size
67
+ output += process(things, :flat => true, :attributes => allowed_attributes)
68
+ output.compact!
69
+ if(output.empty?)
70
+ ui.warn 'No information found' unless args.include?(:ignore_empty_output)
71
+ else
72
+ ui.info "#{what.to_s.capitalize} for stack: #{ui.color(stack, :bold)}" if stack
73
+ ui.info "#{ui.list(output, :uneven_columns_across, columns)}"
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,99 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Helper methods for path selection
7
+ module PathSelector
8
+
9
+ # Humanize the base name of path
10
+ #
11
+ # @param path [String]
12
+ # @return [String]
13
+ def humanize_path_basename(path)
14
+ File.basename(path).sub(
15
+ File.extname(path), ''
16
+ ).split(/[-_]/).map(&:capitalize).join(' ')
17
+ end
18
+
19
+ # Prompt user for file selection
20
+ #
21
+ # @param directory [String] path to directory
22
+ # @param opts [Hash] options
23
+ # @option opts [Array<String>] :ignore_directories directory names
24
+ # @option opts [String] :directories_name title for directories
25
+ # @option opts [String] :files_name title for files
26
+ # @option opts [String] :filter_prefix only return results matching filter
27
+ # @return [String] file path
28
+ def prompt_for_file(directory, opts={})
29
+ file_list = Dir.glob(File.join(directory, '**', '**', '*')).find_all do |file|
30
+ File.file?(file)
31
+ end
32
+ if(opts[:filter_prefix])
33
+ file_list = file_list.find_all do |file|
34
+ file.start_with?(options[:filter_prefix])
35
+ end
36
+ end
37
+ directories = file_list.map do |file|
38
+ File.dirname(file)
39
+ end.uniq
40
+ files = file_list.find_all do |path|
41
+ path.sub(directory, '').split('/').size == 2
42
+ end
43
+ if(opts[:ignore_directories])
44
+ directories.delete_if do |dir|
45
+ opts[:ignore_directories].include?(File.basename(dir))
46
+ end
47
+ end
48
+ if(directories.empty? && files.empty?)
49
+ ui.fatal 'No formation paths discoverable!'
50
+ else
51
+ output = ['Please select an entry']
52
+ output << '(or directory to list):' unless directories.empty?
53
+ ui.info output.join(' ')
54
+ output.clear
55
+ idx = 1
56
+ valid = {}
57
+ unless(directories.empty?)
58
+ output << ui.color("#{opts.fetch(:directories_name, 'Directories')}:", :bold)
59
+ directories.each do |dir|
60
+ valid[idx] = {:path => dir, :type => :directory}
61
+ output << [idx, humanize_path_basename(dir)]
62
+ idx += 1
63
+ end
64
+ end
65
+ unless(files.empty?)
66
+ output << ui.color("#{opts.fetch(:files_name, 'Files')}:", :bold)
67
+ files.each do |file|
68
+ valid[idx] = {:path => file, :type => :file}
69
+ output << [idx, humanize_path_basename(file)]
70
+ idx += 1
71
+ end
72
+ end
73
+ max = idx.to_s.length
74
+ output.map! do |o|
75
+ if(o.is_a?(Array))
76
+ " #{o.first}.#{' ' * (max - o.first.to_s.length)} #{o.last}"
77
+ else
78
+ o
79
+ end
80
+ end
81
+ ui.info "#{output.join("\n")}\n"
82
+ response = ask_question('Enter selection: ').to_i
83
+ unless(valid[response])
84
+ ui.fatal 'How about using a real value'
85
+ exit 1
86
+ else
87
+ entry = valid[response.to_i]
88
+ if(entry[:type] == :directory)
89
+ prompt_for_file(entry[:path], opts)
90
+ else
91
+ "/#{entry[:path]}"
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Helper methods for SSH interactions
7
+ module Ssher
8
+
9
+ # Retrieve file from remote node
10
+ #
11
+ # @param address [String]
12
+ # @param user [String]
13
+ # @param path [String] remote file path
14
+ # @param ssh_opts [Hash]
15
+ # @return [String, NilClass]
16
+ def remote_file_contents(address, user, path, ssh_opts={})
17
+ if(path.to_s.strip.empty?)
18
+ raise ArgumentError.new 'No file path provided!'
19
+ end
20
+ require 'net/ssh'
21
+ content = ''
22
+ ssh_session = Net::SSH.start(address, user, ssh_opts)
23
+ content = ssh_session.exec!("sudo cat #{path}")
24
+ content.empty? ? nil : content
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,271 @@
1
+ require 'chef'
2
+ require 'knife-cloudformation'
3
+
4
+ module KnifeCloudformation
5
+ module Utils
6
+
7
+ # Stack serialization helper
8
+ class StackExporter
9
+
10
+ include KnifeCloudformation::Utils::AnimalStrings
11
+ include KnifeCloudformation::Utils::JSON
12
+
13
+ # default chef environment name
14
+ DEFAULT_CHEF_ENVIRONMENT = '_default'
15
+ # default instance options
16
+ DEFAULT_OPTIONS = Mash.new(
17
+ :chef_popsicle => true,
18
+ :ignored_parameters => ['Environment', 'StackCreator', 'Creator'],
19
+ :chef_environment_parameter => 'Environment'
20
+ )
21
+ # default structure of export payload
22
+ DEFAULT_EXPORT_STRUCTURE = {
23
+ :stack => Mash.new(
24
+ :template => nil,
25
+ :options => {
26
+ :parameters => Mash.new,
27
+ :capabilities => [],
28
+ :notification_topics => []
29
+ }
30
+ ),
31
+ :generator => {
32
+ :timestamp => Time.now.to_i,
33
+ :name => 'knife-cloudformation',
34
+ :version => KnifeCloudformation::VERSION.version,
35
+ :provider => nil
36
+ }
37
+ }
38
+
39
+ # @return [Miasma::Models::Orchestration::Stack]
40
+ attr_reader :stack
41
+ # @return [Hash]
42
+ attr_reader :options
43
+ # @return [Hash]
44
+ attr_reader :stack_export
45
+
46
+ # Create new instance
47
+ #
48
+ # @param stack [Miasma::Models::Orchestration::Stack]
49
+ # @param options [Hash]
50
+ # @option options [KnifeCloudformation::Provider] :provider
51
+ # @option options [TrueClass, FalseClass] :chef_popsicle freeze run list
52
+ # @option options [Array<String>] :ignored_parameters
53
+ # @option options [String] :chef_environment_parameter
54
+ def initialize(stack, options={})
55
+ @stack = stack
56
+ @options = DEFAULT_OPTIONS.merge(options)
57
+ @stack_export = Mash.new
58
+ end
59
+
60
+ # Export stack
61
+ #
62
+ # @return [Hash] exported stack
63
+ def export
64
+ @stack_export = Mash.new(DEFAULT_EXPORT_STRUCTURE).tap do |stack_export|
65
+ [:parameters, :capabilities, :notification_topics].each do |key|
66
+ if(val = stack.send(key))
67
+ stack_export[:stack][key] = val
68
+ end
69
+ end
70
+ stack_export[:stack][:template] = stack.template
71
+ stack_export[:generator][:timestamp] = Time.now.to_i
72
+ stack_export[:generator][:provider] = stack.provider.connection.provider
73
+ if(chef_popsicle?)
74
+ freeze_runlists(stack_export)
75
+ end
76
+ remove_ignored_parameters(stack_export)
77
+ stack_export[:stack][:template] = _to_json(
78
+ stack_export[:stack][:template]
79
+ )
80
+ end
81
+ end
82
+
83
+ # Provide query methods on options hash
84
+ #
85
+ # @param args [Object] argument list
86
+ # @return [Object]
87
+ def method_missing(*args)
88
+ m = args.first.to_s
89
+ if(m.end_with?('?') && options.has_key?(k = m.sub('?', '').to_sym))
90
+ !!options[k]
91
+ else
92
+ super
93
+ end
94
+ end
95
+
96
+ protected
97
+
98
+ # Remove parameter values from export that are configured to be
99
+ # ignored
100
+ #
101
+ # @param export [Hash] stack export
102
+ # @return [Hash]
103
+ def remove_ignored_parameters(export)
104
+ options[:ignored_parameters].each do |param|
105
+ if(export[:stack][:options][:parameters])
106
+ export[:stack][:options][:parameters].delete(param)
107
+ end
108
+ end
109
+ export
110
+ end
111
+
112
+ # Environment name to use when interacting with Chef
113
+ #
114
+ # @param export [Hash] current export state
115
+ # @return [String] environment name
116
+ def chef_environment_name(export)
117
+ if(chef_environment_parameter?)
118
+ name = export[:stack][:options][:parameters][options[:chef_environment_parameter]]
119
+ end
120
+ name || DEFAULT_CHEF_ENVIRONMENT
121
+ end
122
+
123
+ # @return [Chef::Environment]
124
+ def environment
125
+ unless(@env)
126
+ @env = Chef::Environment.load('_default')
127
+ end
128
+ @env
129
+ end
130
+
131
+ # Find latest available cookbook version within
132
+ # the configured environment
133
+ #
134
+ # @param cookbook [String] name of cookbook
135
+ # @return [Chef::Version]
136
+ def allowed_cookbook_version(cookbook)
137
+ restriction = environment.cookbook_versions[cookbook]
138
+ requirement = Gem::Requirement.new(restriction)
139
+ Chef::CookbookVersion.available_versions(cookbook).detect do |v|
140
+ requirement.satisfied_by?(Gem::Version.new(v))
141
+ end
142
+ end
143
+
144
+ # Extract the runlist item. Fully expands roles and provides
145
+ # version pegged runlist.
146
+ #
147
+ # @param item [Chef::RunList::RunListItem, Array<String>]
148
+ # @return [Hash] new chef configuration hash
149
+ # @note this will expand all roles
150
+ def extract_runlist_item(item)
151
+ rl_item = item.is_a?(Chef::RunList::RunListItem) ? item : Chef::RunList::RunListItem.new(item)
152
+ static_content = Mash.new(:run_list => [])
153
+ if(rl_item.recipe?)
154
+ cookbook, recipe = rl_item.name.split('::')
155
+ peg_version = allowed_cookbook_version(cookbook)
156
+ static_content[:run_list] << "recipe[#{[cookbook, recipe || 'default'].join('::')}@#{peg_version}]"
157
+ elsif(rl_item.role?)
158
+ role = Chef::Role.load(rl_item.name)
159
+ role.run_list.each do |item|
160
+ static_content = Chef::Mixin::DeepMerge.merge(static_content, extract_runlist_item(item))
161
+ end
162
+ static_content = Chef::Mixin::DeepMerge.merge(
163
+ static_content, Chef::Mixin::DeepMerge.merge(role.default_attributes, role.override_attributes)
164
+ )
165
+ else
166
+ raise TypeError.new("Unknown chef run list item encountered: #{rl_item.inspect}")
167
+ end
168
+ static_content
169
+ end
170
+
171
+ # Expand any detected chef run lists and freeze them within the
172
+ # stack template
173
+ #
174
+ # @param first_run [Hash] chef first run hash
175
+ # @return [Hash]
176
+ def unpack_and_freeze_runlist(first_run)
177
+ extracted_runlists = first_run['run_list'].map do |item|
178
+ extract_runlist_item(cf_replace(item))
179
+ end
180
+ first_run.delete('run_list')
181
+ first_run.replace(
182
+ extracted_runlists.inject(first_run) do |memo, first_run_item|
183
+ Chef::Mixin::DeepMerge.merge(memo, first_run_item)
184
+ end
185
+ )
186
+ end
187
+
188
+ # Freeze chef run lists
189
+ #
190
+ # @param exported [Hash] stack export
191
+ # @return [Hash]
192
+ def freeze_runlists(exported)
193
+ first_runs = locate_runlists(exported)
194
+ first_runs.each do |first_run|
195
+ unpack_and_freeze_runlist(first_run)
196
+ end
197
+ exported
198
+ end
199
+
200
+ # Locate chef run lists within data collection
201
+ #
202
+ # @param thing [Enumerable] collection from export
203
+ # @return [Enumerable] updated collection from export
204
+ def locate_runlists(thing)
205
+ result = []
206
+ case thing
207
+ when Hash
208
+ if(thing['content'] && thing['content']['run_list'])
209
+ result << thing['content']
210
+ else
211
+ thing.each do |k,v|
212
+ result += locate_runlists(v)
213
+ end
214
+ end
215
+ when Array
216
+ thing.each do |v|
217
+ result += locate_runlists(v)
218
+ end
219
+ end
220
+ result
221
+ end
222
+
223
+ # Apply cloudformation function to data
224
+ #
225
+ # @param hsh [Object] stack template item
226
+ # @return [Object]
227
+ def cf_replace(hsh)
228
+ if(hsh.is_a?(Hash))
229
+ case hsh.keys.first
230
+ when 'Fn::Join'
231
+ cf_join(*hsh.values.first)
232
+ when 'Ref'
233
+ cf_ref(hsh.values.first)
234
+ else
235
+ hsh
236
+ end
237
+ else
238
+ hsh
239
+ end
240
+ end
241
+
242
+ # Apply Ref function
243
+ #
244
+ # @param ref_name [Hash]
245
+ # @return [Object] value in parameters
246
+ def cf_ref(ref_name)
247
+ if(stack.parameters.has_key?(ref_name))
248
+ stack.parameters[ref_name]
249
+ else
250
+ raise KeyError.new("No parameter found with given reference name (#{ref_name}). " <<
251
+ "Only parameter based references supported!")
252
+ end
253
+ end
254
+
255
+ # Apply Join function
256
+ #
257
+ # @param delim [String] join delimiter
258
+ # @param args [String, Hash] items to join
259
+ # @return [String]
260
+ def cf_join(delim, args)
261
+ args.map do |arg|
262
+ if(arg.is_a?(Hash))
263
+ cf_replace(arg)
264
+ else
265
+ arg.to_s
266
+ end
267
+ end.join(delim)
268
+ end
269
+ end
270
+ end
271
+ end