Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .changes/unreleased/NOTES-20250325-115927.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
kind: NOTES
body: This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to
assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `[email protected]`
or `terraform-plugin-sdk/[email protected]` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentityValue` state check.
or `terraform-plugin-sdk/[email protected]` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check.
time: 2025-03-25T11:59:27.455519-04:00
custom:
Issue: "468"
Issue: "470"
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
kind: ENHANCEMENTS
body: 'statecheck: Added `ExpectIdentityValue` state check, which asserts managed resource identity data stored in state.'
body: 'statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state.'
time: 2025-03-25T12:10:07.55484-04:00
custom:
Issue: "468"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: 'statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state.'
time: 2025-03-25T17:45:04.794886-04:00
custom:
Issue: "470"
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/hashicorp/logutils v1.0.0
github.com/hashicorp/terraform-exec v0.22.0
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1
github.com/mitchellh/go-testing-interface v1.14.1
Expand Down Expand Up @@ -58,5 +58,5 @@ require (
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8
github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ=
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab h1:5Qpuprk76zkVEdTCtfoPjUc+1AeUxlgkF6sWTr7qLDs=
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4=
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o=
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b h1:JCAO+OdLztQ6F2bZ8lU93u986UVQl2Y/HNz18/jg3b0=
github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1.0.20250325210248-fa8d1fe4306b/go.mod h1:HFPb73wivXPZy5wMuE7T3WqFbpIj6R6q1svKnZsnMZo=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw=
Expand Down Expand Up @@ -220,8 +220,8 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
45 changes: 3 additions & 42 deletions internal/testing/testsdk/providerserver/tftypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ func IdentityDynamicValueToValue(schema *tfprotov6.ResourceIdentitySchema, dynam
}

if dynamicValue == nil {
return tftypes.NewValue(getIdentitySchemaValueType(schema), nil), nil
return tftypes.NewValue(schema.ValueType(), nil), nil
}

value, err := dynamicValue.Unmarshal(getIdentitySchemaValueType(schema))
value, err := dynamicValue.Unmarshal(schema.ValueType())

if err != nil {
diag := &tfprotov6.Diagnostic{
Expand All @@ -105,7 +105,7 @@ func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value
return nil, diag
}

dynamicValue, err := tfprotov6.NewDynamicValue(getIdentitySchemaValueType(schema), value)
dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value)

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

return &dynamicValue, nil
}

// TODO: This should be replaced by the `ValueType` method from plugin-go:
// https://github.com/hashicorp/terraform-plugin-go/pull/497
func getIdentitySchemaValueType(schema *tfprotov6.ResourceIdentitySchema) tftypes.Type {
if schema == nil || schema.IdentityAttributes == nil {
return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{},
}
}
attributeTypes := map[string]tftypes.Type{}

for _, attribute := range schema.IdentityAttributes {
if attribute == nil {
continue
}

attributeType := getIdentityAttributeValueType(attribute)

if attributeType == nil {
continue
}

attributeTypes[attribute.Name] = attributeType
}

return tftypes.Object{
AttributeTypes: attributeTypes,
}
}

// TODO: This should be replaced by the `ValueType` method from plugin-go:
// https://github.com/hashicorp/terraform-plugin-go/pull/497
func getIdentityAttributeValueType(attr *tfprotov6.ResourceIdentitySchemaAttribute) tftypes.Type {
if attr == nil {
return nil
}

return attr.Type
}
138 changes: 138 additions & 0 deletions statecheck/expect_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package statecheck

import (
"context"
"fmt"
"maps"
"slices"
"sort"

tfjson "github.com/hashicorp/terraform-json"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)

var _ StateCheck = expectIdentity{}

type expectIdentity struct {
resourceAddress string
identity map[string]knownvalue.Check
}

// CheckState implements the state check logic.
func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) {
var resource *tfjson.StateResource

if req.State == nil {
resp.Error = fmt.Errorf("state is nil")

return
}

if req.State.Values == nil {
resp.Error = fmt.Errorf("state does not contain any state values")

return
}

if req.State.Values.RootModule == nil {
resp.Error = fmt.Errorf("state does not contain a root module")

return
}

for _, r := range req.State.Values.RootModule.Resources {
if e.resourceAddress == r.Address {
resource = r

break
}
}

if resource == nil {
resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress)

return
}

if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 {
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)

return
}

if len(resource.IdentityValues) != len(e.identity) {
deltaMsg := ""
if len(resource.IdentityValues) > len(e.identity) {
deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ")
} else {
deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ")
}

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)
return
}

var keys []string

for k := range e.identity {
keys = append(keys, k)
}

sort.SliceStable(keys, func(i, j int) bool {
return keys[i] < keys[j]
})

for _, k := range keys {
actualIdentityVal, ok := resource.IdentityValues[k]

if !ok {
resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k)
return
}

if err := e.identity[k].CheckValue(actualIdentityVal); err != nil {
resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err)
return
}
}
}

// ExpectIdentity returns a state check that asserts that the identity at the given resource matches a known object, where each
// map key represents an identity attribute name. The identity in state must exactly match the given object and any missing/extra
// attributes will raise a diagnostic.
//
// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+
func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) StateCheck {
return expectIdentity{
resourceAddress: resourceAddress,
identity: identity,
}
}

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

deltaMap := make(map[string]T, len(mapA))
maps.Copy(deltaMap, mapA)
for key := range mapB {
delete(deltaMap, key)
}

deltaKeys := slices.Sorted(maps.Keys(deltaMap))

for i, k := range deltaKeys {
if i == 0 {
deltaMsg += msgPrefix
} else if i != 0 {
deltaMsg += ", "
}
deltaMsg += fmt.Sprintf("%q", k)
}

return deltaMsg
}
41 changes: 41 additions & 0 deletions statecheck/expect_identity_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package statecheck_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func ExampleExpectIdentity() {
// A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`.
t := &testing.T{}
t.Parallel()

resource.Test(t, resource.TestCase{
// Resource identity support is only available in Terraform v1.12+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_12_0),
},
// Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes
Steps: []resource.TestStep{
{
Config: `resource "test_resource" "one" {}`,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectIdentity(
"test_resource.one",
map[string]knownvalue.Check{
"id": knownvalue.StringExact("id-123"),
"name": knownvalue.StringExact("John Doe"),
},
),
},
},
},
})
}
Loading
Loading