From 84703bff0c686b024185150864ddd73463347fd7 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 31 Jul 2026 10:51:34 +0100 Subject: [PATCH 1/2] remove reserved capacityType Signed-off-by: kerthcet --- api/v1alpha1/groupversion_info.go | 2 +- api/v1alpha1/nodeclaim_types.go | 2 +- api/v1alpha1/nodepool_types.go | 10 +++------- config/crd/bases/nebula.inftyai.com_nodeclaims.yaml | 3 +-- config/crd/bases/nebula.inftyai.com_nodepools.yaml | 5 +---- config/samples/deployment.yaml | 4 ++-- pkg/provider/catalog/catalog.go | 2 +- pkg/provider/catalog/data/aws.csv | 2 +- pkg/provider/catalog/data/modal.csv | 2 +- pkg/provider/provider.go | 2 +- 10 files changed, 13 insertions(+), 21 deletions(-) diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index f756132..d27ae8b 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -75,7 +75,7 @@ const ( AcceleratorTypeLabel = "nebula.inftyai.com/accelerator-type" // CapacityTypeAnnotation carries the optimizer-chosen purchase tier - // (Spot/OnDemand/Reserved). It is the one provisioning input that cannot be + // (Spot/OnDemand). It is the one provisioning input that cannot be // read off the Pod's own spec, so the placement controller writes it here // when it ungates the Pod. The virtual kubelet — which provisions solely from // the Pod — reads it back on CreatePod. Empty means "let the provider use its diff --git a/api/v1alpha1/nodeclaim_types.go b/api/v1alpha1/nodeclaim_types.go index e1bdec9..3e7a998 100644 --- a/api/v1alpha1/nodeclaim_types.go +++ b/api/v1alpha1/nodeclaim_types.go @@ -23,7 +23,7 @@ type NodeClaimSpec struct { Provider string `json:"provider"` // CapacityType is the purchase tier the placement optimizer selected - // (Spot/OnDemand/Reserved). It is stored durably here because it is the one + // (Spot/OnDemand). It is stored durably here because it is the one // provisioning input that cannot be read off the Pod, and Provision needs it // to re-issue the request after a controller restart. Immutable, like // Provider. Empty means "let the provider use its default" (e.g. Modal is diff --git a/api/v1alpha1/nodepool_types.go b/api/v1alpha1/nodepool_types.go index b73c7dc..cac8bca 100644 --- a/api/v1alpha1/nodepool_types.go +++ b/api/v1alpha1/nodepool_types.go @@ -46,10 +46,9 @@ type NodePoolSpec struct { // CapacityTypes is the OUTER axis: the purchase models to try, in fallback // order. e.g. [Spot, OnDemand] means "use spot on any provider first; only // when spot is exhausted everywhere, drop to on-demand". A single-element - // list pins the pool to that type. This replaces a spot on/off flag and - // extends to Reserved without new fields. + // list pins the pool to that type. This replaces a spot on/off flag. // +kubebuilder:validation:MinItems=1 - // +kubebuilder:default={Reserved,OnDemand,Spot} + // +kubebuilder:default={OnDemand,Spot} CapacityTypes []CapacityType `json:"capacityTypes,omitempty"` // Strategy is the INNER axis: how to rank providers within the active @@ -116,7 +115,7 @@ const ( // CapacityType is the purchase model (the outer axis). Each provider maps it to // its own concept — e.g. RunPod Spot -> interruptible/podRentInterruptable. -// +kubebuilder:validation:Enum=Spot;OnDemand;Reserved +// +kubebuilder:validation:Enum=Spot;OnDemand type CapacityType string const ( @@ -124,9 +123,6 @@ const ( CapacitySpot CapacityType = "Spot" // CapacityOnDemand is standard pay-as-you-go capacity. CapacityOnDemand CapacityType = "OnDemand" - // CapacityReserved is pre-committed/reserved capacity (not all providers - // support it; reserved for future use). - CapacityReserved CapacityType = "Reserved" ) // FailoverPolicy tunes capacity-error failover. Failover is always on — backing diff --git a/config/crd/bases/nebula.inftyai.com_nodeclaims.yaml b/config/crd/bases/nebula.inftyai.com_nodeclaims.yaml index 2aad621..9454426 100644 --- a/config/crd/bases/nebula.inftyai.com_nodeclaims.yaml +++ b/config/crd/bases/nebula.inftyai.com_nodeclaims.yaml @@ -87,7 +87,7 @@ spec: capacityType: description: |- CapacityType is the purchase tier the placement optimizer selected - (Spot/OnDemand/Reserved). It is stored durably here because it is the one + (Spot/OnDemand). It is stored durably here because it is the one provisioning input that cannot be read off the Pod, and Provision needs it to re-issue the request after a controller restart. Immutable, like Provider. Empty means "let the provider use its default" (e.g. Modal is @@ -95,7 +95,6 @@ spec: enum: - Spot - OnDemand - - Reserved type: string podRef: description: |- diff --git a/config/crd/bases/nebula.inftyai.com_nodepools.yaml b/config/crd/bases/nebula.inftyai.com_nodepools.yaml index 8f95a7f..5d4586b 100644 --- a/config/crd/bases/nebula.inftyai.com_nodepools.yaml +++ b/config/crd/bases/nebula.inftyai.com_nodepools.yaml @@ -75,15 +75,13 @@ spec: properties: capacityTypes: default: - - Reserved - OnDemand - Spot description: |- CapacityTypes is the OUTER axis: the purchase models to try, in fallback order. e.g. [Spot, OnDemand] means "use spot on any provider first; only when spot is exhausted everywhere, drop to on-demand". A single-element - list pins the pool to that type. This replaces a spot on/off flag and - extends to Reserved without new fields. + list pins the pool to that type. This replaces a spot on/off flag. items: description: |- CapacityType is the purchase model (the outer axis). Each provider maps it to @@ -91,7 +89,6 @@ spec: enum: - Spot - OnDemand - - Reserved type: string minItems: 1 type: array diff --git a/config/samples/deployment.yaml b/config/samples/deployment.yaml index 84268ae..82b9400 100644 --- a/config/samples/deployment.yaml +++ b/config/samples/deployment.yaml @@ -23,7 +23,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: gpu-workload-sample-a100 + name: gpu-workload-sample namespace: default labels: app.kubernetes.io/managed-by: nebula @@ -38,7 +38,7 @@ spec: app: gpu-workload-sample nebula.inftyai.com/enabled: "true" nebula.inftyai.com/nodepool: sample - nebula.inftyai.com/accelerator-type: a100-40gb + nebula.inftyai.com/accelerator-type: l4 spec: # Do NOT set nodeName or a provider nodeSelector yourself — the placement # controller fills the nodeSelector in when it ungates the Pod. Setting diff --git a/pkg/provider/catalog/catalog.go b/pkg/provider/catalog/catalog.go index e39afe0..382a116 100644 --- a/pkg/provider/catalog/catalog.go +++ b/pkg/provider/catalog/catalog.go @@ -125,7 +125,7 @@ func (c *Catalog) Offerings(providerName string) []provider.Offering { const ( colAccelerator = "accelerator_type" // required: canonical Nebula accelerator type colAcceleratorID = "accelerator_id" // optional: provider's own accelerator id (e.g. AWS instance type) - colCapacityType = "capacity_type" // required: Spot | OnDemand | Reserved + colCapacityType = "capacity_type" // required: Spot | OnDemand colPrice = "price_per_hour" // required: approximate USD/GPU-hour colAvailable = "available" // required: whether the provider offers it colGPUCount = "gpu_count" // optional: accelerators the id provides (AWS: baked into instance type) diff --git a/pkg/provider/catalog/data/aws.csv b/pkg/provider/catalog/data/aws.csv index a3df592..f947d1b 100644 --- a/pkg/provider/catalog/data/aws.csv +++ b/pkg/provider/catalog/data/aws.csv @@ -26,7 +26,7 @@ # accelerator_type canonical Nebula accelerator type # accelerator_id EC2 instance type that serves {accelerator_type, gpu_count} # gpu_count accelerators the instance type provides (the lookup key) -# capacity_type Spot | OnDemand | Reserved +# capacity_type Spot | OnDemand # price_per_hour approximate USD/instance-hour (seed; live probe may override) # available seed availability (the live probe is authoritative per region) # region BLANK — stamped per configured region by the live probe diff --git a/pkg/provider/catalog/data/modal.csv b/pkg/provider/catalog/data/modal.csv index caaae26..2bd131a 100644 --- a/pkg/provider/catalog/data/modal.csv +++ b/pkg/provider/catalog/data/modal.csv @@ -22,7 +22,7 @@ # parameter (off the Pod's nvidia.com/gpu resource), so it is # not a catalog lookup dimension. AWS, where the count is baked # into the instance type, fills it in. -# capacity_type Spot | OnDemand | Reserved +# capacity_type Spot | OnDemand # price_per_hour approximate on-demand USD/GPU-hour # available whether Modal currently offers it (a live probe may override) # region BLANK — Modal is region-simple diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index d2a677c..093f3cb 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -133,7 +133,7 @@ type ProvisionRequest struct { // ClaimName is the NodeClaim name; providers without native tags encode it // into the instance name so List/Terminate can find the instance later. ClaimName string - // CapacityType is the tier the optimizer selected (Spot/OnDemand/Reserved). + // CapacityType is the tier the optimizer selected (Spot/OnDemand). // This is the one workload-independent decision that cannot be expressed on // the Pod, so it must be passed explicitly. CapacityType nebulav1alpha1.CapacityType From f6964907c9313d01a84ca84fedba97a9a0751b31 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 31 Jul 2026 10:55:07 +0100 Subject: [PATCH 2/2] add more tests Signed-off-by: kerthcet --- .../controller/nodepool_validation_test.go | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/internal/controller/nodepool_validation_test.go b/internal/controller/nodepool_validation_test.go index f27c388..0b39583 100644 --- a/internal/controller/nodepool_validation_test.go +++ b/internal/controller/nodepool_validation_test.go @@ -71,4 +71,38 @@ var _ = Describe("NodePool spec validation (CEL)", func() { Expect(k8sClient.Create(ctx, pool)).To(Succeed()) Expect(k8sClient.Delete(ctx, pool)).To(Succeed()) }) + + It("rejects a capacityType outside the Spot;OnDemand enum", func() { + // "Reserved" was a formerly-declared tier that never worked (the AWS adapter + // aliased it to a plain OnDemand launch) and has been removed from the enum. + // The CRD's x-kubernetes enum must now reject it so a stale spec cannot be + // admitted and silently degrade to OnDemand. + pool := &nebulav1alpha1.NodePool{ + ObjectMeta: metav1.ObjectMeta{Name: "bad-capacity-type"}, + Spec: nebulav1alpha1.NodePoolSpec{ + Providers: []nebulav1alpha1.ProviderSpec{{Name: "modal"}}, + CapacityTypes: []nebulav1alpha1.CapacityType{"Reserved"}, + Strategy: nebulav1alpha1.StrategyOrdered, + }, + } + err := k8sClient.Create(ctx, pool) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Unsupported value: \"Reserved\"")) + }) + + It("admits the Spot and OnDemand capacity tiers", func() { + pool := &nebulav1alpha1.NodePool{ + ObjectMeta: metav1.ObjectMeta{Name: "ok-capacity-types"}, + Spec: nebulav1alpha1.NodePoolSpec{ + Providers: []nebulav1alpha1.ProviderSpec{{Name: "modal"}}, + CapacityTypes: []nebulav1alpha1.CapacityType{ + nebulav1alpha1.CapacityOnDemand, + nebulav1alpha1.CapacitySpot, + }, + Strategy: nebulav1alpha1.StrategyOrdered, + }, + } + Expect(k8sClient.Create(ctx, pool)).To(Succeed()) + Expect(k8sClient.Delete(ctx, pool)).To(Succeed()) + }) })