mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
go build / go install should generate tofu binary (#590)
This commit is contained in:
460
cmd/tofu/commands.go
Normal file
460
cmd/tofu/commands.go
Normal file
@@ -0,0 +1,460 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
svchost "github.com/hashicorp/terraform-svchost"
|
||||
"github.com/hashicorp/terraform-svchost/auth"
|
||||
"github.com/hashicorp/terraform-svchost/disco"
|
||||
"github.com/mitchellh/cli"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/command"
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig"
|
||||
"github.com/opentofu/opentofu/internal/command/views"
|
||||
"github.com/opentofu/opentofu/internal/command/webbrowser"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
pluginDiscovery "github.com/opentofu/opentofu/internal/plugin/discovery"
|
||||
"github.com/opentofu/opentofu/internal/terminal"
|
||||
)
|
||||
|
||||
// runningInAutomationEnvName gives the name of an environment variable that
|
||||
// can be set to any non-empty value in order to suppress certain messages
|
||||
// that assume that OpenTofu is being run from a command prompt.
|
||||
const runningInAutomationEnvName = "TF_IN_AUTOMATION"
|
||||
|
||||
// Commands is the mapping of all the available OpenTofu commands.
|
||||
var Commands map[string]cli.CommandFactory
|
||||
|
||||
// PrimaryCommands is an ordered sequence of the top-level commands (not
|
||||
// subcommands) that we emphasize at the top of our help output. This is
|
||||
// ordered so that we can show them in the typical workflow order, rather
|
||||
// than in alphabetical order. Anything not in this sequence or in the
|
||||
// HiddenCommands set appears under "all other commands".
|
||||
var PrimaryCommands []string
|
||||
|
||||
// HiddenCommands is a set of top-level commands (not subcommands) that are
|
||||
// not advertised in the top-level help at all. This is typically because
|
||||
// they are either just stubs that return an error message about something
|
||||
// no longer being supported or backward-compatibility aliases for other
|
||||
// commands.
|
||||
//
|
||||
// No commands in the PrimaryCommands sequence should also appear in the
|
||||
// HiddenCommands set, because that would be rather silly.
|
||||
var HiddenCommands map[string]struct{}
|
||||
|
||||
// Ui is the cli.Ui used for communicating to the outside world.
|
||||
var Ui cli.Ui
|
||||
|
||||
func initCommands(
|
||||
ctx context.Context,
|
||||
originalWorkingDir string,
|
||||
streams *terminal.Streams,
|
||||
config *cliconfig.Config,
|
||||
services *disco.Disco,
|
||||
providerSrc getproviders.Source,
|
||||
providerDevOverrides map[addrs.Provider]getproviders.PackageLocalDir,
|
||||
unmanagedProviders map[addrs.Provider]*plugin.ReattachConfig,
|
||||
) {
|
||||
var inAutomation bool
|
||||
if v := os.Getenv(runningInAutomationEnvName); v != "" {
|
||||
inAutomation = true
|
||||
}
|
||||
|
||||
for userHost, hostConfig := range config.Hosts {
|
||||
host, err := svchost.ForComparison(userHost)
|
||||
if err != nil {
|
||||
// We expect the config was already validated by the time we get
|
||||
// here, so we'll just ignore invalid hostnames.
|
||||
continue
|
||||
}
|
||||
services.ForceHostServices(host, hostConfig.Services)
|
||||
}
|
||||
|
||||
configDir, err := cliconfig.ConfigDir()
|
||||
if err != nil {
|
||||
configDir = "" // No config dir available (e.g. looking up a home directory failed)
|
||||
}
|
||||
|
||||
wd := WorkingDir(originalWorkingDir, os.Getenv("TF_DATA_DIR"))
|
||||
|
||||
meta := command.Meta{
|
||||
WorkingDir: wd,
|
||||
Streams: streams,
|
||||
View: views.NewView(streams).SetRunningInAutomation(inAutomation),
|
||||
|
||||
Color: true,
|
||||
GlobalPluginDirs: globalPluginDirs(),
|
||||
Ui: Ui,
|
||||
|
||||
Services: services,
|
||||
BrowserLauncher: webbrowser.NewNativeLauncher(),
|
||||
|
||||
RunningInAutomation: inAutomation,
|
||||
CLIConfigDir: configDir,
|
||||
PluginCacheDir: config.PluginCacheDir,
|
||||
|
||||
PluginCacheMayBreakDependencyLockFile: config.PluginCacheMayBreakDependencyLockFile,
|
||||
|
||||
ShutdownCh: makeShutdownCh(),
|
||||
CallerContext: ctx,
|
||||
|
||||
ProviderSource: providerSrc,
|
||||
ProviderDevOverrides: providerDevOverrides,
|
||||
UnmanagedProviders: unmanagedProviders,
|
||||
|
||||
AllowExperimentalFeatures: ExperimentsAllowed(),
|
||||
}
|
||||
|
||||
// The command list is included in the tofu -help
|
||||
// output, which is in turn included in the docs at
|
||||
// website/docs/cli/commands/index.html.markdown; if you
|
||||
// add, remove or reclassify commands then consider updating
|
||||
// that to match.
|
||||
|
||||
Commands = map[string]cli.CommandFactory{
|
||||
"apply": func() (cli.Command, error) {
|
||||
return &command.ApplyCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"console": func() (cli.Command, error) {
|
||||
return &command.ConsoleCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"destroy": func() (cli.Command, error) {
|
||||
return &command.ApplyCommand{
|
||||
Meta: meta,
|
||||
Destroy: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env": func() (cli.Command, error) {
|
||||
return &command.WorkspaceCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env list": func() (cli.Command, error) {
|
||||
return &command.WorkspaceListCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env select": func() (cli.Command, error) {
|
||||
return &command.WorkspaceSelectCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env new": func() (cli.Command, error) {
|
||||
return &command.WorkspaceNewCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env delete": func() (cli.Command, error) {
|
||||
return &command.WorkspaceDeleteCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"fmt": func() (cli.Command, error) {
|
||||
return &command.FmtCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"get": func() (cli.Command, error) {
|
||||
return &command.GetCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"graph": func() (cli.Command, error) {
|
||||
return &command.GraphCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"import": func() (cli.Command, error) {
|
||||
return &command.ImportCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"init": func() (cli.Command, error) {
|
||||
return &command.InitCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"login": func() (cli.Command, error) {
|
||||
return &command.LoginCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"logout": func() (cli.Command, error) {
|
||||
return &command.LogoutCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"metadata": func() (cli.Command, error) {
|
||||
return &command.MetadataCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"metadata functions": func() (cli.Command, error) {
|
||||
return &command.MetadataFunctionsCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"output": func() (cli.Command, error) {
|
||||
return &command.OutputCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"plan": func() (cli.Command, error) {
|
||||
return &command.PlanCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"providers": func() (cli.Command, error) {
|
||||
return &command.ProvidersCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"providers lock": func() (cli.Command, error) {
|
||||
return &command.ProvidersLockCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"providers mirror": func() (cli.Command, error) {
|
||||
return &command.ProvidersMirrorCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"providers schema": func() (cli.Command, error) {
|
||||
return &command.ProvidersSchemaCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"push": func() (cli.Command, error) {
|
||||
return &command.PushCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"refresh": func() (cli.Command, error) {
|
||||
return &command.RefreshCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"show": func() (cli.Command, error) {
|
||||
return &command.ShowCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"taint": func() (cli.Command, error) {
|
||||
return &command.TaintCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"test": func() (cli.Command, error) {
|
||||
return &command.TestCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"validate": func() (cli.Command, error) {
|
||||
return &command.ValidateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"version": func() (cli.Command, error) {
|
||||
return &command.VersionCommand{
|
||||
Meta: meta,
|
||||
Version: Version,
|
||||
VersionPrerelease: VersionPrerelease,
|
||||
Platform: getproviders.CurrentPlatform,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"untaint": func() (cli.Command, error) {
|
||||
return &command.UntaintCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"workspace": func() (cli.Command, error) {
|
||||
return &command.WorkspaceCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"workspace list": func() (cli.Command, error) {
|
||||
return &command.WorkspaceListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"workspace select": func() (cli.Command, error) {
|
||||
return &command.WorkspaceSelectCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"workspace show": func() (cli.Command, error) {
|
||||
return &command.WorkspaceShowCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"workspace new": func() (cli.Command, error) {
|
||||
return &command.WorkspaceNewCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"workspace delete": func() (cli.Command, error) {
|
||||
return &command.WorkspaceDeleteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Plumbing
|
||||
//-----------------------------------------------------------
|
||||
|
||||
"force-unlock": func() (cli.Command, error) {
|
||||
return &command.UnlockCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state": func() (cli.Command, error) {
|
||||
return &command.StateCommand{}, nil
|
||||
},
|
||||
|
||||
"state list": func() (cli.Command, error) {
|
||||
return &command.StateListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state rm": func() (cli.Command, error) {
|
||||
return &command.StateRmCommand{
|
||||
StateMeta: command.StateMeta{
|
||||
Meta: meta,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state mv": func() (cli.Command, error) {
|
||||
return &command.StateMvCommand{
|
||||
StateMeta: command.StateMeta{
|
||||
Meta: meta,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state pull": func() (cli.Command, error) {
|
||||
return &command.StatePullCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state push": func() (cli.Command, error) {
|
||||
return &command.StatePushCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state show": func() (cli.Command, error) {
|
||||
return &command.StateShowCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"state replace-provider": func() (cli.Command, error) {
|
||||
return &command.StateReplaceProviderCommand{
|
||||
StateMeta: command.StateMeta{
|
||||
Meta: meta,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
if meta.AllowExperimentalFeatures {
|
||||
Commands["cloud"] = func() (cli.Command, error) {
|
||||
return &command.CloudCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryCommands = []string{
|
||||
"init",
|
||||
"validate",
|
||||
"plan",
|
||||
"apply",
|
||||
"destroy",
|
||||
}
|
||||
|
||||
HiddenCommands = map[string]struct{}{
|
||||
"env": {},
|
||||
"internal-plugin": {},
|
||||
"push": {},
|
||||
}
|
||||
}
|
||||
|
||||
// makeShutdownCh creates an interrupt listener and returns a channel.
|
||||
// A message will be sent on the channel for every interrupt received.
|
||||
func makeShutdownCh() <-chan struct{} {
|
||||
resultCh := make(chan struct{})
|
||||
|
||||
signalCh := make(chan os.Signal, 4)
|
||||
signal.Notify(signalCh, ignoreSignals...)
|
||||
signal.Notify(signalCh, forwardSignals...)
|
||||
go func() {
|
||||
for {
|
||||
<-signalCh
|
||||
resultCh <- struct{}{}
|
||||
}
|
||||
}()
|
||||
|
||||
return resultCh
|
||||
}
|
||||
|
||||
func credentialsSource(config *cliconfig.Config) (auth.CredentialsSource, error) {
|
||||
helperPlugins := pluginDiscovery.FindPlugins("credentials", globalPluginDirs())
|
||||
return config.CredentialsSource(helperPlugins)
|
||||
}
|
||||
27
cmd/tofu/experiments.go
Normal file
27
cmd/tofu/experiments.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
// experimentsAllowed can be set to any non-empty string using Go linker
|
||||
// arguments in order to enable the use of experimental features for a
|
||||
// particular OpenTofu build:
|
||||
//
|
||||
// go install -ldflags="-X 'main.experimentsAllowed=yes'"
|
||||
//
|
||||
// By default this variable is initialized as empty, in which case
|
||||
// experimental features are not available.
|
||||
//
|
||||
// The OpenTofu release process should arrange for this variable to be
|
||||
// set for alpha releases and development snapshots, but _not_ for
|
||||
// betas, release candidates, or final releases.
|
||||
//
|
||||
// (NOTE: Some experimental features predate the rule that experiments
|
||||
// are available only for alpha/dev builds, and so intentionally do not
|
||||
// make use of this setting to avoid retracting a previously-documented
|
||||
// open experiment.)
|
||||
var experimentsAllowed string
|
||||
|
||||
func ExperimentsAllowed() bool {
|
||||
return experimentsAllowed != ""
|
||||
}
|
||||
97
cmd/tofu/help.go
Normal file
97
cmd/tofu/help.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// helpFunc is a cli.HelpFunc that can be used to output the help CLI instructions for OpenTofu.
|
||||
func helpFunc(commands map[string]cli.CommandFactory) string {
|
||||
// Determine the maximum key length, and classify based on type
|
||||
var otherCommands []string
|
||||
maxKeyLen := 0
|
||||
|
||||
for key := range commands {
|
||||
if _, ok := HiddenCommands[key]; ok {
|
||||
// We don't consider hidden commands when deciding the
|
||||
// maximum command length.
|
||||
continue
|
||||
}
|
||||
|
||||
if len(key) > maxKeyLen {
|
||||
maxKeyLen = len(key)
|
||||
}
|
||||
|
||||
isOther := true
|
||||
for _, candidate := range PrimaryCommands {
|
||||
if candidate == key {
|
||||
isOther = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isOther {
|
||||
otherCommands = append(otherCommands, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(otherCommands)
|
||||
|
||||
// The output produced by this is included in the docs at
|
||||
// website/source/docs/cli/commands/index.html.markdown; if you
|
||||
// change this then consider updating that to match.
|
||||
helpText := fmt.Sprintf(`
|
||||
Usage: tofu [global options] <subcommand> [args]
|
||||
|
||||
The available commands for execution are listed below.
|
||||
The primary workflow commands are given first, followed by
|
||||
less common or more advanced commands.
|
||||
|
||||
Main commands:
|
||||
%s
|
||||
All other commands:
|
||||
%s
|
||||
Global options (use these before the subcommand, if any):
|
||||
-chdir=DIR Switch to a different working directory before executing the
|
||||
given subcommand.
|
||||
-help Show this help output, or the help for a specified subcommand.
|
||||
-version An alias for the "version" subcommand.
|
||||
`, listCommands(commands, PrimaryCommands, maxKeyLen), listCommands(commands, otherCommands, maxKeyLen))
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
// listCommands just lists the commands in the map with the
|
||||
// given maximum key length.
|
||||
func listCommands(allCommands map[string]cli.CommandFactory, order []string, maxKeyLen int) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
for _, key := range order {
|
||||
commandFunc, ok := allCommands[key]
|
||||
if !ok {
|
||||
// This suggests an inconsistency in the command table definitions
|
||||
// in commands.go .
|
||||
panic("command not found: " + key)
|
||||
}
|
||||
|
||||
command, err := commandFunc()
|
||||
if err != nil {
|
||||
// This would be really weird since there's no good reason for
|
||||
// any of our command factories to fail.
|
||||
log.Printf("[ERR] cli: Command '%s' failed to load: %s",
|
||||
key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||
buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis()))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
520
cmd/tofu/main.go
Normal file
520
cmd/tofu/main.go
Normal file
@@ -0,0 +1,520 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/apparentlymart/go-shquot/shquot"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/hashicorp/terraform-svchost/disco"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig"
|
||||
"github.com/opentofu/opentofu/internal/command/format"
|
||||
"github.com/opentofu/opentofu/internal/didyoumean"
|
||||
"github.com/opentofu/opentofu/internal/httpclient"
|
||||
"github.com/opentofu/opentofu/internal/logging"
|
||||
"github.com/opentofu/opentofu/internal/terminal"
|
||||
"github.com/opentofu/opentofu/version"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
backendInit "github.com/opentofu/opentofu/internal/backend/init"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvCLI is the environment variable name to set additional CLI args.
|
||||
EnvCLI = "TF_CLI_ARGS"
|
||||
|
||||
// The parent process will create a file to collect crash logs
|
||||
envTmpLogPath = "TF_TEMP_LOG_PATH"
|
||||
)
|
||||
|
||||
// ui wraps the primary output cli.Ui, and redirects Warn calls to Output
|
||||
// calls. This ensures that warnings are sent to stdout, and are properly
|
||||
// serialized within the stdout stream.
|
||||
type ui struct {
|
||||
cli.Ui
|
||||
}
|
||||
|
||||
func (u *ui) Warn(msg string) {
|
||||
u.Ui.Output(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Ui = &ui{&cli.BasicUi{
|
||||
Writer: os.Stdout,
|
||||
ErrorWriter: os.Stderr,
|
||||
Reader: os.Stdin,
|
||||
}}
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(realMain())
|
||||
}
|
||||
|
||||
func realMain() int {
|
||||
defer logging.PanicHandler()
|
||||
|
||||
var err error
|
||||
|
||||
err = openTelemetryInit()
|
||||
if err != nil {
|
||||
// openTelemetryInit can only fail if OpenTofu was run with an
|
||||
// explicit environment variable to enable telemetry collection,
|
||||
// so in typical use we cannot get here.
|
||||
Ui.Error(fmt.Sprintf("Could not initialize telemetry: %s", err))
|
||||
Ui.Error(fmt.Sprintf("Unset environment variable %s if you don't intend to collect telemetry from OpenTofu.", openTelemetryExporterEnvVar))
|
||||
return 1
|
||||
}
|
||||
var ctx context.Context
|
||||
var otelSpan trace.Span
|
||||
{
|
||||
// At minimum we emit a span covering the entire command execution.
|
||||
_, displayArgs := shquot.POSIXShellSplit(os.Args)
|
||||
ctx, otelSpan = tracer.Start(context.Background(), fmt.Sprintf("tofu %s", displayArgs))
|
||||
defer otelSpan.End()
|
||||
}
|
||||
|
||||
tmpLogPath := os.Getenv(envTmpLogPath)
|
||||
if tmpLogPath != "" {
|
||||
f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("[DEBUG] Adding temp file log sink: %s", f.Name())
|
||||
logging.RegisterSink(f)
|
||||
} else {
|
||||
log.Printf("[ERROR] Could not open temp log file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[INFO] OpenTofu version: %s %s",
|
||||
Version, VersionPrerelease)
|
||||
for _, depMod := range version.InterestingDependencies() {
|
||||
log.Printf("[DEBUG] using %s %s", depMod.Path, depMod.Version)
|
||||
}
|
||||
log.Printf("[INFO] Go runtime version: %s", runtime.Version())
|
||||
log.Printf("[INFO] CLI args: %#v", os.Args)
|
||||
if ExperimentsAllowed() {
|
||||
log.Printf("[INFO] This build of OpenTofu allows using experimental features")
|
||||
}
|
||||
|
||||
streams, err := terminal.Init()
|
||||
if err != nil {
|
||||
Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err))
|
||||
return 1
|
||||
}
|
||||
if streams.Stdout.IsTerminal() {
|
||||
log.Printf("[TRACE] Stdout is a terminal of width %d", streams.Stdout.Columns())
|
||||
} else {
|
||||
log.Printf("[TRACE] Stdout is not a terminal")
|
||||
}
|
||||
if streams.Stderr.IsTerminal() {
|
||||
log.Printf("[TRACE] Stderr is a terminal of width %d", streams.Stderr.Columns())
|
||||
} else {
|
||||
log.Printf("[TRACE] Stderr is not a terminal")
|
||||
}
|
||||
if streams.Stdin.IsTerminal() {
|
||||
log.Printf("[TRACE] Stdin is a terminal")
|
||||
} else {
|
||||
log.Printf("[TRACE] Stdin is not a terminal")
|
||||
}
|
||||
|
||||
// NOTE: We're intentionally calling LoadConfig _before_ handling a possible
|
||||
// -chdir=... option on the command line, so that a possible relative
|
||||
// path in the TERRAFORM_CONFIG_FILE environment variable (though probably
|
||||
// ill-advised) will be resolved relative to the true working directory,
|
||||
// not the overridden one.
|
||||
config, diags := cliconfig.LoadConfig()
|
||||
|
||||
if len(diags) > 0 {
|
||||
// Since we haven't instantiated a command.Meta yet, we need to do
|
||||
// some things manually here and use some "safe" defaults for things
|
||||
// that command.Meta could otherwise figure out in smarter ways.
|
||||
Ui.Error("There are some problems with the CLI configuration:")
|
||||
for _, diag := range diags {
|
||||
earlyColor := &colorstring.Colorize{
|
||||
Colors: colorstring.DefaultColors,
|
||||
Disable: true, // Disable color to be conservative until we know better
|
||||
Reset: true,
|
||||
}
|
||||
// We don't currently have access to the source code cache for
|
||||
// the parser used to load the CLI config, so we can't show
|
||||
// source code snippets in early diagnostics.
|
||||
Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
Ui.Error("As a result of the above problems, OpenTofu may not behave as intended.\n\n")
|
||||
// We continue to run anyway, since OpenTofu has reasonable defaults.
|
||||
}
|
||||
}
|
||||
|
||||
// Get any configured credentials from the config and initialize
|
||||
// a service discovery object. The slightly awkward predeclaration of
|
||||
// disco is required to allow us to pass untyped nil as the creds source
|
||||
// when creating the source fails. Otherwise we pass a typed nil which
|
||||
// breaks the nil checks in the disco object
|
||||
var services *disco.Disco
|
||||
credsSrc, err := credentialsSource(config)
|
||||
if err == nil {
|
||||
services = disco.NewWithCredentialsSource(credsSrc)
|
||||
} else {
|
||||
// Most commands don't actually need credentials, and most situations
|
||||
// that would get us here would already have been reported by the config
|
||||
// loading above, so we'll just log this one as an aid to debugging
|
||||
// in the unlikely event that it _does_ arise.
|
||||
log.Printf("[WARN] Cannot initialize remote host credentials manager: %s", err)
|
||||
// passing (untyped) nil as the creds source is okay because the disco
|
||||
// object checks that and just acts as though no credentials are present.
|
||||
services = disco.NewWithCredentialsSource(nil)
|
||||
}
|
||||
services.SetUserAgent(httpclient.OpenTofuUserAgent(version.String()))
|
||||
|
||||
providerSrc, diags := providerSource(config.ProviderInstallation, services)
|
||||
if len(diags) > 0 {
|
||||
Ui.Error("There are some problems with the provider_installation configuration:")
|
||||
for _, diag := range diags {
|
||||
earlyColor := &colorstring.Colorize{
|
||||
Colors: colorstring.DefaultColors,
|
||||
Disable: true, // Disable color to be conservative until we know better
|
||||
Reset: true,
|
||||
}
|
||||
Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
Ui.Error("As a result of the above problems, OpenTofu's provider installer may not behave as intended.\n\n")
|
||||
// We continue to run anyway, because most commands don't do provider installation.
|
||||
}
|
||||
}
|
||||
providerDevOverrides := providerDevOverrides(config.ProviderInstallation)
|
||||
|
||||
// The user can declare that certain providers are being managed on
|
||||
// OpenTofu's behalf using this environment variable. This is used
|
||||
// primarily by the SDK's acceptance testing framework.
|
||||
unmanagedProviders, err := parseReattachProviders(os.Getenv("TF_REATTACH_PROVIDERS"))
|
||||
if err != nil {
|
||||
Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Initialize the backends.
|
||||
backendInit.Init(services)
|
||||
|
||||
// Get the command line args.
|
||||
binName := filepath.Base(os.Args[0])
|
||||
args := os.Args[1:]
|
||||
|
||||
originalWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
// It would be very strange to end up here
|
||||
Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// The arguments can begin with a -chdir option to ask OpenTofu to switch
|
||||
// to a different working directory for the rest of its work. If that
|
||||
// option is present then extractChdirOption returns a trimmed args with that option removed.
|
||||
overrideWd, args, err := extractChdirOption(args)
|
||||
if err != nil {
|
||||
Ui.Error(fmt.Sprintf("Invalid -chdir option: %s", err))
|
||||
return 1
|
||||
}
|
||||
if overrideWd != "" {
|
||||
err := os.Chdir(overrideWd)
|
||||
if err != nil {
|
||||
Ui.Error(fmt.Sprintf("Error handling -chdir option: %s", err))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// In tests, Commands may already be set to provide mock commands
|
||||
if Commands == nil {
|
||||
// Commands get to hold on to the original working directory here,
|
||||
// in case they need to refer back to it for any special reason, though
|
||||
// they should primarily be working with the override working directory
|
||||
// that we've now switched to above.
|
||||
initCommands(ctx, originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders)
|
||||
}
|
||||
|
||||
// Attempt to ensure the config directory exists.
|
||||
configDir, err := cliconfig.ConfigDir()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to find the path to the config directory: %v", err)
|
||||
} else if err := mkConfigDir(configDir); err != nil {
|
||||
log.Printf("[ERROR] Failed to create the config directory at path %s: %v", configDir, err)
|
||||
}
|
||||
|
||||
// Make sure we clean up any managed plugins at the end of this
|
||||
defer plugin.CleanupClients()
|
||||
|
||||
// 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{
|
||||
Name: binName,
|
||||
Args: args,
|
||||
Commands: Commands,
|
||||
HelpFunc: helpFunc,
|
||||
HelpWriter: os.Stdout,
|
||||
|
||||
Autocomplete: true,
|
||||
AutocompleteInstall: "install-autocomplete",
|
||||
AutocompleteUninstall: "uninstall-autocomplete",
|
||||
}
|
||||
|
||||
// Before we continue we'll check whether the requested command is
|
||||
// actually known. If not, we might be able to suggest an alternative
|
||||
// if it seems like the user made a typo.
|
||||
// (This bypasses the built-in help handling in cli.CLI for the situation
|
||||
// where a command isn't found, because it's likely more helpful to
|
||||
// mention what specifically went wrong, rather than just printing out
|
||||
// a big block of usage information.)
|
||||
|
||||
// Check if this is being run via shell auto-complete, which uses the
|
||||
// binary name as the first argument and won't be listed as a subcommand.
|
||||
autoComplete := os.Getenv("COMP_LINE") != ""
|
||||
|
||||
if cmd := cliRunner.Subcommand(); cmd != "" && !autoComplete {
|
||||
// Due to the design of cli.CLI, this special error message only works
|
||||
// for typos of top-level commands. For a subcommand typo, like
|
||||
// "tofu state push", cmd would be "state" here and thus would
|
||||
// be considered to exist, and it would print out its own usage message.
|
||||
if _, exists := Commands[cmd]; !exists {
|
||||
suggestions := make([]string, 0, len(Commands))
|
||||
for name := range Commands {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
suggestion := didyoumean.NameSuggestion(cmd, suggestions)
|
||||
if suggestion != "" {
|
||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "OpenTofu has no command named %q.%s\n\nTo see all of OpenTofu's top-level commands, run:\n tofu -help\n\n", cmd, suggestion)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
exitCode, err := cliRunner.Run()
|
||||
if err != nil {
|
||||
Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error()))
|
||||
return 1
|
||||
}
|
||||
|
||||
// if we are exiting with a non-zero code, check if it was caused by any
|
||||
// plugins crashing
|
||||
if exitCode != 0 {
|
||||
for _, panicLog := range logging.PluginPanics() {
|
||||
Ui.Error(panicLog)
|
||||
}
|
||||
}
|
||||
|
||||
return exitCode
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// parse information on reattaching to unmanaged providers out of a
|
||||
// JSON-encoded environment variable.
|
||||
func parseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfig, error) {
|
||||
unmanagedProviders := map[addrs.Provider]*plugin.ReattachConfig{}
|
||||
if in != "" {
|
||||
type reattachConfig struct {
|
||||
Protocol string
|
||||
ProtocolVersion int
|
||||
Addr struct {
|
||||
Network string
|
||||
String string
|
||||
}
|
||||
Pid int
|
||||
Test bool
|
||||
}
|
||||
var m map[string]reattachConfig
|
||||
err := json.Unmarshal([]byte(in), &m)
|
||||
if err != nil {
|
||||
return unmanagedProviders, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err)
|
||||
}
|
||||
for p, c := range m {
|
||||
a, diags := addrs.ParseProviderSourceString(p)
|
||||
if diags.HasErrors() {
|
||||
return unmanagedProviders, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err())
|
||||
}
|
||||
var addr net.Addr
|
||||
switch c.Addr.Network {
|
||||
case "unix":
|
||||
addr, err = net.ResolveUnixAddr("unix", c.Addr.String)
|
||||
if err != nil {
|
||||
return unmanagedProviders, fmt.Errorf("Invalid unix socket path %q for %q: %w", c.Addr.String, p, err)
|
||||
}
|
||||
case "tcp":
|
||||
addr, err = net.ResolveTCPAddr("tcp", c.Addr.String)
|
||||
if err != nil {
|
||||
return unmanagedProviders, fmt.Errorf("Invalid TCP address %q for %q: %w", c.Addr.String, p, err)
|
||||
}
|
||||
default:
|
||||
return unmanagedProviders, fmt.Errorf("Unknown address type %q for %q", c.Addr.Network, p)
|
||||
}
|
||||
unmanagedProviders[a] = &plugin.ReattachConfig{
|
||||
Protocol: plugin.Protocol(c.Protocol),
|
||||
ProtocolVersion: c.ProtocolVersion,
|
||||
Pid: c.Pid,
|
||||
Test: c.Test,
|
||||
Addr: addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
return unmanagedProviders, nil
|
||||
}
|
||||
|
||||
func extractChdirOption(args []string) (string, []string, error) {
|
||||
if len(args) == 0 {
|
||||
return "", args, nil
|
||||
}
|
||||
|
||||
const argName = "-chdir"
|
||||
const argPrefix = argName + "="
|
||||
var argValue string
|
||||
var argPos int
|
||||
|
||||
for i, arg := range args {
|
||||
if !strings.HasPrefix(arg, "-") {
|
||||
// Because the chdir option is a subcommand-agnostic one, we require
|
||||
// it to appear before any subcommand argument, so if we find a
|
||||
// non-option before we find -chdir then we are finished.
|
||||
break
|
||||
}
|
||||
if arg == argName || arg == argPrefix {
|
||||
return "", args, fmt.Errorf("must include an equals sign followed by a directory path, like -chdir=example")
|
||||
}
|
||||
if strings.HasPrefix(arg, argPrefix) {
|
||||
argPos = i
|
||||
argValue = arg[len(argPrefix):]
|
||||
}
|
||||
}
|
||||
|
||||
// When we fall out here, we'll have populated argValue with a non-empty
|
||||
// string if the -chdir=... option was present and valid, or left it
|
||||
// empty if it wasn't present.
|
||||
if argValue == "" {
|
||||
return "", args, nil
|
||||
}
|
||||
|
||||
// If we did find the option then we'll need to produce a new args that
|
||||
// doesn't include it anymore.
|
||||
if argPos == 0 {
|
||||
// Easy case: we can just slice off the front
|
||||
return argValue, args[1:], nil
|
||||
}
|
||||
// Otherwise we need to construct a new array and copy to it.
|
||||
newArgs := make([]string, len(args)-1)
|
||||
copy(newArgs, args[:argPos])
|
||||
copy(newArgs[argPos:], args[argPos+1:])
|
||||
return argValue, newArgs, nil
|
||||
}
|
||||
|
||||
// Creates the the configuration directory.
|
||||
// `configDir` should refer to `~/.terraform.d` or its equivalent
|
||||
// on non-UNIX platforms.
|
||||
func mkConfigDir(configDir string) error {
|
||||
err := os.Mkdir(configDir, os.ModePerm)
|
||||
|
||||
if err == nil {
|
||||
log.Printf("[DEBUG] Created the config directory: %s", configDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.IsExist(err) {
|
||||
log.Printf("[DEBUG] Found the config directory: %s", configDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
370
cmd/tofu/main_test.go
Normal file
370
cmd/tofu/main_test.go
Normal file
@@ -0,0 +1,370 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestMain_cliArgsFromEnv(t *testing.T) {
|
||||
// Set up the state. This test really messes with the environment and
|
||||
// global state so we set things up to be restored.
|
||||
|
||||
// Restore original CLI args
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
// Set up test command and restore that
|
||||
Commands = make(map[string]cli.CommandFactory)
|
||||
defer func() {
|
||||
Commands = nil
|
||||
}()
|
||||
testCommandName := "unit-test-cli-args"
|
||||
testCommand := &testCommandCLI{}
|
||||
Commands[testCommandName] = func() (cli.Command, error) {
|
||||
return testCommand, nil
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Args []string
|
||||
Value string
|
||||
Expected []string
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"no env",
|
||||
[]string{testCommandName, "foo", "bar"},
|
||||
"",
|
||||
[]string{"foo", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"both env var and CLI",
|
||||
[]string{testCommandName, "foo", "bar"},
|
||||
"-foo baz",
|
||||
[]string{"-foo", "baz", "foo", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"only env var",
|
||||
[]string{testCommandName},
|
||||
"-foo bar",
|
||||
[]string{"-foo", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"cli string has blank values",
|
||||
[]string{testCommandName, "bar", "", "baz"},
|
||||
"-foo bar",
|
||||
[]string{"-foo", "bar", "bar", "", "baz"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"cli string has blank values before the command",
|
||||
[]string{"", testCommandName, "bar"},
|
||||
"-foo bar",
|
||||
[]string{"-foo", "bar", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
// this should fail gracefully, this is just testing
|
||||
// that we don't panic with our slice arithmetic
|
||||
"no command",
|
||||
[]string{},
|
||||
"-foo bar",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"single quoted strings",
|
||||
[]string{testCommandName, "foo"},
|
||||
"-foo 'bar baz'",
|
||||
[]string{"-foo", "bar baz", "foo"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"double quoted strings",
|
||||
[]string{testCommandName, "foo"},
|
||||
`-foo "bar baz"`,
|
||||
[]string{"-foo", "bar baz", "foo"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"double quoted single quoted strings",
|
||||
[]string{testCommandName, "foo"},
|
||||
`-foo "'bar baz'"`,
|
||||
[]string{"-foo", "'bar baz'", "foo"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
||||
os.Unsetenv(EnvCLI)
|
||||
defer os.Unsetenv(EnvCLI)
|
||||
|
||||
// Set the env var value
|
||||
if tc.Value != "" {
|
||||
if err := os.Setenv(EnvCLI, tc.Value); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the args
|
||||
args := make([]string, len(tc.Args)+1)
|
||||
args[0] = oldArgs[0] // process name
|
||||
copy(args[1:], tc.Args)
|
||||
|
||||
// Run it!
|
||||
os.Args = args
|
||||
testCommand.Args = nil
|
||||
exit := realMain()
|
||||
if (exit != 0) != tc.Err {
|
||||
t.Fatalf("bad: %d", exit)
|
||||
}
|
||||
if tc.Err {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
|
||||
t.Fatalf("expected args %#v but got %#v", tc.Expected, testCommand.Args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This test just has more options than the test above. Use this for
|
||||
// more control over behavior at the expense of more complex test structures.
|
||||
func TestMain_cliArgsFromEnvAdvanced(t *testing.T) {
|
||||
// Restore original CLI args
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
// Set up test command and restore that
|
||||
Commands = make(map[string]cli.CommandFactory)
|
||||
defer func() {
|
||||
Commands = nil
|
||||
}()
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Command string
|
||||
EnvVar string
|
||||
Args []string
|
||||
Value string
|
||||
Expected []string
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"targeted to another command",
|
||||
"command",
|
||||
EnvCLI + "_foo",
|
||||
[]string{"command", "foo", "bar"},
|
||||
"-flag",
|
||||
[]string{"foo", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"targeted to this command",
|
||||
"command",
|
||||
EnvCLI + "_command",
|
||||
[]string{"command", "foo", "bar"},
|
||||
"-flag",
|
||||
[]string{"-flag", "foo", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"targeted to a command with a hyphen",
|
||||
"command-name",
|
||||
EnvCLI + "_command_name",
|
||||
[]string{"command-name", "foo", "bar"},
|
||||
"-flag",
|
||||
[]string{"-flag", "foo", "bar"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"targeted to a command with a space",
|
||||
"command name",
|
||||
EnvCLI + "_command_name",
|
||||
[]string{"command", "name", "foo", "bar"},
|
||||
"-flag",
|
||||
[]string{"-flag", "foo", "bar"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
||||
// Set up test command and restore that
|
||||
testCommandName := tc.Command
|
||||
testCommand := &testCommandCLI{}
|
||||
defer func() { delete(Commands, testCommandName) }()
|
||||
Commands[testCommandName] = func() (cli.Command, error) {
|
||||
return testCommand, nil
|
||||
}
|
||||
|
||||
os.Unsetenv(tc.EnvVar)
|
||||
defer os.Unsetenv(tc.EnvVar)
|
||||
|
||||
// Set the env var value
|
||||
if tc.Value != "" {
|
||||
if err := os.Setenv(tc.EnvVar, tc.Value); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the args
|
||||
args := make([]string, len(tc.Args)+1)
|
||||
args[0] = oldArgs[0] // process name
|
||||
copy(args[1:], tc.Args)
|
||||
|
||||
// Run it!
|
||||
os.Args = args
|
||||
testCommand.Args = nil
|
||||
exit := realMain()
|
||||
if (exit != 0) != tc.Err {
|
||||
t.Fatalf("unexpected exit status %d; want 0", exit)
|
||||
}
|
||||
if tc.Err {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
|
||||
t.Fatalf("bad: %#v", testCommand.Args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// verify that we output valid autocomplete results
|
||||
func TestMain_autoComplete(t *testing.T) {
|
||||
// Restore original CLI args
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
// Set up test command and restore that
|
||||
Commands = make(map[string]cli.CommandFactory)
|
||||
defer func() {
|
||||
Commands = nil
|
||||
}()
|
||||
|
||||
// Set up test command and restore that
|
||||
Commands["foo"] = func() (cli.Command, error) {
|
||||
return &testCommandCLI{}, nil
|
||||
}
|
||||
|
||||
os.Setenv("COMP_LINE", "tofu versio")
|
||||
defer os.Unsetenv("COMP_LINE")
|
||||
|
||||
// Run it!
|
||||
os.Args = []string{"tofu", "tofu", "versio"}
|
||||
exit := realMain()
|
||||
if exit != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0", exit)
|
||||
}
|
||||
}
|
||||
|
||||
type testCommandCLI struct {
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (c *testCommandCLI) Run(args []string) int {
|
||||
c.Args = args
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *testCommandCLI) Synopsis() string { return "" }
|
||||
func (c *testCommandCLI) Help() string { return "" }
|
||||
|
||||
func TestWarnOutput(t *testing.T) {
|
||||
mock := cli.NewMockUi()
|
||||
wrapped := &ui{mock}
|
||||
wrapped.Warn("WARNING")
|
||||
|
||||
stderr := mock.ErrorWriter.String()
|
||||
stdout := mock.OutputWriter.String()
|
||||
|
||||
if stderr != "" {
|
||||
t.Fatalf("unexpected stderr: %q", stderr)
|
||||
}
|
||||
|
||||
if stdout != "WARNING\n" {
|
||||
t.Fatalf("unexpected stdout: %q\n", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkConfigDir_new(t *testing.T) {
|
||||
tmpConfigDir := filepath.Join(t.TempDir(), ".terraform.d")
|
||||
|
||||
err := mkConfigDir(tmpConfigDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create the new config directory: %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(tmpConfigDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Directory does not exist after creation: %v", err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
t.Fatalf("%s should be a directory but it's not", tmpConfigDir)
|
||||
}
|
||||
|
||||
mode := int(info.Mode().Perm())
|
||||
expectedMode := 0755
|
||||
if mode != expectedMode {
|
||||
t.Fatalf("Expected mode: %04o, but got: %04o", expectedMode, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkConfigDir_exists(t *testing.T) {
|
||||
tmpConfigDir := filepath.Join(t.TempDir(), ".terraform.d")
|
||||
os.Mkdir(tmpConfigDir, os.ModePerm)
|
||||
|
||||
err := mkConfigDir(tmpConfigDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create the new config directory: %v", err)
|
||||
}
|
||||
|
||||
_, err = os.Stat(tmpConfigDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Directory does not exist after creation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkConfigDir_noparent(t *testing.T) {
|
||||
tmpConfigDir := filepath.Join(t.TempDir(), "nonexistenthomedir", ".terraform.d")
|
||||
|
||||
err := mkConfigDir(tmpConfigDir)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error, but got none")
|
||||
}
|
||||
|
||||
// We wouldn't dare creating the home dir. If the parent of our config dir
|
||||
// is missing, it's likely an issue with the system.
|
||||
expectedError := fmt.Sprintf("mkdir %s: no such file or directory", tmpConfigDir)
|
||||
if err.Error() != expectedError {
|
||||
t.Fatalf("Expected error: %s, but got: %v", expectedError, err)
|
||||
}
|
||||
}
|
||||
34
cmd/tofu/plugins.go
Normal file
34
cmd/tofu/plugins.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig"
|
||||
)
|
||||
|
||||
// globalPluginDirs returns directories that should be searched for
|
||||
// globally-installed plugins (not specific to the current configuration).
|
||||
//
|
||||
// Earlier entries in this slice get priority over later when multiple copies
|
||||
// of the same plugin version are found, but newer versions always override
|
||||
// older versions where both satisfy the provider version constraints.
|
||||
func globalPluginDirs() []string {
|
||||
var ret []string
|
||||
// Look in ~/.terraform.d/plugins/ , or its equivalent on non-UNIX
|
||||
dir, err := cliconfig.ConfigDir()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Error finding global config directory: %s", err)
|
||||
} else {
|
||||
machineDir := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
|
||||
ret = append(ret, filepath.Join(dir, "plugins"))
|
||||
ret = append(ret, filepath.Join(dir, "plugins", machineDir))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
241
cmd/tofu/provider_source.go
Normal file
241
cmd/tofu/provider_source.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apparentlymart/go-userdirs/userdirs"
|
||||
"github.com/hashicorp/terraform-svchost/disco"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// providerSource constructs a provider source based on a combination of the
|
||||
// CLI configuration and some default search locations. This will be the
|
||||
// provider source used for provider installation in the "tofu init"
|
||||
// command, unless overridden by the special -plugin-dir option.
|
||||
func providerSource(configs []*cliconfig.ProviderInstallation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) {
|
||||
if len(configs) == 0 {
|
||||
// If there's no explicit installation configuration then we'll build
|
||||
// up an implicit one with direct registry installation along with
|
||||
// some automatically-selected local filesystem mirrors.
|
||||
return implicitProviderSource(services), nil
|
||||
}
|
||||
|
||||
// There should only be zero or one configurations, which is checked by
|
||||
// the validation logic in the cliconfig package. Therefore we'll just
|
||||
// ignore any additional configurations in here.
|
||||
config := configs[0]
|
||||
return explicitProviderSource(config, services)
|
||||
}
|
||||
|
||||
func explicitProviderSource(config *cliconfig.ProviderInstallation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
var searchRules []getproviders.MultiSourceSelector
|
||||
|
||||
log.Printf("[DEBUG] Explicit provider installation configuration is set")
|
||||
for _, methodConfig := range config.Methods {
|
||||
source, moreDiags := providerSourceForCLIConfigLocation(methodConfig.Location, services)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
include, err := getproviders.ParseMultiSourceMatchingPatterns(methodConfig.Include)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid provider source inclusion patterns",
|
||||
fmt.Sprintf("CLI config specifies invalid provider inclusion patterns: %s.", err),
|
||||
))
|
||||
continue
|
||||
}
|
||||
exclude, err := getproviders.ParseMultiSourceMatchingPatterns(methodConfig.Exclude)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid provider source exclusion patterns",
|
||||
fmt.Sprintf("CLI config specifies invalid provider exclusion patterns: %s.", err),
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
searchRules = append(searchRules, getproviders.MultiSourceSelector{
|
||||
Source: source,
|
||||
Include: include,
|
||||
Exclude: exclude,
|
||||
})
|
||||
|
||||
log.Printf("[TRACE] Selected provider installation method %#v with includes %s and excludes %s", methodConfig.Location, include, exclude)
|
||||
}
|
||||
|
||||
return getproviders.MultiSource(searchRules), diags
|
||||
}
|
||||
|
||||
// implicitProviderSource builds a default provider source to use if there's
|
||||
// no explicit provider installation configuration in the CLI config.
|
||||
//
|
||||
// This implicit source looks in a number of local filesystem directories and
|
||||
// directly in a provider's upstream registry. Any providers that have at least
|
||||
// one version available in a local directory are implicitly excluded from
|
||||
// direct installation, as if the user had listed them explicitly in the
|
||||
// "exclude" argument in the direct provider source in the CLI config.
|
||||
func implicitProviderSource(services *disco.Disco) getproviders.Source {
|
||||
// The local search directories we use for implicit configuration are:
|
||||
// - The "terraform.d/plugins" directory in the current working directory,
|
||||
// which we've historically documented as a place to put plugins as a
|
||||
// way to include them in bundles uploaded to Terraform Cloud, where
|
||||
// there has historically otherwise been no way to use custom providers.
|
||||
// - The "plugins" subdirectory of the CLI config search directory.
|
||||
// (thats ~/.terraform.d/plugins on Unix systems, equivalents elsewhere)
|
||||
// - The "plugins" subdirectory of any platform-specific search paths,
|
||||
// following e.g. the XDG base directory specification on Unix systems,
|
||||
// Apple's guidelines on OS X, and "known folders" on Windows.
|
||||
//
|
||||
// Any provider we find in one of those implicit directories will be
|
||||
// automatically excluded from direct installation from an upstream
|
||||
// registry. Anything not available locally will query its primary
|
||||
// upstream registry.
|
||||
var searchRules []getproviders.MultiSourceSelector
|
||||
|
||||
// We'll track any providers we can find in the local search directories
|
||||
// along the way, and then exclude them from the registry source we'll
|
||||
// finally add at the end.
|
||||
foundLocally := map[addrs.Provider]struct{}{}
|
||||
|
||||
addLocalDir := func(dir string) {
|
||||
// We'll make sure the directory actually exists before we add it,
|
||||
// because otherwise installation would always fail trying to look
|
||||
// in non-existent directories. (This is done here rather than in
|
||||
// the source itself because explicitly-selected directories via the
|
||||
// CLI config, once we have them, _should_ produce an error if they
|
||||
// don't exist to help users get their configurations right.)
|
||||
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
||||
log.Printf("[DEBUG] will search for provider plugins in %s", dir)
|
||||
fsSource := getproviders.NewFilesystemMirrorSource(dir)
|
||||
|
||||
// We'll peep into the source to find out what providers it seems
|
||||
// to be providing, so that we can exclude those from direct
|
||||
// install. This might fail, in which case we'll just silently
|
||||
// ignore it and assume it would fail during installation later too
|
||||
// and therefore effectively doesn't provide _any_ packages.
|
||||
if available, err := fsSource.AllAvailablePackages(); err == nil {
|
||||
for found := range available {
|
||||
foundLocally[found] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
searchRules = append(searchRules, getproviders.MultiSourceSelector{
|
||||
Source: fsSource,
|
||||
})
|
||||
|
||||
} else {
|
||||
log.Printf("[DEBUG] ignoring non-existing provider search directory %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
addLocalDir("terraform.d/plugins") // our "vendor" directory
|
||||
cliConfigDir, err := cliconfig.ConfigDir()
|
||||
if err == nil {
|
||||
addLocalDir(filepath.Join(cliConfigDir, "plugins"))
|
||||
}
|
||||
|
||||
// This "userdirs" library implements an appropriate user-specific and
|
||||
// app-specific directory layout for the current platform, such as XDG Base
|
||||
// Directory on Unix, using the following name strings to construct a
|
||||
// suitable application-specific subdirectory name following the
|
||||
// conventions for each platform:
|
||||
//
|
||||
// XDG (Unix): lowercase of the first string, "terraform"
|
||||
// Windows: two-level hierarchy of first two strings, "HashiCorp\Terraform"
|
||||
// OS X: reverse-DNS unique identifier, "io.terraform".
|
||||
sysSpecificDirs := userdirs.ForApp("Terraform", "HashiCorp", "io.terraform")
|
||||
for _, dir := range sysSpecificDirs.DataSearchPaths("plugins") {
|
||||
addLocalDir(dir)
|
||||
}
|
||||
|
||||
// Anything we found in local directories above is excluded from being
|
||||
// looked up via the registry source we're about to construct.
|
||||
var directExcluded getproviders.MultiSourceMatchingPatterns
|
||||
for addr := range foundLocally {
|
||||
directExcluded = append(directExcluded, addr)
|
||||
}
|
||||
|
||||
// Last but not least, the main registry source! We'll wrap a caching
|
||||
// layer around this one to help optimize the several network requests
|
||||
// we'll end up making to it while treating it as one of several sources
|
||||
// in a MultiSource (as recommended in the MultiSource docs).
|
||||
// This one is listed last so that if a particular version is available
|
||||
// both in one of the above directories _and_ in a remote registry, the
|
||||
// local copy will take precedence.
|
||||
searchRules = append(searchRules, getproviders.MultiSourceSelector{
|
||||
Source: getproviders.NewMemoizeSource(
|
||||
getproviders.NewRegistrySource(services),
|
||||
),
|
||||
Exclude: directExcluded,
|
||||
})
|
||||
|
||||
return getproviders.MultiSource(searchRules)
|
||||
}
|
||||
|
||||
func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) {
|
||||
if loc == cliconfig.ProviderInstallationDirect {
|
||||
return getproviders.NewMemoizeSource(
|
||||
getproviders.NewRegistrySource(services),
|
||||
), nil
|
||||
}
|
||||
|
||||
switch loc := loc.(type) {
|
||||
|
||||
case cliconfig.ProviderInstallationFilesystemMirror:
|
||||
return getproviders.NewFilesystemMirrorSource(string(loc)), nil
|
||||
|
||||
case cliconfig.ProviderInstallationNetworkMirror:
|
||||
url, err := url.Parse(string(loc))
|
||||
if err != nil {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid URL for provider installation source",
|
||||
fmt.Sprintf("Cannot parse %q as a URL for a network provider mirror: %s.", string(loc), err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
if url.Scheme != "https" || url.Host == "" {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid URL for provider installation source",
|
||||
fmt.Sprintf("Cannot use %q as a URL for a network provider mirror: the mirror must be at an https: URL.", string(loc)),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
return getproviders.NewHTTPMirrorSource(url, services.CredentialsSource()), nil
|
||||
|
||||
default:
|
||||
// We should not get here because the set of cases above should
|
||||
// be comprehensive for all of the
|
||||
// cliconfig.ProviderInstallationLocation implementations.
|
||||
panic(fmt.Sprintf("unexpected provider source location type %T", loc))
|
||||
}
|
||||
}
|
||||
|
||||
func providerDevOverrides(configs []*cliconfig.ProviderInstallation) map[addrs.Provider]getproviders.PackageLocalDir {
|
||||
if len(configs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// There should only be zero or one configurations, which is checked by
|
||||
// the validation logic in the cliconfig package. Therefore we'll just
|
||||
// ignore any additional configurations in here.
|
||||
return configs[0].DevOverrides
|
||||
}
|
||||
15
cmd/tofu/signal_unix.go
Normal file
15
cmd/tofu/signal_unix.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var ignoreSignals = []os.Signal{os.Interrupt}
|
||||
var forwardSignals = []os.Signal{syscall.SIGTERM}
|
||||
14
cmd/tofu/signal_windows.go
Normal file
14
cmd/tofu/signal_windows.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
var ignoreSignals = []os.Signal{os.Interrupt}
|
||||
var forwardSignals []os.Signal
|
||||
88
cmd/tofu/telemetry.go
Normal file
88
cmd/tofu/telemetry.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"go.opentelemetry.io/contrib/exporters/autoexport"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/opentofu/opentofu/version"
|
||||
)
|
||||
|
||||
// If this environment variable is set to "otlp" when running OpenTofu CLI
|
||||
// then we'll enable an experimental OTLP trace exporter.
|
||||
//
|
||||
// BEWARE! This is not a committed external interface.
|
||||
//
|
||||
// Everything about this is experimental and subject to change in future
|
||||
// releases. Do not depend on anything about the structure of this output.
|
||||
// This mechanism might be removed altogether if a different strategy seems
|
||||
// better based on experience with this experiment.
|
||||
const openTelemetryExporterEnvVar = "OTEL_TRACES_EXPORTER"
|
||||
|
||||
// tracer is the OpenTelemetry tracer to use for traces in package main only.
|
||||
var tracer trace.Tracer
|
||||
|
||||
func init() {
|
||||
tracer = otel.Tracer("github.com/opentofu/opentofu")
|
||||
}
|
||||
|
||||
// openTelemetryInit initializes the optional OpenTelemetry exporter.
|
||||
//
|
||||
// By default we don't export telemetry information at all, since OpenTofu is
|
||||
// a CLI tool and so we don't assume we're running in an environment with
|
||||
// a telemetry collector available.
|
||||
//
|
||||
// However, for those running OpenTofu in automation we allow setting
|
||||
// the standard OpenTelemetry environment variable OTEL_TRACES_EXPORTER=otlp
|
||||
// to enable an OTLP exporter, which is in turn configured by all of the
|
||||
// standard OTLP exporter environment variables:
|
||||
//
|
||||
// https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options
|
||||
//
|
||||
// We don't currently support any other telemetry export protocols, because
|
||||
// OTLP has emerged as a de-facto standard and each other exporter we support
|
||||
// means another relatively-heavy external dependency. OTLP happens to use
|
||||
// protocol buffers and gRPC, which OpenTofu would depend on for other reasons
|
||||
// anyway.
|
||||
func openTelemetryInit() error {
|
||||
// We'll check the environment variable ourselves first, because the
|
||||
// "autoexport" helper we're about to use is built under the assumption
|
||||
// that exporting should always be enabled and so will expect to find
|
||||
// an OTLP server on localhost if no environment variables are set at all.
|
||||
if os.Getenv(openTelemetryExporterEnvVar) != "otlp" {
|
||||
return nil // By default we just discard all telemetry calls
|
||||
}
|
||||
|
||||
otelResource := resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceNameKey.String("Terraform CLI"),
|
||||
semconv.ServiceVersionKey.String(version.Version),
|
||||
)
|
||||
|
||||
// If the environment variable was set to explicitly enable telemetry
|
||||
// then we'll enable it, using the "autoexport" library to automatically
|
||||
// handle the details based on the other OpenTelemetry standard environment
|
||||
// variables.
|
||||
exp, err := autoexport.NewSpanExporter(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sp := sdktrace.NewSimpleSpanProcessor(exp)
|
||||
provider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSpanProcessor(sp),
|
||||
sdktrace.WithResource(otelResource),
|
||||
)
|
||||
otel.SetTracerProvider(provider)
|
||||
|
||||
pgtr := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
|
||||
otel.SetTextMapPropagator(pgtr)
|
||||
|
||||
return nil
|
||||
}
|
||||
12
cmd/tofu/version.go
Normal file
12
cmd/tofu/version.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/opentofu/opentofu/version"
|
||||
)
|
||||
|
||||
var Version = version.Version
|
||||
|
||||
var VersionPrerelease = version.Prerelease
|
||||
15
cmd/tofu/working_dir.go
Normal file
15
cmd/tofu/working_dir.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/opentofu/opentofu/internal/command/workdir"
|
||||
|
||||
func WorkingDir(originalDir string, overrideDataDir string) *workdir.Dir {
|
||||
ret := workdir.NewDir(".") // caller should already have used os.Chdir in "-chdir=..." mode
|
||||
ret.OverrideOriginalWorkingDir(originalDir)
|
||||
if overrideDataDir != "" {
|
||||
ret.OverrideDataDir(overrideDataDir)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Reference in New Issue
Block a user