class MiasmaTerraform::Stack
Constants
- REQUIRED_ATTRIBUTES
Attributes
Public Class Methods
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 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
@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
# 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 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
@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
@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
@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
@return [TrueClass, FalseClass] stack currently exists
# File lib/miasma-terraform/stack.rb, line 217 def exists? File.directory?(directory) end
@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
@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
@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 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
@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
# 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
@return [TrueClass, FalseClass] stack has state
# File lib/miasma-terraform/stack.rb, line 485 def has_state? File.exists?(tfstate_path) end
@return [String] path to internal info file
# File lib/miasma-terraform/stack.rb, line 475 def info_path File.join(directory, '.info.json') end
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
@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
@return [String] path to lock file
# File lib/miasma-terraform/stack.rb, line 490 def lock_path File.join(directory, '.lck') end
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
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
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 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
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
@return [String] path to template file
# File lib/miasma-terraform/stack.rb, line 465 def tf_path File.join(directory, 'main.tf') end
@return [String] path to state file
# File lib/miasma-terraform/stack.rb, line 480 def tfstate_path File.join(directory, 'terraform.tfstate') end
@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
# 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
@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
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