diff --git a/command/list.go b/command/list.go new file mode 100644 index 0000000..708d391 --- /dev/null +++ b/command/list.go @@ -0,0 +1,103 @@ +package command + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/minamijoyo/tfmigrate/config" + "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", "all", "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 + 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 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 + } + + var migrations []string + switch status { + case "all": + migrations = hc.Migrations() + + case "unapplied": + migrations = hc.UnappliedMigrations() + + default: + return "", fmt.Errorf("unknown filter for status: %s", status) + } + + 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 + Valid values are as follows: + - all (default) + - unapplied +` + return strings.TrimSpace(helpText) +} + +// Synopsis returns one-line help text. +func (c *ListCommand) Synopsis() string { + return "List migrations" +} 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) + } + }) + } +} 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 { 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