diff options
author | John Cai <jcai@gitlab.com> | 2022-06-09 18:09:11 +0300 |
---|---|---|
committer | John Cai <jcai@gitlab.com> | 2022-07-21 23:39:31 +0300 |
commit | e2ae10c6ddb2691ab26c54abf1306b40de10ba26 (patch) | |
tree | 2e54a3bf73fc2cc55c49f25d3b017d01c263ac0f | |
parent | 276d057f21e41c21204f7b83754071438c2cee85 (diff) |
gitlab: Add Features method that returns map of feature flags
Add a Features method on the client that calls the Rails api and contains
logic that turns the response from the API into a map of feature flags
and their associated boolean values.
We have a limitation with the current Rails api endpoint however. We
don't have a way to determine, the case when the feature gate is a
percentage of actors or time, or if its by project or group, whether or
not a feature is enabled. We can tell however, if a feature is
completely turned on or off.
-rw-r--r-- | cmd/gitaly-hooks/hooks_test.go | 6 | ||||
-rw-r--r-- | cmd/gitaly-ssh/auth_test.go | 6 | ||||
-rw-r--r-- | internal/gitaly/hook/postreceive_test.go | 16 | ||||
-rw-r--r-- | internal/gitaly/hook/prereceive_test.go | 16 | ||||
-rw-r--r-- | internal/gitaly/hook/transactions_test.go | 12 | ||||
-rw-r--r-- | internal/gitaly/hook/update_test.go | 12 | ||||
-rw-r--r-- | internal/gitaly/server/auth_test.go | 6 | ||||
-rw-r--r-- | internal/gitaly/service/operations/branches_test.go | 8 | ||||
-rw-r--r-- | internal/gitaly/service/operations/merge_test.go | 1 | ||||
-rw-r--r-- | internal/gitlab/client.go | 25 | ||||
-rw-r--r-- | internal/gitlab/http_client.go | 104 | ||||
-rw-r--r-- | internal/gitlab/http_client_test.go | 158 | ||||
-rw-r--r-- | internal/gitlab/mock_client.go | 16 | ||||
-rw-r--r-- | internal/gitlab/test_server.go | 26 | ||||
-rw-r--r-- | internal/testhelper/testserver/gitaly.go | 6 |
15 files changed, 397 insertions, 21 deletions
diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go index 69ded247c..e22cb792a 100644 --- a/cmd/gitaly-hooks/hooks_test.go +++ b/cmd/gitaly-hooks/hooks_test.go @@ -579,7 +579,11 @@ func TestCheckBadCreds(t *testing.T) { func runHookServiceServer(t *testing.T, cfg config.Cfg, serverOpts ...testserver.GitalyServerOpt) { runHookServiceWithGitlabClient(t, cfg, gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, ), serverOpts...) } diff --git a/cmd/gitaly-ssh/auth_test.go b/cmd/gitaly-ssh/auth_test.go index 3dd9eb1b2..72809cde7 100644 --- a/cmd/gitaly-ssh/auth_test.go +++ b/cmd/gitaly-ssh/auth_test.go @@ -147,7 +147,11 @@ func runServer(t *testing.T, secure bool, cfg config.Cfg, connectionType string, 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, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) limitHandler := limithandler.New(cfg, limithandler.LimitConcurrencyByRepo, limithandler.WithConcurrencyLimiters) diskCache := cache.New(cfg, locator) diff --git a/internal/gitaly/hook/postreceive_test.go b/internal/gitaly/hook/postreceive_test.go index f78265d9f..cc8e867f8 100644 --- a/internal/gitaly/hook/postreceive_test.go +++ b/internal/gitaly/hook/postreceive_test.go @@ -72,7 +72,11 @@ func TestPostReceive_customHook(t *testing.T) { locator := config.NewLocator(cfg) hookManager := NewManager(cfg, locator, gitCmdFactory, transaction.NewManager(cfg, backchannel.NewRegistry()), gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) receiveHooksPayload := &git.UserDetails{ @@ -237,6 +241,10 @@ func (m *postreceiveAPIMock) PostReceive(ctx context.Context, glRepository, glID return m.postreceive(ctx, glRepository, glID, changes, pushOptions...) } +func (m *postreceiveAPIMock) Features(ctx context.Context) (map[featureflag.FeatureFlag]bool, error) { + return nil, nil +} + func TestPostReceive_gitlab(t *testing.T) { cfg, repo, repoPath := testcfg.BuildWithRepo(t) @@ -367,7 +375,11 @@ func TestPostReceive_quarantine(t *testing.T) { require.NoError(t, err) hookManager := NewManager(cfg, config.NewLocator(cfg), gittest.NewCommandFactory(t, cfg), nil, gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) gittest.WriteCustomHook(t, repoPath, "post-receive", []byte(fmt.Sprintf( diff --git a/internal/gitaly/hook/prereceive_test.go b/internal/gitaly/hook/prereceive_test.go index 7dfc05cee..20936cb31 100644 --- a/internal/gitaly/hook/prereceive_test.go +++ b/internal/gitaly/hook/prereceive_test.go @@ -31,7 +31,11 @@ func TestPrereceive_customHooks(t *testing.T) { locator := config.NewLocator(cfg) hookManager := NewManager(cfg, locator, gitCmdFactory, transaction.NewManager(cfg, backchannel.NewRegistry()), gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) receiveHooksPayload := &git.UserDetails{ @@ -186,7 +190,11 @@ func TestPrereceive_quarantine(t *testing.T) { require.NoError(t, err) hookManager := NewManager(cfg, config.NewLocator(cfg), gittest.NewCommandFactory(t, cfg), nil, gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) gittest.WriteCustomHook(t, repoPath, "pre-receive", []byte(fmt.Sprintf( @@ -249,6 +257,10 @@ func (m *prereceiveAPIMock) PostReceive(context.Context, string, string, string, return true, nil, errors.New("unexpected call") } +func (m *prereceiveAPIMock) Features(ctx context.Context) (map[featureflag.FeatureFlag]bool, error) { + return nil, nil +} + func TestPrereceive_gitlab(t *testing.T) { cfg, repo, repoPath := testcfg.BuildWithRepo(t) diff --git a/internal/gitaly/hook/transactions_test.go b/internal/gitaly/hook/transactions_test.go index 727485fd9..388bca3bb 100644 --- a/internal/gitaly/hook/transactions_test.go +++ b/internal/gitaly/hook/transactions_test.go @@ -30,7 +30,11 @@ func TestHookManager_stopCalled(t *testing.T) { var mockTxMgr transaction.MockManager hookManager := NewManager(cfg, config.NewLocator(cfg), gittest.NewCommandFactory(t, cfg), &mockTxMgr, gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) ctx := testhelper.Context(t) @@ -130,7 +134,11 @@ func TestHookManager_contextCancellationCancelsVote(t *testing.T) { } hookManager := NewManager(cfg, config.NewLocator(cfg), gittest.NewCommandFactory(t, cfg), &mockTxMgr, gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) hooksPayload, err := git.NewHooksPayload( diff --git a/internal/gitaly/hook/update_test.go b/internal/gitaly/hook/update_test.go index 326f2dca1..cca6317bc 100644 --- a/internal/gitaly/hook/update_test.go +++ b/internal/gitaly/hook/update_test.go @@ -28,7 +28,11 @@ func TestUpdate_customHooks(t *testing.T) { locator := config.NewLocator(cfg) hookManager := NewManager(cfg, locator, gitCmdFactory, transaction.NewManager(cfg, backchannel.NewRegistry()), gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) receiveHooksPayload := &git.UserDetails{ @@ -217,7 +221,11 @@ func TestUpdate_quarantine(t *testing.T) { require.NoError(t, err) hookManager := NewManager(cfg, config.NewLocator(cfg), gittest.NewCommandFactory(t, cfg), nil, gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) gittest.WriteCustomHook(t, repoPath, "update", []byte(fmt.Sprintf( diff --git a/internal/gitaly/server/auth_test.go b/internal/gitaly/server/auth_test.go index e650fbd10..8e0dae3bc 100644 --- a/internal/gitaly/server/auth_test.go +++ b/internal/gitaly/server/auth_test.go @@ -196,7 +196,11 @@ func runServer(t *testing.T, cfg config.Cfg) string { 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, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, )) catfileCache := catfile.NewCache(cfg) t.Cleanup(catfileCache.Stop) diff --git a/internal/gitaly/service/operations/branches_test.go b/internal/gitaly/service/operations/branches_test.go index 35e9e5fd8..9401c213a 100644 --- a/internal/gitaly/service/operations/branches_test.go +++ b/internal/gitaly/service/operations/branches_test.go @@ -495,7 +495,13 @@ func TestUserDeleteBranch_allowed(t *testing.T) { } { t.Run(tc.desc, func(t *testing.T) { ctx, cfg, client := setupOperationsServiceWithoutRepo(t, ctx, testserver.WithGitLabClient( - gitlab.NewMockClient(t, tc.allowed, gitlab.MockPreReceive, gitlab.MockPostReceive), + gitlab.NewMockClient( + t, + tc.allowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, + ), )) repo, repoPath := gittest.CreateRepository(ctx, t, cfg) diff --git a/internal/gitaly/service/operations/merge_test.go b/internal/gitaly/service/operations/merge_test.go index fabf76446..57404d286 100644 --- a/internal/gitaly/service/operations/merge_test.go +++ b/internal/gitaly/service/operations/merge_test.go @@ -634,6 +634,7 @@ func TestUserMergeBranch_allowed(t *testing.T) { }, gitlab.MockPreReceive, gitlab.MockPostReceive, + gitlab.MockFeatures, )) ctx, cfg, repoProto, repoPath, client := setupOperationsServiceWithCfg( diff --git a/internal/gitlab/client.go b/internal/gitlab/client.go index d4e147451..3f9cfabd2 100644 --- a/internal/gitlab/client.go +++ b/internal/gitlab/client.go @@ -2,6 +2,8 @@ package gitlab import ( "context" + + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" ) // AllowedParams compose set of parameters required to call 'GitlabAPI.Allowed' method. @@ -42,6 +44,27 @@ type CheckInfo struct { RedisReachable bool `json:"redis"` } +// Feature represents the response of GitLab's `features` API endpoint. +// The Flipper gem is used in Rails: https://www.flippercloud.io/docs/api +type Feature struct { + // Name is the name of the feature + Name string `json:"name"` + // State is either "on", "off", or "conditional" + State string `json:"state"` + // Gates is a slice of FeatureGates + Gates []FeatureGate `json:"gates"` +} + +// FeatureGate is a key value pair that controls whether or not a feature +// is enabled. +type FeatureGate struct { + // Key is the type of the feature gate. boolean, groups, actors, + // percentage_of_actors, percentage_of_time. + Key string `json:"key"` + // Value can be a string, bool, array, or integer + Value interface{} `json:"value"` +} + // Client is an interface for accessing the GitLab internal API type Client interface { // Allowed queries the gitlab internal api /allowed endpoint to determine if a ref change for a given repository and user is allowed @@ -52,4 +75,6 @@ type Client interface { PreReceive(ctx context.Context, glRepository string) (bool, error) // PostReceive queries the gitlab internal api /post_receive to decrease the reference counter PostReceive(ctx context.Context, glRepository, glID, changes string, pushOptions ...string) (bool, []PostReceiveMessage, error) + // Features returns features from the api /features + Features(ctx context.Context) (map[featureflag.FeatureFlag]bool, error) } diff --git a/internal/gitlab/http_client.go b/internal/gitlab/http_client.go index 5afab4ad2..588367006 100644 --- a/internal/gitlab/http_client.go +++ b/internal/gitlab/http_client.go @@ -12,10 +12,12 @@ import ( "regexp" "strings" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" - gitalycfgprom "gitlab.com/gitlab-org/gitaly/v15/internal/config/prometheus" + promcfg "gitlab.com/gitlab-org/gitaly/v15/internal/config/prometheus" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v15/internal/prometheus/metrics" "gitlab.com/gitlab-org/gitaly/v15/internal/version" "gitlab.com/gitlab-org/gitlab-shell/v14/client" @@ -35,7 +37,7 @@ func NewHTTPClient( logger logrus.FieldLogger, gitlabCfg config.Gitlab, tlsCfg config.TLS, - promCfg gitalycfgprom.Config, + promCfg promcfg.Config, ) (*HTTPClient, error) { url, err := url.PathUnescape(gitlabCfg.URL) if err != nil { @@ -295,6 +297,36 @@ func (c *HTTPClient) Check(ctx context.Context) (*CheckInfo, error) { return &info, nil } +// Features performs an HTTP request to the /features API endpoint to check +// if any feature flags are enabled. +func (c *HTTPClient) features(ctx context.Context) ([]Feature, error) { + defer prometheus.NewTimer(c.latencyMetric.WithLabelValues("feature")).ObserveDuration() + + resp, err := c.DoRequest( + ctx, + http.MethodGet, + "/api/v4/features", + nil, + ) + if err != nil { + return nil, err + } + + defer c.finalizeResponse(resp) + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Feature HTTP request failed with status: %d", resp.StatusCode) + } + + var features []Feature + + if err := json.NewDecoder(resp.Body).Decode(&features); err != nil { + return nil, fmt.Errorf("failed to decode response from /feature endpoint: %w", err) + } + + return features, nil +} + func (c *HTTPClient) finalizeResponse(resp *http.Response) { if _, err := io.Copy(io.Discard, resp.Body); err != nil { c.logger.WithError(err).Errorf("discard body error for the request %q", resp.Request.RequestURI) @@ -316,3 +348,71 @@ func marshallGitObjectDirs(gitObjectDirRel string, gitAltObjectDirsRel []string) return string(envString), nil } + +var gitalyFeaturePrefix = "gitaly_" + +// Features calls Gitlab's /features API and +// returns all feature flags that are explicitly turned on. +// NOTE: We only inject a feature flag as enabled if it is explicitly turned on +// if a boolean feature gate is set to true. If a boolean feature gate is set to +// false, we don't have a way of knowing whether or not a feature flag should be +// turned on. In other words, whatever uses this function should expect that +// feature toggling by percentage or project will not be seen as an enabled +// feature flag. Only feature flags that are turned completely on will be +// injected into the outgoing context as enabled. +func (c *HTTPClient) Features(ctx context.Context) (map[featureflag.FeatureFlag]bool, error) { + features, err := c.features(ctx) + if err != nil { + return nil, fmt.Errorf("getting features: %w", err) + } + + featureFlags := make(map[featureflag.FeatureFlag]bool) + + for _, feature := range features { + var featureEnabled bool + + if !strings.HasPrefix(feature.Name, gitalyFeaturePrefix) { + continue + } + + featureName := strings.TrimPrefix(feature.Name, gitalyFeaturePrefix) + + if feature.State == "off" { + featureFlags[featureflag.FeatureFlag{Name: featureName}] = false + } else if feature.State == "on" { + if len(feature.Gates) == 0 { + // If there are no feature gates, then we consider the + // feature as turned on if the state is "on". Otherwise, + // we don't make a decision. + featureEnabled = true + } else if len(feature.Gates) > 1 { + // If there are more than 1 feature gate that means + // there is a feature gate with a Key other than + // boolean. In this case, we don't have a way to + // determine if the flag should be on or off. + continue + } else { + // Once we get here, we know that there is only 1 + // feature gate. If it is a boolean, then that + // determines whether the feature is turned on or off. + if feature.Gates[0].Key != "boolean" { + continue + } + + v, ok := feature.Gates[0].Value.(bool) + if !ok { + ctxlogrus.Extract(ctx). + WithField("feature_flag", feature.Name). + Error("feature gate value not a bool") + continue + } + + featureEnabled = v + } + featureFlags[featureflag.FeatureFlag{Name: featureName}] = featureEnabled + } + + } + + return featureFlags, nil +} diff --git a/internal/gitlab/http_client_test.go b/internal/gitlab/http_client_test.go index a1702ce77..85ee7c37f 100644 --- a/internal/gitlab/http_client_test.go +++ b/internal/gitlab/http_client_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/config/prometheus" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/promtest" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" @@ -600,3 +601,160 @@ func TestAccess_postReceive(t *testing.T) { }) } } + +func TestFeatures(t *testing.T) { + testCases := []struct { + desc string + features []Feature + expectedFeatureFlags map[featureflag.FeatureFlag]bool + }{ + { + desc: "one feature flag", + features: []Feature{ + { + Name: "gitaly_1", + State: "off", + }, + { + Name: "gitaly_2", + State: "on", + Gates: []FeatureGate{ + { + Key: "boolean", + Value: true, + }, + }, + }, + { + Name: "gitaly_3", + State: "on", + Gates: []FeatureGate{ + { + Key: "boolean", + Value: false, + }, + }, + }, + }, + expectedFeatureFlags: map[featureflag.FeatureFlag]bool{ + featureflag.FeatureFlag{Name: "1"}: false, + featureflag.FeatureFlag{Name: "2"}: true, + featureflag.FeatureFlag{Name: "3"}: false, + }, + }, + { + desc: "all feature flags disabled", + features: []Feature{ + { + Name: "gitaly_1", + State: "off", + }, + { + Name: "gitaly_2", + State: "off", + }, + { + Name: "gitaly_3", + State: "off", + Gates: []FeatureGate{ + { + Key: "boolean", + Value: false, + }, + }, + }, + }, + expectedFeatureFlags: map[featureflag.FeatureFlag]bool{ + featureflag.FeatureFlag{Name: "1"}: false, + featureflag.FeatureFlag{Name: "2"}: false, + featureflag.FeatureFlag{Name: "3"}: false, + }, + }, + { + desc: "undecided", + features: []Feature{ + { + Name: "gitaly_1", + State: "on", + Gates: []FeatureGate{ + { + Key: "boolean", + Value: true, + }, + { + Key: "percentage_of_time", + Value: 20, + }, + }, + }, + { + Name: "gitaly_2", + State: "on", + Gates: []FeatureGate{ + { + Key: "percentage_of_actorsp", + Value: 10, + }, + }, + }, + }, + expectedFeatureFlags: map[featureflag.FeatureFlag]bool{}, + }, + { + desc: "unknown state", + features: []Feature{ + { + Name: "gitaly_1", + State: "on or off", + Gates: []FeatureGate{ + { + Key: "boolean", + Value: true, + }, + { + Key: "percentage_of_time", + Value: 20, + }, + }, + }, + }, + expectedFeatureFlags: map[featureflag.FeatureFlag]bool{}, + }, + } + + secretToken := "topsecret" + tempDir := testhelper.TempDir(t) + WriteShellSecretFile(t, tempDir, secretToken) + secretFilePath := filepath.Join(tempDir, ".gitlab_shell_secret") + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + serverURL, cleanup := NewTestServer(t, TestServerOptions{ + ServerKeyPath: "testdata/certs/server.key", + SecretToken: secretToken, + Features: tc.features, + }) + defer cleanup() + + c, err := NewHTTPClient( + testhelper.NewDiscardingLogger(t), + config.Gitlab{ + URL: serverURL, + SecretFile: secretFilePath, + HTTPSettings: config.HTTPSettings{ + CAFile: "testdata/certs/server.crt", + }, + }, + config.TLS{}, + prometheus.Config{}, + ) + require.NoError(t, err) + + ctx := testhelper.Context(t) + + features, err := c.Features(ctx) + require.NoError(t, err) + require.Equal(t, tc.expectedFeatureFlags, features) + }) + } +} diff --git a/internal/gitlab/mock_client.go b/internal/gitlab/mock_client.go index fd3a7e023..db1424fea 100644 --- a/internal/gitlab/mock_client.go +++ b/internal/gitlab/mock_client.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" ) var ( @@ -23,6 +24,12 @@ var ( MockPostReceive = func(context.Context, string, string, string, ...string) (bool, []PostReceiveMessage, error) { return true, nil, nil } + + // MockFeatures is a callback for the MockClient's `Features()` function + // which always returns nothing. + MockFeatures = func(ctx context.Context) (map[featureflag.FeatureFlag]bool, error) { + return nil, nil + } ) // MockClient is a mock client of the internal GitLab API. @@ -31,6 +38,7 @@ type MockClient struct { allowed func(context.Context, AllowedParams) (bool, string, error) preReceive func(context.Context, string) (bool, error) postReceive func(context.Context, string, string, string, ...string) (bool, []PostReceiveMessage, error) + features func(context.Context) (map[featureflag.FeatureFlag]bool, error) } // NewMockClient returns a new mock client for the internal GitLab API. @@ -39,12 +47,14 @@ func NewMockClient( allowed func(context.Context, AllowedParams) (bool, string, error), preReceive func(context.Context, string) (bool, error), postReceive func(context.Context, string, string, string, ...string) (bool, []PostReceiveMessage, error), + features func(context.Context) (map[featureflag.FeatureFlag]bool, error), ) Client { return &MockClient{ tb: tb, allowed: allowed, preReceive: preReceive, postReceive: postReceive, + features: features, } } @@ -75,3 +85,9 @@ func (m *MockClient) PostReceive(ctx context.Context, glRepository, glID, change require.NotNil(m.tb, m.postReceive, "postReceive called but not set") return m.postReceive(ctx, glRepository, glID, changes, gitPushOptions...) } + +// Features does nothing and always return an empty slice of Features. +func (m *MockClient) Features(ctx context.Context) (map[featureflag.FeatureFlag]bool, error) { + require.NotNil(m.tb, m.features, "features called but not set") + return m.features(ctx) +} diff --git a/internal/gitlab/test_server.go b/internal/gitlab/test_server.go index 4a3f62d35..97e12dd80 100644 --- a/internal/gitlab/test_server.go +++ b/internal/gitlab/test_server.go @@ -57,6 +57,7 @@ type TestServerOptions struct { ClientCACertPath string // used to verify client certs are valid ServerCertPath string ServerKeyPath string + Features []Feature } // NewTestServer returns a mock gitlab server that responds to the hook api endpoints @@ -64,12 +65,14 @@ func NewTestServer(t testing.TB, options TestServerOptions) (url string, cleanup t.Helper() mux := http.NewServeMux() - prefix := strings.TrimRight(options.RelativeURLRoot, "/") + "/api/v4/internal" - mux.Handle(prefix+"/allowed", http.HandlerFunc(handleAllowed(t, options))) - mux.Handle(prefix+"/pre_receive", http.HandlerFunc(handlePreReceive(t, options))) - mux.Handle(prefix+"/post_receive", http.HandlerFunc(handlePostReceive(options))) - mux.Handle(prefix+"/check", http.HandlerFunc(handleCheck(t, options))) - mux.Handle(prefix+"/lfs", http.HandlerFunc(handleLfs(t, options))) + prefix := strings.TrimRight(options.RelativeURLRoot, "/") + "/api/v4" + internalPrefix := prefix + "/internal" + mux.Handle(internalPrefix+"/allowed", http.HandlerFunc(handleAllowed(t, options))) + mux.Handle(internalPrefix+"/pre_receive", http.HandlerFunc(handlePreReceive(t, options))) + mux.Handle(internalPrefix+"/post_receive", http.HandlerFunc(handlePostReceive(options))) + mux.Handle(internalPrefix+"/check", http.HandlerFunc(handleCheck(t, options))) + mux.Handle(internalPrefix+"/lfs", http.HandlerFunc(handleLfs(t, options))) + mux.Handle(prefix+"/features", http.HandlerFunc(handleFeatures(t, options))) var tlsCfg *tls.Config if options.ClientCACertPath != "" { @@ -557,6 +560,17 @@ func handleLfs(t testing.TB, options TestServerOptions) func(w http.ResponseWrit } } +func handleFeatures(t testing.TB, options TestServerOptions) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not GET", http.StatusMethodNotAllowed) + return + } + + require.NoError(t, json.NewEncoder(w).Encode(options.Features)) + } +} + func startSocketHTTPServer(t testing.TB, mux *http.ServeMux, tlsCfg *tls.Config) (string, func()) { tempDir := testhelper.TempDir(t) diff --git a/internal/testhelper/testserver/gitaly.go b/internal/testhelper/testserver/gitaly.go index 2e8df3a60..56e2bda6d 100644 --- a/internal/testhelper/testserver/gitaly.go +++ b/internal/testhelper/testserver/gitaly.go @@ -261,7 +261,11 @@ func (gsd *gitalyServerDeps) createDependencies(t testing.TB, cfg config.Cfg, ru if gsd.gitlabClient == nil { gsd.gitlabClient = gitlab.NewMockClient( - t, gitlab.MockAllowed, gitlab.MockPreReceive, gitlab.MockPostReceive, + t, + gitlab.MockAllowed, + gitlab.MockPreReceive, + gitlab.MockPostReceive, + gitlab.MockFeatures, ) } |