diff --git a/tfexec/internal/e2etest/taint_test.go b/tfexec/internal/e2etest/taint_test.go new file mode 100644 index 00000000..174f46fc --- /dev/null +++ b/tfexec/internal/e2etest/taint_test.go @@ -0,0 +1,29 @@ +package e2etest + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +func TestTaint(t *testing.T) { + runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + err = tf.Apply(context.Background()) + if err != nil { + t.Fatalf("error running Apply: %s", err) + } + + err = tf.Taint(context.Background(), "null_resource.foo") + if err != nil { + t.Fatalf("error running Taint: %s", err) + } + }) +} diff --git a/tfexec/internal/e2etest/untaint_test.go b/tfexec/internal/e2etest/untaint_test.go new file mode 100644 index 00000000..d5f57210 --- /dev/null +++ b/tfexec/internal/e2etest/untaint_test.go @@ -0,0 +1,34 @@ +package e2etest + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +func TestUntaint(t *testing.T) { + runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + err = tf.Apply(context.Background()) + if err != nil { + t.Fatalf("error running Apply: %s", err) + } + + err = tf.Taint(context.Background(), "null_resource.foo") + if err != nil { + t.Fatalf("error running Taint: %s", err) + } + + err = tf.Untaint(context.Background(), "null_resource.foo") + if err != nil { + t.Fatalf("error running Untaint: %s", err) + } + }) +} diff --git a/tfexec/options.go b/tfexec/options.go index 71638895..4c3e3567 100644 --- a/tfexec/options.go +++ b/tfexec/options.go @@ -14,6 +14,16 @@ func AllowMissingConfig(allowMissingConfig bool) *AllowMissingConfigOption { return &AllowMissingConfigOption{allowMissingConfig} } +// AllowMissingOption represents the -allow-missing flag. +type AllowMissingOption struct { + allowMissing bool +} + +// AllowMissing represents the -allow-missing flag. +func AllowMissing(allowMissing bool) *AllowMissingOption { + return &AllowMissingOption{allowMissing} +} + // BackendOption represents the -backend flag. type BackendOption struct { backend bool diff --git a/tfexec/taint.go b/tfexec/taint.go new file mode 100644 index 00000000..cd69df30 --- /dev/null +++ b/tfexec/taint.go @@ -0,0 +1,78 @@ +package tfexec + +import ( + "context" + "fmt" + "os/exec" + "strconv" +) + +type taintConfig struct { + state string + allowMissing bool + lock bool + lockTimeout string +} + +var defaultTaintOptions = taintConfig{ + allowMissing: false, + lock: true, +} + +// TaintOption represents options used in the Taint method. +type TaintOption interface { + configureTaint(*taintConfig) +} + +func (opt *StateOption) configureTaint(conf *taintConfig) { + conf.state = opt.path +} + +func (opt *AllowMissingOption) configureTaint(conf *taintConfig) { + conf.allowMissing = opt.allowMissing +} + +func (opt *LockOption) configureTaint(conf *taintConfig) { + conf.lock = opt.lock +} + +func (opt *LockTimeoutOption) configureTaint(conf *taintConfig) { + conf.lockTimeout = opt.timeout +} + +// Taint represents the terraform taint subcommand. +func (tf *Terraform) Taint(ctx context.Context, address string, opts ...TaintOption) error { + err := tf.compatible(ctx, tf0_4_1, nil) + if err != nil { + return fmt.Errorf("taint was first introduced in Terraform 0.4.1: %w", err) + } + taintCmd := tf.taintCmd(ctx, address, opts...) + return tf.runTerraformCmd(ctx, taintCmd) +} + +func (tf *Terraform) taintCmd(ctx context.Context, address string, opts ...TaintOption) *exec.Cmd { + c := defaultTaintOptions + + for _, o := range opts { + o.configureTaint(&c) + } + + args := []string{"taint", "-no-color"} + + if c.lockTimeout != "" { + args = append(args, "-lock-timeout="+c.lockTimeout) + } + + // string opts: only pass if set + if c.state != "" { + args = append(args, "-state="+c.state) + } + + args = append(args, "-lock="+strconv.FormatBool(c.lock)) + if c.allowMissing { + args = append(args, "-allow-missing") + } + args = append(args, address) + + return tf.buildTerraformCmd(ctx, nil, args...) +} diff --git a/tfexec/taint_test.go b/tfexec/taint_test.go new file mode 100644 index 00000000..67f1d5ef --- /dev/null +++ b/tfexec/taint_test.go @@ -0,0 +1,49 @@ +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestTaintCmd(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) { + taintCmd := tf.taintCmd(context.Background(), "aws_instance.foo") + + assertCmd(t, []string{ + "taint", + "-no-color", + "-lock=true", + "aws_instance.foo", + }, nil, taintCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + taintCmd := tf.taintCmd(context.Background(), "aws_instance.foo", + State("teststate"), + AllowMissing(true), + LockTimeout("200s"), + Lock(false)) + + assertCmd(t, []string{ + "taint", + "-no-color", + "-lock-timeout=200s", + "-state=teststate", + "-lock=false", + "-allow-missing", + "aws_instance.foo", + }, nil, taintCmd) + }) +} diff --git a/tfexec/untaint.go b/tfexec/untaint.go new file mode 100644 index 00000000..bda12727 --- /dev/null +++ b/tfexec/untaint.go @@ -0,0 +1,78 @@ +package tfexec + +import ( + "context" + "fmt" + "os/exec" + "strconv" +) + +type untaintConfig struct { + state string + allowMissing bool + lock bool + lockTimeout string +} + +var defaultUntaintOptions = untaintConfig{ + allowMissing: false, + lock: true, +} + +// OutputOption represents options used in the Output method. +type UntaintOption interface { + configureUntaint(*untaintConfig) +} + +func (opt *StateOption) configureUntaint(conf *untaintConfig) { + conf.state = opt.path +} + +func (opt *AllowMissingOption) configureUntaint(conf *untaintConfig) { + conf.allowMissing = opt.allowMissing +} + +func (opt *LockOption) configureUntaint(conf *untaintConfig) { + conf.lock = opt.lock +} + +func (opt *LockTimeoutOption) configureUntaint(conf *untaintConfig) { + conf.lockTimeout = opt.timeout +} + +// Untaint represents the terraform untaint subcommand. +func (tf *Terraform) Untaint(ctx context.Context, address string, opts ...UntaintOption) error { + err := tf.compatible(ctx, tf0_6_13, nil) + if err != nil { + return fmt.Errorf("untaint was first introduced in Terraform 0.6.13: %w", err) + } + untaintCmd := tf.untaintCmd(ctx, address, opts...) + return tf.runTerraformCmd(ctx, untaintCmd) +} + +func (tf *Terraform) untaintCmd(ctx context.Context, address string, opts ...UntaintOption) *exec.Cmd { + c := defaultUntaintOptions + + for _, o := range opts { + o.configureUntaint(&c) + } + + args := []string{"untaint", "-no-color"} + + if c.lockTimeout != "" { + args = append(args, "-lock-timeout="+c.lockTimeout) + } + + // string opts: only pass if set + if c.state != "" { + args = append(args, "-state="+c.state) + } + + args = append(args, "-lock="+strconv.FormatBool(c.lock)) + if c.allowMissing { + args = append(args, "-allow-missing") + } + args = append(args, address) + + return tf.buildTerraformCmd(ctx, nil, args...) +} diff --git a/tfexec/untaint_test.go b/tfexec/untaint_test.go new file mode 100644 index 00000000..99075f45 --- /dev/null +++ b/tfexec/untaint_test.go @@ -0,0 +1,49 @@ +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestUntaintCmd(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) { + untaintCmd := tf.untaintCmd(context.Background(), "aws_instance.foo") + + assertCmd(t, []string{ + "untaint", + "-no-color", + "-lock=true", + "aws_instance.foo", + }, nil, untaintCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + untaintCmd := tf.untaintCmd(context.Background(), "aws_instance.foo", + State("teststate"), + AllowMissing(true), + LockTimeout("200s"), + Lock(false)) + + assertCmd(t, []string{ + "untaint", + "-no-color", + "-lock-timeout=200s", + "-state=teststate", + "-lock=false", + "-allow-missing", + "aws_instance.foo", + }, nil, untaintCmd) + }) +} diff --git a/tfexec/version.go b/tfexec/version.go index f42fbfc3..2c60ea43 100644 --- a/tfexec/version.go +++ b/tfexec/version.go @@ -14,6 +14,8 @@ import ( ) var ( + tf0_4_1 = version.Must(version.NewVersion("0.4.1")) + tf0_6_13 = version.Must(version.NewVersion("0.6.13")) tf0_7_7 = version.Must(version.NewVersion("0.7.7")) tf0_10_0 = version.Must(version.NewVersion("0.10.0")) tf0_12_0 = version.Must(version.NewVersion("0.12.0"))