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 ef929cd
🔍 Latest deploy log https://app.netlify.com/projects/urunc/deploys/6a131391dd67d60008b0a5e9
😎 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
Add UruncRootfsView and rootfs_view.enabled to urunc.toml so the shim
can opt in to per-container read-only rootfs views on block/devmapper
rootfs.

Signed-off-by: sidneychang <2190206983@qq.com>
Introduce RootfsViewAccessor to create read-only containerd view snapshots
with leases and persist state in bundle config.json under
com.urunc.internal.rootfs.view. Bind kernel, initrd, and urunc.json from
the view in block rootfs after prepareRoot(), with legacy extract fallback.

Signed-off-by: sidneychang <2190206983@qq.com>
@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch from 731f713 to 1cbdf80 Compare May 24, 2026 13:48
Prepare a read-only rootfs view after guest rootfs choice on Create and
tear down containerd view resources on task Delete when the shim process
is still alive.

Signed-off-by: sidneychang <2190206983@qq.com>
Wrap the runc shim manager so containerd shim delete subcommand removes
rootfs view resources before the bundle is torn down.

Signed-off-by: sidneychang <2190206983@qq.com>
@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch from 1cbdf80 to 8b4d20d Compare May 24, 2026 14:57
Defer writing com.urunc.internal.rootfs.params and .view until after
guest rootfs choice and optional view prepare, then patch both via a
single PatchConfigJSON call instead of separate config.json updates.

Signed-off-by: sidneychang <2190206983@qq.com>
@sidneychang sidneychang force-pushed the feat/per-container-view-reliable branch 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.

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.

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.

}
}

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.

}

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.


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


// 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.


// 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


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.

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.

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