sfn 0.0.1 → 0.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -0
  3. data/LICENSE +13 -0
  4. data/README.md +142 -61
  5. data/bin/sfn +43 -0
  6. data/lib/chef/knife/knife_plugin_seed.rb +117 -0
  7. data/lib/sfn.rb +17 -0
  8. data/lib/sfn/cache.rb +385 -0
  9. data/lib/sfn/command.rb +45 -0
  10. data/lib/sfn/command/create.rb +87 -0
  11. data/lib/sfn/command/describe.rb +87 -0
  12. data/lib/sfn/command/destroy.rb +74 -0
  13. data/lib/sfn/command/events.rb +98 -0
  14. data/lib/sfn/command/export.rb +103 -0
  15. data/lib/sfn/command/import.rb +117 -0
  16. data/lib/sfn/command/inspect.rb +160 -0
  17. data/lib/sfn/command/list.rb +59 -0
  18. data/lib/sfn/command/promote.rb +17 -0
  19. data/lib/sfn/command/update.rb +95 -0
  20. data/lib/sfn/command/validate.rb +34 -0
  21. data/lib/sfn/command_module.rb +9 -0
  22. data/lib/sfn/command_module/base.rb +150 -0
  23. data/lib/sfn/command_module/stack.rb +166 -0
  24. data/lib/sfn/command_module/template.rb +147 -0
  25. data/lib/sfn/config.rb +106 -0
  26. data/lib/sfn/config/create.rb +35 -0
  27. data/lib/sfn/config/describe.rb +19 -0
  28. data/lib/sfn/config/destroy.rb +9 -0
  29. data/lib/sfn/config/events.rb +25 -0
  30. data/lib/sfn/config/export.rb +29 -0
  31. data/lib/sfn/config/import.rb +24 -0
  32. data/lib/sfn/config/inspect.rb +37 -0
  33. data/lib/sfn/config/list.rb +25 -0
  34. data/lib/sfn/config/promote.rb +23 -0
  35. data/lib/sfn/config/update.rb +20 -0
  36. data/lib/sfn/config/validate.rb +49 -0
  37. data/lib/sfn/monkey_patch.rb +8 -0
  38. data/lib/sfn/monkey_patch/stack.rb +200 -0
  39. data/lib/sfn/provider.rb +224 -0
  40. data/lib/sfn/utils.rb +23 -0
  41. data/lib/sfn/utils/debug.rb +31 -0
  42. data/lib/sfn/utils/json.rb +37 -0
  43. data/lib/sfn/utils/object_storage.rb +28 -0
  44. data/lib/sfn/utils/output.rb +79 -0
  45. data/lib/sfn/utils/path_selector.rb +99 -0
  46. data/lib/sfn/utils/ssher.rb +29 -0
  47. data/lib/sfn/utils/stack_exporter.rb +275 -0
  48. data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
  49. data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
  50. data/lib/sfn/version.rb +4 -0
  51. data/sfn.gemspec +19 -0
  52. metadata +110 -4
@@ -0,0 +1,45 @@
1
+ require 'sfn'
2
+ require 'bogo-cli'
3
+
4
+ module Sfn
5
+ class Command < Bogo::Cli::Command
6
+
7
+ autoload :Create, 'sfn/command/create'
8
+ autoload :Describe, 'sfn/command/describe'
9
+ autoload :Destroy, 'sfn/command/destroy'
10
+ autoload :Events, 'sfn/command/events'
11
+ autoload :Export, 'sfn/command/export'
12
+ autoload :Import, 'sfn/command/import'
13
+ autoload :Inspect, 'sfn/command/inspect'
14
+ autoload :List, 'sfn/command/list'
15
+ autoload :Promote, 'sfn/command/promote'
16
+ autoload :Update, 'sfn/command/update'
17
+ autoload :Validate, 'sfn/command/validate'
18
+
19
+ # Override to provide config file searching
20
+ def initialize(opts, args)
21
+ unless(opts[:config])
22
+ opts = opts.to_hash.to_smash(:snake)
23
+ discover_config(opts)
24
+ end
25
+ super
26
+ end
27
+
28
+ protected
29
+
30
+ # Start with current working directory and traverse to root
31
+ # looking for a `.sfn` configuration file
32
+ #
33
+ # @param opts [Smash]
34
+ # @return [Smash]
35
+ def discover_config(opts)
36
+ cwd = Dir.pwd.split(File::SEPARATOR)
37
+ until(cwd.empty? || File.exists?(cwd.push('.sfn').join(File::SEPARATOR)))
38
+ cwd.pop(2)
39
+ end
40
+ opts[:config] = cwd.join(File::SEPARATOR) unless cwd.empty?
41
+ opts
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ require 'sparkle_formation'
2
+ require 'pathname'
3
+ require 'sfn'
4
+
5
+ module Sfn
6
+ class Command
7
+ # Cloudformation create command
8
+ class Create < Command
9
+
10
+ include Sfn::CommandModule::Base
11
+ include Sfn::CommandModule::Template
12
+ include Sfn::CommandModule::Stack
13
+
14
+ # Run the stack creation command
15
+ def execute!
16
+ name = name_args.first
17
+ unless(name)
18
+ ui.fatal "Formation name must be specified!"
19
+ exit 1
20
+ end
21
+ if(config[:template])
22
+ file = config[:template]
23
+ else
24
+ file = load_template_file
25
+ nested_stacks_unpack = file.delete('sfn_nested_stack')
26
+ end
27
+ ui.info "#{ui.color('Cloud Formation:', :bold)} #{ui.color('create', :green)}"
28
+ stack_info = "#{ui.color('Name:', :bold)} #{name}"
29
+ if(config[:path])
30
+ stack_info << " #{ui.color('Path:', :bold)} #{config[:file]}"
31
+ end
32
+
33
+ unless(config[:print_only])
34
+ ui.info " -> #{stack_info}"
35
+ end
36
+
37
+ if(nested_stacks_unpack)
38
+ unpack_nesting(name, file, :create)
39
+ else
40
+
41
+ stack = provider.connection.stacks.build(
42
+ config[:options].dup.merge(
43
+ :name => name,
44
+ :template => file
45
+ )
46
+ )
47
+
48
+ apply_stacks!(stack)
49
+ stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
50
+
51
+ if(config[:print_only])
52
+ ui.info _format_json(translate_template(stack.template))
53
+ return
54
+ end
55
+
56
+ populate_parameters!(stack.template)
57
+ stack.parameters = config[:parameters]
58
+
59
+ stack.template = translate_template(stack.template)
60
+ stack.save
61
+
62
+ end
63
+
64
+ if(stack)
65
+ if(config[:poll])
66
+ poll_stack(stack.name)
67
+ stack = provider.connection.stacks.get(name)
68
+
69
+ if(stack.reload.success?)
70
+ ui.info "Stack create complete: #{ui.color('SUCCESS', :green)}"
71
+ namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
72
+ else
73
+ ui.fatal "Create of new stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
74
+ ui.info ""
75
+ namespace.const_get(:Inspect).new({:instance_failure => true}, [name]).execute!
76
+ raise
77
+ end
78
+ else
79
+ ui.warn 'Stack state polling has been disabled.'
80
+ ui.info "Stack creation initialized for #{ui.color(name, :green)}"
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,87 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Command
5
+ # Cloudformation describe command
6
+ class Describe < Command
7
+
8
+ include Sfn::CommandModule::Base
9
+
10
+ # information available
11
+ unless(defined?(AVAILABLE_DISPLAYS))
12
+ AVAILABLE_DISPLAYS = [:resources, :outputs]
13
+ end
14
+
15
+ # Run the stack describe action
16
+ def execute!
17
+ stack_name = name_args.last
18
+ stack = provider.connection.stacks.get(stack_name)
19
+ if(stack)
20
+ display = [].tap do |to_display|
21
+ AVAILABLE_DISPLAYS.each do |display_option|
22
+ if(config[display_option])
23
+ to_display << display_option
24
+ end
25
+ end
26
+ end
27
+ display = AVAILABLE_DISPLAYS.dup if display.empty?
28
+ display.each do |display_method|
29
+ self.send(display_method, stack)
30
+ end
31
+ else
32
+ ui.fatal "Failed to find requested stack: #{ui.color(stack_name, :bold, :red)}"
33
+ exit -1
34
+ end
35
+ end
36
+
37
+ # Display resources
38
+ #
39
+ # @param stack [Miasma::Models::Orchestration::Stack]
40
+ def resources(stack)
41
+ stack_resources = stack.resources.all.sort do |x, y|
42
+ y.updated <=> x.updated
43
+ end.map do |resource|
44
+ Smash.new(resource.attributes)
45
+ end
46
+ ui.table(self) do
47
+ table(:border => false) do
48
+ row(:header => true) do
49
+ allowed_attributes.each do |attr|
50
+ column as_title(attr), :width => stack_resources.map{|r| r[attr].to_s.length}.push(as_title(attr).length).max + 2
51
+ end
52
+ end
53
+ stack_resources.each do |resource|
54
+ row do
55
+ allowed_attributes.each do |attr|
56
+ column resource[attr]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end.display
62
+ end
63
+
64
+ # Display outputs
65
+ #
66
+ # @param stack [Miasma::Models::Orchestration::Stack]
67
+ def outputs(stack)
68
+ ui.info "Outputs for stack: #{ui.color(stack.name, :bold)}"
69
+ unless(stack.outputs.empty?)
70
+ stack.outputs.each do |output|
71
+ key, value = output.key, output.value
72
+ key = snake(key).to_s.split('_').map(&:capitalize).join(' ')
73
+ ui.info [' ', ui.color("#{key}:", :bold), value].join(' ')
74
+ end
75
+ else
76
+ ui.info " #{ui.color('No outputs found')}"
77
+ end
78
+ end
79
+
80
+ # @return [Array<String>] default attributes
81
+ def default_attributes
82
+ %w(updated logical_id type status status_reason)
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,74 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Command
5
+ class Destroy < Command
6
+
7
+ include Sfn::CommandModule::Base
8
+
9
+ # Run the stack destruction action
10
+ def execute!
11
+ stacks = name_args.sort
12
+ plural = 's' if stacks.size > 1
13
+ globs = stacks.find_all do |s|
14
+ s !~ /^[a-zA-Z0-9-]+$/
15
+ end
16
+ unless(globs.empty?)
17
+ glob_stacks = provider.connection.stacks.all.find_all do |remote_stack|
18
+ globs.detect do |glob|
19
+ File.fnmatch(glob, remote_stack.name)
20
+ end
21
+ end
22
+ stacks += glob_stacks.map(&:name)
23
+ stacks -= globs
24
+ stacks.sort!
25
+ end
26
+ ui.warn "Destroying Stack#{plural}: #{ui.color(stacks.join(', '), :bold)}"
27
+ ui.confirm "Destroy listed stack#{plural}?"
28
+ stacks.each do |stack_name|
29
+ stack = provider.connection.stacks.get(stack_name)
30
+ if(stack)
31
+ nested_stack_cleanup!(stack)
32
+ stack.destroy
33
+ ui.info "Destroy request complete for stack: #{ui.color(stack_name, :red)}"
34
+ else
35
+ ui.warn "Failed to locate requested stack: #{ui.color(stack_name, :bold)}"
36
+ end
37
+ end
38
+ if(config[:poll])
39
+ if(stacks.size == 1)
40
+ poll_stack(stacks.first)
41
+ else
42
+ ui.error "Stack polling is not available when multiple stack deletion is requested!"
43
+ end
44
+ end
45
+ ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
46
+ end
47
+
48
+ # Cleanup persisted templates if nested stack resources are included
49
+ def nested_stack_cleanup!(stack)
50
+ nest_stacks = stack.template.fetch('Resources', {}).values.find_all do |resource|
51
+ resource['Type'] == 'AWS::CloudFormation::Stack'
52
+ end.each do |resource|
53
+ url = resource['Properties']['TemplateURL']
54
+ if(url)
55
+ _, bucket_name, path = URI.parse(url).path.split('/', 3)
56
+ bucket = provider.connection.api_for(:storage).buckets.get(bucket_name)
57
+ if(bucket)
58
+ file = bucket.files.get(path)
59
+ if(file)
60
+ file.destroy
61
+ ui.info "Deleted nested stack template! (Bucket: #{bucket_name} Template: #{path})"
62
+ else
63
+ ui.warn "Failed to locate template file within bucket for deletion! (#{path})"
64
+ end
65
+ else
66
+ ui.warn "Failed to locate bucket containing template file for deletion! (#{bucket_name})"
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,98 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Command
5
+ # Events command
6
+ class Events < Command
7
+
8
+ include Sfn::CommandModule::Base
9
+
10
+ # @return [Miasma::Models::Orchestration::Stack]
11
+ attr_reader :stack
12
+
13
+ # Run the events list action
14
+ def execute!
15
+ name = name_args.first
16
+ ui.info "Events for Stack: #{ui.color(name, :bold)}\n"
17
+ @stacks = []
18
+ @stack = provider.connection.stacks.get(name)
19
+ @stacks << stack
20
+ discover_stacks(stack)
21
+ if(stack)
22
+ table = ui.table(self) do
23
+ table(:border => false) do
24
+ events = get_events
25
+ row(:header => true) do
26
+ allowed_attributes.each do |attr|
27
+ column attr.split('_').map(&:capitalize).join(' '), :width => ((val = events.map{|e| e[attr].to_s.length}.push(attr.length).max + 2) > 70 ? 70 : val)
28
+ end
29
+ end
30
+ events.each do |event|
31
+ row do
32
+ allowed_attributes.each do |attr|
33
+ column event[attr]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end.display
39
+ if(config[:poll])
40
+ while(stack.in_progress?)
41
+ to_wait = config.fetch(:poll_wait_time, 10).to_f
42
+ while(to_wait > 0)
43
+ sleep(0.1)
44
+ to_wait -= 0.1
45
+ end
46
+ stack.reload
47
+ table.display
48
+ end
49
+ end
50
+ else
51
+ ui.fatal "Failed to locate requested stack: #{ui.color(name, :bold, :red)}"
52
+ raise "Failed to locate stack: #{name}!"
53
+ end
54
+ end
55
+
56
+ # Fetch events from stack
57
+ #
58
+ # @param stack [Miasma::Models::Orchestration::Stack]
59
+ # @param last_id [String] only return events after this ID
60
+ # @return [Array<Hash>]
61
+ def get_events(*args)
62
+ discover_stacks(stack)
63
+ stack_events = @stacks.map do |stack|
64
+ stack.events.all.map do |e|
65
+ e.attributes.merge(:stack_name => stack.name).to_smash
66
+ end
67
+ end.flatten.compact
68
+ stack_events.sort do |x,y|
69
+ Time.parse(x[:time].to_s) <=> Time.parse(y[:time].to_s)
70
+ end
71
+ end
72
+
73
+ def discover_stacks(stack)
74
+ stack.resources.reload.all.each do |resource|
75
+ if(resource.type == 'AWS::CloudFormation::Stack')
76
+ nested_stack = provider.connection.stacks.get(resource.id)
77
+ @stacks.push(nested_stack).uniq!
78
+ discover_stacks(nested_stack)
79
+ end
80
+ end
81
+ end
82
+
83
+ # @return [Array<String>] default attributes for events
84
+ def default_attributes
85
+ %w(stack_name time resource_logical_id resource_status resource_status_reason)
86
+ end
87
+
88
+ # @return [Array<String>] allowed attributes for events
89
+ def allowed_attributes
90
+ result = super
91
+ unless(@stacks.size > 1)
92
+ result.delete('stack_name')
93
+ end
94
+ result
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,103 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Command
5
+ # Export command
6
+ class Export < Command
7
+
8
+ include Sfn::CommandModule::Base
9
+ include Sfn::Utils::ObjectStorage
10
+
11
+ # Run export action
12
+ def execute!
13
+ raise NotImplementedError.new 'Implementation updates required'
14
+ stack_name = name_args.first
15
+ ui.info "#{ui.color('Stack Export:', :bold)} #{stack_name}"
16
+ ui.confirm 'Perform export'
17
+ stack = provider.stacks.get(stack_name)
18
+ if(stack)
19
+ export_options = Smash.new.tap do |opts|
20
+ [:chef_popsicle, :chef_environment_parameter, :ignore_parameters].each do |key|
21
+ opts[key] = config[key] unless config[key].nil?
22
+ end
23
+ end
24
+ exporter = Sfn::Utils::StackExporter.new(stack, export_options)
25
+ result = exporter.export
26
+ outputs = [
27
+ write_to_file(result, stack),
28
+ write_to_bucket(result, stack)
29
+ ].compact
30
+ if(outputs.empty?)
31
+ ui.warn 'No persistent output location defined. Printing export:'
32
+ ui.info _format_json(result)
33
+ end
34
+ ui.info "#{ui.color('Stack export', :bold)} (#{name_args.first}): #{ui.color('complete', :green)}"
35
+ unless(outputs.empty?)
36
+ outputs.each do |output|
37
+ ui.info ui.color(" -> #{output}", :blue)
38
+ end
39
+ end
40
+ else
41
+ ui.fatal "Failed to discover requested stack: #{ui.color(stack_name, :red, :bold)}"
42
+ exit -1
43
+ end
44
+ end
45
+
46
+ # Generate file name for stack export JSON contents
47
+ #
48
+ # @param stack [Miasma::Models::Orchestration::Stack]
49
+ # @return [String] file name
50
+ def export_file_name(stack)
51
+ name = config[:file]
52
+ if(name)
53
+ if(name.respond_to?(:call))
54
+ name.call(stack)
55
+ else
56
+ name.to_s
57
+ end
58
+ else
59
+ "#{stack.stack_name}-#{Time.now.to_i}.json"
60
+ end
61
+ end
62
+
63
+ # Write stack export to local file
64
+ #
65
+ # @param payload [Hash] stack export payload
66
+ # @param stack [Misama::Stack::Orchestration::Stack]
67
+ # @return [String, NilClass] path to file
68
+ def write_to_file(payload, stack)
69
+ raise NotImplementedError
70
+ if(config[:path])
71
+ full_path = File.join(
72
+ config[:path],
73
+ export_file_name(stack)
74
+ )
75
+ _, bucket, path = full_path.split('/', 3)
76
+ directory = provider.service_for(:storage,
77
+ :provider => :local,
78
+ :local_root => '/'
79
+ ).directories.get(bucket)
80
+ file_store(payload, path, directory)
81
+ end
82
+ end
83
+
84
+ # Write stack export to remote bucket
85
+ #
86
+ # @param payload [Hash] stack export payload
87
+ # @param stack [Miasma::Models::Orchestration::Stack]
88
+ # @return [String, NilClass] remote bucket key
89
+ def write_to_bucket(payload, stack)
90
+ raise NotImplementedError
91
+ if(bucket = config[:bucket])
92
+ key_path = File.join(*[
93
+ bucket_prefix(stack),
94
+ export_file_name(stack)
95
+ ].compact
96
+ )
97
+ file_store(payload, key_path, provider.service_for(:storage).directories.get(bucket))
98
+ end
99
+ end
100
+
101
+ end
102
+ end
103
+ end