Skip to content

Commit 268a45c

Browse files
committed
represent list as a resource
1 parent 4703686 commit 268a45c

File tree

6 files changed

+130
-78
lines changed

6 files changed

+130
-78
lines changed

internal/addrs/resource.go

+4
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,10 @@ const (
511511
// EphemeralResourceMode indicates an ephemeral resource, as defined by
512512
// "ephemeral" blocks in configuration.
513513
EphemeralResourceMode ResourceMode = 'E'
514+
515+
// ListResourceMode indicates a list resource, as defined by
516+
// "list" blocks in tfquery configuration.
517+
ListResourceMode ResourceMode = 'L'
514518
)
515519

516520
// AbsResourceInstanceObject represents one of the specific remote objects

internal/configs/module.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type Module struct {
4949
ManagedResources map[string]*Resource
5050
DataResources map[string]*Resource
5151
EphemeralResources map[string]*Resource
52+
ListResources map[string]*Resource
5253

5354
Moved []*Moved
5455
Removed []*Removed
@@ -57,8 +58,6 @@ type Module struct {
5758
Checks map[string]*Check
5859

5960
Tests map[string]*TestFile
60-
61-
Lists map[string]*List
6261
}
6362

6463
// File describes the contents of a single configuration file.
@@ -130,10 +129,10 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
130129
ManagedResources: map[string]*Resource{},
131130
EphemeralResources: map[string]*Resource{},
132131
DataResources: map[string]*Resource{},
132+
ListResources: map[string]*Resource{},
133133
Checks: map[string]*Check{},
134134
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
135135
Tests: map[string]*TestFile{},
136-
Lists: map[string]*List{},
137136
}
138137

139138
// Process the required_providers blocks first, to ensure that all
@@ -200,6 +199,8 @@ func (m *Module) ResourceByAddr(addr addrs.Resource) *Resource {
200199
return m.DataResources[key]
201200
case addrs.EphemeralResourceMode:
202201
return m.EphemeralResources[key]
202+
case addrs.ListResourceMode:
203+
return m.ListResources[key]
203204
default:
204205
return nil
205206
}
@@ -547,9 +548,9 @@ func (m *Module) appendQueryFile(file *QueryFile) hcl.Diagnostics {
547548
m.Locals[l.Name] = l
548549
}
549550

550-
for _, ql := range file.Lists {
551+
for _, ql := range file.ListResources {
551552
key := ql.moduleUniqueKey()
552-
if existing, exists := m.Lists[key]; exists {
553+
if existing, exists := m.ListResources[key]; exists {
553554
diags = append(diags, &hcl.Diagnostic{
554555
Severity: hcl.DiagError,
555556
Summary: fmt.Sprintf("Duplicate list %q configuration", existing.Type),
@@ -559,7 +560,7 @@ func (m *Module) appendQueryFile(file *QueryFile) hcl.Diagnostics {
559560
continue
560561
}
561562
// set the provider FQN for the resource
562-
m.Lists[key] = ql
563+
m.ListResources[key] = ql
563564
ql.Provider = m.ProviderForLocalConfig(ql.ProviderConfigAddr())
564565
}
565566

internal/configs/parser_config_dir_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,61 @@ func TestParserLoadConfigDirWithTests(t *testing.T) {
149149
}
150150
}
151151

152+
func TestParserLoadConfigDirWithQueries(t *testing.T) {
153+
tests := []struct {
154+
name string
155+
directory string
156+
shouldFail bool
157+
diagnostics []string
158+
resources int
159+
}{
160+
{
161+
name: "simple",
162+
directory: "testdata/query-files/valid/simple",
163+
resources: 2,
164+
},
165+
{
166+
name: "no-provider",
167+
directory: "testdata/query-files/invalid/no-provider",
168+
shouldFail: true,
169+
diagnostics: []string{
170+
"testdata/query-files/invalid/no-provider/main.tfquery.hcl:1,1-27: Missing \"provider\" attribute; You must specify a provider attribute when defining a list block.",
171+
},
172+
},
173+
}
174+
175+
for _, test := range tests {
176+
t.Run(test.name, func(t *testing.T) {
177+
parser := NewParser(nil)
178+
mod, diags := parser.LoadConfigDir(test.directory, MatchQueryFiles())
179+
if test.shouldFail {
180+
if !diags.HasErrors() {
181+
t.Errorf("expected errors, but found none")
182+
}
183+
if len(diags) != len(test.diagnostics) {
184+
t.Fatalf("expected %d errors, but found %d", len(test.diagnostics), len(diags))
185+
}
186+
for i, diag := range diags {
187+
if diag.Error() != test.diagnostics[i] {
188+
t.Errorf("expected error to be %q, but found %q", test.diagnostics[i], diag.Error())
189+
}
190+
}
191+
} else {
192+
if len(diags) > 0 { // We don't want any warnings or errors.
193+
t.Errorf("unexpected diagnostics")
194+
for _, diag := range diags {
195+
t.Logf("- %s", diag)
196+
}
197+
}
198+
}
199+
200+
if len(mod.ListResources) != test.resources {
201+
t.Errorf("incorrect number of list blocks found: %d", len(mod.ListResources))
202+
}
203+
})
204+
}
205+
}
206+
152207
func TestParserLoadTestFiles_Invalid(t *testing.T) {
153208

154209
tcs := map[string][]string{

internal/configs/query_file.go

+44-72
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// Copyright (c) HashiCorp, Inc.
2-
// SPDX-License-Identifier: BUSL-1.1
3-
41
package configs
52

63
import (
@@ -18,86 +15,36 @@ import (
1815
type QueryFile struct {
1916
// Providers defines a set of providers that are available to the list blocks
2017
// within this query file.
21-
Providers map[string]*Provider
22-
18+
Providers map[string]*Provider
2319
ProviderConfigs []*Provider
2420

25-
Locals []*Local
26-
21+
Locals []*Local
2722
Variables []*Variable
2823

29-
// Lists defines the list of List blocks within the query file.
30-
Lists []*List
24+
// ListResources is a slice of List blocks within the query file.
25+
ListResources []*Resource
3126

3227
VariablesDeclRange hcl.Range
3328
}
3429

35-
// List represents a single list block within a query file.
36-
//
37-
// Each list block represents a single Terraform command to be executed and a set
38-
// of validations to list after the command.
39-
type List struct {
40-
Type string
41-
Name string
42-
43-
ProviderConfigRef *ProviderConfigRef
44-
Provider addrs.Provider
45-
46-
// File is a reference to the parent QueryFile that contains this block.
47-
File *QueryFile
48-
49-
// Config is the main configuration body for the list block.
50-
Config hcl.Body
51-
52-
TypeDeclRange hcl.Range
53-
ConfigDeclRange hcl.Range
54-
DeclRange hcl.Range
55-
}
56-
57-
func (list *List) moduleUniqueKey() string {
58-
return list.Name
59-
}
60-
61-
func (list *List) Addr() addrs.List {
62-
return addrs.List{
63-
Type: list.Type,
64-
Name: list.Name,
65-
}
66-
}
67-
68-
// ProviderConfigAddr returns the address for the provider configuration that
69-
// should be used for this list. This function returns a default provider
70-
// config addr if an explicit "provider" argument was not provided.
71-
func (list *List) ProviderConfigAddr() addrs.LocalProviderConfig {
72-
if list.ProviderConfigRef == nil {
73-
// all lists must have a provider config ref
74-
panic("ProviderConfigRef is nil")
75-
}
76-
77-
return addrs.LocalProviderConfig{
78-
LocalName: list.ProviderConfigRef.Name,
79-
Alias: list.ProviderConfigRef.Alias,
80-
}
81-
}
82-
8330
func loadQueryFile(body hcl.Body) (*QueryFile, hcl.Diagnostics) {
8431
var diags hcl.Diagnostics
8532
file := &QueryFile{
8633
Providers: make(map[string]*Provider),
8734
}
8835

89-
content, contentDiags := body.Content(testQueryFileSchema)
36+
content, contentDiags := body.Content(queryFileSchema)
9037
diags = append(diags, contentDiags...)
9138

9239
listBlockNames := make(map[string]hcl.Range)
9340

9441
for _, block := range content.Blocks {
9542
switch block.Type {
9643
case "list":
97-
list, listDiags := decodeQueryListBlock(block, file)
44+
list, listDiags := decodeQueryListBlock(block)
9845
diags = append(diags, listDiags...)
9946
if !listDiags.HasErrors() {
100-
file.Lists = append(file.Lists, list)
47+
file.ListResources = append(file.ListResources, list)
10148
}
10249

10350
if rng, exists := listBlockNames[list.Name]; exists {
@@ -140,21 +87,19 @@ func loadQueryFile(body hcl.Body) (*QueryFile, hcl.Diagnostics) {
14087
return file, diags
14188
}
14289

143-
func decodeQueryListBlock(block *hcl.Block, file *QueryFile) (*List, hcl.Diagnostics) {
90+
func decodeQueryListBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
14491
var diags hcl.Diagnostics
14592

146-
content, remain, contentDiags := block.Body.PartialContent(&hcl.BodySchema{
147-
Attributes: []hcl.AttributeSchema{{Name: "provider"}},
148-
})
93+
content, remain, contentDiags := block.Body.PartialContent(QueryListResourceBlockSchema)
14994
diags = append(diags, contentDiags...)
15095

151-
r := List{
152-
File: file,
153-
Type: block.Labels[0],
154-
TypeDeclRange: block.LabelRanges[0],
155-
Name: block.Labels[1],
156-
DeclRange: block.DefRange,
157-
Config: remain,
96+
r := Resource{
97+
Mode: addrs.ListResourceMode,
98+
Type: block.Labels[0],
99+
TypeRange: block.LabelRanges[0],
100+
Name: block.Labels[1],
101+
DeclRange: block.DefRange,
102+
Config: remain,
158103
}
159104

160105
if attr, exists := content.Attributes["provider"]; exists {
@@ -180,10 +125,37 @@ func decodeQueryListBlock(block *hcl.Block, file *QueryFile) (*List, hcl.Diagnos
180125
Subject: r.DeclRange.Ptr(),
181126
})
182127
}
128+
129+
if attr, exists := content.Attributes["count"]; exists {
130+
r.Count = attr.Expr
131+
}
132+
133+
if attr, exists := content.Attributes["for_each"]; exists {
134+
r.ForEach = attr.Expr
135+
// Cannot have count and for_each on the same resource block
136+
if r.Count != nil {
137+
diags = append(diags, &hcl.Diagnostic{
138+
Severity: hcl.DiagError,
139+
Summary: `Invalid combination of "count" and "for_each"`,
140+
Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive.`,
141+
Subject: &attr.NameRange,
142+
})
143+
}
144+
}
145+
183146
return &r, diags
184147
}
185148

186-
var testQueryFileSchema = &hcl.BodySchema{
149+
// QueryListResourceBlockSchema is the schema for a list resource type within
150+
// a terraform query file.
151+
var QueryListResourceBlockSchema = &hcl.BodySchema{
152+
Attributes: commonResourceAttributes,
153+
}
154+
155+
// queryFileSchema is the schema for a terraform query file. It defines the
156+
// expected structure of the file, including the types of supported blocks and their
157+
// attributes.
158+
var queryFileSchema = &hcl.BodySchema{
187159
Blocks: []hcl.BlockHeaderSchema{
188160
{
189161
Type: "list",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
list "aws_instance" "test" {
2+
count = 1
3+
tags = {
4+
Name = "test"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
list "aws_instance" "test" {
2+
provider = aws
3+
count = 1
4+
tags = {
5+
Name = "test"
6+
}
7+
}
8+
list "aws_instance" "test2" {
9+
provider = aws
10+
count = 1
11+
tags = {
12+
Name = "test2"
13+
}
14+
}

0 commit comments

Comments
 (0)