Skip to content

Support for Terraform Search (.tfquery.hcl) files #469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1ba4bb1
Search: TF-27258: chore: add initial set of files for earlydecoder/se…
sunnyhashi Jul 1, 2025
f0e7c55
feat(tfsearch): TF-26847 TF-27259: Added: static tfsearch schemas
anubhav-goel Jul 1, 2025
6e6fa1c
Search: TF-27258: feat: complete earlydecoder functionality for search
sunnyhashi Jul 1, 2025
a9d702f
Search: TF-27258: fix: Add license header
sunnyhashi Jul 1, 2025
1a74696
Search: TF-27258: test: fix incorrect test and update test data
sunnyhashi Jul 1, 2025
68f90a2
feat(tfsearch): TF-26847 TF-27259: Modified: include_resource constraint
anubhav-goel Jul 2, 2025
e124841
feat(tfsearch): TF-26847 TF-27259: Modified: limit constraint
anubhav-goel Jul 3, 2025
be3e46d
feat(tfsearch): TF-26847 TF-27259: Modified: added config block to li…
anubhav-goel Jul 4, 2025
20ef96d
feat(tfsearch): TF-26847 TF-27259: Modified: added schema validations…
anubhav-goel Jul 7, 2025
b485530
feat(tfsearch): TF-26847 TF-27259: Modified: allow include_resource t…
anubhav-goel Jul 8, 2025
6d48f7e
feat(tfsearch): TF-26847 TF-27288: Added: dynamic schema for provider…
anubhav-goel Jul 30, 2025
acaa7b9
feat(tfsearch): TF-26847 TF-27260: Added: dynamic list type of list b…
anubhav-goel Jul 31, 2025
7881d2b
feat(tfsearch): TF-26847 TF-27260: Modified: todo comment with details
anubhav-goel Jul 31, 2025
c62e07b
feat(tfsearch): TF-26847 TF-27260: Modified: wip dynamic list config
anubhav-goel Aug 4, 2025
b79b6f0
Added provider blocking processing
sunnyhashi Aug 5, 2025
3386853
Enable dynamic auto complete for list block
sunnyhashi Aug 5, 2025
05f0ab5
Remove TODOs that are done
sunnyhashi Aug 5, 2025
a7f6ada
feat(tfsearch): TF-26847 TF-27260: Fixed: test cases
anubhav-goel Aug 5, 2025
521c888
feat(tfsearch): Modified: terraform-json version to pseudo-version
anubhav-goel Aug 6, 2025
06fbe50
feat(tfsearch): Modified: ran go fmt
anubhav-goel Aug 6, 2025
fbf3add
Update testcases
sunnyhashi Aug 6, 2025
c16e1dd
Merge branch 'main' of github.com:hashicorp/terraform-schema into fea…
anubhav-goel Aug 6, 2025
8e0dc29
feat(tfsearch): TF-26847: Modified: Ran 'go generate ./schema
anubhav-goel Aug 6, 2025
938d2b3
Merge branch 'feature/tfsearch-TF-26847' of github.com:hashicorp/terr…
sunnyhashi Aug 6, 2025
318dd12
Merge branch 'feature/tfsearch-TF-26847' of github.com:hashicorp/terr…
sunnyhashi Aug 6, 2025
353fd94
Merge branch 'feature/tfsearch-TF-26847' of github.com:hashicorp/terr…
sunnyhashi Aug 6, 2025
ad11980
feat(tfsearch): TF-26847: Modified: added terraform version in meta
anubhav-goel Aug 6, 2025
7809a31
feature/tfsearch-TF-26847: test: Add test for search_schema_merge
sunnyhashi Aug 10, 2025
6345a12
Merge branch 'feature/tfsearch-TF-26847' of github.com:hashicorp/terr…
sunnyhashi Aug 11, 2025
b86d395
feat(tfsearch): TF-26847: Fixed: lint error
anubhav-goel Aug 11, 2025
80c1a7a
feat(tfsearch): TF-26847: Bumped: terraform-json
anubhav-goel Aug 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions earlydecoder/search/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package earlydecoder

import (
"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) {
filenames := make([]string, 0)
diags := make(map[string]hcl.Diagnostics, 0)

mod := newDecodedSearch()
for filename, f := range files {
filenames = append(filenames, filename)
if isSearchFile(filename) {
diags[filename] = loadSearchFromFile(f, mod)
}
}

sort.Strings(filenames)

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
}

refs := make(map[search.ProviderRef]tfaddr.Provider, 0)
requirements := make(search.ProviderRequirements, 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: refs,
ProviderRequirements: requirements,
}, diags
}

func isSearchFile(name string) bool {
return strings.HasSuffix(name, ".tfquery.hcl") ||
strings.HasSuffix(name, ".tfquery.hcl.json")
}
162 changes: 162 additions & 0 deletions earlydecoder/search/decoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) HashiCorp, Inc.
// 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"
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 {
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{},
ProviderRequirements: map[tfaddr.Provider]version.Constraints{},
ProviderReferences: map[search.ProviderRef]tfaddr.Provider{},
},
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{},
ProviderRequirements: map[tfaddr.Provider]version.Constraints{},
ProviderReferences: map[search.ProviderRef]tfaddr.Provider{},
},
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 TestLoadSearchDiagnostics(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{},
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, but have tuple.`,
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)
}
139 changes: 139 additions & 0 deletions earlydecoder/search/load_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

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 providerConfig struct {
Name string
Alias string
}

type decodedSearch struct {
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),
ProviderConfigs: make(map[string]*providerConfig),
}
}

func loadSearchFromFile(file *hcl.File, ds *decodedSearch) hcl.Diagnostics {
var diags hcl.Diagnostics

content, _, contentDiags := file.Body.PartialContent(rootSchema)
diags = append(diags, contentDiags...)

for _, block := range content.Blocks {
switch block.Type {
case "variable":
Comment on lines +54 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A list block is missing here. The early decoder is important in early parsing of the file, before the rest of the machinery is "ready". Is there a reason why we're not including the primary block for search here?

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 "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
}
Loading
Loading