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
@@ -1,10 +1,11 @@
1
- require 'knife-cloudformation/cloudformation_base'
1
+ require 'knife-cloudformation'
2
2
 
3
3
  class Chef
4
4
  class Knife
5
+ # Cloudformation describe command
5
6
  class CloudformationDescribe < Knife
6
7
 
7
- include KnifeCloudformation::KnifeBase
8
+ include KnifeCloudformation::Knife::Base
8
9
 
9
10
  banner 'knife cloudformation describe NAME'
10
11
 
@@ -13,13 +14,11 @@ class Chef
13
14
  :long => '--resources',
14
15
  :description => 'Display resources for stack'
15
16
  )
16
-
17
17
  option(:outputs,
18
18
  :short => '-o',
19
19
  :long => '--outputs',
20
20
  :description => 'Display output for stack'
21
21
  )
22
-
23
22
  option(:attribute,
24
23
  :short => '-a ATTR',
25
24
  :long => '--attribute ATTR',
@@ -29,45 +28,70 @@ class Chef
29
28
  Chef::Config[:knife][:cloudformation][:attributes].push(val).uniq!
30
29
  }
31
30
  )
32
-
33
31
  option(:all,
34
32
  :long => '--all-attributes',
35
33
  :description => 'Print all attributes'
36
34
  )
37
35
 
38
- def run
36
+ # information available
37
+ unless(defined?(AVAILABLE_DISPLAYS))
38
+ AVAILABLE_DISPLAYS = [:resources, :outputs]
39
+ end
40
+
41
+ # Run the stack describe action
42
+ def _run
39
43
  stack_name = name_args.last
40
- if(config[:outputs])
41
- ui.info "Outputs for stack: #{ui.color(stack_name, :bold)}:"
42
- unless(stack(stack_name).outputs.empty?)
43
- stack(stack_name).outputs.each do |key, value|
44
- key = snake(key).to_s.split('_').map(&:capitalize).join(' ')
45
- ui.info [' ', ui.color("#{key}:", :bold), value].join(' ')
44
+ stack = provider.stacks.get(stack_name)
45
+ if(stack)
46
+ display = [].tap do |to_display|
47
+ AVAILABLE_DISPLAYS.each do |display_option|
48
+ if(config[display_option])
49
+ to_display << display_option
50
+ end
46
51
  end
47
- else
48
- ui.info " #{ui.color('No outputs found')}"
52
+ end
53
+ display = AVAILABLE_DISPLAYS.dup if display.empty?
54
+ display.each do |display_method|
55
+ self.send(display_method, stack)
56
+ ui.info ''
49
57
  end
50
58
  else
51
- things_output(stack_name,
52
- *(config[:resources] ? [get_resources(stack_name), :resources] : [get_description(stack_name), :description])
53
- )
59
+ ui.fatal "Failed to find requested stack: #{ui.color(stack_name, :bold, :red)}"
60
+ exit -1
54
61
  end
55
62
  end
56
63
 
57
- def default_attributes
58
- config[:resources] ? %w(Timestamp ResourceType ResourceStatus) : %w(StackName CreationTime StackStatus DisableRollback)
64
+ # Display resources
65
+ #
66
+ # @param stack [Miasma::Models::Orchestration::Stack]
67
+ def resources(stack)
68
+ stack_resources = stack.resources.all.sort do |x, y|
69
+ y.updated <=> x.updated
70
+ end.map do |resource|
71
+ Mash.new(resource.attributes)
72
+ end
73
+ things_output(stack.name, stack_resources, :resources)
59
74
  end
60
75
 
61
- def get_description(name)
62
- get_things(name) do
63
- [stack(name).raw_stack]
76
+ # Display outputs
77
+ #
78
+ # @param stack [Miasma::Models::Orchestration::Stack]
79
+ def outputs(stack)
80
+ ui.info "Outputs for stack: #{ui.color(stack.name, :bold)}"
81
+ unless(stack.outputs.empty?)
82
+ stack.outputs.each do |output|
83
+ key, value = output.key, output.value
84
+ key = snake(key).to_s.split('_').map(&:capitalize).join(' ')
85
+ ui.info [' ', ui.color("#{key}:", :bold), value].join(' ')
86
+ end
87
+ else
88
+ ui.info " #{ui.color('No outputs found')}"
64
89
  end
65
90
  end
66
91
 
67
- def get_resources(name)
68
- get_things(name) do
69
- stack(name).resources
70
- end
92
+ # @return [Array<String>] default attributes
93
+ def default_attributes
94
+ %w(updated logical_id type status status_reason)
71
95
  end
72
96
 
73
97
  end
@@ -1,10 +1,11 @@
1
- require 'knife-cloudformation/cloudformation_base'
1
+ require 'knife-cloudformation'
2
2
 
3
3
  class Chef
4
4
  class Knife
5
+ # Cloudformation destroy command
5
6
  class CloudformationDestroy < Knife
6
7
 
7
- include KnifeCloudformation::KnifeBase
8
+ include KnifeCloudformation::Knife::Base
8
9
 
9
10
  banner 'knife cloudformation destroy NAME [NAME]'
10
11
 
@@ -16,31 +17,29 @@ class Chef
16
17
  :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:poll] = val }
17
18
  )
18
19
 
19
- def run
20
+ # Run the stack destruction action
21
+ def _run
20
22
  stacks = name_args.sort
21
23
  plural = 's' if stacks.size > 1
22
24
  ui.warn "Destroying Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold)}"
23
25
  ui.confirm "Destroy formation#{plural}"
24
26
  stacks.each do |stack_name|
25
- destroy_formation!(stack_name)
26
- ui.info "Destroy request sent for stack: #{ui.color(stack_name, :bold)}"
27
+ stack = provider.stacks.get(stack_name)
28
+ if(stack)
29
+ stack.destroy
30
+ else
31
+ ui.warn "Failed to locate requested stack: #{ui.color(stack_name, :bold)}"
32
+ end
27
33
  end
28
34
  if(config[:polling])
29
- begin
30
- stacks.each do |stack_name|
31
- poll_stack(stack_name)
32
- end
33
- rescue SystemExit, Fog::AWS::CloudFormation::NotFound
34
- # ignore this error since this is the end result we want!
35
+ if(stacks.size == 1)
36
+ provider.fetch_stacks
37
+ poll_stack(stacks.first)
38
+ else
39
+ ui.error "Stack polling is not available when multiple stack deletion is requested!"
35
40
  end
36
- ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
37
- end
38
- end
39
-
40
- def destroy_formation!(stack_name)
41
- get_things(stack_name, 'Failed to perform destruction') do
42
- stack(stack_name).destroy
43
41
  end
42
+ ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
44
43
  end
45
44
 
46
45
  end
@@ -1,18 +1,19 @@
1
- require 'knife-cloudformation/cloudformation_base'
1
+ require 'knife-cloudformation'
2
2
 
3
3
  class Chef
4
4
  class Knife
5
+ # Cloudformation list command
5
6
  class CloudformationEvents < Knife
6
7
 
7
8
  banner 'knife cloudformation events NAME'
8
9
 
9
- include KnifeCloudformation::KnifeBase
10
+ include KnifeCloudformation::Knife::Base
10
11
 
11
12
  option(:polling,
12
13
  :short => '-p',
13
14
  :long => '--[no-]poll',
14
15
  :boolean => true,
15
- :default => false,
16
+ :default => true,
16
17
  :description => 'Poll events while stack status is "in progress"',
17
18
  :proc => lambda {|v| Chef::Config[:knife][:cloudformation][:poll] = v }
18
19
  )
@@ -41,28 +42,61 @@ class Chef
41
42
  :description => 'Print all attributes'
42
43
  )
43
44
 
44
- def run
45
+ # Run the events list action
46
+ def _run
45
47
  name = name_args.first
46
48
  ui.info "Cloud Formation Events for Stack: #{ui.color(name, :bold)}\n"
47
- things_output(name, get_events(name), 'events')
48
- if(Chef::Config[:knife][:cloudformation][:poll])
49
- while(stack(name).in_progress?)
50
- sleep(Chef::Config[:knife][:cloudformation][:poll_delay] || 15)
51
- things_output(nil, get_events(name), 'events', :no_title, :ignore_empty_output)
49
+ stack = provider.stacks.get(name)
50
+ last_id = nil
51
+ if(stack)
52
+ events = get_events(stack)
53
+ things_output(name, events, 'events')
54
+ last_id = events.last[:id]
55
+ if(Chef::Config[:knife][:cloudformation][:poll])
56
+ cycle_events = true
57
+ while(cycle_events)
58
+ cycle_events = stack.in_progress?
59
+ sleep(Chef::Config[:knife][:cloudformation][:poll_delay] || 15)
60
+ stack.events.reload
61
+ events = get_events(stack, last_id)
62
+ unless(events.empty?)
63
+ last_id = events.last[:id]
64
+ things_output(nil, events, 'events', :no_title, :ignore_empty_output)
65
+ end
66
+ stack.reload
67
+ end
68
+ # Extra to see completion
69
+ things_output(nil, get_events(stack, last_id), 'events', :no_title, :ignore_empty_output)
52
70
  end
53
- # Extra to see completion
54
- things_output(nil, get_events(name), 'events', :no_title, :ignore_empty_output)
71
+ else
72
+ ui.fatal "Failed to locate requested stack: #{ui.color(name, :bold, :red)}"
73
+ exit -1
55
74
  end
56
75
  end
57
76
 
58
- def get_events(name)
77
+ # Fetch events from stack
78
+ #
79
+ # @param stack [Miasma::Models::Orchestration::Stack]
80
+ # @param last_id [String] only return events after this ID
81
+ # @return [Array<Hash>]
82
+ def get_events(stack, last_id=nil)
59
83
  get_things do
60
- stack(name).events
84
+ stack_events = stack.events.all
85
+ if(last_id)
86
+ start_index = stack_events.index{|event| event.id == last_id}
87
+ events = stack_events.slice(0, start_index.to_i)
88
+ else
89
+ events = stack_events
90
+ end
91
+ events.map do |event|
92
+ Mash.new(event.attributes)
93
+ end
61
94
  end
62
95
  end
63
96
 
97
+ # @return [Array<String>] default attributes for events
64
98
  def default_attributes
65
- %w(Timestamp LogicalResourceId ResourceType ResourceStatus ResourceStatusReason)
99
+ %w(time resource_logical_id resource_status resource_status_reason)
66
100
  end
67
101
  end
68
102
  end
@@ -1,20 +1,37 @@
1
- require 'knife-cloudformation/cloudformation_base'
2
- require 'knife-cloudformation/export'
1
+ require 'knife-cloudformation'
3
2
 
4
3
  class Chef
5
4
  class Knife
5
+ # Cloudformation export command
6
6
  class CloudformationExport < Knife
7
7
 
8
- include KnifeCloudformation::KnifeBase
8
+ include KnifeCloudformation::Knife::Base
9
+ include KnifeCloudformation::Utils::ObjectStorage
9
10
 
10
- banner 'knife cloudformation export NAME'
11
+ banner 'knife cloudformation export STACK_NAME'
11
12
 
12
- # TODO: Add option for s3 exports
13
+ option(:export_name,
14
+ :long => '--export-file-name NAME',
15
+ :description => 'File basename to contain the export. Can be callable block if defined within configuration',
16
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export][:name] = v}
17
+ )
13
18
 
14
19
  option(:path,
15
20
  :long => '--export-path PATH',
16
- :description => 'Path to write export JSON file',
17
- :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export_path] = v }
21
+ :description => 'Directory path write export JSON file',
22
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export][:path] = v}
23
+ )
24
+
25
+ option(:bucket,
26
+ :long => '--export-bucket BUCKET_NAME',
27
+ :description => 'Remote file bucket to write export JSON file',
28
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export][:bucket] = v}
29
+ )
30
+
31
+ option(:bucket_prefix,
32
+ :long => '--bucket-key-prefix PREFIX',
33
+ :description => 'Key prefix for file storage in bucket. Can be callable block if defined within configuration',
34
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export][:bucket_prefix] = v}
18
35
  )
19
36
 
20
37
  option(:ignore_parameters,
@@ -22,8 +39,7 @@ class Chef
22
39
  :long => '--exclude-parameter NAME',
23
40
  :description => 'Exclude parameter from export (can be used multiple times)',
24
41
  :proc => lambda{|v|
25
- Chef::Config[:knife][:cloudformation][:ignore_parameters] ||= []
26
- Chef::Config[:knife][:cloudformation][:ignore_parameters].push(v).uniq!
42
+ Chef::Config[:knife][:cloudformation][:export][:ignore_parameters].push(v).uniq!
27
43
  }
28
44
  )
29
45
 
@@ -31,7 +47,7 @@ class Chef
31
47
  :long => '--chef-environment-parameter NAME',
32
48
  :description => 'Parameter used within stack to specify Chef environment',
33
49
  :proc => lambda{|v|
34
- Chef::Config[:knife][:cloudformation][:chef_environment_parameter] = v
50
+ Chef::Config[:knife][:cloudformation][:export][:chef_environment_parameter] = v
35
51
  }
36
52
  )
37
53
 
@@ -40,38 +56,105 @@ class Chef
40
56
  :boolean => true,
41
57
  :default => true,
42
58
  :description => 'Freezes first run files',
43
- :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:chef_popsicle] = v }
59
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export][:chef_popsicle] = v }
44
60
  )
45
61
 
46
- def run
47
- if(Chef::Config[:knife][:cloudformation][:export_path])
48
- file_path = File.join(
49
- Chef::Config[:knife][:cloudformation][:export_path], "#{name_args.first}-#{Time.now.to_i}.json"
50
- )
51
- end
52
- ui.info "#{ui.color('Stack Export:', :bold)} #{name_args.first}"
53
- if(file_path)
54
- ui.info " - Writing to: #{file_path}"
55
- else
56
- ui.info " - Printing to console"
57
- end
62
+ unless(Chef::Config[:knife][:cloudformation].has_key?(:export))
63
+ Chef::Config[:knife][:cloudformation][:export] = Mash.new(
64
+ :credentials => Mash.new,
65
+ :ignore_parameters => []
66
+ )
67
+ end
68
+
69
+ # Run export action
70
+ def _run
71
+ stack_name = name_args.first
72
+ ui.info "#{ui.color('Stack Export:', :bold)} #{stack_name}"
58
73
  ui.confirm 'Perform export'
59
- ex_opts = {}
60
- [:chef_popsicle, :chef_environment_parameter, :ignore_parameters].each do |key|
61
- unless(Chef::Config[:knife][:cloudformation][key].nil?)
62
- ex_opts[key] = Chef::Config[:knife][:cloudformation][key]
74
+ stack = provider.stacks.get(stack_name)
75
+ if(stack)
76
+ export_options = Mash.new.tap do |opts|
77
+ [:chef_popsicle, :chef_environment_parameter, :ignore_parameters].each do |key|
78
+ unless(Chef::Config[:knife][:cloudformation][:export][key].nil?)
79
+ opts[key] = Chef::Config[:knife][:cloudformation][:export][key]
80
+ end
81
+ end
82
+ end
83
+ exporter = KnifeCloudformation::Utils::StackExporter.new(stack, export_options)
84
+ result = exporter.export
85
+ outputs = [
86
+ write_to_file(result, stack),
87
+ write_to_bucket(result, stack)
88
+ ].compact
89
+ if(outputs.empty?)
90
+ ui.warn 'No persistent output location defined. Printing export:'
91
+ ui.info _format_json(result)
63
92
  end
93
+ ui.info "#{ui.color('Stack export', :bold)} (#{name_args.first}): #{ui.color('complete', :green)}"
94
+ unless(outputs.empty?)
95
+ outputs.each do |output|
96
+ ui.info ui.color(" -> #{output}", :blue)
97
+ end
98
+ end
99
+ else
100
+ ui.fatal "Failed to discover requested stack: #{ui.color(stack_name, :red, :bold)}"
101
+ exit -1
64
102
  end
65
- exporter = KnifeCloudformation::Export.new(name_args.first, ex_opts.merge(:aws_commons => aws))
66
- result = exporter.export
67
- if(file_path)
68
- File.open(file_path, 'w') do |file|
69
- file.puts _format_json(result)
103
+ end
104
+
105
+ # Generate file name for stack export JSON contents
106
+ #
107
+ # @param stack [Miasma::Models::Orchestration::Stack]
108
+ # @return [String] file name
109
+ def export_file_name(stack)
110
+ name = Chef::Config[:knife][:cloudformation][:export][:file]
111
+ if(name)
112
+ if(name.respond_to?(:call))
113
+ name.call(stack)
114
+ else
115
+ name.to_s
70
116
  end
71
117
  else
72
- ui.info _format_json(result)
118
+ "#{stack.stack_name}-#{Time.now.to_i}.json"
119
+ end
120
+ end
121
+
122
+ # Write stack export to local file
123
+ #
124
+ # @param payload [Hash] stack export payload
125
+ # @param stack [Misama::Stack::Orchestration::Stack]
126
+ # @return [String, NilClass] path to file
127
+ def write_to_file(payload, stack)
128
+ raise NotImplementedError
129
+ if(Chef::Config[:knife][:cloudformation][:export][:path])
130
+ full_path = File.join(
131
+ File.expand_path(Chef::Config[:knife][:cloudformation][:export][:path]),
132
+ export_file_name(stack)
133
+ )
134
+ _, bucket, path = full_path.split('/', 3)
135
+ directory = provider.service_for(:storage,
136
+ :provider => :local,
137
+ :local_root => '/'
138
+ ).directories.get(bucket)
139
+ file_store(payload, path, directory)
140
+ end
141
+ end
142
+
143
+ # Write stack export to remote bucket
144
+ #
145
+ # @param payload [Hash] stack export payload
146
+ # @param stack [Miasma::Models::Orchestration::Stack]
147
+ # @return [String, NilClass] remote bucket key
148
+ def write_to_bucket(payload, stack)
149
+ raise NotImplementedError
150
+ if(bucket = Chef::Config[:knife][:cloudformation][:export][:bucket])
151
+ key_path = File.join(*[
152
+ bucket_prefix(stack),
153
+ export_file_name(stack)
154
+ ].compact
155
+ )
156
+ file_store(payload, key_path, provider.service_for(:storage).directories.get(bucket))
73
157
  end
74
- ui.info "#{ui.color('Stack export', :bold)} (#{name_args.first}): #{ui.color('complete', :green)}"
75
158
  end
76
159
 
77
160
  end