From 1ba4bb150900bcc406927164977c9f07919433ac Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 1 Jul 2025 12:41:15 +0530 Subject: [PATCH 01/27] Search: TF-27258: chore: add initial set of files for earlydecoder/search --- earlydecoder/search/decoder.go | 29 ++++++++++++++++++++++++++ earlydecoder/search/decoder_test.go | 4 ++++ earlydecoder/search/load_search.go | 27 ++++++++++++++++++++++++ earlydecoder/search/schema.go | 32 +++++++++++++++++++++++++++++ search/list.go | 4 ++++ search/meta.go | 9 ++++++++ search/variable.go | 24 ++++++++++++++++++++++ 7 files changed, 129 insertions(+) create mode 100644 earlydecoder/search/decoder.go create mode 100644 earlydecoder/search/decoder_test.go create mode 100644 earlydecoder/search/load_search.go create mode 100644 earlydecoder/search/schema.go create mode 100644 search/list.go create mode 100644 search/meta.go create mode 100644 search/variable.go diff --git a/earlydecoder/search/decoder.go b/earlydecoder/search/decoder.go new file mode 100644 index 0000000..b04cf8c --- /dev/null +++ b/earlydecoder/search/decoder.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package earlydecoder + +import ( + "github.com/hashicorp/hcl/v2" + tftest "github.com/hashicorp/terraform-schema/test" + "sort" +) + +func LoadTest(path string, files map[string]*hcl.File) (*tftest.Meta, hcl.Diagnostics) { + var diags hcl.Diagnostics + filenames := make([]string, 0) + + mod := newDecodedSearch() + for filename, f := range files { + filenames = append(filenames, filename) + fDiags := loadSearchFromFile(f, mod) + diags = append(diags, fDiags...) + } + + sort.Strings(filenames) + + return &tftest.Meta{ + Path: path, + Filenames: filenames, + }, diags +} diff --git a/earlydecoder/search/decoder_test.go b/earlydecoder/search/decoder_test.go new file mode 100644 index 0000000..cb1b0a3 --- /dev/null +++ b/earlydecoder/search/decoder_test.go @@ -0,0 +1,4 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package earlydecoder diff --git a/earlydecoder/search/load_search.go b/earlydecoder/search/load_search.go new file mode 100644 index 0000000..fadfbfc --- /dev/null +++ b/earlydecoder/search/load_search.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package earlydecoder + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform-schema/search" +) + +type decodedSearch struct { + List map[string]*search.List + Variables map[string]*search.Variable +} + +func newDecodedSearch() *decodedSearch { + return &decodedSearch{ + List: make(map[string]*search.List), + Variables: make(map[string]*search.Variable), + } +} + +func loadSearchFromFile(file *hcl.File, _ *decodedSearch) hcl.Diagnostics { + var diags hcl.Diagnostics + // TODO Implementation + return diags +} diff --git a/earlydecoder/search/schema.go b/earlydecoder/search/schema.go new file mode 100644 index 0000000..6f6b4a7 --- /dev/null +++ b/earlydecoder/search/schema.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package earlydecoder + +import ( + "github.com/hashicorp/hcl/v2" +) + +var rootSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "list", + LabelNames: []string{"type", "name"}, + }, + { + Type: "variable", + LabelNames: []string{"name"}, + }, + }, +} + +var listSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "include_resource", + }, + { + Name: "limit", + }, + }, +} diff --git a/search/list.go b/search/list.go new file mode 100644 index 0000000..5df1461 --- /dev/null +++ b/search/list.go @@ -0,0 +1,4 @@ +package search + +type List struct { +} diff --git a/search/meta.go b/search/meta.go new file mode 100644 index 0000000..30928ce --- /dev/null +++ b/search/meta.go @@ -0,0 +1,9 @@ +package search + +type Meta struct { + Path string + Filenames []string + + Variables map[string]Variable + Lists map[string]List +} diff --git a/search/variable.go b/search/variable.go new file mode 100644 index 0000000..b86a3d6 --- /dev/null +++ b/search/variable.go @@ -0,0 +1,24 @@ +package search + +import ( + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/zclconf/go-cty/cty" +) + +type Variable struct { + Description string + Type cty.Type + + IsSensitive bool + + // DefaultValue represents default value if one is defined + // and is decodable without errors, else cty.NilVal + DefaultValue cty.Value + + // TypeDefaults represents any default values for optional object + // attributes assuming Type is of cty.Object and has defaults. + // + // Any relationships between DefaultValue & TypeDefaults are left + // for downstream to deal with using e.g. TypeDefaults.Apply(). + TypeDefaults *typeexpr.Defaults +} From f0e7c5540630a968d0b20c638fdcc6186f88533c Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Tue, 1 Jul 2025 17:24:51 +0530 Subject: [PATCH 02/27] feat(tfsearch): TF-26847 TF-27259: Added: static tfsearch schemas --- internal/schema/refscope/scopes.go | 2 + internal/schema/search/1.14/list_block.go | 84 ++++++++++++++++ internal/schema/search/1.14/locals_block.go | 34 +++++++ internal/schema/search/1.14/provider_block.go | 56 +++++++++++ internal/schema/search/1.14/root.go | 22 +++++ internal/schema/search/1.14/variable_block.go | 92 +++++++++++++++++ schema/provider_schema.go | 1 + schema/search/search_schema.go | 17 ++++ schema/search/search_schema_merge.go | 98 +++++++++++++++++++ 9 files changed, 406 insertions(+) create mode 100644 internal/schema/search/1.14/list_block.go create mode 100644 internal/schema/search/1.14/locals_block.go create mode 100644 internal/schema/search/1.14/provider_block.go create mode 100644 internal/schema/search/1.14/root.go create mode 100644 internal/schema/search/1.14/variable_block.go create mode 100644 schema/search/search_schema.go create mode 100644 schema/search/search_schema_merge.go diff --git a/internal/schema/refscope/scopes.go b/internal/schema/refscope/scopes.go index 7fbbdc3..9d9a88b 100644 --- a/internal/schema/refscope/scopes.go +++ b/internal/schema/refscope/scopes.go @@ -22,4 +22,6 @@ var ( IdentityTokenScope = lang.ScopeId("identity_token") StoreScope = lang.ScopeId("store") OrchestrateContext = lang.ScopeId("orchestrate_context") + + ListScope = lang.ScopeId("list") ) diff --git a/internal/schema/search/1.14/list_block.go b/internal/schema/search/1.14/list_block.go new file mode 100644 index 0000000..7a14c8d --- /dev/null +++ b/internal/schema/search/1.14/list_block.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/terraform-schema/internal/schema/refscope" + "github.com/hashicorp/terraform-schema/internal/schema/tokmod" + "github.com/zclconf/go-cty/cty" +) + +func listBlockSchema() *schema.BlockSchema { + return &schema.BlockSchema{ + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.StaticStep{Name: "list"}, + schema.LabelStep{Index: 0}, + schema.LabelStep{Index: 1}, + }, + FriendlyName: "list", + ScopeId: refscope.DataScope, + AsReference: true, + DependentBodyAsData: true, + InferDependentBody: true, + DependentBodySelfRef: true, + }, + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Data}, + Labels: []*schema.LabelSchema{ + { + Name: "type", + Description: lang.PlainText("List Type"), + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Type, lang.TokenModifierDependent}, + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + Description: lang.PlainText("Reference Name"), + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Name}, + }, + }, + Description: lang.PlainText("A list block defines a mechanism to retrieve collections of resources. " + + "It specifies the type of resource to be listed and is uniquely identified by the name."), + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + Count: true, + ForEach: true, + }, + Attributes: map[string]*schema.AttributeSchema{ + "provider": { + Constraint: schema.Reference{OfScopeId: refscope.ProviderScope}, + IsOptional: false, + Description: lang.Markdown("Reference to a `provider` configuration block, e.g. `mycloud.west` or `mycloud`"), + IsDepKey: true, + SemanticTokenModifiers: lang.SemanticTokenModifiers{lang.TokenModifierDependent}, + }, + "include_resource": { + Constraint: schema.LiteralType{Type: cty.Number}, + DefaultValue: schema.DefaultValue{Value: cty.False}, + IsOptional: true, + Description: lang.Markdown("By default, the results of a list resource only include the identities of the discovered resources. " + + "If it is marked true then the provider should include the resource data in the result."), + }, + "limit": { + Constraint: schema.LiteralType{Type: cty.Number}, + IsOptional: true, + Description: lang.Markdown("Limit is an optional value that can be used to limit the " + + "number of results returned by the list resource."), + }, + "depends_on": { + Constraint: schema.Set{ + Elem: schema.OneOf{ + schema.Reference{OfScopeId: refscope.ListScope}, + }, + }, + IsOptional: true, + Description: lang.Markdown("Set of references to hidden dependencies, e.g. other list"), + }, + }, + }, + } +} diff --git a/internal/schema/search/1.14/locals_block.go b/internal/schema/search/1.14/locals_block.go new file mode 100644 index 0000000..eed2ee1 --- /dev/null +++ b/internal/schema/search/1.14/locals_block.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/terraform-schema/internal/schema/refscope" + "github.com/hashicorp/terraform-schema/internal/schema/tokmod" + "github.com/zclconf/go-cty/cty" +) + +func localsBlockSchema() *schema.BlockSchema { + return &schema.BlockSchema{ + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Locals}, + Description: lang.Markdown("Local values assigning names to expressions, so you can use these multiple times without repetition\n" + + "e.g. `service_name = \"forum\"`"), + Body: &schema.BodySchema{ + AnyAttribute: &schema.AttributeSchema{ + Address: &schema.AttributeAddrSchema{ + Steps: []schema.AddrStep{ + schema.StaticStep{Name: "local"}, + schema.AttrNameStep{}, + }, + ScopeId: refscope.LocalScope, + AsExprType: true, + AsReference: true, + }, + Constraint: schema.AnyExpression{OfType: cty.DynamicPseudoType}, + }, + }, + } +} diff --git a/internal/schema/search/1.14/provider_block.go b/internal/schema/search/1.14/provider_block.go new file mode 100644 index 0000000..2d9b2ce --- /dev/null +++ b/internal/schema/search/1.14/provider_block.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/terraform-schema/internal/schema/refscope" + "github.com/hashicorp/terraform-schema/internal/schema/tokmod" + "github.com/zclconf/go-cty/cty" +) + +func providerBlockSchema() *schema.BlockSchema { + return &schema.BlockSchema{ + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + schema.AttrValueStep{Name: "alias", IsOptional: true}, + }, + FriendlyName: "provider", + ScopeId: refscope.ProviderScope, + AsReference: true, + }, + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Provider}, + Labels: []*schema.LabelSchema{ + { + Name: "name", + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Name, lang.TokenModifierDependent}, + Description: lang.PlainText("Provider Name"), + IsDepKey: true, + Completable: true, + }, + }, + Description: lang.PlainText("A provider block is used to specify a provider configuration"), + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + DynamicBlocks: true, + }, + Attributes: map[string]*schema.AttributeSchema{ + "alias": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Description: lang.Markdown("Alias for using the same provider with different configurations for different resources, e.g. `eu-west`"), + }, + "version": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + IsDeprecated: true, + Description: lang.Markdown("Specifies a version constraint for the provider. e.g. `~> 1.0`.\n" + + "**DEPRECATED:** Use `required_providers` block to manage provider version instead."), + }, + }, + }, + } +} diff --git a/internal/schema/search/1.14/root.go b/internal/schema/search/1.14/root.go new file mode 100644 index 0000000..8b34952 --- /dev/null +++ b/internal/schema/search/1.14/root.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl-lang/schema" +) + +// SearchSchema returns the static schema for a search +// configuration (*.tfsearch.hcl) file. +func SearchSchema(_ *version.Version) *schema.BodySchema { + return &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "list": listBlockSchema(), + "locals": localsBlockSchema(), + "provider": providerBlockSchema(), + "variable": variableBlockSchema(), + }, + } +} diff --git a/internal/schema/search/1.14/variable_block.go b/internal/schema/search/1.14/variable_block.go new file mode 100644 index 0000000..43ba5cf --- /dev/null +++ b/internal/schema/search/1.14/variable_block.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform-schema/internal/schema/refscope" + "github.com/hashicorp/terraform-schema/internal/schema/tokmod" +) + +func variableBlockSchema() *schema.BlockSchema { + return &schema.BlockSchema{ + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.StaticStep{Name: "var"}, + schema.LabelStep{Index: 0}, + }, + FriendlyName: "variable", + ScopeId: refscope.VariableScope, + AsReference: true, + AsTypeOf: &schema.BlockAsTypeOf{ + AttributeExpr: "type", + }, + }, + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Variable}, + Labels: []*schema.LabelSchema{ + { + Name: "name", + SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Name}, + Description: lang.PlainText("Variable Name"), + }, + }, + Description: lang.Markdown("Input variable allowing users to customize aspects of the configuration when used directly " + + "(e.g. via CLI, `tfvars` file or via environment variables), or as a module (via `module` arguments)"), + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "description": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Description: lang.Markdown("Description to document the purpose of the variable and what value is expected"), + }, + "type": { + Constraint: schema.TypeDeclaration{}, + IsOptional: true, + Description: lang.Markdown("Type constraint restricting the type of value to accept, e.g. `string` or `list(string)`"), + }, + "sensitive": { + Constraint: schema.LiteralType{Type: cty.Bool}, + DefaultValue: schema.DefaultValue{Value: cty.False}, + IsOptional: true, + Description: lang.Markdown("Whether the variable contains sensitive material and should be hidden in the UI"), + }, + "nullable": { + Constraint: schema.LiteralType{Type: cty.Bool}, + DefaultValue: schema.DefaultValue{Value: cty.False}, + IsOptional: true, + Description: lang.Markdown("Specifies whether `null` is a valid value for this variable"), + }, + "ephemeral": { + IsOptional: true, + Constraint: schema.LiteralType{Type: cty.Bool}, + Description: lang.PlainText("Whether the value is ephemeral and should not be persisted in the state"), + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "validation": { + Description: lang.Markdown("Custom validation rule to restrict what value is expected for the variable"), + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "condition": { + Constraint: schema.LiteralType{Type: cty.Bool}, + IsRequired: true, + Description: lang.Markdown("Condition under which a variable value is valid, " + + "e.g. `length(var.example) >= 4` enforces minimum of 4 characters"), + }, + "error_message": { + Constraint: schema.LiteralType{Type: cty.String}, + IsRequired: true, + Description: lang.Markdown("Error message to present when the variable is considered invalid, " + + "i.e. when `condition` evaluates to `false`"), + }, + }, + }, + }, + }, + }, + } +} diff --git a/schema/provider_schema.go b/schema/provider_schema.go index a5499bb..086305e 100644 --- a/schema/provider_schema.go +++ b/schema/provider_schema.go @@ -15,6 +15,7 @@ type ProviderSchema struct { EphemeralResources map[string]*schema.BodySchema DataSources map[string]*schema.BodySchema Functions map[string]*schema.FunctionSignature + ListResourceTypes map[string]*schema.BodySchema } func (ps *ProviderSchema) Copy() *ProviderSchema { diff --git a/schema/search/search_schema.go b/schema/search/search_schema.go new file mode 100644 index 0000000..42e49e0 --- /dev/null +++ b/schema/search/search_schema.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl-lang/schema" + search_1_14 "github.com/hashicorp/terraform-schema/internal/schema/search/1.14" +) + +// CoreSearchSchemaForVersion finds a schema for search configuration files +// that is relevant for the given Terraform version. +// It will return an error if such schema cannot be found. +func CoreSearchSchemaForVersion(v *version.Version) (*schema.BodySchema, error) { + return search_1_14.SearchSchema(v), nil +} diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go new file mode 100644 index 0000000..ece69b7 --- /dev/null +++ b/schema/search/search_schema_merge.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + tfaddr "github.com/hashicorp/terraform-registry-address" + tfschema "github.com/hashicorp/terraform-schema/schema" + tfsearch "github.com/hashicorp/terraform-schema/search" +) + +type SearchSchemaMerger struct { + coreSchema *schema.BodySchema + stateReader StateReader +} + +// StateReader exposes a set of methods to read data from the internal language server state +type StateReader interface { + // ProviderSchema returns the schema for a provider we have stored in memory. The can come + // from different sources. + ProviderSchema(modPath string, addr tfaddr.Provider, vc version.Constraints) (*tfschema.ProviderSchema, error) +} + +func NewSearchSchemaMerger(coreSchema *schema.BodySchema) *SearchSchemaMerger { + return &SearchSchemaMerger{ + coreSchema: coreSchema, + } +} + +func (m *SearchSchemaMerger) SetStateReader(mr StateReader) { + m.stateReader = mr +} + +func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodySchema, error) { + if m.coreSchema == nil { + return nil, tfschema.CoreSchemaRequiredErr{} + } + + if meta == nil { + return m.coreSchema, nil + } + + if m.stateReader == nil { + return m.coreSchema, nil + } + + mergedSchema := m.coreSchema.Copy() + + if mergedSchema.Blocks["provider"].DependentBody == nil { + mergedSchema.Blocks["provider"].DependentBody = make(map[schema.SchemaKey]*schema.BodySchema) + } + + if mergedSchema.Blocks["list"].DependentBody == nil { + mergedSchema.Blocks["list"].DependentBody = make(map[schema.SchemaKey]*schema.BodySchema) + } + + if _, ok := mergedSchema.Blocks["variable"]; ok { + mergedSchema.Blocks["variable"].Labels = []*schema.LabelSchema{ + { + Name: "name", + IsDepKey: true, + Description: lang.PlainText("Variable name"), + }, + } + mergedSchema.Blocks["variable"].DependentBody = variableDependentBody(meta.Variables) + } + + // TODO merge provider - source them from the Terraform module meta requirements TF-27288 + // TODO merge list config - source them from the Terraform module meta requirements TF-27260 + + return mergedSchema, nil +} + +func variableDependentBody(vars map[string]tfsearch.Variable) map[schema.SchemaKey]*schema.BodySchema { + depBodies := make(map[schema.SchemaKey]*schema.BodySchema) + + for name, mVar := range vars { + depKeys := schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: name}, + }, + } + depBodies[schema.NewSchemaKey(depKeys)] = &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "default": { + Constraint: schema.LiteralType{Type: mVar.Type}, + IsOptional: true, + Description: lang.Markdown("Default value to use when variable is not explicitly set"), + }, + }, + } + } + + return depBodies +} From 6e6fa1c35727b5b4a09dbdee168492f5139e4375 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 1 Jul 2025 17:42:12 +0530 Subject: [PATCH 03/27] Search: TF-27258: feat: complete earlydecoder functionality for search --- earlydecoder/search/decoder.go | 31 ++++- earlydecoder/search/decoder_test.go | 178 ++++++++++++++++++++++++++++ earlydecoder/search/load_search.go | 91 +++++++++++++- earlydecoder/search/schema.go | 16 ++- search/list.go | 1 + 5 files changed, 308 insertions(+), 9 deletions(-) diff --git a/earlydecoder/search/decoder.go b/earlydecoder/search/decoder.go index b04cf8c..7638b1d 100644 --- a/earlydecoder/search/decoder.go +++ b/earlydecoder/search/decoder.go @@ -5,25 +5,44 @@ package earlydecoder import ( "github.com/hashicorp/hcl/v2" - tftest "github.com/hashicorp/terraform-schema/test" + "github.com/hashicorp/terraform-schema/search" "sort" + "strings" ) -func LoadTest(path string, files map[string]*hcl.File) (*tftest.Meta, hcl.Diagnostics) { - var diags hcl.Diagnostics +func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[string]hcl.Diagnostics) { filenames := make([]string, 0) + diags := make(map[string]hcl.Diagnostics, 0) mod := newDecodedSearch() for filename, f := range files { filenames = append(filenames, filename) - fDiags := loadSearchFromFile(f, mod) - diags = append(diags, fDiags...) + if isSearchFile(filename) { + diags[filename] = loadSearchFromFile(f, mod) + } } sort.Strings(filenames) - return &tftest.Meta{ + variables := make(map[string]search.Variable) + for key, variable := range mod.Variables { + variables[key] = *variable + } + + lists := make(map[string]search.List) + for key, list := range mod.List { + lists[key] = *list + } + + return &search.Meta{ Path: path, Filenames: filenames, + Variables: variables, + Lists: lists, }, diags } + +func isSearchFile(name string) bool { + return strings.HasSuffix(name, ".tfquery.hcl") || + strings.HasSuffix(name, ".tfquery.hcl.json") +} diff --git a/earlydecoder/search/decoder_test.go b/earlydecoder/search/decoder_test.go index cb1b0a3..897efeb 100644 --- a/earlydecoder/search/decoder_test.go +++ b/earlydecoder/search/decoder_test.go @@ -2,3 +2,181 @@ // SPDX-License-Identifier: MPL-2.0 package earlydecoder + +import ( + "fmt" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform-schema/search" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "testing" +) + +type testCase struct { + name string + cfg string + fileName string + expectedMeta *search.Meta + expectedError map[string]hcl.Diagnostics +} + +var customComparer = []cmp.Option{ + cmp.Comparer(compareVersionConstraint), + ctydebug.CmpOptions, +} + +var fileName = "test.tfquery.hcl" + +func TestLoadSearch(t *testing.T) { + path := t.TempDir() + + testCases := []testCase{ + { + "empty config", + ``, + fileName, + &search.Meta{ + Path: path, + Filenames: []string{fileName}, Variables: map[string]search.Variable{}, Lists: map[string]search.List{}, + }, + map[string]hcl.Diagnostics{fileName: nil}, + }, + { + "variables", + `variable "example" { + type = string + default = "default_value" + } + + variable "example2" { + description = "description" + sensitive = true + }`, + fileName, + &search.Meta{ + + Path: path, + Filenames: []string{fileName}, + Variables: map[string]search.Variable{ + "example": { + Type: cty.String, + DefaultValue: cty.StringVal("default_value"), + }, + "example2": { + Type: cty.DynamicPseudoType, + Description: "description", + IsSensitive: true, + }, + }, + Lists: map[string]search.List{}, + }, + map[string]hcl.Diagnostics{fileName: nil}, + }, + } + + runTestCases(testCases, t, path) + +} + +func runTestCases(testCases []testCase, t *testing.T, path string) { + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), tc.fileName, hcl.InitialPos) + if len(diags) > 0 { + t.Fatal(diags) + } + files := map[string]*hcl.File{ + tc.fileName: f, + } + + var fdiags map[string]hcl.Diagnostics + meta, fdiags := LoadSearch(path, files) + + if diff := cmp.Diff(tc.expectedError, fdiags, customComparer...); diff != "" { + t.Fatalf("expected errors doesn't match: %s", diff) + } + + if diff := cmp.Diff(tc.expectedMeta, meta, customComparer...); diff != "" { + t.Fatalf("search meta doesn't match: %s", diff) + } + }) + } +} + +func TestLoadStackDiagnostics(t *testing.T) { + path := t.TempDir() + + testCases := []testCase{ + { + "invalid variable default value", + `variable "example" { + type = string + default = [1] + }`, + fileName, + &search.Meta{ + Path: path, + Filenames: []string{fileName}, + Variables: map[string]search.Variable{ + "example": { + Type: cty.String, + DefaultValue: cty.DynamicVal, + }, + }, + Lists: map[string]search.List{}, + }, + map[string]hcl.Diagnostics{ + fileName: { + { + Severity: hcl.DiagError, + Summary: `Invalid default value for variable`, + Detail: `This default value is not compatible with the variable's type constraint: string required.`, + Subject: &hcl.Range{ + Filename: fileName, + Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, + End: hcl.Pos{Line: 3, Column: 16, Byte: 52}, + }, + }, + }, + }, + }, + { + "invalid list include resource value", + `list "resource1" "example1" { + include_resource = yes +}`, + fileName, + &search.Meta{ + Path: path, + Filenames: []string{fileName}, + Variables: map[string]search.Variable{}, + Lists: map[string]search.List{ + "example1": {}, + }, + }, + map[string]hcl.Diagnostics{ + fileName: { + { + Severity: hcl.DiagError, + Summary: `Invalid value for include_resource in list`, + Detail: `This include resource value is not compatible with the type constraint: bool required.`, + Subject: &hcl.Range{ + Filename: fileName, + Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, + End: hcl.Pos{Line: 3, Column: 16, Byte: 52}, + }, + }, + }, + }, + }, + } + + runTestCases(testCases, t, path) +} + +func compareVersionConstraint(x, y *version.Constraint) bool { + return x.Equals(y) +} diff --git a/earlydecoder/search/load_search.go b/earlydecoder/search/load_search.go index fadfbfc..ed7422a 100644 --- a/earlydecoder/search/load_search.go +++ b/earlydecoder/search/load_search.go @@ -4,8 +4,13 @@ package earlydecoder import ( + "fmt" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/terraform-schema/search" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" ) type decodedSearch struct { @@ -20,8 +25,90 @@ func newDecodedSearch() *decodedSearch { } } -func loadSearchFromFile(file *hcl.File, _ *decodedSearch) hcl.Diagnostics { +func loadSearchFromFile(file *hcl.File, ds *decodedSearch) hcl.Diagnostics { var diags hcl.Diagnostics - // TODO Implementation + + content, _, contentDiags := file.Body.PartialContent(rootSchema) + diags = append(diags, contentDiags...) + + for _, block := range content.Blocks { + switch block.Type { + case "variable": + content, _, contentDiags := block.Body.PartialContent(variableSchema) + diags = append(diags, contentDiags...) + + if len(block.Labels) != 1 || block.Labels[0] == "" { + continue + } + + name := block.Labels[0] + description := "" + isSensitive := false + var valDiags hcl.Diagnostics + + if attr, defined := content.Attributes["description"]; defined { + valDiags = gohcl.DecodeExpression(attr.Expr, nil, &description) + diags = append(diags, valDiags...) + } + + if attr, defined := content.Attributes["sensitive"]; defined { + valDiags = gohcl.DecodeExpression(attr.Expr, nil, &isSensitive) + diags = append(diags, valDiags...) + } + + varType := cty.DynamicPseudoType + var defaults *typeexpr.Defaults + if attr, defined := content.Attributes["type"]; defined { + varType, defaults, valDiags = typeexpr.TypeConstraintWithDefaults(attr.Expr) + diags = append(diags, valDiags...) + } + + defaultValue := cty.NilVal + if attr, defined := content.Attributes["default"]; defined { + val, vDiags := attr.Expr.Value(nil) + diags = append(diags, vDiags...) + if !vDiags.HasErrors() { + if varType != cty.NilType { + var err error + val, err = convert.Convert(val, varType) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid default value for variable", + Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), + Subject: attr.Expr.Range().Ptr(), + }) + val = cty.DynamicVal + } + } + + defaultValue = val + } + } + + ds.Variables[name] = &search.Variable{ + Type: varType, + Description: description, + DefaultValue: defaultValue, + TypeDefaults: defaults, + IsSensitive: isSensitive, + } + case "list": + content, _, contentDiags := block.Body.PartialContent(listSchema) + diags = append(diags, contentDiags...) + name := block.Labels[1] + var includeResource bool + var valDiags hcl.Diagnostics + if attr, defined := content.Attributes["include_resource"]; defined { + valDiags = gohcl.DecodeExpression(attr.Expr, nil, &includeResource) + diags = append(diags, valDiags...) + } + + ds.List[name] = &search.List{ + IncludeResource: includeResource, + } + } + } + return diags } diff --git a/earlydecoder/search/schema.go b/earlydecoder/search/schema.go index 6f6b4a7..7b57fce 100644 --- a/earlydecoder/search/schema.go +++ b/earlydecoder/search/schema.go @@ -25,8 +25,22 @@ var listSchema = &hcl.BodySchema{ { Name: "include_resource", }, + }, +} + +var variableSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "description", + }, + { + Name: "type", + }, + { + Name: "default", + }, { - Name: "limit", + Name: "sensitive", }, }, } diff --git a/search/list.go b/search/list.go index 5df1461..4c3d5b3 100644 --- a/search/list.go +++ b/search/list.go @@ -1,4 +1,5 @@ package search type List struct { + IncludeResource bool } From a9d702fcd3ae9e8ccbea6f4fadbae2930592de5a Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 1 Jul 2025 21:33:42 +0530 Subject: [PATCH 04/27] Search: TF-27258: fix: Add license header --- search/list.go | 3 +++ search/meta.go | 3 +++ search/variable.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/search/list.go b/search/list.go index 4c3d5b3..f95a5ba 100644 --- a/search/list.go +++ b/search/list.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package search type List struct { diff --git a/search/meta.go b/search/meta.go index 30928ce..d6526bd 100644 --- a/search/meta.go +++ b/search/meta.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package search type Meta struct { diff --git a/search/variable.go b/search/variable.go index b86a3d6..5702380 100644 --- a/search/variable.go +++ b/search/variable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package search import ( From 1a7469641a8424a75b4d5932e87ebc92893420b3 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 1 Jul 2025 21:38:34 +0530 Subject: [PATCH 05/27] Search: TF-27258: test: fix incorrect test and update test data --- earlydecoder/search/decoder_test.go | 35 +++-------------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/earlydecoder/search/decoder_test.go b/earlydecoder/search/decoder_test.go index 897efeb..d8c03ae 100644 --- a/earlydecoder/search/decoder_test.go +++ b/earlydecoder/search/decoder_test.go @@ -113,9 +113,9 @@ func TestLoadStackDiagnostics(t *testing.T) { { "invalid variable default value", `variable "example" { - type = string - default = [1] - }`, + type = string + default = [1] +}`, fileName, &search.Meta{ Path: path, @@ -143,35 +143,6 @@ func TestLoadStackDiagnostics(t *testing.T) { }, }, }, - { - "invalid list include resource value", - `list "resource1" "example1" { - include_resource = yes -}`, - fileName, - &search.Meta{ - Path: path, - Filenames: []string{fileName}, - Variables: map[string]search.Variable{}, - Lists: map[string]search.List{ - "example1": {}, - }, - }, - map[string]hcl.Diagnostics{ - fileName: { - { - Severity: hcl.DiagError, - Summary: `Invalid value for include_resource in list`, - Detail: `This include resource value is not compatible with the type constraint: bool required.`, - Subject: &hcl.Range{ - Filename: fileName, - Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, - End: hcl.Pos{Line: 3, Column: 16, Byte: 52}, - }, - }, - }, - }, - }, } runTestCases(testCases, t, path) From 68f90a2a005e33a4cb01945689ac41d59eab81f9 Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 2 Jul 2025 13:50:04 +0530 Subject: [PATCH 06/27] feat(tfsearch): TF-26847 TF-27259: Modified: include_resource constraint --- internal/schema/search/1.14/list_block.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/search/1.14/list_block.go b/internal/schema/search/1.14/list_block.go index 7a14c8d..6b7a665 100644 --- a/internal/schema/search/1.14/list_block.go +++ b/internal/schema/search/1.14/list_block.go @@ -57,7 +57,7 @@ func listBlockSchema() *schema.BlockSchema { SemanticTokenModifiers: lang.SemanticTokenModifiers{lang.TokenModifierDependent}, }, "include_resource": { - Constraint: schema.LiteralType{Type: cty.Number}, + Constraint: schema.LiteralType{Type: cty.Bool}, DefaultValue: schema.DefaultValue{Value: cty.False}, IsOptional: true, Description: lang.Markdown("By default, the results of a list resource only include the identities of the discovered resources. " + From e1248415e00336a9c3d7b62955798515a1edcbda Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Thu, 3 Jul 2025 22:15:31 +0530 Subject: [PATCH 07/27] feat(tfsearch): TF-26847 TF-27259: Modified: limit constraint --- internal/schema/search/1.14/list_block.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/schema/search/1.14/list_block.go b/internal/schema/search/1.14/list_block.go index 6b7a665..4a89db6 100644 --- a/internal/schema/search/1.14/list_block.go +++ b/internal/schema/search/1.14/list_block.go @@ -64,7 +64,8 @@ func listBlockSchema() *schema.BlockSchema { "If it is marked true then the provider should include the resource data in the result."), }, "limit": { - Constraint: schema.LiteralType{Type: cty.Number}, + // Constraint: schema.LiteralType{Type: cty.Number}, + Constraint: schema.AnyExpression{OfType: cty.Number}, IsOptional: true, Description: lang.Markdown("Limit is an optional value that can be used to limit the " + "number of results returned by the list resource."), From be3e46d15572c12d149f6900a52d809ad92e1bbb Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Fri, 4 Jul 2025 17:07:50 +0530 Subject: [PATCH 08/27] feat(tfsearch): TF-26847 TF-27259: Modified: added config block to list block --- internal/schema/search/1.14/list_block.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/schema/search/1.14/list_block.go b/internal/schema/search/1.14/list_block.go index 4a89db6..b9a2a81 100644 --- a/internal/schema/search/1.14/list_block.go +++ b/internal/schema/search/1.14/list_block.go @@ -80,6 +80,12 @@ func listBlockSchema() *schema.BlockSchema { Description: lang.Markdown("Set of references to hidden dependencies, e.g. other list"), }, }, + Blocks: map[string]*schema.BlockSchema{ + "config": { + Description: lang.Markdown("Filters specific to the list type"), + MaxItems: 1, + }, + }, }, } } From 20ef96d1bfefd34382b05a5b6def2a693f5a68ee Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Mon, 7 Jul 2025 16:10:43 +0530 Subject: [PATCH 09/27] feat(tfsearch): TF-26847 TF-27259: Modified: added schema validations for config and provider --- internal/schema/search/1.14/list_block.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/schema/search/1.14/list_block.go b/internal/schema/search/1.14/list_block.go index b9a2a81..be93882 100644 --- a/internal/schema/search/1.14/list_block.go +++ b/internal/schema/search/1.14/list_block.go @@ -20,13 +20,12 @@ func listBlockSchema() *schema.BlockSchema { schema.LabelStep{Index: 1}, }, FriendlyName: "list", - ScopeId: refscope.DataScope, + ScopeId: refscope.ListScope, AsReference: true, DependentBodyAsData: true, InferDependentBody: true, DependentBodySelfRef: true, }, - SemanticTokenModifiers: lang.SemanticTokenModifiers{tokmod.Data}, Labels: []*schema.LabelSchema{ { Name: "type", @@ -51,7 +50,7 @@ func listBlockSchema() *schema.BlockSchema { Attributes: map[string]*schema.AttributeSchema{ "provider": { Constraint: schema.Reference{OfScopeId: refscope.ProviderScope}, - IsOptional: false, + IsRequired: true, Description: lang.Markdown("Reference to a `provider` configuration block, e.g. `mycloud.west` or `mycloud`"), IsDepKey: true, SemanticTokenModifiers: lang.SemanticTokenModifiers{lang.TokenModifierDependent}, @@ -84,6 +83,7 @@ func listBlockSchema() *schema.BlockSchema { "config": { Description: lang.Markdown("Filters specific to the list type"), MaxItems: 1, + MinItems: 1, }, }, }, From b4855307d8962464b70ada671ba8e4c87b362f3a Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Tue, 8 Jul 2025 19:44:12 +0530 Subject: [PATCH 10/27] feat(tfsearch): TF-26847 TF-27259: Modified: allow include_resource to take values from local & variables --- earlydecoder/search/load_search.go | 15 +-------------- internal/schema/search/1.14/list_block.go | 3 +-- search/list.go | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/earlydecoder/search/load_search.go b/earlydecoder/search/load_search.go index ed7422a..1cc50a4 100644 --- a/earlydecoder/search/load_search.go +++ b/earlydecoder/search/load_search.go @@ -5,6 +5,7 @@ package earlydecoder import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/typeexpr" "github.com/hashicorp/hcl/v2/gohcl" @@ -93,20 +94,6 @@ func loadSearchFromFile(file *hcl.File, ds *decodedSearch) hcl.Diagnostics { TypeDefaults: defaults, IsSensitive: isSensitive, } - case "list": - content, _, contentDiags := block.Body.PartialContent(listSchema) - diags = append(diags, contentDiags...) - name := block.Labels[1] - var includeResource bool - var valDiags hcl.Diagnostics - if attr, defined := content.Attributes["include_resource"]; defined { - valDiags = gohcl.DecodeExpression(attr.Expr, nil, &includeResource) - diags = append(diags, valDiags...) - } - - ds.List[name] = &search.List{ - IncludeResource: includeResource, - } } } diff --git a/internal/schema/search/1.14/list_block.go b/internal/schema/search/1.14/list_block.go index be93882..b4f61b0 100644 --- a/internal/schema/search/1.14/list_block.go +++ b/internal/schema/search/1.14/list_block.go @@ -56,14 +56,13 @@ func listBlockSchema() *schema.BlockSchema { SemanticTokenModifiers: lang.SemanticTokenModifiers{lang.TokenModifierDependent}, }, "include_resource": { - Constraint: schema.LiteralType{Type: cty.Bool}, + Constraint: schema.AnyExpression{OfType: cty.Bool}, DefaultValue: schema.DefaultValue{Value: cty.False}, IsOptional: true, Description: lang.Markdown("By default, the results of a list resource only include the identities of the discovered resources. " + "If it is marked true then the provider should include the resource data in the result."), }, "limit": { - // Constraint: schema.LiteralType{Type: cty.Number}, Constraint: schema.AnyExpression{OfType: cty.Number}, IsOptional: true, Description: lang.Markdown("Limit is an optional value that can be used to limit the " + diff --git a/search/list.go b/search/list.go index f95a5ba..bd78015 100644 --- a/search/list.go +++ b/search/list.go @@ -4,5 +4,4 @@ package search type List struct { - IncludeResource bool } From 6d48f7e6717b0c68c9b678789c874865244780a3 Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 30 Jul 2025 16:53:05 +0530 Subject: [PATCH 11/27] feat(tfsearch): TF-26847 TF-27288: Added: dynamic schema for provider block --- earlydecoder/search/decoder.go | 16 ++++++++----- schema/search/search_schema_merge.go | 36 +++++++++++++++++++++++++++- search/meta.go | 36 ++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/earlydecoder/search/decoder.go b/earlydecoder/search/decoder.go index 7638b1d..e1e959d 100644 --- a/earlydecoder/search/decoder.go +++ b/earlydecoder/search/decoder.go @@ -4,10 +4,12 @@ package earlydecoder import ( - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-schema/search" "sort" "strings" + + "github.com/hashicorp/hcl/v2" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform-schema/search" ) func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[string]hcl.Diagnostics) { @@ -35,10 +37,12 @@ func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[stri } return &search.Meta{ - Path: path, - Filenames: filenames, - Variables: variables, - Lists: lists, + Path: path, + Filenames: filenames, + Variables: variables, + Lists: lists, + ProviderReferences: make(map[search.ProviderRef]tfaddr.Provider), + ProviderRequirements: make(search.ProviderRequirements), }, diags } diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go index ece69b7..af89fcb 100644 --- a/schema/search/search_schema_merge.go +++ b/schema/search/search_schema_merge.go @@ -68,7 +68,27 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS mergedSchema.Blocks["variable"].DependentBody = variableDependentBody(meta.Variables) } - // TODO merge provider - source them from the Terraform module meta requirements TF-27288 + providerRefs := ProviderReferences(meta.ProviderReferences) + + for pAddr, pVersionCons := range meta.ProviderRequirements { + pSchema, err := m.stateReader.ProviderSchema(meta.Path, pAddr, pVersionCons) + if err != nil { + continue + } + + refs := providerRefs.ReferencesOfProvider(pAddr) + for _, localRef := range refs { + if pSchema.Provider != nil { + mergedSchema.Blocks["provider"].DependentBody[schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: localRef.LocalName}, + }, + })] = pSchema.Provider + } + + } + } + // TODO merge list config - source them from the Terraform module meta requirements TF-27260 return mergedSchema, nil @@ -96,3 +116,17 @@ func variableDependentBody(vars map[string]tfsearch.Variable) map[schema.SchemaK return depBodies } + +type ProviderReferences map[tfsearch.ProviderRef]tfaddr.Provider + +func (pr ProviderReferences) ReferencesOfProvider(addr tfaddr.Provider) []tfsearch.ProviderRef { + refs := make([]tfsearch.ProviderRef, 0) + + for ref, pAddr := range pr { + if pAddr.Equals(addr) { + refs = append(refs, ref) + } + } + + return refs +} diff --git a/search/meta.go b/search/meta.go index d6526bd..b52c7fd 100644 --- a/search/meta.go +++ b/search/meta.go @@ -3,10 +3,46 @@ package search +import ( + "github.com/hashicorp/go-version" + tfaddr "github.com/hashicorp/terraform-registry-address" +) + type Meta struct { Path string Filenames []string Variables map[string]Variable Lists map[string]List + + ProviderReferences map[ProviderRef]tfaddr.Provider + ProviderRequirements ProviderRequirements +} + +type ProviderRequirements map[tfaddr.Provider]version.Constraints + +func (pr ProviderRequirements) Equals(reqs ProviderRequirements) bool { + if len(pr) != len(reqs) { + return false + } + + for pAddr, vCons := range pr { + c, ok := reqs[pAddr] + if !ok { + return false + } + if !vCons.Equals(c) { + return false + } + } + + return true +} + +type ProviderRef struct { + LocalName string + + // If not empty, Alias identifies which non-default (aliased) provider + // configuration this address refers to. + Alias string } From acaa7b94da8e4abeb45935617961c64e0574946a Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Thu, 31 Jul 2025 14:14:37 +0530 Subject: [PATCH 12/27] feat(tfsearch): TF-26847 TF-27260: Added: dynamic list type of list block as per available providers --- schema/convert_json.go | 7 +++++ schema/provider_schema.go | 12 +++++++- schema/search/search_schema_merge.go | 42 ++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/schema/convert_json.go b/schema/convert_json.go index 73d7936..c0894ee 100644 --- a/schema/convert_json.go +++ b/schema/convert_json.go @@ -21,6 +21,7 @@ func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Prov EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if jsonSchema.ConfigSchema != nil { @@ -50,6 +51,12 @@ func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Prov ps.Functions[fnName].Detail = detailForSrcAddr(pAddr, nil) } + // TODO: TF-27260: after terraform-json package is modified, rename it to ListResourceSchemas + for lrName, lrSchema := range jsonSchema.ResourceSchemas { + ps.ListResources[lrName] = bodySchemaFromJson(lrSchema.Block) + ps.ListResources[lrName].Detail = detailForSrcAddr(pAddr, nil) + } + return ps } diff --git a/schema/provider_schema.go b/schema/provider_schema.go index 086305e..94dc897 100644 --- a/schema/provider_schema.go +++ b/schema/provider_schema.go @@ -15,7 +15,7 @@ type ProviderSchema struct { EphemeralResources map[string]*schema.BodySchema DataSources map[string]*schema.BodySchema Functions map[string]*schema.FunctionSignature - ListResourceTypes map[string]*schema.BodySchema + ListResources map[string]*schema.BodySchema } func (ps *ProviderSchema) Copy() *ProviderSchema { @@ -55,6 +55,13 @@ func (ps *ProviderSchema) Copy() *ProviderSchema { } } + if ps.ListResources != nil { + newPs.ListResources = make(map[string]*schema.BodySchema, len(ps.ListResources)) + for name, lsSchema := range ps.ListResources { + newPs.ListResources[name] = lsSchema.Copy() + } + } + return newPs } @@ -76,4 +83,7 @@ func (ps *ProviderSchema) SetProviderVersion(pAddr tfaddr.Provider, v *version.V for _, fSig := range ps.Functions { fSig.Detail = detailForSrcAddr(pAddr, v) } + for _, lsSchema := range ps.ListResources { + lsSchema.Detail = detailForSrcAddr(pAddr, v) + } } diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go index af89fcb..8edaf25 100644 --- a/schema/search/search_schema_merge.go +++ b/schema/search/search_schema_merge.go @@ -4,6 +4,8 @@ package schema import ( + "strings" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" @@ -86,11 +88,38 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS })] = pSchema.Provider } + providerAddr := lang.Address{ + lang.RootStep{Name: localRef.LocalName}, + } + if localRef.Alias != "" { + providerAddr = append(providerAddr, lang.AttrStep{Name: localRef.Alias}) + } + + for lrName, lrSchema := range pSchema.ListResources { + depKeys := schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: lrName}, + }, + Attributes: []schema.AttributeDependent{ + { + Name: "provider", + Expr: schema.ExpressionValue{ + Address: providerAddr, + }, + }, + }, + } + mergedSchema.Blocks["list"].DependentBody[schema.NewSchemaKey(depKeys)] = lrSchema + + // TODO merge list config - source them from the Terraform module meta requirements TF-27260 + if TypeBelongsToProvider(lrName, localRef) { + + } + } + } } - // TODO merge list config - source them from the Terraform module meta requirements TF-27260 - return mergedSchema, nil } @@ -130,3 +159,12 @@ func (pr ProviderReferences) ReferencesOfProvider(addr tfaddr.Provider) []tfsear return refs } + +// TypeBelongsToProvider returns true if the given type +// (resource or data source) name belongs to a particular provider. +// +// This reflects internal implementation in Terraform at +// https://github.com/hashicorp/terraform/blob/488bbd80/internal/addrs/resource.go#L68-L77 +func TypeBelongsToProvider(typeName string, pRef tfsearch.ProviderRef) bool { + return typeName == pRef.LocalName || strings.HasPrefix(typeName, pRef.LocalName+"_") +} From 7881d2b4d0e36c8c175ce10af84f322acc5c13a3 Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Thu, 31 Jul 2025 14:27:49 +0530 Subject: [PATCH 13/27] feat(tfsearch): TF-26847 TF-27260: Modified: todo comment with details --- schema/convert_json.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/schema/convert_json.go b/schema/convert_json.go index c0894ee..c1c9cff 100644 --- a/schema/convert_json.go +++ b/schema/convert_json.go @@ -51,7 +51,13 @@ func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Prov ps.Functions[fnName].Detail = detailForSrcAddr(pAddr, nil) } - // TODO: TF-27260: after terraform-json package is modified, rename it to ListResourceSchemas + /* + TODO: TF-27260: + After terraform-json package is modified, + and new key called ListResourceSchemas is introduced + which reads json key from list_resource_schemas + modify below code to jsonSchema.ListResourceSchemas + */ for lrName, lrSchema := range jsonSchema.ResourceSchemas { ps.ListResources[lrName] = bodySchemaFromJson(lrSchema.Block) ps.ListResources[lrName].Detail = detailForSrcAddr(pAddr, nil) From c62e07bdd74f09e8acf91f24d3d6373a60aab58d Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Mon, 4 Aug 2025 11:20:04 +0530 Subject: [PATCH 14/27] feat(tfsearch): TF-26847 TF-27260: Modified: wip dynamic list config --- schema/search/search_schema_merge.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go index 8edaf25..503041f 100644 --- a/schema/search/search_schema_merge.go +++ b/schema/search/search_schema_merge.go @@ -59,6 +59,10 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS mergedSchema.Blocks["list"].DependentBody = make(map[schema.SchemaKey]*schema.BodySchema) } + // if mergedSchema.Blocks["list"].Body.Blocks["config"].DependentBody == nil { + // mergedSchema.Blocks["list"].Body.Blocks["config"].DependentBody = make(map[schema.SchemaKey]*schema.BodySchema) + // } + if _, ok := mergedSchema.Blocks["variable"]; ok { mergedSchema.Blocks["variable"].Labels = []*schema.LabelSchema{ { @@ -113,6 +117,25 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS // TODO merge list config - source them from the Terraform module meta requirements TF-27260 if TypeBelongsToProvider(lrName, localRef) { + configDepKeys := schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: lrName}, + }, + } + mergedSchema.Blocks["list"].DependentBody[schema.NewSchemaKey(configDepKeys)] = &schema.BodySchema{ + HoverURL: pSchema.Provider.HoverURL, + DocsLink: pSchema.Provider.DocsLink, + Detail: pSchema.Provider.Detail, + Description: pSchema.Provider.Description, + IsDeprecated: pSchema.Provider.IsDeprecated, + Blocks: map[string]*schema.BlockSchema{ + "config": { + Body: lrSchema, + }, + }, + } + + // mergedSchema.Blocks["list"].Body.Blocks["config"].DependentBody[schema.NewSchemaKey(configDepKeys)] = lrSchema } } From b79b6f0adf37b841345620c83a8e870ec1c6e839 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 5 Aug 2025 11:39:41 +0530 Subject: [PATCH 15/27] Added provider blocking processing --- earlydecoder/search/load_search.go | 46 +++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/earlydecoder/search/load_search.go b/earlydecoder/search/load_search.go index 1cc50a4..70f4f6d 100644 --- a/earlydecoder/search/load_search.go +++ b/earlydecoder/search/load_search.go @@ -14,15 +14,33 @@ import ( "github.com/zclconf/go-cty/cty/convert" ) +type providerConfig struct { + Name string + Alias string +} + type decodedSearch struct { - List map[string]*search.List - Variables map[string]*search.Variable + List map[string]*search.List + Variables map[string]*search.Variable + ProviderConfigs map[string]*providerConfig +} + +var providerConfigSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "version", + }, + { + Name: "alias", + }, + }, } func newDecodedSearch() *decodedSearch { return &decodedSearch{ - List: make(map[string]*search.List), - Variables: make(map[string]*search.Variable), + List: make(map[string]*search.List), + Variables: make(map[string]*search.Variable), + ProviderConfigs: make(map[string]*providerConfig), } } @@ -94,7 +112,27 @@ func loadSearchFromFile(file *hcl.File, ds *decodedSearch) hcl.Diagnostics { TypeDefaults: defaults, IsSensitive: isSensitive, } + + case "provider": + content, _, contentDiags := block.Body.PartialContent(providerConfigSchema) + diags = append(diags, contentDiags...) + name := block.Labels[0] + providerKey := name + var alias string + if attr, defined := content.Attributes["alias"]; defined { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &alias) + diags = append(diags, valDiags...) + if !valDiags.HasErrors() && alias != "" { + providerKey = fmt.Sprintf("%s.%s", name, alias) + } + } + + ds.ProviderConfigs[providerKey] = &providerConfig{ + Name: name, + Alias: alias, + } } + } return diags From 3386853a01d9430a6226cebe1ba929dc0a1bb3d7 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 5 Aug 2025 11:50:14 +0530 Subject: [PATCH 16/27] Enable dynamic auto complete for list block --- earlydecoder/search/decoder.go | 30 +++++++++++++------ earlydecoder/search/schema.go | 4 +++ schema/convert_json.go | 9 +----- schema/search/search_schema_merge.go | 43 +++++++--------------------- search/meta.go | 22 +++++++------- 5 files changed, 47 insertions(+), 61 deletions(-) diff --git a/earlydecoder/search/decoder.go b/earlydecoder/search/decoder.go index e1e959d..718502f 100644 --- a/earlydecoder/search/decoder.go +++ b/earlydecoder/search/decoder.go @@ -4,12 +4,11 @@ package earlydecoder import ( - "sort" - "strings" - "github.com/hashicorp/hcl/v2" tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform-schema/search" + "sort" + "strings" ) func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[string]hcl.Diagnostics) { @@ -36,13 +35,26 @@ func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[stri lists[key] = *list } + refs := make(map[search.ProviderRef]tfaddr.Provider, 0) + + for _, cfg := range mod.ProviderConfigs { + src := refs[search.ProviderRef{ + LocalName: cfg.Name, + }] + if cfg.Alias != "" { + refs[search.ProviderRef{ + LocalName: cfg.Name, + Alias: cfg.Alias, + }] = src + } + } + return &search.Meta{ - Path: path, - Filenames: filenames, - Variables: variables, - Lists: lists, - ProviderReferences: make(map[search.ProviderRef]tfaddr.Provider), - ProviderRequirements: make(search.ProviderRequirements), + Path: path, + Filenames: filenames, + Variables: variables, + Lists: lists, + ProviderReferences: refs, }, diags } diff --git a/earlydecoder/search/schema.go b/earlydecoder/search/schema.go index 7b57fce..d776c70 100644 --- a/earlydecoder/search/schema.go +++ b/earlydecoder/search/schema.go @@ -17,6 +17,10 @@ var rootSchema = &hcl.BodySchema{ Type: "variable", LabelNames: []string{"name"}, }, + { + Type: "provider", + LabelNames: []string{"name"}, + }, }, } diff --git a/schema/convert_json.go b/schema/convert_json.go index c1c9cff..b7f690d 100644 --- a/schema/convert_json.go +++ b/schema/convert_json.go @@ -51,14 +51,7 @@ func ProviderSchemaFromJson(jsonSchema *tfjson.ProviderSchema, pAddr tfaddr.Prov ps.Functions[fnName].Detail = detailForSrcAddr(pAddr, nil) } - /* - TODO: TF-27260: - After terraform-json package is modified, - and new key called ListResourceSchemas is introduced - which reads json key from list_resource_schemas - modify below code to jsonSchema.ListResourceSchemas - */ - for lrName, lrSchema := range jsonSchema.ResourceSchemas { + for lrName, lrSchema := range jsonSchema.ListResourceSchemas { ps.ListResources[lrName] = bodySchemaFromJson(lrSchema.Block) ps.ListResources[lrName].Detail = detailForSrcAddr(pAddr, nil) } diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go index 503041f..7705493 100644 --- a/schema/search/search_schema_merge.go +++ b/schema/search/search_schema_merge.go @@ -4,14 +4,13 @@ package schema import ( - "strings" - "github.com/hashicorp/go-version" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" tfaddr "github.com/hashicorp/terraform-registry-address" tfschema "github.com/hashicorp/terraform-schema/schema" tfsearch "github.com/hashicorp/terraform-schema/search" + "strings" ) type SearchSchemaMerger struct { @@ -59,10 +58,6 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS mergedSchema.Blocks["list"].DependentBody = make(map[schema.SchemaKey]*schema.BodySchema) } - // if mergedSchema.Blocks["list"].Body.Blocks["config"].DependentBody == nil { - // mergedSchema.Blocks["list"].Body.Blocks["config"].DependentBody = make(map[schema.SchemaKey]*schema.BodySchema) - // } - if _, ok := mergedSchema.Blocks["variable"]; ok { mergedSchema.Blocks["variable"].Labels = []*schema.LabelSchema{ { @@ -81,8 +76,8 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS if err != nil { continue } - refs := providerRefs.ReferencesOfProvider(pAddr) + for _, localRef := range refs { if pSchema.Provider != nil { mergedSchema.Blocks["provider"].DependentBody[schema.NewSchemaKey(schema.DependencyKeys{ @@ -98,8 +93,7 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS if localRef.Alias != "" { providerAddr = append(providerAddr, lang.AttrStep{Name: localRef.Alias}) } - - for lrName, lrSchema := range pSchema.ListResources { + for lrName, lrSchema := range pSchema.ListResources{ depKeys := schema.DependencyKeys{ Labels: []schema.LabelDependent{ {Index: 0, Value: lrName}, @@ -115,34 +109,23 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS } mergedSchema.Blocks["list"].DependentBody[schema.NewSchemaKey(depKeys)] = lrSchema - // TODO merge list config - source them from the Terraform module meta requirements TF-27260 + // No explicit association is required + // if the resource prefix matches provider name if TypeBelongsToProvider(lrName, localRef) { - configDepKeys := schema.DependencyKeys{ + depKeys := schema.DependencyKeys{ Labels: []schema.LabelDependent{ {Index: 0, Value: lrName}, }, } - mergedSchema.Blocks["list"].DependentBody[schema.NewSchemaKey(configDepKeys)] = &schema.BodySchema{ - HoverURL: pSchema.Provider.HoverURL, - DocsLink: pSchema.Provider.DocsLink, - Detail: pSchema.Provider.Detail, - Description: pSchema.Provider.Description, - IsDeprecated: pSchema.Provider.IsDeprecated, - Blocks: map[string]*schema.BlockSchema{ - "config": { - Body: lrSchema, - }, - }, - } - - // mergedSchema.Blocks["list"].Body.Blocks["config"].DependentBody[schema.NewSchemaKey(configDepKeys)] = lrSchema - + mergedSchema.Blocks["list"].DependentBody[schema.NewSchemaKey(depKeys)] = lrSchema } } - } } + // TODO merge provider - source them from the Terraform module meta requirements TF-27288 + // TODO merge list config - source them from the Terraform module meta requirements TF-27260 + return mergedSchema, nil } @@ -179,15 +162,9 @@ func (pr ProviderReferences) ReferencesOfProvider(addr tfaddr.Provider) []tfsear refs = append(refs, ref) } } - return refs } -// TypeBelongsToProvider returns true if the given type -// (resource or data source) name belongs to a particular provider. -// -// This reflects internal implementation in Terraform at -// https://github.com/hashicorp/terraform/blob/488bbd80/internal/addrs/resource.go#L68-L77 func TypeBelongsToProvider(typeName string, pRef tfsearch.ProviderRef) bool { return typeName == pRef.LocalName || strings.HasPrefix(typeName, pRef.LocalName+"_") } diff --git a/search/meta.go b/search/meta.go index b52c7fd..be79656 100644 --- a/search/meta.go +++ b/search/meta.go @@ -12,11 +12,18 @@ type Meta struct { Path string Filenames []string - Variables map[string]Variable - Lists map[string]List - - ProviderReferences map[ProviderRef]tfaddr.Provider + Variables map[string]Variable + Lists map[string]List ProviderRequirements ProviderRequirements + ProviderReferences map[ProviderRef]tfaddr.Provider +} + +type ProviderRef struct { + LocalName string + + // If not empty, Alias identifies which non-default (aliased) provider + // configuration this address refers to. + Alias string } type ProviderRequirements map[tfaddr.Provider]version.Constraints @@ -39,10 +46,3 @@ func (pr ProviderRequirements) Equals(reqs ProviderRequirements) bool { return true } -type ProviderRef struct { - LocalName string - - // If not empty, Alias identifies which non-default (aliased) provider - // configuration this address refers to. - Alias string -} From 05f0ab53eb96cf6b4559361113568424fd0a2793 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Tue, 5 Aug 2025 11:56:31 +0530 Subject: [PATCH 17/27] Remove TODOs that are done --- schema/search/search_schema_merge.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go index 7705493..68c800e 100644 --- a/schema/search/search_schema_merge.go +++ b/schema/search/search_schema_merge.go @@ -93,7 +93,7 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS if localRef.Alias != "" { providerAddr = append(providerAddr, lang.AttrStep{Name: localRef.Alias}) } - for lrName, lrSchema := range pSchema.ListResources{ + for lrName, lrSchema := range pSchema.ListResources { depKeys := schema.DependencyKeys{ Labels: []schema.LabelDependent{ {Index: 0, Value: lrName}, @@ -122,10 +122,6 @@ func (m *SearchSchemaMerger) SchemaForSearch(meta *tfsearch.Meta) (*schema.BodyS } } } - - // TODO merge provider - source them from the Terraform module meta requirements TF-27288 - // TODO merge list config - source them from the Terraform module meta requirements TF-27260 - return mergedSchema, nil } From a7f6ada39d34b135f77711f809c52ed65e3b44bb Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Tue, 5 Aug 2025 12:13:55 +0530 Subject: [PATCH 18/27] feat(tfsearch): TF-26847 TF-27260: Fixed: test cases --- earlydecoder/search/decoder_test.go | 13 +++++++++---- earlydecoder/stacks/decoder_test.go | 2 +- schema/convert_json_test.go | 7 +++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/earlydecoder/search/decoder_test.go b/earlydecoder/search/decoder_test.go index d8c03ae..55c4f81 100644 --- a/earlydecoder/search/decoder_test.go +++ b/earlydecoder/search/decoder_test.go @@ -5,14 +5,16 @@ package earlydecoder import ( "fmt" + "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform-schema/search" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" - "testing" ) type testCase struct { @@ -41,6 +43,7 @@ func TestLoadSearch(t *testing.T) { &search.Meta{ Path: path, Filenames: []string{fileName}, Variables: map[string]search.Variable{}, Lists: map[string]search.List{}, + ProviderReferences: map[search.ProviderRef]tfaddr.Provider{}, }, map[string]hcl.Diagnostics{fileName: nil}, }, @@ -71,7 +74,8 @@ func TestLoadSearch(t *testing.T) { IsSensitive: true, }, }, - Lists: map[string]search.List{}, + Lists: map[string]search.List{}, + ProviderReferences: map[search.ProviderRef]tfaddr.Provider{}, }, map[string]hcl.Diagnostics{fileName: nil}, }, @@ -126,14 +130,15 @@ func TestLoadStackDiagnostics(t *testing.T) { DefaultValue: cty.DynamicVal, }, }, - Lists: map[string]search.List{}, + Lists: map[string]search.List{}, + ProviderReferences: map[search.ProviderRef]tfaddr.Provider{}, }, map[string]hcl.Diagnostics{ fileName: { { Severity: hcl.DiagError, Summary: `Invalid default value for variable`, - Detail: `This default value is not compatible with the variable's type constraint: string required.`, + Detail: `This default value is not compatible with the variable's type constraint: string required, but have tuple.`, Subject: &hcl.Range{ Filename: fileName, Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, diff --git a/earlydecoder/stacks/decoder_test.go b/earlydecoder/stacks/decoder_test.go index 719b46a..9245832 100644 --- a/earlydecoder/stacks/decoder_test.go +++ b/earlydecoder/stacks/decoder_test.go @@ -264,7 +264,7 @@ func TestLoadStackDiagnostics(t *testing.T) { { Severity: hcl.DiagError, Summary: `Invalid default value for variable`, - Detail: `This default value is not compatible with the variable's type constraint: string required.`, + Detail: `This default value is not compatible with the variable's type constraint: string required, but have tuple.`, Subject: &hcl.Range{ Filename: fileName, Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, diff --git a/schema/convert_json_test.go b/schema/convert_json_test.go index fc54cbc..2bee921 100644 --- a/schema/convert_json_test.go +++ b/schema/convert_json_test.go @@ -27,6 +27,7 @@ func TestProviderSchemaFromJson_empty(t *testing.T) { EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if diff := cmp.Diff(expectedPs, ps, ctydebug.CmpOptions); diff != "" { @@ -297,6 +298,7 @@ func TestProviderSchemaFromJson_basic(t *testing.T) { EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if diff := cmp.Diff(expectedPs, ps, ctydebug.CmpOptions); diff != "" { @@ -506,6 +508,7 @@ func TestProviderSchemaFromJson_nested_set_list(t *testing.T) { EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if diff := cmp.Diff(expectedPs, ps, ctydebug.CmpOptions); diff != "" { @@ -541,6 +544,8 @@ func TestProviderSchemaFromJson_function(t *testing.T) { Resources: map[string]*schema.BodySchema{}, EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, + ListResources: map[string]*schema.BodySchema{}, + Functions: map[string]*schema.FunctionSignature{ "example": { Description: "Echoes given argument as result", @@ -574,6 +579,7 @@ func TestProviderSchemaFromJson_function(t *testing.T) { Resources: map[string]*schema.BodySchema{}, EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, + ListResources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{ "example": { Description: "Returns a string", @@ -612,6 +618,7 @@ func TestProviderSchemaFromJson_function(t *testing.T) { Resources: map[string]*schema.BodySchema{}, EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, + ListResources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{ "example": { Description: "Echoes given argument as result", From 521c8882ef8ef97a3a44c06d33cc93e3eaff82eb Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 6 Aug 2025 10:36:20 +0530 Subject: [PATCH 19/27] feat(tfsearch): Modified: terraform-json version to pseudo-version --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 21cb2d1..a47e895 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-exec v0.23.0 - github.com/hashicorp/terraform-json v0.25.0 + github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 github.com/hashicorp/terraform-registry-address v0.3.0 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 - github.com/zclconf/go-cty v1.16.2 + github.com/zclconf/go-cty v1.16.3 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 ) diff --git a/go.sum b/go.sum index a55bfc8..982480d 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= -github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= -github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= +github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 h1:2roeYw1L7XQ9ggNMM/5YgPrbBpuh44uQk2bJh+7w8g8= +github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -76,8 +76,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= -github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= +github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= From 06fbe50d34ccf8e95c53fc7e3dfc797802104b8d Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 6 Aug 2025 10:37:53 +0530 Subject: [PATCH 20/27] feat(tfsearch): Modified: ran go fmt --- search/meta.go | 1 - 1 file changed, 1 deletion(-) diff --git a/search/meta.go b/search/meta.go index be79656..9e7e064 100644 --- a/search/meta.go +++ b/search/meta.go @@ -45,4 +45,3 @@ func (pr ProviderRequirements) Equals(reqs ProviderRequirements) bool { return true } - From fbf3add6f98a135187576e5c8cb642b1cd1e3666 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Wed, 6 Aug 2025 11:43:25 +0530 Subject: [PATCH 21/27] Update testcases --- earlydecoder/search/decoder.go | 12 +++++++----- earlydecoder/search/decoder_test.go | 21 +++++++++++++++------ earlydecoder/stacks/decoder_test.go | 2 +- go.mod | 4 +++- go.sum | 6 ++---- schema/convert_json_test.go | 6 ++++++ 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/earlydecoder/search/decoder.go b/earlydecoder/search/decoder.go index 718502f..7dad811 100644 --- a/earlydecoder/search/decoder.go +++ b/earlydecoder/search/decoder.go @@ -36,6 +36,7 @@ func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[stri } refs := make(map[search.ProviderRef]tfaddr.Provider, 0) + requirements := make(search.ProviderRequirements, 0) for _, cfg := range mod.ProviderConfigs { src := refs[search.ProviderRef{ @@ -50,11 +51,12 @@ func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[stri } return &search.Meta{ - Path: path, - Filenames: filenames, - Variables: variables, - Lists: lists, - ProviderReferences: refs, + Path: path, + Filenames: filenames, + Variables: variables, + Lists: lists, + ProviderReferences: refs, + ProviderRequirements: requirements, }, diags } diff --git a/earlydecoder/search/decoder_test.go b/earlydecoder/search/decoder_test.go index d8c03ae..ef4f6a1 100644 --- a/earlydecoder/search/decoder_test.go +++ b/earlydecoder/search/decoder_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform-schema/search" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" @@ -39,8 +40,12 @@ func TestLoadSearch(t *testing.T) { ``, fileName, &search.Meta{ - Path: path, - Filenames: []string{fileName}, Variables: map[string]search.Variable{}, Lists: map[string]search.List{}, + Path: path, + Filenames: []string{fileName}, + Variables: map[string]search.Variable{}, + Lists: map[string]search.List{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + ProviderReferences: map[search.ProviderRef]tfaddr.Provider{}, }, map[string]hcl.Diagnostics{fileName: nil}, }, @@ -71,7 +76,9 @@ func TestLoadSearch(t *testing.T) { IsSensitive: true, }, }, - Lists: map[string]search.List{}, + Lists: map[string]search.List{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + ProviderReferences: map[search.ProviderRef]tfaddr.Provider{}, }, map[string]hcl.Diagnostics{fileName: nil}, }, @@ -106,7 +113,7 @@ func runTestCases(testCases []testCase, t *testing.T, path string) { } } -func TestLoadStackDiagnostics(t *testing.T) { +func TestLoadSearchDiagnostics(t *testing.T) { path := t.TempDir() testCases := []testCase{ @@ -126,14 +133,16 @@ func TestLoadStackDiagnostics(t *testing.T) { DefaultValue: cty.DynamicVal, }, }, - Lists: map[string]search.List{}, + Lists: map[string]search.List{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + ProviderReferences: map[search.ProviderRef]tfaddr.Provider{}, }, map[string]hcl.Diagnostics{ fileName: { { Severity: hcl.DiagError, Summary: `Invalid default value for variable`, - Detail: `This default value is not compatible with the variable's type constraint: string required.`, + Detail: `This default value is not compatible with the variable's type constraint: string required, but have tuple.`, Subject: &hcl.Range{ Filename: fileName, Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, diff --git a/earlydecoder/stacks/decoder_test.go b/earlydecoder/stacks/decoder_test.go index 719b46a..9245832 100644 --- a/earlydecoder/stacks/decoder_test.go +++ b/earlydecoder/stacks/decoder_test.go @@ -264,7 +264,7 @@ func TestLoadStackDiagnostics(t *testing.T) { { Severity: hcl.DiagError, Summary: `Invalid default value for variable`, - Detail: `This default value is not compatible with the variable's type constraint: string required.`, + Detail: `This default value is not compatible with the variable's type constraint: string required, but have tuple.`, Subject: &hcl.Range{ Filename: fileName, Start: hcl.Pos{Line: 3, Column: 13, Byte: 49}, diff --git a/go.mod b/go.mod index 21cb2d1..ec4783f 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/terraform-json v0.25.0 github.com/hashicorp/terraform-registry-address v0.3.0 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 - github.com/zclconf/go-cty v1.16.2 + github.com/zclconf/go-cty v1.16.3 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 ) @@ -35,3 +35,5 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/tools v0.34.0 // indirect ) + +replace github.com/hashicorp/terraform-json => ../terraform-json diff --git a/go.sum b/go.sum index a55bfc8..568cec8 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,6 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= -github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= -github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -76,8 +74,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= -github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= +github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= diff --git a/schema/convert_json_test.go b/schema/convert_json_test.go index fc54cbc..736e17f 100644 --- a/schema/convert_json_test.go +++ b/schema/convert_json_test.go @@ -27,6 +27,7 @@ func TestProviderSchemaFromJson_empty(t *testing.T) { EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if diff := cmp.Diff(expectedPs, ps, ctydebug.CmpOptions); diff != "" { @@ -297,6 +298,7 @@ func TestProviderSchemaFromJson_basic(t *testing.T) { EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if diff := cmp.Diff(expectedPs, ps, ctydebug.CmpOptions); diff != "" { @@ -506,6 +508,7 @@ func TestProviderSchemaFromJson_nested_set_list(t *testing.T) { EphemeralResources: map[string]*schema.BodySchema{}, DataSources: map[string]*schema.BodySchema{}, Functions: map[string]*schema.FunctionSignature{}, + ListResources: map[string]*schema.BodySchema{}, } if diff := cmp.Diff(expectedPs, ps, ctydebug.CmpOptions); diff != "" { @@ -556,6 +559,7 @@ func TestProviderSchemaFromJson_function(t *testing.T) { VarParam: nil, }, }, + ListResources: map[string]*schema.BodySchema{}, }, }, { @@ -583,6 +587,7 @@ func TestProviderSchemaFromJson_function(t *testing.T) { VarParam: nil, }, }, + ListResources: map[string]*schema.BodySchema{}, }, }, { @@ -631,6 +636,7 @@ func TestProviderSchemaFromJson_function(t *testing.T) { }, }, }, + ListResources: map[string]*schema.BodySchema{}, }, }, } From 8e0dc29335d4f7096a7cefca74253b20d53b7ccc Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 6 Aug 2025 11:52:48 +0530 Subject: [PATCH 22/27] feat(tfsearch): TF-26847: Modified: Ran 'go generate ./schema --- schema/versions_gen.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/schema/versions_gen.go b/schema/versions_gen.go index a5a4770..62215ad 100755 --- a/schema/versions_gen.go +++ b/schema/versions_gen.go @@ -10,7 +10,10 @@ var ( LatestAvailableVersion = version.Must(version.NewVersion("1.12.2")) terraformVersions = version.Collection{ + version.Must(version.NewVersion("1.14.0-alpha20250724")), version.Must(version.NewVersion("1.14.0-alpha20250716")), + version.Must(version.NewVersion("1.13.0-beta3")), + version.Must(version.NewVersion("1.13.0-beta2")), version.Must(version.NewVersion("1.13.0-beta1")), version.Must(version.NewVersion("1.13.0-alpha20250708")), version.Must(version.NewVersion("1.13.0-alpha20250702")), From 318dd126969ef081607d1be686f2385015c02275 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Wed, 6 Aug 2025 11:57:08 +0530 Subject: [PATCH 23/27] Merge branch 'feature/tfsearch-TF-26847' of github.com:hashicorp/terraform-schema into feature/tfsearch-TF-26847 --- go.mod | 4 +--- go.sum | 2 ++ search/meta.go | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index ec4783f..a47e895 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-exec v0.23.0 - github.com/hashicorp/terraform-json v0.25.0 + github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 github.com/hashicorp/terraform-registry-address v0.3.0 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/zclconf/go-cty v1.16.3 @@ -35,5 +35,3 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/tools v0.34.0 // indirect ) - -replace github.com/hashicorp/terraform-json => ../terraform-json diff --git a/go.sum b/go.sum index 568cec8..982480d 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 h1:2roeYw1L7XQ9ggNMM/5YgPrbBpuh44uQk2bJh+7w8g8= +github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/search/meta.go b/search/meta.go index be79656..9e7e064 100644 --- a/search/meta.go +++ b/search/meta.go @@ -45,4 +45,3 @@ func (pr ProviderRequirements) Equals(reqs ProviderRequirements) bool { return true } - From ad1198036c3ba1cbf649265fa1bec0c03480d095 Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 6 Aug 2025 13:38:29 +0530 Subject: [PATCH 24/27] feat(tfsearch): TF-26847: Modified: added terraform version in meta --- earlydecoder/search/schema.go | 8 -------- search/meta.go | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/earlydecoder/search/schema.go b/earlydecoder/search/schema.go index d776c70..3b75032 100644 --- a/earlydecoder/search/schema.go +++ b/earlydecoder/search/schema.go @@ -24,14 +24,6 @@ var rootSchema = &hcl.BodySchema{ }, } -var listSchema = &hcl.BodySchema{ - Attributes: []hcl.AttributeSchema{ - { - Name: "include_resource", - }, - }, -} - var variableSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { diff --git a/search/meta.go b/search/meta.go index 9e7e064..b6afa01 100644 --- a/search/meta.go +++ b/search/meta.go @@ -12,6 +12,7 @@ type Meta struct { Path string Filenames []string + CoreRequirements version.Constraints Variables map[string]Variable Lists map[string]List ProviderRequirements ProviderRequirements From 7809a317925d5725026e5147d532236867ca4803 Mon Sep 17 00:00:00 2001 From: sunnyhashi Date: Sun, 10 Aug 2025 18:16:39 +0530 Subject: [PATCH 25/27] feature/tfsearch-TF-26847: test: Add test for search_schema_merge --- schema/search/search_schema_merge_test.go | 506 ++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 schema/search/search_schema_merge_test.go diff --git a/schema/search/search_schema_merge_test.go b/schema/search/search_schema_merge_test.go new file mode 100644 index 0000000..39a683e --- /dev/null +++ b/schema/search/search_schema_merge_test.go @@ -0,0 +1,506 @@ +package schema + +import ( + "errors" + "fmt" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + tfjson "github.com/hashicorp/terraform-json" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform-schema/internal/addr" + tfmod "github.com/hashicorp/terraform-schema/module" + tfschema "github.com/hashicorp/terraform-schema/schema" + tfsearch "github.com/hashicorp/terraform-schema/search" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestSearchSchemaMerger_SchemaForSearch_noCoreSchema(t *testing.T) { + sm := NewSearchSchemaMerger(nil) + + _, err := sm.SchemaForSearch(nil) + if err == nil { + t.Fatal("expected error for nil core schema") + } + + if !errors.Is(err, tfschema.CoreSchemaRequiredErr{}) { + t.Fatalf("unexpected error: %#v", err) + } +} + +func TestSearchSchemaMerger_SchemaForSearch_noProviderSchema(t *testing.T) { + testCoreSchema := &schema.BodySchema{} + + sm := NewSearchSchemaMerger(testCoreSchema) + + _, err := sm.SchemaForSearch(&tfsearch.Meta{}) + if err != nil { + t.Fatal(err) + } +} + +func TestSearchSchemaMerger_SchemaForSearch_providerNameMatch(t *testing.T) { + testCoreSchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + }, + "list": {}, + }, + } + sm := NewSearchSchemaMerger(testCoreSchema) + sm.SetStateReader(&testSearchSchemaReader{ + ps: &tfjson.ProviderSchemas{ + FormatVersion: "1.0", + Schemas: map[string]*tfjson.ProviderSchema{ + "registry.terraform.io/hashicorp/data": { + ConfigSchema: &tfjson.Schema{ + Block: &tfjson.SchemaBlock{ + Attributes: map[string]*tfjson.SchemaAttribute{ + "foobar": { + AttributeType: cty.Bool, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }) + + givenBodySchema, err := sm.SchemaForSearch(&tfsearch.Meta{ + ProviderReferences: map[tfsearch.ProviderRef]tfaddr.Provider{ + {LocalName: "data"}: addr.NewDefaultProvider("data"), + }, + ProviderRequirements: tfsearch.ProviderRequirements{ + addr.NewDefaultProvider("data"): version.MustConstraints(version.NewConstraint("1.0")), + }, + }) + + if err != nil { + t.Fatal(err) + } + expectedBodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + `{"labels":[{"index":0,"value":"data"}]}`: { + Blocks: map[string]*schema.BlockSchema{}, + Attributes: map[string]*schema.AttributeSchema{ + "foobar": { + IsOptional: true, + Constraint: schema.AnyExpression{OfType: cty.Bool}, + }, + }, + Detail: "hashicorp/data", + DocsLink: &schema.DocsLink{ + URL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + Tooltip: "hashicorp/data Documentation", + }, + HoverURL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + }, + }, + }, + "list": { + DependentBody: map[schema.SchemaKey]*schema.BodySchema{}, + }, + }, + } + + if diff := cmp.Diff(expectedBodySchema, givenBodySchema, ctydebug.CmpOptions); diff != "" { + t.Fatalf("schema mismatch: %s", diff) + } +} + +func TestSchemaMerger_SchemaForModule_twiceMerged(t *testing.T) { + testCoreSchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + }, + "list": {}, + }, + } + sm := NewSearchSchemaMerger(testCoreSchema) + sm.SetStateReader(&testSearchSchemaReader{ + ps: &tfjson.ProviderSchemas{ + FormatVersion: "1.0", + Schemas: map[string]*tfjson.ProviderSchema{ + "registry.terraform.io/hashicorp/data": { + ConfigSchema: &tfjson.Schema{ + Block: &tfjson.SchemaBlock{ + Attributes: map[string]*tfjson.SchemaAttribute{ + "foobar": { + AttributeType: cty.Bool, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }) + + givenBodySchema, err := sm.SchemaForSearch(&tfsearch.Meta{ + ProviderReferences: map[tfsearch.ProviderRef]tfaddr.Provider{ + {LocalName: "data"}: addr.NewDefaultProvider("data"), + }, + ProviderRequirements: tfsearch.ProviderRequirements{ + addr.NewDefaultProvider("data"): version.MustConstraints(version.NewConstraint("1.0")), + }, + }) + + if err != nil { + t.Fatal(err) + } + expectedBodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + `{"labels":[{"index":0,"value":"data"}]}`: { + Blocks: map[string]*schema.BlockSchema{}, + Attributes: map[string]*schema.AttributeSchema{ + "foobar": { + IsOptional: true, + Constraint: schema.AnyExpression{OfType: cty.Bool}, + }, + }, + Detail: "hashicorp/data", + DocsLink: &schema.DocsLink{ + URL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + Tooltip: "hashicorp/data Documentation", + }, + HoverURL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + }, + }, + }, + "list": { + DependentBody: map[schema.SchemaKey]*schema.BodySchema{}, + }, + }, + } + + if diff := cmp.Diff(expectedBodySchema, givenBodySchema, ctydebug.CmpOptions); diff != "" { + t.Fatalf("schema mismatch: %s", diff) + } + + // now merge again with different local name + givenBodySchema, err = sm.SchemaForSearch(&tfsearch.Meta{ + ProviderReferences: map[tfsearch.ProviderRef]tfaddr.Provider{ + {LocalName: "test_data"}: addr.NewDefaultProvider("data"), + }, + ProviderRequirements: tfsearch.ProviderRequirements{ + addr.NewDefaultProvider("data"): version.MustConstraints(version.NewConstraint("1.0")), + }, + }) + + if err != nil { + t.Fatal(err) + } + expectedBodySchema = &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + `{"labels":[{"index":0,"value":"test_data"}]}`: { + Blocks: map[string]*schema.BlockSchema{}, + Attributes: map[string]*schema.AttributeSchema{ + "foobar": { + IsOptional: true, + Constraint: schema.AnyExpression{OfType: cty.Bool}, + }, + }, + Detail: "hashicorp/data", + DocsLink: &schema.DocsLink{ + URL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + Tooltip: "hashicorp/data Documentation", + }, + HoverURL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + }, + }, + }, + "list": { + DependentBody: map[schema.SchemaKey]*schema.BodySchema{}, + }, + }, + } + + if diff := cmp.Diff(expectedBodySchema, givenBodySchema, ctydebug.CmpOptions); diff != "" { + t.Fatalf("schema mismatch: %s", diff) + } +} + +func TestStackSchemaMerger_SchemaForStack_variables(t *testing.T) { + testCoreSchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": {}, + "list": {}, + "variable": {}, + }, + } + sm := NewSearchSchemaMerger(testCoreSchema) + sm.SetStateReader(&testSearchSchemaReader{}) + + givenBodySchema, err := sm.SchemaForSearch(&tfsearch.Meta{ + Variables: map[string]tfsearch.Variable{ + "foo": {Type: cty.String, Description: "A foo variable", IsSensitive: true, DefaultValue: cty.StringVal("bar")}, + }, + }) + if err != nil { + t.Fatal(err) + } + expectedBodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + DependentBody: map[schema.SchemaKey]*schema.BodySchema{}, + }, + "list": { + DependentBody: map[schema.SchemaKey]*schema.BodySchema{}, + }, + "variable": { + Labels: []*schema.LabelSchema{{Name: "name", IsDepKey: true, Description: lang.MarkupContent{Value: "Variable name", Kind: lang.PlainTextKind}}}, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + `{"labels":[{"index":0,"value":"foo"}]}`: { + Attributes: map[string]*schema.AttributeSchema{ + "default": { + Constraint: schema.LiteralType{Type: cty.String}, + Description: lang.MarkupContent{Value: "Default value to use when variable is not explicitly set", Kind: lang.MarkdownKind}, + IsOptional: true, + }, + }, + }, + }, + }, + }, + } + + if diff := cmp.Diff(expectedBodySchema, givenBodySchema, ctydebug.CmpOptions); diff != "" { + t.Fatalf("schema mismatch: %s", diff) + } +} + +func TestStackSchemaMerger_SchemaForSearch_lists(t *testing.T) { + testCoreSchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + }, + "list": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + }, + }, + } + sm := NewSearchSchemaMerger(testCoreSchema) + sm.SetStateReader(&testSearchSchemaReader{ + ps: &tfjson.ProviderSchemas{ + FormatVersion: "1.0", + Schemas: map[string]*tfjson.ProviderSchema{ + "registry.terraform.io/hashicorp/data": { + ConfigSchema: &tfjson.Schema{ + Block: &tfjson.SchemaBlock{ + Attributes: map[string]*tfjson.SchemaAttribute{ + "foobar": { + AttributeType: cty.Bool, + Optional: true, + }, + }, + }, + }, + ListResourceSchemas: map[string]*tfjson.Schema{ + "dummy_resource": { + Block: &tfjson.SchemaBlock{ + NestedBlocks: map[string]*tfjson.SchemaBlockType{ + "config": { + Block: &tfjson.SchemaBlock{ + Attributes: map[string]*tfjson.SchemaAttribute{ + "count": { + AttributeType: cty.Number, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }) + + givenBodySchema, err := sm.SchemaForSearch(&tfsearch.Meta{ + ProviderReferences: map[tfsearch.ProviderRef]tfaddr.Provider{ + {LocalName: "data"}: addr.NewDefaultProvider("data"), + }, + ProviderRequirements: tfsearch.ProviderRequirements{ + addr.NewDefaultProvider("data"): version.MustConstraints(version.NewConstraint("1.0")), + }, + }) + if err != nil { + t.Fatal(err) + } + expectedBodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "alias": {Constraint: schema.LiteralType{Type: cty.String}, IsOptional: true}, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + `{"labels":[{"index":0,"value":"data"}]}`: { + Blocks: map[string]*schema.BlockSchema{}, + Attributes: map[string]*schema.AttributeSchema{ + "foobar": { + IsOptional: true, + Constraint: schema.AnyExpression{OfType: cty.Bool}, + }, + }, + Detail: "hashicorp/data", + DocsLink: &schema.DocsLink{ + URL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + Tooltip: "hashicorp/data Documentation", + }, + HoverURL: "https://registry.terraform.io/providers/hashicorp/data/latest/docs", + }, + }, + }, + "list": { + Labels: []*schema.LabelSchema{ + {Name: "name"}, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + `{"labels":[{"index":0,"value":"dummy_resource"}],"attrs":[{"name":"provider","expr":{"addr":"data"}}]}`: { + Detail: "hashicorp/data", + Blocks: map[string]*schema.BlockSchema{ + "config": { + Labels: []*schema.LabelSchema{}, + Body: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{}, + Attributes: map[string]*schema.AttributeSchema{ + "count": { + IsOptional: true, + Constraint: schema.AnyExpression{OfType: cty.Number}, + }, + }, + }, + }, + }, + Attributes: map[string]*schema.AttributeSchema{}, + }, + }, + }, + }, + } + + if diff := cmp.Diff(expectedBodySchema, givenBodySchema, ctydebug.CmpOptions); diff != "" { + t.Fatalf("schema mismatch: %s", diff) + } +} + +type testSearchSchemaReader struct { + ps *tfjson.ProviderSchemas +} + +func (r *testSearchSchemaReader) InstalledModulePath(rootPath string, normalizedSource string) (string, bool) { + if normalizedSource == "git::https://example.com/vpc.git" { + return "fake/git/path", true + } + if normalizedSource == "registry.terraform.io/registry/source/test" { + return "fake/registry/path", true + } + + return "", false +} + +func (r *testSearchSchemaReader) ProviderSchema(modPath string, addr tfaddr.Provider, vc version.Constraints) (*tfschema.ProviderSchema, error) { + jsonSchema, ok := r.ps.Schemas[addr.String()] + if !ok { + return nil, fmt.Errorf("%s: schema not found", addr.String()) + } + + return tfschema.ProviderSchemaFromJson(jsonSchema, addr), nil +} + +func (r *testSearchSchemaReader) LocalModuleMeta(modPath string) (*tfmod.Meta, error) { + switch filepath.ToSlash(modPath) { + case "fake/git/path": + return &tfmod.Meta{ + Variables: map[string]tfmod.Variable{ + "foo": {Type: cty.String}, + }, + }, nil + case "fake/registry/path": + return &tfmod.Meta{ + Outputs: map[string]tfmod.Output{ + "bar": {Value: cty.DynamicVal}, + }, + }, nil + case "local/path": + return &tfmod.Meta{ + ProviderReferences: map[tfmod.ProviderRef]tfaddr.Provider{ + {LocalName: "test"}: tfaddr.NewProvider("registry.terraform.io", "hashicorp", "test"), + }, + Filenames: []string{"main.tf"}, + }, nil + } + + return nil, fmt.Errorf("invalid source") +} From b86d395f961670d5aea3546615ddb4f4b8ef2649 Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Mon, 11 Aug 2025 15:16:23 +0530 Subject: [PATCH 26/27] feat(tfsearch): TF-26847: Fixed: lint error --- schema/search/search_schema_merge.go | 3 ++- schema/search/search_schema_merge_test.go | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/schema/search/search_schema_merge.go b/schema/search/search_schema_merge.go index 68c800e..bf86699 100644 --- a/schema/search/search_schema_merge.go +++ b/schema/search/search_schema_merge.go @@ -4,13 +4,14 @@ package schema import ( + "strings" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" tfaddr "github.com/hashicorp/terraform-registry-address" tfschema "github.com/hashicorp/terraform-schema/schema" tfsearch "github.com/hashicorp/terraform-schema/search" - "strings" ) type SearchSchemaMerger struct { diff --git a/schema/search/search_schema_merge_test.go b/schema/search/search_schema_merge_test.go index 39a683e..adbf512 100644 --- a/schema/search/search_schema_merge_test.go +++ b/schema/search/search_schema_merge_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package schema import ( From 80c1a7ad993e9f0056254a666b0da2ea53b47d81 Mon Sep 17 00:00:00 2001 From: Anubhav Goel Date: Wed, 13 Aug 2025 11:12:10 +0530 Subject: [PATCH 27/27] feat(tfsearch): TF-26847: Bumped: terraform-json --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a47e895..7067f61 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-exec v0.23.0 - github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 + github.com/hashicorp/terraform-json v0.26.0 github.com/hashicorp/terraform-registry-address v0.3.0 github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5 github.com/zclconf/go-cty v1.16.3 diff --git a/go.sum b/go.sum index 982480d..07bba1c 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3q github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= -github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 h1:2roeYw1L7XQ9ggNMM/5YgPrbBpuh44uQk2bJh+7w8g8= -github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4= +github.com/hashicorp/terraform-json v0.26.0 h1:+BnJavhRH+oyNWPnfzrfQwVWCZBFMvjdiH2Vi38Udz4= +github.com/hashicorp/terraform-json v0.26.0/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=