-
Notifications
You must be signed in to change notification settings - Fork 47
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
niemeyer
merged 20 commits into
canonical:main
from
letFunny:chisel-db-manifest-package
Sep 14, 2024
Merged
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
e96e87b
feat: internal manifest package
letFunny 1ea56d1
remove unnecessary constants
letFunny fe51371
minor rename
letFunny 401c86e
make validate optional and use structs for prefixes
letFunny 8600db6
indent manifest jsonwall in tests
letFunny e2c2645
address review comments
letFunny 140ab91
change maps for structs with omitempty, jsonwall iterator
letFunny 2ee0286
refactor manifest iteration with generic helper
letFunny a0200f1
restrict generic with interface union type
letFunny cdaf78d
feat: add IterateContents
letFunny efa9b44
feat: use higher level API internally in manifest.Validate
letFunny e6f38d9
validate schema when reading manifest
letFunny f41c82b
TODO
letFunny 48d58ee
duplication is not an error
letFunny 101c3d4
duplication is ub, not allowed
letFunny 89174b4
Merge branch 'main' into chisel-db-manifest-package
letFunny 481f69f
pass by pointer instead of value
letFunny 56dd49c
use double quotes
letFunny 8dfcd2b
Read takes io.Reader
letFunny 4e0e33b
remove unnecessary zstd for manifest_test
letFunny File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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...)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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 | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.