Skip to content

Commit f5a8b98

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

File tree

7 files changed

+136
-78
lines changed

7 files changed

+136
-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/addrs/resourcemode_string.go

+6-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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-69
Original file line numberDiff line numberDiff line change
@@ -18,86 +18,36 @@ import (
1818
type QueryFile struct {
1919
// Providers defines a set of providers that are available to the list blocks
2020
// within this query file.
21-
Providers map[string]*Provider
22-
21+
Providers map[string]*Provider
2322
ProviderConfigs []*Provider
2423

25-
Locals []*Local
26-
24+
Locals []*Local
2725
Variables []*Variable
2826

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

3230
VariablesDeclRange hcl.Range
3331
}
3432

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-
8333
func loadQueryFile(body hcl.Body) (*QueryFile, hcl.Diagnostics) {
8434
var diags hcl.Diagnostics
8535
file := &QueryFile{
8636
Providers: make(map[string]*Provider),
8737
}
8838

89-
content, contentDiags := body.Content(testQueryFileSchema)
39+
content, contentDiags := body.Content(queryFileSchema)
9040
diags = append(diags, contentDiags...)
9141

9242
listBlockNames := make(map[string]hcl.Range)
9343

9444
for _, block := range content.Blocks {
9545
switch block.Type {
9646
case "list":
97-
list, listDiags := decodeQueryListBlock(block, file)
47+
list, listDiags := decodeQueryListBlock(block)
9848
diags = append(diags, listDiags...)
9949
if !listDiags.HasErrors() {
100-
file.Lists = append(file.Lists, list)
50+
file.ListResources = append(file.ListResources, list)
10151
}
10252

10353
if rng, exists := listBlockNames[list.Name]; exists {
@@ -140,21 +90,19 @@ func loadQueryFile(body hcl.Body) (*QueryFile, hcl.Diagnostics) {
14090
return file, diags
14191
}
14292

143-
func decodeQueryListBlock(block *hcl.Block, file *QueryFile) (*List, hcl.Diagnostics) {
93+
func decodeQueryListBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) {
14494
var diags hcl.Diagnostics
14595

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

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,
99+
r := Resource{
100+
Mode: addrs.ListResourceMode,
101+
Type: block.Labels[0],
102+
TypeRange: block.LabelRanges[0],
103+
Name: block.Labels[1],
104+
DeclRange: block.DefRange,
105+
Config: remain,
158106
}
159107

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

186-
var testQueryFileSchema = &hcl.BodySchema{
152+
// QueryListResourceBlockSchema is the schema for a list resource type within
153+
// a terraform query file.
154+
var QueryListResourceBlockSchema = &hcl.BodySchema{
155+
Attributes: commonResourceAttributes,
156+
}
157+
158+
// queryFileSchema is the schema for a terraform query file. It defines the
159+
// expected structure of the file, including the types of supported blocks and their
160+
// attributes.
161+
var queryFileSchema = &hcl.BodySchema{
187162
Blocks: []hcl.BlockHeaderSchema{
188163
{
189164
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)