mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 00:41:27 -06:00
797a1b339d
Implement debugInfo and the DebugGraph DebugInfo will be a global variable through which graph debug information can we written to a compressed archive. The DebugInfo methods are all safe for concurrent use, and noop with a nil receiver. The API outside of the terraform package will be to call SetDebugInfo to create the archive, and CloseDebugInfo() to properly close the file. Each write to the archive will be flushed and sync'ed individually, so in the event of a crash or a missing call to Close, the archive can still be recovered. The DebugGraph is a representation of a terraform Graph to be written to the debug archive, currently in dot format. The DebugGraph also contains an internal buffer with Printf and Write methods to add to this buffer. The buffer will be written to an accompanying file in the debug archive along with the graph. This also adds a GraphNodeDebugger interface. Any node implementing `NodeDebug() string` can output information to annotate the debug graph node, and add the data to the log. This interface may change or be removed to provide richer options for debugging graph nodes. The new graph builders all delegate the build to the BasicGraphBuilder. Having a Name field lets us differentiate the actual builder implementation in the debug graphs.
242 lines
5.5 KiB
Go
242 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/go-plugin"
|
|
"github.com/hashicorp/terraform/helper/logging"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mattn/go-colorable"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/panicwrap"
|
|
"github.com/mitchellh/prefixedio"
|
|
)
|
|
|
|
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
|
|
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] 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
|
|
}
|
|
|
|
// 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. We shortcut "--version" and "-v" to
|
|
// just show the version.
|
|
args := os.Args[1:]
|
|
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
|
|
}
|
|
}
|
|
|
|
cli := &cli.CLI{
|
|
Args: args,
|
|
Commands: Commands,
|
|
HelpFunc: helpFunc,
|
|
HelpWriter: os.Stdout,
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Initialize the TFConfig settings for the commands...
|
|
ContextOpts.Providers = config.ProviderFactories()
|
|
ContextOpts.Provisioners = config.ProvisionerFactories()
|
|
|
|
exitCode, err := cli.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()
|
|
}
|
|
|
|
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()
|
|
}
|