Skip to content

Commit 49004cd

Browse files
committed
Scaffolding for QueryCheck
1 parent 236218d commit 49004cd

37 files changed

+10629
-21
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ require (
1515
github.com/hashicorp/logutils v1.0.0
1616
github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2
1717
github.com/hashicorp/terraform-json v0.25.0
18-
github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1
18+
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
2121
github.com/mitchellh/go-testing-interface v1.14.1
@@ -56,7 +56,7 @@ require (
5656
golang.org/x/text v0.27.0 // indirect
5757
golang.org/x/tools v0.34.0 // indirect
5858
google.golang.org/appengine v1.6.8 // indirect
59-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
60-
google.golang.org/grpc v1.73.0 // indirect
59+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
60+
google.golang.org/grpc v1.74.2 // indirect
6161
google.golang.org/protobuf v1.36.6 // indirect
6262
)

go.sum

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
2929
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
3030
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
3131
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
32-
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
33-
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
32+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
33+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3434
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
3535
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
3636
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
@@ -80,8 +80,8 @@ github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90f
8080
github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA=
8181
github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ=
8282
github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
83-
github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 h1:ZId6oWG8VTKhz207quE/Xh8a3HuoLtM/QkcSSypekIQ=
84-
github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY=
83+
github.com/hashicorp/terraform-plugin-go v0.29.0-beta.1 h1:xeHlRQYev3iMXwX2W7+D1bSfLRBs9jojZXqE6hmNxMI=
84+
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=
8686
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
8787
github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM=
@@ -152,16 +152,16 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6
152152
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
153153
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
154154
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
155-
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
156-
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
157-
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
158-
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
159-
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
160-
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
161-
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
162-
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
163-
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
164-
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
155+
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
156+
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
157+
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
158+
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
159+
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
160+
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
161+
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
162+
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
163+
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
164+
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
165165
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
166166
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
167167
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
@@ -214,10 +214,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
214214
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
215215
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
216216
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
217-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
218-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
219-
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
220-
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
217+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
218+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
219+
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
220+
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
221221
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
222222
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
223223
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package query
5+
6+
import (
7+
"context"
8+
"errors"
9+
10+
tfjson "github.com/hashicorp/terraform-json"
11+
"github.com/mitchellh/go-testing-interface"
12+
13+
"github.com/hashicorp/terraform-plugin-testing/querycheck"
14+
)
15+
16+
func runQueryChecks(ctx context.Context, t testing.T, query *tfjson.Query, queryChecks []querycheck.QueryCheck) error {
17+
t.Helper()
18+
19+
var result []error
20+
21+
for _, queryCheck := range queryChecks {
22+
resp := querycheck.CheckQueryResponse{}
23+
queryCheck.CheckQuery(ctx, querycheck.CheckQueryRequest{Query: query}, &resp)
24+
25+
result = append(result, resp.Error)
26+
}
27+
28+
return errors.Join(result...)
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package query
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/querycheck"
10+
)
11+
12+
var _ querycheck.QueryCheck = &queryCheckSpy{}
13+
14+
type queryCheckSpy struct {
15+
err error
16+
called bool
17+
}
18+
19+
func (s *queryCheckSpy) CheckQuery(ctx context.Context, req querycheck.CheckQueryRequest, resp *querycheck.CheckQueryResponse) {
20+
s.called = true
21+
resp.Error = s.err
22+
}

helper/resource/testing.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"flag"
1010
"fmt"
11+
"github.com/hashicorp/terraform-plugin-testing/querycheck"
1112
"log"
1213
"os"
1314
"regexp"
@@ -640,6 +641,10 @@ type TestStep struct {
640641
// Custom state checks can be created by implementing the [statecheck.StateCheck] interface, or by using a StateCheck implementation from the provided [statecheck] package.
641642
ConfigStateChecks []statecheck.StateCheck
642643

644+
// ConfigQueryChecks allow assertions to be made against the query file during a Config test using a query check.
645+
// Custom query checks can be created by implementing the [querycheck.QueryCheck] interface, or by using a QueryCheck implementation from the provided [querycheck] package.
646+
ConfigQueryChecks []querycheck.QueryCheck
647+
643648
// PlanOnly can be set to only run `plan` with this configuration, and not
644649
// actually apply it. This is useful for ensuring config changes result in
645650
// no-op plans
@@ -855,6 +860,21 @@ type ConfigPlanChecks struct {
855860
PostApplyPostRefresh []plancheck.PlanCheck
856861
}
857862

863+
// ConfigQueryChecks defines the different points in a Config TestStep when query checks can be run.
864+
type ConfigQueryChecks struct {
865+
// PreApply runs all query checks in the slice. This occurs before the apply of a Config test is run. This slice cannot be populated
866+
// with TestStep.QueryOnly, as there is no PreApply query run with that flag set. All errors by query checks in this slice are aggregated, reported, and will result in a test failure.
867+
PreApply []querycheck.QueryCheck
868+
869+
// PostApplyPreRefresh runs all query checks in the slice. This occurs after the apply and before the refresh of a Config test is run.
870+
// All errors by query checks in this slice are aggregated, reported, and will result in a test failure.
871+
PostApplyPreRefresh []querycheck.QueryCheck
872+
873+
// PostApplyPostRefresh runs all query checks in the slice. This occurs after the apply and refresh of a Config test are run.
874+
// All errors by query checks in this slice are aggregated, reported, and will result in a test failure.
875+
PostApplyPostRefresh []querycheck.QueryCheck
876+
}
877+
858878
// ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run.
859879
type ImportPlanChecks struct {
860880
// PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated

internal/plugintest/working_dir.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,8 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) {
532532
var buffer bytes.Buffer
533533
err := wd.tf.QueryJSON(context.Background(), &buffer)
534534

535+
// Marshall buffer? JSON.mashallto___ terraform-json.Query
536+
535537
if err != nil {
536538
return nil, fmt.Errorf("error running terraform query command: %w", err)
537539
}

querycheck/compare_value.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package querycheck
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
tfjson "github.com/hashicorp/terraform-json"
11+
12+
"github.com/hashicorp/terraform-plugin-testing/compare"
13+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
14+
)
15+
16+
// Resource Query Check
17+
var _ QueryCheck = &compareValue{}
18+
19+
type compareValue struct {
20+
resourceAddresses []string
21+
attributePaths []tfjsonpath.Path
22+
queryValues []any
23+
comparer compare.ValueComparer
24+
}
25+
26+
func (e *compareValue) AddQueryValue(resourceAddress string, attributePath tfjsonpath.Path) QueryCheck {
27+
e.resourceAddresses = append(e.resourceAddresses, resourceAddress)
28+
e.attributePaths = append(e.attributePaths, attributePath)
29+
30+
return e
31+
}
32+
33+
// CheckQuery implements the query check logic.
34+
func (e *compareValue) CheckQuery(ctx context.Context, req CheckQueryRequest, resp *CheckQueryResponse) {
35+
var resource *tfjson.QueryResource
36+
37+
if req.Query == nil {
38+
resp.Error = fmt.Errorf("query is nil")
39+
40+
return
41+
}
42+
43+
if req.Query.Values == nil {
44+
resp.Error = fmt.Errorf("query does not contain any query values")
45+
46+
return
47+
}
48+
49+
if req.Query.Values.RootModule == nil {
50+
resp.Error = fmt.Errorf("query does not contain a root module")
51+
52+
return
53+
}
54+
55+
// All calls to AddQueryValue occur before any TestStep is run, populating the resourceAddresses
56+
// and attributePaths slices. The queryValues slice is populated during execution of each TestStep.
57+
// Each call to CheckQuery happens sequentially during each TestStep.
58+
// The currentIndex is reflective of the current query value being checked.
59+
currentIndex := len(e.queryValues)
60+
61+
if len(e.resourceAddresses) <= currentIndex {
62+
resp.Error = fmt.Errorf("resource addresses index out of bounds: %d", currentIndex)
63+
64+
return
65+
}
66+
67+
resourceAddress := e.resourceAddresses[currentIndex]
68+
69+
for _, r := range req.Query.Values.RootModule.Resources {
70+
if resourceAddress == r.Address {
71+
resource = r
72+
73+
break
74+
}
75+
}
76+
77+
if resource == nil {
78+
resp.Error = fmt.Errorf("%s - Resource not found in query", resourceAddress)
79+
80+
return
81+
}
82+
83+
if len(e.attributePaths) <= currentIndex {
84+
resp.Error = fmt.Errorf("attribute paths index out of bounds: %d", currentIndex)
85+
86+
return
87+
}
88+
89+
attributePath := e.attributePaths[currentIndex]
90+
91+
result, err := tfjsonpath.Traverse(resource.AttributeValues, attributePath)
92+
93+
if err != nil {
94+
resp.Error = err
95+
96+
return
97+
}
98+
99+
e.queryValues = append(e.queryValues, result)
100+
101+
err = e.comparer.CompareValues(e.queryValues...)
102+
103+
if err != nil {
104+
resp.Error = err
105+
}
106+
}
107+
108+
// CompareValue returns a query check that compares values retrieved from query using the
109+
// supplied value comparer.
110+
func CompareValue(comparer compare.ValueComparer) *compareValue {
111+
return &compareValue{
112+
comparer: comparer,
113+
}
114+
}

0 commit comments

Comments
 (0)