From d3703fd85517e3acdf26621e62e50d5e8ff2b375 Mon Sep 17 00:00:00 2001 From: micbar Date: Sat, 3 Oct 2026 12:24:35 +0200 Subject: [PATCH] feat: make space2share migration repeatable --- opencloud/pkg/command/shares.go | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/opencloud/pkg/command/shares.go b/opencloud/pkg/command/shares.go index 1aae8f8c66..46667bee13 100644 --- a/opencloud/pkg/command/shares.go +++ b/opencloud/pkg/command/shares.go @@ -3,6 +3,9 @@ package command import ( "context" "errors" + "fmt" + "slices" + "strings" "time" "github.com/spf13/viper" @@ -16,6 +19,7 @@ import ( sharingparser "github.com/opencloud-eu/opencloud/services/sharing/pkg/config/parser" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3" + migration "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/migrations" "github.com/opencloud-eu/reva/v2/pkg/share/manager/registry" "github.com/opencloud-eu/reva/v2/pkg/utils" @@ -31,6 +35,10 @@ import ( // this executions is not returned this timeout is needed const cleanupTimeout = 1 * time.Minute +// migrationTimeout bounds a forced migration run. Importing the grants of many +// project spaces is not fast, so this is generous. +const migrationTimeout = 30 * time.Minute + // SharesCommand is the entrypoint for the groups command. func SharesCommand(cfg *config.Config) *cobra.Command { sharesCmd := &cobra.Command{ @@ -48,6 +56,7 @@ func SharesCommand(cfg *config.Config) *cobra.Command { }, } sharesCmd.AddCommand(cleanupCmd(cfg)) + sharesCmd.AddCommand(migrateCmd(cfg)) return sharesCmd } @@ -139,6 +148,93 @@ func cleanup(_ *cobra.Command, cfg *config.Config) error { return nil } +const migrationWaitTimeout = 30 * time.Minute + +func migrateCmd(cfg *config.Config) *cobra.Command { + migrateCmd := &cobra.Command{ + Use: "migrate [migration]", + Args: func(cmd *cobra.Command, args []string) error { + if list, _ := cmd.Flags().GetBool("list"); list { + return nil + } + if len(args) != 1 { + return fmt.Errorf("name the migration to run, one of: %s", strings.Join(migration.Names(), ", ")) + } + // reject an unknown name before touching the configuration + if !slices.Contains(migration.Names(), args[0]) { + return fmt.Errorf("unknown migration %q, one of: %s", args[0], strings.Join(migration.Names(), ", ")) + } + return nil + }, + Short: `re-run share manager migrations that have already been applied.`, + Long: `Runs a share manager migration by name, whether or not it has been applied +before. On startup a migration runs only once; this runs the one you name. + +The case this exists for is import_space_members: it reads the grants of every +project space from the storage provider and imports them into the share +manager. Space memberships that were written as storage grants by something +other than OpenCloud are invisible until that has run. The import is additive, +so running it again is safe. + +Use --list to see the available migrations.`, + PreRunE: func(cmd *cobra.Command, args []string) error { + // listing the migrations needs no server configuration + if list, _ := cmd.Flags().GetBool("list"); list { + return nil + } + if err := parser.ParseConfig(cfg, true); err != nil { + return configlog.ReturnError(err) + } + + cfg.Sharing.Commons = cfg.Commons + return configlog.ReturnError(sharingparser.ParseConfig(cfg.Sharing)) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return migrate(cmd, cfg, args) + }, + } + + migrateCmd.Flags().BoolP("list", "l", false, "list the available migrations and exit") + _ = viper.BindPFlag("list", migrateCmd.Flags().Lookup("list")) + + return migrateCmd +} + +func migrate(_ *cobra.Command, cfg *config.Config, args []string) error { + if viper.GetBool("list") { + for _, name := range migration.Names() { + fmt.Println(name) + } + return nil + } + + driver := cfg.Sharing.UserSharingDriver + if driver != "jsoncs3" { + return configlog.ReturnError(errors.New("migrate is only implemented for the jsoncs3 share manager")) + } + + l := logger("migrate") + zerolog.SetGlobalLevel(zerolog.InfoLevel) + + // Initialize registry to make service lookup work + _ = mregistry.GetRegistry() + + rcfg := revaShareConfig(cfg.Sharing) + f, ok := registry.NewFuncs[driver] + if !ok { + return configlog.ReturnError(errors.New("Unknown share manager type '" + driver + "'")) + } + mgr, err := f(rcfg[driver].(map[string]any), &l) + if err != nil { + return configlog.ReturnError(err) + } + + ctx, cancel := context.WithTimeout(l.WithContext(context.Background()), migrationTimeout) + defer cancel() + + return configlog.ReturnError(mgr.(*jsoncs3.Manager).RunMigration(ctx, args[0])) +} + func revaShareConfig(cfg *sharing.Config) map[string]any { return map[string]any{ "json": map[string]any{