From e4bb560fd1551913c028e73749d1235b4c9e556d Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Wed, 1 Jul 2026 14:24:38 +0200 Subject: [PATCH 1/2] fix(operator): stage DGD admission path migration Signed-off-by: Dr. Stefan Schimanski --- .../templates/webhook-configuration.yaml | 12 +- .../dynamographdeployment_handler.go | 63 +++++++++- .../dynamographdeployment_handler_test.go | 25 ++++ .../dynamographdeployment_conversion.go | 45 +++++++ .../dynamographdeployment_handler.go | 117 +++++++++++++++--- .../dynamographdeployment_handler_test.go | 63 ++++++++++ 6 files changed, 300 insertions(+), 25 deletions(-) create mode 100644 deploy/operator/internal/webhook/dynamographdeployment_conversion.go create mode 100644 deploy/operator/internal/webhook/validation/dynamographdeployment_handler_test.go diff --git a/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml b/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml index 191f8e2c2ab6..0624937e9373 100644 --- a/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml +++ b/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml @@ -78,7 +78,9 @@ webhooks: service: name: {{ include "dynamo-operator.fullname" . }}-webhook-service namespace: {{ .Release.Namespace }} - path: /validate/nvidia.com/v1beta1/dynamographdeployments + # Keep the legacy registration until 1.4. The operator serves both + # endpoints and converts this v1alpha1 request to v1beta1 internally. + path: /validate-nvidia-com-v1alpha1-dynamographdeployment failurePolicy: {{ .Values.webhook.failurePolicy }} name: vdynamographdeployment.kb.io {{- if .Values.webhook.namespaceSelector }} @@ -93,7 +95,7 @@ webhooks: - apiGroups: - nvidia.com apiVersions: - - v1beta1 + - v1alpha1 operations: - CREATE - UPDATE @@ -234,7 +236,9 @@ webhooks: service: name: {{ include "dynamo-operator.fullname" . }}-webhook-service namespace: {{ .Release.Namespace }} - path: /mutate/nvidia.com/v1beta1/dynamographdeployments + # Keep the legacy registration until 1.4. The operator serves both + # endpoints and converts this v1alpha1 request to v1beta1 internally. + path: /mutate-nvidia-com-v1alpha1-dynamographdeployment failurePolicy: {{ .Values.webhook.failurePolicy }} name: mdynamographdeployment.kb.io {{- if .Values.webhook.namespaceSelector }} @@ -249,7 +253,7 @@ webhooks: - apiGroups: - nvidia.com apiVersions: - - v1beta1 + - v1alpha1 operations: - CREATE - UPDATE diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler.go b/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler.go index e1173f26d188..edd2b4bd6219 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler.go @@ -22,6 +22,7 @@ import ( "fmt" "strings" + nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1" nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" "github.com/ai-dynamo/dynamo/deploy/operator/internal/consts" internalwebhook "github.com/ai-dynamo/dynamo/deploy/operator/internal/webhook" @@ -34,8 +35,9 @@ import ( ) const ( - dgdDefaultingWebhookName = "dynamographdeployment-defaulting-webhook" - dgdDefaultingWebhookPath = "/mutate/nvidia.com/v1beta1/dynamographdeployments" + dgdDefaultingWebhookName = "dynamographdeployment-defaulting-webhook" + dgdV1Alpha1DefaultingWebhookPath = "/mutate-nvidia-com-v1alpha1-dynamographdeployment" + dgdV1Beta1DefaultingWebhookPath = "/mutate/nvidia.com/v1beta1/dynamographdeployments" ) // DGDDefaulter is a mutating webhook handler that stamps DynamoGraphDeployments @@ -46,6 +48,13 @@ type DGDDefaulter struct { GroveEnabled bool } +// dgdV1Alpha1Defaulter keeps the previous endpoint available during the +// v1alpha1-to-v1beta1 admission migration. It applies v1beta1 defaulting and +// converts the result back to the object version used by the legacy endpoint. +type dgdV1Alpha1Defaulter struct { + defaulter *DGDDefaulter +} + // NewDGDDefaulter creates a new DGDDefaulter with the given operator version. func NewDGDDefaulter(operatorVersion string, groveEnabled bool) *DGDDefaulter { return &DGDDefaulter{ @@ -62,8 +71,6 @@ func NewDGDDefaulter(operatorVersion string, groveEnabled bool) *DGDDefaulter { // On CREATE: stamps nvidia.com/dynamo-operator-origin-version with the operator version. // On UPDATE/DELETE: the origin version annotation is immutable once set. func (d *DGDDefaulter) Default(ctx context.Context, obj runtime.Object) error { - logger := log.FromContext(ctx).WithName(dgdDefaultingWebhookName) - if err := internalwebhook.ValidateAdmissionGVK(ctx, nvidiacomv1beta1.DynamoGraphDeploymentGVK); err != nil { return err } @@ -72,6 +79,14 @@ func (d *DGDDefaulter) Default(ctx context.Context, obj runtime.Object) error { if !ok { return fmt.Errorf("expected DynamoGraphDeployment but got %T", obj) } + return d.defaultV1Beta1(ctx, dgd) +} + +func (d *DGDDefaulter) defaultV1Beta1( + ctx context.Context, + dgd *nvidiacomv1beta1.DynamoGraphDeployment, +) error { + logger := log.FromContext(ctx).WithName(dgdDefaultingWebhookName) req, err := admission.RequestFromContext(ctx) if err != nil { @@ -119,9 +134,45 @@ func (d *DGDDefaulter) isGrovePathway(dgd *nvidiacomv1beta1.DynamoGraphDeploymen // RegisterWithManager registers the defaulting webhook with the manager. func (d *DGDDefaulter) RegisterWithManager(mgr manager.Manager) error { - webhook := admission. + betaWebhook := admission. WithCustomDefaulter(mgr.GetScheme(), &nvidiacomv1beta1.DynamoGraphDeployment{}, d). WithRecoverPanic(true) - mgr.GetWebhookServer().Register(dgdDefaultingWebhookPath, webhook) + mgr.GetWebhookServer().Register(dgdV1Beta1DefaultingWebhookPath, betaWebhook) + + // Keep the v1alpha1 endpoint in the binary before the Helm registration + // moves to v1beta1. This lets an upgrade switch the registration only after + // all running operators already serve both endpoints. + alphaDefaulter := &dgdV1Alpha1Defaulter{defaulter: d} + alphaWebhook := admission. + WithCustomDefaulter(mgr.GetScheme(), &nvidiacomv1alpha1.DynamoGraphDeployment{}, alphaDefaulter). + WithRecoverPanic(true) + mgr.GetWebhookServer().Register(dgdV1Alpha1DefaultingWebhookPath, alphaWebhook) + return nil +} + +func (d *dgdV1Alpha1Defaulter) Default(ctx context.Context, obj runtime.Object) error { + if err := internalwebhook.ValidateAdmissionGVK(ctx, nvidiacomv1alpha1.DynamoGraphDeploymentGVK); err != nil { + return err + } + + alpha, ok := obj.(*nvidiacomv1alpha1.DynamoGraphDeployment) + if !ok { + return fmt.Errorf("expected DynamoGraphDeployment but got %T", obj) + } + + beta, err := internalwebhook.ConvertDynamoGraphDeploymentToV1Beta1(alpha) + if err != nil { + return err + } + if err := d.defaulter.defaultV1Beta1(ctx, beta); err != nil { + return err + } + + converted, err := internalwebhook.ConvertDynamoGraphDeploymentToV1Alpha1(beta) + if err != nil { + return err + } + converted.TypeMeta = alpha.TypeMeta + *alpha = *converted return nil } diff --git a/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler_test.go b/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler_test.go index 14723f0dd9d8..83775bd8b9be 100644 --- a/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler_test.go +++ b/deploy/operator/internal/webhook/defaulting/dynamographdeployment_handler_test.go @@ -21,6 +21,7 @@ import ( "context" "testing" + nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1" nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" "github.com/ai-dynamo/dynamo/deploy/operator/internal/consts" admissionv1 "k8s.io/api/admission/v1" @@ -434,3 +435,27 @@ func TestDGDDefaulter_DefaultsGroveMinAvailable(t *testing.T) { }) } } + +func TestDGDV1Alpha1Defaulter_Default(t *testing.T) { + dgd := &nvidiacomv1alpha1.DynamoGraphDeployment{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: nvidiacomv1alpha1.DynamoGraphDeploymentSpec{ + Services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{ + "worker": { + ComponentType: consts.ComponentTypeWorker, + }, + }, + }, + } + defaulter := &dgdV1Alpha1Defaulter{defaulter: NewDGDDefaulter("0.9.0", false)} + + if err := defaulter.Default(admissionCtx(admissionv1.Create, nvidiacomv1alpha1.DynamoGraphDeploymentGVK), dgd); err != nil { + t.Fatalf("Default() unexpected error: %v", err) + } + if got := dgd.Annotations[consts.KubeAnnotationDynamoOperatorOriginVersion]; got != "0.9.0" { + t.Errorf("origin annotation = %q, want %q", got, "0.9.0") + } + if got := dgd.Spec.Services["worker"].Replicas; got == nil || *got != 1 { + t.Errorf("worker replicas = %v, want 1", got) + } +} diff --git a/deploy/operator/internal/webhook/dynamographdeployment_conversion.go b/deploy/operator/internal/webhook/dynamographdeployment_conversion.go new file mode 100644 index 000000000000..a32afc77a0cd --- /dev/null +++ b/deploy/operator/internal/webhook/dynamographdeployment_conversion.go @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * 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 webhook + +import ( + "fmt" + + nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1" + nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" +) + +// ConvertDynamoGraphDeploymentToV1Beta1 converts an admission object from the +// v1alpha1 spoke to the v1beta1 hub used by the DGD admission logic. +func ConvertDynamoGraphDeploymentToV1Beta1(src *nvidiacomv1alpha1.DynamoGraphDeployment) (*nvidiacomv1beta1.DynamoGraphDeployment, error) { + dst := &nvidiacomv1beta1.DynamoGraphDeployment{} + if err := src.ConvertTo(dst); err != nil { + return nil, fmt.Errorf("convert v1alpha1 DynamoGraphDeployment to v1beta1: %w", err) + } + return dst, nil +} + +// ConvertDynamoGraphDeploymentToV1Alpha1 converts a v1beta1 hub object to the +// v1alpha1 spoke expected by the legacy mutation endpoint. +func ConvertDynamoGraphDeploymentToV1Alpha1(src *nvidiacomv1beta1.DynamoGraphDeployment) (*nvidiacomv1alpha1.DynamoGraphDeployment, error) { + dst := &nvidiacomv1alpha1.DynamoGraphDeployment{} + if err := dst.ConvertFrom(src); err != nil { + return nil, fmt.Errorf("convert v1beta1 DynamoGraphDeployment to v1alpha1: %w", err) + } + return dst, nil +} diff --git a/deploy/operator/internal/webhook/validation/dynamographdeployment_handler.go b/deploy/operator/internal/webhook/validation/dynamographdeployment_handler.go index 35180b2dc91d..796935cbb033 100644 --- a/deploy/operator/internal/webhook/validation/dynamographdeployment_handler.go +++ b/deploy/operator/internal/webhook/validation/dynamographdeployment_handler.go @@ -21,12 +21,15 @@ import ( "context" "fmt" + nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1" nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" "github.com/ai-dynamo/dynamo/deploy/operator/internal/consts" "github.com/ai-dynamo/dynamo/deploy/operator/internal/observability" internalwebhook "github.com/ai-dynamo/dynamo/deploy/operator/internal/webhook" authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -34,8 +37,9 @@ import ( const ( // DynamoGraphDeploymentWebhookName is the name of the validating webhook handler for DynamoGraphDeployment. - DynamoGraphDeploymentWebhookName = "dynamographdeployment-validating-webhook" - dynamoGraphDeploymentWebhookPath = "/validate/nvidia.com/v1beta1/dynamographdeployments" + DynamoGraphDeploymentWebhookName = "dynamographdeployment-validating-webhook" + dynamoGraphDeploymentV1Alpha1WebhookPath = "/validate-nvidia-com-v1alpha1-dynamographdeployment" + dynamoGraphDeploymentV1Beta1WebhookPath = "/validate/nvidia.com/v1beta1/dynamographdeployments" ) // DynamoGraphDeploymentHandler is a handler for validating DynamoGraphDeployment resources. @@ -46,6 +50,13 @@ type DynamoGraphDeploymentHandler struct { groveEnabled bool } +// dynamoGraphDeploymentV1Alpha1Handler keeps the previous endpoint available +// during the v1alpha1-to-v1beta1 admission migration. It converts the spoke +// request to the v1beta1 hub before invoking the shared validation logic. +type dynamoGraphDeploymentV1Alpha1Handler struct { + handler *DynamoGraphDeploymentHandler +} + // NewDynamoGraphDeploymentHandler creates a new handler for DynamoGraphDeployment Webhook. // operatorPrincipal is the full Kubernetes SA username of the operator, used to authorize // replica changes on scaling-adapter-enabled components (#7656). @@ -60,9 +71,17 @@ func NewDynamoGraphDeploymentHandler(mgr manager.Manager, operatorPrincipal stri // ValidateCreate validates a DynamoGraphDeployment create request. func (h *DynamoGraphDeploymentHandler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return h.validateCreate(ctx, obj, nvidiacomv1beta1.DynamoGraphDeploymentGVK) +} + +func (h *DynamoGraphDeploymentHandler) validateCreate( + ctx context.Context, + obj runtime.Object, + expectedGVK schema.GroupVersionKind, +) (admission.Warnings, error) { logger := log.FromContext(ctx).WithName(DynamoGraphDeploymentWebhookName) - if err := internalwebhook.ValidateAdmissionGVK(ctx, nvidiacomv1beta1.DynamoGraphDeploymentGVK); err != nil { + if err := internalwebhook.ValidateAdmissionGVK(ctx, expectedGVK); err != nil { return nil, err } @@ -80,9 +99,17 @@ func (h *DynamoGraphDeploymentHandler) ValidateCreate(ctx context.Context, obj r // ValidateUpdate validates a DynamoGraphDeployment update request. func (h *DynamoGraphDeploymentHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + return h.validateUpdate(ctx, oldObj, newObj, nvidiacomv1beta1.DynamoGraphDeploymentGVK) +} + +func (h *DynamoGraphDeploymentHandler) validateUpdate( + ctx context.Context, + oldObj, newObj runtime.Object, + expectedGVK schema.GroupVersionKind, +) (admission.Warnings, error) { logger := log.FromContext(ctx).WithName(DynamoGraphDeploymentWebhookName) - if err := internalwebhook.ValidateAdmissionGVK(ctx, nvidiacomv1beta1.DynamoGraphDeploymentGVK); err != nil { + if err := internalwebhook.ValidateAdmissionGVK(ctx, expectedGVK); err != nil { return nil, err } @@ -139,18 +166,26 @@ func (h *DynamoGraphDeploymentHandler) ValidateUpdate(ctx context.Context, oldOb // ValidateDelete validates a DynamoGraphDeployment delete request. func (h *DynamoGraphDeploymentHandler) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return h.validateDelete(ctx, obj, nvidiacomv1beta1.DynamoGraphDeploymentGVK) +} + +func (h *DynamoGraphDeploymentHandler) validateDelete( + ctx context.Context, + obj runtime.Object, + expectedGVK schema.GroupVersionKind, +) (admission.Warnings, error) { logger := log.FromContext(ctx).WithName(DynamoGraphDeploymentWebhookName) - if err := internalwebhook.ValidateAdmissionGVK(ctx, nvidiacomv1beta1.DynamoGraphDeploymentGVK); err != nil { + if err := internalwebhook.ValidateAdmissionGVK(ctx, expectedGVK); err != nil { return nil, err } - deployment, err := castToDynamoGraphDeployment(obj) + deployment, err := dynamoGraphDeploymentMetadata(obj) if err != nil { return nil, err } - logger.Info("validate delete", "name", deployment.Name, "namespace", deployment.Namespace) + logger.Info("validate delete", "name", deployment.GetName(), "namespace", deployment.GetNamespace()) // No special validation needed for deletion return nil, nil @@ -160,24 +195,76 @@ func (h *DynamoGraphDeploymentHandler) ValidateDelete(ctx context.Context, obj r // The handler is automatically wrapped with LeaseAwareValidator to add namespace exclusion logic // and ObservedValidator to add metrics collection. func (h *DynamoGraphDeploymentHandler) RegisterWithManager(mgr manager.Manager) error { + h.registerWithManager( + mgr, + &nvidiacomv1beta1.DynamoGraphDeployment{}, + dynamoGraphDeploymentV1Beta1WebhookPath, + h, + ) + + // Keep the v1alpha1 endpoint in the binary before the Helm registration + // moves to v1beta1. This lets an upgrade switch the registration only after + // all running operators already serve both endpoints. + alphaHandler := &dynamoGraphDeploymentV1Alpha1Handler{handler: h} + h.registerWithManager( + mgr, + &nvidiacomv1alpha1.DynamoGraphDeployment{}, + dynamoGraphDeploymentV1Alpha1WebhookPath, + alphaHandler, + ) + return nil +} + +func (h *DynamoGraphDeploymentHandler) registerWithManager( + mgr manager.Manager, + object runtime.Object, + path string, + validator admission.CustomValidator, +) { // Wrap the handler with lease-aware logic for cluster-wide coordination - leaseAwareValidator := internalwebhook.NewLeaseAwareValidator(h, internalwebhook.GetExcludedNamespaces()) + leaseAwareValidator := internalwebhook.NewLeaseAwareValidator(validator, internalwebhook.GetExcludedNamespaces()) // Wrap with metrics collection observedValidator := observability.NewObservedValidator(leaseAwareValidator, consts.ResourceTypeDynamoGraphDeployment) webhook := admission. - WithCustomValidator(mgr.GetScheme(), &nvidiacomv1beta1.DynamoGraphDeployment{}, observedValidator). + WithCustomValidator(mgr.GetScheme(), object, observedValidator). WithRecoverPanic(true) - mgr.GetWebhookServer().Register(dynamoGraphDeploymentWebhookPath, webhook) - return nil + mgr.GetWebhookServer().Register(path, webhook) +} + +func (h *dynamoGraphDeploymentV1Alpha1Handler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return h.handler.validateCreate(ctx, obj, nvidiacomv1alpha1.DynamoGraphDeploymentGVK) +} + +func (h *dynamoGraphDeploymentV1Alpha1Handler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + return h.handler.validateUpdate(ctx, oldObj, newObj, nvidiacomv1alpha1.DynamoGraphDeploymentGVK) } -// castToDynamoGraphDeployment attempts to cast a runtime.Object to a DynamoGraphDeployment. +func (h *dynamoGraphDeploymentV1Alpha1Handler) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return h.handler.validateDelete(ctx, obj, nvidiacomv1alpha1.DynamoGraphDeploymentGVK) +} + +// castToDynamoGraphDeployment converts the v1alpha1 spoke to the v1beta1 hub +// used by the DGD validator, or returns a v1beta1 object unchanged. func castToDynamoGraphDeployment(obj runtime.Object) (*nvidiacomv1beta1.DynamoGraphDeployment, error) { - deployment, ok := obj.(*nvidiacomv1beta1.DynamoGraphDeployment) - if !ok { + switch deployment := obj.(type) { + case *nvidiacomv1beta1.DynamoGraphDeployment: + return deployment, nil + case *nvidiacomv1alpha1.DynamoGraphDeployment: + return internalwebhook.ConvertDynamoGraphDeploymentToV1Beta1(deployment) + default: + return nil, fmt.Errorf("expected v1alpha1 or v1beta1 DynamoGraphDeployment but got %T", obj) + } +} + +func dynamoGraphDeploymentMetadata(obj runtime.Object) (metav1.Object, error) { + switch deployment := obj.(type) { + case *nvidiacomv1beta1.DynamoGraphDeployment: + return deployment, nil + case *nvidiacomv1alpha1.DynamoGraphDeployment: + return deployment, nil + default: return nil, fmt.Errorf("expected DynamoGraphDeployment but got %T", obj) } - return deployment, nil } diff --git a/deploy/operator/internal/webhook/validation/dynamographdeployment_handler_test.go b/deploy/operator/internal/webhook/validation/dynamographdeployment_handler_test.go new file mode 100644 index 000000000000..0ac5ae35deff --- /dev/null +++ b/deploy/operator/internal/webhook/validation/dynamographdeployment_handler_test.go @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * 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 validation + +import ( + "context" + "strings" + "testing" + + nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + k8sptr "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestDynamoGraphDeploymentV1Alpha1Handler_ValidateCreate(t *testing.T) { + dgd := newAlphaDGDForCompatibilityValidation() + dgd.Spec.Services["worker"].Replicas = k8sptr.To(int32(-1)) + + handler := &dynamoGraphDeploymentV1Alpha1Handler{ + handler: NewDynamoGraphDeploymentHandler(nil, "", false), + } + _, err := handler.ValidateCreate( + dgdAdmissionContext(admissionv1.Create, nvidiacomv1alpha1.DynamoGraphDeploymentGVK), + dgd, + ) + if err == nil { + t.Fatal("ValidateCreate() error = nil, want converted v1beta1 validation error") + } + if !strings.Contains(err.Error(), "spec.components[worker].replicas must be non-negative") { + t.Fatalf("ValidateCreate() error = %q, want v1beta1 component validation error", err) + } +} + +func dgdAdmissionContext(op admissionv1.Operation, gvk schema.GroupVersionKind) context.Context { + return admission.NewContextWithRequest(context.Background(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: op, + Kind: metav1.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + }, + }, + }) +} From 107ff0f5875934dff92013c786a2804bfcfccbd6 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Wed, 1 Jul 2026 16:35:38 +0200 Subject: [PATCH 2/2] fixup! fix(operator): stage DGD admission path migration Signed-off-by: Dr. Stefan Schimanski --- .../operator/templates/webhook-configuration.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml b/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml index 0624937e9373..d2075d2c7269 100644 --- a/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml +++ b/deploy/helm/charts/platform/components/operator/templates/webhook-configuration.yaml @@ -78,8 +78,8 @@ webhooks: service: name: {{ include "dynamo-operator.fullname" . }}-webhook-service namespace: {{ .Release.Namespace }} - # Keep the legacy registration until 1.4. The operator serves both - # endpoints and converts this v1alpha1 request to v1beta1 internally. + # TODO(1.4): Switch this path to /validate/nvidia.com/v1beta1/dynamographdeployments + # and apiVersions below to v1beta1. The 1.3 operator serves both endpoints. path: /validate-nvidia-com-v1alpha1-dynamographdeployment failurePolicy: {{ .Values.webhook.failurePolicy }} name: vdynamographdeployment.kb.io @@ -236,8 +236,8 @@ webhooks: service: name: {{ include "dynamo-operator.fullname" . }}-webhook-service namespace: {{ .Release.Namespace }} - # Keep the legacy registration until 1.4. The operator serves both - # endpoints and converts this v1alpha1 request to v1beta1 internally. + # TODO(1.4): Switch this path to /mutate/nvidia.com/v1beta1/dynamographdeployments + # and apiVersions below to v1beta1. The 1.3 operator serves both endpoints. path: /mutate-nvidia-com-v1alpha1-dynamographdeployment failurePolicy: {{ .Values.webhook.failurePolicy }} name: mdynamographdeployment.kb.io