generate-provider-main.go 11 KB

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