module Tfctl::Executor

Public Instance Methods

plan_file_args(plan_file, subcmd) click to toggle source

Adds plan file to `plan` and `apply` sub commands

# File lib/tfctl/executor.rb, line 91
def plan_file_args(plan_file, subcmd)
    return ["-out=#{plan_file}"] if subcmd == 'plan'

    if subcmd == 'apply'
        raise Tfctl::Error, "Plan file not found in #{plan_file}.  Run plan first." unless File.exist?(plan_file)

        return [plan_file.to_s]
    end

    return []
end
run(account_name:, config_name:, log:, cmd: nil, argv: [], unbuffered: true) click to toggle source

Execute terraform command

# File lib/tfctl/executor.rb, line 13
def run(account_name:, config_name:, log:, cmd: nil, argv: [], unbuffered: true)

    # Use bin/terraform from a project dir if available
    # Otherwise rely on PATH.
    if cmd.nil?
        cmd = File.exist?("#{PROJECT_ROOT}/bin/terraform") ? "#{PROJECT_ROOT}/bin/terraform" : 'terraform'
    end

    # Fail if there are no arguments for terraform and show terraform -help
    if argv.empty?
        help = `#{cmd} -help`.lines.to_a[1..-1].join
        raise Tfctl::Error, "Missing terraform command.\n #{help}"
    end

    path       = "#{PROJECT_ROOT}/.tfctl/#{config_name}/#{account_name}"
    cwd        = FileUtils.pwd
    plan_file  = "#{path}/tfplan"
    semaphore  = Mutex.new
    output     = []

    # Extract terraform sub command from argument list
    args       = Array.new(argv)
    subcmd     = args[0]
    args.delete_at(0)

    # Enable plan file for `plan` and `apply` sub commands
    args += plan_file_args(plan_file, subcmd)

    # Create the command
    exec = [cmd] + [subcmd] + args

    # Set environment variables for Terraform
    env = {
        'TF_INPUT'           => '0',
        'CHECKPOINT_DISABLE' => '1',
        'TF_IN_AUTOMATION'   => 'true',
        # 'TF_LOG'             => 'TRACE'
    }

    log.debug "#{account_name}: Executing: #{exec.shelljoin}"

    FileUtils.cd path
    Open3.popen3(env, exec.shelljoin) do |stdin, stdout, stderr, wait_thr|
        stdin.close_write

        # capture stdout and stderr in separate threads to prevent deadlocks
        Thread.new do
            stdout.each do |line|
                semaphore.synchronize do
                    unbuffered ? log.info("#{account_name}: #{line.chomp}") : output << ['info', line]
                end
            end
        end
        Thread.new do
            stderr.each do |line|
                semaphore.synchronize do
                    unbuffered ? log.error("#{account_name}: #{line.chomp}") : output << ['error', line]
                end
            end
        end

        status = wait_thr.value

        # log the output
        output.each do |line|
            log.send(line[0], "#{account_name}: #{line[1].chomp}")
        end

        FileUtils.cd cwd
        FileUtils.rm_f plan_file if args[0] == 'apply' # tidy up the plan file

        unless status.exitstatus.zero?
            raise Tfctl::Error, "#{cmd} failed with exit code: #{status.exitstatus}"
        end
    end
end