Skip to content

Commit

Permalink
save identity into state
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielMSchmidt committed Feb 11, 2025
1 parent a1d4c55 commit dc9bd7a
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 12 deletions.
4 changes: 4 additions & 0 deletions internal/providers/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ type ReadResourceResponse struct {
// Deferred if present signals that the provider was not able to fully
// complete this operation and a susequent run is required.
Deferred *Deferred

// Identity is the object-typed value representing the identity of the remote
// object within Terraform.
Identity cty.Value
}

type PlanResourceChangeRequest struct {
Expand Down
5 changes: 5 additions & 0 deletions internal/providers/testing/provider_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ func (p *MockProvider) GetResourceIdentitySchemas() providers.GetResourceIdentit
p.Lock()
defer p.Unlock()
p.GetResourceIdentitySchemasCalled = true

if p.GetResourceIdentitySchemasResponse != nil {
return *p.GetResourceIdentitySchemasResponse
}

return providers.GetResourceIdentitySchemasResponse{
IdentityTypes: map[string]providers.IdentitySchema{},
}
Expand Down
28 changes: 28 additions & 0 deletions internal/states/instance_object_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package states

import (
"fmt"

"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"

Expand Down Expand Up @@ -72,6 +74,32 @@ type ResourceInstanceObjectSrc struct {
decodeValueCache cty.Value
}

// DecodeWithIdentity unmarshals the raw representation of the object attributes
// and identity schema.
func (os *ResourceInstanceObjectSrc) DecodeWithIdentity(ty cty.Type, identityTy cty.Type, identitySchemaVersion uint64) (*ResourceInstanceObject, error) {
// TODO: On call-side this should lead to an upgrade call (plus we need the old schema as well)
// We might have no identity data at all
if len(os.IdentitySchemaJSON) == 0 {
return os.Decode(ty) // Task failed successfully
}

if os.IdentitySchemaVersion != identitySchemaVersion {
return nil, fmt.Errorf("identity schema version mismatch: got %d, want %d", os.IdentitySchemaVersion, identitySchemaVersion)
}

rio, err := os.Decode(ty)
if err != nil {
return nil, fmt.Errorf("failed to decode object schema: %e", err)
}

rio.Identity, err = ctyjson.Unmarshal(os.IdentitySchemaJSON, identityTy)
if err != nil {
return nil, fmt.Errorf("failed to decode identity schema: %e", err)
}

return rio, nil
}

// Decode unmarshals the raw representation of the object attributes. Pass the
// implied type of the corresponding resource type schema for correct operation.
//
Expand Down
4 changes: 2 additions & 2 deletions internal/states/instance_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestResourceInstanceObject_encode(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
rios, err := obj.Encode(value.Type(), 0, cty.EmptyObject, 0) // TODO
rios, err := obj.Encode(value.Type(), 0)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
Expand Down Expand Up @@ -98,7 +98,7 @@ func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) {
Value: value,
Status: ObjectReady,
}
_, err := obj.Encode(value.Type(), 0, cty.EmptyObject, 0)
_, err := obj.Encode(value.Type(), 0)
if err == nil {
t.Fatalf("unexpected success; want error")
}
Expand Down
26 changes: 17 additions & 9 deletions internal/states/state_deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc {
copy(attrsJSON, os.AttrsJSON)
}

var identityJSON []byte
if os.IdentitySchemaJSON != nil {
identityJSON = make([]byte, len(os.IdentitySchemaJSON))
copy(identityJSON, os.IdentitySchemaJSON)
}

var sensitiveAttrPaths []cty.Path
if os.AttrSensitivePaths != nil {
sensitiveAttrPaths = make([]cty.Path, len(os.AttrSensitivePaths))
Expand All @@ -163,15 +169,17 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc {
}

return &ResourceInstanceObjectSrc{
Status: os.Status,
SchemaVersion: os.SchemaVersion,
Private: private,
AttrsFlat: attrsFlat,
AttrsJSON: attrsJSON,
AttrSensitivePaths: sensitiveAttrPaths,
Dependencies: dependencies,
CreateBeforeDestroy: os.CreateBeforeDestroy,
decodeValueCache: os.decodeValueCache,
Status: os.Status,
SchemaVersion: os.SchemaVersion,
Private: private,
AttrsFlat: attrsFlat,
AttrsJSON: attrsJSON,
AttrSensitivePaths: sensitiveAttrPaths,
Dependencies: dependencies,
CreateBeforeDestroy: os.CreateBeforeDestroy,
decodeValueCache: os.decodeValueCache,
IdentitySchemaJSON: identityJSON,
IdentitySchemaVersion: os.IdentitySchemaVersion,
}
}

Expand Down
162 changes: 162 additions & 0 deletions internal/terraform/context_refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1690,3 +1690,165 @@ resource "test_resource" "foo" {
t.Fatalf("invalid state\nexpected: %s\ngot: %s\n", expected, jsonState)
}
}

// TODO: Expectation for resource identity in request
// TODO: Move to plan tests
// TODO: Double check if we need specific refresh tests as well
func TestContext2Refresh_resource_identity_adds_missing(t *testing.T) {

for name, tc := range map[string]struct {
StoredIdentitySchemaVersion uint64
StoredIdentityJSON []byte
IdentitySchema providers.IdentitySchema
IdentityType cty.Type
IdentityData cty.Value
ExpectedIdentity cty.Value
ExpectedError error
}{
"no previous identity": {
IdentitySchema: providers.IdentitySchema{
Version: 0,
Attributes: configschema.IdentityAttributes{
"id": {
Type: cty.String,
RequiredForImport: true,
},
},
},
IdentityType: cty.Object(map[string]cty.Type{
"id": cty.String,
}),
IdentityData: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
}),
ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
}),
},
// "identity version mismatch": {
// StoredIdentitySchemaVersion: 1,
// StoredIdentityJSON: []byte(`{"id": "foo"}`),
// IdentitySchema: providers.IdentitySchema{
// Version: 0,
// Attributes: configschema.IdentityAttributes{
// "id": {
// Type: cty.String,
// RequiredForImport: true,
// },
// },
// },
// IdentityType: cty.Object(map[string]cty.Type{
// "id": cty.String,
// }),
// IdentityData: cty.ObjectVal(map[string]cty.Value{
// "id": cty.StringVal("foo"),
// }),
// ExpectedError: fmt.Errorf("identity schema version mismatch: stored 1, expected 0"),
// },
// "identity type mismatch": {
// StoredIdentitySchemaVersion: 0,
// StoredIdentityJSON: []byte(`{"arn": "foo"}`),
// IdentitySchema: providers.IdentitySchema{
// Version: 0,
// Attributes: configschema.IdentityAttributes{
// "id": {
// Type: cty.String,
// RequiredForImport: true,
// },
// },
// },
// IdentityType: cty.Object(map[string]cty.Type{
// "id": cty.String,
// }),
// IdentityData: cty.ObjectVal(map[string]cty.Value{
// "id": cty.StringVal("foo"),
// }),
// ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{
// "id": cty.StringVal("foo"),
// }),
// ExpectedError: fmt.Errorf("identity schema mismatch, could not decode"),
// },
// "identity upgrade": {},
// "identity recorded, no identity sent": {},
} {
t.Run(name, func(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "refresh-basic")

state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)

root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.web").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`),
IdentitySchemaVersion: tc.StoredIdentitySchemaVersion,
IdentitySchemaJSON: tc.StoredIdentityJSON,
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)

ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})

schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
readState, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"id": "foo", "foo": "baz"}, ty)
if err != nil {
t.Fatal(err)
}

p.GetResourceIdentitySchemasResponse = &providers.GetResourceIdentitySchemasResponse{
IdentityTypes: map[string]providers.IdentitySchema{
"aws_instance": tc.IdentitySchema,
},
}
p.ReadResourceResponse = &providers.ReadResourceResponse{
NewState: readState,
Identity: tc.IdentityData,
}

s, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.RefreshOnlyMode})
if diags.HasErrors() {
t.Fatal(diags.Err())
}

if !p.ReadResourceCalled {
t.Fatal("ReadResource should be called")
}

if !p.GetResourceIdentitySchemasCalled {
t.Fatal("GetResourceIdentitySchemas should be called")
}

mod := s.PriorState.RootModule()
fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.DecodeWithIdentity(ty, tc.IdentityType, uint64(tc.IdentitySchema.Version))
if err != nil {
t.Fatal(err)
}

if tc.ExpectedError != nil {
if err != tc.ExpectedError {
t.Fatalf("unexpected error\nwant: %v\ngot: %v", tc.ExpectedError, err)
}
} else {
newState, err := schema.CoerceValue(fromState.Value)
if err != nil {
t.Fatal(err)
}

if !cmp.Equal(readState, newState, valueComparer) {
t.Fatal(cmp.Diff(readState, newState, valueComparer, equateEmpty))
}

if tc.ExpectedIdentity.Equals(fromState.Identity).False() {
t.Fatalf("wrong identity\nwant: %s\ngot: %s", tc.ExpectedIdentity.GoString(), fromState.Identity.GoString())
}
}
})
}
}
4 changes: 3 additions & 1 deletion internal/terraform/node_resource_abstract_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func (n *NodeAbstractResourceInstance) writeResourceInstanceStateImpl(ctx EvalCo
marshal := func() (*states.ResourceInstanceObjectSrc, error) {
return obj.EncodeWithIdentity(objectSchema.ImpliedType(), currentObjectVersion, identitySchema.ImpliedType(), currentIdentityVersion)
}
if identitySchema == nil {
if identitySchema == nil || obj.Identity == cty.NilVal {
marshal = func() (*states.ResourceInstanceObjectSrc, error) {
return obj.Encode(objectSchema.ImpliedType(), currentObjectVersion)
}
Expand Down Expand Up @@ -652,6 +652,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state
Private: state.Private,
ProviderMeta: metaConfigVal,
ClientCapabilities: ctx.ClientCapabilities(),
// TODO: Send identity in read request
})

// If we don't support deferrals, but the provider reports a deferral and does not
Expand Down Expand Up @@ -736,6 +737,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state
ret := state.DeepCopy()
ret.Value = newState
ret.Private = resp.Private
ret.Identity = resp.Identity

// Call post-refresh hook
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
Expand Down

0 comments on commit dc9bd7a

Please sign in to comment.