Skip to content

Commit 26de354

Browse files
Support for Terraform Search (.tfquery.hcl) files (#469)
* Search: TF-27258: chore: add initial set of files for earlydecoder/search * feat(tfsearch): TF-26847 TF-27259: Added: static tfsearch schemas * Search: TF-27258: feat: complete earlydecoder functionality for search * Search: TF-27258: fix: Add license header * Search: TF-27258: test: fix incorrect test and update test data * feat(tfsearch): TF-26847 TF-27259: Modified: include_resource constraint * feat(tfsearch): TF-26847 TF-27259: Modified: limit constraint * feat(tfsearch): TF-26847 TF-27259: Modified: added config block to list block * feat(tfsearch): TF-26847 TF-27259: Modified: added schema validations for config and provider * feat(tfsearch): TF-26847 TF-27259: Modified: allow include_resource to take values from local & variables * feat(tfsearch): TF-26847 TF-27288: Added: dynamic schema for provider block * feat(tfsearch): TF-26847 TF-27260: Added: dynamic list type of list block as per available providers * feat(tfsearch): TF-26847 TF-27260: Modified: todo comment with details * feat(tfsearch): TF-26847 TF-27260: Modified: wip dynamic list config * Added provider blocking processing * Enable dynamic auto complete for list block * Remove TODOs that are done * feat(tfsearch): TF-26847 TF-27260: Fixed: test cases * feat(tfsearch): Modified: terraform-json version to pseudo-version * feat(tfsearch): Modified: ran go fmt * Update testcases * feat(tfsearch): TF-26847: Modified: Ran 'go generate ./schema * Merge branch 'feature/tfsearch-TF-26847' of github.com:hashicorp/terraform-schema into feature/tfsearch-TF-26847 * feat(tfsearch): TF-26847: Modified: added terraform version in meta * feature/tfsearch-TF-26847: test: Add test for search_schema_merge * feat(tfsearch): TF-26847: Fixed: lint error * feat(tfsearch): TF-26847: Bumped: terraform-json * Search: TF-27258: fix: Remove static config block from list * Search: TF-27258: fix: Remove provider schema attributes from top level list schema * Search: TF-27258: test: update testcases for search * feat(tfsearch): TF-26847: Modified: static config block * feat(tfsearch): TF-26847: Modified: removed deprecated version field in provider * feat(tfsearch): TF-26847: Modified: dependent body config * feat(tfsearch): TF-26847: Modified: removed second dependency key list config * feat(tfsearch): TF-26847: Modified: taking provider requirements from module meta --------- Co-authored-by: sunnyhashi <[email protected]>
1 parent 51263cf commit 26de354

23 files changed

+1520
-7
lines changed

earlydecoder/search/decoder.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package earlydecoder
5+
6+
import (
7+
"sort"
8+
"strings"
9+
10+
"github.com/hashicorp/hcl/v2"
11+
tfaddr "github.com/hashicorp/terraform-registry-address"
12+
"github.com/hashicorp/terraform-schema/search"
13+
)
14+
15+
func LoadSearch(path string, files map[string]*hcl.File) (*search.Meta, map[string]hcl.Diagnostics) {
16+
filenames := make([]string, 0)
17+
diags := make(map[string]hcl.Diagnostics, 0)
18+
19+
mod := newDecodedSearch()
20+
for filename, f := range files {
21+
filenames = append(filenames, filename)
22+
if isSearchFile(filename) {
23+
diags[filename] = loadSearchFromFile(f, mod)
24+
}
25+
}
26+
27+
sort.Strings(filenames)
28+
29+
variables := make(map[string]search.Variable)
30+
for key, variable := range mod.Variables {
31+
variables[key] = *variable
32+
}
33+
34+
lists := make(map[string]search.List)
35+
for key, list := range mod.List {
36+
lists[key] = *list
37+
}
38+
39+
refs := make(map[search.ProviderRef]tfaddr.Provider, 0)
40+
41+
for _, cfg := range mod.ProviderConfigs {
42+
src := refs[search.ProviderRef{
43+
LocalName: cfg.Name,
44+
}]
45+
if cfg.Alias != "" {
46+
refs[search.ProviderRef{
47+
LocalName: cfg.Name,
48+
Alias: cfg.Alias,
49+
}] = src
50+
}
51+
}
52+
53+
return &search.Meta{
54+
Path: path,
55+
Filenames: filenames,
56+
Variables: variables,
57+
Lists: lists,
58+
ProviderReferences: refs,
59+
}, diags
60+
}
61+
62+
func isSearchFile(name string) bool {
63+
return strings.HasSuffix(name, ".tfquery.hcl") ||
64+
strings.HasSuffix(name, ".tfquery.hcl.json")
65+
}

earlydecoder/search/decoder_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package earlydecoder
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/go-version"
12+
"github.com/hashicorp/hcl/v2"
13+
"github.com/hashicorp/hcl/v2/hclsyntax"
14+
tfaddr "github.com/hashicorp/terraform-registry-address"
15+
"github.com/hashicorp/terraform-schema/search"
16+
"github.com/zclconf/go-cty-debug/ctydebug"
17+
"github.com/zclconf/go-cty/cty"
18+
)
19+
20+
type testCase struct {
21+
name string
22+
cfg string
23+
fileName string
24+
expectedMeta *search.Meta
25+
expectedError map[string]hcl.Diagnostics
26+
}
27+
28+
var customComparer = []cmp.Option{
29+
cmp.Comparer(compareVersionConstraint),
30+
ctydebug.CmpOptions,
31+
}
32+
33+
var fileName = "test.tfquery.hcl"
34+
35+
func TestLoadSearch(t *testing.T) {
36+
path := t.TempDir()
37+
38+
testCases := []testCase{
39+
{
40+
"empty config",
41+
``,
42+
fileName,
43+
&search.Meta{
44+
Path: path,
45+
Filenames: []string{fileName},
46+
Variables: map[string]search.Variable{},
47+
Lists: map[string]search.List{},
48+
ProviderReferences: map[search.ProviderRef]tfaddr.Provider{},
49+
},
50+
map[string]hcl.Diagnostics{fileName: nil},
51+
},
52+
{
53+
"variables",
54+
`variable "example" {
55+
type = string
56+
default = "default_value"
57+
}
58+
59+
variable "example2" {
60+
description = "description"
61+
sensitive = true
62+
}`,
63+
fileName,
64+
&search.Meta{
65+
66+
Path: path,
67+
Filenames: []string{fileName},
68+
Variables: map[string]search.Variable{
69+
"example": {
70+
Type: cty.String,
71+
DefaultValue: cty.StringVal("default_value"),
72+
},
73+
"example2": {
74+
Type: cty.DynamicPseudoType,
75+
Description: "description",
76+
IsSensitive: true,
77+
},
78+
},
79+
Lists: map[string]search.List{},
80+
ProviderReferences: map[search.ProviderRef]tfaddr.Provider{},
81+
},
82+
map[string]hcl.Diagnostics{fileName: nil},
83+
},
84+
}
85+
86+
runTestCases(testCases, t, path)
87+
88+
}
89+
90+
func runTestCases(testCases []testCase, t *testing.T, path string) {
91+
for i, tc := range testCases {
92+
t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
93+
f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), tc.fileName, hcl.InitialPos)
94+
if len(diags) > 0 {
95+
t.Fatal(diags)
96+
}
97+
files := map[string]*hcl.File{
98+
tc.fileName: f,
99+
}
100+
101+
var fdiags map[string]hcl.Diagnostics
102+
meta, fdiags := LoadSearch(path, files)
103+
104+
if diff := cmp.Diff(tc.expectedError, fdiags, customComparer...); diff != "" {
105+
t.Fatalf("expected errors doesn't match: %s", diff)
106+
}
107+
108+
if diff := cmp.Diff(tc.expectedMeta, meta, customComparer...); diff != "" {
109+
t.Fatalf("search meta doesn't match: %s", diff)
110+
}
111+
})
112+
}
113+
}
114+
115+
func TestLoadSearchDiagnostics(t *testing.T) {
116+
path := t.TempDir()
117+
118+
testCases := []testCase{
119+
{
120+
"invalid variable default value",
121+
`variable "example" {
122+
type = string
123+
default = [1]
124+
}`,
125+
fileName,
126+
&search.Meta{
127+
Path: path,
128+
Filenames: []string{fileName},
129+
Variables: map[string]search.Variable{
130+
"example": {
131+
Type: cty.String,
132+
DefaultValue: cty.DynamicVal,
133+
},
134+
},
135+
Lists: map[string]search.List{},
136+
ProviderReferences: map[search.ProviderRef]tfaddr.Provider{},
137+
},
138+
map[string]hcl.Diagnostics{
139+
fileName: {
140+
{
141+
Severity: hcl.DiagError,
142+
Summary: `Invalid default value for variable`,
143+
Detail: `This default value is not compatible with the variable's type constraint: string required, but have tuple.`,
144+
Subject: &hcl.Range{
145+
Filename: fileName,
146+
Start: hcl.Pos{Line: 3, Column: 13, Byte: 49},
147+
End: hcl.Pos{Line: 3, Column: 16, Byte: 52},
148+
},
149+
},
150+
},
151+
},
152+
},
153+
}
154+
155+
runTestCases(testCases, t, path)
156+
}
157+
158+
func compareVersionConstraint(x, y *version.Constraint) bool {
159+
return x.Equals(y)
160+
}

earlydecoder/search/load_search.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package earlydecoder
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/hashicorp/hcl/v2"
10+
"github.com/hashicorp/hcl/v2/ext/typeexpr"
11+
"github.com/hashicorp/hcl/v2/gohcl"
12+
"github.com/hashicorp/terraform-schema/search"
13+
"github.com/zclconf/go-cty/cty"
14+
"github.com/zclconf/go-cty/cty/convert"
15+
)
16+
17+
type providerConfig struct {
18+
Name string
19+
Alias string
20+
}
21+
22+
type decodedSearch struct {
23+
List map[string]*search.List
24+
Variables map[string]*search.Variable
25+
ProviderConfigs map[string]*providerConfig
26+
}
27+
28+
var providerConfigSchema = &hcl.BodySchema{
29+
Attributes: []hcl.AttributeSchema{
30+
{
31+
Name: "version",
32+
},
33+
{
34+
Name: "alias",
35+
},
36+
},
37+
}
38+
39+
func newDecodedSearch() *decodedSearch {
40+
return &decodedSearch{
41+
List: make(map[string]*search.List),
42+
Variables: make(map[string]*search.Variable),
43+
ProviderConfigs: make(map[string]*providerConfig),
44+
}
45+
}
46+
47+
func loadSearchFromFile(file *hcl.File, ds *decodedSearch) hcl.Diagnostics {
48+
var diags hcl.Diagnostics
49+
50+
content, _, contentDiags := file.Body.PartialContent(rootSchema)
51+
diags = append(diags, contentDiags...)
52+
53+
for _, block := range content.Blocks {
54+
switch block.Type {
55+
case "variable":
56+
content, _, contentDiags := block.Body.PartialContent(variableSchema)
57+
diags = append(diags, contentDiags...)
58+
59+
if len(block.Labels) != 1 || block.Labels[0] == "" {
60+
continue
61+
}
62+
63+
name := block.Labels[0]
64+
description := ""
65+
isSensitive := false
66+
var valDiags hcl.Diagnostics
67+
68+
if attr, defined := content.Attributes["description"]; defined {
69+
valDiags = gohcl.DecodeExpression(attr.Expr, nil, &description)
70+
diags = append(diags, valDiags...)
71+
}
72+
73+
if attr, defined := content.Attributes["sensitive"]; defined {
74+
valDiags = gohcl.DecodeExpression(attr.Expr, nil, &isSensitive)
75+
diags = append(diags, valDiags...)
76+
}
77+
78+
varType := cty.DynamicPseudoType
79+
var defaults *typeexpr.Defaults
80+
if attr, defined := content.Attributes["type"]; defined {
81+
varType, defaults, valDiags = typeexpr.TypeConstraintWithDefaults(attr.Expr)
82+
diags = append(diags, valDiags...)
83+
}
84+
85+
defaultValue := cty.NilVal
86+
if attr, defined := content.Attributes["default"]; defined {
87+
val, vDiags := attr.Expr.Value(nil)
88+
diags = append(diags, vDiags...)
89+
if !vDiags.HasErrors() {
90+
if varType != cty.NilType {
91+
var err error
92+
val, err = convert.Convert(val, varType)
93+
if err != nil {
94+
diags = append(diags, &hcl.Diagnostic{
95+
Severity: hcl.DiagError,
96+
Summary: "Invalid default value for variable",
97+
Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
98+
Subject: attr.Expr.Range().Ptr(),
99+
})
100+
val = cty.DynamicVal
101+
}
102+
}
103+
104+
defaultValue = val
105+
}
106+
}
107+
108+
ds.Variables[name] = &search.Variable{
109+
Type: varType,
110+
Description: description,
111+
DefaultValue: defaultValue,
112+
TypeDefaults: defaults,
113+
IsSensitive: isSensitive,
114+
}
115+
116+
case "provider":
117+
content, _, contentDiags := block.Body.PartialContent(providerConfigSchema)
118+
diags = append(diags, contentDiags...)
119+
name := block.Labels[0]
120+
providerKey := name
121+
var alias string
122+
if attr, defined := content.Attributes["alias"]; defined {
123+
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &alias)
124+
diags = append(diags, valDiags...)
125+
if !valDiags.HasErrors() && alias != "" {
126+
providerKey = fmt.Sprintf("%s.%s", name, alias)
127+
}
128+
}
129+
130+
ds.ProviderConfigs[providerKey] = &providerConfig{
131+
Name: name,
132+
Alias: alias,
133+
}
134+
}
135+
136+
}
137+
138+
return diags
139+
}

0 commit comments

Comments
 (0)