Skip to content

Commit 941b6f0

Browse files
runsc: add update command
This change enables updating resource constraints of running containers. It is the runsc equivalent of runc update.
1 parent d06b27e commit 941b6f0

File tree

12 files changed

+648
-77
lines changed

12 files changed

+648
-77
lines changed

pkg/test/dockerutil/container.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,14 @@ func (c *Container) CleanUp(ctx context.Context) {
712712
c.mounts = nil
713713
}
714714

715+
// Update is analogous to 'docker update'.
716+
func (c *Container) Update(ctx context.Context, updateConfig container.UpdateConfig) error {
717+
if _, err := c.client.ContainerUpdate(ctx, c.id, updateConfig); err != nil {
718+
return fmt.Errorf("updating container %s: %v", c.id, err)
719+
}
720+
return nil
721+
}
722+
715723
// ContainerPool represents a pool of reusable containers.
716724
// Callers may request a container from the pool, and must release it back
717725
// when they are done with it.

runsc/cgroup/cgroup.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ func loadPathsHelper(cgroup, mountinfo io.Reader, unified bool) (map[string]stri
316316
// Cgroup represents a cgroup configuration.
317317
type Cgroup interface {
318318
Install(res *specs.LinuxResources) error
319+
Update(res *specs.LinuxResources) error
319320
Uninstall() error
320321
Join() (func(), error)
321322
CPUQuota() (float64, error)
@@ -542,6 +543,34 @@ func (c *cgroupV1) Install(res *specs.LinuxResources) error {
542543
return nil
543544
}
544545

546+
// Update updates the cgroup resources.
547+
func (c *cgroupV1) Update(res *specs.LinuxResources) error {
548+
log.Debugf("Updating cgroup resources for %q", c.Name)
549+
for key, ctrlr := range controllers {
550+
if !c.Own[key] {
551+
// cgroup is managed by caller, don't touch it.
552+
continue
553+
}
554+
path := c.MakePath(key)
555+
if _, err := os.Stat(path); err != nil {
556+
if os.IsNotExist(err) && ctrlr.optional() {
557+
if err := ctrlr.skip(res); err != nil {
558+
return err
559+
}
560+
log.Infof("Skipping cgroup %q, path doesn't exist", key)
561+
continue
562+
}
563+
return fmt.Errorf("failed to stat cgroup %q: %w", key, err)
564+
}
565+
566+
if err := ctrlr.set(res, path); err != nil {
567+
return fmt.Errorf("failed to set %q cgroup: %w", key, err)
568+
}
569+
}
570+
571+
return nil
572+
}
573+
545574
// createController creates the controller directory, checking that the
546575
// controller is enabled in the system. It returns a boolean indicating whether
547576
// the controller should be skipped (e.g. controller is disabled). In case it

runsc/cgroup/cgroup_v2.go

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -139,34 +139,44 @@ func (c *cgroupV2) Install(res *specs.LinuxResources) error {
139139
return err
140140
}
141141
if created {
142-
// If we created our final cgroup path then we can set the resources.
143-
for controllerName, ctrlr := range controllers2 {
144-
// First check if our controller is found in the system.
145-
found := false
146-
for _, knownController := range c.Controllers {
147-
if controllerName == knownController {
148-
found = true
149-
}
142+
if err := c.Update(res); err != nil {
143+
return err
144+
}
145+
}
146+
147+
clean.Release()
148+
return nil
149+
}
150+
151+
// Update updates the cgroup resources.
152+
func (c *cgroupV2) Update(res *specs.LinuxResources) error {
153+
path := c.MakePath("")
154+
log.Debugf("Updating cgroup resources for %q", path)
155+
for controllerName, ctrlr := range controllers2 {
156+
// First check if our controller is found in the system.
157+
found := false
158+
for _, knownController := range c.Controllers {
159+
if controllerName == knownController {
160+
found = true
161+
break
150162
}
163+
}
151164

152-
// In case we don't have the controller.
153-
if found {
154-
if err := ctrlr.set(res, c.MakePath("")); err != nil {
155-
return err
156-
}
157-
continue
165+
// In case we don't have the controller.
166+
if found {
167+
if err := ctrlr.set(res, path); err != nil {
168+
return err
158169
}
159-
if ctrlr.optional() {
160-
if err := ctrlr.skip(res); err != nil {
161-
return err
162-
}
163-
} else {
164-
return fmt.Errorf("mandatory cgroup controller %q is missing for %q", controllerName, c.MakePath(""))
170+
continue
171+
}
172+
if ctrlr.optional() {
173+
if err := ctrlr.skip(res); err != nil {
174+
return err
165175
}
176+
} else {
177+
return fmt.Errorf("mandatory cgroup controller %q is missing for %q", controllerName, path)
166178
}
167179
}
168-
169-
clean.Release()
170180
return nil
171181
}
172182

@@ -540,7 +550,7 @@ func (*memory2) set(spec *specs.LinuxResources, path string) error {
540550

541551
swap, err := convertMemorySwapToCgroupV2Value(*spec.Memory.Swap, *spec.Memory.Limit)
542552
if err != nil {
543-
return nil
553+
return err
544554
}
545555
swapStr := numToStr(swap)
546556
// memory and memorySwap set to the same value -- disable swap

runsc/cgroup/cgroup_v2_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,88 @@ func TestParseCPUQuota(t *testing.T) {
432432
}
433433
}
434434
}
435+
436+
func TestUpdate(t *testing.T) {
437+
for _, tc := range []struct {
438+
name string
439+
initialCPUMax string
440+
initialMemMax string
441+
updatedResources *specs.LinuxResources
442+
wantCPUQuota float64
443+
wantMemory uint64
444+
}{
445+
{
446+
name: "update cpu and memory",
447+
initialCPUMax: "100 100",
448+
initialMemMax: "1024",
449+
updatedResources: &specs.LinuxResources{
450+
CPU: &specs.LinuxCPU{
451+
Quota: int64Ptr(100),
452+
Period: uint64Ptr(50),
453+
},
454+
Memory: &specs.LinuxMemory{
455+
Limit: int64Ptr(2048),
456+
},
457+
},
458+
wantCPUQuota: 2.0,
459+
wantMemory: 2048,
460+
},
461+
{
462+
name: "update cpu only",
463+
initialCPUMax: "100 100",
464+
initialMemMax: "1024",
465+
updatedResources: &specs.LinuxResources{
466+
CPU: &specs.LinuxCPU{
467+
Quota: int64Ptr(150),
468+
Period: uint64Ptr(50),
469+
},
470+
},
471+
wantCPUQuota: 3.0,
472+
wantMemory: 1024,
473+
},
474+
} {
475+
t.Run(tc.name, func(t *testing.T) {
476+
dir, err := os.MkdirTemp(testutil.TmpDir(), "cgroup")
477+
if err != nil {
478+
t.Fatalf("error creating temporary directory: %v", err)
479+
}
480+
defer os.RemoveAll(dir)
481+
482+
cg := &cgroupV2{
483+
Mountpoint: dir,
484+
Path: "user.slice",
485+
Controllers: mandatoryControllers,
486+
}
487+
if err := os.MkdirAll(filepath.Join(cg.Mountpoint, cg.Path), 0o777); err != nil {
488+
t.Fatalf("os.MkdirAll(): %v", err)
489+
}
490+
491+
if err := os.WriteFile(filepath.Join(cg.Mountpoint, cg.Path, "cpu.max"), []byte(tc.initialCPUMax), 0o777); err != nil {
492+
t.Fatalf("os.WriteFile(): %v", err)
493+
}
494+
if err := os.WriteFile(filepath.Join(cg.Mountpoint, cg.Path, "memory.max"), []byte(tc.initialMemMax), 0o777); err != nil {
495+
t.Fatalf("os.WriteFile(): %v", err)
496+
}
497+
498+
if err := cg.Update(tc.updatedResources); err != nil {
499+
t.Fatalf("Update(): %v", err)
500+
}
501+
502+
cpuQuota, err := cg.CPUQuota()
503+
if err != nil {
504+
t.Fatalf("CPUQuota(): %v", err)
505+
}
506+
if cpuQuota != tc.wantCPUQuota {
507+
t.Fatalf("After Update(), CPUQuota() = %f, want %f", cpuQuota, tc.wantCPUQuota)
508+
}
509+
510+
mem, err := cg.MemoryLimit()
511+
if err != nil {
512+
t.Fatalf("MemoryLimit(): %v", err)
513+
}
514+
if mem != tc.wantMemory {
515+
t.Fatalf("After Update(), MemoryLimit() = %d, want %d", mem, tc.wantMemory)
516+
}
517+
})
518+
}
519+
}

runsc/cgroup/systemd.go

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,25 @@ func (c *cgroupSystemd) Install(res *specs.LinuxResources) error {
111111
// For compatibility with runc.
112112
c.addProp("DefaultDependencies", false)
113113

114-
for controllerName, ctrlr := range controllers2 {
115-
// First check if our controller is found in the system.
116-
found := false
117-
for _, knownController := range c.Controllers {
118-
if controllerName == knownController {
119-
found = true
120-
}
121-
}
122-
if found {
123-
props, err := ctrlr.generateProperties(res)
124-
if err != nil {
125-
return err
126-
}
127-
c.properties = append(c.properties, props...)
128-
continue
129-
}
130-
if ctrlr.optional() {
131-
if err := ctrlr.skip(res); err != nil {
132-
return err
133-
}
134-
} else {
135-
return fmt.Errorf("mandatory cgroup controller %q is missing for %q", controllerName, c.Path)
136-
}
114+
return c.updateControllersProps(res)
115+
}
116+
117+
// Update updates the cgroup resources of an existing systemd unit.
118+
func (c *cgroupSystemd) Update(res *specs.LinuxResources) error {
119+
log.Debugf("Updating systemd cgroup resource controller under %v", c.Parent)
120+
if err := c.updateControllersProps(res); err != nil {
121+
return err
122+
}
123+
124+
ctx := context.Background()
125+
conn, err := systemdDbus.NewWithContext(ctx)
126+
if err != nil {
127+
return err
128+
}
129+
c.dbusConn = conn
130+
// Our systemd units are transient, hence runtime=true.
131+
if err := c.dbusConn.SetUnitPropertiesContext(ctx, c.unitName(), true /* runtime */, c.properties...); err != nil {
132+
return fmt.Errorf("error setting systemd unit properties: %v", err)
137133
}
138134
return nil
139135
}
@@ -306,6 +302,35 @@ func newProp(name string, units any) systemdDbus.Property {
306302
}
307303
}
308304

305+
func (c *cgroupSystemd) updateControllersProps(res *specs.LinuxResources) error {
306+
for controllerName, ctrlr := range controllers2 {
307+
// First check if our controller is found in the system.
308+
found := false
309+
for _, knownController := range c.Controllers {
310+
if controllerName == knownController {
311+
found = true
312+
break
313+
}
314+
}
315+
if found {
316+
props, err := ctrlr.generateProperties(res)
317+
if err != nil {
318+
return err
319+
}
320+
c.properties = append(c.properties, props...)
321+
continue
322+
}
323+
if ctrlr.optional() {
324+
if err := ctrlr.skip(res); err != nil {
325+
return err
326+
}
327+
} else {
328+
return fmt.Errorf("mandatory cgroup controller %q is missing for %q", controllerName, c.Path)
329+
}
330+
}
331+
return nil
332+
}
333+
309334
// CreateMockSystemdCgroup returns a mock Cgroup configured for systemd. This
310335
// is useful for testing.
311336
func CreateMockSystemdCgroup() Cgroup {

runsc/cgroup/systemd_test.go

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,14 @@ func TestExpandSlice(t *testing.T) {
8585
}
8686

8787
func TestInstall(t *testing.T) {
88+
const dialErr = "dial unix /var/run/dbus/system_bus_socket: connect: no such file or directory"
8889
for _, tc := range []struct {
89-
name string
90-
res *specs.LinuxResources
91-
wantProps []systemdDbus.Property
92-
err error
90+
name string
91+
res *specs.LinuxResources
92+
wantProps []systemdDbus.Property
93+
updatedRes *specs.LinuxResources
94+
wantUpdatedProps []systemdDbus.Property
95+
err error
9396
}{
9497
{
9598
name: "defaults",
@@ -160,6 +163,29 @@ func TestInstall(t *testing.T) {
160163
{"AllowedCPUs", dbus.MakeVariant([]byte{1 << 4})},
161164
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1 << 5})},
162165
},
166+
updatedRes: &specs.LinuxResources{
167+
CPU: &specs.LinuxCPU{
168+
Shares: uint64Ptr(1),
169+
Period: uint64Ptr(10000),
170+
Quota: int64Ptr(300000),
171+
Cpus: "2",
172+
Mems: "3",
173+
},
174+
},
175+
wantUpdatedProps: []systemdDbus.Property{
176+
// initial properties
177+
{"CPUWeight", dbus.MakeVariant(convertCPUSharesToCgroupV2Value(1))},
178+
{"CPUQuotaPeriodUSec", dbus.MakeVariant(uint64(20000))},
179+
{"CPUQuotaPerSecUSec", dbus.MakeVariant(uint64(15000000))},
180+
{"AllowedCPUs", dbus.MakeVariant([]byte{1 << 4})},
181+
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1 << 5})},
182+
// updated properties
183+
{"CPUWeight", dbus.MakeVariant(convertCPUSharesToCgroupV2Value(1))},
184+
{"CPUQuotaPeriodUSec", dbus.MakeVariant(uint64(10000))},
185+
{"CPUQuotaPerSecUSec", dbus.MakeVariant(uint64(30000000))},
186+
{"AllowedCPUs", dbus.MakeVariant([]byte{1 << 2})},
187+
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1 << 3})},
188+
},
163189
},
164190
{
165191
name: "cpuset",
@@ -227,6 +253,18 @@ func TestInstall(t *testing.T) {
227253
if diff := cmp.Diff(filteredProps, tc.wantProps, cmper, sorter); diff != "" {
228254
t.Errorf("cgroup properties list diff %s", diff)
229255
}
256+
257+
if tc.updatedRes != nil {
258+
if err := cg.Update(tc.updatedRes); err != nil && err.Error() != dialErr {
259+
if !errors.Is(err, tc.err) {
260+
t.Fatalf("Wrong error, got: %s, want: %s", err, tc.err)
261+
}
262+
}
263+
filteredProps = filterProperties(cg.properties, tc.wantUpdatedProps)
264+
if diff := cmp.Diff(filteredProps, tc.wantUpdatedProps, cmper, sorter); diff != "" {
265+
t.Errorf("cgroup properties list diff %s", diff)
266+
}
267+
}
230268
})
231269
}
232270
}

runsc/cli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ func forEachCmd(cb func(cmd subcommands.Command, group string)) {
258258
cb(new(cmd.Spec), "")
259259
cb(new(cmd.Start), "")
260260
cb(new(cmd.State), "")
261+
cb(new(cmd.Update), "")
261262
cb(new(cmd.Wait), "")
262263

263264
// Helpers.

runsc/cmd/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ go_library(
7171
"symbolize.go",
7272
"syscalls.go",
7373
"umount_unsafe.go",
74+
"update.go",
7475
"usage.go",
7576
"wait.go",
7677
"write_control.go",

0 commit comments

Comments
 (0)