Skip to content

Commit

Permalink
Add tests for semver tags and support scoped tags
Browse files Browse the repository at this point in the history
  • Loading branch information
errordeveloper committed Jul 5, 2024
1 parent f2f60cd commit 61583a2
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 8 deletions.
258 changes: 258 additions & 0 deletions attest/vcs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package attest_test

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"

gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/google/go-containerregistry/pkg/name"

. "github.com/onsi/gomega"

. "github.com/errordeveloper/tape/attest"
"github.com/errordeveloper/tape/attest/vcs/git"
"github.com/errordeveloper/tape/manifest/imagescanner"
"github.com/errordeveloper/tape/manifest/loader"
"github.com/errordeveloper/tape/oci"
)

type vcsTestCase struct {
URL, CheckoutTag, CheckoutHash, Branch string
LoadPath string
ExpectManifests, ExpectImageTags, ExpectRawTags []string
}

func (tc vcsTestCase) Name() string {
rev := tc.CheckoutTag
if rev == "" {
rev = tc.CheckoutHash
}
return fmt.Sprintf("%s@%s", tc.URL, rev)
}

func TestVCS(t *testing.T) {
testCases := []vcsTestCase{
// {
// URL: "https://github.com/stefanprodan/podinfo",
// CheckoutTag: "6.7.0", // => 0b1481aa8ed0a6c34af84f779824a74200d5c1d6
// LoadPath: "kustomize",
// ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
// ExpectImageTags: []string{"6.7.0"},
// ExpectRawTags: []string{"6.7.0"},
// },
// {
// URL: "https://github.com/stefanprodan/podinfo",
// CheckoutHash: "0b1481aa8ed0a6c34af84f779824a74200d5c1d6", // => 6.7.0
// Branch: "master",
// LoadPath: "kustomize",
// ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
// ExpectImageTags: []string{"6.7.0"},
// ExpectRawTags: []string{"6.7.0"},
// },
{
URL: "https://github.com/errordeveloper/tape-git-testing",
CheckoutHash: "3cad1d255c1d83b5e523de64d34758609498d81b",
Branch: "main",
LoadPath: "",
ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
ExpectImageTags: nil,
ExpectRawTags: nil,
},
{
URL: "https://github.com/errordeveloper/tape-git-testing",
CheckoutTag: "0.0.1",
LoadPath: "",
ExpectManifests: []string{"podinfo/kustomization.yaml", "podinfo/deployment.yaml", "podinfo/hpa.yaml", "podinfo/service.yaml"},
ExpectImageTags: []string{"v0.0.1"},
ExpectRawTags: []string{"0.0.1", "v0.0.1", "podinfo/v6.6.3"},
},
{
URL: "https://github.com/errordeveloper/tape-git-testing",
CheckoutTag: "v0.0.2",
LoadPath: "podinfo",
ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
ExpectImageTags: []string{"v6.7.0"},
ExpectRawTags: []string{"0.0.2", "v0.0.2", "podinfo/v6.7.0"},
},
{
URL: "https://github.com/errordeveloper/tape-git-testing",
CheckoutHash: "9eeeed9f4ff44812ca23dba1bd0af9f509686d21", // => v0.0.1
LoadPath: "podinfo",
ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"},
ExpectImageTags: []string{"v6.6.3"},
ExpectRawTags: []string{"0.0.1", "v0.0.1", "podinfo/v6.6.3"},
},
}

repos := &repos{}
repos.init()
defer repos.cleanup()

for i := range testCases {
tc := testCases[i]
t.Run(tc.Name(), makeVCSTest(repos, tc))
}
}

func makeVCSTest(repos *repos, tc vcsTestCase) func(t *testing.T) {
return func(t *testing.T) {
g := NewWithT(t)

ctx := context.Background()
checkoutPath, err := repos.clone(ctx, tc)
g.Expect(err).NotTo(HaveOccurred())

loadPath := filepath.Join(checkoutPath, tc.LoadPath)
loader := loader.NewRecursiveManifestDirectoryLoader(loadPath)
g.Expect(loader.Load()).To(Succeed())

repoDetected, attreg, err := DetectVCS(loadPath)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(repoDetected).To(BeTrue())
g.Expect(attreg).ToNot(BeNil())

scanner := imagescanner.NewDefaultImageScanner()
scanner.WithProvinanceAttestor(attreg)

if tc.ExpectManifests != nil {
g.Expect(loader.Paths()).To(HaveLen(len(tc.ExpectManifests)))
for _, manifest := range tc.ExpectManifests {
g.Expect(loader.ContainsRelPath(manifest)).To(BeTrue())
}
}

g.Expect(scanner.Scan(loader.RelPaths())).To(Succeed())

collection, err := attreg.MakePathCheckSummarySummaryCollection()
g.Expect(err).NotTo(HaveOccurred())
g.Expect(collection).ToNot(BeNil())
g.Expect(collection.Providers).To(ConsistOf("git"))
g.Expect(collection.EntryGroups).To(HaveLen(1))
g.Expect(collection.EntryGroups[0]).To(HaveLen(5))

vcsSummary := attreg.BaseDirSummary()
g.Expect(vcsSummary).ToNot(BeNil())
summaryJSON, err := json.Marshal(vcsSummary.Full())
g.Expect(err).NotTo(HaveOccurred())
t.Logf("VCS info for %q: %s", tc.LoadPath, summaryJSON)

g.Expect(attreg.AssociateCoreStatements()).To(Succeed())

statements := attreg.GetStatements()
g.Expect(statements).To(HaveLen(1))
g.Expect(statements[0].GetSubject()).To(HaveLen(4))

// TODO: validate schema

groupSummary, ok := vcsSummary.Full().(*git.Summary)
g.Expect(ok).To(BeTrue())
ref := groupSummary.Git.Reference
g.Expect(ref.Tags).To(HaveLen(len(tc.ExpectRawTags)))
imageTagNames := make([]string, len(ref.Tags))
for i, tag := range ref.Tags {
imageTagNames[i] = tag.Name
}
g.Expect(imageTagNames).To(ConsistOf(tc.ExpectRawTags))

image, err := name.NewRepository("podinfo")
g.Expect(err).NotTo(HaveOccurred())

semVerTags := oci.SemVerTagsFromAttestations(ctx, image.Tag("test.123456"), statements...)
g.Expect(semVerTags).To(HaveLen(len(tc.ExpectImageTags)))
semVerTagNames := make([]string, len(semVerTags))
for i, tag := range semVerTags {
semVerTagNames[i] = tag.TagStr()
}
g.Expect(semVerTagNames).To(ConsistOf(tc.ExpectImageTags))
}
}

type repos struct {
workDir string
tempDir string
cache map[string]string
}

func (r *repos) init() error {
workDir, err := os.Getwd()
if err != nil {
return err
}
r.workDir = workDir
tempDir, err := os.MkdirTemp("", ".vcs-test-*")
if err != nil {
return err
}
r.tempDir = tempDir
r.cache = map[string]string{}
return nil
}

func (r *repos) cleanup() error {
if r.tempDir == "" {
return nil
}
return os.RemoveAll(r.tempDir)
}

func (r *repos) mktemp() (string, error) {
return os.MkdirTemp(r.tempDir, "repo-*")
}

func (r *repos) mirror(ctx context.Context, tc vcsTestCase) (string, error) {
if _, ok := r.cache[tc.URL]; !ok {
mirrorDir, err := r.mktemp()
if err != nil {
return "", err
}
_, err = gogit.PlainCloneContext(ctx, mirrorDir, true, &gogit.CloneOptions{Mirror: true, URL: tc.URL})
if err != nil {
return "", err
}
r.cache[tc.URL] = mirrorDir
}
return r.cache[tc.URL], nil
}

func (r *repos) clone(ctx context.Context, tc vcsTestCase) (string, error) {
mirrorDir, err := r.mirror(ctx, tc)
if err != nil {
return "", err
}
checkoutDir, err := r.mktemp()
if err != nil {
return "", err
}

opts := &gogit.CloneOptions{URL: mirrorDir}
if tc.CheckoutTag != "" {
opts.ReferenceName = plumbing.NewTagReferenceName(tc.CheckoutTag)
} else if tc.Branch != "" {
opts.ReferenceName = plumbing.NewBranchReferenceName(tc.Branch)
}

repo, err := gogit.PlainCloneContext(ctx, checkoutDir, false, opts)
if err != nil {
return "", fmt.Errorf("failed to clone: %w", err)
}

if tc.CheckoutHash != "" {
workTree, err := repo.Worktree()
if err != nil {
return "", err
}
opts := &gogit.CheckoutOptions{
Hash: plumbing.NewHash(tc.CheckoutHash),
}

if err := workTree.Checkout(opts); err != nil {
return "", err
}
}
return filepath.Rel(r.workDir, checkoutDir)
}
47 changes: 39 additions & 8 deletions oci/artefact.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"maps"
"os"
"path/filepath"
"strings"
"time"

"golang.org/x/mod/semver"
Expand Down Expand Up @@ -394,21 +395,51 @@ func SemVerTagsFromAttestations(ctx context.Context, tag name.Tag, sourceAttesta
return []name.Tag{}
}
ref := groupSummary.Git.Reference
if len(ref.Tags) == 0 {
numTags := len(ref.Tags)
if numTags == 0 {
return []name.Tag{}
}
// TODO: detect tags with groupSummary.Path+"/" as prefix and priorities them
tags := make([]name.Tag, 0, len(ref.Tags))
tags := newTagtagSet(numTags)
scopedTags := newTagtagSet(numTags)
for i := range ref.Tags {
t := ref.Tags[i].Name
if semver.IsValid(t) || semver.IsValid("v"+t) {
tags = append(tags, tag.Context().Tag(ref.Tags[i].Name))
// this is accounts only for a simple case where tape is pointed at a dir
// and a tags have prefix that matches it exactly, it won't work for cases
// where tape is pointed at a subdir a parent of which has a scoped tag
if strings.HasPrefix(t, groupSummary.Path+"/") {
scopedTags.add(strings.TrimPrefix(t, groupSummary.Path+"/"), tag)
continue
}
tags.add(t, tag)
}
if len(tags) == 0 {
return []name.Tag{}
if len(scopedTags.list) > 0 {
return scopedTags.list
}
return tags.list
}

type tagSet struct {
set map[string]struct{}
list []name.Tag
}

func newTagtagSet(c int) *tagSet {
return &tagSet{
set: make(map[string]struct{}, c),
list: make([]name.Tag, 0, c),
}
}

func (s *tagSet) add(t string, image name.Tag) {
if !strings.HasPrefix(t, "v") {
t = "v" + t
}
if _, ok := s.set[t]; !ok {
if semver.IsValid(t) {
s.list = append(s.list, image.Context().Tag(t))
s.set[t] = struct{}{}
}
}
return tags
}

func makeDescriptorWithPlatform() Descriptor {
Expand Down

0 comments on commit 61583a2

Please sign in to comment.