diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2022-05-30 10:00:00 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2022-05-30 10:00:00 +0300 |
commit | 469ff7a3c1cab392d337389b77ced0a4fbd82bdb (patch) | |
tree | 020da651a0df0f9d5e54d3ba8cbb6e38e09ff3b1 | |
parent | a6c5964bb455f77a0c79898f0b99c4c7df3aee3a (diff) | |
parent | 21d274062414d4a9c8e693647c702aeee7d9b21a (diff) |
Merge branch 'pks-lfs-smudge-refactor' into 'master'
gitaly-lfs-smudge: Refactor code to be more readily extensible
See merge request gitlab-org/gitaly!4587
-rw-r--r-- | cmd/gitaly-lfs-smudge/lfs_smudge.go | 87 | ||||
-rw-r--r-- | cmd/gitaly-lfs-smudge/lfs_smudge_test.go | 184 | ||||
-rw-r--r-- | cmd/gitaly-lfs-smudge/main.go | 47 | ||||
-rw-r--r-- | cmd/gitaly-lfs-smudge/main_test.go | 211 | ||||
-rw-r--r-- | cmd/gitaly-lfs-smudge/testhelper_test.go | 29 | ||||
-rw-r--r-- | internal/git/smudge/config.go | 79 | ||||
-rw-r--r-- | internal/git/smudge/config_test.go | 173 | ||||
-rw-r--r-- | internal/gitaly/config/config.go | 4 | ||||
-rw-r--r-- | internal/gitaly/service/repository/archive.go | 32 | ||||
-rw-r--r-- | internal/gitaly/service/repository/archive_test.go | 16 | ||||
-rw-r--r-- | internal/helper/env/env.go | 15 | ||||
-rw-r--r-- | internal/helper/env/env_test.go | 57 |
12 files changed, 694 insertions, 240 deletions
diff --git a/cmd/gitaly-lfs-smudge/lfs_smudge.go b/cmd/gitaly-lfs-smudge/lfs_smudge.go index b82e38bb8..99b79175a 100644 --- a/cmd/gitaly-lfs-smudge/lfs_smudge.go +++ b/cmd/gitaly-lfs-smudge/lfs_smudge.go @@ -2,69 +2,43 @@ package main import ( "context" - "encoding/json" - "errors" "fmt" "io" "net/url" - "path/filepath" "github.com/git-lfs/git-lfs/lfs" - "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/smudge" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config/prometheus" "gitlab.com/gitlab-org/gitaly/v15/internal/gitlab" - gitalylog "gitlab.com/gitlab-org/gitaly/v15/internal/log" "gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/labkit/tracing" ) -type configProvider interface { - Get(key string) string -} - -func initLogging(p configProvider) (io.Closer, error) { - path := p.Get(gitalylog.GitalyLogDirEnvKey) - if path == "" { - return nil, nil - } - - filepath := filepath.Join(path, "gitaly_lfs_smudge.log") - - return log.Initialize( - log.WithFormatter("json"), - log.WithLogLevel("info"), - log.WithOutputName(filepath), - ) -} - -func smudge(to io.Writer, from io.Reader, cfgProvider configProvider) error { +func smudgeContents(cfg smudge.Config, to io.Writer, from io.Reader) (returnedErr error) { // Since the environment is sanitized at the moment, we're only // using this to extract the correlation ID. The finished() call // to clean up the tracing will be a NOP here. ctx, finished := tracing.ExtractFromEnv(context.Background()) defer finished() - output, err := handleSmudge(ctx, to, from, cfgProvider) + output, err := handleSmudge(ctx, cfg, from) if err != nil { - log.WithError(err).Error(err) - return err + return fmt.Errorf("smudging contents: %w", err) } defer func() { - if err := output.Close(); err != nil { - log.ContextLogger(ctx).WithError(err).Error("closing LFS object: %w", err) + if err := output.Close(); err != nil && returnedErr == nil { + returnedErr = fmt.Errorf("closing LFS object: %w", err) } }() - _, copyErr := io.Copy(to, output) - if copyErr != nil { - log.WithError(err).Error(copyErr) - return copyErr + if _, err := io.Copy(to, output); err != nil { + return fmt.Errorf("writing smudged contents: %w", err) } return nil } -func handleSmudge(ctx context.Context, to io.Writer, from io.Reader, config configProvider) (io.ReadCloser, error) { +func handleSmudge(ctx context.Context, cfg smudge.Config, from io.Reader) (io.ReadCloser, error) { logger := log.ContextLogger(ctx) ptr, contents, err := lfs.DecodeFrom(from) @@ -75,23 +49,14 @@ func handleSmudge(ctx context.Context, to io.Writer, from io.Reader, config conf logger.WithField("oid", ptr.Oid).Debug("decoded LFS OID") - glCfg, tlsCfg, glRepository, err := loadConfig(config) - if err != nil { - return io.NopCloser(contents), err - } - - logger.WithField("gitlab_config", glCfg). - WithField("gitaly_tls_config", tlsCfg). - Debug("loaded GitLab API config") - - client, err := gitlab.NewHTTPClient(logger, glCfg, tlsCfg, prometheus.Config{}) + client, err := gitlab.NewHTTPClient(logger, cfg.Gitlab, cfg.TLS, prometheus.Config{}) if err != nil { return io.NopCloser(contents), err } qs := url.Values{} qs.Set("oid", ptr.Oid) - qs.Set("gl_repository", glRepository) + qs.Set("gl_repository", cfg.GlRepository) u := url.URL{Path: "/lfs", RawQuery: qs.Encode()} response, err := client.Get(ctx, u.String()) @@ -109,33 +74,3 @@ func handleSmudge(ctx context.Context, to io.Writer, from io.Reader, config conf return io.NopCloser(contents), nil } - -func loadConfig(cfgProvider configProvider) (config.Gitlab, config.TLS, string, error) { - var cfg config.Gitlab - var tlsCfg config.TLS - - glRepository := cfgProvider.Get("GL_REPOSITORY") - if glRepository == "" { - return cfg, tlsCfg, "", fmt.Errorf("error loading project: GL_REPOSITORY is not defined") - } - - u := cfgProvider.Get("GL_INTERNAL_CONFIG") - if u == "" { - return cfg, tlsCfg, glRepository, fmt.Errorf("unable to retrieve GL_INTERNAL_CONFIG") - } - - if err := json.Unmarshal([]byte(u), &cfg); err != nil { - return cfg, tlsCfg, glRepository, fmt.Errorf("unable to unmarshal GL_INTERNAL_CONFIG: %v", err) - } - - u = cfgProvider.Get("GITALY_TLS") - if u == "" { - return cfg, tlsCfg, glRepository, errors.New("unable to retrieve GITALY_TLS") - } - - if err := json.Unmarshal([]byte(u), &tlsCfg); err != nil { - return cfg, tlsCfg, glRepository, fmt.Errorf("unable to unmarshal GITALY_TLS: %w", err) - } - - return cfg, tlsCfg, glRepository, nil -} diff --git a/cmd/gitaly-lfs-smudge/lfs_smudge_test.go b/cmd/gitaly-lfs-smudge/lfs_smudge_test.go index a508778ca..477a2131f 100644 --- a/cmd/gitaly-lfs-smudge/lfs_smudge_test.go +++ b/cmd/gitaly-lfs-smudge/lfs_smudge_test.go @@ -2,16 +2,14 @@ package main import ( "bytes" - "encoding/json" "net/http" - "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/smudge" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v15/internal/gitlab" - "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" ) const ( @@ -46,33 +44,6 @@ var defaultOptions = gitlab.TestServerOptions{ ServerKeyPath: keyPath, } -type mapConfig struct { - env map[string]string -} - -func TestMain(m *testing.M) { - testhelper.Run(m) -} - -func (m *mapConfig) Get(key string) string { - return m.env[key] -} - -func runTestServer(t *testing.T, options gitlab.TestServerOptions) (config.Gitlab, func()) { - tempDir := testhelper.TempDir(t) - - gitlab.WriteShellSecretFile(t, tempDir, secretToken) - secretFilePath := filepath.Join(tempDir, ".gitlab_shell_secret") - - serverURL, serverCleanup := gitlab.NewTestServer(t, options) - - c := config.Gitlab{URL: serverURL, SecretFile: secretFilePath, HTTPSettings: config.HTTPSettings{CAFile: certPath}} - - return c, func() { - serverCleanup() - } -} - func TestSuccessfulLfsSmudge(t *testing.T) { testCases := []struct { desc string @@ -93,96 +64,87 @@ func TestSuccessfulLfsSmudge(t *testing.T) { var b bytes.Buffer reader := strings.NewReader(tc.data) - c, cleanup := runTestServer(t, defaultOptions) + gitlabCfg, cleanup := runTestServer(t, defaultOptions) defer cleanup() - cfg, err := json.Marshal(c) - require.NoError(t, err) - - tlsCfg, err := json.Marshal(config.TLS{ - CertPath: certPath, - KeyPath: keyPath, - }) - require.NoError(t, err) - - tmpDir := testhelper.TempDir(t) - - env := map[string]string{ - "GL_REPOSITORY": "project-1", - "GL_INTERNAL_CONFIG": string(cfg), - "GITALY_LOG_DIR": tmpDir, - "GITALY_TLS": string(tlsCfg), + cfg := smudge.Config{ + GlRepository: "project-1", + Gitlab: gitlabCfg, + TLS: config.TLS{ + CertPath: certPath, + KeyPath: keyPath, + }, } - cfgProvider := &mapConfig{env: env} - _, err = initLogging(cfgProvider) - require.NoError(t, err) - err = smudge(&b, reader, cfgProvider) - require.NoError(t, err) + require.NoError(t, smudgeContents(cfg, &b, reader)) require.Equal(t, testData, b.String()) - - logFilename := filepath.Join(tmpDir, "gitaly_lfs_smudge.log") - require.FileExists(t, logFilename) - - data := testhelper.MustReadFile(t, logFilename) - require.NoError(t, err) - d := string(data) - - require.Contains(t, d, `"msg":"Finished HTTP request"`) - require.Contains(t, d, `"status":200`) - require.Contains(t, d, `"content_length_bytes":`) }) } } func TestUnsuccessfulLfsSmudge(t *testing.T) { + defaultConfig := func(t *testing.T, gitlabCfg config.Gitlab) smudge.Config { + return smudge.Config{ + GlRepository: "project-1", + Gitlab: gitlabCfg, + } + } + testCases := []struct { - desc string - data string - missingEnv string - tlsCfg config.TLS - expectedError bool - options gitlab.TestServerOptions - expectedLogMessage string - expectedGitalyTLS string + desc string + setupCfg func(*testing.T, config.Gitlab) smudge.Config + data string + expectedError bool + options gitlab.TestServerOptions + expectedGitalyTLS string }{ { desc: "bad LFS pointer", data: "test data", + setupCfg: defaultConfig, options: defaultOptions, expectedError: false, }, { desc: "invalid LFS pointer", data: invalidLfsPointer, + setupCfg: defaultConfig, options: defaultOptions, expectedError: false, }, { desc: "invalid LFS pointer with non-hex characters", data: invalidLfsPointerWithNonHex, + setupCfg: defaultConfig, options: defaultOptions, expectedError: false, }, { - desc: "missing GL_REPOSITORY", - data: lfsPointer, - missingEnv: "GL_REPOSITORY", - options: defaultOptions, - expectedError: true, - expectedLogMessage: "GL_REPOSITORY is not defined", + desc: "missing GL_REPOSITORY", + data: lfsPointer, + setupCfg: func(t *testing.T, gitlabCfg config.Gitlab) smudge.Config { + cfg := defaultConfig(t, gitlabCfg) + cfg.GlRepository = "" + return cfg + }, + options: defaultOptions, + expectedError: true, }, { - desc: "missing GL_INTERNAL_CONFIG", - data: lfsPointer, - missingEnv: "GL_INTERNAL_CONFIG", - options: defaultOptions, - expectedError: true, - expectedLogMessage: "unable to retrieve GL_INTERNAL_CONFIG", + desc: "missing GL_INTERNAL_CONFIG", + data: lfsPointer, + setupCfg: func(t *testing.T, gitlabCfg config.Gitlab) smudge.Config { + cfg := defaultConfig(t, gitlabCfg) + cfg.Gitlab = config.Gitlab{} + return cfg + }, + options: defaultOptions, + expectedError: true, }, { - desc: "failed HTTP response", - data: lfsPointer, + desc: "failed HTTP response", + data: lfsPointer, + setupCfg: defaultConfig, options: gitlab.TestServerOptions{ SecretToken: secretToken, LfsBody: testData, @@ -190,51 +152,30 @@ func TestUnsuccessfulLfsSmudge(t *testing.T) { GlRepository: glRepository, LfsStatusCode: http.StatusInternalServerError, }, - expectedError: true, - expectedLogMessage: "error loading LFS object", + expectedError: true, }, { - desc: "invalid TLS paths", - data: lfsPointer, + desc: "invalid TLS paths", + data: lfsPointer, + setupCfg: func(t *testing.T, gitlabCfg config.Gitlab) smudge.Config { + cfg := defaultConfig(t, gitlabCfg) + cfg.TLS = config.TLS{CertPath: "fake-path", KeyPath: "not-real"} + return cfg + }, options: defaultOptions, - tlsCfg: config.TLS{CertPath: "fake-path", KeyPath: "not-real"}, expectedError: true, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - c, cleanup := runTestServer(t, tc.options) + gitlabCfg, cleanup := runTestServer(t, tc.options) defer cleanup() - cfg, err := json.Marshal(c) - require.NoError(t, err) - - tlsCfg, err := json.Marshal(tc.tlsCfg) - require.NoError(t, err) - - tmpDir := testhelper.TempDir(t) - - env := map[string]string{ - "GL_REPOSITORY": "project-1", - "GL_INTERNAL_CONFIG": string(cfg), - "GITALY_LOG_DIR": tmpDir, - "GITALY_TLS": string(tlsCfg), - } - - if tc.missingEnv != "" { - delete(env, tc.missingEnv) - } - - cfgProvider := &mapConfig{env: env} + cfg := tc.setupCfg(t, gitlabCfg) var b bytes.Buffer - reader := strings.NewReader(tc.data) - - _, err = initLogging(cfgProvider) - require.NoError(t, err) - - err = smudge(&b, reader, cfgProvider) + err := smudgeContents(cfg, &b, strings.NewReader(tc.data)) if tc.expectedError { require.Error(t, err) @@ -242,15 +183,6 @@ func TestUnsuccessfulLfsSmudge(t *testing.T) { require.NoError(t, err) require.Equal(t, tc.data, b.String()) } - - logFilename := filepath.Join(tmpDir, "gitaly_lfs_smudge.log") - require.FileExists(t, logFilename) - - data := testhelper.MustReadFile(t, logFilename) - - if tc.expectedLogMessage != "" { - require.Contains(t, string(data), tc.expectedLogMessage) - } }) } } diff --git a/cmd/gitaly-lfs-smudge/main.go b/cmd/gitaly-lfs-smudge/main.go index 314105b50..9db0fbd55 100644 --- a/cmd/gitaly-lfs-smudge/main.go +++ b/cmd/gitaly-lfs-smudge/main.go @@ -2,14 +2,15 @@ package main import ( "fmt" + "io" "os" -) - -type envConfig struct{} + "path/filepath" -func (e *envConfig) Get(key string) string { - return os.Getenv(key) -} + "gitlab.com/gitlab-org/gitaly/v15/internal/git/smudge" + "gitlab.com/gitlab-org/gitaly/v15/internal/helper/env" + gitalylog "gitlab.com/gitlab-org/gitaly/v15/internal/log" + "gitlab.com/gitlab-org/labkit/log" +) func requireStdin(msg string) { var out string @@ -30,14 +31,42 @@ func requireStdin(msg string) { func main() { requireStdin("This command should be run by the Git 'smudge' filter") - closer, err := initLogging(&envConfig{}) + closer, err := initLogging(os.Environ()) if err != nil { fmt.Fprintf(os.Stderr, "error initializing log file for gitaly-lfs-smudge: %v", err) } defer closer.Close() - err = smudge(os.Stdout, os.Stdin, &envConfig{}) - if err != nil { + if err := run(os.Environ(), os.Stdout, os.Stdin); err != nil { + log.WithError(err).Error(err) os.Exit(1) } } + +func initLogging(environment []string) (io.Closer, error) { + path := env.ExtractValue(environment, gitalylog.GitalyLogDirEnvKey) + if path == "" { + return log.Initialize(log.WithWriter(io.Discard)) + } + + filepath := filepath.Join(path, "gitaly_lfs_smudge.log") + + return log.Initialize( + log.WithFormatter("json"), + log.WithLogLevel("info"), + log.WithOutputName(filepath), + ) +} + +func run(environment []string, out io.Writer, in io.Reader) error { + cfg, err := smudge.ConfigFromEnvironment(environment) + if err != nil { + return fmt.Errorf("loading configuration: %w", err) + } + + if err := smudgeContents(cfg, out, in); err != nil { + return fmt.Errorf("running smudge filter: %w", err) + } + + return nil +} diff --git a/cmd/gitaly-lfs-smudge/main_test.go b/cmd/gitaly-lfs-smudge/main_test.go new file mode 100644 index 000000000..37814a34b --- /dev/null +++ b/cmd/gitaly-lfs-smudge/main_test.go @@ -0,0 +1,211 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/command" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/smudge" + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" +) + +const logName = "gitaly_lfs_smudge.log" + +func TestGitalyLFSSmudge(t *testing.T) { + ctx := testhelper.Context(t) + cfg := testcfg.Build(t) + binary := testcfg.BuildGitalyLFSSmudge(t, cfg) + + gitlabCfg, cleanup := runTestServer(t, defaultOptions) + defer cleanup() + + tlsCfg := config.TLS{ + CertPath: certPath, + KeyPath: keyPath, + } + + marshalledGitlabCfg, err := json.Marshal(gitlabCfg) + require.NoError(t, err) + + marshalledTLSCfg, err := json.Marshal(tlsCfg) + require.NoError(t, err) + + standardEnv := func(logDir string) []string { + return []string{ + "GL_REPOSITORY=project-1", + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_LOG_DIR=" + logDir, + "GITALY_TLS=" + string(marshalledTLSCfg), + } + } + + for _, tc := range []struct { + desc string + setup func(t *testing.T) ([]string, string) + stdin io.Reader + expectedErr string + expectedStdout string + expectedStderr string + expectedLogRegexp string + }{ + { + desc: "success", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + return standardEnv(logDir), filepath.Join(logDir, logName) + }, + stdin: strings.NewReader(lfsPointer), + expectedStdout: "hello world", + expectedLogRegexp: "Finished HTTP request", + }, + { + desc: "success with single envvar", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + + cfg := smudge.Config{ + GlRepository: "project-1", + Gitlab: gitlabCfg, + TLS: tlsCfg, + } + + env, err := cfg.Environment() + require.NoError(t, err) + + return []string{ + env, + "GITALY_LOG_DIR=" + logDir, + }, filepath.Join(logDir, "gitaly_lfs_smudge.log") + }, + stdin: strings.NewReader(lfsPointer), + expectedStdout: "hello world", + expectedLogRegexp: "Finished HTTP request", + }, + { + desc: "missing Gitlab repository", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + + return []string{ + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_LOG_DIR=" + logDir, + "GITALY_TLS=" + string(marshalledTLSCfg), + }, filepath.Join(logDir, logName) + }, + stdin: strings.NewReader(lfsPointer), + expectedErr: "exit status 1", + expectedLogRegexp: "error loading project: GL_REPOSITORY is not defined", + }, + { + desc: "missing Gitlab configuration", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + + return []string{ + "GL_REPOSITORY=project-1", + "GITALY_LOG_DIR=" + logDir, + "GITALY_TLS=" + string(marshalledTLSCfg), + }, filepath.Join(logDir, logName) + }, + stdin: strings.NewReader(lfsPointer), + expectedErr: "exit status 1", + expectedLogRegexp: "unable to retrieve GL_INTERNAL_CONFIG", + }, + { + desc: "missing TLS configuration", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + + return []string{ + "GL_REPOSITORY=project-1", + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_LOG_DIR=" + logDir, + }, filepath.Join(logDir, logName) + }, + stdin: strings.NewReader(lfsPointer), + expectedErr: "exit status 1", + expectedLogRegexp: "unable to retrieve GITALY_TLS", + }, + { + desc: "missing log configuration", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + + return []string{ + "GL_REPOSITORY=project-1", + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_TLS=" + string(marshalledTLSCfg), + }, filepath.Join(logDir, "gitaly_lfs_smudge.log") + }, + stdin: strings.NewReader(lfsPointer), + expectedStdout: "hello world", + }, + { + desc: "missing stdin", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + return standardEnv(logDir), filepath.Join(logDir, logName) + }, + expectedErr: "exit status 1", + expectedStdout: "Cannot read from STDIN. This command should be run by the Git 'smudge' filter\n", + }, + { + desc: "non-LFS-pointer input", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + return standardEnv(logDir), filepath.Join(logDir, logName) + }, + stdin: strings.NewReader("somethingsomething"), + expectedStdout: "somethingsomething", + expectedLogRegexp: "^$", + }, + { + desc: "mixed input", + setup: func(t *testing.T) ([]string, string) { + logDir := testhelper.TempDir(t) + return standardEnv(logDir), filepath.Join(logDir, logName) + }, + stdin: strings.NewReader(lfsPointer + "\nsomethingsomething\n"), + expectedStdout: lfsPointer + "\nsomethingsomething\n", + expectedLogRegexp: "^$", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + env, logFile := tc.setup(t) + + var stdout, stderr bytes.Buffer + cmd, err := command.New(ctx, + exec.Command(binary), + tc.stdin, + &stdout, + &stderr, + env..., + ) + require.NoError(t, err) + + err = cmd.Wait() + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + require.Equal(t, tc.expectedStdout, stdout.String()) + require.Equal(t, tc.expectedStderr, stderr.String()) + + if tc.expectedLogRegexp == "" { + require.NoFileExists(t, logFile) + } else { + logData := testhelper.MustReadFile(t, logFile) + require.Regexp(t, tc.expectedLogRegexp, string(logData)) + } + }) + } +} diff --git a/cmd/gitaly-lfs-smudge/testhelper_test.go b/cmd/gitaly-lfs-smudge/testhelper_test.go new file mode 100644 index 000000000..3d4d4f31f --- /dev/null +++ b/cmd/gitaly-lfs-smudge/testhelper_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "path/filepath" + "testing" + + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/gitlab" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" +) + +func TestMain(m *testing.M) { + testhelper.Run(m) +} + +func runTestServer(t *testing.T, options gitlab.TestServerOptions) (config.Gitlab, func()) { + tempDir := testhelper.TempDir(t) + + gitlab.WriteShellSecretFile(t, tempDir, secretToken) + secretFilePath := filepath.Join(tempDir, ".gitlab_shell_secret") + + serverURL, serverCleanup := gitlab.NewTestServer(t, options) + + c := config.Gitlab{URL: serverURL, SecretFile: secretFilePath, HTTPSettings: config.HTTPSettings{CAFile: certPath}} + + return c, func() { + serverCleanup() + } +} diff --git a/internal/git/smudge/config.go b/internal/git/smudge/config.go new file mode 100644 index 000000000..79ca4b1ba --- /dev/null +++ b/internal/git/smudge/config.go @@ -0,0 +1,79 @@ +package smudge + +import ( + "encoding/json" + "errors" + "fmt" + + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/helper/env" +) + +// ConfigEnvironmentKey is the key that gitaly-lfs-smudge expects the configuration to exist at. The +// value of this environment variable should be the JSON-encoded `Config` struct. +const ConfigEnvironmentKey = "GITALY_LFS_SMUDGE_CONFIG" + +// Config is the configuration used to run gitaly-lfs-smudge. It must be injected via environment +// variables. +type Config struct { + // GlRepository is the GitLab repository identifier that is required so that we can query + // the corresponding Rails' repository for the respective LFS contents. + GlRepository string `json:"gl_repository"` + // Gitlab contains configuration so that we can connect to Rails in order to retrieve LFS + // contents. + Gitlab config.Gitlab `json:"gitlab"` + // TLS contains configuration for setting up a TLS-encrypted connection to Rails. + TLS config.TLS `json:"tls"` +} + +// ConfigFromEnvironment loads the Config structure from the set of given environment variables. +func ConfigFromEnvironment(environment []string) (Config, error) { + var cfg Config + + // If ConfigEnvironmentKey is set, then we use that instead of the separate environment + // variables queried for below. This has been newly introduced in v15.1, so the fallback + // to the old environment variables can be removed with v15.2. + if encodedCfg := env.ExtractValue(environment, ConfigEnvironmentKey); encodedCfg != "" { + if err := json.Unmarshal([]byte(encodedCfg), &cfg); err != nil { + return Config{}, fmt.Errorf("unable to unmarshal config: %w", err) + } + + return cfg, nil + } + + cfg.GlRepository = env.ExtractValue(environment, "GL_REPOSITORY") + if cfg.GlRepository == "" { + return Config{}, fmt.Errorf("error loading project: GL_REPOSITORY is not defined") + } + + u := env.ExtractValue(environment, "GL_INTERNAL_CONFIG") + if u == "" { + return Config{}, fmt.Errorf("unable to retrieve GL_INTERNAL_CONFIG") + } + + if err := json.Unmarshal([]byte(u), &cfg.Gitlab); err != nil { + return Config{}, fmt.Errorf("unable to unmarshal GL_INTERNAL_CONFIG: %w", err) + } + + u = env.ExtractValue(environment, "GITALY_TLS") + if u == "" { + return Config{}, errors.New("unable to retrieve GITALY_TLS") + } + + if err := json.Unmarshal([]byte(u), &cfg.TLS); err != nil { + return Config{}, fmt.Errorf("unable to unmarshal GITALY_TLS: %w", err) + } + + return cfg, nil +} + +// Environment encodes the given configuration as an environment variable that can be injected into +// `gitaly-lfs-smudge`. +func (c Config) Environment() (string, error) { + marshalled, err := json.Marshal(c) + if err != nil { + return "", fmt.Errorf("marshalling configuration: %w", err) + } + + return fmt.Sprintf("%s=%s", ConfigEnvironmentKey, marshalled), nil +} diff --git a/internal/git/smudge/config_test.go b/internal/git/smudge/config_test.go new file mode 100644 index 000000000..d988bf16c --- /dev/null +++ b/internal/git/smudge/config_test.go @@ -0,0 +1,173 @@ +package smudge + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" +) + +func TestConfigFromEnvironment(t *testing.T) { + gitlabCfg := config.Gitlab{ + URL: "https://example.com", + RelativeURLRoot: "gitlab", + HTTPSettings: config.HTTPSettings{ + ReadTimeout: 1, + User: "user", + Password: "correcthorsebatterystaple", + CAFile: "/ca/file", + CAPath: "/ca/path", + SelfSigned: true, + }, + SecretFile: "/secret/path", + } + + tlsCfg := config.TLS{ + CertPath: "/cert/path", + KeyPath: "/key/path", + } + + fullCfg := Config{ + GlRepository: "repo", + Gitlab: gitlabCfg, + TLS: tlsCfg, + } + + marshalledGitlabCfg, err := json.Marshal(gitlabCfg) + require.NoError(t, err) + + marshalledTLSCfg, err := json.Marshal(tlsCfg) + require.NoError(t, err) + + marshalledFullCfg, err := json.Marshal(fullCfg) + require.NoError(t, err) + + for _, tc := range []struct { + desc string + environment []string + expectedErr string + expectedCfg Config + }{ + { + desc: "empty environment", + expectedErr: "error loading project: GL_REPOSITORY is not defined", + }, + { + desc: "successful via separate envvars", + environment: []string{ + "GL_REPOSITORY=repo", + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_TLS=" + string(marshalledTLSCfg), + }, + expectedCfg: Config{ + GlRepository: "repo", + Gitlab: gitlabCfg, + TLS: tlsCfg, + }, + }, + { + desc: "successful via single envvar", + environment: []string{ + "GITALY_LFS_SMUDGE_CONFIG=" + string(marshalledFullCfg), + "GL_REPOSITORY=garbage", + "GL_INTERNAL_CONFIG=garbage", + "GITALY_TLS=garbage", + }, + expectedCfg: fullCfg, + }, + { + desc: "single envvar overrides separate envvars", + environment: []string{ + "GITALY_LFS_SMUDGE_CONFIG=" + string(marshalledFullCfg), + }, + expectedCfg: fullCfg, + }, + { + desc: "invalid full config", + environment: []string{ + "GITALY_LFS_SMUDGE_CONFIG=invalid", + }, + expectedErr: "unable to unmarshal config: invalid character 'i' looking for beginning of value", + }, + { + desc: "missing GlRepository", + environment: []string{ + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_TLS=" + string(marshalledTLSCfg), + }, + expectedErr: "error loading project: GL_REPOSITORY is not defined", + }, + { + desc: "missing Gitlab config", + environment: []string{ + "GL_REPOSITORY=repo", + "GITALY_TLS=" + string(marshalledTLSCfg), + }, + expectedErr: "unable to retrieve GL_INTERNAL_CONFIG", + }, + { + desc: "invalid Gitlab config", + environment: []string{ + "GL_REPOSITORY=repo", + "GL_INTERNAL_CONFIG=invalid", + "GITALY_TLS=" + string(marshalledTLSCfg), + }, + expectedErr: "unable to unmarshal GL_INTERNAL_CONFIG: invalid character 'i' looking for beginning of value", + }, + { + desc: "missing TLS config", + environment: []string{ + "GL_REPOSITORY=repo", + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + }, + expectedErr: "unable to retrieve GITALY_TLS", + }, + { + desc: "invalid TLS config", + environment: []string{ + "GL_REPOSITORY=repo", + "GL_INTERNAL_CONFIG=" + string(marshalledGitlabCfg), + "GITALY_TLS=invalid", + }, + expectedErr: "unable to unmarshal GITALY_TLS: invalid character 'i' looking for beginning of value", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + cfg, err := ConfigFromEnvironment(tc.environment) + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + require.Equal(t, tc.expectedCfg, cfg) + }) + } +} + +func TestConfig_Environment(t *testing.T) { + cfg := Config{ + GlRepository: "repo", + Gitlab: config.Gitlab{ + URL: "https://example.com", + RelativeURLRoot: "gitlab", + HTTPSettings: config.HTTPSettings{ + ReadTimeout: 1, + User: "user", + Password: "correcthorsebatterystaple", + CAFile: "/ca/file", + CAPath: "/ca/path", + SelfSigned: true, + }, + SecretFile: "/secret/path", + }, + TLS: config.TLS{ + CertPath: "/cert/path", + KeyPath: "/key/path", + }, + } + + env, err := cfg.Environment() + require.NoError(t, err) + require.Equal(t, `GITALY_LFS_SMUDGE_CONFIG={"gl_repository":"repo","gitlab":{"url":"https://example.com","relative_url_root":"gitlab","http_settings":{"read_timeout":1,"user":"user","password":"correcthorsebatterystaple","ca_file":"/ca/file","ca_path":"/ca/path","self_signed_cert":true},"secret_file":"/secret/path"},"tls":{"cert_path":"/cert/path","key_path":"/key/path"}}`, env) +} diff --git a/internal/gitaly/config/config.go b/internal/gitaly/config/config.go index a7b70842c..29718876f 100644 --- a/internal/gitaly/config/config.go +++ b/internal/gitaly/config/config.go @@ -69,8 +69,8 @@ type Cfg struct { // TLS configuration type TLS struct { - CertPath string `toml:"certificate_path,omitempty"` - KeyPath string `toml:"key_path,omitempty"` + CertPath string `toml:"certificate_path,omitempty" json:"cert_path"` + KeyPath string `toml:"key_path,omitempty" json:"key_path"` } // GitlabShell contains the settings required for executing `gitlab-shell` diff --git a/internal/gitaly/service/repository/archive.go b/internal/gitaly/service/repository/archive.go index 66899c7ff..f08ee71a7 100644 --- a/internal/gitaly/service/repository/archive.go +++ b/internal/gitaly/service/repository/archive.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "io" "os" @@ -16,6 +15,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v15/internal/command" "gitlab.com/gitlab-org/gitaly/v15/internal/git" "gitlab.com/gitlab-org/gitaly/v15/internal/git/catfile" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/smudge" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/storage" "gitlab.com/gitlab-org/gitaly/v15/internal/helper" "gitlab.com/gitlab-org/gitaly/v15/internal/log" @@ -33,8 +33,6 @@ type archiveParams struct { format string archivePath string exclude []string - internalCfg []byte - tlsCfg []byte binDir string loggingDir string } @@ -86,16 +84,6 @@ func (s *server) GetArchive(in *gitalypb.GetArchiveRequest, stream gitalypb.Repo return stream.Send(&gitalypb.GetArchiveResponse{Data: p}) }) - gitlabConfig, err := json.Marshal(s.cfg.Gitlab) - if err != nil { - return err - } - - tlsCfg, err := json.Marshal(s.cfg.TLS) - if err != nil { - return err - } - ctxlogrus.Extract(ctx).WithField("request_hash", requestHash(in)).Info("request details") return s.handleArchive(archiveParams{ @@ -106,8 +94,6 @@ func (s *server) GetArchive(in *gitalypb.GetArchiveRequest, stream gitalypb.Repo format: format, archivePath: path, exclude: exclude, - internalCfg: gitlabConfig, - tlsCfg: tlsCfg, binDir: s.binDir, loggingDir: s.loggingCfg.Dir, }) @@ -210,11 +196,19 @@ func (s *server) handleArchive(p archiveParams) error { pathspecs = append(pathspecs, ":(exclude)"+exclude) } + smudgeCfg := smudge.Config{ + GlRepository: p.in.GetRepository().GetGlRepository(), + Gitlab: s.cfg.Gitlab, + TLS: s.cfg.TLS, + } + + smudgeEnv, err := smudgeCfg.Environment() + if err != nil { + return fmt.Errorf("setting up smudge environment: %w", err) + } + env := []string{ - fmt.Sprintf("GL_REPOSITORY=%s", p.in.GetRepository().GetGlRepository()), - fmt.Sprintf("GL_PROJECT_PATH=%s", p.in.GetRepository().GetGlProjectPath()), - fmt.Sprintf("GL_INTERNAL_CONFIG=%s", p.internalCfg), - fmt.Sprintf("GITALY_TLS=%s", p.tlsCfg), + smudgeEnv, fmt.Sprintf("CORRELATION_ID=%s", correlation.ExtractFromContext(p.ctx)), fmt.Sprintf("%s=%s", log.GitalyLogDirEnvKey, p.loggingDir), } diff --git a/internal/gitaly/service/repository/archive_test.go b/internal/gitaly/service/repository/archive_test.go index 75fb00837..995189aaf 100644 --- a/internal/gitaly/service/repository/archive_test.go +++ b/internal/gitaly/service/repository/archive_test.go @@ -3,7 +3,6 @@ package repository import ( "archive/zip" "bytes" - "encoding/json" "fmt" "io" "os" @@ -14,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/git" "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/smudge" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v15/internal/gitlab" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" @@ -502,10 +502,13 @@ func TestGetArchiveEnv(t *testing.T) { CommitId: commitID, } - cfgData, err := json.Marshal(cfg.Gitlab) - require.NoError(t, err) + smudgeCfg := smudge.Config{ + GlRepository: gittest.GlRepository, + Gitlab: cfg.Gitlab, + TLS: cfg.TLS, + } - tlsCfgData, err := json.Marshal(cfg.TLS) + smudgeEnv, err := smudgeCfg.Environment() require.NoError(t, err) stream, err := client.GetArchive(ctx, req) @@ -513,12 +516,9 @@ func TestGetArchiveEnv(t *testing.T) { data, err := consumeArchive(stream) require.NoError(t, err) - require.Contains(t, string(data), "GL_REPOSITORY="+gittest.GlRepository) - require.Contains(t, string(data), "GL_PROJECT_PATH="+gittest.GlProjectPath) - require.Contains(t, string(data), "GL_INTERNAL_CONFIG="+string(cfgData)) - require.Contains(t, string(data), "GITALY_TLS="+string(tlsCfgData)) require.Contains(t, string(data), "CORRELATION_ID="+correlationID) require.Contains(t, string(data), "GITALY_LOG_DIR="+cfg.Logging.Dir) + require.Contains(t, string(data), smudgeEnv) } func compressedFileContents(t *testing.T, format gitalypb.GetArchiveRequest_Format, name string) []byte { diff --git a/internal/helper/env/env.go b/internal/helper/env/env.go index e8fb5c70b..92173d335 100644 --- a/internal/helper/env/env.go +++ b/internal/helper/env/env.go @@ -65,3 +65,18 @@ func GetString(name string, fallback string) string { return strings.TrimSpace(value) } + +// ExtractValue returns the value of the environment variable with the given key. The given key +// should not have a trailing "=". If the same key occurrs multiple times in the environment, then +// any later occurrences will override previous ones. +func ExtractValue(environment []string, key string) string { + var value string + + for _, envvar := range environment { + if strings.HasPrefix(envvar, key+"=") { + value = strings.TrimPrefix(envvar, key+"=") + } + } + + return value +} diff --git a/internal/helper/env/env_test.go b/internal/helper/env/env_test.go index 6bd6ab10b..d38cf0362 100644 --- a/internal/helper/env/env_test.go +++ b/internal/helper/env/env_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/helper/env" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" ) @@ -205,3 +206,59 @@ func TestGetString(t *testing.T) { }) } } + +func TestExtractKey(t *testing.T) { + for _, tc := range []struct { + desc string + environment []string + key string + expectedValue string + }{ + { + desc: "nil", + environment: nil, + key: "something", + expectedValue: "", + }, + { + desc: "found", + environment: []string{ + "FOO=bar", + "BAR=qux", + }, + key: "BAR", + expectedValue: "qux", + }, + { + desc: "found with multiple matches", + environment: []string{ + "FOO=1", + "FOO=2", + }, + key: "FOO", + expectedValue: "2", + }, + { + desc: "not found", + environment: []string{ + "FOO=bar", + "BAR=qux", + }, + key: "doesnotexist", + expectedValue: "", + }, + { + desc: "prefix-match", + environment: []string{ + "FOObar=value", + }, + key: "FOO", + expectedValue: "", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + value := env.ExtractValue(tc.environment, tc.key) + require.Equal(t, tc.expectedValue, value) + }) + } +} |