Skip to content

Commit 9b20edb

Browse files
committed
Make module metadata validators module-scoped and add Blueprint.YamlCtx to detect explicit module settings
1 parent 91c2044 commit 9b20edb

12 files changed

Lines changed: 49 additions & 178 deletions

File tree

community/modules/compute/gke-nodeset/metadata.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ spec:
1818
services:
1919
- container.googleapis.com
2020
- storage.googleapis.com
21+
ghpc:
22+
validators:
23+
- validator: regex
24+
inputs:
25+
vars: [slurm_cluster_name]
26+
pattern: "^[a-z](?:[a-z0-9]{0,9})$"
27+
error_message: "'slurm_cluster_name' must contain only lowercase letters and numbers, start with a letter, and be 1-10 characters long."

community/modules/compute/notebook/metadata.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,3 @@ spec:
1818
services:
1919
- notebooks.googleapis.com
2020
- storage.googleapis.com
21-
ghpc:
22-
validators:
23-
- validator: regex
24-
inputs:
25-
vars: [deployment_name]
26-
pattern: "^[a-z](?:[a-z0-9]{0,6})$"
27-
scope: module
28-
error_message: "'deployment_name' must contain only lowercase letters and numbers, start with a letter, and be 1-7 characters long."

community/modules/compute/schedmd-slurm-gcp-v6-nodeset/metadata.yaml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,3 @@ spec:
1919
ghpc:
2020
inject_module_id: name
2121
has_to_be_used: true
22-
validators:
23-
- validator: regex
24-
inputs:
25-
vars: [slurm_cluster_name]
26-
scope: blueprint
27-
pattern: "^[a-z](?:[a-z0-9]{0,9})$"
28-
error_message: "'slurm_cluster_name' must contain only lowercase letters and numbers, start with a letter, and be 1-10 characters long."

community/modules/compute/schedmd-slurm-gcp-v6-partition/metadata.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,4 @@ ghpc:
2323
inputs:
2424
vars: [partition_name]
2525
pattern: "^[a-z](?:[a-z0-9]*)$"
26-
scope: blueprint
2726
error_message: "'partition_name' must start with a lowercase letter and can only contain lowercase letters and numbers."

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@ spec:
1919
- compute.googleapis.com
2020
- iam.googleapis.com
2121
- storage.googleapis.com
22+
ghpc:
23+
validators:
24+
- validator: regex
25+
inputs:
26+
vars: [slurm_cluster_name]
27+
pattern: "^[a-z](?:[a-z0-9]{0,9})$"
28+
error_message: "'slurm_cluster_name' must contain only lowercase letters and numbers, start with a letter, and be 1-10 characters long."

modules/compute/gke-node-pool/metadata.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,4 @@ ghpc:
2424
inputs:
2525
vars: [name]
2626
pattern: "^[a-z]([-a-z0-9]{0,34}[a-z0-9])?$"
27-
scope: module
2827
error_message: "'name' must start with a lowercase letter, followed by up to 34 lowercase letters, numbers, or hyphens, and cannot end with a hyphen."

modules/compute/resource-policy/metadata.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,4 @@ ghpc:
2323
inputs:
2424
vars: [name]
2525
pattern: "^[a-z]([-a-z0-9]{0,52}[a-z0-9])?$"
26-
scope: module
2726
error_message: "'name' must start with a lowercase letter, followed by up to 52 lowercase letters, numbers, or hyphens, and cannot end with a hyphen."

pkg/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ type Blueprint struct {
291291
path string
292292
// records of intentions to stage file (populated by ghpc_stage function)
293293
stagedFiles map[string]string
294+
// YamlCtx holds parsed YAML positions so validators can tell if a module setting
295+
// was explicitly present in the user's source (runtime-only, not serialized).
296+
YamlCtx *YamlCtx `yaml:"-"`
294297
}
295298

296299
func (bp *Blueprint) Clone() Blueprint {
@@ -467,6 +470,9 @@ func NewBlueprint(path string) (Blueprint, *YamlCtx, error) {
467470
return Blueprint{}, &ctx, err
468471
}
469472
bp.path = absPath
473+
// Attach parsed YAML context to the Blueprint so validators can determine
474+
// whether a module.setting path was explicitly present in the user's YAML.
475+
bp.YamlCtx = &ctx
470476
return bp, &ctx, nil
471477
}
472478

pkg/config/config_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ func (s *zeroSuite) TestNewBlueprint(c *C) {
355355
c.Assert(err, IsNil)
356356

357357
bp.path = outFile // set expected path
358+
// NewBlueprint populates a runtime-only YamlCtx (positions in source YAML).
359+
// Reflect that in the expected blueprint before doing a DeepEquals compare.
360+
bp.YamlCtx = newBp.YamlCtx
358361
c.Assert(bp, DeepEquals, newBp)
359362
}
360363

pkg/validators/metadata_validator_helpers.go

Lines changed: 18 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -62,67 +62,32 @@ func evaluateAndFlatten(val cty.Value) []cty.Value {
6262
return values
6363
}
6464

65-
// getBlueprintValues retrieves a cty.Value from blueprint variables by name,
66-
// evaluates it and returns the flattened values and the path to use for errors.
67-
func getBlueprintValues(bp config.Blueprint, varName string) ([]cty.Value, config.Path, error) {
68-
var nilPath config.Path
69-
70-
if !bp.Vars.Has(varName) {
71-
return nil, nilPath, fmt.Errorf("variable %q not found in blueprint vars", varName)
72-
}
73-
val := bp.Vars.Get(varName)
74-
if evaledVal, err := bp.Eval(val); err == nil {
75-
val = evaledVal
76-
}
77-
values := evaluateAndFlatten(val)
78-
return values, config.Root.Vars.Dot(varName), nil
79-
}
80-
8165
// getModuleSettingValues retrieves a cty.Value from module settings using a dot-separated path,
8266
// evaluates expressions via bp.Eval and returns flattened slice + path for errors.
8367
func getModuleSettingValues(bp config.Blueprint, group config.Group, modIdx int, mod config.Module, settingName string) ([]cty.Value, config.Path, error) {
8468
var nilPath config.Path
8569

86-
val, found := getNestedValue(mod.Settings, settingName)
87-
if !found {
88-
return nil, nilPath, fmt.Errorf("setting %q not found in module %q settings", settingName, mod.ID)
89-
}
90-
if evaledVal, err := bp.Eval(val); err == nil {
91-
val = evaledVal
92-
}
93-
values := evaluateAndFlatten(val)
94-
70+
// Determine canonical path for this module.setting in the blueprint.
9571
groupIndex := bp.GroupIndex(group.Name)
9672
path := config.Root.Groups.At(groupIndex).Modules.At(modIdx).Settings.Dot(settingName)
9773

98-
return values, path, nil
99-
}
74+
// If YAML context exists and the path is not present in the user's YAML,
75+
// treat the setting as absent so validators skip it (honoring optional:true).
10076

101-
// valuesEqualBlueprint returns true when the provided flattened module values are
102-
// equal to the flattened blueprint var with the same name. Comparison is only done
103-
// for string values; non-comparable types return false.
104-
func valuesEqualBlueprint(bp config.Blueprint, varName string, moduleValues []cty.Value) bool {
105-
if !bp.Vars.Has(varName) {
106-
return false
107-
}
108-
bpVal := bp.Vars.Get(varName)
109-
if evaledBp, err := bp.Eval(bpVal); err == nil {
110-
bpVal = evaledBp
77+
if bp.YamlCtx != nil {
78+
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)
80+
}
11181
}
112-
bpVals := evaluateAndFlatten(bpVal)
11382

114-
if len(bpVals) != len(moduleValues) {
115-
return false
116-
}
117-
for i := range bpVals {
118-
if bpVals[i].Type() != cty.String || moduleValues[i].Type() != cty.String {
119-
return false
120-
}
121-
if bpVals[i].AsString() != moduleValues[i].AsString() {
122-
return false
123-
}
83+
val, _ := getNestedValue(mod.Settings, settingName)
84+
85+
if evaledVal, err := bp.Eval(val); err == nil {
86+
val = evaledVal
12487
}
125-
return true
88+
values := evaluateAndFlatten(val)
89+
90+
return values, path, nil
12691
}
12792

12893
// parseStringList normalizes an input that may be a single string, []interface{} or nil into []string.
@@ -179,42 +144,6 @@ func processModuleSettings(bp config.Blueprint, mod config.Module, group config.
179144
return nil
180145
}
181146

182-
// processVarsAsBlueprint processes a list of names as blueprint vars.
183-
func processVarsAsBlueprint(bp config.Blueprint, list []string, optional bool, handler func(Target) error) error {
184-
for _, name := range list {
185-
values, path, err := getBlueprintValues(bp, name)
186-
if err != nil {
187-
if optional {
188-
continue
189-
}
190-
// legacy behavior: skip when missing blueprint var even if optional == false
191-
continue
192-
}
193-
if err := handler(Target{Name: name, Values: values, Path: path, IsBlueprint: true}); err != nil {
194-
return err
195-
}
196-
}
197-
return nil
198-
}
199-
200-
// processVarsPreferModuleSetting prefers module.setting for each name; skips if module.setting absent.
201-
func processVarsPreferModuleSetting(bp config.Blueprint, mod config.Module, group config.Group, modIdx int, list []string, handler func(Target) error) error {
202-
for _, vname := range list {
203-
if val, ok := getNestedValue(mod.Settings, vname); ok {
204-
if evaled, err := bp.Eval(val); err == nil {
205-
val = evaled
206-
}
207-
values := evaluateAndFlatten(val)
208-
path := config.Root.Groups.At(bp.GroupIndex(group.Name)).Modules.At(modIdx).Settings.Dot(vname)
209-
if err := handler(Target{Name: vname, Values: values, Path: path, IsBlueprint: false}); err != nil {
210-
return err
211-
}
212-
}
213-
// else: skip (do not fallback to blueprint var)
214-
}
215-
return nil
216-
}
217-
218147
// IterateRuleTargets resolves vars/settings from a validation rule according to scope and optional semantics,
219148
// and calls the provided handler for each resolved Target. The handler may return an error to stop iteration.
220149
func IterateRuleTargets(
@@ -226,32 +155,16 @@ func IterateRuleTargets(
226155
handler func(Target) error,
227156
) error {
228157

158+
// Only support `vars:` in module metadata validators (each treated as a module.setting name).
159+
// `settings:` support has been removed to enforce a single convention.
229160
varsList, _ := parseStringList(rule.Inputs["vars"])
230-
settingsList, _ := parseStringList(rule.Inputs["settings"])
231-
scope, _ := rule.Inputs["scope"].(string)
232161
optional := true
233162
if v, ok := rule.Inputs["optional"]; ok {
234163
if b, ok := v.(bool); ok {
235164
optional = b
236165
}
237166
}
238167

239-
switch scope {
240-
case "module":
241-
if len(settingsList) > 0 {
242-
return processModuleSettings(bp, mod, group, modIdx, settingsList, optional, handler)
243-
}
244-
return processModuleSettings(bp, mod, group, modIdx, varsList, optional, handler)
245-
246-
case "blueprint":
247-
return processVarsAsBlueprint(bp, varsList, optional, handler)
248-
249-
default:
250-
if len(settingsList) > 0 {
251-
if err := processModuleSettings(bp, mod, group, modIdx, settingsList, optional, handler); err != nil {
252-
return err
253-
}
254-
}
255-
return processVarsPreferModuleSetting(bp, mod, group, modIdx, varsList, handler)
256-
}
168+
// Interpret each var name as module.settings.<name>
169+
return processModuleSettings(bp, mod, group, modIdx, varsList, optional, handler)
257170
}

0 commit comments

Comments
 (0)