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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -0
- data/LICENSE +13 -0
- data/README.md +142 -61
- data/bin/sfn +43 -0
- data/lib/chef/knife/knife_plugin_seed.rb +117 -0
- data/lib/sfn.rb +17 -0
- data/lib/sfn/cache.rb +385 -0
- data/lib/sfn/command.rb +45 -0
- data/lib/sfn/command/create.rb +87 -0
- data/lib/sfn/command/describe.rb +87 -0
- data/lib/sfn/command/destroy.rb +74 -0
- data/lib/sfn/command/events.rb +98 -0
- data/lib/sfn/command/export.rb +103 -0
- data/lib/sfn/command/import.rb +117 -0
- data/lib/sfn/command/inspect.rb +160 -0
- data/lib/sfn/command/list.rb +59 -0
- data/lib/sfn/command/promote.rb +17 -0
- data/lib/sfn/command/update.rb +95 -0
- data/lib/sfn/command/validate.rb +34 -0
- data/lib/sfn/command_module.rb +9 -0
- data/lib/sfn/command_module/base.rb +150 -0
- data/lib/sfn/command_module/stack.rb +166 -0
- data/lib/sfn/command_module/template.rb +147 -0
- data/lib/sfn/config.rb +106 -0
- data/lib/sfn/config/create.rb +35 -0
- data/lib/sfn/config/describe.rb +19 -0
- data/lib/sfn/config/destroy.rb +9 -0
- data/lib/sfn/config/events.rb +25 -0
- data/lib/sfn/config/export.rb +29 -0
- data/lib/sfn/config/import.rb +24 -0
- data/lib/sfn/config/inspect.rb +37 -0
- data/lib/sfn/config/list.rb +25 -0
- data/lib/sfn/config/promote.rb +23 -0
- data/lib/sfn/config/update.rb +20 -0
- data/lib/sfn/config/validate.rb +49 -0
- data/lib/sfn/monkey_patch.rb +8 -0
- data/lib/sfn/monkey_patch/stack.rb +200 -0
- data/lib/sfn/provider.rb +224 -0
- data/lib/sfn/utils.rb +23 -0
- data/lib/sfn/utils/debug.rb +31 -0
- data/lib/sfn/utils/json.rb +37 -0
- data/lib/sfn/utils/object_storage.rb +28 -0
- data/lib/sfn/utils/output.rb +79 -0
- data/lib/sfn/utils/path_selector.rb +99 -0
- data/lib/sfn/utils/ssher.rb +29 -0
- data/lib/sfn/utils/stack_exporter.rb +275 -0
- data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
- data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
- data/lib/sfn/version.rb +4 -0
- data/sfn.gemspec +19 -0
- metadata +110 -4
    
        data/lib/sfn/command.rb
    ADDED
    
    | @@ -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
         |