From c62cde0b8799d3f4075c0418b6b729728566154d Mon Sep 17 00:00:00 2001 From: Igor Karpukhin Date: Tue, 16 Sep 2025 17:04:12 +0200 Subject: [PATCH] Initial commit for Scaffolder --- tools/scaffolder/LICENSE | 201 +++++++++++ tools/scaffolder/Makefile | 6 + tools/scaffolder/README.md | 139 ++++++++ tools/scaffolder/cmd/main.go | 42 +++ tools/scaffolder/devbox.json | 17 + tools/scaffolder/devbox.lock | 105 ++++++ tools/scaffolder/go.mod | 36 ++ tools/scaffolder/go.sum | 106 ++++++ tools/scaffolder/hack/boilerplate.go.txt | 13 + tools/scaffolder/internal/generate/config.go | 174 +++++++++ .../internal/generate/controller.go | 336 ++++++++++++++++++ .../internal/generate/translation.go | 238 +++++++++++++ 12 files changed, 1413 insertions(+) create mode 100644 tools/scaffolder/LICENSE create mode 100644 tools/scaffolder/Makefile create mode 100644 tools/scaffolder/README.md create mode 100644 tools/scaffolder/cmd/main.go create mode 100644 tools/scaffolder/devbox.json create mode 100644 tools/scaffolder/devbox.lock create mode 100644 tools/scaffolder/go.mod create mode 100644 tools/scaffolder/go.sum create mode 100644 tools/scaffolder/hack/boilerplate.go.txt create mode 100644 tools/scaffolder/internal/generate/config.go create mode 100644 tools/scaffolder/internal/generate/controller.go create mode 100644 tools/scaffolder/internal/generate/translation.go diff --git a/tools/scaffolder/LICENSE b/tools/scaffolder/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/tools/scaffolder/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/scaffolder/Makefile b/tools/scaffolder/Makefile new file mode 100644 index 0000000000..6daf8e964d --- /dev/null +++ b/tools/scaffolder/Makefile @@ -0,0 +1,6 @@ + +build: + go build -o bin/ako-controller-scaffolder cmd/main.go + +generate: + controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./pkg/api/..." diff --git a/tools/scaffolder/README.md b/tools/scaffolder/README.md new file mode 100644 index 0000000000..49e94bab3a --- /dev/null +++ b/tools/scaffolder/README.md @@ -0,0 +1,139 @@ +# Atlas Controller Scaffolder + +A tool to generate Kubernetes controllers for MongoDB Atlas resources based on CRD configurations. + +> [!WARNING] +> This is the experimental tool. Bugs and issues are still present + +## Overview + +This scaffolder generates Kubernetes controllers that follow the MongoDB Atlas Kubernetes operator patterns, including: + +- **State machine-based controllers** with proper lifecycle management +- **Translation layers** for Atlas SDK integration +- **Service interfaces** with appropriate Atlas API mappings +- **License headers** and proper package structure + +## Dependencies + +### Required Local Repositories + +The following repositories must be available locally in the parent directory: + +``` +work/ +├── atlas-controller-scaffolder/ # This tool +├── atlas2crd/ # Config definitions +└── mongodb-atlas-kubernetes/ # Target operator +``` + +### Setup + +1. **Clone dependencies:** + + ```bash + cd ../ + git clone https://github.com/mongodb-atlas-kubernetes/atlas2crd + git clone https://github.com/mongodb/mongodb-atlas-kubernetes + ``` + +2. **Replace the crd2go dependency** + Make sure the crd2go dependency is pointing to the local copy. In your `go.mod` file: + + ```bash + replace github.com/josvazg/crd2go => ../crd2go + ``` + +3. **Generate CRD types** + Use `crd2go` tool to generate go types for CRDs: + + ```bash + cd ./crd2go/ + go build -o ./crd2go ./cmd/crd2go/main.go + ./crd2go -input=./pkg/crd2go/samples/crds.yaml -output=../atlas-controller-scaffolder/pkg/api/v1 + ``` + +4. **Build the scaffolder:** + ```bash + cd ../atlas-controller-scaffolder + go build -o ./bin/ako-controller-scaffolder ./cmd/main.go + ``` + + +## Usage + +### List Available CRDs + +```bash +./bin/ako-controller-scaffolder --config ../atlas2crd/config.yaml --list +``` + +### Generate Controller + +```bash +./bin/ako-controller-scaffolder --config ../atlas2crd/config.yaml --crd +``` + +**Examples:** + +```bash +# Generate Team controller +./bin/ako-controller-scaffolder --config ../atlas2crd/config.yaml --crd Team + +# Generate Organization controller +./bin/ako-controller-scaffolder --config ../atlas2crd/config.yaml --crd Organization + +# Generate DatabaseUser controller +./bin/ako-controller-scaffolder --config ../atlas2crd/config.yaml --crd DatabaseUser +``` + +### Show Available CRDs + +If you don't specify a CRD, the tool will show you all available options: + +```bash +./bin/ako-controller-scaffolder --config ../atlas2crd/config.yaml +``` + +## Generated Structure + +The tool generates controllers in the MongoDB Atlas Kubernetes operator directory: + +``` +../mongodb-atlas-kubernetes/internal/ +├── controller/{crd_name}/ +│ ├── {crd_name}_controller.go # Main controller with reconciler +│ └── handler.go # State handlers and setup +└── translation/{crd_name}/ + ├── {crd_name}.go # Translation types + └── service.go # Atlas SDK service interface +``` + +Generated controllers support all specified CRD versions. Translation layers stabs are generated per CRD version, using the Atlas SDK specified in the atlas2crd config file + +## Features + +- **Dynamic API Mapping** - Automatically selects correct Atlas SDK API +- **State Machine Pattern** - Follows existing controller patterns +- **Translation Layer** - Converts between Kubernetes and Atlas types +- **License Headers** - Proper MongoDB license in all files +- **RBAC Annotations** - Kubebuilder RBAC markers included +- **Package Structure** - Consistent with existing codebase + +## Available CRDs + +Run with `--list` to see all available CRDs, including: + +- Team, Organization, DatabaseUser +- Cluster, FlexCluster, SearchIndex +- BackupCompliancePolicy, DataFederation +- NetworkPeeringConnection, CustomRole +- And many more... + +## Development + +The scaffolder uses: + +- **[Jennifer](https://github.com/dave/jennifer)** for Go code generation +- **YAML parsing** for atlas2crd config files +- **Atlas SDK mapping** for correct API selection diff --git a/tools/scaffolder/cmd/main.go b/tools/scaffolder/cmd/main.go new file mode 100644 index 0000000000..e34d14650b --- /dev/null +++ b/tools/scaffolder/cmd/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "os" + "tools/scaffolder/internal/generate" + + "github.com/spf13/cobra" +) + +var ( + configPath string + crdKind string + listCRDs bool +) + +func main() { + rootCmd := &cobra.Command{ + Use: "ako-controller-scaffolder", + Short: "Generate Kubernetes controllers for MongoDB Atlas CRDs", + RunE: func(cmd *cobra.Command, args []string) error { + if configPath == "" { + return fmt.Errorf("--config is required") + } + + if listCRDs { + return generate.PrintCRDs(configPath) + } + + return generate.FromConfig(configPath, crdKind) + }, + } + + rootCmd.Flags().StringVar(&configPath, "config", "", "Path to atlas2crd config file (required)") + rootCmd.Flags().StringVar(&crdKind, "crd", "", "CRD kind to generate controller for") + rootCmd.Flags().BoolVar(&listCRDs, "list", false, "List available CRDs from config file") + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/tools/scaffolder/devbox.json b/tools/scaffolder/devbox.json new file mode 100644 index 0000000000..15021a1fca --- /dev/null +++ b/tools/scaffolder/devbox.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.15.1/.schema/devbox.schema.json", + "packages": [ + "go@latest", + "golangci-lint@latest" + ], + "shell": { + "init_hook": [ + "echo 'Welcome to devbox!' > /dev/null" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} diff --git a/tools/scaffolder/devbox.lock b/tools/scaffolder/devbox.lock new file mode 100644 index 0000000000..a25f5c69ad --- /dev/null +++ b/tools/scaffolder/devbox.lock @@ -0,0 +1,105 @@ +{ + "lockfile_version": "1", + "packages": { + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2025-09-02T13:16:47Z", + "resolved": "github:NixOS/nixpkgs/aaff8c16d7fc04991cac6245bee1baa31f72b1e1?lastModified=1756819007&narHash=sha256-12V64nKG%2FO%2FguxSYnr5%2Fnq1EfqwJCdD2%2BcIGmhz3nrE%3D" + }, + "go@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#go", + "source": "devbox-search", + "version": "1.24.5", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/kw1vd98s15vj700m3gx2x2xca2z477i3-go-1.24.5", + "default": true + } + ], + "store_path": "/nix/store/kw1vd98s15vj700m3gx2x2xca2z477i3-go-1.24.5" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/5bzlaj0c4mqw9p0zrcx5g9vz16vd45dl-go-1.24.5", + "default": true + } + ], + "store_path": "/nix/store/5bzlaj0c4mqw9p0zrcx5g9vz16vd45dl-go-1.24.5" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/b72n20ixzl5ja9vciwahkr30bhmsn5jc-go-1.24.5", + "default": true + } + ], + "store_path": "/nix/store/b72n20ixzl5ja9vciwahkr30bhmsn5jc-go-1.24.5" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/y4awwzp30ka130wmjrpaqjmjdf9p010w-go-1.24.5", + "default": true + } + ], + "store_path": "/nix/store/y4awwzp30ka130wmjrpaqjmjdf9p010w-go-1.24.5" + } + } + }, + "golangci-lint@latest": { + "last_modified": "2025-08-03T19:18:05Z", + "resolved": "github:NixOS/nixpkgs/bf9fa86a9b1005d932f842edf2c38eeecc98eef3#golangci-lint", + "source": "devbox-search", + "version": "2.3.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ahn380skfkdg3ynimjpqakc1wp26mpsi-golangci-lint-2.3.1", + "default": true + } + ], + "store_path": "/nix/store/ahn380skfkdg3ynimjpqakc1wp26mpsi-golangci-lint-2.3.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/5wf61l5pnqlna9mwllllq334rkh1i2a6-golangci-lint-2.3.1", + "default": true + } + ], + "store_path": "/nix/store/5wf61l5pnqlna9mwllllq334rkh1i2a6-golangci-lint-2.3.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/sbqfd50kb6g4gka3w1vixjrnacspdwq5-golangci-lint-2.3.1", + "default": true + } + ], + "store_path": "/nix/store/sbqfd50kb6g4gka3w1vixjrnacspdwq5-golangci-lint-2.3.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/kxbsvkixvkn0ss81rbc1g814sizd1jwj-golangci-lint-2.3.1", + "default": true + } + ], + "store_path": "/nix/store/kxbsvkixvkn0ss81rbc1g814sizd1jwj-golangci-lint-2.3.1" + } + } + } + } +} diff --git a/tools/scaffolder/go.mod b/tools/scaffolder/go.mod new file mode 100644 index 0000000000..3298c41830 --- /dev/null +++ b/tools/scaffolder/go.mod @@ -0,0 +1,36 @@ +module tools/scaffolder + +go 1.24.4 + +// replace github.com/josvazg/crd2go => ../crd2go + +require ( + github.com/dave/jennifer v1.7.1 + // github.com/josvazg/crd2go v0.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.10.1 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.34.0 +) + +require ( + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/text v0.24.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect +) diff --git a/tools/scaffolder/go.sum b/tools/scaffolder/go.sum new file mode 100644 index 0000000000..33e0ca2218 --- /dev/null +++ b/tools/scaffolder/go.sum @@ -0,0 +1,106 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/tools/scaffolder/hack/boilerplate.go.txt b/tools/scaffolder/hack/boilerplate.go.txt new file mode 100644 index 0000000000..04745d1406 --- /dev/null +++ b/tools/scaffolder/hack/boilerplate.go.txt @@ -0,0 +1,13 @@ +//Copyright 2025 MongoDB Inc +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. \ No newline at end of file diff --git a/tools/scaffolder/internal/generate/config.go b/tools/scaffolder/internal/generate/config.go new file mode 100644 index 0000000000..eb6f54dd91 --- /dev/null +++ b/tools/scaffolder/internal/generate/config.go @@ -0,0 +1,174 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generate + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Config structures +type Config struct { + metav1.TypeMeta `yaml:",inline"` + Spec Spec `yaml:"spec"` +} + +type Spec struct { + OpenAPI []OpenAPIConfig `yaml:"openapi,omitempty"` + CRD []CRDConfig `yaml:"crd,omitempty"` +} + +type OpenAPIConfig struct { + Name string `yaml:"name"` + Package string `yaml:"package"` +} + +type CRDConfig struct { + GVK metav1.GroupVersionKind `yaml:"gvk"` + Categories []string `yaml:"categories,omitempty"` + ShortNames []string `yaml:"shortNames,omitempty"` + Mappings []Mapping `yaml:"mappings,omitempty"` +} + +type Mapping struct { + MajorVersion string `yaml:"majorVersion"` + OpenAPIRef OpenAPIRef `yaml:"openAPIRef"` + Parameters *Parameters `yaml:"parameters,omitempty"` + Entry *SchemaRef `yaml:"entry,omitempty"` + Status *SchemaRef `yaml:"status,omitempty"` +} + +type OpenAPIRef struct { + Name string `yaml:"name"` +} + +type Parameters struct { + Path *PathInfo `yaml:"path,omitempty"` + Query []QueryParam `yaml:"query,omitempty"` + Additional []interface{} `yaml:"additional,omitempty"` +} + +type PathInfo struct { + Template string `yaml:"template"` +} + +type QueryParam struct { + Name string `yaml:"name"` + Value string `yaml:"value"` +} + +type SchemaRef struct { + Schema string `yaml:"$ref"` +} + +// MappingWithConfig combines mapping with its OpenAPI config +type MappingWithConfig struct { + Mapping Mapping + OpenAPIConfig OpenAPIConfig +} + +// ParsedConfig contains all parsed configuration data +type ParsedConfig struct { + Config Config + OpenAPIMap map[string]OpenAPIConfig + CRDMap map[string]CRDConfig + SelectedCRD CRDConfig + Mappings []MappingWithConfig + ResourceName string +} + +// ParseAtlas2CRDConfig reads and parses the configuration file, validates CRD selection, +// and returns all necessary data for generating controllers and handlers +func ParseAtlas2CRDConfig(configPath, crdKind string) (*ParsedConfig, error) { + // Read and parse config file + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse config YAML: %w", err) + } + + // Create OpenAPI mapping + openAPIMap := make(map[string]OpenAPIConfig) + for _, openAPIConfig := range config.Spec.OpenAPI { + openAPIMap[openAPIConfig.Name] = openAPIConfig + } + + // Create CRD mapping and find selected CRD + crdMap := make(map[string]CRDConfig) + var selectedCRD CRDConfig + var found bool + + for _, crd := range config.Spec.CRD { + crdMap[crd.GVK.Kind] = crd + if crd.GVK.Kind == crdKind { + selectedCRD = crd + found = true + } + } + + if !found { + return nil, fmt.Errorf("CRD kind '%s' not found in config", crdKind) + } + + if len(selectedCRD.Mappings) == 0 { + return nil, fmt.Errorf("no mappings found for CRD kind '%s'", crdKind) + } + + // Build mappings with their OpenAPI configs + var mappingsWithConfig []MappingWithConfig + for _, mapping := range selectedCRD.Mappings { + openAPIConfig, exists := openAPIMap[mapping.OpenAPIRef.Name] + if !exists { + return nil, fmt.Errorf("OpenAPI config '%s' not found for mapping", mapping.OpenAPIRef.Name) + } + + mappingsWithConfig = append(mappingsWithConfig, MappingWithConfig{ + Mapping: mapping, + OpenAPIConfig: openAPIConfig, + }) + } + + return &ParsedConfig{ + Config: config, + OpenAPIMap: openAPIMap, + CRDMap: crdMap, + SelectedCRD: selectedCRD, + Mappings: mappingsWithConfig, + ResourceName: crdKind, + }, nil +} + +// ListCRDs returns a list of all available CRDs from the config file +func ListCRDs(configPath string) ([]CRDConfig, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse config YAML: %w", err) + } + + return config.Spec.CRD, nil +} + diff --git a/tools/scaffolder/internal/generate/controller.go b/tools/scaffolder/internal/generate/controller.go new file mode 100644 index 0000000000..50acbd421e --- /dev/null +++ b/tools/scaffolder/internal/generate/controller.go @@ -0,0 +1,336 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generate + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/dave/jennifer/jen" +) + +const ( + pkgCtrlState = "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/state" +) + +// FromConfig generates controllers and handlers based on the parsed configuration +func FromConfig(configPath, crdKind string) error { + // Parse config using the new ParseConfig function + parsedConfig, err := ParseAtlas2CRDConfig(configPath, crdKind) + if err != nil { + return err + } + + resourceName := parsedConfig.ResourceName + baseControllerDir := filepath.Join("..", "mongodb-atlas-kubernetes", "internal", "controller", strings.ToLower(resourceName)) + + // Generate translation layers for all mappings + if err := GenerateTranslationLayers(resourceName, parsedConfig.Mappings); err != nil { + return fmt.Errorf("failed to generate translation layers: %w", err) + } + + controllerName := resourceName + controllerDir := baseControllerDir + + if err := os.MkdirAll(controllerDir, 0755); err != nil { + return fmt.Errorf("failed to create controller directory: %w", err) + } + + if err := generateControllerFileWithMultipleVersions(controllerDir, controllerName, resourceName, parsedConfig.Mappings); err != nil { + return fmt.Errorf("failed to generate controller file: %w", err) + } + + if err := generateHandlerFileWithMultipleVersions(controllerDir, controllerName, resourceName, parsedConfig.Mappings); err != nil { + return fmt.Errorf("failed to generate handler file: %w", err) + } + + fmt.Printf("Successfully generated controller %s for resource %s with %d SDK versions at %s\n", + controllerName, resourceName, len(parsedConfig.Mappings), controllerDir) + + return nil +} + +// PrintCRDs displays available CRDs from the config file +func PrintCRDs(configPath string) error { + crds, err := ListCRDs(configPath) + if err != nil { + return err + } + + fmt.Printf("Available CRDs in %s:\n\n", configPath) + for _, crd := range crds { + fmt.Printf("Kind: %s\n", crd.GVK.Kind) + fmt.Printf(" Group: %s\n", crd.GVK.Group) + fmt.Printf(" Version: %s\n", crd.GVK.Version) + if len(crd.ShortNames) > 0 { + fmt.Printf(" Short Names: %s\n", strings.Join(crd.ShortNames, ", ")) + } + if len(crd.Categories) > 0 { + fmt.Printf(" Categories: %s\n", strings.Join(crd.Categories, ", ")) + } + fmt.Println() + } + return nil +} + +func generateControllerFileWithMultipleVersions(dir, controllerName, resourceName string, mappings []MappingWithConfig) error { + atlasResourceName := strings.ToLower(resourceName) + atlasAPI, err := GetAtlasAPIForCRD(resourceName) + if err != nil { + return fmt.Errorf("failed to get Atlas API for CRD %s: %w", resourceName, err) + } + + f := jen.NewFile(atlasResourceName) + AddLicenseHeader(f) + + f.ImportAlias(pkgCtrlState, "ctrlstate") + + // RBAC + f.Comment(fmt.Sprintf("+kubebuilder:rbac:groups=atlas.mongodb.com,resources=%s,verbs=get;list;watch;create;update;patch;delete", strings.ToLower("atlas"+resourceName+"s"))) + f.Comment(fmt.Sprintf("+kubebuilder:rbac:groups=atlas.mongodb.com,resources=%s/status,verbs=get;update;patch", strings.ToLower("atlas"+resourceName+"s"))) + f.Comment(fmt.Sprintf("+kubebuilder:rbac:groups=atlas.mongodb.com,resources=%s/finalizers,verbs=update", strings.ToLower("atlas"+resourceName+"s"))) + f.Comment(fmt.Sprintf("+kubebuilder:rbac:groups=atlas.mongodb.com,namespace=default,resources=%s,verbs=get;list;watch;create;update;patch;delete", strings.ToLower("atlas"+resourceName+"s"))) + f.Comment(fmt.Sprintf("+kubebuilder:rbac:groups=atlas.mongodb.com,namespace=default,resources=%s/status,verbs=get;update;patch", strings.ToLower("atlas"+resourceName+"s"))) + f.Comment(fmt.Sprintf("+kubebuilder:rbac:groups=atlas.mongodb.com,namespace=default,resources=%s/finalizers,verbs=update", strings.ToLower("atlas"+resourceName+"s"))) + + // Service builder types for each version + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + f.Type().Id("serviceBuilderFunc"+versionSuffix).Func().Params( + jen.Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlas", "ClientSet"), + ).Qual(fmt.Sprintf("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/%s%s", atlasResourceName, versionSuffix), "Atlas"+resourceName+"Service") + } + + // Handler struct for ALL CRD versions + handlerFields := []jen.Code{ + jen.Qual(pkgCtrlState, "StateHandler").Types(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName)), + jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/reconciler", "AtlasReconciler"), + } + + // Service builder for each version + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + handlerFields = append(handlerFields, jen.Id("serviceBuilder"+versionSuffix).Id("serviceBuilderFunc"+versionSuffix)) + } + + f.Type().Id(controllerName + "Handler").Struct(handlerFields...) + + // NewReconciler method with all service builders + reconcilerParams := []jen.Code{ + jen.Id("c").Qual("sigs.k8s.io/controller-runtime/pkg/cluster", "Cluster"), + jen.Id("atlasProvider").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlas", "Provider"), + jen.Id("logger").Op("*").Qual("go.uber.org/zap", "Logger"), + jen.Id("globalSecretRef").Qual("sigs.k8s.io/controller-runtime/pkg/client", "ObjectKey"), + jen.Id("reapplySupport").Bool(), + } + + serviceBuilderValues := jen.Dict{ + jen.Id("AtlasReconciler"): jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/reconciler", "AtlasReconciler").Values(jen.Dict{ + jen.Id("Client"): jen.Id("c").Dot("GetClient").Call(), + jen.Id("AtlasProvider"): jen.Id("atlasProvider"), + jen.Id("Log"): jen.Id("logger").Dot("Named").Call(jen.Lit("controllers")).Dot("Named").Call(jen.Lit("Atlas" + resourceName)).Dot("Sugar").Call(), + jen.Id("GlobalSecretRef"): jen.Id("globalSecretRef"), + }), + } + + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + serviceBuilderValues[jen.Id("serviceBuilder"+versionSuffix)] = jen.Func().Params(jen.Id("clientSet").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlas", "ClientSet")).Qual(fmt.Sprintf("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/%s%s", atlasResourceName, versionSuffix), "Atlas"+resourceName+"Service").Block( + jen.Return(jen.Qual(fmt.Sprintf("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/%s%s", atlasResourceName, versionSuffix), "NewAtlas"+resourceName+"Service").Call(jen.Id("clientSet").Dot("SdkClient" + versionSuffix + "006").Dot(atlasAPI))), + ) + } + + f.Func().Id("New"+controllerName+"Reconciler").Params(reconcilerParams...).Op("*").Qual(pkgCtrlState, "Reconciler").Types(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName)).Block( + jen.Id(strings.ToLower(controllerName)+"Handler").Op(":=").Op("&").Id(controllerName+"Handler").Values(serviceBuilderValues), + jen.Return(jen.Qual(pkgCtrlState, "NewStateReconciler").Call( + jen.Id(strings.ToLower(controllerName)+"Handler"), + jen.Qual(pkgCtrlState, "WithCluster").Types(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName)).Call(jen.Id("c")), + jen.Qual(pkgCtrlState, "WithReapplySupport").Types(jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName)).Call(jen.Id("reapplySupport")), + )), + ) + + fileName := filepath.Join(dir, atlasResourceName+"_controller.go") + return f.Save(fileName) +} + +func generateHandlerFileWithMultipleVersions(dir, controllerName, resourceName string, mappings []MappingWithConfig) error { + atlasResourceName := strings.ToLower(resourceName) + + f := jen.NewFile(atlasResourceName) + AddLicenseHeader(f) + + f.ImportAlias(pkgCtrlState, "ctrlstate") + + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + translationPkg := fmt.Sprintf("github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/%s%s", atlasResourceName, versionSuffix) + f.ImportAlias(translationPkg, atlasResourceName+versionSuffix) + } + + f.Type().Id("reconcileRequest").Struct( + jen.Id("version").String(), + jen.Id(strings.ToLower("a"+resourceName)).Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName), + ) + + // Helper method to get service for resource (merged getSDKVersion + getServiceForVersion) + f.Comment("getServiceForResource determines the SDK version from the resource spec and returns the appropriate service") + f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id("getServiceForResource").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(strings.ToLower("a"+resourceName)).Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName), + ).Params(jen.Interface(), jen.Error()).Block( + jen.Comment("Determine which SDK version to use from resource spec"), + jen.Var().Id("version").String(), + jen.BlockFunc(func(g *jen.Group) { + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + // Capitalize first letter of version (e.g., v20250312 -> V20250312) + capitalizedVersion := strings.ToUpper(string(versionSuffix[0])) + versionSuffix[1:] + g.If(jen.Id(strings.ToLower("a" + resourceName)).Dot("Spec").Dot(capitalizedVersion).Op("!=").Nil()).Block( + jen.Id("version").Op("=").Lit(versionSuffix), + ) + } + }), + jen.Comment("Ensure a version was specified"), + jen.If(jen.Id("version").Op("==").Lit("")).Block( + jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("no SDK version specified in resource spec - please specify one of the supported versions"))), + ), + jen.Comment("Get client set"), + jen.Id("clientSet").Op(",").Id("err").Op(":=").Id("h").Dot("AtlasProvider").Dot("SdkClient").Call(jen.Id("ctx").Op(",").Id("h").Dot("GlobalSecretRef").Op(",").Id("h").Dot("Log")), + jen.If(jen.Id("err").Op("!=").Nil()).Block( + jen.Return(jen.Nil().Op(",").Id("err")), + ), + jen.Comment("Return appropriate service for version"), + jen.Switch(jen.Id("version")).BlockFunc(func(g *jen.Group) { + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + g.Case(jen.Lit(versionSuffix)).Block( + jen.Return(jen.Id("h").Dot("serviceBuilder" + versionSuffix).Call(jen.Id("clientSet")).Op(",").Nil()), + ) + } + g.Default().Block( + jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("unsupported SDK version: %s"), jen.Id("version"))), + ) + }), + ) + + generateVersionAwareStateHandlers(f, controllerName, resourceName, mappings) + + fileName := filepath.Join(dir, "handler.go") + return f.Save(fileName) +} + +func generateVersionAwareStateHandlers(f *jen.File, controllerName, resourceName string, mappings []MappingWithConfig) { + // HandleInitial method + f.Comment("HandleInitial handles the initial state") + f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id("HandleInitial").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(strings.ToLower("a"+resourceName)).Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName), + ).Params( + jen.Qual(pkgCtrlState, "Result"), + jen.Error(), + ).Block( + jen.Comment("TODO: Implement initial state logic"), + 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", "StateUpdated"), + jen.Lit("Updated Atlas"+resourceName+"."), + )), + ) + + // HandleUpdated method + f.Comment("HandleUpdated handles the updated state") + f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id("HandleUpdated").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(strings.ToLower("a"+resourceName)).Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName), + ).Params( + jen.Qual(pkgCtrlState, "Result"), + jen.Error(), + ).Block( + jen.Comment("Get the appropriate service for this resource"), + jen.List(jen.Id("svc"), jen.Id("err")).Op(":=").Id("h").Dot("getServiceForResource").Call(jen.Id("ctx"), jen.Id(strings.ToLower("a"+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", "StateUpdated"), + jen.Id("err"), + )), + ), + jen.Comment("TODO: Use the service to implement updated state logic with Atlas API calls"), + jen.Id("_").Op("=").Id("svc").Comment("Use service in implementation"), + 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", "StateUpdated"), + jen.Lit("Ready"), + )), + ) + + // Generate all other state handler methods + handlers := []struct { + name string + nextState string + message string + }{ + {"HandleImportRequested", "StateImported", "Import completed"}, + {"HandleImported", "StateUpdated", "Ready"}, + {"HandleCreating", "StateCreated", "Resource created"}, + {"HandleCreated", "StateUpdated", "Ready"}, + {"HandleUpdating", "StateUpdated", "Update completed"}, + {"HandleDeletionRequested", "StateDeleting", "Deletion started"}, + {"HandleDeleting", "StateDeleted", "Deleted"}, + } + + for _, handler := range handlers { + f.Comment(fmt.Sprintf("%s handles the %s state", handler.name, strings.ToLower(strings.TrimPrefix(handler.name, "Handle")))) + f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"Handler")).Id(handler.name).Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id(strings.ToLower("a"+resourceName)).Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName), + ).Params( + jen.Qual(pkgCtrlState, "Result"), + jen.Error(), + ).Block( + jen.Comment("TODO: Implement "+strings.ToLower(strings.TrimPrefix(handler.name, "Handle"))+" state logic"), + 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), + )), + ) + } + + // For method + f.Comment("For returns the resource and predicates for the controller") + f.Func().Params(jen.Id("h").Op("*").Id(controllerName+"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("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName).Values(), + jen.Comment("TODO: Add appropriate predicates"), + jen.Return(jen.Id("obj"), 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(controllerName+"Handler")).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.Id("h").Dot("Client").Op("=").Id("mgr").Dot("GetClient").Call(), + jen.Return(jen.Qual("sigs.k8s.io/controller-runtime", "NewControllerManagedBy").Call(jen.Id("mgr")). + Dot("Named").Call(jen.Lit("Atlas"+resourceName)). + Dot("For").Call(jen.Id("h").Dot("For").Call()). + Dot("WithOptions").Call(jen.Id("defaultOptions")). + Dot("Complete").Call(jen.Id("rec"))), + ) +} + diff --git a/tools/scaffolder/internal/generate/translation.go b/tools/scaffolder/internal/generate/translation.go new file mode 100644 index 0000000000..f3af0ee1f5 --- /dev/null +++ b/tools/scaffolder/internal/generate/translation.go @@ -0,0 +1,238 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generate + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/dave/jennifer/jen" +) + +// GenerateTranslationLayers generates translation layers for all mappings +func GenerateTranslationLayers(resourceName string, mappings []MappingWithConfig) error { + for _, mapping := range mappings { + versionSuffix := mapping.Mapping.MajorVersion + if err := generateTranslationLayerWithVersion(resourceName, versionSuffix, mapping.OpenAPIConfig); err != nil { + return fmt.Errorf("failed to generate translation layer for version %s: %w", versionSuffix, err) + } + } + return nil +} + +func generateTranslationLayerWithVersion(resourceName, versionSuffix string, openAPIConfig OpenAPIConfig) error { + packageName := strings.ToLower(resourceName) + versionSuffix + translationDir := filepath.Join("..", "mongodb-atlas-kubernetes", "internal", "translation", packageName) + + if err := os.MkdirAll(translationDir, 0755); err != nil { + return fmt.Errorf("failed to create translation directory: %w", err) + } + + // Generate main translation file + if err := generateTranslationFileWithVersion(translationDir, resourceName, packageName); err != nil { + return fmt.Errorf("failed to generate translation file: %w", err) + } + + // Generate service file + if err := generateServiceFileWithVersion(translationDir, resourceName, packageName, openAPIConfig); err != nil { + return fmt.Errorf("failed to generate service file: %w", err) + } + + return nil +} + +func generateTranslationFileWithVersion(dir, resourceName, packageName string) error { + f := jen.NewFile(packageName) + AddLicenseHeader(f) + + // Atlas resource struct + f.Type().Id("Atlas" + resourceName).Struct( + jen.Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName+"Spec"), + ) + + // ConvertFrom method + f.Func().Params(jen.Id("a").Op("*").Id("Atlas"+resourceName)).Id("ConvertFrom").Params( + jen.Id("k8s").Op("*").Qual("github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1", "Atlas"+resourceName), + ).Error().Block( + jen.Comment("TODO: Implement conversion from Kubernetes resource to Atlas resource"), + jen.Return(jen.Nil()), + ) + + // Compare method + f.Func().Params(jen.Id("a").Op("*").Id("Atlas"+resourceName)).Id("Compare").Params( + jen.Id("other").Op("*").Id("Atlas"+resourceName), + ).Bool().Block( + jen.Comment("TODO: Implement field-by-field comparison"), + jen.Return(jen.True()), + ) + + fileName := filepath.Join(dir, strings.ToLower(resourceName)+".go") + return f.Save(fileName) +} + +func generateServiceFileWithVersion(dir, resourceName, packageName string, openAPIConfig OpenAPIConfig) error { + atlasAPI, err := GetAtlasAPIForCRD(resourceName) + if err != nil { + return fmt.Errorf("failed to get Atlas API for CRD %s: %w", resourceName, err) + } + + f := jen.NewFile(packageName) + AddLicenseHeader(f) + + // Atlas SDK import for this version + f.ImportAlias(openAPIConfig.Package, "admin") + + // Service interface + f.Type().Id("Atlas"+resourceName+"Service").Interface( + jen.Id("Get").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + jen.Id("resourceID").String(), + ).Params(jen.Op("*").Id("Atlas"+resourceName), jen.Error()), + jen.Id("List").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + ).Params(jen.Index().Op("*").Id("Atlas"+resourceName), jen.Error()), + jen.Id("Update").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + jen.Id("resourceID").String(), + jen.Id("a"+strings.ToLower(resourceName)).Op("*").Id("Atlas"+resourceName), + ).Params(jen.Op("*").Id("Atlas"+resourceName), jen.Error()), + jen.Id("Delete").Params( + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + jen.Id("resourceID").String(), + ).Error(), + ) + + // Service implementation struct + f.Type().Id("Atlas"+resourceName+"ServiceImpl").Struct( + jen.Id(strings.ToLower(resourceName)+"API").Qual(openAPIConfig.Package, atlasAPI), + ) + + // Constructor + f.Func().Id("NewAtlas"+resourceName+"Service").Params( + jen.Id("api").Qual(openAPIConfig.Package, atlasAPI), + ).Id("Atlas"+resourceName+"Service").Block( + jen.Return(jen.Op("&").Id("Atlas"+resourceName+"ServiceImpl").Values(jen.Dict{ + jen.Id(strings.ToLower(resourceName) + "API"): jen.Id("api"), + })), + ) + + // Method implementations + serviceVar := "s" + methods := []struct { + name string + params []jen.Code + ret []jen.Code + }{ + { + name: "Get", + params: []jen.Code{ + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + jen.Id("resourceID").String(), + }, + ret: []jen.Code{jen.Op("*").Id("Atlas" + resourceName), jen.Error()}, + }, + { + name: "List", + params: []jen.Code{ + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + }, + ret: []jen.Code{jen.Index().Op("*").Id("Atlas" + resourceName), jen.Error()}, + }, + { + name: "Update", + params: []jen.Code{ + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + jen.Id("resourceID").String(), + jen.Id("a"+strings.ToLower(resourceName)).Op("*").Id("Atlas"+resourceName), + }, + ret: []jen.Code{jen.Op("*").Id("Atlas" + resourceName), jen.Error()}, + }, + { + name: "Delete", + params: []jen.Code{ + jen.Id("ctx").Qual("context", "Context"), + jen.Id("orgID").String(), + jen.Id("resourceID").String(), + }, + ret: []jen.Code{jen.Error()}, + }, + } + + for _, method := range methods { + f.Func().Params(jen.Id(serviceVar).Op("*").Id("Atlas"+resourceName+"ServiceImpl")).Id(method.name).Params(method.params...).Params(method.ret...).Block( + jen.Comment("TODO: Implement Atlas API call"), + jen.Return(jen.Nil().Op(",").Qual("fmt", "Errorf").Call(jen.Lit("not implemented"))), + ) + } + + fileName := filepath.Join(dir, "service.go") + return f.Save(fileName) +} + +// GetAtlasAPIForCRD maps CRD kinds to their corresponding Atlas API types +func GetAtlasAPIForCRD(crdKind string) (string, error) { + apiMapping := map[string]string{ + "Project": "ProjectsApi", + "Group": "ProjectsApi", // Groups are managed by ProjectsApi + "Organization": "OrganizationsApi", + "DatabaseUser": "DatabaseUsersApi", + "Deployment": "ClustersApi", + "StreamInstance": "StreamsApi", + "PrivateEndpoint": "PrivateEndpointServicesApi", + "NetworkPeering": "NetworkPeeringApi", + "NetworkContainer": "NetworkPeeringApi", + "IPAccessList": "ProjectIPAccessListApi", + "CustomRole": "CustomDatabaseRolesApi", + "BackupCompliancePolicy": "CompliancePoliciesApi", + "DataFederation": "DataFederationApi", + "ThirdPartyIntegrations": "ThirdPartyIntegrationsApi", + "FederatedAuth": "FederatedAuthenticationApi", + "SearchIndexConfig": "AtlasSearchApi", + "OrgSettings": "OrganizationsApi", + "StreamConnection": "StreamsApi", + "Team": "TeamsApi", + } + + if api, exists := apiMapping[crdKind]; exists { + return api, nil + } + return "", fmt.Errorf("no Atlas API mapping found for CRD kind '%s'", crdKind) +} + +// AddLicenseHeader adds the standard license header to generated files +func AddLicenseHeader(f *jen.File) { + f.HeaderComment("Copyright 2025 MongoDB Inc") + f.HeaderComment("") + f.HeaderComment("Licensed under the Apache License, Version 2.0 (the \"License\");") + f.HeaderComment("you may not use this file except in compliance with the License.") + f.HeaderComment("You may obtain a copy of the License at") + f.HeaderComment("") + f.HeaderComment("\thttp://www.apache.org/licenses/LICENSE-2.0") + f.HeaderComment("") + f.HeaderComment("Unless required by applicable law or agreed to in writing, software") + f.HeaderComment("distributed under the License is distributed on an \"AS IS\" BASIS,") + f.HeaderComment("WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.") + f.HeaderComment("See the License for the specific language governing permissions and") + f.HeaderComment("limitations under the License.") +} \ No newline at end of file