diff options
author | Toon Claes <toon@gitlab.com> | 2022-03-01 18:12:16 +0300 |
---|---|---|
committer | Toon Claes <toon@gitlab.com> | 2022-03-01 18:12:16 +0300 |
commit | 416988ddd41d192114142a828eb039fac450d084 (patch) | |
tree | 8334a0263aaf7993197e1ff84371a5d1f1029b2a | |
parent | 3e44644d1cb16a0f23f6a60b4d04a82e92885c2e (diff) | |
parent | eb4a24b3aad97c1cd68c2e8b7e1bfd29e9212ff8 (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.go | 164 | ||||
-rw-r--r-- | internal/git/command_factory_test.go | 46 | ||||
-rw-r--r-- | internal/git/execution_environment.go | 264 | ||||
-rw-r--r-- | internal/git/execution_environment_test.go | 253 | ||||
-rw-r--r-- | internal/praefect/service/server/info.go | 2 |
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 { |