Skip to content

Commit 12f04ab

Browse files
committed
feat(shim): manage rootfs views in task lifecycle
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>
1 parent c9f736d commit 12f04ab

5 files changed

Lines changed: 158 additions & 39 deletions

File tree

docs/package/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ Except of the above, `urunc` accepts the following optional annotations:
7373
requests from `urunc` to mount the container's image rootfs in the unikernel
7474
(either as a block device or through shared-fs).
7575

76+
Per-container rootfs views are controlled by `[rootfs_view] enabled` in
77+
`/etc/urunc/config.toml`. See
78+
[configuration](../configuration.md#rootfs-view-configuration). When enabled,
79+
the container must also use `com.urunc.unikernel.mountRootfs=true` (typically
80+
from image annotations merged into `config.json` before shim task Create).
81+
Supported snapshotters include `devmapper` and `blockfile`. After the wrapped
82+
task service creates the task and mounts the bundle rootfs, the shim runs
83+
`ChooseRootfs` and prepares a view only when that selection is container block
84+
rootfs.
85+
7686
Due to the fact that [Docker](https://www.docker.com/) and some high-level
7787
container runtimes do not pass the image annotations to the underlying container
7888
runtime, `urunc` can also read the above information from a file inside the

pkg/containerd-shim/containerd/annotations.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func InjectUruncAnnotations(ctx context.Context, session *Session, bundlePath st
8686
return nil
8787
}
8888

89-
return patchConfigJSON(bundlePath, annotations)
89+
return PatchConfigJSON(bundlePath, annotations)
9090
}
9191

9292
func (f *annotationFetcher) fetchUruncAnnotations(ctx context.Context) (map[string]string, error) {
@@ -152,12 +152,12 @@ func readBlob(ctx context.Context, namespace string, contentClient contentapi.Co
152152
return raw, nil
153153
}
154154

155-
// patchConfigJSON injects missing annotations into the OCI runtime spec
156-
// stored in the bundle's config.json.
155+
// PatchConfigJSON injects missing annotations into the OCI runtime spec stored in
156+
// the bundle's config.json.
157157
//
158158
// Existing annotations in config.json are preserved. Only annotation keys that
159159
// are not already present in the runtime spec are added.
160-
func patchConfigJSON(bundlePath string, annotations map[string]string) error {
160+
func PatchConfigJSON(bundlePath string, annotations map[string]string) error {
161161
configPath := filepath.Join(bundlePath, "config.json")
162162

163163
fi, err := os.Stat(configPath)

pkg/containerd-shim/guest_rootfs.go

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,44 +24,38 @@ import (
2424
taskAPI "github.com/containerd/containerd/api/runtime/task/v2"
2525
specs "github.com/opencontainers/runtime-spec/specs-go"
2626
"github.com/urunc-dev/urunc/pkg/unikontainers"
27+
"github.com/urunc-dev/urunc/pkg/unikontainers/types"
2728
)
2829

29-
const annotRootfsParams = "com.urunc.internal.rootfs.params"
30-
3130
var errGuestRootfsChoiceSkipped = errors.New("guest rootfs choice skipped")
3231

3332
// chooseGuestRootfs runs the same ChooseRootfs logic as runtime Exec after inner
34-
// task Create (#684) and records the result in annotRootfsParams so Exec knows
35-
// selection already happened.
36-
func chooseGuestRootfs(r *taskAPI.CreateTaskRequest) error {
33+
// task Create (#684). The caller persists the result in bundle config.json so
34+
// Exec can reuse the selection.
35+
func chooseGuestRootfs(r *taskAPI.CreateTaskRequest) (types.RootfsParams, error) {
3736
configPath := filepath.Join(r.Bundle, "config.json")
38-
info, err := os.Stat(configPath)
39-
if err != nil {
40-
return fmt.Errorf("stat config.json: %w", err)
41-
}
42-
4337
data, err := os.ReadFile(configPath)
4438
if err != nil {
45-
return fmt.Errorf("read config.json: %w", err)
39+
return types.RootfsParams{}, fmt.Errorf("read config.json: %w", err)
4640
}
4741

4842
var spec specs.Spec
4943
if err := json.Unmarshal(data, &spec); err != nil {
50-
return fmt.Errorf("unmarshal config.json: %w", err)
44+
return types.RootfsParams{}, fmt.Errorf("unmarshal config.json: %w", err)
5145
}
5246
if spec.Root == nil {
53-
return fmt.Errorf("invalid OCI spec: root section is required")
47+
return types.RootfsParams{}, fmt.Errorf("invalid OCI spec: root section is required")
5448
}
5549

5650
config, err := unikontainers.GetUnikernelConfig(filepath.Clean(r.Bundle), &spec)
5751
if err != nil {
58-
return fmt.Errorf("%w: %w", errGuestRootfsChoiceSkipped, err)
52+
return types.RootfsParams{}, fmt.Errorf("%w: %w", errGuestRootfsChoiceSkipped, err)
5953
}
6054

6155
annotations := config.Map()
6256
uruncCfg, err := unikontainers.LoadUruncConfig(unikontainers.UruncConfigPath)
6357
if err != nil && uruncCfg == nil {
64-
return err
58+
return types.RootfsParams{}, err
6559
}
6660

6761
rootfsParams, err := unikontainers.ChooseRootfs(
@@ -71,22 +65,8 @@ func chooseGuestRootfs(r *taskAPI.CreateTaskRequest) error {
7165
uruncCfg,
7266
)
7367
if err != nil {
74-
return err
75-
}
76-
77-
encoded, err := json.Marshal(rootfsParams)
78-
if err != nil {
79-
return err
80-
}
81-
if spec.Annotations == nil {
82-
spec.Annotations = make(map[string]string)
83-
}
84-
spec.Annotations[annotRootfsParams] = string(encoded)
85-
86-
patched, err := json.MarshalIndent(spec, "", " ")
87-
if err != nil {
88-
return fmt.Errorf("marshal config.json: %w", err)
68+
return types.RootfsParams{}, err
8969
}
9070

91-
return os.WriteFile(configPath, patched, info.Mode())
71+
return rootfsParams, nil
9272
}

pkg/containerd-shim/task_plugin.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
package containerdshim
1616

1717
import (
18+
"os"
19+
"path/filepath"
20+
1821
"github.com/containerd/containerd/pkg/shutdown"
1922
"github.com/containerd/containerd/plugin"
2023
runcTask "github.com/containerd/containerd/runtime/v2/runc/task"
@@ -45,9 +48,15 @@ func init() {
4548
return nil, err
4649
}
4750

51+
cwd, err := os.Getwd()
52+
if err != nil {
53+
return nil, err
54+
}
55+
4856
return &taskService{
4957
TaskService: inner,
5058
containerdAddress: ic.Address,
59+
stateRoot: filepath.Dir(filepath.Dir(cwd)),
5160
}, nil
5261
},
5362
})

pkg/containerd-shim/task_service.go

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,31 @@ package containerdshim
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"errors"
21+
"fmt"
22+
"path/filepath"
2023

2124
taskAPI "github.com/containerd/containerd/api/runtime/task/v2"
25+
"github.com/containerd/containerd/namespaces"
2226
"github.com/containerd/log"
2327
"github.com/containerd/ttrpc"
2428
containerdShim "github.com/urunc-dev/urunc/pkg/containerd-shim/containerd"
29+
"github.com/urunc-dev/urunc/pkg/unikontainers"
2530
)
2631

32+
// Internal bundle annotation (duplicated in unikontainers; keep in sync).
33+
const annotRootfsParams = "com.urunc.internal.rootfs.params"
34+
2735
// taskService is urunc's shim-side wrapper around containerd's runc task
2836
// service. It wires urunc task setup before forwarding calls to the wrapped
2937
// service.
3038
type taskService struct {
3139
taskAPI.TaskService
3240

3341
containerdAddress string
42+
// Used on Delete, where cwd may no longer be the bundle.
43+
stateRoot string
3444
}
3545

3646
func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (*taskAPI.CreateTaskResponse, error) {
@@ -53,9 +63,8 @@ func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest)
5363
return resp, err
5464
}
5565

56-
// ChooseRootfs after inner task Create so bundle rootfs is mounted;
57-
// params are persisted in bundle config.json for runtime Exec.
58-
if err := chooseGuestRootfs(r); err != nil {
66+
rootfsChoice, err := chooseGuestRootfs(r)
67+
if err != nil {
5968
if errors.Is(err, errGuestRootfsChoiceSkipped) {
6069
log.G(ctx).WithError(err).Debug("urunc(shim): guest rootfs choice skipped")
6170
return resp, nil
@@ -64,14 +73,125 @@ func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest)
6473
return nil, err
6574
}
6675

76+
rootfsViewState, shouldPrepareRootfsView, err := containerdShim.PrepareRootfsView(ctx, session, rootfsChoice, r.Bundle)
77+
rootfsViewPrepared := err == nil && shouldPrepareRootfsView
78+
switch {
79+
case err != nil && shouldPrepareRootfsView:
80+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to prepare rootfs view; falling back to legacy boot artifact extraction")
81+
case err != nil:
82+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to load urunc config; rootfs view disabled")
83+
case rootfsViewPrepared:
84+
log.G(ctx).Debug("urunc(shim): rootfs view prepared")
85+
case session != nil:
86+
log.G(ctx).WithField("rootfs_type", rootfsChoice.Type).Debug("urunc(shim): rootfs view prepare skipped")
87+
}
88+
89+
rootfsViewPersisted := false
90+
defer func() {
91+
if rootfsViewPrepared && !rootfsViewPersisted {
92+
cleanupRootfsView(ctx, session, "", rootfsViewState.Mountpoint, "create rollback")
93+
}
94+
}()
95+
96+
rootfsParamsJSON, err := json.Marshal(rootfsChoice)
97+
if err != nil {
98+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to encode rootfs params")
99+
return nil, err
100+
}
101+
102+
if err := containerdShim.PatchConfigJSON(r.Bundle, map[string]string{
103+
annotRootfsParams: string(rootfsParamsJSON),
104+
}); err != nil {
105+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to persist shim create annotations")
106+
return nil, err
107+
}
108+
109+
if rootfsViewPrepared {
110+
if err := unikontainers.WriteBundleRootfsView(r.Bundle, rootfsViewState); err != nil {
111+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to persist rootfs view state")
112+
return nil, err
113+
}
114+
rootfsViewPersisted = true
115+
}
116+
67117
return resp, nil
68118
}
69119

120+
func cleanupRootfsView(ctx context.Context, session *containerdShim.Session, snapshotter, mountpoint, reason string) {
121+
if err := containerdShim.CleanupRootfsView(ctx, session, snapshotter, mountpoint); err != nil {
122+
log.G(ctx).WithError(err).WithField("reason", reason).Warn("urunc(shim): failed to clean up rootfs view")
123+
}
124+
}
125+
70126
func (s *taskService) Delete(ctx context.Context, r *taskAPI.DeleteRequest) (*taskAPI.DeleteResponse, error) {
71-
return s.TaskService.Delete(ctx, r)
127+
shouldCleanup := false
128+
snapshotter := ""
129+
rootfsViewMountpoint := ""
130+
var loadErr error
131+
132+
if r.ExecID == "" {
133+
bundle, err := s.bundlePathFor(ctx, r.ID)
134+
if err != nil {
135+
log.G(ctx).WithError(err).Warn("urunc(shim): resolve bundle path during Delete failed")
136+
loadErr = err
137+
} else {
138+
// Read view state before inner Delete; snapshotter is taken from bundle
139+
// (written at Prepare) because container metadata may be gone after Delete.
140+
var mountpoint string
141+
shouldCleanup, snapshotter, mountpoint, loadErr = containerdShim.ShouldCleanupRootfsView(bundle)
142+
if loadErr == nil {
143+
rootfsViewMountpoint = mountpoint
144+
}
145+
}
146+
}
147+
148+
// Delete tears down the monitor namespace before removing the view it may pin.
149+
resp, err := s.TaskService.Delete(ctx, r)
150+
151+
if loadErr != nil {
152+
if err != nil {
153+
return resp, err
154+
}
155+
return resp, loadErr
156+
}
157+
158+
if shouldCleanup {
159+
session, sessionErr := containerdShim.OpenSession(ctx, s.containerdAddress, r.ID)
160+
if sessionErr != nil {
161+
log.G(ctx).WithError(sessionErr).Warn("urunc(shim): open containerd session for rootfs view cleanup failed")
162+
if err == nil {
163+
err = sessionErr
164+
}
165+
} else {
166+
defer func() {
167+
if err := session.Close(); err != nil {
168+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to close containerd session after rootfs view cleanup")
169+
}
170+
}()
171+
if cleanupErr := containerdShim.CleanupRootfsView(ctx, session, snapshotter, rootfsViewMountpoint); cleanupErr != nil {
172+
log.G(ctx).WithError(cleanupErr).Warn("urunc(shim): delete rootfs view during Delete failed")
173+
if err == nil {
174+
err = cleanupErr
175+
}
176+
}
177+
}
178+
}
179+
180+
return resp, err
72181
}
73182

74183
func (s *taskService) RegisterTTRPC(server *ttrpc.Server) error {
75184
taskAPI.RegisterTaskService(server, s)
76185
return nil
77186
}
187+
188+
func (s *taskService) bundlePathFor(ctx context.Context, containerID string) (string, error) {
189+
if s.stateRoot == "" {
190+
return "", fmt.Errorf("task service state root is empty (shim cwd layout assumption violated)")
191+
}
192+
ns, err := namespaces.NamespaceRequired(ctx)
193+
if err != nil {
194+
return "", fmt.Errorf("namespace required: %w", err)
195+
}
196+
return filepath.Join(s.stateRoot, ns, containerID), nil
197+
}

0 commit comments

Comments
 (0)