Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions pkg/cri/internal/devboxsnapshotter/labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package devboxsnapshotter

const (
StargzSnapshotter = "stargz"
SealosDevboxContentIDAnnotation = "devbox.sealos.io/content-id"
SealosDevboxStorageLimitAnnotation = "devbox.sealos.io/storage-limit"
)

// LabelsFromAnnotations keeps only the annotations used by the devbox-backed
// writable-layer flow. containerd.WithNewSnapshot will translate these labels
// to the snapshotter-specific containerd.io/snapshot/devbox-* keys when needed.
func LabelsFromAnnotations(annotations map[string]string) map[string]string {
if len(annotations) == 0 {
return nil
}

labels := make(map[string]string)
if contentID := annotations[SealosDevboxContentIDAnnotation]; contentID != "" {
labels[SealosDevboxContentIDAnnotation] = contentID
}
if storageLimit := annotations[SealosDevboxStorageLimitAnnotation]; storageLimit != "" {
labels[SealosDevboxStorageLimitAnnotation] = storageLimit
}
return labels
}
5 changes: 3 additions & 2 deletions pkg/cri/sbserver/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,12 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta
log.G(ctx).Debugf("Container %q spec: %#+v", id, spew.NewFormatter(spec))

// Grab any platform specific snapshotter opts.
sOpts := snapshotterOpts(c.config.ContainerdConfig.Snapshotter, config)
runtimeSnapshotter := c.runtimeSnapshotter(ctx, ociRuntime)
sOpts := snapshotterOpts(runtimeSnapshotter, config, sandboxConfig)

Comment on lines 209 to 212
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snapshotterOpts is now called with sandboxConfig, but the Windows implementation (pkg/cri/sbserver/container_create_windows.go) still has the old 2-arg signature. This will break Windows builds (compile-time mismatch). Update the Windows snapshotterOpts signature to accept sandboxConfig *runtime.PodSandboxConfig (unused) and adjust any callers accordingly.

Copilot uses AI. Check for mistakes.
// Set snapshotter before any other options.
opts := []containerd.NewContainerOpts{
containerd.WithSnapshotter(c.runtimeSnapshotter(ctx, ociRuntime)),
containerd.WithSnapshotter(runtimeSnapshotter),
// Prepare container rootfs. This is always writeable even if
// the container wants a readonly rootfs since we want to give
// the runtime (runc) a chance to modify (e.g. to create mount
Expand Down
14 changes: 12 additions & 2 deletions pkg/cri/sbserver/container_create_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/containerd/containerd/contrib/apparmor"
"github.com/containerd/containerd/contrib/seccomp"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/pkg/cri/internal/devboxsnapshotter"
"github.com/containerd/containerd/snapshots"

customopts "github.com/containerd/containerd/pkg/cri/opts"
Expand Down Expand Up @@ -264,6 +265,15 @@ func appArmorProfileExists(profile string) (bool, error) {
}

// snapshotterOpts returns any Linux specific snapshotter options for the rootfs snapshot
func snapshotterOpts(snapshotterName string, config *runtime.ContainerConfig) []snapshots.Opt {
return []snapshots.Opt{}
func snapshotterOpts(snapshotterName string, _ *runtime.ContainerConfig, sandboxConfig *runtime.PodSandboxConfig) []snapshots.Opt {
if !snapshotterNeedsDevboxLabels(snapshotterName) || sandboxConfig == nil {
return nil
}

labels := devboxsnapshotter.LabelsFromAnnotations(sandboxConfig.Annotations)
if len(labels) == 0 {
return nil
}

return []snapshots.Opt{snapshots.WithLabels(labels)}
}
74 changes: 74 additions & 0 deletions pkg/cri/sbserver/container_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/containerd/containerd/contrib/seccomp"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/platforms"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
Expand All @@ -42,6 +43,7 @@ import (
"github.com/containerd/containerd/pkg/cap"
"github.com/containerd/containerd/pkg/cri/annotations"
"github.com/containerd/containerd/pkg/cri/config"
"github.com/containerd/containerd/pkg/cri/internal/devboxsnapshotter"
"github.com/containerd/containerd/pkg/cri/opts"
customopts "github.com/containerd/containerd/pkg/cri/opts"
"github.com/containerd/containerd/pkg/cri/util"
Expand Down Expand Up @@ -273,6 +275,78 @@ func TestContainerCapabilities(t *testing.T) {
}
}

func TestSnapshotterOptsForDevboxLabels(t *testing.T) {
tests := []struct {
name string
snapshotter string
sandboxConfig *runtime.PodSandboxConfig
expectedLabels map[string]string
}{
{
name: "stargz keeps relevant devbox annotations",
snapshotter: devboxsnapshotter.StargzSnapshotter,
sandboxConfig: &runtime.PodSandboxConfig{
Annotations: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-1",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "20Gi",
"other.annotation": "ignored",
},
},
expectedLabels: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-1",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "20Gi",
},
},
{
name: "devbox keeps relevant devbox annotations",
snapshotter: "devbox",
sandboxConfig: &runtime.PodSandboxConfig{
Annotations: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-9",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "8Gi",
"other.annotation": "ignored",
},
},
expectedLabels: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-9",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "8Gi",
},
},
{
name: "non-stargz skips devbox labels",
snapshotter: "overlayfs",
sandboxConfig: &runtime.PodSandboxConfig{
Annotations: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-2",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "10Gi",
},
},
expectedLabels: nil,
},
{
name: "nil sandbox config produces no opts",
snapshotter: devboxsnapshotter.StargzSnapshotter,
sandboxConfig: nil,
expectedLabels: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := snapshotterOpts(tt.snapshotter, &runtime.ContainerConfig{}, tt.sandboxConfig)
if tt.expectedLabels == nil {
require.Len(t, opts, 0)
return
}

require.Len(t, opts, 1)
info := &snapshots.Info{Labels: make(map[string]string)}
opts[0](info)
assert.Equal(t, tt.expectedLabels, info.Labels)
})
}
}

func TestContainerSpecTty(t *testing.T) {
testID := "test-id"
testSandboxID := "sandbox-id"
Expand Down
2 changes: 1 addition & 1 deletion pkg/cri/sbserver/container_create_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ func (c *criService) containerSpecOpts(config *runtime.ContainerConfig, imageCon
}

// snapshotterOpts returns snapshotter options for the rootfs snapshot
func snapshotterOpts(snapshotterName string, config *runtime.ContainerConfig) []snapshots.Opt {
func snapshotterOpts(snapshotterName string, config *runtime.ContainerConfig, sandboxConfig *runtime.PodSandboxConfig) []snapshots.Opt {
return []snapshots.Opt{}
}
30 changes: 30 additions & 0 deletions pkg/cri/sbserver/devbox_snapshotter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sbserver

import "github.com/containerd/containerd/pkg/cri/internal/devboxsnapshotter"

const devboxSnapshotter = "devbox"

func snapshotterNeedsDevboxLabels(name string) bool {
switch name {
case devboxSnapshotter, devboxsnapshotter.StargzSnapshotter:
return true
default:
return false
}
}
10 changes: 6 additions & 4 deletions pkg/cri/server/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,11 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta
return nil, err
}

// Check if the snapshotter is devbox and add the devbox snapshotter opts
if c.runtimeSnapshotter(ctx, ociRuntime) == "devbox" {
devboxOpt, err := devboxSnapshotterOpts(c.runtimeSnapshotter(ctx, ociRuntime), r.GetSandboxConfig())
// Add devbox snapshot labels for snapshotters that support the devbox
// writable-layer flow.
runtimeSnapshotter := c.runtimeSnapshotter(ctx, ociRuntime)
if isDevboxWritableSnapshotter(runtimeSnapshotter) {
devboxOpt, err := devboxSnapshotterOpts(runtimeSnapshotter, r.GetSandboxConfig())
if err != nil {
return nil, err
}
Expand All @@ -202,7 +204,7 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta

// Set snapshotter before any other options.
opts := []containerd.NewContainerOpts{
containerd.WithSnapshotter(c.runtimeSnapshotter(ctx, ociRuntime)),
containerd.WithSnapshotter(runtimeSnapshotter),
// Prepare container rootfs. This is always writeable even if
// the container wants a readonly rootfs since we want to give
// the runtime (runc) a chance to modify (e.g. to create mount
Expand Down
13 changes: 9 additions & 4 deletions pkg/cri/server/container_create_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,9 +612,14 @@ func generateUserString(username string, uid, gid *runtime.Int64Value) (string,
}

// snapshotterOpts returns any Linux specific snapshotter options for the rootfs snapshot
func devboxSnapshotterOpts(snapshotterName string, config *runtime.PodSandboxConfig) (snapshots.Opt, error) {
// fmt.Printf("devboxSnapshotterOpts: snapshotterName=%s, config=%+v\n", snapshotterName, config)
// add container annotations to snapshot labels
func devboxSnapshotterOpts(_ string, config *runtime.PodSandboxConfig) (snapshots.Opt, error) {
if config == nil || len(config.Annotations) == 0 {
return nil, nil
}

// Preserve the original devbox annotations here and let
// containerd.WithNewSnapshot translate them for snapshotters that consume
// containerd.io/snapshot/devbox-* labels.
labels := make(map[string]string)
maps.Copy(labels, config.Annotations)
return snapshots.WithLabels(labels), nil
Comment on lines +620 to 625
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

devboxSnapshotterOpts currently copies all sandbox annotations into snapshot labels (maps.Copy(labels, config.Annotations)). Only devbox.sealos.io/* keys are meant for the devbox writable-layer flow (and containerd.WithNewSnapshot only translates those), so this can unintentionally propagate unrelated annotations into snapshot metadata. Filter to just the devbox annotations (e.g., via devboxsnapshotter.LabelsFromAnnotations or by prefix) before returning snapshots.WithLabels.

Copilot uses AI. Check for mistakes.
Expand All @@ -634,7 +639,7 @@ func snapshotterOpts(snapshotterName string, config *runtime.ContainerConfig, sa
uid := sandboxConfig.Annotations[devboxUIDEnvKey]
if uid != "" {
snapshotOpts = append(snapshotOpts, snapshots.WithLabels(map[string]string{
"devbox.sealos.io/uid": uid,
devboxUIDEnvKey: uid,
}))
}
return snapshotOpts, nil
Expand Down
49 changes: 49 additions & 0 deletions pkg/cri/server/container_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/containerd/containerd/pkg/cap"
"github.com/containerd/containerd/pkg/cri/annotations"
"github.com/containerd/containerd/pkg/cri/config"
"github.com/containerd/containerd/pkg/cri/internal/devboxsnapshotter"
"github.com/containerd/containerd/pkg/cri/opts"
customopts "github.com/containerd/containerd/pkg/cri/opts"
"github.com/containerd/containerd/pkg/cri/util"
Expand Down Expand Up @@ -2285,3 +2286,51 @@ func TestSnapshotterOpts(t *testing.T) {
})
}
}

func TestDevboxSnapshotterOpts(t *testing.T) {
tests := []struct {
name string
snapshotter string
annotations map[string]string
expectedLabels map[string]string
}{
{
name: "stargz keeps original annotations",
snapshotter: devboxsnapshotter.StargzSnapshotter,
annotations: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-1",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "20Gi",
},
Comment on lines +2298 to +2303
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestDevboxSnapshotterOpts only asserts a round-trip when the input annotations map contains only devbox keys, so it won’t catch accidental propagation of unrelated sandbox annotations into snapshot labels. Add a non-devbox annotation (e.g. "other.annotation") to one case and assert it is not present in info.Labels to cover the intended filtering behavior.

Copilot uses AI. Check for mistakes.
expectedLabels: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-1",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "20Gi",
},
},
{
name: "devbox snapshotter keeps original annotations only",
snapshotter: DevboxSnapshotter,
annotations: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-3",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "5Gi",
},
expectedLabels: map[string]string{
devboxsnapshotter.SealosDevboxContentIDAnnotation: "workspace-3",
devboxsnapshotter.SealosDevboxStorageLimitAnnotation: "5Gi",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opt, err := devboxSnapshotterOpts(tt.snapshotter, &runtime.PodSandboxConfig{
Annotations: tt.annotations,
})
require.NoError(t, err)
require.NotNil(t, opt)

info := &snapshots.Info{Labels: make(map[string]string)}
opt(info)
assert.Equal(t, tt.expectedLabels, info.Labels)
})
}
}
5 changes: 5 additions & 0 deletions pkg/cri/server/container_create_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ func (c *criService) containerSpecOpts(config *runtime.ContainerConfig, imageCon
func snapshotterOpts(snapshotterName string, config *runtime.ContainerConfig, sandboxConfig *runtime.PodSandboxConfig) ([]snapshots.Opt, error) {
return []snapshots.Opt{}, nil
}

// Non-Linux builds don't support the devbox-backed writable-layer flow.
func devboxSnapshotterOpts(snapshotterName string, config *runtime.PodSandboxConfig) (snapshots.Opt, error) {
return nil, nil
}
2 changes: 1 addition & 1 deletion pkg/cri/server/container_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (c *criService) StopContainer(ctx context.Context, r *runtime.StopContainer
log.G(ctx).Infof("Check snapshotter: %s", snapshotter)

// Check if the snapshotter is devbox and update the devbox snapshot
if snapshotter == "devbox" {
if isDevboxWritableSnapshotter(snapshotter) {
err = c.client.UpdateDevboxSnapshot(ctx, snapshotter, i.ID, unmountLvm, "true")
if err != nil {
log.G(ctx).WithError(err).Errorf("Failed to update devbox snapshot: %s", err)
Expand Down
28 changes: 28 additions & 0 deletions pkg/cri/server/devbox_snapshotter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package server

import "github.com/containerd/containerd/pkg/cri/internal/devboxsnapshotter"

func isDevboxWritableSnapshotter(name string) bool {
switch name {
case DevboxSnapshotter, devboxsnapshotter.StargzSnapshotter:
return true
default:
return false
}
}
2 changes: 1 addition & 1 deletion pkg/cri/server/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ func handleContainerExit(ctx context.Context, e *eventtypes.TaskExit, cntr conta
return status, err
}
fmt.Println("Container snapshotter:", container.Snapshotter, "ID:", cntr.Container.ID())
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Println in the event handler writes directly to stdout on every container exit, which can create noisy logs and bypass the configured logger. Replace this with structured logging (e.g. logrus.WithFields(...).Debug(...) / log.G(ctx)), or remove it if it was only for debugging.

Suggested change
fmt.Println("Container snapshotter:", container.Snapshotter, "ID:", cntr.Container.ID())
logrus.WithFields(logrus.Fields{
"snapshotter": container.Snapshotter,
"containerID": cntr.Container.ID(),
}).Debug("Container snapshotter information on exit")

Copilot uses AI. Check for mistakes.
if container.Snapshotter == "devbox" {
if isDevboxWritableSnapshotter(container.Snapshotter) {
err = c.client.UpdateDevboxSnapshot(ctx, container.Snapshotter, container.ID, unmountLvm, "true")
if err != nil {
logrus.WithError(err).Errorf("Failed to update devbox snapshot for container %s", cntr.Container.ID())
Expand Down
2 changes: 1 addition & 1 deletion pkg/cri/server/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const (
// runtimeRunhcsV1 is the runtime type for runhcs.
runtimeRunhcsV1 = "io.containerd.runhcs.v1"

// DevboxSnapshotter is the name of the devbox snapshotter.
// DevboxSnapshotter is the name of the classic devbox snapshotter.
DevboxSnapshotter = "devbox"
)

Expand Down
Loading