knife-cloudformation 0.1.22 → 0.2.0

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