Output resource identities in State JSON (TF-23753) (#36615)

* jsonstate: Marshal identity values

* jsonstate: Test identity marshalling

* Add identity to prepareStateV4

* Check identity schema version when marshaling state

* Marshal identity for deposed resources

* Marshal identity version if `0`

* Check for missing resource identity schema
pull/36706/head
Daniel Banck 1 year ago committed by GitHub
parent 53172a5f8a
commit 1210d68836
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -110,12 +110,26 @@ type Resource struct {
// Deposed is set if the resource is deposed in terraform state.
DeposedKey string `json:"deposed_key,omitempty"`
// The version of the resource identity schema the "identity" property
// conforms to.
// It's a pointer, because it should be optional, but also 0 is a valid
// schema version.
IdentitySchemaVersion *uint64 `json:"identity_schema_version,omitempty"`
// The JSON representation of the resource identity, whose structure
// depends on the resource identity schema.
IdentityValues IdentityValues `json:"identity,omitempty"`
}
// AttributeValues is the JSON representation of the attribute values of the
// resource, whose structure depends on the resource type schema.
type AttributeValues map[string]json.RawMessage
// IdentityValues is the JSON representation of the identity values of the
// resource, whose structure depends on the resource identity schema.
type IdentityValues map[string]json.RawMessage
func marshalAttributeValues(value cty.Value) (unmarkedVal cty.Value, marshalledVals AttributeValues, sensitivePaths []cty.Path, err error) {
// unmark our value to show all values
value, sensitivePaths, err = unmarkValueForMarshaling(value)
@ -138,6 +152,21 @@ func marshalAttributeValues(value cty.Value) (unmarkedVal cty.Value, marshalledV
return value, ret, sensitivePaths, nil
}
func marshalIdentityValues(value cty.Value) (IdentityValues, error) {
if value == cty.NilVal || value.IsNull() {
return nil, nil
}
ret := make(IdentityValues)
it := value.ElementIterator()
for it.Next() {
k, v := it.Element()
vJSON, _ := ctyjson.Marshal(v, v.Type())
ret[k.AsString()] = json.RawMessage(vJSON)
}
return ret, nil
}
// newState() returns a minimally-initialized state
func newState() *state {
return &state{
@ -396,6 +425,20 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
if schema.Body == nil {
return nil, fmt.Errorf("no schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider)
}
// Check if we have an identity in the state
if ri.Current.IdentityJSON != nil {
if schema.IdentityVersion != int64(ri.Current.IdentitySchemaVersion) {
return nil, fmt.Errorf("resource identity schema version %d for %s in state does not match version %d from the provider", ri.Current.IdentitySchemaVersion, resAddr, schema.IdentityVersion)
}
if schema.Identity == nil {
return nil, fmt.Errorf("no resource identity schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider)
}
current.IdentitySchemaVersion = &ri.Current.IdentitySchemaVersion
}
riObj, err := ri.Current.Decode(schema)
if err != nil {
return nil, err
@ -415,6 +458,11 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
}
current.SensitiveValues = v
current.IdentityValues, err = marshalIdentityValues(riObj.Identity)
if err != nil {
return nil, fmt.Errorf("preparing identity values for %s: %w", current.Address, err)
}
if len(riObj.Dependencies) > 0 {
dependencies := make([]string, len(riObj.Dependencies))
for i, v := range riObj.Dependencies {
@ -467,6 +515,11 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
}
deposed.SensitiveValues = v
deposed.IdentityValues, err = marshalIdentityValues(riObj.Identity)
if err != nil {
return nil, fmt.Errorf("preparing identity values for %s: %w", current.Address, err)
}
if len(riObj.Dependencies) > 0 {
dependencies := make([]string, len(riObj.Dependencies))
for i, v := range riObj.Dependencies {

@ -20,6 +20,10 @@ import (
"github.com/hashicorp/terraform/internal/terraform"
)
func ptrOf[T any](v T) *T {
return &v
}
func TestMarshalOutputs(t *testing.T) {
tests := []struct {
Outputs map[string]*states.OutputValue
@ -620,6 +624,115 @@ func TestMarshalResources(t *testing.T) {
},
false,
},
"single resource with identity": {
map[string]*states.Resource{
"test_identity.bar": {
Addr: addrs.AbsResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_identity",
Name: "bar",
},
},
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
addrs.NoKey: {
Current: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles","name":"bar"}`),
IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`),
},
},
},
ProviderConfig: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
},
},
testSchemas(),
[]Resource{
{
Address: "test_identity.bar",
Mode: "managed",
Type: "test_identity",
Name: "bar",
Index: nil,
ProviderName: "registry.terraform.io/hashicorp/test",
AttributeValues: AttributeValues{
"name": json.RawMessage(`"bar"`),
"foozles": json.RawMessage(`"sensuzles"`),
"woozles": json.RawMessage(`"confuzles"`),
},
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
IdentityValues: IdentityValues{
"name": json.RawMessage(`"bar"`),
"foozles": json.RawMessage(`"sensuzles"`),
},
IdentitySchemaVersion: ptrOf[uint64](0),
},
},
false,
},
"single resource wrong identity schema": {
map[string]*states.Resource{
"test_identity.bar": {
Addr: addrs.AbsResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_identity",
Name: "bar",
},
},
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
addrs.NoKey: {
Current: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles","name":"bar"}`),
IdentitySchemaVersion: 1,
IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`),
},
},
},
ProviderConfig: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
},
},
testSchemas(),
nil,
true,
},
"single resource missing identity schema": {
map[string]*states.Resource{
"test_thing.bar": {
Addr: addrs.AbsResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "bar",
},
},
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
addrs.NoKey: {
Current: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles"}`),
IdentitySchemaVersion: 0,
IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`),
},
},
},
ProviderConfig: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
},
},
testSchemas(),
nil,
true,
},
}
for name, test := range tests {
@ -867,6 +980,22 @@ func testSchemas() *terraform.Schemas {
},
},
},
"test_identity": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Required: true},
"woozles": {Type: cty.String, Optional: true, Computed: true},
"foozles": {Type: cty.String, Optional: true, Sensitive: true},
},
},
Identity: &configschema.Object{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Required: true},
"foozles": {Type: cty.String, Optional: true},
},
Nesting: configschema.NestingSingle,
},
},
},
},
},

@ -136,8 +136,9 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
instAddr := rAddr.Instance(key)
obj := &states.ResourceInstanceObjectSrc{
SchemaVersion: isV4.SchemaVersion,
CreateBeforeDestroy: isV4.CreateBeforeDestroy,
SchemaVersion: isV4.SchemaVersion,
CreateBeforeDestroy: isV4.CreateBeforeDestroy,
IdentitySchemaVersion: isV4.IdentitySchemaVersion,
}
{
@ -156,6 +157,10 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) {
}
}
if isV4.IdentityRaw != nil {
obj.IdentityJSON = isV4.IdentityRaw
}
// Sensitive paths
if isV4.AttributeSensitivePaths != nil {
paths, pathsDiags := unmarshalPaths([]byte(isV4.AttributeSensitivePaths))

Loading…
Cancel
Save