Skip to content

Commit

Permalink
Enable component references (#386)
Browse files Browse the repository at this point in the history
This change enables the use of component references in the stack schema. This allows users to reference components in the same stack, and have the schema automatically update to reflect the available outputs of the referenced component.

Co-authored-by: Ansgar Mertens <[email protected]>
  • Loading branch information
jpogran and ansgarm committed Aug 30, 2024
1 parent 46d3204 commit b7d440c
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 9 deletions.
4 changes: 2 additions & 2 deletions earlydecoder/stacks/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ func LoadStack(path string, files map[string]*hcl.File) (*stack.Meta, hcl.Diagno
sort.Strings(filenames)

components := make(map[string]stack.Component)
for key, variable := range mod.Components {
components[key] = *variable
for key, component := range mod.Components {
components[key] = *component
}

variables := make(map[string]stack.Variable)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/hc-install v0.8.0
github.com/hashicorp/hcl-lang v0.0.0-20240722075100-0f6cd5960306
github.com/hashicorp/hcl-lang v0.0.0-20240814155600-0ec9d4da3a96
github.com/hashicorp/hcl/v2 v2.22.0
github.com/hashicorp/terraform-exec v0.21.0
github.com/hashicorp/terraform-json v0.22.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI=
github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU=
github.com/hashicorp/hcl-lang v0.0.0-20240722075100-0f6cd5960306 h1:VQ2epS4r2HtNzKJhJVR8sIHdsMV7L80lCvwJkX02hd0=
github.com/hashicorp/hcl-lang v0.0.0-20240722075100-0f6cd5960306/go.mod h1:4uIIEYVWTw+fDs9LVKy1CPnL6Z0XcLOjmanj7fVCIpE=
github.com/hashicorp/hcl-lang v0.0.0-20240814155600-0ec9d4da3a96 h1:WVwJNemS0Him2WhrRUrc6fO/e49w238IcyKXDYoo71w=
github.com/hashicorp/hcl-lang v0.0.0-20240814155600-0ec9d4da3a96/go.mod h1:4uIIEYVWTw+fDs9LVKy1CPnL6Z0XcLOjmanj7fVCIpE=
github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
Expand Down
2 changes: 2 additions & 0 deletions internal/schema/refscope/scopes.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ var (
ProviderScope = lang.ScopeId("provider")
ResourceScope = lang.ScopeId("resource")
VariableScope = lang.ScopeId("variable")

ComponentScope = lang.ScopeId("component")
)
12 changes: 12 additions & 0 deletions internal/schema/stacks/1.9/component_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ import (
func componentBlockSchema() *schema.BlockSchema {
return &schema.BlockSchema{
Description: lang.Markdown("Component represents the declaration of a single component within a particular Terraform Stack. Components are the most important object in a stack configuration, just as resources are the most important object in a Terraform module: each one refers to a Terraform module that describes the infrastructure that the component is 'made of'."),
Address: &schema.BlockAddrSchema{
Steps: []schema.AddrStep{
schema.StaticStep{Name: "component"},
schema.LabelStep{Index: 0},
},
FriendlyName: "component",
ScopeId: refscope.ComponentScope,
AsReference: true,
},
Labels: []*schema.LabelSchema{
{
Name: "name",
Expand All @@ -32,6 +41,9 @@ func componentBlockSchema() *schema.BlockSchema {
IsDepKey: true,
Constraint: schema.LiteralType{Type: cty.String},
SemanticTokenModifiers: lang.SemanticTokenModifiers{lang.TokenModifierDependent},
CompletionHooks: lang.CompletionHooks{
{Name: "CompleteLocalModuleSources"},
},
},
"inputs": {
Description: lang.Markdown("A mapping of module input variable names to values. The keys of this map must correspond to the Terraform variable names in the module defined by source. Can be any Terraform expression, and can refer to anything which is in scope, including input variables, component outputs, the `each` object, and provider configurations"),
Expand Down
1 change: 1 addition & 0 deletions schema/language_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ package schema
const (
ModuleLanguageID = "terraform"
VariablesLanguageID = "terraform-vars"
StackLanguageID = "terraform-stack"
)
130 changes: 126 additions & 4 deletions schema/stacks/stack_schema_merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ package schema

import (
"path/filepath"
"sort"

"github.com/hashicorp/go-version"
"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"
tfmod "github.com/hashicorp/terraform-schema/module"
tfschema "github.com/hashicorp/terraform-schema/schema"
"github.com/hashicorp/terraform-schema/stack"
Expand Down Expand Up @@ -42,7 +45,7 @@ func (m *StackSchemaMerger) SetStateReader(mr StateReader) {
m.stateReader = mr
}

func (m *StackSchemaMerger) SchemaForModule(meta *stack.Meta) (*schema.BodySchema, error) {
func (m *StackSchemaMerger) SchemaForStack(meta *stack.Meta) (*schema.BodySchema, error) {
if m.coreSchema == nil {
return nil, tfschema.CoreSchemaRequiredErr{}
}
Expand Down Expand Up @@ -101,7 +104,7 @@ func (m *StackSchemaMerger) SchemaForModule(meta *stack.Meta) (*schema.BodySchem
mergedSchema.Blocks["variable"].DependentBody = variableDependentBody(meta.Variables)
}

for _, comp := range meta.Components {
for name, comp := range meta.Components {
depKeys := schema.DependencyKeys{
Attributes: []schema.AttributeDependent{
{
Expand All @@ -112,13 +115,14 @@ func (m *StackSchemaMerger) SchemaForModule(meta *stack.Meta) (*schema.BodySchem
},
},
}

switch sourceAddr := comp.SourceAddr.(type) {
case tfmod.LocalSourceAddr:
path := filepath.Join(meta.Path, sourceAddr.String())

modMeta, err := m.stateReader.LocalModuleMeta(path)
if err == nil {
depSchema, err := schemaForDependentComponentBlock(modMeta)
depSchema, err := schemaForDependentComponentBlock(modMeta, comp, name)
if err == nil {
mergedSchema.Blocks["component"].DependentBody[schema.NewSchemaKey(depKeys)] = depSchema
}
Expand Down Expand Up @@ -152,7 +156,7 @@ func variableDependentBody(vars map[string]stack.Variable) map[schema.SchemaKey]
return depBodies
}

func schemaForDependentComponentBlock(modMeta *tfmod.Meta) (*schema.BodySchema, error) {
func schemaForDependentComponentBlock(modMeta *tfmod.Meta, component stack.Component, componentName string) (*schema.BodySchema, error) {
inputs := make(map[string]*schema.AttributeSchema, 0)
providers := make(map[string]*schema.AttributeSchema, 0)

Expand All @@ -163,6 +167,20 @@ func schemaForDependentComponentBlock(modMeta *tfmod.Meta) (*schema.BodySchema,
}
aSchema := tfschema.ModuleVarToAttribute(modVar)
aSchema.Constraint = tfschema.ConvertAttributeTypeToConstraint(varType)
aSchema.OriginForTarget = &schema.PathTarget{
Address: schema.Address{
schema.StaticStep{Name: "var"},
schema.AttrNameStep{},
},
Path: lang.Path{
Path: modMeta.Path,
LanguageID: tfschema.ModuleLanguageID,
},
Constraints: schema.Constraints{
ScopeId: refscope.VariableScope,
Type: varType,
},
}

inputs[name] = aSchema
}
Expand Down Expand Up @@ -196,5 +214,109 @@ func schemaForDependentComponentBlock(modMeta *tfmod.Meta) (*schema.BodySchema,
},
}

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

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

for name, output := range modMeta.Outputs {
addr := lang.Address{
lang.RootStep{Name: "component"},
lang.AttrStep{Name: componentName},
lang.AttrStep{Name: name},
}

typ := cty.DynamicPseudoType
if !output.Value.IsNull() {
typ = output.Value.Type()
}

targetable := &schema.Targetable{
Address: addr,
ScopeId: refscope.ComponentScope,
AsType: typ,
IsSensitive: output.IsSensitive,
NestedTargetables: schema.NestedTargetablesForValue(addr, refscope.ComponentScope, output.Value),
}
if output.Description != "" {
targetable.Description = lang.PlainText(output.Description)
}

targetableOutputs = append(targetableOutputs, targetable)

modOutputTypes[name] = typ
modOutputVals[name] = output.Value

impliedOrigins = append(impliedOrigins, schema.ImpliedOrigin{
OriginAddress: lang.Address{
lang.RootStep{Name: "component"},
lang.AttrStep{Name: componentName},
lang.AttrStep{Name: name},
},
TargetAddress: lang.Address{
lang.RootStep{Name: "output"},
lang.AttrStep{Name: name},
},
Path: lang.Path{
Path: modMeta.Path,
LanguageID: tfschema.ModuleLanguageID,
},
Constraints: schema.Constraints{
ScopeId: refscope.OutputScope,
},
})
}

bodySchema.ImpliedOrigins = impliedOrigins

sort.Sort(targetableOutputs)

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

if len(modMeta.Filenames) > 0 {
filename := modMeta.Filenames[0]

// Prioritize main.tf based on best practices as documented at
// https://learn.hashicorp.com/tutorials/terraform/module-create
if sliceContains(modMeta.Filenames, "main.tf") {
filename = "main.tf"
}

bodySchema.Targets = &schema.Target{
Path: lang.Path{
Path: modMeta.Path,
LanguageID: tfschema.ModuleLanguageID,
},
Range: hcl.Range{
Filename: filename,
Start: hcl.InitialPos,
End: hcl.InitialPos,
},
}
}

return bodySchema, nil
}

func sliceContains(slice []string, value string) bool {
for _, val := range slice {
if val == value {
return true
}
}
return false
}

0 comments on commit b7d440c

Please sign in to comment.