mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-29 16:17:20 +09:00
The binder, the admission webhooks, the catalog option source and the tenant quota gate carried copies of the tenant release name mapping, the resourceQuotas reader, the permittedHostDevices reader and the per-VMI device coverage, and the copies had started to drift: the catalog skipped a budget it could not read, where the others failed. The VMI webhook still counted one unit per healthy GPU function when a node had lost capacity, so it refused a VM whose devices an mdev parent's remaining slots still served while the binder would have kept them; the units a node serves are now counted once, for both. They now live in the shared model, next to the tenant walk they depend on. Coverage keeps two named readers on purpose: for budgets every attached device covers its pod, since a Lost device still counts itself, while for node capacity only a device holding units does. Merging them would count a Lost device twice or not at all. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
/*
|
|
Copyright 2026 The Cozystack 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 model
|
|
|
|
import (
|
|
"github.com/go-logr/logr"
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
gpuv1alpha1 "github.com/cozystack/cozystack/api/gpu/v1alpha1"
|
|
)
|
|
|
|
// Cordoned reports whether the node is cordoned. Only spec.unschedulable
|
|
// counts: a taint, such as the one Fence Agents Remediation sets, is not a
|
|
// cordon and does not trigger the class's onCordon policy.
|
|
func Cordoned(n *corev1.Node) bool {
|
|
return n.Spec.Unschedulable
|
|
}
|
|
|
|
// Ready reports whether the node's Ready condition is True.
|
|
func Ready(n *corev1.Node) bool {
|
|
for _, c := range n.Status.Conditions {
|
|
if c.Type == corev1.NodeReady {
|
|
return c.Status == corev1.ConditionTrue
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// mirrorsCordonOrReadiness reports whether Kubernetes sets the taint for a
|
|
// cordon or a node that is not Ready, which Cordoned and Ready already
|
|
// decide.
|
|
func mirrorsCordonOrReadiness(key string) bool {
|
|
switch key {
|
|
case corev1.TaintNodeUnschedulable, corev1.TaintNodeNotReady, corev1.TaintNodeUnreachable:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// UntoleratedTaint returns the first NoSchedule or NoExecute taint that
|
|
// tolerations do not tolerate, or nil, skipping the taints that mirror a
|
|
// cordon or readiness. Gt and Lt tolerations never match: where their
|
|
// feature gate is off the scheduler does not honour them, and a node is
|
|
// better skipped than bound to in vain.
|
|
func UntoleratedTaint(n *corev1.Node, tolerations []corev1.Toleration) *corev1.Taint {
|
|
for i := range n.Spec.Taints {
|
|
taint := &n.Spec.Taints[i]
|
|
if taint.Effect != corev1.TaintEffectNoSchedule && taint.Effect != corev1.TaintEffectNoExecute {
|
|
continue
|
|
}
|
|
if mirrorsCordonOrReadiness(taint.Key) {
|
|
continue
|
|
}
|
|
tolerated := false
|
|
for j := range tolerations {
|
|
if tolerations[j].ToleratesTaint(logr.Discard(), taint, false) {
|
|
tolerated = true
|
|
break
|
|
}
|
|
}
|
|
if !tolerated {
|
|
return taint
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Eligible reports whether new binds, moves, rebinds and gathers may use
|
|
// the node, and whether its units count in a class's capacity.
|
|
// tolerations are those of the virt-launcher pod, nil when no VMI is
|
|
// known.
|
|
func Eligible(n *corev1.Node, tolerations []corev1.Toleration) bool {
|
|
return !Cordoned(n) && Ready(n) && UntoleratedTaint(n, tolerations) == nil
|
|
}
|
|
|
|
// ServedUnits is how many units of rn the node can serve. The agent's
|
|
// functions that carry rn count with kubelet's allocatable count for them,
|
|
// or one each when healthy and without a count: kubelet lists only healthy
|
|
// plugin devices, so one bad mdev slot does not take its parent's other
|
|
// slots with it. The node's allocatable caps that sum, since a plugin that
|
|
// names a device by IOMMU group reports it on every function of the group,
|
|
// and judges alone a name no function carries.
|
|
func ServedUnits(n *corev1.Node, devices []gpuv1alpha1.GPUNodeDevice, rn corev1.ResourceName) int64 {
|
|
var served int64
|
|
mapped := false
|
|
for _, d := range devices {
|
|
if corev1.ResourceName(d.ResourceName) != rn {
|
|
continue
|
|
}
|
|
mapped = true
|
|
switch {
|
|
case d.Allocatable != nil:
|
|
served += int64(*d.Allocatable)
|
|
case d.Healthy != nil && *d.Healthy:
|
|
served++
|
|
}
|
|
}
|
|
q := n.Status.Allocatable[rn]
|
|
if !mapped || q.Value() < served {
|
|
return q.Value()
|
|
}
|
|
return served
|
|
}
|