Tests for query command outputs (#37343)

pull/37368/head
Samsondeen 9 months ago committed by GitHub
parent 92db9b8805
commit 36836fd456
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -41,18 +41,30 @@ func (b *Local) opPlan(
return
}
// Local planning requires a config, unless we're planning to destroy.
if op.PlanMode != plans.DestroyMode && !op.HasConfig() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
"Plan requires configuration to be present. Planning without a configuration would "+
"mark everything for destruction, which is normally not what is desired. If you "+
"would like to destroy everything, run plan with the -destroy option. Otherwise, "+
"create a Terraform configuration file (.tf file) and try again.",
))
op.ReportResult(runningOp, diags)
return
if !op.HasConfig() {
switch {
case op.Query:
// Special diag for terraform query command
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
"Query requires a query configuration to be present. Create a Terraform query configuration file (.tfquery.hcl file) and try again.",
))
op.ReportResult(runningOp, diags)
return
case op.PlanMode != plans.DestroyMode:
// Local planning requires a config, unless we're planning to destroy.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No configuration files",
"Plan requires configuration to be present. Planning without a configuration would "+
"mark everything for destruction, which is normally not what is desired. If you "+
"would like to destroy everything, run plan with the -destroy option. Otherwise, "+
"create a Terraform configuration file (.tf file) and try again.",
))
op.ReportResult(runningOp, diags)
return
}
}
if len(op.GenerateConfigOut) > 0 {

@ -0,0 +1,299 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"path"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
"github.com/zclconf/go-cty/cty"
)
func TestQuery(t *testing.T) {
tests := []struct {
name string
directory string
expectedOut string
expectedErr []string
initCode int
}{
{
name: "basic query",
directory: "basic",
expectedOut: `list.test_instance.example id=test-instance-1 Test Instance 1
list.test_instance.example id=test-instance-2 Test Instance 2
`,
},
{
name: "query referencing local variable",
directory: "with-locals",
expectedOut: `list.test_instance.example id=test-instance-1 Test Instance 1
list.test_instance.example id=test-instance-2 Test Instance 2
`,
},
{
name: "config with no query block",
directory: "no-list-block",
expectedOut: "",
expectedErr: []string{`
Error: No resources to query
The configuration does not contain any resources that can be queried.
`},
},
{
name: "missing query file",
directory: "missing-query-file",
expectedOut: "",
expectedErr: []string{`
Error: No resources to query
The configuration does not contain any resources that can be queried.
`},
},
{
name: "missing configuration",
directory: "missing-configuration",
expectedOut: "",
expectedErr: []string{`
Error: No configuration files
Query requires a query configuration to be present. Create a Terraform query
configuration file (.tfquery.hcl file) and try again.
`},
},
{
name: "invalid query syntax",
directory: "invalid-syntax",
expectedOut: "",
initCode: 1,
expectedErr: []string{`
Error: Unsupported block type
on query.tfquery.hcl line 11:
11: resource "test_instance" "example" {
Blocks of type "resource" are not expected here.
`},
},
}
for _, ts := range tests {
t.Run(ts.name, func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("query", ts.directory)), td)
t.Chdir(td)
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
})
defer close()
p := queryFixtureProvider()
view, done := testView(t)
meta := Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
AllowExperimentalFeatures: true,
ProviderSource: providerSource,
}
init := &InitCommand{Meta: meta}
code := init.Run(nil)
output := done(t)
if code != ts.initCode {
t.Fatalf("expected status code %d but got %d: %s", ts.initCode, code, output.All())
}
view, done = testView(t)
meta.View = view
c := &QueryCommand{Meta: meta}
args := []string{"-no-color"}
code = c.Run(args)
output = done(t)
actual := output.All()
if len(ts.expectedErr) == 0 {
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
// Check that we have query output
if diff := cmp.Diff(ts.expectedOut, actual); diff != "" {
t.Errorf("expected query output to contain %q, \ngot: %q, \ndiff: %s", ts.expectedOut, actual, diff)
}
} else {
for _, expected := range ts.expectedErr {
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("expected error message to contain '%s', \ngot: %s, \ndiff: %s", expected, actual, diff)
}
}
}
})
}
}
func queryFixtureProvider() *testing_provider.MockProvider {
p := testProvider()
instanceListSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"data": {
Type: cty.DynamicPseudoType,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"config": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"ami": {
Type: cty.String,
Required: true,
},
},
},
Nesting: configschema.NestingSingle,
},
},
}
databaseListSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"data": {
Type: cty.DynamicPseudoType,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"config": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"engine": {
Type: cty.String,
Optional: true,
},
},
},
Nesting: configschema.NestingSingle,
},
},
}
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"ami": {
Type: cty.String,
Optional: true,
},
},
},
},
"test_database": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"engine": {
Type: cty.String,
Optional: true,
},
},
},
},
},
ListResourceTypes: map[string]providers.Schema{
"test_instance": {Body: instanceListSchema},
"test_database": {Body: databaseListSchema},
},
}
// Mock the ListResources method for query operations
p.ListResourceFn = func(request providers.ListResourceRequest) providers.ListResourceResponse {
// Check the config to determine what kind of response to return
wholeConfigMap := request.Config.AsValueMap()
configMap := wholeConfigMap["config"]
// For empty results test case //TODO: Remove?
if ami, ok := wholeConfigMap["ami"]; ok && ami.AsString() == "ami-nonexistent" {
return providers.ListResourceResponse{
Result: cty.ObjectVal(map[string]cty.Value{
"data": cty.ListVal([]cty.Value{}),
"config": configMap,
}),
}
}
switch request.TypeName {
case "test_instance":
return providers.ListResourceResponse{
Result: cty.ObjectVal(map[string]cty.Value{
"data": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"identity": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test-instance-1"),
}),
"state": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test-instance-1"),
"ami": cty.StringVal("ami-12345"),
}),
"display_name": cty.StringVal("Test Instance 1"),
}),
cty.ObjectVal(map[string]cty.Value{
"identity": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test-instance-2"),
}),
"state": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test-instance-2"),
"ami": cty.StringVal("ami-67890"),
}),
"display_name": cty.StringVal("Test Instance 2"),
}),
}),
"config": configMap,
}),
}
case "test_database":
return providers.ListResourceResponse{
Result: cty.ObjectVal(map[string]cty.Value{
"data": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"identity": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test-db-1"),
}),
"state": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test-db-1"),
"engine": cty.StringVal("mysql"),
}),
"display_name": cty.StringVal("Test Database 1"),
}),
}),
"config": configMap,
}),
}
default:
return providers.ListResourceResponse{
Result: cty.ObjectVal(map[string]cty.Value{
"data": cty.ListVal([]cty.Value{}),
"config": configMap,
}),
}
}
}
return p
}

@ -0,0 +1,13 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
provider "test" {}
resource "test_instance" "example" {
ami = "ami-12345"
}

@ -0,0 +1,7 @@
list "test_instance" "example" {
provider = test
config {
ami = "ami-12345"
}
}

@ -0,0 +1,13 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
provider "test" {}
resource "test_instance" "example" {
ami = "ami-12345"
}

@ -0,0 +1,13 @@
list "test_instance" "example" {
provider = test
config {
ami = "ami-12345"
}
}
// resource type not supported in query files
resource "test_instance" "example" {
provider = test
}

@ -0,0 +1,13 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
provider "test" {}
resource "test_instance" "example" {
ami = "ami-12345"
}

@ -0,0 +1,13 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
provider "test" {}
resource "test_instance" "example" {
ami = "ami-12345"
}

@ -0,0 +1,13 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
provider "test" {}
resource "test_instance" "example" {
ami = "ami-12345"
}

@ -0,0 +1,11 @@
locals {
ami = "ami-12345"
}
list "test_instance" "example" {
provider = test
config {
ami = local.ami
}
}

@ -5,6 +5,7 @@ package configs
import (
"fmt"
"iter"
"log"
"maps"
"slices"
@ -143,15 +144,19 @@ func (c *Config) DeepEach(cb func(c *Config)) {
}
}
// AllModules returns a slice of all the receiver and all of its descendant
// nodes in the module tree, in the same order they would be visited by
// DeepEach.
func (c *Config) AllModules() []*Config {
var ret []*Config
c.DeepEach(func(c *Config) {
ret = append(ret, c)
})
return ret
// AllModules returns an iterator of all the receiver and all of its descendant
// nodes in the module tree until the iterator is exhausted or terminated.
func (c *Config) AllModules() iter.Seq[*Config] {
return func(yield func(*Config) bool) {
if !yield(c) {
return
}
for _, ch := range c.Children {
if !yield(ch) {
return
}
}
}
}
// Descendant returns the descendant config that has the given path beneath

@ -270,6 +270,25 @@ The -target option is not for routine use, and is provided only for exceptional
))
}
if opts.Query {
var hasQuery bool
for c := range config.AllModules() {
if len(c.Module.ListResources) > 0 {
hasQuery = true
break
}
}
if !hasQuery {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No resources to query",
`The configuration does not contain any resources that can be queried.`,
))
return nil, nil, diags
}
}
var plan *plans.Plan
var planDiags tfdiags.Diagnostics
var evalScope *lang.Scope

Loading…
Cancel
Save