Skip to content

Commit

Permalink
feat: add migrate sql up|down|status
Browse files Browse the repository at this point in the history
This patch adds the ability to execute down migrations using:

```
hydra migrate sql down -e --steps {num_of_steps}
```

Please read `hydra migrate sql down --help` carefully.

Going forward, please use the following commands

```
hydra migrate sql up ...
hydra migrate sql status ...
```

instead of the previous, now deprecated

```
hydra migrate sql ...
hydra migrate status ...
```

commands.

See ory-corp/cloud#7350
  • Loading branch information
aeneasr committed Nov 27, 2024
1 parent fa21711 commit 67d2dee
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 194 deletions.
90 changes: 8 additions & 82 deletions cmd/cli/handler_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ package cli

import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/ory/x/popx"
"github.com/ory/x/servicelocatorx"
Expand All @@ -22,8 +20,6 @@ import (

"github.com/ory/x/configx"

"github.com/ory/x/errorsx"

"github.com/ory/x/cmdx"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -317,96 +313,26 @@ func (h *MigrateHandler) makePersister(cmd *cobra.Command, args []string) (p per
return d.Persister(), nil
}

func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) (err error) {
func (h *MigrateHandler) MigrateSQLUp(cmd *cobra.Command, args []string) (err error) {
p, err := h.makePersister(cmd, args)
if err != nil {
return err
}
conn := p.Connection(context.Background())
if conn == nil {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Migrations can only be executed against a SQL-compatible driver but DSN is not a SQL source.")
return cmdx.FailSilently(cmd)
}

if err := conn.Open(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not open the database connection:\n%+v\n", err)
return cmdx.FailSilently(cmd)
}

// convert migration tables
if err := p.PrepareMigration(context.Background()); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not convert the migration table:\n%+v\n", err)
return cmdx.FailSilently(cmd)
}

// print migration status
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "The following migration is planned:")
return popx.MigrateSQLUp(cmd, p)
}

status, err := p.MigrationStatus(context.Background())
func (h *MigrateHandler) MigrateSQLDown(cmd *cobra.Command, args []string) (err error) {
p, err := h.makePersister(cmd, args)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the migration status:\n%+v\n", errorsx.WithStack(err))
return cmdx.FailSilently(cmd)
}
_ = status.Write(os.Stdout)

if !flagx.MustGetBool(cmd, "yes") {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "To skip the next question use flag --yes (at your own risk).")
if !cmdx.AskForConfirmation("Do you wish to execute this migration plan?", nil, nil) {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.")
return nil
}
}

// apply migrations
if err := p.MigrateUp(context.Background()); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations:\n%+v\n", errorsx.WithStack(err))
return cmdx.FailSilently(cmd)
return err
}

_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied migrations!")
return nil
return popx.MigrateSQLDown(cmd, p)
}

func (h *MigrateHandler) MigrateStatus(cmd *cobra.Command, args []string) error {
p, err := h.makePersister(cmd, args)
if err != nil {
return err
}
conn := p.Connection(context.Background())
if conn == nil {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Migrations can only be checked against a SQL-compatible driver but DSN is not a SQL source.")
return cmdx.FailSilently(cmd)
}

if err := conn.Open(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not open the database connection:\n%+v\n", err)
return cmdx.FailSilently(cmd)
}

block := flagx.MustGetBool(cmd, "block")
ctx := cmd.Context()
s, err := p.MigrationStatus(ctx)
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err)
return cmdx.FailSilently(cmd)
}

for block && s.HasPending() {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Waiting for migrations to finish...\n")
for _, m := range s {
if m.State == popx.Pending {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", m.Name)
}
}
time.Sleep(time.Second)
s, err = p.MigrationStatus(ctx)
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err)
return cmdx.FailSilently(cmd)
}
}

cmdx.PrintTable(cmd, s)
return nil

return popx.MigrateStatus(cmd, p)
}
4 changes: 2 additions & 2 deletions cmd/janitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import (

func NewJanitorCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
cmd := &cobra.Command{
Use: "janitor [<database-url>]",
Use: "janitor [[database_url]]",
Short: "This command cleans up stale database rows.",
Example: `hydra janitor --keep-if-younger 23h --access-lifespan 1h --refresh-lifespan 40h --consent-request-lifespan 10m <database-url>`,
Example: `hydra janitor --keep-if-younger 23h --access-lifespan 1h --refresh-lifespan 40h --consent-request-lifespan 10m [database_url]`,
Long: `This command cleans up stale database rows. This will select records to delete with a limit
and delete records in batch to ensure that no table locking issues arise in big production
databases.
Expand Down
20 changes: 12 additions & 8 deletions cmd/migrate_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ package cmd
import (
"github.com/spf13/cobra"

"github.com/ory/hydra/v2/cmd/cli"
"github.com/ory/hydra/v2/driver"
"github.com/ory/x/configx"
"github.com/ory/x/servicelocatorx"

"github.com/ory/hydra/v2/cmd/cli"
)

func NewMigrateSqlCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
func NewMigrateSQLCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
cmd := &cobra.Command{
Use: "sql <database-url>",
Short: "Create SQL schemas and apply migration plans",
Use: "sql [database_url]",
Deprecated: "Please use `hydra migrate sql up` instead.",
Short: "Perform SQL migrations",
Long: `Run this command on a fresh SQL installation and when you upgrade Hydra to a new minor version. For example,
upgrading Hydra 0.7.0 to 0.8.0 requires running this command.
Expand All @@ -25,16 +25,20 @@ This decreases risk of failure and decreases time required.
You can read in the database URL using the -e flag, for example:
export DSN=...
hydra migrate sql -e
hydra migrate sql up -e
### WARNING ###
Before running this command on an existing database, create a back up!`,
RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQL,
RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQLUp,
}

cmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")
cmd.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.")
cmd.PersistentFlags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")

cmd.AddCommand(NewMigrateSQLDownCmd(slOpts, dOpts, cOpts))
cmd.AddCommand(NewMigrateSQLUpCmd(slOpts, dOpts, cOpts))
cmd.AddCommand(NewMigrateSQLStatusCmd(slOpts, dOpts, cOpts))

return cmd
}
18 changes: 18 additions & 0 deletions cmd/migrate_sql_down.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"github.com/spf13/cobra"

"github.com/ory/hydra/v2/cmd/cli"
"github.com/ory/hydra/v2/driver"
"github.com/ory/x/configx"
"github.com/ory/x/popx"
"github.com/ory/x/servicelocatorx"
)

func NewMigrateSQLDownCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
return popx.NewMigrateSQLDownCmd("hydra", cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQLDown)
}
18 changes: 18 additions & 0 deletions cmd/migrate_sql_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"github.com/spf13/cobra"

"github.com/ory/hydra/v2/cmd/cli"
"github.com/ory/hydra/v2/driver"
"github.com/ory/x/configx"
"github.com/ory/x/popx"
"github.com/ory/x/servicelocatorx"
)

func NewMigrateSQLStatusCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
return popx.NewMigrateSQLStatusCmd("hydra", cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateStatus)
}
18 changes: 18 additions & 0 deletions cmd/migrate_sql_up.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"github.com/spf13/cobra"

"github.com/ory/hydra/v2/cmd/cli"
"github.com/ory/hydra/v2/driver"
"github.com/ory/x/configx"
"github.com/ory/x/popx"
"github.com/ory/x/servicelocatorx"
)

func NewMigrateSQLUpCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
return popx.NewMigrateSQLDownCmd("hydra", cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateSQLUp)
}
19 changes: 8 additions & 11 deletions cmd/migrate_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
package cmd

import (
"github.com/ory/x/cmdx"
"github.com/ory/x/configx"
"github.com/ory/x/popx"
"github.com/ory/x/servicelocatorx"

"github.com/spf13/cobra"
Expand All @@ -15,15 +15,12 @@ import (
)

func NewMigrateStatusCmd(slOpts []servicelocatorx.Option, dOpts []driver.OptionsModifier, cOpts []configx.OptionModifier) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Get the current migration status",
RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateStatus,
}

cmdx.RegisterFormatFlags(cmd.PersistentFlags())
cmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")
cmd.Flags().Bool("block", false, "Block until all migrations have been applied")

cmd := popx.RegisterMigrateStatusFlags(&cobra.Command{
Use: "status",
Deprecated: "Please use `hydra migrate sql status` instead.",
Short: "Get the current migration status",
RunE: cli.NewHandler(slOpts, dOpts, cOpts).Migration.MigrateStatus,
})
cmd.PersistentFlags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.")
return cmd
}
13 changes: 9 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"os"

"github.com/pkg/errors"

"github.com/ory/x/cmdx"

"github.com/ory/hydra/v2/driver"
Expand Down Expand Up @@ -72,7 +74,7 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op

migrateCmd := NewMigrateCmd()
migrateCmd.AddCommand(NewMigrateGenCmd())
migrateCmd.AddCommand(NewMigrateSqlCmd(slOpts, dOpts, cOpts))
migrateCmd.AddCommand(NewMigrateSQLCmd(slOpts, dOpts, cOpts))
migrateCmd.AddCommand(NewMigrateStatusCmd(slOpts, dOpts, cOpts))

serveCmd := NewServeCmd()
Expand All @@ -99,8 +101,11 @@ func RegisterCommandRecursive(parent *cobra.Command, slOpts []servicelocatorx.Op

// Execute adds all child commands to the root command sets flags appropriately.
func Execute() {
if err := NewRootCmd(nil, nil, nil).Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
c := NewRootCmd(nil, nil, nil)
if err := c.Execute(); err != nil {
if !errors.Is(err, cmdx.ErrNoPrintButFail) {
_, _ = fmt.Fprintln(c.ErrOrStderr(), err)
}
os.Exit(1)
}
}
3 changes: 3 additions & 0 deletions driver/registry_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ func (m *RegistrySQL) PingContext(ctx context.Context) error {
func (m *RegistrySQL) Ping() error {
return m.PingContext(context.Background())
}
func (m *RegistrySQL) PingContext(ctx context.Context) error {

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run end-to-end tests (cockroach)

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23

Check failure on line 257 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

method RegistrySQL.PingContext already declared at driver/registry_sql.go:250:23
return m.Persister().PingContext(ctx)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run HSM tests

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run end-to-end tests (cockroach)

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)) (typecheck)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)) (typecheck)

Check failure on line 258 in driver/registry_sql.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

m.Persister().PingContext undefined (type persistence.Persister has no field or method PingContext)) (typecheck)
}

func (m *RegistrySQL) ClientManager() client.Manager {
return m.Persister()
Expand Down
Loading

0 comments on commit 67d2dee

Please sign in to comment.