class MiasmaTerraform::Stack

Constants

REQUIRED_ATTRIBUTES

Attributes

actions[R]
bin[R]
container[R]
directory[R]
name[R]
scrub_destroyed[R]

Public Class Methods

cleanup_actions!() click to toggle source

Wait for all actions to complete

# File lib/miasma-terraform/stack.rb, line 32
def self.cleanup_actions!
  @@running_actions.map(&:complete!)
  nil
end
deregister_action(action) click to toggle source

Deregister action from at exit cleanup

@param action [Action] @return [NilClass]

# File lib/miasma-terraform/stack.rb, line 26
def self.deregister_action(action)
  @@running_actions.delete(action)
  nil
end
list(container) click to toggle source

@return [Array<String>]

# File lib/miasma-terraform/stack.rb, line 38
def self.list(container)
  if(container.to_s.empty?)
    raise ArgumentError.new 'Container directory must be set!'
  end
  if(File.directory?(container))
    Dir.new(container).map do |entry|
      next if entry.start_with?('.')
      entry if File.directory?(File.join(container, entry))
    end.compact
  else
    []
  end
end
new(opts={}) click to toggle source
# File lib/miasma-terraform/stack.rb, line 205
def initialize(opts={})
  @options = opts.to_smash
  init!
  @actions = []
  @name = @options[:name]
  @container = @options[:container]
  @scrub_destroyed = @options.fetch(:scrub_destroyed, false)
  @directory = File.join(container, name)
  @bin = @options.fetch(:bin, 'terraform')
end
register_action(action) click to toggle source

Register action to be cleaned up at exit

@param action [Action] @return [NilClass]

# File lib/miasma-terraform/stack.rb, line 15
def self.register_action(action)
  unless(@@running_actions.include?(action))
    @@running_actions.push(action)
  end
  nil
end

Public Instance Methods

active?() click to toggle source

@return [TrueClass, FalseClass] stack is currently active

# File lib/miasma-terraform/stack.rb, line 222
def active?
  actions.any? do |action|
    action.waiter.alive?
  end
end
destroy!() click to toggle source

@return [TrueClass] destroy this stack

# File lib/miasma-terraform/stack.rb, line 373
def destroy!
  must_exist do
    lock_stack
    action = run_action('destroy -force')
    action.on_start do |_|
      update_info do |info|
        info[:state] = "delete_in_progress"
        info
      end
    end
    action.on_complete do |*_|
      unlock_stack
    end
    action.on_complete do |result, _|
      unless(result.success?)
        update_info do |info|
          info[:state] = "delete_failed"
          info
        end
      else
        update_info do |info|
          info[:state] = "delete_complete"
          info
        end
        FileUtils.rm_rf(directory) if scrub_destroyed
      end
    end
    action.start!
  end
  true
end
events() click to toggle source

@return [Array<Hash>] events list

# File lib/miasma-terraform/stack.rb, line 297
def events
  must_exist do
    load_info.fetch(:events, []).map do |item|
      new_item = item.dup
      parts = item[:resource_name].to_s.split('.')
      new_item[:resource_name] = parts[1]
      new_item[:resource_type] = parts[0]
      new_item
    end
  end
end
exists?() click to toggle source

@return [TrueClass, FalseClass] stack currently exists

# File lib/miasma-terraform/stack.rb, line 217
def exists?
  File.directory?(directory)
end
info() click to toggle source

@return [Hash] stack information

# File lib/miasma-terraform/stack.rb, line 339
def info
  must_exist do
    stack_data = load_info
    Smash.new(
      :id => name,
      :name => name,
      :state => stack_data[:state].to_s,
      :status => stack_data[:state].to_s.upcase,
      :updated_time => stack_data[:updated_at],
      :creation_time => stack_data[:created_at],
      :outputs => outputs
    )
  end
end
outputs() click to toggle source

@return [Hash] stack outputs

# File lib/miasma-terraform/stack.rb, line 310
def outputs
  must_exist do
    if(has_state?)
      action = run_action('output -json', :auto_start)
      action.complete!
      successful_action(action) do
        result = JSON.parse(action.stdout.read).to_smash.map do |key, info|
          [key, info[:value]]
        end
        Smash[result]
      end
    else
      Smash.new
    end
  end
end
resources() click to toggle source

@return [Array<Hash>] resource list

# File lib/miasma-terraform/stack.rb, line 259
def resources
  must_exist do
    if(has_state?)
      action = run_action('state list', :auto_start)
      # wait for action to complete
      action.complete!
      successful_action(action) do
        resource_lines = action.stdout.read.split("\n").find_all do |line|
          line.match(/^[^\s]/)
        end
        resource_lines.map do |line|
          parts = line.split('.')
          resource_info = Smash.new(
            :type => parts[0],
            :name => parts[1],
            :status => 'UPDATE_COMPLETE'
          )
          action = run_action("state show #{line}", :auto_start)
          action.complete!
          successful_action(action) do
            info = Smash.new
            action.stdout.read.split("\n").each do |line|
              parts = line.split('=').map(&:strip)
              next if parts.size != 2
              info[parts[0]] = parts[1]
            end
            resource_info[:physical_id] = info[:id] if info[:id]
          end
          resource_info
        end
      end
    else
      []
    end
  end
end
save(opts={}) click to toggle source

Save the TF stack

# File lib/miasma-terraform/stack.rb, line 229
def save(opts={})
  save_opts = opts.to_smash
  type = exists? ? "update" : "create"
  lock_stack
  write_file(tf_path, save_opts[:template].to_json)
  write_file(tfvars_path, save_opts[:parameters].to_json)
  action = run_action('apply')
  store_events(action)
  action.on_start do |_|
    update_info do |info|
      info["state"] = "#{type}_in_progress"
      info
    end
  end
  action.on_complete do |status, this_action|
    update_info do |info|
      if(type == "create")
        info["created_at"] = (Time.now.to_f * 1000).floor
      end
      info["updated_at"] = (Time.now.to_f * 1000).floor
      info["state"] = status.success? ? "#{type}_complete" : "#{type}_failed"
      info
    end
    unlock_stack
  end
  action.start!
  true
end
template() click to toggle source

@return [String] current stack template

# File lib/miasma-terraform/stack.rb, line 328
def template
  must_exist do
    if(File.exists?(tf_path))
      File.read(tf_path)
    else
      "{}"
    end
  end
end
validate(template) click to toggle source
# File lib/miasma-terraform/stack.rb, line 354
def validate(template)
  errors = []
  root_path = Dir.mktmpdir('miasma-')
  template_path = File.join(root_path, 'main.tf')
  File.write(template_path, template)
  action = run_action('validate')
  action.options[:chdir] = root_path
  action.on_io do |line, type|
    if(line.start_with?('*'))
      errors << line
    end
  end.on_complete do |_|
    FileUtils.rm_rf(root_path)
  end
  action.complete!
  errors
end

Protected Instance Methods

has_state?() click to toggle source

@return [TrueClass, FalseClass] stack has state

# File lib/miasma-terraform/stack.rb, line 485
def has_state?
  File.exists?(tfstate_path)
end
info_path() click to toggle source

@return [String] path to internal info file

# File lib/miasma-terraform/stack.rb, line 475
def info_path
  File.join(directory, '.info.json')
end
init!() click to toggle source

Validate initialization

# File lib/miasma-terraform/stack.rb, line 548
def init!
  missing_attrs = REQUIRED_ATTRIBUTES.find_all do |key|
    !@options[key]
  end
  unless(missing_attrs.empty?)
    raise ArgumentError.new("Missing required attributes: #{missing_attrs.sort}")
  end
  # TODO: Add tf bin check
end
load_info() click to toggle source

@return [Smash] stack info

# File lib/miasma-terraform/stack.rb, line 495
def load_info
  if(File.exists?(info_path))
    result = JSON.parse(File.read(info_path)).to_smash
  else
    result = Smash.new
  end
  result[:created_at] = (Time.now.to_f * 1000).floor unless result[:created_at]
  result[:state] = 'unknown' unless result[:state]
  result
end
lock_path() click to toggle source

@return [String] path to lock file

# File lib/miasma-terraform/stack.rb, line 490
def lock_path
  File.join(directory, '.lck')
end
lock_stack() { || ... } click to toggle source

Lock stack and run block

# File lib/miasma-terraform/stack.rb, line 436
def lock_stack
  FileUtils.mkdir_p(directory)
  @lock_file = File.open(lock_path, File::RDWR|File::CREAT)
  if(@lock_file.flock(File::LOCK_EX | File::LOCK_NB))
    if(block_given?)
      result = yield
      @lock_file.flock(File::LOCK_UN)
      @lock_file = nil
      result
    else
      true
    end
  else
    raise Error::Busy.new "Failed to aquire process lock for `#{name}`. Stack busy."
  end
end
must_exist(lock=false) { || ... } click to toggle source

Validate stack exists before running block

# File lib/miasma-terraform/stack.rb, line 421
def must_exist(lock=false)
  if(exists?)
    if(lock)
      lock_stack do
        yield
      end
    else
      yield
    end
  else
    raise Error::NotFound.new "Stack does not exist `#{name}`"
  end
end
run_action(cmd, auto_start=false) click to toggle source

Start running a terraform process

# File lib/miasma-terraform/stack.rb, line 408
def run_action(cmd, auto_start=false)
  action = Action.new("#{bin} #{cmd} -no-color", :chdir => directory)
  action.on_start do |this_action|
    actions << this_action
  end
  action.on_complete do |_, this_action|
    actions.delete(this_action)
  end
  action.start! if auto_start
  action
end
store_events(action) click to toggle source

Store stack events generated by action

@param action [Action]

# File lib/miasma-terraform/stack.rb, line 526
def store_events(action)
  action.on_io do |line, type|
    result = line.match(/^(\*\s+)?(?<name>[^\s]+): (?<status>.+)$/)
    if(result)
      resource_name = result["name"]
      resource_status = result["status"]
      event = Smash.new(
        :timestamp => (Time.now.to_f * 1000).floor,
        :resource_name => resource_name,
        :resource_status => resource_status,
        :id => SecureRandom.uuid
      )
      update_info do |info|
        info[:events] ||= []
        info[:events].unshift(event)
        info
      end
    end
  end
end
successful_action(action) { || ... } click to toggle source

Raise exception if action was not completed successfully

# File lib/miasma-terraform/stack.rb, line 514
def successful_action(action)
  status = action.complete!
  unless(status.success?)
    raise Error::CommandFailed.new "Command failed `#{action.command}` - #{action.stderr.read}"
  else
    yield
  end
end
tf_path() click to toggle source

@return [String] path to template file

# File lib/miasma-terraform/stack.rb, line 465
def tf_path
  File.join(directory, 'main.tf')
end
tfstate_path() click to toggle source

@return [String] path to state file

# File lib/miasma-terraform/stack.rb, line 480
def tfstate_path
  File.join(directory, 'terraform.tfstate')
end
tfvars_path() click to toggle source

@return [String] path to variables file

# File lib/miasma-terraform/stack.rb, line 470
def tfvars_path
  File.join(directory, 'terraform.tfvars')
end
unlock_stack() click to toggle source

Unlock stack

# File lib/miasma-terraform/stack.rb, line 454
def unlock_stack
  if(@lock_file)
    @lock_file.flock(File::LOCK_UN)
    @lock_file = nil
    true
  else
    false
  end
end
update_info() { |load_info| ... } click to toggle source

@return [TrueClass]

# File lib/miasma-terraform/stack.rb, line 507
def update_info
  result = yield(load_info)
  write_file(info_path, result.to_json)
  true
end
write_file(path, contents=nil) { |tmp_file| ... } click to toggle source

File write helper that proxies via temporary file to prevent corrupted writes on unexpected interrupt

@param path [String] path to file @param contents [String] contents of file @return [TrueClass]

# File lib/miasma-terraform/stack.rb, line 564
def write_file(path, contents=nil)
  tmp_file = Tempfile.new('miasma')
  yield(tmp_file) if block_given?
  tmp_file.print(contents.to_s) if contents
  tmp_file.close
  FileUtils.mv(tmp_file.path, path)
  true
end