diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index fc2e0be61691..7ccd79fb8284 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -643,6 +643,12 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, } } +// driftedResources is a best-effort attempt to compare the current and prior +// state. If we cannot decode the prior state for some reason, this should only +// return warnings to help the user correlate any missing resources in the +// report. This is known to happen when targeting a subset of resources, +// because the excluded instances will have been removed from the plan and +// not upgraded. func (c *Context) driftedResources(config *configs.Config, oldState, newState *states.State, moves refactoring.MoveResults) ([]*plans.ResourceInstanceChangeSrc, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -690,35 +696,35 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s addr.Resource.Resource.Type, ) if schema == nil { - // This should never happen, but just in case - return nil, diags.Append(tfdiags.Sourceless( - tfdiags.Error, + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, "Missing resource schema from provider", - fmt.Sprintf("No resource schema found for %s.", addr.Resource.Resource.Type), + fmt.Sprintf("No resource schema found for %s when decoding prior state", addr.Resource.Resource.Type), )) + continue } ty := schema.ImpliedType() oldObj, err := oldIS.Current.Decode(ty) if err != nil { - // This should also never happen - return nil, diags.Append(tfdiags.Sourceless( - tfdiags.Error, + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, "Failed to decode resource from state", - fmt.Sprintf("Error decoding %q from previous state: %s", addr.String(), err), + fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err), )) + continue } var newObj *states.ResourceInstanceObject if newIS != nil && newIS.Current != nil { newObj, err = newIS.Current.Decode(ty) if err != nil { - // This should also never happen - return nil, diags.Append(tfdiags.Sourceless( - tfdiags.Error, + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, "Failed to decode resource from state", fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err), )) + continue } } diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 634144dee048..65f743a39c06 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -1705,6 +1705,60 @@ The -target option is not for routine use, and is provided only for exceptional }) } +func TestContext2Plan_untargetedResourceSchemaChange(t *testing.T) { + // an untargeted resource which requires a schema migration should not + // block planning due external changes in the plan. + addrA := mustResourceInstanceAddr("test_object.a") + addrB := mustResourceInstanceAddr("test_object.b") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { +} +resource "test_object" "b" { +}`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ + // old_list is no longer in the schema + AttrsJSON: []byte(`{"old_list":["used to be","a list here"]}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + + // external changes trigger a "drift report", but because test_object.b was + // not targeted, the state was not fixed to match the schema and cannot be + // deocded for the report. + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + obj := req.PriorState.AsValueMap() + // test_number changed externally + obj["test_number"] = cty.NumberIntVal(1) + resp.NewState = cty.ObjectVal(obj) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + Targets: []addrs.Targetable{ + addrA, + }, + }) + // + assertNoErrors(t, diags) +} + func TestContext2Plan_movedResourceRefreshOnly(t *testing.T) { addrA := mustResourceInstanceAddr("test_object.a") addrB := mustResourceInstanceAddr("test_object.b")