| // Copyright (c) The OpenTofu Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Copyright (c) 2023 HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package command | |
| import ( | |
| "context" | |
| "fmt" | |
| "strings" | |
| "github.com/opentofu/opentofu/internal/backend" | |
| "github.com/opentofu/opentofu/internal/command/arguments" | |
| "github.com/opentofu/opentofu/internal/command/views" | |
| "github.com/opentofu/opentofu/internal/configs/configload" | |
| "github.com/opentofu/opentofu/internal/encryption" | |
| "github.com/opentofu/opentofu/internal/plans/planfile" | |
| "github.com/opentofu/opentofu/internal/tfdiags" | |
| ) | |
| func ApplyCommander() Command { | |
| cmd := Command{ | |
| Name: "apply", | |
| Short: "Create or update infrastructure", | |
| Long: `Creates or updates infrastructure according to OpenTofu configuration files in the current directory. | |
| By default, OpenTofu will generate a new plan and present it for your approval before taking any action. You can optionally provide a plan file created by a previous call to "tofu plan", in which case OpenTofu will take the actions described in that plan without any confirmation prompt.`, | |
| GroupID: MainCommandGroup.ID, | |
| } | |
| args := arguments.BindApply(&cmd.CommandLine) | |
| cmd.Run = func(meta Meta) int { | |
| return ApplyCommand{meta, false}.Execute(args, views.NewApply(args.View, false, meta.View)) | |
| } | |
| return cmd | |
| } | |
| func DestroyCommander() Command { | |
| cmd := Command{ | |
| Name: "destroy", | |
| Short: "Destroy previously-created infrastructure", | |
| Long: `Destroy OpenTofu-managed infrastructure. | |
| This command is a convenience alias for: | |
| tofu apply -destroy`, | |
| GroupID: MainCommandGroup.ID, | |
| } | |
| args := arguments.BindApplyDestroy(&cmd.CommandLine) | |
| cmd.Run = func(meta Meta) int { | |
| return ApplyCommand{meta, true}.Execute(args, views.NewApply(args.View, true, meta.View)) | |
| } | |
| return cmd | |
| } | |
| // ApplyCommand is a Command implementation that applies a OpenTofu | |
| // configuration and actually builds or changes infrastructure. | |
| type ApplyCommand struct { | |
| Meta | |
| // If true, then this apply command will become the "destroy" | |
| // command. It is just like apply but only processes a destroy. | |
| Destroy bool | |
| } | |
| func (c *ApplyCommand) Run(rawArgs []string) int { | |
| if c.Destroy { | |
| return RunCommand(DestroyCommander(), c.Meta, rawArgs) | |
| } | |
| return RunCommand(ApplyCommander(), c.Meta, rawArgs) | |
| } | |
| func (c ApplyCommand) Execute(args *arguments.Apply, view views.Apply) int { | |
| var diags tfdiags.Diagnostics | |
| ctx := c.CommandContext() | |
| ctx = tfdiags.ContextWithLintFilterHints(ctx, args.View.LintInclude, args.View.LintExclude) | |
| diags = diags.Append(tfdiags.ExperimentalLintWarn(ctx)) | |
| // Check for user-supplied plugin path | |
| var err error | |
| if c.pluginPath, err = c.loadPluginPath(); err != nil { | |
| diags = diags.Append(err) | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| // Load the encryption configuration | |
| enc, encDiags := c.Encryption(ctx) | |
| diags = diags.Append(encDiags) | |
| if encDiags.HasErrors() { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| // Attempt to load the plan file, if specified | |
| planFile, planDiags := c.LoadPlanFile(args.PlanPath, enc) | |
| diags = diags.Append(planDiags) | |
| if diags.HasErrors() { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| // Prepare the backend, passing the plan file if present, and the | |
| // backend-specific arguments | |
| be, beDiags := c.PrepareBackend(ctx, planFile, args.State, view.Backend(), enc.State()) | |
| diags = diags.Append(beDiags) | |
| if diags.HasErrors() { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| // Build the operation request | |
| opReq, opDiags := c.OperationRequest(ctx, be, view, args, planFile, enc) | |
| diags = diags.Append(opDiags) | |
| // Before we delegate to the backend, we'll print any warning diagnostics | |
| // we've accumulated here, since the backend will start fresh with its own | |
| // diagnostics. | |
| view.Diagnostics(diags) | |
| if diags.HasErrors() { | |
| return 1 | |
| } | |
| diags = nil | |
| // Run the operation | |
| op, diags := c.RunOperation(ctx, be, opReq) | |
| view.Diagnostics(diags) | |
| if diags.HasErrors() { | |
| return 1 | |
| } | |
| if op.Result != backend.OperationSuccess { | |
| return op.Result.ExitStatus() | |
| } | |
| // Render the resource count and outputs, unless those counts are being | |
| // rendered already in a remote OpenTofu process. | |
| if rb, isRemoteBackend := be.(BackendWithRemoteTerraformVersion); !isRemoteBackend || rb.IsLocalOperations() { | |
| view.ResourceCount(args.State.StateOutPath) | |
| if !c.Destroy && op.State != nil { | |
| view.Outputs(op.State.RootModule().OutputValues) | |
| } | |
| } | |
| view.Diagnostics(diags) | |
| if diags.HasErrors() { | |
| return 1 | |
| } | |
| return 0 | |
| } | |
| func (c *ApplyCommand) LoadPlanFile(path string, enc encryption.Encryption) (*planfile.WrappedPlanFile, tfdiags.Diagnostics) { | |
| var planFile *planfile.WrappedPlanFile | |
| var diags tfdiags.Diagnostics | |
| // Try to load plan if path is specified | |
| if path != "" { | |
| var err error | |
| planFile, err = c.PlanFile(path, enc.Plan()) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| fmt.Sprintf("Failed to load %q as a plan file", path), | |
| fmt.Sprintf("Error: %s", err), | |
| )) | |
| return nil, diags | |
| } | |
| // If the path doesn't look like a plan, both planFile and err will be | |
| // nil. In that case, the user is probably trying to use the positional | |
| // argument to specify a configuration path. Point them at -chdir. | |
| if planFile == nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| fmt.Sprintf("Failed to load %q as a plan file", path), | |
| "The specified path is a directory, not a plan file. You can use the global -chdir flag to use this directory as the configuration root.", | |
| )) | |
| return nil, diags | |
| } | |
| // If we successfully loaded a plan but this is a destroy operation, | |
| // explain that this is not supported. | |
| if c.Destroy { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Destroy can't be called with a plan file", | |
| fmt.Sprintf("If this plan was created using plan -destroy, apply it using:\n tofu apply %q", path), | |
| )) | |
| return nil, diags | |
| } | |
| } | |
| return planFile, diags | |
| } | |
| func (c *ApplyCommand) PrepareBackend(ctx context.Context, planFile *planfile.WrappedPlanFile, args *arguments.State, backendView views.Backend, enc encryption.StateEncryption) (backend.Enhanced, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| // Load the backend | |
| var be backend.Enhanced | |
| var beDiags tfdiags.Diagnostics | |
| if lp, ok := planFile.Local(); ok { | |
| plan, err := lp.ReadPlan() | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to read plan from plan file", | |
| fmt.Sprintf("Cannot read the plan from the given plan file: %s.", err), | |
| )) | |
| return nil, diags | |
| } | |
| if plan.Backend.Config == nil { | |
| // Should never happen; always indicates a bug in the creation of the plan file | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to read plan from plan file", | |
| "The given plan file does not have a valid backend configuration. This is a bug in the OpenTofu command that generated this plan file.", | |
| )) | |
| return nil, diags | |
| } | |
| be, beDiags = c.BackendForLocalPlan(ctx, plan.Backend, enc) | |
| } else { | |
| // Both new plans and saved cloud plans load their backend from config. | |
| backendConfig, configDiags := c.loadBackendConfig(ctx, ".") | |
| diags = diags.Append(configDiags) | |
| if configDiags.HasErrors() { | |
| return nil, diags | |
| } | |
| be, beDiags = c.Backend(ctx, &BackendOpts{ | |
| Config: backendConfig, | |
| View: backendView, | |
| }, enc) | |
| } | |
| diags = diags.Append(beDiags) | |
| if beDiags.HasErrors() { | |
| return nil, diags | |
| } | |
| return be, diags | |
| } | |
| func (c *ApplyCommand) OperationRequest( | |
| ctx context.Context, | |
| be backend.Enhanced, | |
| view views.Apply, | |
| applyArgs *arguments.Apply, | |
| planFile *planfile.WrappedPlanFile, | |
| enc encryption.Encryption, | |
| ) (*backend.Operation, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| // Applying changes with dev overrides in effect could make it impossible | |
| // to switch back to a release version if the schema isn't compatible, | |
| // so we'll warn about it. | |
| diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) | |
| // Build the operation | |
| opReq := c.Operation(ctx, be, view.Backend(), enc) | |
| opReq.AutoApprove = applyArgs.AutoApprove | |
| opReq.SuppressForgetErrorsDuringDestroy = applyArgs.SuppressForgetErrorsDuringDestroy | |
| opReq.ConfigDir = "." | |
| opReq.PlanMode = applyArgs.Operation.PlanMode | |
| opReq.Hooks = view.Hooks() | |
| if c.SystemCfg.E2ETestingFeaturesEnabled { | |
| opReq.Hooks = append(opReq.Hooks, &e2eTestingApplyHook{}) | |
| } | |
| opReq.PlanFile = planFile | |
| opReq.PlanRefresh = applyArgs.Operation.Refresh | |
| opReq.Targets = applyArgs.Operation.Targets | |
| opReq.Excludes = applyArgs.Operation.Excludes | |
| opReq.ForceReplace = applyArgs.Operation.ForceReplace | |
| opReq.Type = backend.OperationTypeApply | |
| opReq.View = view.Operation() | |
| var err error | |
| opReq.ConfigLoader, err = configload.Initialise(c.configLoader()) | |
| if err != nil { | |
| diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %w", err)) | |
| return nil, diags | |
| } | |
| return opReq, diags | |
| } | |
| func (c *ApplyCommand) Help() string { | |
| if c.Destroy { | |
| return c.helpDestroy() | |
| } | |
| return c.helpApply() | |
| } | |
| func (c *ApplyCommand) Synopsis() string { | |
| if c.Destroy { | |
| return "Destroy previously-created infrastructure" | |
| } | |
| return "Create or update infrastructure" | |
| } | |
| func (c *ApplyCommand) helpApply() string { | |
| helpText := ` | |
| Usage: tofu [global options] apply [options] [PLAN] | |
| Creates or updates infrastructure according to OpenTofu configuration | |
| files in the current directory. | |
| By default, OpenTofu will generate a new plan and present it for your | |
| approval before taking any action. You can optionally provide a plan | |
| file created by a previous call to "tofu plan", in which case | |
| OpenTofu will take the actions described in that plan without any | |
| confirmation prompt. | |
| Options: | |
| -auto-approve Skip interactive approval of plan before applying. | |
| -backup=path Path to backup the existing state file before | |
| modifying. Defaults to the "-state-out" path with | |
| ".backup" extension. Set to "-" to disable backup. | |
| -compact-warnings If OpenTofu produces any warnings that are not | |
| accompanied by errors, show them in a more compact | |
| form that includes only the summary messages. | |
| -consolidate-warnings=false If OpenTofu produces any warnings, no consolidation | |
| will be performed. All locations, for all warnings | |
| will be listed. Enabled by default. | |
| -consolidate-errors If OpenTofu produces any errors, no consolidation | |
| will be performed. All locations, for all errors | |
| will be listed. Disabled by default. | |
| -destroy Destroy OpenTofu-managed infrastructure. | |
| The command "tofu destroy" is a convenience alias | |
| for this option. | |
| -lock=false Don't hold a state lock during the operation. | |
| This is dangerous if others might concurrently | |
| run commands against the same workspace. | |
| -lock-timeout=0s Duration to retry a state lock. | |
| -input=true Ask for input for variables if not directly set. | |
| -no-color If specified, output won't contain any color. | |
| -concise Disables progress-related messages in the output. | |
| -parallelism=n Limit the number of parallel resource operations. | |
| Defaults to 10. | |
| -state=path Path to read and save state (unless state-out | |
| is specified). Defaults to "terraform.tfstate". | |
| -state-out=path Path to write state to that is different than | |
| "-state". This can be used to preserve the old | |
| state. | |
| -show-sensitive If specified, sensitive values will be displayed. | |
| -suppress-forget-errors Suppress the error that occurs when a destroy | |
| operation completes successfully but leaves | |
| forgotten instances behind. | |
| -var 'foo=bar' Set a variable in the OpenTofu configuration. | |
| This flag can be set multiple times. | |
| -var-file=foo Set variables in the OpenTofu configuration from | |
| a file. | |
| If "terraform.tfvars" or any ".auto.tfvars" | |
| files are present, they will be automatically | |
| loaded. | |
| -json Produce output in a machine-readable JSON format, | |
| suitable for use in text editor integrations and | |
| other automated systems. Always disables color. | |
| -json-into=out.json Produce the same output as -json, but sent directly | |
| to the given file. This allows automation to preserve | |
| the original human-readable output streams, while | |
| capturing more detailed logs for machine analysis. | |
| -deprecation=module:m Specify what type of warnings are shown. Accepted | |
| values for "m": all, local, none. Default: all. | |
| When "all" is selected, OpenTofu will show the | |
| deprecation warnings for all modules. When "local" | |
| is selected, the warns will be shown only for the | |
| modules that are imported with a relative path. | |
| When "none" is selected, all the deprecation | |
| warnings will be dropped. | |
| -lint=all Configures the linting rules to be executed during | |
| this command. By specifying this flag, the built-in | |
| linting will be enabled, which will start issuing | |
| warning diagnostics if any included rule will be | |
| violated. For more details on the format and | |
| available linting rules, refer to the official | |
| documentation. | |
| If you don't provide a saved plan file then this command will also accept | |
| all of the plan-customization options accepted by the tofu plan command. | |
| For more information on those options, run: | |
| tofu plan -help | |
| ` | |
| return strings.TrimSpace(helpText) | |
| } | |
| func (c *ApplyCommand) helpDestroy() string { | |
| helpText := ` | |
| Usage: tofu [global options] destroy [options] | |
| Destroy OpenTofu-managed infrastructure. | |
| This command is a convenience alias for: | |
| tofu apply -destroy | |
| Options: | |
| -suppress-forget-errors Suppress the error that occurs when a destroy | |
| operation completes successfully but leaves | |
| forgotten instances behind. | |
| This command also accepts many of the plan-customization options accepted by | |
| the tofu plan command. For more information on those options, run: | |
| tofu plan -help | |
| ` | |
| return strings.TrimSpace(helpText) | |
| } |