Skip to content

Commit

Permalink
Parse module calls + build module schema based on Registry data (#113)
Browse files Browse the repository at this point in the history
* progress

* add failing test + TODOs

* Use cty for variable type & default

* implement dependent schema for registry module

* adjust names & interfaces to reflect other module sources

* reflect module input types of default vals

* make input & output descriptions markup-type-aware

Co-authored-by: Radek Simko <radek.simko@gmail.com>
  • Loading branch information
jpogran and radeksimko authored Jun 17, 2022
1 parent c432f4c commit 9aece33
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 10 deletions.
12 changes: 10 additions & 2 deletions earlydecoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ module "name" {
ModuleCalls: map[string]module.DeclaredModuleCall{
"name": {
LocalName: "name",
SourceAddr: "registry.terraform.io/terraform-aws-modules/vpc/aws",
SourceAddr: MustParseRawModuleSourceRegistry("registry.terraform.io/terraform-aws-modules/vpc/aws"),
},
},
},
Expand Down Expand Up @@ -1153,7 +1153,7 @@ module "name" {
ModuleCalls: map[string]module.DeclaredModuleCall{
"name": {
LocalName: "name",
SourceAddr: "terraform-aws-modules/vpc/aws",
SourceAddr: MustParseRawModuleSourceRegistry("terraform-aws-modules/vpc/aws"),
Version: version.MustConstraints(version.NewConstraint("1.0.0")),
},
},
Expand Down Expand Up @@ -1192,3 +1192,11 @@ func runTestCases(testCases []testCase, t *testing.T, path string) {
func compareVersionConstraint(x, y *version.Constraint) bool {
return x.Equals(y)
}

func MustParseRawModuleSourceRegistry(source string) tfaddr.ModuleSourceRegistry {
m, err := tfaddr.ParseRawModuleSourceRegistry(source)
if err != nil {
panic(err)
}
return m
}
10 changes: 9 additions & 1 deletion earlydecoder/load_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform-schema/backend"
"github.com/hashicorp/terraform-schema/internal/typeexpr"
"github.com/hashicorp/terraform-schema/module"
Expand Down Expand Up @@ -305,9 +306,16 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics {
}
}

var sourceAddr module.ModuleSourceAddr
registryAddr, err := tfaddr.ParseRawModuleSourceRegistry(source)
if err == nil {
sourceAddr = registryAddr
}
// TODO: module.LocalSourceAddr

mod.ModuleCalls[name] = &module.DeclaredModuleCall{
LocalName: name,
SourceAddr: source,
SourceAddr: sourceAddr,
Version: versionCons,
}
}
Expand Down
13 changes: 11 additions & 2 deletions module/module_calls.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package module

import "github.com/hashicorp/go-version"
import (
"github.com/hashicorp/go-version"
)

type ModuleCalls struct {
Installed map[string]InstalledModuleCall
Expand All @@ -16,6 +18,13 @@ type InstalledModuleCall struct {

type DeclaredModuleCall struct {
LocalName string
SourceAddr string
SourceAddr ModuleSourceAddr
Version version.Constraints
}

type ModuleSourceAddr interface {
ForDisplay() string
String() string
}

// TODO: type LocalSourceAddr
26 changes: 26 additions & 0 deletions registry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package registry

import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/lang"
"github.com/zclconf/go-cty/cty"
)

type ModuleData struct {
Version *version.Version
Inputs []Input
Outputs []Output
}

type Input struct {
Name string
Type cty.Type
Description lang.MarkupContent
Default cty.Value
Required bool
}

type Output struct {
Name string
Description lang.MarkupContent
}
91 changes: 91 additions & 0 deletions schema/module_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,100 @@ import (
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform-schema/internal/schema/refscope"
"github.com/hashicorp/terraform-schema/module"
"github.com/hashicorp/terraform-schema/registry"
"github.com/zclconf/go-cty/cty"
)

func schemaForDeclaredDependentModuleBlock(module module.DeclaredModuleCall, modMeta *registry.ModuleData) (*schema.BodySchema, error) {
attributes := make(map[string]*schema.AttributeSchema, 0)

for _, input := range modMeta.Inputs {
aSchema := &schema.AttributeSchema{
Description: input.Description,
}
if input.Required {
aSchema.IsRequired = true
} else {
aSchema.IsOptional = true
}

typ := input.Type
defaultType := input.Default.Type()
if typ == cty.DynamicPseudoType && (defaultType != cty.DynamicPseudoType && defaultType != cty.NilType) {
typ = defaultType
}

aSchema.Expr = convertAttributeTypeToExprConstraints(typ)

attributes[input.Name] = aSchema
}

bodySchema := &schema.BodySchema{
Attributes: attributes,
}

if module.LocalName == "" {
// avoid creating output refs if we don't have reference name
return bodySchema, nil
}

modOutputTypes := make(map[string]cty.Type, 0)
targetableOutputs := make(schema.Targetables, 0)

for _, output := range modMeta.Outputs {
addr := lang.Address{
lang.RootStep{Name: "module"},
lang.AttrStep{Name: module.LocalName},
lang.AttrStep{Name: output.Name},
}

targetable := &schema.Targetable{
Address: addr,
AsType: cty.DynamicPseudoType,
ScopeId: refscope.ModuleScope,
Description: output.Description,
// The Registry API doesn't tell us anything more about output type structure
// so we cannot target nested fields within objects, maps or lists
}

modOutputTypes[output.Name] = cty.DynamicPseudoType
targetableOutputs = append(targetableOutputs, targetable)
}

sort.Sort(targetableOutputs)

addr := lang.Address{
lang.RootStep{Name: "module"},
lang.AttrStep{Name: module.LocalName},
}
bodySchema.TargetableAs = append(bodySchema.TargetableAs, &schema.Targetable{
Address: addr,
ScopeId: refscope.ModuleScope,
AsType: cty.Object(modOutputTypes),
NestedTargetables: targetableOutputs,
})

sourceAddr, ok := module.SourceAddr.(tfaddr.ModuleSourceRegistry)
if ok && sourceAddr.PackageAddr.Host == "registry.terraform.io" {
versionStr := ""
if modMeta.Version == nil {
versionStr = "latest"
} else {
versionStr = modMeta.Version.String()
}

bodySchema.DocsLink = &schema.DocsLink{
URL: fmt.Sprintf(
`https://registry.terraform.io/modules/%s/%s`,
sourceAddr.PackageAddr.ForRegistryProtocol(),
versionStr,
),
}
}

return bodySchema, nil
}

func schemaForDependentModuleBlock(module module.InstalledModuleCall, modMeta *module.Meta) (*schema.BodySchema, error) {
attributes := make(map[string]*schema.AttributeSchema, 0)

Expand Down
118 changes: 118 additions & 0 deletions schema/module_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform-schema/internal/schema/refscope"
"github.com/hashicorp/terraform-schema/module"
"github.com/hashicorp/terraform-schema/registry"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)
Expand Down Expand Up @@ -423,3 +425,119 @@ func TestSchemaForDependentModuleBlock_DocsLink(t *testing.T) {
}
}
}

func TestSchemaForDeclaredDependentModuleBlock_basic(t *testing.T) {
meta := &registry.ModuleData{
Version: version.Must(version.NewVersion("1.0.0")),
Inputs: []registry.Input{
{
Name: "example_var",
Type: cty.String,
Description: lang.PlainText("Test var"),
Required: true,
},
{
Name: "foo_var",
Type: cty.DynamicPseudoType,
Default: cty.NumberIntVal(42),
},
{
Name: "another_var",
Type: cty.DynamicPseudoType,
},
},
Outputs: []registry.Output{
{
Name: "first",
Description: lang.PlainText("first output"),
},
{
Name: "second",
Description: lang.PlainText("second output"),
},
},
}
module := module.DeclaredModuleCall{
LocalName: "refname",
SourceAddr: MustParseModuleSource("terraform-aws-modules/eks/aws"),
}
depSchema, err := schemaForDeclaredDependentModuleBlock(module, meta)
if err != nil {
t.Fatal(err)
}
expectedDepSchema := &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"example_var": {
Expr: schema.ExprConstraints{
schema.TraversalExpr{OfType: cty.String},
schema.LiteralTypeExpr{Type: cty.String},
},
Description: lang.PlainText("Test var"),
IsRequired: true,
},
"foo_var": {
Expr: schema.ExprConstraints{
schema.TraversalExpr{OfType: cty.Number},
schema.LiteralTypeExpr{Type: cty.Number},
},
IsOptional: true,
},
"another_var": {
Expr: schema.ExprConstraints{
schema.TraversalExpr{OfType: cty.DynamicPseudoType},
schema.LiteralTypeExpr{Type: cty.DynamicPseudoType},
},
IsOptional: true,
},
},
TargetableAs: []*schema.Targetable{
{
Address: lang.Address{
lang.RootStep{Name: "module"},
lang.AttrStep{Name: "refname"},
},
ScopeId: refscope.ModuleScope,
AsType: cty.Object(map[string]cty.Type{
"first": cty.DynamicPseudoType,
"second": cty.DynamicPseudoType,
}),
NestedTargetables: []*schema.Targetable{
{
Address: lang.Address{
lang.RootStep{Name: "module"},
lang.AttrStep{Name: "refname"},
lang.AttrStep{Name: "first"},
},
ScopeId: refscope.ModuleScope,
AsType: cty.DynamicPseudoType,
Description: lang.PlainText("first output"),
},
{
Address: lang.Address{
lang.RootStep{Name: "module"},
lang.AttrStep{Name: "refname"},
lang.AttrStep{Name: "second"},
},
ScopeId: refscope.ModuleScope,
AsType: cty.DynamicPseudoType,
Description: lang.PlainText("second output"),
},
},
},
},
DocsLink: &schema.DocsLink{
URL: "https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/1.0.0",
},
}
if diff := cmp.Diff(expectedDepSchema, depSchema, ctydebug.CmpOptions); diff != "" {
t.Fatalf("schema mismatch: %s", diff)
}
}

func MustParseModuleSource(raw string) tfaddr.ModuleSourceRegistry {
addr, err := tfaddr.ParseRawModuleSourceRegistry(raw)
if err != nil {
panic(err)
}
return addr
}
Loading

0 comments on commit 9aece33

Please sign in to comment.