Files
visor/runsc/cgroup/systemd_test.go
Kevin Mingtarja 645953d947 runsc: add update command
This change enables updating resource constraints of running containers. It is the runsc equivalent of runc update.
2026-01-23 23:01:08 -08:00

290 lines
8.5 KiB
Go

// Copyright The runc Authors.
// Copyright The containerd Authors.
// Copyright 2022 The gVisor 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
//
// https://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 cgroup
import (
"errors"
"testing"
systemdDbus "github.com/coreos/go-systemd/v22/dbus"
dbus "github.com/godbus/dbus/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
var (
defaultProps = []systemdDbus.Property{}
mandatoryControllers = []string{"cpu", "cpuset", "io", "memory", "pids"}
)
func TestIsValidSlice(t *testing.T) {
for _, tc := range []struct {
name string
slice string
err error
}{
{
name: "success",
slice: "system.slice",
},
{
name: "root slice",
slice: "-.slice",
},
{
name: "path in slice",
slice: "system-child-grandchild.slice",
},
{
name: "bad suffix",
slice: "system.scope",
err: ErrInvalidSlice,
},
{
name: "has path separators",
slice: "systemd.slice/child.slice",
err: ErrInvalidSlice,
},
{
name: "invalid separator pattern",
slice: "systemd--child.slice",
err: ErrInvalidSlice,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := validSlice(tc.slice)
if !errors.Is(err, tc.err) {
t.Errorf("validSlice(%s) = %v, want %v", tc.slice, err, tc.err)
}
})
}
}
func TestExpandSlice(t *testing.T) {
original := "test-a-b.slice"
want := "/test.slice/test-a.slice/test-a-b.slice"
expanded := expandSlice(original)
if expanded != want {
t.Errorf("expandSlice(%q) = %q, want %q", original, expanded, want)
}
}
func TestInstall(t *testing.T) {
const dialErr = "dial unix /var/run/dbus/system_bus_socket: connect: no such file or directory"
for _, tc := range []struct {
name string
res *specs.LinuxResources
wantProps []systemdDbus.Property
updatedRes *specs.LinuxResources
wantUpdatedProps []systemdDbus.Property
err error
}{
{
name: "defaults",
res: nil,
wantProps: []systemdDbus.Property{
{"Slice", dbus.MakeVariant("parent.slice")},
{Name: "Description", Value: dbus.MakeVariant("Secure container 123")},
{Name: "MemoryAccounting", Value: dbus.MakeVariant(true)},
{Name: "CPUAccounting", Value: dbus.MakeVariant(true)},
{Name: "TasksAccounting", Value: dbus.MakeVariant(true)},
{Name: "IOAccounting", Value: dbus.MakeVariant(true)},
{Name: "Delegate", Value: dbus.MakeVariant(true)},
{Name: "DefaultDependencies", Value: dbus.MakeVariant(false)},
},
},
{
name: "memory",
res: &specs.LinuxResources{
Memory: &specs.LinuxMemory{
Limit: int64Ptr(1),
Swap: int64Ptr(2),
Reservation: int64Ptr(3),
},
},
wantProps: []systemdDbus.Property{
{"MemoryMax", dbus.MakeVariant(uint64(1))},
{"MemoryLow", dbus.MakeVariant(uint64(3))},
{"MemorySwapMax", dbus.MakeVariant(uint64(1))},
},
},
{
name: "memory no limit",
res: &specs.LinuxResources{
Memory: &specs.LinuxMemory{
Swap: int64Ptr(1),
},
},
err: ErrBadResourceSpec,
},
{
name: "cpu defaults",
res: &specs.LinuxResources{
CPU: &specs.LinuxCPU{
Shares: uint64Ptr(0),
Quota: int64Ptr(5),
Period: uint64Ptr(0),
},
},
wantProps: []systemdDbus.Property{
{"CPUQuotaPerSecUSec", dbus.MakeVariant(uint64(10000))},
},
},
{
name: "cpu",
res: &specs.LinuxResources{
CPU: &specs.LinuxCPU{
Shares: uint64Ptr(1),
Period: uint64Ptr(20000),
Quota: int64Ptr(300000),
Cpus: "4",
Mems: "5",
},
},
wantProps: []systemdDbus.Property{
{"CPUWeight", dbus.MakeVariant(convertCPUSharesToCgroupV2Value(1))},
{"CPUQuotaPeriodUSec", dbus.MakeVariant(uint64(20000))},
{"CPUQuotaPerSecUSec", dbus.MakeVariant(uint64(15000000))},
{"AllowedCPUs", dbus.MakeVariant([]byte{1 << 4})},
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1 << 5})},
},
updatedRes: &specs.LinuxResources{
CPU: &specs.LinuxCPU{
Shares: uint64Ptr(1),
Period: uint64Ptr(10000),
Quota: int64Ptr(300000),
Cpus: "2",
Mems: "3",
},
},
wantUpdatedProps: []systemdDbus.Property{
// initial properties
{"CPUWeight", dbus.MakeVariant(convertCPUSharesToCgroupV2Value(1))},
{"CPUQuotaPeriodUSec", dbus.MakeVariant(uint64(20000))},
{"CPUQuotaPerSecUSec", dbus.MakeVariant(uint64(15000000))},
{"AllowedCPUs", dbus.MakeVariant([]byte{1 << 4})},
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1 << 5})},
// updated properties
{"CPUWeight", dbus.MakeVariant(convertCPUSharesToCgroupV2Value(1))},
{"CPUQuotaPeriodUSec", dbus.MakeVariant(uint64(10000))},
{"CPUQuotaPerSecUSec", dbus.MakeVariant(uint64(30000000))},
{"AllowedCPUs", dbus.MakeVariant([]byte{1 << 2})},
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1 << 3})},
},
},
{
name: "cpuset",
res: &specs.LinuxResources{
CPU: &specs.LinuxCPU{
Cpus: "1-3,5",
Mems: "5-8",
},
},
wantProps: []systemdDbus.Property{
{"AllowedCPUs", dbus.MakeVariant([]byte{0b_101110})},
{"AllowedMemoryNodes", dbus.MakeVariant([]byte{1, 0b_11100000})},
},
},
{
name: "io",
res: &specs.LinuxResources{
BlockIO: &specs.LinuxBlockIO{
Weight: uint16Ptr(1),
WeightDevice: []specs.LinuxWeightDevice{
makeLinuxWeightDevice(2, 3, uint16Ptr(4), uint16Ptr(0)),
makeLinuxWeightDevice(5, 6, uint16Ptr(7), uint16Ptr(0)),
},
ThrottleReadBpsDevice: []specs.LinuxThrottleDevice{
makeLinuxThrottleDevice(8, 9, 10),
makeLinuxThrottleDevice(11, 12, 13),
},
ThrottleWriteBpsDevice: []specs.LinuxThrottleDevice{
makeLinuxThrottleDevice(14, 15, 16),
},
ThrottleReadIOPSDevice: []specs.LinuxThrottleDevice{
makeLinuxThrottleDevice(17, 18, 19),
},
ThrottleWriteIOPSDevice: []specs.LinuxThrottleDevice{
makeLinuxThrottleDevice(20, 21, 22),
},
},
},
wantProps: []systemdDbus.Property{
{"IOWeight", dbus.MakeVariant(convertBlkIOToIOWeightValue(1))},
{"IODeviceWeight", dbus.MakeVariant("2:3 4")},
{"IODeviceWeight", dbus.MakeVariant("5:6 7")},
{"IOReadBandwidthMax", dbus.MakeVariant("8:9 10")},
{"IOReadBandwidthMax", dbus.MakeVariant("11:12 13")},
{"IOWriteBandwidthMax", dbus.MakeVariant("14:15 16")},
{"IOReadIOPSMax", dbus.MakeVariant("17:18 19")},
{"IOWriteIOPSMax", dbus.MakeVariant("20:21 22")},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
cg := cgroupSystemd{Name: "123", Parent: "parent.slice"}
cg.Controllers = mandatoryControllers
err := cg.Install(tc.res)
if !errors.Is(err, tc.err) {
t.Fatalf("Wrong error, got: %s, want: %s", err, tc.err)
}
cmper := cmp.Comparer(func(a dbus.Variant, b dbus.Variant) bool {
return a.String() == b.String()
})
sorter := cmpopts.SortSlices(func(a systemdDbus.Property, b systemdDbus.Property) bool {
return (a.Name + a.Value.String()) > (b.Name + b.Value.String())
})
filteredProps := filterProperties(cg.properties, tc.wantProps)
if diff := cmp.Diff(filteredProps, tc.wantProps, cmper, sorter); diff != "" {
t.Errorf("cgroup properties list diff %s", diff)
}
if tc.updatedRes != nil {
if err := cg.Update(tc.updatedRes); err != nil && err.Error() != dialErr {
if !errors.Is(err, tc.err) {
t.Fatalf("Wrong error, got: %s, want: %s", err, tc.err)
}
}
filteredProps = filterProperties(cg.properties, tc.wantUpdatedProps)
if diff := cmp.Diff(filteredProps, tc.wantUpdatedProps, cmper, sorter); diff != "" {
t.Errorf("cgroup properties list diff %s", diff)
}
}
})
}
}
// filterProperties filters the list of properties in got to ones with
// the names of properties specified in want.
func filterProperties(got []systemdDbus.Property, want []systemdDbus.Property) []systemdDbus.Property {
if want == nil {
return nil
}
filterMap := map[string]any{}
for _, prop := range want {
filterMap[prop.Name] = nil
}
filtered := []systemdDbus.Property{}
for _, prop := range got {
if _, ok := filterMap[prop.Name]; ok {
filtered = append(filtered, prop)
}
}
return filtered
}