Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new OCI image “resolver” that can read container filesystem contents directly from layer tars (without full extraction) and wires in an initial pkg_diff CLI command that uses Syft catalogers to enumerate packages from an image.
Changes:
- Introduce
OCIResolver(Syftfile.Resolverimplementation) that builds an in-memory path index from OCI layer tar streams, including basic whiteout handling and symlink resolution. - Add a new
pkg_diffCLI command (currently prints packages from one image) and register it in the main CLI. - Add resolver tests plus a local test image Containerfile and Makefile target to build it; update Go deps to include Syft/Stereoscope/Doublestar.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/oci_resolver.go |
New OCIResolver implementation for indexing and resolving file locations/contents/metadata from OCI layers. |
pkg/oci_resolver_test.go |
Adds resolver tests (remote image fetch + local-image whiteout checks) and a TestMain that reexecs into a user namespace. |
pkg/TestImage.containerfile |
Defines a local image used for whiteout behavior testing. |
cmd/skiff/pkg-diff.go |
Adds pkg_diff command using Syft’s RedHat DB cataloger (currently only catalogs one image). |
cmd/skiff/main.go |
Registers the new pkgDiffCommand. |
Makefile |
Adds test-image target and makes unit-tests depend on it; expands tests to ./.... |
go.mod |
Bumps Go version and adds Syft/Stereoscope/Doublestar + indirect dependency updates. |
go.sum |
Updates checksums for the new/updated module graph. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var pkgDiffCommand = cli.Command{ | ||
| Name: "pkg_diff", | ||
| Usage: "Diff the packages of two container images", | ||
| Arguments: []cli.Argument{ | ||
| &cli.StringArg{Name: "oldImage", UsageText: "Original Container Image ref"}, | ||
| &cli.StringArg{Name: "newImage", UsageText: "New Container Image ref"}, | ||
| }, |
There was a problem hiding this comment.
Command/arg naming is inconsistent with existing commands (layers, top) which use lowercase names and simple argument keys (e.g., url, image). Consider renaming pkg_diff to pkg-diff (or similar) and using lowercase arg names (e.g., old, new) for consistency.
| fmt.Printf("Found %d packages in %s\n", len(pkgs), uri) | ||
| for _, p := range pkgs { | ||
| fmt.Printf("- %s %s\n", p.Name, p.Version) | ||
| } |
There was a problem hiding this comment.
Other commands write output via the CLI’s writer (c.Writer) rather than directly to stdout. Using fmt.Printf here makes output harder to test and inconsistent with the rest of the CLI. Prefer writing to the command’s writer (and consider returning structured data from getPkgMetadataFromImg).
| func TestOCIResolver(t *testing.T) { | ||
| // Use a known stable image digest | ||
| // registry.suse.com/bci/bci-busybox@sha256:7c7d8d3ff8813e62ec46501b458c47cabf818867afd8ade209f729e181a3f7ea | ||
| uri := "docker://registry.suse.com/bci/bci-busybox@sha256:7c7d8d3ff8813e62ec46501b458c47cabf818867afd8ade209f729e181a3f7ea" | ||
|
|
||
| ctx := context.Background() | ||
| sysCtx := &types.SystemContext{} | ||
|
|
||
| img, _, err := ImageAndLayersFromURI(ctx, sysCtx, uri) | ||
| if err != nil { | ||
| t.Logf("Skipping test: unable to fetch image (network/auth issue?): %v", err) | ||
| t.SkipNow() | ||
| } |
There was a problem hiding this comment.
This test performs a live pull from an external registry. Even with the skip-on-error behavior, it introduces non-determinism and can hang/flake in CI due to network/auth/rate limits. Consider marking it as an integration test (build tag) or skipping when testing.Short() is set, and prefer a local fixture image for unit tests.
pkg/oci_resolver.go
Outdated
| if strings.HasPrefix(file, ".wh.") { | ||
| whiteoutName := strings.TrimPrefix(file, ".wh.") | ||
| if whiteoutName == ".wh.opq" { | ||
| // Opaque whiteout: remove all siblings in this directory | ||
| // We need to find all entries in fsIndex that start with dir | ||
| for p := range r.fsIndex { |
There was a problem hiding this comment.
OCI opaque whiteout handling looks incorrect: OCI uses the special filename .wh..wh..opq to mark a directory as opaque, but this code checks whiteoutName == ".wh.opq" after trimming .wh., which won’t match. As a result, opaque directories won’t be applied correctly. Update the condition to detect the OCI opaque marker and apply the opaque semantics accordingly.
| } else { | ||
| // Regular whiteout: remove specific file | ||
| target := filepath.Join(dir, whiteoutName) | ||
| delete(r.fsIndex, target) | ||
| } |
There was a problem hiding this comment.
Regular whiteouts can delete directories as well as files. Deleting only the exact target path leaves any previously-indexed children (e.g., /dir/file) still present, which is incorrect for a directory whiteout. Consider deleting all indexed paths with target as a prefix (and target itself) when the whiteout refers to a directory.
| Action: func(ctx context.Context, c *cli.Command) error { | ||
| oldImage := c.StringArg("oldImage") | ||
| if oldImage == "" { | ||
| return fmt.Errorf("old image URL is required") | ||
| } | ||
| newImage := c.StringArg("newImage") | ||
| if newImage == "" { | ||
| return fmt.Errorf("newImage URL is required") | ||
| } | ||
|
|
||
| sysCtx := types.SystemContext{} | ||
| return getPkgMetadataFromImg(oldImage, ctx, &sysCtx) | ||
| }, |
There was a problem hiding this comment.
This command collects newImage but never uses it, so pkg_diff doesn’t actually perform a diff despite its name/usage. Either implement the comparison between oldImage and newImage here or remove the unused argument until the diff logic is available.
| @@ -0,0 +1,9 @@ | |||
| FROM registry.opensuse.org/opensuse/leap:16.0 | |||
|
|
|||
| RUN zypper -n in vim-small; zypper -n clean | |||
There was a problem hiding this comment.
Using RUN cmd1; cmd2 causes the layer’s exit status to be that of cmd2, so a failed zypper -n in vim-small can be masked if zypper -n clean succeeds. Use && (or set -e) so the build reliably fails when package installation fails.
| test-image: | ||
| pushd pkg; buildah bud --layers -t skiff-test-image -f TestImage.containerfile .; popd | ||
|
|
||
| # Run unit tests | ||
| unit-tests: | ||
| go test -v ./cmd/skiff/... | ||
| unit-tests: test-image | ||
| go test -v ./... |
There was a problem hiding this comment.
This unit-tests target now requires buildah (via test-image) and runs go test ./..., which pulls in network/local-image dependent tests. That’s more of an integration test workflow and is likely to break in CI or on developer machines without buildah. Consider splitting into separate unit-tests (pure go test fast tests) and integration-tests (buildah + image-based tests).
pkg/oci_resolver_test.go
Outdated
| // Uses localhost/leap-test-image:latest which must be built locally | ||
| // Contains: | ||
| // - /src/data/foo/bar (created) | ||
| // - /src/data/foo/bar (deleted) -> file whiteout | ||
| // - /src/data/foo (deleted) -> directory whiteout | ||
| // - /src/data (remains) | ||
| uri := "localhost/skiff-test-image:latest" |
There was a problem hiding this comment.
The comment says this test uses localhost/leap-test-image:latest, but the code uses localhost/skiff-test-image:latest. Update the comment to match the actual image name to avoid confusion when running/maintaining the test.
| func TestOCIResolver_Whiteouts(t *testing.T) { | ||
| // Uses localhost/leap-test-image:latest which must be built locally | ||
| // Contains: | ||
| // - /src/data/foo/bar (created) | ||
| // - /src/data/foo/bar (deleted) -> file whiteout | ||
| // - /src/data/foo (deleted) -> directory whiteout | ||
| // - /src/data (remains) | ||
| uri := "localhost/skiff-test-image:latest" | ||
|
|
||
| ctx := context.Background() | ||
| sysCtx := &types.SystemContext{} | ||
|
|
||
| img, _, err := ImageAndLayersFromURI(ctx, sysCtx, uri) | ||
| if err != nil { | ||
| t.Fatalf("Failed to load test image %s: %v. Ensure image is built locally.", uri, err) | ||
| } |
There was a problem hiding this comment.
This test hard-requires a locally built image and fails the suite if it’s missing. That makes go test ./... brittle outside of the Makefile. Consider skipping (with a clear message) when the image isn’t present, or gate it behind an integration build tag so unit tests remain self-contained.
97b6b2e to
053a30f
Compare
053a30f to
02039a6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var errs error | ||
| for _, c := range t.closers { | ||
| if err := c.Close(); err != nil { | ||
| if errs == nil { | ||
| errs = err | ||
| } else { | ||
| errs = fmt.Errorf("%v; %v", errs, err) | ||
| } | ||
| } | ||
| } | ||
| return errs |
There was a problem hiding this comment.
The error aggregation in the Close method can lose error information. When multiple closers fail, only the first and last errors are preserved. Consider using a more robust error aggregation approach like errors.Join (available since Go 1.20) or a similar multi-error pattern to preserve all error details.
| var errs error | |
| for _, c := range t.closers { | |
| if err := c.Close(); err != nil { | |
| if errs == nil { | |
| errs = err | |
| } else { | |
| errs = fmt.Errorf("%v; %v", errs, err) | |
| } | |
| } | |
| } | |
| return errs | |
| var allErrs []error | |
| for _, c := range t.closers { | |
| if err := c.Close(); err != nil { | |
| allErrs = append(allErrs, err) | |
| } | |
| } | |
| if len(allErrs) == 0 { | |
| return nil | |
| } | |
| return errors.Join(allErrs...) |
| ) | ||
|
|
||
| var pkgDiffCommand = cli.Command{ | ||
| Name: "pkg_diff", |
There was a problem hiding this comment.
The command name uses underscore ("pkg_diff") which is inconsistent with the other command "topCommand" and typical CLI conventions that use hyphens. Consider renaming to "pkg-diff" to match CLI naming conventions.
| Name: "pkg_diff", | |
| Name: "pkg-diff", |
| resolver, err := NewOCIResolver(ctx, img, sysCtx) | ||
| if err != nil { | ||
| t.Fatalf("Failed to create OCIResolver: %v", err) | ||
| } | ||
| defer resolver.Close() |
There was a problem hiding this comment.
Resource leak: The resolver is not closed in the test. Add defer resolver.Close() after line 324 to ensure proper cleanup of resources even if the test fails.
No description provided.