Skip to content

Commit 0bebb21

Browse files
committed
update terraform-json and terraform-exec dependencies and update query checks to be consistent with existing statechecks
1 parent 695a600 commit 0bebb21

File tree

7 files changed

+91
-95
lines changed

7 files changed

+91
-95
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ require (
1313
github.com/hashicorp/hc-install v0.9.2
1414
github.com/hashicorp/hcl/v2 v2.24.0
1515
github.com/hashicorp/logutils v1.0.0
16-
github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503
17-
github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4
16+
github.com/hashicorp/terraform-exec v0.24.0
17+
github.com/hashicorp/terraform-json v0.27.2
1818
github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1
1919
github.com/hashicorp/terraform-plugin-log v0.9.0
2020
github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx
7676
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
7777
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
7878
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
79-
github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503 h1:A/cbqqEjJmPouKHgWlIBQOBPROLO4ubPmHFl7yKZ+As=
80-
github.com/hashicorp/terraform-exec v0.23.2-0.20250903143921-c05687bce503/go.mod h1:rG9V56jmBbB5hXCo4MpZTr6tYKLaykUONa8mX01yBhg=
81-
github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4 h1:qErXY0TfojskxvAlCiqS4IMmXulQ4TfApiO8IlKkImc=
82-
github.com/hashicorp/terraform-json v0.26.1-0.20250829125600-5c1a00f3ccc4/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE=
79+
github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE=
80+
github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4=
81+
github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU=
82+
github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE=
8383
github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI=
8484
github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1/go.mod h1:5pww/UULn9C2tItq6o5sbScEkJxBUt9X9kI4DkeRsIw=
8585
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=

helper/resource/query/query_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,16 @@ func TestQuery(t *testing.T) {
7878
"location": knownvalue.StringExact("westeurope3"),
7979
}),
8080
querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{
81-
"id": knownvalue.StringExact("westeurope/somevalue1"),
81+
"id": knownvalue.StringExact("westeurope/somevalue1"),
82+
"location": knownvalue.StringExact("westeurope"),
8283
}),
8384
querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{
84-
"id": knownvalue.StringExact("westeurope/somevalue2"),
85+
"id": knownvalue.StringExact("westeurope/somevalue2"),
86+
"location": knownvalue.StringExact("westeurope"),
8587
}),
8688
querycheck.ExpectIdentity("examplecloud_containerette.test2", map[string]knownvalue.Check{
87-
"id": knownvalue.StringExact("westeurope/somevalue3"),
89+
"id": knownvalue.StringExact("westeurope/somevalue3"),
90+
"location": knownvalue.StringExact("westeurope"),
8891
}),
8992
},
9093
},

internal/plugintest/working_dir.go

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ package plugintest
66
import (
77
"context"
88
"fmt"
9+
"github.com/hashicorp/terraform-exec/tfexec"
10+
tfjson "github.com/hashicorp/terraform-json"
911
"io"
1012
"os"
1113
"path/filepath"
12-
"strings"
13-
14-
"github.com/hashicorp/terraform-exec/tfexec"
15-
tfjson "github.com/hashicorp/terraform-json"
1614

1715
"github.com/hashicorp/terraform-plugin-testing/config"
1816
"github.com/hashicorp/terraform-plugin-testing/internal/logging"
@@ -527,38 +525,32 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err
527525

528526
func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) {
529527
var messages []tfjson.LogMsg
530-
var logEmit *tfexec.LogMsgEmitter
531-
var execErr, err error
532-
var message tfjson.LogMsg
533-
var related bool
528+
var diags []tfjson.LogMsg
534529

535530
logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command")
536531

537532
args := []tfexec.QueryOption{tfexec.Reattach(wd.reattachInfo)}
538533

539-
logEmit, execErr = wd.tf.QueryJSON(context.Background(), args...)
534+
logs, err := wd.tf.QueryJSON(context.Background(), args...)
540535

541-
if execErr != nil {
542-
return nil, fmt.Errorf("Error running terraform query command: %w", err)
543-
}
544-
545-
message, related, err = logEmit.NextMessage()
546-
547-
if !related && err != nil {
548-
return nil, fmt.Errorf("Error no messages found from terraform query command: %w", err)
536+
if err != nil {
537+
return nil, fmt.Errorf("running terraform query command: %w", err)
549538
}
550539

551-
for err != nil || message != nil {
552-
message, related, err = logEmit.NextMessage()
553-
messages = append(messages, message)
554-
555-
if message != nil && strings.Contains(message.Message(), "Invalid provider configuration") {
556-
return nil, fmt.Errorf("Provider requires explicit configuration. Add a provider block to the root module and configure the provider's required arguments as described in the provider documentation.")
540+
for msg := range logs {
541+
if msg.Err != nil {
542+
return nil, fmt.Errorf("retrieving next message: %w", msg.Err)
543+
}
544+
if msg.Msg.Level() == tfjson.Error {
545+
// TODO reimplement missing .tf config error
546+
diags = append(diags, msg.Msg)
547+
continue
557548
}
549+
messages = append(messages, msg.Msg)
558550
}
559551

560-
if related {
561-
return nil, fmt.Errorf("Error running terraform query command: %w", err)
552+
if len(diags) > 0 {
553+
return nil, fmt.Errorf("running terraform query command returned diagnostics: %+v", diags)
562554
}
563555

564556
logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command")

querycheck/expect_identity.go

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,67 +5,82 @@ package querycheck
55

66
import (
77
"context"
8-
"encoding/json"
98
"errors"
109
"fmt"
10+
"sort"
11+
"strings"
1112

1213
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
14+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
1315
)
1416

1517
var _ QueryResultCheck = expectIdentity{}
1618

1719
type expectIdentity struct {
18-
resourceAddress string
19-
check map[string]knownvalue.Check
20+
listResourceAddress string
21+
check map[string]knownvalue.Check
2022
}
2123

2224
// CheckQuery implements the query check logic.
23-
func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) {
25+
func (e expectIdentity) CheckQuery(_ context.Context, req CheckQueryRequest, resp *CheckQueryResponse) {
2426
for _, res := range *req.Query {
2527
var errCollection []error
2628

27-
for attribute := range e.check {
28-
var val any
29-
var unmarshalledVal any
29+
if e.listResourceAddress != strings.TrimPrefix(res.Address, "list.") {
30+
continue
31+
}
3032

31-
val, ok := res.Identity[attribute]
32-
if !ok {
33-
resp.Error = fmt.Errorf("%s - expected attribute %q not in actual identity object", e.resourceAddress, attribute)
34-
return
33+
if len(res.Identity) != len(e.check) {
34+
deltaMsg := ""
35+
if len(res.Identity) > len(e.check) {
36+
deltaMsg = statecheck.CreateDeltaString(res.Identity, e.check, "actual identity has extra attribute(s): ")
37+
} else {
38+
deltaMsg = statecheck.CreateDeltaString(e.check, res.Identity, "actual identity is missing attribute(s): ")
3539
}
3640

37-
rawMessage, ok := val.(json.RawMessage)
38-
if !ok {
39-
resp.Error = fmt.Errorf("%s - expected json.RawMessage but got %T", e.resourceAddress, val)
40-
return
41-
}
42-
err := json.Unmarshal(rawMessage, &unmarshalledVal)
41+
resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.listResourceAddress, len(e.check), len(res.Identity), deltaMsg)
42+
return
43+
}
44+
45+
var keys []string
4346

44-
if err != nil {
45-
resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.resourceAddress, err)
47+
for k := range e.check {
48+
keys = append(keys, k)
49+
}
50+
51+
sort.SliceStable(keys, func(i, j int) bool {
52+
return keys[i] < keys[j]
53+
})
54+
55+
for _, k := range keys {
56+
actualIdentityVal, ok := res.Identity[k]
57+
58+
if !ok {
59+
resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.listResourceAddress, k)
4660
return
4761
}
4862

49-
if err = e.check[attribute].CheckValue(unmarshalledVal); err != nil {
50-
errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s\n", e.resourceAddress, e.check, err))
63+
if err := e.check[k].CheckValue(actualIdentityVal); err != nil {
64+
errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %s", e.listResourceAddress, k, err))
5165
}
5266
}
67+
5368
if errCollection == nil {
5469
return
5570
}
5671
}
5772

5873
var errCollection []error
59-
60-
errCollection = append(errCollection, fmt.Errorf("An identity with the following attributes was not found:"))
74+
errCollection = append(errCollection, fmt.Errorf("an identity with the following attributes was not found"))
6175

6276
// wrap errors for each check
6377
for attr, check := range e.check {
64-
errCollection = append(errCollection, fmt.Errorf("Attribute %s: %s", attr, check))
78+
errCollection = append(errCollection, fmt.Errorf("attribute %q: %s", attr, check))
6579
}
66-
errCollection = append(errCollection, fmt.Errorf("Address: %s\n", e.resourceAddress))
67-
80+
errCollection = append(errCollection, fmt.Errorf("address: %s\n", e.listResourceAddress))
6881
resp.Error = errors.Join(errCollection...)
82+
83+
return
6984
}
7085

7186
// ExpectIdentity returns a query check that asserts that the identity at the given resource matches a known object, where each
@@ -74,7 +89,7 @@ func (e expectIdentity) CheckQuery(ctx context.Context, req CheckQueryRequest, r
7489
// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+
7590
func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) QueryResultCheck {
7691
return expectIdentity{
77-
resourceAddress: resourceAddress,
78-
check: identity,
92+
listResourceAddress: resourceAddress,
93+
check: identity,
7994
}
8095
}

querycheck/expect_known_value.go

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package querycheck
55

66
import (
77
"context"
8-
"encoding/json"
98
"fmt"
109
"strings"
1110

@@ -32,36 +31,18 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r
3231
return
3332
}
3433

35-
// Ideally we can do the check like below which is identical to the expect known value state check but... terraform-json hasn't
36-
// defined the resource object as a map[string]interface, we so we need to iterate over it manually
37-
//resource, err := tfjsonpath.Traverse(res.ResourceObject, e.attributePath)
38-
//if err != nil {
39-
// resp.Error = err
40-
// return
41-
//}
42-
//
43-
//if err := e.knownValue.CheckValue(resource); err != nil {
44-
// diags = append(diags, fmt.Errorf("error checking value for attribute at path: %s for resource %s, err: %s", e.attributePath.String(), e.resourceName, err))
45-
//}
46-
//
47-
//if diags == nil {
48-
// return
49-
//}
50-
51-
for k, v := range res.ResourceObject {
52-
if k == e.attributePath.String() {
53-
var val any
34+
resource, err := tfjsonpath.Traverse(res.ResourceObject, e.attributePath)
35+
if err != nil {
36+
resp.Error = err
37+
return
38+
}
5439

55-
err := json.Unmarshal(v, &val)
56-
if err != nil {
57-
resp.Error = fmt.Errorf("%s - Error decoding message type: %s", e.listResourceAddress, err)
58-
return
59-
}
40+
if err := e.knownValue.CheckValue(resource); err != nil {
41+
diags = append(diags, fmt.Errorf("error checking value for attribute at path: %s for resource %s, err: %s", e.attributePath.String(), e.resourceName, err))
42+
}
6043

61-
if err := e.knownValue.CheckValue(val); err != nil {
62-
diags = append(diags, fmt.Errorf("error checking value for attribute at path: %s for resource %s, err: %s", e.attributePath.String(), e.resourceName, err))
63-
}
64-
}
44+
if diags == nil {
45+
return
6546
}
6647
}
6748

@@ -78,6 +59,11 @@ func (e expectKnownValue) CheckQuery(_ context.Context, req CheckQueryRequest, r
7859
resp.Error = fmt.Errorf("%s - the resource %s was not found", e.listResourceAddress, e.resourceName)
7960
}
8061

62+
// ExpectKnownValue returns a query check that asserts the specified attribute values are present for a given resource object
63+
// returned by a list query. The resource object can only be identified by providing the list resource address as well as the
64+
// resource name (display name).
65+
//
66+
// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+
8167
func ExpectKnownValue(listResourceAddress string, resourceName string, attributePath tfjsonpath.Path, knownValue knownvalue.Check) QueryResultCheck {
8268
return expectKnownValue{
8369
listResourceAddress: listResourceAddress,

statecheck/expect_identity.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, r
6767
if len(resource.IdentityValues) != len(e.identity) {
6868
deltaMsg := ""
6969
if len(resource.IdentityValues) > len(e.identity) {
70-
deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ")
70+
deltaMsg = CreateDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ")
7171
} else {
72-
deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ")
72+
deltaMsg = CreateDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ")
7373
}
7474

7575
resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg)
@@ -113,8 +113,8 @@ func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check
113113
}
114114
}
115115

116-
// createDeltaString prints the map keys that are present in mapA and not present in mapB
117-
func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string {
116+
// CreateDeltaString prints the map keys that are present in mapA and not present in mapB
117+
func CreateDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string {
118118
deltaMsg := ""
119119

120120
deltaMap := make(map[string]T, len(mapA))

0 commit comments

Comments
 (0)