Skip to content

Commit 504a138

Browse files
Make controllers to watch for referenced resources. Limit indexers to specific Refs. No array based indexers are supported (#2866)
1 parent 8800931 commit 504a138

File tree

2 files changed

+195
-31
lines changed

2 files changed

+195
-31
lines changed

tools/scaffolder/internal/generate/controller.go

Lines changed: 168 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ func FromConfig(resultPath, crdKind, controllerOutDir, indexerOutDir, typesPath
5353
return fmt.Errorf("failed to generate indexers: %w", err)
5454
}
5555

56+
// Parse reference fields for watch generation
57+
referenceFields, err := ParseReferenceFields(resultPath, crdKind)
58+
if err != nil {
59+
return fmt.Errorf("failed to parse reference fields: %w", err)
60+
}
61+
62+
// Group references by target kind
63+
refsByKind := make(map[string][]ReferenceField)
64+
for _, ref := range referenceFields {
65+
refsByKind[ref.ReferencedKind] = append(refsByKind[ref.ReferencedKind], ref)
66+
}
67+
5668
baseControllerDir := filepath.Join(controllerOutDir, strings.ToLower(resourceName))
5769

5870
controllerName := resourceName
@@ -66,7 +78,7 @@ func FromConfig(resultPath, crdKind, controllerOutDir, indexerOutDir, typesPath
6678
return fmt.Errorf("failed to generate controller file: %w", err)
6779
}
6880

69-
if err := generateMainHandlerFile(controllerDir, controllerName, resourceName, typesPath, parsedConfig.Mappings); err != nil {
81+
if err := generateMainHandlerFile(controllerDir, controllerName, resourceName, typesPath, parsedConfig.Mappings, refsByKind); err != nil {
7082
return fmt.Errorf("failed to generate main handler file: %w", err)
7183
}
7284

@@ -193,7 +205,7 @@ func generateControllerFileWithMultipleVersions(dir, controllerName, resourceNam
193205
return f.Save(fileName)
194206
}
195207

196-
func generateMainHandlerFile(dir, controllerName, resourceName, typesPath string, mappings []MappingWithConfig) error {
208+
func generateMainHandlerFile(dir, controllerName, resourceName, typesPath string, mappings []MappingWithConfig, refsByKind map[string][]ReferenceField) error {
197209
atlasResourceName := strings.ToLower(resourceName)
198210
apiPkg := typesPath
199211

@@ -231,13 +243,13 @@ func generateMainHandlerFile(dir, controllerName, resourceName, typesPath string
231243
jen.Return(jen.Id("selectedHandler").Op(",").Nil()),
232244
)
233245

234-
generateDelegatingStateHandlers(f, controllerName, resourceName, apiPkg)
246+
generateDelegatingStateHandlers(f, controllerName, resourceName, apiPkg, refsByKind)
235247

236248
fileName := filepath.Join(dir, "handler.go")
237249
return f.Save(fileName)
238250
}
239251

240-
func generateDelegatingStateHandlers(f *jen.File, controllerName, resourceName, apiPkg string) {
252+
func generateDelegatingStateHandlers(f *jen.File, controllerName, resourceName, apiPkg string, refsByKind map[string][]ReferenceField) {
241253
handlers := []string{
242254
"HandleInitial",
243255
"HandleImportRequested",
@@ -280,19 +292,9 @@ func generateDelegatingStateHandlers(f *jen.File, controllerName, resourceName,
280292
jen.Return(jen.Id("obj"), jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "WithPredicates").Call()),
281293
)
282294

283-
f.Comment("SetupWithManager sets up the controller with the Manager")
284-
f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id("SetupWithManager").Params(
285-
jen.Id("mgr").Qual("sigs.k8s.io/controller-runtime", "Manager"),
286-
jen.Id("rec").Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Reconciler"),
287-
jen.Id("defaultOptions").Qual("sigs.k8s.io/controller-runtime/pkg/controller", "Options"),
288-
).Error().Block(
289-
jen.Id("h").Dot("Client").Op("=").Id("mgr").Dot("GetClient").Call(),
290-
jen.Return(jen.Qual("sigs.k8s.io/controller-runtime", "NewControllerManagedBy").Call(jen.Id("mgr")).
291-
Dot("Named").Call(jen.Lit(resourceName)).
292-
Dot("For").Call(jen.Id("h").Dot("For").Call()).
293-
Dot("WithOptions").Call(jen.Id("defaultOptions")).
294-
Dot("Complete").Call(jen.Id("rec"))),
295-
)
295+
generateMapperFunctions(f, controllerName, resourceName, apiPkg, refsByKind)
296+
297+
generateSetupWithManager(f, controllerName, resourceName, refsByKind)
296298
}
297299

298300
func generateVersionHandlerFile(dir, controllerName, resourceName, typesPath string, mapping MappingWithConfig) error {
@@ -393,3 +395,152 @@ func generateVersionInterfaceMethods(f *jen.File, controllerName, resourceName,
393395
jen.Return(jen.Nil()),
394396
)
395397
}
398+
399+
func generateSetupWithManager(f *jen.File, controllerName, resourceName string, refsByKind map[string][]ReferenceField) {
400+
f.Comment("SetupWithManager sets up the controller with the Manager")
401+
402+
setupChain := jen.Qual("sigs.k8s.io/controller-runtime", "NewControllerManagedBy").Call(jen.Id("mgr")).
403+
Dot("Named").Call(jen.Lit(resourceName)).
404+
Dot("For").Call(jen.Id("h").Dot("For").Call())
405+
406+
// Add Watches()
407+
for kind := range refsByKind {
408+
watchedTypeInstance := getWatchedTypeInstance(kind)
409+
mapperFuncName := fmt.Sprintf("%sFor%sMapFunc", strings.ToLower(resourceName), kind)
410+
411+
setupChain = setupChain.
412+
Dot("Watches").Call(
413+
watchedTypeInstance,
414+
jen.Qual("sigs.k8s.io/controller-runtime/pkg/handler", "EnqueueRequestsFromMapFunc").Call(
415+
jen.Id("h").Dot(mapperFuncName).Call(),
416+
),
417+
jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "WithPredicates").Call(
418+
jen.Qual("sigs.k8s.io/controller-runtime/pkg/predicate", "ResourceVersionChangedPredicate").Values(),
419+
),
420+
)
421+
}
422+
423+
setupChain = setupChain.
424+
Dot("WithOptions").Call(jen.Id("defaultOptions")).
425+
Dot("Complete").Call(jen.Id("rec"))
426+
427+
f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id("SetupWithManager").Params(
428+
jen.Id("mgr").Qual("sigs.k8s.io/controller-runtime", "Manager"),
429+
jen.Id("rec").Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Reconciler"),
430+
jen.Id("defaultOptions").Qual("sigs.k8s.io/controller-runtime/pkg/controller", "Options"),
431+
).Error().Block(
432+
jen.Id("h").Dot("Client").Op("=").Id("mgr").Dot("GetClient").Call(),
433+
jen.Return(setupChain),
434+
)
435+
}
436+
437+
func generateMapperFunctions(f *jen.File, controllerName, resourceName, apiPkg string, refsByKind map[string][]ReferenceField) {
438+
for kind, refs := range refsByKind {
439+
if len(refs) == 0 {
440+
continue
441+
}
442+
443+
indexerType := refs[0].IndexerType
444+
mapperFuncName := fmt.Sprintf("%sFor%sMapFunc", strings.ToLower(resourceName), kind)
445+
446+
switch indexerType {
447+
case "project":
448+
generateIndexerBasedMapperFunction(f, controllerName, resourceName, apiPkg, kind, mapperFuncName, "ProjectsIndexMapperFunc")
449+
case "credentials":
450+
generateIndexerBasedMapperFunction(f, controllerName, resourceName, apiPkg, kind, mapperFuncName, "CredentialsIndexMapperFunc")
451+
case "resource":
452+
generateResourceMapperFunction(f, controllerName, resourceName, apiPkg, kind, mapperFuncName, refs)
453+
}
454+
}
455+
}
456+
457+
// For Group or Secrets
458+
func generateIndexerBasedMapperFunction(f *jen.File, controllerName, resourceName, apiPkg, referencedKind, mapperFuncName, indexerHelperFunc string) {
459+
indexName := fmt.Sprintf("%sBy%sIndex", resourceName, referencedKind)
460+
listTypeName := fmt.Sprintf("%sList", resourceName)
461+
requestsFuncName := fmt.Sprintf("%sRequestsFrom%s", resourceName, referencedKind)
462+
463+
f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id(mapperFuncName).Params().Qual("sigs.k8s.io/controller-runtime/pkg/handler", "MapFunc").Block(
464+
jen.Return(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/indexer", indexerHelperFunc).Call(
465+
jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/indexer", indexName),
466+
jen.Func().Params().Op("*").Qual(apiPkg, listTypeName).Block(
467+
jen.Return(jen.Op("&").Qual(apiPkg, listTypeName).Values()),
468+
),
469+
jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/indexer", requestsFuncName),
470+
jen.Id("h").Dot("Client"),
471+
jen.Id("h").Dot("Log"),
472+
)),
473+
)
474+
}
475+
476+
func generateResourceMapperFunction(f *jen.File, controllerName, resourceName, apiPkg, referencedKind, mapperFuncName string, refs []ReferenceField) {
477+
indexName := fmt.Sprintf("%sBy%sIndex", resourceName, referencedKind)
478+
listTypeName := fmt.Sprintf("%sList", resourceName)
479+
watchedType := getWatchedType(referencedKind)
480+
481+
f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id(mapperFuncName).Params().Qual("sigs.k8s.io/controller-runtime/pkg/handler", "MapFunc").Block(
482+
jen.Return(jen.Func().Params(
483+
jen.Id("ctx").Qual("context", "Context"),
484+
jen.Id("obj").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Object"),
485+
).Index().Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Request").Block(
486+
487+
jen.List(jen.Id("refObj"), jen.Id("ok")).Op(":=").Id("obj").Assert(jen.Op("*").Add(watchedType)),
488+
jen.If(jen.Op("!").Id("ok")).Block(
489+
jen.Id("h").Dot("Log").Dot("Warnf").Call(
490+
jen.Lit(fmt.Sprintf("watching %s but got %%T", referencedKind)),
491+
jen.Id("obj"),
492+
),
493+
jen.Return(jen.Nil()),
494+
),
495+
496+
jen.Id("listOpts").Op(":=").Op("&").Qual("sigs.k8s.io/controller-runtime/pkg/client", "ListOptions").Values(jen.Dict{
497+
jen.Id("FieldSelector"): jen.Qual("k8s.io/apimachinery/pkg/fields", "OneTermEqualSelector").Call(
498+
jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/indexer", indexName),
499+
jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKeyFromObject").Call(jen.Id("refObj")).Dot("String").Call(),
500+
),
501+
}),
502+
jen.Id("list").Op(":=").Op("&").Qual(apiPkg, listTypeName).Values(),
503+
jen.Id("err").Op(":=").Id("h").Dot("Client").Dot("List").Call(jen.Id("ctx"), jen.Id("list"), jen.Id("listOpts")),
504+
jen.If(jen.Id("err").Op("!=").Nil()).Block(
505+
jen.Id("h").Dot("Log").Dot("Errorf").Call(
506+
jen.Lit(fmt.Sprintf("failed to list from indexer %s: %%v", indexName)),
507+
jen.Id("err"),
508+
),
509+
jen.Return(jen.Nil()),
510+
),
511+
512+
jen.Id("requests").Op(":=").Make(jen.Index().Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Request"), jen.Lit(0), jen.Len(jen.Id("list").Dot("Items"))),
513+
jen.For(jen.List(jen.Id("_"), jen.Id("item")).Op(":=").Range().Id("list").Dot("Items")).Block(
514+
jen.Id("requests").Op("=").Append(jen.Id("requests"), jen.Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Request").Values(jen.Dict{
515+
jen.Id("NamespacedName"): jen.Qual("k8s.io/apimachinery/pkg/types", "NamespacedName").Values(jen.Dict{
516+
jen.Id("Name"): jen.Id("item").Dot("Name"),
517+
jen.Id("Namespace"): jen.Id("item").Dot("Namespace"),
518+
}),
519+
})),
520+
),
521+
jen.Return(jen.Id("requests")),
522+
)),
523+
)
524+
}
525+
526+
func getWatchedTypeInstance(kind string) *jen.Statement {
527+
switch kind {
528+
case "Secret":
529+
return jen.Op("&").Qual("k8s.io/api/core/v1", "Secret").Values()
530+
case "Group":
531+
return jen.Op("&").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", "Group").Values()
532+
default:
533+
return jen.Op("&").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", kind).Values()
534+
}
535+
}
536+
537+
func getWatchedType(kind string) *jen.Statement {
538+
switch kind {
539+
case "Secret":
540+
return jen.Qual("k8s.io/api/core/v1", "Secret")
541+
case "Group":
542+
return jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", "Group")
543+
default:
544+
return jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", kind)
545+
}
546+
}

tools/scaffolder/internal/generate/indexers.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,14 @@ func GenerateIndexers(resultPath, crdKind, indexerOutDir string) error {
268268
}
269269

270270
// Group references by target kind (e.g., all Secret refs together, all Group refs together)
271+
// Skip array-based references for now as they require iteration logic
271272
refsByKind := make(map[string][]ReferenceField)
272273
for _, ref := range references {
274+
// Skip references that are arrays for now
275+
if strings.Contains(ref.FieldPath, ".items.") {
276+
fmt.Printf("Skipping array-based reference %s in %s (array indexing not yet supported)\n", ref.FieldName, crdKind)
277+
continue
278+
}
273279
refsByKind[ref.ReferencedKind] = append(refsByKind[ref.ReferencedKind], ref)
274280
}
275281

@@ -353,10 +359,8 @@ func generateIndexerFile(crdKind string, indexer IndexerInfo, indexerOutDir stri
353359
// Keys method with logic for all reference fields
354360
generateKeysMethod(f, structName, crdKind, indexer)
355361

356-
// For Secret references, add helper Requests function
357-
if indexer.TargetKind == "Secret" {
358-
generateRequestsFunction(f, crdKind)
359-
}
362+
// Always generate helper Requests function for all reference types
363+
generateRequestsFunction(f, crdKind, indexer.TargetKind)
360364

361365
if err := f.Save(filePath); err != nil {
362366
return fmt.Errorf("failed to save file %s: %w", filePath, err)
@@ -399,12 +403,12 @@ func generateFieldExtractionCode(fields []ReferenceField, targetKind string) []j
399403

400404
for _, field := range fields {
401405
// Build the field path from the FieldPath
402-
// FieldPath looks like: "properties.spec.properties.v20250312.properties.groupRef"
403-
// We need to convert this to: resource.Spec.V20250312.GroupRef
406+
// FieldPath looks like: "properties.spec.properties.<version>.properties.groupRef"
407+
// We need to convert this to: resource.Spec.<version>.GroupRef
404408
fieldAccessPath := buildFieldAccessPath(field.FieldPath)
405409

406-
// Generate: if resource.Spec.V20250312.GroupRef != nil && resource.Spec.V20250312.GroupRef.Name != "" {
407-
// keys = append(keys, resource.Spec.V20250312.GroupRef.GetObject(resource.Namespace).String())
410+
// Generate: if resource.Spec.<version>.GroupRef != nil && resource.Spec.<version>.GroupRef.Name != "" {
411+
// keys = append(keys, types.NamespacedName{Name: resource.Spec.<version>.GroupRef.Name, Namespace: resource.Namespace}.String())
408412
// }
409413
code = append(code,
410414
jen.If(
@@ -416,7 +420,10 @@ func generateFieldExtractionCode(fields []ReferenceField, targetKind string) []j
416420
).Block(
417421
jen.Id("keys").Op("=").Append(
418422
jen.Id("keys"),
419-
jen.Id(fieldAccessPath).Dot("GetObject").Call(jen.Id("resource").Dot("Namespace")).Dot("String").Call(),
423+
jen.Qual("k8s.io/apimachinery/pkg/types", "NamespacedName").Values(jen.Dict{
424+
jen.Id("Name"): jen.Id(fieldAccessPath).Dot("Name"),
425+
jen.Id("Namespace"): jen.Id("resource").Dot("Namespace"),
426+
}).Dot("String").Call(),
420427
),
421428
),
422429
)
@@ -432,8 +439,8 @@ func buildFieldAccessPath(fieldPath string) string {
432439
for i := 0; i < len(parts); i++ {
433440
part := parts[i]
434441

435-
// Skip "properties" keywords
436-
if part == "properties" {
442+
// Skip "properties" and "items" keywords. Array based indexers are not supported for now
443+
if part == "properties" || part == "items" {
437444
continue
438445
}
439446

@@ -451,15 +458,21 @@ func capitalizeFirst(s string) string {
451458
return strings.ToUpper(s[:1]) + s[1:]
452459
}
453460

454-
func generateRequestsFunction(f *jen.File, crdKind string) {
461+
func generateRequestsFunction(f *jen.File, crdKind string, targetKind string) {
455462
listTypeName := fmt.Sprintf("%sList", crdKind)
456-
requestsFuncName := fmt.Sprintf("%sRequests", crdKind)
463+
// Make function name unique per targetKind to avoid duplicates when multiple indexers exist for same CRD
464+
requestsFuncName := fmt.Sprintf("%sRequestsFrom%s", crdKind, targetKind)
457465
f.Func().Id(requestsFuncName).Params(
458466
jen.Id("list").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", listTypeName),
459467
).Index().Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Request").Block(
460468
jen.Id("requests").Op(":=").Make(jen.Index().Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Request"), jen.Lit(0), jen.Len(jen.Id("list").Dot("Items"))),
461469
jen.For(jen.List(jen.Id("_"), jen.Id("item")).Op(":=").Range().Id("list").Dot("Items")).Block(
462-
jen.Id("requests").Op("=").Append(jen.Id("requests"), jen.Id("toRequest").Call(jen.Op("&").Id("item"))),
470+
jen.Id("requests").Op("=").Append(jen.Id("requests"), jen.Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Request").Values(jen.Dict{
471+
jen.Id("NamespacedName"): jen.Qual("k8s.io/apimachinery/pkg/types", "NamespacedName").Values(jen.Dict{
472+
jen.Id("Name"): jen.Id("item").Dot("Name"),
473+
jen.Id("Namespace"): jen.Id("item").Dot("Namespace"),
474+
}),
475+
})),
463476
),
464477
jen.Return(jen.Id("requests")),
465478
)

0 commit comments

Comments
 (0)