Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-09 21:06:01 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-09 21:06:01 +0300
commit8a8571c673b641b90ae30b86ed263a7c0bd38cbe (patch)
treefc126038c67dc3c4a2b27a25d2b667d8e203bd1d
parent226cd68c45bc2054868712d72f5a2ba94f4e9bd7 (diff)
parentdfb4948bc6ff36210613e918b0c2c37c7ee336da (diff)
Automatic merge of gitlab-org/gitaly master
-rw-r--r--cmd/gitaly-ssh/auth_test.go53
-rw-r--r--cmd/gitaly-ssh/upload_pack_test.go15
-rw-r--r--internal/gitaly/server/auth_test.go7
-rw-r--r--internal/gitaly/server/server.go68
-rw-r--r--internal/gitaly/server/server_factory.go21
-rw-r--r--internal/gitaly/service/commit/check_objects_exist_test.go51
-rw-r--r--internal/gitaly/service/commit/tree_entry_test.go6
-rw-r--r--internal/gitaly/service/diff/patch_id_test.go258
-rw-r--r--internal/gitaly/service/objectpool/create_test.go5
-rw-r--r--internal/gitaly/service/operations/branches_test.go10
-rw-r--r--internal/gitaly/service/operations/cherry_pick_test.go6
-rw-r--r--internal/gitaly/service/operations/commit_files_test.go6
-rw-r--r--internal/gitaly/service/operations/merge_test.go12
-rw-r--r--internal/gitaly/service/operations/revert_test.go6
-rw-r--r--internal/gitaly/service/operations/tags_test.go6
-rw-r--r--internal/structerr/error.go12
-rw-r--r--internal/structerr/fields_producer.go26
-rw-r--r--internal/structerr/grpc_server.go70
-rw-r--r--internal/structerr/grpc_server_test.go (renamed from internal/structerr/fields_producer_test.go)53
-rw-r--r--internal/structerr/testhelper_test.go11
-rw-r--r--internal/testhelper/testserver/gitaly.go14
-rw-r--r--proto/go/gitalypb/testproto/error_metadata.pb.go159
-rw-r--r--proto/testproto/error_metadata.proto15
23 files changed, 569 insertions, 321 deletions
diff --git a/cmd/gitaly-ssh/auth_test.go b/cmd/gitaly-ssh/auth_test.go
index db33fbab3..5e6b9e6f8 100644
--- a/cmd/gitaly-ssh/auth_test.go
+++ b/cmd/gitaly-ssh/auth_test.go
@@ -4,26 +4,15 @@ package main
import (
"fmt"
- "net"
"os"
"path/filepath"
- "strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/gitaly/v15/client"
- "gitlab.com/gitlab-org/gitaly/v15/internal/backchannel"
- "gitlab.com/gitlab-org/gitaly/v15/internal/cache"
"gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest"
"gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config"
- "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/hook"
- "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/server"
- "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/service"
"gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/service/setup"
- "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/transaction"
- "gitlab.com/gitlab-org/gitaly/v15/internal/gitlab"
- "gitlab.com/gitlab-org/gitaly/v15/internal/middleware/limithandler"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testserver"
@@ -141,45 +130,3 @@ func TestConnectivity(t *testing.T) {
})
}
}
-
-func runServer(t *testing.T, secure bool, cfg config.Cfg, connectionType string, addr string) (int, func()) {
- conns := client.NewPool()
- locator := config.NewLocator(cfg)
- registry := backchannel.NewRegistry()
- txManager := transaction.NewManager(cfg, registry)
- gitCmdFactory := gittest.NewCommandFactory(t, cfg)
- hookManager := hook.NewManager(cfg, locator, gitCmdFactory, txManager, gitlab.NewMockClient(
- t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive,
- ))
- limitHandler := limithandler.New(cfg, limithandler.LimitConcurrencyByRepo, limithandler.WithConcurrencyLimiters)
- diskCache := cache.New(cfg, locator)
- srv, err := server.New(secure, cfg, testhelper.NewDiscardingLogEntry(t), registry, diskCache, []*limithandler.LimiterMiddleware{limitHandler})
- require.NoError(t, err)
- setup.RegisterAll(srv, &service.Dependencies{
- Cfg: cfg,
- GitalyHookManager: hookManager,
- TransactionManager: txManager,
- StorageLocator: locator,
- ClientPool: conns,
- GitCmdFactory: gitCmdFactory,
- })
-
- listener, err := net.Listen(connectionType, addr)
- require.NoError(t, err)
-
- go testhelper.MustServe(t, srv, listener)
-
- port := 0
- if connectionType != "unix" {
- addrSplit := strings.Split(listener.Addr().String(), ":")
- portString := addrSplit[len(addrSplit)-1]
-
- port, err = strconv.Atoi(portString)
- require.NoError(t, err)
- }
-
- return port, func() {
- conns.Close()
- srv.Stop()
- }
-}
diff --git a/cmd/gitaly-ssh/upload_pack_test.go b/cmd/gitaly-ssh/upload_pack_test.go
index 844e000b1..efcf881d5 100644
--- a/cmd/gitaly-ssh/upload_pack_test.go
+++ b/cmd/gitaly-ssh/upload_pack_test.go
@@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
+ "strings"
"testing"
"github.com/stretchr/testify/require"
@@ -12,8 +13,10 @@ import (
"gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest"
"gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo"
"gitlab.com/gitlab-org/gitaly/v15/internal/git/updateref"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/service/setup"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testserver"
"gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb"
"google.golang.org/protobuf/encoding/protojson"
)
@@ -21,19 +24,15 @@ import (
const keepAroundNamespace = "refs/keep-around"
func TestVisibilityOfHiddenRefs(t *testing.T) {
+ t.Parallel()
+
ctx := testhelper.Context(t)
cfg, repo, repoPath := testcfg.BuildWithRepo(t)
testcfg.BuildGitalySSH(t, cfg)
testcfg.BuildGitalyHooks(t, cfg)
- socketPath := testhelper.GetTemporaryGitalySocketFileName(t)
-
- _, clean := runServer(t, false, cfg, "unix", socketPath)
- defer clean()
-
- _, clean = runServer(t, false, cfg, "unix", cfg.InternalSocketPath())
- defer clean()
+ address := testserver.RunGitalyServer(t, cfg, nil, setup.RegisterAll, testserver.WithDisablePraefect())
// Create a keep-around ref
existingSha := git.ObjectID("1e292f8fedd741b75372e19097c76d327140c312")
@@ -84,7 +83,7 @@ func TestVisibilityOfHiddenRefs(t *testing.T) {
stdout := gittest.ExecOpts(t, cfg, gittest.ExecConfig{
Env: []string{
fmt.Sprintf("GITALY_PAYLOAD=%s", payload),
- fmt.Sprintf("GITALY_ADDRESS=unix:%s", socketPath),
+ fmt.Sprintf("GITALY_ADDRESS=unix:%s", strings.TrimPrefix(address, "unix://")),
fmt.Sprintf("GITALY_WD=%s", wd),
fmt.Sprintf("PATH=.:%s", os.Getenv("PATH")),
fmt.Sprintf("GIT_SSH_COMMAND=%s upload-pack", cfg.BinaryPath("gitaly-ssh")),
diff --git a/internal/gitaly/server/auth_test.go b/internal/gitaly/server/auth_test.go
index aa8e64f50..b0322c6f6 100644
--- a/internal/gitaly/server/auth_test.go
+++ b/internal/gitaly/server/auth_test.go
@@ -206,7 +206,7 @@ func runServer(t *testing.T, cfg config.Cfg) string {
limitHandler := limithandler.New(cfg, limithandler.LimitConcurrencyByRepo, limithandler.WithConcurrencyLimiters)
updaterWithHooks := updateref.NewUpdaterWithHooks(cfg, locator, hookManager, gitCmdFactory, catfileCache)
- srv, err := New(false, cfg, testhelper.NewDiscardingLogEntry(t), registry, diskCache, []*limithandler.LimiterMiddleware{limitHandler})
+ srv, err := NewGitalyServerFactory(cfg, testhelper.NewDiscardingLogEntry(t), registry, diskCache, []*limithandler.LimiterMiddleware{limitHandler}).New(false)
require.NoError(t, err)
setup.RegisterAll(srv, &service.Dependencies{
@@ -241,14 +241,13 @@ func runSecureServer(t *testing.T, cfg config.Cfg) string {
conns := client.NewPool()
t.Cleanup(func() { conns.Close() })
- srv, err := New(
- true,
+ srv, err := NewGitalyServerFactory(
cfg,
testhelper.NewDiscardingLogEntry(t),
backchannel.NewRegistry(),
cache.New(cfg, config.NewLocator(cfg)),
[]*limithandler.LimiterMiddleware{limithandler.New(cfg, limithandler.LimitConcurrencyByRepo, limithandler.WithConcurrencyLimiters)},
- )
+ ).New(true)
require.NoError(t, err)
healthpb.RegisterHealthServer(srv, health.NewServer())
diff --git a/internal/gitaly/server/server.go b/internal/gitaly/server/server.go
index fe1b87891..94f11279b 100644
--- a/internal/gitaly/server/server.go
+++ b/internal/gitaly/server/server.go
@@ -9,11 +9,8 @@ import (
grpcmwlogrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
grpcmwtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
- log "github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitaly/v15/internal/backchannel"
- diskcache "gitlab.com/gitlab-org/gitaly/v15/internal/cache"
"gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/client"
- "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config"
"gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/server/auth"
"gitlab.com/gitlab-org/gitaly/v15/internal/grpcstats"
"gitlab.com/gitlab-org/gitaly/v15/internal/helper/fieldextractors"
@@ -23,7 +20,6 @@ import (
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/cache"
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/commandstatshandler"
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/featureflag"
- "gitlab.com/gitlab-org/gitaly/v15/internal/middleware/limithandler"
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/metadatahandler"
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/panichandler"
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/sentryhandler"
@@ -53,16 +49,35 @@ func init() {
grpcmwlogrus.ReplaceGrpcLogger(gitalylog.GrpcGo())
}
+type serverConfig struct {
+ unaryInterceptors []grpc.UnaryServerInterceptor
+ streamInterceptors []grpc.StreamServerInterceptor
+}
+
+// Option is an option that can be passed to `New()`.
+type Option func(*serverConfig)
+
+// WithUnaryInterceptor adds another interceptor that shall be executed for unary RPC calls.
+func WithUnaryInterceptor(interceptor grpc.UnaryServerInterceptor) Option {
+ return func(cfg *serverConfig) {
+ cfg.unaryInterceptors = append(cfg.unaryInterceptors, interceptor)
+ }
+}
+
+// WithStreamInterceptor adds another interceptor that shall be executed for streaming RPC calls.
+func WithStreamInterceptor(interceptor grpc.StreamServerInterceptor) Option {
+ return func(cfg *serverConfig) {
+ cfg.streamInterceptors = append(cfg.streamInterceptors, interceptor)
+ }
+}
+
// New returns a GRPC server instance with a set of interceptors configured.
-// If logrusEntry is nil the default logger will be used.
-func New(
- secure bool,
- cfg config.Cfg,
- logrusEntry *log.Entry,
- registry *backchannel.Registry,
- cacheInvalidator diskcache.Invalidator,
- limitHandlers []*limithandler.LimiterMiddleware,
-) (*grpc.Server, error) {
+func (s *GitalyServerFactory) New(secure bool, opts ...Option) (*grpc.Server, error) {
+ var cfg serverConfig
+ for _, opt := range opts {
+ opt(&cfg)
+ }
+
ctxTagOpts := []grpcmwtags.Option{
grpcmwtags.WithFieldExtractorForInitialReq(fieldextractors.FieldExtractor),
}
@@ -71,7 +86,7 @@ func New(
// If tls config is specified attempt to extract tls options and use it
// as a grpc.ServerOption
if secure {
- cert, err := tls.LoadX509KeyPair(cfg.TLS.CertPath, cfg.TLS.KeyPath)
+ cert, err := tls.LoadX509KeyPair(s.cfg.TLS.CertPath, s.cfg.TLS.KeyPath)
if err != nil {
return nil, fmt.Errorf("error reading certificate and key paths: %v", err)
}
@@ -84,8 +99,8 @@ func New(
lm := listenmux.New(transportCredentials)
lm.Register(backchannel.NewServerHandshaker(
- logrusEntry,
- registry,
+ s.logger,
+ s.registry,
[]grpc.DialOption{client.UnaryInterceptor()},
))
@@ -105,7 +120,7 @@ func New(
metadatahandler.StreamInterceptor,
grpcprometheus.StreamServerInterceptor,
commandstatshandler.StreamInterceptor,
- grpcmwlogrus.StreamServerInterceptor(logrusEntry,
+ grpcmwlogrus.StreamServerInterceptor(s.logger,
grpcmwlogrus.WithTimestampFormat(gitalylog.LogTimestampFormat),
logMsgProducer,
gitalylog.DeciderOption(),
@@ -113,7 +128,7 @@ func New(
gitalylog.StreamLogDataCatcherServerInterceptor(),
sentryhandler.StreamLogHandler,
statushandler.Stream, // Should be below LogHandler
- auth.StreamServerInterceptor(cfg.Auth),
+ auth.StreamServerInterceptor(s.cfg.Auth),
}
unaryServerInterceptors := []grpc.UnaryServerInterceptor{
grpcmwtags.UnaryServerInterceptor(ctxTagOpts...),
@@ -121,7 +136,7 @@ func New(
metadatahandler.UnaryInterceptor,
grpcprometheus.UnaryServerInterceptor,
commandstatshandler.UnaryInterceptor,
- grpcmwlogrus.UnaryServerInterceptor(logrusEntry,
+ grpcmwlogrus.UnaryServerInterceptor(s.logger,
grpcmwlogrus.WithTimestampFormat(gitalylog.LogTimestampFormat),
logMsgProducer,
gitalylog.DeciderOption(),
@@ -129,30 +144,33 @@ func New(
gitalylog.UnaryLogDataCatcherServerInterceptor(),
sentryhandler.UnaryLogHandler,
statushandler.Unary, // Should be below LogHandler
- auth.UnaryServerInterceptor(cfg.Auth),
+ auth.UnaryServerInterceptor(s.cfg.Auth),
}
// Should be below auth handler to prevent v2 hmac tokens from timing out while queued
- for _, limitHandler := range limitHandlers {
+ for _, limitHandler := range s.limitHandlers {
streamServerInterceptors = append(streamServerInterceptors, limitHandler.StreamInterceptor())
unaryServerInterceptors = append(unaryServerInterceptors, limitHandler.UnaryInterceptor())
}
streamServerInterceptors = append(streamServerInterceptors,
grpctracing.StreamServerTracingInterceptor(),
- cache.StreamInvalidator(cacheInvalidator, protoregistry.GitalyProtoPreregistered),
+ cache.StreamInvalidator(s.cacheInvalidator, protoregistry.GitalyProtoPreregistered),
// Panic handler should remain last so that application panics will be
// converted to errors and logged
panichandler.StreamPanicHandler,
)
unaryServerInterceptors = append(unaryServerInterceptors,
- cache.UnaryInvalidator(cacheInvalidator, protoregistry.GitalyProtoPreregistered),
+ cache.UnaryInvalidator(s.cacheInvalidator, protoregistry.GitalyProtoPreregistered),
// Panic handler should remain last so that application panics will be
// converted to errors and logged
panichandler.UnaryPanicHandler,
)
- opts := []grpc.ServerOption{
+ streamServerInterceptors = append(streamServerInterceptors, cfg.streamInterceptors...)
+ unaryServerInterceptors = append(unaryServerInterceptors, cfg.unaryInterceptors...)
+
+ serverOptions := []grpc.ServerOption{
grpc.StatsHandler(gitalylog.PerRPCLogHandler{
Underlying: &grpcstats.PayloadBytes{},
FieldProducers: []gitalylog.FieldsProducer{grpcstats.FieldsProducer},
@@ -173,5 +191,5 @@ func New(
}),
}
- return grpc.NewServer(opts...), nil
+ return grpc.NewServer(serverOptions...), nil
}
diff --git a/internal/gitaly/server/server_factory.go b/internal/gitaly/server/server_factory.go
index 2a88ba4ba..f9de03235 100644
--- a/internal/gitaly/server/server_factory.go
+++ b/internal/gitaly/server/server_factory.go
@@ -77,15 +77,8 @@ func (s *GitalyServerFactory) GracefulStop() {
// CreateExternal creates a new external gRPC server. The external servers are closed
// before the internal servers when gracefully shutting down.
-func (s *GitalyServerFactory) CreateExternal(secure bool) (*grpc.Server, error) {
- server, err := New(
- secure,
- s.cfg,
- s.logger,
- s.registry,
- s.cacheInvalidator,
- s.limitHandlers,
- )
+func (s *GitalyServerFactory) CreateExternal(secure bool, opts ...Option) (*grpc.Server, error) {
+ server, err := s.New(secure, opts...)
if err != nil {
return nil, err
}
@@ -96,14 +89,8 @@ func (s *GitalyServerFactory) CreateExternal(secure bool) (*grpc.Server, error)
// CreateInternal creates a new internal gRPC server. Internal servers are closed
// after the external ones when gracefully shutting down.
-func (s *GitalyServerFactory) CreateInternal() (*grpc.Server, error) {
- server, err := New(
- false,
- s.cfg,
- s.logger,
- s.registry,
- s.cacheInvalidator,
- s.limitHandlers)
+func (s *GitalyServerFactory) CreateInternal(opts ...Option) (*grpc.Server, error) {
+ server, err := s.New(false, opts...)
if err != nil {
return nil, err
}
diff --git a/internal/gitaly/service/commit/check_objects_exist_test.go b/internal/gitaly/service/commit/check_objects_exist_test.go
index ab6d0383e..09d41d07b 100644
--- a/internal/gitaly/service/commit/check_objects_exist_test.go
+++ b/internal/gitaly/service/commit/check_objects_exist_test.go
@@ -7,12 +7,10 @@ import (
"io"
"testing"
- "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest"
"gitlab.com/gitlab-org/gitaly/v15/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
- "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testserver"
"gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -22,8 +20,7 @@ func TestCheckObjectsExist(t *testing.T) {
t.Parallel()
ctx := testhelper.Context(t)
- logger, hook := test.NewNullLogger()
- cfg, client := setupCommitService(t, ctx, testserver.WithLogger(logger))
+ cfg, client := setupCommitService(t, ctx)
repo, repoPath := gittest.CreateRepository(t, ctx, cfg)
@@ -38,11 +35,10 @@ func TestCheckObjectsExist(t *testing.T) {
)
for _, tc := range []struct {
- desc string
- requests []*gitalypb.CheckObjectsExistRequest
- expectedResults map[string]bool
- expectedErr error
- expectedErrorMetadata any
+ desc string
+ requests []*gitalypb.CheckObjectsExistRequest
+ expectedResults map[string]bool
+ expectedErr error
}{
{
desc: "no repository provided",
@@ -170,10 +166,8 @@ func TestCheckObjectsExist(t *testing.T) {
},
},
},
- expectedErr: structerr.NewInvalidArgument("invalid revision: revision can't start with '-'"),
- expectedErrorMetadata: map[string]any{
- "revision": "-not-a-rev",
- },
+ expectedErr: structerr.NewInvalidArgument("invalid revision: revision can't start with '-'").
+ WithInterceptedMetadata("revision", "-not-a-rev"),
},
{
desc: "input with whitespace",
@@ -185,10 +179,8 @@ func TestCheckObjectsExist(t *testing.T) {
},
},
},
- expectedErr: structerr.NewInvalidArgument("invalid revision: revision can't contain whitespace"),
- expectedErrorMetadata: map[string]any{
- "revision": fmt.Sprintf("%s\n%s", commitID1, commitID2),
- },
+ expectedErr: structerr.NewInvalidArgument("invalid revision: revision can't contain whitespace").
+ WithInterceptedMetadata("revision", fmt.Sprintf("%s\n%s", commitID1, commitID2)),
},
{
desc: "chunked invalid input",
@@ -205,15 +197,11 @@ func TestCheckObjectsExist(t *testing.T) {
},
},
},
- expectedErr: structerr.NewInvalidArgument("invalid revision: revision can't start with '-'"),
- expectedErrorMetadata: map[string]any{
- "revision": "-not-a-rev",
- },
+ expectedErr: structerr.NewInvalidArgument("invalid revision: revision can't start with '-'").
+ WithInterceptedMetadata("revision", "-not-a-rev"),
},
} {
t.Run(tc.desc, func(t *testing.T) {
- hook.Reset()
-
client, err := client.CheckObjectsExist(ctx)
require.NoError(t, err)
@@ -244,23 +232,6 @@ func TestCheckObjectsExist(t *testing.T) {
testhelper.RequireGrpcError(t, tc.expectedErr, err)
require.Equal(t, tc.expectedResults, results)
-
- if tc.expectedErrorMetadata == nil {
- tc.expectedErrorMetadata = map[string]any{}
- }
-
- metadata := map[string]any{}
- for _, entry := range hook.AllEntries() {
- errorMetadata, ok := entry.Data["error_metadata"]
- if !ok {
- continue
- }
-
- for key, value := range errorMetadata.(map[string]any) {
- metadata[key] = value
- }
- }
- require.Equal(t, tc.expectedErrorMetadata, metadata)
})
}
}
diff --git a/internal/gitaly/service/commit/tree_entry_test.go b/internal/gitaly/service/commit/tree_entry_test.go
index 781f1cd1b..0077201b5 100644
--- a/internal/gitaly/service/commit/tree_entry_test.go
+++ b/internal/gitaly/service/commit/tree_entry_test.go
@@ -214,17 +214,17 @@ func TestFailedTreeEntry(t *testing.T) {
{
name: "Path is outside of repository",
req: &gitalypb.TreeEntryRequest{Repository: repo, Revision: []byte("913c66a37b4a45b9769037c55c2d238bd0942d2e"), Path: []byte("../bar/.gitkeep")}, // Git blows up on paths like this
- expectedErr: structerr.NewNotFound("tree entry not found"),
+ expectedErr: structerr.NewNotFound("tree entry not found").WithInterceptedMetadata("path", "../bar/.gitkeep"),
},
{
name: "Missing file with space in path",
req: &gitalypb.TreeEntryRequest{Repository: repo, Revision: []byte("deadfacedeadfacedeadfacedeadfacedeadface"), Path: []byte("with space/README.md")},
- expectedErr: structerr.NewNotFound("tree entry not found"),
+ expectedErr: structerr.NewNotFound("tree entry not found").WithInterceptedMetadata("path", "with space/README.md"),
},
{
name: "Missing file",
req: &gitalypb.TreeEntryRequest{Repository: repo, Revision: []byte("e63f41fe459e62e1228fcef60d7189127aeba95a"), Path: []byte("missing.rb")},
- expectedErr: structerr.NewNotFound("tree entry not found"),
+ expectedErr: structerr.NewNotFound("tree entry not found").WithInterceptedMetadata("path", "missing.rb"),
},
} {
t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/gitaly/service/diff/patch_id_test.go b/internal/gitaly/service/diff/patch_id_test.go
index ef12951c6..aefee8d6b 100644
--- a/internal/gitaly/service/diff/patch_id_test.go
+++ b/internal/gitaly/service/diff/patch_id_test.go
@@ -21,15 +21,19 @@ func TestGetPatchID(t *testing.T) {
gitVersion, err := gittest.NewCommandFactory(t, cfg).GitVersion(ctx)
require.NoError(t, err)
- testCases := []struct {
- desc string
- setup func(t *testing.T) *gitalypb.GetPatchIDRequest
+ type setupData struct {
+ request *gitalypb.GetPatchIDRequest
expectedResponse *gitalypb.GetPatchIDResponse
expectedErr error
+ }
+
+ testCases := []struct {
+ desc string
+ setup func(t *testing.T) setupData
}{
{
desc: "retruns patch-id successfully",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
oldCommit := gittest.WriteCommit(t, cfg, repoPath,
@@ -45,19 +49,21 @@ func TestGetPatchID(t *testing.T) {
),
)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte("main~"),
- NewRevision: []byte("main"),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte("main~"),
+ NewRevision: []byte("main"),
+ },
+ expectedResponse: &gitalypb.GetPatchIDResponse{
+ PatchId: "a79c7e9df0094ee44fa7a2a9ae27e914e6b7e00b",
+ },
}
},
- expectedResponse: &gitalypb.GetPatchIDResponse{
- PatchId: "a79c7e9df0094ee44fa7a2a9ae27e914e6b7e00b",
- },
},
{
desc: "returns patch-id successfully with commit ids",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
oldCommit := gittest.WriteCommit(t, cfg, repoPath,
@@ -71,19 +77,21 @@ func TestGetPatchID(t *testing.T) {
),
)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(oldCommit),
- NewRevision: []byte(newCommit),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(oldCommit),
+ NewRevision: []byte(newCommit),
+ },
+ expectedResponse: &gitalypb.GetPatchIDResponse{
+ PatchId: "a79c7e9df0094ee44fa7a2a9ae27e914e6b7e00b",
+ },
}
},
- expectedResponse: &gitalypb.GetPatchIDResponse{
- PatchId: "a79c7e9df0094ee44fa7a2a9ae27e914e6b7e00b",
- },
},
{
desc: "returns patch-id successfully for a specific file",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
oldCommit := gittest.WriteCommit(t, cfg, repoPath,
@@ -97,19 +105,21 @@ func TestGetPatchID(t *testing.T) {
),
)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(fmt.Sprintf("%s:file", oldCommit)),
- NewRevision: []byte(fmt.Sprintf("%s:file", newCommit)),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(fmt.Sprintf("%s:file", oldCommit)),
+ NewRevision: []byte(fmt.Sprintf("%s:file", newCommit)),
+ },
+ expectedResponse: &gitalypb.GetPatchIDResponse{
+ PatchId: "a79c7e9df0094ee44fa7a2a9ae27e914e6b7e00b",
+ },
}
},
- expectedResponse: &gitalypb.GetPatchIDResponse{
- PatchId: "a79c7e9df0094ee44fa7a2a9ae27e914e6b7e00b",
- },
},
{
desc: "returns patch-id with binary file",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
oldCommit := gittest.WriteCommit(t, cfg, repoPath,
@@ -123,33 +133,35 @@ func TestGetPatchID(t *testing.T) {
),
)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(oldCommit),
- NewRevision: []byte(newCommit),
- }
- },
- expectedResponse: &gitalypb.GetPatchIDResponse{
- PatchId: func() string {
- // Before Git v2.39.0, git-patch-id(1) would skip over any
- // lines that have an "index " prefix. This causes issues
- // with diffs of binaries though: we don't generate the diff
- // with `--binary`, so the "index" line that contains the
- // pre- and post-image blob IDs of the binary is the only
- // bit of information we have that something changed. But
- // because Git used to skip over it we wouldn't actually
- // take into account the contents of the changed blob at
- // all.
- //
- // This was fixed in Git v2.39.0 so that "index" lines will
- // now be hashed to correctly account for binary changes. As
- // a result, the patch ID has changed.
- if gitVersion.PatchIDRespectsBinaries() {
- return "13e4e9b9cd44ec511bac24fdbdeab9b74ba3000b"
- }
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(oldCommit),
+ NewRevision: []byte(newCommit),
+ },
+ expectedResponse: &gitalypb.GetPatchIDResponse{
+ PatchId: func() string {
+ // Before Git v2.39.0, git-patch-id(1) would skip over any
+ // lines that have an "index " prefix. This causes issues
+ // with diffs of binaries though: we don't generate the diff
+ // with `--binary`, so the "index" line that contains the
+ // pre- and post-image blob IDs of the binary is the only
+ // bit of information we have that something changed. But
+ // because Git used to skip over it we wouldn't actually
+ // take into account the contents of the changed blob at
+ // all.
+ //
+ // This was fixed in Git v2.39.0 so that "index" lines will
+ // now be hashed to correctly account for binary changes. As
+ // a result, the patch ID has changed.
+ if gitVersion.PatchIDRespectsBinaries() {
+ return "13e4e9b9cd44ec511bac24fdbdeab9b74ba3000b"
+ }
- return "715883c1b90a5b4450072e22fefec769ad346266"
- }(),
+ return "715883c1b90a5b4450072e22fefec769ad346266"
+ }(),
+ },
+ }
},
},
{
@@ -157,7 +169,7 @@ func TestGetPatchID(t *testing.T) {
// with different binary contents. This is done to ensure that we indeed
// generate different patch IDs as expected.
desc: "different binary diff has different patch ID",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
oldCommit := gittest.WriteCommit(t, cfg, repoPath,
@@ -171,31 +183,33 @@ func TestGetPatchID(t *testing.T) {
),
)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(oldCommit),
- NewRevision: []byte(newCommit),
- }
- },
- expectedResponse: &gitalypb.GetPatchIDResponse{
- PatchId: func() string {
- if gitVersion.PatchIDRespectsBinaries() {
- // When respecting binary diffs we indeed have a
- // different patch ID compared to the preceding
- // testcase.
- return "f678855867b112ac2c5466260b3b3a5e75fca875"
- }
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(oldCommit),
+ NewRevision: []byte(newCommit),
+ },
+ expectedResponse: &gitalypb.GetPatchIDResponse{
+ PatchId: func() string {
+ if gitVersion.PatchIDRespectsBinaries() {
+ // When respecting binary diffs we indeed have a
+ // different patch ID compared to the preceding
+ // testcase.
+ return "f678855867b112ac2c5466260b3b3a5e75fca875"
+ }
- // But when git-patch-id(1) is not paying respect to binary
- // diffs we incorrectly return the same patch ID. This is
- // nothing we can easily fix though.
- return "715883c1b90a5b4450072e22fefec769ad346266"
- }(),
+ // But when git-patch-id(1) is not paying respect to binary
+ // diffs we incorrectly return the same patch ID. This is
+ // nothing we can easily fix though.
+ return "715883c1b90a5b4450072e22fefec769ad346266"
+ }(),
+ },
+ }
},
},
{
desc: "file didn't exist in the old revision",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
oldCommit := gittest.WriteCommit(t, cfg, repoPath)
@@ -203,81 +217,95 @@ func TestGetPatchID(t *testing.T) {
gittest.TreeEntry{Path: "file", Mode: "100644", Content: "new"},
))
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(fmt.Sprintf("%s:file", oldCommit)),
- NewRevision: []byte(fmt.Sprintf("%s:file", newCommit)),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(fmt.Sprintf("%s:file", oldCommit)),
+ NewRevision: []byte(fmt.Sprintf("%s:file", newCommit)),
+ },
+ expectedErr: structerr.New("waiting for git-diff: exit status 128").
+ WithInterceptedMetadata("stderr", fmt.Sprintf("fatal: path 'file' does not exist in '%s'\n", oldCommit)),
}
},
- expectedErr: structerr.New("waiting for git-diff: exit status 128"),
},
{
desc: "unknown revisions",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, _ := gittest.CreateRepository(t, ctx, cfg)
newRevision := strings.Replace(string(gittest.DefaultObjectHash.ZeroOID), "0", "1", -1)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(gittest.DefaultObjectHash.ZeroOID),
- NewRevision: []byte(newRevision),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(gittest.DefaultObjectHash.ZeroOID),
+ NewRevision: []byte(newRevision),
+ },
+ expectedErr: structerr.New("waiting for git-diff: exit status 128").
+ WithInterceptedMetadata("stderr", fmt.Sprintf("fatal: bad object %s\n", gittest.DefaultObjectHash.ZeroOID)),
}
},
- expectedErr: structerr.New("waiting for git-diff: exit status 128").WithMetadata("stderr", ""),
},
{
desc: "no diff from the given revisions",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg)
commit := gittest.WriteCommit(t, cfg, repoPath)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte(commit),
- NewRevision: []byte(commit),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte(commit),
+ NewRevision: []byte(commit),
+ },
+ expectedErr: structerr.NewFailedPrecondition("no difference between old and new revision"),
}
},
- expectedErr: structerr.NewFailedPrecondition("no difference between old and new revision"),
},
{
desc: "empty repository",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
- return &gitalypb.GetPatchIDRequest{
- Repository: nil,
- OldRevision: []byte("HEAD~1"),
- NewRevision: []byte("HEAD"),
+ setup: func(t *testing.T) setupData {
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: nil,
+ OldRevision: []byte("HEAD~1"),
+ NewRevision: []byte("HEAD"),
+ },
+ expectedErr: structerr.NewInvalidArgument(testhelper.GitalyOrPraefect(
+ "empty Repository",
+ "repo scoped: empty Repository",
+ )),
}
},
- expectedErr: structerr.NewInvalidArgument(testhelper.GitalyOrPraefect(
- "empty Repository",
- "repo scoped: empty Repository",
- )),
},
{
desc: "empty old revision",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, _ := gittest.CreateRepository(t, ctx, cfg)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- NewRevision: []byte("HEAD"),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ NewRevision: []byte("HEAD"),
+ },
+ expectedErr: structerr.NewInvalidArgument("empty OldRevision"),
}
},
- expectedErr: structerr.NewInvalidArgument("empty OldRevision"),
},
{
desc: "empty new revision",
- setup: func(t *testing.T) *gitalypb.GetPatchIDRequest {
+ setup: func(t *testing.T) setupData {
repoProto, _ := gittest.CreateRepository(t, ctx, cfg)
- return &gitalypb.GetPatchIDRequest{
- Repository: repoProto,
- OldRevision: []byte("HEAD~1"),
+ return setupData{
+ request: &gitalypb.GetPatchIDRequest{
+ Repository: repoProto,
+ OldRevision: []byte("HEAD~1"),
+ },
+ expectedErr: structerr.NewInvalidArgument("empty NewRevision"),
}
},
- expectedErr: structerr.NewInvalidArgument("empty NewRevision"),
},
}
@@ -286,11 +314,11 @@ func TestGetPatchID(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
- request := tc.setup(t)
- response, err := client.GetPatchID(ctx, request)
+ setupData := tc.setup(t)
+ response, err := client.GetPatchID(ctx, setupData.request)
- testhelper.RequireGrpcError(t, tc.expectedErr, err)
- testhelper.ProtoEqual(t, tc.expectedResponse, response)
+ testhelper.RequireGrpcError(t, setupData.expectedErr, err)
+ testhelper.ProtoEqual(t, setupData.expectedResponse, response)
})
}
}
diff --git a/internal/gitaly/service/objectpool/create_test.go b/internal/gitaly/service/objectpool/create_test.go
index b90b5e6d2..9948de9e0 100644
--- a/internal/gitaly/service/objectpool/create_test.go
+++ b/internal/gitaly/service/objectpool/create_test.go
@@ -113,6 +113,7 @@ func testCreateUnsuccessful(t *testing.T, ctx context.Context) {
Origin: repo,
})
require.NoError(t, err)
+ preexistingPoolPath := filepath.Join(cfg.Storages[0].Path, preexistingPool.Repository.RelativePath)
for _, tc := range []struct {
desc string
@@ -218,7 +219,9 @@ func testCreateUnsuccessful(t *testing.T, ctx context.Context) {
if featureflag.AtomicCreateObjectPool.IsEnabled(ctx) {
return structerr.NewFailedPrecondition("creating object pool: repository exists already")
}
- return structerr.NewFailedPrecondition("creating object pool: target path exists already")
+
+ return structerr.NewFailedPrecondition("creating object pool: target path exists already").
+ WithInterceptedMetadata("object_pool_path", preexistingPoolPath)
}(),
},
} {
diff --git a/internal/gitaly/service/operations/branches_test.go b/internal/gitaly/service/operations/branches_test.go
index 73cbd5667..4c93de134 100644
--- a/internal/gitaly/service/operations/branches_test.go
+++ b/internal/gitaly/service/operations/branches_test.go
@@ -549,8 +549,9 @@ func TestUserDeleteBranch(t *testing.T) {
BranchName: []byte(branchName),
ExpectedOldOid: "foobar",
},
- repoPath: repoPath,
- expectedErr: structerr.NewInvalidArgument("invalid expected old object ID: invalid object ID: \"foobar\""),
+ repoPath: repoPath,
+ expectedErr: structerr.NewInvalidArgument("invalid expected old object ID: invalid object ID: \"foobar\"").
+ WithInterceptedMetadata("old_object_id", "foobar"),
expectedRefs: []string{"master", branchName},
}
},
@@ -574,8 +575,9 @@ func TestUserDeleteBranch(t *testing.T) {
BranchName: []byte(branchName),
ExpectedOldOid: gittest.DefaultObjectHash.ZeroOID.String(),
},
- repoPath: repoPath,
- expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ repoPath: repoPath,
+ expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
expectedRefs: []string{"master", branchName},
}
},
diff --git a/internal/gitaly/service/operations/cherry_pick_test.go b/internal/gitaly/service/operations/cherry_pick_test.go
index 8d5b6b183..71cae333f 100644
--- a/internal/gitaly/service/operations/cherry_pick_test.go
+++ b/internal/gitaly/service/operations/cherry_pick_test.go
@@ -205,7 +205,8 @@ func TestUserCherryPick(t *testing.T) {
ExpectedOldOid: "foobar",
}, &gitalypb.UserCherryPickResponse{}
},
- expectedErr: structerr.NewInvalidArgument("invalid expected old object ID: invalid object ID: \"foobar\""),
+ expectedErr: structerr.NewInvalidArgument("invalid expected old object ID: invalid object ID: \"foobar\"").
+ WithInterceptedMetadata("old_object_id", "foobar"),
},
{
desc: "valid but non-existent expectedOldOID",
@@ -221,7 +222,8 @@ func TestUserCherryPick(t *testing.T) {
ExpectedOldOid: gittest.DefaultObjectHash.ZeroOID.String(),
}, &gitalypb.UserCherryPickResponse{}
},
- expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
},
{
desc: "incorrect expectedOldOID",
diff --git a/internal/gitaly/service/operations/commit_files_test.go b/internal/gitaly/service/operations/commit_files_test.go
index 7c764ae77..2bff1f4db 100644
--- a/internal/gitaly/service/operations/commit_files_test.go
+++ b/internal/gitaly/service/operations/commit_files_test.go
@@ -1154,7 +1154,8 @@ func testSuccessfulUserCommitFilesRequest(t *testing.T, ctx context.Context) {
repoPath: repoPath,
branchName: "few-commits",
expectedOldOID: "foobar",
- expectedError: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "foobar"`),
+ expectedError: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "foobar"`).
+ WithInterceptedMetadata("old_object_id", "foobar"),
},
{
desc: "existing repo and branch + valid expectedOldOID but invalid object",
@@ -1162,7 +1163,8 @@ func testSuccessfulUserCommitFilesRequest(t *testing.T, ctx context.Context) {
repoPath: repoPath,
branchName: "few-commits",
expectedOldOID: gittest.DefaultObjectHash.ZeroOID.String(),
- expectedError: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ expectedError: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
},
{
desc: "existing repo and branch + incorrect expectedOldOID",
diff --git a/internal/gitaly/service/operations/merge_test.go b/internal/gitaly/service/operations/merge_test.go
index 80a4b8f65..f799d0b11 100644
--- a/internal/gitaly/service/operations/merge_test.go
+++ b/internal/gitaly/service/operations/merge_test.go
@@ -135,7 +135,8 @@ func TestUserMergeBranch(t *testing.T) {
Message: []byte(data.message),
ExpectedOldOid: "foobar",
},
- firstExpectedErr: structerr.NewInvalidArgument("invalid expected old object ID: invalid object ID: \"foobar\""),
+ firstExpectedErr: structerr.NewInvalidArgument("invalid expected old object ID: invalid object ID: \"foobar\"").
+ WithInterceptedMetadata("old_object_id", "foobar"),
}
},
},
@@ -152,7 +153,8 @@ func TestUserMergeBranch(t *testing.T) {
Message: []byte(data.message),
ExpectedOldOid: gittest.DefaultObjectHash.ZeroOID.String(),
},
- firstExpectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ firstExpectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
}
},
},
@@ -1254,7 +1256,8 @@ func TestUserFFBranch(t *testing.T) {
},
}
},
- expectedErr: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "foobar"`),
+ expectedErr: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "foobar"`).
+ WithInterceptedMetadata("old_object_id", "foobar"),
},
{
desc: "valid SHA, but not existing expectedOldOID",
@@ -1275,7 +1278,8 @@ func TestUserFFBranch(t *testing.T) {
},
}
},
- expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
},
{
desc: "expectedOldOID pointing to old commit",
diff --git a/internal/gitaly/service/operations/revert_test.go b/internal/gitaly/service/operations/revert_test.go
index 088739913..78e100b59 100644
--- a/internal/gitaly/service/operations/revert_test.go
+++ b/internal/gitaly/service/operations/revert_test.go
@@ -320,7 +320,8 @@ func TestUserRevert(t *testing.T) {
},
}
},
- expectedErr: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "foobar"`),
+ expectedErr: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "foobar"`).
+ WithInterceptedMetadata("old_object_id", "foobar"),
},
{
desc: "expectedOldOID with valid SHA, but not present in repo",
@@ -342,7 +343,8 @@ func TestUserRevert(t *testing.T) {
},
}
},
- expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
},
{
desc: "expectedOldOID pointing to old commit",
diff --git a/internal/gitaly/service/operations/tags_test.go b/internal/gitaly/service/operations/tags_test.go
index b2aac01dd..7dee225ed 100644
--- a/internal/gitaly/service/operations/tags_test.go
+++ b/internal/gitaly/service/operations/tags_test.go
@@ -190,7 +190,8 @@ func TestUserDeleteTag(t *testing.T) {
ExpectedOldOid: "io",
}
},
- expectedErr: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "io"`),
+ expectedErr: structerr.NewInvalidArgument(`invalid expected old object ID: invalid object ID: "io"`).
+ WithInterceptedMetadata("old_object_id", "io"),
expectedTags: []string{"europa"},
},
{
@@ -207,7 +208,8 @@ func TestUserDeleteTag(t *testing.T) {
ExpectedOldOid: gittest.DefaultObjectHash.ZeroOID.String(),
}
},
- expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found"),
+ expectedErr: structerr.NewInvalidArgument("cannot resolve expected old object ID: reference not found").
+ WithInterceptedMetadata("old_object_id", gittest.DefaultObjectHash.ZeroOID),
expectedTags: []string{"europa"},
},
{
diff --git a/internal/structerr/error.go b/internal/structerr/error.go
index 7cff2bb0d..fdaf132df 100644
--- a/internal/structerr/error.go
+++ b/internal/structerr/error.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
+ "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb/testproto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
@@ -288,6 +289,17 @@ func (e Error) WithMetadata(key string, value any) Error {
return e
}
+// WithInterceptedMetadata adds an additional metadata item to the Error in the form of an error
+// detail. Note that this is only intended to be used in the context of tests where we convert error
+// metadata into structured errors via the UnaryInterceptor and StreamInterceptor so that we can
+// test that metadata has been set as expected on the client-side of a gRPC call.
+func (e Error) WithInterceptedMetadata(key string, value any) Error {
+ return e.WithDetail(&testproto.ErrorMetadata{
+ Key: []byte(key),
+ Value: []byte(fmt.Sprintf("%v", value)),
+ })
+}
+
// Details returns the chain error details set by this and any wrapped Error. The returned array
// contains error details ordered from top-level error details to bottom-level error details.
func (e Error) Details() []proto.Message {
diff --git a/internal/structerr/fields_producer.go b/internal/structerr/fields_producer.go
deleted file mode 100644
index 367f149e7..000000000
--- a/internal/structerr/fields_producer.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package structerr
-
-import (
- "context"
- "errors"
-
- "github.com/sirupsen/logrus"
-)
-
-// FieldsProducer extracts metadata from err if it contains a `structerr.Error` and exposes it as
-// logged fields. This function is supposed to be used with `log.MessageProducer()`.
-func FieldsProducer(_ context.Context, err error) logrus.Fields {
- var structErr Error
- if errors.As(err, &structErr) {
- metadata := structErr.Metadata()
- if len(metadata) == 0 {
- return nil
- }
-
- return logrus.Fields{
- "error_metadata": metadata,
- }
- }
-
- return nil
-}
diff --git a/internal/structerr/grpc_server.go b/internal/structerr/grpc_server.go
new file mode 100644
index 000000000..dec2d44d2
--- /dev/null
+++ b/internal/structerr/grpc_server.go
@@ -0,0 +1,70 @@
+package structerr
+
+import (
+ "context"
+ "errors"
+ "sort"
+
+ "github.com/sirupsen/logrus"
+ "google.golang.org/grpc"
+)
+
+// UnaryInterceptor is an interceptor for unary RPC calls that injects error metadata as detailed
+// error. This is only supposed to be used for testing purposes as error metadata is considered to
+// be a server-side detail. No clients should start to rely on it.
+func UnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
+ response, err := handler(ctx, req)
+ return response, interceptedError(err)
+}
+
+// StreamInterceptor is an interceptor for streaming RPC calls that injects error metadata as
+// detailed error. This is only supposed to be used for testing purposes as error metadata is
+// considered to be a server-side detail. No clients should start to rely on it.
+func StreamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
+ return interceptedError(handler(srv, stream))
+}
+
+func interceptedError(err error) error {
+ var structErr Error
+ if errors.As(err, &structErr) {
+ metadata := structErr.Metadata()
+ if len(metadata) == 0 {
+ return err
+ }
+
+ keys := make([]string, 0, len(metadata))
+ for key := range metadata {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+
+ // We need to wrap the top-level error with the expected intercepted metadata, or
+ // otherwise we lose error context.
+ errWithMetadata := New("%w", err)
+ for _, key := range keys {
+ errWithMetadata = errWithMetadata.WithInterceptedMetadata(key, metadata[key])
+ }
+
+ return errWithMetadata
+ }
+
+ return err
+}
+
+// FieldsProducer extracts metadata from err if it contains a `structerr.Error` and exposes it as
+// logged fields. This function is supposed to be used with `log.MessageProducer()`.
+func FieldsProducer(_ context.Context, err error) logrus.Fields {
+ var structErr Error
+ if errors.As(err, &structErr) {
+ metadata := structErr.Metadata()
+ if len(metadata) == 0 {
+ return nil
+ }
+
+ return logrus.Fields{
+ "error_metadata": metadata,
+ }
+ }
+
+ return nil
+}
diff --git a/internal/structerr/fields_producer_test.go b/internal/structerr/grpc_server_test.go
index 27bde9a32..b9c48d6b9 100644
--- a/internal/structerr/fields_producer_test.go
+++ b/internal/structerr/grpc_server_test.go
@@ -3,6 +3,7 @@ package structerr
import (
"context"
"errors"
+ "fmt"
"net"
"testing"
@@ -13,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitaly/v15/internal/log"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb/testproto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
@@ -20,6 +22,57 @@ import (
"google.golang.org/grpc/test/grpc_testing"
)
+func TestInterceptedError(t *testing.T) {
+ t.Parallel()
+
+ for _, tc := range []struct {
+ desc string
+ err error
+ expectedErr error
+ }{
+ {
+ desc: "normal error",
+ err: fmt.Errorf("test"),
+ expectedErr: fmt.Errorf("test"),
+ },
+ {
+ desc: "structured error",
+ err: NewNotFound("not found"),
+ expectedErr: NewNotFound("not found"),
+ },
+ {
+ desc: "wrapped structured error",
+ err: fmt.Errorf("wrapped: %w", NewNotFound("not found")),
+ expectedErr: fmt.Errorf("wrapped: %w", NewNotFound("not found")),
+ },
+ {
+ desc: "metadata",
+ err: NewNotFound("not found").WithMetadata("key", "value"),
+ expectedErr: NewNotFound("not found").WithDetail(
+ &testproto.ErrorMetadata{
+ Key: []byte("key"),
+ Value: []byte("value"),
+ },
+ ),
+ },
+ {
+ desc: "wrapped error with metadata",
+ err: fmt.Errorf("wrapped: %w", NewNotFound("not found").WithMetadata("key", "value")),
+ expectedErr: NewNotFound("wrapped: not found").WithDetail(
+ &testproto.ErrorMetadata{
+ Key: []byte("key"),
+ Value: []byte("value"),
+ },
+ ),
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ err := interceptedError(tc.err)
+ testhelper.RequireGrpcError(t, tc.expectedErr, err)
+ })
+ }
+}
+
type mockService struct {
grpc_testing.UnimplementedTestServiceServer
err error
diff --git a/internal/structerr/testhelper_test.go b/internal/structerr/testhelper_test.go
deleted file mode 100644
index b9934fecc..000000000
--- a/internal/structerr/testhelper_test.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package structerr
-
-import (
- "testing"
-
- "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
-)
-
-func TestMain(m *testing.M) {
- testhelper.Run(m)
-}
diff --git a/internal/testhelper/testserver/gitaly.go b/internal/testhelper/testserver/gitaly.go
index 6401cd25b..d2a377710 100644
--- a/internal/testhelper/testserver/gitaly.go
+++ b/internal/testhelper/testserver/gitaly.go
@@ -33,6 +33,7 @@ import (
"gitlab.com/gitlab-org/gitaly/v15/internal/middleware/limithandler"
praefectconfig "gitlab.com/gitlab-org/gitaly/v15/internal/praefect/config"
"gitlab.com/gitlab-org/gitaly/v15/internal/streamcache"
+ "gitlab.com/gitlab-org/gitaly/v15/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testdb"
"google.golang.org/grpc"
@@ -153,6 +154,13 @@ func runGitaly(tb testing.TB, cfg config.Cfg, rubyServer *rubyserver.Server, reg
gsd = opt(gsd)
}
+ // We set up the structerr interceptors so that any error metadata that gets set via
+ // `structerr.WithMetadata()` is not only logged, but also present in the error details.
+ serverOpts := []server.Option{
+ server.WithUnaryInterceptor(structerr.UnaryInterceptor),
+ server.WithStreamInterceptor(structerr.StreamInterceptor),
+ }
+
deps := gsd.createDependencies(tb, cfg, rubyServer)
tb.Cleanup(func() { gsd.conns.Close() })
@@ -165,7 +173,7 @@ func runGitaly(tb testing.TB, cfg config.Cfg, rubyServer *rubyserver.Server, reg
)
if cfg.RuntimeDir != "" {
- internalServer, err := serverFactory.CreateInternal()
+ internalServer, err := serverFactory.CreateInternal(serverOpts...)
require.NoError(tb, err)
tb.Cleanup(internalServer.Stop)
@@ -185,7 +193,9 @@ func runGitaly(tb testing.TB, cfg config.Cfg, rubyServer *rubyserver.Server, reg
waitHealthy(tb, ctx, "unix://"+internalListener.Addr().String(), cfg.Auth.Token)
}
- externalServer, err := serverFactory.CreateExternal(cfg.TLS.CertPath != "" && cfg.TLS.KeyPath != "")
+ secure := cfg.TLS.CertPath != "" && cfg.TLS.KeyPath != ""
+
+ externalServer, err := serverFactory.CreateExternal(secure, serverOpts...)
require.NoError(tb, err)
tb.Cleanup(externalServer.Stop)
diff --git a/proto/go/gitalypb/testproto/error_metadata.pb.go b/proto/go/gitalypb/testproto/error_metadata.pb.go
new file mode 100644
index 000000000..56f0b4a17
--- /dev/null
+++ b/proto/go/gitalypb/testproto/error_metadata.pb.go
@@ -0,0 +1,159 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.28.1
+// protoc v3.21.7
+// source: testproto/error_metadata.proto
+
+package testproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// ErrorMetadata is a key-value metadata item that may be attached to errors. We only use this
+// infrastructure for testing purposes to assert that we add error metadata as expected that would
+// otherwise only get logged.
+type ErrorMetadata struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // Key is the key of the item.
+ Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+ // Value is the value of the item.
+ Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+}
+
+func (x *ErrorMetadata) Reset() {
+ *x = ErrorMetadata{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_testproto_error_metadata_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ErrorMetadata) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ErrorMetadata) ProtoMessage() {}
+
+func (x *ErrorMetadata) ProtoReflect() protoreflect.Message {
+ mi := &file_testproto_error_metadata_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ErrorMetadata.ProtoReflect.Descriptor instead.
+func (*ErrorMetadata) Descriptor() ([]byte, []int) {
+ return file_testproto_error_metadata_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ErrorMetadata) GetKey() []byte {
+ if x != nil {
+ return x.Key
+ }
+ return nil
+}
+
+func (x *ErrorMetadata) GetValue() []byte {
+ if x != nil {
+ return x.Value
+ }
+ return nil
+}
+
+var File_testproto_error_metadata_proto protoreflect.FileDescriptor
+
+var file_testproto_error_metadata_proto_rawDesc = []byte{
+ 0x0a, 0x1e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x72, 0x72, 0x6f,
+ 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x12, 0x09, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x37, 0x0a, 0x0d, 0x45,
+ 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03,
+ 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14,
+ 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63,
+ 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, 0x67, 0x69,
+ 0x74, 0x61, 0x6c, 0x79, 0x2f, 0x76, 0x31, 0x35, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74,
+ 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_testproto_error_metadata_proto_rawDescOnce sync.Once
+ file_testproto_error_metadata_proto_rawDescData = file_testproto_error_metadata_proto_rawDesc
+)
+
+func file_testproto_error_metadata_proto_rawDescGZIP() []byte {
+ file_testproto_error_metadata_proto_rawDescOnce.Do(func() {
+ file_testproto_error_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(file_testproto_error_metadata_proto_rawDescData)
+ })
+ return file_testproto_error_metadata_proto_rawDescData
+}
+
+var file_testproto_error_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_testproto_error_metadata_proto_goTypes = []interface{}{
+ (*ErrorMetadata)(nil), // 0: testproto.ErrorMetadata
+}
+var file_testproto_error_metadata_proto_depIdxs = []int32{
+ 0, // [0:0] is the sub-list for method output_type
+ 0, // [0:0] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_testproto_error_metadata_proto_init() }
+func file_testproto_error_metadata_proto_init() {
+ if File_testproto_error_metadata_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_testproto_error_metadata_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ErrorMetadata); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_testproto_error_metadata_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_testproto_error_metadata_proto_goTypes,
+ DependencyIndexes: file_testproto_error_metadata_proto_depIdxs,
+ MessageInfos: file_testproto_error_metadata_proto_msgTypes,
+ }.Build()
+ File_testproto_error_metadata_proto = out.File
+ file_testproto_error_metadata_proto_rawDesc = nil
+ file_testproto_error_metadata_proto_goTypes = nil
+ file_testproto_error_metadata_proto_depIdxs = nil
+}
diff --git a/proto/testproto/error_metadata.proto b/proto/testproto/error_metadata.proto
new file mode 100644
index 000000000..e97c9dc0d
--- /dev/null
+++ b/proto/testproto/error_metadata.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+package testproto;
+
+option go_package = "gitlab.com/gitlab-org/gitaly/v15/proto/testproto";
+
+// ErrorMetadata is a key-value metadata item that may be attached to errors. We only use this
+// infrastructure for testing purposes to assert that we add error metadata as expected that would
+// otherwise only get logged.
+message ErrorMetadata {
+ // Key is the key of the item.
+ bytes key = 1;
+ // Value is the value of the item.
+ bytes value = 2;
+}