mirror of https://github.com/hashicorp/terraform
refactor: Update `workspace select` and `delete` subcommands to use the arguments package for parsing arguments and flags (#38429)
* feat: Add `workspace show`-related code to arguments package * refactor: Update `workspace show` to use the arguments package when parsing arguments * refactor: Split code for workspace subcommands into separate files in arguments package * refactor: Move code common to all workspace commands into separate file in arguments package * feat: Add `workspace delete`-related code to arguments package * refactor: Update `workspace delete` to use the arguments package when parsing argumentspull/38469/head
parent
aa28bfa063
commit
e24abdf6ff
@ -0,0 +1,78 @@
|
||||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// WorkspaceDelete represent flags and arguments specific to the `terraform workspace delete` command.
|
||||
type WorkspaceDelete struct {
|
||||
Workspace
|
||||
|
||||
// Flags
|
||||
Lock bool
|
||||
LockTimeout time.Duration
|
||||
Force bool
|
||||
|
||||
// Positional arguments
|
||||
Name string
|
||||
}
|
||||
|
||||
// ParseWorkspaceDelete processes CLI arguments, returning a WorkspaceDelete value and errors.
|
||||
// If errors are encountered, an WorkspaceDelete value is still returned representing
|
||||
// the best effort interpretation of the arguments.
|
||||
func ParseWorkspaceDelete(args []string) (*WorkspaceDelete, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
var force bool
|
||||
var stateLock bool
|
||||
var stateLockTimeout time.Duration
|
||||
cmdFlags := defaultFlagSet("workspace delete")
|
||||
cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty workspace")
|
||||
cmdFlags.BoolVar(&stateLock, "lock", true, "lock state")
|
||||
cmdFlags.DurationVar(&stateLockTimeout, "lock-timeout", 0, "lock timeout")
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
// `workspace delete` takes only one positional argument: workspace name.
|
||||
args = cmdFlags.Args()
|
||||
var name string
|
||||
if len(args) == 0 {
|
||||
diags = diags.Append(errors.New("Expected a single argument: NAME.")) // Recreating pre-existing error from command package
|
||||
} else {
|
||||
|
||||
// Obtain and validate name argument
|
||||
//
|
||||
// We purposefully don't use ValidWorkspaceName here; if a user
|
||||
// creates a workspace with an invalid name they should be able to
|
||||
// delete it easily.
|
||||
name = args[0]
|
||||
if name == "" {
|
||||
diags = diags.Append(fmt.Errorf("Expected a workspace name as an argument, instead got an empty string: %q\n", args[0]))
|
||||
}
|
||||
|
||||
args = args[1:]
|
||||
if len(args) != 0 {
|
||||
diags = diags.Append(errors.New("Expected a single argument: NAME."))
|
||||
}
|
||||
}
|
||||
|
||||
return &WorkspaceDelete{
|
||||
Workspace: Workspace{ViewType: ViewHuman},
|
||||
Name: name,
|
||||
Lock: stateLock,
|
||||
LockTimeout: stateLockTimeout,
|
||||
Force: force,
|
||||
}, diags
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestParseWorkspaceDelete_valid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *WorkspaceDelete
|
||||
}{
|
||||
"name specified & default flags": {
|
||||
[]string{"my-new-workspace"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "my-new-workspace",
|
||||
Lock: true,
|
||||
Force: false,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
},
|
||||
"invalid names are tolerated during delete": {
|
||||
[]string{"§@!invalid-name!@§"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "§@!invalid-name!@§",
|
||||
Lock: true,
|
||||
Force: false,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
},
|
||||
"lock flag specified": {
|
||||
[]string{"-lock=false", "my-new-workspace"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "my-new-workspace",
|
||||
Lock: false,
|
||||
Force: false,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
},
|
||||
"force flag specified": {
|
||||
[]string{"-force=true", "my-new-workspace"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "my-new-workspace",
|
||||
Lock: true,
|
||||
Force: true,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
},
|
||||
"lock-timeout flag specified": {
|
||||
[]string{"-lock-timeout=30s", "my-new-workspace"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "my-new-workspace",
|
||||
Lock: true,
|
||||
Force: false,
|
||||
LockTimeout: 30 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := ParseWorkspaceDelete(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 TestParseWorkspaceDelete_invalid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *WorkspaceDelete
|
||||
wantDiags tfdiags.Diagnostics
|
||||
}{
|
||||
"unknown flag": {
|
||||
[]string{"-boop", "my-new-workspace"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "my-new-workspace",
|
||||
Force: false,
|
||||
Lock: true,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
"flag provided but not defined: -boop",
|
||||
),
|
||||
},
|
||||
},
|
||||
"too many arguments": {
|
||||
[]string{"my-new-workspace", "bar"},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Name: "my-new-workspace", // First positional argument is still captured``
|
||||
Force: false,
|
||||
Lock: true,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Expected a single argument: NAME.",
|
||||
"", // No detail
|
||||
),
|
||||
},
|
||||
},
|
||||
"no arguments": {
|
||||
[]string{},
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Force: false,
|
||||
Lock: true,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Expected a single argument: NAME.",
|
||||
"", // No detail
|
||||
),
|
||||
},
|
||||
},
|
||||
"empty string as workspace name": {
|
||||
[]string{""}, // empty string
|
||||
&WorkspaceDelete{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
Force: false,
|
||||
Lock: true,
|
||||
LockTimeout: 0,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Expected a workspace name as an argument, instead got an empty string: \"\"\n",
|
||||
"", // No detail
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, gotDiags := ParseWorkspaceDelete(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,48 @@
|
||||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// WorkspaceList represent arguments specific to the `terraform workspace list` command.
|
||||
type WorkspaceList struct {
|
||||
Workspace
|
||||
}
|
||||
|
||||
// ParseWorkspaceList processes CLI arguments, returning a WorkspaceList value and errors.
|
||||
// If errors are encountered, an WorkspaceList value is still returned representing
|
||||
// the best effort interpretation of the arguments.
|
||||
func ParseWorkspaceList(args []string) (*WorkspaceList, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
var jsonOutput bool
|
||||
cmdFlags := defaultFlagSet("workspace list")
|
||||
cmdFlags.BoolVar(&jsonOutput, "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(),
|
||||
))
|
||||
}
|
||||
|
||||
// `workspace list` takes no positional arguments. Historically there was a DIR argument that was replaced with the -chdir flag.
|
||||
// Here we replicate the old behaviour of suggesting the user to use -chdir if they provide any positional arguments.
|
||||
args = cmdFlags.Args()
|
||||
if len(args) != 0 {
|
||||
diags = diags.Append(errors.New("Too many command line arguments. Did you mean to use -chdir?"))
|
||||
}
|
||||
|
||||
switch {
|
||||
case jsonOutput:
|
||||
return &WorkspaceList{Workspace: Workspace{ViewType: ViewJSON}}, diags
|
||||
default:
|
||||
return &WorkspaceList{Workspace: Workspace{ViewType: ViewHuman}}, diags
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
type WorkspaceShow struct {
|
||||
Workspace
|
||||
}
|
||||
|
||||
func ParseWorkspaceShow(args []string) (*WorkspaceShow, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
cmdFlags := defaultFlagSet("workspace show")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
// `workspace show` takes no positional arguments.
|
||||
// We could add validation here to return an error when unexpected arguments are present,
|
||||
// but this would be a breaking change as no validation was performed in this case before.
|
||||
|
||||
return &WorkspaceShow{Workspace: Workspace{ViewType: ViewHuman}}, diags
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestParseWorkspaceShow_valid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *WorkspaceShow
|
||||
}{
|
||||
"defaults": {
|
||||
nil,
|
||||
&WorkspaceShow{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
},
|
||||
},
|
||||
"currently there is no validation about too many arguments": {
|
||||
[]string{"bar"},
|
||||
&WorkspaceShow{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := ParseWorkspaceShow(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 TestParseWorkspaceShow_invalid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *WorkspaceShow
|
||||
wantDiags tfdiags.Diagnostics
|
||||
}{
|
||||
"unknown flag": {
|
||||
[]string{"-boop"},
|
||||
&WorkspaceShow{
|
||||
Workspace: Workspace{
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
},
|
||||
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 := ParseWorkspaceShow(tc.args)
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue