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:
authorJohn Cai <jcai@gitlab.com>2022-06-09 18:09:11 +0300
committerJohn Cai <jcai@gitlab.com>2022-07-21 23:39:31 +0300
commite2ae10c6ddb2691ab26c54abf1306b40de10ba26 (patch)
tree2e54a3bf73fc2cc55c49f25d3b017d01c263ac0f
parent276d057f21e41c21204f7b83754071438c2cee85 (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.go6
-rw-r--r--cmd/gitaly-ssh/auth_test.go6
-rw-r--r--internal/gitaly/hook/postreceive_test.go16
-rw-r--r--internal/gitaly/hook/prereceive_test.go16
-rw-r--r--internal/gitaly/hook/transactions_test.go12
-rw-r--r--internal/gitaly/hook/update_test.go12
-rw-r--r--internal/gitaly/server/auth_test.go6
-rw-r--r--internal/gitaly/service/operations/branches_test.go8
-rw-r--r--internal/gitaly/service/operations/merge_test.go1
-rw-r--r--internal/gitlab/client.go25
-rw-r--r--internal/gitlab/http_client.go104
-rw-r--r--internal/gitlab/http_client_test.go158
-rw-r--r--internal/gitlab/mock_client.go16
-rw-r--r--internal/gitlab/test_server.go26
-rw-r--r--internal/testhelper/testserver/gitaly.go6
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,
)
}