Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/driver/nri_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ func (cp *CPUDriver) CreateContainer(ctx context.Context, pod *api.PodSandbox, c
klog.Infof("No guaranteed CPUs found in DRA env for pod %s/%s container %s. Using shared CPUs %s", pod.Namespace, pod.Name, ctr.Name, sharedCPUs.String())
adjust.SetLinuxCPUSetCPUs(sharedCPUs.String())
} else {
// Validate CPU requests match claim allocations
if err := validateCPURequests(pod, ctr, claimAllocations); err != nil {
klog.Errorf("CPU request validation failed for pod %s/%s container %s: %v",
pod.Namespace, pod.Name, ctr.Name, err)
return nil, nil, fmt.Errorf("CPU request validation failed: %w", err)
}

guaranteedCPUs := cpuset.New()
claimUIDs := []types.UID{}
for uid, cpus := range claimAllocations {
Expand Down
72 changes: 72 additions & 0 deletions pkg/driver/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright The Kubernetes 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

http://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 driver

import (
"fmt"

"github.com/containerd/nri/pkg/api"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"k8s.io/utils/cpuset"
)

// validateCPURequests validates that container CPU requests match claim allocations.
func validateCPURequests(
pod *api.PodSandbox,
ctr *api.Container,
claimAllocations map[types.UID]cpuset.CPUSet,
) error {
totalClaimCPUs := 0
for _, cpus := range claimAllocations {
totalClaimCPUs += cpus.Size()
}

if ctr.Linux != nil && ctr.Linux.Resources != nil && ctr.Linux.Resources.Cpu != nil {
if ctr.Linux.Resources.Cpu.Shares != nil && ctr.Linux.Resources.Cpu.Shares.Value > 0 {
containerCPUShares := ctr.Linux.Resources.Cpu.Shares.Value
containerCPURequest := float64(containerCPUShares) / 1024.0

const tolerance = 0.01
diff := containerCPURequest - float64(totalClaimCPUs)
if diff < -tolerance || diff > tolerance {
return fmt.Errorf(
"container %s CPU request (%.2f cores, %d shares) does not match claim allocation (%d CPUs): "+
"when using DRA CPU claims, container CPU requests must exactly equal the claim size",
ctr.Name, containerCPURequest, containerCPUShares, totalClaimCPUs,
)
}
}
}

if pod.Linux != nil && pod.Linux.PodResources != nil && pod.Linux.PodResources.Cpu != nil {
if pod.Linux.PodResources.Cpu.Shares != nil && pod.Linux.PodResources.Cpu.Shares.Value > 0 {
podLevelCPUShares := pod.Linux.PodResources.Cpu.Shares.Value
podLevelCPURequest := float64(podLevelCPUShares) / 1024.0

klog.V(4).InfoS("pod has pod-level CPU request",
"namespace", pod.Namespace,
"pod", pod.Name,
"podLevelCPURequest", podLevelCPURequest,
"container", ctr.Name,
"claimCPUs", totalClaimCPUs,
)
}
}

return nil
}
265 changes: 265 additions & 0 deletions pkg/driver/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
Copyright The Kubernetes 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

http://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 driver

import (
"strings"
"testing"

"github.com/containerd/nri/pkg/api"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/cpuset"
)

func TestValidateCPURequests(t *testing.T) {
tests := []struct {
name string
pod *api.PodSandbox
container *api.Container
claims map[types.UID]cpuset.CPUSet
expectError bool
errorContains string
}{
{
name: "matching container request - 10 CPUs",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 10240}, // 10 CPUs * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), // 10 CPUs
},
expectError: false,
},
{
name: "matching container request - 4 CPUs",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 4096}, // 4 CPUs * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3), // 4 CPUs
},
expectError: false,
},
{
name: "mismatched container request - request too low",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 5120}, // 5 CPUs * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), // 10 CPUs
},
expectError: true,
errorContains: "does not match claim allocation",
},
{
name: "mismatched container request - request too high",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 20480}, // 20 CPUs * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), // 10 CPUs
},
expectError: true,
errorContains: "does not match claim allocation",
},
{
name: "no container CPU request specified - should pass",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), // 10 CPUs
},
expectError: false,
},
{
name: "container with zero CPU shares - should pass",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 0},
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), // 10 CPUs
},
expectError: false,
},
{
name: "multiple claims - matching total",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 10240}, // 10 CPUs * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3), // 4 CPUs
"claim-2": cpuset.New(4, 5, 6, 7, 8, 9), // 6 CPUs
},
expectError: false,
},
{
name: "multiple claims - mismatched total",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 8192}, // 8 CPUs * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3), // 4 CPUs
"claim-2": cpuset.New(4, 5, 6, 7, 8, 9), // 6 CPUs (total = 10)
},
expectError: true,
errorContains: "does not match claim allocation",
},
{
name: "container without Linux resources - should pass",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
// No Linux field
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), // 10 CPUs
},
expectError: false,
},
{
name: "single CPU claim - matching",
pod: &api.PodSandbox{
Name: "test-pod",
Namespace: "default",
},
container: &api.Container{
Name: "test-container",
Linux: &api.LinuxContainer{
Resources: &api.LinuxResources{
Cpu: &api.LinuxCPU{
Shares: &api.OptionalUInt64{Value: 1024}, // 1 CPU * 1024
},
},
},
},
claims: map[types.UID]cpuset.CPUSet{
"claim-1": cpuset.New(0), // 1 CPU
},
expectError: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateCPURequests(tc.pod, tc.container, tc.claims)

if tc.expectError && err == nil {
t.Errorf("expected error but got none")
}

if !tc.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}

if tc.expectError && err != nil {
if !strings.Contains(err.Error(), tc.errorContains) {
t.Errorf("error %q does not contain expected substring %q", err.Error(), tc.errorContains)
}
}
})
}
}
Loading
Loading