| // 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" | |
| "io" | |
| "os" | |
| "strings" | |
| "github.com/opentofu/opentofu/internal/tfdiags" | |
| "github.com/opentofu/opentofu/internal/command/arguments" | |
| "github.com/opentofu/opentofu/internal/command/clistate" | |
| "github.com/opentofu/opentofu/internal/command/views" | |
| "github.com/opentofu/opentofu/internal/encryption" | |
| "github.com/opentofu/opentofu/internal/states/statefile" | |
| "github.com/opentofu/opentofu/internal/states/statemgr" | |
| "github.com/opentofu/opentofu/internal/tofu" | |
| ) | |
| func StatePushCommander() Command { | |
| cmd := Command{ | |
| Name: "push", | |
| Short: "Update remote state from a local state file", | |
| Long: `Update remote state from a local state file at PATH. | |
| This command "pushes" a local state and overwrites remote state with a local state file. The command will protect you against writing an older serial or a different state file lineage unless you specify the "-force" flag. | |
| This command works with local state (it will overwrite the local state), but is less useful for this use case. | |
| If PATH is "-", then this command will read the state to push from stdin. | |
| Data from stdin is not streamed to the backend: it is loaded completely (until pipe close), verified, and then pushed.`, | |
| DiagsWithNewline: true, | |
| } | |
| args := arguments.BindStatePush(&cmd.CommandLine) | |
| cmd.Run = func(meta Meta) int { | |
| return StatePushCommand{StateMeta{meta}}.Execute(args, views.NewState(args.View, meta.View)) | |
| } | |
| return cmd | |
| } | |
| // StatePushCommand is a Command implementation that shows a single resource. | |
| type StatePushCommand struct { | |
| StateMeta | |
| } | |
| func (c *StatePushCommand) Run(rawArgs []string) int { | |
| return RunCommand(StatePushCommander(), c.Meta, rawArgs) | |
| } | |
| func (c StatePushCommand) Execute(args *arguments.StatePush, view views.State) int { | |
| var diags tfdiags.Diagnostics | |
| ctx := c.CommandContext() | |
| if diags := c.Meta.checkRequiredVersion(ctx); diags != nil { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| // Load the encryption configuration | |
| enc, encDiags := c.Encryption(ctx) | |
| if encDiags.HasErrors() { | |
| view.Diagnostics(encDiags) | |
| return 1 | |
| } | |
| // Determine our reader for the input state. This is the filepath | |
| // or stdin if "-" is given. | |
| var r io.Reader = os.Stdin | |
| if src := args.StateSrc; src != "-" { | |
| f, err := os.Open(src) | |
| if err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to open the given state file", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| // Note: we don't need to defer a Close here because we do a close | |
| // automatically below directly after the read. | |
| r = f | |
| } | |
| // Read the state | |
| srcStateFile, err := statefile.Read(r, encryption.StateEncryptionDisabled()) // Assume the given statefile is not encrypted | |
| if c, ok := r.(io.Closer); ok { | |
| // Close the reader if possible right now since we're done with it. | |
| c.Close() | |
| } | |
| if err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| fmt.Sprintf("Failed to read source state %q", args.StateSrc), | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| // Load the backend | |
| b, backendDiags := c.Backend(ctx, nil, enc.State()) | |
| if backendDiags.HasErrors() { | |
| view.Diagnostics(backendDiags) | |
| return 1 | |
| } | |
| // Determine the workspace name | |
| workspace, err := c.Workspace(ctx) | |
| if err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Error selecting workspace", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| // Check remote OpenTofu version is compatible | |
| remoteVersionDiags := c.remoteVersionCheck(b, workspace) | |
| view.Diagnostics(remoteVersionDiags) | |
| if remoteVersionDiags.HasErrors() { | |
| return 1 | |
| } | |
| // Get the state manager for the currently-selected workspace | |
| stateMgr, err := b.StateMgr(ctx, workspace) | |
| if err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to load destination state", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| if c.stateArgs.Lock { | |
| stateLocker := clistate.NewLocker(c.stateArgs.LockTimeout, view.Backend().StateLocker()) | |
| if diags := stateLocker.Lock(stateMgr, "state-push"); diags.HasErrors() { | |
| view.Diagnostics(diags) | |
| return 1 | |
| } | |
| defer func() { | |
| if diags := stateLocker.Unlock(); diags.HasErrors() { | |
| view.Diagnostics(diags) | |
| } | |
| }() | |
| } | |
| if err := stateMgr.RefreshState(context.TODO()); err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to refresh destination state", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| if srcStateFile == nil { | |
| // We'll push a new empty state instead | |
| srcStateFile = statemgr.NewStateFile() | |
| } | |
| // Import it, forcing through the lineage/serial if requested and possible. | |
| if err := statemgr.Import(srcStateFile, stateMgr, args.Force); err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to write the imported state", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| // Get schemas, if possible, before writing state | |
| var schemas *tofu.Schemas | |
| if isCloudMode(b) { | |
| schemas, diags = c.MaybeGetSchemas(ctx, srcStateFile.State, nil) | |
| } | |
| if err := stateMgr.WriteState(srcStateFile.State); err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to write state", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| if err := stateMgr.PersistState(context.TODO(), schemas); err != nil { | |
| view.Diagnostics(diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to persist state", | |
| err.Error(), | |
| ))) | |
| return 1 | |
| } | |
| view.Diagnostics(diags) | |
| return 0 | |
| } | |
| func (c *StatePushCommand) Help() string { | |
| helpText := ` | |
| Usage: tofu [global options] state push [options] PATH | |
| Update remote state from a local state file at PATH. | |
| This command "pushes" a local state and overwrites remote state | |
| with a local state file. The command will protect you against writing | |
| an older serial or a different state file lineage unless you specify the | |
| "-force" flag. | |
| This command works with local state (it will overwrite the local | |
| state), but is less useful for this use case. | |
| If PATH is "-", then this command will read the state to push from stdin. | |
| Data from stdin is not streamed to the backend: it is loaded completely | |
| (until pipe close), verified, and then pushed. | |
| Options: | |
| -force Write the state even if lineages don't match or the | |
| remote serial is higher. | |
| -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. | |
| -var 'foo=bar' Set a value for one of the input variables in the root | |
| module of the configuration. Use this option more than | |
| once to set more than one variable. | |
| -var-file=filename Load variable values from the given file, in addition | |
| to the default files terraform.tfvars and *.auto.tfvars. | |
| Use this option more than once to include more than one | |
| variables file. | |
| -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. | |
| ` | |
| return strings.TrimSpace(helpText) | |
| } | |
| func (c *StatePushCommand) Synopsis() string { | |
| return "Update remote state from a local state file" | |
| } |