class Hygroscope::Cli

Public Class Methods

new(*args) click to toggle source
Calls superclass method
# File lib/hygroscope/cli.rb, line 7
def initialize(*args)
  super(*args)
end

Public Instance Methods

check_path() click to toggle source
# File lib/hygroscope/cli.rb, line 38
def check_path
  say_fail('Hygroscope must be run from the top level of a hygroscopic directory.') unless
    File.directory?(File.join(Dir.pwd, 'template')) &&
    File.directory?(File.join(Dir.pwd, 'paramsets'))
end
colorize_status(status) click to toggle source
# File lib/hygroscope/cli.rb, line 17
def colorize_status(status)
  case status.downcase
  when /failed$/
    set_color(status, :red)
  when /progress$/
    set_color(status, :yellow)
  when /complete$/
    set_color(status, :green)
  end
end
countdown(text, time = 5) click to toggle source
# File lib/hygroscope/cli.rb, line 28
def countdown(text, time = 5)
  print "#{text}  "
  time.downto(0) do |i|
    $stdout.write("\b")
    $stdout.write(i)
    $stdout.flush
    sleep 1
  end
end
create() click to toggle source
# File lib/hygroscope/cli.rb, line 244
def create
  check_path
  validate

  # Prepare task takes care of shared logic between "create" and "update"
  template, paramset = prepare('create')

  stack              = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])
  stack.parameters   = paramset.parameters
  stack.template     = options[:compress] ? template.compress : template.process
  stack.capabilities = options[:capabilities]
  stack.timeout      = 60

  stack.tags['X-Hygroscope-Template'] = hygro_name
  options[:tags].each do |tag|
    if paramset.get(tag)
      stack.tags[tag] = paramset.get(tag)
    else
      say_status('info', "Skipping tag #{tag} because it does not exist", :blue)
    end
  end

  stack.create!
  status
end
delete() click to toggle source
# File lib/hygroscope/cli.rb, line 327
def delete
  check_path
  abort unless options[:force] ||
               yes?("Really delete stack #{options[:name]} [y/N]?")

  say('Deleting stack!')
  stack = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])
  stack.delete!
  status
end
generate() click to toggle source
# File lib/hygroscope/cli.rb, line 412
def generate
  check_path
  t = Hygroscope::Template.new(template_path, options[:region], options[:profile])
  if options[:color]
    require 'json_color'
    puts JsonColor.colorize(t.process)
  else
    puts t.process
  end
end
hygro_name() click to toggle source
# File lib/hygroscope/cli.rb, line 48
def hygro_name
  File.basename(Dir.pwd)
end
hygro_path() click to toggle source
# File lib/hygroscope/cli.rb, line 44
def hygro_path
  Dir.pwd
end
paramset() click to toggle source
# File lib/hygroscope/cli.rb, line 446
def paramset
  if options[:name]
    say_paramset(options[:name])
  else
    say_paramset_list
  end
end
prepare(action = 'create') click to toggle source
# File lib/hygroscope/cli.rb, line 95
def prepare(action = 'create')
  # Generate the template
  t = Hygroscope::Template.new(template_path, options[:region], options[:profile])

  # If the paramset exists load it, otherwise instantiate an empty one
  p = Hygroscope::ParamSet.new(options[:paramset])

  if options[:paramset]
    # User provided a paramset, so load it and determine which parameters
    # are set and which need to be prompted.
    paramset_keys = p.parameters.keys
    template_keys = t.parameters.keys

    # Reject any keys in paramset that are not requested by template
    rejected_keys = paramset_keys - template_keys
    say_status('info', "Keys in paramset not requested by template: #{rejected_keys.join(', ')}", :blue) unless rejected_keys.empty?

    # Prompt for any key that is missing. If "ask" option was passed,
    # prompt for every key.
    missing = options[:ask] ? template_keys : template_keys - paramset_keys
  else
    # No paramset provided, so every parameter is missing!
    missing = if t.parameters.empty?
                {}
              else
                t.parameters.keys
              end
  end

  options[:existing].each do |existing|
    # User specified an existing stack from which to pull outputs and
    # translate into parameters. Load the existing stack.
    e = Hygroscope::Stack.new(existing, options[:region], options[:profile])
    say_status('info', "Populating parameters from #{existing} stack", :blue)

    # Fill any template parameter that matches an output from the existing
    # stack, overwriting values from the paramset object. The user will
    # be prompted to change these if they were not in the paramset or the
    # --ask option was passed.
    e.describe.outputs.each do |o|
      p.set(o.output_key, o.output_value) if t.parameters.keys.include?(o.output_key)
    end
  end if options[:existing].is_a?(Array)

  # If this is an update and ask was not specified, set any missing
  # parameters to use_previous_value
  if action == 'update' && !options[:ask]
    missing.each do |key|
      p.set(key, nil, use_previous_value: true) unless p.get(key)
    end
  else
    # Prompt for each missing parameter and save it in the paramset object
    missing.each do |key|
      # Do not prompt for keys prefixed with the "Hygroscope" reserved word.
      # These parameters are populated internally without user input.
      next if key =~ /^Hygroscope/

      type = t.parameters[key]['Type']
      default = p.get(key) ? p.get(key) : t.parameters[key]['Default'] || ''
      description = t.parameters[key]['Description'] || false
      values = t.parameters[key]['AllowedValues'] || false
      no_echo = t.parameters[key]['NoEcho'] || false

      # Thor conveniently provides some nice logic for formatting,
      # allowing defaults, and validating user input
      ask_opts = {}
      ask_opts[:default] = default unless default.to_s.empty?
      ask_opts[:limited_to] = values if values
      ask_opts[:echo] = false if no_echo

      puts
      say("#{description} (#{type})") if description
      # Make sure user enters a value
      # TODO: Better input validation
      answer = ''
      answer = ask(key, :cyan, ask_opts) until answer != ''

      # Save answer to paramset object
      p.set(key, answer)

      # Add a line break
      say if no_echo
    end

    # Offer to save paramset if it was modified
    # Filter out keys beginning with "Hygroscope" since they are not visible
    # to the user and may be modified on each invocation.
    unless missing.reject { |k| k =~ /^Hygroscope/ }.empty?
      puts
      if yes?('Save changes to paramset?')
        unless options[:paramset]
          p.name = ask('Paramset name', :cyan, default: options[:name])
        end
        p.save!
      end
    end
  end

  # Upload payload
  payload_path = File.join(Dir.pwd, 'payload')
  if File.directory?(payload_path)
    payload = Hygroscope::Payload.new(payload_path, options[:region], options[:profile])
    payload.prefix = options[:name]
    payload.upload!
    p.set('HygroscopePayloadBucket', payload.bucket) if missing.include?('HygroscopePayloadBucket')
    p.set('HygroscopePayloadKey', payload.key) if missing.include?('HygroscopePayloadKey')
    p.set('HygroscopePayloadSignedUrl', payload.generate_url) if missing.include?('HygroscopePayloadSignedUrl')
    say_status('ok', 'Payload uploaded to:', :green)
    say_status('', "s3://#{payload.bucket}/#{payload.key}")
  end

  [t, p]
end
say_fail(message) click to toggle source
# File lib/hygroscope/cli.rb, line 12
def say_fail(message)
  say_status('error', message, :red)
  abort
end
say_paramset(name) click to toggle source
# File lib/hygroscope/cli.rb, line 56
def say_paramset(name)
  begin
    p = Hygroscope::ParamSet.new(name)
  rescue Hygroscope::ParamSetNotFoundError
    say_fail("Paramset #{name} does not exist!")
  end

  say "Parameters for '#{File.basename(Dir.pwd)}' paramset '#{p.name}':", :yellow
  print_table p.parameters, indent: 2
  say "\nTo edit existing parameters, use the 'create' command with the --ask flag."
end
say_paramset_list() click to toggle source
# File lib/hygroscope/cli.rb, line 68
def say_paramset_list
  files = Dir.glob(File.join(hygro_path, 'paramsets', '*.{yml,yaml}'))

  say_fail("No saved paramsets for #{hygro_name}.") if files.empty?

  say "Saved paramsets for '#{hygro_name}':", :yellow
  files.map do |f|
    say '  ' + File.basename(f, File.extname(f))
  end
  say "\nTo list parameters in a set, use the --name option."
end
status() click to toggle source
# File lib/hygroscope/cli.rb, line 343
def status
  check_path
  stack = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])

  # Query and display the status of the stack and its resources. Refresh
  # every 10 seconds until the user aborts or an error is encountered.
  loop do
    begin
      s = stack.describe

      system('clear') || system('cls')

      header = {
        'Name:'    => s.stack_name,
        'Created:' => s.creation_time,
        'Status:'  => colorize_status(s.stack_status)
      }

      print_table header
      puts

      # Fancy acrobatics to fit output to terminal width. If the terminal
      # window is too small, fallback to something appropriate for ~80 chars
      term_width = `stty size 2>/dev/null`.split[1].to_i || `tput cols 2>/dev/null`.to_i
      type_width   = term_width < 80 ? 30 : term_width - 50
      output_width = term_width < 80 ? 54 : term_width - 31

      # Header row
      puts set_color(format(' %-28s %-*s %-18s ', 'Resource', type_width, 'Type', 'Status'), :white, :on_blue)
      resources = stack.list_resources
      resources.each do |r|
        puts format(' %-28s %-*s %-18s ', r[:name][0..26], type_width, r[:type][0..type_width], colorize_status(r[:status]))
      end

      if s.stack_status.downcase =~ /complete$/
        # If the stack is complete display any available outputs and stop refreshing
        puts
        puts set_color(format(' %-28s %-*s ', 'Output', output_width, 'Value'), :white, :on_yellow)
        s.outputs.each do |o|
          puts format(' %-28s %-*s ', o.output_key, output_width, o.output_value)
        end

        puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
        break
      elsif s.stack_status.downcase =~ /failed$/
        # If the stack failed to create, stop refreshing
        puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
        break
      else
        puts "\nMore information: https://console.aws.amazon.com/cloudformation/home"
        countdown('Updating in', 9)
        puts
      end
    rescue Aws::CloudFormation::Errors::ValidationError
      say_fail('Stack not found')
    rescue Interrupt
      abort
    end
  end
end
template_path() click to toggle source
# File lib/hygroscope/cli.rb, line 52
def template_path
  File.join(hygro_path, 'template')
end
update() click to toggle source
# File lib/hygroscope/cli.rb, line 299
def update
  # TODO: Make re-uploading the payload optional
  check_path
  validate

  # Prepare task takes care of shared logic between "create" and "update"
  template, paramset = prepare('update')

  stack              = Hygroscope::Stack.new(options[:name], options[:region], options[:profile])
  stack.parameters   = paramset.parameters
  stack.template     = options[:compress] ? template.compress : template.process
  stack.capabilities = options[:capabilities]
  stack.timeout      = 60

  stack.update!
  status
end
validate() click to toggle source
# File lib/hygroscope/cli.rb, line 424
def validate
  check_path

  begin
    t = Hygroscope::Template.new(template_path, options[:region], options[:profile])
    t.validate
  rescue Aws::CloudFormation::Errors::ValidationError => e
    say_fail("Validation error: #{e.message}")
  rescue Hygroscope::TemplateYamlParseError => e
    say_fail("YAML parsing error: #{e.message}")
  rescue => e
    say_fail(e.message)
  else
    say_status('ok', 'Template is valid', :green)
  end
end