From 17c46874f4da329ae9c3730d3be238c59004c081 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 29 Nov 2021 18:31:08 +0900 Subject: [PATCH 1/4] Add list command for listing unapplied migrations --- command/list.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 5 +++ 2 files changed, 102 insertions(+) create mode 100644 command/list.go diff --git a/command/list.go b/command/list.go new file mode 100644 index 0000000..f2c256e --- /dev/null +++ b/command/list.go @@ -0,0 +1,97 @@ +package command + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/minamijoyo/tfmigrate/history" + flag "github.com/spf13/pflag" +) + +// ListCommand is a command which lists migrations. +type ListCommand struct { + Meta + status string +} + +// Run runs the procedure of this command. +func (c *ListCommand) Run(args []string) int { + cmdFlags := flag.NewFlagSet("list", flag.ContinueOnError) + cmdFlags.StringVar(&c.configFile, "config", defaultConfigFile, "A path to tfmigrate config file") + cmdFlags.StringVar(&c.status, "status", "unapplied", "A filter for migration status") + + if err := cmdFlags.Parse(args); err != nil { + c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err)) + return 1 + } + + var err error + if c.config, err = newConfig(c.configFile); err != nil { + c.UI.Error(fmt.Sprintf("failed to load config file: %s", err)) + return 1 + } + log.Printf("[DEBUG] [command] config: %#v\n", c.config) + + c.Option = newOption() + // The option may contains sensitive values such as environment variables. + // So logging the option set log level to DEBUG instead of INFO. + log.Printf("[DEBUG] [command] option: %#v\n", c.Option) + + if c.config.History == nil { + // non-history mode + c.UI.Error("no history setting") + return 1 + } + + // history mode + switch c.status { + case "unapplied": + out, err := c.listUnapplied() + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + c.UI.Output(out) + return 0 + + default: + c.UI.Error(fmt.Sprintf("unknown filter for status: %s", c.status)) + return 1 + } +} + +// listUnapplied lists unapplied migrations. +func (c *ListCommand) listUnapplied() (string, error) { + ctx := context.Background() + hc, err := history.NewController(ctx, c.config.MigrationDir, c.config.History) + if err != nil { + return "", err + } + + migrations := hc.UnappliedMigrations() + out := strings.Join(migrations, "\n") + + return out, nil +} + +// Help returns long-form help text. +func (c *ListCommand) Help() string { + helpText := ` +Usage: tfmigrate list + +List migrations. + +Options: + --config A path to tfmigrate config file + --status A filter for migration status + A valid value is only "unapplied" (default) +` + return strings.TrimSpace(helpText) +} + +// Synopsis returns one-line help text. +func (c *ListCommand) Synopsis() string { + return "List migrations" +} diff --git a/main.go b/main.go index 6167b3e..538246f 100644 --- a/main.go +++ b/main.go @@ -82,6 +82,11 @@ func initCommands(ui cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "list": func() (cli.Command, error) { + return &command.ListCommand{ + Meta: meta, + }, nil + }, } return commands From 67cb1e937defff080ed197e8fc12a2066cf5fd71 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 29 Nov 2021 22:31:22 +0900 Subject: [PATCH 2/4] Add list --status=all and set it as default --- command/list.go | 29 +++++++++++++++++++++++++++-- history/controller.go | 5 +++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/command/list.go b/command/list.go index f2c256e..9841464 100644 --- a/command/list.go +++ b/command/list.go @@ -20,7 +20,7 @@ type ListCommand struct { func (c *ListCommand) Run(args []string) int { cmdFlags := flag.NewFlagSet("list", flag.ContinueOnError) cmdFlags.StringVar(&c.configFile, "config", defaultConfigFile, "A path to tfmigrate config file") - cmdFlags.StringVar(&c.status, "status", "unapplied", "A filter for migration status") + cmdFlags.StringVar(&c.status, "status", "all", "A filter for migration status") if err := cmdFlags.Parse(args); err != nil { c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err)) @@ -47,6 +47,15 @@ func (c *ListCommand) Run(args []string) int { // history mode switch c.status { + case "all": + out, err := c.listMigrations() + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + c.UI.Output(out) + return 0 + case "unapplied": out, err := c.listUnapplied() if err != nil { @@ -62,6 +71,20 @@ func (c *ListCommand) Run(args []string) int { } } +// listMigrations lists all migrations. +func (c *ListCommand) listMigrations() (string, error) { + ctx := context.Background() + hc, err := history.NewController(ctx, c.config.MigrationDir, c.config.History) + if err != nil { + return "", err + } + + migrations := hc.Migrations() + out := strings.Join(migrations, "\n") + + return out, nil +} + // listUnapplied lists unapplied migrations. func (c *ListCommand) listUnapplied() (string, error) { ctx := context.Background() @@ -86,7 +109,9 @@ List migrations. Options: --config A path to tfmigrate config file --status A filter for migration status - A valid value is only "unapplied" (default) + Valid values are as follows: + - all (default) + - unapplied ` return strings.TrimSpace(helpText) } diff --git a/history/controller.go b/history/controller.go index 50df08c..788d08f 100644 --- a/history/controller.go +++ b/history/controller.go @@ -127,6 +127,11 @@ func (c *Controller) Save(ctx context.Context) error { return s.Write(ctx, b) } +// Migrations returns a list of all migration file names. +func (c *Controller) Migrations() []string { + return c.migrations +} + // UnappliedMigrations returns a list of migration file names which have not // been applied yet. func (c *Controller) UnappliedMigrations() []string { From 1e3e212c16840100bc15aa45443933c6426f8945 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 29 Nov 2021 22:49:23 +0900 Subject: [PATCH 3/4] Move switch into a function for easy testing --- command/list.go | 55 ++++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/command/list.go b/command/list.go index 9841464..708d391 100644 --- a/command/list.go +++ b/command/list.go @@ -6,6 +6,7 @@ import ( "log" "strings" + "github.com/minamijoyo/tfmigrate/config" "github.com/minamijoyo/tfmigrate/history" flag "github.com/spf13/pflag" ) @@ -46,56 +47,36 @@ func (c *ListCommand) Run(args []string) int { } // history mode - switch c.status { - case "all": - out, err := c.listMigrations() - if err != nil { - c.UI.Error(err.Error()) - return 1 - } - c.UI.Output(out) - return 0 - - case "unapplied": - out, err := c.listUnapplied() - if err != nil { - c.UI.Error(err.Error()) - return 1 - } - c.UI.Output(out) - return 0 - - default: - c.UI.Error(fmt.Sprintf("unknown filter for status: %s", c.status)) + ctx := context.Background() + out, err := listMigrations(ctx, c.config, c.status) + if err != nil { + c.UI.Error(err.Error()) return 1 } + c.UI.Output(out) + return 0 } -// listMigrations lists all migrations. -func (c *ListCommand) listMigrations() (string, error) { - ctx := context.Background() - hc, err := history.NewController(ctx, c.config.MigrationDir, c.config.History) +// listMigrations lists migrations. +func listMigrations(ctx context.Context, config *config.TfmigrateConfig, status string) (string, error) { + hc, err := history.NewController(ctx, config.MigrationDir, config.History) if err != nil { return "", err } - migrations := hc.Migrations() - out := strings.Join(migrations, "\n") + var migrations []string + switch status { + case "all": + migrations = hc.Migrations() - return out, nil -} + case "unapplied": + migrations = hc.UnappliedMigrations() -// listUnapplied lists unapplied migrations. -func (c *ListCommand) listUnapplied() (string, error) { - ctx := context.Background() - hc, err := history.NewController(ctx, c.config.MigrationDir, c.config.History) - if err != nil { - return "", err + default: + return "", fmt.Errorf("unknown filter for status: %s", status) } - migrations := hc.UnappliedMigrations() out := strings.Join(migrations, "\n") - return out, nil } From 98850bbe3ebf2822807b4a2be12411fbfb885ed2 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 29 Nov 2021 23:05:23 +0900 Subject: [PATCH 4/4] Add test for listMigrations --- command/list_test.go | 118 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 command/list_test.go diff --git a/command/list_test.go b/command/list_test.go new file mode 100644 index 0000000..4eea68c --- /dev/null +++ b/command/list_test.go @@ -0,0 +1,118 @@ +package command + +import ( + "context" + "testing" + + "github.com/minamijoyo/tfmigrate/config" + "github.com/minamijoyo/tfmigrate/history" +) + +func TestListMigrations(t *testing.T) { + migrations := map[string]string{ + "20201109000001_test1.hcl": ` +migration "mock" "test1" { + plan_error = false + apply_error = false +} +`, + "20201109000002_test2.hcl": ` +migration "mock" "test2" { + plan_error = false + apply_error = false +} +`, + "20201109000003_test3.hcl": ` +migration "mock" "test3" { + plan_error = false + apply_error = false +} +`, + "20201109000004_test4.hcl": ` +migration "mock" "test4" { + plan_error = false + apply_error = false +} +`, + } + historyFile := `{ + "version": 1, + "records": { + "20201109000001_test1.hcl": { + "type": "mock", + "name": "test1", + "applied_at": "2020-11-10T00:00:01Z" + }, + "20201109000002_test2.hcl": { + "type": "mock", + "name": "test2", + "applied_at": "2020-11-10T00:00:02Z" + } + } +}` + + cases := []struct { + desc string + status string + migrations map[string]string + historyFile string + want string + ok bool + }{ + { + desc: "all", + status: "all", + migrations: migrations, + historyFile: historyFile, + want: `20201109000001_test1.hcl +20201109000002_test2.hcl +20201109000003_test3.hcl +20201109000004_test4.hcl`, + ok: true, + }, + { + desc: "unapplied", + status: "unapplied", + migrations: migrations, + historyFile: historyFile, + want: `20201109000003_test3.hcl +20201109000004_test4.hcl`, + ok: true, + }, + { + desc: "unknown status", + status: "foo", + migrations: migrations, + historyFile: historyFile, + want: "", + ok: false, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + migrationDir := setupMigrationDir(t, tc.migrations) + storage := &history.MockStorageConfig{ + Data: tc.historyFile, + WriteError: false, + ReadError: false, + } + config := &config.TfmigrateConfig{ + MigrationDir: migrationDir, + History: &history.Config{ + Storage: storage, + }, + } + got, err := listMigrations(context.Background(), config, tc.status) + if tc.ok && err != nil { + t.Fatalf("unexpected err: %s", err) + } + if !tc.ok && err == nil { + t.Fatal("expected to return an error, but no error") + } + if got != tc.want { + t.Errorf("got = %#v, want = %#v", got, tc.want) + } + }) + } +}