Skip to content

Conversation

@upils
Copy link
Owner

@upils upils commented Nov 26, 2025

  • Have you signed the CLA?

Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
@upils upils marked this pull request as ready for review November 27, 2025 10:45
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
@upils upils requested a review from Copilot December 2, 2025 10:54
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements functionality to search, load, and validate manifests from a rootfs directory. The main purpose is to enable chisel to detect and reuse existing rootfs installations by reading and validating their manifests.

Key changes:

  • Added manifest verification logic to validate rootfs contents against manifest entries
  • Implemented manifest loading and consistency checking across multiple manifest files
  • Integrated rootfs detection into the cut command to merge existing slice keys with newly requested ones

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
internal/manifestutil/verify.go New file implementing rootfs validation against manifest entries, including path grouping, hardlink verification, and file integrity checks
internal/manifestutil/manifestutil.go Extended with functions to find manifests in releases, load and validate manifest files, check consistency between manifests, and extract reports from rootfs
cmd/chisel/cmd_cut.go Modified to detect pre-existing rootfs and merge slice keys from existing manifests with requested slices

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Paul Mars <paul.mars@canonical.com>
@upils upils requested a review from Copilot December 2, 2025 13:13
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

upils added 5 commits December 2, 2025 15:46
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Copy link

@TheSignPainter98 TheSignPainter98 left a comment

Choose a reason for hiding this comment

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

Here are a few things to watch out for :)

upils added 5 commits December 8, 2025 08:21
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Copy link

@TheSignPainter98 TheSignPainter98 left a comment

Choose a reason for hiding this comment

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

This is looking much better now, thanks for the changes! Just a few minor things then I think this looks good

Signed-off-by: Paul Mars <paul.mars@canonical.com>
if mfest != nil {
err = manifestutil.CheckDir(mfest, cmd.RootDir)
if err != nil {
return err
Copy link
Owner Author

Choose a reason for hiding this comment

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

[Note to reviewer]: At this point a valid manifest was extracted from the root dir but the dir is invalid (or we failed to validate it) so we return the error.

}
return nil, fmt.Errorf("cannot read manifest %q from the root directory: %v", manifestPath, err)
}
err = Validate(mfest)
Copy link
Owner Author

Choose a reason for hiding this comment

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

[Note to reviewer]: The manifest validation was moved out of the load function to be called here instead.

@upils upils requested a review from letFunny January 16, 2026 08:01

manifestPaths := manifestutil.FindPathsInRelease(release)
if len(manifestPaths) >= 0 {
mfest, mfestPath, err := manifestutil.FromDir(manifestPaths, cmd.RootDir)
Copy link
Owner Author

Choose a reason for hiding this comment

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

[Note to reviewer]: I am not very happy about having to also return the manifest path so I am open to suggestions to avoid that.

Choose a reason for hiding this comment

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

I am not very happy about having to also return the manifest path so I am open to suggestions to avoid that.

I have the same suggestion as in all the comments above :)

return nil
}

func (manifest *Manifest) Schema() string {
Copy link
Owner Author

Choose a reason for hiding this comment

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

[Note to reviewer]: This is needed to compare manifests by schema versions but exposing it while the format of this value is not precisely defined might cause issues in the future. Should we hold on exposing and for now internally rely on the assumption that all valid manifests are necessarily using the 1.0 schema?

}
defer r.Close()

mfest, err = manifest.Read(r)
Copy link
Owner Author

Choose a reason for hiding this comment

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

[Note to reviewer]: This call can return 2 kinds of errors:

  • Errors actually about reading from the reader, such as the OS unable to read a piece of the file from disk.
  • Unmarshalling and invalid schema errors. So "business logic" errors.

Ideally I would like to differentiate these errors: return in the first case and continue in the second. But I cannot see a proper solution that would not require changing the API of manifest.Read to do that.

So my approach here is to continue, because in that case this file is not a good fit to get the manifest and the main goal is to get one valid manifest, not identify bad candidates.

Choose a reason for hiding this comment

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

invalid schema errors

As I said on MM, I think we named a named error for this one in particular if we want to ignore them. Old versions of Chisel will not parse correctly new versions of the manifest, so it is useful to know if the error is related to schema version and skip. If it is not then fail.

Copy link

@letFunny letFunny left a comment

Choose a reason for hiding this comment

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

Thanks for this Paul, I wrote a lot of comments mainly minor, which is always a good sign :). This is looking much better already. I think we are at a stage where you need to write the tests so that we know how the feature looks like, how it is called and what is implemented.

return nil
}

// CompareSchemas compare two manifest schema strings

Choose a reason for hiding this comment

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

How does it compare them? What is the return value? This looks a bit artificial at the moment, it is saying:

  • If you give me two manifests with the same version and it is manifest.Schema then 0.
  • Else -1.

Which is a little bit weird for a comparator, right? Normally, either -1, 0 and 1 are returned based on a total order or true, false is returned if the order is partial.

Copy link
Owner Author

Choose a reason for hiding this comment

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

It is a little weird on purpose. My goal was to have a signature consistent with other comparison functions in the project and avoid changing it when actually supporting multiple schema versions. The end goal is to provide a total order, but since the schema format is not yet formally defined, I did not want to implement a complex comparison making assumptions (that could be wrong in the future). When we actually support multiple schema, the format will be properly defined and we should naturally change this implementation because it will look obviously wrong.

For now it indeed feels a bit artificial but that may be a good thing(?).

allSlices = append(allSlices, slice)
}
}
manifestMap := FindPaths(allSlices)

Choose a reason for hiding this comment

The 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?

Copy link
Owner Author

@upils upils Jan 26, 2026

Choose a reason for hiding this comment

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

After implementing a naive version of FindPathsInRelease I thought the same. I did the extraction and ended up with something longer and harder to understand. So I opted for simplicity at this time but now I think I may have underestimated the problem when in the future the number of slices is more than 50x what we have today. I will rework that.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Reworked in 4ecb555

// the manifest.
// This function works under the assumption the manifest is valid.
// Files not managed by chisel are ignored.
func checkDir(mfest *manifest.Manifest, rootDir string) error {

Choose a reason for hiding this comment

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

I think checkRootDir might be a more appropriate name as we expect the directory to be the root dir of the installation that the manifest describes.

I am still not happy with having it here, but let's postpone that discussion. It is not blocking and it is not major (for now if we don't introduce a chisel validate).

Copy link
Owner Author

Choose a reason for hiding this comment

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

I think checkRootDir might be a more appropriate name as we expect the directory to be the root dir of the installation that the manifest describ

Good point, and this is in line with the rootDir argument. Fixed in 15550ad.

I am still not happy with having it here, but let's postpone that discussion. It is not blocking and it is not major (for now if we don't introduce a chisel validate).

I agree.

inode := stat.Ino

if path.Inode == 0 {
// This path must not be linked to any other

Choose a reason for hiding this comment

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

Make sure all comments are punctuated.

Copy link
Owner Author

Choose a reason for hiding this comment

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

See 15550ad

Comment on lines +571 to +578
targetDir = filepath.Clean(targetDir)
if !filepath.IsAbs(targetDir) {
dir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("cannot obtain current directory: %w", err)
}
targetDir = filepath.Join(dir, targetDir)
}

Choose a reason for hiding this comment

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

You are doing this every time for an internal function. If the function is internal then you can expect an absolute path and you can document it or check with IsAbs and return an error, either one of them work. If the function is public then you have to choice to make it absolute or fail. In that case I prefer the former to follow convention.

defer r.Close()
mfest, err := manifest.Read(r)
if err != nil {
return nil

Choose a reason for hiding this comment

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

See my other other comment. Here it is even more important. And manifest.Validate has to be an error.

return nil
}

if selected == nil || manifestutil.CompareSchemas(mfest.Schema(), selected.Schema()) > 0 {

Choose a reason for hiding this comment

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

Compare is never > 0. In my opinion, in terms of logic the code should check all manifests of the same version have to be equal. You don't have to use the content has because you have both manifests in memory and you need to return selected anyways, so you can check whether they match or not (in a separate function please).

if !ok {
schemaManifest[mfest.Schema()] = manifestHash{mfestPath, mfestHash}
} else if refMfest.hash != mfestHash {
return fmt.Errorf("inconsistent manifests: %q and %q", refMfest.path, mfestPath)

Choose a reason for hiding this comment

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

This will likely change, but this error message doesn't state that the contents of the manifests should be equal and they are not.

@upils
Copy link
Owner Author

upils commented Feb 4, 2026

Closing in favor of canonical#264

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants