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:
authorToon Claes <toon@gitlab.com>2022-03-01 18:12:16 +0300
committerToon Claes <toon@gitlab.com>2022-03-01 18:12:16 +0300
commit416988ddd41d192114142a828eb039fac450d084 (patch)
tree8334a0263aaf7993197e1ff84371a5d1f1029b2a
parent3e44644d1cb16a0f23f6a60b4d04a82e92885c2e (diff)
parenteb4a24b3aad97c1cd68c2e8b7e1bfd29e9212ff8 (diff)
Merge branch 'pks-git-execution-environment-constructors' into 'master'
git: Prepare for multiple bundled Git versions See merge request gitlab-org/gitaly!4372
-rw-r--r--internal/git/command_factory.go164
-rw-r--r--internal/git/command_factory_test.go46
-rw-r--r--internal/git/execution_environment.go264
-rw-r--r--internal/git/execution_environment_test.go253
-rw-r--r--internal/praefect/service/server/info.go2
5 files changed, 581 insertions, 148 deletions
diff --git a/internal/git/command_factory.go b/internal/git/command_factory.go
index 197f7b859..a8a9bdbf9 100644
--- a/internal/git/command_factory.go
+++ b/internal/git/command_factory.go
@@ -10,7 +10,6 @@ import (
"sync"
"github.com/prometheus/client_golang/prometheus"
- "github.com/sirupsen/logrus"
"gitlab.com/gitlab-org/gitaly/v14/internal/cgroups"
"gitlab.com/gitlab-org/gitaly/v14/internal/command"
"gitlab.com/gitlab-org/gitaly/v14/internal/git/alternates"
@@ -44,19 +43,6 @@ var globalOptions = []GlobalOption{
ConfigPair{Key: "core.useReplaceRefs", Value: "false"},
}
-type executionEnvironments struct {
- externalGit ExecutionEnvironment
- bundledGit ExecutionEnvironment
-}
-
-// ExecutionEnvironment describes the environment required to execute a Git command
-type ExecutionEnvironment struct {
- // BinaryPath is the path to the Git binary.
- BinaryPath string
- // EnvironmentVariables are variables which must be set when running the Git binary.
- EnvironmentVariables []string
-}
-
// CommandFactory is designed to create and run git commands in a protected and fully managed manner.
type CommandFactory interface {
// New creates a new command for the repo repository.
@@ -115,7 +101,7 @@ type cachedGitVersion struct {
type ExecCommandFactory struct {
locator storage.Locator
cfg config.Cfg
- execEnvs executionEnvironments
+ execEnvs []ExecutionEnvironment
cgroupsManager cgroups.Manager
invalidCommandsMetric *prometheus.CounterVec
hookDirs hookDirectories
@@ -178,125 +164,38 @@ func NewExecCommandFactory(cfg config.Cfg, opts ...ExecCommandFactoryOption) (_
// setupGitExecutionEnvironments assembles a Git execution environment that can be used to run Git
// commands. It warns if no path was specified in the configuration.
-func setupGitExecutionEnvironments(cfg config.Cfg, factoryCfg execCommandFactoryConfig) (executionEnvironments, func(), error) {
+func setupGitExecutionEnvironments(cfg config.Cfg, factoryCfg execCommandFactoryConfig) ([]ExecutionEnvironment, func(), error) {
if factoryCfg.gitBinaryPath != "" {
- return executionEnvironments{
- externalGit: ExecutionEnvironment{
- BinaryPath: factoryCfg.gitBinaryPath,
- },
+ return []ExecutionEnvironment{
+ {BinaryPath: factoryCfg.gitBinaryPath},
}, func() {}, nil
}
- binaryPath := cfg.Git.BinPath
- if override := os.Getenv("GITALY_TESTING_GIT_BINARY"); binaryPath == "" && override != "" {
- binaryPath = override
- }
-
- useBundledBinaries := cfg.Git.UseBundledBinaries
- if bundledGitPath := os.Getenv("GITALY_TESTING_BUNDLED_GIT_PATH"); bundledGitPath != "" {
- if cfg.BinDir == "" {
- return executionEnvironments{}, nil, errors.New("cannot use bundled binaries without bin path being set")
- }
-
- // We need to symlink pre-built Git binaries into Gitaly's binary directory.
- // Normally they would of course already exist there, but in tests we create a new
- // binary directory for each server and thus need to populate it first.
- for _, binary := range []string{"gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend"} {
- bundledGitBinary := filepath.Join(bundledGitPath, binary)
- if _, err := os.Stat(bundledGitBinary); err != nil {
- return executionEnvironments{}, nil, fmt.Errorf("statting %q: %w", binary, err)
- }
-
- if err := os.Symlink(bundledGitBinary, filepath.Join(cfg.BinDir, binary)); err != nil {
- // While Gitaly's Go tests use a temporary binary directory, Ruby
- // rspecs set up the binary directory to point to our build
- // directory. They thus already contain the Git binaries and don't
- // need symlinking.
- if errors.Is(err, os.ErrExist) {
- continue
- }
- return executionEnvironments{}, nil, fmt.Errorf("symlinking bundled %q: %w", binary, err)
- }
- }
-
- useBundledBinaries = true
- }
-
- var execEnvs executionEnvironments
- var cleanups []func()
-
- if binaryPath != "" {
- execEnvs.externalGit = ExecutionEnvironment{
- BinaryPath: binaryPath,
- }
- }
-
- if useBundledBinaries {
- // In order to support having a single Git binary only as compared to a complete Git
- // installation, we create our own GIT_EXEC_PATH which contains symlinks to the Git
- // binary for executables which Git expects to be present.
- gitExecPath, err := os.MkdirTemp("", "gitaly-git-exec-path-*")
+ var execEnvs []ExecutionEnvironment
+ for _, constructor := range ExecutionEnvironmentConstructors {
+ execEnv, err := constructor.Construct(cfg)
if err != nil {
- return executionEnvironments{}, nil, fmt.Errorf("creating Git exec path: %w", err)
- }
-
- for executable, target := range map[string]string{
- "git": "gitaly-git",
- "git-receive-pack": "gitaly-git",
- "git-upload-pack": "gitaly-git",
- "git-upload-archive": "gitaly-git",
- "git-http-backend": "gitaly-git-http-backend",
- "git-remote-http": "gitaly-git-remote-http",
- "git-remote-https": "gitaly-git-remote-http",
- "git-remote-ftp": "gitaly-git-remote-http",
- "git-remote-ftps": "gitaly-git-remote-http",
- } {
- if err := os.Symlink(
- filepath.Join(cfg.BinDir, target),
- filepath.Join(gitExecPath, executable),
- ); err != nil {
- return executionEnvironments{}, nil, fmt.Errorf("linking Git executable %q: %w", executable, err)
- }
- }
-
- cleanups = append(cleanups, func() {
- if err := os.RemoveAll(gitExecPath); err != nil {
- logrus.WithError(err).Error("cleanup of Git execution environment failed")
+ // In case the environment has not been configured by the user we simply
+ // skip it.
+ if errors.Is(err, ErrNotConfigured) {
+ continue
}
- })
- execEnvs.bundledGit = ExecutionEnvironment{
- BinaryPath: filepath.Join(gitExecPath, "git"),
- EnvironmentVariables: []string{
- "GIT_EXEC_PATH=" + gitExecPath,
- },
+ // But if it has been configured and we fail to set it up then it signifies
+ // a real error.
+ return nil, nil, fmt.Errorf("constructing Git environment: %w", err)
}
- }
- // If neither an external Git distribution nor a bundled Git was configured we need to
- // fall back to resolve the Git executable via PATH. We explicitly don't want to do this
- // in case either one of those has been configured though: we have no clue where system Git
- // comes from and what its version is, so it's the worst of all choices.
- if execEnvs.externalGit.BinaryPath == "" && execEnvs.bundledGit.BinaryPath == "" {
- resolvedPath, err := exec.LookPath("git")
- if err != nil {
- if errors.Is(err, exec.ErrNotFound) {
- return executionEnvironments{}, nil, fmt.Errorf(`"git" executable not found, set path to it in the configuration file or add it to the PATH`)
- }
- }
-
- logrus.WithFields(logrus.Fields{
- "resolvedPath": resolvedPath,
- }).Warn("git path not configured. Using default path resolution")
+ execEnvs = append(execEnvs, execEnv)
+ }
- execEnvs.externalGit = ExecutionEnvironment{
- BinaryPath: resolvedPath,
- }
+ if len(execEnvs) == 0 {
+ return nil, nil, fmt.Errorf("could not set up any Git execution environments")
}
return execEnvs, func() {
- for i := len(cleanups) - 1; i >= 0; i-- {
- cleanups[i]()
+ for _, execEnv := range execEnvs {
+ execEnv.Cleanup()
}
}, nil
}
@@ -335,21 +234,18 @@ func (cf *ExecCommandFactory) NewWithDir(ctx context.Context, dir string, sc Cmd
// GetExecutionEnvironment returns parameters required to execute Git commands.
func (cf *ExecCommandFactory) GetExecutionEnvironment(ctx context.Context) ExecutionEnvironment {
- switch {
- case cf.execEnvs.bundledGit.BinaryPath != "" && cf.execEnvs.externalGit.BinaryPath != "":
- if featureflag.UseBundledGit.IsEnabled(ctx) {
- return cf.execEnvs.bundledGit
+ // We first go through all execution environments and check whether any of them is enabled
+ // in the current context, which most importantly will check their respective feature flags.
+ for _, execEnv := range cf.execEnvs {
+ if execEnv.IsEnabled(ctx) {
+ return execEnv
}
- return cf.execEnvs.externalGit
- case cf.execEnvs.bundledGit.BinaryPath != "":
- return cf.execEnvs.bundledGit
- case cf.execEnvs.externalGit.BinaryPath != "":
- return cf.execEnvs.externalGit
- default:
- // This shouldn't ever happen given that `setupGitExecutionEnvironments()` returns
- // an error in case no environment was found.
- panic("no Git execution environment defined")
}
+
+ // If none is enabled though, we simply use the first execution environment, which is also
+ // the one with highest priority. This can for example happen in case we only were able to
+ // construct a single execution environment that is currently feature flagged.
+ return cf.execEnvs[0]
}
// HooksPath returns the path where Gitaly's Git hooks reside.
diff --git a/internal/git/command_factory_test.go b/internal/git/command_factory_test.go
index 385d21459..f960c2c2e 100644
--- a/internal/git/command_factory_test.go
+++ b/internal/git/command_factory_test.go
@@ -155,7 +155,12 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) {
gitCmdFactory, cleanup, err := git.NewExecCommandFactory(cfg, git.WithSkipHooks())
require.NoError(t, err)
defer cleanup()
- require.Equal(t, expectedExecEnv, gitCmdFactory.GetExecutionEnvironment(ctx))
+
+ // We need to compare the execution environments manually because they also have
+ // some private variables which we cannot easily check here.
+ actualExecEnv := gitCmdFactory.GetExecutionEnvironment(ctx)
+ require.Equal(t, expectedExecEnv.BinaryPath, actualExecEnv.BinaryPath)
+ require.Equal(t, expectedExecEnv.EnvironmentVariables, actualExecEnv.EnvironmentVariables)
}
t.Run("set in config", func(t *testing.T) {
@@ -176,21 +181,32 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) {
t.Run("set using GITALY_TESTING_BUNDLED_GIT_PATH", func(t *testing.T) {
bundledGitDir := testhelper.TempDir(t)
- bundledGitExecutable := filepath.Join(bundledGitDir, "gitaly-git")
- bundledGitRemoteExecutable := filepath.Join(bundledGitDir, "gitaly-git-remote-http")
- bundledGitHTTPBackendExecutable := filepath.Join(bundledGitDir, "gitaly-git-http-backend")
+
+ var bundledGitConstructors []git.BundledGitEnvironmentConstructor
+ for _, constructor := range git.ExecutionEnvironmentConstructors {
+ bundledGitConstructor, ok := constructor.(git.BundledGitEnvironmentConstructor)
+ if ok {
+ bundledGitConstructors = append(bundledGitConstructors, bundledGitConstructor)
+ }
+ }
+ require.NotEmpty(t, bundledGitConstructors)
+
+ suffix := bundledGitConstructors[0].Suffix
+
+ bundledGitExecutable := filepath.Join(bundledGitDir, "gitaly-git"+suffix)
+ bundledGitRemoteExecutable := filepath.Join(bundledGitDir, "gitaly-git-remote-http"+suffix)
testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitDir)
t.Run("missing bin_dir", func(t *testing.T) {
_, _, err := git.NewExecCommandFactory(config.Cfg{Git: config.Git{}}, git.WithSkipHooks())
- require.EqualError(t, err, "setting up Git execution environment: cannot use bundled binaries without bin path being set")
+ require.EqualError(t, err, "setting up Git execution environment: constructing Git environment: cannot use bundled binaries without bin path being set")
})
t.Run("missing gitaly-git executable", func(t *testing.T) {
_, _, err := git.NewExecCommandFactory(config.Cfg{BinDir: testhelper.TempDir(t)}, git.WithSkipHooks())
require.Error(t, err)
- require.Contains(t, err.Error(), `statting "gitaly-git":`)
+ require.Contains(t, err.Error(), fmt.Sprintf(`statting "gitaly-git%s":`, suffix))
require.True(t, errors.Is(err, os.ErrNotExist))
})
@@ -199,7 +215,7 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) {
_, _, err := git.NewExecCommandFactory(config.Cfg{BinDir: testhelper.TempDir(t)}, git.WithSkipHooks())
require.Error(t, err)
- require.Contains(t, err.Error(), "statting \"gitaly-git-remote-http\":")
+ require.Contains(t, err.Error(), fmt.Sprintf("statting \"gitaly-git-remote-http%s\":", suffix))
require.True(t, errors.Is(err, os.ErrNotExist))
})
@@ -209,14 +225,16 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) {
_, _, err := git.NewExecCommandFactory(config.Cfg{BinDir: testhelper.TempDir(t)}, git.WithSkipHooks())
require.Error(t, err)
- require.Contains(t, err.Error(), "statting \"gitaly-git-http-backend\":")
+ require.Contains(t, err.Error(), fmt.Sprintf("statting \"gitaly-git-http-backend%s\":", suffix))
require.True(t, errors.Is(err, os.ErrNotExist))
})
t.Run("bin_dir with executables", func(t *testing.T) {
- require.NoError(t, os.WriteFile(bundledGitExecutable, []byte{}, 0o777))
- require.NoError(t, os.WriteFile(bundledGitRemoteExecutable, []byte{}, 0o777))
- require.NoError(t, os.WriteFile(bundledGitHTTPBackendExecutable, []byte{}, 0o777))
+ for _, bundledGitConstructor := range bundledGitConstructors {
+ for _, gitBinary := range []string{"gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend"} {
+ require.NoError(t, os.WriteFile(filepath.Join(bundledGitDir, gitBinary+bundledGitConstructor.Suffix), []byte{}, 0o777))
+ }
+ }
binDir := testhelper.TempDir(t)
@@ -232,14 +250,14 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) {
// executable in Gitaly's BinDir.
target, err := os.Readlink(symlinkPath)
require.NoError(t, err)
- require.Equal(t, filepath.Join(binDir, "gitaly-"+executable), target)
+ require.Equal(t, filepath.Join(binDir, "gitaly-"+executable+suffix), target)
// And in a test setup, the symlink in Gitaly's BinDir must point to
// the Git binary pointed to by the GITALY_TESTING_BUNDLED_GIT_PATH
// environment variable.
target, err = os.Readlink(target)
require.NoError(t, err)
- require.Equal(t, filepath.Join(bundledGitDir, "gitaly-"+executable), target)
+ require.Equal(t, filepath.Join(bundledGitDir, "gitaly-"+executable+suffix), target)
}
})
})
@@ -257,7 +275,7 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) {
testhelper.ModifyEnvironment(t, "PATH", "")
_, _, err := git.NewExecCommandFactory(config.Cfg{}, git.WithSkipHooks())
- require.EqualError(t, err, `setting up Git execution environment: "git" executable not found, set path to it in the configuration file or add it to the PATH`)
+ require.EqualError(t, err, "setting up Git execution environment: could not set up any Git execution environments")
})
}
diff --git a/internal/git/execution_environment.go b/internal/git/execution_environment.go
new file mode 100644
index 000000000..d140c6f44
--- /dev/null
+++ b/internal/git/execution_environment.go
@@ -0,0 +1,264 @@
+package git
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/sirupsen/logrus"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/metadata/featureflag"
+ "golang.org/x/sys/unix"
+)
+
+var (
+ // ErrNotConfigured may be returned by an ExecutionEnvironmentConstructor in case an
+ // environment was not configured.
+ ErrNotConfigured = errors.New("execution environment is not configured")
+
+ // ExecutionEnvironmentConstructors is the list of Git environments supported by the Git
+ // command factory. The order is important and signifies the priority in which the
+ // environments will be used: the environment created by the first constructor is the one
+ // that will be preferred when executing Git commands. Later environments may be used in
+ // case `IsEnabled()` returns `false` though.
+ ExecutionEnvironmentConstructors = []ExecutionEnvironmentConstructor{
+ BundledGitEnvironmentConstructor{
+ // This is the current default bundled Git environment, which does not yet
+ // have a version suffix.
+ Suffix: "",
+ FeatureFlags: []featureflag.FeatureFlag{
+ featureflag.UseBundledGit,
+ },
+ },
+ DistributedGitEnvironmentConstructor{},
+ FallbackGitEnvironmentConstructor{},
+ }
+)
+
+// ExecutionEnvironmentConstructor is an interface for constructors of Git execution environments.
+// A constructor should be able to set up an environment in which it is possible to run Git
+// executables.
+type ExecutionEnvironmentConstructor interface {
+ Construct(config.Cfg) (ExecutionEnvironment, error)
+}
+
+// ExecutionEnvironment describes the environment required to execute a Git command
+type ExecutionEnvironment struct {
+ // BinaryPath is the path to the Git binary.
+ BinaryPath string
+ // EnvironmentVariables are variables which must be set when running the Git binary.
+ EnvironmentVariables []string
+
+ isEnabled func(context.Context) bool
+ cleanup func()
+}
+
+// Cleanup cleans up any state set up by this ExecutionEnvironment.
+func (e ExecutionEnvironment) Cleanup() {
+ if e.cleanup != nil {
+ e.cleanup()
+ }
+}
+
+// IsEnabled checks whether the ExecutionEnvironment is enabled in the given context. An execution
+// environment will typically be enabled by default, except if it's feature-flagged.
+func (e ExecutionEnvironment) IsEnabled(ctx context.Context) bool {
+ if e.isEnabled != nil {
+ return e.isEnabled(ctx)
+ }
+
+ return true
+}
+
+// DistributedGitEnvironmentConstructor creates ExecutionEnvironments via the Git binary path
+// configured in the Gitaly configuration. This expects a complete Git installation with all its
+// components. The installed distribution must either have its prefix compiled into the binaries or
+// alternatively be compiled with runtime-detection of the prefix such that Git is able to locate
+// its auxiliary helper binaries correctly.
+type DistributedGitEnvironmentConstructor struct{}
+
+// Construct sets up an ExecutionEnvironment for a complete Git distribution. No setup needs to be
+// performed given that the Git environment is expected to be self-contained.
+//
+// For testing purposes, this function overrides the configured Git binary path if the
+// `GITALY_TESTING_GIT_BINARY` environment variable is set.
+func (c DistributedGitEnvironmentConstructor) Construct(cfg config.Cfg) (ExecutionEnvironment, error) {
+ binaryPath := cfg.Git.BinPath
+ if override := os.Getenv("GITALY_TESTING_GIT_BINARY"); binaryPath == "" && override != "" {
+ binaryPath = override
+ }
+
+ if binaryPath == "" {
+ return ExecutionEnvironment{}, ErrNotConfigured
+ }
+
+ return ExecutionEnvironment{
+ BinaryPath: binaryPath,
+ }, nil
+}
+
+// BundledGitEnvironmentConstructor sets up an ExecutionEnvironment for a bundled Git installation.
+// Bundled Git is a partial Git installation, where only a subset of Git binaries are installed
+// into Gitaly's binary directory. The binaries must have a `gitaly-` prefix like e.g. `gitaly-git`.
+// Bundled Git installations can be installed with Gitaly's Makefile via `make install
+// WITH_BUNDLED_GIT=YesPlease`.
+type BundledGitEnvironmentConstructor struct {
+ // Suffix is the version suffix used for this specific bundled Git environment. In case
+ // multiple sets of bundled Git versions are installed it is possible to also have multiple
+ // of these bundled Git environments with different suffixes.
+ Suffix string
+ // FeatureFlags is the set of feature flags which must be enabled in order for the bundled
+ // Git environment to be enabled. Note that _all_ feature flags must be set to `true` in the
+ // context.
+ FeatureFlags []featureflag.FeatureFlag
+}
+
+// Construct sets up an ExecutionEnvironment for a bundled Git installation. Because bundled Git
+// installations are not complete Git installations we need to set up a usable environment at
+// runtime. This is done by creating a temporary directory into which we symlink the bundled
+// binaries with their usual names as expected by Git. Furthermore, we configure the GIT_EXEC_PATH
+// environment variable to point to that directory such that Git is able to locate its auxiliary
+// binaries.
+//
+// For testing purposes, this function will automatically enable use of bundled Git in case the
+// `GITALY_TESTING_BUNDLED_GIT_PATH` environment variable is set.
+func (c BundledGitEnvironmentConstructor) Construct(cfg config.Cfg) (_ ExecutionEnvironment, returnedErr error) {
+ useBundledBinaries := cfg.Git.UseBundledBinaries
+
+ if bundledGitPath := os.Getenv("GITALY_TESTING_BUNDLED_GIT_PATH"); bundledGitPath != "" {
+ if cfg.BinDir == "" {
+ return ExecutionEnvironment{}, errors.New("cannot use bundled binaries without bin path being set")
+ }
+
+ // We need to symlink pre-built Git binaries into Gitaly's binary directory.
+ // Normally they would of course already exist there, but in tests we create a new
+ // binary directory for each server and thus need to populate it first.
+ for _, binary := range []string{"gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend"} {
+ binary := binary + c.Suffix
+
+ bundledGitBinary := filepath.Join(bundledGitPath, binary)
+ if _, err := os.Stat(bundledGitBinary); err != nil {
+ return ExecutionEnvironment{}, fmt.Errorf("statting %q: %w", binary, err)
+ }
+
+ if err := os.Symlink(bundledGitBinary, filepath.Join(cfg.BinDir, binary)); err != nil {
+ // While Gitaly's Go tests use a temporary binary directory, Ruby
+ // rspecs set up the binary directory to point to our build
+ // directory. They thus already contain the Git binaries and don't
+ // need symlinking.
+ if errors.Is(err, os.ErrExist) {
+ continue
+ }
+ return ExecutionEnvironment{}, fmt.Errorf("symlinking bundled %q: %w", binary, err)
+ }
+ }
+
+ useBundledBinaries = true
+ }
+
+ if !useBundledBinaries {
+ return ExecutionEnvironment{}, ErrNotConfigured
+ }
+
+ if cfg.BinDir == "" {
+ return ExecutionEnvironment{}, errors.New("cannot use bundled binaries without bin path being set")
+ }
+
+ // In order to support having a single Git binary only as compared to a complete Git
+ // installation, we create our own GIT_EXEC_PATH which contains symlinks to the Git
+ // binary for executables which Git expects to be present.
+ gitExecPath, err := os.MkdirTemp("", "gitaly-git-exec-path-*")
+ if err != nil {
+ return ExecutionEnvironment{}, fmt.Errorf("creating Git exec path: %w", err)
+ }
+
+ cleanup := func() {
+ if err := os.RemoveAll(gitExecPath); err != nil {
+ logrus.WithError(err).Error("cleanup of Git execution environment failed")
+ }
+ }
+ defer func() {
+ if returnedErr != nil {
+ cleanup()
+ }
+ }()
+
+ for executable, target := range map[string]string{
+ "git": "gitaly-git",
+ "git-receive-pack": "gitaly-git",
+ "git-upload-pack": "gitaly-git",
+ "git-upload-archive": "gitaly-git",
+ "git-http-backend": "gitaly-git-http-backend",
+ "git-remote-http": "gitaly-git-remote-http",
+ "git-remote-https": "gitaly-git-remote-http",
+ "git-remote-ftp": "gitaly-git-remote-http",
+ "git-remote-ftps": "gitaly-git-remote-http",
+ } {
+ target := target + c.Suffix
+ targetPath := filepath.Join(cfg.BinDir, target)
+
+ if err := unix.Access(targetPath, unix.X_OK); err != nil {
+ return ExecutionEnvironment{}, fmt.Errorf("checking bundled Git binary %q: %w", target, err)
+ }
+
+ if err := os.Symlink(
+ targetPath,
+ filepath.Join(gitExecPath, executable),
+ ); err != nil {
+ return ExecutionEnvironment{}, fmt.Errorf("linking Git executable %q: %w", executable, err)
+ }
+ }
+
+ return ExecutionEnvironment{
+ BinaryPath: filepath.Join(gitExecPath, "git"),
+ EnvironmentVariables: []string{
+ "GIT_EXEC_PATH=" + gitExecPath,
+ },
+ cleanup: cleanup,
+ isEnabled: func(ctx context.Context) bool {
+ for _, flag := range c.FeatureFlags {
+ if flag.IsDisabled(ctx) {
+ return false
+ }
+ }
+
+ return true
+ },
+ }, nil
+}
+
+// FallbackGitEnvironmentConstructor sets up a fallback execution environment where Git is resolved
+// via the `PATH` environment variable. This is only intended as a last resort in case no other
+// environments have been set up.
+type FallbackGitEnvironmentConstructor struct{}
+
+// Construct sets up an execution environment by searching `PATH` for a `git` executable.
+func (c FallbackGitEnvironmentConstructor) Construct(config.Cfg) (ExecutionEnvironment, error) {
+ resolvedPath, err := exec.LookPath("git")
+ if err != nil {
+ if errors.Is(err, exec.ErrNotFound) {
+ return ExecutionEnvironment{}, fmt.Errorf("%w: no git executable found in PATH", ErrNotConfigured)
+ }
+
+ return ExecutionEnvironment{}, fmt.Errorf("resolving git executable: %w", err)
+ }
+
+ logrus.WithFields(logrus.Fields{
+ "resolvedPath": resolvedPath,
+ }).Warn("git path not configured. Using default path resolution")
+
+ return ExecutionEnvironment{
+ BinaryPath: resolvedPath,
+ // We always pretend that this environment is disabled. This has the effect that
+ // even if all the other existing execution environments are disabled via feature
+ // flags, we will still not use the fallback Git environment but will instead use
+ // the feature flagged environments. The fallback will then only be used in the
+ // case it is the only existing environment with no other better alternatives.
+ isEnabled: func(context.Context) bool {
+ return false
+ },
+ }, nil
+}
diff --git a/internal/git/execution_environment_test.go b/internal/git/execution_environment_test.go
new file mode 100644
index 000000000..e3775c113
--- /dev/null
+++ b/internal/git/execution_environment_test.go
@@ -0,0 +1,253 @@
+package git_test
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/git"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper"
+)
+
+func TestDistributedGitEnvironmentConstructor(t *testing.T) {
+ constructor := git.DistributedGitEnvironmentConstructor{}
+
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "")
+
+ t.Run("empty configuration fails", func(t *testing.T) {
+ _, err := constructor.Construct(config.Cfg{})
+ require.Equal(t, git.ErrNotConfigured, err)
+ })
+
+ t.Run("configuration with Git binary path succeeds", func(t *testing.T) {
+ execEnv, err := constructor.Construct(config.Cfg{
+ Git: config.Git{
+ BinPath: "/foo/bar",
+ },
+ })
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ require.Equal(t, "/foo/bar", execEnv.BinaryPath)
+ require.Equal(t, []string(nil), execEnv.EnvironmentVariables)
+ })
+
+ t.Run("empty configuration with environment override", func(t *testing.T) {
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "/foo/bar")
+
+ execEnv, err := constructor.Construct(config.Cfg{})
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ require.Equal(t, "/foo/bar", execEnv.BinaryPath)
+ require.Equal(t, []string(nil), execEnv.EnvironmentVariables)
+ })
+
+ t.Run("configuration overrides environment variable", func(t *testing.T) {
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "envvar")
+
+ execEnv, err := constructor.Construct(config.Cfg{
+ Git: config.Git{
+ BinPath: "config",
+ },
+ })
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ require.Equal(t, "config", execEnv.BinaryPath)
+ require.Equal(t, []string(nil), execEnv.EnvironmentVariables)
+ })
+}
+
+func TestBundledGitEnvironmentConstructor(t *testing.T) {
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "")
+
+ constructor := git.BundledGitEnvironmentConstructor{}
+
+ seedDirWithExecutables := func(t *testing.T, executableNames ...string) string {
+ dir := testhelper.TempDir(t)
+ for _, executableName := range executableNames {
+ require.NoError(t, os.WriteFile(filepath.Join(dir, executableName), nil, 0o777))
+ }
+ return dir
+ }
+
+ t.Run("disabled bundled Git fails", func(t *testing.T) {
+ _, err := constructor.Construct(config.Cfg{})
+ require.Equal(t, git.ErrNotConfigured, err)
+ })
+
+ t.Run("bundled Git without binary directory fails", func(t *testing.T) {
+ _, err := constructor.Construct(config.Cfg{
+ Git: config.Git{
+ UseBundledBinaries: true,
+ },
+ })
+ require.Equal(t, errors.New("cannot use bundled binaries without bin path being set"), err)
+ })
+
+ t.Run("incomplete binary directory succeeds", func(t *testing.T) {
+ _, err := constructor.Construct(config.Cfg{
+ BinDir: seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http"),
+ Git: config.Git{
+ UseBundledBinaries: true,
+ },
+ })
+ require.Error(t, err)
+ require.Equal(t, "checking bundled Git binary \"gitaly-git-http-backend\": no such file or directory", err.Error())
+ })
+
+ t.Run("complete binary directory succeeds", func(t *testing.T) {
+ binDir := seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend")
+
+ execEnv, err := constructor.Construct(config.Cfg{
+ BinDir: binDir,
+ Git: config.Git{
+ UseBundledBinaries: true,
+ },
+ })
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ // We create a temporary directory where the symlinks are created, and we cannot
+ // predict its exact path.
+ require.Equal(t, "git", filepath.Base(execEnv.BinaryPath))
+
+ execPrefix := filepath.Dir(execEnv.BinaryPath)
+ require.Equal(t, []string{
+ "GIT_EXEC_PATH=" + execPrefix,
+ }, execEnv.EnvironmentVariables)
+
+ for _, binary := range []string{"git", "git-remote-http", "git-http-backend"} {
+ target, err := filepath.EvalSymlinks(filepath.Join(execPrefix, binary))
+ require.NoError(t, err)
+ require.Equal(t, filepath.Join(binDir, "gitaly-"+binary), target)
+ }
+ })
+
+ t.Run("cleanup removes temporary directory", func(t *testing.T) {
+ execEnv, err := constructor.Construct(config.Cfg{
+ BinDir: seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend"),
+ Git: config.Git{
+ UseBundledBinaries: true,
+ },
+ })
+ require.NoError(t, err)
+
+ execPrefix := filepath.Dir(execEnv.BinaryPath)
+ require.DirExists(t, execPrefix)
+
+ execEnv.Cleanup()
+
+ require.NoDirExists(t, execPrefix)
+ })
+
+ t.Run("bundled Git path without binary directory fails", func(t *testing.T) {
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "/does/not/exist")
+ _, err := constructor.Construct(config.Cfg{})
+ require.Equal(t, errors.New("cannot use bundled binaries without bin path being set"), err)
+ })
+
+ t.Run("nonexistent bundled Git path via environment fails", func(t *testing.T) {
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "/does/not/exist")
+ _, err := constructor.Construct(config.Cfg{
+ BinDir: testhelper.TempDir(t),
+ })
+ require.Error(t, err)
+ require.Equal(t, err.Error(), "statting \"gitaly-git\": stat /does/not/exist/gitaly-git: no such file or directory")
+ })
+
+ t.Run("incomplete bundled Git environment fails", func(t *testing.T) {
+ bundledGitPath := seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http")
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitPath)
+
+ _, err := constructor.Construct(config.Cfg{
+ BinDir: testhelper.TempDir(t),
+ })
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "statting \"gitaly-git-http-backend\": ")
+ })
+
+ t.Run("complete bundled Git environment populates binary directory", func(t *testing.T) {
+ bundledGitPath := seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend")
+ testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitPath)
+
+ execEnv, err := constructor.Construct(config.Cfg{
+ BinDir: testhelper.TempDir(t),
+ })
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ require.Equal(t, "git", filepath.Base(execEnv.BinaryPath))
+ execPrefix := filepath.Dir(execEnv.BinaryPath)
+
+ require.Equal(t, []string{
+ "GIT_EXEC_PATH=" + execPrefix,
+ }, execEnv.EnvironmentVariables)
+
+ for _, binary := range []string{"git", "git-remote-http", "git-http-backend"} {
+ target, err := filepath.EvalSymlinks(filepath.Join(execPrefix, binary))
+ require.NoError(t, err)
+ require.Equal(t, filepath.Join(bundledGitPath, "gitaly-"+binary), target)
+ }
+ })
+
+ t.Run("with version suffix", func(t *testing.T) {
+ constructor := git.BundledGitEnvironmentConstructor{
+ Suffix: "-v2.35.1",
+ }
+
+ binDir := seedDirWithExecutables(t, "gitaly-git-v2.35.1", "gitaly-git-remote-http-v2.35.1", "gitaly-git-http-backend-v2.35.1")
+
+ execEnv, err := constructor.Construct(config.Cfg{
+ BinDir: binDir,
+ Git: config.Git{
+ UseBundledBinaries: true,
+ },
+ })
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ require.Equal(t, "git", filepath.Base(execEnv.BinaryPath))
+ execPrefix := filepath.Dir(execEnv.BinaryPath)
+ require.Equal(t, []string{
+ "GIT_EXEC_PATH=" + execPrefix,
+ }, execEnv.EnvironmentVariables)
+
+ for _, binary := range []string{"git", "git-remote-http", "git-http-backend"} {
+ target, err := filepath.EvalSymlinks(filepath.Join(execPrefix, binary))
+ require.NoError(t, err)
+ require.Equal(t, filepath.Join(binDir, "gitaly-"+binary+"-v2.35.1"), target)
+ }
+ })
+}
+
+func TestFallbackGitEnvironmentConstructor(t *testing.T) {
+ constructor := git.FallbackGitEnvironmentConstructor{}
+
+ t.Run("failing lookup of executable causes failure", func(t *testing.T) {
+ testhelper.ModifyEnvironment(t, "PATH", "/does/not/exist")
+
+ _, err := constructor.Construct(config.Cfg{})
+ require.Equal(t, fmt.Errorf("%w: no git executable found in PATH", git.ErrNotConfigured), err)
+ })
+
+ t.Run("successfully resolved executable", func(t *testing.T) {
+ tempDir := testhelper.TempDir(t)
+ gitPath := filepath.Join(tempDir, "git")
+ require.NoError(t, os.WriteFile(gitPath, nil, 0o755))
+
+ testhelper.ModifyEnvironment(t, "PATH", "/does/not/exist:"+tempDir)
+
+ execEnv, err := constructor.Construct(config.Cfg{})
+ require.NoError(t, err)
+ defer execEnv.Cleanup()
+
+ require.Equal(t, gitPath, execEnv.BinaryPath)
+ require.Equal(t, []string(nil), execEnv.EnvironmentVariables)
+ })
+}
diff --git a/internal/praefect/service/server/info.go b/internal/praefect/service/server/info.go
index 1ca09a871..841860b52 100644
--- a/internal/praefect/service/server/info.go
+++ b/internal/praefect/service/server/info.go
@@ -6,6 +6,7 @@ import (
"github.com/google/uuid"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
+ "gitlab.com/gitlab-org/gitaly/v14/internal/metadata"
"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
"google.golang.org/grpc"
)
@@ -29,6 +30,7 @@ func (s *Server) ServerInfo(ctx context.Context, in *gitalypb.ServerInfoRequest)
var wg sync.WaitGroup
storageStatuses := make([]*gitalypb.ServerInfoResponse_StorageStatus, len(s.conns))
+ ctx = metadata.IncomingToOutgoing(ctx)
i := 0
for virtualStorage, storages := range s.conns {