Skip to content

Commit d35b4a8

Browse files
s-urbaniakigor-karpukhin
authored andcommitted
add tracking required fields
1 parent 707ef71 commit d35b4a8

File tree

2 files changed

+166
-10
lines changed

2 files changed

+166
-10
lines changed

tools/scaffolder/internal/generate/indexers.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ type ReferenceField struct {
3939
ReferencedKind string
4040
ReferencedGroup string
4141
ReferencedVersion string
42+
// RequiredSegments tracks whether each meaningful path segment is required.
43+
// The slice aligns with the meaningful parts of FieldPath (excluding "properties", "items").
44+
// For example, for FieldPath "properties.spec.properties.groupRef",
45+
// RequiredSegments[0] indicates if "spec" is required, RequiredSegments[1] if "groupRef" is required.
46+
RequiredSegments []bool
4247
}
4348

4449
type IndexerInfo struct {
@@ -179,12 +184,13 @@ func parseAPIMapping(apiMappingsStr string, schema *apiextensionsv1.JSONSchemaPr
179184
var references []ReferenceField
180185

181186
// Recursively search for x-kubernetes-mapping
182-
findReferences(mapping, "", schema, &references)
187+
// Start with empty requiredSegments slice
188+
findReferences(mapping, "", schema, nil, &references)
183189

184190
return references, nil
185191
}
186192

187-
func processKubernetesMapping(v map[string]any, path string, references *[]ReferenceField) {
193+
func processKubernetesMapping(v map[string]any, path string, requiredSegments []bool, references *[]ReferenceField) {
188194
kubeMapping, exists := v["x-kubernetes-mapping"]
189195
if !exists {
190196
return
@@ -217,22 +223,27 @@ func processKubernetesMapping(v map[string]any, path string, references *[]Refer
217223
}
218224

219225
if kind != "" && fieldName != "" {
226+
// Make a copy of requiredSegments to avoid sharing the underlying array
227+
reqCopy := make([]bool, len(requiredSegments))
228+
copy(reqCopy, requiredSegments)
229+
220230
ref := ReferenceField{
221231
FieldName: fieldName,
222232
FieldPath: path,
223233
ReferencedKind: kind,
224234
ReferencedGroup: group,
225235
ReferencedVersion: version,
236+
RequiredSegments: reqCopy,
226237
}
227238
*references = append(*references, ref)
228239
}
229240
}
230241

231-
func findReferences(data any, path string, schema *apiextensionsv1.JSONSchemaProps, references *[]ReferenceField) {
242+
func findReferences(data any, path string, schema *apiextensionsv1.JSONSchemaProps, requiredSegments []bool, references *[]ReferenceField) {
232243
switch v := data.(type) {
233244
case map[string]any:
234245
// Check if this is a kubernetes mapping and process it
235-
processKubernetesMapping(v, path, references)
246+
processKubernetesMapping(v, path, requiredSegments, references)
236247

237248
for key, value := range v {
238249
newPath := path
@@ -243,8 +254,18 @@ func findReferences(data any, path string, schema *apiextensionsv1.JSONSchemaPro
243254

244255
// Resolve the child schema for this key and pass it into the recursive call
245256
required, childSchema := getSchemaForPathSegment(schema, key)
246-
_ = required
247-
findReferences(value, newPath, childSchema, references)
257+
258+
// Track required status for meaningful segments (not "properties" or "items")
259+
newRequiredSegments := requiredSegments
260+
if key != "properties" && key != "items" {
261+
// Special case: the top-level "spec" property is never required per Kubernetes convention
262+
if key == "spec" && len(requiredSegments) == 0 {
263+
required = false
264+
}
265+
newRequiredSegments = append(requiredSegments, required)
266+
}
267+
268+
findReferences(value, newPath, childSchema, newRequiredSegments, references)
248269
}
249270
case []any:
250271
for i, item := range v {
@@ -253,11 +274,9 @@ func findReferences(data any, path string, schema *apiextensionsv1.JSONSchemaPro
253274
// For arrays pass the items schema if available
254275
var childSchema *apiextensionsv1.JSONSchemaProps
255276
if schema != nil {
256-
var required bool
257-
required, childSchema = getSchemaForPathSegment(schema, "items")
258-
_ = required
277+
_, childSchema = getSchemaForPathSegment(schema, "items")
259278
}
260-
findReferences(item, newPath, childSchema, references)
279+
findReferences(item, newPath, childSchema, requiredSegments, references)
261280
}
262281
}
263282
}
@@ -285,6 +304,10 @@ func getSchemaForPathSegment(schema *apiextensionsv1.JSONSchemaProps, key string
285304
}
286305

287306
if key == "items" {
307+
// Return the items schema for arrays
308+
if schema.Items != nil && schema.Items.Schema != nil {
309+
return false, schema.Items.Schema
310+
}
288311
return false, nil
289312
}
290313

tools/scaffolder/internal/generate/indexers_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,139 @@ spec:
141141
assert.Contains(t, refs[0].FieldPath, ".items.")
142142
}
143143

144+
func TestParseReferenceFields_RequiredSegments(t *testing.T) {
145+
testYAML := `
146+
apiVersion: apiextensions.k8s.io/v1
147+
kind: CustomResourceDefinition
148+
metadata:
149+
name: clusters.atlas.generated.mongodb.com
150+
annotations:
151+
api-mappings: |
152+
properties:
153+
spec:
154+
properties:
155+
v20250312:
156+
properties:
157+
groupRef:
158+
x-kubernetes-mapping:
159+
type:
160+
kind: Group
161+
group: atlas.generated.mongodb.com
162+
version: v1
163+
spec:
164+
group: atlas.generated.mongodb.com
165+
names:
166+
kind: Cluster
167+
versions:
168+
- name: v1
169+
schema:
170+
openAPIV3Schema:
171+
type: object
172+
required:
173+
- spec
174+
properties:
175+
spec:
176+
type: object
177+
required:
178+
- v20250312
179+
properties:
180+
v20250312:
181+
type: object
182+
required:
183+
- groupRef
184+
properties:
185+
groupRef:
186+
type: object
187+
properties:
188+
name:
189+
type: string
190+
`
191+
192+
tmpDir := t.TempDir()
193+
testFile := filepath.Join(tmpDir, "test.yaml")
194+
err := os.WriteFile(testFile, []byte(testYAML), 0644)
195+
require.NoError(t, err)
196+
197+
refs, err := ParseReferenceFields(testFile, "Cluster")
198+
require.NoError(t, err)
199+
require.Len(t, refs, 1)
200+
201+
ref := refs[0]
202+
assert.Equal(t, "groupRef", ref.FieldName)
203+
assert.Equal(t, "Group", ref.ReferencedKind)
204+
assert.Equal(t, "properties.spec.properties.v20250312.properties.groupRef", ref.FieldPath)
205+
// spec is never required (Kubernetes convention), v20250312 is required in spec, groupRef is required in v20250312
206+
assert.Equal(t, []bool{false, true, true}, ref.RequiredSegments)
207+
}
208+
209+
func TestParseReferenceFields_MixedRequiredSegments(t *testing.T) {
210+
testYAML := `
211+
apiVersion: apiextensions.k8s.io/v1
212+
kind: CustomResourceDefinition
213+
metadata:
214+
name: clusters.atlas.generated.mongodb.com
215+
annotations:
216+
api-mappings: |
217+
properties:
218+
spec:
219+
properties:
220+
v20250312:
221+
properties:
222+
optionalSection:
223+
properties:
224+
groupRef:
225+
x-kubernetes-mapping:
226+
type:
227+
kind: Group
228+
group: atlas.generated.mongodb.com
229+
version: v1
230+
spec:
231+
group: atlas.generated.mongodb.com
232+
names:
233+
kind: Cluster
234+
versions:
235+
- name: v1
236+
schema:
237+
openAPIV3Schema:
238+
type: object
239+
required:
240+
- spec
241+
properties:
242+
spec:
243+
type: object
244+
required:
245+
- v20250312
246+
properties:
247+
v20250312:
248+
type: object
249+
properties:
250+
optionalSection:
251+
type: object
252+
required:
253+
- groupRef
254+
properties:
255+
groupRef:
256+
type: object
257+
properties:
258+
name:
259+
type: string
260+
`
261+
262+
tmpDir := t.TempDir()
263+
testFile := filepath.Join(tmpDir, "test.yaml")
264+
err := os.WriteFile(testFile, []byte(testYAML), 0644)
265+
require.NoError(t, err)
266+
267+
refs, err := ParseReferenceFields(testFile, "Cluster")
268+
require.NoError(t, err)
269+
require.Len(t, refs, 1)
270+
271+
ref := refs[0]
272+
assert.Equal(t, "groupRef", ref.FieldName)
273+
// spec is never required (Kubernetes convention), v20250312 is required, optionalSection is NOT required, groupRef IS required
274+
assert.Equal(t, []bool{false, true, false, true}, ref.RequiredSegments)
275+
}
276+
144277
func TestBuildFieldAccessPath(t *testing.T) {
145278
tests := []struct {
146279
name string

0 commit comments

Comments
 (0)