mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
proxy mirror, mirror API jobs and SBOMs take pkg:git/{host}/{path}@{commit}
and cache the repository and commit in the cache /git serves, so a later
clone is a cache hit, also with the upstream down. Part of #403.
241 lines
7.6 KiB
Go
241 lines
7.6 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"net/http/cgi"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
"github.com/git-pkgs/proxy/internal/mirror"
|
|
)
|
|
|
|
func gitTestEnv(home string) []string {
|
|
return []string{
|
|
"PATH=" + os.Getenv("PATH"),
|
|
"HOME=" + home,
|
|
"GIT_CONFIG_NOSYSTEM=1",
|
|
"GIT_TERMINAL_PROMPT=0",
|
|
"GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.com",
|
|
"GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.com",
|
|
}
|
|
}
|
|
|
|
func gitCommand(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = gitTestEnv(t.TempDir())
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// gitHTTPBackend finds git http-backend, which serves the test upstream, and
|
|
// skips the test where git is installed without it.
|
|
func gitHTTPBackend(t *testing.T) string {
|
|
t.Helper()
|
|
dir := gitCommand(t, "", "--exec-path")
|
|
for _, name := range []string{"git-http-backend", "git-http-backend.exe"} {
|
|
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
|
return filepath.Join(dir, name)
|
|
}
|
|
}
|
|
t.Skip("git http-backend is not installed")
|
|
return ""
|
|
}
|
|
|
|
// syncBuffer is a log destination the server and the test share.
|
|
type syncBuffer struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
}
|
|
|
|
func (b *syncBuffer) Write(p []byte) (int, error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.buf.Write(p)
|
|
}
|
|
|
|
func (b *syncBuffer) String() string {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.buf.String()
|
|
}
|
|
|
|
// newGitTestRepo creates the bare repository {root}/{path}.git with one
|
|
// commit on main and returns the commit.
|
|
func newGitTestRepo(t *testing.T, root, path string) string {
|
|
t.Helper()
|
|
bare := filepath.Join(root, filepath.FromSlash(path)+".git")
|
|
gitCommand(t, "", "init", "--quiet", "--bare", "--initial-branch=main", bare)
|
|
work := t.TempDir()
|
|
gitCommand(t, "", "clone", "--quiet", bare, work)
|
|
if err := os.WriteFile(filepath.Join(work, "file.txt"), []byte("hello\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gitCommand(t, work, "add", "file.txt")
|
|
gitCommand(t, work, "commit", "--quiet", "-m", "first")
|
|
gitCommand(t, work, "push", "--quiet", "origin", "HEAD:main")
|
|
return gitCommand(t, work, "rev-parse", "HEAD")
|
|
}
|
|
|
|
// gitBackend serves the repositories under root with git http-backend.
|
|
func gitBackend(t *testing.T, root string) http.Handler {
|
|
t.Helper()
|
|
return &cgi.Handler{
|
|
Path: gitHTTPBackend(t),
|
|
Env: append(gitTestEnv(t.TempDir()), "GIT_PROJECT_ROOT="+root, "GIT_HTTP_EXPORT_ALL=1"),
|
|
}
|
|
}
|
|
|
|
// startGitTestServer starts the whole server with the git cache, after
|
|
// configure has set up cfg, and returns cfg and the server's log.
|
|
func startGitTestServer(t *testing.T, configure func(*config.Config)) (*config.Config, *syncBuffer) {
|
|
t.Helper()
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tempDir := t.TempDir()
|
|
cfg := config.Default()
|
|
cfg.Listen = listener.Addr().String()
|
|
cfg.BaseURL = "http://" + cfg.Listen
|
|
cfg.Database.Path = filepath.Join(tempDir, "proxy.db")
|
|
cfg.Storage.URL = "file://" + filepath.Join(tempDir, "artifacts")
|
|
cfg.Upstream.AllowLoopback = true
|
|
cfg.Git.CacheDir = filepath.Join(tempDir, "git")
|
|
configure(cfg)
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("validating config: %v", err)
|
|
}
|
|
|
|
logs := &syncBuffer{}
|
|
proxyServer, err := New(cfg, slog.New(slog.NewTextHandler(logs, nil)), BuildInfo{Version: "test", Commit: "test"})
|
|
if err != nil {
|
|
t.Fatalf("creating server: %v", err)
|
|
}
|
|
startErr := make(chan error, 1)
|
|
go func() { startErr <- proxyServer.Start(listener) }()
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := proxyServer.Shutdown(ctx); err != nil {
|
|
t.Errorf("shutting down server: %v", err)
|
|
}
|
|
if err := <-startErr; !errors.Is(err, http.ErrServerClosed) {
|
|
t.Errorf("Start() error = %v, want %v", err, http.ErrServerClosed)
|
|
}
|
|
})
|
|
return cfg, logs
|
|
}
|
|
|
|
// TestGitRouteServesClonesWithUpstreamCredentials clones through the whole
|
|
// server, with credentials from upstream.auth that only the proxy holds.
|
|
func TestGitRouteServesClonesWithUpstreamCredentials(t *testing.T) {
|
|
root := t.TempDir()
|
|
head := newGitTestRepo(t, root, "team/repo")
|
|
backend := gitBackend(t, root)
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer git-secret" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
backend.ServeHTTP(w, r)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
cfg, logs := startGitTestServer(t, func(cfg *config.Config) {
|
|
cfg.Upstream.Git = map[string]config.GitUpstream{"private": {URL: upstream.URL, Repos: []string{"team/*"}}}
|
|
cfg.Upstream.Auth = map[string]config.AuthConfig{upstream.URL: {Type: "bearer", Token: "git-secret"}}
|
|
})
|
|
|
|
clone := filepath.Join(t.TempDir(), "clone")
|
|
gitCommand(t, "", "clone", "--quiet", cfg.BaseURL+"/git/private/team/repo.git", clone)
|
|
if got := gitCommand(t, clone, "rev-parse", "HEAD"); got != head {
|
|
t.Errorf("HEAD = %s, want %s", got, head)
|
|
}
|
|
if strings.Contains(logs.String(), "request deadlines") {
|
|
t.Errorf("request deadlines not extended:\n%s", logs.String())
|
|
}
|
|
|
|
resp, err := http.Get(cfg.BaseURL + "/git/private/other/repo.git/info/refs?service=git-upload-pack")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("repository outside repos: status %d, want 404", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// TestMirrorAPICachesGitCommit mirrors a pkg:git commit through the mirror
|
|
// API, then clones it through /git while the upstream is down.
|
|
func TestMirrorAPICachesGitCommit(t *testing.T) {
|
|
root := t.TempDir()
|
|
head := newGitTestRepo(t, root, "team/repo")
|
|
upstream := httptest.NewServer(gitBackend(t, root))
|
|
defer upstream.Close()
|
|
cfg, _ := startGitTestServer(t, func(cfg *config.Config) {
|
|
cfg.MirrorAPI = true
|
|
cfg.Upstream.Git = map[string]config.GitUpstream{"up": {URL: upstream.URL}}
|
|
})
|
|
|
|
location := strings.TrimPrefix(upstream.URL, "http://") + "/team/repo"
|
|
body, err := json.Marshal(map[string][]string{"purls": {"pkg:git/" + location + "@" + head}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp, err := http.Post(cfg.BaseURL+"/api/mirror", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var created struct{ ID string }
|
|
err = json.NewDecoder(resp.Body).Decode(&created)
|
|
_ = resp.Body.Close()
|
|
if err != nil || resp.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("creating job: status %d, %v", resp.StatusCode, err)
|
|
}
|
|
|
|
var job mirror.Job
|
|
deadline := time.Now().Add(30 * time.Second)
|
|
for job.State != mirror.JobStateComplete && job.State != mirror.JobStateFailed && job.State != mirror.JobStateCanceled {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("job did not finish: %+v", job)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
resp, err := http.Get(cfg.BaseURL + "/api/mirror/" + created.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = json.NewDecoder(resp.Body).Decode(&job)
|
|
_ = resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if job.State != mirror.JobStateComplete || job.Progress.Completed != 1 {
|
|
t.Fatalf("job = %+v, want one repository mirrored", job)
|
|
}
|
|
|
|
upstream.Close()
|
|
clone := filepath.Join(t.TempDir(), "clone")
|
|
gitCommand(t, "", "clone", "--quiet", cfg.BaseURL+"/git/up/team/repo.git", clone)
|
|
if got := gitCommand(t, clone, "rev-parse", "HEAD"); got != head {
|
|
t.Errorf("HEAD = %s, want %s", got, head)
|
|
}
|
|
}
|