scott-the-programmer/terraform-provider-minikube

View on GitHub
minikube/generator/schema_builder.go

Summary

Maintainability
A
1 hr
Test Coverage
A
93%
//go:generate go run github.com/golang/mock/mockgen -source=$GOFILE -destination=mock_minikube_binary.go -package=$GOPACKAGE
package generator

import (
    "bufio"
    "context"
    "errors"
    "fmt"
    "log"
    "os"
    "os/exec"
    "regexp"
    "strconv"
    "strings"
    "time"
)

type MinikubeBinary interface {
    GetVersion(ctx context.Context) (string, error)
    GetStartHelpText(ctx context.Context) (string, error)
}

type MinikubeHostBinary struct {
}

func (m *MinikubeHostBinary) GetVersion(ctx context.Context) (string, error) {
    return run(ctx, "version")
}

func (m *MinikubeHostBinary) GetStartHelpText(ctx context.Context) (string, error) {
    return run(ctx, "start", "--help")
}

var computedFields []string = []string{
    "apiserver_ips",
    "apiserver_names",
    "hyperkit_vsock_ports",
    "insecure_registry",
    "iso_url",
    "nfs_share",
    "ports",
    "registry_mirror",
}

type SchemaOverride struct {
    Description      string
    Default          string
    Type             SchemaType
    DefaultFunc      string
    StateFunc        string
    ValidateDiagFunc string
}

var updateFields = []string{
    "addons",
}

var schemaOverrides map[string]SchemaOverride = map[string]SchemaOverride{
    "memory": {
        Default:          "4g",
        Description:      "Amount of RAM to allocate to Kubernetes (format: <number>[<unit>(case-insensitive)], where unit = b, k, kb, m, mb, g or gb)",
        Type:             String,
        StateFunc:        "state_utils.MemoryConverter()",
        ValidateDiagFunc: "state_utils.MemoryValidator()",
    },
    "cpus": {
        Default:     "2",
        Description: "Amount of CPUs to allocate to Kubernetes",
        Type:        Int,
    },
    // Customize the description to be the fullset of drivers
    "driver": {
        Default:     "docker",
        Description: "Driver is one of the following - Windows: (hyperv, docker, virtualbox, vmware, qemu2, ssh) - OSX: (virtualbox, parallels, vmwarefusion, hyperkit, vmware, qemu2, docker, podman, ssh) - Linux: (docker, kvm2, virtualbox, qemu2, none, podman, ssh)",
        Type:        String,
    },
    "container_runtime": {
        Default:     "docker",
        Description: "The container runtime to be used. Valid options: docker, cri-o, containerd (default: docker)",
        Type:        String,
    },
    // Default schema to unix file paths first and let the provider translate them during runtime
    "mount_string": {
        Description: "The argument to pass the minikube mount command on start.",
        Type:        String,
        DefaultFunc: `func() (any, error) {
                if runtime.GOOS == "windows" {
                    home, err := os.UserHomeDir()
                    if err != nil {
                        return nil, err
                    }
                    return home + ":" + "/minikube-host", nil
                } else if runtime.GOOS == "darwin" {
                    return "/Users:/minikube-host", nil
                } 
                return "/home:/minikube-host", nil
            }`,
    },
    "extra_config": {
        Description: "A set of key=value pairs that describe configuration that may be passed to different components.         The key should be '.' separated, and the first part before the dot is the component to apply the configuration to.         Valid components are: kubelet, kubeadm, apiserver, controller-manager, etcd, proxy, scheduler         Valid kubeadm parameters: ignore-preflight-errors, dry-run, kubeconfig, kubeconfig-dir, node-name, cri-socket, experimental-upload-certs, certificate-key, rootfs, skip-phases, pod-network-cidr",
        Type:        Array,
    },
    "socket_vmnet_path": {
        Description: "Path to socket vmnet binary (QEMU driver only)",
        Type:        String,
        DefaultFunc: `func() (any, error) {
        var prefix string
        if runtime.GOARCH == "arm64" {
            prefix = "/opt/homebrew"
        } else {
            prefix = "/usr/local"
        }
        return prefix + "/var/run/socket_vmnet", nil
    }`,
    },
    "socket_vmnet_client_path": {
        Description: "Path to the socket vmnet client binary (QEMU driver only)",
        Type:        String,
        DefaultFunc: `func() (any, error) {
        var prefix string
        if runtime.GOARCH == "arm64" {
            prefix = "/opt/homebrew"
        } else {
            prefix = "/usr/local"
        }
        return prefix + "/opt/socket_vmnet/bin/socket_vmnet_client", nil
    }`,
    },
}

func run(ctx context.Context, args ...string) (string, error) {
    cmd := exec.CommandContext(ctx, "minikube", args...)
    out, err := cmd.Output()
    if err != nil {
        return "", err
    }

    return string(out), nil
}

type SchemaEntry struct {
    Parameter        string
    Default          string
    DefaultFunc      string
    StateFunc        string
    ValidateDiagFunc string
    Description      string
    Type             SchemaType
    ArrayType        SchemaType
}

type SchemaBuilder struct {
    targetFile string
    minikube   MinikubeBinary
}

type SchemaType string

const (
    String SchemaType = "String"
    Int    SchemaType = "Int"
    Bool   SchemaType = "Bool"
    Array  SchemaType = "Set"
)

func NewSchemaBuilder(targetFile string, minikube MinikubeBinary) *SchemaBuilder {
    return &SchemaBuilder{targetFile: targetFile, minikube: minikube}
}

func (s *SchemaBuilder) Build() (string, error) {
    minikubeVersion, err := s.minikube.GetVersion(context.Background())
    if err != nil {
        return "", errors.New("could not run minikube binary. please ensure that you have minikube installed and available")
    }

    log.Printf("building schema for minikube version: %s", minikubeVersion)

    help, err := s.minikube.GetStartHelpText(context.Background())
    if err != nil {
        return "", err
    }

    scanner := bufio.NewScanner(strings.NewReader(help))

    entries := make([]SchemaEntry, 0)

    currentEntry := SchemaEntry{}

    pattern := "^-[a-zA-Z], "

    srg := regexp.MustCompile(pattern)

    for scanner.Scan() {
        line := scanner.Text()
        line = strings.TrimSpace(line)

        // trim the short parameter e.g. -g
        line = srg.ReplaceAllString(line, "")

        if strings.HasPrefix(line, "--") {
            currentEntry = loadParameter(line)
        } else if line != "" {
            if currentEntry.Description != "" { // Let's read a space between line blocks
                currentEntry.Description += " "
            }
            currentEntry.Description += line
            currentEntry.Description = strings.ReplaceAll(currentEntry.Description, "\\", "\\\\")
            currentEntry.Description = strings.ReplaceAll(currentEntry.Description, "\"", "\\\"")
        } else if currentEntry.Parameter != "" {
            // Apply description override once we've built the description
            val, ok := schemaOverrides[currentEntry.Parameter]
            if ok {
                currentEntry.Description = val.Description
            }

            entries, err = addEntry(entries, currentEntry)
            if err != nil {
                return "", err
            }

            currentEntry.Parameter = ""
        }
    }

    schema := constructSchema(entries)

    return schema, err
}

func loadParameter(line string) SchemaEntry {
    schemaEntry := SchemaEntry{}
    schemaEntry.Description = ""
    seg := strings.Split(line, "=")
    schemaEntry.Parameter = strings.TrimPrefix(seg[0], "--")
    schemaEntry.Parameter = strings.Replace(schemaEntry.Parameter, "-", "_", -1)
    schemaEntry.Default = strings.TrimSuffix(seg[1], ":")
    schemaEntry.Default = strings.ReplaceAll(schemaEntry.Default, "\\", "\\\\")
    schemaEntry.Type = getSchemaType(schemaEntry.Default)

    // Apply explicit overrides
    val, ok := schemaOverrides[schemaEntry.Parameter]
    if ok {
        schemaEntry.Default = val.Default
        schemaEntry.DefaultFunc = val.DefaultFunc
        schemaEntry.Type = val.Type
        schemaEntry.StateFunc = val.StateFunc
        schemaEntry.ValidateDiagFunc = val.ValidateDiagFunc
    }

    if schemaEntry.Type == String {
        schemaEntry.Default = strings.Trim(schemaEntry.Default, "'")
    }

    return schemaEntry
}

func addEntry(entries []SchemaEntry, currentEntry SchemaEntry) ([]SchemaEntry, error) {
    switch currentEntry.Type {
    case String:
        entries = append(entries, SchemaEntry{
            Parameter:        currentEntry.Parameter,
            Default:          fmt.Sprintf("\"%s\"", currentEntry.Default),
            Type:             currentEntry.Type,
            Description:      currentEntry.Description,
            DefaultFunc:      currentEntry.DefaultFunc,
            StateFunc:        currentEntry.StateFunc,
            ValidateDiagFunc: currentEntry.ValidateDiagFunc,
        })
    case Bool:
        entries = append(entries, SchemaEntry{
            Parameter:        currentEntry.Parameter,
            Default:          currentEntry.Default,
            Type:             currentEntry.Type,
            Description:      currentEntry.Description,
            DefaultFunc:      currentEntry.DefaultFunc,
            StateFunc:        currentEntry.StateFunc,
            ValidateDiagFunc: currentEntry.ValidateDiagFunc,
        })
    case Int:
        val, err := strconv.Atoi(currentEntry.Default)
        if err != nil {
            // is it a timestamp?
            time, err := time.ParseDuration(currentEntry.Default)
            if err != nil {
                return nil, err
            }
            val = int(time.Minutes())
            currentEntry.Description = fmt.Sprintf("%s (Configured in minutes)", currentEntry.Description)
        }
        entries = append(entries, SchemaEntry{
            Parameter:        currentEntry.Parameter,
            Default:          strconv.Itoa(val),
            Type:             currentEntry.Type,
            Description:      currentEntry.Description,
            DefaultFunc:      currentEntry.DefaultFunc,
            StateFunc:        currentEntry.StateFunc,
            ValidateDiagFunc: currentEntry.ValidateDiagFunc,
        })
    case Array:
        entries = append(entries, SchemaEntry{
            Parameter:        currentEntry.Parameter,
            Type:             Array,
            ArrayType:        String,
            Description:      currentEntry.Description,
            DefaultFunc:      currentEntry.DefaultFunc,
            StateFunc:        currentEntry.StateFunc,
            ValidateDiagFunc: currentEntry.ValidateDiagFunc,
        })
    }

    return entries, nil
}

func (s *SchemaBuilder) Write(schema string) error {
    return os.WriteFile(s.targetFile, []byte(schema), 0644)
}

func constructSchema(entries []SchemaEntry) string {

    header := `//go:generate go run ../generate/main.go -target $GOFILE
// THIS FILE IS GENERATED DO NOT EDIT
package minikube

import (
    "runtime"
    "os"

    "github.com/scott-the-programmer/terraform-provider-minikube/minikube/state_utils"

    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var (
    clusterSchema = map[string]*schema.Schema{
        "cluster_name": {
            Type:                    schema.TypeString,
            Optional:            true,
            ForceNew:            true,
            Description:    "The name of the minikube cluster",
            Default:            "terraform-provider-minikube",
        },

        "client_key": {
            Type:        schema.TypeString,
            Computed:    true,
            Description: "client key for cluster",
            Sensitive:   true,
        },

        "client_certificate": {
            Type:        schema.TypeString,
            Computed:    true,
            Description: "client certificate used in cluster",
            Sensitive:   true,
        },

        "cluster_ca_certificate": {
            Type:        schema.TypeString,
            Computed:    true,
            Description: "certificate authority for cluster",
            Sensitive:   true,
        },

        "host": {
            Type:        schema.TypeString,
            Computed:    true,
            Description: "the host name for the cluster",
        },
`

    body := ""
    for _, entry := range entries {
        extraParams := ""
        if contains(computedFields, entry.Parameter) {
            extraParams = `
            Computed:            true,
            `
        }

        if !contains(updateFields, entry.Parameter) {
            extraParams += `
            Optional:            true,
            ForceNew:            true,
            `
        } else {
            extraParams += `
            Optional:            true,
            `
        }

        if entry.Type == Array {
            extraParams += fmt.Sprintf(`
            Elem: &schema.Schema{
                Type:    %s,
            },
            `, "schema.Type"+entry.ArrayType)
        } else if entry.DefaultFunc != "" {
            extraParams += fmt.Sprintf(`
            DefaultFunc:    %s,`, entry.DefaultFunc)
        } else {
            extraParams += fmt.Sprintf(`
            Default:    %s,`, entry.Default)
        }

        if entry.StateFunc != "" {
            extraParams += fmt.Sprintf(`
            StateFunc:    %s,`, entry.StateFunc)
        }

        if entry.ValidateDiagFunc != "" {
            extraParams += fmt.Sprintf(`
            ValidateDiagFunc:    %s,`, entry.ValidateDiagFunc)
        }

        body = body + fmt.Sprintf(`
        "%s": {
            Type:                    %s,
            Description:    "%s",
            %s
        },
    `, entry.Parameter, "schema.Type"+entry.Type, entry.Description, extraParams)
    }

    footer := `
    }
)

func GetClusterSchema() map[string]*schema.Schema {
    return clusterSchema
}
    `

    return header + body + footer
}

func getSchemaType(s string) SchemaType {
    if strings.Count(s, "'") == 2 || s == "" {
        return String
    } else if s == "true" || s == "false" {
        return Bool
    } else if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
        return Array
    }
    return Int
}

func contains(s []string, e string) bool {
    for _, a := range s {
        if a == e {
            return true
        }
    }
    return false
}