commands/generate.go (392 lines of code) (raw):

package commands import ( "flag" "fmt" "log" "os" "path" "path/filepath" "sort" "strings" "github.com/azure/armstrong/autorest" "github.com/azure/armstrong/resource" "github.com/azure/armstrong/resource/resolver" "github.com/azure/armstrong/resource/types" "github.com/azure/armstrong/swagger" "github.com/azure/armstrong/utils" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) type GenerateCommand struct { // common options verbose bool workingDir string useRawJsonPayload bool // create with example path path string resourceType string overwrite bool // create with swagger path swaggerPath string // create with autorest config, TODO: remove them? because the tag contains swaggers from different api-versions readmePath string tag string } func (c *GenerateCommand) flags() *flag.FlagSet { fs := defaultFlagSet("generate") // common options fs.StringVar(&c.workingDir, "working-dir", "", "output path to Terraform configuration files") fs.BoolVar(&c.useRawJsonPayload, "raw", false, "whether use raw json payload in 'body'") fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") // generate with example options fs.StringVar(&c.path, "path", "", "path to a swagger 'Create' example") fs.StringVar(&c.resourceType, "type", "resource", "the type of the resource to be generated, allowed values: 'resource'(supports CRUD) and 'data'(read-only). Defaults to 'resource'") fs.BoolVar(&c.overwrite, "overwrite", false, "whether overwrite existing terraform configurations") // generate with swagger options fs.StringVar(&c.swaggerPath, "swagger", "", "path or directory to swagger.json files") // generate with autorest config fs.StringVar(&c.readmePath, "readme", "", "path to the autorest config file(readme.md)") fs.StringVar(&c.tag, "tag", "", "tag in the autorest config file(readme.md)") fs.Usage = func() { logrus.Error(c.Help()) } return fs } func (c GenerateCommand) Help() string { helpText := ` Usage: armstrong generate -path <path to a swagger 'Create' example> [-working-dir <output path to Terraform configuration files>] armstrong generate -swagger <path/dir to the swagger files> [-working-dir <output path to Terraform configuration files>] ` + c.Synopsis() + "\n\n" + helpForFlags(c.flags()) return strings.TrimSpace(helpText) } func (c GenerateCommand) Synopsis() string { return "Generate testing files including terraform configuration for dependencies and testing resource." } func (c GenerateCommand) Run(args []string) int { logrus.Debugf("args: %+v", args) f := c.flags() if err := f.Parse(args); err != nil { logrus.Errorf("Error parsing command-line flags: %+v", err) return 1 } logrus.Debugf("flags: %+v", f) if c.verbose { log.SetOutput(os.Stdout) logrus.SetLevel(logrus.DebugLevel) logrus.Infof("verbose mode enabled") } if c.swaggerPath != "" && c.path != "" && c.readmePath != "" { logrus.Error("only one of 'swagger', 'path' and 'readme' can be specified") return 1 } if c.path == "" && c.swaggerPath == "" && c.readmePath == "" { logrus.Error(c.Help()) return 1 } if c.readmePath != "" && c.tag == "" { logrus.Error("tag must be specified when 'readme' is specified") return 1 } if c.readmePath == "" && c.tag != "" { logrus.Errorf("tag can only be specified when 'readme' is specified") return 1 } return c.Execute() } func (c GenerateCommand) Execute() int { wd, err := os.Getwd() if err != nil { logrus.Errorf("Error getting working directory: %+v", err) return 1 } if c.workingDir != "" { wd, err = filepath.Abs(c.workingDir) if err != nil { logrus.Errorf("Error getting absolute path of working directory: %+v", err) return 1 } } c.workingDir = wd logrus.Infof("working directory: %s", c.workingDir) switch { case c.swaggerPath != "": return c.fromSwaggerPath() case c.path != "": return c.fromExamplePath() case c.readmePath != "": return c.fromAutorestConfig() } // should not reach here logrus.Println(c.Help()) return 1 } func (c GenerateCommand) fromExamplePath() int { wd := c.workingDir if c.overwrite { logrus.Infof("overwriting existing terraform configurations...") _ = os.RemoveAll(path.Join(wd, "testing.tf")) _ = os.RemoveAll(path.Join(wd, "dependency.tf")) } err := os.WriteFile(path.Join(wd, "provider.tf"), hclwrite.Format([]byte(resource.DefaultProviderConfig)), 0644) if err != nil { logrus.Errorf("writing provider.tf: %+v", err) } logrus.Infof("provider configuration is written to %s", path.Join(wd, "provider.tf")) // load example logrus.Infof("loading example: %s", c.path) example, err := resource.NewAzapiDefinitionFromExample(c.path, c.resourceType) if err != nil { logrus.Fatalf("loading example: %+v", err) } if c.useRawJsonPayload { logrus.Infof("using raw json payload in 'body'...") example.BodyFormat = types.BodyFormatJson } // load dependencies logrus.Infof("loading dependencies...") referenceResolvers := []resolver.ReferenceResolver{ resolver.NewExistingDependencyResolver(wd), resolver.NewAzapiDependencyResolver(), resolver.NewAzurermDependencyResolver(), resolver.NewProviderIDResolver(), resolver.NewLocationIDResolver(), resolver.NewAzapiResourcePlaceholderResolver(), } context := resource.NewContext(referenceResolvers) err = context.InitFile(allTerraformConfig(wd)) if err != nil { logrus.Errorf("initializing terraform configurations: %+v", err) return 1 } logrus.Infof("generating terraform configurations...") err = context.AddAzapiDefinition(example) if err != nil { return 0 } logrus.Infof("writing terraform configurations...") blockMap := blockFileMap(wd) contentToAppend := make(map[string]string) len := len(context.File.Body().Blocks()) for i, block := range context.File.Body().Blocks() { switch block.Type() { case "terraform", "provider", "variable": continue default: key := fmt.Sprintf("%s.%s", block.Type(), strings.Join(block.Labels(), ".")) if _, ok := blockMap[key]; ok { continue } outputFilename := "dependency.tf" if i == len-1 { outputFilename = "testing.tf" } contentToAppend[outputFilename] = contentToAppend[outputFilename] + "\n" + string(block.BuildTokens(nil).Bytes()) } } for filename, content := range contentToAppend { err := appendContent(path.Join(wd, filename), content) if err != nil { logrus.Errorf("writing %s: %+v", filename, err) } logrus.Infof("configuration is written to %s", path.Join(wd, filename)) } return 0 } func (c GenerateCommand) fromSwaggerPath() int { swaggerPath, err := filepath.Abs(c.swaggerPath) if err == nil { c.swaggerPath = swaggerPath } logrus.Infof("loading swagger spec: %s...", c.swaggerPath) file, err := os.Stat(c.swaggerPath) if err != nil { logrus.Fatalf("loading swagger spec: %+v", err) } apiPathsAll := make([]swagger.ApiPath, 0) if file.IsDir() { logrus.Infof("swagger spec is a directory") logrus.Infof("loading swagger spec directory: %s...", c.swaggerPath) filenames, err := utils.ListFiles(c.swaggerPath, ".json", 1) if err != nil { logrus.Fatalf("reading swagger spec directory: %+v", err) } for _, filename := range filenames { logrus.Infof("parsing swagger spec: %s...", filename) apiPaths, err := swagger.Load(filename) if err != nil { logrus.Fatalf("parsing swagger spec: %+v", err) } apiPathsAll = append(apiPathsAll, apiPaths...) } } else { logrus.Infof("parsing swagger spec: %s...", c.swaggerPath) apiPaths, err := swagger.Load(c.swaggerPath) if err != nil { logrus.Fatalf("parsing swagger spec: %+v", err) } apiPathsAll = append(apiPathsAll, apiPaths...) } logrus.Infof("found %d api paths", len(apiPathsAll)) return c.generate(apiPathsAll) } func (c *GenerateCommand) fromAutorestConfig() int { logrus.Infof("parsing autorest config: %s...", c.readmePath) packages := autorest.ParseAutoRestConfig(c.readmePath) logrus.Debugf("found %d packages", len(packages)) var targetPackage *autorest.Package for _, pkg := range packages { if pkg.Tag == c.tag { targetPackage = &pkg break } } if targetPackage == nil { logrus.Fatalf("package with tag %s not found in %s", c.tag, c.readmePath) } apiPathsAll := make([]swagger.ApiPath, 0) for _, swaggerPath := range targetPackage.InputFiles { logrus.Infof("parsing swagger spec: %s...", swaggerPath) azapiPaths, err := swagger.Load(swaggerPath) if err != nil { logrus.Fatalf("parsing swagger spec: %+v", err) } apiPathsAll = append(apiPathsAll, azapiPaths...) } return c.generate(apiPathsAll) } func (c *GenerateCommand) generate(apiPaths []swagger.ApiPath) int { wd := c.workingDir azapiDefinitionsAll := make([]types.AzapiDefinition, 0) for _, apiPath := range apiPaths { azapiDefinitionsAll = append(azapiDefinitionsAll, resource.NewAzapiDefinitionsFromSwagger(apiPath)...) } if c.useRawJsonPayload { logrus.Infof("using raw json payload in 'body'...") for i := range azapiDefinitionsAll { azapiDefinitionsAll[i].BodyFormat = types.BodyFormatJson } } azapiDefinitionByResourceType := make(map[string][]types.AzapiDefinition) for _, azapiDefinition := range azapiDefinitionsAll { azureResourceType := azapiDefinition.AzureResourceType // To avoid the case that there are multiple resource types with the same name but different casing for resourceType := range azapiDefinitionByResourceType { if strings.EqualFold(resourceType, azureResourceType) { azureResourceType = resourceType } } azapiDefinitionByResourceType[azureResourceType] = append(azapiDefinitionByResourceType[azureResourceType], azapiDefinition) } resourceTypes := make([]string, 0) for resourceType := range azapiDefinitionByResourceType { slices.SortFunc(azapiDefinitionByResourceType[resourceType], func(i, j types.AzapiDefinition) int { return azapiDefinitionOrder(i) - azapiDefinitionOrder(j) }) resourceTypes = append(resourceTypes, resourceType) } sort.Strings(resourceTypes) referenceResolvers := []resolver.ReferenceResolver{ resolver.NewAzapiDependencyResolver(), resolver.NewAzapiDefinitionResolver(azapiDefinitionsAll), resolver.NewProviderIDResolver(), resolver.NewLocationIDResolver(), resolver.NewAzapiResourceIdResolver(), } for _, resourceType := range resourceTypes { logrus.Infof("generating terraform configurations for %s...", resourceType) azapiDefinitions := azapiDefinitionByResourceType[resourceType] // remove existing folders by default folderName := strings.ReplaceAll(resourceType, "/", "_") err := os.RemoveAll(path.Join(wd, folderName)) if err != nil { logrus.Errorf("removing existing folder: %+v", err) } err = os.MkdirAll(path.Join(wd, folderName), 0755) if err != nil { logrus.Fatalf("creating folder: %+v", err) } context := resource.NewContext(referenceResolvers) for _, azapiDefinition := range azapiDefinitions { logrus.Debugf("generating terraform configurations for %s...", azapiDefinition.Id) err = context.AddAzapiDefinition(azapiDefinition) if err != nil { logrus.Warnf("adding azapi definition for %s: %+v", azapiDefinition.Id, err) } } filename := path.Join(wd, folderName, "main.tf") err = os.WriteFile(filename, hclwrite.Format([]byte(context.String())), 0644) if err != nil { logrus.Errorf("writing %s: %+v", filename, err) } } return 0 } func azapiDefinitionOrder(azapiDefinition types.AzapiDefinition) int { // 0. resource.azapi_resource // 1. resource.azapi_update_resource Note: Now it will not be generated // 2. azapi_resource_action with empty action // 3. azapi_resource_action with action // 4. data.azapi_resource // 5. azapi_resource_list switch azapiDefinition.ResourceName { case "azapi_resource": if azapiDefinition.Kind == types.KindResource { return 0 } return 4 case "azapi_update_resource": return 1 case "azapi_resource_action": if actionField := azapiDefinition.AdditionalFields["action"]; actionField == nil || actionField.String() == `""` { return 2 } return 3 case "azapi_resource_list": return 5 } return 6 } func appendContent(filename string, hclContent string) error { content := hclContent if _, err := os.Stat(filename); err == nil { existingHcl, err := os.ReadFile(filename) if err != nil { logrus.Warnf("reading existing file: %+v", err) } content = string(existingHcl) + "\n" + content } return os.WriteFile(filename, hclwrite.Format([]byte(content)), 0644) } func blockFileMap(workingDirectory string) map[string]string { files, err := os.ReadDir(workingDirectory) if err != nil { logrus.Warnf("reading dir %s: %+v", workingDirectory, err) return nil } out := make(map[string]string) for _, file := range files { if !strings.HasSuffix(file.Name(), ".tf") { continue } src, err := os.ReadFile(path.Join(workingDirectory, file.Name())) if err != nil { logrus.Warnf("reading file %s: %+v", file.Name(), err) continue } f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos) if diag.HasErrors() { logrus.Warnf("parsing file %s: %+v", file.Name(), diag.Error()) continue } if f == nil || f.Body() == nil { continue } for _, block := range f.Body().Blocks() { key := fmt.Sprintf("%s.%s", block.Type(), strings.Join(block.Labels(), ".")) out[key] = file.Name() } } return out } func allTerraformConfig(workingDirectory string) string { out := "" files, err := os.ReadDir(workingDirectory) if err != nil { logrus.Warnf("reading dir %s: %+v", workingDirectory, err) return out } for _, file := range files { if !strings.HasSuffix(file.Name(), ".tf") { continue } src, err := os.ReadFile(path.Join(workingDirectory, file.Name())) if err != nil { logrus.Warnf("reading file %s: %+v", file.Name(), err) continue } out += string(src) } return out }