github.com/opentofu/opentofu@v1.7.1/internal/command/state_push.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  	"fmt"
    10  	"io"
    11  	"os"
    12  	"strings"
    13  
    14  	"github.com/mitchellh/cli"
    15  
    16  	"github.com/opentofu/opentofu/internal/command/arguments"
    17  	"github.com/opentofu/opentofu/internal/command/clistate"
    18  	"github.com/opentofu/opentofu/internal/command/views"
    19  	"github.com/opentofu/opentofu/internal/encryption"
    20  	"github.com/opentofu/opentofu/internal/states/statefile"
    21  	"github.com/opentofu/opentofu/internal/states/statemgr"
    22  	"github.com/opentofu/opentofu/internal/tfdiags"
    23  	"github.com/opentofu/opentofu/internal/tofu"
    24  )
    25  
    26  // StatePushCommand is a Command implementation that shows a single resource.
    27  type StatePushCommand struct {
    28  	Meta
    29  	StateMeta
    30  }
    31  
    32  func (c *StatePushCommand) Run(args []string) int {
    33  	args = c.Meta.process(args)
    34  	var flagForce bool
    35  	cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state push")
    36  	cmdFlags.BoolVar(&flagForce, "force", false, "")
    37  	cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
    38  	cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
    39  	if err := cmdFlags.Parse(args); err != nil {
    40  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    41  		return 1
    42  	}
    43  	args = cmdFlags.Args()
    44  
    45  	if len(args) != 1 {
    46  		c.Ui.Error("Exactly one argument expected.\n")
    47  		return cli.RunResultHelp
    48  	}
    49  
    50  	if diags := c.Meta.checkRequiredVersion(); diags != nil {
    51  		c.showDiagnostics(diags)
    52  		return 1
    53  	}
    54  
    55  	// Load the encryption configuration
    56  	enc, encDiags := c.Encryption()
    57  	if encDiags.HasErrors() {
    58  		c.showDiagnostics(encDiags)
    59  		return 1
    60  	}
    61  
    62  	// Determine our reader for the input state. This is the filepath
    63  	// or stdin if "-" is given.
    64  	var r io.Reader = os.Stdin
    65  	if args[0] != "-" {
    66  		f, err := os.Open(args[0])
    67  		if err != nil {
    68  			c.Ui.Error(err.Error())
    69  			return 1
    70  		}
    71  
    72  		// Note: we don't need to defer a Close here because we do a close
    73  		// automatically below directly after the read.
    74  
    75  		r = f
    76  	}
    77  
    78  	// Read the state
    79  	srcStateFile, err := statefile.Read(r, encryption.StateEncryptionDisabled()) // Assume the given statefile is not encrypted
    80  	if c, ok := r.(io.Closer); ok {
    81  		// Close the reader if possible right now since we're done with it.
    82  		c.Close()
    83  	}
    84  	if err != nil {
    85  		c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err))
    86  		return 1
    87  	}
    88  
    89  	// Load the backend
    90  	b, backendDiags := c.Backend(nil, enc.State())
    91  	if backendDiags.HasErrors() {
    92  		c.showDiagnostics(backendDiags)
    93  		return 1
    94  	}
    95  
    96  	// Determine the workspace name
    97  	workspace, err := c.Workspace()
    98  	if err != nil {
    99  		c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
   100  		return 1
   101  	}
   102  
   103  	// Check remote OpenTofu version is compatible
   104  	remoteVersionDiags := c.remoteVersionCheck(b, workspace)
   105  	c.showDiagnostics(remoteVersionDiags)
   106  	if remoteVersionDiags.HasErrors() {
   107  		return 1
   108  	}
   109  
   110  	// Get the state manager for the currently-selected workspace
   111  	stateMgr, err := b.StateMgr(workspace)
   112  	if err != nil {
   113  		c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
   114  		return 1
   115  	}
   116  
   117  	if c.stateLock {
   118  		stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
   119  		if diags := stateLocker.Lock(stateMgr, "state-push"); diags.HasErrors() {
   120  			c.showDiagnostics(diags)
   121  			return 1
   122  		}
   123  		defer func() {
   124  			if diags := stateLocker.Unlock(); diags.HasErrors() {
   125  				c.showDiagnostics(diags)
   126  			}
   127  		}()
   128  	}
   129  
   130  	if err := stateMgr.RefreshState(); err != nil {
   131  		c.Ui.Error(fmt.Sprintf("Failed to refresh destination state: %s", err))
   132  		return 1
   133  	}
   134  
   135  	if srcStateFile == nil {
   136  		// We'll push a new empty state instead
   137  		srcStateFile = statemgr.NewStateFile()
   138  	}
   139  
   140  	// Import it, forcing through the lineage/serial if requested and possible.
   141  	if err := statemgr.Import(srcStateFile, stateMgr, flagForce); err != nil {
   142  		c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
   143  		return 1
   144  	}
   145  
   146  	// Get schemas, if possible, before writing state
   147  	var schemas *tofu.Schemas
   148  	var diags tfdiags.Diagnostics
   149  	if isCloudMode(b) {
   150  		schemas, diags = c.MaybeGetSchemas(srcStateFile.State, nil)
   151  	}
   152  
   153  	if err := stateMgr.WriteState(srcStateFile.State); err != nil {
   154  		c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
   155  		return 1
   156  	}
   157  	if err := stateMgr.PersistState(schemas); err != nil {
   158  		c.Ui.Error(fmt.Sprintf("Failed to persist state: %s", err))
   159  		return 1
   160  	}
   161  
   162  	c.showDiagnostics(diags)
   163  	return 0
   164  }
   165  
   166  func (c *StatePushCommand) Help() string {
   167  	helpText := `
   168  Usage: tofu [global options] state push [options] PATH
   169  
   170    Update remote state from a local state file at PATH.
   171  
   172    This command "pushes" a local state and overwrites remote state
   173    with a local state file. The command will protect you against writing
   174    an older serial or a different state file lineage unless you specify the
   175    "-force" flag.
   176  
   177    This command works with local state (it will overwrite the local
   178    state), but is less useful for this use case.
   179  
   180    If PATH is "-", then this command will read the state to push from stdin.
   181    Data from stdin is not streamed to the backend: it is loaded completely
   182    (until pipe close), verified, and then pushed.
   183  
   184  Options:
   185  
   186    -force              Write the state even if lineages don't match or the
   187                        remote serial is higher.
   188  
   189    -lock=false         Don't hold a state lock during the operation. This is
   190                        dangerous if others might concurrently run commands
   191                        against the same workspace.
   192  
   193    -lock-timeout=0s    Duration to retry a state lock.
   194  
   195  `
   196  	return strings.TrimSpace(helpText)
   197  }
   198  
   199  func (c *StatePushCommand) Synopsis() string {
   200  	return "Update remote state from a local state file"
   201  }