Files
cozystack/packages/system/cozystack-controller/definitions/gpu.cozystack.io_gpuclasses.yaml
Aleksei Sviridkin 18a3ce5a24 feat(api): add gpu.cozystack.io GPUClass, GPUDevice and GPUNode
GPUs become a catalog of hoster offers, tenant reservations against
them and a per-node inventory, so a tenant can hold a GPU while its VM
is off and an order that does not fit is refused at create time.

The CRDs enforce what the apiserver alone can: a class name is a DNS
label other than total because it becomes part of a quota key, the
source is a union with dra reserved for a later member, a device's
spec is immutable and orders at least one GPU, and bound devices are
listable per node through a field selector on status.nodeName. A
class resource name must be a valid extended resource outside
gpu.cozystack.io, where it would read as a GPU budget key. The CRDs
are kept on chart uninstall, since they ship as plain templates and
deleting GPUDevice would drop every hold and usage record.

maxCount carries no schema default: it defaults to 1 for vgpu classes
and to no limit for passthrough, which a structural default cannot
express, so readers resolve it through the shared model.

A GPUNode device reports how many device-plugin devices kubelet
counts as allocatable for it, because an mdev parent serves several
from one PCI function and one healthy entry per function would
undercount the node.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-21 03:04:35 +03:00

241 lines
10 KiB
YAML

---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.4
helm.sh/resource-policy: keep
name: gpuclasses.gpu.cozystack.io
spec:
group: gpu.cozystack.io
names:
kind: GPUClass
listKind: GPUClassList
plural: gpuclasses
singular: gpuclass
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .spec.mode
name: Mode
type: string
- jsonPath: .spec.model
name: Model
type: string
- jsonPath: .spec.profile
name: Profile
type: string
- jsonPath: .status.free
name: Free
type: integer
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: GPUClass is one GPU offer of the hoster's catalog.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: GPUClassSpec defines one GPU offer of the hoster's catalog.
properties:
displayName:
description: DisplayName is the human-readable name of the offer.
maxLength: 256
type: string
maxCount:
description: |-
MaxCount is how many GPUs of this class one VM may take. Unset means 1
for mode vgpu and, for mode passthrough, no limit beyond the GPUs of
one node; read it through the shared model rather than the raw field.
format: int32
minimum: 1
type: integer
memory:
anyOf:
- type: integer
- type: string
description: Memory is the GPU memory of one unit of the class.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
mode:
description: GPUMode is how a class's GPUs reach a VM.
enum:
- passthrough
- vgpu
type: string
model:
maxLength: 63
minLength: 1
type: string
nodeSelector:
additionalProperties:
type: string
description: NodeSelector selects the nodes that serve this class.
type: object
onCordon:
default: Move
description: OnCordon decides whether an unattached device leaves
a cordoned node.
enum:
- Move
- Stay
type: string
profile:
description: Profile is the vGPU profile, such as L40S-24C. Set only
for mode vgpu.
maxLength: 63
minLength: 1
type: string
source:
description: |-
GPUClassSource is the backend that serves a class. Exactly one member is
set. The name "dra" is reserved for a DRA-backed member.
properties:
devicePlugin:
description: DevicePluginSource serves a class through a Kubernetes
device plugin.
properties:
resourceName:
description: |-
ResourceName is the extended resource the device plugin advertises for
this class's GPUs. Two classes may share it only when their node
selectors do not overlap.
maxLength: 317
minLength: 3
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$
type: string
x-kubernetes-validations:
- message: resourceName must not be in gpu.cozystack.io, whose
keys are GPU budgets
rule: '!self.startsWith(''gpu.cozystack.io/'')'
required:
- resourceName
type: object
type: object
x-kubernetes-validations:
- message: exactly one source must be set; devicePlugin is the only
source supported, dra is reserved
rule: '[has(self.devicePlugin)].filter(x, x).size() == 1'
vendor:
maxLength: 63
minLength: 1
type: string
required:
- mode
- model
- source
- vendor
type: object
x-kubernetes-validations:
- message: profile is required for mode vgpu and not allowed for mode
passthrough
rule: 'self.mode == ''vgpu'' ? has(self.profile) : !has(self.profile)'
status:
description: |-
GPUClassStatus is readable by every authenticated user, so it carries no
node names and no counts beyond free capacity.
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
free:
description: |-
Free is the number of free units of the class on eligible nodes.
Absent until the controller has computed it.
format: int32
type: integer
maxFreePerNode:
description: MaxFreePerNode is the largest number of free units on
one node.
format: int32
type: integer
type: object
required:
- spec
type: object
x-kubernetes-validations:
- message: GPUClass name must be a DNS label, since it becomes part of a quota
key
rule: size(self.metadata.name) <= 63 && self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
- message: GPUClass name total is reserved for the gpu.cozystack.io/total
quota key
rule: self.metadata.name != 'total'
served: true
storage: true
subresources:
status: {}