Skip to content

Commit 2da344c

Browse files
committed
feat(shim): implement containerd Task.Update for cgroup resize
Wire shim Task.Update to runsc update --resources (same contract as go-runc). Add proc and runsc tests for validation; add runsccmd Linux test for CLI stdin/argv. Closes #12790
1 parent e2588d7 commit 2da344c

11 files changed

Lines changed: 251 additions & 3 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/deleted_state.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/containerd/console"
2323
"github.com/containerd/errdefs"
2424
runc "github.com/containerd/go-runc"
25+
google_protobuf "github.com/gogo/protobuf/types"
2526
"gvisor.dev/gvisor/pkg/shim/v1/extension"
2627
)
2728

@@ -57,3 +58,7 @@ func (s *deletedState) State(context.Context) (string, error) {
5758
func (s *deletedState) Stats(context.Context, string) (*runc.Stats, error) {
5859
return nil, fmt.Errorf("cannot stat a stopped container/process")
5960
}
61+
62+
func (*deletedState) Update(context.Context, *google_protobuf.Any) error {
63+
return fmt.Errorf("cannot update a deleted container/process: %w", errdefs.ErrNotFound)
64+
}

pkg/shim/v1/proc/init.go

Lines changed: 21 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,26 @@ 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 a containerd task Update request (JSON
457+
// LinuxResources in the protobuf Any), matching containerd/pkg/process Init.Update.
458+
func (p *Init) Update(ctx context.Context, r *google_protobuf.Any) error {
459+
p.mu.Lock()
460+
defer p.mu.Unlock()
461+
462+
return p.initState.Update(ctx, r)
463+
}
464+
465+
func (p *Init) update(ctx context.Context, r *google_protobuf.Any) error {
466+
if r == nil {
467+
return fmt.Errorf("resources are required: %w", errdefs.ErrInvalidArgument)
468+
}
469+
var resources specs.LinuxResources
470+
if err := json.Unmarshal(r.Value, &resources); err != nil {
471+
return fmt.Errorf("decoding resources: %w", err)
472+
}
473+
return p.runtime.Update(ctx, p.id, &resources)
474+
}
475+
455476
func (p *Init) stats(ctx context.Context, id string) (*runc.Stats, error) {
456477
return p.Runtime().Stats(ctx, id)
457478
}

pkg/shim/v1/proc/init_state.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/containerd/errdefs"
2323
runc "github.com/containerd/go-runc"
24+
google_protobuf "github.com/gogo/protobuf/types"
2425
"golang.org/x/sys/unix"
2526

2627
"gvisor.dev/gvisor/pkg/shim/v1/extension"
@@ -57,6 +58,7 @@ type initState interface {
5758
Stats(context.Context, string) (*runc.Stats, error)
5859
Kill(context.Context, uint32, bool) error
5960
SetExited(int)
61+
Update(context.Context, *google_protobuf.Any) error
6062
}
6163

6264
type createdState struct {
@@ -132,6 +134,10 @@ func (s *createdState) Stats(ctx context.Context, id string) (*runc.Stats, error
132134
return s.p.stats(ctx, id)
133135
}
134136

137+
func (s *createdState) Update(ctx context.Context, r *google_protobuf.Any) error {
138+
return s.p.update(ctx, r)
139+
}
140+
135141
type runningState struct {
136142
p *Init
137143
}
@@ -182,6 +188,10 @@ func (s *runningState) Stats(ctx context.Context, id string) (*runc.Stats, error
182188
return s.p.stats(ctx, id)
183189
}
184190

191+
func (s *runningState) Update(ctx context.Context, r *google_protobuf.Any) error {
192+
return s.p.update(ctx, r)
193+
}
194+
185195
type stoppedState struct {
186196
process *Init
187197
}
@@ -231,6 +241,10 @@ func (s *stoppedState) Stats(context.Context, string) (*runc.Stats, error) {
231241
return nil, fmt.Errorf("cannot stat a stopped container")
232242
}
233243

244+
func (s *stoppedState) Update(context.Context, *google_protobuf.Any) error {
245+
return fmt.Errorf("cannot update a stopped container: %w", errdefs.ErrFailedPrecondition)
246+
}
247+
234248
func handleStoppedKill(signal uint32) error {
235249
switch unix.Signal(signal) {
236250
case unix.SIGTERM, unix.SIGKILL:

pkg/shim/v1/proc/update_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
"context"
19+
"encoding/json"
20+
"errors"
21+
"strings"
22+
"testing"
23+
24+
"github.com/containerd/containerd/pkg/stdio"
25+
"github.com/containerd/errdefs"
26+
"github.com/gogo/protobuf/types"
27+
specs "github.com/opencontainers/runtime-spec/specs-go"
28+
"gvisor.dev/gvisor/pkg/shim/v1/runsccmd"
29+
)
30+
31+
func TestInitUpdateNilAny(t *testing.T) {
32+
p := New("id", &runsccmd.Runsc{}, stdio.Stdio{})
33+
p.initState = &runningState{p: p}
34+
err := p.Update(context.Background(), nil)
35+
if !errors.Is(err, errdefs.ErrInvalidArgument) {
36+
t.Fatalf("Update(nil): %v, want ErrInvalidArgument", err)
37+
}
38+
}
39+
40+
func TestInitUpdateInvalidJSON(t *testing.T) {
41+
p := New("id", &runsccmd.Runsc{}, stdio.Stdio{})
42+
p.initState = &runningState{p: p}
43+
err := p.Update(context.Background(), &types.Any{Value: []byte(`{`)})
44+
if err == nil || !strings.Contains(err.Error(), "decoding resources") {
45+
t.Fatalf("Update(bad JSON): %v", err)
46+
}
47+
}
48+
49+
func TestInitUpdateStoppedState(t *testing.T) {
50+
p := New("id", &runsccmd.Runsc{}, stdio.Stdio{})
51+
p.initState = &stoppedState{process: p}
52+
empty, err := json.Marshal(&specs.LinuxResources{})
53+
if err != nil {
54+
t.Fatal(err)
55+
}
56+
err = p.Update(context.Background(), &types.Any{Value: empty})
57+
if !errors.Is(err, errdefs.ErrFailedPrecondition) {
58+
t.Fatalf("Update(stopped): %v, want ErrFailedPrecondition", err)
59+
}
60+
}

pkg/shim/v1/runsc/container.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,22 @@ 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 (containerd Task.Update).
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+
initp, ok := p.(*proc.Init)
228+
if !ok {
229+
return fmt.Errorf("update requires init process: %w", errdefs.ErrFailedPrecondition)
230+
}
231+
return initp.Update(ctx, r.Resources)
232+
}
233+
218234
// Restore a process in the container.
219235
func (c *Container) Restore(ctx context.Context, r *extension.RestoreRequest) (extension.Process, error) {
220236
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,24 @@
1515
package runsc
1616

1717
import (
18+
"context"
19+
"errors"
1820
"testing"
1921

22+
"github.com/containerd/containerd/runtime/v2/task"
23+
"github.com/containerd/errdefs"
2024
specs "github.com/opencontainers/runtime-spec/specs-go"
2125
"gvisor.dev/gvisor/pkg/shim/v1/utils"
2226
)
2327

28+
func TestContainerUpdateNilResources(t *testing.T) {
29+
c := &Container{}
30+
err := c.Update(context.Background(), &task.UpdateTaskRequest{ID: "x", Resources: nil})
31+
if !errors.Is(err, errdefs.ErrInvalidArgument) {
32+
t.Fatalf("Update(nil Resources): %v, want ErrInvalidArgument", err)
33+
}
34+
}
35+
2436
func TestCgroupPath(t *testing.T) {
2537
for _, tc := range []struct {
2638
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,18 @@ func (r *Runsc) Resume(context context.Context, id string) error {
205205
return nil
206206
}
207207

208+
// Update applies cgroup resource changes to a created or running container
209+
// (OCI runtime update). This matches github.com/containerd/go-runc Update.
210+
func (r *Runsc) Update(ctx context.Context, id string, resources *specs.LinuxResources) error {
211+
buf := new(bytes.Buffer)
212+
if err := json.NewEncoder(buf).Encode(resources); err != nil {
213+
return err
214+
}
215+
cmd := r.command(ctx, append([]string{"update", "--resources", "-", id})...)
216+
cmd.Stdin = buf
217+
return r.runOrError(cmd)
218+
}
219+
208220
// Start will start an already created container.
209221
func (r *Runsc) Start(context context.Context, id string, cio runc.IO) error {
210222
return r.start(context, cio, r.command(context, "start", id))

0 commit comments

Comments
 (0)