Skip to content

Commit 36836fd

Browse files
authored
Tests for query command outputs (#37343)
1 parent 92db9b8 commit 36836fd

File tree

14 files changed

+452
-21
lines changed

14 files changed

+452
-21
lines changed

internal/backend/local/backend_plan.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,30 @@ func (b *Local) opPlan(
4141
return
4242
}
4343

44-
// Local planning requires a config, unless we're planning to destroy.
45-
if op.PlanMode != plans.DestroyMode && !op.HasConfig() {
46-
diags = diags.Append(tfdiags.Sourceless(
47-
tfdiags.Error,
48-
"No configuration files",
49-
"Plan requires configuration to be present. Planning without a configuration would "+
50-
"mark everything for destruction, which is normally not what is desired. If you "+
51-
"would like to destroy everything, run plan with the -destroy option. Otherwise, "+
52-
"create a Terraform configuration file (.tf file) and try again.",
53-
))
54-
op.ReportResult(runningOp, diags)
55-
return
44+
if !op.HasConfig() {
45+
switch {
46+
case op.Query:
47+
// Special diag for terraform query command
48+
diags = diags.Append(tfdiags.Sourceless(
49+
tfdiags.Error,
50+
"No configuration files",
51+
"Query requires a query configuration to be present. Create a Terraform query configuration file (.tfquery.hcl file) and try again.",
52+
))
53+
op.ReportResult(runningOp, diags)
54+
return
55+
case op.PlanMode != plans.DestroyMode:
56+
// Local planning requires a config, unless we're planning to destroy.
57+
diags = diags.Append(tfdiags.Sourceless(
58+
tfdiags.Error,
59+
"No configuration files",
60+
"Plan requires configuration to be present. Planning without a configuration would "+
61+
"mark everything for destruction, which is normally not what is desired. If you "+
62+
"would like to destroy everything, run plan with the -destroy option. Otherwise, "+
63+
"create a Terraform configuration file (.tf file) and try again.",
64+
))
65+
op.ReportResult(runningOp, diags)
66+
return
67+
}
5668
}
5769

5870
if len(op.GenerateConfigOut) > 0 {

internal/command/query_test.go

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package command
5+
6+
import (
7+
"path"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform/internal/configs/configschema"
12+
"github.com/hashicorp/terraform/internal/providers"
13+
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
14+
"github.com/zclconf/go-cty/cty"
15+
)
16+
17+
func TestQuery(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
directory string
21+
expectedOut string
22+
expectedErr []string
23+
initCode int
24+
}{
25+
{
26+
name: "basic query",
27+
directory: "basic",
28+
expectedOut: `list.test_instance.example id=test-instance-1 Test Instance 1
29+
list.test_instance.example id=test-instance-2 Test Instance 2
30+
31+
`,
32+
},
33+
{
34+
name: "query referencing local variable",
35+
directory: "with-locals",
36+
expectedOut: `list.test_instance.example id=test-instance-1 Test Instance 1
37+
list.test_instance.example id=test-instance-2 Test Instance 2
38+
39+
`,
40+
},
41+
{
42+
name: "config with no query block",
43+
directory: "no-list-block",
44+
expectedOut: "",
45+
expectedErr: []string{`
46+
Error: No resources to query
47+
48+
The configuration does not contain any resources that can be queried.
49+
`},
50+
},
51+
{
52+
name: "missing query file",
53+
directory: "missing-query-file",
54+
expectedOut: "",
55+
expectedErr: []string{`
56+
Error: No resources to query
57+
58+
The configuration does not contain any resources that can be queried.
59+
`},
60+
},
61+
{
62+
name: "missing configuration",
63+
directory: "missing-configuration",
64+
expectedOut: "",
65+
expectedErr: []string{`
66+
Error: No configuration files
67+
68+
Query requires a query configuration to be present. Create a Terraform query
69+
configuration file (.tfquery.hcl file) and try again.
70+
`},
71+
},
72+
{
73+
name: "invalid query syntax",
74+
directory: "invalid-syntax",
75+
expectedOut: "",
76+
initCode: 1,
77+
expectedErr: []string{`
78+
Error: Unsupported block type
79+
80+
on query.tfquery.hcl line 11:
81+
11: resource "test_instance" "example" {
82+
83+
Blocks of type "resource" are not expected here.
84+
`},
85+
},
86+
}
87+
88+
for _, ts := range tests {
89+
t.Run(ts.name, func(t *testing.T) {
90+
td := t.TempDir()
91+
testCopyDir(t, testFixturePath(path.Join("query", ts.directory)), td)
92+
t.Chdir(td)
93+
providerSource, close := newMockProviderSource(t, map[string][]string{
94+
"hashicorp/test": {"1.0.0"},
95+
})
96+
defer close()
97+
98+
p := queryFixtureProvider()
99+
view, done := testView(t)
100+
meta := Meta{
101+
testingOverrides: metaOverridesForProvider(p),
102+
View: view,
103+
AllowExperimentalFeatures: true,
104+
ProviderSource: providerSource,
105+
}
106+
107+
init := &InitCommand{Meta: meta}
108+
code := init.Run(nil)
109+
output := done(t)
110+
if code != ts.initCode {
111+
t.Fatalf("expected status code %d but got %d: %s", ts.initCode, code, output.All())
112+
}
113+
114+
view, done = testView(t)
115+
meta.View = view
116+
117+
c := &QueryCommand{Meta: meta}
118+
args := []string{"-no-color"}
119+
code = c.Run(args)
120+
output = done(t)
121+
actual := output.All()
122+
if len(ts.expectedErr) == 0 {
123+
if code != 0 {
124+
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
125+
}
126+
127+
// Check that we have query output
128+
if diff := cmp.Diff(ts.expectedOut, actual); diff != "" {
129+
t.Errorf("expected query output to contain %q, \ngot: %q, \ndiff: %s", ts.expectedOut, actual, diff)
130+
}
131+
132+
} else {
133+
for _, expected := range ts.expectedErr {
134+
if diff := cmp.Diff(expected, actual); diff != "" {
135+
t.Errorf("expected error message to contain '%s', \ngot: %s, \ndiff: %s", expected, actual, diff)
136+
}
137+
}
138+
}
139+
})
140+
}
141+
}
142+
143+
func queryFixtureProvider() *testing_provider.MockProvider {
144+
p := testProvider()
145+
instanceListSchema := &configschema.Block{
146+
Attributes: map[string]*configschema.Attribute{
147+
"data": {
148+
Type: cty.DynamicPseudoType,
149+
Computed: true,
150+
},
151+
},
152+
BlockTypes: map[string]*configschema.NestedBlock{
153+
"config": {
154+
Block: configschema.Block{
155+
Attributes: map[string]*configschema.Attribute{
156+
"ami": {
157+
Type: cty.String,
158+
Required: true,
159+
},
160+
},
161+
},
162+
Nesting: configschema.NestingSingle,
163+
},
164+
},
165+
}
166+
databaseListSchema := &configschema.Block{
167+
Attributes: map[string]*configschema.Attribute{
168+
"data": {
169+
Type: cty.DynamicPseudoType,
170+
Computed: true,
171+
},
172+
},
173+
BlockTypes: map[string]*configschema.NestedBlock{
174+
"config": {
175+
Block: configschema.Block{
176+
Attributes: map[string]*configschema.Attribute{
177+
"engine": {
178+
Type: cty.String,
179+
Optional: true,
180+
},
181+
},
182+
},
183+
Nesting: configschema.NestingSingle,
184+
},
185+
},
186+
}
187+
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
188+
ResourceTypes: map[string]providers.Schema{
189+
"test_instance": {
190+
Body: &configschema.Block{
191+
Attributes: map[string]*configschema.Attribute{
192+
"id": {
193+
Type: cty.String,
194+
Computed: true,
195+
},
196+
"ami": {
197+
Type: cty.String,
198+
Optional: true,
199+
},
200+
},
201+
},
202+
},
203+
"test_database": {
204+
Body: &configschema.Block{
205+
Attributes: map[string]*configschema.Attribute{
206+
"id": {
207+
Type: cty.String,
208+
Computed: true,
209+
},
210+
"engine": {
211+
Type: cty.String,
212+
Optional: true,
213+
},
214+
},
215+
},
216+
},
217+
},
218+
ListResourceTypes: map[string]providers.Schema{
219+
"test_instance": {Body: instanceListSchema},
220+
"test_database": {Body: databaseListSchema},
221+
},
222+
}
223+
224+
// Mock the ListResources method for query operations
225+
p.ListResourceFn = func(request providers.ListResourceRequest) providers.ListResourceResponse {
226+
// Check the config to determine what kind of response to return
227+
wholeConfigMap := request.Config.AsValueMap()
228+
229+
configMap := wholeConfigMap["config"]
230+
231+
// For empty results test case //TODO: Remove?
232+
if ami, ok := wholeConfigMap["ami"]; ok && ami.AsString() == "ami-nonexistent" {
233+
return providers.ListResourceResponse{
234+
Result: cty.ObjectVal(map[string]cty.Value{
235+
"data": cty.ListVal([]cty.Value{}),
236+
"config": configMap,
237+
}),
238+
}
239+
}
240+
241+
switch request.TypeName {
242+
case "test_instance":
243+
return providers.ListResourceResponse{
244+
Result: cty.ObjectVal(map[string]cty.Value{
245+
"data": cty.ListVal([]cty.Value{
246+
cty.ObjectVal(map[string]cty.Value{
247+
"identity": cty.ObjectVal(map[string]cty.Value{
248+
"id": cty.StringVal("test-instance-1"),
249+
}),
250+
"state": cty.ObjectVal(map[string]cty.Value{
251+
"id": cty.StringVal("test-instance-1"),
252+
"ami": cty.StringVal("ami-12345"),
253+
}),
254+
"display_name": cty.StringVal("Test Instance 1"),
255+
}),
256+
cty.ObjectVal(map[string]cty.Value{
257+
"identity": cty.ObjectVal(map[string]cty.Value{
258+
"id": cty.StringVal("test-instance-2"),
259+
}),
260+
"state": cty.ObjectVal(map[string]cty.Value{
261+
"id": cty.StringVal("test-instance-2"),
262+
"ami": cty.StringVal("ami-67890"),
263+
}),
264+
"display_name": cty.StringVal("Test Instance 2"),
265+
}),
266+
}),
267+
"config": configMap,
268+
}),
269+
}
270+
case "test_database":
271+
return providers.ListResourceResponse{
272+
Result: cty.ObjectVal(map[string]cty.Value{
273+
"data": cty.ListVal([]cty.Value{
274+
cty.ObjectVal(map[string]cty.Value{
275+
"identity": cty.ObjectVal(map[string]cty.Value{
276+
"id": cty.StringVal("test-db-1"),
277+
}),
278+
"state": cty.ObjectVal(map[string]cty.Value{
279+
"id": cty.StringVal("test-db-1"),
280+
"engine": cty.StringVal("mysql"),
281+
}),
282+
"display_name": cty.StringVal("Test Database 1"),
283+
}),
284+
}),
285+
"config": configMap,
286+
}),
287+
}
288+
default:
289+
return providers.ListResourceResponse{
290+
Result: cty.ObjectVal(map[string]cty.Value{
291+
"data": cty.ListVal([]cty.Value{}),
292+
"config": configMap,
293+
}),
294+
}
295+
}
296+
}
297+
298+
return p
299+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
}
8+
9+
provider "test" {}
10+
11+
resource "test_instance" "example" {
12+
ami = "ami-12345"
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
list "test_instance" "example" {
2+
provider = test
3+
4+
config {
5+
ami = "ami-12345"
6+
}
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
terraform {
2+
required_providers {
3+
test = {
4+
source = "hashicorp/test"
5+
}
6+
}
7+
}
8+
9+
provider "test" {}
10+
11+
resource "test_instance" "example" {
12+
ami = "ami-12345"
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
list "test_instance" "example" {
2+
provider = test
3+
4+
config {
5+
ami = "ami-12345"
6+
}
7+
}
8+
9+
10+
// resource type not supported in query files
11+
resource "test_instance" "example" {
12+
provider = test
13+
}

internal/command/testdata/query/missing-configuration/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)