generate-provider-main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. // Package main provides a tool to generate provider main.go files from provider configuration.
  13. package main
  14. import (
  15. "bytes"
  16. "embed"
  17. "encoding/json"
  18. "flag"
  19. "fmt"
  20. "log"
  21. "os"
  22. "os/exec"
  23. "path/filepath"
  24. "strings"
  25. "text/template"
  26. "github.com/xeipuuv/gojsonschema"
  27. "gopkg.in/yaml.v3"
  28. )
  29. //go:embed schema/provider-config.schema.json templates/*.tmpl
  30. var embeddedFS embed.FS
  31. // ProviderConfig represents the structure of provider.yaml.
  32. type ProviderConfig struct {
  33. Provider struct {
  34. Name string `yaml:"name" json:"name"`
  35. DisplayName string `yaml:"displayName" json:"displayName"`
  36. V2Package string `yaml:"v2Package" json:"v2Package"`
  37. } `yaml:"provider" json:"provider"`
  38. Stores []struct {
  39. GVK struct {
  40. Group string `yaml:"group" json:"group"`
  41. Version string `yaml:"version" json:"version"`
  42. Kind string `yaml:"kind" json:"kind"`
  43. } `yaml:"gvk" json:"gvk"`
  44. V1Provider string `yaml:"v1Provider" json:"v1Provider"`
  45. V1ProviderFunc string `yaml:"v1ProviderFunc" json:"v1ProviderFunc"`
  46. } `yaml:"stores" json:"stores"`
  47. Generators []struct {
  48. GVK struct {
  49. Group string `yaml:"group" json:"group"`
  50. Version string `yaml:"version" json:"version"`
  51. Kind string `yaml:"kind" json:"kind"`
  52. } `yaml:"gvk" json:"gvk"`
  53. V1Generator string `yaml:"v1Generator" json:"v1Generator"`
  54. V1GeneratorFunc string `yaml:"v1GeneratorFunc" json:"v1GeneratorFunc"`
  55. } `yaml:"generators" json:"generators"`
  56. ConfigPackage string `yaml:"configPackage" json:"configPackage"`
  57. }
  58. // ImportInfo tracks package imports with aliases.
  59. type ImportInfo struct {
  60. Path string
  61. Alias string
  62. }
  63. // TemplateData contains all data needed for template rendering.
  64. type TemplateData struct {
  65. Provider ProviderConfig
  66. HasStores bool
  67. HasGenerators bool
  68. UniqueStoreImports []ImportInfo
  69. UniqueGeneratorImports []ImportInfo
  70. Stores []StoreInfo
  71. Generators []GeneratorInfo
  72. }
  73. type StoreInfo struct {
  74. GVK struct {
  75. Group string
  76. Version string
  77. Kind string
  78. }
  79. V1Provider string
  80. V1ProviderFunc string
  81. ImportAlias string
  82. }
  83. type GeneratorInfo struct {
  84. GVK struct {
  85. Group string
  86. Version string
  87. Kind string
  88. }
  89. V1Generator string
  90. V1GeneratorFunc string
  91. ImportAlias string
  92. }
  93. var (
  94. providersDir = flag.String("providers-dir", "providers/v2", "Base directory for v2 providers")
  95. dryRun = flag.Bool("dry-run", false, "Print what would be generated without writing files")
  96. verbose = flag.Bool("verbose", false, "Enable verbose output")
  97. )
  98. func main() {
  99. flag.Parse()
  100. log.SetFlags(0)
  101. // Load the JSON schema
  102. schemaBytes, err := embeddedFS.ReadFile("schema/provider-config.schema.json")
  103. if err != nil {
  104. log.Fatalf("Failed to read schema: %v", err)
  105. }
  106. schemaLoader := gojsonschema.NewBytesLoader(schemaBytes)
  107. // Find all provider.yaml files
  108. providerConfigs, err := findProviderConfigs(*providersDir)
  109. if err != nil {
  110. log.Fatalf("Failed to find provider configs: %v", err)
  111. }
  112. if len(providerConfigs) == 0 {
  113. log.Printf("No provider.yaml files found in %s", *providersDir)
  114. return
  115. }
  116. log.Printf("Found %d provider configuration(s)", len(providerConfigs))
  117. // Load templates
  118. mainTemplate, err := loadTemplate("templates/main.go.tmpl")
  119. if err != nil {
  120. log.Fatalf("Failed to load main.go template: %v", err)
  121. }
  122. dockerfileTemplate, err := loadTemplate("templates/Dockerfile.tmpl")
  123. if err != nil {
  124. log.Fatalf("Failed to load Dockerfile template: %v", err)
  125. }
  126. // Process each provider
  127. var generatedCount int
  128. for _, configPath := range providerConfigs {
  129. providerDir := filepath.Dir(configPath)
  130. log.Printf("\nProcessing: %s", configPath)
  131. // Load and validate config
  132. config, err := loadAndValidateConfig(configPath, schemaLoader)
  133. if err != nil {
  134. log.Fatalf("Failed to load/validate config %s: %v", configPath, err)
  135. }
  136. if *verbose {
  137. log.Printf(" Provider: %s (%s)", config.Provider.Name, config.Provider.DisplayName)
  138. log.Printf(" Stores: %d, Generators: %d", len(config.Stores), len(config.Generators))
  139. }
  140. // Prepare template data
  141. templateData := prepareTemplateData(config)
  142. // Generate main.go
  143. mainContent, err := executeTemplate(mainTemplate, templateData)
  144. if err != nil {
  145. log.Fatalf("Failed to generate main.go for %s: %v", config.Provider.Name, err)
  146. }
  147. // Format with goimports/gofmt
  148. formattedMain, err := formatGoCode(mainContent)
  149. if err != nil {
  150. log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err)
  151. formattedMain = mainContent // Use unformatted if formatting fails
  152. }
  153. mainPath := filepath.Join(providerDir, "main.go")
  154. if *dryRun {
  155. log.Printf(" Would write: %s (%d bytes)", mainPath, len(formattedMain))
  156. } else {
  157. if err := os.WriteFile(mainPath, formattedMain, 0600); err != nil {
  158. log.Fatalf("Failed to write main.go: %v", err)
  159. }
  160. log.Printf(" ✓ Generated: main.go")
  161. }
  162. // Generate Dockerfile
  163. dockerContent, err := executeTemplate(dockerfileTemplate, templateData)
  164. if err != nil {
  165. log.Fatalf("Failed to generate Dockerfile for %s: %v", config.Provider.Name, err)
  166. }
  167. dockerPath := filepath.Join(providerDir, "Dockerfile")
  168. if *dryRun {
  169. log.Printf(" Would write: %s (%d bytes)", dockerPath, len(dockerContent))
  170. } else {
  171. if err := os.WriteFile(dockerPath, dockerContent, 0600); err != nil {
  172. log.Fatalf("Failed to write Dockerfile: %v", err)
  173. }
  174. log.Printf(" ✓ Generated: Dockerfile")
  175. }
  176. generatedCount++
  177. }
  178. log.Printf("\n✓ Successfully generated files for %d provider(s)", generatedCount)
  179. }
  180. func findProviderConfigs(baseDir string) ([]string, error) {
  181. var configs []string
  182. err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
  183. if err != nil {
  184. return err
  185. }
  186. if !info.IsDir() && info.Name() == "provider.yaml" {
  187. configs = append(configs, path)
  188. }
  189. return nil
  190. })
  191. return configs, err
  192. }
  193. func loadAndValidateConfig(configPath string, schemaLoader gojsonschema.JSONLoader) (*ProviderConfig, error) {
  194. // Read YAML file
  195. yamlBytes, err := os.ReadFile(configPath)
  196. if err != nil {
  197. return nil, fmt.Errorf("read file: %w", err)
  198. }
  199. // Parse YAML into config struct
  200. var config ProviderConfig
  201. if err := yaml.Unmarshal(yamlBytes, &config); err != nil {
  202. return nil, fmt.Errorf("parse YAML: %w", err)
  203. }
  204. // Convert to JSON for schema validation
  205. jsonBytes, err := json.Marshal(config)
  206. if err != nil {
  207. return nil, fmt.Errorf("convert to JSON: %w", err)
  208. }
  209. // Validate against schema
  210. documentLoader := gojsonschema.NewBytesLoader(jsonBytes)
  211. result, err := gojsonschema.Validate(schemaLoader, documentLoader)
  212. if err != nil {
  213. return nil, fmt.Errorf("validate schema: %w", err)
  214. }
  215. if !result.Valid() {
  216. var errMsgs []string
  217. for _, desc := range result.Errors() {
  218. errMsgs = append(errMsgs, fmt.Sprintf(" - %s", desc))
  219. }
  220. return nil, fmt.Errorf("schema validation failed:\n%s", strings.Join(errMsgs, "\n"))
  221. }
  222. return &config, nil
  223. }
  224. func loadTemplate(name string) (*template.Template, error) {
  225. content, err := embeddedFS.ReadFile(name)
  226. if err != nil {
  227. return nil, err
  228. }
  229. tmpl, err := template.New(name).Parse(string(content))
  230. if err != nil {
  231. return nil, err
  232. }
  233. return tmpl, nil
  234. }
  235. func prepareTemplateData(config *ProviderConfig) *TemplateData {
  236. data := &TemplateData{
  237. Provider: *config,
  238. HasStores: len(config.Stores) > 0,
  239. HasGenerators: len(config.Generators) > 0,
  240. }
  241. // Collect unique imports for stores
  242. storeImports := make(map[string]string) // path -> alias
  243. seenStoreImports := make(map[string]int)
  244. for _, store := range config.Stores {
  245. alias := generateImportAlias(store.V1Provider, seenStoreImports)
  246. storeImports[store.V1Provider] = alias
  247. storeInfo := StoreInfo{
  248. V1Provider: store.V1Provider,
  249. V1ProviderFunc: store.V1ProviderFunc,
  250. ImportAlias: alias,
  251. }
  252. storeInfo.GVK.Group = store.GVK.Group
  253. storeInfo.GVK.Version = store.GVK.Version
  254. storeInfo.GVK.Kind = store.GVK.Kind
  255. data.Stores = append(data.Stores, storeInfo)
  256. }
  257. for path, alias := range storeImports {
  258. data.UniqueStoreImports = append(data.UniqueStoreImports, ImportInfo{
  259. Path: path,
  260. Alias: alias,
  261. })
  262. }
  263. // Collect unique imports for generators
  264. generatorImports := make(map[string]string) // path -> alias
  265. seenGenImports := make(map[string]int)
  266. for _, gen := range config.Generators {
  267. alias := generateImportAlias(gen.V1Generator, seenGenImports)
  268. generatorImports[gen.V1Generator] = alias
  269. genInfo := GeneratorInfo{
  270. V1Generator: gen.V1Generator,
  271. V1GeneratorFunc: gen.V1GeneratorFunc,
  272. ImportAlias: alias,
  273. }
  274. genInfo.GVK.Group = gen.GVK.Group
  275. genInfo.GVK.Version = gen.GVK.Version
  276. genInfo.GVK.Kind = gen.GVK.Kind
  277. data.Generators = append(data.Generators, genInfo)
  278. }
  279. for path, alias := range generatorImports {
  280. data.UniqueGeneratorImports = append(data.UniqueGeneratorImports, ImportInfo{
  281. Path: path,
  282. Alias: alias,
  283. })
  284. }
  285. return data
  286. }
  287. func generateImportAlias(importPath string, seenImports map[string]int) string {
  288. // Extract the last segment of the import path
  289. parts := strings.Split(importPath, "/")
  290. lastPart := parts[len(parts)-1]
  291. // If this is the first time we see this import path, use the package name
  292. _, exists := seenImports[importPath]
  293. if !exists {
  294. seenImports[importPath] = 1
  295. return lastPart
  296. }
  297. // If we've seen it before, return the same alias
  298. return lastPart
  299. }
  300. func executeTemplate(tmpl *template.Template, data interface{}) ([]byte, error) {
  301. var buf bytes.Buffer
  302. if err := tmpl.Execute(&buf, data); err != nil {
  303. return nil, err
  304. }
  305. return buf.Bytes(), nil
  306. }
  307. func formatGoCode(code []byte) ([]byte, error) {
  308. // Try goimports first (better formatting)
  309. cmd := exec.Command("goimports")
  310. cmd.Stdin = bytes.NewReader(code)
  311. var out bytes.Buffer
  312. var stderr bytes.Buffer
  313. cmd.Stdout = &out
  314. cmd.Stderr = &stderr
  315. err := cmd.Run()
  316. if err == nil {
  317. return out.Bytes(), nil
  318. }
  319. // Fallback to gofmt if goimports is not available
  320. cmd = exec.Command("gofmt")
  321. cmd.Stdin = bytes.NewReader(code)
  322. out.Reset()
  323. stderr.Reset()
  324. cmd.Stdout = &out
  325. cmd.Stderr = &stderr
  326. err = cmd.Run()
  327. if err != nil {
  328. return nil, fmt.Errorf("gofmt failed: %w, stderr: %s", err, stderr.String())
  329. }
  330. return out.Bytes(), nil
  331. }