Skip to content

Commit a0c7e1e

Browse files
committed
Update range validation logic
1 parent 662b462 commit a0c7e1e

3 files changed

Lines changed: 97 additions & 73 deletions

File tree

community/modules/scheduler/schedmd-slurm-gcp-v6-controller/metadata.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ghpc:
2525
inputs:
2626
vars: [static_ips]
2727
max: 1
28+
length_check: true
2829
error_message: "The Slurm modules supports 0 or 1 static IPs on controller instance."
2930
- validator: regex
3031
inputs:

pkg/validators/metadata_validator_helpers.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025 "Google LLC"
1+
// Copyright 2025, 2026 "Google LLC"
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@ func evaluateAndFlatten(val cty.Value) []cty.Value {
6464

6565
// getModuleSettingValues retrieves a cty.Value from module settings using a dot-separated path,
6666
// evaluates expressions via bp.Eval and returns flattened slice + path for errors.
67-
func getModuleSettingValues(bp config.Blueprint, group config.Group, modIdx int, mod config.Module, settingName string) ([]cty.Value, config.Path, error) {
67+
func getModuleSettingValues(bp config.Blueprint, group config.Group, modIdx int, mod config.Module, settingName string) (cty.Value, config.Path, error) {
6868
var nilPath config.Path
6969

7070
// Determine canonical path for this module.setting in the blueprint.
@@ -76,7 +76,7 @@ func getModuleSettingValues(bp config.Blueprint, group config.Group, modIdx int,
7676

7777
if bp.YamlCtx != nil {
7878
if _, ok := bp.YamlCtx.Pos(path); !ok {
79-
return nil, nilPath, fmt.Errorf("setting %q not present in blueprint YAML for module %q", settingName, mod.ID)
79+
return cty.NilVal, nilPath, fmt.Errorf("setting %q not present in blueprint YAML for module %q", settingName, mod.ID)
8080
}
8181
}
8282

@@ -85,9 +85,7 @@ func getModuleSettingValues(bp config.Blueprint, group config.Group, modIdx int,
8585
if evaledVal, err := bp.Eval(val); err == nil {
8686
val = evaledVal
8787
}
88-
values := evaluateAndFlatten(val)
89-
90-
return values, path, nil
88+
return val, path, nil
9189
}
9290

9391
// parseStringList normalizes an input that may be a single string, []interface{} or nil into []string.
@@ -118,33 +116,35 @@ func parseStringList(v interface{}) ([]string, bool) {
118116
// Target represents a resolved target (module-setting or blueprint var) for validation.
119117
type Target struct {
120118
Name string
121-
Values []cty.Value
119+
Value cty.Value
122120
Path config.Path
123121
IsBlueprint bool // true if came from blueprint vars, false if module.settings
124122
}
125123

126-
// parseIntInput retrieves an integer value from a map by key.
127-
// If the key is missing, it returns the defaultVal.
128-
func parseIntInput(inputs map[string]interface{}, key string, defaultVal int) (int, error) {
124+
// parseIntInputRefactor parses an integer from the input map and returns a pointer.
125+
// It returns nil if the key is not found.
126+
func parseIntInput(inputs map[string]interface{}, key string) (*int, error) {
129127
v, ok := inputs[key]
130128
if !ok {
131-
return defaultVal, nil
129+
return nil, nil
132130
}
133131

132+
var val int
134133
switch t := v.(type) {
135134
case int:
136-
return t, nil
135+
val = t
137136
case float64:
138-
return int(t), nil
137+
val = int(t)
139138
default:
140-
return 0, fmt.Errorf("'%s' must be an integer, not %T", key, v)
139+
return nil, fmt.Errorf("'%s' must be an integer, not %T", key, v)
141140
}
141+
return &val, nil
142142
}
143143

144144
// processModuleSettings processes a list of names interpreted as module settings.
145145
func processModuleSettings(bp config.Blueprint, mod config.Module, group config.Group, modIdx int, list []string, optional bool, handler func(Target) error) error {
146146
for _, s := range list {
147-
values, path, err := getModuleSettingValues(bp, group, modIdx, mod, s)
147+
value, path, err := getModuleSettingValues(bp, group, modIdx, mod, s)
148148
if err != nil {
149149
if optional {
150150
continue
@@ -155,7 +155,7 @@ func processModuleSettings(bp config.Blueprint, mod config.Module, group config.
155155
Path: missingPath,
156156
}
157157
}
158-
if err := handler(Target{Name: s, Values: values, Path: path, IsBlueprint: false}); err != nil {
158+
if err := handler(Target{Name: s, Value: value, Path: path, IsBlueprint: false}); err != nil {
159159
return err
160160
}
161161
}

pkg/validators/metadata_validators.go

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025 Google LLC
1+
// Copyright 2025, 2026 Google LLC
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -13,7 +13,6 @@ package validators
1313

1414
import (
1515
"fmt"
16-
"math"
1716
"regexp"
1817
"strings"
1918

@@ -55,7 +54,8 @@ func (r *RegexValidator) Validate(
5554
}
5655

5756
// helper: validate flattened cty.Values against regex, returning first error
58-
validateValues := func(values []cty.Value, path config.Path) error {
57+
validateValues := func(value cty.Value, path config.Path) error {
58+
values := evaluateAndFlatten(value)
5959
for _, val := range values {
6060
if val.Type() != cty.String {
6161
continue
@@ -73,7 +73,7 @@ func (r *RegexValidator) Validate(
7373

7474
// iterate targets using shared logic
7575
err = IterateRuleTargets(bp, mod, rule, group, modIdx, func(t Target) error {
76-
return validateValues(t.Values, t.Path)
76+
return validateValues(t.Value, t.Path)
7777
})
7878
return err
7979
}
@@ -100,7 +100,8 @@ func (v *AllowedEnumValidator) normalizeAllowed(allowedRaw interface{}) ([]strin
100100
}
101101

102102
// checkValues iterates through cty.Values to ensure they exist within the allowed set, handling nulls and casing.
103-
func (v *AllowedEnumValidator) checkValues(values []cty.Value, path config.Path, allowedSet map[string]struct{}, allowedList []string, caseSensitive bool, allowNull bool, errMsg string) error {
103+
func (v *AllowedEnumValidator) checkValues(value cty.Value, path config.Path, allowedSet map[string]struct{}, allowedList []string, caseSensitive bool, allowNull bool, errMsg string) error {
104+
values := evaluateAndFlatten(value)
104105
for _, val := range values {
105106
if val.IsNull() {
106107
if allowNull {
@@ -190,15 +191,72 @@ func (v *AllowedEnumValidator) Validate(
190191

191192
// 4. Iterate and validate user-provided values
192193
return IterateRuleTargets(bp, mod, rule, group, modIdx, func(t Target) error {
193-
return v.checkValues(t.Values, t.Path, allowedSet, allowedList, caseSensitive, allowNull, rule.ErrorMessage)
194+
return v.checkValues(t.Value, t.Path, allowedSet, allowedList, caseSensitive, allowNull, rule.ErrorMessage)
194195
})
195196
}
196197

197198
// RangeValidator implements the Validator interface for 'range' type.
198199
type RangeValidator struct{}
199200

201+
// checkBounds validates a single integer value against the provided min and max bounds.
202+
func (r *RangeValidator) checkBounds(value int, min *int, max *int, customErrMsg string, path config.Path) error {
203+
if min != nil && value < *min {
204+
msg := customErrMsg
205+
if msg == "" {
206+
msg = fmt.Sprintf("value %d is less than the minimum allowed value of %d", value, *min)
207+
}
208+
return config.BpError{Err: fmt.Errorf("%s", msg), Path: path}
209+
}
210+
if max != nil && value > *max {
211+
msg := customErrMsg
212+
if msg == "" {
213+
msg = fmt.Sprintf("value %d is greater than the maximum allowed value of %d", value, *max)
214+
}
215+
return config.BpError{Err: fmt.Errorf("%s", msg), Path: path}
216+
}
217+
return nil
218+
}
219+
220+
// validateTarget applies range validation to a given cty.Value.
221+
func (r *RangeValidator) validateTarget(
222+
value cty.Value,
223+
path config.Path,
224+
min *int,
225+
max *int,
226+
lengthCheck bool,
227+
customErrMsg string) error {
228+
229+
if lengthCheck {
230+
if !value.Type().IsListType() && !value.Type().IsTupleType() {
231+
return config.BpError{
232+
Err: fmt.Errorf("length_check can only be used with list types, not %s", value.Type().FriendlyName()),
233+
Path: path,
234+
}
235+
}
236+
return r.checkBounds(value.LengthInt(), min, max, customErrMsg, path)
237+
}
238+
239+
values := evaluateAndFlatten(value)
240+
for _, val := range values {
241+
if val.IsNull() || !val.IsKnown() {
242+
continue
243+
}
244+
if val.Type() == cty.Number {
245+
f, _ := val.AsBigFloat().Float64()
246+
if err := r.checkBounds(int(f), min, max, customErrMsg, path); err != nil {
247+
return err
248+
}
249+
} else {
250+
return config.BpError{
251+
Err: fmt.Errorf("range validator only supports numbers, not %s", val.Type().FriendlyName()),
252+
Path: path,
253+
}
254+
}
255+
}
256+
return nil
257+
}
258+
200259
// Validate checks if the variables specified in the rule match the provided range pattern.
201-
// This function focuses on the predicate and uses IterateRuleTargets from targets.go to resolve targets.
202260
func (r *RangeValidator) Validate(
203261
bp config.Blueprint,
204262
mod config.Module,
@@ -207,65 +265,30 @@ func (r *RangeValidator) Validate(
207265
modIdx int) error {
208266

209267
modPath := config.Root.Groups.At(bp.GroupIndex(group.Name)).Modules.At(modIdx).Source
210-
// Extract min
211268

212-
min, err := parseIntInput(rule.Inputs, "min", math.MinInt)
269+
min, err := parseIntInput(rule.Inputs, "min")
213270
if err != nil {
214-
return config.BpError{
215-
Err: fmt.Errorf(
216-
"validation rule for module %q: %v", mod.ID, err),
217-
Path: modPath,
218-
}
271+
return config.BpError{Err: fmt.Errorf("validation rule for module %q: %v", mod.ID, err), Path: modPath}
219272
}
220-
// Extract max
221-
max, err := parseIntInput(rule.Inputs, "max", math.MaxInt)
273+
274+
max, err := parseIntInput(rule.Inputs, "max")
222275
if err != nil {
276+
return config.BpError{Err: fmt.Errorf("validation rule for module %q: %v", mod.ID, err), Path: modPath}
277+
}
278+
279+
if min == nil && max == nil {
223280
return config.BpError{
224-
Err: fmt.Errorf(
225-
"validation rule for module %q: %v", mod.ID, err),
281+
Err: fmt.Errorf("range validator for module %q must have at least one of 'min' or 'max' defined", mod.ID),
226282
Path: modPath,
227283
}
228284
}
229285

230-
// helper: validate flattened cty.Values against range, returning first error
231-
validateValues := func(values []cty.Value, path config.Path) error {
232-
for _, val := range values {
233-
if val.IsNull() || !val.IsKnown() {
234-
continue
235-
}
236-
var checkValue int
237-
var isList bool
238-
if val.Type().IsListType() {
239-
checkValue = val.LengthInt()
240-
isList = true
241-
} else if val.Type() == cty.Number {
242-
f, _ := val.AsBigFloat().Float64()
243-
checkValue = int(f)
244-
} else {
245-
return config.BpError{
246-
Err: fmt.Errorf("range validator only supports numbers or lists, not %s", val.Type().FriendlyName()),
247-
Path: path,
248-
}
249-
}
250-
effectiveMin := min
251-
if isList {
252-
effectiveMin = 0 // Default min is 0 for lists if not specified
253-
}
254-
255-
if checkValue < effectiveMin || checkValue > max {
256-
msg := rule.ErrorMessage
257-
if msg == "" {
258-
msg = fmt.Sprintf("value %d is out of range [%d, %d]", checkValue, effectiveMin, max)
259-
}
260-
return config.BpError{Err: fmt.Errorf("error: %s", msg), Path: path}
261-
}
262-
}
263-
return nil
286+
checkListLength, err := parseBoolInput(rule.Inputs, "length_check", false)
287+
if err != nil {
288+
return config.BpError{Err: fmt.Errorf("validation rule for module %q: %v", mod.ID, err), Path: modPath}
264289
}
265290

266-
// iterate targets using shared logic
267-
err = IterateRuleTargets(bp, mod, rule, group, modIdx, func(t Target) error {
268-
return validateValues(t.Values, t.Path)
291+
return IterateRuleTargets(bp, mod, rule, group, modIdx, func(t Target) error {
292+
return r.validateTarget(t.Value, t.Path, min, max, checkListLength, rule.ErrorMessage)
269293
})
270-
return err
271-
}
294+
}

0 commit comments

Comments
 (0)