mmv1/main.go (253 lines of code) (raw):
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strings"
"sync"
"time"
"golang.org/x/exp/slices"
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api"
"github.com/GoogleCloudPlatform/magic-modules/mmv1/openapi_generate"
"github.com/GoogleCloudPlatform/magic-modules/mmv1/provider"
)
var wg sync.WaitGroup
// TODO rewrite: additional flags
// Example usage: --output $GOPATH/src/github.com/terraform-providers/terraform-provider-google-beta
var outputPath = flag.String("output", "", "path to output generated files to")
// Example usage: --version beta
var version = flag.String("version", "", "optional version name. If specified, this version is preferred for resource generation when applicable")
var overrideDirectory = flag.String("overrides", "", "directory containing yaml overrides")
var product = flag.String("product", "", "optional product name. If specified, the resources under the specific product will be generated. Otherwise, resources under all products will be generated.")
var resourceToGenerate = flag.String("resource", "", "optional resource name. Limits generation to the specified resource within a particular product.")
var doNotGenerateCode = flag.Bool("no-code", false, "do not generate code")
var doNotGenerateDocs = flag.Bool("no-docs", false, "do not generate docs")
var forceProvider = flag.String("provider", "", "optional provider name. If specified, a non-default provider will be used.")
var openapiGenerate = flag.Bool("openapi-generate", false, "Generate MMv1 YAML from openapi directory (Experimental)")
// Example usage: --yaml
var yamlMode = flag.Bool("yaml", false, "copy text over from ruby yaml to go yaml")
var showImportDiffs = flag.Bool("show-import-diffs", false, "write go import diffs to stdout")
func main() {
flag.Parse()
if *openapiGenerate {
parser := openapi_generate.NewOpenapiParser("openapi_generate/openapi", "products")
parser.Run()
return
}
if outputPath == nil || *outputPath == "" {
log.Printf("No output path specified, exiting")
return
}
if version == nil || *version == "" {
log.Printf("No version specified, assuming ga")
*version = "ga"
}
var generateCode = !*doNotGenerateCode
var generateDocs = !*doNotGenerateDocs
var productsToGenerate []string
var allProducts = false
if product == nil || *product == "" {
allProducts = true
} else {
var productToGenerate = fmt.Sprintf("products/%s", *product)
productsToGenerate = []string{productToGenerate}
}
var allProductFiles []string = make([]string, 0)
files, err := filepath.Glob("products/**/product.yaml")
if err != nil {
panic(err)
}
for _, filePath := range files {
dir := filepath.Dir(filePath)
allProductFiles = append(allProductFiles, fmt.Sprintf("products/%s", filepath.Base(dir)))
}
if *overrideDirectory != "" {
log.Printf("Using override directory %s", *overrideDirectory)
// Normalize override dir to a path that is relative to the magic-modules directory
// This is needed for templates that concatenate pwd + override dir + path
if filepath.IsAbs(*overrideDirectory) {
wd, err := os.Getwd()
if err != nil {
panic(err)
}
*overrideDirectory, err = filepath.Rel(wd, *overrideDirectory)
log.Printf("Override directory normalized to relative path %s", *overrideDirectory)
}
overrideFiles, err := filepath.Glob(fmt.Sprintf("%s/products/**/product.yaml", *overrideDirectory))
if err != nil {
panic(err)
}
for _, filePath := range overrideFiles {
product, err := filepath.Rel(*overrideDirectory, filePath)
if err != nil {
panic(err)
}
dir := filepath.Dir(product)
productFile := fmt.Sprintf("products/%s", filepath.Base(dir))
if !slices.Contains(allProductFiles, productFile) {
allProductFiles = append(allProductFiles, productFile)
}
}
}
if allProducts {
productsToGenerate = allProductFiles
}
if productsToGenerate == nil || len(productsToGenerate) == 0 {
log.Fatalf("No product.yaml file found.")
}
startTime := time.Now()
providerName := "default (terraform)"
if *forceProvider != "" {
providerName = *forceProvider
}
log.Printf("Generating MM output to '%s'", *outputPath)
log.Printf("Building %s version", *version)
log.Printf("Building %s provider", providerName)
// Building compute takes a long time and can't be parallelized within the product
// so lets build it first
sort.Slice(allProductFiles, func(i int, j int) bool {
if allProductFiles[i] == "products/compute" {
return true
}
return false
})
var providerToGenerate provider.Provider
productFileChannel := make(chan string, len(allProductFiles))
productsForVersionChannel := make(chan *api.Product, len(allProductFiles))
for _, pf := range allProductFiles {
productFileChannel <- pf
}
for i := 0; i < len(allProductFiles); i++ {
wg.Add(1)
go GenerateProduct(productFileChannel, providerToGenerate, productsForVersionChannel, startTime, productsToGenerate, *resourceToGenerate, *overrideDirectory, generateCode, generateDocs)
}
wg.Wait()
close(productFileChannel)
close(productsForVersionChannel)
var productsForVersion []*api.Product
for p := range productsForVersionChannel {
productsForVersion = append(productsForVersion, p)
}
slices.SortFunc(productsForVersion, func(p1, p2 *api.Product) int {
return strings.Compare(strings.ToLower(p1.Name), strings.ToLower(p2.Name))
})
// In order to only copy/compile files once per provider this must be called outside
// of the products loop. This will get called with the provider from the final iteration
// of the loop
providerToGenerate = setProvider(*forceProvider, *version, productsForVersion[0], startTime)
providerToGenerate.CopyCommonFiles(*outputPath, generateCode, generateDocs)
if generateCode {
providerToGenerate.CompileCommonFiles(*outputPath, productsForVersion, "")
}
provider.FixImports(*outputPath, *showImportDiffs)
}
func GenerateProduct(productChannel chan string, providerToGenerate provider.Provider, productsForVersionChannel chan *api.Product, startTime time.Time, productsToGenerate []string, resourceToGenerate, overrideDirectory string, generateCode, generateDocs bool) {
defer wg.Done()
productName := <-productChannel
productYamlPath := path.Join(productName, "product.yaml")
var productOverridePath string
if overrideDirectory != "" {
productOverridePath = filepath.Join(overrideDirectory, productName, "product.yaml")
}
_, baseProductErr := os.Stat(productYamlPath)
baseProductExists := !errors.Is(baseProductErr, os.ErrNotExist)
_, overrideProductErr := os.Stat(productOverridePath)
overrideProductExists := !errors.Is(overrideProductErr, os.ErrNotExist)
if !(baseProductExists || overrideProductExists) {
log.Fatalf("%s does not contain a product.yaml file", productName)
}
productApi := &api.Product{}
if overrideProductExists {
if baseProductExists {
api.Compile(productYamlPath, productApi, overrideDirectory)
overrideApiProduct := &api.Product{}
api.Compile(productOverridePath, overrideApiProduct, overrideDirectory)
api.Merge(reflect.ValueOf(productApi), reflect.ValueOf(*overrideApiProduct))
} else {
api.Compile(productOverridePath, productApi, overrideDirectory)
}
} else {
api.Compile(productYamlPath, productApi, overrideDirectory)
}
var resources []*api.Resource = make([]*api.Resource, 0)
if !productApi.ExistsAtVersionOrLower(*version) {
log.Printf("%s does not have a '%s' version, skipping", productName, *version)
return
}
resourceFiles, err := filepath.Glob(fmt.Sprintf("%s/*", productName))
if err != nil {
log.Fatalf("Cannot get resources files: %v", err)
}
// Base resource loop
for _, resourceYamlPath := range resourceFiles {
if filepath.Base(resourceYamlPath) == "product.yaml" || filepath.Ext(resourceYamlPath) != ".yaml" {
continue
}
if overrideDirectory != "" {
// skip if resource will be merged in the override loop
resourceOverridePath := filepath.Join(overrideDirectory, resourceYamlPath)
_, overrideResourceErr := os.Stat(resourceOverridePath)
overrideResourceExists := !errors.Is(overrideResourceErr, os.ErrNotExist)
if overrideResourceExists {
continue
}
}
resource := &api.Resource{}
api.Compile(resourceYamlPath, resource, overrideDirectory)
resource.SourceYamlFile = resourceYamlPath
resource.TargetVersionName = *version
resource.Properties = resource.AddLabelsRelatedFields(resource.PropertiesWithExcluded(), nil)
resource.SetDefault(productApi)
resource.Validate()
resources = append(resources, resource)
}
// Override Resource Loop
if overrideDirectory != "" {
productOverrideDir := filepath.Dir(productOverridePath)
overrideFiles, err := filepath.Glob(fmt.Sprintf("%s/*", productOverrideDir))
if err != nil {
log.Fatalf("Cannot get override files: %v", err)
}
for _, overrideYamlPath := range overrideFiles {
if filepath.Base(overrideYamlPath) == "product.yaml" || filepath.Ext(overrideYamlPath) != ".yaml" {
continue
}
resource := &api.Resource{}
baseResourcePath := filepath.Join(productName, filepath.Base(overrideYamlPath))
_, baseResourceErr := os.Stat(baseResourcePath)
baseResourceExists := !errors.Is(baseResourceErr, os.ErrNotExist)
if baseResourceExists {
api.Compile(baseResourcePath, resource, overrideDirectory)
overrideResource := &api.Resource{}
api.Compile(overrideYamlPath, overrideResource, overrideDirectory)
api.Merge(reflect.ValueOf(resource), reflect.ValueOf(*overrideResource))
} else {
api.Compile(overrideYamlPath, resource, overrideDirectory)
}
resource.TargetVersionName = *version
resource.Properties = resource.AddLabelsRelatedFields(resource.PropertiesWithExcluded(), nil)
resource.SetDefault(productApi)
resource.Validate()
resources = append(resources, resource)
}
// Sort resources by name
sort.Slice(resources, func(i, j int) bool {
return resources[i].Name < resources[j].Name
})
}
productApi.Objects = resources
productApi.Validate()
providerToGenerate = setProvider(*forceProvider, *version, productApi, startTime)
productsForVersionChannel <- productApi
if !slices.Contains(productsToGenerate, productName) {
log.Printf("%s not specified, skipping generation", productName)
return
}
log.Printf("%s: Generating files", productName)
providerToGenerate.Generate(*outputPath, productName, resourceToGenerate, generateCode, generateDocs)
}
// Sets provider via flag
func setProvider(forceProvider, version string, productApi *api.Product, startTime time.Time) provider.Provider {
switch forceProvider {
case "tgc":
return provider.NewTerraformGoogleConversion(productApi, version, startTime)
case "tgc_cai2hcl":
return provider.NewCaiToTerraformConversion(productApi, version, startTime)
case "tgc_next":
return provider.NewTerraformGoogleConversionNext(productApi, version, startTime)
case "oics":
return provider.NewTerraformOiCS(productApi, version, startTime)
default:
return provider.NewTerraform(productApi, version, startTime)
}
}