From 998ab404956bfb240b056f0df8c6a9050cfff087 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Thu, 22 Oct 2026 10:03:39 +0200 Subject: [PATCH] operator/admission: derive default AISimulate image Signed-off-by: Dr. Stefan Schimanski --- .../platform/components/operator/values.yaml | 2 +- .../dynamographdeploymentrequest_types.go | 2 +- deploy/operator/cmd/main.go | 2 +- .../dynamographdeploymentrequest_handler.go | 16 ++++---- ...namographdeploymentrequest_handler_test.go | 7 ++-- ...namographdeploymentrequest_routing_test.go | 37 ++++++++++++++----- .../dynamographdeploymentrequest_v1beta2.go | 2 +- ...ographdeploymentrequest_v1beta2_helpers.go | 5 +-- .../webhook/validation/suite_envtest_test.go | 5 +-- .../additional-resources/api-reference-k8s.md | 2 +- .../kubernetes-api/full-api-reference.mdx | 2 +- 11 files changed, 48 insertions(+), 34 deletions(-) diff --git a/deploy/helm/charts/platform/components/operator/values.yaml b/deploy/helm/charts/platform/components/operator/values.yaml index bbc3df59e7c7..36f69db1aba7 100644 --- a/deploy/helm/charts/platform/components/operator/values.yaml +++ b/deploy/helm/charts/platform/components/operator/values.yaml @@ -116,7 +116,7 @@ controllerManager: # releases. dgdrDefaultImage: "" # AISimulate runner image put into v1beta2 DGDR spec.aisimulate.image when unset. - # Empty leaves it unset, and v1beta2 Requests without an image are rejected. + # Empty derives nvcr.io/nvidia/ai-dynamo/dynamo-planner:. dgdrAISimulateDefaultImage: "" resources: limits: diff --git a/deploy/operator/api/v1beta2/dynamographdeploymentrequest_types.go b/deploy/operator/api/v1beta2/dynamographdeploymentrequest_types.go index 703825595547..e20fc4d782d9 100644 --- a/deploy/operator/api/v1beta2/dynamographdeploymentrequest_types.go +++ b/deploy/operator/api/v1beta2/dynamographdeploymentrequest_types.go @@ -185,7 +185,7 @@ type ObjectiveSpec struct { // AISimulateSpec selects the AISimulate execution artifact and supplies native input. type AISimulateSpec struct { // Image selects a Dynamo-compatible AISimulate runner. When omitted, admission - // persists the operator's configured image. + // persists the configured override or a version-matched Dynamo planner image. // +optional // +kubebuilder:validation:MinLength=1 Image string `json:"image,omitempty"` diff --git a/deploy/operator/cmd/main.go b/deploy/operator/cmd/main.go index ce8c7a2503d6..3c762841c7a2 100644 --- a/deploy/operator/cmd/main.go +++ b/deploy/operator/cmd/main.go @@ -179,7 +179,7 @@ func main() { "Default DGDR profiler image, put into DGDR spec.image when unset; empty derives dynamo-planner:") flag.StringVar(&dgdrAISimulateDefaultImage, "dgdr-aisimulate-default-image", "", "AISimulate runner image, put into v1beta2 DGDR spec.aisimulate.image when unset; "+ - "empty leaves the image unset and the Request is rejected") + "empty derives dynamo-planner:") opts := zap.Options{ Development: true, } diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler.go b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler.go index eb6ef7dca92e..8826650e166c 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler.go @@ -76,6 +76,7 @@ func NewDGDRDefaulter(operatorVersion, defaultImage string) *DGDRDefaulter { // WithV1beta2 sets the defaults of v1beta2 Requests and returns the defaulter. func (d *DGDRDefaulter) WithV1beta2(options DGDRV1beta2Options) *DGDRDefaulter { + options.AISimulateImage = defaultPlannerImageFor(d.OperatorVersion, options.AISimulateImage) d.v1beta2 = options return d } @@ -127,7 +128,7 @@ func (d *DGDRDefaulter) Default(ctx context.Context, obj runtime.Object) error { // Image derivation remains create-only so updates do not rewrite an // intentionally cleared image. if req.Operation == admissionv1.Create && dgdr.Spec.Image == "" { - if img := d.defaultImageFor(); img != "" { + if img := defaultPlannerImageFor(d.OperatorVersion, d.DefaultImage); img != "" { dgdr.Spec.Image = img logger.Info("defaulted spec.image from operator version", "name", dgdr.Name, @@ -140,14 +141,13 @@ func (d *DGDRDefaulter) Default(ctx context.Context, obj runtime.Object) error { return nil } -// defaultImageFor returns the profiler image for spec.image: DefaultImage when -// set, else the derived image with a canonical semver tag, or an empty string -// when the operator version cannot be parsed. -func (d *DGDRDefaulter) defaultImageFor() string { - if d.DefaultImage != "" { - return d.DefaultImage +// defaultPlannerImageFor returns explicitImage when set, else the planner image with a +// canonical operator-version tag, or an empty string when the version cannot be parsed. +func defaultPlannerImageFor(operatorVersion, explicitImage string) string { + if explicitImage != "" { + return explicitImage } - version, err := semver.NewVersion(d.OperatorVersion) + version, err := semver.NewVersion(operatorVersion) if err != nil { return "" } diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler_test.go b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler_test.go index 4f2c35382611..48fafb9ead7c 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler_test.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_handler_test.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -func TestDGDRDefaulter_defaultImageFor(t *testing.T) { +func TestDefaultPlannerImageFor(t *testing.T) { // Note: the default planner image is only published starting from Dynamo 1.1.0, // so these tests use 1.1.0 as the earliest known valid version. tests := []struct { @@ -77,10 +77,9 @@ func TestDGDRDefaulter_defaultImageFor(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - d := NewDGDRDefaulter(tt.operatorVersion, tt.defaultImage) - got := d.defaultImageFor() + got := defaultPlannerImageFor(tt.operatorVersion, tt.defaultImage) if got != tt.expectedImage { - t.Errorf("defaultImageFor() = %q, want %q", got, tt.expectedImage) + t.Errorf("defaultPlannerImageFor() = %q, want %q", got, tt.expectedImage) } }) } diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_routing_test.go b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_routing_test.go index 7dbb6acd8950..416fe004f280 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_routing_test.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_routing_test.go @@ -67,17 +67,34 @@ func aisimulateImageOf(t *testing.T, raw []byte) string { } func TestDGDRDefaulter_RoutesByRequestedVersion(t *testing.T) { - d := NewDGDRDefaulter("1.1.0", "").WithV1beta2(DGDRV1beta2Options{AISimulateImage: "aisim:1"}) + for _, tt := range []struct { + name string + options DGDRV1beta2Options + want string + }{ + { + name: "an omitted image is derived from the operator version", + want: "nvcr.io/nvidia/ai-dynamo/dynamo-planner:1.6.0", + }, + { + name: "an explicit default image overrides derivation", + options: DGDRV1beta2Options{AISimulateImage: "registry.example/aisimulate:custom"}, + want: "registry.example/aisimulate:custom", + }, + } { + t.Run(tt.name, func(t *testing.T) { + d := NewDGDRDefaulter("1.6.0", "").WithV1beta2(tt.options) + dgdr := carrierRequest() + if err := d.Default(ctxForRequest(admissionv1.Create, "v1beta2"), dgdr); err != nil { + t.Fatalf("Default() error = %v", err) + } + if got := aisimulateImageOf(t, dgdr.Spec.V1Beta2.Raw); got != tt.want { + t.Fatalf("aisimulate.image = %q, want %q", got, tt.want) + } + }) + } - t.Run("a native v1beta2 request is defaulted from its carrier", func(t *testing.T) { - dgdr := carrierRequest() - if err := d.Default(ctxForRequest(admissionv1.Create, "v1beta2"), dgdr); err != nil { - t.Fatalf("Default() error = %v", err) - } - if got := aisimulateImageOf(t, dgdr.Spec.V1Beta2.Raw); got != "aisim:1" { - t.Fatalf("aisimulate.image = %q, want aisim:1", got) - } - }) + d := NewDGDRDefaulter("1.6.0", "").WithV1beta2(DGDRV1beta2Options{}) t.Run("RequestKind decides, not the converted Kind", func(t *testing.T) { dgdr := carrierRequest() diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2.go b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2.go index e8a6eebbc89d..ae9014d1e006 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2.go @@ -95,7 +95,7 @@ func defaultDynamoGraphDeploymentRequestV1beta2( type dynamoGraphDeploymentRequestV1beta2DefaultOptions struct { // hardware is the GPU identity and budget discovered from the cluster. hardware hardwareDefaultsV1beta2 - // aisimulateImage is the operator's configured AISimulate image. + // aisimulateImage is the operator's resolved AISimulate image. aisimulateImage string } diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2_helpers.go b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2_helpers.go index 31a9996534ea..56271a21b82b 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2_helpers.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeploymentrequest_v1beta2_helpers.go @@ -57,9 +57,8 @@ type HardwareDiscoverer interface { // DGDRV1beta2Options configures the defaults of v1beta2 Requests. type DGDRV1beta2Options struct { - // AISimulateImage is the operator's AISimulate runner image, persisted into - // spec.aisimulate.image when the caller omits it. Empty persists nothing, and - // validation then requires the image to be spelled out. + // AISimulateImage overrides the version-matched Dynamo planner image persisted + // into spec.aisimulate.image when the caller omits it. AISimulateImage string // Hardware discovers the cluster's GPUs. Nil disables hardware defaulting, and // validation then requires the hardware to be spelled out. diff --git a/deploy/operator/internal/webhook/validation/suite_envtest_test.go b/deploy/operator/internal/webhook/validation/suite_envtest_test.go index c09e02c99d5b..0886a298281e 100644 --- a/deploy/operator/internal/webhook/validation/suite_envtest_test.go +++ b/deploy/operator/internal/webhook/validation/suite_envtest_test.go @@ -44,7 +44,7 @@ const ( frontendImage150 = "registry.example/dynamo-frontend:1.5.0" legacyEPPImage140 = "registry.example/epp-image:1.4.0" legacySeedUsername = "operatorenv-legacy-seeder" - admissionAISimulateImage = "registry.example/aisimulate:1.2.0" + admissionAISimulateImage = "nvcr.io/nvidia/ai-dynamo/dynamo-planner:1.6.0" ) func groveProviderOverride(target, value string) *nvidiacomv1beta1.ProviderOverride { @@ -97,8 +97,7 @@ func setupAdmissionWebhooks(mgr ctrl.Manager, opts operatorenv.WebhookSetupOptio OperatorPrincipal: opts.OperatorPrincipal, Gate: admissionGate, DGDRV1beta2: webhookdefaulting.DGDRV1beta2Options{ - AISimulateImage: admissionAISimulateImage, - Hardware: admissionHardware, + Hardware: admissionHardware, }, }) } diff --git a/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md b/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md index 2fb7313d7700..899252723888 100644 --- a/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md +++ b/docs/fern/pages/reference/kubernetes-api/additional-resources/api-reference-k8s.md @@ -3242,7 +3242,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `image` _string_ | Image selects a Dynamo-compatible AISimulate runner. When omitted, admission
persists the operator's configured image. | | MinLength: 1
Optional: \{\}
| +| `image` _string_ | Image selects a Dynamo-compatible AISimulate runner. When omitted, admission
persists the configured override or a version-matched Dynamo planner image. | | MinLength: 1
Optional: \{\}
| | `versionOverride` _string_ | VersionOverride declares the AISimulate compatibility version as three numeric parts
(for example 0.13.0), not a semver version: no v prefix, pre-release or build suffix. It is required
when the image tag is not a version and wins over a version in the tag. | | MinLength: 1
Pattern: `^(0\|[1-9][0-9]\{0,3\})\.(0\|[1-9][0-9]\{0,3\})\.(0\|[1-9][0-9]\{0,3\})$`
Optional: \{\}
| | `parameters` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#rawextension-runtime-pkg)_ | Parameters contains native AISimulate input. Field names and nesting use
the upstream AISimulate JSON contract, including snake_case names. | | Type: object
Optional: \{\}
| diff --git a/docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx b/docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx index dfad335c1aff..019faa3c58ee 100644 --- a/docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx +++ b/docs/fern/pages/reference/kubernetes-api/full-api-reference.mdx @@ -4531,7 +4531,7 @@ AISimulateSpec selects the AISimulate execution artifact and supplies native inp **Appears in:** [DynamoGraphDeploymentRequestNativeSpec](#v1beta2-dynamographdeploymentrequestnativespec), [DynamoGraphDeploymentRequestSpec](#v1beta2-dynamographdeploymentrequestspec), [DynamoGraphDeploymentRunSpec](#dynamographdeploymentrunspec), [NativeSearchInput](#nativesearchinput) -Image selects a Dynamo-compatible AISimulate runner. When omitted, admission persists the operator's configured image. +Image selects a Dynamo-compatible AISimulate runner. When omitted, admission persists the configured override or a version-matched Dynamo planner image. **Validation:** MinLength: 1 Optional: \{\}