Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions deploy/operator/internal/features/compatibility/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// Package compatibility defines per-resource gates that preserve behavior
// across operator upgrades.
//
// Compatibility gates are evaluated from resource metadata, such as the
// operator version that originally created a resource. Unlike the operator-wide
// gates in the parent features package, they are not runtime capabilities and
// must not be included in operator gate snapshots or namespace ownership Leases.
// Compatibility gates are evaluated per resource from durable metadata or the
// resolved Dynamo runtime version. Unlike the operator-wide gates in the parent
// features package, they must not be included in operator gate snapshots or
// namespace ownership Leases.
package compatibility
5 changes: 2 additions & 3 deletions deploy/operator/internal/features/compatibility/gates.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ package compatibility

import semver "github.com/Masterminds/semver/v3"

// Feature gates gated on the operator origin version (the operator version that
// first reconciled / created the DGD resource).
// Compatibility feature gates.

var (
// VLLMMultiprocessing gates the use of vLLM native multiprocessing (mp)
// instead of Ray for multi-node deployments. Enabled for DGDs originally
// created by operator >= 1.0.0.
VLLMMultiprocessing = Gate{
VLLMMultiprocessing = OriginGate{
Name: "VLLMMultiprocessing",
MinOriginVersion: *semver.MustParse("1.0.0"),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)

// Gate represents a feature gated on the operator version
// OriginGate represents a feature gated on the operator version
// that originally created the DGD (nvidia.com/dynamo-operator-origin-version).
//
// The origin version is stamped by the mutating webhook at CREATE time and never
Expand All @@ -32,7 +32,7 @@ import (
//
// When the annotation is absent (pre-upgrade DGD), Enabled returns false
// to preserve backward compatibility.
type Gate struct {
type OriginGate struct {
Name string // Human-readable feature name (for logging)
MinOriginVersion semver.Version // Minimum origin version required (semver)
}
Expand All @@ -45,7 +45,7 @@ type Gate struct {
// - origin version annotation is absent (pre-upgrade DGD)
// - origin version is not valid semver
// - origin version < MinOriginVersion
func (fg Gate) Enabled(annotations map[string]string) bool {
func (fg OriginGate) Enabled(annotations map[string]string) bool {
logger := log.Log.WithName("compatibility").WithValues("feature", fg.Name)

originVersion, exists := annotations[consts.KubeAnnotationDynamoOperatorOriginVersion]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
"github.com/ai-dynamo/dynamo/deploy/operator/internal/consts"
)

func TestGateEnabled(t *testing.T) {
gate := Gate{
func TestOriginGateEnabled(t *testing.T) {
gate := OriginGate{
Name: "TestFeature",
MinOriginVersion: *semver.MustParse("1.0.0"),
}
Expand Down Expand Up @@ -113,7 +113,7 @@ func TestGateEnabled(t *testing.T) {
}
}

func TestGateDifferentThresholds(t *testing.T) {
func TestOriginGateDifferentThresholds(t *testing.T) {
annotations := map[string]string{
consts.KubeAnnotationDynamoOperatorOriginVersion: "0.9.0",
}
Expand Down Expand Up @@ -142,7 +142,7 @@ func TestGateDifferentThresholds(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gate := Gate{
gate := OriginGate{
Name: "TestFeature",
MinOriginVersion: tt.minOriginVersion,
}
Expand Down
37 changes: 37 additions & 0 deletions deploy/operator/internal/features/compatibility/runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package compatibility

import (
"github.com/ai-dynamo/dynamo/deploy/operator/internal/runtimeversion"
"sigs.k8s.io/controller-runtime/pkg/log"
)

// RuntimeGate represents a feature gated on the resolved Dynamo runtime
// compatibility version.
type RuntimeGate struct {
Name string // Human-readable feature name (for logging)
MinRuntimeVersion runtimeversion.Version // Minimum runtime version required
}

// Enabled returns true if runtimeVersion is known and meets or exceeds the
// gate's minimum runtime version.
func (fg RuntimeGate) Enabled(runtimeVersion *runtimeversion.Version) bool {
logger := log.Log.WithName("compatibility").WithValues("feature", fg.Name)
if runtimeVersion == nil {
logger.V(1).Info("Runtime version unknown, feature disabled")
return false
}

enabled := runtimeVersion.AtLeast(fg.MinRuntimeVersion)
Comment thread
xianlubird marked this conversation as resolved.
Outdated

logger.V(1).Info("Runtime feature gate evaluated",
"runtimeVersion", runtimeVersion.String(),
"threshold", fg.MinRuntimeVersion,
"enabled", enabled)
Comment thread
xianlubird marked this conversation as resolved.
Outdated

return enabled
}
54 changes: 54 additions & 0 deletions deploy/operator/internal/features/compatibility/runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package compatibility

import (
"testing"

"github.com/ai-dynamo/dynamo/deploy/operator/internal/runtimeversion"
)

func TestRuntimeGateEnabled(t *testing.T) {
gate := RuntimeGate{
Name: "TestFeature",
MinRuntimeVersion: runtimeversion.Version{Major: 1, Minor: 4, Patch: 0},
}

tests := []struct {
name string
version *runtimeversion.Version
want bool
}{
{
name: "unknown runtime version is disabled",
version: nil,
want: false,
},
{
name: "runtime version below threshold is disabled",
version: &runtimeversion.Version{Major: 1, Minor: 3, Patch: 9},
want: false,
},
{
name: "runtime version at threshold is enabled",
version: &runtimeversion.Version{Major: 1, Minor: 4, Patch: 0},
want: true,
},
{
name: "runtime version above threshold is enabled",
version: &runtimeversion.Version{Major: 2, Minor: 0, Patch: 0},
want: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := gate.Enabled(tt.version); got != tt.want {
t.Fatalf("Enabled(%v) = %t, want %t", tt.version, got, tt.want)
}
})
}
Comment thread
xianlubird marked this conversation as resolved.
Comment thread
xianlubird marked this conversation as resolved.
Outdated
}
11 changes: 11 additions & 0 deletions deploy/operator/internal/runtimeversion/runtimeversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ func (v Version) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}

// AtLeast reports whether v is greater than or equal to minimum.
func (v Version) AtLeast(minimum Version) bool {
Comment thread
tmonty12 marked this conversation as resolved.
Outdated
if v.Major != minimum.Major {
return v.Major > minimum.Major
}
if v.Minor != minimum.Minor {
return v.Minor > minimum.Minor
}
return v.Patch >= minimum.Patch
}

// Parse returns the compatibility version represented by an explicit override.
func Parse(value string) (Version, error) {
version, err := semver.StrictNewVersion(value)
Expand Down
25 changes: 25 additions & 0 deletions deploy/operator/internal/runtimeversion/runtimeversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ package runtimeversion

import "testing"

func TestVersionAtLeast(t *testing.T) {
minimum := Version{Major: 1, Minor: 4, Patch: 1}
tests := []struct {
name string
version Version
want bool
}{
{name: "below major", version: Version{Major: 0, Minor: 9, Patch: 9}, want: false},
{name: "below minor", version: Version{Major: 1, Minor: 3, Patch: 9}, want: false},
{name: "below patch", version: Version{Major: 1, Minor: 4, Patch: 0}, want: false},
{name: "equal", version: Version{Major: 1, Minor: 4, Patch: 1}, want: true},
{name: "above patch", version: Version{Major: 1, Minor: 4, Patch: 2}, want: true},
{name: "above minor", version: Version{Major: 1, Minor: 5, Patch: 0}, want: true},
{name: "above major", version: Version{Major: 2, Minor: 0, Patch: 0}, want: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.version.AtLeast(minimum); got != tt.want {
t.Fatalf("%s.AtLeast(%s) = %t, want %t", tt.version, minimum, got, tt.want)
}
})
}
}

func TestParse(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading