bootstrap.go 18 KB

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