From 5192c09625b315b28fab91f3a3369c586d8cff76 Mon Sep 17 00:00:00 2001 From: David Dollar Date: Tue, 28 Jan 2020 13:43:12 -0500 Subject: [PATCH] cli: allow terraform options on rack install/update (#85) --- pkg/cli/helpers.go | 70 +++++++++-- pkg/cli/rack.go | 44 ++++--- pkg/cli/rack_test.go | 109 +++++++++++++++++- pkg/cli/testdata/terraform/dev1.args.tf | 16 +++ .../{terraform.local.tf => terraform/dev1.tf} | 2 +- pkg/cli/testdata/terraform/dev2.args.tf | 16 +++ pkg/cli/testdata/terraform/dev2.update.tf | 16 +++ 7 files changed, 246 insertions(+), 27 deletions(-) create mode 100644 pkg/cli/testdata/terraform/dev1.args.tf rename pkg/cli/testdata/{terraform.local.tf => terraform/dev1.tf} (90%) create mode 100644 pkg/cli/testdata/terraform/dev2.args.tf create mode 100644 pkg/cli/testdata/terraform/dev2.update.tf diff --git a/pkg/cli/helpers.go b/pkg/cli/helpers.go index 380903e..f521cdf 100644 --- a/pkg/cli/helpers.go +++ b/pkg/cli/helpers.go @@ -404,7 +404,6 @@ func switchRack(c *stdcli.Context, name string) error { func tag(name, value string) string { return fmt.Sprintf("<%s>%s", name, value, name) } - func terraform(c *stdcli.Context, dir string, env map[string]string, args ...string) error { wd, err := os.Getwd() if err != nil { @@ -439,7 +438,45 @@ func terraformEnv(provider string) (map[string]string, error) { } } -func terraformVars(provider string) (map[string]string, error) { +func terraformOptionVars(dir string, args []string) (map[string]string, error) { + vars := map[string]string{} + + vf := filepath.Join(dir, "vars.json") + + if _, err := os.Stat(vf); !os.IsNotExist(err) { + data, err := ioutil.ReadFile(vf) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(data, &vars); err != nil { + return nil, err + } + } + + for _, arg := range args { + parts := strings.Split(arg, "=") + k := strings.TrimSpace(parts[0]) + if v := strings.TrimSpace(parts[1]); v != "" { + vars[k] = v + } else { + delete(vars, k) + } + } + + data, err := json.MarshalIndent(vars, "", " ") + if err != nil { + return nil, err + } + + if err := ioutil.WriteFile(vf, data, 0600); err != nil { + return nil, err + } + + return vars, nil +} + +func terraformProviderVars(provider string) (map[string]string, error) { switch provider { case "do": env, err := requireEnv("DIGITALOCEAN_ACCESS_ID", "DIGITALOCEAN_SECRET_KEY", "DIGITALOCEAN_TOKEN") @@ -449,11 +486,28 @@ func terraformVars(provider string) (map[string]string, error) { vars := map[string]string{ "access_id": env["DIGITALOCEAN_ACCESS_ID"], "secret_key": env["DIGITALOCEAN_SECRET_KEY"], + "release": "", "token": env["DIGITALOCEAN_TOKEN"], } return vars, nil default: - return map[string]string{}, nil + vars := map[string]string{ + "release": "", + } + return vars, nil + } +} + +func terraformTemplateHelpers() template.FuncMap { + return template.FuncMap{ + "keys": func(h map[string]string) []string { + ks := []string{} + for k := range h { + ks = append(ks, k) + } + sort.Strings(ks) + return ks + }, } } @@ -464,14 +518,14 @@ func terraformWriteTemplate(filename string, params map[string]interface{}) erro params["Source"] = fmt.Sprintf("github.com/convox/convox//terraform/system/%s", params["Provider"]) } - t, err := template.New("main").Parse(` + t, err := template.New("main").Funcs(terraformTemplateHelpers()).Parse(` module "system" { source = "{{.Source}}" - name = "{{.Name}}" - release = "{{.Release}}" - {{- range $k, $v := .Vars }} - {{$k}} = "{{$v}}" + name = "{{.Name}}" + + {{- range (keys .Vars) }} + {{.}} = "{{index $.Vars .}}" {{- end }} } diff --git a/pkg/cli/rack.go b/pkg/cli/rack.go index 1a6aa46..4410f50 100644 --- a/pkg/cli/rack.go +++ b/pkg/cli/rack.go @@ -22,8 +22,8 @@ func init() { }) registerWithoutProvider("rack install", "install a new rack", RackInstall, stdcli.CommandOptions{ - Usage: " ", - Validate: stdcli.Args(2), + Usage: " [option=value]...", + Validate: stdcli.ArgsMin(2), }) register("rack logs", "get logs for the rack", RackLogs, stdcli.CommandOptions{ @@ -67,8 +67,8 @@ func init() { }) registerWithoutProvider("rack update", "update a rack", RackUpdate, stdcli.CommandOptions{ - Usage: "", - Validate: stdcli.Args(1), + Usage: " [option=value]...", + Validate: stdcli.ArgsMin(1), }) } @@ -110,11 +110,6 @@ func RackInstall(rack sdk.Interface, c *stdcli.Context) error { return err } - vars, err := terraformVars(provider) - if err != nil { - return err - } - dir, err := c.SettingDirectory(fmt.Sprintf("racks/%s", name)) if err != nil { return err @@ -124,6 +119,20 @@ func RackInstall(rack sdk.Interface, c *stdcli.Context) error { return err } + vars, err := terraformProviderVars(provider) + if err != nil { + return err + } + + ov, err := terraformOptionVars(dir, c.Args[2:]) + if err != nil { + return err + } + + for k, v := range ov { + vars[k] = v + } + tf := filepath.Join(dir, "main.tf") if _, err := os.Stat(tf); !os.IsNotExist(err) { @@ -133,7 +142,6 @@ func RackInstall(rack sdk.Interface, c *stdcli.Context) error { params := map[string]interface{}{ "Name": name, "Provider": provider, - "Release": "", "Vars": vars, } @@ -367,27 +375,35 @@ func RackUpdate(rack sdk.Interface, c *stdcli.Context) error { return rackUpdateRemote(c, name) } + dir, err := c.SettingDirectory(fmt.Sprintf("racks/%s", name)) + if err != nil { + return err + } + env, err := terraformEnv(r.Provider) if err != nil { return err } - vars, err := terraformVars(r.Provider) + vars, err := terraformProviderVars(r.Provider) if err != nil { return err } - dir, err := c.SettingDirectory(fmt.Sprintf("racks/%s", name)) + ov, err := terraformOptionVars(dir, c.Args[1:]) if err != nil { return err } + for k, v := range ov { + vars[k] = v + } + tf := filepath.Join(dir, "main.tf") params := map[string]interface{}{ "Name": name, "Provider": r.Provider, - "Release": "", "Vars": vars, } @@ -403,7 +419,7 @@ func RackUpdate(rack sdk.Interface, c *stdcli.Context) error { return err } - return c.OK() + return nil } func rackUninstallRemote(c *stdcli.Context, name string) error { diff --git a/pkg/cli/rack_test.go b/pkg/cli/rack_test.go index 2549b5e..840a12b 100644 --- a/pkg/cli/rack_test.go +++ b/pkg/cli/rack_test.go @@ -73,7 +73,46 @@ func TestRackInstall(t *testing.T) { tfdata, err := ioutil.ReadFile(tf) require.NoError(t, err) - testdata, err := ioutil.ReadFile("testdata/terraform.local.tf") + testdata, err := ioutil.ReadFile("testdata/terraform/dev1.tf") + require.NoError(t, err) + + require.Equal(t, strings.Trim(string(tfdata), "\n"), strings.Trim(string(testdata), "\n")) + + res, err = testExecute(e, "switch", nil) + require.NoError(t, err) + require.Equal(t, 0, res.Code) + res.RequireStderr(t, []string{""}) + res.RequireStdout(t, []string{"dev1"}) + + me.AssertExpectations(t) + }) +} + +func TestRackInstallArgs(t *testing.T) { + testClientWait(t, 50*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) { + me := &mockstdcli.Executor{} + me.On("Terminal", "terraform", "init").Return(nil) + me.On("Terminal", "terraform", "apply", "-auto-approve").Return(nil) + e.Executor = me + + res, err := testExecute(e, "rack install local dev1 foo=bar baz=qux", nil) + require.NoError(t, err) + require.Equal(t, 0, res.Code) + res.RequireStderr(t, []string{""}) + res.RequireStdout(t, []string{""}) + + dir := filepath.Join(e.Settings, "racks", "dev1") + tf := filepath.Join(dir, "main.tf") + + _, err = os.Stat(dir) + require.NoError(t, err) + _, err = os.Stat(tf) + require.NoError(t, err) + + tfdata, err := ioutil.ReadFile(tf) + require.NoError(t, err) + + testdata, err := ioutil.ReadFile("testdata/terraform/dev1.args.tf") require.NoError(t, err) require.Equal(t, strings.Trim(string(tfdata), "\n"), strings.Trim(string(testdata), "\n")) @@ -417,9 +456,71 @@ func TestRackUpdate(t *testing.T) { require.NoError(t, err) require.Equal(t, 0, res.Code) res.RequireStderr(t, []string{""}) - res.RequireStdout(t, []string{ - "OK", - }) + res.RequireStdout(t, []string{""}) + + dir := filepath.Join(e.Settings, "racks", "dev1") + tf := filepath.Join(dir, "main.tf") + + _, err = os.Stat(dir) + require.NoError(t, err) + _, err = os.Stat(tf) + require.NoError(t, err) + + tfdata, err := ioutil.ReadFile(tf) + require.NoError(t, err) + + testdata, err := ioutil.ReadFile("testdata/terraform/dev1.tf") + require.NoError(t, err) + + require.Equal(t, strings.Trim(string(tfdata), "\n"), strings.Trim(string(testdata), "\n")) + + me.AssertExpectations(t) + }) +} + +func TestRackUpdateArgs(t *testing.T) { + testClientWait(t, 50*time.Millisecond, func(e *cli.Engine, i *mocksdk.Interface) { + require.NoError(t, testLocalRack(e, "dev1", "local", "https://host1")) + + me := e.Executor.(*mockstdcli.Executor) + + me.On("Terminal", "terraform", "init").Return(nil).Once() + me.On("Terminal", "terraform", "apply", "-auto-approve").Return(nil).Once() + + res, err := testExecute(e, "rack install local dev2 foo=bar baz=qux", nil) + require.NoError(t, err) + require.Equal(t, 0, res.Code) + res.RequireStderr(t, []string{""}) + res.RequireStdout(t, []string{""}) + + tf := filepath.Join(e.Settings, "racks", "dev2", "main.tf") + + fmt.Printf("tf: %+v\n", tf) + + tfdata, err := ioutil.ReadFile(tf) + require.NoError(t, err) + + testdata, err := ioutil.ReadFile("testdata/terraform/dev2.args.tf") + require.NoError(t, err) + + require.Equal(t, strings.Trim(string(testdata), "\n"), strings.Trim(string(tfdata), "\n")) + + me.On("Terminal", "terraform", "init", "-upgrade").Return(nil).Once() + me.On("Terminal", "terraform", "apply", "-auto-approve").Return(nil).Once() + + res, err = testExecute(e, "rack update dev2 foo= other=side", nil) + require.NoError(t, err) + require.Equal(t, 0, res.Code) + res.RequireStderr(t, []string{""}) + res.RequireStdout(t, []string{""}) + + tfdata2, err := ioutil.ReadFile(tf) + require.NoError(t, err) + + testdata2, err := ioutil.ReadFile("testdata/terraform/dev2.update.tf") + require.NoError(t, err) + + require.Equal(t, strings.Trim(string(tfdata2), "\n"), strings.Trim(string(testdata2), "\n")) me.AssertExpectations(t) }) diff --git a/pkg/cli/testdata/terraform/dev1.args.tf b/pkg/cli/testdata/terraform/dev1.args.tf new file mode 100644 index 0000000..6c8b9df --- /dev/null +++ b/pkg/cli/testdata/terraform/dev1.args.tf @@ -0,0 +1,16 @@ + module "system" { + source = "github.com/convox/convox//terraform/system/local" + + name = "dev1" + baz = "qux" + foo = "bar" + release = "" + } + + output "api" { + value = module.system.api + } + + output "provider" { + value = "local" + } diff --git a/pkg/cli/testdata/terraform.local.tf b/pkg/cli/testdata/terraform/dev1.tf similarity index 90% rename from pkg/cli/testdata/terraform.local.tf rename to pkg/cli/testdata/terraform/dev1.tf index 4fa8124..f5c2970 100644 --- a/pkg/cli/testdata/terraform.local.tf +++ b/pkg/cli/testdata/terraform/dev1.tf @@ -1,7 +1,7 @@ module "system" { source = "github.com/convox/convox//terraform/system/local" - name = "dev1" + name = "dev1" release = "" } diff --git a/pkg/cli/testdata/terraform/dev2.args.tf b/pkg/cli/testdata/terraform/dev2.args.tf new file mode 100644 index 0000000..ee548eb --- /dev/null +++ b/pkg/cli/testdata/terraform/dev2.args.tf @@ -0,0 +1,16 @@ + module "system" { + source = "github.com/convox/convox//terraform/system/local" + + name = "dev2" + baz = "qux" + foo = "bar" + release = "" + } + + output "api" { + value = module.system.api + } + + output "provider" { + value = "local" + } diff --git a/pkg/cli/testdata/terraform/dev2.update.tf b/pkg/cli/testdata/terraform/dev2.update.tf new file mode 100644 index 0000000..29f415d --- /dev/null +++ b/pkg/cli/testdata/terraform/dev2.update.tf @@ -0,0 +1,16 @@ + module "system" { + source = "github.com/convox/convox//terraform/system/local" + + name = "dev2" + baz = "qux" + other = "side" + release = "" + } + + output "api" { + value = module.system.api + } + + output "provider" { + value = "local" + }