class CfnFlow::CLI

Public Class Methods

exit_on_failure?() click to toggle source
# File lib/cfn_flow/cli.rb, line 4
def self.exit_on_failure?
  CfnFlow.exit_on_failure?
end

Public Instance Methods

delete(name) click to toggle source
# File lib/cfn_flow/cli.rb, line 194
def delete(name)
  stack = find_stack_in_service(name)
  if options[:force] || yes?("Are you sure you want to shut down #{name}?", :red)
    stack.delete
    say "Deleted stack #{name}"

    say "Polling for events..."
    invoke :events, [stack.name], ['--poll']
  end
end
deploy(environment) click to toggle source
# File lib/cfn_flow/cli.rb, line 54
def deploy(environment)
  # Export environment as an env var so it can be interpolated in config
  ENV['CFN_FLOW_ENVIRONMENT'] = environment

  begin
    params = CfnFlow.stack_params(environment)
    stack = CfnFlow.cfn_resource.create_stack(params)
  rescue Aws::CloudFormation::Errors::ValidationError => e
    raise Thor::Error.new(e.message)
  end

  say "Launching stack #{stack.name}"

  # Invoke events
  say "Polling for events..."
  invoke :events, [stack.name], ['--poll']

  say "Stack Outputs:"
  invoke :show, [stack.name], ['--format=outputs-table']

  # Optionally cleanup other stacks in this environment
  if options[:cleanup]
    puts "Finding stacks to clean up"
    list_stacks_in_service.select {|s|
      s.name != stack.name && \
        s.tags.any? {|tag| tag.key == 'CfnFlowEnvironment' && tag.value == environment }
    }.map(&:name).each do |name|
      delete(name)
    end
  end
end
deploy ENVIRONMENT() click to toggle source

Stack methods

# File lib/cfn_flow/cli.rb, line 52
desc 'deploy ENVIRONMENT', 'Launch a stack'
events(name) click to toggle source
# File lib/cfn_flow/cli.rb, line 171
def events(name)
  stack = find_stack_in_service(name)

  say EventPresenter.header unless options['no-header']
  EventPresenter.present(stack.events) {|p| say p }

  if options[:poll]
    # Display events until we're COMPLETE/FAILED
    delay = (ENV['CFN_FLOW_EVENT_POLL_INTERVAL'] || 2).to_i
    begin
      stack.wait_until(max_attempts: -1, delay: delay) do |s|
        EventPresenter.present(s.events) {|p| say p }
        # Wait until the stack status ends with _FAILED or _COMPLETE
        s.stack_status.match(/_(FAILED|COMPLETE)$/)
      end
    rescue Aws::CloudFormation::Errors::ValidationError
      # The stack was deleted. Keep on trucking.
    end
  end
end
list(environment=nil) click to toggle source
# File lib/cfn_flow/cli.rb, line 123
def list(environment=nil)
  stacks = list_stacks_in_service
  if environment
    stacks.select! do |stack|
      stack.tags.any? {|tag| tag.key == 'CfnFlowEnvironment' && tag.value == environment }
    end
  end

  return if stacks.empty?

  table_header = options['no-header'] ? [] : [['NAME',  'ENVIRONMENT', 'STATUS', 'CREATED']]
  table_data = stacks.map do |s|
    env_tag = s.tags.detect {|tag| tag.key == 'CfnFlowEnvironment'}
    env = env_tag ? env_tag.value : 'NONE'

    [ s.name, env, s.stack_status, s.creation_time ]
  end

  print_table(table_header + table_data)
end
publish(*templates) click to toggle source
# File lib/cfn_flow/cli.rb, line 33
def publish(*templates)
  if templates.empty?
    raise Thor::RequiredArgumentMissingError.new('You must specify a template to publish')
  end

  validate(*templates)

  release = publish_release
  templates.each do |path|
    t = Template.new(path)

    say "Publishing #{t.local_path} to #{t.url(release)}"
    t.upload(release)
  end
end
show(name) click to toggle source
# File lib/cfn_flow/cli.rb, line 146
def show(name)
  formatters = {
    'json' =>          ->(stack) { say MultiJson.dump(stack.data.to_hash, pretty: true) },
    'yaml' =>          ->(stack) { say stack.data.to_hash.to_yaml },
    'outputs-table' => ->(stack) do
      outputs = stack.outputs.to_a
      if outputs.any?
        table_header = [['KEY',  'VALUE', 'DESCRIPTION']]
        table_data = outputs.map do |s|
          [ s.output_key, s.output_value, s.description ]
        end

        print_table(table_header + table_data)
      else
        say "No stack outputs to show."
      end
    end
  }
  stack = find_stack_in_service(name)
  formatters[options[:format]].call(stack)
end
update(environment, name) click to toggle source
# File lib/cfn_flow/cli.rb, line 87
def update(environment, name)
  # Export environment as an env var so it can be interpolated in config
  ENV['CFN_FLOW_ENVIRONMENT'] = environment

  stack = find_stack_in_service(name)

  # Check that environment matches
  unless stack.tags.any?{|tag| tag.key == 'CfnFlowEnvironment' && tag.value == environment }
    raise Thor::Error.new "Stack #{name} is not tagged for environment #{environment}"
  end

  begin
    params = CfnFlow.stack_params(environment)
    params.delete(:tags) # No allowed for Stack#update
    stack.update(params)
  rescue Aws::CloudFormation::Errors::ValidationError => e
    raise Thor::Error.new(e.message)
  end

  say "Updating stack #{stack.name}"

  # NB: there's a potential race condition where polling for events would
  # see the last complete state before the stack has a chance to begin updating.
  # Consider putting a sleep, wait_for an UPDATE_IN_PROGRESS state beforehand,
  # or look for events newer than the last event before updating.

  # Invoke events
  say "Polling for events..."
  invoke :events, [stack.name], ['--poll']

  say "Stack Outputs:"
  invoke :show, [stack.name], ['--format=outputs-table']
end
validate(*templates) click to toggle source
# File lib/cfn_flow/cli.rb, line 12
def validate(*templates)

  if templates.empty?
    raise Thor::RequiredArgumentMissingError.new('You must specify a template to validate')
  end

  templates.map{|path| Template.new(path) }.each do |template|
    say "Validating #{template.local_path}... "
    template.validate!
    say 'valid.', :green
  end
rescue Aws::CloudFormation::Errors::ValidationError => e
  raise Thor::Error.new("Invalid template. Message: #{e.message}")
rescue CfnFlow::Template::Error => e
  raise Thor::Error.new("Error loading template. (#{e.class}) Message: #{e.message}")
end
validate TEMPLATE [...]() click to toggle source

Template methods

# File lib/cfn_flow/cli.rb, line 11
desc 'validate TEMPLATE [...]', 'Validates templates'
version() click to toggle source

Version command

# File lib/cfn_flow/cli.rb, line 207
desc "version", "Prints the version information"

Private Instance Methods

find_stack_in_service(name) click to toggle source
# File lib/cfn_flow/cli.rb, line 214
def find_stack_in_service(name)
  stack = CfnFlow.cfn_resource.stack(name).load
  unless stack.tags.any? {|tag| tag.key == 'CfnFlowService' && tag.value == CfnFlow.service }
    raise Thor::Error.new "Stack #{name} is not tagged for service #{CfnFlow.service}"
  end
  stack
rescue Aws::CloudFormation::Errors::ValidationError => e
  # Handle missing stacks: 'Stack with id blah does not exist'
  raise Thor::Error.new(e.message)
end
list_stacks_in_service() click to toggle source
# File lib/cfn_flow/cli.rb, line 225
def list_stacks_in_service
  CfnFlow.cfn_resource.stacks.select do |stack|
    stack.tags.any? {|tag| tag.key == 'CfnFlowService' && tag.value == CfnFlow.service }
  end
end
publish_release() click to toggle source
# File lib/cfn_flow/cli.rb, line 231
def publish_release
  # Add the release or dev name to the prefix
  if options[:release]
    release = options[:release] == true ? CfnFlow::Git.sha : options[:release]
    'release/' + release
  elsif options['dev-name']
    'dev/' + options['dev-name']
  elsif ENV['CFN_FLOW_DEV_NAME']
    'dev/' + ENV['CFN_FLOW_DEV_NAME']
  else
    raise Thor::Error.new("Must specify --release or --dev-name; or set CFN_FLOW_DEV_NAME env var")
  end
end