opentofu/main.go
Martin Atkins 5ac311e2a9 main: synchronize writes to VT100-faker on Windows
We use a third-party library "colorable" to translate VT100 color
sequences into Windows console attribute-setting calls when Terraform is
running on Windows.

colorable is not concurrency-safe for multiple writes to the same console,
because it writes to the console one character at a time and so two
concurrent writers get their characters interleaved, creating unreadable
garble.

Here we wrap around it a synchronization mechanism to ensure that there
can be only one Write call outstanding across both stderr and stdout,
mimicking the usual behavior we expect (when stderr/stdout are a normal
file handle) of each Write being completed atomically.
2017-05-04 15:36:51 -07:00

333 lines
8.0 KiB
Go

package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"runtime"
"strings"
"sync"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/terraform"
"github.com/mattn/go-colorable"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/cli"
"github.com/mitchellh/panicwrap"
"github.com/mitchellh/prefixedio"
)
const (
// EnvCLI is the environment variable name to set additional CLI args.
EnvCLI = "TF_CLI_ARGS"
)
func main() {
// Override global prefix set by go-dynect during init()
log.SetPrefix("")
os.Exit(realMain())
}
func realMain() int {
var wrapConfig panicwrap.WrapConfig
// don't re-exec terraform as a child process for easier debugging
if os.Getenv("TF_FORK") == "0" {
return wrappedMain()
}
if !panicwrap.Wrapped(&wrapConfig) {
// Determine where logs should go in general (requested by the user)
logWriter, err := logging.LogOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
return 1
}
// We always send logs to a temporary file that we use in case
// there is a panic. Otherwise, we delete it.
logTempFile, err := ioutil.TempFile("", "terraform-log")
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
return 1
}
defer os.Remove(logTempFile.Name())
defer logTempFile.Close()
// Setup the prefixed readers that send data properly to
// stdout/stderr.
doneCh := make(chan struct{})
outR, outW := io.Pipe()
go copyOutput(outR, doneCh)
// Create the configuration for panicwrap and wrap our executable
wrapConfig.Handler = panicHandler(logTempFile)
wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter)
wrapConfig.Stdout = outW
wrapConfig.IgnoreSignals = ignoreSignals
wrapConfig.ForwardSignals = forwardSignals
exitStatus, err := panicwrap.Wrap(&wrapConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err)
return 1
}
// If >= 0, we're the parent, so just exit
if exitStatus >= 0 {
// Close the stdout writer so that our copy process can finish
outW.Close()
// Wait for the output copying to finish
<-doneCh
return exitStatus
}
// We're the child, so just close the tempfile we made in order to
// save file handles since the tempfile is only used by the parent.
logTempFile.Close()
}
// Call the real main
return wrappedMain()
}
func wrappedMain() int {
// We always need to close the DebugInfo before we exit.
defer terraform.CloseDebugInfo()
log.SetOutput(os.Stderr)
log.Printf(
"[INFO] Terraform version: %s %s %s",
Version, VersionPrerelease, GitCommit)
log.Printf("[INFO] Go runtime version: %s", runtime.Version())
log.Printf("[INFO] CLI args: %#v", os.Args)
// Load the configuration
config := BuiltinConfig
if err := config.Discover(Ui); err != nil {
Ui.Error(fmt.Sprintf("Error discovering plugins: %s", err))
return 1
}
// Load the configuration file if we have one, that can be used to
// define extra providers and provisioners.
clicfgFile, err := cliConfigFile()
if err != nil {
Ui.Error(fmt.Sprintf("Error loading CLI configuration: \n\n%s", err))
return 1
}
if clicfgFile != "" {
usrcfg, err := LoadConfig(clicfgFile)
if err != nil {
Ui.Error(fmt.Sprintf("Error loading CLI configuration: \n\n%s", err))
return 1
}
config = *config.Merge(usrcfg)
}
// Run checkpoint
go runCheckpoint(&config)
// Make sure we clean up any managed plugins at the end of this
defer plugin.CleanupClients()
// Get the command line args.
args := os.Args[1:]
// Build the CLI so far, we do this so we can query the subcommand.
cliRunner := &cli.CLI{
Args: args,
Commands: Commands,
HelpFunc: helpFunc,
HelpWriter: os.Stdout,
}
// Prefix the args with any args from the EnvCLI
args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args)
if err != nil {
Ui.Error(err.Error())
return 1
}
// Prefix the args with any args from the EnvCLI targeting this command
suffix := strings.Replace(strings.Replace(
cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1)
args, err = mergeEnvArgs(
fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args)
if err != nil {
Ui.Error(err.Error())
return 1
}
// We shortcut "--version" and "-v" to just show the version
for _, arg := range args {
if arg == "-v" || arg == "-version" || arg == "--version" {
newArgs := make([]string, len(args)+1)
newArgs[0] = "version"
copy(newArgs[1:], args)
args = newArgs
break
}
}
// Rebuild the CLI with any modified args.
log.Printf("[INFO] CLI command args: %#v", args)
cliRunner = &cli.CLI{
Args: args,
Commands: Commands,
HelpFunc: helpFunc,
HelpWriter: os.Stdout,
}
// Initialize the TFConfig settings for the commands...
ContextOpts.Providers = config.ProviderFactories()
ContextOpts.Provisioners = config.ProvisionerFactories()
exitCode, err := cliRunner.Run()
if err != nil {
Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error()))
return 1
}
return exitCode
}
func cliConfigFile() (string, error) {
mustExist := true
configFilePath := os.Getenv("TERRAFORM_CONFIG")
if configFilePath == "" {
var err error
configFilePath, err = ConfigFile()
mustExist = false
if err != nil {
log.Printf(
"[ERROR] Error detecting default CLI config file path: %s",
err)
}
}
log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath)
f, err := os.Open(configFilePath)
if err == nil {
f.Close()
return configFilePath, nil
}
if mustExist || !os.IsNotExist(err) {
return "", err
}
log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.")
return "", nil
}
// copyOutput uses output prefixes to determine whether data on stdout
// should go to stdout or stderr. This is due to panicwrap using stderr
// as the log and error channel.
func copyOutput(r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
pr, err := prefixedio.NewReader(r)
if err != nil {
panic(err)
}
stderrR, err := pr.Prefix(ErrorPrefix)
if err != nil {
panic(err)
}
stdoutR, err := pr.Prefix(OutputPrefix)
if err != nil {
panic(err)
}
defaultR, err := pr.Prefix("")
if err != nil {
panic(err)
}
var stdout io.Writer = os.Stdout
var stderr io.Writer = os.Stderr
if runtime.GOOS == "windows" {
stdout = colorable.NewColorableStdout()
stderr = colorable.NewColorableStderr()
// colorable is not concurrency-safe when stdout and stderr are the
// same console, so we need to add some synchronization to ensure that
// we can't be concurrently writing to both stderr and stdout at
// once, or else we get intermingled writes that create gibberish
// in the console.
wrapped := synchronizedWriters(stdout, stderr)
stdout = wrapped[0]
stderr = wrapped[1]
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
io.Copy(stderr, stderrR)
}()
go func() {
defer wg.Done()
io.Copy(stdout, stdoutR)
}()
go func() {
defer wg.Done()
io.Copy(stdout, defaultR)
}()
wg.Wait()
}
func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
v := os.Getenv(envName)
if v == "" {
return args, nil
}
log.Printf("[INFO] %s value: %q", envName, v)
extra, err := shellwords.Parse(v)
if err != nil {
return nil, fmt.Errorf(
"Error parsing extra CLI args from %s: %s",
envName, err)
}
// Find the command to look for in the args. If there is a space,
// we need to find the last part.
search := cmd
if idx := strings.LastIndex(search, " "); idx >= 0 {
search = cmd[idx+1:]
}
// Find the index to place the flags. We put them exactly
// after the first non-flag arg.
idx := -1
for i, v := range args {
if v == search {
idx = i
break
}
}
// idx points to the exact arg that isn't a flag. We increment
// by one so that all the copying below expects idx to be the
// insertion point.
idx++
// Copy the args
newArgs := make([]string, len(args)+len(extra))
copy(newArgs, args[:idx])
copy(newArgs[idx:], extra)
copy(newArgs[len(extra)+idx:], args[idx:])
return newArgs, nil
}