bootstrap.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Package generator provides functionality for bootstrapping new generators.
  14. package generator
  15. import (
  16. "embed"
  17. "fmt"
  18. "os"
  19. "path/filepath"
  20. "strings"
  21. "text/template"
  22. )
  23. //go:embed templates/*.tmpl
  24. var templates embed.FS
  25. // Config holds the configuration for bootstrapping a generator.
  26. type Config struct {
  27. GeneratorName string
  28. PackageName string
  29. Description string
  30. GeneratorKind string
  31. }
  32. // Bootstrap creates a new generator with all necessary files.
  33. func Bootstrap(rootDir string, cfg Config) error {
  34. // Create generator CRD
  35. if err := createGeneratorCRD(rootDir, cfg); err != nil {
  36. return fmt.Errorf("failed to create CRD: %w", err)
  37. }
  38. // Create generator implementation
  39. if err := createGeneratorImplementation(rootDir, cfg); err != nil {
  40. return fmt.Errorf("failed to create implementation: %w", err)
  41. }
  42. // Update register file
  43. if err := updateRegisterFile(rootDir, cfg); err != nil {
  44. return fmt.Errorf("failed to update register file: %w", err)
  45. }
  46. // Update types_cluster.go
  47. if err := updateTypesClusterFile(rootDir, cfg); err != nil {
  48. return fmt.Errorf("failed to update types_cluster.go: %w", err)
  49. }
  50. // Update main go.mod
  51. if err := updateMainGoMod(rootDir, cfg); err != nil {
  52. return fmt.Errorf("failed to update main go.mod: %w", err)
  53. }
  54. // Update resolver file
  55. if err := updateResolverFile(rootDir, cfg); err != nil {
  56. return fmt.Errorf("failed to update resolver file: %w", err)
  57. }
  58. // Update register kind file
  59. if err := updateRegisterKindFile(rootDir, cfg); err != nil {
  60. return fmt.Errorf("failed to update register kind file: %w", err)
  61. }
  62. return nil
  63. }
  64. func createGeneratorCRD(rootDir string, cfg Config) error {
  65. crdDir := filepath.Join(rootDir, "apis", "generators", "v1alpha1")
  66. crdFile := filepath.Join(crdDir, fmt.Sprintf("types_%s.go", cfg.PackageName))
  67. // Check if file already exists
  68. if _, err := os.Stat(crdFile); err == nil {
  69. return fmt.Errorf("CRD file already exists: %s", crdFile)
  70. }
  71. tmplContent, err := templates.ReadFile("templates/crd.go.tmpl")
  72. if err != nil {
  73. return fmt.Errorf("failed to read template: %w", err)
  74. }
  75. tmpl := template.Must(template.New("crd").Parse(string(tmplContent)))
  76. f, err := os.Create(filepath.Clean(crdFile))
  77. if err != nil {
  78. return err
  79. }
  80. defer func() { _ = f.Close() }()
  81. if err := tmpl.Execute(f, cfg); err != nil {
  82. return err
  83. }
  84. fmt.Printf("✓ Created CRD: %s\n", crdFile)
  85. return nil
  86. }
  87. func createGeneratorImplementation(rootDir string, cfg Config) error {
  88. genDir := filepath.Join(rootDir, "generators", "v1", cfg.PackageName)
  89. if err := os.MkdirAll(genDir, 0o750); err != nil {
  90. return err
  91. }
  92. // Create main generator file
  93. genFile := filepath.Join(genDir, fmt.Sprintf("%s.go", cfg.PackageName))
  94. if _, err := os.Stat(genFile); err == nil {
  95. return fmt.Errorf("implementation file already exists: %s", genFile)
  96. }
  97. if err := createFromTemplate("templates/implementation.go.tmpl", genFile, cfg); err != nil {
  98. return err
  99. }
  100. fmt.Printf("✓ Created implementation: %s\n", genFile)
  101. // Create test file
  102. testFile := filepath.Join(genDir, fmt.Sprintf("%s_test.go", cfg.PackageName))
  103. if err := createFromTemplate("templates/test.go.tmpl", testFile, cfg); err != nil {
  104. return err
  105. }
  106. fmt.Printf("✓ Created test file: %s\n", testFile)
  107. // Create go.mod
  108. goModFile := filepath.Join(genDir, "go.mod")
  109. if err := createFromTemplate("templates/go.mod.tmpl", goModFile, cfg); err != nil {
  110. return err
  111. }
  112. fmt.Printf("✓ Created go.mod: %s\n", goModFile)
  113. // Create empty go.sum
  114. goSumFile := filepath.Join(genDir, "go.sum")
  115. if err := os.WriteFile(goSumFile, []byte(""), 0o600); err != nil {
  116. return err
  117. }
  118. fmt.Printf("✓ Created go.sum: %s\n", goSumFile)
  119. return nil
  120. }
  121. func createFromTemplate(tmplPath, outputFile string, cfg Config) error {
  122. tmplContent, err := templates.ReadFile(tmplPath)
  123. if err != nil {
  124. return fmt.Errorf("failed to read template %s: %w", tmplPath, err)
  125. }
  126. tmpl := template.Must(template.New(filepath.Base(tmplPath)).Parse(string(tmplContent)))
  127. f, err := os.Create(filepath.Clean(outputFile))
  128. if err != nil {
  129. return err
  130. }
  131. defer func() { _ = f.Close() }()
  132. return tmpl.Execute(f, cfg)
  133. }
  134. func updateRegisterFile(rootDir string, cfg Config) error {
  135. registerFile := filepath.Join(rootDir, "pkg", "register", "generators.go")
  136. data, err := os.ReadFile(filepath.Clean(registerFile))
  137. if err != nil {
  138. return err
  139. }
  140. content := string(data)
  141. // Check if already registered
  142. if strings.Contains(content, fmt.Sprintf("%q", cfg.PackageName)) {
  143. fmt.Printf("⚠ Generator already registered in %s\n", registerFile)
  144. return nil
  145. }
  146. // Add import
  147. importLine := fmt.Sprintf("\t%s \"github.com/external-secrets/external-secrets/generators/v1/%s\"",
  148. cfg.PackageName, cfg.PackageName)
  149. // Find the last import before the closing parenthesis
  150. lines := strings.Split(content, "\n")
  151. newLines := make([]string, 0, len(lines)+2)
  152. importAdded := false
  153. registerAdded := false
  154. for i, line := range lines {
  155. newLines = append(newLines, line)
  156. // Add import after the last generator import
  157. if !importAdded && strings.Contains(line, "\"github.com/external-secrets/external-secrets/generators/v1/") {
  158. // Look ahead to see if next line is still an import or closing paren
  159. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
  160. newLines = append(newLines, importLine)
  161. importAdded = true
  162. }
  163. }
  164. // Add register call after the last Register call
  165. if !registerAdded && strings.Contains(line, "genv1alpha1.Register(") {
  166. // Look ahead to see if next line is closing brace
  167. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "}" {
  168. registerLine := fmt.Sprintf("\tgenv1alpha1.Register(%s.Kind(), %s.NewGenerator())",
  169. cfg.PackageName, cfg.PackageName)
  170. newLines = append(newLines, registerLine)
  171. registerAdded = true
  172. }
  173. }
  174. }
  175. if !importAdded || !registerAdded {
  176. return fmt.Errorf("failed to add import or register call to %s", registerFile)
  177. }
  178. if err := os.WriteFile(filepath.Clean(registerFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  179. return err
  180. }
  181. fmt.Printf("✓ Updated register file: %s\n", registerFile)
  182. return nil
  183. }
  184. func updateTypesClusterFile(rootDir string, cfg Config) error {
  185. typesClusterFile := filepath.Join(rootDir, "apis", "generators", "v1alpha1", "types_cluster.go")
  186. data, err := os.ReadFile(filepath.Clean(typesClusterFile))
  187. if err != nil {
  188. return err
  189. }
  190. content := string(data)
  191. // Check if already exists
  192. if strings.Contains(content, cfg.GeneratorKind) {
  193. fmt.Printf("⚠ Generator kind already exists in types_cluster.go\n")
  194. return nil
  195. }
  196. lines := strings.Split(content, "\n")
  197. newLines := make([]string, 0, len(lines)+2)
  198. enumAdded := false
  199. constAdded := false
  200. specAdded := false
  201. for i, line := range lines {
  202. // Update the enum validation annotation
  203. if !enumAdded && strings.Contains(line, "+kubebuilder:validation:Enum=") {
  204. // Add the new generator to the enum list
  205. line = strings.TrimRight(line, "\n")
  206. if strings.HasSuffix(line, "Grafana") {
  207. line = line + ";" + cfg.GeneratorName
  208. }
  209. enumAdded = true
  210. }
  211. newLines = append(newLines, line)
  212. // Add const after the last GeneratorKind const
  213. if !constAdded && strings.Contains(line, "GeneratorKind") && strings.Contains(line, "GeneratorKind = \"") {
  214. // Look ahead to check if next line is closing paren
  215. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
  216. constLine := fmt.Sprintf("\t// %s represents a %s generator.",
  217. cfg.GeneratorKind, strings.ToLower(cfg.GeneratorName))
  218. newLines = append(newLines, constLine)
  219. constValueLine := fmt.Sprintf("\t%s GeneratorKind = %q",
  220. cfg.GeneratorKind, cfg.GeneratorName)
  221. newLines = append(newLines, constValueLine)
  222. constAdded = true
  223. }
  224. }
  225. // Add spec field to GeneratorSpec struct
  226. if !specAdded && strings.Contains(line, "Spec") && strings.Contains(line, "`json:") && strings.Contains(line, "omitempty") {
  227. // Look ahead to check if next line is closing brace of the struct
  228. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "}" {
  229. // Add the new spec field
  230. jsonTag := strings.ToLower(cfg.GeneratorName) + "Spec"
  231. specLine := fmt.Sprintf("\t%sSpec *%sSpec `json:\"%s,omitempty\"`",
  232. cfg.GeneratorName, cfg.GeneratorName, jsonTag)
  233. newLines = append(newLines, specLine)
  234. specAdded = true
  235. }
  236. }
  237. }
  238. if !enumAdded || !constAdded || !specAdded {
  239. fmt.Printf("⚠ Warning: Could not fully update types_cluster.go. Please manually add:\n")
  240. if !enumAdded {
  241. fmt.Printf(" 1. Add '%s' to the kubebuilder:validation:Enum annotation\n", cfg.GeneratorName)
  242. }
  243. if !constAdded {
  244. fmt.Printf(" 2. Add the const: %s GeneratorKind = \"%s\"\n", cfg.GeneratorKind, cfg.GeneratorName)
  245. }
  246. if !specAdded {
  247. fmt.Printf(" 3. Add to GeneratorSpec struct: %sSpec *%sSpec `json:\"%sSpec,omitempty\"`\n",
  248. cfg.GeneratorName, cfg.GeneratorName, strings.ToLower(cfg.GeneratorName))
  249. }
  250. } else {
  251. if err := os.WriteFile(filepath.Clean(typesClusterFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  252. return err
  253. }
  254. fmt.Printf("✓ Updated types_cluster.go\n")
  255. }
  256. return nil
  257. }
  258. func updateMainGoMod(rootDir string, cfg Config) error {
  259. goModFile := filepath.Join(rootDir, "go.mod")
  260. data, err := os.ReadFile(filepath.Clean(goModFile))
  261. if err != nil {
  262. return err
  263. }
  264. content := string(data)
  265. replaceLine := fmt.Sprintf("\tgithub.com/external-secrets/external-secrets/generators/v1/%s => ./generators/v1/%s",
  266. cfg.PackageName, cfg.PackageName)
  267. // Check if already exists
  268. if strings.Contains(content, replaceLine) {
  269. fmt.Printf("⚠ Replace directive already exists in go.mod\n")
  270. return nil
  271. }
  272. lines := strings.Split(content, "\n")
  273. newLines := make([]string, 0, len(lines)+1)
  274. added := false
  275. lastGeneratorIdx := -1
  276. // First pass: find where to insert
  277. for i, line := range lines {
  278. if strings.Contains(line, "github.com/external-secrets/external-secrets/generators/v1/") {
  279. lastGeneratorIdx = i
  280. // Extract the package name from the current line
  281. currentPkg := extractGeneratorPackage(line)
  282. if currentPkg != "" && cfg.PackageName < currentPkg && !added {
  283. // Insert before this line (alphabetically)
  284. newLines = append(newLines, replaceLine)
  285. added = true
  286. }
  287. }
  288. newLines = append(newLines, line)
  289. // If this was the last generator and we haven't added yet, add after it
  290. if i == lastGeneratorIdx && !added && lastGeneratorIdx != -1 {
  291. // Check if next line is NOT a generator (meaning this is the last one)
  292. if i+1 >= len(lines) || !strings.Contains(lines[i+1], "github.com/external-secrets/external-secrets/generators/v1/") {
  293. newLines = append(newLines, replaceLine)
  294. added = true
  295. }
  296. }
  297. }
  298. // This shouldn't happen in practice but handle it
  299. if !added {
  300. return fmt.Errorf("could not find appropriate position to insert replace directive")
  301. }
  302. if err := os.WriteFile(filepath.Clean(goModFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  303. return err
  304. }
  305. fmt.Printf("✓ Updated main go.mod\n")
  306. return nil
  307. }
  308. func extractGeneratorPackage(line string) string {
  309. if !strings.Contains(line, "github.com/external-secrets/external-secrets/generators/v1/") {
  310. return ""
  311. }
  312. // Extract package name from line like:
  313. // "\tgithub.com/external-secrets/external-secrets/generators/v1/uuid => ./generators/v1/uuid"
  314. parts := strings.Split(line, "/")
  315. if len(parts) == 0 {
  316. return ""
  317. }
  318. lastPart := parts[len(parts)-1]
  319. // Remove everything after space (the => part)
  320. if idx := strings.Index(lastPart, " "); idx != -1 {
  321. lastPart = lastPart[:idx]
  322. }
  323. return strings.TrimSpace(lastPart)
  324. }
  325. func updateResolverFile(rootDir string, cfg Config) error {
  326. resolverFile := filepath.Join(rootDir, "runtime", "esutils", "resolvers", "generator.go")
  327. data, err := os.ReadFile(filepath.Clean(resolverFile))
  328. if err != nil {
  329. return err
  330. }
  331. content := string(data)
  332. // Check if already exists
  333. if strings.Contains(content, fmt.Sprintf("GeneratorKind%s", cfg.GeneratorName)) {
  334. fmt.Printf("⚠ Generator already exists in resolver file\n")
  335. return nil
  336. }
  337. // Create the case statement to add
  338. caseBlock := fmt.Sprintf(` case genv1alpha1.GeneratorKind%s:
  339. if gen.Spec.Generator.%sSpec == nil {
  340. return nil, fmt.Errorf("when kind is %%s, %sSpec must be set", gen.Spec.Kind)
  341. }
  342. return &genv1alpha1.%s{
  343. TypeMeta: metav1.TypeMeta{
  344. APIVersion: genv1alpha1.SchemeGroupVersion.String(),
  345. Kind: genv1alpha1.%sKind,
  346. },
  347. Spec: *gen.Spec.Generator.%sSpec,
  348. }, nil`,
  349. cfg.GeneratorName, cfg.GeneratorName, cfg.GeneratorName,
  350. cfg.GeneratorName, cfg.GeneratorName, cfg.GeneratorName)
  351. lines := strings.Split(content, "\n")
  352. newLines := make([]string, 0, len(lines)+10)
  353. added := false
  354. for _, line := range lines {
  355. // Find the default case and add before it
  356. if !added && strings.TrimSpace(line) == "default:" {
  357. newLines = append(newLines, caseBlock)
  358. added = true
  359. }
  360. newLines = append(newLines, line)
  361. }
  362. if !added {
  363. return fmt.Errorf("could not find default case in resolver file")
  364. }
  365. if err := os.WriteFile(filepath.Clean(resolverFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  366. return err
  367. }
  368. fmt.Printf("✓ Updated resolver file: %s\n", resolverFile)
  369. return nil
  370. }
  371. func updateRegisterKindFile(rootDir string, cfg Config) error {
  372. registerFile := filepath.Join(rootDir, "apis", "generators", "v1alpha1", "register.go")
  373. data, err := os.ReadFile(filepath.Clean(registerFile))
  374. if err != nil {
  375. return err
  376. }
  377. content := string(data)
  378. // Check if already exists
  379. if strings.Contains(content, fmt.Sprintf("%sKind", cfg.GeneratorName)) {
  380. fmt.Printf("⚠ Generator kind already exists in register.go\n")
  381. return nil
  382. }
  383. lines := strings.Split(content, "\n")
  384. newLines := make([]string, 0, len(lines)+4)
  385. kindAdded := false
  386. schemeAdded := false
  387. for i, line := range lines {
  388. newLines = append(newLines, line)
  389. // Add Kind constant before closing paren of var block
  390. if !kindAdded && strings.Contains(line, "Kind = reflect.TypeOf(") && strings.Contains(line, "{}).Name()") {
  391. // Look ahead to see if next line is closing paren
  392. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
  393. // Add blank line and then the new kind
  394. newLines = append(newLines, "")
  395. kindComment := fmt.Sprintf("\t// %sKind is the kind name for %s resource.", cfg.GeneratorName, cfg.GeneratorName)
  396. newLines = append(newLines, kindComment)
  397. kindLine := fmt.Sprintf("\t%sKind = reflect.TypeOf(%s{}).Name()", cfg.GeneratorName, cfg.GeneratorName)
  398. newLines = append(newLines, kindLine)
  399. kindAdded = true
  400. }
  401. }
  402. // Add SchemeBuilder.Register call before closing brace of init function
  403. if !schemeAdded && strings.Contains(line, "SchemeBuilder.Register(&") && strings.Contains(line, "List{})") {
  404. // Look ahead to see if next line is closing brace
  405. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "}" {
  406. registerLine := fmt.Sprintf("\tSchemeBuilder.Register(&%s{}, &%sList{})", cfg.GeneratorName, cfg.GeneratorName)
  407. newLines = append(newLines, registerLine)
  408. schemeAdded = true
  409. }
  410. }
  411. }
  412. if !kindAdded || !schemeAdded {
  413. fmt.Printf("⚠ Warning: Could not fully update register.go. Please manually add:\n")
  414. if !kindAdded {
  415. fmt.Printf(" 1. Add Kind constant: %sKind = reflect.TypeOf(%s{}).Name()\n", cfg.GeneratorName, cfg.GeneratorName)
  416. }
  417. if !schemeAdded {
  418. fmt.Printf(" 2. Add SchemeBuilder registration: SchemeBuilder.Register(&%s{}, &%sList{})\n", cfg.GeneratorName, cfg.GeneratorName)
  419. }
  420. } else {
  421. if err := os.WriteFile(filepath.Clean(registerFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  422. return err
  423. }
  424. fmt.Printf("✓ Updated register.go\n")
  425. }
  426. return nil
  427. }