-
Notifications
You must be signed in to change notification settings - Fork 0
feat: search, load and validates manifests #1
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
Changes from 22 commits
60320ba
c67c661
22ae256
6cb437f
00825de
442bcc6
e19f6f1
b6ecc27
1406b18
8aba035
70b2c5e
c8b7a39
1181871
31f1d3d
df49dfc
f4169b9
aade454
e0bd4a4
2e572fb
3c5de1c
7eff973
b7c7c66
c7196e9
7539db7
cd22c1f
27d8645
373f3c1
1414990
2ce5b21
17162f4
c21ef34
77f5771
40ef8e8
0a25844
71c4496
4696dcc
089109d
09faa6a
bd7a707
3ea70ce
3efb7d2
a421818
e88de2d
acac421
0262cb2
7445fcd
5fca0c5
fb6500b
d7fa155
43e8a71
46d2119
164c68b
15550ad
4ecb555
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,16 @@ package main | |
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "slices" | ||
| "time" | ||
|
|
||
| "github.com/jessevdk/go-flags" | ||
|
|
||
| "github.com/canonical/chisel/internal/archive" | ||
| "github.com/canonical/chisel/internal/cache" | ||
| "github.com/canonical/chisel/internal/manifestutil" | ||
| "github.com/canonical/chisel/internal/setup" | ||
| "github.com/canonical/chisel/internal/slicer" | ||
| ) | ||
|
|
@@ -73,6 +76,44 @@ func (cmd *cmdCut) Execute(args []string) error { | |
| } | ||
| } | ||
|
|
||
| // Get targetDir path | ||
| // Note: This is already done in `slicer.Run`. Extract it and avoid doing it twice? | ||
| targetDir := filepath.Clean(cmd.RootDir) | ||
| if !filepath.IsAbs(targetDir) { | ||
| dir, err := os.Getwd() | ||
| if err != nil { | ||
| return fmt.Errorf("cannot obtain current directory: %w", err) | ||
| } | ||
| targetDir = filepath.Join(dir, targetDir) | ||
| } | ||
|
|
||
| targetDirEmpty, err := dirIsEmpty(targetDir) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to break compatibility will all previous versions of Chisel. I also think it is not ideal as many of our users expect to do: and the target directory might not be empty. What about checking if the manifest exists directly? If it exists then we are recutting, if it doesn't then we cut normally overriding everything as usual. |
||
| if err != nil { | ||
| return err | ||
| } | ||
|
upils marked this conversation as resolved.
|
||
| if !targetDirEmpty { | ||
| manifest, err := manifestutil.FromDir(release, targetDir) | ||
| if err != nil { | ||
| // TODO: When enabling the feature, error out. | ||
| logf("Warning: %v", err) | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| err = manifestutil.VerifyDir(manifest, targetDir) | ||
| if err != nil { | ||
| // TODO: When enabling the feature, error out. | ||
| logf("Warning: %v", err) | ||
| } | ||
| // Merge the slice keys used to build the existing rootfs with the ones | ||
| // explicitly requested slices. | ||
| // Previous slice keys contain both explicitly requested slices and slices | ||
| // resolved following essentials. | ||
| for _, s := range manifestutil.SliceKeys(manifest) { | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| if !slices.Contains(sliceKeys, s) { | ||
| sliceKeys = append(sliceKeys, s) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| selection, err := setup.Select(release, sliceKeys, cmd.Arch) | ||
| if err != nil { | ||
| return err | ||
|
|
@@ -128,3 +169,12 @@ func (cmd *cmdCut) Execute(args []string) error { | |
| }) | ||
| return err | ||
| } | ||
|
|
||
| // dirIsEmpty checks whether the given directory is empty. | ||
| func dirIsEmpty(targetDir string) (bool, error) { | ||
| entries, err := os.ReadDir(targetDir) | ||
| if err != nil { | ||
| return false, fmt.Errorf("cannot read root directory %q: %v", targetDir, err) | ||
| } | ||
| return len(entries) == 0, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| package manifestutil | ||
|
|
||
| import ( | ||
| "crypto/sha256" | ||
| "fmt" | ||
| "io" | ||
| "io/fs" | ||
| "os" | ||
| "path" | ||
| "path/filepath" | ||
| "slices" | ||
| "sort" | ||
|
|
@@ -14,6 +17,7 @@ import ( | |
| "github.com/canonical/chisel/internal/setup" | ||
| "github.com/canonical/chisel/public/jsonwall" | ||
| "github.com/canonical/chisel/public/manifest" | ||
| "github.com/klauspost/compress/zstd" | ||
| ) | ||
|
|
||
| const DefaultFilename = "manifest.wall" | ||
|
|
@@ -34,6 +38,26 @@ func FindPaths(slices []*setup.Slice) map[string][]*setup.Slice { | |
| return manifestSlices | ||
| } | ||
|
|
||
| // FindPathsInRelease finds all the paths marked with "generate:manifest" | ||
| // for the given release. | ||
| func FindPathsInRelease(r *setup.Release) []string { | ||
| allSlices := []*setup.Slice{} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use the
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate? Do you mean use |
||
| for _, pkg := range r.Packages { | ||
| for _, slice := range pkg.Slices { | ||
| allSlices = append(allSlices, slice) | ||
| } | ||
| } | ||
|
|
||
| manifestMap := FindPaths(allSlices) | ||
|
upils marked this conversation as resolved.
Outdated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit unfortunate that we have to iterate over all slices to collect them just to iterate over them again. Because the real business logic takes a path + info and returns the manifest path by validating then appending a filename, what about extracting only that bit into an auxiliary function instead? Then have both functions call the auxiliary one. Thoughts?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After implementing a naive version of
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reworked in 4ecb555 |
||
|
|
||
| paths := make([]string, 0, len(manifestMap)) | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| for path := range manifestMap { | ||
| paths = append(paths, path) | ||
| } | ||
|
upils marked this conversation as resolved.
Outdated
|
||
|
|
||
|
upils marked this conversation as resolved.
Outdated
|
||
| return paths | ||
| } | ||
|
|
||
| type WriteOptions struct { | ||
| PackageInfo []*archive.PackageInfo | ||
| Selection []*setup.Slice | ||
|
|
@@ -340,3 +364,123 @@ func Validate(mfest *manifest.Manifest) (err error) { | |
| } | ||
| return nil | ||
| } | ||
|
|
||
| // FromDir extracts, validates and returns the manifest from a targetDir | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| func FromDir(release *setup.Release, targetDir string) (*manifest.Manifest, error) { | ||
| manifestPaths := FindPathsInRelease(release) | ||
| if len(manifestPaths) == 0 { | ||
| // No manifest in the release means it cannot produce a rootfs that can | ||
| // be recut. Treat this case as cutting a new rootfs. | ||
| return nil, fmt.Errorf("no manifest generated for this release") | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // Select the first manifest of the list as the reference one for now. | ||
| manifestPath := manifestPaths[0] | ||
| manifest, err := load(path.Join(targetDir, manifestPath)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("cannot read manifest %q from the root directory: %v", manifestPath, err) | ||
| } | ||
|
|
||
| err = checkIdentical(targetDir, manifestPaths) | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return manifest, nil | ||
| } | ||
|
|
||
| // load reads, validates and returns a manifest. | ||
| func load(manifestPath string) (*manifest.Manifest, error) { | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| f, err := os.Open(manifestPath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| r, err := zstd.NewReader(f) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer r.Close() | ||
|
|
||
| mfest, err := manifest.Read(r) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| err = Validate(mfest) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return mfest, nil | ||
| } | ||
|
|
||
| // checkIdentical checks all manifests in the passed list are identical | ||
| // to the reference one. | ||
| func checkIdentical(targetDir string, manifestPaths []string) error { | ||
| if len(manifestPaths) == 0 { | ||
| return nil | ||
| } | ||
| refRelPath := manifestPaths[0] | ||
|
|
||
| ref := path.Join(targetDir, refRelPath) | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| refHash, err := contentHash(ref) | ||
| if err != nil { | ||
| return fmt.Errorf("internal error: cannot compute hash for %q: %w", refRelPath, err) | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| refInfo, err := os.Stat(ref) | ||
| if err != nil { | ||
| return fmt.Errorf("internal error: cannot get file info for %q: %w", refRelPath, err) | ||
| } | ||
| refMode := refInfo.Mode() | ||
| for _, manifestRelPath := range manifestPaths { | ||
| manifestPath := path.Join(targetDir, manifestRelPath) | ||
| manifestInfo, err := os.Stat(manifestPath) | ||
| if err != nil { | ||
| return fmt.Errorf("internal error: cannot get file info for %q: %w", manifestRelPath, err) | ||
| } | ||
| manifestMode := manifestInfo.Mode() | ||
| if manifestMode != refMode { | ||
| return fmt.Errorf("invalid manifest: permissions on %s (%s) different from %s (%s)", manifestRelPath, manifestMode, refRelPath, refMode) | ||
| } | ||
|
|
||
| manifestHash, err := contentHash(manifestPath) | ||
| if err != nil { | ||
| return fmt.Errorf("internal error: cannot compute hash for %q: %w", manifestRelPath, err) | ||
| } | ||
| if !slices.Equal(manifestHash, refHash) { | ||
| return fmt.Errorf("invalid manifest: %s is inconsistent with %s", manifestRelPath, refRelPath) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func contentHash(path string) ([]byte, error) { | ||
| f, err := os.Open(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| h := sha256.New() | ||
| if _, err := io.Copy(h, f); err != nil { | ||
| return nil, err | ||
| } | ||
| return h.Sum(nil), nil | ||
| } | ||
|
|
||
| // SliceKeys returns setup.SliceKeys from a manifest. | ||
| func SliceKeys(mfest *manifest.Manifest) []setup.SliceKey { | ||
|
upils marked this conversation as resolved.
Outdated
|
||
| sliceKeys := []setup.SliceKey{} | ||
| mfest.IterateSlices("", func(slice *manifest.Slice) error { | ||
| sk, err := setup.ParseSliceKey(slice.Name) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| sliceKeys = append(sliceKeys, sk) | ||
| return nil | ||
| }) | ||
| return sliceKeys | ||
| } | ||
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 think this feature should be somewhere in the internal modules, not here necessarily. Maybe the functions can be internal and can be called and coordinated from here but the least amount of logic we have in
cmd_cut.gothe better.Regarding the comment, I think it is good to extract it as long as inside cut we check that the path is absolute and return an error if it isn't. This is to maintain boundaries between components and not make them tightly coupled, but instead to document assumptions across boundaries as errors.