Skip to content
Merged
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
37 changes: 37 additions & 0 deletions cli/verifier/cpu_count_valid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 Edgeless Systems GmbH
// SPDX-License-Identifier: BUSL-1.1

package verifier

import (
"errors"
"fmt"
"strings"

"github.com/edgelesssys/contrast/internal/kuberesource"
applycorev1 "k8s.io/client-go/applyconfigurations/core/v1"
)

// CPUCountValid verifies that all workloads have a valid vCPU count.
type CPUCountValid struct{}

// Verify checks that pods do not require more than the currently supported 8 vCPUs.
func (v *CPUCountValid) Verify(toVerify any) error {
var findings error

kuberesource.MapPodSpec(toVerify, func(spec *applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration {
if spec == nil || spec.RuntimeClassName == nil || !strings.HasPrefix(*spec.RuntimeClassName, "contrast-cc") {
return spec
}

cpuCount := kuberesource.GetPodCPUCount(spec)
if cpuCount < 1 || cpuCount > 8 {
// TODO(charludo): find way to add pod name to error message
findings = errors.Join(findings, fmt.Errorf("pod failed verification: currently only 0-7 additional vCPUs are supported (1-8 vCPUs in total), but %d were requested", cpuCount))
}

return spec
})

return findings
}
103 changes: 103 additions & 0 deletions cli/verifier/cpu_count_valid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2026 Edgeless Systems GmbH
// SPDX-License-Identifier: BUSL-1.1

package verifier_test

import (
"testing"

"github.com/edgelesssys/contrast/cli/verifier"
"github.com/edgelesssys/contrast/internal/kuberesource"
"github.com/stretchr/testify/require"
)

const deploymentWithoutExplicitCPUCount = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
template:
spec:
runtimeClassName: contrast-cc
containers:
- name: app
resources:
limits:
cpu: 2000m
`

const deploymentWithValidCPUCount = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
template:
spec:
runtimeClassName: contrast-cc
containers:
- name: app
resources:
limits:
cpu: 2000m
`

const deploymentWithTooManyCPUs = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
template:
metadata:
name: web-pod
namespace: test
spec:
runtimeClassName: contrast-cc
containers:
- name: app
resources:
limits:
cpu: "8" # invalid because of always-added CPU
`

func TestCPUCountValid(t *testing.T) {
testCases := map[string]struct {
k8sYaml string
wantErr bool
}{
"no explicit cpu count": {
k8sYaml: deploymentWithoutExplicitCPUCount,
},
"valid cpu count": {
k8sYaml: deploymentWithValidCPUCount,
},
"too many cpus": {
k8sYaml: deploymentWithTooManyCPUs,
wantErr: true,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
require := require.New(t)
toVerifySlice, err := kuberesource.UnmarshalApplyConfigurations([]byte(tc.k8sYaml))
require.NoError(err)

verifier := verifier.CPUCountValid{}

for _, toVerify := range toVerifySlice {
err := verifier.Verify(toVerify)
if tc.wantErr {
require.Error(err)
} else {
require.NoError(err)
}
}
})
}
}
1 change: 1 addition & 0 deletions cli/verifier/verifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func AllVerifiersBeforeGenerate(cmd *cobra.Command) []Verifier {
&ImageRefValid{ExcludeContrastImages: true},
&ServiceMeshEgressNotEmpty{},
&RuntimeClassesExist{Command: cmd},
&CPUCountValid{},
}
}

Expand Down
35 changes: 35 additions & 0 deletions internal/kuberesource/parts.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,38 @@ func getPorts(podSpec *applycorev1.PodSpecApplyConfiguration) (ports []applycore
}
return ports
}

// GetCPUCount returns the CPU count in milliCPUs from resources.
func GetCPUCount(resources *applycorev1.ResourceRequirementsApplyConfiguration) int64 {
if resources != nil && resources.Limits != nil {
return resources.Limits.Cpu().MilliValue()
}
return 0
}

// GetPodCPUCount computes the total CPU count (in whole CPUs) required by a pod, adding hypervisor overhead.
func GetPodCPUCount(spec *applycorev1.PodSpecApplyConfiguration) uint64 {
var regularContainersCPU int64
for _, container := range spec.Containers {
regularContainersCPU += GetCPUCount(container.Resources)
}
var maxInitContainerCPU int64
for _, container := range spec.InitContainers {
cpuCount := GetCPUCount(container.Resources)
// Sidecar containers remain running alongside the actual application, consuming CPU resources
if container.RestartPolicy != nil && *container.RestartPolicy == corev1.ContainerRestartPolicyAlways {
regularContainersCPU += cpuCount
} else {
if cpuCount > maxInitContainerCPU {
maxInitContainerCPU = cpuCount
}
}
}
podLevelCPU := GetCPUCount(spec.Resources)

// Convert milliCPUs to number of CPUs (rounding up), and add 1 for hypervisor overhead.
// Pod resources are added to the maximum of regular containers and init containers.
totalMilliCPUs := max(regularContainersCPU, maxInitContainerCPU) + podLevelCPU
totalCPUs := (totalMilliCPUs+999)/1000 + 1
return uint64(totalCPUs)
}
Loading