sfn 0.0.1 → 0.3.0

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