Skip to content

Commit 5dd7333

Browse files
authored
statecheck: Add ExpectIdentity state check for asserting an entire identity object (#470)
* update plugin-go to use `ValueType` method * add `ExpectIdentity` state check * changelogs * mistyped * silly silly 😆
1 parent a5a8d6a commit 5dd7333

File tree

10 files changed

+538
-52
lines changed

10 files changed

+538
-52
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
kind: NOTES
22
body: This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to
33
assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `[email protected]`
4-
or `terraform-plugin-sdk/[email protected]` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentityValue` state check.
4+
or `terraform-plugin-sdk/[email protected]` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check.
55
time: 2025-03-25T11:59:27.455519-04:00
66
custom:
7-
Issue: "468"
7+
Issue: "470"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
kind: ENHANCEMENTS
2-
body: 'statecheck: Added `ExpectIdentityValue` state check, which asserts managed resource identity data stored in state.'
2+
body: 'statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state.'
33
time: 2025-03-25T12:10:07.55484-04:00
44
custom:
55
Issue: "468"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state.'
3+
time: 2025-03-25T17:45:04.794886-04:00
4+
custom:
5+
Issue: "470"

go.mod

Lines changed: 2 additions & 2 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.22.0
1717
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab
18-
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1
18+
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b
1919
github.com/hashicorp/terraform-plugin-log v0.9.0
2020
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1
2121
github.com/mitchellh/go-testing-interface v1.14.1
@@ -58,5 +58,5 @@ require (
5858
google.golang.org/appengine v1.6.8 // indirect
5959
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
6060
google.golang.org/grpc v1.71.0 // indirect
61-
google.golang.org/protobuf v1.36.5 // indirect
61+
google.golang.org/protobuf v1.36.6 // indirect
6262
)

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8
8080
github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ=
8181
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab h1:5Qpuprk76zkVEdTCtfoPjUc+1AeUxlgkF6sWTr7qLDs=
8282
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
83-
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4=
84-
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o=
83+
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b h1:JCAO+OdLztQ6F2bZ8lU93u986UVQl2Y/HNz18/jg3b0=
84+
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b/go.mod h1:HFPb73wivXPZy5wMuE7T3WqFbpIj6R6q1svKnZsnMZo=
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.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw=
@@ -220,8 +220,8 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
220220
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
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=
223-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
224-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
223+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
224+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
225225
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
226226
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
227227
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/testing/testsdk/providerserver/tftypes.go

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ func IdentityDynamicValueToValue(schema *tfprotov6.ResourceIdentitySchema, dynam
7676
}
7777

7878
if dynamicValue == nil {
79-
return tftypes.NewValue(getIdentitySchemaValueType(schema), nil), nil
79+
return tftypes.NewValue(schema.ValueType(), nil), nil
8080
}
8181

82-
value, err := dynamicValue.Unmarshal(getIdentitySchemaValueType(schema))
82+
value, err := dynamicValue.Unmarshal(schema.ValueType())
8383

8484
if err != nil {
8585
diag := &tfprotov6.Diagnostic{
@@ -105,7 +105,7 @@ func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value
105105
return nil, diag
106106
}
107107

108-
dynamicValue, err := tfprotov6.NewDynamicValue(getIdentitySchemaValueType(schema), value)
108+
dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value)
109109

110110
if err != nil {
111111
diag := &tfprotov6.Diagnostic{
@@ -119,42 +119,3 @@ func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value
119119

120120
return &dynamicValue, nil
121121
}
122-
123-
// TODO: This should be replaced by the `ValueType` method from plugin-go:
124-
// https://github.com/hashicorp/terraform-plugin-go/pull/497
125-
func getIdentitySchemaValueType(schema *tfprotov6.ResourceIdentitySchema) tftypes.Type {
126-
if schema == nil || schema.IdentityAttributes == nil {
127-
return tftypes.Object{
128-
AttributeTypes: map[string]tftypes.Type{},
129-
}
130-
}
131-
attributeTypes := map[string]tftypes.Type{}
132-
133-
for _, attribute := range schema.IdentityAttributes {
134-
if attribute == nil {
135-
continue
136-
}
137-
138-
attributeType := getIdentityAttributeValueType(attribute)
139-
140-
if attributeType == nil {
141-
continue
142-
}
143-
144-
attributeTypes[attribute.Name] = attributeType
145-
}
146-
147-
return tftypes.Object{
148-
AttributeTypes: attributeTypes,
149-
}
150-
}
151-
152-
// TODO: This should be replaced by the `ValueType` method from plugin-go:
153-
// https://github.com/hashicorp/terraform-plugin-go/pull/497
154-
func getIdentityAttributeValueType(attr *tfprotov6.ResourceIdentitySchemaAttribute) tftypes.Type {
155-
if attr == nil {
156-
return nil
157-
}
158-
159-
return attr.Type
160-
}

knownvalue/object.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPr
106106
for i, k := range deltaKeys {
107107
if i == 0 {
108108
deltaMsg += msgPrefix
109-
} else if i != 0 {
109+
} else {
110110
deltaMsg += ", "
111111
}
112112
deltaMsg += fmt.Sprintf("%q", k)

statecheck/expect_identity.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package statecheck
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"maps"
10+
"slices"
11+
"sort"
12+
13+
tfjson "github.com/hashicorp/terraform-json"
14+
15+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
16+
)
17+
18+
var _ StateCheck = expectIdentity{}
19+
20+
type expectIdentity struct {
21+
resourceAddress string
22+
identity map[string]knownvalue.Check
23+
}
24+
25+
// CheckState implements the state check logic.
26+
func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
27+
var resource *tfjson.StateResource
28+
29+
if req.State == nil {
30+
resp.Error = fmt.Errorf("state is nil")
31+
32+
return
33+
}
34+
35+
if req.State.Values == nil {
36+
resp.Error = fmt.Errorf("state does not contain any state values")
37+
38+
return
39+
}
40+
41+
if req.State.Values.RootModule == nil {
42+
resp.Error = fmt.Errorf("state does not contain a root module")
43+
44+
return
45+
}
46+
47+
for _, r := range req.State.Values.RootModule.Resources {
48+
if e.resourceAddress == r.Address {
49+
resource = r
50+
51+
break
52+
}
53+
}
54+
55+
if resource == nil {
56+
resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress)
57+
58+
return
59+
}
60+
61+
if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 {
62+
resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress)
63+
64+
return
65+
}
66+
67+
if len(resource.IdentityValues) != len(e.identity) {
68+
deltaMsg := ""
69+
if len(resource.IdentityValues) > len(e.identity) {
70+
deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ")
71+
} else {
72+
deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ")
73+
}
74+
75+
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)
76+
return
77+
}
78+
79+
var keys []string
80+
81+
for k := range e.identity {
82+
keys = append(keys, k)
83+
}
84+
85+
sort.SliceStable(keys, func(i, j int) bool {
86+
return keys[i] < keys[j]
87+
})
88+
89+
for _, k := range keys {
90+
actualIdentityVal, ok := resource.IdentityValues[k]
91+
92+
if !ok {
93+
resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k)
94+
return
95+
}
96+
97+
if err := e.identity[k].CheckValue(actualIdentityVal); err != nil {
98+
resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err)
99+
return
100+
}
101+
}
102+
}
103+
104+
// ExpectIdentity returns a state check that asserts that the identity at the given resource matches a known object, where each
105+
// map key represents an identity attribute name. The identity in state must exactly match the given object and any missing/extra
106+
// attributes will raise a diagnostic.
107+
//
108+
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
109+
func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) StateCheck {
110+
return expectIdentity{
111+
resourceAddress: resourceAddress,
112+
identity: identity,
113+
}
114+
}
115+
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 {
118+
deltaMsg := ""
119+
120+
deltaMap := make(map[string]T, len(mapA))
121+
maps.Copy(deltaMap, mapA)
122+
for key := range mapB {
123+
delete(deltaMap, key)
124+
}
125+
126+
deltaKeys := slices.Sorted(maps.Keys(deltaMap))
127+
128+
for i, k := range deltaKeys {
129+
if i == 0 {
130+
deltaMsg += msgPrefix
131+
} else {
132+
deltaMsg += ", "
133+
}
134+
deltaMsg += fmt.Sprintf("%q", k)
135+
}
136+
137+
return deltaMsg
138+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package statecheck_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
11+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
12+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
13+
)
14+
15+
func ExampleExpectIdentity() {
16+
// A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`.
17+
t := &testing.T{}
18+
t.Parallel()
19+
20+
resource.Test(t, resource.TestCase{
21+
// Resource identity support is only available in Terraform v1.12+
22+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
23+
tfversion.SkipBelow(tfversion.Version1_12_0),
24+
},
25+
// Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes
26+
Steps: []resource.TestStep{
27+
{
28+
Config: `resource "test_resource" "one" {}`,
29+
ConfigStateChecks: []statecheck.StateCheck{
30+
statecheck.ExpectIdentity(
31+
"test_resource.one",
32+
map[string]knownvalue.Check{
33+
"id": knownvalue.StringExact("id-123"),
34+
"name": knownvalue.StringExact("John Doe"),
35+
},
36+
),
37+
},
38+
},
39+
},
40+
})
41+
}

0 commit comments

Comments
 (0)