tfexec: Add provider reattach support to all `terraform workspace` subcommands (#556)
diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd7d72..ac93c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,3 +1,8 @@ +# 0.25.0 (Unreleased) + +ENHANCEMENTS: +- tfexec: Added provider reattach support to all `terraform workspace` subcommands ([#556](https://github.com/hashicorp/terraform-exec/pull/556)) + # 0.24.0 (September 17, 2025) ENHANCEMENTS:
diff --git a/tfexec/workspace_delete.go b/tfexec/workspace_delete.go index f2a17e6..30797fc 100644 --- a/tfexec/workspace_delete.go +++ b/tfexec/workspace_delete.go
@@ -11,9 +11,10 @@ ) type workspaceDeleteConfig struct { - lock bool - lockTimeout string - force bool + lock bool + lockTimeout string + force bool + reattachInfo ReattachInfo } var defaultWorkspaceDeleteOptions = workspaceDeleteConfig{ @@ -38,6 +39,10 @@ conf.force = opt.force } +func (opt *ReattachOption) configureWorkspaceDelete(conf *workspaceDeleteConfig) { + conf.reattachInfo = opt.info +} + // WorkspaceDelete represents the workspace delete subcommand to the Terraform CLI. func (tf *Terraform) WorkspaceDelete(ctx context.Context, workspace string, opts ...WorkspaceDeleteCmdOption) error { cmd, err := tf.workspaceDeleteCmd(ctx, workspace, opts...) @@ -78,7 +83,16 @@ args = append(args, workspace) - cmd := tf.buildTerraformCmd(ctx, nil, args...) + mergeEnv := map[string]string{} + if c.reattachInfo != nil { + reattachStr, err := c.reattachInfo.marshalString() + if err != nil { + return nil, err + } + mergeEnv[reattachEnvVar] = reattachStr + } + + cmd := tf.buildTerraformCmd(ctx, mergeEnv, args...) return cmd, nil }
diff --git a/tfexec/workspace_delete_test.go b/tfexec/workspace_delete_test.go index 41cd52a..bfc00e5 100644 --- a/tfexec/workspace_delete_test.go +++ b/tfexec/workspace_delete_test.go
@@ -50,4 +50,30 @@ "workspace-name", }, nil, workspaceDeleteCmd) }) + + t.Run("reattach config", func(t *testing.T) { + workspaceDeleteCmd, err := tf.workspaceDeleteCmd(context.Background(), "workspace-name", Reattach(map[string]ReattachConfig{ + "registry.terraform.io/hashicorp/examplecloud": { + Protocol: "grpc", + ProtocolVersion: 6, + Pid: 1234, + Test: true, + Addr: ReattachConfigAddr{ + Network: "unix", + String: "/fake_folder/T/plugin123", + }, + }, + })) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", "delete", + "-no-color", + "workspace-name", + }, map[string]string{ + "TF_REATTACH_PROVIDERS": `{"registry.terraform.io/hashicorp/examplecloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":1234,"Test":true,"Addr":{"Network":"unix","String":"/fake_folder/T/plugin123"}}}`, + }, workspaceDeleteCmd) + }) }
diff --git a/tfexec/workspace_list.go b/tfexec/workspace_list.go index 1b4bec3..b451731 100644 --- a/tfexec/workspace_list.go +++ b/tfexec/workspace_list.go
@@ -5,18 +5,35 @@ import ( "context" + "os/exec" "strings" ) +type workspaceListConfig struct { + reattachInfo ReattachInfo +} + +var defaultWorkspaceListOptions = workspaceListConfig{} + +type WorkspaceListOption interface { + configureWorkspaceList(*workspaceListConfig) +} + +func (opt *ReattachOption) configureWorkspaceList(conf *workspaceListConfig) { + conf.reattachInfo = opt.info +} + // WorkspaceList represents the workspace list subcommand to the Terraform CLI. -func (tf *Terraform) WorkspaceList(ctx context.Context) ([]string, string, error) { - // TODO: [DIR] param option - wlCmd := tf.buildTerraformCmd(ctx, nil, "workspace", "list", "-no-color") +func (tf *Terraform) WorkspaceList(ctx context.Context, opts ...WorkspaceListOption) ([]string, string, error) { + wlCmd, err := tf.workspaceListCmd(ctx, opts...) + if err != nil { + return nil, "", err + } var outBuf strings.Builder wlCmd.Stdout = &outBuf - err := tf.runTerraformCmd(ctx, wlCmd) + err = tf.runTerraformCmd(ctx, wlCmd) if err != nil { return nil, "", err } @@ -28,6 +45,25 @@ const currentWorkspacePrefix = "* " +func (tf *Terraform) workspaceListCmd(ctx context.Context, opts ...WorkspaceListOption) (*exec.Cmd, error) { + c := defaultWorkspaceListOptions + + for _, o := range opts { + o.configureWorkspaceList(&c) + } + + mergeEnv := map[string]string{} + if c.reattachInfo != nil { + reattachStr, err := c.reattachInfo.marshalString() + if err != nil { + return nil, err + } + mergeEnv[reattachEnvVar] = reattachStr + } + + return tf.buildTerraformCmd(ctx, mergeEnv, "workspace", "list", "-no-color"), nil +} + func parseWorkspaceList(stdout string) ([]string, string) { lines := strings.Split(stdout, "\n")
diff --git a/tfexec/workspace_list_test.go b/tfexec/workspace_list_test.go index 60bb67e..72d305e 100644 --- a/tfexec/workspace_list_test.go +++ b/tfexec/workspace_list_test.go
@@ -4,11 +4,61 @@ package tfexec import ( + "context" "fmt" "reflect" "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" ) +func TestWorkspaceListCmd(t *testing.T) { + tf, err := NewTerraform(t.TempDir(), tfVersion(t, testutil.Latest_v1)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + workspaceListCmd, err := tf.workspaceListCmd(context.Background()) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", "list", + "-no-color", + }, nil, workspaceListCmd) + }) + + t.Run("reattach config", func(t *testing.T) { + workspaceListCmd, err := tf.workspaceListCmd(context.Background(), Reattach(map[string]ReattachConfig{ + "registry.terraform.io/hashicorp/examplecloud": { + Protocol: "grpc", + ProtocolVersion: 6, + Pid: 1234, + Test: true, + Addr: ReattachConfigAddr{ + Network: "unix", + String: "/fake_folder/T/plugin123", + }, + }, + })) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", "list", + "-no-color", + }, map[string]string{ + "TF_REATTACH_PROVIDERS": `{"registry.terraform.io/hashicorp/examplecloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":1234,"Test":true,"Addr":{"Network":"unix","String":"/fake_folder/T/plugin123"}}}`, + }, workspaceListCmd) + }) +} + func TestParseWorkspaceList(t *testing.T) { for i, c := range []struct { expected []string
diff --git a/tfexec/workspace_new.go b/tfexec/workspace_new.go index 921a118..e1871ec 100644 --- a/tfexec/workspace_new.go +++ b/tfexec/workspace_new.go
@@ -11,9 +11,10 @@ ) type workspaceNewConfig struct { - lock bool - lockTimeout string - copyState string + lock bool + lockTimeout string + copyState string + reattachInfo ReattachInfo } var defaultWorkspaceNewOptions = workspaceNewConfig{ @@ -38,6 +39,10 @@ conf.copyState = opt.path } +func (opt *ReattachOption) configureWorkspaceNew(conf *workspaceNewConfig) { + conf.reattachInfo = opt.info +} + // WorkspaceNew represents the workspace new subcommand to the Terraform CLI. func (tf *Terraform) WorkspaceNew(ctx context.Context, workspace string, opts ...WorkspaceNewCmdOption) error { cmd, err := tf.workspaceNewCmd(ctx, workspace, opts...) @@ -48,8 +53,6 @@ } func (tf *Terraform) workspaceNewCmd(ctx context.Context, workspace string, opts ...WorkspaceNewCmdOption) (*exec.Cmd, error) { - // TODO: [DIR] param option - c := defaultWorkspaceNewOptions for _, o := range opts { @@ -80,7 +83,16 @@ args = append(args, workspace) - cmd := tf.buildTerraformCmd(ctx, nil, args...) + mergeEnv := map[string]string{} + if c.reattachInfo != nil { + reattachStr, err := c.reattachInfo.marshalString() + if err != nil { + return nil, err + } + mergeEnv[reattachEnvVar] = reattachStr + } + + cmd := tf.buildTerraformCmd(ctx, mergeEnv, args...) return cmd, nil }
diff --git a/tfexec/workspace_new_test.go b/tfexec/workspace_new_test.go index 744c8f6..08c4c02 100644 --- a/tfexec/workspace_new_test.go +++ b/tfexec/workspace_new_test.go
@@ -56,4 +56,30 @@ "workspace-name", }, nil, workspaceNewCmd) }) + + t.Run("reattach config", func(t *testing.T) { + workspaceNewCmd, err := tf.workspaceNewCmd(context.Background(), "workspace-name", Reattach(map[string]ReattachConfig{ + "registry.terraform.io/hashicorp/examplecloud": { + Protocol: "grpc", + ProtocolVersion: 6, + Pid: 1234, + Test: true, + Addr: ReattachConfigAddr{ + Network: "unix", + String: "/fake_folder/T/plugin123", + }, + }, + })) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", "new", + "-no-color", + "workspace-name", + }, map[string]string{ + "TF_REATTACH_PROVIDERS": `{"registry.terraform.io/hashicorp/examplecloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":1234,"Test":true,"Addr":{"Network":"unix","String":"/fake_folder/T/plugin123"}}}`, + }, workspaceNewCmd) + }) }
diff --git a/tfexec/workspace_select.go b/tfexec/workspace_select.go index da88472..c69ff4f 100644 --- a/tfexec/workspace_select.go +++ b/tfexec/workspace_select.go
@@ -3,11 +3,50 @@ package tfexec -import "context" +import ( + "context" + "os/exec" +) + +type workspaceSelectConfig struct { + reattachInfo ReattachInfo +} + +var defaultWorkspaceSelectOptions = workspaceSelectConfig{} + +type WorkspaceSelectOption interface { + configureWorkspaceSelect(*workspaceSelectConfig) +} + +func (opt *ReattachOption) configureWorkspaceSelect(conf *workspaceSelectConfig) { + conf.reattachInfo = opt.info +} // WorkspaceSelect represents the workspace select subcommand to the Terraform CLI. -func (tf *Terraform) WorkspaceSelect(ctx context.Context, workspace string) error { - // TODO: [DIR] param option +func (tf *Terraform) WorkspaceSelect(ctx context.Context, workspace string, opts ...WorkspaceSelectOption) error { + cmd, err := tf.workspaceSelectCmd(ctx, workspace, opts...) + if err != nil { + return err + } - return tf.runTerraformCmd(ctx, tf.buildTerraformCmd(ctx, nil, "workspace", "select", "-no-color", workspace)) + return tf.runTerraformCmd(ctx, cmd) +} + +func (tf *Terraform) workspaceSelectCmd(ctx context.Context, workspace string, opts ...WorkspaceSelectOption) (*exec.Cmd, error) { + c := defaultWorkspaceSelectOptions + + for _, o := range opts { + o.configureWorkspaceSelect(&c) + } + + mergeEnv := map[string]string{} + if c.reattachInfo != nil { + reattachStr, err := c.reattachInfo.marshalString() + if err != nil { + return nil, err + } + mergeEnv[reattachEnvVar] = reattachStr + } + + return tf.buildTerraformCmd(ctx, mergeEnv, "workspace", "select", "-no-color", workspace), nil }
diff --git a/tfexec/workspace_select_test.go b/tfexec/workspace_select_test.go new file mode 100644 index 0000000..6d06c2c --- /dev/null +++ b/tfexec/workspace_select_test.go
@@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestWorkspaceSelectCmd(t *testing.T) { + tf, err := NewTerraform(t.TempDir(), tfVersion(t, testutil.Latest_v1)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + workspaceSelectCmd, err := tf.workspaceSelectCmd(context.Background(), "workspace-name") + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", "select", + "-no-color", + "workspace-name", + }, nil, workspaceSelectCmd) + }) + + t.Run("reattach config", func(t *testing.T) { + workspaceSelectCmd, err := tf.workspaceSelectCmd(context.Background(), "workspace-name", Reattach(map[string]ReattachConfig{ + "registry.terraform.io/hashicorp/examplecloud": { + Protocol: "grpc", + ProtocolVersion: 6, + Pid: 1234, + Test: true, + Addr: ReattachConfigAddr{ + Network: "unix", + String: "/fake_folder/T/plugin123", + }, + }, + })) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", "select", + "-no-color", + "workspace-name", + }, map[string]string{ + "TF_REATTACH_PROVIDERS": `{"registry.terraform.io/hashicorp/examplecloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":1234,"Test":true,"Addr":{"Network":"unix","String":"/fake_folder/T/plugin123"}}}`, + }, workspaceSelectCmd) + }) +}
diff --git a/tfexec/workspace_show.go b/tfexec/workspace_show.go index 840eff9..9ecfe47 100644 --- a/tfexec/workspace_show.go +++ b/tfexec/workspace_show.go
@@ -10,9 +10,23 @@ "strings" ) +type workspaceShowConfig struct { + reattachInfo ReattachInfo +} + +var defaultWorkspaceShowOptions = workspaceShowConfig{} + +type WorkspaceShowOption interface { + configureWorkspaceShow(*workspaceShowConfig) +} + +func (opt *ReattachOption) configureWorkspaceShow(conf *workspaceShowConfig) { + conf.reattachInfo = opt.info +} + // WorkspaceShow represents the workspace show subcommand to the Terraform CLI. -func (tf *Terraform) WorkspaceShow(ctx context.Context) (string, error) { - workspaceShowCmd, err := tf.workspaceShowCmd(ctx) +func (tf *Terraform) WorkspaceShow(ctx context.Context, opts ...WorkspaceShowOption) (string, error) { + workspaceShowCmd, err := tf.workspaceShowCmd(ctx, opts...) if err != nil { return "", err } @@ -28,11 +42,26 @@ return strings.TrimSpace(outBuffer.String()), nil } -func (tf *Terraform) workspaceShowCmd(ctx context.Context) (*exec.Cmd, error) { +func (tf *Terraform) workspaceShowCmd(ctx context.Context, opts ...WorkspaceShowOption) (*exec.Cmd, error) { err := tf.compatible(ctx, tf0_10_0, nil) if err != nil { return nil, fmt.Errorf("workspace show was first introduced in Terraform 0.10.0: %w", err) } - return tf.buildTerraformCmd(ctx, nil, "workspace", "show", "-no-color"), nil + c := defaultWorkspaceShowOptions + + for _, o := range opts { + o.configureWorkspaceShow(&c) + } + + mergeEnv := map[string]string{} + if c.reattachInfo != nil { + reattachStr, err := c.reattachInfo.marshalString() + if err != nil { + return nil, err + } + mergeEnv[reattachEnvVar] = reattachStr + } + + return tf.buildTerraformCmd(ctx, mergeEnv, "workspace", "show", "-no-color"), nil }
diff --git a/tfexec/workspace_show_test.go b/tfexec/workspace_show_test.go index cf6fe8b..0ab3289 100644 --- a/tfexec/workspace_show_test.go +++ b/tfexec/workspace_show_test.go
@@ -21,14 +21,42 @@ // empty env, to avoid environ mismatch in testing tf.SetEnv(map[string]string{}) - cmd, err := tf.workspaceShowCmd(context.Background()) - if err != nil { - t.Fatal(err) - } + t.Run("defaults", func(t *testing.T) { + cmd, err := tf.workspaceShowCmd(context.Background()) + if err != nil { + t.Fatal(err) + } - assertCmd(t, []string{ - "workspace", - "show", - "-no-color", - }, map[string]string{}, cmd) + assertCmd(t, []string{ + "workspace", + "show", + "-no-color", + }, nil, cmd) + }) + + t.Run("reattach config", func(t *testing.T) { + cmd, err := tf.workspaceShowCmd(context.Background(), Reattach(map[string]ReattachConfig{ + "registry.terraform.io/hashicorp/examplecloud": { + Protocol: "grpc", + ProtocolVersion: 6, + Pid: 1234, + Test: true, + Addr: ReattachConfigAddr{ + Network: "unix", + String: "/fake_folder/T/plugin123", + }, + }, + })) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "workspace", + "show", + "-no-color", + }, map[string]string{ + "TF_REATTACH_PROVIDERS": `{"registry.terraform.io/hashicorp/examplecloud":{"Protocol":"grpc","ProtocolVersion":6,"Pid":1234,"Test":true,"Addr":{"Network":"unix","String":"/fake_folder/T/plugin123"}}}`, + }, cmd) + }) }