thomasfinstad / terraform-provider-vyos-rolling

Terraform provider for VyOS with a focus on automatic resource generation
6 stars 0 forks source link

autogenerate resource import doc #203

Open github-actions[bot] opened 7 months ago

github-actions[bot] commented 7 months ago

milestone:2

making it easier for users to import resources by following the documentation

explain how the resource ID is built up by using a template

something like policy__access-list__\<access-list id>__rule__\<rule id>

https://github.com/thomasfinstad/terraform-provider-vyos/blob/c3bd4b16ebe7a43fe5699e92a09c689c41d3aa65/tools/build-terraform-resource-full/main.go#L108


package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "runtime"
    "strings"
    "text/template"

    vyosinterfaces "github.com/thomasfinstad/terraform-provider-vyos/.build/vyosinterfaces"
    schemadefinition "github.com/thomasfinstad/terraform-provider-vyos/internal/vyos/schemadefinition"
)

type autogenTemplateInfo struct {
    PkgName        string
    PkgPath        string
    PkgConstructor string
}

func main() {
    args := os.Args[1:]
    rootOutputDirectory := args[0]
    selfImportRoot := args[1]
    skipDirAbsNames := strings.Split(args[2], ",")

    vyosInterfaces := vyosinterfaces.GetInterfaces()

    var packagesToGenerate []autogenTemplateInfo

    // Compile resources
    for _, vyosInterface := range vyosInterfaces {

        // Named (TagNode) resources
        TagNodes, ok := vyosInterface.TagNodes()
        if ok {
            for _, tagNode := range TagNodes {
                packagesToGenerate = append(packagesToGenerate, namedResources(tagNode, skipDirAbsNames, fmt.Sprintf("%s/named", rootOutputDirectory), "named", selfImportRoot)...)
            }
        }

        // Global (Node) resources
        Nodes, ok := vyosInterface.Nodes()
        if ok {
            for _, node := range Nodes {
                packagesToGenerate = append(packagesToGenerate, globalResources(node, skipDirAbsNames, fmt.Sprintf("%s/global", rootOutputDirectory), "global", selfImportRoot)...)
            }
        }
    }

    //sort.Slice(pkgs, func(i, j int) bool { return reflect.DeepEqual(pkgs[i], pkgs[j]) })
    //slices.Compact[[]autogenTemplateInfo](pkgs)
    // ugly ugly boy needs to remove duplicates of himself, but is too scared of the mirror
    var packagesToGenerateDeduped []autogenTemplateInfo
    for _, currentPkg := range packagesToGenerate {
        matchedConstructor := false
        matchedName := false
        matchedPath := false
        for _, preHandledPackage := range packagesToGenerateDeduped {
            if currentPkg.PkgConstructor == preHandledPackage.PkgConstructor {
                matchedConstructor = true
            }
            if currentPkg.PkgName == preHandledPackage.PkgName {
                matchedName = true
            }
            if currentPkg.PkgPath == preHandledPackage.PkgPath {
                matchedPath = true
            }
            if matchedConstructor || matchedName || matchedPath {
                if !(matchedConstructor && matchedName && matchedPath) {
                    fmt.Println("Found duplicate, but not all fields match!")
                    fmt.Printf("npkg: %#v \t opkg: %#v", preHandledPackage, currentPkg)
                }
                break
            }
        }
        if matchedConstructor || matchedName || matchedPath {
            continue
        }
        packagesToGenerateDeduped = append(packagesToGenerateDeduped, currentPkg)
    }

    // Create package resource inclusion function
    _, thisFilename, _, ok := runtime.Caller(0)
    if !ok {
        panic("Did not get path info")
    }
    thisDir := filepath.Dir(thisFilename)
    outputFile := fmt.Sprintf("%s/package.go", rootOutputDirectory)
    fmt.Printf("Creating autogen resource include file: %s\n", outputFile)
    file, err := os.Create(outputFile)
    if err != nil {
        return
    }
    defer file.Close()

    // Render and write file
    t, err := template.New("package_generation").ParseFiles(filepath.Join(thisDir, "package-resource-include.gotmpl"))
    if err != nil {
        die(err)
    }
    err = t.ExecuteTemplate(file, "package", map[string]any{"pkgName": "autogen", "importRoot": strings.ToLower(selfImportRoot), "pkgs": packagesToGenerateDeduped})
    if err != nil {
        die(err)
    }

    // TODO autogenerate resource import doc
    //  milestone:2
    //  making it easier for users to import resources by following the documentation
    //  explain how the resource ID is built up by using a template
    //  something like policy__access-list__<access-list id>__rule__<rule id>

    // TODO look into marking values as sensitive
    //  milestone:3
    //  Is there any way to detect this from the shcema?
    //  If not a manual overwrite feature during code generation
    //  will be the next best thing.
    //  Example of sensitive value:
}

func namedResources(tagNode *schemadefinition.TagNode, skipDirAbsNames []string, rootOutputDirectory string, rootPkgName string, selfImportRoot string) (pkgs []autogenTemplateInfo) {
    _, thisFilename, _, ok := runtime.Caller(0)
    if !ok {
        panic("Did not get path info")
    }
    thisDir := filepath.Dir(thisFilename)

    fmt.Printf("BaseTagNode: %s\n", tagNode.AbsName())

    // Absolute dir name
    absDirNameComponents := []string{tagNode.AbsName()[0], strings.Join(tagNode.AbsName()[1:], "-")}
    for i := len(absDirNameComponents) - 1; i > 0; i-- {
        if absDirNameComponents[i] == "" {
            absDirNameComponents = append(absDirNameComponents[:i], absDirNameComponents[i+1:]...)
        }
    }
    absDirName := strings.Join(absDirNameComponents, "/")

    // Check if blacklisted
    shouldSkip := false
    for _, skipDirAbsName := range skipDirAbsNames {
        if absDirName == skipDirAbsName {
            shouldSkip = true
            break
        }
    }
    if shouldSkip {
        fmt.Printf("\nWARNING: [%s] Has been marked for skipping\n", tagNode.AbsName())
        return
    }

    // Create output dir
    resourceOutputDir := strings.Join([]string{rootOutputDirectory, absDirName}, "/")
    if err := os.MkdirAll(resourceOutputDir, os.ModePerm); err != nil {
        log.Fatal(err)
    }

    // Compile template
    t, err := template.New("resource_generation").ParseFiles(filepath.Join(thisDir, "named-template.gotmpl"))
    if err != nil {
        die(err)
    }

    resourceModelSubDir := "resourcemodel"

    // Key/Value map of template-name = template-data
    templateRuns := map[string]any{
        "validate":                    tagNode,
        "resource-tagnode-based-full": tagNode,
        "metadata":                    tagNode,
        "schema":                      tagNode,
        "crud":                        tagNode,
    }

    for templateName, data := range templateRuns {
        pkgs = append(pkgs, namedResourceGeneration(resourceOutputDir, templateName, thisFilename, rootPkgName, tagNode, selfImportRoot, resourceModelSubDir, t, data))
    }

    // Create Resource model in subdir
    resourceModelOutputDir := strings.Join([]string{resourceOutputDir, resourceModelSubDir}, "/")
    if err := os.MkdirAll(resourceModelOutputDir, os.ModePerm); err != nil {
        log.Fatal(err)
    }

    namedResourceModelGeneration(resourceModelOutputDir, tagNode, t, thisFilename, resourceModelSubDir)

    fmt.Printf("Done...\n\n")

    return pkgs
}

func globalResources(node *schemadefinition.Node, skipDirAbsNames []string, rootOutputDirectory string, rootPkgName string, selfImportRoot string) (pkgs []autogenTemplateInfo) {
    _, thisFilename, _, ok := runtime.Caller(0)
    if !ok {
        panic("Did not get path info")
    }
    thisDir := filepath.Dir(thisFilename)

    fmt.Printf("BaseNode: %s\n", node.AbsName())

    // Absolute dir name
    absDirNameComponents := []string{node.AbsName()[0], strings.Join(node.AbsName()[1:], "-")}
    for i := len(absDirNameComponents) - 1; i > 0; i-- {
        if absDirNameComponents[i] == "" {
            absDirNameComponents = append(absDirNameComponents[:i], absDirNameComponents[i+1:]...)
        }
    }
    absDirName := strings.Join(absDirNameComponents, "/")

    // Check if blacklisted
    shouldSkip := false
    for _, skipDirAbsName := range skipDirAbsNames {
        if absDirName == skipDirAbsName {
            shouldSkip = true
            break
        }
    }
    if shouldSkip {
        fmt.Printf("\nWARNING: [%s] Has been marked for skipping\n", node.AbsName())
        return
    }

    // Create output dir
    resourceOutputDir := strings.Join([]string{rootOutputDirectory, absDirName}, "/")
    if err := os.MkdirAll(resourceOutputDir, os.ModePerm); err != nil {
        log.Fatal(err)
    }

    // Compile template
    t, err := template.New("resource_generation").ParseFiles(filepath.Join(thisDir, "global-template.gotmpl"))
    if err != nil {
        die(err)
    }

    resourceModelSubDir := "resourcemodel"

    // Key/Value map of template-name = template-data
    templateRuns := map[string]any{
        "validate":                 node,
        "resource-node-based-full": node,
        "metadata":                 node,
        "schema":                   node,
        "crud":                     node,
    }

    for templateName, data := range templateRuns {
        pkgs = append(pkgs, globalResourceGeneration(resourceOutputDir, templateName, thisFilename, rootPkgName, node, selfImportRoot, resourceModelSubDir, t, data))
    }

    // Create Resource model in subdir
    resourceModelOutputDir := strings.Join([]string{resourceOutputDir, resourceModelSubDir}, "/")
    if err := os.MkdirAll(resourceModelOutputDir, os.ModePerm); err != nil {
        log.Fatal(err)
    }

    globalResourceModelGeneration(resourceModelOutputDir, node, t, thisFilename, resourceModelSubDir)

    fmt.Printf("Done...\n\n")

    return pkgs
}

func die(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func globalResourceGeneration(resourceOutputDir string, templateName string, thisFilename string, rootPkgName string, rootNode *schemadefinition.Node, selfImportRoot string, resourceModelSubDir string, t *template.Template, data any) (pkg autogenTemplateInfo) {
    // Format outpout file name
    outputFile := fmt.Sprintf(
        "%s.go",
        strings.Join(
            []string{resourceOutputDir, strings.Split(templateName, "-")[0]},
            "/",
        ),
    )
    fmt.Printf("Creating global resource: %s\n", outputFile)
    file, err := os.Create(outputFile)
    if err != nil {
        return
    }
    defer file.Close()

    pkg.PkgPath = resourceOutputDir
    pkg.PkgName = strings.ToLower(rootPkgName + rootNode.BaseNameCG())
    pkg.PkgConstructor = "New" + rootNode.BaseNameCG()

    // Write Package
    err = t.ExecuteTemplate(file, "package", map[string]string{"caller": thisFilename, "pkg": pkg.PkgName})
    if err != nil {
        die(err)
    }

    // Write Imports
    var importExtra []string
    if templateName == "resource-node-based-full" {
        importExtra = []string{
            fmt.Sprintf("%s/%s/%s", selfImportRoot, resourceOutputDir, resourceModelSubDir),
        }
    }
    err = t.ExecuteTemplate(file, "imports", importExtra)
    if err != nil {
        die(err)
    }

    // Write template data
    err = t.ExecuteTemplate(file, templateName, data)
    if err != nil {
        die(err)
    }

    return pkg
}

func namedResourceGeneration(resourceOutputDir string, templateName string, thisFilename string, rootPkgName string, rootTagNode *schemadefinition.TagNode, selfImportRoot string, resourceModelSubDir string, t *template.Template, data any) (pkg autogenTemplateInfo) {
    // Format outpout file name
    outputFile := fmt.Sprintf(
        "%s.go",
        strings.Join(
            []string{resourceOutputDir, strings.Split(templateName, "-")[0]},
            "/",
        ),
    )
    fmt.Printf("Creating named resource: %s\n", outputFile)
    file, err := os.Create(outputFile)
    if err != nil {
        return
    }
    defer file.Close()

    pkg.PkgPath = resourceOutputDir
    pkg.PkgName = strings.ToLower(rootPkgName + rootTagNode.BaseNameCG())
    pkg.PkgConstructor = "New" + rootTagNode.BaseNameCG()

    // Write Package
    err = t.ExecuteTemplate(file, "package", map[string]string{"caller": thisFilename, "pkg": pkg.PkgName})
    if err != nil {
        die(err)
    }

    // Write Imports
    var importExtra []string
    if templateName == "resource-tagnode-based-full" {
        importExtra = []string{
            fmt.Sprintf("%s/%s/%s", selfImportRoot, resourceOutputDir, resourceModelSubDir),
        }
    }
    err = t.ExecuteTemplate(file, "imports", importExtra)
    if err != nil {
        die(err)
    }

    // Write template data
    err = t.ExecuteTemplate(file, templateName, data)
    if err != nil {
        die(err)
    }

    return pkg
}

func namedResourceModelGeneration(resourceModelOutputDir string, node schemadefinition.NodeParent, t *template.Template, thisFilename string, resourceModelSubDir string) {
    outputFile := fmt.Sprintf(
        "%s.go",
        strings.Join(
            []string{resourceModelOutputDir, strings.Join(node.AbsName(), "-")},
            "/",
        ),
    )
    fmt.Printf("Creating named resource model: %s\n", outputFile)
    file, err := os.Create(outputFile)
    if err != nil {
        return
    }
    defer file.Close()

    // Write Package
    err = t.ExecuteTemplate(file, "package", map[string]string{"caller": thisFilename, "pkg": resourceModelSubDir})
    if err != nil {
        die(err)
    }

    // Write Imports
    err = t.ExecuteTemplate(file, "imports", nil)
    if err != nil {
        die(err)
    }

    // Write the resource model
    err = t.ExecuteTemplate(file, "resource-model", node)
    if err != nil {
        die(err)
    }

    // Recurse
    c := node.GetChildren()
    for _, n := range c.Nodes() {
        namedResourceModelGeneration(resourceModelOutputDir, n, t, thisFilename, resourceModelSubDir)
    }
}

func globalResourceModelGeneration(resourceModelOutputDir string, node schemadefinition.NodeParent, t *template.Template, thisFilename string, resourceModelSubDir string) {
    outputFile := fmt.Sprintf(
        "%s.go",
        strings.Join(
            []string{resourceModelOutputDir, strings.Join(node.AbsName(), "-")},
            "/",
        ),
    )
    fmt.Printf("Creating global resource model: %s\n", outputFile)
    file, err := os.Create(outputFile)
    if err != nil {
        return
    }
    defer file.Close()

    // Write Package
    err = t.ExecuteTemplate(file, "package", map[string]string{"caller": thisFilename, "pkg": resourceModelSubDir})
    if err != nil {
        die(err)
    }

    // Write Imports
    err = t.ExecuteTemplate(file, "imports", nil)
    if err != nil {
        die(err)
    }

    // Write the resource model
    err = t.ExecuteTemplate(file, "resource-model", node)
    if err != nil {
        die(err)
    }
}