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:
authorPatrick Steinhardt <psteinhardt@gitlab.com>2022-05-30 10:00:00 +0300
committerPatrick Steinhardt <psteinhardt@gitlab.com>2022-05-30 10:00:00 +0300
commit469ff7a3c1cab392d337389b77ced0a4fbd82bdb (patch)
tree020da651a0df0f9d5e54d3ba8cbb6e38e09ff3b1
parenta6c5964bb455f77a0c79898f0b99c4c7df3aee3a (diff)
parent21d274062414d4a9c8e693647c702aeee7d9b21a (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.go87
-rw-r--r--cmd/gitaly-lfs-smudge/lfs_smudge_test.go184
-rw-r--r--cmd/gitaly-lfs-smudge/main.go47
-rw-r--r--cmd/gitaly-lfs-smudge/main_test.go211
-rw-r--r--cmd/gitaly-lfs-smudge/testhelper_test.go29
-rw-r--r--internal/git/smudge/config.go79
-rw-r--r--internal/git/smudge/config_test.go173
-rw-r--r--internal/gitaly/config/config.go4
-rw-r--r--internal/gitaly/service/repository/archive.go32
-rw-r--r--internal/gitaly/service/repository/archive_test.go16
-rw-r--r--internal/helper/env/env.go15
-rw-r--r--internal/helper/env/env_test.go57
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)
+ })
+ }
+}