mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-02 18:16:53 +09:00
The binder re-checks what admission checked, and cozystack-api shows the remaining budget a later order is checked against; computing these in three places would let them disagree. The model is pure, so each caller builds a snapshot from its own cache. GPU budgets cap the whole subtree of the tenant that declares them, walked over the namespace ancestry and independent of the tenant quota pools. A raw GPU key blocks a class until the same tenant also declares a GPU key, and then caps devices and raw-name pods together. Pod usage follows the scheduler's arithmetic, sidecars included, so the binder never books a unit a raw-name pod holds. A node takes binds only when it is schedulable, Ready and carries no NoSchedule or NoExecute taint the pod does not tolerate; the unschedulable, not-ready and unreachable taints are left to the first two checks, and a taint never counts as a cordon. Gt and Lt tolerations do not count, since the scheduler ignores them where their feature gate is off. Template device names are derived from the VMI and shortened with a hash when the VMI name leaves no room, so they stay valid subdomains. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
106 lines
3.5 KiB
Go
106 lines
3.5 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 (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
)
|
|
|
|
// DeviceRef is one entry of the gpu.AnnotationDevices list on a VMI:
|
|
// exactly one of Device and DeviceTemplate is set.
|
|
type DeviceRef struct {
|
|
// Device names a GPUDevice in the VMI's namespace.
|
|
Device string `json:"device,omitempty"`
|
|
DeviceTemplate *DeviceTemplate `json:"deviceTemplate,omitempty"`
|
|
}
|
|
|
|
// DeviceTemplate asks for a GPUDevice created at VMI start and deleted
|
|
// with the VMI.
|
|
type DeviceTemplate struct {
|
|
ClassName string `json:"className"`
|
|
Count int32 `json:"count"`
|
|
}
|
|
|
|
// ParseDeviceRefs reads the gpu.AnnotationDevices value and checks its
|
|
// shape. Device and class names are not checked here; a malformed one is
|
|
// refused when the webhook looks it up.
|
|
func ParseDeviceRefs(s string) ([]DeviceRef, error) {
|
|
dec := json.NewDecoder(bytes.NewReader([]byte(s)))
|
|
dec.DisallowUnknownFields()
|
|
var refs []DeviceRef
|
|
if err := dec.Decode(&refs); err != nil {
|
|
return nil, fmt.Errorf("parse device references: %w", err)
|
|
}
|
|
if _, err := dec.Token(); err != io.EOF {
|
|
return nil, errors.New("parse device references: data after the list")
|
|
}
|
|
if len(refs) == 0 {
|
|
return nil, errors.New("device references: empty list")
|
|
}
|
|
seen := map[string]bool{}
|
|
for i, r := range refs {
|
|
switch {
|
|
case (r.Device == "") == (r.DeviceTemplate == nil):
|
|
return nil, fmt.Errorf("device reference %d: set exactly one of device and deviceTemplate", i)
|
|
case r.DeviceTemplate != nil && r.DeviceTemplate.ClassName == "":
|
|
return nil, fmt.Errorf("device reference %d: deviceTemplate needs className", i)
|
|
case r.DeviceTemplate != nil && r.DeviceTemplate.Count < 1:
|
|
return nil, fmt.Errorf("device reference %d: deviceTemplate count must be at least 1", i)
|
|
case seen[r.Device]:
|
|
return nil, fmt.Errorf("device reference %d: device %s is referenced twice", i, r.Device)
|
|
}
|
|
if r.Device != "" {
|
|
seen[r.Device] = true
|
|
}
|
|
}
|
|
return refs, nil
|
|
}
|
|
|
|
// FormatDeviceRefs renders refs as the gpu.AnnotationDevices value.
|
|
func FormatDeviceRefs(refs []DeviceRef) (string, error) {
|
|
b, err := json.Marshal(refs)
|
|
if err != nil {
|
|
return "", fmt.Errorf("format device references: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// TemplateDeviceName names the GPUDevice created for the deviceTemplate
|
|
// entry at index of a VMI's references, so a repeated admission or a
|
|
// recreated VMI finds the same device. A VMI name too long to take the
|
|
// suffix is cut short and followed by a hash of the full name.
|
|
func TemplateDeviceName(vmi string, index int) string {
|
|
suffix := "-gpu-" + strconv.Itoa(index)
|
|
if len(vmi)+len(suffix) <= validation.DNS1123SubdomainMaxLength {
|
|
return vmi + suffix
|
|
}
|
|
sum := sha256.Sum256([]byte(vmi))
|
|
hash := hex.EncodeToString(sum[:])[:10]
|
|
keep := validation.DNS1123SubdomainMaxLength - len(suffix) - len(hash) - 1
|
|
return strings.TrimRight(vmi[:keep], "-.") + "-" + hash + suffix
|
|
}
|