Skip to content

Commit

Permalink
Add kube play support for CDI resource allocation
Browse files Browse the repository at this point in the history
We now handle CDI qualified names being passed to resources.limits. The
support for that was already in libpod as of ab7f609
when passed via the devices list. this just hooks the kube yaml parser
up to it.

Additionally we introduce `podman.io/device` that accepts device paths
as names and is transparently translated to mimick --device. This allows
bringing arbitrary devices into the container via similar, although
incompatible with, k8s mechanics:

```yaml
resources:
  requests:
    podman.io/device=/dev/kmsg: 1
```

Fixes: #17833

Signed-off-by: Robert Günzler <r@gnzler.io>
  • Loading branch information
robertgzr committed Jan 14, 2025
1 parent 9f1fee2 commit 0d0a78c
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 26 deletions.
110 changes: 84 additions & 26 deletions pkg/specgen/generate/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
cdiparser "tags.cncf.io/container-device-interface/pkg/parser"
)

func ToPodOpt(ctx context.Context, podName string, p entities.PodCreateOptions, publishAllPorts bool, podYAML *v1.PodTemplateSpec) (entities.PodCreateOptions, error) {
Expand Down Expand Up @@ -276,36 +277,14 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener
// but apply to the containers with the prefixed name
s.SeccompProfilePath = opts.SeccompPaths.FindForContainer(opts.Container.Name)

s.ResourceLimits = &spec.LinuxResources{}
milliCPU := opts.Container.Resources.Limits.Cpu().MilliValue()
if milliCPU > 0 {
period, quota := util.CoresToPeriodAndQuota(float64(milliCPU) / 1000)
s.ResourceLimits.CPU = &spec.LinuxCPU{
Quota: &quota,
Period: &period,
}
}

limit, err := quantityToInt64(opts.Container.Resources.Limits.Memory())
err = setupContainerResources(s, opts.Container)
if err != nil {
return nil, fmt.Errorf("failed to set memory limit: %w", err)
return nil, fmt.Errorf("failed to configure container resources: %w", err)
}

memoryRes, err := quantityToInt64(opts.Container.Resources.Requests.Memory())
err = setupContainerDevices(s, opts.Container)
if err != nil {
return nil, fmt.Errorf("failed to set memory reservation: %w", err)
}

if limit > 0 || memoryRes > 0 {
s.ResourceLimits.Memory = &spec.LinuxMemory{}
}

if limit > 0 {
s.ResourceLimits.Memory.Limit = &limit
}

if memoryRes > 0 {
s.ResourceLimits.Memory.Reservation = &memoryRes
return nil, fmt.Errorf("failed to configure container devices: %w", err)
}

ulimitVal, ok := opts.Annotations[define.UlimitAnnotation]
Expand Down Expand Up @@ -840,6 +819,85 @@ func makeHealthCheck(inCmd string, interval int32, retries int32, timeout int32,
return &hc, nil
}

func setupContainerResources(s *specgen.SpecGenerator, containerYAML v1.Container) error {
s.ResourceLimits = &spec.LinuxResources{}
milliCPU := containerYAML.Resources.Limits.Cpu().MilliValue()
if milliCPU > 0 {
period, quota := util.CoresToPeriodAndQuota(float64(milliCPU) / 1000)
s.ResourceLimits.CPU = &spec.LinuxCPU{
Quota: &quota,
Period: &period,
}
}

limit, err := quantityToInt64(containerYAML.Resources.Limits.Memory())
if err != nil {
return fmt.Errorf("failed to set memory limit: %w", err)
}

memoryRes, err := quantityToInt64(containerYAML.Resources.Requests.Memory())
if err != nil {
return fmt.Errorf("failed to set memory reservation: %w", err)
}

if limit > 0 || memoryRes > 0 {
s.ResourceLimits.Memory = &spec.LinuxMemory{}
}

if limit > 0 {
s.ResourceLimits.Memory.Limit = &limit
}

if memoryRes > 0 {
s.ResourceLimits.Memory.Reservation = &memoryRes
}

return nil
}

const PodmanDeviceResourcePrefix = "io.podman/device"

func setupContainerDevices(s *specgen.SpecGenerator, containerYAML v1.Container) error {
s.Devices = make([]spec.LinuxDevice, 0)
// avoid duplicates
devices := make(map[string]bool, 0)

parse := func(device string) error {
vendor, class, name := cdiparser.ParseDevice(device)
if vendor == "" {
return nil
}

if err := cdiparser.ValidateDeviceName(name); err != nil {
// handle internal "fake" CDI
if vendor == "podman.io" && class == "device" {
device = name
} else {
return fmt.Errorf("not a qualified name %v: %w", device, err)
}
}

if _, ok := devices[device]; !ok {
devices[device] = true
s.Devices = append(s.Devices, spec.LinuxDevice{Path: device})
}
return nil
}

for key := range containerYAML.Resources.Requests {
if err := parse(key.String()); err != nil {
return err
}
}
for key := range containerYAML.Resources.Limits {
if err := parse(key.String()); err != nil {
return err
}
}

return nil
}

func setupSecurityContext(s *specgen.SpecGenerator, securityContext *v1.SecurityContext, podSecurityContext *v1.PodSecurityContext) {
if securityContext == nil {
securityContext = &v1.SecurityContext{}
Expand Down
65 changes: 65 additions & 0 deletions pkg/specgen/generate/kube/play_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/containers/podman/v5/pkg/k8s.io/apimachinery/pkg/util/intstr"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/docker/docker/pkg/meminfo"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/yaml"
)
Expand Down Expand Up @@ -1400,3 +1401,67 @@ func TestTCPLivenessProbe(t *testing.T) {
})
}
}

func TestDeviceResource(t *testing.T) {
tests := []struct {
name string
specGenerator specgen.SpecGenerator
container v1.Container
succeed bool
devices []spec.LinuxDevice
}{
{
"ParseQualifiedCDI",
specgen.SpecGenerator{},
v1.Container{
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
"nvidia.com/gpu=0": resource.MustParse("1"),
},
},
},
true,
[]spec.LinuxDevice{
{Path: "nvidia.com/gpu=0"},
},
},
{
"ParsePodmanDeviceResource",
specgen.SpecGenerator{},
v1.Container{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
"podman.io/device=/dev/kmsg": resource.MustParse("1"),
},
},
},
true,
[]spec.LinuxDevice{
{Path: "/dev/kmsg"},
},
},
{
"InvalidCDI",
specgen.SpecGenerator{},
v1.Container{
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
"foobar.net/class=///": resource.MustParse("1"),
},
},
},
false,
[]spec.LinuxDevice{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := setupContainerDevices(&test.specGenerator, test.container)
assert.Equal(t, err == nil, test.succeed)
if err == nil {
assert.Equal(t, test.specGenerator.Devices, test.devices)
}
})
}
}

0 comments on commit 0d0a78c

Please sign in to comment.