Skip to content

Commit 789063e

Browse files
committed
feat(unikontainers): consume per-container snapshot views
Load shim-written snapshot view state from the bundle and mount the read-only view when extracting boot artifacts for block-backed guests. Bind kernel, initrd, and urunc.json from the prepared view into the monitor rootfs, while keeping the existing block extraction path as the fallback when no per-container view is available. Add the snapshotView annotation to unikernel config parsing and log the selected rootfs configuration for debugging. Signed-off-by: sidneychang <2190206983@qq.com>
1 parent 8d524e7 commit 789063e

4 files changed

Lines changed: 287 additions & 11 deletions

File tree

pkg/unikontainers/block.go

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package unikontainers
1616

1717
import (
1818
"bufio"
19+
"context"
1920
"errors"
2021
"fmt"
2122
"os"
@@ -120,6 +121,33 @@ func getMountInfo(path string) (types.BlockDevParams, error) {
120121
return blockDev, nil
121122
}
122123

124+
// bindBootArtifactsToMonRootfs bind-mounts boot files from an external
125+
// artifact root into the monitor rootfs without changing source permissions.
126+
func bindBootArtifactsToMonRootfs(artifactRoot, monRootfs, unikernelPath, initrdPath, uruncJSON string) ([]string, error) {
127+
norm := func(p string) string { return strings.TrimPrefix(filepath.Clean(p), "/") }
128+
files := []struct{ src, target string }{
129+
{filepath.Join(artifactRoot, unikernelPath), norm(unikernelPath)},
130+
{filepath.Join(artifactRoot, uruncJSON), norm(uruncJSON)},
131+
}
132+
if initrdPath != "" {
133+
files = append(files, struct{ src, target string }{
134+
filepath.Join(artifactRoot, initrdPath), norm(initrdPath),
135+
})
136+
}
137+
138+
targets := make([]string, 0, len(files))
139+
for _, f := range files {
140+
dstPath := filepath.Join(monRootfs, f.target)
141+
dstDir := filepath.Dir(dstPath)
142+
if err := bindMountFile(f.src, dstDir, dstPath, 0, unix.MS_BIND|unix.MS_PRIVATE, false); err != nil {
143+
rollbackPerContainerViewTargets(targets)
144+
return nil, fmt.Errorf("bind view %s -> monRootfs/%s: %w", f.src, f.target, err)
145+
}
146+
targets = append(targets, dstPath)
147+
}
148+
return targets, nil
149+
}
150+
123151
// extractUnikernelFromBlock moves unikernel binary, initrd and urunc.json
124152
// files from old rootfsPath to newRootfsPath
125153
// FIXME: This approach fills up /run with unikernel binaries, initrds and urunc.json
@@ -148,7 +176,6 @@ func extractBootFiles(rootfsPath string, newRootfsPath string, unikernel string,
148176
if err != nil {
149177
return fmt.Errorf("Could not move %s to %s: %w", currentConfigPath, newRootfsPath, err)
150178
}
151-
152179
return nil
153180
}
154181

@@ -226,25 +253,49 @@ func getBlockVolumes(monRootfs string, mounts []specs.Mount, ukernel types.Unike
226253
}
227254

228255
func (b blockRootfs) preSetup() error {
256+
// Preserve main's propagation fix: consume boot artifacts and unmount the
257+
// container rootfs before prepareRoot() makes the mount tree private/slave.
229258
if b.mountedPath == "" {
230259
return nil
231260
}
232261

233-
err := copyMountfiles(b.mountedPath, b.mounts)
262+
bundleDir := filepath.Dir(b.monRootfs)
263+
var viewTargets []string
264+
fromPerContainerView, err := tryRunOnPerContainerView(context.Background(), bundleDir, func(root string) error {
265+
var bindErr error
266+
viewTargets, bindErr = bindBootArtifactsToMonRootfs(root, b.monRootfs, b.kernelPath, b.initrdPath, b.uruncJSONPath)
267+
return bindErr
268+
})
234269
if err != nil {
235-
return fmt.Errorf("failed to copy files from mount list: %w", err)
270+
uniklog.WithError(err).Warn("snapshot view unavailable; falling back to legacy boot file extraction")
271+
fromPerContainerView = false
236272
}
237273

238-
// FIXME: This approach fills up /run with unikernel binaries and
239-
// urunc.json files for each unikernel instance we run
240-
err = extractBootFiles(b.mountedPath, b.monRootfs, b.kernelPath, b.uruncJSONPath, b.initrdPath)
241-
if err != nil {
242-
return fmt.Errorf("failed to extract boot files from rootfs: %w", err)
274+
if err := copyMountfiles(b.mountedPath, b.mounts); err != nil {
275+
return fmt.Errorf("failed to copy files from mount list: %w", err)
243276
}
244277

245-
err = mount.Unmount(b.mountedPath)
246-
if err != nil {
247-
return fmt.Errorf("failed to unmount rootfs: %w", err)
278+
if fromPerContainerView {
279+
if err := persistPerContainerViewMountsState(bundleDir, viewTargets); err != nil {
280+
rollbackPerContainerViewTargets(viewTargets)
281+
return err
282+
}
283+
if err := mount.Unmount(b.mountedPath); err != nil {
284+
return fmt.Errorf("failed to unmount rootfs: %w", err)
285+
}
286+
} else {
287+
if err := removePerContainerViewMountsState(bundleDir); err != nil {
288+
return err
289+
}
290+
// FIXME: This approach fills up /run with unikernel binaries and
291+
// urunc.json files for each unikernel instance we run
292+
if err := extractBootFiles(b.mountedPath, b.monRootfs, b.kernelPath, b.uruncJSONPath, b.initrdPath); err != nil {
293+
return fmt.Errorf("failed to extract boot files from rootfs: %w", err)
294+
}
295+
296+
if err := mount.Unmount(b.mountedPath); err != nil {
297+
return fmt.Errorf("failed to unmount rootfs: %w", err)
298+
}
248299
}
249300

250301
return nil

pkg/unikontainers/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const (
4545
annotBlock = "com.urunc.unikernel.block"
4646
annotBlockMntPoint = "com.urunc.unikernel.blkMntPoint"
4747
annotMountRootfs = "com.urunc.unikernel.mountRootfs"
48+
annotSnapshotView = "com.urunc.unikernel.snapshotView"
4849
)
4950

5051
// A UnikernelConfig struct holds the info provided by bima image on how to execute our unikernel
@@ -58,6 +59,7 @@ type UnikernelConfig struct {
5859
Block string `json:"com.urunc.unikernel.block,omitempty"`
5960
BlkMntPoint string `json:"com.urunc.unikernel.blkMntPoint,omitempty"`
6061
MountRootfs string `json:"com.urunc.unikernel.mountRootfs"`
62+
SnapshotView string `json:"com.urunc.unikernel.snapshotView,omitempty"`
6163
}
6264

6365
// validate checks if the mandatory configuration fields are present.
@@ -127,6 +129,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig {
127129
block := spec.Annotations[annotBlock]
128130
blkMntPoint := spec.Annotations[annotBlockMntPoint]
129131
MountRootfs := spec.Annotations[annotMountRootfs]
132+
snapshotView := spec.Annotations[annotSnapshotView]
130133
uniklog.WithFields(logrus.Fields{
131134
"unikernelType": tryDecode(unikernelType),
132135
"unikernelVersion": tryDecode(unikernelVersion),
@@ -137,6 +140,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig {
137140
"block": tryDecode(block),
138141
"blkMntPoint": tryDecode(blkMntPoint),
139142
"mountRootfs": tryDecode(MountRootfs),
143+
"snapshotView": tryDecode(snapshotView),
140144
}).WithField("source", "spec").Debug("urunc annotations")
141145

142146
return &UnikernelConfig{
@@ -149,6 +153,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig {
149153
Block: block,
150154
BlkMntPoint: blkMntPoint,
151155
MountRootfs: MountRootfs,
156+
SnapshotView: snapshotView,
152157
}
153158
}
154159

@@ -188,6 +193,7 @@ func getConfigFromJSON(jsonFilePath string) (*UnikernelConfig, error) {
188193
"block": tryDecode(conf.Block),
189194
"blkMntPoint": tryDecode(conf.BlkMntPoint),
190195
"mountRootfs": tryDecode(conf.MountRootfs),
196+
"snapshotView": tryDecode(conf.SnapshotView),
191197
}).WithField("source", uruncJSONFilename).Debug("urunc annotations")
192198

193199
return &conf, nil
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) 2023-2026, Nubificus LTD
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Per-container snapshot view: read-only mounts from shim-written urunc-view.json
16+
// (kernel, initrd, urunc.json bind-copied from image root). Mount descriptors are
17+
// persisted by the shim; this package does not dial containerd.
18+
19+
package unikontainers
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"errors"
25+
"fmt"
26+
"os"
27+
"path/filepath"
28+
29+
"github.com/containerd/containerd/mount"
30+
"golang.org/x/sys/unix"
31+
)
32+
33+
const (
34+
perContainerViewStateFile = "urunc-view.json"
35+
perContainerViewMountsStateFile = "urunc-view-mounts.json"
36+
)
37+
38+
var errPerContainerViewNotPrepared = errors.New("snapshot view not prepared")
39+
40+
type perContainerViewState struct {
41+
ViewKey string `json:"view_key"`
42+
Snapshotter string `json:"snapshotter"`
43+
Namespace string `json:"namespace"`
44+
Mounts []mount.Mount `json:"mounts,omitempty"`
45+
}
46+
47+
type perContainerViewMountsState struct {
48+
Targets []string `json:"targets,omitempty"`
49+
}
50+
51+
// tryRunOnPerContainerView loads shim-written per-container view metadata from
52+
// the bundle directory, mounts that snapshot view read-only on a temp
53+
// directory, runs fn with that mount root, then unmounts and removes the temp dir.
54+
//
55+
// If no view was prepared (missing state file), returns (false, nil) so the
56+
// caller can fall back to the legacy path.
57+
//
58+
// If state exists, returns (true, err) where err is from mounting, fn, or
59+
// unmount (unmount failure is returned only when fn succeeded).
60+
func tryRunOnPerContainerView(ctx context.Context, bundle string, fn func(mountRoot string) error) (ok bool, retErr error) {
61+
_ = ctx // API stability for callers; mount uses host paths from shim-persisted state only.
62+
state, err := loadPerContainerViewState(bundle)
63+
if err != nil {
64+
if errors.Is(err, errPerContainerViewNotPrepared) {
65+
return false, nil
66+
}
67+
return false, err
68+
}
69+
70+
mountpoint, err := mkTempSnapshotViewDir()
71+
if err != nil {
72+
return false, err
73+
}
74+
defer os.RemoveAll(mountpoint)
75+
76+
if err := mountPerContainerViewReadonly(state, mountpoint); err != nil {
77+
return false, err
78+
}
79+
defer func() {
80+
if uerr := unmountSnapshotViewTemp(mountpoint); uerr != nil {
81+
if retErr == nil {
82+
retErr = uerr
83+
return
84+
}
85+
uniklog.WithError(uerr).WithField("path", mountpoint).Warn("failed to unmount temporary snapshot view mount")
86+
}
87+
}()
88+
89+
retErr = fn(mountpoint)
90+
return true, retErr
91+
}
92+
93+
func loadPerContainerViewState(bundle string) (*perContainerViewState, error) {
94+
path := filepath.Join(bundle, perContainerViewStateFile)
95+
data, err := os.ReadFile(path)
96+
if err != nil {
97+
if os.IsNotExist(err) {
98+
return nil, errPerContainerViewNotPrepared
99+
}
100+
return nil, fmt.Errorf("read snapshot view state %s: %w", path, err)
101+
}
102+
103+
var state perContainerViewState
104+
if err := json.Unmarshal(data, &state); err != nil {
105+
return nil, fmt.Errorf("unmarshal snapshot view state %s: %w", path, err)
106+
}
107+
return &state, nil
108+
}
109+
110+
func mountPerContainerViewReadonly(state *perContainerViewState, target string) error {
111+
if len(state.Mounts) == 0 {
112+
return fmt.Errorf("snapshot view state %s has no mounts (recreate the container with an updated shim)", perContainerViewStateFile)
113+
}
114+
115+
return mountReadonlySnapshotView(target, state.Mounts)
116+
}
117+
118+
func mkTempSnapshotViewDir() (string, error) {
119+
dir, err := os.MkdirTemp("", "urunc-snapshot-view-")
120+
if err != nil {
121+
return "", fmt.Errorf("create temporary snapshot view mountpoint: %w", err)
122+
}
123+
return dir, nil
124+
}
125+
126+
func unmountSnapshotViewTemp(path string) error {
127+
if err := mount.Unmount(path, 0); err != nil {
128+
if os.IsNotExist(err) || err == unix.EINVAL {
129+
return nil
130+
}
131+
uniklog.WithError(err).WithField("path", path).Warn("failed to unmount snapshot view")
132+
return err
133+
}
134+
return nil
135+
}
136+
137+
func mountReadonlySnapshotView(target string, mounts []mount.Mount) error {
138+
if err := mount.All(mounts, target); err != nil {
139+
return fmt.Errorf("mount snapshot view at %s for boot file bind: %w", target, err)
140+
}
141+
142+
return nil
143+
}
144+
145+
func persistPerContainerViewMountsState(bundle string, targets []string) error {
146+
state := perContainerViewMountsState{Targets: targets}
147+
data, err := json.Marshal(state)
148+
if err != nil {
149+
return fmt.Errorf("marshal snapshot view mount state: %w", err)
150+
}
151+
152+
path := filepath.Join(bundle, perContainerViewMountsStateFile)
153+
if err := os.WriteFile(path, data, 0o600); err != nil {
154+
return fmt.Errorf("write snapshot view mount state %s: %w", path, err)
155+
}
156+
157+
return nil
158+
}
159+
160+
func loadPerContainerViewMountsState(bundle string) (*perContainerViewMountsState, error) {
161+
path := filepath.Join(bundle, perContainerViewMountsStateFile)
162+
data, err := os.ReadFile(path)
163+
if err != nil {
164+
if os.IsNotExist(err) {
165+
return nil, nil
166+
}
167+
return nil, fmt.Errorf("read snapshot view mount state %s: %w", path, err)
168+
}
169+
170+
var state perContainerViewMountsState
171+
if err := json.Unmarshal(data, &state); err != nil {
172+
return nil, fmt.Errorf("unmarshal snapshot view mount state %s: %w", path, err)
173+
}
174+
175+
return &state, nil
176+
}
177+
178+
func removePerContainerViewMountsState(bundle string) error {
179+
path := filepath.Join(bundle, perContainerViewMountsStateFile)
180+
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
181+
return fmt.Errorf("remove snapshot view mount state %s: %w", path, err)
182+
}
183+
184+
return nil
185+
}
186+
187+
func cleanupPerContainerViewMounts(bundle string) error {
188+
state, err := loadPerContainerViewMountsState(bundle)
189+
if err != nil || state == nil {
190+
return err
191+
}
192+
193+
for i := len(state.Targets) - 1; i >= 0; i-- {
194+
target := filepath.Clean(state.Targets[i])
195+
if err := unix.Unmount(target, unix.MNT_DETACH); err != nil {
196+
if err == unix.EINVAL || err == unix.ENOENT || os.IsNotExist(err) {
197+
continue
198+
}
199+
return fmt.Errorf("failed to unmount snapshot view target %s: %w", target, err)
200+
}
201+
}
202+
203+
return removePerContainerViewMountsState(bundle)
204+
}
205+
206+
func rollbackPerContainerViewTargets(targets []string) {
207+
for i := len(targets) - 1; i >= 0; i-- {
208+
target := filepath.Clean(targets[i])
209+
if err := unix.Unmount(target, unix.MNT_DETACH); err != nil {
210+
if err == unix.EINVAL || err == unix.ENOENT || os.IsNotExist(err) {
211+
continue
212+
}
213+
uniklog.WithError(err).WithField("target", target).Warn("failed to roll back snapshot view bind mount")
214+
}
215+
}
216+
}

pkg/unikontainers/unikontainers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,9 @@ func (u *Unikontainer) Delete() error {
725725
// if the monitorRootfsDirName directory exists under the bundle.
726726
_, err = os.Stat(monRootfs)
727727
if !os.IsNotExist(err) {
728+
if err := cleanupPerContainerViewMounts(bundleDir); err != nil {
729+
uniklog.WithError(err).Warn("failed to cleanup snapshot view mounts during delete")
730+
}
728731
// Since there was no block defined for the unikernel
729732
// and we created a new rootfs for the monitor, we need to
730733
// clean it up.

0 commit comments

Comments
 (0)