Skip to content

Commit 8c655a0

Browse files
committed
feat(shim): implement containerd Task.Update for cgroup resize
Wire shim Task.Update to runsc update with OCI LinuxResources JSON. Extend runsc Container.Update for workload containers via compat cgroup alongside the root sandbox cgroup. Consolidate runsc update CLI on the shared merge path. Add criutil helper and crictl integration coverage.
1 parent c85244f commit 8c655a0

11 files changed

Lines changed: 299 additions & 4 deletions

File tree

pkg/shim/v1/proc/BUILD

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tools:defs.bzl", "go_library")
1+
load("//tools:defs.bzl", "go_library", "go_test")
22

33
package(
44
default_applicable_licenses = ["//:license"],
@@ -40,3 +40,10 @@ go_library(
4040
"@org_golang_x_sys//unix:go_default_library",
4141
],
4242
)
43+
44+
go_test(
45+
name = "proc_test",
46+
size = "small",
47+
srcs = ["update_test.go"],
48+
library = ":proc",
49+
)

pkg/shim/v1/proc/init.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
"github.com/containerd/fifo"
3636
runc "github.com/containerd/go-runc"
37+
google_protobuf "github.com/gogo/protobuf/types"
3738
specs "github.com/opencontainers/runtime-spec/specs-go"
3839
"golang.org/x/sys/unix"
3940
"gvisor.dev/gvisor/pkg/shim/v1/extension"
@@ -452,6 +453,21 @@ func (p *Init) Stats(ctx context.Context, id string) (*runc.Stats, error) {
452453
return p.initState.Stats(ctx, id)
453454
}
454455

456+
// Update applies resource changes from JSON LinuxResources in the protobuf Any.
457+
func (p *Init) Update(ctx context.Context, r *google_protobuf.Any) error {
458+
p.mu.Lock()
459+
defer p.mu.Unlock()
460+
461+
if r == nil {
462+
return fmt.Errorf("resources are required: %w", errdefs.ErrInvalidArgument)
463+
}
464+
var resources specs.LinuxResources
465+
if err := json.Unmarshal(r.Value, &resources); err != nil {
466+
return fmt.Errorf("decoding resources: %w", err)
467+
}
468+
return p.runtime.Update(ctx, p.id, &resources)
469+
}
470+
455471
func (p *Init) stats(ctx context.Context, id string) (*runc.Stats, error) {
456472
return p.Runtime().Stats(ctx, id)
457473
}

pkg/shim/v1/proc/update_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2026 The gVisor Authors.
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+
// https://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 proc
16+
17+
import (
18+
"errors"
19+
"strings"
20+
"testing"
21+
22+
"github.com/containerd/containerd/pkg/stdio"
23+
"github.com/containerd/errdefs"
24+
"github.com/gogo/protobuf/types"
25+
"gvisor.dev/gvisor/pkg/shim/v1/runsccmd"
26+
)
27+
28+
func TestInitUpdateNilAny(t *testing.T) {
29+
p := New("id", &runsccmd.Runsc{}, stdio.Stdio{})
30+
p.initState = &runningState{p: p}
31+
err := p.Update(t.Context(), nil)
32+
if !errors.Is(err, errdefs.ErrInvalidArgument) {
33+
t.Fatalf("Update(nil): %v, want ErrInvalidArgument", err)
34+
}
35+
}
36+
37+
func TestInitUpdateInvalidJSON(t *testing.T) {
38+
p := New("id", &runsccmd.Runsc{}, stdio.Stdio{})
39+
p.initState = &runningState{p: p}
40+
err := p.Update(t.Context(), &types.Any{Value: []byte(`{`)})
41+
if err == nil || !strings.Contains(err.Error(), "decoding resources") {
42+
t.Fatalf("Update(bad JSON): %v", err)
43+
}
44+
}
45+

pkg/shim/v1/runsc/container.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ func (c *Container) CloseIO(ctx context.Context, r *task.CloseIORequest) error {
215215
return nil
216216
}
217217

218+
// Update applies cgroup resource limits for the init task.
219+
func (c *Container) Update(ctx context.Context, r *task.UpdateTaskRequest) error {
220+
if r.Resources == nil {
221+
return fmt.Errorf("resources are required: %w", errdefs.ErrInvalidArgument)
222+
}
223+
p, err := c.Process("")
224+
if err != nil {
225+
return err
226+
}
227+
return p.(*proc.Init).Update(ctx, r.Resources)
228+
}
229+
218230
// Restore a process in the container.
219231
func (c *Container) Restore(ctx context.Context, r *extension.RestoreRequest) (extension.Process, error) {
220232
p, err := c.Process(r.Start.ExecID)

pkg/shim/v1/runsc/service.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,14 @@ func (s *runscService) getV2Stats(stats *runc.Stats, r *taskAPI.StatsRequest) (*
766766

767767
// Update updates a running container.
768768
func (s *runscService) Update(ctx context.Context, r *taskAPI.UpdateTaskRequest) (*types.Empty, error) {
769-
return empty, errdefs.ErrNotImplemented
769+
c, err := s.getContainer(r.ID)
770+
if err != nil {
771+
return nil, err
772+
}
773+
if err := c.Update(ctx, r); err != nil {
774+
return nil, errdefs.ToGRPC(err)
775+
}
776+
return empty, nil
770777
}
771778

772779
// Wait waits for the container to exit.

pkg/shim/v1/runsc/service_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,23 @@
1515
package runsc
1616

1717
import (
18+
"errors"
1819
"testing"
1920

21+
"github.com/containerd/containerd/runtime/v2/task"
22+
"github.com/containerd/errdefs"
2023
specs "github.com/opencontainers/runtime-spec/specs-go"
2124
"gvisor.dev/gvisor/pkg/shim/v1/utils"
2225
)
2326

27+
func TestContainerUpdateNilResources(t *testing.T) {
28+
c := &Container{}
29+
err := c.Update(t.Context(), &task.UpdateTaskRequest{ID: "x", Resources: nil})
30+
if !errors.Is(err, errdefs.ErrInvalidArgument) {
31+
t.Fatalf("Update(nil Resources): %v, want ErrInvalidArgument", err)
32+
}
33+
}
34+
2435
func TestCgroupPath(t *testing.T) {
2536
for _, tc := range []struct {
2637
name string

pkg/shim/v1/runsccmd/BUILD

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tools:defs.bzl", "go_library")
1+
load("//tools:defs.bzl", "go_library", "go_test")
22

33
package(
44
default_applicable_licenses = ["//:license"],
@@ -19,3 +19,10 @@ go_library(
1919
"@org_golang_x_sys//unix:go_default_library",
2020
],
2121
)
22+
23+
go_test(
24+
name = "runsccmd_test",
25+
size = "small",
26+
srcs = ["runsc_test.go"],
27+
library = ":runsccmd",
28+
)

pkg/shim/v1/runsccmd/runsc.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,21 @@ func (r *Runsc) Resume(context context.Context, id string) error {
205205
return nil
206206
}
207207

208+
// Update the current container with the provided resource spec
209+
// The "-" path reads resources JSON from stdin.
210+
func (r *Runsc) Update(ctx context.Context, id string, resources *specs.LinuxResources) error {
211+
buf := getBuf()
212+
defer putBuf(buf)
213+
214+
if err := json.NewEncoder(buf).Encode(resources); err != nil {
215+
return err
216+
}
217+
args := []string{"update", "--resources=-", id}
218+
cmd := r.command(ctx, args...)
219+
cmd.Stdin = buf
220+
return r.runOrError(cmd)
221+
}
222+
208223
// Start will start an already created container.
209224
func (r *Runsc) Start(context context.Context, id string, cio runc.IO) error {
210225
return r.start(context, cio, r.command(context, "start", id))

pkg/shim/v1/runsccmd/runsc_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2026 The gVisor Authors.
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+
// https://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+
//go:build linux
16+
17+
package runsccmd
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
"strings"
25+
"testing"
26+
27+
specs "github.com/opencontainers/runtime-spec/specs-go"
28+
)
29+
30+
// TestRunscUpdateCLI verifies Update runs the OCI update flow: JSON resources on
31+
// stdin and "update --resources - <id>" in argv (same contract as go-runc).
32+
func TestRunscUpdateCLI(t *testing.T) {
33+
ctx := t.Context()
34+
dir := t.TempDir()
35+
argsLog := filepath.Join(dir, "args.txt")
36+
stdinLog := filepath.Join(dir, "stdin.txt")
37+
script := filepath.Join(dir, "fake-runsc")
38+
scriptBody := fmt.Sprintf(`#!/bin/sh
39+
printf '%%s\n' "$*" >%q
40+
cat >%q
41+
exit 0
42+
`, argsLog, stdinLog)
43+
if err := os.WriteFile(script, []byte(scriptBody), 0o755); err != nil {
44+
t.Fatal(err)
45+
}
46+
47+
limit := int64(4096)
48+
wantRes := &specs.LinuxResources{
49+
Memory: &specs.LinuxMemory{Limit: &limit},
50+
}
51+
52+
r := &Runsc{
53+
Command: script,
54+
Root: filepath.Join(dir, "root"),
55+
}
56+
if err := r.Update(ctx, "cid-1", wantRes); err != nil {
57+
t.Fatalf("Update: %v", err)
58+
}
59+
60+
rawArgs, err := os.ReadFile(argsLog)
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
args := strings.Fields(string(rawArgs))
65+
var joined strings.Builder
66+
for _, a := range args {
67+
joined.WriteString(a)
68+
joined.WriteByte(' ')
69+
}
70+
s := joined.String()
71+
// go-runc may pass "--resources -"; we use "--resources=-" (same stdin contract).
72+
if !strings.Contains(s, "update ") || !strings.Contains(s, "--resources") || !strings.Contains(s, "cid-1") {
73+
t.Fatalf("argv missing expected update flags: %q", string(rawArgs))
74+
}
75+
76+
rawStdin, err := os.ReadFile(stdinLog)
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
var got specs.LinuxResources
81+
if err := json.Unmarshal(rawStdin, &got); err != nil {
82+
t.Fatalf("stdin JSON: %v", err)
83+
}
84+
if got.Memory == nil || got.Memory.Limit == nil || *got.Memory.Limit != limit {
85+
t.Fatalf("stdin resources: got %+v, want memory limit %d", got.Memory, limit)
86+
}
87+
}

pkg/test/criutil/criutil.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ func (cc *Crictl) Create(podID, contSpecFile, sbSpecFile string) (string, error)
110110
if err != nil {
111111
return "", err
112112
}
113-
r := regexp.MustCompile(`crictl version ([0-9]+)\.([0-9]+)\.([0-9+])`)
113+
// Newer crictl prints "version v1.2.3"; older prints "version 1.2.3".
114+
r := regexp.MustCompile(`crictl version v?([0-9]+)\.([0-9]+)\.([0-9]+)`)
114115
vs := r.FindStringSubmatch(out)
115116
if len(vs) != 4 {
116117
return "", fmt.Errorf("crictl -v had unexpected output: %s", out)
@@ -151,6 +152,26 @@ func (cc *Crictl) Start(contID string) (string, error) {
151152
return output, nil
152153
}
153154

155+
// UpdateContainerResources runs `crictl update` to set the Linux memory limit
156+
// (CRI UpdateContainerResources). memoryLimitBytes must be positive.
157+
func (cc *Crictl) UpdateContainerResources(contID string, memoryLimitBytes int64) error {
158+
if memoryLimitBytes <= 0 {
159+
return fmt.Errorf("memory limit must be positive")
160+
}
161+
_, err := cc.run("update", "--memory", strconv.FormatInt(memoryLimitBytes, 10), contID)
162+
return err
163+
}
164+
165+
// InspectGoTemplate runs `crictl inspect` with a Go template and returns the
166+
// trimmed output.
167+
func (cc *Crictl) InspectGoTemplate(contID, tmpl string) (string, error) {
168+
out, err := cc.run("inspect", "-o", "go-template", "--template", tmpl, contID)
169+
if err != nil {
170+
return "", err
171+
}
172+
return strings.TrimSpace(out), nil
173+
}
174+
154175
// Stop stops a container. It corresponds to `crictl stop`.
155176
func (cc *Crictl) Stop(contID string) error {
156177
_, err := cc.run("stop", contID)

0 commit comments

Comments
 (0)