github.com/opentofu/opentofu@v1.7.1/internal/command/meta.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package command 7 8 import ( 9 "bytes" 10 "context" 11 "errors" 12 "flag" 13 "fmt" 14 "io" 15 "log" 16 "os" 17 "path/filepath" 18 "strconv" 19 "strings" 20 "time" 21 22 "github.com/hashicorp/go-plugin" 23 "github.com/hashicorp/terraform-svchost/disco" 24 "github.com/mitchellh/cli" 25 "github.com/mitchellh/colorstring" 26 27 "github.com/opentofu/opentofu/internal/addrs" 28 "github.com/opentofu/opentofu/internal/backend" 29 "github.com/opentofu/opentofu/internal/backend/local" 30 "github.com/opentofu/opentofu/internal/command/arguments" 31 "github.com/opentofu/opentofu/internal/command/format" 32 "github.com/opentofu/opentofu/internal/command/views" 33 "github.com/opentofu/opentofu/internal/command/webbrowser" 34 "github.com/opentofu/opentofu/internal/command/workdir" 35 "github.com/opentofu/opentofu/internal/configs" 36 "github.com/opentofu/opentofu/internal/configs/configload" 37 "github.com/opentofu/opentofu/internal/getproviders" 38 legacy "github.com/opentofu/opentofu/internal/legacy/tofu" 39 "github.com/opentofu/opentofu/internal/providers" 40 "github.com/opentofu/opentofu/internal/provisioners" 41 "github.com/opentofu/opentofu/internal/states" 42 "github.com/opentofu/opentofu/internal/terminal" 43 "github.com/opentofu/opentofu/internal/tfdiags" 44 "github.com/opentofu/opentofu/internal/tofu" 45 ) 46 47 // Meta are the meta-options that are available on all or most commands. 48 type Meta struct { 49 // The exported fields below should be set by anyone using a 50 // command with a Meta field. These are expected to be set externally 51 // (not from within the command itself). 52 53 // WorkingDir is an object representing the "working directory" where we're 54 // running commands. In the normal case this literally refers to the 55 // working directory of the OpenTofu process, though this can take on 56 // a more symbolic meaning when the user has overridden default behavior 57 // to specify a different working directory or to override the special 58 // data directory where we'll persist settings that must survive between 59 // consecutive commands. 60 // 61 // We're currently gradually migrating the various bits of state that 62 // must persist between consecutive commands in a session to be encapsulated 63 // in here, but we're not there yet and so there are also some methods on 64 // Meta which directly read and modify paths inside the data directory. 65 WorkingDir *workdir.Dir 66 67 // Streams tracks the raw Stdout, Stderr, and Stdin handles along with 68 // some basic metadata about them, such as whether each is connected to 69 // a terminal, how wide the possible terminal is, etc. 70 // 71 // For historical reasons this might not be set in unit test code, and 72 // so functions working with this field must check if it's nil and 73 // do some default behavior instead if so, rather than panicking. 74 Streams *terminal.Streams 75 76 View *views.View 77 78 Color bool // True if output should be colored 79 GlobalPluginDirs []string // Additional paths to search for plugins 80 Ui cli.Ui // Ui for output 81 82 // Services provides access to remote endpoint information for 83 // 'tofu-native' services running at a specific user-facing hostname. 84 Services *disco.Disco 85 86 // RunningInAutomation indicates that commands are being run by an 87 // automated system rather than directly at a command prompt. 88 // 89 // This is a hint to various command routines that it may be confusing 90 // to print out messages that suggest running specific follow-up 91 // commands, since the user consuming the output will not be 92 // in a position to run such commands. 93 // 94 // The intended use-case of this flag is when OpenTofu is running in 95 // some sort of workflow orchestration tool which is abstracting away 96 // the specific commands being run. 97 RunningInAutomation bool 98 99 // CLIConfigDir is the directory from which CLI configuration files were 100 // read by the caller and the directory where any changes to CLI 101 // configuration files by commands should be made. 102 // 103 // If this is empty then no configuration directory is available and 104 // commands which require one cannot proceed. 105 CLIConfigDir string 106 107 // PluginCacheDir, if non-empty, enables caching of downloaded plugins 108 // into the given directory. 109 PluginCacheDir string 110 111 // PluginCacheMayBreakDependencyLockFile is a temporary CLI configuration-based 112 // opt out for the behavior of only using the plugin cache dir if its 113 // contents match checksums recorded in the dependency lock file. 114 // 115 // This is an accommodation for those who currently essentially ignore the 116 // dependency lock file -- treating it only as transient working directory 117 // state -- and therefore don't care if the plugin cache dir causes the 118 // checksums inside to only be sufficient for the computer where OpenTofu 119 // is currently running. 120 // 121 // We intend to remove this exception again (making the CLI configuration 122 // setting a silent no-op) in future once we've improved the dependency 123 // lock file mechanism so that it's usable for everyone and there are no 124 // longer any compelling reasons for folks to not lock their dependencies. 125 PluginCacheMayBreakDependencyLockFile bool 126 127 // ProviderSource allows determining the available versions of a provider 128 // and determines where a distribution package for a particular 129 // provider version can be obtained. 130 ProviderSource getproviders.Source 131 132 // BrowserLauncher is used by commands that need to open a URL in a 133 // web browser. 134 BrowserLauncher webbrowser.Launcher 135 136 // A context.Context provided by the caller -- typically "package main" -- 137 // which might be carrying telemetry-related metadata and so should be 138 // used when creating downstream traces, etc. 139 // 140 // This isn't guaranteed to be set, so use [Meta.CommandContext] to 141 // safely create a context for the entire execution of a command, which 142 // will be connected to this parent context if it's present. 143 CallerContext context.Context 144 145 // When this channel is closed, the command will be cancelled. 146 ShutdownCh <-chan struct{} 147 148 // ProviderDevOverrides are providers where we ignore the lock file, the 149 // configured version constraints, and the local cache directory and just 150 // always use exactly the path specified. This is intended to allow 151 // provider developers to easily test local builds without worrying about 152 // what version number they might eventually be released as, or what 153 // checksums they have. 154 ProviderDevOverrides map[addrs.Provider]getproviders.PackageLocalDir 155 156 // UnmanagedProviders are a set of providers that exist as processes 157 // predating OpenTofu, which OpenTofu should use but not worry about the 158 // lifecycle of. 159 // 160 // This is essentially a more extreme version of ProviderDevOverrides where 161 // OpenTofu doesn't even worry about how the provider server gets launched, 162 // just trusting that someone else did it before running OpenTofu. 163 UnmanagedProviders map[addrs.Provider]*plugin.ReattachConfig 164 165 // AllowExperimentalFeatures controls whether a command that embeds this 166 // Meta is permitted to make use of experimental OpenTofu features. 167 // 168 // Set this field only during the initial creation of Meta. If you change 169 // this field after calling methods of type Meta then the resulting 170 // behavior is undefined. 171 // 172 // In normal code this would be set by package main only in builds 173 // explicitly marked as being alpha releases or development snapshots, 174 // making experimental features unavailable otherwise. Test code may 175 // choose to set this if it needs to exercise experimental features. 176 // 177 // Some experiments predated the addition of this setting, and may 178 // therefore still be available even if this flag is false. Our intent 179 // is that all/most _future_ experiments will be unavailable unless this 180 // flag is set, to reinforce that experiments are not for production use. 181 AllowExperimentalFeatures bool 182 183 //---------------------------------------------------------- 184 // Protected: commands can set these 185 //---------------------------------------------------------- 186 187 // pluginPath is a user defined set of directories to look for plugins. 188 // This is set during init with the `-plugin-dir` flag, saved to a file in 189 // the data directory. 190 // This overrides all other search paths when discovering plugins. 191 pluginPath []string 192 193 // Override certain behavior for tests within this package 194 testingOverrides *testingOverrides 195 196 //---------------------------------------------------------- 197 // Private: do not set these 198 //---------------------------------------------------------- 199 200 // configLoader is a shared configuration loader that is used by 201 // LoadConfig and other commands that access configuration files. 202 // It is initialized on first use. 203 configLoader *configload.Loader 204 205 // backendState is the currently active backend state 206 backendState *legacy.BackendState 207 208 // Variables for the context (private) 209 variableArgs rawFlags 210 input bool 211 212 // Targets for this context (private) 213 targets []addrs.Targetable 214 targetFlags []string 215 216 // Internal fields 217 color bool 218 oldUi cli.Ui 219 220 // The fields below are expected to be set by the command via 221 // command line flags. See the Apply command for an example. 222 // 223 // statePath is the path to the state file. If this is empty, then 224 // no state will be loaded. It is also okay for this to be a path to 225 // a file that doesn't exist; it is assumed that this means that there 226 // is simply no state. 227 // 228 // stateOutPath is used to override the output path for the state. 229 // If not provided, the StatePath is used causing the old state to 230 // be overridden. 231 // 232 // backupPath is used to backup the state file before writing a modified 233 // version. It defaults to stateOutPath + DefaultBackupExtension 234 // 235 // parallelism is used to control the number of concurrent operations 236 // allowed when walking the graph 237 // 238 // provider is to specify specific resource providers 239 // 240 // stateLock is set to false to disable state locking 241 // 242 // stateLockTimeout is the optional duration to retry a state locks locks 243 // when it is already locked by another process. 244 // 245 // forceInitCopy suppresses confirmation for copying state data during 246 // init. 247 // 248 // reconfigure forces init to ignore any stored configuration. 249 // 250 // migrateState confirms the user wishes to migrate from the prior backend 251 // configuration to a new configuration. 252 // 253 // compactWarnings (-compact-warnings) selects a more compact presentation 254 // of warnings in the output when they are not accompanied by errors. 255 statePath string 256 stateOutPath string 257 backupPath string 258 parallelism int 259 stateLock bool 260 stateLockTimeout time.Duration 261 forceInitCopy bool 262 reconfigure bool 263 migrateState bool 264 compactWarnings bool 265 266 // Used with commands which write state to allow users to write remote 267 // state even if the remote and local OpenTofu versions don't match. 268 ignoreRemoteVersion bool 269 270 outputInJSON bool 271 } 272 273 type testingOverrides struct { 274 Providers map[addrs.Provider]providers.Factory 275 Provisioners map[string]provisioners.Factory 276 } 277 278 // initStatePaths is used to initialize the default values for 279 // statePath, stateOutPath, and backupPath 280 func (m *Meta) initStatePaths() { 281 if m.statePath == "" { 282 m.statePath = DefaultStateFilename 283 } 284 if m.stateOutPath == "" { 285 m.stateOutPath = m.statePath 286 } 287 if m.backupPath == "" { 288 m.backupPath = m.stateOutPath + DefaultBackupExtension 289 } 290 } 291 292 // StateOutPath returns the true output path for the state file 293 func (m *Meta) StateOutPath() string { 294 return m.stateOutPath 295 } 296 297 // Colorize returns the colorization structure for a command. 298 func (m *Meta) Colorize() *colorstring.Colorize { 299 colors := make(map[string]string) 300 for k, v := range colorstring.DefaultColors { 301 colors[k] = v 302 } 303 colors["purple"] = "38;5;57" 304 305 return &colorstring.Colorize{ 306 Colors: colors, 307 Disable: !m.color, 308 Reset: true, 309 } 310 } 311 312 // fixupMissingWorkingDir is a compensation for various existing tests which 313 // directly construct incomplete "Meta" objects. Specifically, it deals with 314 // a test that omits a WorkingDir value by constructing one just-in-time. 315 // 316 // We shouldn't ever rely on this in any real codepath, because it doesn't 317 // take into account the various ways users can override our default 318 // directory selection behaviors. 319 func (m *Meta) fixupMissingWorkingDir() { 320 if m.WorkingDir == nil { 321 log.Printf("[WARN] This 'Meta' object is missing its WorkingDir, so we're creating a default one suitable only for tests") 322 m.WorkingDir = workdir.NewDir(".") 323 } 324 } 325 326 // DataDir returns the directory where local data will be stored. 327 // Defaults to DefaultDataDir in the current working directory. 328 func (m *Meta) DataDir() string { 329 m.fixupMissingWorkingDir() 330 return m.WorkingDir.DataDir() 331 } 332 333 const ( 334 // InputModeEnvVar is the environment variable that, if set to "false" or 335 // "0", causes tofu commands to behave as if the `-input=false` flag was 336 // specified. 337 InputModeEnvVar = "TF_INPUT" 338 ) 339 340 // InputMode returns the type of input we should ask for in the form of 341 // tofu.InputMode which is passed directly to Context.Input. 342 func (m *Meta) InputMode() tofu.InputMode { 343 if test || !m.input { 344 return 0 345 } 346 347 if envVar := os.Getenv(InputModeEnvVar); envVar != "" { 348 if v, err := strconv.ParseBool(envVar); err == nil { 349 if !v { 350 return 0 351 } 352 } 353 } 354 355 var mode tofu.InputMode 356 mode |= tofu.InputModeProvider 357 358 return mode 359 } 360 361 // UIInput returns a UIInput object to be used for asking for input. 362 func (m *Meta) UIInput() tofu.UIInput { 363 return &UIInput{ 364 Colorize: m.Colorize(), 365 } 366 } 367 368 // OutputColumns returns the number of columns that normal (non-error) UI 369 // output should be wrapped to fill. 370 // 371 // This is the column count to use if you'll be printing your message via 372 // the Output or Info methods of m.Ui. 373 func (m *Meta) OutputColumns() int { 374 if m.Streams == nil { 375 // A default for unit tests that don't populate Meta fully. 376 return 78 377 } 378 return m.Streams.Stdout.Columns() 379 } 380 381 // ErrorColumns returns the number of columns that error UI output should be 382 // wrapped to fill. 383 // 384 // This is the column count to use if you'll be printing your message via 385 // the Error or Warn methods of m.Ui. 386 func (m *Meta) ErrorColumns() int { 387 if m.Streams == nil { 388 // A default for unit tests that don't populate Meta fully. 389 return 78 390 } 391 return m.Streams.Stderr.Columns() 392 } 393 394 // StdinPiped returns true if the input is piped. 395 func (m *Meta) StdinPiped() bool { 396 if m.Streams == nil { 397 // If we don't have m.Streams populated then we're presumably in a unit 398 // test that doesn't properly populate Meta, so we'll just say the 399 // output _isn't_ piped because that's the common case and so most likely 400 // to be useful to a unit test. 401 return false 402 } 403 return !m.Streams.Stdin.IsTerminal() 404 } 405 406 // InterruptibleContext returns a context.Context that will be cancelled 407 // if the process is interrupted by a platform-specific interrupt signal. 408 // 409 // The typical way to use this is to pass the result of [Meta.CommandContext] 410 // as the base context, but that's appropriate only if the interruptible 411 // context is being created directly inside the "Run" method of a particular 412 // command, to create a context representing the entire remaining runtime of 413 // that command: 414 // 415 // As usual with cancelable contexts, the caller must always call the given 416 // cancel function once all operations are complete in order to make sure 417 // that the context resources will still be freed even if there is no 418 // interruption. 419 // 420 // // This example is only for when using this function very early in 421 // // the "Run" method of a Command implementation. If you already have 422 // // an active context, pass that in as base instead. 423 // ctx, done := c.InterruptibleContext(c.CommandContext()) 424 // defer done() 425 func (m *Meta) InterruptibleContext(base context.Context) (context.Context, context.CancelFunc) { 426 if m.ShutdownCh == nil { 427 // If we're running in a unit testing context without a shutdown 428 // channel populated then we'll return an uncancelable channel. 429 return base, func() {} 430 } 431 432 ctx, cancel := context.WithCancel(base) 433 go func() { 434 select { 435 case <-m.ShutdownCh: 436 cancel() 437 case <-ctx.Done(): 438 // finished without being interrupted 439 } 440 }() 441 return ctx, cancel 442 } 443 444 // CommandContext returns the "root context" to use in the main Run function 445 // of a command. 446 // 447 // This method is just a substitute for passing a context directly to the 448 // "Run" method of a command, which we can't do because that API is owned by 449 // mitchellh/cli rather than by OpenTofu. Use this only in situations 450 // comparable to the context having been passed in as an argument to Run. 451 // 452 // If the caller (e.g. "package main") provided a context when it instantiated 453 // the Meta then the returned context will inherit all of its values, deadlines, 454 // etc. If the caller did not provide a context then the result is an inert 455 // background context ready to be passed to other functions. 456 func (m *Meta) CommandContext() context.Context { 457 if m.CallerContext == nil { 458 return context.Background() 459 } 460 // We just return the caller context directly for now, since we don't 461 // have anything to add to it. 462 return m.CallerContext 463 } 464 465 // RunOperation executes the given operation on the given backend, blocking 466 // until that operation completes or is interrupted, and then returns 467 // the RunningOperation object representing the completed or 468 // aborted operation that is, despite the name, no longer running. 469 // 470 // An error is returned if the operation either fails to start or is cancelled. 471 // If the operation runs to completion then no error is returned even if the 472 // operation itself is unsuccessful. Use the "Result" field of the 473 // returned operation object to recognize operation-level failure. 474 func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) { 475 if opReq.View == nil { 476 panic("RunOperation called with nil View") 477 } 478 if opReq.ConfigDir != "" { 479 opReq.ConfigDir = m.normalizePath(opReq.ConfigDir) 480 } 481 482 op, err := b.Operation(context.Background(), opReq) 483 if err != nil { 484 return nil, fmt.Errorf("error starting operation: %w", err) 485 } 486 487 // Wait for the operation to complete or an interrupt to occur 488 select { 489 case <-m.ShutdownCh: 490 // gracefully stop the operation 491 op.Stop() 492 493 // Notify the user 494 opReq.View.Interrupted() 495 496 // Still get the result, since there is still one 497 select { 498 case <-m.ShutdownCh: 499 opReq.View.FatalInterrupt() 500 501 // cancel the operation completely 502 op.Cancel() 503 504 // the operation should return asap 505 // but timeout just in case 506 select { 507 case <-op.Done(): 508 case <-time.After(5 * time.Second): 509 } 510 511 return nil, errors.New("operation canceled") 512 513 case <-op.Done(): 514 // operation completed after Stop 515 } 516 case <-op.Done(): 517 // operation completed normally 518 } 519 520 return op, nil 521 } 522 523 // contextOpts returns the options to use to initialize a OpenTofu 524 // context with the settings from this Meta. 525 func (m *Meta) contextOpts() (*tofu.ContextOpts, error) { 526 workspace, err := m.Workspace() 527 if err != nil { 528 return nil, err 529 } 530 531 var opts tofu.ContextOpts 532 533 opts.UIInput = m.UIInput() 534 opts.Parallelism = m.parallelism 535 536 // If testingOverrides are set, we'll skip the plugin discovery process 537 // and just work with what we've been given, thus allowing the tests 538 // to provide mock providers and provisioners. 539 if m.testingOverrides != nil { 540 opts.Providers = m.testingOverrides.Providers 541 opts.Provisioners = m.testingOverrides.Provisioners 542 } else { 543 var providerFactories map[addrs.Provider]providers.Factory 544 providerFactories, err = m.providerFactories() 545 opts.Providers = providerFactories 546 opts.Provisioners = m.provisionerFactories() 547 } 548 549 opts.Meta = &tofu.ContextMeta{ 550 Env: workspace, 551 OriginalWorkingDir: m.WorkingDir.OriginalWorkingDir(), 552 } 553 554 return &opts, err 555 } 556 557 // defaultFlagSet creates a default flag set for commands. 558 // See also command/arguments/default.go 559 func (m *Meta) defaultFlagSet(n string) *flag.FlagSet { 560 f := flag.NewFlagSet(n, flag.ContinueOnError) 561 f.SetOutput(io.Discard) 562 563 // Set the default Usage to empty 564 f.Usage = func() {} 565 566 return f 567 } 568 569 // ignoreRemoteVersionFlagSet add the ignore-remote version flag to suppress 570 // the error when the configured OpenTofu version on the remote workspace 571 // does not match the local OpenTofu version. 572 func (m *Meta) ignoreRemoteVersionFlagSet(n string) *flag.FlagSet { 573 f := m.defaultFlagSet(n) 574 575 f.BoolVar(&m.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local OpenTofu versions are incompatible") 576 577 return f 578 } 579 580 // extendedFlagSet adds custom flags that are mostly used by commands 581 // that are used to run an operation like plan or apply. 582 func (m *Meta) extendedFlagSet(n string) *flag.FlagSet { 583 f := m.defaultFlagSet(n) 584 585 f.BoolVar(&m.input, "input", true, "input") 586 f.Var((*FlagStringSlice)(&m.targetFlags), "target", "resource to target") 587 f.BoolVar(&m.compactWarnings, "compact-warnings", false, "use compact warnings") 588 589 if m.variableArgs.items == nil { 590 m.variableArgs = newRawFlags("-var") 591 } 592 varValues := m.variableArgs.Alias("-var") 593 varFiles := m.variableArgs.Alias("-var-file") 594 f.Var(varValues, "var", "variables") 595 f.Var(varFiles, "var-file", "variable file") 596 597 // commands that bypass locking will supply their own flag on this var, 598 // but set the initial meta value to true as a failsafe. 599 m.stateLock = true 600 601 return f 602 } 603 604 // process will process any -no-color entries out of the arguments. This 605 // will potentially modify the args in-place. It will return the resulting 606 // slice, and update the Meta and Ui. 607 func (m *Meta) process(args []string) []string { 608 // We do this so that we retain the ability to technically call 609 // process multiple times, even if we have no plans to do so 610 if m.oldUi != nil { 611 m.Ui = m.oldUi 612 } 613 614 // Set colorization 615 m.color = m.Color 616 i := 0 // output index 617 for _, v := range args { 618 if v == "-no-color" { 619 m.color = false 620 m.Color = false 621 } else { 622 // copy and increment index 623 args[i] = v 624 i++ 625 } 626 } 627 args = args[:i] 628 629 // Set the UI 630 m.oldUi = m.Ui 631 m.Ui = &cli.ConcurrentUi{ 632 Ui: &ColorizeUi{ 633 Colorize: m.Colorize(), 634 ErrorColor: "[red]", 635 WarnColor: "[yellow]", 636 Ui: m.oldUi, 637 }, 638 } 639 640 // Reconfigure the view. This is necessary for commands which use both 641 // views.View and cli.Ui during the migration phase. 642 if m.View != nil { 643 m.View.Configure(&arguments.View{ 644 CompactWarnings: m.compactWarnings, 645 NoColor: !m.Color, 646 }) 647 } 648 649 return args 650 } 651 652 // uiHook returns the UiHook to use with the context. 653 func (m *Meta) uiHook() *views.UiHook { 654 return views.NewUiHook(m.View) 655 } 656 657 // confirm asks a yes/no confirmation. 658 func (m *Meta) confirm(opts *tofu.InputOpts) (bool, error) { 659 if !m.Input() { 660 return false, errors.New("input is disabled") 661 } 662 663 for i := 0; i < 2; i++ { 664 v, err := m.UIInput().Input(context.Background(), opts) 665 if err != nil { 666 return false, fmt.Errorf( 667 "Error asking for confirmation: %w", err) 668 } 669 670 switch strings.ToLower(v) { 671 case "no": 672 return false, nil 673 case "yes": 674 return true, nil 675 } 676 } 677 return false, nil 678 } 679 680 // showDiagnostics displays error and warning messages in the UI. 681 // 682 // "Diagnostics" here means the Diagnostics type from the tfdiag package, 683 // though as a convenience this function accepts anything that could be 684 // passed to the "Append" method on that type, converting it to Diagnostics 685 // before displaying it. 686 // 687 // Internally this function uses Diagnostics.Append, and so it will panic 688 // if given unsupported value types, just as Append does. 689 func (m *Meta) showDiagnostics(vals ...interface{}) { 690 var diags tfdiags.Diagnostics 691 diags = diags.Append(vals...) 692 diags.Sort() 693 694 if len(diags) == 0 { 695 return 696 } 697 698 if m.outputInJSON { 699 jsonView := views.NewJSONView(m.View) 700 jsonView.Diagnostics(diags) 701 return 702 } 703 704 outputWidth := m.ErrorColumns() 705 706 diags = diags.ConsolidateWarnings(1) 707 708 // Since warning messages are generally competing 709 if m.compactWarnings { 710 // If the user selected compact warnings and all of the diagnostics are 711 // warnings then we'll use a more compact representation of the warnings 712 // that only includes their summaries. 713 // We show full warnings if there are also errors, because a warning 714 // can sometimes serve as good context for a subsequent error. 715 useCompact := true 716 for _, diag := range diags { 717 if diag.Severity() != tfdiags.Warning { 718 useCompact = false 719 break 720 } 721 } 722 if useCompact { 723 msg := format.DiagnosticWarningsCompact(diags, m.Colorize()) 724 msg = "\n" + msg + "\nTo see the full warning notes, run OpenTofu without -compact-warnings.\n" 725 m.Ui.Warn(msg) 726 return 727 } 728 } 729 730 for _, diag := range diags { 731 var msg string 732 if m.Color { 733 msg = format.Diagnostic(diag, m.configSources(), m.Colorize(), outputWidth) 734 } else { 735 msg = format.DiagnosticPlain(diag, m.configSources(), outputWidth) 736 } 737 738 switch diag.Severity() { 739 case tfdiags.Error: 740 m.Ui.Error(msg) 741 case tfdiags.Warning: 742 m.Ui.Warn(msg) 743 default: 744 m.Ui.Output(msg) 745 } 746 } 747 } 748 749 // WorkspaceNameEnvVar is the name of the environment variable that can be used 750 // to set the name of the OpenTofu workspace, overriding the workspace chosen 751 // by `tofu workspace select`. 752 // 753 // Note that this environment variable is ignored by `tofu workspace new` 754 // and `tofu workspace delete`. 755 const WorkspaceNameEnvVar = "TF_WORKSPACE" 756 757 var errInvalidWorkspaceNameEnvVar = fmt.Errorf("Invalid workspace name set using %s", WorkspaceNameEnvVar) 758 759 // Workspace returns the name of the currently configured workspace, corresponding 760 // to the desired named state. 761 func (m *Meta) Workspace() (string, error) { 762 current, overridden := m.WorkspaceOverridden() 763 if overridden && !validWorkspaceName(current) { 764 return "", errInvalidWorkspaceNameEnvVar 765 } 766 return current, nil 767 } 768 769 // WorkspaceOverridden returns the name of the currently configured workspace, 770 // corresponding to the desired named state, as well as a bool saying whether 771 // this was set via the TF_WORKSPACE environment variable. 772 func (m *Meta) WorkspaceOverridden() (string, bool) { 773 if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" { 774 return envVar, true 775 } 776 777 envData, err := os.ReadFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile)) 778 current := string(bytes.TrimSpace(envData)) 779 if current == "" { 780 current = backend.DefaultStateName 781 } 782 783 if err != nil && !os.IsNotExist(err) { 784 // always return the default if we can't get a workspace name 785 log.Printf("[ERROR] failed to read current workspace: %s", err) 786 } 787 788 return current, false 789 } 790 791 // SetWorkspace saves the given name as the current workspace in the local 792 // filesystem. 793 func (m *Meta) SetWorkspace(name string) error { 794 err := os.MkdirAll(m.DataDir(), 0755) 795 if err != nil { 796 return err 797 } 798 799 err = os.WriteFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile), []byte(name), 0644) 800 if err != nil { 801 return err 802 } 803 return nil 804 } 805 806 // isAutoVarFile determines if the file ends with .auto.tfvars or .auto.tfvars.json 807 func isAutoVarFile(path string) bool { 808 return strings.HasSuffix(path, ".auto.tfvars") || 809 strings.HasSuffix(path, ".auto.tfvars.json") 810 } 811 812 // FIXME: as an interim refactoring step, we apply the contents of the state 813 // arguments directly to the Meta object. Future work would ideally update the 814 // code paths which use these arguments to be passed them directly for clarity. 815 func (m *Meta) applyStateArguments(args *arguments.State) { 816 m.stateLock = args.Lock 817 m.stateLockTimeout = args.LockTimeout 818 m.statePath = args.StatePath 819 m.stateOutPath = args.StateOutPath 820 m.backupPath = args.BackupPath 821 } 822 823 // checkRequiredVersion loads the config and check if the 824 // core version requirements are satisfied. 825 func (m *Meta) checkRequiredVersion() tfdiags.Diagnostics { 826 var diags tfdiags.Diagnostics 827 828 loader, err := m.initConfigLoader() 829 if err != nil { 830 diags = diags.Append(err) 831 return diags 832 } 833 834 pwd, err := os.Getwd() 835 if err != nil { 836 diags = diags.Append(fmt.Errorf("Error getting pwd: %w", err)) 837 return diags 838 } 839 840 config, configDiags := loader.LoadConfig(pwd) 841 if configDiags.HasErrors() { 842 diags = diags.Append(configDiags) 843 return diags 844 } 845 846 versionDiags := tofu.CheckCoreVersionRequirements(config) 847 if versionDiags.HasErrors() { 848 diags = diags.Append(versionDiags) 849 return diags 850 } 851 852 return nil 853 } 854 855 // MaybeGetSchemas attempts to load and return the schemas 856 // If there is not enough information to return the schemas, 857 // it could potentially return nil without errors. It is the 858 // responsibility of the caller to handle the lack of schema 859 // information accordingly 860 func (c *Meta) MaybeGetSchemas(state *states.State, config *configs.Config) (*tofu.Schemas, tfdiags.Diagnostics) { 861 var diags tfdiags.Diagnostics 862 863 path, err := os.Getwd() 864 if err != nil { 865 diags.Append(tfdiags.SimpleWarning(failedToLoadSchemasMessage)) 866 return nil, diags 867 } 868 869 if config == nil { 870 config, diags = c.loadConfig(path) 871 if diags.HasErrors() { 872 diags.Append(tfdiags.SimpleWarning(failedToLoadSchemasMessage)) 873 return nil, diags 874 } 875 } 876 877 if config != nil || state != nil { 878 opts, err := c.contextOpts() 879 if err != nil { 880 diags = diags.Append(err) 881 return nil, diags 882 } 883 tfCtx, ctxDiags := tofu.NewContext(opts) 884 diags = diags.Append(ctxDiags) 885 if ctxDiags.HasErrors() { 886 return nil, diags 887 } 888 var schemaDiags tfdiags.Diagnostics 889 schemas, schemaDiags := tfCtx.Schemas(config, state) 890 diags = diags.Append(schemaDiags) 891 if schemaDiags.HasErrors() { 892 return nil, diags 893 } 894 return schemas, diags 895 896 } 897 return nil, diags 898 }