diff --git a/go.mod b/go.mod index df15aa5..7dd6f4b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/rs/zerolog v1.32.0 + github.com/stretchr/testify v1.9.0 github.com/thegeeklab/wp-plugin-go/v2 v2.3.1 github.com/urfave/cli/v2 v2.27.2 golang.org/x/sys v0.20.0 @@ -14,6 +15,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect @@ -22,10 +24,12 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 46f5585..41f134d 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= diff --git a/plugin/impl.go b/plugin/impl.go index 8e709a0..462ab5f 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -8,7 +8,9 @@ import ( "io" "os" + "github.com/thegeeklab/wp-opentofu/tofu" "github.com/thegeeklab/wp-plugin-go/v2/trace" + "github.com/thegeeklab/wp-plugin-go/v2/types" ) var ( @@ -43,21 +45,21 @@ func (p *Plugin) run(ctx context.Context) error { func (p *Plugin) FlagsFromContext() error { if p.Context.String("init-option") != "" { - initOptions := InitOptions{} + initOptions := tofu.InitOptions{} if err := json.Unmarshal([]byte(p.Context.String("init-option")), &initOptions); err != nil { return fmt.Errorf("cannot unmarshal init_option: %w", err) } - p.Settings.InitOptions = initOptions + p.Settings.Tofu.InitOptions = initOptions } if p.Context.String("fmt-option") != "" { - fmtOptions := FmtOptions{} + fmtOptions := tofu.FmtOptions{} if err := json.Unmarshal([]byte(p.Context.String("fmt-option")), &fmtOptions); err != nil { return fmt.Errorf("cannot unmarshal fmt_option: %w", err) } - p.Settings.FmtOptions = fmtOptions + p.Settings.Tofu.FmtOptions = fmtOptions } return nil @@ -70,9 +72,9 @@ func (p *Plugin) Validate() error { p.Settings.DataDir = value } - p.Settings.OutFile = "plan.tfout" + p.Settings.Tofu.OutFile = "plan.tfout" if p.Settings.DataDir == ".terraform" { - p.Settings.OutFile = fmt.Sprintf("%s.plan.tfout", p.Settings.DataDir) + p.Settings.Tofu.OutFile = fmt.Sprintf("%s.plan.tfout", p.Settings.DataDir) } return nil @@ -80,8 +82,8 @@ func (p *Plugin) Validate() error { // Execute provides the implementation of the plugin. func (p *Plugin) Execute() error { - batchCmd := make([]*Cmd, 0) - batchCmd = append(batchCmd, p.versionCommand()) + batchCmd := make([]*types.Cmd, 0) + batchCmd = append(batchCmd, p.Settings.Tofu.Version()) if p.Settings.TofuVersion != "" { err := installPackage(p.Plugin.Network.Context, p.Plugin.Network.Client, p.Settings.TofuVersion, maxDecompressionSize) @@ -90,29 +92,29 @@ func (p *Plugin) Execute() error { } } - batchCmd = append(batchCmd, p.initCommand()) - batchCmd = append(batchCmd, p.getModulesCommand()) + batchCmd = append(batchCmd, p.Settings.Tofu.Init()) + batchCmd = append(batchCmd, p.Settings.Tofu.GetModules()) for _, action := range p.Settings.Action.Value() { switch action { case "fmt": - batchCmd = append(batchCmd, p.fmtCommand()) + batchCmd = append(batchCmd, p.Settings.Tofu.Fmt()) case "validate": - batchCmd = append(batchCmd, p.validateCommand()) + batchCmd = append(batchCmd, p.Settings.Tofu.Validate()) case "plan": - batchCmd = append(batchCmd, p.planCommand(false)) + batchCmd = append(batchCmd, p.Settings.Tofu.Plan(false)) case "plan-destroy": - batchCmd = append(batchCmd, p.planCommand(true)) + batchCmd = append(batchCmd, p.Settings.Tofu.Plan(true)) case "apply": - batchCmd = append(batchCmd, p.applyCommand()) + batchCmd = append(batchCmd, p.Settings.Tofu.Apply()) case "destroy": - batchCmd = append(batchCmd, p.destroyCommand()) + batchCmd = append(batchCmd, p.Settings.Tofu.Destroy()) default: return fmt.Errorf("%w: %s", ErrActionUnknown, action) } } - if err := deleteDir(p.Settings.DataDir); err != nil { + if err := os.RemoveAll(p.Settings.DataDir); err != nil { return err } @@ -136,5 +138,5 @@ func (p *Plugin) Execute() error { } } - return deleteDir(p.Settings.DataDir) + return os.RemoveAll(p.Settings.DataDir) } diff --git a/plugin/plugin.go b/plugin/plugin.go index a1e8758..ba09e0d 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -3,9 +3,9 @@ package plugin import ( "fmt" + "github.com/thegeeklab/wp-opentofu/tofu" wp "github.com/thegeeklab/wp-plugin-go/v2/plugin" "github.com/urfave/cli/v2" - "golang.org/x/sys/execabs" ) //go:generate go run ../internal/docs/main.go -output=../docs/data/data-raw.yaml @@ -18,39 +18,11 @@ type Plugin struct { // Settings for the Plugin. type Settings struct { - Action cli.StringSlice - - TofuVersion string - InitOptions InitOptions - FmtOptions FmtOptions - + Action cli.StringSlice RootDir string DataDir string - OutFile string - Parallelism int - Targets cli.StringSlice - Refresh bool - NoLog bool -} - -// InitOptions include options for the OpenTofu init command. -type InitOptions struct { - BackendConfig []string `json:"backend-config"` - Lock *bool `json:"lock"` - LockTimeout string `json:"lock-timeout"` -} - -// FmtOptions fmt options for the OpenTofu fmt command. -type FmtOptions struct { - List *bool `json:"list"` - Write *bool `json:"write"` - Diff *bool `json:"diff"` - Check *bool `json:"check"` -} - -type Cmd struct { - *execabs.Cmd - Private bool + TofuVersion string + Tofu tofu.Tofu } func New(e wp.ExecuteFunc, build ...string) *Plugin { @@ -123,14 +95,14 @@ func Flags(settings *Settings, category string) []cli.Flag { Name: "no-log", Usage: "suppress tofu command output for `plan`, `apply` and `destroy` action", EnvVars: []string{"PLUGIN_NO_LOG"}, - Destination: &settings.NoLog, + Destination: &settings.Tofu.NoLog, Category: category, }, &cli.StringSliceFlag{ Name: "targets", Usage: "targets to run `plan` or `apply` action on", EnvVars: []string{"PLUGIN_TARGETS"}, - Destination: &settings.Targets, + Destination: &settings.Tofu.Targets, Category: category, }, &cli.StringFlag{ @@ -144,7 +116,7 @@ func Flags(settings *Settings, category string) []cli.Flag { Name: "refresh", Usage: "enables refreshing of the state before `plan` and `apply` commands", EnvVars: []string{"PLUGIN_REFRESH"}, - Destination: &settings.Refresh, + Destination: &settings.Tofu.Refresh, Value: true, Category: category, }, diff --git a/plugin/tofu.go b/plugin/tofu.go deleted file mode 100644 index e4c2e0c..0000000 --- a/plugin/tofu.go +++ /dev/null @@ -1,172 +0,0 @@ -package plugin - -import ( - "fmt" - - "golang.org/x/sys/execabs" -) - -const ( - tofuBin = "/usr/local/bin/tofu" -) - -func (p *Plugin) versionCommand() *Cmd { - return &Cmd{ - Cmd: execabs.Command(tofuBin, "version"), - Private: p.Settings.NoLog, - } -} - -func (p *Plugin) initCommand() *Cmd { - args := []string{ - "init", - } - - for _, v := range p.Settings.InitOptions.BackendConfig { - args = append(args, fmt.Sprintf("-backend-config=%s", v)) - } - - // Fail tofu execution on prompt - args = append(args, "-input=false") - - return &Cmd{ - Cmd: execabs.Command(tofuBin, args...), - } -} - -func (p *Plugin) getModulesCommand() *Cmd { - return &Cmd{ - Cmd: execabs.Command(tofuBin, "get"), - } -} - -func (p *Plugin) validateCommand() *Cmd { - return &Cmd{ - Cmd: execabs.Command(tofuBin, "validate"), - } -} - -func (p *Plugin) fmtCommand() *Cmd { - args := []string{ - "fmt", - } - - if p.Settings.FmtOptions.List != nil { - args = append(args, fmt.Sprintf("-list=%t", *p.Settings.FmtOptions.List)) - } - - if p.Settings.FmtOptions.Write != nil { - args = append(args, fmt.Sprintf("-write=%t", *p.Settings.FmtOptions.Write)) - } - - if p.Settings.FmtOptions.Diff != nil { - args = append(args, fmt.Sprintf("-diff=%t", *p.Settings.FmtOptions.Diff)) - } - - if p.Settings.FmtOptions.Check != nil { - args = append(args, fmt.Sprintf("-check=%t", *p.Settings.FmtOptions.Check)) - } - - return &Cmd{ - Cmd: execabs.Command(tofuBin, args...), - } -} - -func (p *Plugin) planCommand(destroy bool) *Cmd { - args := []string{ - "plan", - } - - if destroy { - args = append(args, "-destroy") - } else { - args = append(args, fmt.Sprintf("-out=%s", p.Settings.OutFile)) - } - - for _, value := range p.Settings.Targets.Value() { - args = append(args, "--target", value) - } - - if p.Settings.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", p.Settings.Parallelism)) - } - - if p.Settings.InitOptions.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *p.Settings.InitOptions.Lock)) - } - - if p.Settings.InitOptions.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", p.Settings.InitOptions.LockTimeout)) - } - - if !p.Settings.Refresh { - args = append(args, "-refresh=false") - } - - return &Cmd{ - Cmd: execabs.Command(tofuBin, args...), - Private: p.Settings.NoLog, - } -} - -func (p *Plugin) applyCommand() *Cmd { - args := []string{ - "apply", - } - - for _, v := range p.Settings.Targets.Value() { - args = append(args, "--target", v) - } - - if p.Settings.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", p.Settings.Parallelism)) - } - - if p.Settings.InitOptions.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *p.Settings.InitOptions.Lock)) - } - - if p.Settings.InitOptions.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", p.Settings.InitOptions.LockTimeout)) - } - - if !p.Settings.Refresh { - args = append(args, "-refresh=false") - } - - args = append(args, p.Settings.OutFile) - - return &Cmd{ - Cmd: execabs.Command(tofuBin, args...), - Private: p.Settings.NoLog, - } -} - -func (p *Plugin) destroyCommand() *Cmd { - args := []string{ - "destroy", - } - - for _, v := range p.Settings.Targets.Value() { - args = append(args, fmt.Sprintf("-target=%s", v)) - } - - if p.Settings.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", p.Settings.Parallelism)) - } - - if p.Settings.InitOptions.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *p.Settings.InitOptions.Lock)) - } - - if p.Settings.InitOptions.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", p.Settings.InitOptions.LockTimeout)) - } - - args = append(args, "-auto-approve") - - return &Cmd{ - Cmd: execabs.Command(tofuBin, args...), - Private: p.Settings.NoLog, - } -} diff --git a/plugin/utils.go b/plugin/utils.go index eb694fe..2689996 100644 --- a/plugin/utils.go +++ b/plugin/utils.go @@ -13,6 +13,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/rs/zerolog/log" + "github.com/thegeeklab/wp-opentofu/tofu" ) func installPackage(ctx context.Context, client *http.Client, version string, maxSize int64) error { @@ -34,7 +35,7 @@ func installPackage(ctx context.Context, client *http.Client, version string, ma } defer func() { - _ = deleteDir(tmpdir) + _ = os.RemoveAll(tmpdir) }() log.Debug(). @@ -51,7 +52,7 @@ func installPackage(ctx context.Context, client *http.Client, version string, ma return fmt.Errorf("failed to unzip: %w", err) } - if err := os.Rename(filepath.Join(tmpdir, "tofu"), tofuBin); err != nil { + if err := os.Rename(filepath.Join(tmpdir, "tofu"), tofu.TofuBin); err != nil { return fmt.Errorf("failed to rename: %w", err) } @@ -169,7 +170,3 @@ func sanitizeArchivePath(d, t string) (string, error) { return "", fmt.Errorf("%w: %v", ErrTaintedPath, t) } - -func deleteDir(path string) error { - return os.RemoveAll(path) -} diff --git a/tofu/tofu.go b/tofu/tofu.go new file mode 100644 index 0000000..91732f4 --- /dev/null +++ b/tofu/tofu.go @@ -0,0 +1,200 @@ +package tofu + +import ( + "fmt" + + "github.com/thegeeklab/wp-plugin-go/v2/types" + "github.com/urfave/cli/v2" + "golang.org/x/sys/execabs" +) + +const TofuBin = "/usr/local/bin/tofu" + +type Tofu struct { + InitOptions InitOptions + FmtOptions FmtOptions + + OutFile string + Parallelism int + Targets cli.StringSlice + Refresh bool + NoLog bool +} + +// InitOptions include options for the OpenTofu init command. +type InitOptions struct { + BackendConfig []string `json:"backend-config"` + Lock *bool `json:"lock"` + LockTimeout string `json:"lock-timeout"` +} + +// FmtOptions fmt options for the OpenTofu fmt command. +type FmtOptions struct { + List *bool `json:"list"` + Write *bool `json:"write"` + Diff *bool `json:"diff"` + Check *bool `json:"check"` +} + +func (t *Tofu) Version() *types.Cmd { + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, "version"), + Private: t.NoLog, + } +} + +func (t *Tofu) Init() *types.Cmd { + args := []string{ + "init", + } + + for _, v := range t.InitOptions.BackendConfig { + args = append(args, fmt.Sprintf("-backend-config=%s", v)) + } + + // Fail tofu execution on prompt + args = append(args, "-input=false") + + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, args...), + } +} + +func (t *Tofu) GetModules() *types.Cmd { + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, "get"), + } +} + +func (t *Tofu) Validate() *types.Cmd { + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, "validate"), + } +} + +func (t *Tofu) Fmt() *types.Cmd { + args := []string{ + "fmt", + } + + if t.FmtOptions.List != nil { + args = append(args, fmt.Sprintf("-list=%t", *t.FmtOptions.List)) + } + + if t.FmtOptions.Write != nil { + args = append(args, fmt.Sprintf("-write=%t", *t.FmtOptions.Write)) + } + + if t.FmtOptions.Diff != nil { + args = append(args, fmt.Sprintf("-diff=%t", *t.FmtOptions.Diff)) + } + + if t.FmtOptions.Check != nil { + args = append(args, fmt.Sprintf("-check=%t", *t.FmtOptions.Check)) + } + + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, args...), + } +} + +func (t *Tofu) Plan(destroy bool) *types.Cmd { + args := []string{ + "plan", + } + + if destroy { + args = append(args, "-destroy") + } else if t.OutFile != "" { + args = append(args, fmt.Sprintf("-out=%s", t.OutFile)) + } + + for _, value := range t.Targets.Value() { + args = append(args, "--target", value) + } + + if t.Parallelism > 0 { + args = append(args, fmt.Sprintf("-parallelism=%d", t.Parallelism)) + } + + if t.InitOptions.Lock != nil { + args = append(args, fmt.Sprintf("-lock=%t", *t.InitOptions.Lock)) + } + + if t.InitOptions.LockTimeout != "" { + args = append(args, fmt.Sprintf("-lock-timeout=%s", t.InitOptions.LockTimeout)) + } + + if !t.Refresh { + args = append(args, "-refresh=false") + } + + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, args...), + Private: t.NoLog, + } +} + +func (t *Tofu) Apply() *types.Cmd { + args := []string{ + "apply", + } + + for _, v := range t.Targets.Value() { + args = append(args, "--target", v) + } + + if t.Parallelism > 0 { + args = append(args, fmt.Sprintf("-parallelism=%d", t.Parallelism)) + } + + if t.InitOptions.Lock != nil { + args = append(args, fmt.Sprintf("-lock=%t", *t.InitOptions.Lock)) + } + + if t.InitOptions.LockTimeout != "" { + args = append(args, fmt.Sprintf("-lock-timeout=%s", t.InitOptions.LockTimeout)) + } + + if !t.Refresh { + args = append(args, "-refresh=false") + } + + if t.OutFile != "" { + args = append(args, t.OutFile) + } + + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, args...), + Private: t.NoLog, + } +} + +func (t *Tofu) Destroy() *types.Cmd { + args := []string{ + "destroy", + } + + for _, v := range t.Targets.Value() { + args = append(args, fmt.Sprintf("-target=%s", v)) + } + + if t.Parallelism > 0 { + args = append(args, fmt.Sprintf("-parallelism=%d", t.Parallelism)) + } + + if t.InitOptions.Lock != nil { + args = append(args, fmt.Sprintf("-lock=%t", *t.InitOptions.Lock)) + } + + if t.InitOptions.LockTimeout != "" { + args = append(args, fmt.Sprintf("-lock-timeout=%s", t.InitOptions.LockTimeout)) + } + + args = append(args, "-auto-approve") + + return &types.Cmd{ + Cmd: execabs.Command(TofuBin, args...), + Private: t.NoLog, + } +} diff --git a/tofu/tofu_test.go b/tofu/tofu_test.go new file mode 100644 index 0000000..c8bdbd7 --- /dev/null +++ b/tofu/tofu_test.go @@ -0,0 +1,545 @@ +package tofu + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +func boolPtr(b bool) *bool { + return &b +} + +func TestTofu_Version(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "test version command", + tofu: &Tofu{}, + want: []string{TofuBin, "version"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Version() + assert.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} + +func TestTofu_Init(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "init with no backend config", + tofu: &Tofu{}, + want: []string{ + TofuBin, + "init", + "-input=false", + }, + }, + { + name: "init with single backend config", + tofu: &Tofu{ + InitOptions: InitOptions{ + BackendConfig: []string{"key=value"}, + }, + }, + want: []string{ + TofuBin, + "init", + "-backend-config=key=value", + "-input=false", + }, + }, + { + name: "init with multiple backend configs", + tofu: &Tofu{ + InitOptions: InitOptions{ + BackendConfig: []string{"key1=value1", "key2=value2"}, + }, + }, + want: []string{ + TofuBin, + "init", + "-backend-config=key1=value1", + "-backend-config=key2=value2", + "-input=false", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Init() + assert.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} + +func TestTofu_GetModules(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "get modules command", + tofu: &Tofu{}, + want: []string{TofuBin, "get"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.GetModules() + assert.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} + +func TestTofu_Validate(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "validate command", + tofu: &Tofu{}, + want: []string{TofuBin, "validate"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Validate() + assert.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} + +func TestTofu_Fmt(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "fmt with no options", + tofu: &Tofu{}, + want: []string{ + TofuBin, + "fmt", + }, + }, + { + name: "fmt with list option", + tofu: &Tofu{ + FmtOptions: FmtOptions{ + List: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "fmt", + "-list=true", + }, + }, + { + name: "fmt with write option", + tofu: &Tofu{ + FmtOptions: FmtOptions{ + Write: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "fmt", + "-write=true", + }, + }, + { + name: "fmt with diff option", + tofu: &Tofu{ + FmtOptions: FmtOptions{ + Diff: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "fmt", + "-diff=true", + }, + }, + { + name: "fmt with check option", + tofu: &Tofu{ + FmtOptions: FmtOptions{ + Check: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "fmt", + "-check=true", + }, + }, + { + name: "fmt with multiple options", + tofu: &Tofu{ + FmtOptions: FmtOptions{ + List: boolPtr(true), + Write: boolPtr(true), + Diff: boolPtr(true), + Check: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "fmt", + "-list=true", + "-write=true", + "-diff=true", + "-check=true", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Fmt() + assert.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} + +func TestTofu_Plan(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + destroy bool + want []string + }{ + { + name: "plan with no options", + tofu: &Tofu{}, + destroy: false, + want: []string{ + TofuBin, + "plan", + "-refresh=false", + }, + }, + { + name: "plan with output options", + tofu: &Tofu{ + OutFile: "plan.tfout", + }, + destroy: false, + want: []string{ + TofuBin, + "plan", + "-out=plan.tfout", + "-refresh=false", + }, + }, + { + name: "plan with destroy option", + tofu: &Tofu{}, + destroy: true, + want: []string{ + TofuBin, + "plan", + "-destroy", + "-refresh=false", + }, + }, + { + name: "plan with targets", + tofu: &Tofu{ + Targets: *cli.NewStringSlice("target1", "target2"), + }, + destroy: false, + want: []string{ + TofuBin, + "plan", + "--target", "target1", + "--target", "target2", + "-refresh=false", + }, + }, + { + name: "plan with parallelism", + tofu: &Tofu{ + Parallelism: 10, + }, + destroy: false, + want: []string{ + TofuBin, + "plan", + "-parallelism=10", + "-refresh=false", + }, + }, + { + name: "plan with lock option", + tofu: &Tofu{ + InitOptions: InitOptions{ + Lock: boolPtr(true), + }, + }, + destroy: false, + want: []string{ + TofuBin, + "plan", + "-lock=true", + "-refresh=false", + }, + }, + { + name: "plan with lock timeout", + tofu: &Tofu{ + InitOptions: InitOptions{ + LockTimeout: "10s", + }, + }, + destroy: false, + want: []string{ + TofuBin, + "plan", + "-lock-timeout=10s", + "-refresh=false", + }, + }, + { + name: "plan with refresh option", + tofu: &Tofu{ + Refresh: true, + }, + destroy: false, + want: []string{ + TofuBin, + "plan", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Plan(tt.destroy) + assert.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} + +func TestTofu_Apply(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "apply with no options", + tofu: &Tofu{}, + want: []string{ + TofuBin, + "apply", + "-refresh=false", + }, + }, + { + name: "apply with targets", + tofu: &Tofu{ + Targets: *cli.NewStringSlice("target1", "target2"), + }, + want: []string{ + TofuBin, + "apply", + "--target", "target1", + "--target", "target2", + "-refresh=false", + }, + }, + { + name: "apply with parallelism", + tofu: &Tofu{ + Parallelism: 10, + }, + want: []string{ + TofuBin, + "apply", + "-parallelism=10", + "-refresh=false", + }, + }, + { + name: "apply with lock option", + tofu: &Tofu{ + InitOptions: InitOptions{ + Lock: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "apply", + "-lock=true", + "-refresh=false", + }, + }, + { + name: "apply with lock timeout", + tofu: &Tofu{ + InitOptions: InitOptions{ + LockTimeout: "10s", + }, + }, + want: []string{ + TofuBin, + "apply", + "-lock-timeout=10s", + "-refresh=false", + }, + }, + { + name: "apply with refresh option", + tofu: &Tofu{ + Refresh: true, + }, + want: []string{ + TofuBin, + "apply", + }, + }, + { + name: "apply with output file", + tofu: &Tofu{ + OutFile: "out.tfout", + }, + want: []string{ + TofuBin, + "apply", + "-refresh=false", + "out.tfout", + }, + }, + { + name: "apply with no log", + tofu: &Tofu{ + NoLog: true, + }, + want: []string{ + TofuBin, + "apply", + "-refresh=false", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Apply() + assert.Equal(t, tt.want, cmd.Cmd.Args) + assert.Equal(t, tt.tofu.NoLog, cmd.Private) + }) + } +} + +func TestTofu_Destroy(t *testing.T) { + tests := []struct { + name string + tofu *Tofu + want []string + }{ + { + name: "destroy with no options", + tofu: &Tofu{}, + want: []string{ + TofuBin, + "destroy", + "-auto-approve", + }, + }, + { + name: "destroy with targets", + tofu: &Tofu{ + Targets: *cli.NewStringSlice("target1", "target2"), + }, + want: []string{ + TofuBin, + "destroy", + "-target=target1", + "-target=target2", + "-auto-approve", + }, + }, + { + name: "destroy with parallelism", + tofu: &Tofu{ + Parallelism: 10, + }, + want: []string{ + TofuBin, + "destroy", + "-parallelism=10", + "-auto-approve", + }, + }, + { + name: "destroy with lock option", + tofu: &Tofu{ + InitOptions: InitOptions{ + Lock: boolPtr(true), + }, + }, + want: []string{ + TofuBin, + "destroy", + "-lock=true", + "-auto-approve", + }, + }, + { + name: "destroy with lock timeout", + tofu: &Tofu{ + InitOptions: InitOptions{ + LockTimeout: "10s", + }, + }, + want: []string{ + TofuBin, + "destroy", + "-lock-timeout=10s", + "-auto-approve", + }, + }, + { + name: "destroy with no log", + tofu: &Tofu{ + NoLog: true, + }, + want: []string{ + TofuBin, + "destroy", + "-auto-approve", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.tofu.Destroy() + assert.Equal(t, tt.want, cmd.Cmd.Args) + assert.Equal(t, tt.tofu.NoLog, cmd.Private) + }) + } +}