Skip to content

Commit

Permalink
feat: internal manifest package (#144)
Browse files Browse the repository at this point in the history
Add Manifest abstraction on top of the jsonwall format.
  • Loading branch information
letFunny authored Sep 14, 2024
1 parent ae52f84 commit 2cea830
Show file tree
Hide file tree
Showing 4 changed files with 468 additions and 0 deletions.
53 changes: 53 additions & 0 deletions internal/manifest/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package manifest

import (
"fmt"
"sync"
)

// Avoid importing the log type information unnecessarily. There's a small cost
// associated with using an interface rather than the type. Depending on how
// often the logger is plugged in, it would be worth using the type instead.
type log_Logger interface {
Output(calldepth int, s string) error
}

var globalLoggerLock sync.Mutex
var globalLogger log_Logger
var globalDebug bool

// Specify the *log.Logger object where log messages should be sent to.
func SetLogger(logger log_Logger) {
globalLoggerLock.Lock()
globalLogger = logger
globalLoggerLock.Unlock()
}

// Enable the delivery of debug messages to the logger. Only meaningful
// if a logger is also set.
func SetDebug(debug bool) {
globalLoggerLock.Lock()
globalDebug = debug
globalLoggerLock.Unlock()
}

// logf sends to the logger registered via SetLogger the string resulting
// from running format and args through Sprintf.
func logf(format string, args ...interface{}) {
globalLoggerLock.Lock()
defer globalLoggerLock.Unlock()
if globalLogger != nil {
globalLogger.Output(2, fmt.Sprintf(format, args...))
}
}

// debugf sends to the logger registered via SetLogger the string resulting
// from running format and args through Sprintf, but only if debugging was
// enabled via SetDebug.
func debugf(format string, args ...interface{}) {
globalLoggerLock.Lock()
defer globalLoggerLock.Unlock()
if globalDebug && globalLogger != nil {
globalLogger.Output(2, fmt.Sprintf(format, args...))
}
}
182 changes: 182 additions & 0 deletions internal/manifest/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package manifest

import (
"fmt"
"io"
"slices"

"github.com/canonical/chisel/internal/jsonwall"
"github.com/canonical/chisel/internal/setup"
)

const schema = "1.0"

type Package struct {
Kind string `json:"kind"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Digest string `json:"sha256,omitempty"`
Arch string `json:"arch,omitempty"`
}

type Slice struct {
Kind string `json:"kind"`
Name string `json:"name,omitempty"`
}

type Path struct {
Kind string `json:"kind"`
Path string `json:"path,omitempty"`
Mode string `json:"mode,omitempty"`
Slices []string `json:"slices,omitempty"`
Hash string `json:"sha256,omitempty"`
FinalHash string `json:"final_sha256,omitempty"`
Size uint64 `json:"size,omitempty"`
Link string `json:"link,omitempty"`
}

type Content struct {
Kind string `json:"kind"`
Slice string `json:"slice,omitempty"`
Path string `json:"path,omitempty"`
}

type Manifest struct {
db *jsonwall.DB
}

// Read loads a Manifest without performing any validation. The data is assumed
// to be both valid jsonwall and a valid Manifest (see Validate).
func Read(reader io.Reader) (manifest *Manifest, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("cannot read manifest: %s", err)
}
}()

db, err := jsonwall.ReadDB(reader)
if err != nil {
return nil, err
}
mfestSchema := db.Schema()
if mfestSchema != schema {
return nil, fmt.Errorf("unknown schema version %q", mfestSchema)
}

manifest = &Manifest{db: db}
return manifest, nil
}

func (manifest *Manifest) IteratePaths(pathPrefix string, onMatch func(*Path) error) (err error) {
return iteratePrefix(manifest, &Path{Kind: "path", Path: pathPrefix}, onMatch)
}

func (manifest *Manifest) IteratePackages(onMatch func(*Package) error) (err error) {
return iteratePrefix(manifest, &Package{Kind: "package"}, onMatch)
}

func (manifest *Manifest) IterateSlices(pkgName string, onMatch func(*Slice) error) (err error) {
return iteratePrefix(manifest, &Slice{Kind: "slice", Name: pkgName}, onMatch)
}

func (manifest *Manifest) IterateContents(slice string, onMatch func(*Content) error) (err error) {
return iteratePrefix(manifest, &Content{Kind: "content", Slice: slice}, onMatch)
}

// Validate checks that the Manifest is valid. Note that to do that it has to
// load practically the whole manifest into memory and unmarshall all the
// entries.
func Validate(manifest *Manifest) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("invalid manifest: %s", err)
}
}()

pkgExist := map[string]bool{}
err = manifest.IteratePackages(func(pkg *Package) error {
pkgExist[pkg.Name] = true
return nil
})
if err != nil {
return err
}

sliceExist := map[string]bool{}
err = manifest.IterateSlices("", func(slice *Slice) error {
sk, err := setup.ParseSliceKey(slice.Name)
if err != nil {
return err
}
if !pkgExist[sk.Package] {
return fmt.Errorf("package %q not found in packages", sk.Package)
}
sliceExist[slice.Name] = true
return nil
})
if err != nil {
return err
}

pathToSlices := map[string][]string{}
err = manifest.IterateContents("", func(content *Content) error {
if !sliceExist[content.Slice] {
return fmt.Errorf("slice %s not found in slices", content.Slice)
}
if !slices.Contains(pathToSlices[content.Path], content.Slice) {
pathToSlices[content.Path] = append(pathToSlices[content.Path], content.Slice)
}
return nil
})
if err != nil {
return err
}

done := map[string]bool{}
err = manifest.IteratePaths("", func(path *Path) error {
pathSlices, ok := pathToSlices[path.Path]
if !ok {
return fmt.Errorf("path %s has no matching entry in contents", path.Path)
}
slices.Sort(pathSlices)
slices.Sort(path.Slices)
if !slices.Equal(pathSlices, path.Slices) {
return fmt.Errorf("path %s and content have diverging slices: %q != %q", path.Path, path.Slices, pathSlices)
}
done[path.Path] = true
return nil
})
if err != nil {
return err
}

if len(done) != len(pathToSlices) {
for path := range pathToSlices {
return fmt.Errorf("content path %s has no matching entry in paths", path)
}
}
return nil
}

type prefixable interface {
Path | Content | Package | Slice
}

func iteratePrefix[T prefixable](manifest *Manifest, prefix *T, onMatch func(*T) error) error {
iter, err := manifest.db.IteratePrefix(prefix)
if err != nil {
return err
}
for iter.Next() {
var val T
err := iter.Get(&val)
if err != nil {
return fmt.Errorf("cannot read manifest: %s", err)
}
err = onMatch(&val)
if err != nil {
return err
}
}
return nil
}
Loading

0 comments on commit 2cea830

Please sign in to comment.