package git import ( "bytes" "context" "fmt" "io" "net/http" "net/http/cgi" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "testing" "time" "github.com/containerd/containerd/v2/core/diff/apply" ctdmetadata "github.com/containerd/containerd/v2/core/metadata" "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/containerd/v2/pkg/namespaces" "github.com/containerd/containerd/v2/plugins/content/local" "github.com/containerd/containerd/v2/plugins/diff/walking" "github.com/containerd/containerd/v2/plugins/snapshots/native" "github.com/moby/buildkit/cache" "github.com/moby/buildkit/cache/metadata" "github.com/moby/buildkit/client" "github.com/moby/buildkit/session" "github.com/moby/buildkit/snapshot" containerdsnapshot "github.com/moby/buildkit/snapshot/containerd" "github.com/moby/buildkit/solver" "github.com/moby/buildkit/solver/llbsolver/compat" "github.com/moby/buildkit/util/gitutil" "github.com/moby/buildkit/util/gitutil/gitobject" "github.com/moby/buildkit/util/gitutil/gitsign" "github.com/moby/buildkit/util/leaseutil" "github.com/moby/buildkit/util/progress" "github.com/moby/buildkit/util/progress/logs" "github.com/moby/buildkit/util/winlayers" "github.com/pkg/errors" "github.com/stretchr/testify/require" bolt "go.etcd.io/bbolt" ) const signFixturesPathEnv = "BUILDKIT_TEST_SIGN_FIXTURES" func TestRepeatedFetchSHA1(t *testing.T) { testRepeatedFetch(t, false, "sha1") } func TestRepeatedFetchKeepGitDirSHA1(t *testing.T) { testRepeatedFetch(t, true, "sha1") } func TestRepeatedFetchSHA256(t *testing.T) { testRepeatedFetch(t, false, "sha256") } func TestRepeatedFetchKeepGitDirSHA256(t *testing.T) { testRepeatedFetch(t, true, "sha256") } func TestFetchAfterSubmoduleRemovalSHA1(t *testing.T) { testFetchAfterSubmoduleRemoval(t, "sha1") } func TestFetchAfterSubmoduleRemovalSHA256(t *testing.T) { testFetchAfterSubmoduleRemoval(t, "sha256") } func testRepeatedFetch(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) expLen := 40 if format == "sha256" { expLen = 64 } expPinLen := expLen if keepGitDir { expLen += 4 require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, expPinLen, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err := os.ReadFile(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, "bar\n", string(dt)) _, err = os.Lstat(filepath.Join(dir, "ghi")) require.ErrorIs(t, err, os.ErrNotExist) _, err = os.Lstat(filepath.Join(dir, "sub/subfile")) require.ErrorIs(t, err, os.ErrNotExist) // second fetch returns same dir id = &GitIdentifier{Remote: repo.mainURL, Ref: "master", KeepGitDir: keepGitDir} g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key2, pin2, _, _, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, key1, key2) require.Equal(t, pin1, pin2) ref2, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref2.Release(context.TODO()) require.Equal(t, ref1.ID(), ref2.ID()) id = &GitIdentifier{Remote: repo.mainURL, Ref: "feature", KeepGitDir: keepGitDir} g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key3, pin3, _, _, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.NotEqual(t, key1, key3) require.NotEqual(t, pin1, pin3) ref3, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref3.Release(context.TODO()) mount, err = ref3.Mount(ctx, true, nil) require.NoError(t, err) lm = snapshot.LocalMounter(mount) dir, err = lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err = os.ReadFile(filepath.Join(dir, "ghi")) require.NoError(t, err) require.Equal(t, "baz\n", string(dt)) dt, err = os.ReadFile(filepath.Join(dir, "sub/subfile")) require.NoError(t, err) require.Equal(t, "subcontents\n", string(dt)) // The key should not change regardless to the existence of Checksum // https://github.com/moby/buildkit/pull/5975#discussion_r2092206059 id.Checksum = pin3 g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key4, pin4, _, _, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, key3, key4) require.Equal(t, pin3, pin4) } func testFetchAfterSubmoduleRemoval(t *testing.T, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, Ref: "feature"} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) runShell(t, repo.mainPath, "git checkout feature", "git submodule deinit -f sub", "git rm sub", "git commit -m removesub", ) g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key2, pin2, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.NotEqual(t, key1, key2) require.NotEqual(t, pin1, pin2) ref2, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref2.Release(context.TODO()) mount, err := ref2.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err := os.ReadFile(filepath.Join(dir, "ghi")) require.NoError(t, err) require.Equal(t, "baz\n", string(dt)) _, err = os.Lstat(filepath.Join(dir, "sub")) require.ErrorIs(t, err, os.ErrNotExist) } func TestFetchBySHA1(t *testing.T) { testFetchBySHA(t, "sha1", false) } func TestFetchBySHA1KeepGitDir(t *testing.T) { testFetchBySHA(t, "sha1", true) } func TestFetchBySHA256(t *testing.T) { testFetchBySHA(t, "sha256", false) } func TestFetchBySHA256KeepGitDir(t *testing.T) { testFetchBySHA(t, "sha256", true) } func testFetchBySHA(t *testing.T, format string, keepGitDir bool) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "feature") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) var shaLen int switch format { case "sha1": shaLen = 40 case "sha256": shaLen = 64 default: t.Fatalf("unexpected format: %q", format) } sha := strings.TrimSpace(string(out)) require.Equal(t, shaLen, len(sha)) id := &GitIdentifier{Remote: repo.mainURL, Ref: sha, KeepGitDir: keepGitDir} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) expLen := shaLen if keepGitDir { expLen += 4 require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, shaLen, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err := os.ReadFile(filepath.Join(dir, "ghi")) require.NoError(t, err) require.Equal(t, "baz\n", string(dt)) dt, err = os.ReadFile(filepath.Join(dir, "sub/subfile")) require.NoError(t, err) require.Equal(t, "subcontents\n", string(dt)) } func TestFetchUnreferencedTagShaSHA1(t *testing.T) { testFetchUnreferencedRefSha(t, "v1.2.3-special", false, "sha1") } func TestFetchUnreferencedTagShaKeepGitDirSHA1(t *testing.T) { testFetchUnreferencedRefSha(t, "v1.2.3-special", true, "sha1") } func TestFetchUnreferencedRefShaSHA1(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special", false, "sha1") } func TestFetchUnreferencedRefShaKeepGitDirSHA1(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special", true, "sha1") } func TestFetchUnadvertisedRefShaSHA1(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special~", false, "sha1") } func TestFetchUnadvertisedRefShaKeepGitDirSHA1(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special~", true, "sha1") } func TestFetchUnreferencedTagShaSHA256(t *testing.T) { testFetchUnreferencedRefSha(t, "v1.2.3-special", false, "sha256") } func TestFetchUnreferencedTagShaKeepGitDirSHA256(t *testing.T) { testFetchUnreferencedRefSha(t, "v1.2.3-special", true, "sha256") } func TestFetchUnreferencedRefShaSHA256(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special", false, "sha256") } func TestFetchUnreferencedRefShaKeepGitDirSHA256(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special", true, "sha256") } func TestFetchUnadvertisedRefShaSHA256(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special~", false, "sha256") } func TestFetchUnadvertisedRefShaKeepGitDirSHA256(t *testing.T) { testFetchUnreferencedRefSha(t, "refs/special~", true, "sha256") } // testFetchUnreferencedRefSha tests fetching a SHA that points to a ref that is not reachable from any branch. func testFetchUnreferencedRefSha(t *testing.T, ref string, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", ref) cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expSHALen := 40 if format == "sha256" { expSHALen = 64 } sha := strings.TrimSpace(string(out)) require.Equal(t, expSHALen, len(sha)) id := &GitIdentifier{Remote: repo.mainURL, Ref: sha, KeepGitDir: keepGitDir} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) expLen := 40 if format == "sha256" { expLen = 64 } expPinLen := expLen if keepGitDir { expLen += 4 require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, expPinLen, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err := os.ReadFile(filepath.Join(dir, "bar")) require.NoError(t, err) require.Equal(t, "foo\n", string(dt)) } func TestFetchByCommitSHA1(t *testing.T) { testFetchByCommit(t, "sha1", false) } func TestFetchByCommitKeepGitDirSHA1(t *testing.T) { testFetchByCommit(t, "sha1", true) } func TestFetchByCommitSHA256(t *testing.T) { testFetchByCommit(t, "sha256", false) } func TestFetchByCommitKeepGitDirSHA256(t *testing.T) { testFetchByCommit(t, "sha256", true) } // testFetchByCommit exercises the FetchByCommit mode: the branch points to // commit A initially, then it is moved to commit B on the remote. A second // fetch that asks for commit A via FetchByCommit with the branch ref must // still match the first cache key and produce identical content. func testFetchByCommit(t *testing.T, format string, keepGitDir bool) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) // Capture the SHA of the current master branch (points at "third"). cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "refs/heads/master") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) firstSha := strings.TrimSpace(string(out)) // First fetch: branch "master" at commit firstSha. id := &GitIdentifier{Remote: repo.mainURL, Ref: "master", KeepGitDir: keepGitDir} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, _, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, firstSha, pin1) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) dt1, err := os.ReadFile(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, "bar\n", string(dt1)) if keepGitDir { require.Equal(t, firstSha, gitRevParse(t, dir, "refs/heads/master")) } require.NoError(t, lm.Unmount()) // Move master on the remote to a different commit. runShell(t, repo.mainPath, "git checkout master", "echo moved > newfile", "git add newfile", "git commit -m moved", ) cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "refs/heads/master") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) secondSha := strings.TrimSpace(string(out)) require.NotEqual(t, firstSha, secondSha) // Sanity check: without FetchByCommit, resolving "master" now returns the // new commit, which yields a different cache key. idMoved := &GitIdentifier{Remote: repo.mainURL, Ref: "master", KeepGitDir: keepGitDir} gMoved, err := gs.Resolve(ctx, idMoved, nil, nil) require.NoError(t, err) keyMoved, pinMoved, _, _, err := gMoved.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, secondSha, pinMoved) require.NotEqual(t, key1, keyMoved) // Without FetchByCommit, passing the old checksum for the now-moved ref // is rejected because the ref resolves to a different commit. idStale := &GitIdentifier{ Remote: repo.mainURL, Ref: "refs/heads/master", Checksum: firstSha, KeepGitDir: keepGitDir, } gStale, err := gs.Resolve(ctx, idStale, nil, nil) require.NoError(t, err) _, _, _, _, err = gStale.CacheKey(ctx, nil, 0) require.Error(t, err) require.ErrorContains(t, err, "expected checksum to match") // Fetch the old commit by checksum with FetchByCommit and the same // fully-qualified ref name that was originally produced. The cache key // and pin should match the first fetch. idFbc := &GitIdentifier{ Remote: repo.mainURL, Ref: "refs/heads/master", Checksum: firstSha, KeepGitDir: keepGitDir, FetchByCommit: true, } gFbc, err := gs.Resolve(ctx, idFbc, nil, nil) require.NoError(t, err) key2, pin2, _, _, err := gFbc.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, key1, key2) require.Equal(t, pin1, pin2) ref2, err := gFbc.Snapshot(ctx, nil) require.NoError(t, err) defer ref2.Release(context.TODO()) // Snapshot is reused from the first fetch since the cache key matches. require.Equal(t, ref1.ID(), ref2.ID()) // Unqualified short refs must be canonicalized to refs/heads/ so // that FetchByCommit produces the same cache key as the original fetch, // which used ls-remote-driven normalization. See discussion around // shaToCacheKey and the partialRef/headRef resolution in resolveMetadata. idFbcShort := &GitIdentifier{ Remote: repo.mainURL, Ref: "master", Checksum: firstSha, KeepGitDir: keepGitDir, FetchByCommit: true, } gFbcShort, err := gs.Resolve(ctx, idFbcShort, nil, nil) require.NoError(t, err) keyShort, pinShort, _, _, err := gFbcShort.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, key1, keyShort) require.Equal(t, pin1, pinShort) // A fresh git source (empty cache) fetching the same commit via // FetchByCommit must produce the same cache key and identical content, // even though no prior snapshot exists to reuse. gsFresh := setupGitSource(t, t.TempDir()) gFresh, err := gsFresh.Resolve(ctx, idFbc, nil, nil) require.NoError(t, err) key3, pin3, _, _, err := gFresh.CacheKey(ctx, nil, 0) require.NoError(t, err) require.Equal(t, key1, key3) require.Equal(t, pin1, pin3) ref3, err := gFresh.Snapshot(ctx, nil) require.NoError(t, err) defer ref3.Release(context.TODO()) require.NotEqual(t, ref1.ID(), ref3.ID()) mount, err = ref3.Mount(ctx, true, nil) require.NoError(t, err) lm = snapshot.LocalMounter(mount) dir, err = lm.Mount() require.NoError(t, err) defer lm.Unmount() dt3, err := os.ReadFile(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, "bar\n", string(dt3)) // File added by the "moved" commit must not be present, since we fetched // the original firstSha rather than the new tip of master. _, err = os.Lstat(filepath.Join(dir, "newfile")) require.ErrorIs(t, err, os.ErrNotExist) if keepGitDir { require.Equal(t, firstSha, gitRevParse(t, dir, "refs/heads/master")) } } func gitRevParse(t *testing.T, workDir, rev string) string { t.Helper() cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", rev) cmd.Dir = workDir out, err := cmd.Output() require.NoError(t, err, "git rev-parse %s in %s", rev, workDir) return strings.TrimSpace(string(out)) } func TestFetchByCommitRequiresChecksumSHA1(t *testing.T) { testFetchByCommitRequiresChecksum(t, "sha1") } func testFetchByCommitRequiresChecksum(t *testing.T, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, Ref: "refs/heads/master", FetchByCommit: true} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, _, err = g.CacheKey(ctx, nil, 0) require.Error(t, err) require.ErrorContains(t, err, "fetch-by-commit") } func TestFetchByTagSHA1(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeNone, "sha1") } func TestFetchByTagKeepGitDirSHA1(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, true, testChecksumModeNone, "sha1") } func TestFetchByTagFullSHA1(t *testing.T) { testFetchByTag(t, "refs/tags/lightweight-tag", "third", false, true, true, testChecksumModeNone, "sha1") } func TestFetchByAnnotatedTagSHA1(t *testing.T) { testFetchByTag(t, "v1.2.3", "second", true, false, false, testChecksumModeNone, "sha1") } func TestFetchByAnnotatedTagKeepGitDirSHA1(t *testing.T) { testFetchByTag(t, "v1.2.3", "second", true, false, true, testChecksumModeNone, "sha1") } func TestFetchByAnnotatedTagFullSHA1(t *testing.T) { testFetchByTag(t, "refs/tags/v1.2.3", "second", true, false, true, testChecksumModeNone, "sha1") } func TestFetchByBranchSHA1(t *testing.T) { testFetchByTag(t, "feature", "withsub", false, true, false, testChecksumModeNone, "sha1") } func TestFetchByBranchKeepGitDirSHA1(t *testing.T) { testFetchByTag(t, "feature", "withsub", false, true, true, testChecksumModeNone, "sha1") } func TestFetchByBranchFullSHA1(t *testing.T) { testFetchByTag(t, "refs/heads/feature", "withsub", false, true, true, testChecksumModeNone, "sha1") } func TestFetchByRefSHA1(t *testing.T) { testFetchByTag(t, "test", "feature", false, true, false, testChecksumModeNone, "sha1") } func TestFetchByRefKeepGitDirSHA1(t *testing.T) { testFetchByTag(t, "test", "feature", false, true, true, testChecksumModeNone, "sha1") } func TestFetchByRefFullSHA1(t *testing.T) { testFetchByTag(t, "refs/test", "feature", false, true, true, testChecksumModeNone, "sha1") } func TestFetchByTagWithChecksumSHA1(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeValid, "sha1") } func TestFetchByTagWithChecksumPartialSHA1(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeValidPartial, "sha1") } func TestFetchByTagWithChecksumInvalidSHA1(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeInvalid, "sha1") } func TestFetchByTagSHA256(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeNone, "sha256") } func TestFetchByTagKeepGitDirSHA256(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, true, testChecksumModeNone, "sha256") } func TestFetchByTagFullSHA256(t *testing.T) { testFetchByTag(t, "refs/tags/lightweight-tag", "third", false, true, true, testChecksumModeNone, "sha256") } func TestFetchByAnnotatedTagSHA256(t *testing.T) { testFetchByTag(t, "v1.2.3", "second", true, false, false, testChecksumModeNone, "sha256") } func TestFetchByAnnotatedTagKeepGitDirSHA256(t *testing.T) { testFetchByTag(t, "v1.2.3", "second", true, false, true, testChecksumModeNone, "sha256") } func TestFetchByAnnotatedTagFullSHA256(t *testing.T) { testFetchByTag(t, "refs/tags/v1.2.3", "second", true, false, true, testChecksumModeNone, "sha256") } func TestFetchByBranchSHA256(t *testing.T) { testFetchByTag(t, "feature", "withsub", false, true, false, testChecksumModeNone, "sha256") } func TestFetchByBranchKeepGitDirSHA256(t *testing.T) { testFetchByTag(t, "feature", "withsub", false, true, true, testChecksumModeNone, "sha256") } func TestFetchByBranchFullSHA256(t *testing.T) { testFetchByTag(t, "refs/heads/feature", "withsub", false, true, true, testChecksumModeNone, "sha256") } func TestFetchByRefSHA256(t *testing.T) { testFetchByTag(t, "test", "feature", false, true, false, testChecksumModeNone, "sha256") } func TestFetchByRefKeepGitDirSHA256(t *testing.T) { testFetchByTag(t, "test", "feature", false, true, true, testChecksumModeNone, "sha256") } func TestFetchByRefFullSHA256(t *testing.T) { testFetchByTag(t, "refs/test", "feature", false, true, true, testChecksumModeNone, "sha256") } func TestFetchByTagWithChecksumSHA256(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeValid, "sha256") } func TestFetchByTagWithChecksumPartialSHA256(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeValidPartial, "sha256") } func TestFetchByTagWithChecksumInvalidSHA256(t *testing.T) { testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeInvalid, "sha256") } type testChecksumMode int const ( testChecksumModeNone testChecksumMode = iota testChecksumModeValid testChecksumModeValidPartial testChecksumModeInvalid ) func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotatedTag, hasFoo13File, keepGitDir bool, checksumMode testChecksumMode, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, Ref: tag, KeepGitDir: keepGitDir} if checksumMode != testChecksumModeNone { cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", tag) cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } sha := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(sha)) switch checksumMode { case testChecksumModeValid: id.Checksum = sha case testChecksumModeValidPartial: id.Checksum = sha[:8] case testChecksumModeInvalid: id.Checksum = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" if format == "sha256" { id.Checksum = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" } default: // NOTREACHED } } g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) if checksumMode == testChecksumModeInvalid { require.ErrorContains(t, err, "expected checksum to match "+id.Checksum) return } require.NoError(t, err) require.True(t, done) expLen := 40 if format == "sha256" { expLen = 64 } expPinLen := expLen if keepGitDir { expLen += 4 require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, expPinLen, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() st, err := os.Lstat(filepath.Join(dir, "subdir")) require.NoError(t, err) require.True(t, st.IsDir()) require.Equal(t, strconv.FormatInt(0755, 8), strconv.FormatInt(int64(st.Mode()&os.ModePerm), 8)) dt, err := os.ReadFile(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, "bar\n", string(dt)) st, err = os.Lstat(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, strconv.FormatInt(0644, 8), strconv.FormatInt(int64(st.Mode()&os.ModePerm), 8)) dt, err = os.ReadFile(filepath.Join(dir, "foo13")) if hasFoo13File { require.NoError(t, err) require.Equal(t, "sbb\n", string(dt)) } else { require.ErrorIs(t, err, os.ErrNotExist) } if keepGitDir { git := gitutil.NewGitCLI( gitutil.WithExec(runWithStandardUmask), gitutil.WithStreams(func(ctx context.Context) (stdout, stderr io.WriteCloser, flush func()) { return logs.NewLogStreams(ctx, false) }), gitutil.WithWorkTree(dir), ) // get current commit sha headCommit, err := git.Run(ctx, "rev-parse", "HEAD") require.NoError(t, err) if isAnnotatedTag { // get commit sha that the annotated tag points to annotatedTagCommit, err := git.Run(ctx, "rev-list", "-n", "1", tag) require.NoError(t, err) // HEAD should match the actual commit sha (and not the sha of the annotated tag, // since it's not possible to checkout a non-commit object) require.Equal(t, string(annotatedTagCommit), string(headCommit)) annotatedTagSHA, err := git.Run(ctx, "rev-parse", tag) require.NoError(t, err) require.Equal(t, strings.TrimSpace(string(annotatedTagSHA)), pin1) } else { // ensure that we checked out the same commit as was in the cache key require.Equal(t, strings.TrimSpace(string(headCommit)), pin1) } // test that we checked out the correct commit // (in the case of an annotated tag, this message is of the commit the annotated tag points to // and not the message of the tag) gitLogOutput, err := git.Run(ctx, "log", "-n", "1", "--format=%s") require.NoError(t, err) require.Contains(t, strings.TrimSpace(string(gitLogOutput)), expectedCommitSubject) } } func TestFetchAnnotatedTagAfterCloneSHA1(t *testing.T) { testFetchAnnotatedTagAfterClone(t, "sha1") } func TestFetchAnnotatedTagAfterCloneSHA256(t *testing.T) { testFetchAnnotatedTagAfterClone(t, "sha256") } func testFetchAnnotatedTagAfterClone(t *testing.T, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "HEAD") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } sha := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(sha)) gs := setupGitSource(t, t.TempDir()) id := &GitIdentifier{Remote: repo.mainURL, Ref: sha, KeepGitDir: true} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.GreaterOrEqual(t, len(key1), expLen+4) require.Equal(t, expLen, len(pin1)) ref, err := g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) id = &GitIdentifier{Remote: repo.mainURL, Ref: "refs/tags/v1.2.3", KeepGitDir: true} g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err = g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.GreaterOrEqual(t, len(key1), expLen+4) require.Equal(t, expLen, len(pin1)) ref, err = g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) } func TestFetchAnnotatedTagChecksumsSHA1(t *testing.T) { testFetchAnnotatedTagChecksums(t, "sha1", false) } func TestFetchAnnotatedTagChecksumsSHA256(t *testing.T) { testFetchAnnotatedTagChecksums(t, "sha256", false) } func TestFetchAnnotatedTagChecksumsKeepGitDirSHA1(t *testing.T) { testFetchAnnotatedTagChecksums(t, "sha1", true) } func TestFetchAnnotatedTagChecksumsKeepGitDirSHA256(t *testing.T) { testFetchAnnotatedTagChecksums(t, "sha256", true) } func testFetchAnnotatedTagChecksums(t *testing.T, format string, keepGitDir bool) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } shaTag := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaTag)) // make sure this is an annotated tag cmd = exec.CommandContext(context.TODO(), "git", "cat-file", "-t", shaTag) cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) require.Equal(t, "tag\n", string(out)) // get commit that the tag points to cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3^{}") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) shaCommit := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaCommit)) require.NotEqual(t, shaTag, shaCommit) gs := setupGitSource(t, t.TempDir()) tcases := []*GitIdentifier{ {Remote: repo.mainURL, Ref: "v1.2.3", KeepGitDir: keepGitDir}, {Remote: repo.mainURL, Ref: "refs/tags/v1.2.3", KeepGitDir: keepGitDir}, {Remote: repo.mainURL, Ref: "v1.2.3", KeepGitDir: keepGitDir, Checksum: shaTag}, {Remote: repo.mainURL, Ref: "v1.2.3", KeepGitDir: keepGitDir, Checksum: shaCommit}, } for _, id := range tcases { g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key, pin, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) if keepGitDir { require.GreaterOrEqual(t, len(key), expLen+4) } else { require.Equal(t, expLen, len(key)) } require.Equal(t, expLen, len(pin)) require.Equal(t, shaTag, pin) // key is based on commit sha if keepGitDir is false if keepGitDir { require.Equal(t, shaTag+".git#refs/tags/v1.2.3", key) } else { require.Equal(t, shaCommit, key) } ref, err := g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) if keepGitDir { mountable, err := ref.Mount(ctx, true, nil) require.NoError(t, err) lm1 := snapshot.LocalMounter(mountable) dir, err := lm1.Mount() require.NoError(t, err) defer lm1.Unmount() cmd = exec.CommandContext(context.TODO(), "git", "tag", "--points-at", "HEAD") cmd.Dir = dir out, err = cmd.Output() require.NoError(t, err) require.Equal(t, "v1.2.3\n", string(out)) cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3") cmd.Dir = dir out, err = cmd.Output() require.NoError(t, err) require.Equal(t, shaTag, strings.TrimSpace(string(out))) } } } func TestFetchTagChangeRaceSHA1(t *testing.T) { testFetchTagChangeRace(t, "sha1", false) } func TestFetchTagChangeRaceKeepGitDirSHA1(t *testing.T) { testFetchTagChangeRace(t, "sha1", true) } func TestFetchTagChangeRaceSHA256(t *testing.T) { testFetchTagChangeRace(t, "sha256", false) } func TestFetchTagChangeRaceKeepGitDirSHA256(t *testing.T) { testFetchTagChangeRace(t, "sha256", true) } // testFetchTagChangeRace tests case where tag is change in between cache key and snapshot calls. func testFetchTagChangeRace(t *testing.T, format string, keepGitDir bool) { ctx := t.Context() if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } shaTag := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaTag)) cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3^{}") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) shaTagCommit := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaTagCommit)) id := &GitIdentifier{Remote: repo.mainURL, Ref: "v1.2.3", KeepGitDir: keepGitDir} gs := setupGitSource(t, t.TempDir()) g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.Equal(t, shaTag, pin1) if keepGitDir { require.Equal(t, shaTag+".git#refs/tags/v1.2.3", key1) } else { require.Equal(t, shaTagCommit, key1) } cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3-special") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) shaTagNew := strings.TrimSpace(string(out)) require.NotEqual(t, shaTag, shaTagNew) // delete tag v1.2.3 cmd = exec.CommandContext(context.TODO(), "git", "tag", "-d", "v1.2.3") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) // recreate tag v1.2.3 to point to another commit cmd = exec.CommandContext(context.TODO(), "git", "tag", "v1.2.3", shaTagNew) cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) ref, err := g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) mount, err := ref.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() // bar only exists in the new commit _, err = os.ReadFile(filepath.Join(dir, "bar")) require.Error(t, err) require.True(t, os.IsNotExist(err)) if keepGitDir { // read current HEAD commit cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "HEAD") cmd.Dir = dir out, err = cmd.Output() require.NoError(t, err) shaHead := strings.TrimSpace(string(out)) require.Equal(t, shaTagCommit, shaHead) } } func TestFetchBranchChangeRaceSHA1(t *testing.T) { testFetchBranchChangeRace(t, "sha1", false) } func TestFetchBranchChangeRaceKeepGitDirSHA1(t *testing.T) { testFetchBranchChangeRace(t, "sha1", true) } func TestFetchBranchChangeRaceSHA256(t *testing.T) { testFetchBranchChangeRace(t, "sha256", false) } func TestFetchBranchChangeRaceKeepGitDirSHA256(t *testing.T) { testFetchBranchChangeRace(t, "sha256", true) } func testFetchBranchChangeRace(t *testing.T, format string, keepGitDir bool) { ctx := t.Context() if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "master") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } shaMaster := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaMaster)) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir} gs := setupGitSource(t, t.TempDir()) g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.Equal(t, shaMaster, pin1) if keepGitDir { require.Equal(t, shaMaster+".git#refs/heads/master", key1) } else { require.Equal(t, shaMaster, key1) } cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "feature") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) shaFeature := strings.TrimSpace(string(out)) require.NotEqual(t, shaMaster, shaFeature) // checkout feature as master cmd = exec.CommandContext(context.TODO(), "git", "checkout", "-B", "master", "feature") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) ref, err := g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) mount, err := ref.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() // ghi only exists in the feature branch _, err = os.ReadFile(filepath.Join(dir, "ghi")) require.Error(t, err) require.True(t, os.IsNotExist(err)) if keepGitDir { // read current HEAD commit cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "HEAD") cmd.Dir = dir out, err = cmd.Output() require.NoError(t, err) shaHead := strings.TrimSpace(string(out)) require.Equal(t, shaMaster, shaHead) } } func TestFetchBranchRemoveRaceSHA1(t *testing.T) { testFetchBranchRemoveRace(t, "sha1", false) } func TestFetchBranchRemoveRaceKeepGitDirSHA1(t *testing.T) { testFetchBranchRemoveRace(t, "sha1", true) } func TestFetchBranchRemoveRaceSHA256(t *testing.T) { testFetchBranchRemoveRace(t, "sha256", false) } func TestFetchBranchRemoveRaceKeepGitDirSHA256(t *testing.T) { testFetchBranchRemoveRace(t, "sha256", true) } func testFetchBranchRemoveRace(t *testing.T, format string, keepGitDir bool) { ctx := t.Context() if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "feature") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } shaFeature := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaFeature)) id := &GitIdentifier{Remote: repo.mainURL, Ref: "feature", KeepGitDir: keepGitDir} gs := setupGitSource(t, t.TempDir()) g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.Equal(t, shaFeature, pin1) if keepGitDir { require.Equal(t, shaFeature+".git#refs/heads/feature", key1) } else { require.Equal(t, shaFeature, key1) } cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "master") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) shaMaster := strings.TrimSpace(string(out)) require.NotEqual(t, shaMaster, shaFeature) // change feature to point to master cmd = exec.CommandContext(context.TODO(), "git", "branch", "-f", "feature", "master") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) cmd = exec.CommandContext(context.TODO(), "git", "reflog", "expire", "--expire-unreachable=now", "--expire=now", "--all") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) cmd = exec.CommandContext(context.TODO(), "git", "gc", "--prune=now", "--aggressive") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) cmd = exec.CommandContext(context.TODO(), "git", "cat-file", "-t", shaFeature) cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.Error(t, err, string(out)) _, err = g.Snapshot(ctx, nil) require.Error(t, err) require.ErrorContains(t, err, "fetched ref feature does not match expected commit "+shaFeature) } func TestFetchMutatedTagSHA1(t *testing.T) { testFetchMutatedTag(t, "sha1", false) } func TestFetchMutatedTagKeepGitDirSHA1(t *testing.T) { testFetchMutatedTag(t, "sha1", true) } func TestFetchMutatedTagSHA256(t *testing.T) { testFetchMutatedTag(t, "sha256", false) } func TestFetchMutatedTagKeepGitDirSHA256(t *testing.T) { testFetchMutatedTag(t, "sha256", true) } func testFetchMutatedTag(t *testing.T, format string, keepGitDir bool) { ctx := t.Context() if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } shaTag := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaTag)) id := &GitIdentifier{Remote: repo.mainURL, Ref: shaTag, KeepGitDir: keepGitDir} gs := setupGitSource(t, t.TempDir()) g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) ref, err := g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) // mutate the tag to point to another commit cmd = exec.CommandContext(context.TODO(), "git", "tag", "-f", "v1.2.3", "feature") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) // verify that the tag now points to a different commit cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v1.2.3") cmd.Dir = repo.mainPath out, err = cmd.Output() require.NoError(t, err) shaTagMutated := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaTagMutated)) require.NotEqual(t, shaTag, shaTagMutated) id = &GitIdentifier{Remote: repo.mainURL, Ref: shaTagMutated, KeepGitDir: keepGitDir} // fetching the original tag should still return the original commit because of caching g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key2, pin2, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.NotEqual(t, key1, key2) require.NotEqual(t, pin1, pin2) ref, err = g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) } func TestFetchMutatedBranchSHA1(t *testing.T) { testFetchMutatedBranch(t, "sha1", false) } func TestFetchMutatedBranchKeepGitDirSHA1(t *testing.T) { testFetchMutatedBranch(t, "sha1", true) } func TestFetchMutatedBranchSHA256(t *testing.T) { testFetchMutatedBranch(t, "sha256", false) } func TestFetchMutatedBranchKeepGitDirSHA256(t *testing.T) { testFetchMutatedBranch(t, "sha256", true) } // testFetchMutatedBranch tests that if a branch is mutated in a way that previous // ref becomes parent directory of new ref, causing collision to existing checkouts func testFetchMutatedBranch(t *testing.T, format string, keepGitDir bool) { ctx := t.Context() if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } repo := setupGitRepo(t, format) cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "feature") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } shaBranch := strings.TrimSpace(string(out)) require.Equal(t, expLen, len(shaBranch)) id := &GitIdentifier{Remote: repo.mainURL, Ref: "feature", KeepGitDir: keepGitDir} gs := setupGitSource(t, t.TempDir()) g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) ref, err := g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) // mutate the branch to point to become parent dir cmd = exec.CommandContext(context.TODO(), "git", "branch", "-D", "feature") cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) cmd = exec.CommandContext(context.TODO(), "git", "branch", "feature/new", shaBranch) cmd.Dir = repo.mainPath out, err = cmd.CombinedOutput() require.NoError(t, err, string(out)) id = &GitIdentifier{Remote: repo.mainURL, Ref: "feature/new", KeepGitDir: keepGitDir} g, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key2, pin2, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) if keepGitDir { require.NotEqual(t, key1, key2) // key contains new ref } else { require.Equal(t, key1, key2) } require.Equal(t, pin1, pin2) ref, err = g.Snapshot(ctx, nil) require.NoError(t, err) ref.Release(context.TODO()) } func TestMultipleTagAccessKeepGitDirSHA1(t *testing.T) { testMultipleTagAccess(t, true, "sha1") } func TestMultipleTagAccessSHA1(t *testing.T) { testMultipleTagAccess(t, false, "sha1") } func TestMultipleTagAccessKeepGitDirSHA256(t *testing.T) { testMultipleTagAccess(t, true, "sha256") } func TestMultipleTagAccessSHA256(t *testing.T) { testMultipleTagAccess(t, false, "sha256") } func testMultipleTagAccess(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "a/v1.2.3"} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } expPinLen := expLen if keepGitDir { expLen += 4 } key1, pin1, _, _, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) if keepGitDir { require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, expPinLen, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) id2 := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "a/v1.2.3-same"} g2, err := gs.Resolve(ctx, id2, nil, nil) require.NoError(t, err) key2, pin2, _, _, err := g2.CacheKey(ctx, nil, 0) require.NoError(t, err) if keepGitDir { require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, expPinLen, len(pin2)) require.Equal(t, pin1, pin2) if !keepGitDir { require.Equal(t, key1, key2) return } // key should be different because of the ref require.NotEqual(t, key1, key2) ref2, err := g2.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount1, err := ref2.Mount(ctx, true, nil) require.NoError(t, err) lm1 := snapshot.LocalMounter(mount1) dir1, err := lm1.Mount() require.NoError(t, err) defer lm1.Unmount() workDir := t.TempDir() runShell(t, dir1, fmt.Sprintf(`git rev-parse a/v1.2.3 > %s/ref1`, workDir)) dt1, err := os.ReadFile(filepath.Join(workDir, "ref1")) require.NoError(t, err) mount2, err := ref2.Mount(ctx, true, nil) require.NoError(t, err) lm2 := snapshot.LocalMounter(mount2) dir2, err := lm2.Mount() require.NoError(t, err) defer lm2.Unmount() runShell(t, dir2, fmt.Sprintf(`git rev-parse a/v1.2.3-same > %s/ref2`, workDir)) dt2, err := os.ReadFile(filepath.Join(workDir, "ref2")) require.NoError(t, err) require.Equal(t, string(dt1), string(dt2)) } func TestResolveMetadataObjectSHA1(t *testing.T) { testResolveMetadataObject(t, false, "sha1") } func TestResolveMetadataObjectKeepGitDirSHA1(t *testing.T) { testResolveMetadataObject(t, true, "sha1") } func TestResolveMetadataObjectSHA256(t *testing.T) { testResolveMetadataObject(t, false, "sha256") } func TestResolveMetadataObjectKeepGitDirSHA256(t *testing.T) { testResolveMetadataObject(t, true, "sha256") } func testResolveMetadataObject(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3"} md, err := gs.ResolveMetadata(ctx, id, nil, nil, MetadataOpts{ ReturnObject: true, }) require.NoError(t, err) expLen := 40 if format == "sha256" { expLen = 64 } require.Len(t, md.Checksum, expLen) require.Len(t, md.CommitChecksum, expLen) tagObject, err := gitobject.Parse(md.TagObject) require.NoError(t, err) err = tagObject.VerifyChecksum(md.Checksum) require.NoError(t, err) tag, err := tagObject.ToTag() require.NoError(t, err) require.Equal(t, "v1.2.3", tag.Tag) require.Equal(t, "this is an annotated tag", tag.Message) require.Equal(t, "test-user", tag.Tagger.Name) require.Equal(t, "test-user@example.com", tag.Tagger.Email) tagTime := tag.Tagger.When require.NotNil(t, tagTime) require.WithinDuration(t, time.Now(), *tagTime, 5*time.Minute) require.Equal(t, "commit", tag.Type) require.Equal(t, md.CommitChecksum, tag.Object) commitObject, err := gitobject.Parse(md.CommitObject) require.NoError(t, err) err = commitObject.VerifyChecksum(md.CommitChecksum) require.NoError(t, err) commit, err := commitObject.ToCommit() require.NoError(t, err) require.Equal(t, "second", commit.Message) require.Equal(t, "test-user", commit.Author.Name) require.Equal(t, "test-user@example.com", commit.Author.Email) require.Equal(t, "test-user", commit.Committer.Name) require.Equal(t, "test-user@example.com", commit.Committer.Email) commitTime := commit.Committer.When require.NotNil(t, commitTime) require.WithinDuration(t, time.Now(), *commitTime, 5*time.Minute) require.Equal(t, 1, len(commit.Parents)) require.Len(t, commit.Tree, len(md.Checksum)) } func TestMultipleReposSHA1(t *testing.T) { testMultipleRepos(t, false, "sha1") } func TestMultipleReposKeepGitDir(t *testing.T) { testMultipleRepos(t, true, "sha1") } func TestMultipleReposSHA256(t *testing.T) { testMultipleRepos(t, false, "sha256") } func TestMultipleReposKeepGitDirSHA256(t *testing.T) { testMultipleRepos(t, true, "sha256") } func testMultipleRepos(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) repodir2 := t.TempDir() runShell(t, repodir2, "git -c init.defaultBranch=master init", "git config --local user.email test", "git config --local user.name test", "echo xyz > xyz", "git add xyz", "git commit -m initial", ) repoURL2 := serveGitRepo(t, repodir2) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir} id2 := &GitIdentifier{Remote: repoURL2, KeepGitDir: keepGitDir} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) g2, err := gs.Resolve(ctx, id2, nil, nil) require.NoError(t, err) expLen := 40 expLen2 := expLen if format == "sha256" { expLen = 64 } expPinLen := expLen if keepGitDir { expLen += 4 expLen2 += 4 } key1, pin1, _, _, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) if keepGitDir { require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, expPinLen, len(pin1)) key2, pin2, _, _, err := g2.CacheKey(ctx, nil, 0) require.NoError(t, err) if keepGitDir { require.GreaterOrEqual(t, len(key2), expLen2) } else { require.Equal(t, expLen2, len(key2)) } require.Equal(t, 40, len(pin2)) require.NotEqual(t, key1, key2) require.NotEqual(t, pin1, pin2) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() ref2, err := g2.Snapshot(ctx, nil) require.NoError(t, err) defer ref2.Release(context.TODO()) mount, err = ref2.Mount(ctx, true, nil) require.NoError(t, err) lm = snapshot.LocalMounter(mount) dir2, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err := os.ReadFile(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, "bar\n", string(dt)) dt, err = os.ReadFile(filepath.Join(dir2, "xyz")) require.NoError(t, err) require.Equal(t, "xyz\n", string(dt)) } func TestCredentialRedaction(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) url := "https://user:keepthissecret@non-existant-host/user/private-repo.git" id := &GitIdentifier{Remote: url} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, _, err = g.CacheKey(ctx, nil, 0) require.Error(t, err) require.NotContains(t, err.Error(), "keepthissecret") } func TestSubmoduleSubdirSHA1(t *testing.T) { testSubmoduleSubdir(t, false, "sha1") } func TestSubmoduleSubdirKeepGitDirSHA1(t *testing.T) { testSubmoduleSubdir(t, true, "sha1") } func TestSubmoduleSubdirSHA256(t *testing.T) { testSubmoduleSubdir(t, false, "sha256") } func TestSubmoduleSubdirKeepGitDirSHA256(t *testing.T) { testSubmoduleSubdir(t, true, "sha256") } func testSubmoduleSubdir(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "feature", Subdir: "sub"} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) expLen := 44 expPinLen := 40 if format == "sha256" { expLen = 68 expPinLen = 64 } require.GreaterOrEqual(t, len(key1), expLen) require.Equal(t, expPinLen, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() dt, err := os.ReadFile(filepath.Join(dir, "subfile")) require.NoError(t, err) require.Equal(t, "subcontents\n", string(dt)) } func TestCheckSignaturesSHA1(t *testing.T) { testCheckSignatures(t, false, "sha1") } func TestCheckSignaturesKeepGitDirSHA1(t *testing.T) { testCheckSignatures(t, true, "sha1") } func TestCheckSignaturesSHA256(t *testing.T) { testCheckSignatures(t, false, "sha256") } func TestCheckSignaturesKeepGitDirSHA256(t *testing.T) { testCheckSignatures(t, true, "sha256") } func testCheckSignatures(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() requireSignFixtures(t) ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) id := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "a/v1.2.3"} md, err := gs.ResolveMetadata(ctx, id, nil, nil, MetadataOpts{ ReturnObject: true, }) require.NoError(t, err) ob, err := gitobject.Parse(md.CommitObject) require.NoError(t, err) require.Greater(t, len(ob.Signature), 50) fixturesBase := os.Getenv("BUILDKIT_TEST_SIGN_FIXTURES") fixturesRoot, err := os.OpenRoot(fixturesBase) require.NoError(t, err) defer fixturesRoot.Close() user1GPGPub, err := fixturesRoot.ReadFile("user1.gpg.pub") require.NoError(t, err) err = gitsign.VerifySignature(ob, user1GPGPub, nil) require.NoError(t, err) user2GPGPub, err := fixturesRoot.ReadFile("user2.gpg.pub") require.NoError(t, err) err = gitsign.VerifySignature(ob, user2GPGPub, nil) require.ErrorContains(t, err, "signature made by unknown entity") require.ErrorContains(t, err, "signature by") mergedWrongFirst := append(append([]byte{}, user2GPGPub...), '\n') mergedWrongFirst = append(mergedWrongFirst, user1GPGPub...) err = gitsign.VerifySignature(ob, mergedWrongFirst, nil) require.NoError(t, err) mergedRightFirst := append(append([]byte{}, user1GPGPub...), '\n') mergedRightFirst = append(mergedRightFirst, user2GPGPub...) err = gitsign.VerifySignature(ob, mergedRightFirst, nil) require.NoError(t, err) id = &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3"} md, err = gs.ResolveMetadata(ctx, id, nil, nil, MetadataOpts{ ReturnObject: true, }) require.NoError(t, err) ob, err = gitobject.Parse(md.TagObject) require.NoError(t, err) require.Greater(t, len(ob.Signature), 50) sshkey1, err := fixturesRoot.ReadFile("user1.ssh.pub") require.NoError(t, err) sshkey2, err := fixturesRoot.ReadFile("user2.ssh.pub") require.NoError(t, err) err = gitsign.VerifySignature(ob, sshkey2, nil) require.NoError(t, err) err = gitsign.VerifySignature(ob, sshkey1, nil) require.ErrorContains(t, err, "failed to verify ssh signature") require.ErrorContains(t, err, "public key does not match") cob, err := gitobject.Parse(md.CommitObject) require.NoError(t, err) err = gitsign.VerifySignature(cob, sshkey1, nil) require.NoError(t, err) err = gitsign.VerifySignature(cob, sshkey2, nil) require.ErrorContains(t, err, "failed to verify ssh signature") require.ErrorContains(t, err, "public key does not match") } func TestVerifySignaturesSHA1(t *testing.T) { testVerifySignatures(t, false, "sha1") } func TestVerifySignaturesKeepGitDirSHA1(t *testing.T) { testVerifySignatures(t, true, "sha1") } func TestVerifySignaturesSHA256(t *testing.T) { testVerifySignatures(t, false, "sha256") } func TestVerifySignaturesKeepGitDirSHA256(t *testing.T) { testVerifySignatures(t, true, "sha256") } func testVerifySignatures(t *testing.T, keepGitDir bool, format string) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() requireSignFixtures(t) ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) fixturesBase := os.Getenv("BUILDKIT_TEST_SIGN_FIXTURES") fixturesRoot, err := os.OpenRoot(fixturesBase) require.NoError(t, err) defer fixturesRoot.Close() user1GPGPub, err := fixturesRoot.ReadFile("user1.gpg.pub") require.NoError(t, err) user2GPGPub, err := fixturesRoot.ReadFile("user2.gpg.pub") require.NoError(t, err) user1SSHPub, err := fixturesRoot.ReadFile("user1.ssh.pub") require.NoError(t, err) user2SSHPub, err := fixturesRoot.ReadFile("user2.ssh.pub") require.NoError(t, err) // a/v1.2.3 commit is signed by user1 gpg // v1.2.3 commit is signed by user1 ssh // v1.2.3 is a signed tag by user2 ssh // v1.2.3-special is not signed id := &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "a/v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user2GPGPub, // wrong key }, } gsi, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, _, err = gsi.CacheKey(ctx, nil, 0) require.ErrorContains(t, err, "signature made by unknown entity") require.ErrorContains(t, err, "signature by") id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3-special", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user2GPGPub, }, } gsi, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, _, err = gsi.CacheKey(ctx, nil, 0) require.ErrorContains(t, err, "git object is not signed") id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "a/v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user1GPGPub, // correct }, } gsi, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, pin, _, done, err := gsi.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) expLen := 40 if format == "sha256" { expLen = 64 } require.Equal(t, expLen, len(pin)) id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "a/v1.2.3", // not signed tag VerifySignature: &GitSignatureVerifyOptions{ PubKey: user1GPGPub, RequireSignedTag: true, }, } gsi, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, _, err = gsi.CacheKey(ctx, nil, 0) require.ErrorContains(t, err, "signed tag required but no signed tag found") // signed tag can be validated via commit signature id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user1SSHPub, RequireSignedTag: false, }, } gsi, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, pin, _, done, err = gsi.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.Equal(t, expLen, len(pin)) // but not when signed tag is required id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user1SSHPub, RequireSignedTag: true, }, } gsi, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, _, err = gsi.CacheKey(ctx, nil, 0) require.ErrorContains(t, err, "failed to verify ssh signature") // correct key for signed tag id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user2SSHPub, RequireSignedTag: true, }, } gsi, err = gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, pin, _, done, err = gsi.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) require.Equal(t, expLen, len(pin)) // repeat three last checks via ResolveSourceMetadata id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user1SSHPub, RequireSignedTag: false, }, } _, err = gs.ResolveMetadata(ctx, id, nil, nil, MetadataOpts{}) require.NoError(t, err) id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user1SSHPub, RequireSignedTag: true, }, } _, err = gs.ResolveMetadata(ctx, id, nil, nil, MetadataOpts{}) require.ErrorContains(t, err, "failed to verify ssh signature") id = &GitIdentifier{ Remote: repo.mainURL, KeepGitDir: keepGitDir, Ref: "v1.2.3", VerifySignature: &GitSignatureVerifyOptions{ PubKey: user2SSHPub, RequireSignedTag: true, }, } _, err = gs.ResolveMetadata(ctx, id, nil, nil, MetadataOpts{}) require.NoError(t, err) } func TestSubdir(t *testing.T) { testSubdir(t, false) } func TestSubdirKeepGitDir(t *testing.T) { testSubdir(t, true) } func testSubdir(t *testing.T, keepGitDir bool) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repodir := t.TempDir() runShell(t, repodir, "git -c init.defaultBranch=master init", "git config --local user.email test", "git config --local user.name test", "echo foo > abc", "mkdir sub", "echo abc > sub/bar", "git add abc sub", "git commit -m initial", ) repoURL := serveGitRepo(t, repodir) id := &GitIdentifier{Remote: repoURL, KeepGitDir: keepGitDir, Subdir: "sub"} g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) key1, pin1, _, done, err := g.CacheKey(ctx, nil, 0) require.NoError(t, err) require.True(t, done) expLen := 44 if keepGitDir { expLen += 4 require.GreaterOrEqual(t, len(key1), expLen) } else { require.Equal(t, expLen, len(key1)) } require.Equal(t, 40, len(pin1)) ref1, err := g.Snapshot(ctx, nil) require.NoError(t, err) defer ref1.Release(context.TODO()) mount, err := ref1.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() fis, err := os.ReadDir(dir) require.NoError(t, err) require.Equal(t, 1, len(fis)) dt, err := os.ReadFile(filepath.Join(dir, "bar")) require.NoError(t, err) require.Equal(t, "abc\n", string(dt)) } func setupGitSource(t *testing.T, tmpdir string) *Source { snapshotter, err := native.NewSnapshotter(filepath.Join(tmpdir, "snapshots")) require.NoError(t, err) store, err := local.NewStore(tmpdir) require.NoError(t, err) db, err := bolt.Open(filepath.Join(tmpdir, "containerdmeta.db"), 0644, nil) require.NoError(t, err) mdb := ctdmetadata.NewDB(db, store, map[string]snapshots.Snapshotter{ "native": snapshotter, }) md, err := metadata.NewStore(filepath.Join(tmpdir, "metadata.db")) require.NoError(t, err) lm := leaseutil.WithNamespace(ctdmetadata.NewLeaseManager(mdb), "buildkit") c := mdb.ContentStore() applier := winlayers.NewFileSystemApplierWithWindows(c, apply.NewFileSystemApplier(c)) differ := winlayers.NewWalkingDiffWithWindows(c, walking.NewWalkingDiff(c)) cm, err := cache.NewManager(cache.ManagerOpt{ Snapshotter: snapshot.FromContainerdSnapshotter("native", containerdsnapshot.NSSnapshotter("buildkit", mdb.Snapshotter("native")), nil), MetadataStore: md, LeaseManager: lm, ContentStore: c, Applier: applier, Differ: differ, GarbageCollect: mdb.GarbageCollect, Root: tmpdir, MountPoolRoot: filepath.Join(tmpdir, "cachemounts"), }) require.NoError(t, err) gs, err := NewSource(Opt{ CacheAccessor: cm, }) require.NoError(t, err) return gs } type testJobContext struct { compatibilityVersion int } func (j testJobContext) Session() session.Group { return nil } func (j testJobContext) Cleanup(func() error) error { return nil } func (j testJobContext) ResolverCache() solver.ResolverCache { return nil } func (j testJobContext) CompatibilityVersion() (int, error) { return j.compatibilityVersion, nil } func gitSnapshotMode(ctx context.Context, t *testing.T, gs *Source, id *GitIdentifier, jobCtx solver.JobContext) os.FileMode { t.Helper() g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) _, _, _, done, err := g.CacheKey(ctx, jobCtx, 0) require.NoError(t, err) require.True(t, done) ref, err := g.Snapshot(ctx, jobCtx) require.NoError(t, err) defer ref.Release(context.TODO()) mount, err := ref.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() st, err := os.Lstat(filepath.Join(dir, "def")) require.NoError(t, err) return st.Mode() & os.ModePerm } type gitRepoFixture struct { mainPath, subPath string // Filesystem paths to the respective repos mainURL, subURL string // HTTP URLs for the respective repos } func checkSignFixtures() error { fixturesPath, ok := os.LookupEnv(signFixturesPathEnv) if !ok { return errors.Errorf("environment variable %s must be set to run signing tests", signFixturesPathEnv) } for _, method := range []string{"gpg", "ssh"} { for _, user := range []string{"user1", "user2"} { path := filepath.Join(fixturesPath, user+"."+method+".gitconfig") _, err := os.Stat(path) if err != nil { return errors.Errorf("missing signing fixture at %s", path) } } } return nil } func requireSignFixtures(t *testing.T) { t.Helper() require.NoError(t, checkSignFixtures()) } func setupGitRepo(t *testing.T, format string) gitRepoFixture { t.Helper() dir := t.TempDir() srv := serveGitRepo(t, dir) fixture := gitRepoFixture{ subPath: filepath.Join(dir, "sub"), subURL: srv + "/sub", mainPath: filepath.Join(dir, "main"), mainURL: srv + "/main", } require.NoError(t, os.MkdirAll(fixture.subPath, 0700)) require.NoError(t, os.MkdirAll(fixture.mainPath, 0700)) withSignatures := checkSignFixtures() == nil withSign := func(user, method string) []string { return []string{ "GIT_CONFIG_GLOBAL=" + filepath.Join(os.Getenv(signFixturesPathEnv), user+"."+method+".gitconfig"), } } runShell(t, fixture.subPath, "git -c init.defaultBranch=master init --object-format="+format, "git config --local user.email test", "git config --local user.name test", "echo subcontents > subfile", "git add subfile", "git commit -m initial", ) // * (refs/heads/feature) withsub // * feature // * (HEAD -> refs/heads/master, tag: refs/tags/lightweight-tag) third // | * ref only // | * commit only // | * (tag: refs/tags/v1.2.3-special) tagonly-leaf // |/ // * (tag: refs/tags/v1.2.3) second // * (tag: refs/tags/a/v1.2.3, refs/tags/a/v1.2.3-same) initial runShell(t, fixture.mainPath, "git -c init.defaultBranch=master init --object-format="+format, "git config --local user.email test-user@example.com", "git config --local user.name test-user", "echo foo > abc", "git add abc", ) if withSignatures { runShellEnv(t, fixture.mainPath, withSign("user1", "gpg"), "git commit -S -m initial", ) } else { runShell(t, fixture.mainPath, "git commit -m initial", ) } runShell(t, fixture.mainPath, "git tag --no-sign a/v1.2.3", "git tag --no-sign a/v1.2.3-same", "echo bar > def", "mkdir subdir", "echo subcontents > subdir/subfile", "git add def subdir", ) if withSignatures { runShellEnv(t, fixture.mainPath, withSign("user1", "ssh"), "git commit -S -m second", ) runShellEnv(t, fixture.mainPath, withSign("user2", "ssh"), "git tag -s -a -m \"this is an annotated tag\" v1.2.3", ) } else { runShell(t, fixture.mainPath, "git commit -m second", "git tag -a -m \"this is an annotated tag\" v1.2.3", ) } runShell(t, fixture.mainPath, "echo foo > bar", "git add bar", "git commit -m tagonly-leaf", "git tag --no-sign v1.2.3-special", "echo foo2 > bar2", "git add bar2", "git commit -m \"commit only\"", "echo foo3 > bar3", "git add bar3", "git commit -m \"ref only\"", "git update-ref refs/special $(git rev-parse HEAD)", // switch master back to v1.2.3 "git checkout -B master v1.2.3", "echo sbb > foo13", "git add foo13", "git commit -m third", "git tag --no-sign lightweight-tag", "git checkout -B feature", "echo baz > ghi", "git add ghi", "git commit -m feature", "git update-ref refs/test $(git rev-parse HEAD)", "git submodule add "+fixture.subURL+" sub", "git add -A", "git commit -m withsub", "git checkout master", // "git log --oneline --graph --decorate=full --all", ) return fixture } func serveGitRepo(t *testing.T, root string) string { t.Helper() gitpath, err := exec.LookPath("git") require.NoError(t, err) gitversion, _ := exec.CommandContext(context.TODO(), gitpath, "version").CombinedOutput() t.Logf("%s", gitversion) // E.g. "git version 2.30.2" // Serve all repositories under root using the Smart HTTP protocol so // they can be cloned as we explicitly disable the file protocol. // (Another option would be to use `git daemon` and the Git protocol, // but that listens on a fixed port number which is a recipe for // disaster in CI. Funnily enough, `git daemon --port=0` works but there // is no easy way to discover which port got picked!) githttp := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var logs bytes.Buffer (&cgi.Handler{ Path: gitpath, Args: []string{"http-backend"}, Dir: root, Env: []string{ "GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1", }, Stderr: &logs, }).ServeHTTP(w, r) if logs.Len() == 0 { return } for { line, err := logs.ReadString('\n') t.Log("git-http-backend: " + line) if err != nil { break } } }) server := httptest.NewServer(&githttp) t.Cleanup(server.Close) return server.URL } func runShell(t *testing.T, dir string, cmds ...string) { t.Helper() runShellEnv(t, dir, nil, cmds...) } func runShellEnv(t *testing.T, dir string, env []string, cmds ...string) { t.Helper() 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.Dir = dir cmd.Env = append(os.Environ(), env...) // cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr require.NoErrorf(t, cmd.Run(), "error running %v", args) } } func logProgressStreams(ctx context.Context, t *testing.T) context.Context { pr, ctx, cancel := progress.NewContext(ctx) done := make(chan struct{}) t.Cleanup(func() { cancel(errors.WithStack(context.Canceled)) <-done }) go func() { defer close(done) for { prog, err := pr.Read(ctx) if err != nil { return } for _, log := range prog { switch lsys := log.Sys.(type) { case client.VertexLog: var stream string switch lsys.Stream { case 1: stream = "stdout" case 2: stream = "stderr" default: stream = strconv.FormatInt(int64(lsys.Stream), 10) } t.Logf("(%v) %s", stream, lsys.Data) default: t.Logf("(%T) %+v", log.Sys, log) } } } }() return ctx } func TestResetSnapshotMtimes(t *testing.T) { t.Parallel() dir := t.TempDir() // Create a tree: // dir/ // dir/file.txt // dir/subdir/ // dir/subdir/nested.txt // dir/link -> file.txt (symlink) require.NoError(t, os.MkdirAll(filepath.Join(dir, "subdir"), 0755)) require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("hello"), 0644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "subdir", "nested.txt"), []byte("world"), 0644)) require.NoError(t, os.Symlink("file.txt", filepath.Join(dir, "link"))) target := time.Date(2023, 6, 15, 12, 0, 0, 0, time.UTC) root, err := os.OpenRoot(dir) require.NoError(t, err) require.NoError(t, resetSnapshotMtimes(root, target)) require.NoError(t, root.Close()) // Regular files should have mtime set fi, err := os.Lstat(filepath.Join(dir, "file.txt")) require.NoError(t, err) require.Equal(t, target, fi.ModTime().UTC()) fi, err = os.Lstat(filepath.Join(dir, "subdir", "nested.txt")) require.NoError(t, err) require.Equal(t, target, fi.ModTime().UTC()) // Directories should have mtime set fi, err = os.Lstat(dir) require.NoError(t, err) require.Equal(t, target, fi.ModTime().UTC()) fi, err = os.Lstat(filepath.Join(dir, "subdir")) require.NoError(t, err) require.Equal(t, target, fi.ModTime().UTC()) // Symlink itself should have mtime set via lutimes fi, err = os.Lstat(filepath.Join(dir, "link")) require.NoError(t, err) require.Equal(t, target, fi.ModTime().UTC()) // Verify symlink itself still exists and points correctly linkTarget, err := os.Readlink(filepath.Join(dir, "link")) require.NoError(t, err) require.Equal(t, "file.txt", linkTarget) } // TestBundleStagedRefShape verifies that the unified metadata-resolution // path returns the same ref shape for a bundle as for an origin fetch: // resolving the symbolic ref "master" must yield Metadata.Ref == // "refs/heads/master", not "refs/tags/master" (which is what the shared // bare repo carries after the main-path fetch maps branches into tags/*). func TestBundleStagedRefShape(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := namespaces.WithNamespace(context.Background(), "buildkit-test") ctx = logProgressStreams(ctx, t) // Build a source-of-truth git repo with a single commit on master. seedDir := t.TempDir() runShell(t, seedDir, "git -c init.defaultBranch=master init", "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", ) cmd := exec.CommandContext(ctx, "git", "rev-parse", "HEAD") cmd.Dir = seedDir out, err := cmd.Output() require.NoError(t, err) headSha := strings.TrimSpace(string(out)) // Create a bundle of refs/heads/master, then import it into a fresh // bare repo — emulating stageBundle's output. ls-remote against its // file:// URL should see refs/heads/master. bundlePath := filepath.Join(t.TempDir(), "bundle.pack") cmd = exec.CommandContext(ctx, "git", "bundle", "create", bundlePath, "refs/heads/master") cmd.Dir = seedDir out, err = cmd.CombinedOutput() require.NoError(t, err, "bundle create: %s", out) stagedRepoDir := filepath.Join(t.TempDir(), "repo.git") require.NoError(t, os.MkdirAll(stagedRepoDir, 0700)) runShell(t, stagedRepoDir, "git -c init.defaultBranch=master init --bare", "git fetch "+bundlePath+" +refs/*:refs/*", ) gs := setupGitSource(t, t.TempDir()) gsh := &gitSourceHandler{ src: GitIdentifier{Remote: "https://example.com/repo.git", Ref: "master", Checksum: headSha}, Source: gs, } md, err := gsh.resolveMetadataFromURL(ctx, nil, "file://"+stagedRepoDir) require.NoError(t, err) require.Equal(t, "refs/heads/master", md.Ref, "bundle-mode resolveMetadataFromURL should return refs/heads/master (parity with origin path)") require.Equal(t, headSha, md.Checksum) } func TestGetDefaultBranchRejectsDashPrefixedRef(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Depends on git shell helpers") } t.Parallel() ctx := t.Context() work := t.TempDir() runShell(t, work, "git init -q seed", "git -C seed -c user.email=test@example.com -c user.name=test commit -q --allow-empty -m seed", "git clone -q --bare seed mal.git", ) shaCmd := exec.CommandContext(ctx, "git", "-C", filepath.Join(work, "seed"), "rev-parse", "HEAD") //nolint:gosec // test uses t.TempDir-owned path out, err := shaCmd.Output() require.NoError(t, err) sha := strings.TrimSpace(string(out)) malDir := filepath.Join(work, "mal.git") require.NoError(t, os.RemoveAll(filepath.Join(malDir, "refs", "heads"))) require.NoError(t, os.WriteFile(filepath.Join(malDir, "packed-refs"), fmt.Appendf(nil, "# pack-refs with: peeled fully-peeled sorted\n%s refs/heads/--upload-pack=/tmp/evil.sh\n", sha), 0600)) require.NoError(t, os.WriteFile(filepath.Join(malDir, "HEAD"), []byte("ref: refs/heads/--upload-pack=/tmp/evil.sh\n"), 0600)) ref, err := getDefaultBranch(ctx, gitutil.NewGitCLI(), "file://"+malDir) require.ErrorContains(t, err, `invalid git ref "--upload-pack=/tmp/evil.sh"`) require.Empty(t, ref) } func TestFetchCommitForBundleUsesOptionTerminator(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Depends on shell script git stub") } t.Parallel() ctx := t.Context() dir := t.TempDir() capture := filepath.Join(dir, "args") fakeGit := filepath.Join(dir, "git") require.NoError(t, os.WriteFile(fakeGit, fmt.Appendf(nil, `#!/bin/sh for arg do printf '%%s\n' "$arg" done > %s `, strconv.Quote(capture)), 0600)) require.NoError(t, os.Chmod(fakeGit, 0700)) sharedURL := "file:///tmp/shared.git" commit := "--upload-pack=/tmp/evil.sh" err := fetchCommitForBundle(ctx, gitutil.NewGitCLI(gitutil.WithGitBinary(fakeGit)), sharedURL, commit) require.NoError(t, err) buf, err := os.ReadFile(capture) require.NoError(t, err) args := strings.Split(strings.TrimSpace(string(buf)), "\n") fetchIdx := -1 for i, arg := range args { if arg == "fetch" { fetchIdx = i break } } require.NotEqual(t, -1, fetchIdx, "fake git args: %v", args) require.GreaterOrEqual(t, len(args), fetchIdx+4, "fake git args: %v", args) require.Equal(t, sharedURL, args[fetchIdx+1]) require.Equal(t, "--", args[fetchIdx+2]) require.Equal(t, commit, args[fetchIdx+3]) } func TestDetectBundleSHA256(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Depends on git bundle support exercised via shell helpers") } t.Parallel() ctx := logProgressStreams(context.Background(), t) for _, format := range []string{"sha1", "sha256"} { t.Run(format, func(t *testing.T) { repo := setupGitRepo(t, format) bundlePath := filepath.Join(t.TempDir(), "bundle.pack") runShell(t, repo.mainPath, "git bundle create "+bundlePath+" refs/heads/master") sha256, err := detectBundleSHA256(ctx, bundlePath) require.NoError(t, err) require.Equal(t, format == "sha256", sha256) stagedRepoDir := filepath.Join(t.TempDir(), "repo.git") require.NoError(t, os.MkdirAll(stagedRepoDir, 0700)) initArgs := "git -c init.defaultBranch=master init --bare" if sha256 { initArgs += " --object-format=sha256" } runShell(t, stagedRepoDir, initArgs, "git fetch "+bundlePath+" +refs/*:refs/*", "git cat-file -e refs/heads/master^{commit}", ) }) } } func TestCommitTimeMtimesSHA1(t *testing.T) { testCommitTimeMtimes(t, "sha1", false) } func TestCommitTimeMtimesKeepGitDirSHA1(t *testing.T) { testCommitTimeMtimes(t, "sha1", true) } func TestCommitTimeMtimesSHA256(t *testing.T) { testCommitTimeMtimes(t, "sha256", false) } func TestCommitTimeMtimesKeepGitDirSHA256(t *testing.T) { testCommitTimeMtimes(t, "sha256", true) } func TestCompatibility014FileModes(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, "sha1") id := &GitIdentifier{Remote: repo.mainURL, Ref: "master"} currentMode := gitSnapshotMode(ctx, t, gs, id, testJobContext{compatibilityVersion: compat.CompatibilityVersionCurrent}) compat013Mode := gitSnapshotMode(ctx, t, gs, id, testJobContext{compatibilityVersion: compat.CompatibilityVersion013}) require.Equal(t, os.FileMode(0o644), currentMode) require.Equal(t, os.FileMode(0o666), compat013Mode) } func testCommitTimeMtimes(t *testing.T, format string, keepGitDir bool) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } t.Parallel() ctx := logProgressStreams(context.Background(), t) gs := setupGitSource(t, t.TempDir()) repo := setupGitRepo(t, format) // Get the commit timestamp of the master branch cmd := exec.CommandContext(ctx, "git", "log", "-1", "--format=%ct", "master^{commit}") cmd.Dir = repo.mainPath out, err := cmd.Output() require.NoError(t, err) ts, err := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64) require.NoError(t, err) expectedTime := time.Unix(ts, 0) // Snapshot without MTime=commit idDefault := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir} gDefault, err := gs.Resolve(ctx, idDefault, nil, nil) require.NoError(t, err) keyDefault, _, _, _, err := gDefault.CacheKey(ctx, nil, 0) require.NoError(t, err) // Snapshot with MTime=commit idCommit := &GitIdentifier{Remote: repo.mainURL, KeepGitDir: keepGitDir, MTime: "commit"} gCommit, err := gs.Resolve(ctx, idCommit, nil, nil) require.NoError(t, err) keyCommit, _, _, _, err := gCommit.CacheKey(ctx, nil, 0) require.NoError(t, err) // Cache keys must differ require.NotEqual(t, keyDefault, keyCommit) require.Contains(t, keyCommit, "(mtime=commit)") refCommit, err := gCommit.Snapshot(ctx, nil) require.NoError(t, err) defer refCommit.Release(context.TODO()) mount, err := refCommit.Mount(ctx, true, nil) require.NoError(t, err) lm := snapshot.LocalMounter(mount) dir, err := lm.Mount() require.NoError(t, err) defer lm.Unmount() // Verify file mtimes match the commit timestamp fi, err := os.Lstat(filepath.Join(dir, "abc")) require.NoError(t, err) require.Equal(t, expectedTime.Unix(), fi.ModTime().Unix(), "file mtime should match commit time") fi, err = os.Lstat(filepath.Join(dir, "def")) require.NoError(t, err) require.Equal(t, expectedTime.Unix(), fi.ModTime().Unix(), "file mtime should match commit time") // Verify directory mtime fi, err = os.Lstat(dir) require.NoError(t, err) require.Equal(t, expectedTime.Unix(), fi.ModTime().Unix(), "dir mtime should match commit time") if keepGitDir { // .git directory should also have normalized mtime fi, err = os.Lstat(filepath.Join(dir, ".git")) require.NoError(t, err) require.Equal(t, expectedTime.Unix(), fi.ModTime().Unix(), ".git dir mtime should match commit time") } }