Skip to content

Commit 00713bc

Browse files
authored
feat: migration to single table SQL schema (#707)
This change adds a migration path from Keto version v0.6.x to the new persistence structure introduced by #638. Every namespace has to be migrated separately, or you can use the CLI to detect and migrate all namespaces at once. Have a look at `keto help namespace migrate legacy` for all details. **Please make sure that you backup the database before running the migration command**. Please note that this migration might be a bit slower than usual, as we have to pull the data from the database, transcode it in Keto, and then write it to the new table structure. Versions of Keto >v0.7 will not include this migration script, so you will first have to migrate to v0.7 and move on from there.
1 parent 09ef4b3 commit 00713bc

File tree

56 files changed

+1085
-123
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1085
-123
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Run full e2e test of the migration to single table persister (see https://github.com/ory/keto/issues/628)
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- feat/persistence-migration-path
8+
9+
jobs:
10+
test-migration:
11+
runs-on: ubuntu-latest
12+
name: Test Migration
13+
steps:
14+
- uses: actions/checkout@v2
15+
- uses: actions/setup-go@v2
16+
with:
17+
go-version: '1.16'
18+
- name: Run test script
19+
run: ./scripts/single-table-migration-e2e.sh
20+
- uses: actions/upload-artifact@v2
21+
if: failure()
22+
with:
23+
name: sqlite-db
24+
path: migrate_e2e.sqlite

cmd/migrate/down.go

+8-9
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ func newDownCmd() *cobra.Command {
1919
Use: "down <steps>",
2020
Short: "Migrate the database down",
2121
Long: "Migrate the database down a specific amount of steps.\n" +
22-
"Pass 0 steps to fully migrate down.\n" +
23-
"This does not affect namespaces. Use `keto namespace migrate down` for migrating namespaces.",
22+
"Pass 0 steps to fully migrate down.",
2423
Args: cobra.ExactArgs(1),
2524
RunE: func(cmd *cobra.Command, args []string) error {
2625
steps, err := strconv.ParseInt(args[0], 0, 0)
@@ -39,7 +38,7 @@ func newDownCmd() *cobra.Command {
3938
return err
4039
}
4140

42-
return BoxDown(cmd, mb, int(steps), "")
41+
return BoxDown(cmd, mb, int(steps))
4342
},
4443
}
4544

@@ -49,27 +48,27 @@ func newDownCmd() *cobra.Command {
4948
return cmd
5049
}
5150

52-
func BoxDown(cmd *cobra.Command, mb *popx.MigrationBox, steps int, msgPrefix string) error {
51+
func BoxDown(cmd *cobra.Command, mb *popx.MigrationBox, steps int) error {
5352
s, err := mb.Status(cmd.Context())
5453
if err != nil {
55-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%sCould not get migration status: %+v\n", msgPrefix, err)
54+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err)
5655
return cmdx.FailSilently(cmd)
5756
}
5857
cmdx.PrintTable(cmd, s)
5958

60-
if !flagx.MustGetBool(cmd, FlagYes) && !cmdx.AskForConfirmation(msgPrefix+"Do you really want to migrate down? This will delete data.", cmd.InOrStdin(), cmd.OutOrStdout()) {
61-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), msgPrefix+"Migration aborted.")
59+
if !flagx.MustGetBool(cmd, FlagYes) && !cmdx.AskForConfirmation("Do you really want to migrate down? This will delete data.", cmd.InOrStdin(), cmd.OutOrStdout()) {
60+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.")
6261
return nil
6362
}
6463

6564
if err := mb.Down(cmd.Context(), steps); err != nil {
66-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%sCould apply down migrations: %+v\n", msgPrefix, err)
65+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could apply down migrations: %+v\n", err)
6766
return cmdx.FailSilently(cmd)
6867
}
6968

7069
s, err = mb.Status(cmd.Context())
7170
if err != nil {
72-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%sCould not get migration status: %+v\n", msgPrefix, err)
71+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err)
7372
return cmdx.FailSilently(cmd)
7473
}
7574
cmdx.PrintTable(cmd, s)

cmd/migrate/up.go

+21-18
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@ import (
1616
)
1717

1818
const (
19-
FlagYes = "yes"
20-
FlagAllNamespace = "all-namespaces"
19+
FlagYes = "yes"
2120
)
2221

2322
func newUpCmd() *cobra.Command {
24-
var allNamespaces bool
25-
2623
cmd := &cobra.Command{
2724
Use: "up",
2825
Short: "Migrate the database up",
29-
Long: "Migrate the database up.\n" +
30-
"This does not affect namespaces. Use `keto namespace migrate up` for migrating namespaces.",
26+
Long: `Run this command on a fresh SQL installation and when you upgrade Ory Keto from version v0.7.0 and later.
27+
28+
It is recommended to run this command close to the SQL instance (e.g. same subnet) instead of over the public internet.
29+
This decreases risk of failure and decreases time required.
30+
31+
### WARNING ###
32+
33+
Before running this command on an existing database, create a back up!
34+
`,
3135
Args: cobra.NoArgs,
3236
RunE: func(cmd *cobra.Command, _ []string) error {
3337
ctx := cmd.Context()
@@ -42,7 +46,7 @@ func newUpCmd() *cobra.Command {
4246
return err
4347
}
4448

45-
if err := BoxUp(cmd, mb, ""); err != nil {
49+
if err := BoxUp(cmd, mb); err != nil {
4650
return err
4751
}
4852

@@ -51,7 +55,6 @@ func newUpCmd() *cobra.Command {
5155
}
5256

5357
RegisterYesFlag(cmd.Flags())
54-
cmd.Flags().BoolVar(&allNamespaces, FlagAllNamespace, false, "migrate all pending namespaces as well")
5558

5659
cmdx.RegisterFormatFlags(cmd.Flags())
5760

@@ -62,38 +65,38 @@ func RegisterYesFlag(flags *pflag.FlagSet) {
6265
flags.BoolP(FlagYes, "y", false, "yes to all questions, no user input required")
6366
}
6467

65-
func BoxUp(cmd *cobra.Command, mb *popx.MigrationBox, msgPrefix string) error {
66-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), msgPrefix+"Current status:")
68+
func BoxUp(cmd *cobra.Command, mb *popx.MigrationBox) error {
69+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Current status:")
6770

6871
s, err := mb.Status(cmd.Context())
6972
if err != nil {
70-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%sCould not get migration status: %+v\n", msgPrefix, err)
73+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err)
7174
return cmdx.FailSilently(cmd)
7275
}
7376
cmdx.PrintTable(cmd, s)
7477

7578
if !s.HasPending() {
76-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), msgPrefix+"All migrations are already applied, there is nothing to do.")
79+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "All migrations are already applied, there is nothing to do.")
7780
return nil
7881
}
7982

80-
if !flagx.MustGetBool(cmd, FlagYes) && !cmdx.AskForConfirmation(msgPrefix+"Are you sure that you want to apply this migration? Make sure to check the CHANGELOG.md for breaking changes beforehand.", cmd.InOrStdin(), cmd.OutOrStdout()) {
81-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), msgPrefix+"Aborting")
83+
if !flagx.MustGetBool(cmd, FlagYes) && !cmdx.AskForConfirmation("Are you sure that you want to apply this migration? Make sure to check the CHANGELOG.md for breaking changes beforehand.", cmd.InOrStdin(), cmd.OutOrStdout()) {
84+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Aborting")
8285
return nil
8386
}
8487

85-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), msgPrefix+"Applying migrations...")
88+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Applying migrations...")
8689

8790
if err := mb.Up(cmd.Context()); err != nil {
88-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%sCould not apply migrations: %+v\n", msgPrefix, err)
91+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations: %+v\n", err)
8992
return cmdx.FailSilently(cmd)
9093
}
9194

92-
_, _ = fmt.Fprintln(cmd.OutOrStdout(), msgPrefix+"Successfully applied all migrations:")
95+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied all migrations:")
9396

9497
s, err = mb.Status(cmd.Context())
9598
if err != nil {
96-
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%sCould not get migration status: %+v\n", msgPrefix, err)
99+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err)
97100
return cmdx.FailSilently(cmd)
98101
}
99102

cmd/namespace/migrate_legacy.go

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package namespace
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ory/x/cmdx"
7+
"github.com/ory/x/flagx"
8+
"github.com/pkg/errors"
9+
"github.com/spf13/cobra"
10+
11+
"github.com/ory/keto/cmd/migrate"
12+
"github.com/ory/keto/internal/driver"
13+
"github.com/ory/keto/internal/namespace"
14+
"github.com/ory/keto/internal/persistence"
15+
"github.com/ory/keto/internal/persistence/sql/migrations"
16+
)
17+
18+
func NewMigrateLegacyCmd() *cobra.Command {
19+
downOnly := false
20+
21+
cmd := &cobra.Command{
22+
Use: "legacy [<namespace-name>]",
23+
Short: "Migrate a namespace from v0.6.x to v0.7.x and later.",
24+
Long: "Migrate a legacy namespaces from v0.6.x to the v0.7.x and later.\n" +
25+
"This step only has to be executed once.\n" +
26+
"If no namespace is specified, all legacy namespaces will be migrated.\n" +
27+
"Please ensure that namespace IDs did not change in the config file and you have a backup in case something goes wrong!",
28+
Args: cobra.MaximumNArgs(1),
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
reg, err := driver.NewDefaultRegistry(cmd.Context(), cmd.Flags(), false)
31+
if errors.Is(err, persistence.ErrNetworkMigrationsMissing) {
32+
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Migrations were not applied yet, please apply them first using `keto migrate up`.")
33+
return cmdx.FailSilently(cmd)
34+
} else if err != nil {
35+
return err
36+
}
37+
38+
migrator := migrations.NewToSingleTableMigrator(reg)
39+
40+
var nn []*namespace.Namespace
41+
if len(args) == 1 {
42+
nm, err := reg.Config().NamespaceManager()
43+
if err != nil {
44+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "There seems to be a problem with the config: %s\n", err.Error())
45+
return cmdx.FailSilently(cmd)
46+
}
47+
n, err := nm.GetNamespaceByName(cmd.Context(), args[0])
48+
if err != nil {
49+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "There seems to be a problem with the config: %s\n", err.Error())
50+
return cmdx.FailSilently(cmd)
51+
}
52+
53+
nn = []*namespace.Namespace{n}
54+
55+
if !flagx.MustGetBool(cmd, migrate.FlagYes) &&
56+
!cmdx.AskForConfirmation(
57+
fmt.Sprintf("Are you sure that you want to migrate the namespace '%s'?", args[0]),
58+
cmd.InOrStdin(), cmd.OutOrStdout()) {
59+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "OK, aborting.")
60+
return nil
61+
}
62+
} else {
63+
nn, err = migrator.LegacyNamespaces(cmd.Context())
64+
if err != nil {
65+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get legacy namespaces: %s\n", err.Error())
66+
return cmdx.FailSilently(cmd)
67+
}
68+
69+
if len(nn) == 0 {
70+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Could not find legacy namespaces, there seems nothing to be done.")
71+
return nil
72+
}
73+
74+
var names string
75+
for _, n := range nn {
76+
names += " " + n.Name + "\n"
77+
}
78+
if !flagx.MustGetBool(cmd, migrate.FlagYes) &&
79+
!cmdx.AskForConfirmation(
80+
fmt.Sprintf("I found the following legacy namespaces:\n%sDo you want to migrate all of them?", names),
81+
cmd.InOrStdin(), cmd.OutOrStdout()) {
82+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "OK, aborting.")
83+
return nil
84+
}
85+
}
86+
87+
for _, n := range nn {
88+
if !downOnly {
89+
if err := migrator.MigrateNamespace(cmd.Context(), n); err != nil {
90+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Encountered error while migrating: %s\nAborting.\n", err.Error())
91+
if errors.Is(err, migrations.ErrInvalidTuples(nil)) {
92+
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Please see https://github.com/ory/keto/issues/661 for why this happens and how to resolve this.")
93+
}
94+
return cmdx.FailSilently(cmd)
95+
}
96+
}
97+
if flagx.MustGetBool(cmd, migrate.FlagYes) ||
98+
cmdx.AskForConfirmation(
99+
fmt.Sprintf("Do you want to migrate namespace %s down? This will delete all data in the legacy table.", n.Name),
100+
cmd.InOrStdin(), cmd.OutOrStdout()) {
101+
if err := migrator.MigrateDown(cmd.Context(), n); err != nil {
102+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not migrate down: %s\n", err.Error())
103+
return cmdx.FailSilently(cmd)
104+
}
105+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Successfully migrated down namespace %s.\n", n.Name)
106+
}
107+
}
108+
109+
return nil
110+
},
111+
}
112+
113+
migrate.RegisterYesFlag(cmd.Flags())
114+
registerPackageFlags(cmd.Flags())
115+
cmd.Flags().BoolVar(&downOnly, "down-only", false, "Migrate legacy namespace(s) only down.")
116+
117+
return cmd
118+
}
76 KB
Binary file not shown.

cmd/namespace/migrate_legacy_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package namespace
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
"path"
8+
"testing"
9+
10+
"github.com/ory/keto/internal/relationtuple"
11+
12+
"github.com/ory/x/cmdx"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/ory/keto/internal/driver"
17+
"github.com/ory/keto/internal/driver/config"
18+
"github.com/ory/keto/internal/namespace"
19+
"github.com/ory/keto/internal/x/dbx"
20+
)
21+
22+
func TestMigrateLegacy(t *testing.T) {
23+
setup := func(t *testing.T) (*cmdx.CommandExecuter, *driver.RegistryDefault) {
24+
fp := path.Join(t.TempDir(), "db.sqlite")
25+
dst, err := os.Create(fp)
26+
require.NoError(t, err)
27+
defer dst.Close()
28+
src, err := os.Open("migrate_legacy_snapshot.sqlite")
29+
require.NoError(t, err)
30+
defer src.Close()
31+
_, err = io.Copy(dst, src)
32+
require.NoError(t, err)
33+
34+
reg := driver.NewTestRegistry(t, &dbx.DsnT{
35+
Name: "sqlite",
36+
Conn: "sqlite://" + fp + "?_fk=true",
37+
MigrateUp: true,
38+
MigrateDown: false,
39+
})
40+
nspaces := []*namespace.Namespace{
41+
{
42+
ID: 0,
43+
Name: "a",
44+
},
45+
{
46+
ID: 1,
47+
Name: "b",
48+
},
49+
}
50+
require.NoError(t, reg.Config().Set(config.KeyNamespaces, nspaces))
51+
52+
c := &cmdx.CommandExecuter{
53+
New: NewMigrateLegacyCmd,
54+
Ctx: context.WithValue(context.Background(), driver.RegistryContextKey, reg),
55+
}
56+
57+
return c, reg
58+
}
59+
60+
t.Run("case=invalid subject", func(t *testing.T) {
61+
c, reg := setup(t)
62+
63+
conn, err := reg.PopConnection()
64+
require.NoError(t, err)
65+
require.NoError(t, conn.RawQuery("insert into keto_0000000000_relation_tuples (shard_id, object, relation, subject, commit_time) values ('foo', 'obj', 'rel', 'invalid#subject', 'now')").Exec())
66+
67+
stdErr := c.ExecExpectedErr(t, "-y", "a")
68+
assert.Contains(t, stdErr, "found non-deserializable relationtuples")
69+
assert.Contains(t, stdErr, "invalid#subject")
70+
71+
assert.Contains(t, c.ExecNoErr(t, "-y", "--down-only", "a"), "Successfully migrated down")
72+
})
73+
74+
t.Run("case=migrates down only", func(t *testing.T) {
75+
c, reg := setup(t)
76+
77+
conn, err := reg.PopConnection()
78+
require.NoError(t, err)
79+
require.NoError(t, conn.RawQuery("insert into keto_0000000000_relation_tuples (shard_id, object, relation, subject, commit_time) values ('foo', 'obj', 'rel', 'sub', 'now')").Exec())
80+
81+
c.ExecNoErr(t, "-y", "--down-only", "a")
82+
83+
rts, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), &relationtuple.RelationQuery{
84+
Namespace: "a",
85+
Object: "obj",
86+
})
87+
require.NoError(t, err)
88+
assert.Len(t, rts, 0)
89+
})
90+
}

cmd/namespace/root.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func NewMigrateCmd() *cobra.Command {
2525
func RegisterCommandsRecursive(parent *cobra.Command) {
2626
rootCmd := NewNamespaceCmd()
2727
migrateCmd := NewMigrateCmd()
28-
migrateCmd.AddCommand(NewMigrateUpCmd(), NewMigrateDownCmd(), NewMigrateStatusCmd())
28+
migrateCmd.AddCommand(NewMigrateUpCmd(), NewMigrateDownCmd(), NewMigrateStatusCmd(), NewMigrateLegacyCmd())
2929

3030
rootCmd.AddCommand(migrateCmd, NewValidateCmd())
3131

0 commit comments

Comments
 (0)