-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: internal manifest package (#144)
Add Manifest abstraction on top of the jsonwall format.
- Loading branch information
Showing
4 changed files
with
468 additions
and
0 deletions.
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,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 | ||
} |
Oops, something went wrong.