diff --git a/internal/command/query_test.go b/internal/command/query_test.go index 0fe101263c..e29dda83f5 100644 --- a/internal/command/query_test.go +++ b/internal/command/query_test.go @@ -5,6 +5,7 @@ package command import ( "path" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -83,6 +84,15 @@ Error: Unsupported block type 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.`, + initCode: 0, + }, } for _, ts := range tests { @@ -118,19 +128,22 @@ Blocks of type "resource" are not expected here. 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()) } + actual := strings.TrimSpace(output.Stdout()) // 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) + 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()) for _, expected := range ts.expectedErr { + expected := strings.TrimSpace(expected) if diff := cmp.Diff(expected, actual); diff != "" { t.Errorf("expected error message to contain '%s', \ngot: %s, \ndiff: %s", expected, actual, diff) } @@ -228,11 +241,12 @@ func queryFixtureProvider() *testing_provider.MockProvider { configMap := wholeConfigMap["config"] - // For empty results test case //TODO: Remove? - if ami, ok := wholeConfigMap["ami"]; ok && ami.AsString() == "ami-nonexistent" { + // 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.ListVal([]cty.Value{}), + "data": cty.ListValEmpty(cty.DynamicPseudoType), "config": configMap, }), } diff --git a/internal/command/testdata/query/empty-result/main.tf b/internal/command/testdata/query/empty-result/main.tf new file mode 100644 index 0000000000..2090cb13d4 --- /dev/null +++ b/internal/command/testdata/query/empty-result/main.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +provider "test" {} + +resource "test_instance" "example" { + ami = "ami-12345" +} diff --git a/internal/command/testdata/query/empty-result/query.tfquery.hcl b/internal/command/testdata/query/empty-result/query.tfquery.hcl new file mode 100644 index 0000000000..fbdf2b4106 --- /dev/null +++ b/internal/command/testdata/query/empty-result/query.tfquery.hcl @@ -0,0 +1,15 @@ +list "test_instance" "example" { + provider = test + + config { + ami = "ami-12345" + } +} + +list "test_instance" "example2" { + provider = test + + config { + ami = "ami-nonexistent" + } +} diff --git a/internal/command/views/hook_json.go b/internal/command/views/hook_json.go index 98d3d15d26..9f274ce348 100644 --- a/internal/command/views/hook_json.go +++ b/internal/command/views/hook_json.go @@ -263,6 +263,11 @@ func (h *jsonHook) PostListQuery(id terraform.HookResourceIdentity, results plan json.MessageListResourceFound, result, ) } + h.view.log.Info( + fmt.Sprintf("%s: List complete", addr.String()), + "type", json.MessageListComplete, + "total", data.LengthInt(), + ) return terraform.HookActionContinue, nil } diff --git a/internal/command/views/hook_ui.go b/internal/command/views/hook_ui.go index dc16651ec2..6f418749a2 100644 --- a/internal/command/views/hook_ui.go +++ b/internal/command/views/hook_ui.go @@ -534,7 +534,9 @@ func (h *UiHook) PostListQuery(id terraform.HookResourceIdentity, results plans. result.WriteString(fmt.Sprintf("%s %-*s %s\n", addr.String(), maxIdentityLen, identity, displayNames[i])) } - h.println(result.String()) + if result.Len() > 0 { + h.println(result.String()) + } return terraform.HookActionContinue, nil } diff --git a/internal/command/views/json/message_types.go b/internal/command/views/json/message_types.go index b0f4073cec..b1d0cbd62c 100644 --- a/internal/command/views/json/message_types.go +++ b/internal/command/views/json/message_types.go @@ -50,6 +50,7 @@ const ( // List messages MessageListStart MessageType = "list_start" MessageListResourceFound MessageType = "list_resource_found" + MessageListComplete MessageType = "list_complete" // Action messages MessageActionStart MessageType = "action_start" diff --git a/internal/command/views/query_operation.go b/internal/command/views/query_operation.go index f97b298e2c..d9268328df 100644 --- a/internal/command/views/query_operation.go +++ b/internal/command/views/query_operation.go @@ -5,6 +5,7 @@ package views import ( "fmt" + "strings" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/format" @@ -59,6 +60,31 @@ func (v *QueryOperationHuman) EmergencyDumpState(stateFile *statefile.File) erro } func (v *QueryOperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) { + // The hook for individual query blocks do not display any output when the results are empty, + // so we will display a grouped warning message here for the empty queries. + emptyBlocks := []string{} + for _, query := range plan.Changes.Queries { + pSchema := schemas.ProviderSchema(query.ProviderAddr.Provider) + addr := query.Addr + schema := pSchema.ListResourceTypes[addr.Resource.Resource.Type] + + results, err := query.Decode(schema) + if err != nil { + v.view.streams.Eprintln(err) + continue + } + + data := results.Results.Value.GetAttr("data") + if data.LengthInt() == 0 { + emptyBlocks = append(emptyBlocks, addr.String()) + } + + } + + if len(emptyBlocks) > 0 { + msg := fmt.Sprintf(v.view.colorize.Color("[bold][yellow]Warning:[reset][bold] list block(s) [%s] returned 0 results.\n"), strings.Join(emptyBlocks, ", ")) + v.view.streams.Println(format.WordWrap(msg, v.view.outputColumns())) + } } func (v *QueryOperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) {