diff --git a/earlydecoder/stacks/decoder.go b/earlydecoder/stacks/decoder.go index 4edc0606..e2009850 100644 --- a/earlydecoder/stacks/decoder.go +++ b/earlydecoder/stacks/decoder.go @@ -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) diff --git a/go.mod b/go.mod index 2a17f6d5..f03c4a61 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 950d9976..9a003dcc 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/schema/refscope/scopes.go b/internal/schema/refscope/scopes.go index ad7f7e92..17a06105 100644 --- a/internal/schema/refscope/scopes.go +++ b/internal/schema/refscope/scopes.go @@ -16,4 +16,6 @@ var ( ProviderScope = lang.ScopeId("provider") ResourceScope = lang.ScopeId("resource") VariableScope = lang.ScopeId("variable") + + ComponentScope = lang.ScopeId("component") ) diff --git a/internal/schema/stacks/1.9/component_block.go b/internal/schema/stacks/1.9/component_block.go index 62739a22..b4df0e18 100644 --- a/internal/schema/stacks/1.9/component_block.go +++ b/internal/schema/stacks/1.9/component_block.go @@ -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", @@ -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"), diff --git a/schema/language_ids.go b/schema/language_ids.go index d8376e95..eb6820be 100644 --- a/schema/language_ids.go +++ b/schema/language_ids.go @@ -6,4 +6,5 @@ package schema const ( ModuleLanguageID = "terraform" VariablesLanguageID = "terraform-vars" + StackLanguageID = "terraform-stack" ) diff --git a/schema/stacks/stack_schema_merge.go b/schema/stacks/stack_schema_merge.go index f89148ee..35660909 100644 --- a/schema/stacks/stack_schema_merge.go +++ b/schema/stacks/stack_schema_merge.go @@ -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" @@ -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{} } @@ -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{ { @@ -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 } @@ -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) @@ -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 } @@ -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 +}