| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- // Package main provides a tool to generate provider main.go files from provider configuration.
- package main
- import (
- "bytes"
- "embed"
- "encoding/json"
- "flag"
- "fmt"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "text/template"
- "github.com/xeipuuv/gojsonschema"
- "gopkg.in/yaml.v3"
- )
- //go:embed schema/provider-config.schema.json templates/*.tmpl
- var embeddedFS embed.FS
- type providerMetadata struct {
- Name string `yaml:"name" json:"name"`
- DisplayName string `yaml:"displayName" json:"displayName"`
- V2Package string `yaml:"v2Package" json:"v2Package"`
- }
- type gvkConfig struct {
- Group string `yaml:"group" json:"group"`
- Version string `yaml:"version" json:"version"`
- Kind string `yaml:"kind" json:"kind"`
- }
- type storeConfig struct {
- GVK gvkConfig `yaml:"gvk" json:"gvk"`
- V1Provider string `yaml:"v1Provider" json:"v1Provider"`
- V1ProviderFunc string `yaml:"v1ProviderFunc" json:"v1ProviderFunc"`
- }
- type generatorConfig struct {
- GVK gvkConfig `yaml:"gvk" json:"gvk"`
- V1Generator string `yaml:"v1Generator" json:"v1Generator"`
- V1GeneratorFunc string `yaml:"v1GeneratorFunc" json:"v1GeneratorFunc"`
- }
- // ProviderConfig represents the structure of provider.yaml.
- type ProviderConfig struct {
- Provider providerMetadata `yaml:"provider" json:"provider"`
- Stores []storeConfig `yaml:"stores" json:"stores"`
- Generators []generatorConfig `yaml:"generators" json:"generators"`
- ConfigPackage string `yaml:"configPackage" json:"configPackage"`
- }
- // ImportInfo tracks package imports with aliases.
- type ImportInfo struct {
- Path string
- Alias string
- }
- // TemplateData contains all data needed for template rendering.
- type TemplateData struct {
- Provider ProviderConfig
- HasStores bool
- HasGenerators bool
- UniqueStoreImports []ImportInfo
- UniqueGeneratorImports []ImportInfo
- Stores []StoreInfo
- Generators []GeneratorInfo
- }
- type StoreInfo struct {
- GVK struct {
- Group string
- Version string
- Kind string
- }
- V1Provider string
- V1ProviderFunc string
- ImportAlias string
- }
- type GeneratorInfo struct {
- GVK struct {
- Group string
- Version string
- Kind string
- }
- V1Generator string
- V1GeneratorFunc string
- ImportAlias string
- }
- var (
- providersDir = flag.String("providers-dir", "providers/v2", "Base directory for v2 providers")
- dryRun = flag.Bool("dry-run", false, "Print what would be generated without writing files")
- verbose = flag.Bool("verbose", false, "Enable verbose output")
- )
- func main() {
- flag.Parse()
- log.SetFlags(0)
- // Load the JSON schema
- schemaBytes, err := embeddedFS.ReadFile("schema/provider-config.schema.json")
- if err != nil {
- log.Fatalf("Failed to read schema: %v", err)
- }
- schemaLoader := gojsonschema.NewBytesLoader(schemaBytes)
- // Find all provider.yaml files
- providerConfigs, err := findProviderConfigs(*providersDir)
- if err != nil {
- log.Fatalf("Failed to find provider configs: %v", err)
- }
- if len(providerConfigs) == 0 {
- log.Printf("No provider.yaml files found in %s", *providersDir)
- return
- }
- log.Printf("Found %d provider configuration(s)", len(providerConfigs))
- // Load templates
- mainTemplate, err := loadTemplate("templates/main.go.tmpl")
- if err != nil {
- log.Fatalf("Failed to load main.go template: %v", err)
- }
- dockerfileTemplate, err := loadTemplate("templates/Dockerfile.tmpl")
- if err != nil {
- log.Fatalf("Failed to load Dockerfile template: %v", err)
- }
- // Process each provider
- var generatedCount int
- for _, configPath := range providerConfigs {
- providerDir := filepath.Dir(configPath)
- log.Printf("\nProcessing: %s", configPath)
- // Load and validate config
- config, err := loadAndValidateConfig(configPath, schemaLoader)
- if err != nil {
- log.Fatalf("Failed to load/validate config %s: %v", configPath, err)
- }
- if *verbose {
- log.Printf(" Provider: %s (%s)", config.Provider.Name, config.Provider.DisplayName)
- log.Printf(" Stores: %d, Generators: %d", len(config.Stores), len(config.Generators))
- }
- // Prepare template data
- templateData := prepareTemplateData(config)
- // Generate main.go
- mainContent, err := executeTemplate(mainTemplate, templateData)
- if err != nil {
- log.Fatalf("Failed to generate main.go for %s: %v", config.Provider.Name, err)
- }
- // Format with goimports/gofmt
- formattedMain, err := formatGoCode(mainContent)
- if err != nil {
- log.Printf("Warning: Failed to format main.go for %s: %v", config.Provider.Name, err)
- formattedMain = mainContent // Use unformatted if formatting fails
- }
- mainPath := filepath.Join(providerDir, "main.go")
- if *dryRun {
- log.Printf(" Would write: %s (%d bytes)", mainPath, len(formattedMain))
- } else {
- if err := os.WriteFile(mainPath, formattedMain, 0600); err != nil {
- log.Fatalf("Failed to write main.go: %v", err)
- }
- log.Printf(" ✓ Generated: main.go")
- }
- // Generate Dockerfile
- dockerContent, err := executeTemplate(dockerfileTemplate, templateData)
- if err != nil {
- log.Fatalf("Failed to generate Dockerfile for %s: %v", config.Provider.Name, err)
- }
- dockerPath := filepath.Join(providerDir, "Dockerfile")
- if *dryRun {
- log.Printf(" Would write: %s (%d bytes)", dockerPath, len(dockerContent))
- } else {
- if err := os.WriteFile(dockerPath, dockerContent, 0600); err != nil {
- log.Fatalf("Failed to write Dockerfile: %v", err)
- }
- log.Printf(" ✓ Generated: Dockerfile")
- }
- generatedCount++
- }
- log.Printf("\n✓ Successfully generated files for %d provider(s)", generatedCount)
- }
- func findProviderConfigs(baseDir string) ([]string, error) {
- var configs []string
- err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if !info.IsDir() && info.Name() == "provider.yaml" {
- configs = append(configs, path)
- }
- return nil
- })
- return configs, err
- }
- func loadAndValidateConfig(configPath string, schemaLoader gojsonschema.JSONLoader) (*ProviderConfig, error) {
- // Read YAML file
- //nolint:gosec // configPath comes from controlled provider config discovery under providers-dir.
- yamlBytes, err := os.ReadFile(configPath)
- if err != nil {
- return nil, fmt.Errorf("read file: %w", err)
- }
- // Parse YAML into config struct
- var config ProviderConfig
- if err := yaml.Unmarshal(yamlBytes, &config); err != nil {
- return nil, fmt.Errorf("parse YAML: %w", err)
- }
- // Convert to JSON for schema validation
- jsonBytes, err := json.Marshal(config)
- if err != nil {
- return nil, fmt.Errorf("convert to JSON: %w", err)
- }
- // Validate against schema
- documentLoader := gojsonschema.NewBytesLoader(jsonBytes)
- result, err := gojsonschema.Validate(schemaLoader, documentLoader)
- if err != nil {
- return nil, fmt.Errorf("validate schema: %w", err)
- }
- if !result.Valid() {
- var errMsgs []string
- for _, desc := range result.Errors() {
- errMsgs = append(errMsgs, fmt.Sprintf(" - %s", desc))
- }
- return nil, fmt.Errorf("schema validation failed:\n%s", strings.Join(errMsgs, "\n"))
- }
- return &config, nil
- }
- func loadTemplate(name string) (*template.Template, error) {
- content, err := embeddedFS.ReadFile(name)
- if err != nil {
- return nil, err
- }
- tmpl, err := template.New(name).Parse(string(content))
- if err != nil {
- return nil, err
- }
- return tmpl, nil
- }
- func prepareTemplateData(config *ProviderConfig) *TemplateData {
- data := &TemplateData{
- Provider: *config,
- HasStores: len(config.Stores) > 0,
- HasGenerators: len(config.Generators) > 0,
- }
- // Collect unique imports for stores
- storeImports := make(map[string]string) // path -> alias
- seenStoreImports := make(map[string]int)
- for _, store := range config.Stores {
- alias := generateImportAlias(store.V1Provider, seenStoreImports)
- storeImports[store.V1Provider] = alias
- storeInfo := StoreInfo{
- V1Provider: store.V1Provider,
- V1ProviderFunc: store.V1ProviderFunc,
- ImportAlias: alias,
- }
- storeInfo.GVK.Group = store.GVK.Group
- storeInfo.GVK.Version = store.GVK.Version
- storeInfo.GVK.Kind = store.GVK.Kind
- data.Stores = append(data.Stores, storeInfo)
- }
- for path, alias := range storeImports {
- data.UniqueStoreImports = append(data.UniqueStoreImports, ImportInfo{
- Path: path,
- Alias: alias,
- })
- }
- // Collect unique imports for generators
- generatorImports := make(map[string]string) // path -> alias
- seenGenImports := make(map[string]int)
- for _, gen := range config.Generators {
- alias := generateImportAlias(gen.V1Generator, seenGenImports)
- generatorImports[gen.V1Generator] = alias
- genInfo := GeneratorInfo{
- V1Generator: gen.V1Generator,
- V1GeneratorFunc: gen.V1GeneratorFunc,
- ImportAlias: alias,
- }
- genInfo.GVK.Group = gen.GVK.Group
- genInfo.GVK.Version = gen.GVK.Version
- genInfo.GVK.Kind = gen.GVK.Kind
- data.Generators = append(data.Generators, genInfo)
- }
- for path, alias := range generatorImports {
- data.UniqueGeneratorImports = append(data.UniqueGeneratorImports, ImportInfo{
- Path: path,
- Alias: alias,
- })
- }
- return data
- }
- func generateImportAlias(importPath string, seenImports map[string]int) string {
- // Extract the last segment of the import path
- parts := strings.Split(importPath, "/")
- lastPart := parts[len(parts)-1]
- // If this is the first time we see this import path, use the package name
- _, exists := seenImports[importPath]
- if !exists {
- seenImports[importPath] = 1
- return lastPart
- }
- // If we've seen it before, return the same alias
- return lastPart
- }
- func executeTemplate(tmpl *template.Template, data any) ([]byte, error) {
- var buf bytes.Buffer
- if err := tmpl.Execute(&buf, data); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
- }
- func formatGoCode(code []byte) ([]byte, error) {
- // Try goimports first (better formatting)
- cmd := exec.Command("goimports")
- cmd.Stdin = bytes.NewReader(code)
- var out bytes.Buffer
- var stderr bytes.Buffer
- cmd.Stdout = &out
- cmd.Stderr = &stderr
- err := cmd.Run()
- if err == nil {
- return out.Bytes(), nil
- }
- // Fallback to gofmt if goimports is not available
- cmd = exec.Command("gofmt")
- cmd.Stdin = bytes.NewReader(code)
- out.Reset()
- stderr.Reset()
- cmd.Stdout = &out
- cmd.Stderr = &stderr
- err = cmd.Run()
- if err != nil {
- return nil, fmt.Errorf("gofmt failed: %w, stderr: %s", err, stderr.String())
- }
- return out.Bytes(), nil
- }
|