diff --git a/internal/command/arguments/import.go b/internal/command/arguments/import.go new file mode 100644 index 0000000000..48d373d76e --- /dev/null +++ b/internal/command/arguments/import.go @@ -0,0 +1,108 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "os" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// getwd is a package-level variable that defaults to os.Getwd. +// It can be overridden in tests to provide a mock implementation. +var getwd = os.Getwd + +// Import represents the command-line arguments for the import command. +type Import struct { + // State, Vars are the common extended flags + State *State + Vars *Vars + + // ConfigPath is the path to a directory of Terraform configuration files + // to use to configure the provider. An empty string means the caller + // should use the current working directory. + ConfigPath string + + // Parallelism is the limit Terraform places on total parallel operations + // as it walks the dependency graph. + Parallelism int + + // IgnoreRemoteVersion controls whether to suppress the error when the + // configured Terraform version on the remote workspace does not match the + // local Terraform version. + IgnoreRemoteVersion bool + + // InputEnabled is used to disable interactive input for unspecified + // variable and backend config values. Default is true. + InputEnabled bool + + // CompactWarnings enables compact warning output. + CompactWarnings bool + + // TargetFlags are the raw -target flag values. + TargetFlags []string + + // Addr is the resource address to import into. + Addr string + + // ID is the provider-specific ID of the resource to import. + ID string +} + +// ParseImport processes CLI arguments, returning an Import value and errors. +// If errors are encountered, an Import value is still returned representing +// the best effort interpretation of the arguments. +func ParseImport(args []string) (*Import, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + imp := &Import{ + State: &State{ + Lock: true, + }, + Vars: &Vars{}, + } + // Get the pwd since its our default -config flag value + pwd, err := getwd() + if err != nil { + return nil, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error getting pwd", + err.Error(), + )) + } + + cmdFlags := extendedFlagSet("import", imp.State, nil, imp.Vars) + cmdFlags.BoolVar(&imp.IgnoreRemoteVersion, "ignore-remote-version", false, "ignore-remote-version") + cmdFlags.IntVar(&imp.Parallelism, "parallelism", DefaultParallelism, "parallelism") + cmdFlags.StringVar(&imp.ConfigPath, "config", pwd, "config") + cmdFlags.BoolVar(&imp.InputEnabled, "input", true, "input") + cmdFlags.BoolVar(&imp.CompactWarnings, "compact-warnings", false, "compact-warnings") + cmdFlags.Var((*FlagStringSlice)(&imp.TargetFlags), "target", "target") + + 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, + "Wrong number of arguments", + "The import command expects two arguments: ADDR and ID.", + )) + } + + if len(args) > 0 { + imp.Addr = args[0] + } + if len(args) > 1 { + imp.ID = args[1] + } + + return imp, diags +} diff --git a/internal/command/arguments/import_test.go b/internal/command/arguments/import_test.go new file mode 100644 index 0000000000..d2936785a5 --- /dev/null +++ b/internal/command/arguments/import_test.go @@ -0,0 +1,271 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func init() { + // Mock getwd for tests to return empty string + getwd = func() (string, error) { + return "", nil + } +} + +func TestParseImport_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Import + }{ + "defaults": { + []string{"test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + ConfigPath: "", + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "state flag": { + []string{"-state", "mystate.tfstate", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true, StatePath: "mystate.tfstate"}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "state-out and backup flags": { + []string{"-state-out", "out.tfstate", "-backup", "backup.tfstate", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true, StateOutPath: "out.tfstate", BackupPath: "backup.tfstate"}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "lock disabled": { + []string{"-lock=false", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: false}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "config path": { + []string{"-config=/tmp/config", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + ConfigPath: "/tmp/config", + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "parallelism": { + []string{"-parallelism=5", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: 5, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "ignore remote version": { + []string{"-ignore-remote-version", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + IgnoreRemoteVersion: true, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + "input disabled": { + []string{"-input=false", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: false, + Addr: "test_instance.foo", + ID: "bar", + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Vars{}, State{}) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseImport(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseImport_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Import + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-boop", "test_instance.foo", "bar"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -boop", + ), + }, + }, + "missing all arguments": { + nil, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Wrong number of arguments", + "The import command expects two arguments: ADDR and ID.", + ), + }, + }, + "only one argument": { + []string{"test_instance.foo"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Wrong number of arguments", + "The import command expects two arguments: ADDR and ID.", + ), + }, + }, + "too many arguments": { + []string{"test_instance.foo", "bar", "baz"}, + &Import{ + State: &State{Lock: true}, + Vars: &Vars{}, + Parallelism: DefaultParallelism, + InputEnabled: true, + Addr: "test_instance.foo", + ID: "bar", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Wrong number of arguments", + "The import command expects two arguments: ADDR and ID.", + ), + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Vars{}, State{}) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseImport(tc.args) + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} + +func TestParseImport_vars(t *testing.T) { + testCases := map[string]struct { + args []string + want []FlagNameValue + }{ + "no var flags by default": { + args: []string{"test_instance.foo", "bar"}, + want: nil, + }, + "one var": { + args: []string{"-var", "foo=bar", "test_instance.foo", "bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + }, + }, + "one var-file": { + args: []string{"-var-file", "cool.tfvars", "test_instance.foo", "bar"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, + }, + }, + "ordering preserved": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + "test_instance.foo", "bar", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseImport(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want)) + } + }) + } +} diff --git a/internal/command/arguments/providers.go b/internal/command/arguments/providers.go new file mode 100644 index 0000000000..5637f47f03 --- /dev/null +++ b/internal/command/arguments/providers.go @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// Providers represents the command-line arguments for the providers command. +type Providers struct { + // TestsDirectory is the directory containing Terraform test files. + TestsDirectory string +} + +// ParseProviders processes CLI arguments, returning a Providers value and +// errors. If errors are encountered, a Providers value is still returned +// representing the best effort interpretation of the arguments. +func ParseProviders(args []string) (*Providers, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + providers := &Providers{} + + cmdFlags := defaultFlagSet("providers") + cmdFlags.StringVar(&providers.TestsDirectory, "test-directory", "tests", "test-directory") + + 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) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "Did you mean to use -chdir?", + )) + } + + return providers, diags +} diff --git a/internal/command/arguments/providers_lock.go b/internal/command/arguments/providers_lock.go new file mode 100644 index 0000000000..5793927152 --- /dev/null +++ b/internal/command/arguments/providers_lock.go @@ -0,0 +1,52 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// ProvidersLock represents the command-line arguments for the providers lock +// command. +type ProvidersLock struct { + Platforms FlagStringSlice + FSMirrorDir string + NetMirrorURL string + TestsDirectory string + EnablePluginCache bool + Providers []string +} + +// ParseProvidersLock processes CLI arguments, returning a ProvidersLock value +// and errors. If errors are encountered, a ProvidersLock value is still +// returned representing the best effort interpretation of the arguments. +func ParseProvidersLock(args []string) (*ProvidersLock, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + providersLock := &ProvidersLock{} + + cmdFlags := defaultFlagSet("providers lock") + cmdFlags.Var(&providersLock.Platforms, "platform", "target platform") + cmdFlags.StringVar(&providersLock.FSMirrorDir, "fs-mirror", "", "filesystem mirror directory") + cmdFlags.StringVar(&providersLock.NetMirrorURL, "net-mirror", "", "network mirror base URL") + cmdFlags.StringVar(&providersLock.TestsDirectory, "test-directory", "tests", "test-directory") + cmdFlags.BoolVar(&providersLock.EnablePluginCache, "enable-plugin-cache", false, "") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + if providersLock.FSMirrorDir != "" && providersLock.NetMirrorURL != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid installation method options", + "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", + )) + } + + providersLock.Providers = cmdFlags.Args() + + return providersLock, diags +} diff --git a/internal/command/arguments/providers_lock_test.go b/internal/command/arguments/providers_lock_test.go new file mode 100644 index 0000000000..a59c836453 --- /dev/null +++ b/internal/command/arguments/providers_lock_test.go @@ -0,0 +1,124 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseProvidersLock_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersLock + }{ + "defaults": { + nil, + &ProvidersLock{ + TestsDirectory: "tests", + }, + }, + "all options": { + []string{ + "-platform=linux_amd64", + "-platform=darwin_arm64", + "-fs-mirror=mirror", + "-test-directory=integration-tests", + "-enable-plugin-cache", + "hashicorp/test", + }, + &ProvidersLock{ + Platforms: FlagStringSlice{"linux_amd64", "darwin_arm64"}, + FSMirrorDir: "mirror", + TestsDirectory: "integration-tests", + EnablePluginCache: true, + Providers: []string{"hashicorp/test"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseProvidersLock(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseProvidersLock_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersLock + wantDiags tfdiags.Diagnostics + }{ + "mirror collision": { + []string{ + "-fs-mirror=foo", + "-net-mirror=https://example.com", + }, + &ProvidersLock{ + FSMirrorDir: "foo", + NetMirrorURL: "https://example.com", + TestsDirectory: "tests", + Providers: []string{}, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Invalid installation method options", + "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", + ), + }, + }, + "unknown flag": { + []string{"-wat"}, + &ProvidersLock{ + TestsDirectory: "tests", + Providers: []string{}, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + }, + }, + "unknown flag and mirror collision": { + []string{ + "-wat", + "-fs-mirror=foo", + "-net-mirror=https://example.com", + }, + &ProvidersLock{ + TestsDirectory: "tests", + Providers: []string{"-fs-mirror=foo", "-net-mirror=https://example.com"}, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseProvidersLock(tc.args) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/providers_mirror.go b/internal/command/arguments/providers_mirror.go new file mode 100644 index 0000000000..219ad50032 --- /dev/null +++ b/internal/command/arguments/providers_mirror.go @@ -0,0 +1,48 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// ProvidersMirror represents the command-line arguments for the providers +// mirror command. +type ProvidersMirror struct { + Platforms FlagStringSlice + LockFile bool + OutputDir string +} + +// ParseProvidersMirror processes CLI arguments, returning a ProvidersMirror +// value and errors. If errors are encountered, a ProvidersMirror value is +// still returned representing the best effort interpretation of the arguments. +func ParseProvidersMirror(args []string) (*ProvidersMirror, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + providersMirror := &ProvidersMirror{} + + cmdFlags := defaultFlagSet("providers mirror") + cmdFlags.Var(&providersMirror.Platforms, "platform", "target platform") + cmdFlags.BoolVar(&providersMirror.LockFile, "lock-file", true, "use lock file") + + 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 { + return providersMirror, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + )) + } + + providersMirror.OutputDir = args[0] + + return providersMirror, diags +} diff --git a/internal/command/arguments/providers_mirror_test.go b/internal/command/arguments/providers_mirror_test.go new file mode 100644 index 0000000000..d6fb5598ee --- /dev/null +++ b/internal/command/arguments/providers_mirror_test.go @@ -0,0 +1,113 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseProvidersMirror_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersMirror + }{ + "defaults": { + []string{"./mirror"}, + &ProvidersMirror{ + LockFile: true, + OutputDir: "./mirror", + }, + }, + "all options": { + []string{ + "-platform=linux_amd64", + "-platform=darwin_arm64", + "-lock-file=false", + "./mirror", + }, + &ProvidersMirror{ + Platforms: FlagStringSlice{"linux_amd64", "darwin_arm64"}, + OutputDir: "./mirror", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseProvidersMirror(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseProvidersMirror_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersMirror + wantDiags tfdiags.Diagnostics + }{ + "missing output directory": { + nil, + &ProvidersMirror{ + LockFile: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + ), + }, + }, + "too many arguments": { + []string{"./mirror", "./extra"}, + &ProvidersMirror{ + LockFile: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + ), + }, + }, + "unknown flag and missing output directory": { + []string{"-wat"}, + &ProvidersMirror{ + LockFile: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseProvidersMirror(tc.args) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/providers_schema.go b/internal/command/arguments/providers_schema.go new file mode 100644 index 0000000000..15afa0acfc --- /dev/null +++ b/internal/command/arguments/providers_schema.go @@ -0,0 +1,50 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// ProvidersSchema represents the command-line arguments for the providers +// schema command. +type ProvidersSchema struct { + JSON bool +} + +// ParseProvidersSchema processes CLI arguments, returning a ProvidersSchema +// value and errors. If errors are encountered, a ProvidersSchema value is +// still returned representing the best effort interpretation of the arguments. +func ParseProvidersSchema(args []string) (*ProvidersSchema, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + providersSchema := &ProvidersSchema{} + + cmdFlags := defaultFlagSet("providers schema") + cmdFlags.BoolVar(&providersSchema.JSON, "json", false, "produce JSON output") + + 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) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "Expected no positional arguments.", + )) + } + + if !providersSchema.JSON { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -json flag is required", + "The `terraform providers schema` command requires the `-json` flag.", + )) + } + + return providersSchema, diags +} diff --git a/internal/command/arguments/providers_schema_test.go b/internal/command/arguments/providers_schema_test.go new file mode 100644 index 0000000000..8f6df7220c --- /dev/null +++ b/internal/command/arguments/providers_schema_test.go @@ -0,0 +1,96 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseProvidersSchema_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersSchema + }{ + "json": { + []string{"-json"}, + &ProvidersSchema{ + JSON: true, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseProvidersSchema(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseProvidersSchema_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersSchema + wantDiags tfdiags.Diagnostics + }{ + "missing json": { + nil, + &ProvidersSchema{}, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "The -json flag is required", + "The `terraform providers schema` command requires the `-json` flag.", + ), + }, + }, + "too many positional arguments": { + []string{"-json", "extra"}, + &ProvidersSchema{ + JSON: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "Expected no positional arguments.", + ), + }, + }, + "unknown flag and missing json": { + []string{"-wat"}, + &ProvidersSchema{}, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + tfdiags.Sourceless( + tfdiags.Error, + "The -json flag is required", + "The `terraform providers schema` command requires the `-json` flag.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseProvidersSchema(tc.args) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/providers_test.go b/internal/command/arguments/providers_test.go new file mode 100644 index 0000000000..b55f69704a --- /dev/null +++ b/internal/command/arguments/providers_test.go @@ -0,0 +1,88 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseProviders_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Providers + }{ + "defaults": { + nil, + &Providers{ + TestsDirectory: "tests", + }, + }, + "test directory": { + []string{"-test-directory=integration-tests"}, + &Providers{ + TestsDirectory: "integration-tests", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseProviders(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseProviders_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Providers + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-wat"}, + &Providers{ + TestsDirectory: "tests", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + }, + }, + "too many positional arguments": { + []string{"foo"}, + &Providers{ + TestsDirectory: "tests", + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "Did you mean to use -chdir?", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseProviders(tc.args) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/state_list.go b/internal/command/arguments/state_list.go new file mode 100644 index 0000000000..2f67c4b850 --- /dev/null +++ b/internal/command/arguments/state_list.go @@ -0,0 +1,49 @@ +// Copyright IBM Corp. 2014, 2026 +// 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..1018ee130c --- /dev/null +++ b/internal/command/arguments/state_list_test.go @@ -0,0 +1,104 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{}, + }, + "state path": { + []string{"-state=foobar.tfstate"}, + &StateList{ + StatePath: "foobar.tfstate", + }, + }, + "id filter": { + []string{"-id=bar"}, + &StateList{ + ID: "bar", + }, + }, + "with addresses": { + []string{"module.example", "aws_instance.foo"}, + &StateList{ + 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{}, + 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..475c5cabcf --- /dev/null +++ b/internal/command/arguments/state_mv.go @@ -0,0 +1,91 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{} + + 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..be1d084dbf --- /dev/null +++ b/internal/command/arguments/state_mv_test.go @@ -0,0 +1,165 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{ + BackupPath: "-", + BackupOutPath: "-", + StateLock: true, + 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, + 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..85bc653760 --- /dev/null +++ b/internal/command/arguments/state_pull.go @@ -0,0 +1,32 @@ +// Copyright IBM Corp. 2014, 2026 +// 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..37b9922853 --- /dev/null +++ b/internal/command/arguments/state_pull_test.go @@ -0,0 +1,64 @@ +// Copyright IBM Corp. 2014, 2026 +// 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..f12d0670c8 --- /dev/null +++ b/internal/command/arguments/state_push.go @@ -0,0 +1,67 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{} + + 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.", + )) + return push, diags + } + + 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..70a0c8b112 --- /dev/null +++ b/internal/command/arguments/state_push_test.go @@ -0,0 +1,138 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{ + StateLock: true, + Path: "replace.tfstate", + }, + }, + "stdin": { + []string{"-"}, + &StatePush{ + StateLock: true, + Path: "-", + }, + }, + "force": { + []string{"-force", "replace.tfstate"}, + &StatePush{ + Force: true, + StateLock: true, + Path: "replace.tfstate", + }, + }, + "lock disabled": { + []string{"-lock=false", "replace.tfstate"}, + &StatePush{ + Path: "replace.tfstate", + }, + }, + "lock timeout": { + []string{"-lock-timeout=5s", "replace.tfstate"}, + &StatePush{ + StateLock: true, + StateLockTimeout: 5 * time.Second, + Path: "replace.tfstate", + }, + }, + "ignore remote version": { + []string{"-ignore-remote-version", "replace.tfstate"}, + &StatePush{ + StateLock: true, + 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, + }, + 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..57f36d2c85 --- /dev/null +++ b/internal/command/arguments/state_replace_provider.go @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{} + + 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.", + )) + return rp, diags + } + rp.FromProviderAddr = args[0] + 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..2938cfe513 --- /dev/null +++ b/internal/command/arguments/state_replace_provider_test.go @@ -0,0 +1,135 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{ + BackupPath: "-", + StateLock: true, + FromProviderAddr: "hashicorp/aws", + ToProviderAddr: "acmecorp/aws", + }, + }, + "auto approve": { + []string{"-auto-approve", "hashicorp/aws", "acmecorp/aws"}, + &StateReplaceProvider{ + AutoApprove: true, + BackupPath: "-", + StateLock: true, + 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, + }, + 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..2421903afd --- /dev/null +++ b/internal/command/arguments/state_rm.go @@ -0,0 +1,74 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{} + + 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..c23887406d --- /dev/null +++ b/internal/command/arguments/state_rm_test.go @@ -0,0 +1,118 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{ + BackupPath: "-", + StateLock: true, + Addrs: []string{"test_instance.foo"}, + }, + }, + "multiple addresses": { + []string{"test_instance.foo", "test_instance.bar"}, + &StateRm{ + BackupPath: "-", + StateLock: true, + 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..3b69a02c15 --- /dev/null +++ b/internal/command/arguments/state_show.go @@ -0,0 +1,54 @@ +// Copyright IBM Corp. 2014, 2026 +// 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..6c988deeb4 --- /dev/null +++ b/internal/command/arguments/state_show_test.go @@ -0,0 +1,102 @@ +// Copyright IBM Corp. 2014, 2026 +// 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{ + 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{}, + 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{ + 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{}, + 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/arguments/taint.go b/internal/command/arguments/taint.go new file mode 100644 index 0000000000..3be7a20d2e --- /dev/null +++ b/internal/command/arguments/taint.go @@ -0,0 +1,86 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Taint represents the command-line arguments for the taint command. +type Taint struct { + // Address is the address of the resource instance to taint. + Address string + + // AllowMissing, if true, means the command will succeed even if the + // resource is not found in state. + AllowMissing bool + + // BackupPath is the path to backup the existing state file before + // modifying. + BackupPath string + + // StateLock, if true, locks the state file during operations. + StateLock bool + + // StateLockTimeout is the duration to retry a state lock. + StateLockTimeout time.Duration + + // StatePath is the path to the state file to read and modify. + StatePath string + + // StateOutPath is the path to write the updated state file. + StateOutPath string + + // IgnoreRemoteVersion, if true, continues even if remote and local + // Terraform versions are incompatible. + IgnoreRemoteVersion bool +} + +// ParseTaint processes CLI arguments, returning a Taint value and errors. +// If errors are encountered, a Taint value is still returned representing +// the best effort interpretation of the arguments. +func ParseTaint(args []string) (*Taint, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + taint := &Taint{} + + cmdFlags := defaultFlagSet("taint") + cmdFlags.BoolVar(&taint.AllowMissing, "allow-missing", false, "allow missing") + cmdFlags.StringVar(&taint.BackupPath, "backup", "", "path") + cmdFlags.BoolVar(&taint.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&taint.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.StringVar(&taint.StatePath, "state", "", "path") + cmdFlags.StringVar(&taint.StateOutPath, "state-out", "", "path") + cmdFlags.BoolVar(&taint.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) == 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "The taint command expects exactly one argument: the address of the resource to taint.", + )) + } else if len(args) > 1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "The taint command expects exactly one argument: the address of the resource to taint.", + )) + } + + if len(args) > 0 { + taint.Address = args[0] + } + + return taint, diags +} diff --git a/internal/command/arguments/taint_test.go b/internal/command/arguments/taint_test.go new file mode 100644 index 0000000000..7a6a3c07cd --- /dev/null +++ b/internal/command/arguments/taint_test.go @@ -0,0 +1,177 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseTaint_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Taint + }{ + "defaults with address": { + []string{"test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + StateLock: true, + }, + }, + "allow-missing": { + []string{"-allow-missing", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + AllowMissing: true, + StateLock: true, + }, + }, + "backup": { + []string{"-backup", "backup.tfstate", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + BackupPath: "backup.tfstate", + StateLock: true, + }, + }, + "lock disabled": { + []string{"-lock=false", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + }, + }, + "lock-timeout": { + []string{"-lock-timeout=10s", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + StateLock: true, + StateLockTimeout: 10 * time.Second, + }, + }, + "state": { + []string{"-state=foo.tfstate", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + StateLock: true, + StatePath: "foo.tfstate", + }, + }, + "state-out": { + []string{"-state-out=foo.tfstate", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + StateLock: true, + StateOutPath: "foo.tfstate", + }, + }, + "ignore-remote-version": { + []string{"-ignore-remote-version", "test_instance.foo"}, + &Taint{ + Address: "test_instance.foo", + StateLock: true, + IgnoreRemoteVersion: true, + }, + }, + "all flags": { + []string{ + "-allow-missing", + "-backup=backup.tfstate", + "-lock=false", + "-lock-timeout=10s", + "-state=foo.tfstate", + "-state-out=bar.tfstate", + "-ignore-remote-version", + "module.child.test_instance.foo", + }, + &Taint{ + Address: "module.child.test_instance.foo", + AllowMissing: true, + BackupPath: "backup.tfstate", + StateLockTimeout: 10 * time.Second, + StatePath: "foo.tfstate", + StateOutPath: "bar.tfstate", + IgnoreRemoteVersion: true, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseTaint(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 TestParseTaint_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Taint + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-unknown"}, + &Taint{ + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -unknown", + ), + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "The taint command expects exactly one argument: the address of the resource to taint.", + ), + }, + }, + "missing address": { + nil, + &Taint{ + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "The taint command expects exactly one argument: the address of the resource to taint.", + ), + }, + }, + "too many arguments": { + []string{"test_instance.foo", "test_instance.bar"}, + &Taint{ + Address: "test_instance.foo", + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "The taint command expects exactly one argument: the address of the resource to taint.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseTaint(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/untaint.go b/internal/command/arguments/untaint.go new file mode 100644 index 0000000000..41640f0bd1 --- /dev/null +++ b/internal/command/arguments/untaint.go @@ -0,0 +1,86 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Untaint represents the command-line arguments for the untaint command. +type Untaint struct { + // Address is the address of the resource instance to untaint. + Address string + + // AllowMissing, if true, means the command will succeed even if the + // resource is not found in state. + AllowMissing bool + + // BackupPath is the path to backup the existing state file before + // modifying. + BackupPath string + + // StateLock, if true, locks the state file during operations. + StateLock bool + + // StateLockTimeout is the duration to retry a state lock. + StateLockTimeout time.Duration + + // StatePath is the path to the state file to read and modify. + StatePath string + + // StateOutPath is the path to write the updated state file. + StateOutPath string + + // IgnoreRemoteVersion, if true, continues even if remote and local + // Terraform versions are incompatible. + IgnoreRemoteVersion bool +} + +// ParseUntaint processes CLI arguments, returning an Untaint value and errors. +// If errors are encountered, an Untaint value is still returned representing +// the best effort interpretation of the arguments. +func ParseUntaint(args []string) (*Untaint, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + untaint := &Untaint{} + + cmdFlags := defaultFlagSet("untaint") + cmdFlags.BoolVar(&untaint.AllowMissing, "allow-missing", false, "allow missing") + cmdFlags.StringVar(&untaint.BackupPath, "backup", "", "path") + cmdFlags.BoolVar(&untaint.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&untaint.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.StringVar(&untaint.StatePath, "state", "", "path") + cmdFlags.StringVar(&untaint.StateOutPath, "state-out", "", "path") + cmdFlags.BoolVar(&untaint.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) == 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "The untaint command expects exactly one argument: the address of the resource to untaint.", + )) + } else if len(args) > 1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "The untaint command expects exactly one argument: the address of the resource to untaint.", + )) + } + + if len(args) > 0 { + untaint.Address = args[0] + } + + return untaint, diags +} diff --git a/internal/command/arguments/untaint_test.go b/internal/command/arguments/untaint_test.go new file mode 100644 index 0000000000..f9645d3e8b --- /dev/null +++ b/internal/command/arguments/untaint_test.go @@ -0,0 +1,177 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseUntaint_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Untaint + }{ + "defaults with address": { + []string{"test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + StateLock: true, + }, + }, + "allow-missing": { + []string{"-allow-missing", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + AllowMissing: true, + StateLock: true, + }, + }, + "backup": { + []string{"-backup", "backup.tfstate", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + BackupPath: "backup.tfstate", + StateLock: true, + }, + }, + "lock disabled": { + []string{"-lock=false", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + }, + }, + "lock-timeout": { + []string{"-lock-timeout=10s", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + StateLock: true, + StateLockTimeout: 10 * time.Second, + }, + }, + "state": { + []string{"-state=foo.tfstate", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + StateLock: true, + StatePath: "foo.tfstate", + }, + }, + "state-out": { + []string{"-state-out=foo.tfstate", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + StateLock: true, + StateOutPath: "foo.tfstate", + }, + }, + "ignore-remote-version": { + []string{"-ignore-remote-version", "test_instance.foo"}, + &Untaint{ + Address: "test_instance.foo", + StateLock: true, + IgnoreRemoteVersion: true, + }, + }, + "all flags": { + []string{ + "-allow-missing", + "-backup=backup.tfstate", + "-lock=false", + "-lock-timeout=10s", + "-state=foo.tfstate", + "-state-out=bar.tfstate", + "-ignore-remote-version", + "module.child.test_instance.foo", + }, + &Untaint{ + Address: "module.child.test_instance.foo", + AllowMissing: true, + BackupPath: "backup.tfstate", + StateLockTimeout: 10 * time.Second, + StatePath: "foo.tfstate", + StateOutPath: "bar.tfstate", + IgnoreRemoteVersion: true, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseUntaint(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 TestParseUntaint_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Untaint + wantDiags tfdiags.Diagnostics + }{ + "unknown flag": { + []string{"-unknown"}, + &Untaint{ + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -unknown", + ), + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "The untaint command expects exactly one argument: the address of the resource to untaint.", + ), + }, + }, + "missing address": { + nil, + &Untaint{ + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Required argument missing", + "The untaint command expects exactly one argument: the address of the resource to untaint.", + ), + }, + }, + "too many arguments": { + []string{"test_instance.foo", "test_instance.bar"}, + &Untaint{ + Address: "test_instance.foo", + StateLock: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "The untaint command expects exactly one argument: the address of the resource to untaint.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseUntaint(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/import.go b/internal/command/import.go index 4f67be6be1..0e63856977 100644 --- a/internal/command/import.go +++ b/internal/command/import.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "log" - "os" "strings" "github.com/hashicorp/hcl/v2" @@ -29,41 +28,34 @@ type ImportCommand struct { } func (c *ImportCommand) Run(args []string) int { - // Get the pwd since its our default -config flag value - pwd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - return 1 + parsedArgs, diags := arguments.ParseImport(c.Meta.process(args)) + + // Copy parsed flags back to Meta + c.Meta.statePath = parsedArgs.State.StatePath + c.Meta.stateOutPath = parsedArgs.State.StateOutPath + c.Meta.backupPath = parsedArgs.State.BackupPath + c.Meta.stateLock = parsedArgs.State.Lock + c.Meta.stateLockTimeout = parsedArgs.State.LockTimeout + c.Meta.parallelism = parsedArgs.Parallelism + c.ignoreRemoteVersion = parsedArgs.IgnoreRemoteVersion + c.Meta.input = parsedArgs.InputEnabled + c.Meta.compactWarnings = parsedArgs.CompactWarnings + c.Meta.targetFlags = parsedArgs.TargetFlags + + varItems := parsedArgs.Vars.All() + c.Meta.variableArgs = arguments.FlagNameValueSlice{ + FlagName: "-var", + Items: &varItems, } - var configPath string - args = c.Meta.process(args) - - cmdFlags := c.Meta.extendedFlagSet("import") - cmdFlags.BoolVar(&c.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") - cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") - cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") - cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") - cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") - cmdFlags.StringVar(&configPath, "config", pwd, "path") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { + if diags.HasErrors() { + c.showDiagnostics(diags) + c.Ui.Error(c.Help()) return 1 } - args = cmdFlags.Args() - if len(args) != 2 { - c.Ui.Error("The import command expects two arguments.") - cmdFlags.Usage() - return 1 - } - - var diags tfdiags.Diagnostics - // Parse the provided resource address. - traversalSrc := []byte(args[0]) + traversalSrc := []byte(parsedArgs.Addr) traversal, travDiags := hclsyntax.ParseTraversalAbs(traversalSrc, "", hcl.Pos{Line: 1, Column: 1}) diags = diags.Append(travDiags) if travDiags.HasErrors() { @@ -87,13 +79,13 @@ func (c *ImportCommand) Run(args []string) int { return 1 } - if !c.dirIsConfigPath(configPath) { + if !c.dirIsConfigPath(parsedArgs.ConfigPath) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "No Terraform configuration files", Detail: fmt.Sprintf( "The directory %s does not contain any Terraform configuration files (.tf or .tf.json). To specify a different configuration directory, use the -config=\"...\" command line option.", - configPath, + parsedArgs.ConfigPath, ), }) c.showDiagnostics(diags) @@ -102,7 +94,7 @@ func (c *ImportCommand) Run(args []string) int { // Load the full config, so we can verify that the target resource is // already configured. - config, configDiags := c.loadConfig(configPath) + config, configDiags := c.loadConfig(parsedArgs.ConfigPath) diags = diags.Append(configDiags) if configDiags.HasErrors() { c.showDiagnostics(diags) @@ -158,6 +150,7 @@ func (c *ImportCommand) Run(args []string) int { } // Check for user-supplied plugin path + var err error if c.pluginPath, err = c.loadPluginPath(); err != nil { c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) return 1 @@ -184,7 +177,7 @@ func (c *ImportCommand) Run(args []string) int { // Build the operation opReq := c.Operation(b, arguments.ViewHuman) - opReq.ConfigDir = configPath + opReq.ConfigDir = parsedArgs.ConfigPath opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { diags = diags.Append(err) @@ -234,7 +227,7 @@ func (c *ImportCommand) Run(args []string) int { Targets: []*terraform.ImportTarget{ { LegacyAddr: addr, - LegacyID: args[1], + LegacyID: parsedArgs.ID, }, }, diff --git a/internal/command/meta.go b/internal/command/meta.go index 8070ace991..17ba612a68 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -575,17 +575,6 @@ func (m *Meta) defaultFlagSet(n string) *flag.FlagSet { return f } -// ignoreRemoteVersionFlagSet add the ignore-remote version flag to suppress -// the error when the configured Terraform version on the remote workspace -// does not match the local Terraform version. -func (m *Meta) ignoreRemoteVersionFlagSet(n string) *flag.FlagSet { - f := m.defaultFlagSet(n) - - f.BoolVar(&m.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") - - return f -} - // extendedFlagSet adds custom flags that are mostly used by commands // that are used to run an operation like plan or apply. func (m *Meta) extendedFlagSet(n string) *flag.FlagSet { diff --git a/internal/command/providers.go b/internal/command/providers.go index b7bf61efee..4ada467523 100644 --- a/internal/command/providers.go +++ b/internal/command/providers.go @@ -31,26 +31,19 @@ func (c *ProvidersCommand) Synopsis() string { } func (c *ProvidersCommand) Run(args []string) int { - var testsDirectory string - - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("providers") - cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + parsedArgs, diags := arguments.ParseProviders(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - configPath, err := ModulePath(cmdFlags.Args()) + configPath, err := ModulePath(nil) if err != nil { c.Ui.Error(err.Error()) return 1 } - var diags tfdiags.Diagnostics - - empty, err := configs.IsEmptyDir(configPath, testsDirectory) + empty, err := configs.IsEmptyDir(configPath, parsedArgs.TestsDirectory) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -74,7 +67,7 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } - config, configDiags := c.loadConfigWithTests(configPath, testsDirectory) + config, configDiags := c.loadConfigWithTests(configPath, parsedArgs.TestsDirectory) diags = diags.Append(configDiags) if configDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/providers_lock.go b/internal/command/providers_lock.go index 1a1d66897c..a13a1bc86e 100644 --- a/internal/command/providers_lock.go +++ b/internal/command/providers_lock.go @@ -39,44 +39,18 @@ func (c *ProvidersLockCommand) Synopsis() string { } func (c *ProvidersLockCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("providers lock") - var optPlatforms arguments.FlagStringSlice - var fsMirrorDir string - var netMirrorURL string - var testDirectory string - - cmdFlags.Var(&optPlatforms, "platform", "target platform") - cmdFlags.StringVar(&fsMirrorDir, "fs-mirror", "", "filesystem mirror directory") - cmdFlags.StringVar(&netMirrorURL, "net-mirror", "", "network mirror base URL") - cmdFlags.StringVar(&testDirectory, "test-directory", "tests", "test-directory") - pluginCache := cmdFlags.Bool("enable-plugin-cache", false, "") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) - return 1 - } - - var diags tfdiags.Diagnostics - - if fsMirrorDir != "" && netMirrorURL != "" { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid installation method options", - "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", - )) + parsedArgs, diags := arguments.ParseProvidersLock(c.Meta.process(args)) + if diags.HasErrors() { c.showDiagnostics(diags) return 1 } - providerStrs := cmdFlags.Args() - var platforms []getproviders.Platform - if len(optPlatforms) == 0 { + if len(parsedArgs.Platforms) == 0 { platforms = []getproviders.Platform{getproviders.CurrentPlatform} } else { - platforms = make([]getproviders.Platform, 0, len(optPlatforms)) - for _, platformStr := range optPlatforms { + platforms = make([]getproviders.Platform, 0, len(parsedArgs.Platforms)) + for _, platformStr := range parsedArgs.Platforms { platform, err := getproviders.ParsePlatform(platformStr) if err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -104,10 +78,10 @@ func (c *ProvidersLockCommand) Run(args []string) int { // against the upstream checksums. var source getproviders.Source switch { - case fsMirrorDir != "": - source = getproviders.NewFilesystemMirrorSource(fsMirrorDir) - case netMirrorURL != "": - u, err := url.Parse(netMirrorURL) + case parsedArgs.FSMirrorDir != "": + source = getproviders.NewFilesystemMirrorSource(parsedArgs.FSMirrorDir) + case parsedArgs.NetMirrorURL != "": + u, err := url.Parse(parsedArgs.NetMirrorURL) if err != nil || u.Scheme != "https" { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -125,7 +99,7 @@ func (c *ProvidersLockCommand) Run(args []string) int { source = getproviders.NewRegistrySource(c.Services) } - config, confDiags := c.loadConfigWithTests(".", testDirectory) + config, confDiags := c.loadConfigWithTests(".", parsedArgs.TestsDirectory) diags = diags.Append(confDiags) reqs, hclDiags := config.ProviderRequirements() diags = diags.Append(hclDiags) @@ -134,9 +108,9 @@ func (c *ProvidersLockCommand) Run(args []string) int { // we'll modify "reqs" to only include those. Modifying this is okay // because config.ProviderRequirements generates a fresh map result // for each call. - if len(providerStrs) != 0 { + if len(parsedArgs.Providers) != 0 { providers := map[addrs.Provider]struct{}{} - for _, raw := range providerStrs { + for _, raw := range parsedArgs.Providers { addr, moreDiags := addrs.ParseProviderSourceString(raw) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -253,7 +227,7 @@ func (c *ProvidersLockCommand) Run(args []string) int { // Use global plugin cache for extra speed if present and flag is set globalCacheDir := c.providerGlobalCacheDir() - if *pluginCache && globalCacheDir != nil { + if parsedArgs.EnablePluginCache && globalCacheDir != nil { installer.SetGlobalCacheDir(globalCacheDir.WithPlatform(platform)) installer.SetGlobalCacheDirMayBreakDependencyLockFile(c.PluginCacheMayBreakDependencyLockFile) } diff --git a/internal/command/providers_mirror.go b/internal/command/providers_mirror.go index 80162c59f9..cfff1b2e2c 100644 --- a/internal/command/providers_mirror.go +++ b/internal/command/providers_mirror.go @@ -32,41 +32,18 @@ func (c *ProvidersMirrorCommand) Synopsis() string { } func (c *ProvidersMirrorCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("providers mirror") - - var optPlatforms arguments.FlagStringSlice - cmdFlags.Var(&optPlatforms, "platform", "target platform") - - var optLockFile bool - cmdFlags.BoolVar(&optLockFile, "lock-file", true, "use lock file") - - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) - return 1 - } - - var diags tfdiags.Diagnostics - - args = cmdFlags.Args() - if len(args) != 1 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "No output directory specified", - "The providers mirror command requires an output directory as a command-line argument.", - )) + parsedArgs, diags := arguments.ParseProvidersMirror(c.Meta.process(args)) + if diags.HasErrors() { c.showDiagnostics(diags) return 1 } - outputDir := args[0] var platforms []getproviders.Platform - if len(optPlatforms) == 0 { + if len(parsedArgs.Platforms) == 0 { platforms = []getproviders.Platform{getproviders.CurrentPlatform} } else { - platforms = make([]getproviders.Platform, 0, len(optPlatforms)) - for _, platformStr := range optPlatforms { + platforms = make([]getproviders.Platform, 0, len(parsedArgs.Platforms)) + for _, platformStr := range parsedArgs.Platforms { platform, err := getproviders.ParsePlatform(platformStr) if err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -94,7 +71,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { diags = diags.Append(lockedDepsDiags) // If lock file is present, validate it against configuration - if !lockedDeps.Empty() && optLockFile { + if !lockedDeps.Empty() && parsedArgs.LockFile { if errs := config.VerifyDependencySelections(lockedDeps); len(errs) > 0 { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -166,7 +143,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { continue } selected := candidates.Newest() - if !lockedDeps.Empty() && optLockFile { + if !lockedDeps.Empty() && parsedArgs.LockFile { selected = lockedDeps.Provider(provider).Version() c.Ui.Output(fmt.Sprintf(" - Selected v%s to match dependency lock file", selected.String())) } else if len(constraintsStr) > 0 { @@ -214,7 +191,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // so we can verify its checksums and signatures before making // it discoverable to mirror clients. (stagingPath intentionally // does not follow the filesystem mirror file naming convention.) - targetPath := meta.PackedFilePath(outputDir) + targetPath := meta.PackedFilePath(parsedArgs.OutputDir) stagingPath := filepath.Join(filepath.Dir(targetPath), "."+filepath.Base(targetPath)) err = httpGetter.GetFile(stagingPath, urlObj) if err != nil { @@ -255,7 +232,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // by relying on the selections we made above, because we want to still // include in the indices any packages that were already present and // not affected by the changes we just made. - available, err := getproviders.SearchLocalDirectory(outputDir) + available, err := getproviders.SearchLocalDirectory(parsedArgs.OutputDir) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -273,7 +250,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // we'll ask the getproviders package to build an archive filename // for a fictitious package and then use the directory portion of it. indexDir := filepath.Dir(getproviders.PackedFilePathForPackage( - outputDir, provider, versions.Unspecified, getproviders.CurrentPlatform, + parsedArgs.OutputDir, provider, versions.Unspecified, getproviders.CurrentPlatform, )) indexVersions := map[string]interface{}{} indexArchives := map[getproviders.Version]map[string]interface{}{} diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index 064dfb760d..6af9b75f3a 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/jsonprovider" - "github.com/hashicorp/terraform/internal/tfdiags" ) // ProvidersCommand is a Command implementation that prints out information @@ -28,23 +27,12 @@ func (c *ProvidersSchemaCommand) Synopsis() string { } func (c *ProvidersSchemaCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("providers schema") - var jsonOutput bool - cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output") - - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + _, diags := arguments.ParseProvidersSchema(c.Meta.process(args)) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } - if !jsonOutput { - c.Ui.Error( - "The `terraform providers schema` command requires the `-json` flag.\n") - cmdFlags.Usage() - return 1 - } viewType := arguments.ViewJSON // See above; enforced use of JSON output // Check for user-supplied plugin path @@ -53,9 +41,6 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) return 1 } - - var diags tfdiags.Diagnostics - // Load the backend b, backendDiags := c.backend(".", viewType) diags = diags.Append(backendDiags) diff --git a/internal/command/state_list.go b/internal/command/state_list.go index 0bbd89970f..8eaefbe3b8 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 7936b6c1ce..64d47e6eba 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 781295170b..6eb6da85b5 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 ea4e805a77..b99ca05cf3 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 6864896f7c..73967806c3 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,24 +26,17 @@ type StateReplaceProviderCommand struct { } func (c *StateReplaceProviderCommand) Run(args []string) int { - args = c.Meta.process(args) + parsedArgs, parseDiags := arguments.ParseStateReplaceProvider(c.Meta.process(args)) + if parseDiags.HasErrors() { + c.showDiagnostics(parseDiags) + return 1 + } - 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 - } + 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) @@ -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 fa30605a72..042586a163 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 7e3419ca66..1c555497de 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,8 +74,7 @@ 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 { + for _, addrStr := range parsedArgs.Addrs { moreAddrs, moreDiags := c.lookupResourceInstanceAddr(state, true, addrStr) addrs = append(addrs, moreAddrs...) diags = diags.Append(moreDiags) @@ -94,7 +85,7 @@ func (c *StateRmCommand) Run(args []string) int { } prefix := "Removed " - if dryRun { + if parsedArgs.DryRun { prefix = "Would remove " } @@ -103,13 +94,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.") } diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 7a5b4d8dfc..e25f42d2e5 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 } diff --git a/internal/command/taint.go b/internal/command/taint.go index c666aabac2..ba9ef4c82d 100644 --- a/internal/command/taint.go +++ b/internal/command/taint.go @@ -22,33 +22,24 @@ type TaintCommand struct { Meta } -func (c *TaintCommand) Run(args []string) int { - args = c.Meta.process(args) - var allowMissing bool - cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("taint") - cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "allow missing") - cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") - cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) +func (c *TaintCommand) Run(rawArgs []string) int { + parsedArgs, parseDiags := arguments.ParseTaint(c.Meta.process(rawArgs)) + if parseDiags.HasErrors() { + c.showDiagnostics(parseDiags) return 1 } + // Copy parsed flags to Meta + c.Meta.backupPath = parsedArgs.BackupPath + c.Meta.stateLock = parsedArgs.StateLock + c.Meta.stateLockTimeout = parsedArgs.StateLockTimeout + c.Meta.statePath = parsedArgs.StatePath + c.Meta.stateOutPath = parsedArgs.StateOutPath + c.Meta.ignoreRemoteVersion = parsedArgs.IgnoreRemoteVersion + var diags tfdiags.Diagnostics - // Require the one argument for the resource to taint - args = cmdFlags.Args() - if len(args) != 1 { - c.Ui.Error("The taint command expects exactly one argument.") - cmdFlags.Usage() - return 1 - } - - addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) + addr, addrDiags := addrs.ParseAbsResourceInstanceStr(parsedArgs.Address) diags = diags.Append(addrDiags) if addrDiags.HasErrors() { c.showDiagnostics(diags) @@ -117,7 +108,7 @@ func (c *TaintCommand) Run(args []string) int { // Get the actual state structure state := stateMgr.State() if state.Empty() { - if allowMissing { + if parsedArgs.AllowMissing { return c.allowMissingExit(addr) } @@ -144,7 +135,7 @@ func (c *TaintCommand) Run(args []string) int { rs := ss.Resource(addr.ContainingResource()) is := ss.ResourceInstance(addr) if is == nil { - if allowMissing { + if parsedArgs.AllowMissing { return c.allowMissingExit(addr) } diff --git a/internal/command/untaint.go b/internal/command/untaint.go index 3e01829686..b8d95b04f7 100644 --- a/internal/command/untaint.go +++ b/internal/command/untaint.go @@ -22,33 +22,24 @@ type UntaintCommand struct { Meta } -func (c *UntaintCommand) Run(args []string) int { - args = c.Meta.process(args) - var allowMissing bool - cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("untaint") - cmdFlags.BoolVar(&allowMissing, "allow-missing", false, "allow missing") - cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") - cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) +func (c *UntaintCommand) Run(rawArgs []string) int { + parsedArgs, parseDiags := arguments.ParseUntaint(c.Meta.process(rawArgs)) + if parseDiags.HasErrors() { + c.showDiagnostics(parseDiags) return 1 } + // Copy parsed flags to Meta + c.Meta.backupPath = parsedArgs.BackupPath + c.Meta.stateLock = parsedArgs.StateLock + c.Meta.stateLockTimeout = parsedArgs.StateLockTimeout + c.Meta.statePath = parsedArgs.StatePath + c.Meta.stateOutPath = parsedArgs.StateOutPath + c.Meta.ignoreRemoteVersion = parsedArgs.IgnoreRemoteVersion + var diags tfdiags.Diagnostics - // Require the one argument for the resource to untaint - args = cmdFlags.Args() - if len(args) != 1 { - c.Ui.Error("The untaint command expects exactly one argument.") - cmdFlags.Usage() - return 1 - } - - addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) + addr, addrDiags := addrs.ParseAbsResourceInstanceStr(parsedArgs.Address) diags = diags.Append(addrDiags) if addrDiags.HasErrors() { c.showDiagnostics(diags) @@ -107,7 +98,7 @@ func (c *UntaintCommand) Run(args []string) int { // Get the actual state structure state := stateMgr.State() if state.Empty() { - if allowMissing { + if parsedArgs.AllowMissing { return c.allowMissingExit(addr) } @@ -126,7 +117,7 @@ func (c *UntaintCommand) Run(args []string) int { rs := ss.Resource(addr.ContainingResource()) is := ss.ResourceInstance(addr) if is == nil { - if allowMissing { + if parsedArgs.AllowMissing { return c.allowMissingExit(addr) }