tools/generator-example-doc/main.go (250 lines of code) (raw):
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path"
"slices"
"strings"
"github.com/Azure/terraform-provider-azapi/internal/azure"
"github.com/Azure/terraform-provider-azapi/internal/azure/types"
"github.com/Azure/terraform-provider-azapi/utils"
)
var template string
type ResourceProvider struct {
ResourceProviderNamespace string `json:"resourceProviderNamespace"`
FriendlyName string `json:"friendlyName"`
}
type ResourceType struct {
ResourceType string `json:"resourceType"`
FriendlyName string `json:"friendlyName"`
}
var (
resourceProviders map[string]ResourceProvider
resourceTypes map[string]ResourceType
)
func init() {
var items []ResourceProvider
mappingJsonPath := path.Join("tools", "generator-example-doc", "resource_providers.json")
data, err := os.ReadFile(mappingJsonPath)
if err != nil {
panic(err)
}
err = json.Unmarshal(data, &items)
if err != nil {
panic(err)
}
resourceProviders = make(map[string]ResourceProvider)
for _, item := range items {
resourceProviders[strings.ToLower(item.ResourceProviderNamespace)] = item
}
var resourceTypeItems []ResourceType
resourceTypeJsonPath := path.Join("tools", "generator-example-doc", "resource_types.json")
data, err = os.ReadFile(resourceTypeJsonPath)
if err != nil {
panic(err)
}
err = json.Unmarshal(data, &resourceTypeItems)
if err != nil {
panic(err)
}
resourceTypes = make(map[string]ResourceType)
for _, item := range resourceTypeItems {
resourceTypes[strings.ToLower(item.ResourceType)] = item
}
// check duplicate resource type
resourceTypeMap := make(map[string]int)
for _, item := range resourceTypeItems {
resourceTypeMap[item.ResourceType]++
}
duplicateResourceTypes := make([]string, 0)
for k, v := range resourceTypeMap {
if v > 1 {
duplicateResourceTypes = append(duplicateResourceTypes, k)
}
}
slices.Sort(duplicateResourceTypes)
for _, item := range duplicateResourceTypes {
log.Printf("Duplicate resource type: %s", item)
}
data, err = os.ReadFile(path.Join("tools", "generator-example-doc", "template.md"))
if err != nil {
panic(err)
}
template = string(data)
}
func main() {
inputDir := flag.String("input-dir", "./examples", "directory to scan for example files")
outputDir := flag.String("output-dir", "./docs/guides", "directory to write documentation files")
flag.Parse()
if *inputDir == "" || *outputDir == "" {
log.Fatal("input-dir and output-dir flags are required")
}
resourceTypeDirs, err := os.ReadDir(*inputDir)
if err != nil {
log.Fatalf("Error reading input directory: %s", err)
}
for _, resourceTypeDir := range resourceTypeDirs {
if !resourceTypeDir.IsDir() {
continue
}
if !strings.Contains(resourceTypeDir.Name(), "@") {
continue
}
content, err := generateDocumentation(path.Join(*inputDir, resourceTypeDir.Name()))
if err != nil {
log.Fatalf("Error generating documentation for %s: %s", resourceTypeDir.Name(), err)
}
resourceTypeName := strings.Split(resourceTypeDir.Name(), "@")[0]
outputFile := path.Join(*outputDir, resourceTypeName+".md")
err = os.WriteFile(outputFile, []byte(content), 0644)
if err != nil {
log.Printf("Error writing documentation for %s: %s", resourceTypeDir.Name(), err)
}
}
}
func generateDocumentation(inputDir string) (string, error) {
resourceType := strings.Split(path.Base(inputDir), "@")[0]
resourceType = strings.ReplaceAll(resourceType, "_", "/")
resourceProviderName := strings.Split(resourceType, "/")[0]
resourceTypeWithoutRP := strings.Join(strings.Split(resourceType, "/")[1:], "/")
resourceProviderFriendlyName := GetResourceProviderFriendlyName(resourceProviderName)
if resourceProviderFriendlyName == "" {
return "", fmt.Errorf("resource provider %s friendly name not found, please add it to resource_providers.json", resourceProviderName)
}
resourceTypeFriendlyName := GetResourceTypeFriendlyName(resourceType)
if resourceTypeFriendlyName == "" {
return "", fmt.Errorf("resource type %s friendly name not found, please add it to resource_types.json", resourceType)
}
apiVersions := azure.GetApiVersions(resourceType)
apiVersion := "API_VERSION"
if len(apiVersions) > 0 {
apiVersion = apiVersions[len(apiVersions)-1]
}
parentIds := getParentIds(resourceType)
resourceId := ""
if len(parentIds) > 0 {
lastSegment := resourceType[strings.LastIndex(resourceType, "/")+1:]
if utils.IsTopLevelResourceType(resourceType) {
resourceId = fmt.Sprintf("%s/providers/%s/%s/{resourceName}", parentIds[0], resourceProviderName, lastSegment)
} else {
resourceId = fmt.Sprintf("%s/%s/{resourceName}", parentIds[0], lastSegment)
}
}
out := template
out = strings.ReplaceAll(out, "{{.subcategory}}", fmt.Sprintf("%s - %s", resourceProviderName, resourceProviderFriendlyName))
out = strings.ReplaceAll(out, "{{.page_title}}", resourceTypeWithoutRP)
out = strings.ReplaceAll(out, "{{.resource_type}}", resourceType)
out = strings.ReplaceAll(out, "{{.resource_type_friendly_name}}", resourceTypeFriendlyName)
out = strings.ReplaceAll(out, "{{.reference_link}}", fmt.Sprintf("https://learn.microsoft.com/en-us/azure/templates/%s?pivots=deployment-language-terraform", resourceType))
out = strings.ReplaceAll(out, "{{.api_versions}}", strings.Join(addBackticks(apiVersions), ", "))
out = strings.ReplaceAll(out, "{{.api_version}}", apiVersion)
out = strings.ReplaceAll(out, "{{.resource_id}}", resourceId)
out = strings.ReplaceAll(out, "{{.parent_id}}", strings.Join(addBackticks(parentIds), " \n "))
// key is the scenario name, value is the example content
exampleMap := make(map[string]string)
scenarioDirs, err := os.ReadDir(inputDir)
if err != nil {
return "", fmt.Errorf("error reading directory: %w", err)
}
for _, scenarioDir := range scenarioDirs {
if !scenarioDir.IsDir() || scenarioDir.Name() == "testdata" {
continue
}
scenarioName := scenarioDir.Name()
exampleFilePath := path.Join(inputDir, scenarioName, "main.tf")
exampleContent, err := os.ReadFile(exampleFilePath)
if err != nil {
log.Printf("Error reading example file for %s: %s", exampleFilePath, err)
continue
}
exampleMap[scenarioName] = string(exampleContent)
}
// check if there's main.tf in the inputDir
mainFilePath := path.Join(inputDir, "main.tf")
if _, err := os.Stat(mainFilePath); err == nil {
exampleContent, err := os.ReadFile(mainFilePath)
if err != nil {
log.Printf("Error reading example file for %s: %s", mainFilePath, err)
return "", err
}
exampleMap["default"] = string(exampleContent)
}
example := ""
for scenarioName, exampleContent := range exampleMap {
example += fmt.Sprintf("### %s\n\n", scenarioName)
example += fmt.Sprintf("```hcl\n%s\n```\n\n", exampleContent)
}
out = strings.ReplaceAll(out, "{{.example}}", example)
return out, nil
}
func getParentIds(resourceType string) []string {
const defaultParentId = "{any azure resource id}"
apiVersions := azure.GetApiVersions(resourceType)
if len(apiVersions) == 0 {
return nil
}
resourceDef, err := azure.GetResourceDefinition(resourceType, apiVersions[0])
if err != nil || resourceDef == nil {
return nil
}
if utils.IsTopLevelResourceType(resourceType) {
scopeIds := make([]string, 0)
for _, scope := range resourceDef.ScopeTypes {
switch scope {
case types.Tenant:
scopeIds = append(scopeIds, "/")
case types.Subscription:
scopeIds = append(scopeIds, "/subscriptions/{subscriptionId}")
case types.ManagementGroup:
scopeIds = append(scopeIds, "/providers/Microsoft.Management/managementGroups/{managementGroupId}")
case types.ResourceGroup:
scopeIds = append(scopeIds, "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}")
case types.Extension:
scopeIds = append(scopeIds, defaultParentId)
default:
scopeIds = append(scopeIds, defaultParentId)
}
}
return scopeIds
}
parentResourceType := resourceType[:strings.LastIndex(resourceType, "/")]
parentResourceParentIds := getParentIds(parentResourceType)
if len(parentResourceParentIds) == 0 {
return []string{defaultParentId}
}
parentIds := make([]string, 0)
lastSegment := parentResourceType[strings.LastIndex(parentResourceType, "/")+1:]
resourceProvider := strings.Split(parentResourceType, "/")[0]
for _, parentId := range parentResourceParentIds {
if parentId == defaultParentId {
parentIds = append(parentIds, defaultParentId)
continue
}
if utils.IsTopLevelResourceType(parentResourceType) {
parentIds = append(parentIds, fmt.Sprintf("%s/providers/%s/%s/{resourceName}", parentId, resourceProvider, lastSegment))
} else {
parentIds = append(parentIds, fmt.Sprintf("%s/%s/{resourceName}", parentId, lastSegment))
}
}
return parentIds
}
func GetResourceProviderFriendlyName(resourceProviderName string) string {
if resourceProvider, ok := resourceProviders[strings.ToLower(resourceProviderName)]; ok {
return resourceProvider.FriendlyName
}
return ""
}
func GetResourceTypeFriendlyName(resourceType string) string {
if v, ok := resourceTypes[strings.ToLower(resourceType)]; ok {
return v.FriendlyName
}
return ""
}
func addBackticks(input []string) []string {
for i, str := range input {
if str != "" {
input[i] = fmt.Sprintf("`%s`", str)
}
}
return input
}