Skip to content

feat(view): add per-container snapshot views for boot artifacts#639

Open
sidneychang wants to merge 5 commits into
urunc-dev:mainfrom
sidneychang:feat/per-container-view-reliable
Open

feat(view): add per-container snapshot views for boot artifacts#639
sidneychang wants to merge 5 commits into
urunc-dev:mainfrom
sidneychang:feat/per-container-view-reliable

Conversation

@sidneychang
Copy link
Copy Markdown
Contributor

@sidneychang sidneychang commented May 7, 2026

Description

Add a shim-managed per-container snapshot-view path for block-backed rootfs setups so urunc can reuse a prepared read-only view of the container image when retrieving boot artifacts.

The shim now wraps task Create/Delete to prepare a snapshot view ahead of container startup, persist the view metadata and mounts into the bundle, and clean up the containerd view and lease during deletion. On the runtime side, unikontainers consume that shim-written state to bind the unikernel binary, initrd, and urunc.json from the prepared view into the monitor rootfs, while keeping the legacy extraction path as a fallback when no per-container view is available.

The PR also documents the new com.urunc.unikernel.snapshotView runtime annotation and its interaction with mountRootfs for supported block snapshotters.

Related issues

How was this tested?

LLM usage

Codex

Checklist

  • I have read the contribution guide.
  • The linter passes locally (make lint).
  • The e2e tests of at least one tool pass locally (make test_ctr, make test_nerdctl, make test_docker, make test_crictl).
  • If LLMs were used: I have read the llm policy.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 7, 2026

Deploy Preview for urunc ready!

Name Link
🔨 Latest commit f281afe
🔍 Latest deploy log https://app.netlify.com/projects/urunc/deploys/6a1db582818bc8000816cff9
😎 Deploy Preview https://deploy-preview-639--urunc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch from b529bb2 to af30554 Compare May 7, 2026 14:20
@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch 6 times, most recently from 549974e to 731f713 Compare May 23, 2026 16:15
@sidneychang sidneychang marked this pull request as ready for review May 23, 2026 16:16
@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch 3 times, most recently from 8b4d20d to ef929cd Compare May 24, 2026 15:04
Copy link
Copy Markdown
Contributor

@cmainas cmainas left a comment

Choose a reason for hiding this comment

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

Thank you @sidneychang for this PR. I have added several comments regarding the urunc side and let's go together over the shim changes in today's sync.

func (u *Unikontainer) Exec(metrics m.Writer) error {
metrics.Capture(m.TS15)

// Reload annotations written by the shim after Create.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need to reload the spec? The urunc process starts after shim has done its work.

Copy link
Copy Markdown
Contributor Author

@sidneychang sidneychang May 28, 2026

Choose a reason for hiding this comment

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

I further validated this with a real container run after removing the runtime reload and rebuilding. The result shows that the reload is required rather than redundant.

The key reason is that the shim patches the bundle config.json only after the inner Create returns:

func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (*taskAPI.CreateTaskResponse, error) {
    resp, err := s.TaskService.Create(ctx, r)
    if err != nil {
        return resp, err
    }

    if err := containerdShim.PatchConfigJSON(r.Bundle, shimAnnotations); err != nil {
        return nil, err
    }

    return resp, nil
}

This means runtime/reexec has already loaded and kept an in-memory spec, namely u.Spec, before the shim persists com.urunc.internal.rootfs.params into the bundle config.json. The persisted config.json is updated successfully, but u.Spec is not automatically refreshed.

I also added a diagnostic log at the exact place where Exec() consumes the annotation:

if rootfsParamsJSON := u.Spec.Annotations[annotRootfsParams]; rootfsParamsJSON != "" {
    uniklog.WithField("len", len(rootfsParamsJSON)).
        Debugf("runtime: found %s in in-memory spec", annotRootfsParams)
} else {
    uniklog.WithField("present", false).
        Warnf("runtime: missing %s in in-memory spec", annotRootfsParams)
}

The runtime logs confirm this ordering:

urunc(shim): bundle spec missing com.urunc.internal.rootfs.params before persist

runtime: missing com.urunc.internal.rootfs.params in in-memory spec

I also checked the bundle config.json for the same container and confirmed that com.urunc.internal.rootfs.params was present there. Therefore, the patch itself succeeded; the issue is that runtime/reexec was still using a stale in-memory spec.

So the runtime reload is needed here to make runtime/reexec reload the patched config.json, ensuring that Exec() can see com.urunc.internal.rootfs.params.

Comment thread pkg/containerd-shim/guest_rootfs.go Outdated
return types.RootfsParams{}, "", err
}

encoded, err := json.Marshal(rootfsParams)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we refactor this function like that. There is no reason to create the json inside here. We can do it later. In that way we do not need to return an extra value.

Comment thread pkg/unikontainers/unikontainers.go Outdated
if err := b.rebindRootfsViewBootAfterPrepareRoot(); err != nil {
return fmt.Errorf("boot artifact setup after prepareRoot failed: %w", err)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be moved in the respective setup step for the block based rootfs.

Comment thread pkg/unikontainers/rootfs_view_boot.go Outdated
}
}

if uerr := mount.Unmount(mountpoint, 0); uerr != nil && !os.IsNotExist(uerr) && uerr != unix.EINVAL {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please simplify this line.

Comment thread pkg/unikontainers/rootfs_view_boot.go Outdated
}

func rootfsViewRelPath(p string) string {
return strings.TrimPrefix(filepath.Clean(p), "/")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use full paths for bind mounts.

Comment thread pkg/unikontainers/rootfs_view_boot.go Outdated

// probeRootfsViewBootArtifacts keeps the legacy extract fallback available:
// preSetup still has mountedPath, but does not keep boot bind mounts.
func probeRootfsViewBootArtifacts(rootfsViewState *rootfsViewState, unikernelPath, initrdPath, uruncJSON string) (useView bool, err error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I do not really understand the purpose of this function. Can you help me?

Comment thread pkg/unikontainers/block.go Outdated

// rebindRootfsViewBootAfterPrepareRoot binds boot artifacts into the rootfs
// tree that qemu sees after chroot.
func (b blockRootfs) rebindRootfsViewBootAfterPrepareRoot() error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

THe logic of this fucntion should be part of one of the setup steps for block based rootfs.

Comment thread pkg/unikontainers/rootfs_view_boot.go Outdated

// prepareRootfsViewBootBinds runs after prepareRoot, so the binds live in the
// monitor mount namespace and are released with it.
func prepareRootfsViewBootBinds(rootfsViewState *rootfsViewState, monRootfs, unikernelPath, initrdPath, uruncJSON string) (useView bool, err error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do not use booleans as a return value for the success or failure of a function. Use errors instead

Comment thread pkg/unikontainers/rootfs_view_boot.go Outdated

bindErr := bindBootArtifactsFromView(mountpoint, monRootfs, unikernelPath, initrdPath, uruncJSON, &bindTargets)

uerr := mount.Unmount(mountpoint, 0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can not unmount the source of the files we later bind mount. This may lead to corrupted data for the boot files.

Comment thread pkg/unikontainers/rootfs_view_boot.go Outdated
return files
}

func bindBootArtifactsFromView(viewRoot, monRootfs, unikernelPath, initrdPath, uruncJSON string, bindTargets *[]string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function could simply be part of the caller and hence avoid the bindTarget hack.

@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch 8 times, most recently from 23e52a1 to f281afe Compare June 1, 2026 16:38
Add a rootfs_view.enabled configuration knob and document it.
The flag keeps rootfs view preparation disabled unless deployments
explicitly opt in.

Signed-off-by: sidneychang <2190206983@qq.com>
Add bundle state for shim-prepared rootfs views and containerd helpers
to prepare and clean those views. Keep the accessor internal so shim
code only passes a session and persisted cleanup state.

Signed-off-by: sidneychang <2190206983@qq.com>
Load shim-prepared rootfs view state for block rootfs setup. Probe the
view before unmounting the container rootfs, then bind the boot artifacts
after prepareRoot in the block postSetup step.

Signed-off-by: sidneychang <2190206983@qq.com>
Choose guest rootfs parameters after inner task creation and persist them
for runtime Exec. When enabled, prepare a rootfs view during Create, roll
it back on persistence failures, and clean it during Delete.

Signed-off-by: sidneychang <2190206983@qq.com>
Wire a custom shim manager Stop path that reads persisted rootfs view
state from the bundle and removes the view snapshot and lease. This covers
cleanup paths where task Delete is not the final teardown hook.

Signed-off-by: sidneychang <2190206983@qq.com>
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.

Use a RO snapshot of container to retrieve unikernel binary

2 participants