preserve resource private data on error

If the provider returns an empty response during apply, we restore the
prior state for the resource, but the private data was not being
restored.
This commit is contained in:
James Bardin 2021-01-08 09:56:03 -05:00
parent 7e880299c0
commit e0d3b97c8e
2 changed files with 51 additions and 16 deletions

View File

@ -12468,3 +12468,45 @@ func TestContext2Apply_dataSensitive(t *testing.T) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks)
} }
} }
func TestContext2Apply_errorRestorePrivateData(t *testing.T) {
// empty config to remove our resource
m := testModuleInline(t, map[string]string{
"main.tf": "",
})
p := simpleMockProvider()
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{
// we error during apply, which will trigger core to preserve the last
// known state, including private data
Diagnostics: tfdiags.Diagnostics(nil).Append(errors.New("oops")),
}
addr := mustResourceInstanceAddr("test_object.a")
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Private: []byte("private"),
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
})
ctx := testContext2(t, &ContextOpts{
Config: m,
State: state,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan()
if diags.HasErrors() {
t.Fatal(diags.Err())
}
state, _ = ctx.Apply()
if string(state.ResourceInstance(addr).Current.Private) != "private" {
t.Fatal("missing private data in state")
}
}

View File

@ -1821,16 +1821,16 @@ func (n *NodeAbstractResourceInstance) apply(
if state == nil { if state == nil {
state = &states.ResourceInstanceObject{} state = &states.ResourceInstanceObject{}
} }
var newState *states.ResourceInstanceObject
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
if err != nil { if err != nil {
return newState, diags.Append(err), applyError return nil, diags.Append(err), applyError
} }
schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type) schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type)
if schema == nil { if schema == nil {
// Should be caught during validation, so we don't bother with a pretty error here // Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type)) diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type))
return newState, diags, applyError return nil, diags, applyError
} }
log.Printf("[INFO] Starting apply for %s", n.Addr) log.Printf("[INFO] Starting apply for %s", n.Addr)
@ -1843,7 +1843,7 @@ func (n *NodeAbstractResourceInstance) apply(
configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData) configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData)
diags = diags.Append(configDiags) diags = diags.Append(configDiags)
if configDiags.HasErrors() { if configDiags.HasErrors() {
return newState, diags, applyError return nil, diags, applyError
} }
} }
@ -1852,13 +1852,13 @@ func (n *NodeAbstractResourceInstance) apply(
"configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)", "configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)",
n.Addr, n.Addr,
)) ))
return newState, diags, applyError return nil, diags, applyError
} }
metaConfigVal, metaDiags := n.providerMetas(ctx) metaConfigVal, metaDiags := n.providerMetas(ctx)
diags = diags.Append(metaDiags) diags = diags.Append(metaDiags)
if diags.HasErrors() { if diags.HasErrors() {
return newState, diags, applyError return nil, diags, applyError
} }
log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr, change.Action) log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr, change.Action)
@ -1870,6 +1870,7 @@ func (n *NodeAbstractResourceInstance) apply(
unmarkedBefore, beforePaths := change.Before.UnmarkDeepWithPaths() unmarkedBefore, beforePaths := change.Before.UnmarkDeepWithPaths()
unmarkedAfter, afterPaths := change.After.UnmarkDeepWithPaths() unmarkedAfter, afterPaths := change.After.UnmarkDeepWithPaths()
var newState *states.ResourceInstanceObject
// If we have an Update action, our before and after values are equal, // If we have an Update action, our before and after values are equal,
// and only differ on their sensitivity, the newVal is the after val // and only differ on their sensitivity, the newVal is the after val
// and we should not communicate with the provider. We do need to update // and we should not communicate with the provider. We do need to update
@ -2071,8 +2072,6 @@ func (n *NodeAbstractResourceInstance) apply(
} }
} }
newStatus := states.ObjectReady
// Sometimes providers return a null value when an operation fails for some // Sometimes providers return a null value when an operation fails for some
// reason, but we'd rather keep the prior state so that the error can be // reason, but we'd rather keep the prior state so that the error can be
// corrected on a subsequent run. We must only do this for null new value // corrected on a subsequent run. We must only do this for null new value
@ -2084,18 +2083,12 @@ func (n *NodeAbstractResourceInstance) apply(
// deleted then our next refresh will detect that and fix it up. // deleted then our next refresh will detect that and fix it up.
// If change.Action is Create then change.Before will also be null, // If change.Action is Create then change.Before will also be null,
// which is fine. // which is fine.
newVal = change.Before newState = state.DeepCopy()
// If we're recovering the previous state, we also want to restore the
// the tainted status of the object.
if state.Status == states.ObjectTainted {
newStatus = states.ObjectTainted
}
} }
if !newVal.IsNull() { // null value indicates that the object is deleted, so we won't set a new state in that case if !newVal.IsNull() { // null value indicates that the object is deleted, so we won't set a new state in that case
newState = &states.ResourceInstanceObject{ newState = &states.ResourceInstanceObject{
Status: newStatus, Status: states.ObjectReady,
Value: newVal, Value: newVal,
Private: resp.Private, Private: resp.Private,
CreateBeforeDestroy: createBeforeDestroy, CreateBeforeDestroy: createBeforeDestroy,