Skip to content

Commit 0e15b92

Browse files
committed
feat(rootfs-view): add snapshot view plumbing
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>
1 parent e9c212e commit 0e15b92

5 files changed

Lines changed: 519 additions & 8 deletions

File tree

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
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+
"fmt"
20+
"os"
21+
"path/filepath"
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/urunc-dev/urunc/pkg/unikontainers"
29+
"github.com/urunc-dev/urunc/pkg/unikontainers/types"
30+
"golang.org/x/sys/unix"
31+
"google.golang.org/grpc/metadata"
32+
)
33+
34+
const (
35+
rootfsViewKeyPrefix = "urunc-rootfs-view-"
36+
rootfsViewLeasePrefix = "urunc-rootfs-view-lease-"
37+
rootfsViewMountpointName = "rootfs-view-mount"
38+
)
39+
40+
type rootfsViewAccessor struct {
41+
namespace string
42+
containerID string
43+
snapshotter string
44+
snapshotKey string
45+
snapshots snapshotsapi.SnapshotsClient
46+
leases leasesapi.LeasesClient
47+
}
48+
49+
func newRootfsViewAccessor(session *Session) *rootfsViewAccessor {
50+
a := &rootfsViewAccessor{
51+
namespace: session.GetNamespace(),
52+
containerID: session.GetContainerID(),
53+
snapshots: session.snapshotsClient(),
54+
leases: session.leasesClient(),
55+
}
56+
ctr := session.GetContainer()
57+
if ctr != nil && ctr.GetSnapshotKey() != "" {
58+
a.snapshotter = ctr.GetSnapshotter()
59+
a.snapshotKey = ctr.GetSnapshotKey()
60+
}
61+
return a
62+
}
63+
64+
// PrepareRootfsView prepares a rootfs view when the container and
65+
// rootfs choice support it. The returned shouldPrepare value lets callers
66+
// distinguish config/check failures from prepare failures for logging.
67+
func PrepareRootfsView(ctx context.Context, session *Session, rootfs types.RootfsParams, bundle string) (types.RootfsViewState, bool, error) {
68+
if session == nil {
69+
return types.RootfsViewState{}, false, nil
70+
}
71+
72+
accessor := newRootfsViewAccessor(session)
73+
shouldPrepare, err := accessor.shouldPrepare(rootfs)
74+
if err != nil {
75+
return types.RootfsViewState{}, false, err
76+
}
77+
if !shouldPrepare {
78+
return types.RootfsViewState{}, false, nil
79+
}
80+
81+
state, err := accessor.prepare(ctx, bundle)
82+
if err != nil {
83+
return types.RootfsViewState{}, true, err
84+
}
85+
86+
return state, true, nil
87+
}
88+
89+
// CleanupRootfsView removes a rootfs view using container metadata from the
90+
// session and cleanup state read from the bundle.
91+
func CleanupRootfsView(ctx context.Context, session *Session, snapshotter, mountpoint string) error {
92+
if session == nil {
93+
return fmt.Errorf("containerd session is nil")
94+
}
95+
return newRootfsViewAccessor(session).cleanup(ctx, snapshotter, mountpoint)
96+
}
97+
98+
func (a *rootfsViewAccessor) shouldPrepare(rootfs types.RootfsParams) (bool, error) {
99+
if a == nil ||
100+
a.snapshotter == "" ||
101+
a.snapshotKey == "" ||
102+
(a.snapshotter != "devmapper" && a.snapshotter != "blockfile") ||
103+
rootfs.Type != "block" ||
104+
rootfs.MountedPath == "" {
105+
return false, nil
106+
}
107+
108+
uruncCfg, cfgErr := unikontainers.LoadUruncConfig(unikontainers.UruncConfigPath)
109+
if cfgErr != nil {
110+
return false, cfgErr
111+
}
112+
return uruncCfg.RootfsView.Enabled, nil
113+
}
114+
115+
// prepare records a read-only view of the committed rootfs snapshot for runtime use.
116+
// On success it returns view state for the caller to persist in bundle rootfs-view.json.
117+
func (a *rootfsViewAccessor) prepare(ctx context.Context, bundle string) (types.RootfsViewState, error) {
118+
if a == nil {
119+
return types.RootfsViewState{}, fmt.Errorf("rootfs view accessor is nil")
120+
}
121+
122+
snapshotKey, err := a.resolveCommittedSnapshotBase(ctx, a.snapshotter, a.snapshotKey)
123+
if err != nil {
124+
return types.RootfsViewState{}, err
125+
}
126+
127+
viewKey := rootfsViewKeyPrefix + a.containerID
128+
leaseID := rootfsViewLeasePrefix + a.containerID
129+
130+
nsCtx := withNamespace(ctx, a.namespace)
131+
if _, err := a.leases.Create(nsCtx, &leasesapi.CreateRequest{ID: leaseID}); err != nil {
132+
err = containerdErr(err)
133+
if err != nil && !errdefs.IsAlreadyExists(err) {
134+
return types.RootfsViewState{}, fmt.Errorf("create rootfs view lease %s: %w", leaseID, err)
135+
}
136+
}
137+
138+
leaseCtx := metadata.AppendToOutgoingContext(nsCtx, "containerd-lease", leaseID)
139+
mounts, err := a.createRootfsView(leaseCtx, viewKey, snapshotKey)
140+
if err != nil {
141+
_ = deleteRootfsViewLease(ctx, a.namespace, leaseID, a.leases)
142+
return types.RootfsViewState{}, err
143+
}
144+
145+
mountpoint := filepath.Join(filepath.Clean(bundle), rootfsViewMountpointName)
146+
keepView := false
147+
defer func() {
148+
if !keepView {
149+
_ = cleanupRootfsViewMountpoint(mountpoint)
150+
_ = removeRootfsViewSnapshotAndLease(ctx, a.namespace, a.containerID, a.snapshotter, a.snapshots, a.leases)
151+
}
152+
}()
153+
154+
if err := prepareRootfsViewMountpoint(mountpoint, mounts); err != nil {
155+
return types.RootfsViewState{}, err
156+
}
157+
158+
keepView = true
159+
return types.RootfsViewState{
160+
Snapshotter: a.snapshotter,
161+
Mountpoint: mountpoint,
162+
Mounts: mounts,
163+
}, nil
164+
}
165+
166+
// Rootfs view cleanup (call chain):
167+
//
168+
// Delete / Stop: ShouldCleanupRootfsView(bundle) → CleanupRootfsView(ctx, session, snapshotter, mountpoint)
169+
// Create rollback: CleanupRootfsView(ctx, session, "", state.Mountpoint)
170+
//
171+
// cleanup → removeRootfsViewSnapshotAndLease (view snapshot + lease in containerd)
172+
// prepare failure after lease create → deleteRootfsViewLease (lease only)
173+
174+
// cleanup unmounts the shim-mounted rootfs view, then removes its snapshot and lease.
175+
func (a *rootfsViewAccessor) cleanup(ctx context.Context, snapshotter, mountpoint string) error {
176+
if a == nil {
177+
return fmt.Errorf("rootfs view accessor is nil")
178+
}
179+
if a.containerID == "" {
180+
return fmt.Errorf("container id is empty")
181+
}
182+
183+
effectiveSnapshotter := snapshotter
184+
if effectiveSnapshotter == "" {
185+
effectiveSnapshotter = a.snapshotter
186+
}
187+
if effectiveSnapshotter == "" {
188+
return fmt.Errorf("snapshotter name required for rootfs view cleanup")
189+
}
190+
191+
if err := cleanupRootfsViewMountpoint(mountpoint); err != nil {
192+
return err
193+
}
194+
195+
return removeRootfsViewSnapshotAndLease(
196+
ctx, a.namespace, a.containerID, effectiveSnapshotter, a.snapshots, a.leases,
197+
)
198+
}
199+
200+
func (a *rootfsViewAccessor) statSnapshot(ctx context.Context, snapshotter, key string) (parent string, committed bool, err error) {
201+
resp, err := a.snapshots.Stat(withNamespace(ctx, a.namespace), &snapshotsapi.StatSnapshotRequest{
202+
Snapshotter: snapshotter,
203+
Key: key,
204+
})
205+
if err = containerdErr(err); err != nil {
206+
return "", false, err
207+
}
208+
info := resp.GetInfo()
209+
if info == nil {
210+
return "", false, fmt.Errorf("stat snapshot %s (%s): empty info", key, snapshotter)
211+
}
212+
return info.GetParent(), info.GetKind() == snapshotsapi.Kind_COMMITTED, nil
213+
}
214+
215+
func (a *rootfsViewAccessor) resolveCommittedSnapshotBase(ctx context.Context, snapshotter, snapshotKey string) (string, error) {
216+
parent, committed, err := a.statSnapshot(ctx, snapshotter, snapshotKey)
217+
if err != nil {
218+
return "", fmt.Errorf("stat snapshot %s (%s): %w", snapshotKey, snapshotter, err)
219+
}
220+
if committed {
221+
return snapshotKey, nil
222+
}
223+
if parent == "" {
224+
return snapshotKey, nil
225+
}
226+
227+
current := parent
228+
for {
229+
parent, committed, err = a.statSnapshot(ctx, snapshotter, current)
230+
if err != nil {
231+
return "", fmt.Errorf("stat snapshot %s (%s parent walk): %w", current, snapshotter, err)
232+
}
233+
if committed {
234+
return current, nil
235+
}
236+
if parent == "" {
237+
return "", fmt.Errorf("%s snapshot %s has no committed parent in chain", snapshotter, snapshotKey)
238+
}
239+
current = parent
240+
}
241+
}
242+
243+
func (a *rootfsViewAccessor) createRootfsView(ctx context.Context, viewKey, parentKey string) ([]mount.Mount, error) {
244+
nsCtx := withNamespace(ctx, a.namespace)
245+
viewResp, err := a.snapshots.View(nsCtx, &snapshotsapi.ViewSnapshotRequest{
246+
Snapshotter: a.snapshotter,
247+
Key: viewKey,
248+
Parent: parentKey,
249+
})
250+
if err = containerdErr(err); err == nil {
251+
return protoMountsToMounts(viewResp.GetMounts()), nil
252+
}
253+
if !errdefs.IsAlreadyExists(err) {
254+
return nil, fmt.Errorf("create rootfs view %s from %s: %w", viewKey, parentKey, err)
255+
}
256+
257+
// Reuse an existing view left by a retry or partial prepare.
258+
mountsResp, err := a.snapshots.Mounts(nsCtx, &snapshotsapi.MountsRequest{
259+
Snapshotter: a.snapshotter,
260+
Key: viewKey,
261+
})
262+
if err = containerdErr(err); err != nil {
263+
return nil, fmt.Errorf("create rootfs view %s from %s: %w", viewKey, parentKey, err)
264+
}
265+
return protoMountsToMounts(mountsResp.GetMounts()), nil
266+
}
267+
268+
func protoMountsToMounts(mm []*cntrtypes.Mount) []mount.Mount {
269+
out := make([]mount.Mount, len(mm))
270+
for i, m := range mm {
271+
out[i] = mount.Mount{
272+
Type: m.Type,
273+
Source: m.Source,
274+
Target: m.Target,
275+
Options: m.Options,
276+
}
277+
}
278+
return out
279+
}
280+
281+
// ShouldCleanupRootfsView reports whether bundle rootfs-view.json exists and returns cleanup state.
282+
func ShouldCleanupRootfsView(bundle string) (bool, string, string, error) {
283+
state, err := unikontainers.LoadBundleRootfsView(bundle)
284+
if err != nil {
285+
return false, "", "", err
286+
}
287+
if state == nil || state.Snapshotter == "" {
288+
return false, "", "", nil
289+
}
290+
return true, state.Snapshotter, state.Mountpoint, nil
291+
}
292+
293+
func prepareRootfsViewMountpoint(mountpoint string, mounts []mount.Mount) error {
294+
if err := cleanupRootfsViewMountpoint(mountpoint); err != nil {
295+
return err
296+
}
297+
if err := os.MkdirAll(mountpoint, 0o755); err != nil {
298+
return fmt.Errorf("create rootfs view mountpoint %s: %w", mountpoint, err)
299+
}
300+
if err := mount.All(mounts, mountpoint); err != nil {
301+
_ = cleanupRootfsViewMountpoint(mountpoint)
302+
return fmt.Errorf("mount rootfs view at %s: %w", mountpoint, err)
303+
}
304+
return nil
305+
}
306+
307+
func cleanupRootfsViewMountpoint(mountpoint string) error {
308+
if mountpoint == "" {
309+
return nil
310+
}
311+
mountpoint = filepath.Clean(mountpoint)
312+
if err := mount.Unmount(mountpoint, 0); err != nil && !os.IsNotExist(err) && err != unix.EINVAL {
313+
return fmt.Errorf("unmount rootfs view mountpoint %s: %w", mountpoint, err)
314+
}
315+
if err := os.RemoveAll(mountpoint); err != nil {
316+
return fmt.Errorf("remove rootfs view mountpoint %s: %w", mountpoint, err)
317+
}
318+
return nil
319+
}
320+
321+
// removeRootfsViewSnapshotAndLease deletes the view snapshot and its lease in containerd.
322+
func removeRootfsViewSnapshotAndLease(
323+
ctx context.Context,
324+
namespace, containerID, snapshotter string,
325+
snapshots snapshotsapi.SnapshotsClient,
326+
leases leasesapi.LeasesClient,
327+
) error {
328+
if containerID == "" || snapshotter == "" {
329+
return nil
330+
}
331+
nsCtx := withNamespace(ctx, namespace)
332+
_, err := snapshots.Remove(nsCtx, &snapshotsapi.RemoveSnapshotRequest{
333+
Snapshotter: snapshotter,
334+
Key: rootfsViewKeyPrefix + containerID,
335+
})
336+
if err = containerdErr(err); err != nil && !errdefs.IsNotFound(err) {
337+
return err
338+
}
339+
return deleteRootfsViewLease(ctx, namespace, rootfsViewLeasePrefix+containerID, leases)
340+
}
341+
342+
// deleteRootfsViewLease removes only the containerd lease (Prepare rollback after lease create).
343+
func deleteRootfsViewLease(ctx context.Context, namespace, leaseID string, leases leasesapi.LeasesClient) error {
344+
if leaseID == "" {
345+
return nil
346+
}
347+
_, err := leases.Delete(withNamespace(ctx, namespace), &leasesapi.DeleteRequest{ID: leaseID})
348+
if err = containerdErr(err); err != nil && !errdefs.IsNotFound(err) {
349+
return err
350+
}
351+
return nil
352+
}

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)