diff --git a/tools/scaffolder/internal/generate/controller.go b/tools/scaffolder/internal/generate/controller.go index a6996301a9..3c0bb429fb 100644 --- a/tools/scaffolder/internal/generate/controller.go +++ b/tools/scaffolder/internal/generate/controller.go @@ -27,10 +27,6 @@ const ( pkgCtrlState = "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/state" ) -func getAPIPackage(apiVersion string) string { - return fmt.Sprintf("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/%s", apiVersion) -} - // FromConfig generates controllers and handlers based on the parsed CRD result file func FromConfig(resultPath, crdKind, controllerOutDir, indexerOutDir, typesPath string, override bool) error { parsedConfig, err := ParseCRDConfig(resultPath, crdKind) @@ -239,356 +235,6 @@ func generateControllerFileWithMultipleVersions(dir, controllerName, resourceNam return f.Save(fileName) } -func generatePackageLevelTranslationHelper(f *jen.File) { - f.Comment("getTranslationRequest creates a translation request for converting entities between API and AKO.") - f.Comment("This is a package-level function that can be called from any handler.") - f.Func().Id("getTranslationRequest").Params( - jen.Id("ctx").Qual("context", "Context"), - jen.Id("kubeClient").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Client"), - jen.Id("crdName").String(), - jen.Id("storageVersion").String(), - jen.Id("targetVersion").String(), - ).Params( - jen.Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request"), - jen.Error(), - ).Block( - jen.Id("crd").Op(":=").Op("&").Qual("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1", "CustomResourceDefinition").Values(), - jen.Id("err").Op(":=").Id("kubeClient").Dot("Get").Call( - jen.Id("ctx"), - jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKey").Values(jen.Dict{ - jen.Id("Name"): jen.Id("crdName"), - }), - jen.Id("crd"), - ), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( - jen.Lit("failed to resolve CRD %s: %w"), - jen.Id("crdName"), - jen.Id("err"), - )), - ), - jen.Line(), - jen.List(jen.Id("translator"), jen.Id("err")).Op(":=").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "NewTranslator").Call( - jen.Id("crd"), - jen.Id("storageVersion"), - jen.Id("targetVersion"), - ), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( - jen.Lit("failed to setup translator: %w"), - jen.Id("err"), - )), - ), - jen.Line(), - jen.Return(jen.Op("&").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request").Values(jen.Dict{ - jen.Id("Translator"): jen.Id("translator"), - jen.Id("Dependencies"): jen.Nil(), - }), jen.Nil()), - ) -} - -func generateMainHandlerFile(dir, resourceName, typesPath string, mappings []MappingWithConfig, refsByKind map[string][]ReferenceField, config *ParsedConfig) error { - atlasResourceName := strings.ToLower(resourceName) - apiPkg := typesPath - - f := jen.NewFile(atlasResourceName) - AddLicenseHeader(f) - - f.ImportAlias(pkgCtrlState, "ctrlstate") - f.ImportAlias("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1", "apiextensionsv1") - f.ImportAlias(apiPkg, "akov2generated") - - f.Comment("getHandlerForResource selects the appropriate version-specific handler based on which resource spec version is set") - f.Func().Params(jen.Id("h").Op("*").Id("Handler")).Id("getHandlerForResource").Params( - jen.Id("ctx").Qual("context", "Context"), - jen.Id(strings.ToLower(resourceName)).Op("*").Qual(apiPkg, resourceName), - ).Params(jen.Qual(pkgCtrlState, "StateHandler").Types(jen.Qual(apiPkg, resourceName)), jen.Error()).Block( - jen.List(jen.Id("atlasClients"), jen.Id("err")).Op(":=").Id("h").Dot("getSDKClientSet").Call( - jen.Id("ctx"), - jen.Id("group"), - ), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Nil(), jen.Id("err")), - ), - - jen.Comment("Check which resource spec version is set and validate that only one is specified"), - jen.Var().Id("versionCount").Int(), - jen.Var().Id("selectedHandler").Qual(pkgCtrlState, "StateHandler").Types(jen.Qual(apiPkg, resourceName)), - jen.Line(), - jen.CustomFunc(jen.Options{Multi: true}, func(g *jen.Group) { - for _, mapping := range mappings { - versionSuffix := mapping.Version - // Capitalize first letter of version (e.g., v20250312 -> V20250312) - capitalizedVersion := strings.ToUpper(string(versionSuffix[0])) + versionSuffix[1:] - // Construct CRD name: {plural}.{group} - crdName := config.PluralName + "." + config.CRDGroup - sdkImportPathSplit := strings.Split(mapping.OpenAPIConfig.Package, "/") - sdkVersionSuffix := strings.TrimPrefix(sdkImportPathSplit[len(sdkImportPathSplit)-2], "v") - - g.If(jen.Id(strings.ToLower(resourceName)).Dot("Spec").Dot(capitalizedVersion).Op("!=").Nil()).Block( - - jen.List(jen.Id("translationReq"), jen.Id("err")).Op(":=").Id("getTranslationRequest").Call( - jen.Id("ctx"), - jen.Id("h").Dot("Client"), - jen.Lit(crdName), - jen.Lit(config.StorageVersion), - jen.Lit(versionSuffix), - ), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Nil(), jen.Id("err")), - ), - jen.Id("versionCount").Op("++"), - jen.Id("selectedHandler"). - Op("=").Id("h"). - Dot("handler"+versionSuffix). - Call( - jen.Id("h"). - Dot("Client"), - jen.Id("atlasClients").Dot("SdkClient"+sdkVersionSuffix), - jen.Id("translationReq"), - ), - ) - } - }), - jen.Line(), - jen.If(jen.Id("versionCount").Op("==").Lit(0)).Block( - jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("no resource spec version specified - please set one of the available spec versions"))), - ), - jen.If(jen.Id("versionCount").Op(">").Lit(1)).Block( - jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("multiple resource spec versions specified - please set only one spec version"))), - ), - jen.Return(jen.Id("selectedHandler").Op(",").Nil()), - ) - - generateDelegatingStateHandlers(f, resourceName, apiPkg, refsByKind) - // ClientSet and translation request helpers - generateSDKClientSetMethod(f, resourceName, apiPkg) - - // Generate package-level helper function attached to the handler - generatePackageLevelTranslationHelper(f) - - fileName := filepath.Join(dir, "handler.go") - return f.Save(fileName) -} - -func generateDelegatingStateHandlers(f *jen.File, resourceName, apiPkg string, refsByKind map[string][]ReferenceField) { - handlers := []string{ - "HandleInitial", - "HandleImportRequested", - "HandleImported", - "HandleCreating", - "HandleCreated", - "HandleUpdating", - "HandleUpdated", - "HandleDeletionRequested", - "HandleDeleting", - } - startStateMap := map[string]string{ - "HandleInitial": "StateInitial", - "HandleImportRequested": "StateImportRequested", - "HandleImported": "StateImported", - "HandleCreating": "StateCreating", - "HandleCreated": "StateCreated", - "HandleUpdating": "StateUpdating", - "HandleUpdated": "StateUpdated", - "HandleDeletionRequested": "StateDeletionRequested", - "HandleDeleting": "StateDeleting", - } - - for _, handlerName := range handlers { - f.Comment(fmt.Sprintf("%s delegates to the version-specific handler", handlerName)) - f.Func().Params(jen.Id("h").Op("*").Id("Handler")).Id(handlerName).Params( - jen.Id("ctx").Qual("context", "Context"), - jen.Id(strings.ToLower(resourceName)).Op("*").Qual(apiPkg, resourceName), - ).Params( - jen.Qual(pkgCtrlState, "Result"), - jen.Error(), - ).Block( - jen.List(jen.Id("handler"), jen.Id("err")). - Op(":="). - Id("h"). - Dot("getHandlerForResource"). - Call(jen.Id("ctx"), jen.Id(strings.ToLower(resourceName))), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/result", "Error").Call( - jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state", startStateMap[handlerName]), - jen.Id("err"), - )), - ), - jen.Return(jen.Id("handler").Dot(handlerName).Call(jen.Id("ctx"), jen.Id(strings.ToLower(resourceName)))), - ) - } - - f.Comment("For returns the resource and predicates for the controller") - f.Func().Params(jen.Id("h").Op("*").Id("Handler")).Id("For").Params().Params( - jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "Object"), - jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "Predicates"), - ).Block( - jen.Id("obj").Op(":=").Op("&").Qual(apiPkg, resourceName).Values(), - jen.Return( - jen.Id("obj"), - jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "WithPredicates").Call(jen.Id("h").Dot("predicates").Op("...")), - ), - ) - - generateMapperFunctions(f, resourceName, apiPkg, refsByKind) - - generateSetupWithManager(f, resourceName, refsByKind) -} - -func generateSDKClientSetMethod(f *jen.File, resourceName, apiPkg string) { - resourceLower := strings.ToLower(resourceName) - - f.Comment("getSDKClientSet creates an Atlas SDK client set using credentials from the resource's connection secret") - f.Func().Params( - jen.Id("h").Op("*").Id("Handler"), - ).Id("getSDKClientSet").Params( - jen.Id("ctx").Qual("context", "Context"), - jen.Id(resourceLower).Op("*").Qual(apiPkg, resourceName), - ).Params( - jen.Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlas", "ClientSet"), - jen.Error(), - ).Block( - jen.List(jen.Id("connectionConfig"), jen.Id("err")).Op(":=").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/reconciler", "GetConnectionConfig").Call( - jen.Id("ctx"), - jen.Id("h").Dot("Client"), - jen.Op("&").Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKey").Values(jen.Dict{ - jen.Id("Namespace"): jen.Id(resourceLower).Dot("Namespace"), - jen.Id("Name"): jen.Id(resourceLower).Dot("Spec").Dot("ConnectionSecretRef").Dot("Name"), - }), - jen.Op("&").Id("h").Dot("GlobalSecretRef"), - ), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( - jen.Lit("failed to resolve Atlas credentials: %w"), - jen.Id("err"), - )), - ), - jen.Line(), - jen.List(jen.Id("clientSet"), jen.Id("err")).Op(":=").Id("h").Dot("AtlasProvider").Dot("SdkClientSet").Call( - jen.Id("ctx"), - jen.Id("connectionConfig").Dot("Credentials"), - jen.Id("h").Dot("Log"), - ), - jen.If(jen.Id("err").Op("!=").Nil()).Block( - jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( - jen.Lit("failed to setup Atlas SDK client: %w"), - jen.Id("err"), - )), - ), - jen.Line(), - jen.Return(jen.Id("clientSet"), jen.Nil()), - ) -} - -func generateVersionHandlerFile(dir, resourceName, typesPath string, mapping MappingWithConfig, override bool) error { - atlasResourceName := strings.ToLower(resourceName) - versionSuffix := mapping.Version - apiPkg := typesPath - sdkImportPath := mapping.OpenAPIConfig.Package - - fileName := filepath.Join(dir, "handler_"+versionSuffix+".go") - - // Check if a versioned handler file exists - if !override { - if _, err := os.Stat(fileName); err == nil { - fmt.Printf("Skipping versioned handler %s (already exists, use --override to overwrite)\n", fileName) - return nil - } - } - - f := jen.NewFile(atlasResourceName) - AddLicenseHeader(f) - - f.ImportAlias(pkgCtrlState, "ctrlstate") - f.ImportAlias(apiPkg, "akov2generated") - f.ImportAlias(sdkImportPath, versionSuffix+"sdk") - - f.Type().Id("Handler"+versionSuffix).Struct( - jen.Id("kubeClient").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Client"), - jen.Id("atlasClient").Op("*").Qual(sdkImportPath, "APIClient"), - jen.Id("translationRequest").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request"), - ) - - f.Func().Id("NewHandler"+versionSuffix).Params( - jen.Id("kubeClient").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Client"), - jen.Id("atlasClient").Op("*").Qual(sdkImportPath, "APIClient"), - jen.Id("translationRequest").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request"), - ).Op("*").Id("Handler" + versionSuffix).Block( - jen.Return(jen.Op("&").Id("Handler" + versionSuffix).Values(jen.Dict{ - jen.Id("kubeClient"): jen.Id("kubeClient"), - jen.Id("atlasClient"): jen.Id("atlasClient"), - jen.Id("translationRequest"): jen.Id("translationRequest"), - })), - ) - - generateVersionStateHandlers(f, resourceName, apiPkg, versionSuffix) - - // Generate For and SetupWithManager methods to satisfy StateHandler interface - generateVersionInterfaceMethods(f, resourceName, apiPkg, versionSuffix) - - return f.Save(fileName) -} - -func generateVersionStateHandlers(f *jen.File, resourceName, apiPkg, versionSuffix string) { - handlers := []struct { - name string - nextState string - message string - }{ - {"HandleInitial", "StateUpdated", "Updated Atlas" + resourceName + "."}, - {"HandleImportRequested", "StateImported", "Import completed"}, - {"HandleImported", "StateUpdated", "Ready"}, - {"HandleCreating", "StateCreated", "Resource created"}, - {"HandleCreated", "StateUpdated", "Ready"}, - {"HandleUpdating", "StateUpdated", "Update completed"}, - {"HandleUpdated", "StateUpdated", "Ready"}, - {"HandleDeletionRequested", "StateDeleting", "Deletion started"}, - {"HandleDeleting", "StateDeleted", "Deleted"}, - } - - for _, handler := range handlers { - f.Comment(fmt.Sprintf("%s handles the %s state for version %s", handler.name, strings.ToLower(strings.TrimPrefix(handler.name, "Handle")), versionSuffix)) - f.Func().Params(jen.Id("h").Op("*").Id("Handler"+versionSuffix)).Id(handler.name).Params( - jen.Id("ctx").Qual("context", "Context"), - jen.Id(strings.ToLower(resourceName)).Op("*").Qual(apiPkg, resourceName), - ).Params( - jen.Qual(pkgCtrlState, "Result"), - jen.Error(), - ).Block( - jen.Comment("TODO: Implement "+strings.ToLower(strings.TrimPrefix(handler.name, "Handle"))+" state logic"), - jen.Comment("TODO: Use h.atlasProvider.SdkClientSet(ctx, h.globalSecretRef, h.log) to get Atlas SDK client"), - jen.Return(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/result", "NextState").Call( - jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state", handler.nextState), - jen.Lit(handler.message), - )), - ) - } -} - -// generateVersionInterfaceMethods generates For and SetupWithManager methods for version-specific handlers -func generateVersionInterfaceMethods(f *jen.File, resourceName, apiPkg, versionSuffix string) { - // For method - f.Comment("For returns the resource and predicates for the controller") - f.Func().Params(jen.Id("h").Op("*").Id("Handler"+versionSuffix)).Id("For").Params().Params( - jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "Object"), - jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "Predicates"), - ).Block( - jen.Return(jen.Op("&").Qual(apiPkg, resourceName).Values(), jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "WithPredicates").Call()), - ) - - // SetupWithManager method - f.Comment("SetupWithManager sets up the controller with the Manager") - f.Func().Params(jen.Id("h").Op("*").Id("Handler"+versionSuffix)).Id("SetupWithManager").Params( - jen.Id("mgr").Qual("sigs.k8s.io/controller-runtime", "Manager"), - jen.Id("rec").Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Reconciler"), - jen.Id("defaultOptions").Qual("sigs.k8s.io/controller-runtime/pkg/controller", "Options"), - ).Error().Block( - jen.Comment("This method is not used for version-specific handlers but required by StateHandler interface"), - jen.Return(jen.Nil()), - ) -} - func generateSetupWithManager(f *jen.File, resourceName string, refsByKind map[string][]ReferenceField) { f.Comment("SetupWithManager sets up the controller with the Manager") @@ -642,7 +288,7 @@ func generateMapperFunctions(f *jen.File, resourceName, apiPkg string, refsByKin case "credentials": generateIndexerBasedMapperFunction(f, resourceName, apiPkg, kind, mapperFuncName, "CredentialsIndexMapperFunc") case "resource": - generateResourceMapperFunction(f, resourceName, apiPkg, kind, mapperFuncName, refs) + generateResourceMapperFunction(f, resourceName, apiPkg, kind, mapperFuncName) } } } @@ -666,7 +312,7 @@ func generateIndexerBasedMapperFunction(f *jen.File, resourceName, apiPkg, refer ) } -func generateResourceMapperFunction(f *jen.File, resourceName, apiPkg, referencedKind, mapperFuncName string, refs []ReferenceField) { +func generateResourceMapperFunction(f *jen.File, resourceName, apiPkg, referencedKind, mapperFuncName string) { indexName := fmt.Sprintf("%sBy%sIndex", resourceName, referencedKind) listTypeName := fmt.Sprintf("%sList", resourceName) watchedType := getWatchedType(referencedKind) diff --git a/tools/scaffolder/internal/generate/controller_test.go b/tools/scaffolder/internal/generate/controller_test.go index 7384ef8323..e20bb9a0c2 100644 --- a/tools/scaffolder/internal/generate/controller_test.go +++ b/tools/scaffolder/internal/generate/controller_test.go @@ -349,131 +349,3 @@ spec: assert.Contains(t, contentStr, "+kubebuilder:rbac:groups=atlas.generated.mongodb.com,resources=resources") assert.Contains(t, contentStr, "func NewResourceReconciler") } - -func TestGeneratedHandlerDelegation(t *testing.T) { - testYAML := ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: resources.atlas.generated.mongodb.com - annotations: - api-mappings: | - properties: - spec: - properties: - v20250312: - x-atlas-sdk-version: go.mongodb.org/atlas-sdk/v20250312008/admin - v20250401: - x-atlas-sdk-version: go.mongodb.org/atlas-sdk/v20250401001/admin -spec: - group: atlas.generated.mongodb.com - names: - kind: Resource - plural: resources - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - spec: - properties: - v20250312: - type: object - v20250401: - type: object -` - - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.yaml") - err := os.WriteFile(testFile, []byte(testYAML), 0644) - require.NoError(t, err) - - controllerDir := filepath.Join(tmpDir, "controllers") - indexerDir := filepath.Join(tmpDir, "indexers") - - err = FromConfig(testFile, "Resource", controllerDir, indexerDir, "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", true) - require.NoError(t, err) - - handlerFile := filepath.Join(controllerDir, "resource", "handler.go") - content, err := os.ReadFile(handlerFile) - require.NoError(t, err) - - contentStr := string(content) - - assert.Contains(t, contentStr, "func (h *Handler) getHandlerForResource") - assert.Contains(t, contentStr, "func (h *Handler) HandleInitial") - assert.Contains(t, contentStr, "func (h *Handler) HandleCreating") - assert.Contains(t, contentStr, "func (h *Handler) HandleDeletionRequested") - v1Handler := filepath.Join(controllerDir, "resource", "handler_v20250312.go") - assert.FileExists(t, v1Handler) - - v2Handler := filepath.Join(controllerDir, "resource", "handler_v20250401.go") - assert.FileExists(t, v2Handler) -} - -func TestGeneratedHelperFunctions(t *testing.T) { - testYAML := ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: groups.atlas.generated.mongodb.com - annotations: - api-mappings: | - properties: - spec: - properties: - v20250312: - x-atlas-sdk-version: go.mongodb.org/atlas-sdk/v20250312008/admin -spec: - group: atlas.generated.mongodb.com - names: - kind: Group - plural: groups - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - spec: - properties: - v20250312: - type: object -` - - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.yaml") - err := os.WriteFile(testFile, []byte(testYAML), 0644) - require.NoError(t, err) - - controllerDir := filepath.Join(tmpDir, "controllers") - indexerDir := filepath.Join(tmpDir, "indexers") - - err = FromConfig(testFile, "Group", controllerDir, indexerDir, "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", true) - require.NoError(t, err) - - // Test handler.go contains package-level getTranslationRequest function - handlerFile := filepath.Join(controllerDir, "group", "handler.go") - content, err := os.ReadFile(handlerFile) - require.NoError(t, err) - contentStr := string(content) - - // Verify package-level getTranslationRequest function - assert.Contains(t, contentStr, "func getTranslationRequest(") - assert.Contains(t, contentStr, "ctx context.Context") - assert.Contains(t, contentStr, "kubeClient client.Client") - assert.Contains(t, contentStr, "crdName string") - assert.Contains(t, contentStr, "storageVersion string") - assert.Contains(t, contentStr, "targetVersion string") - assert.Contains(t, contentStr, "NewTranslator") - - // Verify getSDKClientSet method - assert.Contains(t, contentStr, "func (h *Handler) getSDKClientSet(") - assert.Contains(t, contentStr, "GetConnectionConfig") - assert.Contains(t, contentStr, "SdkClientSet") - assert.Contains(t, contentStr, "ConnectionSecretRef") - - // Verify getTranslationRequest wrapper method - assert.Contains(t, contentStr, "func getTranslationRequest(") - assert.Contains(t, contentStr, "translate.NewTranslator(") - assert.Contains(t, contentStr, "return &translate.Request{") -} diff --git a/tools/scaffolder/internal/generate/handler.go b/tools/scaffolder/internal/generate/handler.go new file mode 100644 index 0000000000..ce8605d96b --- /dev/null +++ b/tools/scaffolder/internal/generate/handler.go @@ -0,0 +1,257 @@ +package generate + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/dave/jennifer/jen" +) + +func generateMainHandlerFile(dir, resourceName, typesPath string, mappings []MappingWithConfig, refsByKind map[string][]ReferenceField, config *ParsedConfig) error { + atlasResourceName := strings.ToLower(resourceName) + apiPkg := typesPath + + f := jen.NewFile(atlasResourceName) + AddLicenseHeader(f) + + f.ImportAlias(pkgCtrlState, "ctrlstate") + f.ImportAlias("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1", "apiextensionsv1") + f.ImportAlias(apiPkg, "akov2generated") + + f.Comment("getHandlerForResource selects the appropriate version-specific handler based on which resource spec version is set") + f.Func().Params(jen.Id("h").Op("*").Id("Handler")).Id("getHandlerForResource").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(atlasResourceName).Op("*").Qual(apiPkg, resourceName), + ).Params(jen.Qual(pkgCtrlState, "StateHandler").Types(jen.Qual(apiPkg, resourceName)), jen.Error()).Block( + jen.List(jen.Id("atlasClients"), jen.Id("err")).Op(":=").Id("h").Dot("getSDKClientSet").Call( + jen.Id("ctx"), + jen.Id(atlasResourceName), + ), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil(), jen.Id("err")), + ), + + jen.Comment("Check which resource spec version is set and validate that only one is specified"), + jen.Var().Id("versionCount").Int(), + jen.Var().Id("selectedHandler").Qual(pkgCtrlState, "StateHandler").Types(jen.Qual(apiPkg, resourceName)), + jen.Line(), + jen.CustomFunc(jen.Options{Multi: true}, func(g *jen.Group) { + for _, mapping := range mappings { + versionSuffix := mapping.Version + // Capitalize first letter of version (e.g., v20250312 -> V20250312) + capitalizedVersion := strings.ToUpper(string(versionSuffix[0])) + versionSuffix[1:] + // Construct CRD name: {plural}.{group} + crdName := config.PluralName + "." + config.CRDGroup + sdkImportPathSplit := strings.Split(mapping.OpenAPIConfig.Package, "/") + sdkVersionSuffix := strings.TrimPrefix(sdkImportPathSplit[len(sdkImportPathSplit)-2], "v") + + g.If(jen.Id(strings.ToLower(resourceName)).Dot("Spec").Dot(capitalizedVersion).Op("!=").Nil()).Block( + + jen.List(jen.Id("translationReq"), jen.Id("err")).Op(":=").Id("getTranslationRequest").Call( + jen.Id("ctx"), + jen.Id("h").Dot("Client"), + jen.Lit(crdName), + jen.Lit(config.StorageVersion), + jen.Lit(versionSuffix), + ), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil(), jen.Id("err")), + ), + jen.Id("versionCount").Op("++"), + jen.Id("selectedHandler"). + Op("=").Id("h"). + Dot("handler"+versionSuffix). + Call( + jen.Id("h"). + Dot("Client"), + jen.Id("atlasClients").Dot("SdkClient"+sdkVersionSuffix), + jen.Id("translationReq"), + ), + ) + } + }), + jen.Line(), + jen.If(jen.Id("versionCount").Op("==").Lit(0)).Block( + jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("no resource spec version specified - please set one of the available spec versions"))), + ), + jen.If(jen.Id("versionCount").Op(">").Lit(1)).Block( + jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("multiple resource spec versions specified - please set only one spec version"))), + ), + jen.Return(jen.Id("selectedHandler").Op(",").Nil()), + ) + + generateDelegatingStateHandlers(f, resourceName, apiPkg, refsByKind) + // ClientSet and translation request helpers + generateSDKClientSetMethod(f, resourceName, apiPkg) + + // Generate package-level helper function attached to the handler + generatePackageLevelTranslationHelper(f) + + fileName := filepath.Join(dir, "handler.go") + return f.Save(fileName) +} + +func generateDelegatingStateHandlers(f *jen.File, resourceName, apiPkg string, refsByKind map[string][]ReferenceField) { + handlers := []string{ + "HandleInitial", + "HandleImportRequested", + "HandleImported", + "HandleCreating", + "HandleCreated", + "HandleUpdating", + "HandleUpdated", + "HandleDeletionRequested", + "HandleDeleting", + } + startStateMap := map[string]string{ + "HandleInitial": "StateInitial", + "HandleImportRequested": "StateImportRequested", + "HandleImported": "StateImported", + "HandleCreating": "StateCreating", + "HandleCreated": "StateCreated", + "HandleUpdating": "StateUpdating", + "HandleUpdated": "StateUpdated", + "HandleDeletionRequested": "StateDeletionRequested", + "HandleDeleting": "StateDeleting", + } + + for _, handlerName := range handlers { + f.Comment(fmt.Sprintf("%s delegates to the version-specific handler", handlerName)) + f.Func().Params(jen.Id("h").Op("*").Id("Handler")).Id(handlerName).Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(strings.ToLower(resourceName)).Op("*").Qual(apiPkg, resourceName), + ).Params( + jen.Qual(pkgCtrlState, "Result"), + jen.Error(), + ).Block( + jen.List(jen.Id("handler"), jen.Id("err")). + Op(":="). + Id("h"). + Dot("getHandlerForResource"). + Call(jen.Id("ctx"), jen.Id(strings.ToLower(resourceName))), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/result", "Error").Call( + jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state", startStateMap[handlerName]), + jen.Id("err"), + )), + ), + jen.Return(jen.Id("handler").Dot(handlerName).Call(jen.Id("ctx"), jen.Id(strings.ToLower(resourceName)))), + ) + } + + f.Comment("For returns the resource and predicates for the controller") + f.Func().Params(jen.Id("h").Op("*").Id("Handler")).Id("For").Params().Params( + jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "Object"), + jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "Predicates"), + ).Block( + jen.Id("obj").Op(":=").Op("&").Qual(apiPkg, resourceName).Values(), + jen.Return( + jen.Id("obj"), + jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "WithPredicates").Call(jen.Id("h").Dot("predicates").Op("...")), + ), + ) + + generateMapperFunctions(f, resourceName, apiPkg, refsByKind) + + generateSetupWithManager(f, resourceName, refsByKind) +} + +func generateSDKClientSetMethod(f *jen.File, resourceName, apiPkg string) { + resourceLower := strings.ToLower(resourceName) + + f.Comment("getSDKClientSet creates an Atlas SDK client set using credentials from the resource's connection secret") + f.Func().Params( + jen.Id("h").Op("*").Id("Handler"), + ).Id("getSDKClientSet").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(resourceLower).Op("*").Qual(apiPkg, resourceName), + ).Params( + jen.Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlas", "ClientSet"), + jen.Error(), + ).Block( + jen.Var().Id("connectionSecretRef").Op("*").Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKey"), + jen.If(jen.Id(resourceLower).Dot("Spec").Dot("ConnectionSecretRef").Op("!=").Nil()).Block( + jen.Id("connectionSecretRef").Op("=").Op("&").Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKey").Values(jen.Dict{ + jen.Id("Name"): jen.Id(resourceLower).Dot("Spec").Dot("ConnectionSecretRef").Dot("Name"), + jen.Id("Namespace"): jen.Id(resourceLower).Dot("Namespace"), + }), + ), + jen.Line(), + + jen.List(jen.Id("connectionConfig"), jen.Id("err")).Op(":=").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/reconciler", "GetConnectionConfig").Call( + jen.Id("ctx"), + jen.Id("h").Dot("Client"), + jen.Id("connectionSecretRef"), + jen.Op("&").Id("h").Dot("GlobalSecretRef"), + ), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( + jen.Lit("failed to resolve Atlas credentials: %w"), + jen.Id("err"), + )), + ), + jen.Line(), + jen.List(jen.Id("clientSet"), jen.Id("err")).Op(":=").Id("h").Dot("AtlasProvider").Dot("SdkClientSet").Call( + jen.Id("ctx"), + jen.Id("connectionConfig").Dot("Credentials"), + jen.Id("h").Dot("Log"), + ), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( + jen.Lit("failed to setup Atlas SDK client: %w"), + jen.Id("err"), + )), + ), + jen.Line(), + jen.Return(jen.Id("clientSet"), jen.Nil()), + ) +} + +func generatePackageLevelTranslationHelper(f *jen.File) { + f.Comment("getTranslationRequest creates a translation request for converting entities between API and AKO.") + f.Comment("This is a package-level function that can be called from any handler.") + f.Func().Id("getTranslationRequest").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id("kubeClient").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Client"), + jen.Id("crdName").String(), + jen.Id("storageVersion").String(), + jen.Id("targetVersion").String(), + ).Params( + jen.Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request"), + jen.Error(), + ).Block( + jen.Id("crd").Op(":=").Op("&").Qual("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1", "CustomResourceDefinition").Values(), + jen.Id("err").Op(":=").Id("kubeClient").Dot("Get").Call( + jen.Id("ctx"), + jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKey").Values(jen.Dict{ + jen.Id("Name"): jen.Id("crdName"), + }), + jen.Id("crd"), + ), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( + jen.Lit("failed to resolve CRD %s: %w"), + jen.Id("crdName"), + jen.Id("err"), + )), + ), + jen.Line(), + jen.List(jen.Id("translator"), jen.Id("err")).Op(":=").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "NewTranslator").Call( + jen.Id("crd"), + jen.Id("storageVersion"), + jen.Id("targetVersion"), + ), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil(), jen.Qual("fmt", "Errorf").Call( + jen.Lit("failed to setup translator: %w"), + jen.Id("err"), + )), + ), + jen.Line(), + jen.Return(jen.Op("&").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request").Values(jen.Dict{ + jen.Id("Translator"): jen.Id("translator"), + jen.Id("Dependencies"): jen.Nil(), + }), jen.Nil()), + ) +} diff --git a/tools/scaffolder/internal/generate/handler_test.go b/tools/scaffolder/internal/generate/handler_test.go new file mode 100644 index 0000000000..7ce7bebc41 --- /dev/null +++ b/tools/scaffolder/internal/generate/handler_test.go @@ -0,0 +1,138 @@ +package generate + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGeneratedHandlerDelegation(t *testing.T) { + testYAML := ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: resources.atlas.generated.mongodb.com + annotations: + api-mappings: | + properties: + spec: + properties: + v20250312: + x-atlas-sdk-version: go.mongodb.org/atlas-sdk/v20250312008/admin + v20250401: + x-atlas-sdk-version: go.mongodb.org/atlas-sdk/v20250401001/admin +spec: + group: atlas.generated.mongodb.com + names: + kind: Resource + plural: resources + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + v20250312: + type: object + v20250401: + type: object +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.yaml") + err := os.WriteFile(testFile, []byte(testYAML), 0644) + require.NoError(t, err) + + controllerDir := filepath.Join(tmpDir, "controllers") + indexerDir := filepath.Join(tmpDir, "indexers") + + err = FromConfig(testFile, "Resource", controllerDir, indexerDir, "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", true) + require.NoError(t, err) + + handlerFile := filepath.Join(controllerDir, "resource", "handler.go") + content, err := os.ReadFile(handlerFile) + require.NoError(t, err) + + contentStr := string(content) + + assert.Contains(t, contentStr, "func (h *Handler) getHandlerForResource") + assert.Contains(t, contentStr, "func (h *Handler) HandleInitial") + assert.Contains(t, contentStr, "func (h *Handler) HandleCreating") + assert.Contains(t, contentStr, "func (h *Handler) HandleDeletionRequested") + v1Handler := filepath.Join(controllerDir, "resource", "handler_v20250312.go") + assert.FileExists(t, v1Handler) + + v2Handler := filepath.Join(controllerDir, "resource", "handler_v20250401.go") + assert.FileExists(t, v2Handler) +} + +func TestGeneratedHelperFunctions(t *testing.T) { + testYAML := ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: groups.atlas.generated.mongodb.com + annotations: + api-mappings: | + properties: + spec: + properties: + v20250312: + x-atlas-sdk-version: go.mongodb.org/atlas-sdk/v20250312008/admin +spec: + group: atlas.generated.mongodb.com + names: + kind: Group + plural: groups + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + v20250312: + type: object +` + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.yaml") + err := os.WriteFile(testFile, []byte(testYAML), 0644) + require.NoError(t, err) + + controllerDir := filepath.Join(tmpDir, "controllers") + indexerDir := filepath.Join(tmpDir, "indexers") + + err = FromConfig(testFile, "Group", controllerDir, indexerDir, "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1", true) + require.NoError(t, err) + + // Test handler.go contains package-level getTranslationRequest function + handlerFile := filepath.Join(controllerDir, "group", "handler.go") + content, err := os.ReadFile(handlerFile) + require.NoError(t, err) + contentStr := string(content) + + // Verify package-level getTranslationRequest function + assert.Contains(t, contentStr, "func getTranslationRequest(") + assert.Contains(t, contentStr, "ctx context.Context") + assert.Contains(t, contentStr, "kubeClient client.Client") + assert.Contains(t, contentStr, "crdName string") + assert.Contains(t, contentStr, "storageVersion string") + assert.Contains(t, contentStr, "targetVersion string") + assert.Contains(t, contentStr, "NewTranslator") + + // Verify getSDKClientSet method + assert.Contains(t, contentStr, "func (h *Handler) getSDKClientSet(") + assert.Contains(t, contentStr, "GetConnectionConfig") + assert.Contains(t, contentStr, "SdkClientSet") + assert.Contains(t, contentStr, "ConnectionSecretRef") + + // Verify getTranslationRequest wrapper method + assert.Contains(t, contentStr, "func getTranslationRequest(") + assert.Contains(t, contentStr, "translate.NewTranslator(") + assert.Contains(t, contentStr, "return &translate.Request{") +} diff --git a/tools/scaffolder/internal/generate/versioned_handler.go b/tools/scaffolder/internal/generate/versioned_handler.go new file mode 100644 index 0000000000..15b55f837f --- /dev/null +++ b/tools/scaffolder/internal/generate/versioned_handler.go @@ -0,0 +1,118 @@ +package generate + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/dave/jennifer/jen" +) + +func generateVersionHandlerFile(dir, resourceName, typesPath string, mapping MappingWithConfig, override bool) error { + atlasResourceName := strings.ToLower(resourceName) + versionSuffix := mapping.Version + apiPkg := typesPath + sdkImportPath := mapping.OpenAPIConfig.Package + + fileName := filepath.Join(dir, "handler_"+versionSuffix+".go") + + // Check if a versioned handler file exists + if !override { + if _, err := os.Stat(fileName); err == nil { + fmt.Printf("Skipping versioned handler %s (already exists, use --override to overwrite)\n", fileName) + return nil + } + } + + f := jen.NewFile(atlasResourceName) + AddLicenseHeader(f) + + f.ImportAlias(pkgCtrlState, "ctrlstate") + f.ImportAlias(apiPkg, "akov2generated") + f.ImportAlias(sdkImportPath, versionSuffix+"sdk") + + f.Type().Id("Handler"+versionSuffix).Struct( + jen.Id("kubeClient").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Client"), + jen.Id("atlasClient").Op("*").Qual(sdkImportPath, "APIClient"), + jen.Id("translationRequest").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request"), + ) + + f.Func().Id("NewHandler"+versionSuffix).Params( + jen.Id("kubeClient").Qual("sigs.k8s.io/controller-runtime/pkg/client", "Client"), + jen.Id("atlasClient").Op("*").Qual(sdkImportPath, "APIClient"), + jen.Id("translationRequest").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/generated/translate", "Request"), + ).Op("*").Id("Handler" + versionSuffix).Block( + jen.Return(jen.Op("&").Id("Handler" + versionSuffix).Values(jen.Dict{ + jen.Id("kubeClient"): jen.Id("kubeClient"), + jen.Id("atlasClient"): jen.Id("atlasClient"), + jen.Id("translationRequest"): jen.Id("translationRequest"), + })), + ) + + generateVersionStateHandlers(f, resourceName, apiPkg, versionSuffix) + + // Generate For and SetupWithManager methods to satisfy StateHandler interface + generateVersionInterfaceMethods(f, resourceName, apiPkg, versionSuffix) + + return f.Save(fileName) +} + +func generateVersionStateHandlers(f *jen.File, resourceName, apiPkg, versionSuffix string) { + handlers := []struct { + name string + nextState string + message string + }{ + {"HandleInitial", "StateUpdated", "Updated Atlas" + resourceName + "."}, + {"HandleImportRequested", "StateImported", "Import completed"}, + {"HandleImported", "StateUpdated", "Ready"}, + {"HandleCreating", "StateCreated", "Resource created"}, + {"HandleCreated", "StateUpdated", "Ready"}, + {"HandleUpdating", "StateUpdated", "Update completed"}, + {"HandleUpdated", "StateUpdated", "Ready"}, + {"HandleDeletionRequested", "StateDeleting", "Deletion started"}, + {"HandleDeleting", "StateDeleted", "Deleted"}, + } + + for _, handler := range handlers { + f.Comment(fmt.Sprintf("%s handles the %s state for version %s", handler.name, strings.ToLower(strings.TrimPrefix(handler.name, "Handle")), versionSuffix)) + f.Func().Params(jen.Id("h").Op("*").Id("Handler"+versionSuffix)).Id(handler.name).Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(strings.ToLower(resourceName)).Op("*").Qual(apiPkg, resourceName), + ).Params( + jen.Qual(pkgCtrlState, "Result"), + jen.Error(), + ).Block( + jen.Comment("TODO: Implement "+strings.ToLower(strings.TrimPrefix(handler.name, "Handle"))+" state logic"), + jen.Comment("TODO: Use h.atlasProvider.SdkClientSet(ctx, h.globalSecretRef, h.log) to get Atlas SDK client"), + jen.Return(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/result", "NextState").Call( + jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state", handler.nextState), + jen.Lit(handler.message), + )), + ) + } +} + +// generateVersionInterfaceMethods generates For and SetupWithManager methods for version-specific handlers +func generateVersionInterfaceMethods(f *jen.File, resourceName, apiPkg, versionSuffix string) { + // For method + f.Comment("For returns the resource and predicates for the controller") + f.Func().Params(jen.Id("h").Op("*").Id("Handler"+versionSuffix)).Id("For").Params().Params( + jen.Qual("sigs.k8s.io/controller-runtime/pkg/client", "Object"), + jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "Predicates"), + ).Block( + jen.Return(jen.Op("&").Qual(apiPkg, resourceName).Values(), jen.Qual("sigs.k8s.io/controller-runtime/pkg/builder", "WithPredicates").Call()), + ) + + // SetupWithManager method + f.Comment("SetupWithManager sets up the controller with the Manager") + f.Func().Params(jen.Id("h").Op("*").Id("Handler"+versionSuffix)).Id("SetupWithManager").Params( + jen.Id("mgr").Qual("sigs.k8s.io/controller-runtime", "Manager"), + jen.Id("rec").Qual("sigs.k8s.io/controller-runtime/pkg/reconcile", "Reconciler"), + jen.Id("defaultOptions").Qual("sigs.k8s.io/controller-runtime/pkg/controller", "Options"), + ).Error().Block( + jen.Comment("This method is not used for version-specific handlers but required by StateHandler interface"), + jen.Return(jen.Nil()), + ) +}