| // Copyright IBM Corp. 2020, 2026 | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package tfexec | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "log" | |
| "os" | |
| "runtime" | |
| "sync" | |
| "time" | |
| "github.com/hashicorp/go-version" | |
| ) | |
| type printfer interface { | |
| Printf(format string, v ...interface{}) | |
| } | |
| // Terraform represents the Terraform CLI executable and working directory. | |
| // | |
| // Typically this is constructed against the root module of a Terraform configuration | |
| // but you can override paths used in some commands depending on the available | |
| // options. | |
| // | |
| // All functions that execute CLI commands take a context.Context. It should be noted that | |
| // exec.Cmd.Run will not return context.DeadlineExceeded or context.Canceled by default, we | |
| // have augmented our wrapped errors to respond true to errors.Is for context.DeadlineExceeded | |
| // and context.Canceled if those are present on the context when the error is parsed. See | |
| // https://github.com/golang/go/issues/21880 for more about the Go limitations. | |
| // | |
| // By default, the instance inherits the environment from the calling code (using os.Environ) | |
| // but it ignores certain environment variables that are managed within the code and prohibits | |
| // setting them through SetEnv: | |
| // | |
| // - TF_APPEND_USER_AGENT | |
| // - TF_IN_AUTOMATION | |
| // - TF_INPUT | |
| // - TF_LOG | |
| // - TF_LOG_PATH | |
| // - TF_REATTACH_PROVIDERS | |
| // - TF_DISABLE_PLUGIN_TLS | |
| // - TF_SKIP_PROVIDER_VERIFY | |
| type Terraform struct { | |
| execPath string | |
| workingDir string | |
| appendUserAgent string | |
| disablePluginTLS bool | |
| skipProviderVerify bool | |
| env map[string]string | |
| stdout io.Writer | |
| stderr io.Writer | |
| logger printfer | |
| // TF_LOG environment variable, defaults to TRACE if logPath is set. | |
| log string | |
| // TF_LOG_CORE environment variable | |
| logCore string | |
| // TF_LOG_PATH environment variable | |
| logPath string | |
| // TF_LOG_PROVIDER environment variable | |
| logProvider string | |
| // waitDelay represents the WaitDelay field of the [exec.Cmd] of Terraform | |
| waitDelay time.Duration | |
| // enableLegacyPipeClosing closes the stdout/stderr pipes before calling [exec.Cmd.Wait] | |
| enableLegacyPipeClosing bool | |
| versionLock sync.Mutex | |
| execVersion *version.Version | |
| provVersions map[string]*version.Version | |
| } | |
| // NewTerraform returns a Terraform struct with default values for all fields. | |
| // If a blank execPath is supplied, NewTerraform will error. | |
| // Use hc-install or output from os.LookPath to get a desirable execPath. | |
| func NewTerraform(workingDir string, execPath string) (*Terraform, error) { | |
| if workingDir == "" { | |
| return nil, fmt.Errorf("Terraform cannot be initialised with empty workdir") | |
| } | |
| if _, err := os.Stat(workingDir); err != nil { | |
| return nil, fmt.Errorf("error initialising Terraform with workdir %s: %s", workingDir, err) | |
| } | |
| if execPath == "" { | |
| err := fmt.Errorf("NewTerraform: please supply the path to a Terraform executable using execPath, e.g. using the github.com/hashicorp/hc-install module.") | |
| return nil, &ErrNoSuitableBinary{ | |
| err: err, | |
| } | |
| } | |
| tf := Terraform{ | |
| execPath: execPath, | |
| workingDir: workingDir, | |
| env: nil, // explicit nil means copy os.Environ | |
| logger: log.New(ioutil.Discard, "", 0), | |
| waitDelay: 60 * time.Second, | |
| } | |
| return &tf, nil | |
| } | |
| // SetEnv allows you to override environment variables, this should not be used for any well known | |
| // Terraform environment variables that are already covered in options. Pass nil to copy the values | |
| // from os.Environ. Attempting to set environment variables that should be managed manually will | |
| // result in ErrManualEnvVar being returned. | |
| func (tf *Terraform) SetEnv(env map[string]string) error { | |
| prohibited := ProhibitedEnv(env) | |
| if len(prohibited) > 0 { | |
| // just error on the first instance | |
| return &ErrManualEnvVar{prohibited[0]} | |
| } | |
| tf.env = env | |
| return nil | |
| } | |
| // SetLogger specifies a logger for tfexec to use. | |
| func (tf *Terraform) SetLogger(logger printfer) { | |
| tf.logger = logger | |
| } | |
| // SetStdout specifies a writer to stream stdout to for every command. | |
| // | |
| // This should be used for information or logging purposes only, not control | |
| // flow. Any parsing necessary should be added as functionality to this package. | |
| func (tf *Terraform) SetStdout(w io.Writer) { | |
| tf.stdout = w | |
| } | |
| // SetStderr specifies a writer to stream stderr to for every command. | |
| // | |
| // This should be used for information or logging purposes only, not control | |
| // flow. Any parsing necessary should be added as functionality to this package. | |
| func (tf *Terraform) SetStderr(w io.Writer) { | |
| tf.stderr = w | |
| } | |
| // SetLog sets the TF_LOG environment variable for Terraform CLI execution. | |
| // This must be combined with a call to SetLogPath to take effect. | |
| // | |
| // This is only compatible with Terraform CLI 0.15.0 or later as setting the | |
| // log level was unreliable in earlier versions. It will default to TRACE when | |
| // SetLogPath is called on versions 0.14.11 and earlier, or if SetLogCore and | |
| // SetLogProvider have not been called before SetLogPath on versions 0.15.0 and | |
| // later. | |
| func (tf *Terraform) SetLog(log string) error { | |
| err := tf.compatible(context.Background(), tf0_15_0, nil) | |
| if err != nil { | |
| return err | |
| } | |
| tf.log = log | |
| return nil | |
| } | |
| // SetLogCore sets the TF_LOG_CORE environment variable for Terraform CLI | |
| // execution. This must be combined with a call to SetLogPath to take effect. | |
| // | |
| // This is only compatible with Terraform CLI 0.15.0 or later. | |
| func (tf *Terraform) SetLogCore(logCore string) error { | |
| err := tf.compatible(context.Background(), tf0_15_0, nil) | |
| if err != nil { | |
| return err | |
| } | |
| tf.logCore = logCore | |
| return nil | |
| } | |
| // SetLogPath sets the TF_LOG_PATH environment variable for Terraform CLI | |
| // execution. | |
| func (tf *Terraform) SetLogPath(path string) error { | |
| tf.logPath = path | |
| // Prevent setting the log path without enabling logging | |
| if tf.log == "" && tf.logCore == "" && tf.logProvider == "" { | |
| tf.log = "TRACE" | |
| } | |
| return nil | |
| } | |
| // SetLogProvider sets the TF_LOG_PROVIDER environment variable for Terraform | |
| // CLI execution. This must be combined with a call to SetLogPath to take | |
| // effect. | |
| // | |
| // This is only compatible with Terraform CLI 0.15.0 or later. | |
| func (tf *Terraform) SetLogProvider(logProvider string) error { | |
| err := tf.compatible(context.Background(), tf0_15_0, nil) | |
| if err != nil { | |
| return err | |
| } | |
| tf.logProvider = logProvider | |
| return nil | |
| } | |
| // SetAppendUserAgent sets the TF_APPEND_USER_AGENT environment variable for | |
| // Terraform CLI execution. | |
| func (tf *Terraform) SetAppendUserAgent(ua string) error { | |
| tf.appendUserAgent = ua | |
| return nil | |
| } | |
| // SetDisablePluginTLS sets the TF_DISABLE_PLUGIN_TLS environment variable for | |
| // Terraform CLI execution. | |
| func (tf *Terraform) SetDisablePluginTLS(disabled bool) error { | |
| tf.disablePluginTLS = disabled | |
| return nil | |
| } | |
| // SetSkipProviderVerify sets the TF_SKIP_PROVIDER_VERIFY environment variable | |
| // for Terraform CLI execution. This is no longer used in 0.13.0 and greater. | |
| func (tf *Terraform) SetSkipProviderVerify(skip bool) error { | |
| err := tf.compatible(context.Background(), nil, tf0_13_0) | |
| if err != nil { | |
| return err | |
| } | |
| tf.skipProviderVerify = skip | |
| return nil | |
| } | |
| // SetWaitDelay sets the WaitDelay of running Terraform process as [exec.Cmd] | |
| func (tf *Terraform) SetWaitDelay(delay time.Duration) error { | |
| if runtime.GOOS == "windows" { | |
| return errors.New("cannot set WaitDelay, graceful cancellation not supported on windows") | |
| } | |
| tf.waitDelay = delay | |
| return nil | |
| } | |
| // SetEnableLegacyPipeClosing causes the library to "force-close" stdio pipes. | |
| // This works around a bug in Terraform < v1.1 that would otherwise leave | |
| // the process (and caller) hanging after graceful shutdown. | |
| // | |
| // This option can be safely ignored (set to false) with Terraform 1.1+. | |
| func (tf *Terraform) SetEnableLegacyPipeClosing(enabled bool) error { | |
| tf.enableLegacyPipeClosing = enabled | |
| return nil | |
| } | |
| // WorkingDir returns the working directory for Terraform. | |
| func (tf *Terraform) WorkingDir() string { | |
| return tf.workingDir | |
| } | |
| // ExecPath returns the path to the Terraform executable. | |
| func (tf *Terraform) ExecPath() string { | |
| return tf.execPath | |
| } |