mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
turns out the invalid id attributes were captured in the command test, so removing them caused that to fail too.
863 lines
28 KiB
Go
863 lines
28 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"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"
|
|
tfversion "github.com/hashicorp/terraform/version"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestQuery(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
directory string
|
|
expectedOut string
|
|
expectedErr []string
|
|
initCode int
|
|
args []string
|
|
}{
|
|
{
|
|
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: 0,
|
|
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.
|
|
`},
|
|
},
|
|
{
|
|
name: "empty result",
|
|
directory: "empty-result",
|
|
expectedOut: `list.test_instance.example id=test-instance-1 Test Instance 1
|
|
list.test_instance.example id=test-instance-2 Test Instance 2
|
|
|
|
Warning: list block(s) [list.test_instance.example2] returned 0 results.`,
|
|
},
|
|
{
|
|
name: "error - extra variables",
|
|
directory: "basic",
|
|
args: []string{"-var", "instance_name=test-instance"},
|
|
expectedErr: []string{`
|
|
Error: Value for undeclared variable
|
|
|
|
A variable named "instance_name" was assigned on the command line, but the
|
|
root module does not declare a variable of that name. To use this value, add
|
|
a "variable" block to the configuration.
|
|
`},
|
|
},
|
|
{
|
|
name: "query with variables",
|
|
directory: "with-variables",
|
|
args: []string{"-var", "instance_name=test-instance"},
|
|
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 with variables defined in tf file",
|
|
directory: "with-variables-in-tf",
|
|
args: []string{"-var", "instance_name=test-instance"},
|
|
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 with variable files",
|
|
directory: "with-variables-file",
|
|
args: []string{"-var-file=custom.tfvars"},
|
|
expectedOut: `list.test_instance.example id=test-instance-1 Test Instance 1
|
|
list.test_instance.example id=test-instance-2 Test Instance 2
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "error - query with invalid variable value",
|
|
directory: "with-invalid-variables",
|
|
args: []string{"-var", "target_ami=ami-123"},
|
|
expectedErr: []string{`AMI ID must be longer than 10 characters.`},
|
|
},
|
|
{
|
|
name: "error - query with missing required variable",
|
|
directory: "with-variables",
|
|
expectedOut: "",
|
|
expectedErr: []string{`
|
|
Error: No value for required variable
|
|
|
|
on query.tfquery.hcl line 7:
|
|
7: variable "instance_name" {
|
|
|
|
The root module input variable "instance_name" is not set, and has no default
|
|
value. Use a -var or -var-file command line argument to provide a value for
|
|
this variable.
|
|
`},
|
|
},
|
|
{
|
|
name: "error - query with missing required variable in tf file",
|
|
directory: "with-variables-in-tf",
|
|
expectedOut: "",
|
|
expectedErr: []string{`
|
|
Error: No value for required variable
|
|
|
|
on main.tf line 15:
|
|
15: variable "instance_name" {
|
|
|
|
The root module input variable "instance_name" is not set, and has no default
|
|
value. Use a -var or -var-file command line argument to provide a value for
|
|
this variable.
|
|
`},
|
|
},
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
// helper for asserting against the expected error(s)
|
|
assertErr := func(actual string) (errored bool) {
|
|
for _, expected := range ts.expectedErr {
|
|
expected := strings.TrimSpace(expected)
|
|
if !strings.Contains(actual, expected) {
|
|
errored = true
|
|
t.Errorf("expected error message to contain '%s', \ngot: %s: diff: %s", expected, actual, cmp.Diff(expected, actual))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
// If we expect an init error, perhaps we want to assert the error message
|
|
if ts.initCode != 0 {
|
|
actual := output.All()
|
|
if errored := assertErr(actual); errored {
|
|
t.FailNow()
|
|
return
|
|
}
|
|
}
|
|
|
|
view, done = testView(t)
|
|
meta.View = view
|
|
|
|
c := &QueryCommand{Meta: meta}
|
|
code = c.Run(append([]string{"-no-color"}, ts.args...))
|
|
output = done(t)
|
|
if len(ts.expectedErr) == 0 {
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
actual := strings.TrimSpace(output.Stdout())
|
|
|
|
// Check that we have query output
|
|
expected := strings.TrimSpace(ts.expectedOut)
|
|
if diff := cmp.Diff(expected, actual); diff != "" {
|
|
t.Errorf("expected query output to contain \n%q, \ngot: \n%q, \ndiff: %s", expected, actual, diff)
|
|
}
|
|
|
|
} else {
|
|
actual := strings.TrimSpace(output.Stderr())
|
|
assertErr(actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
"foo": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Nesting: configschema.NestingSingle,
|
|
MinItems: 1,
|
|
MaxItems: 1,
|
|
},
|
|
},
|
|
}
|
|
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,
|
|
MinItems: 1,
|
|
MaxItems: 1,
|
|
},
|
|
},
|
|
}
|
|
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,
|
|
},
|
|
},
|
|
},
|
|
Identity: &configschema.Object{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {
|
|
Type: cty.String,
|
|
Computed: true,
|
|
},
|
|
},
|
|
Nesting: configschema.NestingSingle,
|
|
},
|
|
IdentityVersion: 1,
|
|
},
|
|
"test_database": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {
|
|
Type: cty.String,
|
|
Computed: true,
|
|
},
|
|
"engine": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Identity: &configschema.Object{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {
|
|
Type: cty.String,
|
|
Computed: true,
|
|
},
|
|
},
|
|
Nesting: configschema.NestingSingle,
|
|
},
|
|
},
|
|
},
|
|
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
|
|
ami, ok := configMap.AsValueMap()["ami"]
|
|
if ok && ami.AsString() == "ami-nonexistent" {
|
|
return providers.ListResourceResponse{
|
|
Result: cty.ObjectVal(map[string]cty.Value{
|
|
"data": cty.ListValEmpty(cty.DynamicPseudoType),
|
|
"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
|
|
}
|
|
|
|
func TestQuery_JSON(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
tests := []struct {
|
|
name string
|
|
directory string
|
|
expectedRes []map[string]any
|
|
initCode int
|
|
opts []string
|
|
commandErrMsg string // non-empty when the query command fails after init
|
|
selectResource []string // only these resources will be selected in the result if given
|
|
}{
|
|
{
|
|
name: "basic query",
|
|
directory: "basic",
|
|
expectedRes: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Starting query...",
|
|
"list_start": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"resource_type": "test_instance",
|
|
"input_config": map[string]any{"ami": "ami-12345", "foo": nil},
|
|
},
|
|
"type": "list_start",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Result found",
|
|
"list_resource_found": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"display_name": "Test Instance 1",
|
|
"identity": map[string]any{
|
|
"id": "test-instance-1",
|
|
},
|
|
"identity_version": float64(1),
|
|
"resource_type": "test_instance",
|
|
"resource_object": map[string]any{
|
|
"ami": "ami-12345",
|
|
"id": "test-instance-1",
|
|
},
|
|
},
|
|
"type": "list_resource_found",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Result found",
|
|
"list_resource_found": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"display_name": "Test Instance 2",
|
|
"identity": map[string]any{
|
|
"id": "test-instance-2",
|
|
},
|
|
"identity_version": float64(1),
|
|
"resource_type": "test_instance",
|
|
"resource_object": map[string]any{
|
|
"ami": "ami-67890",
|
|
"id": "test-instance-2",
|
|
},
|
|
},
|
|
"type": "list_resource_found",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: List complete",
|
|
"list_complete": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"resource_type": "test_instance",
|
|
"total": float64(2),
|
|
},
|
|
"type": "list_complete",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "basic query - generate config",
|
|
directory: "basic",
|
|
opts: []string{fmt.Sprintf("-generate-config-out=%s/new.tf", tmp)},
|
|
expectedRes: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Starting query...",
|
|
"list_start": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"resource_type": "test_instance",
|
|
"input_config": map[string]any{"ami": "ami-12345", "foo": nil},
|
|
},
|
|
"type": "list_start",
|
|
},
|
|
{"@level": "info",
|
|
"@message": "list.test_instance.example: Result found",
|
|
"list_resource_found": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"display_name": "Test Instance 1",
|
|
"identity": map[string]any{
|
|
"id": "test-instance-1",
|
|
},
|
|
"identity_version": float64(1),
|
|
"resource_type": "test_instance",
|
|
"resource_object": map[string]any{
|
|
"ami": "ami-12345",
|
|
"id": "test-instance-1",
|
|
},
|
|
"config": "resource \"test_instance\" \"example_0\" {\n provider = test\n ami = \"ami-12345\"\n}",
|
|
"import_config": "import {\n to = test_instance.example_0\n provider = test\n identity = {\n id = \"test-instance-1\"\n }\n}",
|
|
},
|
|
"type": "list_resource_found",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Result found",
|
|
"list_resource_found": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"display_name": "Test Instance 2",
|
|
"identity": map[string]any{
|
|
"id": "test-instance-2",
|
|
},
|
|
"identity_version": float64(1),
|
|
"resource_type": "test_instance",
|
|
"resource_object": map[string]any{
|
|
"ami": "ami-67890",
|
|
"id": "test-instance-2",
|
|
},
|
|
"config": "resource \"test_instance\" \"example_1\" {\n provider = test\n ami = \"ami-67890\"\n}",
|
|
"import_config": "import {\n to = test_instance.example_1\n provider = test\n identity = {\n id = \"test-instance-2\"\n }\n}",
|
|
},
|
|
"type": "list_resource_found",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: List complete",
|
|
"list_complete": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"resource_type": "test_instance",
|
|
"total": float64(2),
|
|
},
|
|
"type": "list_complete",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "list resource with an empty result",
|
|
directory: "empty-result",
|
|
expectedRes: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example2: List complete",
|
|
"list_complete": map[string]any{
|
|
"address": "list.test_instance.example2",
|
|
"resource_type": "test_instance",
|
|
"total": float64(0),
|
|
},
|
|
"type": "list_complete",
|
|
},
|
|
},
|
|
selectResource: []string{"list.test_instance.example2"},
|
|
},
|
|
{
|
|
name: "error - list resource with an unknown config",
|
|
directory: "unknown-config",
|
|
expectedRes: []map[string]any{
|
|
{
|
|
"@level": "error",
|
|
"@message": "Error: config is not known",
|
|
"diagnostic": map[string]any{
|
|
"severity": "error",
|
|
"summary": "config is not known",
|
|
"detail": "",
|
|
},
|
|
"type": "diagnostic",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "error - generate-config-path already exists",
|
|
directory: "basic",
|
|
opts: []string{fmt.Sprintf("-generate-config-out=%s", t.TempDir())},
|
|
expectedRes: []map[string]any{
|
|
{
|
|
"@level": "error",
|
|
"@message": "Error: Target generated file already exists",
|
|
"diagnostic": map[string]any{
|
|
"detail": "Terraform can only write generated config into a new file. Either choose a different target location or move all existing configuration out of the target file, delete it and try again.",
|
|
"severity": "error",
|
|
"summary": "Target generated file already exists",
|
|
},
|
|
"type": "diagnostic",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "success with variables",
|
|
directory: "with-variables",
|
|
opts: []string{"-var", "instance_name=test-instance"},
|
|
expectedRes: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Starting query...",
|
|
"list_start": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"resource_type": "test_instance",
|
|
"input_config": map[string]any{"ami": "ami-12345", "foo": "test-instance"},
|
|
},
|
|
"type": "list_start",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Result found",
|
|
"list_resource_found": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"display_name": "Test Instance 1",
|
|
"identity": map[string]any{
|
|
"id": "test-instance-1",
|
|
},
|
|
"identity_version": float64(1),
|
|
"resource_type": "test_instance",
|
|
"resource_object": map[string]any{
|
|
"ami": "ami-12345",
|
|
"id": "test-instance-1",
|
|
},
|
|
},
|
|
"type": "list_resource_found",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: Result found",
|
|
"list_resource_found": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"display_name": "Test Instance 2",
|
|
"identity": map[string]any{
|
|
"id": "test-instance-2",
|
|
},
|
|
"identity_version": float64(1),
|
|
"resource_type": "test_instance",
|
|
"resource_object": map[string]any{
|
|
"ami": "ami-67890",
|
|
"id": "test-instance-2",
|
|
},
|
|
},
|
|
"type": "list_resource_found",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "list.test_instance.example: List complete",
|
|
"list_complete": map[string]any{
|
|
"address": "list.test_instance.example",
|
|
"resource_type": "test_instance",
|
|
"total": float64(2),
|
|
},
|
|
"type": "list_complete",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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", "-json"}
|
|
code = c.Run(append(args, ts.opts...))
|
|
output = done(t)
|
|
if code != 0 {
|
|
t.Logf("query command returned non-zero code '%d' and an error: \n\n%s", code, output.All())
|
|
}
|
|
|
|
// convert output to JSON array
|
|
actual := strings.TrimSpace(output.Stdout())
|
|
conc := fmt.Sprintf("[%s]", strings.Join(strings.Split(actual, "\n"), ","))
|
|
rawRes := make([]map[string]any, 0)
|
|
err := json.NewDecoder(strings.NewReader(conc)).Decode(&rawRes)
|
|
if err != nil {
|
|
t.Fatalf("failed to unmarshal: %s", err)
|
|
}
|
|
|
|
// remove unnecessary fields before comparison
|
|
actualRes := make([]map[string]any, 0, len(rawRes))
|
|
for _, item := range rawRes {
|
|
delete(item, "@module")
|
|
delete(item, "@timestamp")
|
|
delete(item, "ui")
|
|
|
|
// Clean up diagnostic fields that we don't want to compare
|
|
if diagnostic, ok := item["diagnostic"].(map[string]any); ok {
|
|
delete(diagnostic, "range")
|
|
delete(diagnostic, "snippet")
|
|
}
|
|
|
|
// if we have a select list of resource addresses, we only check those addresses
|
|
if len(ts.selectResource) > 0 {
|
|
for _, addr := range ts.selectResource {
|
|
if strings.Contains(item["@message"].(string), addr) {
|
|
actualRes = append(actualRes, item)
|
|
}
|
|
}
|
|
} else {
|
|
actualRes = append(actualRes, item)
|
|
}
|
|
}
|
|
|
|
// remove the version entry. Not relevant for testing
|
|
actualRes = slices.Delete(actualRes, 0, 1)
|
|
|
|
if diff := cmp.Diff(ts.expectedRes, actualRes); diff != "" {
|
|
// Check that the output matches the expected results
|
|
t.Errorf("expected query output to contain \n%q, \ngot: \n%q, \ndiff: %s", ts.expectedRes, actualRes, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuery_JSON_Raw(t *testing.T) {
|
|
|
|
tfVer := tfversion.String()
|
|
tests := []struct {
|
|
name string
|
|
directory string
|
|
expectedOut string
|
|
expectedErr []string
|
|
initCode int
|
|
args []string
|
|
}{
|
|
{
|
|
name: "basic query",
|
|
directory: "basic",
|
|
expectedOut: `{"@level":"info","@message":"Terraform ` + tfVer + `","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.596469+02:00","terraform":"1.14.0-dev","type":"version","ui":"1.2"}
|
|
{"@level":"info","@message":"list.test_instance.example: Starting query...","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600609+02:00","list_start":{"address":"list.test_instance.example","resource_type":"test_instance","input_config":{"ami":"ami-12345","foo":null}},"type":"list_start"}
|
|
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600729+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 1","identity":{"id":"test-instance-1"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-12345","id":"test-instance-1"}},"type":"list_resource_found"}
|
|
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600759+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 2","identity":{"id":"test-instance-2"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-67890","id":"test-instance-2"}},"type":"list_resource_found"}
|
|
{"@level":"info","@message":"list.test_instance.example: List complete","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600770+02:00","list_complete":{"address":"list.test_instance.example","resource_type":"test_instance","total":2},"type":"list_complete"}
|
|
`,
|
|
},
|
|
{
|
|
name: "empty result",
|
|
directory: "empty-result",
|
|
expectedOut: `{"@level":"info","@message":"Terraform ` + tfVer + `","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.596469+02:00","terraform":"1.14.0-dev","type":"version","ui":"1.2"}
|
|
{"@level":"info","@message":"list.test_instance.example: Starting query...","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600609+02:00","list_start":{"address":"list.test_instance.example","resource_type":"test_instance","input_config":{"ami":"ami-12345","foo":null}},"type":"list_start"}
|
|
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600729+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 1","identity":{"id":"test-instance-1"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-12345","id":"test-instance-1"}},"type":"list_resource_found"}
|
|
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600759+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 2","identity":{"id":"test-instance-2"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-67890","id":"test-instance-2"}},"type":"list_resource_found"}
|
|
{"@level":"info","@message":"list.test_instance.example: List complete","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600770+02:00","list_complete":{"address":"list.test_instance.example","resource_type":"test_instance","total":2},"type":"list_complete"}
|
|
{"@level":"info","@message":"list.test_instance.example2: Starting query...","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600609+02:00","list_start":{"address":"list.test_instance.example2","resource_type":"test_instance","input_config":{"ami":"ami-nonexistent","foo":"test-instance-1"}},"type":"list_start"}
|
|
{"@level":"info","@message":"list.test_instance.example2: List complete","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600770+02:00","list_complete":{"address":"list.test_instance.example2","resource_type":"test_instance","total":0},"type":"list_complete"}
|
|
`,
|
|
},
|
|
}
|
|
|
|
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 != 0 {
|
|
t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
|
|
}
|
|
|
|
view, done = testView(t)
|
|
meta.View = view
|
|
|
|
c := &QueryCommand{Meta: meta}
|
|
args := []string{"-no-color", "-json"}
|
|
code = c.Run(args)
|
|
output = done(t)
|
|
if code != 0 {
|
|
t.Logf("query command returned non-zero code '%d' and an error: \n\n%s", code, output.All())
|
|
}
|
|
|
|
// Use regex to normalize timestamps and version numbers for comparison
|
|
timestampRegex := regexp.MustCompile(`"@timestamp":"[^"]*"`)
|
|
versionRegex := regexp.MustCompile(`"terraform":"[^"]*"`)
|
|
|
|
actualOutput := output.Stdout()
|
|
expectedOutput := ts.expectedOut
|
|
|
|
// Replace timestamps and version numbers with placeholders
|
|
actualNormalized := timestampRegex.ReplaceAllString(actualOutput, `"@timestamp":"TIMESTAMP"`)
|
|
actualNormalized = versionRegex.ReplaceAllString(actualNormalized, `"terraform":"VERSION"`)
|
|
|
|
expectedNormalized := timestampRegex.ReplaceAllString(expectedOutput, `"@timestamp":"TIMESTAMP"`)
|
|
expectedNormalized = versionRegex.ReplaceAllString(expectedNormalized, `"terraform":"VERSION"`)
|
|
if diff := cmp.Diff(expectedNormalized, actualNormalized); diff != "" {
|
|
t.Errorf("expected query output to match, diff: %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|