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

feat: internal manifest package #144

Merged
merged 20 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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...))
}
}
245 changes: 245 additions & 0 deletions internal/manifest/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package manifest

import (
"fmt"
"os"
"slices"

"github.com/klauspost/compress/zstd"

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

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

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

type Path struct {
Kind string `json:"kind"`
Path string `json:"path"`
Mode string `json:"mode"`
Slices []string `json:"slices"`
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"`
Path string `json:"path"`
}

type Manifest struct {
db *jsonwall.DB
}

func Read(absPath string) (manifest *Manifest, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've missed this on the first review: why is this not an io.Reader? It seems curious that it's not only a filename, but a filename to a zstd compressed file. Doesn't feel like this package should care about the compression format of the manifest, or how the manifest is being read from disk. Note how the jsonwall.ReadDB function blended nicely below. We won't be able to do this with this function.

Copy link
Collaborator Author

@letFunny letFunny Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree I was thinking that zstd was part of the "format" but there is nothing dictating that, we could be storing it in a completely different way and this package should continue working correctly. The initial idea was to get the filename to be able to do some more things in the validation but I think that is better suited for slicer not for the general manifest.

I have made the changes in https://github.com/canonical/chisel/pull/144/files/56dd49ca33944ab4e09cc7778cdaec7245732924..4e0e33b13cd8319e31fb0a0c36587cfa479dfd7c.

defer func() {
if err != nil {
err = fmt.Errorf("cannot read manifest: %s", err)
}
}()

file, err := os.OpenFile(absPath, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
defer file.Close()
r, err := zstd.NewReader(file)
if err != nil {
return nil, err
}
defer r.Close()
jsonwallDB, err := jsonwall.ReadDB(r)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

manifest = &Manifest{db: jsonwallDB}
err = validate(manifest)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
return manifest, nil
}

func (manifest *Manifest) IteratePath(prefix string, f func(Path) error) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("cannot read manifest: %s", err)
}
}()

iter, err := manifest.db.IteratePrefix(map[string]string{"kind": "path", "path": prefix})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var path Path
err := iter.Get(&path)
if err != nil {
return err
}
err = f(path)
if err != nil {
return err
}
}
return nil
}

func (manifest *Manifest) IteratePkgs(f func(Package) error) (err error) {
letFunny marked this conversation as resolved.
Show resolved Hide resolved
defer func() {
if err != nil {
err = fmt.Errorf("cannot read manifest: %s", err)
}
}()

iter, err := manifest.db.Iterate(map[string]string{"kind": "package"})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var pkg Package
err := iter.Get(&pkg)
if err != nil {
return err
}
err = f(pkg)
if err != nil {
return err
}
}
return nil
}

func (manifest *Manifest) IterateSlices(pkgName string, f func(Slice) error) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("cannot read manifest: %s", err)
}
}()

iter, err := manifest.db.IteratePrefix(map[string]string{"kind": "slice", "name": pkgName})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var slice Slice
err := iter.Get(&slice)
if err != nil {
return err
}
err = f(slice)
if err != nil {
return err
}
}
return nil
}

func validate(manifest *Manifest) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf(`invalid manifest: %s`, err)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
}
}()

pkgExist := map[string]bool{}
iter, err := manifest.db.Iterate(map[string]string{"kind": "package"})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var pkg Package
err := iter.Get(&pkg)
if err != nil {
return err
}
if pkg.Kind != "package" {
return fmt.Errorf(`in packages expected kind "package", got %q`, pkg.Kind)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
}
pkgExist[pkg.Name] = true
}

sliceExist := map[string]bool{}
iter, err = manifest.db.Iterate(map[string]string{"kind": "slice"})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var slice Slice
err := iter.Get(&slice)
if err != nil {
return err
}
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)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
}
sliceExist[slice.Name] = true
}

pathToSlices := map[string][]string{}
iter, err = manifest.db.Iterate(map[string]string{"kind": "content"})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var content Content
err := iter.Get(&content)
if err != nil {
return err
}
if content.Kind != "content" {
return fmt.Errorf(`in contents expected kind "content", got "%s"`, content.Kind)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
}
if !sliceExist[content.Slice] {
return fmt.Errorf(`slice %s not found in slices`, content.Slice)
}
pathToSlices[content.Path] = append(pathToSlices[content.Path], content.Slice)
}

iter, err = manifest.db.Iterate(map[string]string{"kind": "path"})
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
for iter.Next() {
var path Path
err := iter.Get(&path)
if err != nil {
return err
}
if path.Kind != "path" {
return fmt.Errorf(`in paths expected kind "path", got "%s"`, path.Kind)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
}
if pathSlices, ok := pathToSlices[path.Path]; !ok {
return fmt.Errorf(`path %s has no matching entry in contents`, path.Path)
} else if !slices.Equal(pathSlices, path.Slices) {
return fmt.Errorf(`path %s and content have diverging slices: %q != %q`, path.Path, path.Slices, pathSlices)
}
delete(pathToSlices, path.Path)
}

if len(pathToSlices) > 0 {
for path := range pathToSlices {
return fmt.Errorf(`content path %s has no matching entry in paths`, path)
}
}
return nil
}
Loading
Loading