Skip to content

Commit

Permalink
Merge pull request #6 from emilgelman/refactor-gengo
Browse files Browse the repository at this point in the history
simplify mapping by using gengo to load structs from source code
  • Loading branch information
emilgelman authored Oct 30, 2021
2 parents 8a22613 + 6a8419e commit 44d9356
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 289 deletions.
37 changes: 9 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,28 @@
# terraform-schema-gen

This repository contains a CLI to
generate [Terraform schema](https://www.terraform.io/docs/extend/schemas/schema-types.html) out of Go structs. The
generator relies on [kube-openapi](https://github.com/kubernetes/kube-openapi) as an intermediate step in the generation
process.
generate [Terraform schema](https://www.terraform.io/docs/extend/schemas/schema-types.html) out of Go structs.

The generator will convert nested structs (at any level) to the equivalent Terraform schema.
The generator will convert nested Go structs (at any level) to the equivalent Terraform schema.

Supported terraform schema properties:
* Required
* Description
The schemas' `Required` or `Optional` property is set based on the json `omitempty` tag.
If omitempty is set, the property is marked as Optional, else as Required.

### Known limitations

Terraform functions and validations are not supported, as this generator relies on Go struct tags.
Terraform functions and validations are not supported, as there is no current way to express them from struct properties.

## Usage

1. Mark your structs with the following comment `// +k8s:openapi-gen=true`
2. Create a header.txt file, to be used with the kube-openapi generator as heading for generated files (can be empty)
2. Run the kube-openapi generator to generate an OpenAPI spec of your structs:

```shell
go run k8s.io/kube-openapi/cmd/openapi-gen -i <input directory> -p <output directory> -h ./header.txt -o <output base>
```

3. Compile the generated OpenAPI spec as a Go plugin

```shell
go build -buildmode=plugin -o <output_file> <openapi_generated.go file>
```

4. Run the terraform-schema-gen CLI to convert the compiled plugin to Terraform schema:
1. Run the terraform-schema-gen CLI to convert a directory containing Go structs to a Terraform schema:
```
go run github.com/emilgelman/terraform-schema-gen gen --input <openapi_generated.so> --output terraform_schema_generated.go --package <output package name>
go run github.com/emilgelman/terraform-schema-gen gen --input <input directory> --output terraform_schema_generated.go --package <output package name>
```

The entire process can be bundled in a single go file utilizing `go generate`, for example:
The command can be bundled in a `go generate` for automation, for example:
```go
package generate

//go:generate go run k8s.io/kube-openapi/cmd/openapi-gen -i ./v1 -p ./output/v1/main -h ./header.txt -o .
//go:generate go build -buildmode=plugin -o ./output/v1/main/openapi_generated.so ./output/v1/main/openapi_generated.go
//go:generate go run github.com/emilgelman/terraform-schema-gen gen --input ./output/v1/main/openapi_generated.so --output ./output/v1/terraform_schema_generated.go --package v1
//go:generate go run github.com/emilgelman/terraform-schema-gen gen --input ./input --output ./output/v1/terraform_schema_generated.go --package v1
```

6 changes: 3 additions & 3 deletions cmd/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"github.com/emilgelman/terraform-schema-gen/pkg/exporter"
"github.com/emilgelman/terraform-schema-gen/pkg/mapper"
"github.com/emilgelman/terraform-schema-gen/pkg/openapi"
"github.com/emilgelman/terraform-schema-gen/pkg/parser"
"github.com/spf13/cobra"

"github.com/emilgelman/terraform-schema-gen/pkg/generator"
Expand All @@ -17,7 +17,7 @@ var genCmd = &cobra.Command{
Long: `gen`,
Run: nil,
RunE: func(cmd *cobra.Command, args []string) error {
loader := openapi.New(config.Input)
loader := parser.New(config.Input)
mapper := mapper.New()
exporter := exporter.New(config.Output, config.OutputPackage)
g := generator.New(loader, mapper, exporter)
Expand All @@ -27,7 +27,7 @@ var genCmd = &cobra.Command{

//nolint: errcheck
func init() {
genCmd.Flags().StringVarP(&config.Input, "input", "i", "", "input file")
genCmd.Flags().StringVarP(&config.Input, "input", "i", "", "input directory")
genCmd.MarkFlagRequired("input")
genCmd.Flags().StringVarP(&config.Output, "output", "o", "", "output file")
genCmd.MarkFlagRequired("output")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ require (
github.com/hexops/valast v1.4.0
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect
k8s.io/kube-openapi v0.0.0-20211014175136-b3fe75cc9b2f
)
22 changes: 11 additions & 11 deletions pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,36 @@ package generator

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/gengo/types"
)

type OpenAPIDefinitionLoader interface {
Load() (map[string]common.OpenAPIDefinition, error)
type StructParser interface {
Parse() ([]*types.Type, error)
}
type OpenAPIDefinitionMapper interface {
Map(map[string]common.OpenAPIDefinition) map[string]map[string]*schema.Schema
Map([]*types.Type) map[string]map[string]*schema.Schema
}

type SchemaExporter interface {
Export(map[string]map[string]*schema.Schema) error
}

type Generator struct {
definitionLoader OpenAPIDefinitionLoader
mapper OpenAPIDefinitionMapper
exporter SchemaExporter
parser StructParser
mapper OpenAPIDefinitionMapper
exporter SchemaExporter
}

func New(definitionLoader OpenAPIDefinitionLoader, mapper OpenAPIDefinitionMapper, exporter SchemaExporter) *Generator {
return &Generator{definitionLoader: definitionLoader, mapper: mapper, exporter: exporter}
func New(parser StructParser, mapper OpenAPIDefinitionMapper, exporter SchemaExporter) *Generator {
return &Generator{parser: parser, mapper: mapper, exporter: exporter}
}

func (g *Generator) Generate() error {
definitions, err := g.definitionLoader.Load()
parsedTypes, err := g.parser.Parse()
if err != nil {
return err
}
schemas := g.mapper.Map(definitions)
schemas := g.mapper.Map(parsedTypes)
if err := g.exporter.Export(schemas); err != nil {
return err
}
Expand Down
165 changes: 91 additions & 74 deletions pkg/mapper/mapper.go
Original file line number Diff line number Diff line change
@@ -1,103 +1,120 @@
package mapper

import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/gengo/types"
)

const array = "array"

type Mapper struct {
}

type SchemaDefinition struct {
Name string
Definition common.OpenAPIDefinition
}

func New() *Mapper {
return &Mapper{}
}

func (m *Mapper) Map(definitions map[string]common.OpenAPIDefinition) map[string]map[string]*schema.Schema {
stack := m.createDefinitionsStack(definitions)
return m.parseDefinitionsStack(stack)
}

func (m *Mapper) createDefinitionsStack(definitions map[string]common.OpenAPIDefinition) []SchemaDefinition {
var stack []SchemaDefinition
for name := range definitions {
definition := definitions[name]
stack = append(stack, SchemaDefinition{Name: strings.ToLower(name), Definition: definition})
for _, dependency := range definition.Dependencies {
stack = append(stack, SchemaDefinition{Name: strings.ToLower(dependency), Definition: definitions[dependency]})
}
func (m *Mapper) Map(parsedTypes []*types.Type) map[string]map[string]*schema.Schema {
res := make(map[string]map[string]*schema.Schema)
for t := range parsedTypes {
s := make(map[string]*schema.Schema)
name := parsedTypes[t].Name.Name
traverse(parsedTypes[t], name, name, "", s)
res[name] = s
}
return stack
return res
}

func (m *Mapper) parseDefinitionsStack(stack []SchemaDefinition) map[string]map[string]*schema.Schema {
schemas := make(map[string]map[string]*schema.Schema)
for len(stack) > 0 {
definition := stack[len(stack)-1]
stack = stack[:len(stack)-1]
tfSchema := make(map[string]*schema.Schema)
m.parseDefinition(definition.Name, definition.Name, &definition.Definition.Schema, tfSchema, schemas)
schemas[strings.ToLower(definition.Name)] = tfSchema
}
return schemas
}
func traverse(t *types.Type, name, rootName, tags string, s map[string]*schema.Schema) {
name = strings.ToLower(name)
rootName = strings.ToLower(rootName)
switch t.Kind {
// The first cases handles nested structures and translates them recursively
// If it is a pointer we need to unwrap and recurse
case types.Pointer:
traverse(t.Elem, name, rootName, "", s)

func (m *Mapper) parseDefinition(rootName, name string, openapiSchema *spec.Schema,
tfSchema map[string]*schema.Schema, schemas map[string]map[string]*schema.Schema) {
for i := range openapiSchema.Properties {
prop := openapiSchema.Properties[i]
if prop.SchemaProps.Type == nil {
path := prop.Ref.Ref.GetURL().Path
ss := schemas[strings.ToLower(path)]
tfSchema[i] = &schema.Schema{Type: schema.TypeList, Elem: &schema.Resource{Schema: ss}}
continue
// If it is a struct we translate each field
case types.Struct:
x := make(map[string]*schema.Schema)
for _, member := range t.Members {
traverse(member.Type, member.Name, rootName, member.Tags, x)
}
if prop.SchemaProps.Type[0] == array {
if len(prop.SchemaProps.Items.Schema.Type) > 0 {
t := prop.SchemaProps.Items.Schema.Type[0]
tfSchema[i] = &schema.Schema{Type: schema.TypeList, Elem: &schema.Schema{Type: mapType(t)}}
continue
if name == rootName {
for k, v := range x {
s[k] = v
}
path := prop.SchemaProps.Items.Schema.Ref.Ref.GetURL().Path
ss := schemas[strings.ToLower(path)]
tfSchema[i] = &schema.Schema{Type: schema.TypeList, Elem: &schema.Resource{
Schema: ss,
}}
continue
return
}
m.parseDefinition(rootName, i, &prop, tfSchema, schemas)
}
if name == rootName {
for i := range openapiSchema.Required {
tfSchema[openapiSchema.Required[i]].Required = true
converted := &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Resource{Schema: x},
}
setOptionalOrRequired(converted, tags)
s[name] = converted

// If it is a slice we translate the inner element type
case types.Slice:
x := make(map[string]*schema.Schema)
traverse(t.Elem, t.Name.Name, rootName, "", x)

if t.Elem.Kind == types.Builtin {
converted := &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString}, //TODO: handle other types of primitive slices
}
setOptionalOrRequired(converted, tags)
s[name] = converted
return
}
s[name] = x[strings.ToLower(t.Name.Name)]

// If it is a map return map[string]string //TODO: handle complex map structures
case types.Map:
converted := &schema.Schema{
Type: schema.TypeMap,
Elem: &schema.Schema{Type: schema.TypeString},
}
return

setOptionalOrRequired(converted, tags)
s[name] = converted

// Otherwise we cannot traverse anywhere so this finishes the the recursion
// If it is a builtin type translate it
case types.Builtin:
converted := convertBuiltinType(t, tags)
s[name] = converted

default:
fmt.Printf("Unknown type %+v", t)
}
}

func convertBuiltinType(t *types.Type, tags string) *schema.Schema {
var schemaType schema.ValueType
switch t.Name {
case types.String.Name:
schemaType = schema.TypeString
case types.Int.Name, types.Int32.Name, types.Int64.Name:
schemaType = schema.TypeInt
case types.Float.Name, types.Float32.Name, types.Float64.Name:
schemaType = schema.TypeFloat
case types.Bool.Name:
schemaType = schema.TypeBool

}
if openapiSchema.Type == nil {
return
converted := &schema.Schema{
Type: schemaType,
}
newSchema := &schema.Schema{}
tType := openapiSchema.Type[0]
newSchema.Type = mapType(tType)
newSchema.Description = openapiSchema.Description
tfSchema[name] = newSchema
setOptionalOrRequired(converted, tags)
return converted
}

func mapType(t string) schema.ValueType {
switch t {
case "object":
return schema.TypeMap
case array:
return schema.TypeList
func setOptionalOrRequired(s *schema.Schema, tags string) {
if strings.Contains(tags, "omitempty") {
s.Optional = true
} else {
s.Required = true
}
return schema.TypeString
}
Loading

0 comments on commit 44d9356

Please sign in to comment.