bootstrap.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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. // Update ExternalSecret GeneratorRef validation
  63. if err := updateExternalSecretGeneratorRef(rootDir, cfg); err != nil {
  64. return fmt.Errorf("failed to update ExternalSecret GeneratorRef: %w", err)
  65. }
  66. return nil
  67. }
  68. func createGeneratorCRD(rootDir string, cfg Config) error {
  69. crdDir := filepath.Join(rootDir, "apis", "generators", "v1alpha1")
  70. crdFile := filepath.Join(crdDir, fmt.Sprintf("types_%s.go", cfg.PackageName))
  71. // Check if file already exists
  72. if _, err := os.Stat(crdFile); err == nil {
  73. return fmt.Errorf("CRD file already exists: %s", crdFile)
  74. }
  75. tmplContent, err := templates.ReadFile("templates/crd.go.tmpl")
  76. if err != nil {
  77. return fmt.Errorf("failed to read template: %w", err)
  78. }
  79. tmpl := template.Must(template.New("crd").Parse(string(tmplContent)))
  80. f, err := os.Create(filepath.Clean(crdFile))
  81. if err != nil {
  82. return err
  83. }
  84. defer func() { _ = f.Close() }()
  85. if err := tmpl.Execute(f, cfg); err != nil {
  86. return err
  87. }
  88. fmt.Printf("✓ Created CRD: %s\n", crdFile)
  89. return nil
  90. }
  91. func createGeneratorImplementation(rootDir string, cfg Config) error {
  92. genDir := filepath.Join(rootDir, "generators", "v1", cfg.PackageName)
  93. if err := os.MkdirAll(genDir, 0o750); err != nil {
  94. return err
  95. }
  96. // Create main generator file
  97. genFile := filepath.Join(genDir, fmt.Sprintf("%s.go", cfg.PackageName))
  98. if _, err := os.Stat(genFile); err == nil {
  99. return fmt.Errorf("implementation file already exists: %s", genFile)
  100. }
  101. if err := createFromTemplate("templates/implementation.go.tmpl", genFile, cfg); err != nil {
  102. return err
  103. }
  104. fmt.Printf("✓ Created implementation: %s\n", genFile)
  105. // Create test file
  106. testFile := filepath.Join(genDir, fmt.Sprintf("%s_test.go", cfg.PackageName))
  107. if err := createFromTemplate("templates/test.go.tmpl", testFile, cfg); err != nil {
  108. return err
  109. }
  110. fmt.Printf("✓ Created test file: %s\n", testFile)
  111. // Create go.mod
  112. goModFile := filepath.Join(genDir, "go.mod")
  113. if err := createFromTemplate("templates/go.mod.tmpl", goModFile, cfg); err != nil {
  114. return err
  115. }
  116. fmt.Printf("✓ Created go.mod: %s\n", goModFile)
  117. // Create empty go.sum
  118. goSumFile := filepath.Join(genDir, "go.sum")
  119. if err := os.WriteFile(goSumFile, []byte(""), 0o600); err != nil {
  120. return err
  121. }
  122. fmt.Printf("✓ Created go.sum: %s\n", goSumFile)
  123. return nil
  124. }
  125. func createFromTemplate(tmplPath, outputFile string, cfg Config) error {
  126. tmplContent, err := templates.ReadFile(tmplPath)
  127. if err != nil {
  128. return fmt.Errorf("failed to read template %s: %w", tmplPath, err)
  129. }
  130. tmpl := template.Must(template.New(filepath.Base(tmplPath)).Parse(string(tmplContent)))
  131. f, err := os.Create(filepath.Clean(outputFile))
  132. if err != nil {
  133. return err
  134. }
  135. defer func() { _ = f.Close() }()
  136. return tmpl.Execute(f, cfg)
  137. }
  138. func updateRegisterFile(rootDir string, cfg Config) error {
  139. registerFile := filepath.Join(rootDir, "pkg", "register", "generators.go")
  140. data, err := os.ReadFile(filepath.Clean(registerFile))
  141. if err != nil {
  142. return err
  143. }
  144. content := string(data)
  145. // Check if already registered
  146. if strings.Contains(content, fmt.Sprintf("%q", cfg.PackageName)) {
  147. fmt.Printf("⚠ Generator already registered in %s\n", registerFile)
  148. return nil
  149. }
  150. // Add import
  151. importLine := fmt.Sprintf("\t%s \"github.com/external-secrets/external-secrets/generators/v1/%s\"",
  152. cfg.PackageName, cfg.PackageName)
  153. // Find the last import before the closing parenthesis
  154. lines := strings.Split(content, "\n")
  155. newLines := make([]string, 0, len(lines)+2)
  156. importAdded := false
  157. registerAdded := false
  158. for i, line := range lines {
  159. newLines = append(newLines, line)
  160. // Add import after the last generator import
  161. if !importAdded && strings.Contains(line, "\"github.com/external-secrets/external-secrets/generators/v1/") {
  162. // Look ahead to see if next line is still an import or closing paren
  163. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
  164. newLines = append(newLines, importLine)
  165. importAdded = true
  166. }
  167. }
  168. // Add register call after the last Register call
  169. if !registerAdded && strings.Contains(line, "genv1alpha1.Register(") {
  170. // Look ahead to see if next line is closing brace
  171. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "}" {
  172. registerLine := fmt.Sprintf("\tgenv1alpha1.Register(%s.Kind(), %s.NewGenerator())",
  173. cfg.PackageName, cfg.PackageName)
  174. newLines = append(newLines, registerLine)
  175. registerAdded = true
  176. }
  177. }
  178. }
  179. if !importAdded || !registerAdded {
  180. return fmt.Errorf("failed to add import or register call to %s", registerFile)
  181. }
  182. if err := os.WriteFile(filepath.Clean(registerFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  183. return err
  184. }
  185. fmt.Printf("✓ Updated register file: %s\n", registerFile)
  186. return nil
  187. }
  188. func updateTypesClusterFile(rootDir string, cfg Config) error {
  189. typesClusterFile := filepath.Join(rootDir, "apis", "generators", "v1alpha1", "types_cluster.go")
  190. data, err := os.ReadFile(filepath.Clean(typesClusterFile))
  191. if err != nil {
  192. return err
  193. }
  194. content := string(data)
  195. // Check if already exists
  196. if strings.Contains(content, cfg.GeneratorKind) {
  197. fmt.Printf("⚠ Generator kind already exists in types_cluster.go\n")
  198. return nil
  199. }
  200. lines := strings.Split(content, "\n")
  201. newLines := make([]string, 0, len(lines)+2)
  202. enumAdded := false
  203. constAdded := false
  204. specAdded := false
  205. for i, line := range lines {
  206. // Update the enum validation annotation
  207. if !enumAdded && strings.Contains(line, "+kubebuilder:validation:Enum=") {
  208. // Add the new generator to the enum list
  209. line = strings.TrimRight(line, "\n")
  210. if strings.HasSuffix(line, "Grafana") {
  211. line = line + ";" + cfg.GeneratorName
  212. }
  213. enumAdded = true
  214. }
  215. newLines = append(newLines, line)
  216. // Add const after the last GeneratorKind const
  217. if !constAdded && strings.Contains(line, "GeneratorKind") && strings.Contains(line, "GeneratorKind = \"") {
  218. // Look ahead to check if next line is closing paren
  219. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
  220. constLine := fmt.Sprintf("\t// %s represents a %s generator.",
  221. cfg.GeneratorKind, strings.ToLower(cfg.GeneratorName))
  222. newLines = append(newLines, constLine)
  223. constValueLine := fmt.Sprintf("\t%s GeneratorKind = %q",
  224. cfg.GeneratorKind, cfg.GeneratorName)
  225. newLines = append(newLines, constValueLine)
  226. constAdded = true
  227. }
  228. }
  229. // Add spec field to GeneratorSpec struct
  230. if !specAdded && strings.Contains(line, "Spec") && strings.Contains(line, "`json:") && strings.Contains(line, "omitempty") {
  231. // Look ahead to check if next line is closing brace of the struct
  232. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "}" {
  233. // Add the new spec field
  234. jsonTag := strings.ToLower(cfg.GeneratorName) + "Spec"
  235. specLine := fmt.Sprintf("\t%sSpec *%sSpec `json:\"%s,omitempty\"`",
  236. cfg.GeneratorName, cfg.GeneratorName, jsonTag)
  237. newLines = append(newLines, specLine)
  238. specAdded = true
  239. }
  240. }
  241. }
  242. if !enumAdded || !constAdded || !specAdded {
  243. fmt.Printf("⚠ Warning: Could not fully update types_cluster.go. Please manually add:\n")
  244. if !enumAdded {
  245. fmt.Printf(" 1. Add '%s' to the kubebuilder:validation:Enum annotation\n", cfg.GeneratorName)
  246. }
  247. if !constAdded {
  248. fmt.Printf(" 2. Add the const: %s GeneratorKind = \"%s\"\n", cfg.GeneratorKind, cfg.GeneratorName)
  249. }
  250. if !specAdded {
  251. fmt.Printf(" 3. Add to GeneratorSpec struct: %sSpec *%sSpec `json:\"%sSpec,omitempty\"`\n",
  252. cfg.GeneratorName, cfg.GeneratorName, strings.ToLower(cfg.GeneratorName))
  253. }
  254. } else {
  255. if err := os.WriteFile(filepath.Clean(typesClusterFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  256. return err
  257. }
  258. fmt.Printf("✓ Updated types_cluster.go\n")
  259. }
  260. return nil
  261. }
  262. func updateMainGoMod(rootDir string, cfg Config) error {
  263. goModFile := filepath.Join(rootDir, "go.mod")
  264. data, err := os.ReadFile(filepath.Clean(goModFile))
  265. if err != nil {
  266. return err
  267. }
  268. content := string(data)
  269. replaceLine := fmt.Sprintf("\tgithub.com/external-secrets/external-secrets/generators/v1/%s => ./generators/v1/%s",
  270. cfg.PackageName, cfg.PackageName)
  271. // Check if already exists
  272. if strings.Contains(content, replaceLine) {
  273. fmt.Printf("⚠ Replace directive already exists in go.mod\n")
  274. return nil
  275. }
  276. lines := strings.Split(content, "\n")
  277. newLines := make([]string, 0, len(lines)+1)
  278. added := false
  279. lastGeneratorIdx := -1
  280. // First pass: find where to insert
  281. for i, line := range lines {
  282. if strings.Contains(line, "github.com/external-secrets/external-secrets/generators/v1/") {
  283. lastGeneratorIdx = i
  284. // Extract the package name from the current line
  285. currentPkg := extractGeneratorPackage(line)
  286. if currentPkg != "" && cfg.PackageName < currentPkg && !added {
  287. // Insert before this line (alphabetically)
  288. newLines = append(newLines, replaceLine)
  289. added = true
  290. }
  291. }
  292. newLines = append(newLines, line)
  293. // If this was the last generator and we haven't added yet, add after it
  294. if i == lastGeneratorIdx && !added && lastGeneratorIdx != -1 {
  295. // Check if next line is NOT a generator (meaning this is the last one)
  296. if i+1 >= len(lines) || !strings.Contains(lines[i+1], "github.com/external-secrets/external-secrets/generators/v1/") {
  297. newLines = append(newLines, replaceLine)
  298. added = true
  299. }
  300. }
  301. }
  302. // This shouldn't happen in practice but handle it
  303. if !added {
  304. return fmt.Errorf("could not find appropriate position to insert replace directive")
  305. }
  306. if err := os.WriteFile(filepath.Clean(goModFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  307. return err
  308. }
  309. fmt.Printf("✓ Updated main go.mod\n")
  310. return nil
  311. }
  312. func extractGeneratorPackage(line string) string {
  313. if !strings.Contains(line, "github.com/external-secrets/external-secrets/generators/v1/") {
  314. return ""
  315. }
  316. // Extract package name from line like:
  317. // "\tgithub.com/external-secrets/external-secrets/generators/v1/uuid => ./generators/v1/uuid"
  318. parts := strings.Split(line, "/")
  319. if len(parts) == 0 {
  320. return ""
  321. }
  322. lastPart := parts[len(parts)-1]
  323. // Remove everything after space (the => part)
  324. if idx := strings.Index(lastPart, " "); idx != -1 {
  325. lastPart = lastPart[:idx]
  326. }
  327. return strings.TrimSpace(lastPart)
  328. }
  329. func updateResolverFile(rootDir string, cfg Config) error {
  330. resolverFile := filepath.Join(rootDir, "runtime", "esutils", "resolvers", "generator.go")
  331. data, err := os.ReadFile(filepath.Clean(resolverFile))
  332. if err != nil {
  333. return err
  334. }
  335. content := string(data)
  336. // Check if already exists
  337. if strings.Contains(content, fmt.Sprintf("GeneratorKind%s", cfg.GeneratorName)) {
  338. fmt.Printf("⚠ Generator already exists in resolver file\n")
  339. return nil
  340. }
  341. // Create the case statement to add
  342. caseBlock := fmt.Sprintf(` case genv1alpha1.GeneratorKind%s:
  343. if gen.Spec.Generator.%sSpec == nil {
  344. return nil, fmt.Errorf("when kind is %%s, %sSpec must be set", gen.Spec.Kind)
  345. }
  346. return &genv1alpha1.%s{
  347. TypeMeta: metav1.TypeMeta{
  348. APIVersion: genv1alpha1.SchemeGroupVersion.String(),
  349. Kind: genv1alpha1.%sKind,
  350. },
  351. Spec: *gen.Spec.Generator.%sSpec,
  352. }, nil`,
  353. cfg.GeneratorName, cfg.GeneratorName, cfg.GeneratorName,
  354. cfg.GeneratorName, cfg.GeneratorName, cfg.GeneratorName)
  355. lines := strings.Split(content, "\n")
  356. newLines := make([]string, 0, len(lines)+10)
  357. added := false
  358. for _, line := range lines {
  359. // Find the default case and add before it
  360. if !added && strings.TrimSpace(line) == "default:" {
  361. newLines = append(newLines, caseBlock)
  362. added = true
  363. }
  364. newLines = append(newLines, line)
  365. }
  366. if !added {
  367. return fmt.Errorf("could not find default case in resolver file")
  368. }
  369. if err := os.WriteFile(filepath.Clean(resolverFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  370. return err
  371. }
  372. fmt.Printf("✓ Updated resolver file: %s\n", resolverFile)
  373. return nil
  374. }
  375. func updateRegisterKindFile(rootDir string, cfg Config) error {
  376. registerFile := filepath.Join(rootDir, "apis", "generators", "v1alpha1", "register.go")
  377. data, err := os.ReadFile(filepath.Clean(registerFile))
  378. if err != nil {
  379. return err
  380. }
  381. content := string(data)
  382. // Check if already exists
  383. if strings.Contains(content, fmt.Sprintf("%sKind", cfg.GeneratorName)) {
  384. fmt.Printf("⚠ Generator kind already exists in register.go\n")
  385. return nil
  386. }
  387. lines := strings.Split(content, "\n")
  388. newLines := make([]string, 0, len(lines)+4)
  389. kindAdded := false
  390. schemeAdded := false
  391. for i, line := range lines {
  392. newLines = append(newLines, line)
  393. // Add Kind constant before closing paren of var block
  394. if !kindAdded && strings.Contains(line, "Kind = reflect.TypeOf(") && strings.Contains(line, "{}).Name()") {
  395. // Look ahead to see if next line is closing paren
  396. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == ")" {
  397. // Add blank line and then the new kind
  398. newLines = append(newLines, "")
  399. kindComment := fmt.Sprintf("\t// %sKind is the kind name for %s resource.", cfg.GeneratorName, cfg.GeneratorName)
  400. newLines = append(newLines, kindComment)
  401. kindLine := fmt.Sprintf("\t%sKind = reflect.TypeOf(%s{}).Name()", cfg.GeneratorName, cfg.GeneratorName)
  402. newLines = append(newLines, kindLine)
  403. kindAdded = true
  404. }
  405. }
  406. // Add SchemeBuilder.Register call before closing brace of init function
  407. if !schemeAdded && strings.Contains(line, "SchemeBuilder.Register(&") && strings.Contains(line, "List{})") {
  408. // Look ahead to see if next line is closing brace
  409. if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "}" {
  410. registerLine := fmt.Sprintf("\tSchemeBuilder.Register(&%s{}, &%sList{})", cfg.GeneratorName, cfg.GeneratorName)
  411. newLines = append(newLines, registerLine)
  412. schemeAdded = true
  413. }
  414. }
  415. }
  416. if !kindAdded || !schemeAdded {
  417. fmt.Printf("⚠ Warning: Could not fully update register.go. Please manually add:\n")
  418. if !kindAdded {
  419. fmt.Printf(" 1. Add Kind constant: %sKind = reflect.TypeOf(%s{}).Name()\n", cfg.GeneratorName, cfg.GeneratorName)
  420. }
  421. if !schemeAdded {
  422. fmt.Printf(" 2. Add SchemeBuilder registration: SchemeBuilder.Register(&%s{}, &%sList{})\n", cfg.GeneratorName, cfg.GeneratorName)
  423. }
  424. } else {
  425. if err := os.WriteFile(filepath.Clean(registerFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  426. return err
  427. }
  428. fmt.Printf("✓ Updated register.go\n")
  429. }
  430. return nil
  431. }
  432. func updateExternalSecretGeneratorRef(rootDir string, cfg Config) error {
  433. // Update v1 ExternalSecret types
  434. externalSecretFile := filepath.Join(rootDir, "apis", "externalsecrets", "v1", "externalsecret_types.go")
  435. data, err := os.ReadFile(filepath.Clean(externalSecretFile))
  436. if err != nil {
  437. return fmt.Errorf("failed to read v1 externalsecret_types.go: %w", err)
  438. }
  439. content := string(data)
  440. // Check if already exists
  441. if strings.Contains(content, ";"+cfg.GeneratorName) {
  442. fmt.Printf("⚠ Generator %s already exists in v1 GeneratorRef enum\n", cfg.GeneratorName)
  443. return nil
  444. }
  445. lines := strings.Split(content, "\n")
  446. newLines := make([]string, 0, len(lines))
  447. updated := false
  448. for _, line := range lines {
  449. // Look for the GeneratorRef Kind enum validation line
  450. // Pattern: // +kubebuilder:validation:Enum=ACRAccessToken;ClusterGenerator;...
  451. if !updated && strings.Contains(line, "+kubebuilder:validation:Enum=") &&
  452. strings.Contains(line, "ACRAccessToken") &&
  453. strings.Contains(line, "ClusterGenerator") {
  454. // This is the enum line we need to update
  455. // Add the new generator to the end of the enum list
  456. line = strings.TrimRight(line, "\n")
  457. line = line + ";" + cfg.GeneratorName
  458. updated = true
  459. }
  460. newLines = append(newLines, line)
  461. }
  462. if !updated {
  463. fmt.Printf("⚠ Warning: Could not find GeneratorRef Kind enum in v1. Please manually add '%s' to the enum.\n",
  464. cfg.GeneratorName)
  465. return nil
  466. }
  467. if err := os.WriteFile(filepath.Clean(externalSecretFile), []byte(strings.Join(newLines, "\n")), 0o600); err != nil {
  468. return fmt.Errorf("failed to write v1 externalsecret_types.go: %w", err)
  469. }
  470. fmt.Printf("✓ Updated v1 ExternalSecret GeneratorRef enum\n")
  471. return nil
  472. }