terraform-provider-google/scripts/schemagen.go
Dana Hoffman 7faec27fba Schema Generator script (#215)
* Initial commit for schema generator script

* vendor discovery api

* respond to comments

* Don't fail on formatting errors

* respond to comments

* update tests with latest changes
2017-08-01 14:46:55 -07:00

312 lines
8.5 KiB
Go

// Generates an initial version of a schema for a new resource type.
//
// This script draws heavily from https://github.com/radeksimko/terraform-gen,
// but uses GCP's discovery API instead of the struct definition to generate
// the schemas.
//
// This is not meant to be a definitive source of truth for resource schemas,
// just a starting point. It has some notable deficiencies, such as:
// * No way to differentiate between fields that are/are not updateable.
// * Required/Optional/Computed are set based on keywords in the description.
//
// Usage requires credentials. Obtain via gcloud:
//
// gcloud auth application-default login
//
// Usage example (from root dir):
//
// go run ./scripts/schemagen.go -api pubsub -resource Subscription -version v1
//
// This will output a file in the directory from which the script is run named `gen_resource_[api]_[resource].go`.
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"log"
"os"
"regexp"
"sort"
"strings"
"text/template"
"github.com/hashicorp/terraform/helper/schema"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/discovery/v1"
)
func main() {
api := flag.String("api", "", "api to query")
resource := flag.String("resource", "", "resource to generate")
version := flag.String("version", "v1", "api version to query")
flag.Parse()
if *api == "" || *resource == "" {
flag.PrintDefaults()
log.Fatal("usage: go run schemagen.go -api $API -resource $RESOURCE -version $VERSION")
}
// Discovery API doesn't need authentication
client, err := google.DefaultClient(oauth2.NoContext, []string{}...)
if err != nil {
log.Fatal(fmt.Errorf("Error creating client: %v", err))
}
discoveryService, err := discovery.New(client)
if err != nil {
log.Fatal(fmt.Errorf("Error creating service: %v", err))
}
resp, err := discoveryService.Apis.GetRest(*api, *version).Fields("schemas").Do()
if err != nil {
log.Fatal(fmt.Errorf("Error reading API: %v", err))
}
fileName := fmt.Sprintf("gen_resource_%s_%s.go", *api, underscore(*resource))
f, err := os.Create(fileName)
defer f.Close()
if err != nil {
log.Fatal(err)
}
required, optional, computed := generateFields(resp.Schemas, *resource)
buf := &bytes.Buffer{}
err = googleTemplate.Execute(buf, struct {
TypeName string
ReqFields map[string]string
OptFields map[string]string
ComFields map[string]string
}{
// Capitalize the first letter of the api name, then concatenate the resource name onto it.
// e.g. compute, instance -> ComputeInstance
TypeName: strings.ToUpper((*api)[0:1]) + (*api)[1:] + *resource,
ReqFields: required,
OptFields: optional,
ComFields: computed,
})
if err != nil {
log.Fatal(err)
}
fmtd, err := format.Source(buf.Bytes())
if err != nil {
log.Printf("Formatting error: %s", err)
}
if _, err := f.Write(fmtd); err != nil {
log.Fatal(err)
}
}
func generateFields(jsonSchemas map[string]discovery.JsonSchema, property string) (required, optional, computed map[string]string) {
required = make(map[string]string, 0)
optional = make(map[string]string, 0)
computed = make(map[string]string, 0)
for k, v := range jsonSchemas[property].Properties {
content, err := generateField(jsonSchemas, k, v, false)
if err != nil {
log.Printf("ERROR: %s", err)
} else {
if strings.Contains(content, "Required:") {
required[underscore(k)] = content
} else if strings.Contains(content, "Optional:") {
optional[underscore(k)] = content
} else if strings.Contains(content, "Computed:") {
computed[underscore(k)] = content
} else {
log.Println("ERROR: Found property that is neither required, optional, nor computed")
}
}
}
return
}
func generateField(jsonSchemas map[string]discovery.JsonSchema, field string, v discovery.JsonSchema, isNested bool) (string, error) {
s := &schema.Schema{
Description: v.Description,
}
if field != "" {
setProperties(v, s)
}
// JSON field types: https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
switch v.Type {
case "integer":
s.Type = schema.TypeInt
case "number":
s.Type = schema.TypeFloat
case "string":
s.Type = schema.TypeString
case "boolean":
s.Type = schema.TypeBool
case "array":
s.Type = schema.TypeList
elem, err := generateField(jsonSchemas, "", *v.Items, true)
if err != nil {
return "", fmt.Errorf("Unable to generate Elem for %q: %s", field, err)
}
s.Elem = elem
case "object":
s.Type = schema.TypeMap
case "":
s.Type = schema.TypeList
s.MaxItems = 1
elem := "&schema.Resource{\nSchema: map[string]*schema.Schema{\n"
required, optional, computed := generateFields(jsonSchemas, v.Ref)
elem += generateNestedElem(required)
elem += generateNestedElem(optional)
elem += generateNestedElem(computed)
elem += "},\n}"
if isNested {
return elem, nil
}
s.Elem = elem
default:
return "", fmt.Errorf("Unable to process: %s %s", field, v.Type)
}
return schemaCode(s, isNested)
}
func setProperties(v discovery.JsonSchema, s *schema.Schema) {
if v.ReadOnly || strings.HasPrefix(v.Description, "Output-only") || strings.HasPrefix(v.Description, "[Output Only]") {
s.Computed = true
} else {
if v.Required || strings.HasPrefix(v.Description, "Required") {
s.Required = true
} else {
s.Optional = true
}
}
s.ForceNew = true
}
func generateNestedElem(fields map[string]string) (elem string) {
fieldNames := []string{}
for k, _ := range fields {
fieldNames = append(fieldNames, k)
}
sort.Strings(fieldNames)
for _, k := range fieldNames {
elem += fmt.Sprintf("%q: %s,\n", k, fields[k])
}
return
}
func schemaCode(s *schema.Schema, isNested bool) (string, error) {
buf := bytes.NewBuffer([]byte{})
err := schemaTemplate.Execute(buf, struct {
Schema *schema.Schema
IsNested bool
}{
Schema: s,
IsNested: isNested,
})
if err != nil {
return "", err
}
return buf.String(), nil
}
// go version of https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case,
// with some extra logic around ending in 's' to handle the "externalIPs" case.
func underscore(name string) string {
endsInS := strings.HasSuffix(name, "s")
if endsInS {
name = strings.TrimSuffix(name, "s")
}
firstCap := regexp.MustCompile("(.)([A-Z][a-z]+)").ReplaceAllString(name, "${1}_${2}")
allCap := regexp.MustCompile("([a-z0-9])([A-Z])").ReplaceAllString(firstCap, "${1}_${2}")
if endsInS {
allCap = allCap + "s"
}
return strings.ToLower(allCap)
}
var schemaTemplate = template.Must(template.New("schema").Parse(`{{if .IsNested}}&schema.Schema{{end}}{{"{"}}{{if not .IsNested}}
{{end}}Type: schema.{{.Schema.Type}},{{if ne .Schema.Description ""}}
Description: {{printf "%q" .Schema.Description}},{{end}}{{if .Schema.Required}}
Required: {{.Schema.Required}},{{end}}{{if .Schema.Optional}}
Optional: {{.Schema.Optional}},{{end}}{{if .Schema.ForceNew}}
ForceNew: {{.Schema.ForceNew}},{{end}}{{if .Schema.Computed}}
Computed: {{.Schema.Computed}},{{end}}{{if gt .Schema.MaxItems 0}}
MaxItems: {{.Schema.MaxItems}},{{end}}{{if .Schema.Elem}}
Elem: {{.Schema.Elem}},{{end}}{{if not .IsNested}}
{{end}}{{"}"}}`))
var googleTemplate = template.Must(template.New("google").Parse(`package google
import(
"github.com/hashicorp/terraform/helper/schema"
)
func resource{{.TypeName}}() *schema.Resource {
return &schema.Resource{
Create: resource{{.TypeName}}Create,
Read: resource{{.TypeName}}Read,
Update: resource{{.TypeName}}Update,
Delete: resource{{.TypeName}}Delete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{ {{range $name, $schema := .ReqFields}}
"{{ $name }}": {{ $schema }},
{{end}}{{range $name, $schema := .OptFields}}
"{{ $name }}": {{ $schema }},
{{end}}{{range $name, $schema := .ComFields}}
"{{ $name }}": {{ $schema }},
{{end}}
},
}
}
func resource{{.TypeName}}Create(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
}
func resource{{.TypeName}}Read(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
}
func resource{{.TypeName}}Update(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
}
func resource{{.TypeName}}Delete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
}
`))