Skip to content

Commit ef929cd

Browse files
committed
refactor(shim): persist create annotations in one config.json write
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>
1 parent 26c6b6a commit ef929cd

5 files changed

Lines changed: 78 additions & 106 deletions

File tree

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/containerd/rootfs_view.go

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
cntrtypes "github.com/containerd/containerd/api/types"
2626
"github.com/containerd/containerd/errdefs"
2727
"github.com/containerd/containerd/mount"
28-
"github.com/sirupsen/logrus"
2928
"github.com/urunc-dev/urunc/pkg/unikontainers"
3029
"github.com/urunc-dev/urunc/pkg/unikontainers/types"
3130
"google.golang.org/grpc/metadata"
@@ -37,10 +36,7 @@ const (
3736
rootfsViewAnnotation = "com.urunc.internal.rootfs.view"
3837
)
3938

40-
var (
41-
ErrRootfsViewNotPrepared = errors.New("rootfs view not prepared")
42-
rootfsViewLog = logrus.WithField("subsystem", "containerd-shim-rootfs-view")
43-
)
39+
var ErrRootfsViewNotPrepared = errors.New("rootfs view not prepared")
4440

4541
type rootfsViewState struct {
4642
Snapshotter string `json:"snapshotter"`
@@ -79,33 +75,34 @@ func (a *RootfsViewAccessor) leaseID() string {
7975
return rootfsViewLeasePrefix + a.containerID
8076
}
8177

82-
func (a *RootfsViewAccessor) ShouldPrepare(rootfs types.RootfsParams) bool {
78+
func (a *RootfsViewAccessor) ShouldPrepare(rootfs types.RootfsParams) (bool, error) {
8379
if a == nil ||
8480
a.snapshotter == "" ||
8581
a.snapshotKey == "" ||
8682
(a.snapshotter != "devmapper" && a.snapshotter != "blockfile") ||
8783
rootfs.Type != "block" ||
8884
rootfs.MountedPath == "" {
89-
return false
85+
return false, nil
9086
}
9187

9288
uruncCfg, cfgErr := unikontainers.LoadUruncConfig(unikontainers.UruncConfigPath)
9389
if cfgErr != nil {
94-
rootfsViewLog.WithError(cfgErr).Warn("failed to load urunc config; rootfs view disabled")
95-
return false
90+
return false, cfgErr
9691
}
97-
return uruncCfg.RootfsView.Enabled
92+
return uruncCfg.RootfsView.Enabled, nil
9893
}
9994

10095
// 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 {
96+
// On success it returns JSON-encoded view state for the caller to persist in bundle
97+
// config.json together with other shim Create annotations.
98+
func (a *RootfsViewAccessor) Prepare(ctx context.Context) (string, error) {
10299
if a == nil {
103-
return fmt.Errorf("rootfs view accessor is nil")
100+
return "", fmt.Errorf("rootfs view accessor is nil")
104101
}
105102

106103
snapshotKey, err := a.resolveCommittedSnapshotBase(ctx, a.snapshotter, a.snapshotKey)
107104
if err != nil {
108-
return err
105+
return "", err
109106
}
110107

111108
viewKey := a.viewKey()
@@ -115,17 +112,15 @@ func (a *RootfsViewAccessor) Prepare(ctx context.Context, bundle string) error {
115112
if _, err := a.leases.Create(nsCtx, &leasesapi.CreateRequest{ID: leaseID}); err != nil {
116113
err = containerdErr(err)
117114
if err != nil && !errdefs.IsAlreadyExists(err) {
118-
return fmt.Errorf("create rootfs view lease %s: %w", leaseID, err)
115+
return "", fmt.Errorf("create rootfs view lease %s: %w", leaseID, err)
119116
}
120117
}
121118

122119
leaseCtx := metadata.AppendToOutgoingContext(nsCtx, "containerd-lease", leaseID)
123120
mounts, err := a.createRootfsView(leaseCtx, viewKey, snapshotKey)
124121
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
122+
_ = cleanupRootfsViewLease(ctx, a.namespace, a.leaseID(), a.leases)
123+
return "", err
129124
}
130125

131126
state := &rootfsViewState{
@@ -135,26 +130,28 @@ func (a *RootfsViewAccessor) Prepare(ctx context.Context, bundle string) error {
135130

136131
encoded, err := json.Marshal(state)
137132
if err != nil {
138-
return fmt.Errorf("marshal rootfs view state: %w", err)
133+
return "", fmt.Errorf("marshal rootfs view state: %w", err)
139134
}
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
135+
return string(encoded), nil
149136
}
150137

138+
// Cleanup removes the per-container rootfs view snapshot and lease.
139+
// snapshotter names the devmapper/blockfile plugin that owns the view; callers
140+
// on Delete should pass the value persisted in bundle config.json at Prepare
141+
// time (see ShouldCleanupRootfsView). After inner task Delete, containerd
142+
// container metadata may no longer be loadable, so do not rely on a.snapshotter
143+
// alone on that path. When snapshotter is empty, a.snapshotter from the session
144+
// is used (Create-time rollback while the container record still exists).
151145
func (a *RootfsViewAccessor) Cleanup(ctx context.Context, snapshotter string) error {
152146
if a == nil {
153147
return fmt.Errorf("rootfs view accessor is nil")
154148
}
155149
if a.containerID == "" {
156150
return fmt.Errorf("container id is empty")
157151
}
152+
if snapshotter == "" {
153+
snapshotter = a.snapshotter
154+
}
158155
if snapshotter == "" {
159156
return fmt.Errorf("snapshotter is empty")
160157
}
@@ -300,7 +297,6 @@ func cleanupRootfsView(
300297
Key: rootfsViewKey(containerID),
301298
})
302299
if err = containerdErr(err); err != nil && !errdefs.IsNotFound(err) {
303-
rootfsViewLog.WithError(err).Warn("failed to remove rootfs view from containerd")
304300
return err
305301
}
306302
return cleanupRootfsViewLease(ctx, namespace, rootfsViewLeaseID(containerID), leases)
@@ -312,7 +308,6 @@ func cleanupRootfsViewLease(ctx context.Context, namespace, leaseID string, leas
312308
}
313309
_, err := leases.Delete(withNamespace(ctx, namespace), &leasesapi.DeleteRequest{ID: leaseID})
314310
if err = containerdErr(err); err != nil && !errdefs.IsNotFound(err) {
315-
rootfsViewLog.WithError(err).Warn("failed to remove rootfs view lease from containerd")
316311
return err
317312
}
318313
return nil

pkg/containerd-shim/guest_rootfs.go

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,42 +27,35 @@ import (
2727
"github.com/urunc-dev/urunc/pkg/unikontainers/types"
2828
)
2929

30-
const annotRootfsParams = "com.urunc.internal.rootfs.params"
31-
3230
var errGuestRootfsChoiceSkipped = errors.New("guest rootfs choice skipped")
3331

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

4942
var spec specs.Spec
5043
if err := json.Unmarshal(data, &spec); err != nil {
51-
return types.RootfsParams{}, fmt.Errorf("unmarshal config.json: %w", err)
44+
return types.RootfsParams{}, "", fmt.Errorf("unmarshal config.json: %w", err)
5245
}
5346
if spec.Root == nil {
54-
return types.RootfsParams{}, fmt.Errorf("invalid OCI spec: root section is required")
47+
return types.RootfsParams{}, "", fmt.Errorf("invalid OCI spec: root section is required")
5548
}
5649

5750
config, err := unikontainers.GetUnikernelConfig(filepath.Clean(r.Bundle), &spec)
5851
if err != nil {
59-
return types.RootfsParams{}, fmt.Errorf("%w: %w", errGuestRootfsChoiceSkipped, err)
52+
return types.RootfsParams{}, "", fmt.Errorf("%w: %w", errGuestRootfsChoiceSkipped, err)
6053
}
6154

6255
annotations := config.Map()
6356
uruncCfg, err := unikontainers.LoadUruncConfig(unikontainers.UruncConfigPath)
6457
if err != nil && uruncCfg == nil {
65-
return types.RootfsParams{}, err
58+
return types.RootfsParams{}, "", err
6659
}
6760

6861
rootfsParams, err := unikontainers.ChooseRootfs(
@@ -72,24 +65,12 @@ func chooseGuestRootfs(r *taskAPI.CreateTaskRequest) (types.RootfsParams, error)
7265
uruncCfg,
7366
)
7467
if err != nil {
75-
return types.RootfsParams{}, err
68+
return types.RootfsParams{}, "", err
7669
}
7770

7871
encoded, err := json.Marshal(rootfsParams)
7972
if err != nil {
80-
return types.RootfsParams{}, err
81-
}
82-
if spec.Annotations == nil {
83-
spec.Annotations = make(map[string]string)
84-
}
85-
spec.Annotations[annotRootfsParams] = string(encoded)
86-
87-
patched, err := json.MarshalIndent(spec, "", " ")
88-
if err != nil {
89-
return types.RootfsParams{}, fmt.Errorf("marshal config.json: %w", err)
90-
}
91-
if err := os.WriteFile(configPath, patched, info.Mode()); err != nil {
92-
return types.RootfsParams{}, err
73+
return types.RootfsParams{}, "", err
9374
}
94-
return rootfsParams, nil
75+
return rootfsParams, string(encoded), nil
9576
}

pkg/containerd-shim/task_service.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ import (
2727
containerdShim "github.com/urunc-dev/urunc/pkg/containerd-shim/containerd"
2828
)
2929

30+
// Internal bundle annotations (duplicated in unikontainers; keep in sync).
31+
const (
32+
annotRootfsParams = "com.urunc.internal.rootfs.params"
33+
annotRootfsView = "com.urunc.internal.rootfs.view"
34+
)
35+
3036
// taskService is urunc's shim-side wrapper around containerd's runc task
3137
// service. It wires urunc task setup before forwarding calls to the wrapped
3238
// service.
@@ -58,7 +64,7 @@ func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest)
5864
return resp, err
5965
}
6066

61-
rootfsChoice, err := chooseGuestRootfs(r)
67+
rootfsChoice, rootfsParamsJSON, err := chooseGuestRootfs(r)
6268
if err != nil {
6369
if errors.Is(err, errGuestRootfsChoiceSkipped) {
6470
log.G(ctx).WithError(err).Debug("urunc(shim): guest rootfs choice skipped")
@@ -68,17 +74,18 @@ func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest)
6874
return nil, err
6975
}
7076

71-
log.G(ctx).WithFields(map[string]any{
72-
"rootfs_type": rootfsChoice.Type,
73-
"rootfs_path": rootfsChoice.Path,
74-
"mon_rootfs": rootfsChoice.MonRootfs,
75-
}).Debug("urunc(shim): guest rootfs chosen")
76-
77+
rootfsViewJSON := ""
78+
var rootfsViewAccessor *containerdShim.RootfsViewAccessor
7779
if session != nil {
78-
rootfsViewAccessor := containerdShim.NewRootfsViewAccessor(session)
79-
if rootfsViewAccessor.ShouldPrepare(rootfsChoice) {
80-
if err := rootfsViewAccessor.Prepare(ctx, r.Bundle); err != nil {
80+
rootfsViewAccessor = containerdShim.NewRootfsViewAccessor(session)
81+
shouldPrepare, shouldPrepareErr := rootfsViewAccessor.ShouldPrepare(rootfsChoice)
82+
if shouldPrepareErr != nil {
83+
log.G(ctx).WithError(shouldPrepareErr).Warn("urunc(shim): failed to load urunc config; rootfs view disabled")
84+
} else if shouldPrepare {
85+
rootfsViewJSON, err = rootfsViewAccessor.Prepare(ctx)
86+
if err != nil {
8187
log.G(ctx).WithError(err).Warn("urunc(shim): failed to prepare rootfs view; falling back to legacy boot artifact extraction")
88+
rootfsViewJSON = ""
8289
} else {
8390
log.G(ctx).Debug("urunc(shim): rootfs view prepared")
8491
}
@@ -87,6 +94,27 @@ func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest)
8794
}
8895
}
8996

97+
var shimAnnotations map[string]string
98+
if rootfsViewJSON != "" {
99+
shimAnnotations = map[string]string{
100+
annotRootfsParams: rootfsParamsJSON,
101+
annotRootfsView: rootfsViewJSON,
102+
}
103+
} else {
104+
shimAnnotations = map[string]string{
105+
annotRootfsParams: rootfsParamsJSON,
106+
}
107+
}
108+
if err := containerdShim.PatchConfigJSON(r.Bundle, shimAnnotations); err != nil {
109+
if rootfsViewJSON != "" && rootfsViewAccessor != nil {
110+
if cleanupErr := rootfsViewAccessor.Cleanup(ctx, ""); cleanupErr != nil {
111+
log.G(ctx).WithError(cleanupErr).Warn("urunc(shim): failed to clean up rootfs view after annotation persistence failure")
112+
}
113+
}
114+
log.G(ctx).WithError(err).Warn("urunc(shim): failed to persist shim create annotations")
115+
return nil, err
116+
}
117+
90118
return resp, nil
91119
}
92120

pkg/unikontainers/rootfs_view_boot.go

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

1717
import (
18-
"encoding/json"
1918
"fmt"
2019
"os"
2120
"path/filepath"
@@ -32,37 +31,6 @@ type rootfsViewState struct {
3231
Mounts []mount.Mount `json:"mounts,omitempty"`
3332
}
3433

35-
// PatchBundleRootfsView writes shim-prepared view state into bundle config.json.
36-
func PatchBundleRootfsView(bundleDir, rootfsViewJSON string) error {
37-
configPath := filepath.Join(bundleDir, configFilename)
38-
fi, err := os.Stat(configPath)
39-
if err != nil {
40-
return fmt.Errorf("stat config.json: %w", err)
41-
}
42-
43-
spec, err := loadSpec(bundleDir)
44-
if err != nil {
45-
return fmt.Errorf("load bundle spec: %w", err)
46-
}
47-
if spec.Annotations == nil {
48-
spec.Annotations = make(map[string]string)
49-
}
50-
if rootfsViewJSON == "" {
51-
delete(spec.Annotations, rootfsViewAnnotation)
52-
} else {
53-
spec.Annotations[rootfsViewAnnotation] = rootfsViewJSON
54-
}
55-
56-
patched, err := json.MarshalIndent(spec, "", " ")
57-
if err != nil {
58-
return fmt.Errorf("marshal config.json: %w", err)
59-
}
60-
if err := os.WriteFile(configPath, patched, fi.Mode()); err != nil {
61-
return fmt.Errorf("write config.json: %w", err)
62-
}
63-
return nil
64-
}
65-
6634
func ReadBundleRootfsView(bundleDir string) (string, error) {
6735
configPath := filepath.Join(bundleDir, configFilename)
6836
if _, err := os.Stat(configPath); err != nil {

0 commit comments

Comments
 (0)