Skip to content

Commit 61583a2

Browse files
Add tests for semver tags and support scoped tags
1 parent f2f60cd commit 61583a2

File tree

2 files changed

+297
-8
lines changed

2 files changed

+297
-8
lines changed

attest/vcs_test.go

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package attest_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
gogit "github.com/go-git/go-git/v5"
12+
"github.com/go-git/go-git/v5/plumbing"
13+
"github.com/google/go-containerregistry/pkg/name"
14+
15+
. "github.com/onsi/gomega"
16+
17+
. "github.com/errordeveloper/tape/attest"
18+
"github.com/errordeveloper/tape/attest/vcs/git"
19+
"github.com/errordeveloper/tape/manifest/imagescanner"
20+
"github.com/errordeveloper/tape/manifest/loader"
21+
"github.com/errordeveloper/tape/oci"
22+
)
23+
24+
type vcsTestCase struct {
25+
URL, CheckoutTag, CheckoutHash, Branch string
26+
LoadPath string
27+
ExpectManifests, ExpectImageTags, ExpectRawTags []string
28+
}
29+
30+
func (tc vcsTestCase) Name() string {
31+
rev := tc.CheckoutTag
32+
if rev == "" {
33+
rev = tc.CheckoutHash
34+
}
35+
return fmt.Sprintf("%s@%s", tc.URL, rev)
36+
}
37+
38+
func TestVCS(t *testing.T) {
39+
testCases := []vcsTestCase{
40+
// {
41+
// URL: "https://github.com/stefanprodan/podinfo",
42+
// CheckoutTag: "6.7.0", // => 0b1481aa8ed0a6c34af84f779824a74200d5c1d6
43+
// LoadPath: "kustomize",
44+
// ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
45+
// ExpectImageTags: []string{"6.7.0"},
46+
// ExpectRawTags: []string{"6.7.0"},
47+
// },
48+
// {
49+
// URL: "https://github.com/stefanprodan/podinfo",
50+
// CheckoutHash: "0b1481aa8ed0a6c34af84f779824a74200d5c1d6", // => 6.7.0
51+
// Branch: "master",
52+
// LoadPath: "kustomize",
53+
// ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
54+
// ExpectImageTags: []string{"6.7.0"},
55+
// ExpectRawTags: []string{"6.7.0"},
56+
// },
57+
{
58+
URL: "https://github.com/errordeveloper/tape-git-testing",
59+
CheckoutHash: "3cad1d255c1d83b5e523de64d34758609498d81b",
60+
Branch: "main",
61+
LoadPath: "",
62+
ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
63+
ExpectImageTags: nil,
64+
ExpectRawTags: nil,
65+
},
66+
{
67+
URL: "https://github.com/errordeveloper/tape-git-testing",
68+
CheckoutTag: "0.0.1",
69+
LoadPath: "",
70+
ExpectManifests: []string{"podinfo/kustomization.yaml", "podinfo/deployment.yaml", "podinfo/hpa.yaml", "podinfo/service.yaml"},
71+
ExpectImageTags: []string{"v0.0.1"},
72+
ExpectRawTags: []string{"0.0.1", "v0.0.1", "podinfo/v6.6.3"},
73+
},
74+
{
75+
URL: "https://github.com/errordeveloper/tape-git-testing",
76+
CheckoutTag: "v0.0.2",
77+
LoadPath: "podinfo",
78+
ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
79+
ExpectImageTags: []string{"v6.7.0"},
80+
ExpectRawTags: []string{"0.0.2", "v0.0.2", "podinfo/v6.7.0"},
81+
},
82+
{
83+
URL: "https://github.com/errordeveloper/tape-git-testing",
84+
CheckoutHash: "9eeeed9f4ff44812ca23dba1bd0af9f509686d21", // => v0.0.1
85+
LoadPath: "podinfo",
86+
ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
87+
ExpectImageTags: []string{"v6.6.3"},
88+
ExpectRawTags: []string{"0.0.1", "v0.0.1", "podinfo/v6.6.3"},
89+
},
90+
}
91+
92+
repos := &repos{}
93+
repos.init()
94+
defer repos.cleanup()
95+
96+
for i := range testCases {
97+
tc := testCases[i]
98+
t.Run(tc.Name(), makeVCSTest(repos, tc))
99+
}
100+
}
101+
102+
func makeVCSTest(repos *repos, tc vcsTestCase) func(t *testing.T) {
103+
return func(t *testing.T) {
104+
g := NewWithT(t)
105+
106+
ctx := context.Background()
107+
checkoutPath, err := repos.clone(ctx, tc)
108+
g.Expect(err).NotTo(HaveOccurred())
109+
110+
loadPath := filepath.Join(checkoutPath, tc.LoadPath)
111+
loader := loader.NewRecursiveManifestDirectoryLoader(loadPath)
112+
g.Expect(loader.Load()).To(Succeed())
113+
114+
repoDetected, attreg, err := DetectVCS(loadPath)
115+
g.Expect(err).NotTo(HaveOccurred())
116+
g.Expect(repoDetected).To(BeTrue())
117+
g.Expect(attreg).ToNot(BeNil())
118+
119+
scanner := imagescanner.NewDefaultImageScanner()
120+
scanner.WithProvinanceAttestor(attreg)
121+
122+
if tc.ExpectManifests != nil {
123+
g.Expect(loader.Paths()).To(HaveLen(len(tc.ExpectManifests)))
124+
for _, manifest := range tc.ExpectManifests {
125+
g.Expect(loader.ContainsRelPath(manifest)).To(BeTrue())
126+
}
127+
}
128+
129+
g.Expect(scanner.Scan(loader.RelPaths())).To(Succeed())
130+
131+
collection, err := attreg.MakePathCheckSummarySummaryCollection()
132+
g.Expect(err).NotTo(HaveOccurred())
133+
g.Expect(collection).ToNot(BeNil())
134+
g.Expect(collection.Providers).To(ConsistOf("git"))
135+
g.Expect(collection.EntryGroups).To(HaveLen(1))
136+
g.Expect(collection.EntryGroups[0]).To(HaveLen(5))
137+
138+
vcsSummary := attreg.BaseDirSummary()
139+
g.Expect(vcsSummary).ToNot(BeNil())
140+
summaryJSON, err := json.Marshal(vcsSummary.Full())
141+
g.Expect(err).NotTo(HaveOccurred())
142+
t.Logf("VCS info for %q: %s", tc.LoadPath, summaryJSON)
143+
144+
g.Expect(attreg.AssociateCoreStatements()).To(Succeed())
145+
146+
statements := attreg.GetStatements()
147+
g.Expect(statements).To(HaveLen(1))
148+
g.Expect(statements[0].GetSubject()).To(HaveLen(4))
149+
150+
// TODO: validate schema
151+
152+
groupSummary, ok := vcsSummary.Full().(*git.Summary)
153+
g.Expect(ok).To(BeTrue())
154+
ref := groupSummary.Git.Reference
155+
g.Expect(ref.Tags).To(HaveLen(len(tc.ExpectRawTags)))
156+
imageTagNames := make([]string, len(ref.Tags))
157+
for i, tag := range ref.Tags {
158+
imageTagNames[i] = tag.Name
159+
}
160+
g.Expect(imageTagNames).To(ConsistOf(tc.ExpectRawTags))
161+
162+
image, err := name.NewRepository("podinfo")
163+
g.Expect(err).NotTo(HaveOccurred())
164+
165+
semVerTags := oci.SemVerTagsFromAttestations(ctx, image.Tag("test.123456"), statements...)
166+
g.Expect(semVerTags).To(HaveLen(len(tc.ExpectImageTags)))
167+
semVerTagNames := make([]string, len(semVerTags))
168+
for i, tag := range semVerTags {
169+
semVerTagNames[i] = tag.TagStr()
170+
}
171+
g.Expect(semVerTagNames).To(ConsistOf(tc.ExpectImageTags))
172+
}
173+
}
174+
175+
type repos struct {
176+
workDir string
177+
tempDir string
178+
cache map[string]string
179+
}
180+
181+
func (r *repos) init() error {
182+
workDir, err := os.Getwd()
183+
if err != nil {
184+
return err
185+
}
186+
r.workDir = workDir
187+
tempDir, err := os.MkdirTemp("", ".vcs-test-*")
188+
if err != nil {
189+
return err
190+
}
191+
r.tempDir = tempDir
192+
r.cache = map[string]string{}
193+
return nil
194+
}
195+
196+
func (r *repos) cleanup() error {
197+
if r.tempDir == "" {
198+
return nil
199+
}
200+
return os.RemoveAll(r.tempDir)
201+
}
202+
203+
func (r *repos) mktemp() (string, error) {
204+
return os.MkdirTemp(r.tempDir, "repo-*")
205+
}
206+
207+
func (r *repos) mirror(ctx context.Context, tc vcsTestCase) (string, error) {
208+
if _, ok := r.cache[tc.URL]; !ok {
209+
mirrorDir, err := r.mktemp()
210+
if err != nil {
211+
return "", err
212+
}
213+
_, err = gogit.PlainCloneContext(ctx, mirrorDir, true, &gogit.CloneOptions{Mirror: true, URL: tc.URL})
214+
if err != nil {
215+
return "", err
216+
}
217+
r.cache[tc.URL] = mirrorDir
218+
}
219+
return r.cache[tc.URL], nil
220+
}
221+
222+
func (r *repos) clone(ctx context.Context, tc vcsTestCase) (string, error) {
223+
mirrorDir, err := r.mirror(ctx, tc)
224+
if err != nil {
225+
return "", err
226+
}
227+
checkoutDir, err := r.mktemp()
228+
if err != nil {
229+
return "", err
230+
}
231+
232+
opts := &gogit.CloneOptions{URL: mirrorDir}
233+
if tc.CheckoutTag != "" {
234+
opts.ReferenceName = plumbing.NewTagReferenceName(tc.CheckoutTag)
235+
} else if tc.Branch != "" {
236+
opts.ReferenceName = plumbing.NewBranchReferenceName(tc.Branch)
237+
}
238+
239+
repo, err := gogit.PlainCloneContext(ctx, checkoutDir, false, opts)
240+
if err != nil {
241+
return "", fmt.Errorf("failed to clone: %w", err)
242+
}
243+
244+
if tc.CheckoutHash != "" {
245+
workTree, err := repo.Worktree()
246+
if err != nil {
247+
return "", err
248+
}
249+
opts := &gogit.CheckoutOptions{
250+
Hash: plumbing.NewHash(tc.CheckoutHash),
251+
}
252+
253+
if err := workTree.Checkout(opts); err != nil {
254+
return "", err
255+
}
256+
}
257+
return filepath.Rel(r.workDir, checkoutDir)
258+
}

oci/artefact.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"maps"
1212
"os"
1313
"path/filepath"
14+
"strings"
1415
"time"
1516

1617
"golang.org/x/mod/semver"
@@ -394,21 +395,51 @@ func SemVerTagsFromAttestations(ctx context.Context, tag name.Tag, sourceAttesta
394395
return []name.Tag{}
395396
}
396397
ref := groupSummary.Git.Reference
397-
if len(ref.Tags) == 0 {
398+
numTags := len(ref.Tags)
399+
if numTags == 0 {
398400
return []name.Tag{}
399401
}
400-
// TODO: detect tags with groupSummary.Path+"/" as prefix and priorities them
401-
tags := make([]name.Tag, 0, len(ref.Tags))
402+
tags := newTagtagSet(numTags)
403+
scopedTags := newTagtagSet(numTags)
402404
for i := range ref.Tags {
403405
t := ref.Tags[i].Name
404-
if semver.IsValid(t) || semver.IsValid("v"+t) {
405-
tags = append(tags, tag.Context().Tag(ref.Tags[i].Name))
406+
// this is accounts only for a simple case where tape is pointed at a dir
407+
// and a tags have prefix that matches it exactly, it won't work for cases
408+
// where tape is pointed at a subdir a parent of which has a scoped tag
409+
if strings.HasPrefix(t, groupSummary.Path+"/") {
410+
scopedTags.add(strings.TrimPrefix(t, groupSummary.Path+"/"), tag)
411+
continue
406412
}
413+
tags.add(t, tag)
407414
}
408-
if len(tags) == 0 {
409-
return []name.Tag{}
415+
if len(scopedTags.list) > 0 {
416+
return scopedTags.list
417+
}
418+
return tags.list
419+
}
420+
421+
type tagSet struct {
422+
set map[string]struct{}
423+
list []name.Tag
424+
}
425+
426+
func newTagtagSet(c int) *tagSet {
427+
return &tagSet{
428+
set: make(map[string]struct{}, c),
429+
list: make([]name.Tag, 0, c),
430+
}
431+
}
432+
433+
func (s *tagSet) add(t string, image name.Tag) {
434+
if !strings.HasPrefix(t, "v") {
435+
t = "v" + t
436+
}
437+
if _, ok := s.set[t]; !ok {
438+
if semver.IsValid(t) {
439+
s.list = append(s.list, image.Context().Tag(t))
440+
s.set[t] = struct{}{}
441+
}
410442
}
411-
return tags
412443
}
413444

414445
func makeDescriptorWithPlatform() Descriptor {

0 commit comments

Comments
 (0)