Skip to content

Commit 8176802

Browse files
authored
Auto-MNNVL: validate annotation values and sync design doc (#386)
Update the design document to match the implementation details. Add strict validation for grove.io/auto-mnnvl annotation values Changes: - Update design doc with correct config paths and annotation values - Reject invalid annotation values (must be "enabled" or "disabled")
1 parent 185ab67 commit 8176802

File tree

5 files changed

+145
-47
lines changed

5 files changed

+145
-47
lines changed

docs/designs/mnnvl-design.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -94,25 +94,26 @@ This document covers **Phase 1** of MNNVL support in Grove. See GREP-270 for the
9494
Enabling and disabling the feature will be done by the cluster admin by setting a flag in the Grove OperatorConfiguration.
9595

9696
```go
97-
// MNNVLConfiguration defines the configuration for MNNVL (Multi-Node NVLink) support.
98-
type MNNVLConfiguration struct {
99-
// Enabled indicates whether MNNVL support is enabled.
100-
// When true, the operator validates that the ComputeDomain CRD is installed at startup.
97+
// NetworkAcceleration defines the configuration for network acceleration features.
98+
type NetworkAcceleration struct {
99+
// AutoMNNVLEnabled indicates whether automatic MNNVL (Multi-Node NVLink) support is enabled.
100+
// When true, the operator validates that the ComputeDomain CRD is installed at startup
101+
// and automatically creates ComputeDomain resources for GPU workloads.
101102
// When MNNVL support is enabled, cluster admin should ensure that the ComputeDomain CRD has been installed.
102103
// If this prerequisite fails then Grove will exit with a non-zero exit code.
103104
// Default: false
104-
Enabled bool `json:"enabled"`
105+
AutoMNNVLEnabled bool `json:"autoMNNVLEnabled"`
105106
}
106107
```
107108

108-
The default value of `Enabled` is `false`, meaning MNNVL support is disabled unless explicitly enabled by the cluster administrator.
109+
The default value of `AutoMNNVLEnabled` is `false`, meaning MNNVL support is disabled unless explicitly enabled by the cluster administrator.
109110

110111
The value could be set from a Helm chart under the config attribute
111112

112113
```yaml
113114
config:
114-
mnnvl:
115-
enabled: false
115+
network:
116+
autoMNNVLEnabled: false
116117
```
117118
118119
> **Note:** Using the `OperatorConfiguration` for feature enablement is chosen for simplicity in Phase 1. However, a plugin-based approach would provide better decoupling between the MNNVL feature and Grove core, and should be considered for future phases.
@@ -129,7 +130,7 @@ When a PodCliqueSet is created, webhooks determine and enforce the MNNVL enablem
129130

130131
#### Mutating Webhook (on Create)
131132

132-
The mutating webhook adds the `grove.io/auto-mnnvl: "true"` annotation **only** when all conditions are met:
133+
The mutating webhook adds the `grove.io/auto-mnnvl: "enabled"` annotation **only** when all conditions are met:
133134
134135
1. Annotation does not already exist
135136
2. MNNVL feature is enabled in `OperatorConfiguration`
@@ -162,7 +163,7 @@ kind: PodCliqueSet
162163
metadata:
163164
name: my-pcs
164165
annotations:
165-
grove.io/auto-mnnvl: "true" # Added by webhook
166+
grove.io/auto-mnnvl: "enabled" # Added by webhook
166167
spec:
167168
# ... same spec
168169
```
@@ -171,25 +172,26 @@ spec:
171172

172173
A validating webhook runs **on PCS creation only** to reject invalid MNNVL configurations:
173174

174-
- **Reject** if `grove.io/auto-mnnvl: "true"` is set but MNNVL feature is **disabled** globally
175+
- **Reject** if annotation value is not `"enabled"` or `"disabled"` (e.g., `"true"`, `"false"`, empty string)
176+
- **Reject** if `grove.io/auto-mnnvl: "enabled"` is set but MNNVL feature is **disabled** globally
175177

176-
This prevents users from explicitly requesting MNNVL when the cluster doesn't support it.
178+
This prevents users from explicitly requesting MNNVL when the cluster doesn't support it, and ensures only valid annotation values are accepted.
177179

178180
#### Validating Webhook (on Update)
179181

180182
A validating webhook ensures the `grove.io/auto-mnnvl` annotation is **immutable** after PCS creation. Any attempt to add, modify, or remove the annotation on an existing PCS is rejected.
181183

182184
#### Opt-out Behavior
183185

184-
Users can opt-out of MNNVL for a specific PCS by explicitly setting `grove.io/auto-mnnvl: "false"` **before creation**. When the mutating webhook sees the annotation already exists, it does not override it.
186+
Users can opt-out of MNNVL for a specific PCS by explicitly setting `grove.io/auto-mnnvl: "disabled"` **before creation**. When the mutating webhook sees the annotation already exists, it does not override it.
185187
186188
```yaml
187189
apiVersion: grove.io/v1alpha1
188190
kind: PodCliqueSet
189191
metadata:
190192
name: my-pcs
191193
annotations:
192-
grove.io/auto-mnnvl: "false" # Explicit opt-out
194+
grove.io/auto-mnnvl: "disabled" # Explicit opt-out
193195
spec:
194196
# ... GPU workload that won't use MNNVL
195197
```
@@ -204,8 +206,8 @@ The PCS controller has a reconciliation flow for managing resources in a specifi
204206

205207
Before creating the `CD`, the controller checks the `grove.io/auto-mnnvl` annotation on the PCS:
206208

207-
- If `grove.io/auto-mnnvl: "true"` → Create ComputeDomains for each replica
208-
- If `grove.io/auto-mnnvl: "false"` or annotation is absent → Skip ComputeDomain creation
209+
- If `grove.io/auto-mnnvl: "enabled"` → Create ComputeDomains for each replica
210+
- If `grove.io/auto-mnnvl: "disabled"` or annotation is absent → Skip ComputeDomain creation
209211

210212
Since the annotation is set by the mutating webhook at PCS creation time (based on feature enablement and GPU requirements), the controller logic is simplified to a single annotation check.
211213

@@ -253,7 +255,7 @@ ComputeDomain creation follows the same observability pattern as other Grove-man
253255

254256
Deleting a ComputeDomain while pods are actively using its RCT causes significant workload disruption. To prevent accidental deletion, Grove adds a **finalizer** to each ComputeDomain it creates.
255257

256-
**Finalizer:** `grove.io/protect-computedomain`
258+
**Finalizer:** `grove.io/computedomain-finalizer`
257259

258260
With this finalizer, a ComputeDomain cannot be deleted until Grove explicitly removes the finalizer. This provides stronger protection than a watch-and-recreate approach, which would leave a gap where the workload is in a broken state.
259261

@@ -263,7 +265,7 @@ kind: ComputeDomain
263265
metadata:
264266
name: my-pcs-0
265267
finalizers:
266-
- grove.io/protect-computedomain
268+
- grove.io/computedomain-finalizer
267269
labels:
268270
app.kubernetes.io/managed-by: grove
269271
app.kubernetes.io/part-of: my-pcs
@@ -283,6 +285,8 @@ spec:
283285
284286
If a user attempts to delete a CD manually, it will remain in `Terminating` state until the PCS is deleted or scaled down.
285287

288+
> **Note:** The finalizer name `grove.io/computedomain-finalizer` follows the pattern `grove.io/{resource}-finalizer` for clarity and consistency.
289+
286290
### Scale-Out and Scale-In
287291

288292
When scaling out (replicas increased), the subsequent reconciliation process will identify the ComputeDomains missing for the new replica indices and create them using the identical logic as the initial creation.
@@ -293,7 +297,7 @@ The controller lists existing `ComputeDomains` by label selector, computes expec
293297

294298
### PCS Deletion
295299

296-
When a PodCliqueSet is deleted, the PCS controller's finalizer logic removes the `grove.io/protect-computedomain` finalizer from all owned ComputeDomains. Once the finalizer is removed, Kubernetes garbage-collects the CDs through the owner reference mechanism.
300+
When a PodCliqueSet is deleted, the PCS controller's finalizer logic removes the `grove.io/computedomain-finalizer` finalizer from all owned ComputeDomains. Once the finalizer is removed, Kubernetes garbage-collects the CDs through the owner reference mechanism.
297301

298302
## PCLQ Creation and RCT Injection
299303

@@ -302,7 +306,7 @@ The `resourceClaims` reference is injected into the PCLQ's pod spec template at
302306
### PCS Creating PCLQ
303307

304308
When the PCS controller creates a PCLQ, it checks:
305-
1. Does the PCS have `grove.io/auto-mnnvl: "true"`?
309+
1. Does the PCS have `grove.io/auto-mnnvl: "enabled"`?
306310
2. Does the PCLQ's pod spec require GPU (`nvidia.com/gpu`)?
307311

308312
If both conditions are true, the controller injects `resourceClaims` into the PCLQ's pod spec template before creation:
@@ -340,7 +344,7 @@ kind: PodCliqueScalingGroup
340344
metadata:
341345
name: my-pcs-0-scaling
342346
annotations:
343-
grove.io/auto-mnnvl: "true" # Propagated from PCS
347+
grove.io/auto-mnnvl: "enabled" # Propagated from PCS
344348
labels:
345349
app.kubernetes.io/part-of: my-pcs
346350
grove.io/podcliqueset-replica-index: "0"
@@ -351,7 +355,7 @@ spec:
351355
### PCSG Creating PCLQ
352356

353357
When the PCSG controller creates a PCLQ, it uses the **same injection logic** as the PCS controller:
354-
1. Check if PCSG has `grove.io/auto-mnnvl: "true"` annotation
358+
1. Check if PCSG has `grove.io/auto-mnnvl: "enabled"` annotation
355359
2. Check if the PCLQ's pod spec requires GPU
356360
357361
If both are true, inject `resourceClaims` into the PCLQ's pod spec template.

operator/internal/mnnvl/constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const (
5959
// FinalizerComputeDomain is the finalizer added to ComputeDomains to prevent accidental
6060
// deletion while workloads are using them. This finalizer is removed by the PCS controller
6161
// during scale-in or PCS deletion.
62-
FinalizerComputeDomain = "grove.io/" + ComputeDomainCRDName
62+
FinalizerComputeDomain = "grove.io/computedomain-finalizer"
6363

6464
// MNNVLClaimName is the name used for the MNNVL resource claim in pod specs.
6565
MNNVLClaimName = "mnnvl-claim"

operator/internal/mnnvl/webhook.go

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,69 +64,118 @@ func MutateAutoMNNVL(logger logr.Logger, pcs *grovecorev1alpha1.PodCliqueSet, au
6464
"value", AnnotationAutoMNNVLEnabled)
6565
}
6666

67-
// ValidateAutoMNNVLOnCreate validates the MNNVL annotation on PCS creation.
68-
// Returns field errors if the annotation is set to "enabled" but the MNNVL feature is disabled.
69-
// This prevents users from explicitly requesting MNNVL when the cluster doesn't support it.
70-
func ValidateAutoMNNVLOnCreate(pcs *grovecorev1alpha1.PodCliqueSet, autoMNNVLEnabled bool) field.ErrorList {
67+
// ValidateMetadataOnCreate validates metadata-level concerns on PCS creation.
68+
// This is the entry point for metadata validation during create operations.
69+
func ValidateMetadataOnCreate(pcs *grovecorev1alpha1.PodCliqueSet, autoMNNVLEnabled bool) field.ErrorList {
70+
return validateAutoMNNVLAnnotationOnCreate(pcs, autoMNNVLEnabled)
71+
}
72+
73+
// validateAutoMNNVLAnnotationOnCreate validates the grove.io/auto-mnnvl annotation on creation.
74+
// Returns field errors if the annotation value is invalid or if MNNVL is requested but not enabled.
75+
func validateAutoMNNVLAnnotationOnCreate(pcs *grovecorev1alpha1.PodCliqueSet, autoMNNVLEnabled bool) field.ErrorList {
7176
value, exists := pcs.Annotations[AnnotationAutoMNNVL]
7277
if !exists {
7378
return nil
7479
}
7580

76-
// If annotation is "enabled" but feature is disabled, reject
81+
annotationPath := field.NewPath("metadata", "annotations", AnnotationAutoMNNVL)
82+
83+
allErrs := field.ErrorList{}
84+
allErrs = append(allErrs, validateAutoMNNVLAnnotationValue(value, annotationPath)...)
85+
allErrs = append(allErrs, validateMNNVLFeatureEnabled(value, autoMNNVLEnabled, annotationPath)...)
86+
87+
return allErrs
88+
}
89+
90+
// validateAutoMNNVLAnnotationValue validates the annotation has an allowed value.
91+
func validateAutoMNNVLAnnotationValue(value string, path *field.Path) field.ErrorList {
92+
if value != AnnotationAutoMNNVLEnabled && value != AnnotationAutoMNNVLDisabled {
93+
return field.ErrorList{
94+
field.Invalid(
95+
path,
96+
value,
97+
fmt.Sprintf("must be %q or %q", AnnotationAutoMNNVLEnabled, AnnotationAutoMNNVLDisabled),
98+
),
99+
}
100+
}
101+
return nil
102+
}
103+
104+
// validateMNNVLFeatureEnabled validates MNNVL is enabled when annotation requests it.
105+
// This prevents users from explicitly requesting MNNVL when the cluster doesn't support it.
106+
func validateMNNVLFeatureEnabled(value string, autoMNNVLEnabled bool, path *field.Path) field.ErrorList {
77107
if value == AnnotationAutoMNNVLEnabled && !autoMNNVLEnabled {
78108
return field.ErrorList{
79109
field.Invalid(
80-
field.NewPath("metadata", "annotations", AnnotationAutoMNNVL),
110+
path,
81111
value,
82112
fmt.Sprintf("MNNVL is not enabled in the operator configuration. "+
83113
"Either enable MNNVL globally or remove the %s annotation", AnnotationAutoMNNVL),
84114
),
85115
}
86116
}
87-
88117
return nil
89118
}
90119

91-
// ValidateAutoMNNVLOnUpdate ensures the grove.io/auto-mnnvl annotation is immutable.
120+
// ValidateMetadataOnUpdate validates metadata-level concerns on PCS update.
121+
// This is the entry point for metadata validation during update operations.
122+
func ValidateMetadataOnUpdate(oldPCS, newPCS *grovecorev1alpha1.PodCliqueSet) field.ErrorList {
123+
return validateAutoMNNVLAnnotationOnUpdate(oldPCS, newPCS)
124+
}
125+
126+
// validateAutoMNNVLAnnotationOnUpdate ensures the grove.io/auto-mnnvl annotation is immutable.
92127
// Returns field errors if the annotation was added, removed, or its value was changed.
93-
func ValidateAutoMNNVLOnUpdate(oldPCS, newPCS *grovecorev1alpha1.PodCliqueSet) field.ErrorList {
128+
func validateAutoMNNVLAnnotationOnUpdate(oldPCS, newPCS *grovecorev1alpha1.PodCliqueSet) field.ErrorList {
94129
oldValue, oldExists := getAnnotationValue(oldPCS, AnnotationAutoMNNVL)
95130
newValue, newExists := getAnnotationValue(newPCS, AnnotationAutoMNNVL)
96131

97132
annotationPath := field.NewPath("metadata", "annotations", AnnotationAutoMNNVL)
98133

99-
// Check if annotation was added
134+
allErrs := field.ErrorList{}
135+
allErrs = append(allErrs, validateAnnotationNotAdded(oldExists, newExists, annotationPath)...)
136+
allErrs = append(allErrs, validateAnnotationNotRemoved(oldExists, newExists, annotationPath)...)
137+
allErrs = append(allErrs, validateAnnotationNotChanged(oldValue, newValue, oldExists, newExists, annotationPath)...)
138+
139+
return allErrs
140+
}
141+
142+
// validateAnnotationNotAdded validates that the annotation was not added after creation.
143+
func validateAnnotationNotAdded(oldExists, newExists bool, path *field.Path) field.ErrorList {
100144
if !oldExists && newExists {
101145
return field.ErrorList{
102146
field.Forbidden(
103-
annotationPath,
147+
path,
104148
fmt.Sprintf("annotation %s cannot be added after PodCliqueSet creation", AnnotationAutoMNNVL),
105149
),
106150
}
107151
}
152+
return nil
153+
}
108154

109-
// Check if annotation was removed
155+
// validateAnnotationNotRemoved validates that the annotation was not removed after creation.
156+
func validateAnnotationNotRemoved(oldExists, newExists bool, path *field.Path) field.ErrorList {
110157
if oldExists && !newExists {
111158
return field.ErrorList{
112159
field.Forbidden(
113-
annotationPath,
160+
path,
114161
fmt.Sprintf("annotation %s cannot be removed after PodCliqueSet creation", AnnotationAutoMNNVL),
115162
),
116163
}
117164
}
165+
return nil
166+
}
118167

119-
// Check if annotation value was changed
120-
if newExists && oldValue != newValue {
168+
// validateAnnotationNotChanged validates that the annotation value was not changed.
169+
func validateAnnotationNotChanged(oldValue, newValue string, oldExists, newExists bool, path *field.Path) field.ErrorList {
170+
if newExists && oldExists && oldValue != newValue {
121171
return field.ErrorList{
122172
field.Invalid(
123-
annotationPath,
173+
path,
124174
newValue,
125175
fmt.Sprintf("annotation %s is immutable and cannot be changed from %q to %q",
126176
AnnotationAutoMNNVL, oldValue, newValue),
127177
),
128178
}
129179
}
130-
131180
return nil
132181
}

0 commit comments

Comments
 (0)