From ef4771bba489c7e4f2a62ce11152ab55a259b302 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Fri, 13 Feb 2026 13:50:47 +0100 Subject: [PATCH] use the arguments package for state commands --- internal/command/arguments/state_list.go | 49 +++++ internal/command/arguments/state_list_test.go | 118 ++++++++++++ internal/command/arguments/state_mv.go | 93 ++++++++++ internal/command/arguments/state_mv_test.go | 174 ++++++++++++++++++ internal/command/arguments/state_pull.go | 32 ++++ internal/command/arguments/state_pull_test.go | 64 +++++++ internal/command/arguments/state_push.go | 70 +++++++ internal/command/arguments/state_push_test.go | 155 ++++++++++++++++ .../arguments/state_replace_provider.go | 85 +++++++++ .../arguments/state_replace_provider_test.go | 144 +++++++++++++++ internal/command/arguments/state_rm.go | 76 ++++++++ internal/command/arguments/state_rm_test.go | 126 +++++++++++++ internal/command/arguments/state_show.go | 54 ++++++ internal/command/arguments/state_show_test.go | 110 +++++++++++ internal/command/state_list.go | 24 +-- internal/command/state_mv.go | 55 +++--- internal/command/state_pull.go | 7 +- internal/command/state_push.go | 28 +-- internal/command/state_replace_provider.go | 38 ++-- .../command/state_replace_provider_test.go | 2 +- internal/command/state_rm.go | 54 +++--- internal/command/state_show.go | 21 +-- 22 files changed, 1439 insertions(+), 140 deletions(-) create mode 100644 internal/command/arguments/state_list.go create mode 100644 internal/command/arguments/state_list_test.go create mode 100644 internal/command/arguments/state_mv.go create mode 100644 internal/command/arguments/state_mv_test.go create mode 100644 internal/command/arguments/state_pull.go create mode 100644 internal/command/arguments/state_pull_test.go create mode 100644 internal/command/arguments/state_push.go create mode 100644 internal/command/arguments/state_push_test.go create mode 100644 internal/command/arguments/state_replace_provider.go create mode 100644 internal/command/arguments/state_replace_provider_test.go create mode 100644 internal/command/arguments/state_rm.go create mode 100644 internal/command/arguments/state_rm_test.go create mode 100644 internal/command/arguments/state_show.go create mode 100644 internal/command/arguments/state_show_test.go diff --git a/internal/command/arguments/state_list.go b/internal/command/arguments/state_list.go new file mode 100644 index 0000000000..d6aaaf73b5 --- /dev/null +++ b/internal/command/arguments/state_list.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateList represents the command-line arguments for the state list command. +type StateList struct { + // StatePath is an optional path to a state file, overriding the default. + StatePath string + + // ID filters the results to include only instances whose resource types + // have an attribute named "id" whose value equals this string. + ID string + + // Addrs are optional resource or module addresses used to filter the + // listed instances. + Addrs []string +} + +// ParseStateList processes CLI arguments, returning a StateList value and +// diagnostics. If errors are encountered, a StateList value is still returned +// representing the best effort interpretation of the arguments. +func ParseStateList(args []string) (*StateList, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + list := &StateList{} + + var statePath, id string + cmdFlags := defaultFlagSet("state list") + cmdFlags.StringVar(&statePath, "state", "", "path") + cmdFlags.StringVar(&id, "id", "", "Restrict output to paths with a resource having the specified ID.") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + list.StatePath = statePath + list.ID = id + list.Addrs = cmdFlags.Args() + + return list, diags +} diff --git a/internal/command/arguments/state_list_test.go b/internal/command/arguments/state_list_test.go new file mode 100644 index 0000000000..422b6efb63 --- /dev/null +++ b/internal/command/arguments/state_list_test.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStateList_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateList + }{ + "defaults": { + nil, + &StateList{ + StatePath: "", + ID: "", + Addrs: nil, + }, + }, + "state path": { + []string{"-state=foobar.tfstate"}, + &StateList{ + StatePath: "foobar.tfstate", + ID: "", + Addrs: nil, + }, + }, + "id filter": { + []string{"-id=bar"}, + &StateList{ + StatePath: "", + ID: "bar", + Addrs: nil, + }, + }, + "with addresses": { + []string{"module.example", "aws_instance.foo"}, + &StateList{ + StatePath: "", + ID: "", + Addrs: []string{"module.example", "aws_instance.foo"}, + }, + }, + "all options": { + []string{"-state=foobar.tfstate", "-id=bar", "module.example"}, + &StateList{ + StatePath: "foobar.tfstate", + ID: "bar", + Addrs: []string{"module.example"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStateList(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if got.StatePath != tc.want.StatePath { + t.Fatalf("unexpected StatePath\n got: %q\nwant: %q", got.StatePath, tc.want.StatePath) + } + if got.ID != tc.want.ID { + t.Fatalf("unexpected ID\n got: %q\nwant: %q", got.ID, tc.want.ID) + } + if len(got.Addrs) != len(tc.want.Addrs) { + t.Fatalf("unexpected Addrs length\n got: %d\nwant: %d", len(got.Addrs), len(tc.want.Addrs)) + } + for i := range got.Addrs { + if got.Addrs[i] != tc.want.Addrs[i] { + t.Fatalf("unexpected Addrs[%d]\n got: %q\nwant: %q", i, got.Addrs[i], tc.want.Addrs[i]) + } + } + }) + } +} + +func TestParseStateList_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateList + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-boop"}, + &StateList{ + StatePath: "", + ID: "", + Addrs: nil, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -boop", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStateList(tc.args) + if got.StatePath != tc.want.StatePath { + t.Fatalf("unexpected StatePath\n got: %q\nwant: %q", got.StatePath, tc.want.StatePath) + } + if got.ID != tc.want.ID { + t.Fatalf("unexpected ID\n got: %q\nwant: %q", got.ID, tc.want.ID) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/state_mv.go b/internal/command/arguments/state_mv.go new file mode 100644 index 0000000000..146f3d8a26 --- /dev/null +++ b/internal/command/arguments/state_mv.go @@ -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 +} diff --git a/internal/command/arguments/state_mv_test.go b/internal/command/arguments/state_mv_test.go new file mode 100644 index 0000000000..acaad320df --- /dev/null +++ b/internal/command/arguments/state_mv_test.go @@ -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) + }) + } +} diff --git a/internal/command/arguments/state_pull.go b/internal/command/arguments/state_pull.go new file mode 100644 index 0000000000..caebef0c47 --- /dev/null +++ b/internal/command/arguments/state_pull.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StatePull represents the command-line arguments for the state pull command. +type StatePull struct { +} + +// ParseStatePull processes CLI arguments, returning a StatePull value and +// diagnostics. If errors are encountered, a StatePull value is still returned +// representing the best effort interpretation of the arguments. +func ParseStatePull(args []string) (*StatePull, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + pull := &StatePull{} + + cmdFlags := defaultFlagSet("state pull") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + return pull, diags +} diff --git a/internal/command/arguments/state_pull_test.go b/internal/command/arguments/state_pull_test.go new file mode 100644 index 0000000000..a1b95f5ec3 --- /dev/null +++ b/internal/command/arguments/state_pull_test.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStatePull_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StatePull + }{ + "defaults": { + nil, + &StatePull{}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStatePull(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 TestParseStatePull_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StatePull + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-boop"}, + &StatePull{}, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -boop", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStatePull(tc.args) + if *got != *tc.want { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/state_push.go b/internal/command/arguments/state_push.go new file mode 100644 index 0000000000..b83e34926d --- /dev/null +++ b/internal/command/arguments/state_push.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StatePush represents the command-line arguments for the state push command. +type StatePush struct { + // Force writes the state even if lineages don't match or the remote + // serial is higher. + Force bool + + // 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 + + // IgnoreRemoteVersion, if true, continues even if remote and local + // Terraform versions are incompatible. + IgnoreRemoteVersion bool + + // Path is the path to the state file to push, or "-" for stdin. + Path string +} + +// ParseStatePush processes CLI arguments, returning a StatePush value and +// diagnostics. If errors are encountered, a StatePush value is still returned +// representing the best effort interpretation of the arguments. +func ParseStatePush(args []string) (*StatePush, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + push := &StatePush{ + StateLock: true, + } + + cmdFlags := defaultFlagSet("state push") + cmdFlags.BoolVar(&push.Force, "force", false, "") + cmdFlags.BoolVar(&push.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&push.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.BoolVar(&push.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) != 1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly one argument expected: the path to a Terraform state file.", + )) + } + + if len(args) > 0 { + push.Path = args[0] + } + + return push, diags +} diff --git a/internal/command/arguments/state_push_test.go b/internal/command/arguments/state_push_test.go new file mode 100644 index 0000000000..990852714a --- /dev/null +++ b/internal/command/arguments/state_push_test.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStatePush_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StatePush + }{ + "path only": { + []string{"replace.tfstate"}, + &StatePush{ + Force: false, + StateLock: true, + StateLockTimeout: 0, + IgnoreRemoteVersion: false, + Path: "replace.tfstate", + }, + }, + "stdin": { + []string{"-"}, + &StatePush{ + Force: false, + StateLock: true, + StateLockTimeout: 0, + IgnoreRemoteVersion: false, + Path: "-", + }, + }, + "force": { + []string{"-force", "replace.tfstate"}, + &StatePush{ + Force: true, + StateLock: true, + StateLockTimeout: 0, + IgnoreRemoteVersion: false, + Path: "replace.tfstate", + }, + }, + "lock disabled": { + []string{"-lock=false", "replace.tfstate"}, + &StatePush{ + Force: false, + StateLock: false, + StateLockTimeout: 0, + IgnoreRemoteVersion: false, + Path: "replace.tfstate", + }, + }, + "lock timeout": { + []string{"-lock-timeout=5s", "replace.tfstate"}, + &StatePush{ + Force: false, + StateLock: true, + StateLockTimeout: 5 * time.Second, + IgnoreRemoteVersion: false, + Path: "replace.tfstate", + }, + }, + "ignore remote version": { + []string{"-ignore-remote-version", "replace.tfstate"}, + &StatePush{ + Force: false, + StateLock: true, + StateLockTimeout: 0, + IgnoreRemoteVersion: true, + Path: "replace.tfstate", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStatePush(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 TestParseStatePush_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StatePush + wantDiags tfdiags.Diagnostics + }{ + "no arguments": { + nil, + &StatePush{ + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly one argument expected: the path to a Terraform state file.", + ), + }, + }, + "too many arguments": { + []string{"foo.tfstate", "bar.tfstate"}, + &StatePush{ + StateLock: true, + Path: "foo.tfstate", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly one argument expected: the path to a Terraform state file.", + ), + }, + }, + "unknown flag": { + []string{"-boop"}, + &StatePush{ + 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 one argument expected: the path to a Terraform state file.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStatePush(tc.args) + if *got != *tc.want { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/state_replace_provider.go b/internal/command/arguments/state_replace_provider.go new file mode 100644 index 0000000000..64583b7c6b --- /dev/null +++ b/internal/command/arguments/state_replace_provider.go @@ -0,0 +1,85 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateReplaceProvider represents the command-line arguments for the state +// replace-provider command. +type StateReplaceProvider struct { + // AutoApprove, if true, skips the interactive approval step. + AutoApprove bool + + // BackupPath is the path where Terraform should write the backup state. + BackupPath 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 + + // IgnoreRemoteVersion, if true, continues even if remote and local + // Terraform versions are incompatible. + IgnoreRemoteVersion bool + + // FromProviderAddr is the provider address to replace. + FromProviderAddr string + + // ToProviderAddr is the replacement provider address. + ToProviderAddr string +} + +// ParseStateReplaceProvider processes CLI arguments, returning a +// StateReplaceProvider value and diagnostics. If errors are encountered, a +// StateReplaceProvider value is still returned representing the best effort +// interpretation of the arguments. +func ParseStateReplaceProvider(args []string) (*StateReplaceProvider, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + rp := &StateReplaceProvider{ + StateLock: true, + } + + cmdFlags := defaultFlagSet("state replace-provider") + cmdFlags.BoolVar(&rp.AutoApprove, "auto-approve", false, "skip interactive approval of replacements") + cmdFlags.StringVar(&rp.BackupPath, "backup", "-", "backup") + cmdFlags.BoolVar(&rp.StateLock, "lock", true, "lock states") + cmdFlags.DurationVar(&rp.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.StringVar(&rp.StatePath, "state", "", "path") + cmdFlags.BoolVar(&rp.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 from and to provider addresses.", + )) + } + + if len(args) > 0 { + rp.FromProviderAddr = args[0] + } + if len(args) > 1 { + rp.ToProviderAddr = args[1] + } + + return rp, diags +} diff --git a/internal/command/arguments/state_replace_provider_test.go b/internal/command/arguments/state_replace_provider_test.go new file mode 100644 index 0000000000..c70b76c139 --- /dev/null +++ b/internal/command/arguments/state_replace_provider_test.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStateReplaceProvider_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateReplaceProvider + }{ + "provider addresses only": { + []string{"hashicorp/aws", "acmecorp/aws"}, + &StateReplaceProvider{ + AutoApprove: false, + BackupPath: "-", + StateLock: true, + StateLockTimeout: 0, + StatePath: "", + IgnoreRemoteVersion: false, + FromProviderAddr: "hashicorp/aws", + ToProviderAddr: "acmecorp/aws", + }, + }, + "auto approve": { + []string{"-auto-approve", "hashicorp/aws", "acmecorp/aws"}, + &StateReplaceProvider{ + AutoApprove: true, + BackupPath: "-", + StateLock: true, + StateLockTimeout: 0, + StatePath: "", + IgnoreRemoteVersion: false, + FromProviderAddr: "hashicorp/aws", + ToProviderAddr: "acmecorp/aws", + }, + }, + "all options": { + []string{ + "-auto-approve", + "-backup=backup.tfstate", + "-lock=false", + "-lock-timeout=5s", + "-state=state.tfstate", + "-ignore-remote-version", + "hashicorp/aws", + "acmecorp/aws", + }, + &StateReplaceProvider{ + AutoApprove: true, + BackupPath: "backup.tfstate", + StateLock: false, + StateLockTimeout: 5 * time.Second, + StatePath: "state.tfstate", + IgnoreRemoteVersion: true, + FromProviderAddr: "hashicorp/aws", + ToProviderAddr: "acmecorp/aws", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStateReplaceProvider(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 TestParseStateReplaceProvider_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateReplaceProvider + wantDiags tfdiags.Diagnostics + }{ + "no arguments": { + nil, + &StateReplaceProvider{ + BackupPath: "-", + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly two arguments expected: the from and to provider addresses.", + ), + }, + }, + "too many arguments": { + []string{"a", "b", "c", "d"}, + &StateReplaceProvider{ + BackupPath: "-", + StateLock: true, + FromProviderAddr: "a", + ToProviderAddr: "b", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly two arguments expected: the from and to provider addresses.", + ), + }, + }, + "unknown flag": { + []string{"-invalid", "hashicorp/google", "acmecorp/google"}, + &StateReplaceProvider{ + BackupPath: "-", + StateLock: true, + FromProviderAddr: "hashicorp/google", + ToProviderAddr: "acmecorp/google", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -invalid", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStateReplaceProvider(tc.args) + if *got != *tc.want { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/state_rm.go b/internal/command/arguments/state_rm.go new file mode 100644 index 0000000000..1ae46a3dcc --- /dev/null +++ b/internal/command/arguments/state_rm.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateRm represents the command-line arguments for the state rm command. +type StateRm struct { + // DryRun, if true, prints out what would be removed without actually + // removing anything. + DryRun bool + + // BackupPath is the path where Terraform should write the backup state. + BackupPath 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 + + // IgnoreRemoteVersion, if true, continues even if remote and local + // Terraform versions are incompatible. + IgnoreRemoteVersion bool + + // Addrs are the resource instance addresses to remove. + Addrs []string +} + +// ParseStateRm processes CLI arguments, returning a StateRm value and +// diagnostics. If errors are encountered, a StateRm value is still returned +// representing the best effort interpretation of the arguments. +func ParseStateRm(args []string) (*StateRm, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + rm := &StateRm{ + StateLock: true, + } + + cmdFlags := defaultFlagSet("state rm") + cmdFlags.BoolVar(&rm.DryRun, "dry-run", false, "dry run") + cmdFlags.StringVar(&rm.BackupPath, "backup", "-", "backup") + cmdFlags.BoolVar(&rm.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&rm.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.StringVar(&rm.StatePath, "state", "", "path") + cmdFlags.BoolVar(&rm.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) < 1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "At least one address is required.", + )) + } + + rm.Addrs = args + + return rm, diags +} diff --git a/internal/command/arguments/state_rm_test.go b/internal/command/arguments/state_rm_test.go new file mode 100644 index 0000000000..79f30fc7de --- /dev/null +++ b/internal/command/arguments/state_rm_test.go @@ -0,0 +1,126 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStateRm_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateRm + }{ + "single address": { + []string{"test_instance.foo"}, + &StateRm{ + DryRun: false, + BackupPath: "-", + StateLock: true, + StateLockTimeout: 0, + StatePath: "", + IgnoreRemoteVersion: false, + Addrs: []string{"test_instance.foo"}, + }, + }, + "multiple addresses": { + []string{"test_instance.foo", "test_instance.bar"}, + &StateRm{ + DryRun: false, + BackupPath: "-", + StateLock: true, + StateLockTimeout: 0, + StatePath: "", + IgnoreRemoteVersion: false, + Addrs: []string{"test_instance.foo", "test_instance.bar"}, + }, + }, + "all options": { + []string{"-dry-run", "-backup=backup.tfstate", "-lock=false", "-lock-timeout=5s", "-state=state.tfstate", "-ignore-remote-version", "test_instance.foo"}, + &StateRm{ + DryRun: true, + BackupPath: "backup.tfstate", + StateLock: false, + StateLockTimeout: 5 * time.Second, + StatePath: "state.tfstate", + IgnoreRemoteVersion: true, + Addrs: []string{"test_instance.foo"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStateRm(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if got.DryRun != tc.want.DryRun || + got.BackupPath != tc.want.BackupPath || + got.StateLock != tc.want.StateLock || + got.StateLockTimeout != tc.want.StateLockTimeout || + got.StatePath != tc.want.StatePath || + got.IgnoreRemoteVersion != tc.want.IgnoreRemoteVersion { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + if len(got.Addrs) != len(tc.want.Addrs) { + t.Fatalf("unexpected Addrs length\n got: %d\nwant: %d", len(got.Addrs), len(tc.want.Addrs)) + } + for i := range got.Addrs { + if got.Addrs[i] != tc.want.Addrs[i] { + t.Fatalf("unexpected Addrs[%d]\n got: %q\nwant: %q", i, got.Addrs[i], tc.want.Addrs[i]) + } + } + }) + } +} + +func TestParseStateRm_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + wantAddrs int + wantDiags tfdiags.Diagnostics + }{ + "no arguments": { + nil, + 0, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "At least one address is required.", + ), + }, + }, + "unknown flag": { + []string{"-boop"}, + 0, + 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", + "At least one address is required.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStateRm(tc.args) + if len(got.Addrs) != tc.wantAddrs { + t.Fatalf("unexpected Addrs length\n got: %d\nwant: %d", len(got.Addrs), tc.wantAddrs) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/state_show.go b/internal/command/arguments/state_show.go new file mode 100644 index 0000000000..afd2798fc9 --- /dev/null +++ b/internal/command/arguments/state_show.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateShow represents the command-line arguments for the state show command. +type StateShow struct { + // StatePath is an optional path to a state file, overriding the default. + StatePath string + + // Address is the resource instance address to show. + Address string +} + +// ParseStateShow processes CLI arguments, returning a StateShow value and +// diagnostics. If errors are encountered, a StateShow value is still returned +// representing the best effort interpretation of the arguments. +func ParseStateShow(args []string) (*StateShow, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + show := &StateShow{} + + var statePath string + cmdFlags := defaultFlagSet("state show") + cmdFlags.StringVar(&statePath, "state", "", "path") + + 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) != 1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly one argument expected: the address of a resource instance to show.", + )) + } + + show.StatePath = statePath + + if len(args) > 0 { + show.Address = args[0] + } + + return show, diags +} diff --git a/internal/command/arguments/state_show_test.go b/internal/command/arguments/state_show_test.go new file mode 100644 index 0000000000..2358b29834 --- /dev/null +++ b/internal/command/arguments/state_show_test.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseStateShow_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateShow + }{ + "address only": { + []string{"test_instance.foo"}, + &StateShow{ + StatePath: "", + Address: "test_instance.foo", + }, + }, + "with state path": { + []string{"-state=foobar.tfstate", "test_instance.foo"}, + &StateShow{ + StatePath: "foobar.tfstate", + Address: "test_instance.foo", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseStateShow(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 TestParseStateShow_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *StateShow + wantDiags tfdiags.Diagnostics + }{ + "no arguments": { + nil, + &StateShow{ + StatePath: "", + Address: "", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly one argument expected: the address of a resource instance to show.", + ), + }, + }, + "too many arguments": { + []string{"test_instance.foo", "test_instance.bar"}, + &StateShow{ + StatePath: "", + Address: "test_instance.foo", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "Exactly one argument expected: the address of a resource instance to show.", + ), + }, + }, + "unknown flag": { + []string{"-boop"}, + &StateShow{ + StatePath: "", + Address: "", + }, + 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 one argument expected: the address of a resource instance to show.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseStateShow(tc.args) + if *got != *tc.want { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/state_list.go b/internal/command/state_list.go index 55d39934db..c0cd9fa50c 100644 --- a/internal/command/state_list.go +++ b/internal/command/state_list.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/states" @@ -21,19 +20,14 @@ type StateListCommand struct { } func (c *StateListCommand) Run(args []string) int { - args = c.Meta.process(args) - var statePath string - cmdFlags := c.Meta.defaultFlagSet("state list") - cmdFlags.StringVar(&statePath, "state", "", "path") - lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.") - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) - return cli.RunResultHelp + parsedArgs, diags := arguments.ParseStateList(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 } - args = cmdFlags.Args() - if statePath != "" { - c.Meta.statePath = statePath + if parsedArgs.StatePath != "" { + c.Meta.statePath = parsedArgs.StatePath } // Load the backend @@ -69,10 +63,10 @@ func (c *StateListCommand) Run(args []string) int { } var addrs []addrs.AbsResourceInstance - if len(args) == 0 { + if len(parsedArgs.Addrs) == 0 { addrs, diags = c.lookupAllResourceInstanceAddrs(state) } else { - addrs, diags = c.lookupResourceInstanceAddrs(state, args...) + addrs, diags = c.lookupResourceInstanceAddrs(state, parsedArgs.Addrs...) } if diags.HasErrors() { c.showDiagnostics(diags) @@ -81,7 +75,7 @@ func (c *StateListCommand) Run(args []string) int { for _, addr := range addrs { if is := state.ResourceInstance(addr); is != nil { - if *lookupId == "" || *lookupId == states.LegacyInstanceObjectID(is.Current) { + if parsedArgs.ID == "" || parsedArgs.ID == states.LegacyInstanceObjectID(is.Current) { c.Ui.Output(addr.String()) } } diff --git a/internal/command/state_mv.go b/internal/command/state_mv.go index 940a272c8e..2498341d8d 100644 --- a/internal/command/state_mv.go +++ b/internal/command/state_mv.go @@ -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.") } diff --git a/internal/command/state_pull.go b/internal/command/state_pull.go index 19f3e76ede..c2ff830a92 100644 --- a/internal/command/state_pull.go +++ b/internal/command/state_pull.go @@ -21,10 +21,9 @@ type StatePullCommand struct { } func (c *StatePullCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("state pull") - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + _, diags := arguments.ParseStatePull(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/internal/command/state_push.go b/internal/command/state_push.go index b594eedc29..ae8a57a60e 100644 --- a/internal/command/state_push.go +++ b/internal/command/state_push.go @@ -9,7 +9,6 @@ import ( "os" "strings" - "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -26,22 +25,15 @@ type StatePushCommand struct { } func (c *StatePushCommand) Run(args []string) int { - args = c.Meta.process(args) - var flagForce bool - cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state push") - cmdFlags.BoolVar(&flagForce, "force", false, "") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + parsedArgs, diags := arguments.ParseStatePush(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - args = cmdFlags.Args() - if len(args) != 1 { - c.Ui.Error("Exactly one argument expected.\n") - return cli.RunResultHelp - } + c.Meta.stateLock = parsedArgs.StateLock + c.Meta.stateLockTimeout = parsedArgs.StateLockTimeout + c.Meta.ignoreRemoteVersion = parsedArgs.IgnoreRemoteVersion if diags := c.Meta.checkRequiredVersion(); diags != nil { c.showDiagnostics(diags) @@ -51,8 +43,8 @@ func (c *StatePushCommand) Run(args []string) int { // Determine our reader for the input state. This is the filepath // or stdin if "-" is given. var r io.Reader = os.Stdin - if args[0] != "-" { - f, err := os.Open(args[0]) + if parsedArgs.Path != "-" { + f, err := os.Open(parsedArgs.Path) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -71,7 +63,7 @@ func (c *StatePushCommand) Run(args []string) int { c.Close() } if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) + c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", parsedArgs.Path, err)) return 1 } @@ -128,7 +120,7 @@ func (c *StatePushCommand) Run(args []string) int { } // Import it, forcing through the lineage/serial if requested and possible. - if err := statemgr.Import(srcStateFile, stateMgr, flagForce); err != nil { + if err := statemgr.Import(srcStateFile, stateMgr, parsedArgs.Force); err != nil { c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) return 1 } diff --git a/internal/command/state_replace_provider.go b/internal/command/state_replace_provider.go index 07ce2d8336..513f85557f 100644 --- a/internal/command/state_replace_provider.go +++ b/internal/command/state_replace_provider.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" @@ -27,25 +26,18 @@ type StateReplaceProviderCommand struct { } func (c *StateReplaceProviderCommand) Run(args []string) int { - args = c.Meta.process(args) - - var autoApprove bool - cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state replace-provider") - cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of replacements") - cmdFlags.StringVar(&c.backupPath, "backup", "-", "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") - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) - return cli.RunResultHelp - } - args = cmdFlags.Args() - if len(args) != 2 { - c.Ui.Error("Exactly two arguments expected.\n") - return cli.RunResultHelp + parsedArgs, parseDiags := arguments.ParseStateReplaceProvider(c.Meta.process(args)) + if parseDiags.HasErrors() { + c.showDiagnostics(parseDiags) + return 1 } + 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) return 1 @@ -54,19 +46,19 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { var diags tfdiags.Diagnostics // Parse from/to arguments into providers - from, fromDiags := addrs.ParseProviderSourceString(args[0]) + from, fromDiags := addrs.ParseProviderSourceString(parsedArgs.FromProviderAddr) if fromDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - fmt.Sprintf(`Invalid "from" provider %q`, args[0]), + fmt.Sprintf(`Invalid "from" provider %q`, parsedArgs.FromProviderAddr), fromDiags.Err().Error(), )) } - to, toDiags := addrs.ParseProviderSourceString(args[1]) + to, toDiags := addrs.ParseProviderSourceString(parsedArgs.ToProviderAddr) if toDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - fmt.Sprintf(`Invalid "to" provider %q`, args[1]), + fmt.Sprintf(`Invalid "to" provider %q`, parsedArgs.ToProviderAddr), toDiags.Err().Error(), )) } @@ -144,7 +136,7 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { } // Confirm - if !autoApprove { + if !parsedArgs.AutoApprove { c.Ui.Output(colorize.Color( "\n[bold]Do you want to make these changes?[reset]\n" + "Only 'yes' will be accepted to continue.\n", diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index e7afaa5981..72b9e377db 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -225,7 +225,7 @@ func TestStateReplaceProvider(t *testing.T) { t.Fatalf("successful exit; want error") } - if got, want := ui.ErrorWriter.String(), "Error parsing command-line flags"; !strings.Contains(got, want) { + if got, want := ui.ErrorWriter.String(), "Failed to parse command-line flags"; !strings.Contains(got, want) { t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got) } }) diff --git a/internal/command/state_rm.go b/internal/command/state_rm.go index b973f08148..de98320dc6 100644 --- a/internal/command/state_rm.go +++ b/internal/command/state_rm.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" @@ -23,24 +22,17 @@ type StateRmCommand struct { } func (c *StateRmCommand) Run(args []string) int { - args = c.Meta.process(args) - var dryRun bool - cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state rm") - cmdFlags.BoolVar(&dryRun, "dry-run", false, "dry run") - cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.StringVar(&c.statePath, "state", "", "path") - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + parsedArgs, diags := arguments.ParseStateRm(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - args = cmdFlags.Args() - if len(args) < 1 { - c.Ui.Error("At least one address is required.\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) @@ -82,19 +74,19 @@ func (c *StateRmCommand) Run(args []string) int { // This command primarily works with resource instances, though it will // also clean up any modules and resources left empty by actions it takes. var addrs []addrs.AbsResourceInstance - var diags tfdiags.Diagnostics - for _, addrStr := range args { + var rmDiags tfdiags.Diagnostics + for _, addrStr := range parsedArgs.Addrs { moreAddrs, moreDiags := c.lookupResourceInstanceAddr(state, true, addrStr) addrs = append(addrs, moreAddrs...) - diags = diags.Append(moreDiags) + rmDiags = rmDiags.Append(moreDiags) } - if diags.HasErrors() { - c.showDiagnostics(diags) + if rmDiags.HasErrors() { + c.showDiagnostics(rmDiags) return 1 } prefix := "Removed " - if dryRun { + if parsedArgs.DryRun { prefix = "Would remove " } @@ -103,13 +95,13 @@ func (c *StateRmCommand) Run(args []string) int { for _, addr := range addrs { isCount++ c.Ui.Output(prefix + addr.String()) - if !dryRun { + if !parsedArgs.DryRun { ss.ForgetResourceInstanceAll(addr) ss.RemoveResourceIfEmpty(addr.ContainingResource()) } } - if dryRun { + if parsedArgs.DryRun { if isCount == 0 { c.Ui.Output("Would have removed nothing.") } @@ -118,9 +110,9 @@ func (c *StateRmCommand) Run(args []string) int { // Load the backend b, backendDiags := c.backend(".", view) - diags = diags.Append(backendDiags) + rmDiags = rmDiags.Append(backendDiags) if backendDiags.HasErrors() { - c.showDiagnostics(diags) + c.showDiagnostics(rmDiags) return 1 } @@ -129,7 +121,7 @@ func (c *StateRmCommand) Run(args []string) int { if isCloudMode(b) { var schemaDiags tfdiags.Diagnostics schemas, schemaDiags = c.MaybeGetSchemas(state, nil) - diags = diags.Append(schemaDiags) + rmDiags = rmDiags.Append(schemaDiags) } if err := stateMgr.WriteState(state); err != nil { @@ -141,17 +133,17 @@ func (c *StateRmCommand) Run(args []string) int { return 1 } - if len(diags) > 0 && isCount != 0 { - c.showDiagnostics(diags) + if len(rmDiags) > 0 && isCount != 0 { + c.showDiagnostics(rmDiags) } if isCount == 0 { - diags = diags.Append(tfdiags.Sourceless( + rmDiags = rmDiags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid target address", "No matching objects found. To view the available instances, use \"terraform state list\". Please modify the address to reference a specific instance.", )) - c.showDiagnostics(diags) + c.showDiagnostics(rmDiags) return 1 } diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 6374ec51af..3532f50cfa 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -8,8 +8,6 @@ import ( "os" "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" @@ -27,18 +25,13 @@ type StateShowCommand struct { } func (c *StateShowCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("state show") - cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") - if err := cmdFlags.Parse(args); err != nil { - c.Streams.Eprintf("Error parsing command-line flags: %s\n", err.Error()) + parsedArgs, diags := arguments.ParseStateShow(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - args = cmdFlags.Args() - if len(args) != 1 { - c.Streams.Eprint("Exactly one argument expected.\n") - return cli.RunResultHelp - } + + c.Meta.statePath = parsedArgs.StatePath // Check for user-supplied plugin path var err error @@ -66,9 +59,9 @@ func (c *StateShowCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(b) // Check if the address can be parsed - addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) + addr, addrDiags := addrs.ParseAbsResourceInstanceStr(parsedArgs.Address) if addrDiags.HasErrors() { - c.Streams.Eprintln(fmt.Sprintf(errParsingAddress, args[0])) + c.Streams.Eprintln(fmt.Sprintf(errParsingAddress, parsedArgs.Address)) return 1 }