Skip to content

Commit 3c2345e

Browse files
committed
feat: add rootfs view accessor and runtime boot binds
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>
1 parent fb286f3 commit 3c2345e

5 files changed

Lines changed: 630 additions & 34 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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+
package containerd
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"errors"
21+
"fmt"
22+
23+
leasesapi "github.com/containerd/containerd/api/services/leases/v1"
24+
snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1"
25+
cntrtypes "github.com/containerd/containerd/api/types"
26+
"github.com/containerd/containerd/errdefs"
27+
"github.com/containerd/containerd/mount"
28+
"github.com/sirupsen/logrus"
29+
"github.com/urunc-dev/urunc/pkg/unikontainers"
30+
"github.com/urunc-dev/urunc/pkg/unikontainers/types"
31+
"google.golang.org/grpc/metadata"
32+
)
33+
34+
const (
35+
rootfsViewKeyPrefix = "urunc-rootfs-view-"
36+
rootfsViewLeasePrefix = "urunc-rootfs-view-lease-"
37+
rootfsViewAnnotation = "com.urunc.internal.rootfs.view"
38+
)
39+
40+
var (
41+
ErrRootfsViewNotPrepared = errors.New("rootfs view not prepared")
42+
rootfsViewLog = logrus.WithField("subsystem", "containerd-shim-rootfs-view")
43+
)
44+
45+
type rootfsViewState struct {
46+
Snapshotter string `json:"snapshotter"`
47+
Mounts []mount.Mount `json:"mounts,omitempty"`
48+
}
49+
50+
type RootfsViewAccessor struct {
51+
namespace string
52+
containerID string
53+
snapshotter string
54+
snapshotKey string
55+
snapshots snapshotsapi.SnapshotsClient
56+
leases leasesapi.LeasesClient
57+
}
58+
59+
func NewRootfsViewAccessor(s *Session) *RootfsViewAccessor {
60+
a := &RootfsViewAccessor{
61+
namespace: s.namespace,
62+
containerID: s.containerID,
63+
snapshots: s.snapshotsClient(),
64+
leases: s.leasesClient(),
65+
}
66+
ctr := s.GetContainer()
67+
if ctr != nil && ctr.GetSnapshotKey() != "" {
68+
a.snapshotter = ctr.GetSnapshotter()
69+
a.snapshotKey = ctr.GetSnapshotKey()
70+
}
71+
return a
72+
}
73+
74+
func (a *RootfsViewAccessor) viewKey() string {
75+
return rootfsViewKeyPrefix + a.containerID
76+
}
77+
78+
func (a *RootfsViewAccessor) leaseID() string {
79+
return rootfsViewLeasePrefix + a.containerID
80+
}
81+
82+
func (a *RootfsViewAccessor) ShouldPrepare(rootfs types.RootfsParams) bool {
83+
if a == nil ||
84+
a.snapshotter == "" ||
85+
a.snapshotKey == "" ||
86+
(a.snapshotter != "devmapper" && a.snapshotter != "blockfile") ||
87+
rootfs.Type != "block" ||
88+
rootfs.MountedPath == "" {
89+
return false
90+
}
91+
92+
uruncCfg, cfgErr := unikontainers.LoadUruncConfig(unikontainers.UruncConfigPath)
93+
if cfgErr != nil {
94+
rootfsViewLog.WithError(cfgErr).Warn("failed to load urunc config; rootfs view disabled")
95+
return false
96+
}
97+
return uruncCfg.RootfsView.Enabled
98+
}
99+
100+
// Prepare records a read-only view of the committed rootfs snapshot for runtime use.
101+
func (a *RootfsViewAccessor) Prepare(ctx context.Context, bundle string) error {
102+
if a == nil {
103+
return fmt.Errorf("rootfs view accessor is nil")
104+
}
105+
106+
snapshotKey, err := a.resolveCommittedSnapshotBase(ctx, a.snapshotter, a.snapshotKey)
107+
if err != nil {
108+
return err
109+
}
110+
111+
viewKey := a.viewKey()
112+
leaseID := a.leaseID()
113+
114+
nsCtx := withNamespace(ctx, a.namespace)
115+
if _, err := a.leases.Create(nsCtx, &leasesapi.CreateRequest{ID: leaseID}); err != nil {
116+
err = containerdErr(err)
117+
if err != nil && !errdefs.IsAlreadyExists(err) {
118+
return fmt.Errorf("create rootfs view lease %s: %w", leaseID, err)
119+
}
120+
}
121+
122+
leaseCtx := metadata.AppendToOutgoingContext(nsCtx, "containerd-lease", leaseID)
123+
mounts, err := a.createRootfsView(leaseCtx, viewKey, snapshotKey)
124+
if err != nil {
125+
if cleanupErr := cleanupRootfsViewLease(ctx, a.namespace, a.leaseID(), a.leases); cleanupErr != nil {
126+
rootfsViewLog.WithError(cleanupErr).Warn("failed to clean up rootfs view lease after prepare failure")
127+
}
128+
return err
129+
}
130+
131+
state := &rootfsViewState{
132+
Snapshotter: a.snapshotter,
133+
Mounts: mounts,
134+
}
135+
136+
encoded, err := json.Marshal(state)
137+
if err != nil {
138+
return fmt.Errorf("marshal rootfs view state: %w", err)
139+
}
140+
if err := unikontainers.PatchBundleRootfsView(bundle, string(encoded)); err != nil {
141+
if cleanupErr := cleanupRootfsView(ctx, a.namespace, a.containerID, a.snapshotter, a.snapshots, a.leases); cleanupErr != nil {
142+
rootfsViewLog.WithError(cleanupErr).Warn("failed to clean up rootfs view after state persistence failure")
143+
return fmt.Errorf("persist rootfs view state: %w (cleanup also failed: %v)", err, cleanupErr)
144+
}
145+
return fmt.Errorf("persist rootfs view state: %w", err)
146+
}
147+
148+
return nil
149+
}
150+
151+
func (a *RootfsViewAccessor) Cleanup(ctx context.Context, snapshotter string) error {
152+
if a == nil {
153+
return fmt.Errorf("rootfs view accessor is nil")
154+
}
155+
if a.containerID == "" {
156+
return fmt.Errorf("container id is empty")
157+
}
158+
if snapshotter == "" {
159+
return fmt.Errorf("snapshotter is empty")
160+
}
161+
return cleanupRootfsView(ctx, a.namespace, a.containerID, snapshotter, a.snapshots, a.leases)
162+
}
163+
164+
func (a *RootfsViewAccessor) CleanupFromBundle(ctx context.Context, bundle string) error {
165+
if a == nil {
166+
return fmt.Errorf("rootfs view accessor is nil")
167+
}
168+
snapshotter, err := GetSnapshotterFromBundle(bundle)
169+
if err != nil {
170+
if errors.Is(err, ErrRootfsViewNotPrepared) {
171+
return nil
172+
}
173+
return err
174+
}
175+
return a.Cleanup(ctx, snapshotter)
176+
}
177+
178+
func (a *RootfsViewAccessor) statSnapshot(ctx context.Context, snapshotter, key string) (parent string, committed bool, err error) {
179+
resp, err := a.snapshots.Stat(withNamespace(ctx, a.namespace), &snapshotsapi.StatSnapshotRequest{
180+
Snapshotter: snapshotter,
181+
Key: key,
182+
})
183+
if err = containerdErr(err); err != nil {
184+
return "", false, err
185+
}
186+
info := resp.GetInfo()
187+
if info == nil {
188+
return "", false, fmt.Errorf("stat snapshot %s (%s): empty info", key, snapshotter)
189+
}
190+
return info.GetParent(), info.GetKind() == snapshotsapi.Kind_COMMITTED, nil
191+
}
192+
193+
func (a *RootfsViewAccessor) resolveCommittedSnapshotBase(ctx context.Context, snapshotter, snapshotKey string) (string, error) {
194+
parent, committed, err := a.statSnapshot(ctx, snapshotter, snapshotKey)
195+
if err != nil {
196+
return "", fmt.Errorf("stat snapshot %s (%s): %w", snapshotKey, snapshotter, err)
197+
}
198+
if committed {
199+
return snapshotKey, nil
200+
}
201+
if parent == "" {
202+
return snapshotKey, nil
203+
}
204+
205+
current := parent
206+
for {
207+
parent, committed, err = a.statSnapshot(ctx, snapshotter, current)
208+
if err != nil {
209+
return "", fmt.Errorf("stat snapshot %s (%s parent walk): %w", current, snapshotter, err)
210+
}
211+
if committed {
212+
return current, nil
213+
}
214+
if parent == "" {
215+
return "", fmt.Errorf("%s snapshot %s has no committed parent in chain", snapshotter, snapshotKey)
216+
}
217+
current = parent
218+
}
219+
}
220+
221+
func (a *RootfsViewAccessor) createRootfsView(ctx context.Context, viewKey, parentKey string) ([]mount.Mount, error) {
222+
nsCtx := withNamespace(ctx, a.namespace)
223+
viewResp, err := a.snapshots.View(nsCtx, &snapshotsapi.ViewSnapshotRequest{
224+
Snapshotter: a.snapshotter,
225+
Key: viewKey,
226+
Parent: parentKey,
227+
})
228+
if err = containerdErr(err); err == nil {
229+
return protoMountsToMounts(viewResp.GetMounts()), nil
230+
}
231+
if !errdefs.IsAlreadyExists(err) {
232+
return nil, fmt.Errorf("create rootfs view %s from %s: %w", viewKey, parentKey, err)
233+
}
234+
235+
// Reuse an existing view left by a retry or partial prepare.
236+
mountsResp, err := a.snapshots.Mounts(nsCtx, &snapshotsapi.MountsRequest{
237+
Snapshotter: a.snapshotter,
238+
Key: viewKey,
239+
})
240+
if err = containerdErr(err); err != nil {
241+
return nil, fmt.Errorf("create rootfs view %s from %s: %w", viewKey, parentKey, err)
242+
}
243+
return protoMountsToMounts(mountsResp.GetMounts()), nil
244+
}
245+
246+
func protoMountsToMounts(mm []*cntrtypes.Mount) []mount.Mount {
247+
out := make([]mount.Mount, len(mm))
248+
for i, m := range mm {
249+
out[i] = mount.Mount{
250+
Type: m.Type,
251+
Source: m.Source,
252+
Target: m.Target,
253+
Options: m.Options,
254+
}
255+
}
256+
return out
257+
}
258+
259+
func GetSnapshotterFromBundle(bundle string) (string, error) {
260+
raw, err := unikontainers.ReadBundleRootfsView(bundle)
261+
if err != nil {
262+
return "", err
263+
}
264+
if raw == "" {
265+
return "", ErrRootfsViewNotPrepared
266+
}
267+
var state rootfsViewState
268+
if err := json.Unmarshal([]byte(raw), &state); err != nil {
269+
return "", fmt.Errorf("unmarshal rootfs view state %s: %w", rootfsViewAnnotation, err)
270+
}
271+
if state.Snapshotter == "" {
272+
return "", ErrRootfsViewNotPrepared
273+
}
274+
return state.Snapshotter, nil
275+
}
276+
277+
func ShouldCleanupRootfsView(bundle string) (bool, string, error) {
278+
snapshotter, err := GetSnapshotterFromBundle(bundle)
279+
if err != nil {
280+
if errors.Is(err, ErrRootfsViewNotPrepared) {
281+
return false, "", nil
282+
}
283+
return false, "", err
284+
}
285+
return true, snapshotter, nil
286+
}
287+
288+
func cleanupRootfsView(
289+
ctx context.Context,
290+
namespace, containerID, snapshotter string,
291+
snapshots snapshotsapi.SnapshotsClient,
292+
leases leasesapi.LeasesClient,
293+
) error {
294+
if containerID == "" || snapshotter == "" {
295+
return nil
296+
}
297+
nsCtx := withNamespace(ctx, namespace)
298+
_, err := snapshots.Remove(nsCtx, &snapshotsapi.RemoveSnapshotRequest{
299+
Snapshotter: snapshotter,
300+
Key: rootfsViewKey(containerID),
301+
})
302+
if err = containerdErr(err); err != nil && !errdefs.IsNotFound(err) {
303+
rootfsViewLog.WithError(err).Warn("failed to remove rootfs view from containerd")
304+
return err
305+
}
306+
return cleanupRootfsViewLease(ctx, namespace, rootfsViewLeaseID(containerID), leases)
307+
}
308+
309+
func cleanupRootfsViewLease(ctx context.Context, namespace, leaseID string, leases leasesapi.LeasesClient) error {
310+
if leaseID == "" {
311+
return nil
312+
}
313+
_, err := leases.Delete(withNamespace(ctx, namespace), &leasesapi.DeleteRequest{ID: leaseID})
314+
if err = containerdErr(err); err != nil && !errdefs.IsNotFound(err) {
315+
rootfsViewLog.WithError(err).Warn("failed to remove rootfs view lease from containerd")
316+
return err
317+
}
318+
return nil
319+
}
320+
321+
func rootfsViewKey(containerID string) string {
322+
return rootfsViewKeyPrefix + containerID
323+
}
324+
325+
func rootfsViewLeaseID(containerID string) string {
326+
return rootfsViewLeasePrefix + containerID
327+
}

pkg/containerd-shim/containerd/session.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,10 @@ func (s *Session) contentClient() contentapi.ContentClient {
158158
return contentapi.NewContentClient(s.conn)
159159
}
160160

161-
//nolint:unused // Used by follow-up feature-specific access constructors.
162161
func (s *Session) snapshotsClient() snapshotsapi.SnapshotsClient {
163162
return snapshotsapi.NewSnapshotsClient(s.conn)
164163
}
165164

166-
//nolint:unused // Used by follow-up feature-specific access constructors.
167165
func (s *Session) leasesClient() leasesapi.LeasesClient {
168166
return leasesapi.NewLeasesClient(s.conn)
169167
}

0 commit comments

Comments
 (0)