mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-18 18:29:44 -05:00
refactor state-mv command argument parsing
This commit is contained in:
parent
52bbc57c62
commit
9f3c282096
3 changed files with 288 additions and 34 deletions
93
internal/command/arguments/state_mv.go
Normal file
93
internal/command/arguments/state_mv.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// StateMv represents the command-line arguments for the state mv command.
|
||||
type StateMv struct {
|
||||
// DryRun, if true, prints out what would be moved without actually
|
||||
// moving anything.
|
||||
DryRun bool
|
||||
|
||||
// BackupPath is the path where Terraform should write the backup state.
|
||||
BackupPath string
|
||||
|
||||
// BackupOutPath is the path where Terraform should write the backup of
|
||||
// the destination state.
|
||||
BackupOutPath string
|
||||
|
||||
// StateLock, if true, requests that the backend lock the state for this
|
||||
// operation.
|
||||
StateLock bool
|
||||
|
||||
// StateLockTimeout is the duration to retry a state lock.
|
||||
StateLockTimeout time.Duration
|
||||
|
||||
// StatePath is an optional path to a local state file.
|
||||
StatePath string
|
||||
|
||||
// StateOutPath is an optional path to write the destination state.
|
||||
StateOutPath string
|
||||
|
||||
// IgnoreRemoteVersion, if true, continues even if remote and local
|
||||
// Terraform versions are incompatible.
|
||||
IgnoreRemoteVersion bool
|
||||
|
||||
// SourceAddr is the source resource address.
|
||||
SourceAddr string
|
||||
|
||||
// DestAddr is the destination resource address.
|
||||
DestAddr string
|
||||
}
|
||||
|
||||
// ParseStateMv processes CLI arguments, returning a StateMv value and
|
||||
// diagnostics. If errors are encountered, a StateMv value is still returned
|
||||
// representing the best effort interpretation of the arguments.
|
||||
func ParseStateMv(args []string) (*StateMv, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
mv := &StateMv{
|
||||
StateLock: true,
|
||||
}
|
||||
|
||||
cmdFlags := defaultFlagSet("state mv")
|
||||
cmdFlags.BoolVar(&mv.DryRun, "dry-run", false, "dry run")
|
||||
cmdFlags.StringVar(&mv.BackupPath, "backup", "-", "backup")
|
||||
cmdFlags.StringVar(&mv.BackupOutPath, "backup-out", "-", "backup")
|
||||
cmdFlags.BoolVar(&mv.StateLock, "lock", true, "lock states")
|
||||
cmdFlags.DurationVar(&mv.StateLockTimeout, "lock-timeout", 0, "lock timeout")
|
||||
cmdFlags.StringVar(&mv.StatePath, "state", "", "path")
|
||||
cmdFlags.StringVar(&mv.StateOutPath, "state-out", "", "path")
|
||||
cmdFlags.BoolVar(&mv.IgnoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
args = cmdFlags.Args()
|
||||
if len(args) != 2 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required argument missing",
|
||||
"Exactly two arguments expected: the source and destination addresses.",
|
||||
))
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
mv.SourceAddr = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
mv.DestAddr = args[1]
|
||||
}
|
||||
|
||||
return mv, diags
|
||||
}
|
||||
174
internal/command/arguments/state_mv_test.go
Normal file
174
internal/command/arguments/state_mv_test.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestParseStateMv_valid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *StateMv
|
||||
}{
|
||||
"addresses only": {
|
||||
[]string{"test_instance.foo", "test_instance.bar"},
|
||||
&StateMv{
|
||||
DryRun: false,
|
||||
BackupPath: "-",
|
||||
BackupOutPath: "-",
|
||||
StateLock: true,
|
||||
StateLockTimeout: 0,
|
||||
StatePath: "",
|
||||
StateOutPath: "",
|
||||
IgnoreRemoteVersion: false,
|
||||
SourceAddr: "test_instance.foo",
|
||||
DestAddr: "test_instance.bar",
|
||||
},
|
||||
},
|
||||
"dry run": {
|
||||
[]string{"-dry-run", "test_instance.foo", "test_instance.bar"},
|
||||
&StateMv{
|
||||
DryRun: true,
|
||||
BackupPath: "-",
|
||||
BackupOutPath: "-",
|
||||
StateLock: true,
|
||||
StateLockTimeout: 0,
|
||||
StatePath: "",
|
||||
StateOutPath: "",
|
||||
IgnoreRemoteVersion: false,
|
||||
SourceAddr: "test_instance.foo",
|
||||
DestAddr: "test_instance.bar",
|
||||
},
|
||||
},
|
||||
"all options": {
|
||||
[]string{
|
||||
"-dry-run",
|
||||
"-backup=backup.tfstate",
|
||||
"-backup-out=backup-out.tfstate",
|
||||
"-lock=false",
|
||||
"-lock-timeout=5s",
|
||||
"-state=state.tfstate",
|
||||
"-state-out=state-out.tfstate",
|
||||
"-ignore-remote-version",
|
||||
"test_instance.foo",
|
||||
"test_instance.bar",
|
||||
},
|
||||
&StateMv{
|
||||
DryRun: true,
|
||||
BackupPath: "backup.tfstate",
|
||||
BackupOutPath: "backup-out.tfstate",
|
||||
StateLock: false,
|
||||
StateLockTimeout: 5 * time.Second,
|
||||
StatePath: "state.tfstate",
|
||||
StateOutPath: "state-out.tfstate",
|
||||
IgnoreRemoteVersion: true,
|
||||
SourceAddr: "test_instance.foo",
|
||||
DestAddr: "test_instance.bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := ParseStateMv(tc.args)
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStateMv_invalid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *StateMv
|
||||
wantDiags tfdiags.Diagnostics
|
||||
}{
|
||||
"no arguments": {
|
||||
nil,
|
||||
&StateMv{
|
||||
BackupPath: "-",
|
||||
BackupOutPath: "-",
|
||||
StateLock: true,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required argument missing",
|
||||
"Exactly two arguments expected: the source and destination addresses.",
|
||||
),
|
||||
},
|
||||
},
|
||||
"one argument": {
|
||||
[]string{"test_instance.foo"},
|
||||
&StateMv{
|
||||
BackupPath: "-",
|
||||
BackupOutPath: "-",
|
||||
StateLock: true,
|
||||
SourceAddr: "test_instance.foo",
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required argument missing",
|
||||
"Exactly two arguments expected: the source and destination addresses.",
|
||||
),
|
||||
},
|
||||
},
|
||||
"too many arguments": {
|
||||
[]string{"a", "b", "c"},
|
||||
&StateMv{
|
||||
BackupPath: "-",
|
||||
BackupOutPath: "-",
|
||||
StateLock: true,
|
||||
SourceAddr: "a",
|
||||
DestAddr: "b",
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required argument missing",
|
||||
"Exactly two arguments expected: the source and destination addresses.",
|
||||
),
|
||||
},
|
||||
},
|
||||
"unknown flag": {
|
||||
[]string{"-boop"},
|
||||
&StateMv{
|
||||
BackupPath: "-",
|
||||
BackupOutPath: "-",
|
||||
StateLock: true,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
"flag provided but not defined: -boop",
|
||||
),
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required argument missing",
|
||||
"Exactly two arguments expected: the source and destination addresses.",
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, gotDiags := ParseStateMv(tc.args)
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,6 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/cli"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/backend/backendrun"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
|
|
@ -26,28 +24,17 @@ type StateMvCommand struct {
|
|||
}
|
||||
|
||||
func (c *StateMvCommand) Run(args []string) int {
|
||||
args = c.Meta.process(args)
|
||||
// We create two metas to track the two states
|
||||
var backupPathOut, statePathOut string
|
||||
|
||||
var dryRun bool
|
||||
cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state mv")
|
||||
cmdFlags.BoolVar(&dryRun, "dry-run", false, "dry run")
|
||||
cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup")
|
||||
cmdFlags.StringVar(&backupPathOut, "backup-out", "-", "backup")
|
||||
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states")
|
||||
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
|
||||
cmdFlags.StringVar(&c.statePath, "state", "", "path")
|
||||
cmdFlags.StringVar(&statePathOut, "state-out", "", "path")
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
||||
parsedArgs, parseDiags := arguments.ParseStateMv(c.Meta.process(args))
|
||||
if parseDiags.HasErrors() {
|
||||
c.showDiagnostics(parseDiags)
|
||||
return 1
|
||||
}
|
||||
args = cmdFlags.Args()
|
||||
if len(args) != 2 {
|
||||
c.Ui.Error("Exactly two arguments expected.\n")
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
c.backupPath = parsedArgs.BackupPath
|
||||
c.Meta.stateLock = parsedArgs.StateLock
|
||||
c.Meta.stateLockTimeout = parsedArgs.StateLockTimeout
|
||||
c.statePath = parsedArgs.StatePath
|
||||
c.Meta.ignoreRemoteVersion = parsedArgs.IgnoreRemoteVersion
|
||||
|
||||
if diags := c.Meta.checkRequiredVersion(); diags != nil {
|
||||
c.showDiagnostics(diags)
|
||||
|
|
@ -58,7 +45,7 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
// and the state option is not set, make sure
|
||||
// the backend is local
|
||||
backupOptionSetWithoutStateOption := c.backupPath != "-" && c.statePath == ""
|
||||
backupOutOptionSetWithoutStateOption := backupPathOut != "-" && c.statePath == ""
|
||||
backupOutOptionSetWithoutStateOption := parsedArgs.BackupOutPath != "-" && c.statePath == ""
|
||||
|
||||
var setLegacyLocalBackendOptions []string
|
||||
if backupOptionSetWithoutStateOption {
|
||||
|
|
@ -127,9 +114,9 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
stateToMgr := stateFromMgr
|
||||
stateTo := stateFrom
|
||||
|
||||
if statePathOut != "" {
|
||||
c.statePath = statePathOut
|
||||
c.backupPath = backupPathOut
|
||||
if parsedArgs.StateOutPath != "" {
|
||||
c.statePath = parsedArgs.StateOutPath
|
||||
c.backupPath = parsedArgs.BackupOutPath
|
||||
|
||||
stateToMgr, err = c.State(view)
|
||||
if err != nil {
|
||||
|
|
@ -162,9 +149,9 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
sourceAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, args[0])
|
||||
sourceAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, parsedArgs.SourceAddr)
|
||||
diags = diags.Append(moreDiags)
|
||||
destAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, args[1])
|
||||
destAddr, moreDiags := c.lookupSingleStateObjectAddr(stateFrom, parsedArgs.DestAddr)
|
||||
diags = diags.Append(moreDiags)
|
||||
if diags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
|
|
@ -172,7 +159,7 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
prefix := "Move"
|
||||
if dryRun {
|
||||
if parsedArgs.DryRun {
|
||||
prefix = "Would move"
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +218,7 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
|
||||
moved++
|
||||
c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), addrTo.String()))
|
||||
if !dryRun {
|
||||
if !parsedArgs.DryRun {
|
||||
ssFrom.RemoveModule(addrFrom)
|
||||
|
||||
// Update the address before adding it to the state.
|
||||
|
|
@ -276,7 +263,7 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
|
||||
moved++
|
||||
c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), addrTo.String()))
|
||||
if !dryRun {
|
||||
if !parsedArgs.DryRun {
|
||||
ssFrom.RemoveResource(addrFrom)
|
||||
|
||||
// Update the address before adding it to the state.
|
||||
|
|
@ -329,8 +316,8 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
moved++
|
||||
c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), args[1]))
|
||||
if !dryRun {
|
||||
c.Ui.Output(fmt.Sprintf("%s %q to %q", prefix, addrFrom.String(), parsedArgs.DestAddr))
|
||||
if !parsedArgs.DryRun {
|
||||
fromResourceAddr := addrFrom.ContainingResource()
|
||||
fromResource := ssFrom.Resource(fromResourceAddr)
|
||||
fromProviderAddr := fromResource.ProviderConfig
|
||||
|
|
@ -385,7 +372,7 @@ func (c *StateMvCommand) Run(args []string) int {
|
|||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
if parsedArgs.DryRun {
|
||||
if moved == 0 {
|
||||
c.Ui.Output("Would have moved nothing.")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue