use the arguments package for taint commands

pull/38186/head
Daniel Schmidt 5 days ago
parent 492e98ab75
commit 6b83486498
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -0,0 +1,88 @@
// Copyright (c) HashiCorp, Inc.
// 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{
StateLock: true,
}
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
}

@ -0,0 +1,179 @@
// Copyright (c) HashiCorp, Inc.
// 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",
StateLock: false,
},
},
"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",
StateLock: false,
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)
})
}
}

@ -0,0 +1,88 @@
// Copyright (c) HashiCorp, Inc.
// 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{
StateLock: true,
}
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
}

@ -0,0 +1,179 @@
// Copyright (c) HashiCorp, Inc.
// 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",
StateLock: false,
},
},
"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",
StateLock: false,
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)
})
}
}

@ -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
}
var diags tfdiags.Diagnostics
// 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
// 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
}
var diags tfdiags.Diagnostics
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)
}

@ -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
}
var diags tfdiags.Diagnostics
// 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
// 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
}
var diags tfdiags.Diagnostics
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)
}

Loading…
Cancel
Save