Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding name option for output command. #256

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions tfexec/internal/e2etest/state_replace_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package e2etest

import (
"context"
"encoding/json"
"testing"

"github.com/hashicorp/go-version"
tfjson "github.com/hashicorp/terraform-json"

"github.com/hashicorp/terraform-exec/tfexec"
)

func TestStateReplaceProvider(t *testing.T) {
runTest(t, "basic_with_state", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(providerAddressMinVersion) {
t.Skip("state file provider FQNs not compatible with this Terraform version")
}

providerName := "registry.terraform.io/mildred/null"

err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

err = tf.StateReplaceProvider(context.Background(), "hashicorp/null", "mildred/null")
if err != nil {
t.Fatalf("error running StateReplaceProvider: %s", err)
}

err = tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

formatVersion := "0.1"
var sensitiveValues json.RawMessage
if tfv.Core().GreaterThanOrEqual(v1_0_1) {
formatVersion = "0.2"
sensitiveValues = json.RawMessage([]byte("{}"))
}
if tfv.Core().GreaterThanOrEqual(v1_1) {
formatVersion = "1.0"
}

// test that the new state is as expected
expected := &tfjson.State{
FormatVersion: formatVersion,
// TerraformVersion is ignored to facilitate latest version testing
Values: &tfjson.StateValues{
RootModule: &tfjson.StateModule{
Resources: []*tfjson.StateResource{{
Address: "null_resource.foo",
AttributeValues: map[string]interface{}{
"id": "5510719323588825107",
"triggers": nil,
},
SensitiveValues: sensitiveValues,
Mode: tfjson.ManagedResourceMode,
Type: "null_resource",
Name: "foo",
ProviderName: providerName,
}},
},
},
}

actual, err := tf.Show(context.Background())
if err != nil {
t.Fatal(err)
}

if diff := diffState(expected, actual); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
})
}
8 changes: 8 additions & 0 deletions tfexec/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ func Out(path string) *OutOption {
return &OutOption{path}
}

type OutputNameOption struct {
name string
}

func OutputName(name string) *OutputNameOption {
return &OutputNameOption{name}
}

type ParallelismOption struct {
parallelism int
}
Expand Down
41 changes: 31 additions & 10 deletions tfexec/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

type outputConfig struct {
state string
name string
json bool
}

Expand All @@ -22,6 +23,10 @@ func (opt *StateOption) configureOutput(conf *outputConfig) {
conf.state = opt.path
}

func (opt *OutputNameOption) configureOutput(conf *outputConfig) {
conf.name = opt.name
}

// OutputMeta represents the JSON output of 'terraform output -json',
// which resembles state format version 3 due to a historical accident.
// Please see hashicorp/terraform/command/output.go.
Expand All @@ -34,23 +39,34 @@ type OutputMeta struct {

// Output represents the terraform output subcommand.
func (tf *Terraform) Output(ctx context.Context, opts ...OutputOption) (map[string]OutputMeta, error) {
outputCmd := tf.outputCmd(ctx, opts...)
c := defaultOutputOptions

for _, o := range opts {
o.configureOutput(&c)
}
outputCmd := tf.outputCmd(ctx, c)

outputs := map[string]OutputMeta{}
err := tf.runTerraformCmdJSON(ctx, outputCmd, &outputs)
if err != nil {
return nil, err
if c.name != "" {
var outputValue json.RawMessage
err := tf.runTerraformCmdJSON(ctx, outputCmd, &outputValue)
if err != nil {
return nil, err
}
output := OutputMeta{}
output.Value = outputValue
outputs[c.name] = output
} else {
err := tf.runTerraformCmdJSON(ctx, outputCmd, &outputs)
if err != nil {
return nil, err
}
}

return outputs, nil
}

func (tf *Terraform) outputCmd(ctx context.Context, opts ...OutputOption) *exec.Cmd {
c := defaultOutputOptions

for _, o := range opts {
o.configureOutput(&c)
}
func (tf *Terraform) outputCmd(ctx context.Context, c outputConfig) *exec.Cmd {

args := []string{"output", "-no-color", "-json"}

Expand All @@ -59,5 +75,10 @@ func (tf *Terraform) outputCmd(ctx context.Context, opts ...OutputOption) *exec.
args = append(args, "-state="+c.state)
}

// output name: only pass if set
if c.name != "" {
args = append(args, c.name)
}

return tf.buildTerraformCmd(ctx, nil, args...)
}
23 changes: 20 additions & 3 deletions tfexec/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ func TestOutputCmd(t *testing.T) {
tf.SetEnv(map[string]string{})

t.Run("defaults", func(t *testing.T) {
outputCmd := tf.outputCmd(context.Background())
var config = outputConfig{}
outputCmd := tf.outputCmd(context.Background(), config)

assertCmd(t, []string{
"output",
Expand All @@ -29,8 +30,9 @@ func TestOutputCmd(t *testing.T) {
})

t.Run("override all defaults", func(t *testing.T) {
outputCmd := tf.outputCmd(context.Background(),
State("teststate"))
var config = outputConfig{}
config.state = "teststate"
outputCmd := tf.outputCmd(context.Background(), config)

assertCmd(t, []string{
"output",
Expand All @@ -39,4 +41,19 @@ func TestOutputCmd(t *testing.T) {
"-state=teststate",
}, nil, outputCmd)
})

t.Run("defaults with single output", func(t *testing.T) {
var config = outputConfig{}
config.state = "teststate"
config.name = "testoutput"
outputCmd := tf.outputCmd(context.Background(), config)

assertCmd(t, []string{
"output",
"-no-color",
"-json",
"-state=teststate",
"testoutput",
}, nil, outputCmd)
})
}
87 changes: 87 additions & 0 deletions tfexec/state_replace_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package tfexec

import (
"context"
"os/exec"
"strconv"
)

type stateReplaceProviderConfig struct {
backup string
lock bool
lockTimeout string
state string
stateOut string
}

var defaultStateReplaceProviderOptions = stateReplaceProviderConfig{
lock: true,
lockTimeout: "0s",
}

// StateReplaceProviderCmdOption represents options used in the Refresh method.
type StateReplaceProviderCmdOption interface {
configureStateReplaceProvider(*stateReplaceProviderConfig)
}

func (opt *BackupOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) {
conf.backup = opt.path
}

func (opt *LockOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) {
conf.lock = opt.lock
}

func (opt *LockTimeoutOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) {
conf.lockTimeout = opt.timeout
}

func (opt *StateOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) {
conf.state = opt.path
}

func (opt *StateOutOption) configureStateReplaceProvider(conf *stateReplaceProviderConfig) {
conf.stateOut = opt.path
}

// StateMv represents the terraform state mv subcommand.
func (tf *Terraform) StateReplaceProvider(ctx context.Context, fromProviderFqn string, toProviderFqn string, opts ...StateReplaceProviderCmdOption) error {
cmd, err := tf.stateReplaceProviderCmd(ctx, fromProviderFqn, toProviderFqn, opts...)
if err != nil {
return err
}
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) stateReplaceProviderCmd(ctx context.Context, fromProviderFqn string, toProviderFqn string, opts ...StateReplaceProviderCmdOption) (*exec.Cmd, error) {
c := defaultStateReplaceProviderOptions

for _, o := range opts {
o.configureStateReplaceProvider(&c)
}

args := []string{"state", "replace-provider", "-no-color", "-auto-approve"}

// string opts: only pass if set
if c.backup != "" {
args = append(args, "-backup="+c.backup)
}
if c.lockTimeout != "" {
args = append(args, "-lock-timeout="+c.lockTimeout)
}
if c.state != "" {
args = append(args, "-state="+c.state)
}
if c.stateOut != "" {
args = append(args, "-state-out="+c.stateOut)
}

// boolean and numerical opts: always pass
args = append(args, "-lock="+strconv.FormatBool(c.lock))

// positional arguments
args = append(args, fromProviderFqn)
args = append(args, toProviderFqn)

return tf.buildTerraformCmd(ctx, nil, args...), nil
}
59 changes: 59 additions & 0 deletions tfexec/state_replace_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package tfexec

import (
"context"
"testing"

"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
)

func TestStateReplaceProviderCmd(t *testing.T) {
td := t.TempDir()

tf, err := NewTerraform(td, tfVersion(t, testutil.Latest013))
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) {
stateReplaceProviderCmd, err := tf.stateReplaceProviderCmd(context.Background(), "testfromprovider", "testtoprovider")
if err != nil {
t.Fatal(err)
}

assertCmd(t, []string{
"state",
"replace-provider",
"-no-color",
"-auto-approve",
"-lock-timeout=0s",
"-lock=true",
"testfromprovider",
"testtoprovider",
}, nil, stateReplaceProviderCmd)
})

t.Run("override all defaults", func(t *testing.T) {
stateReplaceProviderCmd, err := tf.stateReplaceProviderCmd(context.Background(), "testfromprovider", "testtoprovider", Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), Lock(false))
if err != nil {
t.Fatal(err)
}

assertCmd(t, []string{
"state",
"replace-provider",
"-no-color",
"-auto-approve",
"-backup=testbackup",
"-lock-timeout=200s",
"-state=teststate",
"-state-out=teststateout",
"-lock=false",
"testfromprovider",
"testtoprovider",
}, nil, stateReplaceProviderCmd)
})
}