-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from emilgelman/refactor-gengo
simplify mapping by using gengo to load structs from source code
- Loading branch information
Showing
10 changed files
with
333 additions
and
289 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.