Files
buildkit/client/client_git_source_test.go
T
Tonis Tiigi 16c6856432 source/git: fix daemon panic on resolver cache poisoning
resolveMetadata stored its result into the resolver cache through
release(v) where v was a *Metadata. On a failed resolve or a cache
hit it set the pointer to nil, but passing a typed nil pointer as an
interface stores a non-nil value, so a nil *Metadata was cached for
that remote#ref. A later resolve of the same key read it back and
dereferenced nil, crashing buildkitd. Because the cache is shared
across the jobs on a solve state, an untrusted Dockerfile or frontend
could crash the daemon, and one build could poison another's.

Store an untyped nil instead, matching the HTTP source, so only a
successfully resolved Metadata is ever cached.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 4dd98355a71d79bdf93bd3456fda3a64847ce1de)
(cherry picked from commit c1399f7266310397f5615f4751147f886afff6d2)
2026-11-03 10:39:04 +01:00

792 lines
26 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/plugins/content/local"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
gateway "github.com/moby/buildkit/frontend/gateway/client"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/gitutil/gitobject"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/moby/buildkit/util/testutil/workers"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
// testGitBundleRoundTrip exercises the bundle round-trip end to end:
//
// 1. Seed a git repo served over HTTP.
// 2. Build 1 uses GitCheckoutBundle() to export a single-file git bundle.
// 3. Build 2 imports that bundle via GitBundleURL("oci-layout+blob://...")
// against a client-side OCI layout store, once with the sha pinned as ref
// and once with a symbolic ref ("master"). Both checkouts must succeed
// and produce a worktree matching Build 0 (plain git clone).
// 4. A KeepGitDir subtest exercises llb.KeepGitDir() + bundle and asserts
// no fake refs/buildkit/* namespace leaks into the checkout's .git dir.
// 5. A provenance subtest enables attest:provenance and asserts both the
// git-commit material and the bundle-blob material are recorded.
func testGitBundleRoundTrip(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCILayout)
integration.SkipOnPlatform(t, "windows")
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
gitDir := t.TempDir()
err = runInDir(gitDir,
"git init -b master",
"git config --local user.email test@example.com",
"git config --local user.name test",
"echo hello > file.txt",
"git add file.txt",
"git commit -m initial",
"git update-server-info",
)
require.NoError(t, err)
cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
cmd.Dir = gitDir
out, err := cmd.Output()
require.NoError(t, err)
headSha := strings.TrimSpace(string(out))
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
defer server.Close()
repoURL := server.URL + "/.git"
// Build 1: export a bundle via GitCheckoutBundle.
bundleDir := t.TempDir()
st := llb.Git(repoURL, "master", llb.GitChecksum(headSha), llb.GitCheckoutBundle())
def, err := st.Marshal(ctx)
require.NoError(t, err)
_, err = c.Solve(ctx, def, SolveOpt{
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: bundleDir}},
}, nil)
require.NoError(t, err)
bundleBytes, err := os.ReadFile(filepath.Join(bundleDir, "bundle"))
require.NoError(t, err)
require.NotEmpty(t, bundleBytes)
bundleDgst := digest.FromBytes(bundleBytes)
// Sanity-check the bundle locally: it must contain the commit we pinned,
// landing in the natural ref namespace (refs/heads/master).
localBare := t.TempDir()
err = runInDir(localBare, "git init --bare")
require.NoError(t, err)
bundleRoot := t.TempDir()
root, err := os.OpenRoot(bundleRoot)
require.NoError(t, err)
require.NoError(t, root.WriteFile("bundle.pack", bundleBytes, 0644))
require.NoError(t, root.Close())
bundlePath := filepath.Join(bundleRoot, "bundle.pack")
err = runInDir(localBare, fmt.Sprintf("git fetch %s +refs/*:refs/*", bundlePath))
require.NoError(t, err)
//nolint:gosec // Test-controlled temp dir and commit SHA are not attacker input.
cmd = exec.CommandContext(t.Context(), "git", "--git-dir="+localBare, "cat-file", "-e", headSha+"^{commit}")
require.NoError(t, cmd.Run(), "bundle does not contain expected commit")
//nolint:gosec // Test-controlled temp dir is not attacker input.
cmd = exec.CommandContext(t.Context(), "git", "--git-dir="+localBare, "rev-parse", "--verify", "refs/heads/master")
masterOut, err := cmd.CombinedOutput()
require.NoError(t, err, "bundle should carry user's ref refs/heads/master; output=%q", string(masterOut))
require.Equal(t, headSha, strings.TrimSpace(string(masterOut)))
// Stage the bundle in a client-side OCI layout store so Build 2 can
// resolve it through an oci-layout+blob:// locator.
storeDir := t.TempDir()
store, err := local.NewStore(storeDir)
require.NoError(t, err)
err = content.WriteBlob(ctx, store, "bundle-"+bundleDgst.String(), bytes.NewReader(bundleBytes),
ocispecs.Descriptor{Digest: bundleDgst, Size: int64(len(bundleBytes))})
require.NoError(t, err)
csID := "git-bundle-oci-store"
bundleLocator := fmt.Sprintf("oci-layout+blob://not/real@%s", bundleDgst.String())
cases := []struct {
name string
ref string // value passed as the fragment/ref to llb.Git
}{
{"sha-ref", headSha},
{"symbolic-ref", "master"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
destDir := t.TempDir()
st := llb.Git(
repoURL,
tc.ref,
llb.GitChecksum(headSha),
llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
)
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
OCIStores: map[string]content.Store{
csID: store,
},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
require.NoError(t, err)
require.Equal(t, "hello\n", string(dt))
})
}
// KeepGitDir + bundle: the checkout's .git must not contain any leaked
// refs/buildkit/* namespace. `.git/refs/heads/*` and `.git/refs/tags/*`
// are populated by the plain-git checkout path (it runs its own fetch
// into a freshly inited repo), so we only assert no refs/buildkit/
// leak — the whole point of Item A is that the bundle no longer uses
// that namespace.
t.Run("keepgitdir-no-namespace-leak", func(t *testing.T) {
destDir := t.TempDir()
st := llb.Git(
repoURL,
"master",
llb.GitChecksum(headSha),
llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
llb.KeepGitDir(),
)
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
OCIStores: map[string]content.Store{
csID: store,
},
}, nil)
require.NoError(t, err)
// .git dir should be present.
st2, err := os.Stat(filepath.Join(destDir, ".git"))
require.NoError(t, err)
require.True(t, st2.IsDir(), ".git should be a directory")
// No fake internal namespace anywhere under the checkout's .git.
leakRoot := filepath.Join(destDir, ".git", "refs", "buildkit")
_, err = os.Stat(leakRoot)
require.True(t, os.IsNotExist(err), ".git/refs/buildkit should not exist: %v", err)
// Also scan packed-refs, which is where cloned/fetched refs may
// live on disk after git gc / pack-refs.
if pr, err := os.ReadFile(filepath.Join(destDir, ".git", "packed-refs")); err == nil {
require.NotContains(t, string(pr), "refs/buildkit/",
"packed-refs should not carry refs/buildkit/* entries")
}
// The user's ref name must survive into .git: passing "master" to
// llb.Git should produce a checkout whose .git carries
// refs/heads/master, either loose or packed.
//nolint:gosec // Test-controlled export dir is not attacker input.
cmd := exec.CommandContext(t.Context(), "git",
"--git-dir="+filepath.Join(destDir, ".git"),
"rev-parse", "--verify", "refs/heads/master")
refOut, err := cmd.CombinedOutput()
require.NoError(t, err, ".git should carry refs/heads/master; output=%q", string(refOut))
require.Equal(t, headSha, strings.TrimSpace(string(refOut)))
// HEAD must point at the pinned commit.
//nolint:gosec // Test-controlled export dir is not attacker input.
cmd = exec.CommandContext(t.Context(), "git",
"--git-dir="+filepath.Join(destDir, ".git"),
"rev-parse", "HEAD")
gotOut, err := cmd.CombinedOutput()
require.NoError(t, err, "output=%q", string(gotOut))
require.Equal(t, headSha, strings.TrimSpace(string(gotOut)))
// file.txt content matches.
dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
require.NoError(t, err)
require.Equal(t, "hello\n", string(dt))
})
// Provenance subtest: assert that the git-commit material and the
// bundle-blob material are both recorded.
t.Run("provenance", func(t *testing.T) {
workers.CheckFeatureCompat(t, sb, workers.FeatureProvenance)
destDir := t.TempDir()
st := llb.Scratch().File(
llb.Copy(
llb.Git(
repoURL,
"master",
llb.GitChecksum(headSha),
llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
),
"file.txt",
"file.txt",
),
)
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
FrontendAttrs: map[string]string{
"attest:provenance": "",
},
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
OCIStores: map[string]content.Store{
csID: store,
},
}, nil)
require.NoError(t, err)
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
require.NoError(t, err)
var stmt struct {
intoto.StatementHeader
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
}
require.NoError(t, json.Unmarshal(provDt, &stmt))
// Bundle-backed git sources emit two materials: the git source
// at its raw repository URL (same shape as a non-bundle git
// source), and the bundle blob as a purl with a vcs_url
// qualifier linking back to the git URL.
expectedGitURI := repoURL + "#master"
var foundCommit, foundBundle bool
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
if m.URI == expectedGitURI && m.Digest["sha1"] == headSha {
foundCommit = true
}
if strings.HasPrefix(m.URI, "pkg:oci/") && strings.Contains(m.URI, "ref_type=bundle") && strings.Contains(m.URI, "vcs_url=") && m.Digest["sha256"] == bundleDgst.Hex() {
foundBundle = true
}
}
require.True(t, foundCommit, "expected git commit material %q with sha1=%s in %+v", expectedGitURI, headSha, stmt.Predicate.BuildDefinition.ResolvedDependencies)
require.True(t, foundBundle, "expected bundle blob material (pkg:oci/...?ref_type=bundle) with sha256=%s in %+v", bundleDgst.Hex(), stmt.Predicate.BuildDefinition.ResolvedDependencies)
})
checkAllReleasable(t, c, sb, false)
}
// testGitBundleRoundTripRegistry mirrors testGitBundleRoundTrip but resolves
// the bundle blob through a registry using the docker-image+blob:// scheme.
// The bundle bytes are uploaded directly to the sandbox registry by digest
// and then re-imported in Build 2.
func testGitBundleRoundTripRegistry(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
integration.SkipOnPlatform(t, "windows")
requiresLinux(t)
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
gitDir := t.TempDir()
err = runInDir(gitDir,
"git init -b master",
"git config --local user.email test@example.com",
"git config --local user.name test",
"echo hello > file.txt",
"git add file.txt",
"git commit -m initial",
"git update-server-info",
)
require.NoError(t, err)
cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
cmd.Dir = gitDir
out, err := cmd.Output()
require.NoError(t, err)
headSha := strings.TrimSpace(string(out))
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
defer server.Close()
repoURL := server.URL + "/.git"
// Build 1: export a bundle via GitCheckoutBundle.
bundleDir := t.TempDir()
st := llb.Git(repoURL, "master", llb.GitChecksum(headSha), llb.GitCheckoutBundle())
def, err := st.Marshal(ctx)
require.NoError(t, err)
_, err = c.Solve(ctx, def, SolveOpt{
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: bundleDir}},
}, nil)
require.NoError(t, err)
bundleBytes, err := os.ReadFile(filepath.Join(bundleDir, "bundle"))
require.NoError(t, err)
require.NotEmpty(t, bundleBytes)
bundleDgst := digest.FromBytes(bundleBytes)
// Push the bundle as a raw blob into the sandbox registry under a
// repository name. The docker registry accepts arbitrary blobs by
// digest; the repository exists implicitly once any blob is uploaded
// under its name.
bundleRepoRef := registry + "/foo/bundle@" + bundleDgst.String()
ingester, err := contentutil.IngesterFromRef(sb.Context(), bundleRepoRef)
require.NoError(t, err)
err = content.WriteBlob(ctx, ingester, "bundle-"+bundleDgst.String(), bytes.NewReader(bundleBytes),
ocispecs.Descriptor{Digest: bundleDgst, Size: int64(len(bundleBytes))})
require.NoError(t, err)
bundleLocator := "docker-image+blob://" + registry + "/foo/bundle@" + bundleDgst.String()
t.Run("checkout", func(t *testing.T) {
destDir := t.TempDir()
st := llb.Git(
repoURL,
"master",
llb.GitChecksum(headSha),
llb.GitBundleURL(bundleLocator),
)
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
require.NoError(t, err)
require.Equal(t, "hello\n", string(dt))
})
t.Run("provenance", func(t *testing.T) {
workers.CheckFeatureCompat(t, sb, workers.FeatureProvenance)
destDir := t.TempDir()
st := llb.Scratch().File(
llb.Copy(
llb.Git(
repoURL,
"master",
llb.GitChecksum(headSha),
llb.GitBundleURL(bundleLocator),
),
"file.txt",
"file.txt",
),
)
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
FrontendAttrs: map[string]string{
"attest:provenance": "",
},
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
}, nil)
require.NoError(t, err)
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
require.NoError(t, err)
var stmt struct {
intoto.StatementHeader
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
}
require.NoError(t, json.Unmarshal(provDt, &stmt))
expectedGitURI := repoURL + "#master"
var foundCommit, foundBundle bool
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
if m.URI == expectedGitURI && m.Digest["sha1"] == headSha {
foundCommit = true
}
if strings.HasPrefix(m.URI, "pkg:docker/") && strings.Contains(m.URI, "ref_type=bundle") && strings.Contains(m.URI, "vcs_url=") && m.Digest["sha256"] == bundleDgst.Hex() {
foundBundle = true
}
}
require.True(t, foundCommit, "expected git commit material %q with sha1=%s in %+v", expectedGitURI, headSha, stmt.Predicate.BuildDefinition.ResolvedDependencies)
require.True(t, foundBundle, "expected bundle blob material (pkg:docker/...?ref_type=bundle) with sha256=%s in %+v", bundleDgst.Hex(), stmt.Predicate.BuildDefinition.ResolvedDependencies)
})
checkAllReleasable(t, c, sb, false)
}
func testGitResolveMutatedSource(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
gitDir := t.TempDir()
gitCommands := []string{
"git init",
"git config --local user.email test",
"git config --local user.name test",
"echo a > a",
"git add a",
"git commit -m a",
"git tag -a v0.1 -m v0.1",
"echo b > b",
"git add b",
"git commit -m b",
"git checkout -B v2",
"git update-server-info",
}
err = runInDir(gitDir, gitCommands...)
require.NoError(t, err)
cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1")
cmd.Dir = gitDir
out, err := cmd.Output()
require.NoError(t, err)
commitTag := strings.TrimSpace(string(out))
cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1^{commit}")
cmd.Dir = gitDir
out, err = cmd.Output()
require.NoError(t, err)
commitTagCommit := strings.TrimSpace(string(out))
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
defer server.Close()
dest := t.TempDir()
_, err = c.Build(ctx, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: dest,
},
},
}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
id := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git#v0.1"
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
Attrs: map[string]string{
"git.fullurl": server.URL + "/.git",
},
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.Git)
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
require.Equal(t, commitTag, md.Git.Checksum)
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
require.Equal(t, id, md.Op.Identifier)
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
// update the tag to point to a different commit
err = runInDir(gitDir, []string{
"git tag -f v0.1",
"git update-server-info",
}...)
require.NoError(t, err)
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
Attrs: map[string]string{
"git.fullurl": server.URL + "/.git",
},
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.Git)
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
require.Equal(t, commitTag, md.Git.Checksum)
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
require.Equal(t, id, md.Op.Identifier)
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
st := llb.Git(server.URL+"/.git", "", llb.GitRef("v0.1"))
def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}
return c.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
}, nil)
require.NoError(t, err)
_, err = os.ReadFile(filepath.Join(dest, "b"))
require.Error(t, err)
require.True(t, os.IsNotExist(err), "expected file b to not exist")
dt, err := os.ReadFile(filepath.Join(dest, "a"))
require.NoError(t, err)
require.Equal(t, "a\n", string(dt))
checkAllReleasable(t, c, sb, false)
}
// testGitResolveCacheAfterFailure resolves the same ref repeatedly in one
// build after a failed and a cached resolve, so the resolver cache is read
// after both have released their lock.
func testGitResolveCacheAfterFailure(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
gitDir := t.TempDir()
gitCommands := []string{
"git init",
"git config --local user.email test",
"git config --local user.name test",
"echo a > a",
"git add a",
"git commit -m a",
"git branch v1",
"echo b > b",
"git add b",
"git commit -m b",
"git update-server-info",
}
err = runInDir(gitDir, gitCommands...)
require.NoError(t, err)
cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "v1")
cmd.Dir = gitDir
out, err := cmd.Output()
require.NoError(t, err)
commitV1 := strings.TrimSpace(string(out))
cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
cmd.Dir = gitDir
out, err = cmd.Output()
require.NoError(t, err)
commitHead := strings.TrimSpace(string(out))
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
defer server.Close()
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
resolve := func(checksum string) (*sourceresolver.MetaResponse, error) {
attrs := map[string]string{
"git.fullurl": server.URL + "/.git",
}
if checksum != "" {
attrs[pb.AttrGitChecksum] = checksum
}
return c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git#v1",
Attrs: attrs,
}, sourceresolver.Opt{})
}
// failed resolve
_, err := resolve(commitHead)
require.ErrorContains(t, err, "expected checksum to match")
md, err := resolve("")
require.NoError(t, err)
require.NotNil(t, md.Git)
require.Equal(t, commitV1, md.Git.Checksum)
// resolved from cache
md, err = resolve("")
require.NoError(t, err)
require.NotNil(t, md.Git)
require.Equal(t, commitV1, md.Git.Checksum)
// cached value does not match the checksum
_, err = resolve(commitHead)
require.ErrorContains(t, err, "expected checksum to match")
return gateway.NewResult(), nil
}, nil)
require.NoError(t, err)
checkAllReleasable(t, c, sb, false)
}
func testGitResolveSourceMetadata(t *testing.T, sb integration.Sandbox) {
// TODO: remove once CI has Git for Windows >= 2.56.0.windows.2
// https://github.com/git-for-windows/git/issues/6449
integration.SkipOnPlatform(t, "windows")
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
gitDir := t.TempDir()
gitCommands := []string{
"git init",
"git config --local user.email test@example.com",
"git config --local user.name test",
"touch a",
"git add a",
"git commit -m msg",
"git tag -a v0.1 -m v0.1release",
"echo b > b",
"git add b",
"git commit -m b",
"git checkout -B v2",
"git update-server-info",
}
err = runInDir(gitDir, gitCommands...)
require.NoError(t, err)
cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "HEAD")
cmd.Dir = gitDir
out, err := cmd.Output()
require.NoError(t, err)
commitHEAD := strings.TrimSpace(string(out))
cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1")
cmd.Dir = gitDir
out, err = cmd.Output()
require.NoError(t, err)
commitTag := strings.TrimSpace(string(out))
cmd = exec.CommandContext(t.Context(), "git", "rev-parse", "v0.1^{commit}")
cmd.Dir = gitDir
out, err = cmd.Output()
require.NoError(t, err)
commitTagCommit := strings.TrimSpace(string(out))
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
defer server.Close()
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
id := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git"
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
Attrs: map[string]string{
"git.fullurl": server.URL + "/.git",
},
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.Git)
require.Equal(t, "refs/heads/v2", md.Git.Ref) // default to branch head
require.Equal(t, commitHEAD, md.Git.Checksum)
require.Equal(t, "", md.Git.CommitChecksum) // not annotated tag
require.Equal(t, id, md.Op.Identifier)
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
require.Nil(t, md.Git.CommitObject)
require.Nil(t, md.Git.TagObject)
id += "#v0.1"
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
Attrs: map[string]string{
"git.fullurl": server.URL + "/.git",
},
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.Git)
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
require.Equal(t, commitTag, md.Git.Checksum) // annotated tag
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
require.Equal(t, id, md.Op.Identifier)
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
require.Nil(t, md.Git.CommitObject)
require.Nil(t, md.Git.TagObject)
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
Attrs: map[string]string{
"git.fullurl": server.URL + "/.git",
},
}, sourceresolver.Opt{
GitOpt: &sourceresolver.ResolveGitOpt{
ReturnObject: true,
},
})
if err != nil {
return nil, err
}
require.NotNil(t, md.Git)
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
require.Equal(t, commitTag, md.Git.Checksum) // annotated tag
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
require.Equal(t, id, md.Op.Identifier)
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
require.NotNil(t, md.Git.CommitObject)
require.NotNil(t, md.Git.TagObject)
commitObj, err := gitobject.Parse(md.Git.CommitObject)
require.NoError(t, err)
require.NoError(t, commitObj.VerifyChecksum(md.Git.CommitChecksum))
commit, err := commitObj.ToCommit()
require.NoError(t, err)
require.Equal(t, "msg", commit.Message)
require.Equal(t, "test", commit.Author.Name)
require.Equal(t, "test@example.com", commit.Author.Email)
require.Equal(t, "test", commit.Committer.Name)
require.Equal(t, "test@example.com", commit.Committer.Email)
commitTime := commit.Committer.When
require.NotNil(t, commitTime)
require.WithinDuration(t, time.Now(), *commitTime, 2*time.Minute)
tagObj, err := gitobject.Parse(md.Git.TagObject)
require.NoError(t, err)
require.NoError(t, tagObj.VerifyChecksum(md.Git.Checksum))
tag, err := tagObj.ToTag()
require.NoError(t, err)
require.Equal(t, "v0.1release", tag.Message)
require.Equal(t, "v0.1", tag.Tag)
require.Equal(t, "test", tag.Tagger.Name)
require.Equal(t, "test@example.com", tag.Tagger.Email)
tagTime := tag.Tagger.When
require.NotNil(t, tagTime)
require.WithinDuration(t, time.Now(), *tagTime, 2*time.Minute)
return nil, nil
}, nil)
require.NoError(t, err)
}
func runInDir(dir string, cmds ...string) error {
return runInDirEnv(dir, nil, cmds...)
}
func runInDirEnv(dir string, env []string, cmds ...string) error {
for _, args := range cmds {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.CommandContext(context.TODO(), "powershell", "-command", args)
} else {
cmd = exec.CommandContext(context.TODO(), "sh", "-c", args)
}
cmd.Env = append(os.Environ(), env...)
cmd.Dir = dir
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "error running %v", args)
}
}
return nil
}