Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic VCS tagging #1

Merged
merged 7 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions attest/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"cmp"

"github.com/errordeveloper/tape/attest/types"
attestTypes "github.com/errordeveloper/tape/attest/types"
)

const (
ManifestDirPredicateType = "docker.com/tape/ManifestDir/v0.1"
ManifestDirPredicateType = "docker.com/tape/ManifestDir/v0.2"
)

var (
_ types.Statement = (*DirContents)(nil)
)

type DirContents struct {
types.GenericStatement[SourceDirectory]
types.GenericStatement[SourceDirectoryContents]
}

type SourceDirectory struct {
Expand All @@ -24,14 +25,16 @@ type SourceDirectory struct {
VCSEntries *types.PathCheckSummaryCollection `json:"vcsEntries"`
}

type SourceDirectoryContents struct {
SourceDirectory `json:"containedInDirectory"`
}

func MakeDirContentsStatement(dir string, entries *types.PathCheckSummaryCollection) types.Statement {
return &DirContents{
types.MakeStatement[SourceDirectory](
types.MakeStatement[SourceDirectoryContents](
ManifestDirPredicateType,
struct {
SourceDirectory `json:"containedInDirectory"`
}{
SourceDirectory{
SourceDirectoryContents{
SourceDirectory: SourceDirectory{
Path: dir,
VCSEntries: entries,
},
Expand All @@ -41,6 +44,14 @@ func MakeDirContentsStatement(dir string, entries *types.PathCheckSummaryCollect
}
}

func MakeDirContentsStatementFrom(statement types.Statement) DirContents {
dirContents := DirContents{
GenericStatement: attestTypes.GenericStatement[SourceDirectoryContents]{},
}
dirContents.ConvertFrom(statement)
return dirContents
}

func (a SourceDirectory) Compare(b SourceDirectory) types.Cmp {
if cmp := cmp.Compare(a.Path, b.Path); cmp != 0 {
return &cmp
Expand All @@ -54,3 +65,7 @@ func (a SourceDirectory) Compare(b SourceDirectory) types.Cmp {
cmp := a.VCSEntries.Compare(*b.VCSEntries)
return &cmp
}

func (a SourceDirectoryContents) Compare(b SourceDirectoryContents) types.Cmp {
return a.SourceDirectory.Compare(b.SourceDirectory)
}
4 changes: 2 additions & 2 deletions attest/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ func makeRegistryTest(tc testdata.TestCase) func(t *testing.T) {
loader := loader.NewRecursiveManifestDirectoryLoader(tc.Directory)
g.Expect(loader.Load()).To(Succeed())

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

scanner := imagescanner.NewDefaultImageScanner()
Expand Down
29 changes: 27 additions & 2 deletions attest/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ func Export(s ExportableStatement) toto.Statement {
}
}

func FilterByPredicateType(t string, s Statements) Statements {
results := Statements{}
for i := range s {
if s[i].GetType() == t {
results = append(results, s[i])
}
}
return results
}

type StamentConverter[T any] struct {
Statement
}

func (s *GenericStatement[T]) ConvertFrom(statement Statement) error {
predicate, ok := statement.GetPredicate().(ComparablePredicate[T])
if !ok {
return fmt.Errorf("cannot convert statement with predicte of type %T into %T", statement.GetPredicate(), GenericStatement[T]{})
}

*s = MakeStatement[T](statement.GetType(), predicate, statement.GetSubject()...)
return nil
}

func (s Statements) Export() []toto.Statement {
statements := make([]toto.Statement, len(s))
for i := range s {
Expand Down Expand Up @@ -368,8 +392,9 @@ func comparePathCheckSummaries(a, b PathCheckSummary) int {
return cmp.Compare(a.Common().Path, b.Common().Path)
}

func (p Predicate[T]) GetType() string { return p.Type }
func (p Predicate[T]) GetPredicate() any { return p.ComparablePredicate }
func (p Predicate[T]) GetType() string { return p.Type }
func (p Predicate[T]) GetPredicate() any { return p.ComparablePredicate }
func (p Predicate[T]) GetUnderlyingPredicate() T { return p.ComparablePredicate.(T) }

func (p Predicate[T]) Compare(b any) Cmp {
if b, ok := b.(Predicate[T]); ok {
Expand Down
162 changes: 129 additions & 33 deletions attest/vcs/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ const (
DefaultPrimaryRemoteName = "origin"
)

// TODO: need a way to detect multiple repos, for now PathChecker is only meant
// to be used for the manifest dir iteself, and assume there is no nested repos

func NewPathChecker(path string, digest digest.SHA256) types.PathChecker {
return &PathChecker{
path: path,
Expand All @@ -47,16 +44,35 @@ type (
}

GitSummary struct {
ObjectHash *string `json:"objectHash,omitempty"`
Remotes map[string][]string `json:"remotes,omitempty"`
Reference GitReference `json:"reference,omitempty"`
Object GitObject `json:"object,omitempty"`
Remotes map[string][]string `json:"remotes,omitempty"`
Reference GitReference `json:"reference,omitempty"`
}

GitObject struct {
TreeHash string `json:"treeHash,omitempty"`
CommitHash string `json:"commitHash,omitempty"`
}

Signature struct {
PGP []byte `json:"pgp"`
Validated bool `json:"validated"`
}

GitTag struct {
Name string `json:"name"`
Hash string `json:"hash,omitempty"`
Target string `json:"target,omitempty"`
Signature *Signature `json:"signature,omitempty"`
}

GitReference struct {
Name string `json:"name,omitempty"`
Hash string `json:"hash,omitempty"`
Type string `json:"type,omitempty"`
Target string `json:"target,omitempty"`
Name string `json:"name,omitempty"`
Hash string `json:"hash,omitempty"`
Type string `json:"type,omitempty"`
Target string `json:"target,omitempty"`
Tags []GitTag `json:"tags,omitempty"`
Signature *Signature `json:"signature,omitempty"`
}
)

Expand Down Expand Up @@ -142,19 +158,101 @@ func (c *PathChecker) MakeSummary() (types.PathCheckSummary, error) {
Git: &git,
}

// TODO: determine position of local branch against remote
// TODO: introduce notion of primary remote branch to determine the possition of the working branch
// TODO: determine if a tag is used
// TODO: also check if local tag in sync wirth remote tag
// TODO: provide info on singed tags/commits
head, err := c.cache.repo.Head()
if err != nil {
return nil, err
}

ref := GitReference{
Name: head.Name().String(),
Hash: head.Hash().String(),
Type: head.Type().String(),
Target: head.Target().String(),
}

obj := &GitObject{}
if summary.Unmodified {
git.ObjectHash = new(string)
*git.ObjectHash = c.cache.obj.ID().String()
obj.TreeHash = c.cache.obj.ID().String()
} else if c.IsBlob() {
// there is currently no easy way to obtain a hash for a subtree
git.ObjectHash = new(string)
*git.ObjectHash = c.cache.blobHash
obj.TreeHash = c.cache.blobHash
}

headCommit, err := c.cache.repo.CommitObject(head.Hash())
if err != nil {
return nil, err
}
if headCommit.PGPSignature != "" {
ref.Signature = &Signature{
PGP: []byte(headCommit.PGPSignature),
Validated: false,
}
}

if summary.Unmodified {
commitIter := object.NewCommitPathIterFromIter(
func(path string) bool {
switch {
case c.IsTree():
return strings.HasPrefix(c.cache.repoPath, path)
case c.IsBlob():
return c.cache.repoPath == path
default:
return false
}
},
object.NewCommitIterCTime(headCommit, nil, nil),
true,
)
defer commitIter.Close()
// only need first commit, avoid looping over all commits with ForEach
commit, err := commitIter.Next()
if err == nil {
obj.CommitHash = commit.Hash.String()
} else if err != io.EOF {
return nil, err
}
}

tags, err := c.cache.repo.Tags()
if err != nil {
return nil, err
}

if err := tags.ForEach(func(t *plumbing.Reference) error {
target, err := c.cache.repo.ResolveRevision(plumbing.Revision(t.Name()))
if err != nil {
return err
}
if *target != head.Hash() {
// doesn't point to HEAD
return nil
}

tag := GitTag{
Name: t.Name().Short(),
Hash: t.Hash().String(),
Target: target.String(),
}

if tag.Target != tag.Hash {
// annotated tags have own object hash, while has of a leightweight tag is the same as target
tagObject, err := c.cache.repo.TagObject(t.Hash())
if err != nil {
return err
}
if tagObject.PGPSignature != "" {
tag.Signature = &Signature{
PGP: []byte(tagObject.PGPSignature),
Validated: false,
}
}
}

ref.Tags = append(ref.Tags, tag)
return nil
}); err != nil {
return nil, err
}

remotes, err := c.cache.repo.Remotes()
Expand Down Expand Up @@ -189,17 +287,8 @@ func (c *PathChecker) MakeSummary() (types.PathCheckSummary, error) {
git.Remotes[remoteConfig.Name] = remoteConfig.URLs
}

head, err := c.cache.repo.Head()
if err != nil {
return nil, err
}

git.Reference = GitReference{
Name: head.Name().String(),
Hash: head.Hash().String(),
Type: head.Type().String(),
Target: head.Target().String(),
}
git.Reference = ref
git.Object = *obj

return summary, nil
}
Expand Down Expand Up @@ -288,6 +377,10 @@ func (c *PathChecker) Check() (bool, bool, error) {
}
unmodified, _, err := isBlobUnmodified(worktree, &f.Blob, filepath.Join(repoPath, f.Name))
if err != nil {
if f.Mode == filemode.Symlink {
// TODO: should at least log a warning for broken symlink
return nil
}
return err
}
if !unmodified {
Expand Down Expand Up @@ -419,6 +512,9 @@ func findByPath(repo *gogit.Repository, path string) (object.Object, error) {
if err != nil {
return nil, err
}
if path == "." {
return tree, nil
}
treeEntry, err := tree.FindEntry(path)
switch err {
case nil:
Expand All @@ -438,12 +534,12 @@ func findByPath(repo *gogit.Repository, path string) (object.Object, error) {
}

func detectRepo(path string) (*gogit.Repository, bool) {
if repo, err := gogit.PlainOpen(path); err == nil {
return repo, true
}
dir := filepath.Dir(path)
if dir == path { // reached root
return nil, false
}
if repo, err := gogit.PlainOpen(dir); err == nil {
return repo, true
}
return detectRepo(dir)
}
Loading
Loading