diff --git a/deploy/operator/internal/features/runtime/doc.go b/deploy/operator/internal/features/runtime/doc.go new file mode 100644 index 000000000000..efc4630fdc95 --- /dev/null +++ b/deploy/operator/internal/features/runtime/doc.go @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Package runtime defines explicit feature gates controlled by the Dynamo +// runtime compatibility version. +// +// Runtime gates make rendered defaults stable across operator upgrades. A +// runtime version change is the trigger for adopting new defaults, while +// explicit user configuration remains authoritative. +package runtime diff --git a/deploy/operator/internal/features/runtime/gate.go b/deploy/operator/internal/features/runtime/gate.go new file mode 100644 index 000000000000..4f624da77d9e --- /dev/null +++ b/deploy/operator/internal/features/runtime/gate.go @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package runtime + +import "github.com/ai-dynamo/dynamo/deploy/operator/internal/runtimeversion" + +// Gate controls a feature's rendered defaults by Dynamo runtime version. +type Gate struct { + Name string + MinRuntimeVersion runtimeversion.Version +} + +// Enabled reports whether a known runtime version meets the feature threshold. +func (g Gate) Enabled(version *runtimeversion.Version) bool { + if version == nil { + return false + } + + return version.Compare(g.MinRuntimeVersion) >= 0 +} diff --git a/deploy/operator/internal/features/runtime/gate_test.go b/deploy/operator/internal/features/runtime/gate_test.go new file mode 100644 index 000000000000..9fb5a47b51ce --- /dev/null +++ b/deploy/operator/internal/features/runtime/gate_test.go @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package runtime + +import ( + "testing" + + "github.com/ai-dynamo/dynamo/deploy/operator/internal/runtimeversion" +) + +func TestGateEnabled(t *testing.T) { + t.Log("define a feature introduced by Dynamo runtime 1.4.0") + gate := Gate{ + Name: "TestFeature", + MinRuntimeVersion: runtimeversion.Version{Major: 1, Minor: 4, Patch: 0}, + } + + t.Log("define versions below, at, and above the feature threshold") + tests := []struct { + name string + version *runtimeversion.Version + want bool + }{ + { + name: "unknown runtime", + }, + { + name: "older runtime", + version: &runtimeversion.Version{Major: 1, Minor: 3, Patch: 9}, + }, + { + name: "minimum supported runtime", + version: &runtimeversion.Version{Major: 1, Minor: 4, Patch: 0}, + want: true, + }, + { + name: "newer runtime", + version: &runtimeversion.Version{Major: 2, Minor: 0, Patch: 0}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Log("evaluate the feature for the resolved runtime version") + got := gate.Enabled(tt.version) + + t.Log("compare the gate decision") + if got != tt.want { + t.Fatalf("Enabled(%v) = %t, want %t", tt.version, got, tt.want) + } + }) + } +} + +func TestCanaryHealthChecksThreshold(t *testing.T) { + t.Log("inspect the central canary health-check feature gate") + got := CanaryHealthChecks.MinRuntimeVersion.String() + + t.Log("verify canary health checks are introduced by runtime 1.4.0") + if got != "1.4.0" { + t.Fatalf("MinRuntimeVersion = %s, want 1.4.0", got) + } +} diff --git a/deploy/operator/internal/features/runtime/gates.go b/deploy/operator/internal/features/runtime/gates.go new file mode 100644 index 000000000000..3b7f3d09851f --- /dev/null +++ b/deploy/operator/internal/features/runtime/gates.go @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package runtime + +import "github.com/ai-dynamo/dynamo/deploy/operator/internal/runtimeversion" + +var ( + // CanaryHealthChecks gates the canary health-check rendering defaults + // introduced for Dynamo runtime 1.4.0. + CanaryHealthChecks = Gate{ + Name: "CanaryHealthChecks", + MinRuntimeVersion: runtimeversion.Version{Major: 1, Minor: 4, Patch: 0}, + } +) diff --git a/deploy/operator/internal/runtimeversion/runtimeversion.go b/deploy/operator/internal/runtimeversion/runtimeversion.go index a31ea7175de0..9716d0b23f21 100644 --- a/deploy/operator/internal/runtimeversion/runtimeversion.go +++ b/deploy/operator/internal/runtimeversion/runtimeversion.go @@ -6,6 +6,7 @@ package runtimeversion import ( + "cmp" "fmt" "regexp" "strings" @@ -26,6 +27,17 @@ func (v Version) String() string { return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) } +// Compare returns -1, 0, or 1 when v's normalized compatibility core is less +// than, equal to, or greater than other. Image-tag prerelease and build +// suffixes are intentionally excluded by ParseImageVersion before comparison. +func (v Version) Compare(other Version) int { + return cmp.Or( + cmp.Compare(v.Major, other.Major), + cmp.Compare(v.Minor, other.Minor), + cmp.Compare(v.Patch, other.Patch), + ) +} + // Parse returns the compatibility version represented by an explicit override. func Parse(value string) (Version, error) { version, err := semver.StrictNewVersion(value) diff --git a/deploy/operator/internal/runtimeversion/runtimeversion_test.go b/deploy/operator/internal/runtimeversion/runtimeversion_test.go index 5c2a72040b7f..60aeda93468e 100644 --- a/deploy/operator/internal/runtimeversion/runtimeversion_test.go +++ b/deploy/operator/internal/runtimeversion/runtimeversion_test.go @@ -7,6 +7,35 @@ package runtimeversion import "testing" +func TestVersionCompare(t *testing.T) { + t.Log("define the reference runtime compatibility core") + reference := Version{Major: 1, Minor: 4, Patch: 1} + + t.Log("define comparisons below, equal to, and above the reference") + tests := []struct { + name string + version Version + want int + }{ + {name: "below major", version: Version{Major: 0, Minor: 9, Patch: 9}, want: -1}, + {name: "below minor", version: Version{Major: 1, Minor: 3, Patch: 9}, want: -1}, + {name: "below patch", version: Version{Major: 1, Minor: 4, Patch: 0}, want: -1}, + {name: "equal", version: Version{Major: 1, Minor: 4, Patch: 1}, want: 0}, + {name: "above patch", version: Version{Major: 1, Minor: 4, Patch: 2}, want: 1}, + {name: "above minor", version: Version{Major: 1, Minor: 5, Patch: 0}, want: 1}, + {name: "above major", version: Version{Major: 2, Minor: 0, Patch: 0}, want: 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Log("compare the normalized runtime compatibility cores") + if got := tt.version.Compare(reference); got != tt.want { + t.Fatalf("%s.Compare(%s) = %d, want %d", tt.version, reference, got, tt.want) + } + }) + } +} + func TestParse(t *testing.T) { tests := []struct { name string