diff --git a/client/llb/source.go b/client/llb/source.go index cb1f47206de3..d6eba0758981 100644 --- a/client/llb/source.go +++ b/client/llb/source.go @@ -322,6 +322,12 @@ func Git(url, ref string, opts ...GitOption) State { addCap(&gi.Constraints, pb.CapSourceGitMountSSHSock) } + checksum := gi.Checksum + if checksum != "" { + attrs[pb.AttrGitChecksum] = checksum + addCap(&gi.Constraints, pb.CapSourceGitChecksum) + } + addCap(&gi.Constraints, pb.CapSourceGit) source := NewSource("git://"+id, attrs, gi.Constraints) @@ -345,6 +351,7 @@ type GitInfo struct { addAuthCap bool KnownSSHHosts string MountSSHSock string + Checksum string } func KeepGitDir() GitOption { @@ -373,6 +380,12 @@ func MountSSHSock(sshID string) GitOption { }) } +func GitChecksum(v string) GitOption { + return gitOptionFunc(func(gi *GitInfo) { + gi.Checksum = v + }) +} + // AuthOption can be used with either HTTP or Git sources. type AuthOption interface { GitOption diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 71ef28a3a2b0..b52c03155baf 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -937,27 +937,21 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { case *instructions.WorkdirCommand: err = dispatchWorkdir(d, c, true, &opt) case *instructions.AddCommand: - var checksum digest.Digest - if c.Checksum != "" { - checksum, err = digest.Parse(c.Checksum) - } - if err == nil { - err = dispatchCopy(d, copyConfig{ - params: c.SourcesAndDest, - excludePatterns: c.ExcludePatterns, - source: opt.buildContext, - isAddCommand: true, - cmdToPrint: c, - chown: c.Chown, - chmod: c.Chmod, - link: c.Link, - keepGitDir: c.KeepGitDir, - checksum: checksum, - location: c.Location(), - ignoreMatcher: opt.dockerIgnoreMatcher, - opt: opt, - }) - } + err = dispatchCopy(d, copyConfig{ + params: c.SourcesAndDest, + excludePatterns: c.ExcludePatterns, + source: opt.buildContext, + isAddCommand: true, + cmdToPrint: c, + chown: c.Chown, + chmod: c.Chmod, + link: c.Link, + keepGitDir: c.KeepGitDir, + checksum: c.Checksum, + location: c.Location(), + ignoreMatcher: opt.dockerIgnoreMatcher, + opt: opt, + }) if err == nil { for _, src := range c.SourcePaths { if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { @@ -1470,8 +1464,8 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error { if len(cfg.params.SourcePaths) != 1 { return errors.New("checksum can't be specified for multiple sources") } - if !isHTTPSource(cfg.params.SourcePaths[0]) { - return errors.New("checksum can't be specified for non-HTTP(S) sources") + if !isHTTPSource(cfg.params.SourcePaths[0]) && !isGitSource(cfg.params.SourcePaths[0]) { + return errors.New("checksum requires HTTP(S) or Git sources") } } @@ -1519,6 +1513,9 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error { if cfg.keepGitDir { gitOptions = append(gitOptions, llb.KeepGitDir()) } + if cfg.checksum != "" { + gitOptions = append(gitOptions, llb.GitChecksum(cfg.checksum)) + } st := llb.Git(gitRef.Remote, commit, gitOptions...) opts := append([]llb.CopyOption{&llb.CopyInfo{ Mode: chopt, @@ -1547,7 +1544,15 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error { } } - st := llb.HTTP(src, llb.Filename(f), llb.WithCustomName(pgName), llb.Checksum(cfg.checksum), dfCmd(cfg.params)) + var checksum digest.Digest + if cfg.checksum != "" { + checksum, err = digest.Parse(cfg.checksum) + if err != nil { + return err + } + } + + st := llb.HTTP(src, llb.Filename(f), llb.WithCustomName(pgName), llb.Checksum(checksum), dfCmd(cfg.params)) opts := append([]llb.CopyOption{&llb.CopyInfo{ Mode: chopt, @@ -1674,7 +1679,7 @@ type copyConfig struct { chmod string link bool keepGitDir bool - checksum digest.Digest + checksum string parents bool location []parser.Range ignoreMatcher *patternmatcher.PatternMatcher @@ -2265,11 +2270,15 @@ func isHTTPSource(src string) bool { if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { return false } + return !isGitSource(src) +} + +func isGitSource(src string) bool { // https://github.com/ORG/REPO.git is a git source, not an http source if gitRef, gitErr := gitutil.ParseGitRef(src); gitRef != nil && gitErr == nil { - return false + return true } - return true + return false } func isEnabledForStage(stage string, value string) bool { diff --git a/frontend/dockerfile/dockerfile_addchecksum_test.go b/frontend/dockerfile/dockerfile_addchecksum_test.go index 050291845168..c2b77ff1f4bd 100644 --- a/frontend/dockerfile/dockerfile_addchecksum_test.go +++ b/frontend/dockerfile/dockerfile_addchecksum_test.go @@ -162,6 +162,6 @@ ADD --checksum=%s foo /tmp/foo dockerui.DefaultLocalNameContext: dir, }, }, nil) - require.Error(t, err, "checksum can't be specified for non-HTTP(S) sources") + require.Error(t, err, "checksum requires HTTP(S) or Git sources") }) } diff --git a/frontend/dockerfile/dockerfile_addgit_test.go b/frontend/dockerfile/dockerfile_addgit_test.go index db8d0d9649fa..60b56de630a7 100644 --- a/frontend/dockerfile/dockerfile_addgit_test.go +++ b/frontend/dockerfile/dockerfile_addgit_test.go @@ -5,7 +5,9 @@ import ( "net/http" "net/http/httptest" "os" + "os/exec" "path/filepath" + "strings" "testing" "text/template" @@ -19,6 +21,7 @@ import ( var addGitTests = integration.TestFuncs( testAddGit, + testAddGitChecksumCache, ) func init() { @@ -52,6 +55,18 @@ func testAddGit(t *testing.T, sb integration.Sandbox) { err = runShell(gitDir, gitCommands...) require.NoError(t, err) + revParseCmd := exec.Command("git", "rev-parse", "v0.0.2") + revParseCmd.Dir = gitDir + commitHashB, err := revParseCmd.Output() + require.NoError(t, err) + commitHashV2 := strings.TrimSpace(string(commitHashB)) + + revParseCmd = exec.Command("git", "rev-parse", "v0.0.3") + revParseCmd.Dir = gitDir + commitHashB, err = revParseCmd.Output() + require.NoError(t, err) + commitHashV3 := strings.TrimSpace(string(commitHashB)) + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir)))) defer server.Close() serverURL := server.URL @@ -68,7 +83,7 @@ RUN cd /x && \ # Complicated case ARG REPO="{{.ServerURL}}/.git" ARG TAG="v0.0.2" -ADD --keep-git-dir=true --chown=4242:8484 ${REPO}#${TAG} /buildkit-chowned +ADD --keep-git-dir=true --chown=4242:8484 --checksum={{.Checksum}} ${REPO}#${TAG} /buildkit-chowned RUN apk add git USER 4242 RUN cd /buildkit-chowned && \ @@ -78,9 +93,9 @@ RUN cd /buildkit-chowned && \ [ -z "$(git status -s)" ] `, map[string]string{ "ServerURL": serverURL, + "Checksum": commitHashV2, }) require.NoError(t, err) - t.Logf("dockerfile=%s", dockerfile) dir := integration.Tmpdir(t, fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600), @@ -97,6 +112,242 @@ RUN cd /buildkit-chowned && \ }, }, nil) require.NoError(t, err) + + // Additional test: ADD from Git URL with checksum but without keep-git-dir flag + dockerfile2, err := applyTemplate(` +FROM alpine +ARG REPO="{{.ServerURL}}/.git" +ARG TAG="v0.0.3" +ADD --checksum={{.Checksum}} ${REPO}#${TAG} /nogitdir +RUN [ -f /nogitdir/foo ] +RUN [ "$(cat /nogitdir/foo)" = "foo of v0.0.3" ] +RUN [ ! -d /nogitdir/.git ] +`, map[string]string{ + "ServerURL": serverURL, + "Checksum": commitHashV3, + }) + require.NoError(t, err) + + dir2 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile2), 0600), + ) + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir2, + dockerui.DefaultLocalNameContext: dir2, + }, + }, nil) + require.NoError(t, err) + + // access initial ref again that was already pulled + dockerfile3, err := applyTemplate(` + FROM alpine + ARG REPO="{{.ServerURL}}/.git" + ARG TAG="v0.0.2" + ADD --keep-git-dir --checksum={{.Checksum}} ${REPO}#${TAG} /nogitdir + RUN [ -f /nogitdir/foo ] + RUN [ "$(cat /nogitdir/foo)" = "foo of v0.0.2" ] + RUN [ -d /nogitdir/.git ] + `, map[string]string{ + "ServerURL": serverURL, + "Checksum": commitHashV2, + }) + require.NoError(t, err) + + dir3 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile3), 0600), + ) + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir3, + dockerui.DefaultLocalNameContext: dir3, + }, + }, nil) + require.NoError(t, err) + + // Additional test: ADD from Git URL using commitHashV3 for both checksum and ref + dockerfile4, err := applyTemplate(` + FROM alpine + ARG REPO="{{.ServerURL}}/.git" + ARG COMMIT="{{.Checksum}}" + ADD --keep-git-dir=true --checksum={{.Checksum}} ${REPO}#${COMMIT} /commitdir + RUN [ -f /commitdir/foo ] + RUN [ "$(cat /commitdir/foo)" = "foo of v0.0.3" ] + RUN [ -d /commitdir/.git ] + `, map[string]string{ + "ServerURL": serverURL, + "Checksum": commitHashV3, + }) + require.NoError(t, err) + + dir4 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile4), 0600), + ) + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir4, + dockerui.DefaultLocalNameContext: dir4, + }, + }, nil) + require.NoError(t, err) + + // checksum does not match + dockerfile5, err := applyTemplate(` + FROM alpine + ARG REPO="{{.ServerURL}}/.git" + ARG TAG="v0.0.3" + ADD --checksum={{.WrongChecksum}} ${REPO}#${TAG} /faildir + `, map[string]string{ + "ServerURL": serverURL, + "WrongChecksum": commitHashV2, // v0.0.2 hash, but ref is v0.0.3 + }) + require.NoError(t, err) + + dir5 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile5), 0600), + ) + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir5, + dockerui.DefaultLocalNameContext: dir5, + }, + }, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "expected checksum to match") + + // checksum is garbage + dockerfile6, err := applyTemplate(` + FROM alpine + ARG REPO="{{.ServerURL}}/.git" + ARG TAG="v0.0.3" + ADD --checksum=foobar ${REPO}#${TAG} /faildir + `, map[string]string{ + "ServerURL": serverURL, + }) + require.NoError(t, err) + + dir6 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile6), 0600), + ) + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir6, + dockerui.DefaultLocalNameContext: dir6, + }, + }, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid checksum") + require.Contains(t, err.Error(), "expected hex commit hash") +} + +func testAddGitChecksumCache(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + f := getFrontend(t, sb) + + gitDir, err := os.MkdirTemp("", "buildkit") + require.NoError(t, err) + defer os.RemoveAll(gitDir) + gitCommands := []string{ + "git init", + "git config --local user.email test", + "git config --local user.name test", + } + makeCommit := func(tag string) []string { + return []string{ + "echo foo of " + tag + " >foo", + "git add foo", + "git commit -m " + tag, + "git tag " + tag, + } + } + gitCommands = append(gitCommands, makeCommit("v0.0.1")...) + gitCommands = append(gitCommands, makeCommit("v0.0.2")...) + gitCommands = append(gitCommands, "git update-server-info") + err = runShell(gitDir, gitCommands...) + require.NoError(t, err) + + revParseCmd := exec.Command("git", "rev-parse", "v0.0.2") + revParseCmd.Dir = gitDir + commitHashB, err := revParseCmd.Output() + require.NoError(t, err) + commitHash := strings.TrimSpace(string(commitHashB)) + + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir)))) + defer server.Close() + serverURL := server.URL + + // First build: without checksum, from tag, generate unique.txt from /dev/urandom and copy to scratch + dockerfile1 := ` +FROM alpine AS src +ADD --keep-git-dir ` + serverURL + `/.git#v0.0.2 /repo +RUN head -c 16 /dev/urandom | base64 > /repo/unique.txt + +FROM scratch +COPY --from=src /repo/unique.txt / +` + dir1 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile1), 0600), + ) + + c, err := client.New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + destDir1 := t.TempDir() + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir1, + }, + }, + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir1, + dockerui.DefaultLocalNameContext: dir1, + }, + }, nil) + require.NoError(t, err) + + unique1, err := os.ReadFile(filepath.Join(destDir1, "unique.txt")) + require.NoError(t, err) + + // Second build: with checksum, should match cache even though this one sets commitHash and get same unique.txt + dockerfile2 := ` +FROM alpine AS src +ADD --keep-git-dir --checksum=` + commitHash + ` ` + serverURL + `/.git#v0.0.2 /repo +RUN head -c 16 /dev/urandom | base64 > /repo/unique.txt + +FROM scratch +COPY --from=src /repo/unique.txt / +` + dir2 := integration.Tmpdir(t, + fstest.CreateFile("Dockerfile", []byte(dockerfile2), 0600), + ) + + destDir2 := t.TempDir() + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir2, + }, + }, + LocalMounts: map[string]fsutil.FS{ + dockerui.DefaultLocalNameDockerfile: dir2, + dockerui.DefaultLocalNameContext: dir2, + }, + }, nil) + require.NoError(t, err) + + unique2, err := os.ReadFile(filepath.Join(destDir2, "unique.txt")) + require.NoError(t, err) + + require.Equal(t, string(unique1), string(unique2), "cache should be matched and unique file content should be the same") } func applyTemplate(tmpl string, x any) (string, error) { diff --git a/solver/pb/attr.go b/solver/pb/attr.go index b18223dcdc6a..1157d987750e 100644 --- a/solver/pb/attr.go +++ b/solver/pb/attr.go @@ -6,6 +6,8 @@ const AttrAuthHeaderSecret = "git.authheadersecret" const AttrAuthTokenSecret = "git.authtokensecret" const AttrKnownSSHHosts = "git.knownsshhosts" const AttrMountSSHSock = "git.mountsshsock" +const AttrGitChecksum = "git.checksum" + const AttrLocalSessionID = "local.session" const AttrLocalUniqueID = "local.unique" const AttrIncludePatterns = "local.includepattern" diff --git a/solver/pb/caps.go b/solver/pb/caps.go index ce5b0d4ea9d2..75c298ae0d5c 100644 --- a/solver/pb/caps.go +++ b/solver/pb/caps.go @@ -30,6 +30,7 @@ const ( CapSourceGitKnownSSHHosts apicaps.CapID = "source.git.knownsshhosts" CapSourceGitMountSSHSock apicaps.CapID = "source.git.mountsshsock" CapSourceGitSubdir apicaps.CapID = "source.git.subdir" + CapSourceGitChecksum apicaps.CapID = "source.git.checksum" CapSourceHTTP apicaps.CapID = "source.http" CapSourceHTTPAuth apicaps.CapID = "source.http.auth" @@ -222,6 +223,12 @@ func init() { Status: apicaps.CapStatusExperimental, }) + Caps.Init(apicaps.Cap{ + ID: CapSourceGitChecksum, + Enabled: true, + Status: apicaps.CapStatusExperimental, + }) + Caps.Init(apicaps.Cap{ ID: CapSourceHTTP, Enabled: true, diff --git a/source/git/identifier.go b/source/git/identifier.go index 77951399b08a..ac2b0dbe6c04 100644 --- a/source/git/identifier.go +++ b/source/git/identifier.go @@ -13,6 +13,7 @@ import ( type GitIdentifier struct { Remote string Ref string + Checksum string Subdir string KeepGitDir bool AuthTokenSecret string diff --git a/source/git/source.go b/source/git/source.go index acba38551ea1..a1671796ecb4 100644 --- a/source/git/source.go +++ b/source/git/source.go @@ -92,6 +92,8 @@ func (gs *gitSource) Identifier(scheme, ref string, attrs map[string]string, pla id.KnownSSHHosts = v case pb.AttrMountSSHSock: id.MountSSHSock = v + case pb.AttrGitChecksum: + id.Checksum = v } } @@ -349,10 +351,26 @@ func (gs *gitSourceHandler) CacheKey(ctx context.Context, g session.Group, index gs.locker.Lock(remote) defer gs.locker.Unlock(remote) - if ref := gs.src.Ref; ref != "" && gitutil.IsCommitSHA(ref) { - cacheKey := gs.shaToCacheKey(ref, "") + if gs.src.Checksum != "" { + matched, err := regexp.MatchString("^[a-fA-F0-9]+$", gs.src.Checksum) + if err != nil || !matched { + return "", "", nil, false, errors.Errorf("invalid checksum %s for Git URL, expected hex commit hash", gs.src.Checksum) + } + } + + var refCommitFullHash, ref2 string + if gitutil.IsCommitSHA(gs.src.Checksum) && !gs.src.KeepGitDir { + refCommitFullHash = gs.src.Checksum + ref2 = gs.src.Ref + } + if refCommitFullHash == "" && gitutil.IsCommitSHA(gs.src.Ref) { + refCommitFullHash = gs.src.Ref + } + if refCommitFullHash != "" { + cacheKey := gs.shaToCacheKey(refCommitFullHash, ref2) gs.cacheKey = cacheKey - return cacheKey, ref, nil, true, nil + // gs.src.Checksum is verified when checking out the commit + return cacheKey, refCommitFullHash, nil, true, nil } gs.getAuthToken(ctx, g) @@ -415,7 +433,9 @@ func (gs *gitSourceHandler) CacheKey(ctx context.Context, g session.Group, index if !gitutil.IsCommitSHA(sha) { return "", "", nil, false, errors.Errorf("invalid commit sha %q", sha) } - + if gs.src.Checksum != "" && !strings.HasPrefix(sha, gs.src.Checksum) { + return "", "", nil, false, errors.Errorf("expected checksum to match %s, got %s", gs.src.Checksum, sha) + } cacheKey := gs.shaToCacheKey(sha, usedRef) gs.cacheKey = cacheKey return cacheKey, sha, nil, true, nil @@ -536,6 +556,17 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (out subdir = "." } + if gs.src.Checksum != "" { + actualHashBuf, err := git.Run(ctx, "rev-parse", ref) + if err != nil { + return nil, errors.Wrapf(err, "failed to rev-parse %s for %s", ref, urlutil.RedactCredentials(gs.src.Remote)) + } + actualHash := strings.TrimSpace(string(actualHashBuf)) + if !strings.HasPrefix(actualHash, gs.src.Checksum) { + return nil, errors.Errorf("expected checksum to match %s, got %s", gs.src.Checksum, actualHash) + } + } + if gs.src.KeepGitDir && subdir == "." { checkoutDirGit := filepath.Join(checkoutDir, ".git") if err := os.MkdirAll(checkoutDir, 0711); err != nil { diff --git a/source/git/source_test.go b/source/git/source_test.go index 5962814af1ff..9e9f568f15dd 100644 --- a/source/git/source_test.go +++ b/source/git/source_test.go @@ -148,6 +148,16 @@ func testRepeatedFetch(t *testing.T, keepGitDir bool) { 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 TestFetchBySHA(t *testing.T) { @@ -304,54 +314,75 @@ func testFetchUnreferencedRefSha(t *testing.T, ref string, keepGitDir bool) { } func TestFetchByTag(t *testing.T) { - testFetchByTag(t, "lightweight-tag", "third", false, true, false) + testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeNone) } func TestFetchByTagKeepGitDir(t *testing.T) { - testFetchByTag(t, "lightweight-tag", "third", false, true, true) + testFetchByTag(t, "lightweight-tag", "third", false, true, true, testChecksumModeNone) } func TestFetchByTagFull(t *testing.T) { - testFetchByTag(t, "refs/tags/lightweight-tag", "third", false, true, true) + testFetchByTag(t, "refs/tags/lightweight-tag", "third", false, true, true, testChecksumModeNone) } func TestFetchByAnnotatedTag(t *testing.T) { - testFetchByTag(t, "v1.2.3", "second", true, false, false) + testFetchByTag(t, "v1.2.3", "second", true, false, false, testChecksumModeNone) } func TestFetchByAnnotatedTagKeepGitDir(t *testing.T) { - testFetchByTag(t, "v1.2.3", "second", true, false, true) + testFetchByTag(t, "v1.2.3", "second", true, false, true, testChecksumModeNone) } func TestFetchByAnnotatedTagFull(t *testing.T) { - testFetchByTag(t, "refs/tags/v1.2.3", "second", true, false, true) + testFetchByTag(t, "refs/tags/v1.2.3", "second", true, false, true, testChecksumModeNone) } func TestFetchByBranch(t *testing.T) { - testFetchByTag(t, "feature", "withsub", false, true, false) + testFetchByTag(t, "feature", "withsub", false, true, false, testChecksumModeNone) } func TestFetchByBranchKeepGitDir(t *testing.T) { - testFetchByTag(t, "feature", "withsub", false, true, true) + testFetchByTag(t, "feature", "withsub", false, true, true, testChecksumModeNone) } func TestFetchByBranchFull(t *testing.T) { - testFetchByTag(t, "refs/heads/feature", "withsub", false, true, true) + testFetchByTag(t, "refs/heads/feature", "withsub", false, true, true, testChecksumModeNone) } func TestFetchByRef(t *testing.T) { - testFetchByTag(t, "test", "feature", false, true, false) + testFetchByTag(t, "test", "feature", false, true, false, testChecksumModeNone) } func TestFetchByRefKeepGitDir(t *testing.T) { - testFetchByTag(t, "test", "feature", false, true, true) + testFetchByTag(t, "test", "feature", false, true, true, testChecksumModeNone) } func TestFetchByRefFull(t *testing.T) { - testFetchByTag(t, "refs/test", "feature", false, true, true) + testFetchByTag(t, "refs/test", "feature", false, true, true, testChecksumModeNone) +} + +func TestFetchByTagWithChecksum(t *testing.T) { + testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeValid) } -func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotatedTag, hasFoo13File, keepGitDir bool) { +func TestFetchByTagWithChecksumPartial(t *testing.T) { + testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeValidPartial) +} + +func TestFetchByTagWithChecksumInvalid(t *testing.T) { + testFetchByTag(t, "lightweight-tag", "third", false, true, false, testChecksumModeInvalid) +} + +type testChecksumMode int + +const ( + testChecksumModeNone testChecksumMode = iota + testChecksumModeValid + testChecksumModeValidPartial + testChecksumModeInvalid +) + +func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotatedTag, hasFoo13File, keepGitDir bool, checksumMode testChecksumMode) { if runtime.GOOS == "windows" { t.Skip("Depends on unimplemented containerd bind-mount support on Windows") } @@ -366,6 +397,28 @@ func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotated id := &GitIdentifier{Remote: repo.mainURL, Ref: tag, KeepGitDir: keepGitDir} + if checksumMode != testChecksumModeNone { + cmd := exec.Command("git", "rev-parse", tag) + cmd.Dir = repo.mainPath + + out, err := cmd.Output() + require.NoError(t, err) + + sha := strings.TrimSpace(string(out)) + require.Equal(t, 40, len(sha)) + + switch checksumMode { + case testChecksumModeValid: + id.Checksum = sha + case testChecksumModeValidPartial: + id.Checksum = sha[:8] + case testChecksumModeInvalid: + id.Checksum = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + default: + // NOTREACHED + } + } + g, err := gs.Resolve(ctx, id, nil, nil) require.NoError(t, err) @@ -383,6 +436,10 @@ func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotated require.Equal(t, 40, len(pin1)) ref1, err := g.Snapshot(ctx, nil) + if checksumMode == testChecksumModeInvalid { + require.ErrorContains(t, err, "expected checksum to match "+id.Checksum) + return + } require.NoError(t, err) defer ref1.Release(context.TODO())