diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-11-30 17:59:58 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-11-30 17:59:58 +0300 |
commit | c023d0aaa1691096ddd1bd782af428ba5dfb19c8 (patch) | |
tree | 1f2132edd3a47f8991db4c63be2852997f988843 /cmd | |
parent | 9a53844de5035a693534d17715dcb77b95f7bf16 (diff) | |
parent | 64ad803a11f85abbf055114f795c8fd1bdfe0e08 (diff) |
Merge branch 'smh-generalize-dataloss' into 'master'
Add 'praefect metadata' subcommand
Closes #3481
See merge request gitlab-org/gitaly!4122
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/praefect/subcmd.go | 1 | ||||
-rw-r--r-- | cmd/praefect/subcmd_metadata.go | 107 | ||||
-rw-r--r-- | cmd/praefect/subcmd_metadata_test.go | 125 |
3 files changed, 233 insertions, 0 deletions
diff --git a/cmd/praefect/subcmd.go b/cmd/praefect/subcmd.go index 2e966e809..af1502f31 100644 --- a/cmd/praefect/subcmd.go +++ b/cmd/praefect/subcmd.go @@ -48,6 +48,7 @@ var subcommands = map[string]subcmd{ praefect.NewGitalyNodeConnectivityCheck, praefect.NewPostgresReadWriteCheck, ), + metadataCmdName: newMetadataSubcommand(os.Stdout), } // subCommand returns an exit code, to be fed into os.Exit. diff --git a/cmd/praefect/subcmd_metadata.go b/cmd/praefect/subcmd_metadata.go new file mode 100644 index 000000000..e913a0eec --- /dev/null +++ b/cmd/praefect/subcmd_metadata.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + + "gitlab.com/gitlab-org/gitaly/v14/internal/praefect/config" + "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" +) + +const metadataCmdName = "metadata" + +type metadataSubcommand struct { + stdout io.Writer + repositoryID int64 + virtualStorage string + relativePath string +} + +func newMetadataSubcommand(stdout io.Writer) *metadataSubcommand { + return &metadataSubcommand{stdout: stdout} +} + +func (cmd *metadataSubcommand) FlagSet() *flag.FlagSet { + fs := flag.NewFlagSet(metadataCmdName, flag.ContinueOnError) + fs.Int64Var(&cmd.repositoryID, "repository-id", 0, "the repository's ID") + fs.StringVar(&cmd.virtualStorage, "virtual-storage", "", "the repository's virtual storage") + fs.StringVar(&cmd.relativePath, "relative-path", "", "the repository's relative path in the virtual storage") + return fs +} + +func (cmd *metadataSubcommand) println(format string, args ...interface{}) { + fmt.Fprintf(cmd.stdout, format+"\n", args...) +} + +func (cmd *metadataSubcommand) Exec(flags *flag.FlagSet, cfg config.Config) error { + if flags.NArg() > 0 { + return unexpectedPositionalArgsError{Command: flags.Name()} + } + + var request gitalypb.GetRepositoryMetadataRequest + switch { + case cmd.repositoryID != 0: + if cmd.virtualStorage != "" || cmd.relativePath != "" { + return errors.New("virtual storage and relative path can't be provided with a repository ID") + } + request.Query = &gitalypb.GetRepositoryMetadataRequest_RepositoryId{RepositoryId: cmd.repositoryID} + case cmd.virtualStorage != "" || cmd.relativePath != "": + if cmd.virtualStorage == "" { + return errors.New("virtual storage is required with relative path") + } else if cmd.relativePath == "" { + return errors.New("relative path is required with virtual storage") + } + request.Query = &gitalypb.GetRepositoryMetadataRequest_Path_{ + Path: &gitalypb.GetRepositoryMetadataRequest_Path{ + VirtualStorage: cmd.virtualStorage, + RelativePath: cmd.relativePath, + }, + } + default: + return errors.New("repository id or virtual storage and relative path required") + } + + nodeAddr, err := getNodeAddress(cfg) + if err != nil { + return fmt.Errorf("get node address: %w", err) + } + + ctx := context.TODO() + conn, err := subCmdDial(ctx, nodeAddr, cfg.Auth.Token, defaultDialTimeout) + if err != nil { + return fmt.Errorf("dial: %w", err) + } + defer conn.Close() + + metadata, err := gitalypb.NewPraefectInfoServiceClient(conn).GetRepositoryMetadata(ctx, &request) + if err != nil { + return fmt.Errorf("get metadata: %w", err) + } + + cmd.println("Repository ID: %d", metadata.RepositoryId) + cmd.println("Virtual Storage: %q", metadata.VirtualStorage) + cmd.println("Relative Path: %q", metadata.RelativePath) + cmd.println("Replica Path: %q", metadata.ReplicaPath) + cmd.println("Primary: %q", metadata.Primary) + cmd.println("Generation: %d", metadata.Generation) + cmd.println("Replicas:") + for _, replica := range metadata.Replicas { + cmd.println("- Storage: %q", replica.Storage) + cmd.println(" Assigned: %v", replica.Assigned) + + generationText := fmt.Sprintf("%d, fully up to date", replica.Generation) + if replica.Generation == -1 { + generationText = "replica not yet created" + } else if replica.Generation < metadata.Generation { + generationText = fmt.Sprintf("%d, behind by %d changes", replica.Generation, metadata.Generation-replica.Generation) + } + + cmd.println(" Generation: %s", generationText) + cmd.println(" Healthy: %v", replica.Healthy) + cmd.println(" Valid Primary: %v", replica.ValidPrimary) + } + return nil +} diff --git a/cmd/praefect/subcmd_metadata_test.go b/cmd/praefect/subcmd_metadata_test.go new file mode 100644 index 000000000..d0bba3ec4 --- /dev/null +++ b/cmd/praefect/subcmd_metadata_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v14/internal/praefect/config" + "gitlab.com/gitlab-org/gitaly/v14/internal/praefect/datastore" + "gitlab.com/gitlab-org/gitaly/v14/internal/praefect/datastore/glsql" + "gitlab.com/gitlab-org/gitaly/v14/internal/praefect/service/info" + "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testassert" + "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testdb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestMetadataSubcommand(t *testing.T) { + t.Parallel() + + ctx, cancel := testhelper.Context() + defer cancel() + + tx := glsql.NewDB(t).Begin(t) + defer tx.Rollback(t) + + testdb.SetHealthyNodes(t, ctx, tx, map[string]map[string][]string{ + "praefect": {"virtual-storage": {"primary", "secondary-1"}}, + }) + + rs := datastore.NewPostgresRepositoryStore(tx, map[string][]string{ + "virtual-storage": {"primary", "secondary-1", "secondary-2"}, + }) + require.NoError(t, rs.CreateRepository(ctx, 1, "virtual-storage", "relative-path", "replica-path", "primary", []string{"secondary-1"}, []string{"secondary-2"}, true, true)) + require.NoError(t, rs.IncrementGeneration(ctx, 1, "primary", nil)) + + ln, clean := listenAndServe(t, []svcRegistrar{ + registerPraefectInfoServer(info.NewServer(config.Config{}, rs, nil, nil, nil)), + }) + defer clean() + + for _, tc := range []struct { + desc string + args []string + error error + }{ + { + desc: "missing parameters fails", + error: errors.New("repository id or virtual storage and relative path required"), + }, + { + desc: "repository id with virtual storage fails", + args: []string{"-repository-id=1", "-virtual-storage=virtual-storage"}, + error: errors.New("virtual storage and relative path can't be provided with a repository ID"), + }, + { + desc: "repository id with relative path fails", + args: []string{"-repository-id=1", "-relative-path=relative-path"}, + error: errors.New("virtual storage and relative path can't be provided with a repository ID"), + }, + { + desc: "virtual storage without relative path fails", + args: []string{"-virtual-storage=virtual-storage"}, + error: errors.New("relative path is required with virtual storage"), + }, + { + desc: "relative path without virtual storage fails", + args: []string{"-relative-path=relative-path"}, + error: errors.New("virtual storage is required with relative path"), + }, + { + desc: "repository not found", + args: []string{"-repository-id=2"}, + error: fmt.Errorf("get metadata: %w", status.Error(codes.NotFound, "repository not found")), + }, + { + desc: "repository found with repository id", + args: []string{"-repository-id=1"}, + }, + { + desc: "repository found with virtual storage and relative path", + args: []string{"-virtual-storage=virtual-storage", "-relative-path=relative-path"}, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + stdout := &bytes.Buffer{} + cmd := newMetadataSubcommand(stdout) + + fs := cmd.FlagSet() + require.NoError(t, fs.Parse(tc.args)) + err := cmd.Exec(fs, config.Config{SocketPath: ln.Addr().String()}) + testassert.GrpcEqualErr(t, tc.error, err) + if tc.error != nil { + return + } + + require.Equal(t, `Repository ID: 1 +Virtual Storage: "virtual-storage" +Relative Path: "relative-path" +Replica Path: "replica-path" +Primary: "primary" +Generation: 1 +Replicas: +- Storage: "primary" + Assigned: true + Generation: 1, fully up to date + Healthy: true + Valid Primary: true +- Storage: "secondary-1" + Assigned: true + Generation: 0, behind by 1 changes + Healthy: true + Valid Primary: false +- Storage: "secondary-2" + Assigned: true + Generation: replica not yet created + Healthy: false + Valid Primary: false +`, stdout.String()) + }) + } +} |