diff --git a/go.mod b/go.mod index e455ebd..4886719 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/cloudrecipes/packagejson v1.0.0 github.com/digitalocean/godo v1.131.0 github.com/foomo/posh v0.8.2 + github.com/golang-migrate/migrate/v4 v4.18.1 github.com/google/go-github/v47 v47.1.0 github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 @@ -40,7 +41,9 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect @@ -73,7 +76,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect golang.org/x/text v0.20.0 // indirect diff --git a/go.sum b/go.sum index de836f0..62633f1 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= +github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= @@ -70,10 +72,15 @@ github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -145,8 +152,8 @@ github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaK github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -216,8 +223,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/golang-migrate/migrate/README.md b/golang-migrate/migrate/README.md new file mode 100644 index 0000000..cec29e2 --- /dev/null +++ b/golang-migrate/migrate/README.md @@ -0,0 +1,52 @@ +# POSH Golang Migrate provider + +## Usage + +### Plugin + +```go +package main + +import ( + // ... + // import database drivers + _ "github.com/golang-migrate/migrate/v4/database/pgx/v5" + // import source drivers + _ "github.com/golang-migrate/migrate/v4/source/file" + // ... +) + +type Plugin struct { + l log.Logger +} + +func New(l log.Logger) (plugin.Plugin, error) { + var err error + inst := &Plugin{ + l: l, + commands: command.Commands{}, + } + + // ... + + inst.commands.Add(migrate.NewCommand(l)) + + // ... + + return inst, nil +} + +``` + +### Config + +```yaml +## Migrate +migrate: + # https://github.com/golang-migrate/migrate/blob/master/README.md#migration-sources + databases: + local: pgx5://postgres:postgres@localhost:5432/admin?sslmode=disable + # https://github.com/golang-migrate/migrate/blob/master/README.md#migration-sources + sources: + default: file://migrations +``` diff --git a/golang-migrate/migrate/command.go b/golang-migrate/migrate/command.go new file mode 100644 index 0000000..9a93172 --- /dev/null +++ b/golang-migrate/migrate/command.go @@ -0,0 +1,213 @@ +package migrate + +import ( + "context" + "strconv" + + "github.com/foomo/posh/pkg/command/tree" + "github.com/foomo/posh/pkg/log" + "github.com/foomo/posh/pkg/prompt/goprompt" + "github.com/foomo/posh/pkg/readline" + "github.com/foomo/posh/pkg/util/suggests" + "github.com/golang-migrate/migrate/v4" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +type ( + Command struct { + l log.Logger + name string + config Config + configKey string + commandTree tree.Root + } + CommandOption func(*Command) +) + +// ------------------------------------------------------------------------------------------------ +// ~ Options +// ------------------------------------------------------------------------------------------------ + +func CommandWithName(v string) CommandOption { + return func(o *Command) { + o.name = v + } +} + +func CommandWithConfigKey(v string) CommandOption { + return func(o *Command) { + o.configKey = v + } +} + +// ------------------------------------------------------------------------------------------------ +// ~ Constructor +// ------------------------------------------------------------------------------------------------ + +func NewCommand(l log.Logger, opts ...CommandOption) (*Command, error) { + inst := &Command{ + l: l.Named("migrate"), + name: "migrate", + configKey: "migrate", + } + + for _, opt := range opts { + if opt != nil { + opt(inst) + } + } + + if err := viper.UnmarshalKey(inst.configKey, &inst.config); err != nil { + return nil, err + } + + inst.commandTree = tree.New(&tree.Node{ + Name: inst.name, + Description: "Manage database migrations", + Nodes: tree.Nodes{ + { + Name: "database", + Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest { + return suggests.List(inst.config.Databases()) + }, + Nodes: tree.Nodes{ + { + Name: "source", + Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest { + return suggests.List(inst.config.Sources()) + }, + Nodes: tree.Nodes{ + { + Name: "up", + Description: "Migrate the DB to the most recent version available", + Execute: inst.execute, + }, + { + Name: "up-by-one", + Description: "Migrate the DB up by 1", + Execute: inst.execute, + }, + { + Name: "down", + Description: "Roll back the version by 1", + Execute: inst.execute, + }, + { + Name: "down-by-one", + Description: "Migrate the DB down by 1", + Execute: inst.execute, + }, + { + Name: "force", + Description: "Sets a migration version", + Args: tree.Args{ + { + Name: "version", + Description: "Version to migrate", + }, + }, + Execute: inst.execute, + }, + { + Name: "version", + Description: "Print the current version of the database", + Execute: inst.execute, + }, + { + Name: "drop", + Description: "Deletes everything in the database", + Execute: inst.execute, + }, + }, + }, + }, + }, + }, + }) + + return inst, nil +} + +// ------------------------------------------------------------------------------------------------ +// ~ Public methods +// ------------------------------------------------------------------------------------------------ + +func (c *Command) Name() string { + return c.commandTree.Node().Name +} + +func (c *Command) Description() string { + return c.commandTree.Node().Description +} + +func (c *Command) Complete(ctx context.Context, r *readline.Readline) []goprompt.Suggest { + return c.commandTree.Complete(ctx, r) +} + +func (c *Command) Execute(ctx context.Context, r *readline.Readline) error { + return c.commandTree.Execute(ctx, r) +} + +func (c *Command) Help(ctx context.Context, r *readline.Readline) string { + return c.commandTree.Help(ctx, r) +} + +// ------------------------------------------------------------------------------------------------ +// ~ Private methods +// ------------------------------------------------------------------------------------------------ + +func (c *Command) execute(ctx context.Context, r *readline.Readline) error { + database := c.config.Database(r.Args().At(0)) + source := c.config.Source(r.Args().At(1)) + + m, err := migrate.New(source, database) + if err != nil { + return err + } + + m.Log = &logger{l: c.l} + + go func() { + if <-ctx.Done(); true { + c.l.Info("triggering graceful migration shutdown") + m.GracefulStop <- true + } + }() + + defer func() { + if dErr, sErr := m.Close(); dErr != nil { + c.l.Warn(dErr) + } else if sErr != nil { + c.l.Warn(sErr) + } + }() + + switch r.Args().At(2) { + case "up": + return m.Up() + case "up-by-one": + return m.Steps(1) + case "down": + return m.Down() + case "down-by-one": + return m.Steps(-1) + case "force": + i, err := strconv.Atoi(r.Args().At(3)) + if err != nil { + return err + } + return m.Force(i) + case "drop": + return m.Drop() + case "version": + version, dirty, err := m.Version() + if err != nil { + return err + } + c.l.Infof("Version: %d, Dirty: %t", version, dirty) + return nil + default: + return errors.Errorf("unkown command: %s", r.Args().At(2)) + } +} diff --git a/golang-migrate/migrate/config.go b/golang-migrate/migrate/config.go new file mode 100644 index 0000000..1283d3e --- /dev/null +++ b/golang-migrate/migrate/config.go @@ -0,0 +1,32 @@ +package migrate + +import ( + "github.com/samber/lo" +) + +type Config struct { + SourcesMap map[string]string `json:"sources" yaml:"sources" mapstructure:"sources"` + DatabasesMap map[string]string `json:"databases" yaml:"databases" mapstructure:"databases"` +} + +func (c Config) Sources() []string { + return lo.Keys(c.SourcesMap) +} + +func (c Config) Source(name string) string { + if value, ok := c.SourcesMap[name]; ok { + return value + } + return "" +} + +func (c Config) Databases() []string { + return lo.Keys(c.DatabasesMap) +} + +func (c Config) Database(name string) string { + if value, ok := c.DatabasesMap[name]; ok { + return value + } + return "" +} diff --git a/golang-migrate/migrate/logger.go b/golang-migrate/migrate/logger.go new file mode 100644 index 0000000..1391299 --- /dev/null +++ b/golang-migrate/migrate/logger.go @@ -0,0 +1,17 @@ +package migrate + +import ( + "github.com/foomo/posh/pkg/log" +) + +type logger struct { + l log.Logger +} + +func (l *logger) Printf(format string, v ...interface{}) { + l.l.Infof(format, v...) +} + +func (l *logger) Verbose() bool { + return l.l.IsLevel(log.LevelDebug) +} diff --git a/grafana/k6/command.go b/grafana/k6/command.go index 209bfed..3bfca55 100644 --- a/grafana/k6/command.go +++ b/grafana/k6/command.go @@ -41,7 +41,7 @@ func CommandWithName(v string) CommandOption { func CommandWithConfigKey(v string) CommandOption { return func(o *Command) { - o.name = v + o.configKey = v } }