github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/terraform/context_plan2_test.go (about)

     1  package terraform
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/davecgh/go-spew/spew"
    10  	"github.com/hashicorp/terraform/internal/addrs"
    11  	"github.com/hashicorp/terraform/internal/configs/configschema"
    12  	"github.com/hashicorp/terraform/internal/lang/marks"
    13  	"github.com/hashicorp/terraform/internal/plans"
    14  	"github.com/hashicorp/terraform/internal/providers"
    15  	"github.com/hashicorp/terraform/internal/states"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  func TestContext2Plan_removedDuringRefresh(t *testing.T) {
    20  	// This tests the situation where an object tracked in the previous run
    21  	// state has been deleted outside of Terraform, which we should detect
    22  	// during the refresh step and thus ultimately produce a plan to recreate
    23  	// the object, since it's still present in the configuration.
    24  	m := testModuleInline(t, map[string]string{
    25  		"main.tf": `
    26  resource "test_object" "a" {
    27  }
    28  `,
    29  	})
    30  
    31  	p := simpleMockProvider()
    32  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
    33  		Provider: providers.Schema{Block: simpleTestSchema()},
    34  		ResourceTypes: map[string]providers.Schema{
    35  			"test_object": {
    36  				Block: &configschema.Block{
    37  					Attributes: map[string]*configschema.Attribute{
    38  						"arg": {Type: cty.String, Optional: true},
    39  					},
    40  				},
    41  			},
    42  		},
    43  	}
    44  	p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
    45  		resp.NewState = cty.NullVal(req.PriorState.Type())
    46  		return resp
    47  	}
    48  	p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
    49  		// We should've been given the prior state JSON as our input to upgrade.
    50  		if !bytes.Contains(req.RawStateJSON, []byte("previous_run")) {
    51  			t.Fatalf("UpgradeResourceState request doesn't contain the previous run object\n%s", req.RawStateJSON)
    52  		}
    53  
    54  		// We'll put something different in "arg" as part of upgrading, just
    55  		// so that we can verify below that PrevRunState contains the upgraded
    56  		// (but NOT refreshed) version of the object.
    57  		resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{
    58  			"arg": cty.StringVal("upgraded"),
    59  		})
    60  		return resp
    61  	}
    62  
    63  	addr := mustResourceInstanceAddr("test_object.a")
    64  	state := states.BuildState(func(s *states.SyncState) {
    65  		s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
    66  			AttrsJSON: []byte(`{"arg":"previous_run"}`),
    67  			Status:    states.ObjectTainted,
    68  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
    69  	})
    70  
    71  	ctx := testContext2(t, &ContextOpts{
    72  		Providers: map[addrs.Provider]providers.Factory{
    73  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
    74  		},
    75  	})
    76  
    77  	plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
    78  	assertNoErrors(t, diags)
    79  
    80  	if !p.UpgradeResourceStateCalled {
    81  		t.Errorf("Provider's UpgradeResourceState wasn't called; should've been")
    82  	}
    83  	if !p.ReadResourceCalled {
    84  		t.Errorf("Provider's ReadResource wasn't called; should've been")
    85  	}
    86  
    87  	// The object should be absent from the plan's prior state, because that
    88  	// records the result of refreshing.
    89  	if got := plan.PriorState.ResourceInstance(addr); got != nil {
    90  		t.Errorf(
    91  			"instance %s is in the prior state after planning; should've been removed\n%s",
    92  			addr, spew.Sdump(got),
    93  		)
    94  	}
    95  
    96  	// However, the object should still be in the PrevRunState, because
    97  	// that reflects what we believed to exist before refreshing.
    98  	if got := plan.PrevRunState.ResourceInstance(addr); got == nil {
    99  		t.Errorf(
   100  			"instance %s is missing from the previous run state after planning; should've been preserved",
   101  			addr,
   102  		)
   103  	} else {
   104  		if !bytes.Contains(got.Current.AttrsJSON, []byte("upgraded")) {
   105  			t.Fatalf("previous run state has non-upgraded object\n%s", got.Current.AttrsJSON)
   106  		}
   107  	}
   108  
   109  	// Because the configuration still mentions test_object.a, we should've
   110  	// planned to recreate it in order to fix the drift.
   111  	for _, c := range plan.Changes.Resources {
   112  		if c.Action != plans.Create {
   113  			t.Fatalf("expected Create action for missing %s, got %s", c.Addr, c.Action)
   114  		}
   115  	}
   116  }
   117  
   118  func TestContext2Plan_noChangeDataSourceSensitiveNestedSet(t *testing.T) {
   119  	m := testModuleInline(t, map[string]string{
   120  		"main.tf": `
   121  variable "bar" {
   122    sensitive = true
   123    default   = "baz"
   124  }
   125  
   126  data "test_data_source" "foo" {
   127    foo {
   128      bar = var.bar
   129    }
   130  }
   131  `,
   132  	})
   133  
   134  	p := new(MockProvider)
   135  	p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
   136  		DataSources: map[string]*configschema.Block{
   137  			"test_data_source": {
   138  				Attributes: map[string]*configschema.Attribute{
   139  					"id": {
   140  						Type:     cty.String,
   141  						Computed: true,
   142  					},
   143  				},
   144  				BlockTypes: map[string]*configschema.NestedBlock{
   145  					"foo": {
   146  						Block: configschema.Block{
   147  							Attributes: map[string]*configschema.Attribute{
   148  								"bar": {Type: cty.String, Optional: true},
   149  							},
   150  						},
   151  						Nesting: configschema.NestingSet,
   152  					},
   153  				},
   154  			},
   155  		},
   156  	})
   157  
   158  	p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
   159  		State: cty.ObjectVal(map[string]cty.Value{
   160  			"id":  cty.StringVal("data_id"),
   161  			"foo": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")})}),
   162  		}),
   163  	}
   164  
   165  	state := states.NewState()
   166  	root := state.EnsureModule(addrs.RootModuleInstance)
   167  	root.SetResourceInstanceCurrent(
   168  		mustResourceInstanceAddr("data.test_data_source.foo").Resource,
   169  		&states.ResourceInstanceObjectSrc{
   170  			Status:    states.ObjectReady,
   171  			AttrsJSON: []byte(`{"id":"data_id", "foo":[{"bar":"baz"}]}`),
   172  			AttrSensitivePaths: []cty.PathValueMarks{
   173  				{
   174  					Path:  cty.GetAttrPath("foo"),
   175  					Marks: cty.NewValueMarks(marks.Sensitive),
   176  				},
   177  			},
   178  		},
   179  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   180  	)
   181  
   182  	ctx := testContext2(t, &ContextOpts{
   183  		Providers: map[addrs.Provider]providers.Factory{
   184  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   185  		},
   186  	})
   187  
   188  	plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
   189  	assertNoErrors(t, diags)
   190  
   191  	for _, res := range plan.Changes.Resources {
   192  		if res.Action != plans.NoOp {
   193  			t.Fatalf("expected NoOp, got: %q %s", res.Addr, res.Action)
   194  		}
   195  	}
   196  }
   197  
   198  func TestContext2Plan_orphanDataInstance(t *testing.T) {
   199  	// ensure the planned replacement of the data source is evaluated properly
   200  	m := testModuleInline(t, map[string]string{
   201  		"main.tf": `
   202  data "test_object" "a" {
   203    for_each = { new = "ok" }
   204  }
   205  
   206  output "out" {
   207    value = [ for k, _ in data.test_object.a: k ]
   208  }
   209  `,
   210  	})
   211  
   212  	p := simpleMockProvider()
   213  	p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) {
   214  		resp.State = req.Config
   215  		return resp
   216  	}
   217  
   218  	state := states.BuildState(func(s *states.SyncState) {
   219  		s.SetResourceInstanceCurrent(mustResourceInstanceAddr(`data.test_object.a["old"]`), &states.ResourceInstanceObjectSrc{
   220  			AttrsJSON: []byte(`{"test_string":"foo"}`),
   221  			Status:    states.ObjectReady,
   222  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   223  	})
   224  
   225  	ctx := testContext2(t, &ContextOpts{
   226  		Providers: map[addrs.Provider]providers.Factory{
   227  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   228  		},
   229  	})
   230  
   231  	plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
   232  	assertNoErrors(t, diags)
   233  
   234  	change, err := plan.Changes.Outputs[0].Decode()
   235  	if err != nil {
   236  		t.Fatal(err)
   237  	}
   238  
   239  	expected := cty.TupleVal([]cty.Value{cty.StringVal("new")})
   240  
   241  	if change.After.Equals(expected).False() {
   242  		t.Fatalf("expected %#v, got %#v\n", expected, change.After)
   243  	}
   244  }
   245  
   246  func TestContext2Plan_basicConfigurationAliases(t *testing.T) {
   247  	m := testModuleInline(t, map[string]string{
   248  		"main.tf": `
   249  provider "test" {
   250    alias = "z"
   251    test_string = "config"
   252  }
   253  
   254  module "mod" {
   255    source = "./mod"
   256    providers = {
   257      test.x = test.z
   258    }
   259  }
   260  `,
   261  
   262  		"mod/main.tf": `
   263  terraform {
   264    required_providers {
   265      test = {
   266        source = "registry.terraform.io/hashicorp/test"
   267        configuration_aliases = [ test.x ]
   268  	}
   269    }
   270  }
   271  
   272  resource "test_object" "a" {
   273    provider = test.x
   274  }
   275  
   276  `,
   277  	})
   278  
   279  	p := simpleMockProvider()
   280  
   281  	// The resource within the module should be using the provider configured
   282  	// from the root module. We should never see an empty configuration.
   283  	p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
   284  		if req.Config.GetAttr("test_string").IsNull() {
   285  			resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing test_string value"))
   286  		}
   287  		return resp
   288  	}
   289  
   290  	ctx := testContext2(t, &ContextOpts{
   291  		Providers: map[addrs.Provider]providers.Factory{
   292  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   293  		},
   294  	})
   295  
   296  	_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
   297  	assertNoErrors(t, diags)
   298  }
   299  
   300  func TestContext2Plan_dataReferencesResourceInModules(t *testing.T) {
   301  	p := testProvider("test")
   302  	p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) {
   303  		cfg := req.Config.AsValueMap()
   304  		cfg["id"] = cty.StringVal("d")
   305  		resp.State = cty.ObjectVal(cfg)
   306  		return resp
   307  	}
   308  
   309  	m := testModuleInline(t, map[string]string{
   310  		"main.tf": `
   311  locals {
   312    things = {
   313      old = "first"
   314      new = "second"
   315    }
   316  }
   317  
   318  module "mod" {
   319    source = "./mod"
   320    for_each = local.things
   321  }
   322  `,
   323  
   324  		"./mod/main.tf": `
   325  resource "test_resource" "a" {
   326  }
   327  
   328  data "test_data_source" "d" {
   329    depends_on = [test_resource.a]
   330  }
   331  
   332  resource "test_resource" "b" {
   333    value = data.test_data_source.d.id
   334  }
   335  `})
   336  
   337  	oldDataAddr := mustResourceInstanceAddr(`module.mod["old"].data.test_data_source.d`)
   338  
   339  	state := states.BuildState(func(s *states.SyncState) {
   340  		s.SetResourceInstanceCurrent(
   341  			mustResourceInstanceAddr(`module.mod["old"].test_resource.a`),
   342  			&states.ResourceInstanceObjectSrc{
   343  				AttrsJSON: []byte(`{"id":"a"}`),
   344  				Status:    states.ObjectReady,
   345  			}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   346  		)
   347  		s.SetResourceInstanceCurrent(
   348  			mustResourceInstanceAddr(`module.mod["old"].test_resource.b`),
   349  			&states.ResourceInstanceObjectSrc{
   350  				AttrsJSON: []byte(`{"id":"b","value":"d"}`),
   351  				Status:    states.ObjectReady,
   352  			}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   353  		)
   354  		s.SetResourceInstanceCurrent(
   355  			oldDataAddr,
   356  			&states.ResourceInstanceObjectSrc{
   357  				AttrsJSON: []byte(`{"id":"d"}`),
   358  				Status:    states.ObjectReady,
   359  			}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
   360  		)
   361  	})
   362  
   363  	ctx := testContext2(t, &ContextOpts{
   364  		Providers: map[addrs.Provider]providers.Factory{
   365  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   366  		},
   367  	})
   368  
   369  	plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
   370  	assertNoErrors(t, diags)
   371  
   372  	oldMod := oldDataAddr.Module
   373  
   374  	for _, c := range plan.Changes.Resources {
   375  		// there should be no changes from the old module instance
   376  		if c.Addr.Module.Equal(oldMod) && c.Action != plans.NoOp {
   377  			t.Errorf("unexpected change %s for %s\n", c.Action, c.Addr)
   378  		}
   379  	}
   380  }
   381  
   382  func TestContext2Plan_destroyWithRefresh(t *testing.T) {
   383  	m := testModuleInline(t, map[string]string{
   384  		"main.tf": `
   385  resource "test_object" "a" {
   386  }
   387  `,
   388  	})
   389  
   390  	p := simpleMockProvider()
   391  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   392  		Provider: providers.Schema{Block: simpleTestSchema()},
   393  		ResourceTypes: map[string]providers.Schema{
   394  			"test_object": {
   395  				Block: &configschema.Block{
   396  					Attributes: map[string]*configschema.Attribute{
   397  						"arg": {Type: cty.String, Optional: true},
   398  					},
   399  				},
   400  			},
   401  		},
   402  	}
   403  	p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
   404  		newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) {
   405  			if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) {
   406  				return cty.StringVal("current"), nil
   407  			}
   408  			return v, nil
   409  		})
   410  		if err != nil {
   411  			// shouldn't get here
   412  			t.Fatalf("ReadResourceFn transform failed")
   413  			return providers.ReadResourceResponse{}
   414  		}
   415  		return providers.ReadResourceResponse{
   416  			NewState: newVal,
   417  		}
   418  	}
   419  	p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
   420  		t.Logf("UpgradeResourceState %s", req.RawStateJSON)
   421  
   422  		// In the destroy-with-refresh codepath we end up calling
   423  		// UpgradeResourceState twice, because we do so once during refreshing
   424  		// (as part making a normal plan) and then again during the plan-destroy
   425  		// walk. The second call recieves the result of the earlier refresh,
   426  		// so we need to tolerate both "before" and "current" as possible
   427  		// inputs here.
   428  		if !bytes.Contains(req.RawStateJSON, []byte("before")) {
   429  			if !bytes.Contains(req.RawStateJSON, []byte("current")) {
   430  				t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object or the 'current' object\n%s", req.RawStateJSON)
   431  			}
   432  		}
   433  
   434  		// We'll put something different in "arg" as part of upgrading, just
   435  		// so that we can verify below that PrevRunState contains the upgraded
   436  		// (but NOT refreshed) version of the object.
   437  		resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{
   438  			"arg": cty.StringVal("upgraded"),
   439  		})
   440  		return resp
   441  	}
   442  
   443  	addr := mustResourceInstanceAddr("test_object.a")
   444  	state := states.BuildState(func(s *states.SyncState) {
   445  		s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
   446  			AttrsJSON: []byte(`{"arg":"before"}`),
   447  			Status:    states.ObjectReady,
   448  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   449  	})
   450  
   451  	ctx := testContext2(t, &ContextOpts{
   452  		Providers: map[addrs.Provider]providers.Factory{
   453  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   454  		},
   455  	})
   456  
   457  	plan, diags := ctx.Plan(m, state, &PlanOpts{
   458  		Mode:        plans.DestroyMode,
   459  		SkipRefresh: false, // the default
   460  	})
   461  	assertNoErrors(t, diags)
   462  
   463  	if !p.UpgradeResourceStateCalled {
   464  		t.Errorf("Provider's UpgradeResourceState wasn't called; should've been")
   465  	}
   466  	if !p.ReadResourceCalled {
   467  		t.Errorf("Provider's ReadResource wasn't called; should've been")
   468  	}
   469  
   470  	if plan.PriorState == nil {
   471  		t.Fatal("missing plan state")
   472  	}
   473  
   474  	for _, c := range plan.Changes.Resources {
   475  		if c.Action != plans.Delete {
   476  			t.Errorf("unexpected %s change for %s", c.Action, c.Addr)
   477  		}
   478  	}
   479  
   480  	if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil {
   481  		t.Errorf("%s has no previous run state at all after plan", addr)
   482  	} else {
   483  		if instState.Current == nil {
   484  			t.Errorf("%s has no current object in the previous run state", addr)
   485  		} else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) {
   486  			t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
   487  		}
   488  	}
   489  	if instState := plan.PriorState.ResourceInstance(addr); instState == nil {
   490  		t.Errorf("%s has no prior state at all after plan", addr)
   491  	} else {
   492  		if instState.Current == nil {
   493  			t.Errorf("%s has no current object in the prior state", addr)
   494  		} else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) {
   495  			t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
   496  		}
   497  	}
   498  }
   499  
   500  func TestContext2Plan_destroySkipRefresh(t *testing.T) {
   501  	m := testModuleInline(t, map[string]string{
   502  		"main.tf": `
   503  resource "test_object" "a" {
   504  }
   505  `,
   506  	})
   507  
   508  	p := simpleMockProvider()
   509  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   510  		Provider: providers.Schema{Block: simpleTestSchema()},
   511  		ResourceTypes: map[string]providers.Schema{
   512  			"test_object": {
   513  				Block: &configschema.Block{
   514  					Attributes: map[string]*configschema.Attribute{
   515  						"arg": {Type: cty.String, Optional: true},
   516  					},
   517  				},
   518  			},
   519  		},
   520  	}
   521  	p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
   522  		t.Helper()
   523  		t.Errorf("unexpected call to ReadResource")
   524  		resp.NewState = req.PriorState
   525  		return resp
   526  	}
   527  	p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
   528  		t.Logf("UpgradeResourceState %s", req.RawStateJSON)
   529  		// We should've been given the prior state JSON as our input to upgrade.
   530  		if !bytes.Contains(req.RawStateJSON, []byte("before")) {
   531  			t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON)
   532  		}
   533  
   534  		// We'll put something different in "arg" as part of upgrading, just
   535  		// so that we can verify below that PrevRunState contains the upgraded
   536  		// (but NOT refreshed) version of the object.
   537  		resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{
   538  			"arg": cty.StringVal("upgraded"),
   539  		})
   540  		return resp
   541  	}
   542  
   543  	addr := mustResourceInstanceAddr("test_object.a")
   544  	state := states.BuildState(func(s *states.SyncState) {
   545  		s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
   546  			AttrsJSON: []byte(`{"arg":"before"}`),
   547  			Status:    states.ObjectReady,
   548  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   549  	})
   550  
   551  	ctx := testContext2(t, &ContextOpts{
   552  		Providers: map[addrs.Provider]providers.Factory{
   553  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   554  		},
   555  	})
   556  
   557  	plan, diags := ctx.Plan(m, state, &PlanOpts{
   558  		Mode:        plans.DestroyMode,
   559  		SkipRefresh: true,
   560  	})
   561  	assertNoErrors(t, diags)
   562  
   563  	if !p.UpgradeResourceStateCalled {
   564  		t.Errorf("Provider's UpgradeResourceState wasn't called; should've been")
   565  	}
   566  	if p.ReadResourceCalled {
   567  		t.Errorf("Provider's ReadResource was called; shouldn't have been")
   568  	}
   569  
   570  	if plan.PriorState == nil {
   571  		t.Fatal("missing plan state")
   572  	}
   573  
   574  	for _, c := range plan.Changes.Resources {
   575  		if c.Action != plans.Delete {
   576  			t.Errorf("unexpected %s change for %s", c.Action, c.Addr)
   577  		}
   578  	}
   579  
   580  	if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil {
   581  		t.Errorf("%s has no previous run state at all after plan", addr)
   582  	} else {
   583  		if instState.Current == nil {
   584  			t.Errorf("%s has no current object in the previous run state", addr)
   585  		} else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) {
   586  			t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
   587  		}
   588  	}
   589  	if instState := plan.PriorState.ResourceInstance(addr); instState == nil {
   590  		t.Errorf("%s has no prior state at all after plan", addr)
   591  	} else {
   592  		if instState.Current == nil {
   593  			t.Errorf("%s has no current object in the prior state", addr)
   594  		} else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) {
   595  			// NOTE: The prior state should still have been _upgraded_, even
   596  			// though we skipped running refresh after upgrading it.
   597  			t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
   598  		}
   599  	}
   600  }
   601  
   602  func TestContext2Plan_unmarkingSensitiveAttributeForOutput(t *testing.T) {
   603  	m := testModuleInline(t, map[string]string{
   604  		"main.tf": `
   605  resource "test_resource" "foo" {
   606  }
   607  
   608  output "result" {
   609    value = nonsensitive(test_resource.foo.sensitive_attr)
   610  }
   611  `,
   612  	})
   613  
   614  	p := new(MockProvider)
   615  	p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
   616  		ResourceTypes: map[string]*configschema.Block{
   617  			"test_resource": {
   618  				Attributes: map[string]*configschema.Attribute{
   619  					"id": {
   620  						Type:     cty.String,
   621  						Computed: true,
   622  					},
   623  					"sensitive_attr": {
   624  						Type:      cty.String,
   625  						Computed:  true,
   626  						Sensitive: true,
   627  					},
   628  				},
   629  			},
   630  		},
   631  	})
   632  
   633  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   634  		return providers.PlanResourceChangeResponse{
   635  			PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{
   636  				"id":             cty.String,
   637  				"sensitive_attr": cty.String,
   638  			})),
   639  		}
   640  	}
   641  
   642  	state := states.NewState()
   643  
   644  	ctx := testContext2(t, &ContextOpts{
   645  		Providers: map[addrs.Provider]providers.Factory{
   646  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   647  		},
   648  	})
   649  
   650  	plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
   651  	assertNoErrors(t, diags)
   652  
   653  	for _, res := range plan.Changes.Resources {
   654  		if res.Action != plans.Create {
   655  			t.Fatalf("expected create, got: %q %s", res.Addr, res.Action)
   656  		}
   657  	}
   658  }
   659  
   660  func TestContext2Plan_destroyNoProviderConfig(t *testing.T) {
   661  	// providers do not need to be configured during a destroy plan
   662  	p := simpleMockProvider()
   663  	p.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
   664  		v := req.Config.GetAttr("test_string")
   665  		if v.IsNull() || !v.IsKnown() || v.AsString() != "ok" {
   666  			resp.Diagnostics = resp.Diagnostics.Append(errors.New("invalid provider configuration"))
   667  		}
   668  		return resp
   669  	}
   670  
   671  	m := testModuleInline(t, map[string]string{
   672  		"main.tf": `
   673  locals {
   674    value = "ok"
   675  }
   676  
   677  provider "test" {
   678    test_string = local.value
   679  }
   680  `,
   681  	})
   682  
   683  	addr := mustResourceInstanceAddr("test_object.a")
   684  	state := states.BuildState(func(s *states.SyncState) {
   685  		s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
   686  			AttrsJSON: []byte(`{"test_string":"foo"}`),
   687  			Status:    states.ObjectReady,
   688  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   689  	})
   690  
   691  	ctx := testContext2(t, &ContextOpts{
   692  		Providers: map[addrs.Provider]providers.Factory{
   693  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   694  		},
   695  	})
   696  
   697  	_, diags := ctx.Plan(m, state, &PlanOpts{
   698  		Mode: plans.DestroyMode,
   699  	})
   700  	assertNoErrors(t, diags)
   701  }
   702  
   703  func TestContext2Plan_movedResourceBasic(t *testing.T) {
   704  	addrA := mustResourceInstanceAddr("test_object.a")
   705  	addrB := mustResourceInstanceAddr("test_object.b")
   706  	m := testModuleInline(t, map[string]string{
   707  		"main.tf": `
   708  			resource "test_object" "b" {
   709  			}
   710  
   711  			moved {
   712  				from = test_object.a
   713  				to   = test_object.b
   714  			}
   715  
   716  			terraform {
   717  				experiments = [config_driven_move]
   718  			}
   719  		`,
   720  	})
   721  
   722  	state := states.BuildState(func(s *states.SyncState) {
   723  		// The prior state tracks test_object.a, which we should treat as
   724  		// test_object.b because of the "moved" block in the config.
   725  		s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
   726  			AttrsJSON: []byte(`{}`),
   727  			Status:    states.ObjectReady,
   728  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   729  	})
   730  
   731  	p := simpleMockProvider()
   732  	ctx := testContext2(t, &ContextOpts{
   733  		Providers: map[addrs.Provider]providers.Factory{
   734  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   735  		},
   736  	})
   737  
   738  	plan, diags := ctx.Plan(m, state, &PlanOpts{
   739  		Mode: plans.NormalMode,
   740  		ForceReplace: []addrs.AbsResourceInstance{
   741  			addrA,
   742  		},
   743  	})
   744  	if diags.HasErrors() {
   745  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
   746  	}
   747  
   748  	t.Run(addrA.String(), func(t *testing.T) {
   749  		instPlan := plan.Changes.ResourceInstance(addrA)
   750  		if instPlan != nil {
   751  			t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB)
   752  		}
   753  	})
   754  	t.Run(addrB.String(), func(t *testing.T) {
   755  		instPlan := plan.Changes.ResourceInstance(addrB)
   756  		if instPlan == nil {
   757  			t.Fatalf("no plan for %s at all", addrB)
   758  		}
   759  
   760  		if got, want := instPlan.Addr, addrB; !got.Equal(want) {
   761  			t.Errorf("wrong current address\ngot:  %s\nwant: %s", got, want)
   762  		}
   763  		if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) {
   764  			t.Errorf("wrong previous run address\ngot:  %s\nwant: %s", got, want)
   765  		}
   766  		if got, want := instPlan.Action, plans.NoOp; got != want {
   767  			t.Errorf("wrong planned action\ngot:  %s\nwant: %s", got, want)
   768  		}
   769  		if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
   770  			t.Errorf("wrong action reason\ngot:  %s\nwant: %s", got, want)
   771  		}
   772  	})
   773  }
   774  
   775  func TestContext2Plan_refreshOnlyMode(t *testing.T) {
   776  	addr := mustResourceInstanceAddr("test_object.a")
   777  
   778  	// The configuration, the prior state, and the refresh result intentionally
   779  	// have different values for "test_string" so we can observe that the
   780  	// refresh took effect but the configuration change wasn't considered.
   781  	m := testModuleInline(t, map[string]string{
   782  		"main.tf": `
   783  			resource "test_object" "a" {
   784  				arg = "after"
   785  			}
   786  
   787  			output "out" {
   788  				value = test_object.a.arg
   789  			}
   790  		`,
   791  	})
   792  	state := states.BuildState(func(s *states.SyncState) {
   793  		s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
   794  			AttrsJSON: []byte(`{"arg":"before"}`),
   795  			Status:    states.ObjectReady,
   796  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   797  	})
   798  
   799  	p := simpleMockProvider()
   800  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   801  		Provider: providers.Schema{Block: simpleTestSchema()},
   802  		ResourceTypes: map[string]providers.Schema{
   803  			"test_object": {
   804  				Block: &configschema.Block{
   805  					Attributes: map[string]*configschema.Attribute{
   806  						"arg": {Type: cty.String, Optional: true},
   807  					},
   808  				},
   809  			},
   810  		},
   811  	}
   812  	p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
   813  		newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) {
   814  			if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) {
   815  				return cty.StringVal("current"), nil
   816  			}
   817  			return v, nil
   818  		})
   819  		if err != nil {
   820  			// shouldn't get here
   821  			t.Fatalf("ReadResourceFn transform failed")
   822  			return providers.ReadResourceResponse{}
   823  		}
   824  		return providers.ReadResourceResponse{
   825  			NewState: newVal,
   826  		}
   827  	}
   828  	p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
   829  		// We should've been given the prior state JSON as our input to upgrade.
   830  		if !bytes.Contains(req.RawStateJSON, []byte("before")) {
   831  			t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON)
   832  		}
   833  
   834  		// We'll put something different in "arg" as part of upgrading, just
   835  		// so that we can verify below that PrevRunState contains the upgraded
   836  		// (but NOT refreshed) version of the object.
   837  		resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{
   838  			"arg": cty.StringVal("upgraded"),
   839  		})
   840  		return resp
   841  	}
   842  
   843  	ctx := testContext2(t, &ContextOpts{
   844  		Providers: map[addrs.Provider]providers.Factory{
   845  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   846  		},
   847  	})
   848  
   849  	plan, diags := ctx.Plan(m, state, &PlanOpts{
   850  		Mode: plans.RefreshOnlyMode,
   851  	})
   852  	if diags.HasErrors() {
   853  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
   854  	}
   855  
   856  	if !p.UpgradeResourceStateCalled {
   857  		t.Errorf("Provider's UpgradeResourceState wasn't called; should've been")
   858  	}
   859  	if !p.ReadResourceCalled {
   860  		t.Errorf("Provider's ReadResource wasn't called; should've been")
   861  	}
   862  
   863  	if got, want := len(plan.Changes.Resources), 0; got != want {
   864  		t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources))
   865  	}
   866  
   867  	if instState := plan.PriorState.ResourceInstance(addr); instState == nil {
   868  		t.Errorf("%s has no prior state at all after plan", addr)
   869  	} else {
   870  		if instState.Current == nil {
   871  			t.Errorf("%s has no current object after plan", addr)
   872  		} else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) {
   873  			// Should've saved the result of refreshing
   874  			t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
   875  		}
   876  	}
   877  	if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil {
   878  		t.Errorf("%s has no previous run state at all after plan", addr)
   879  	} else {
   880  		if instState.Current == nil {
   881  			t.Errorf("%s has no current object in the previous run state", addr)
   882  		} else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) {
   883  			// Should've saved the result of upgrading
   884  			t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
   885  		}
   886  	}
   887  
   888  	// The output value should also have updated. If not, it's likely that we
   889  	// skipped updating the working state to match the refreshed state when we
   890  	// were evaluating the resource.
   891  	if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil {
   892  		t.Errorf("no change planned for output value 'out'")
   893  	} else {
   894  		outChange, err := outChangeSrc.Decode()
   895  		if err != nil {
   896  			t.Fatalf("failed to decode output value 'out': %s", err)
   897  		}
   898  		got := outChange.After
   899  		want := cty.StringVal("current")
   900  		if !want.RawEquals(got) {
   901  			t.Errorf("wrong value for output value 'out'\ngot:  %#v\nwant: %#v", got, want)
   902  		}
   903  	}
   904  }
   905  
   906  func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) {
   907  	addr := mustResourceInstanceAddr("test_object.a")
   908  	deposedKey := states.DeposedKey("byebye")
   909  
   910  	// The configuration, the prior state, and the refresh result intentionally
   911  	// have different values for "test_string" so we can observe that the
   912  	// refresh took effect but the configuration change wasn't considered.
   913  	m := testModuleInline(t, map[string]string{
   914  		"main.tf": `
   915  			resource "test_object" "a" {
   916  				arg = "after"
   917  			}
   918  
   919  			output "out" {
   920  				value = test_object.a.arg
   921  			}
   922  		`,
   923  	})
   924  	state := states.BuildState(func(s *states.SyncState) {
   925  		// Note that we're intentionally recording a _deposed_ object here,
   926  		// and not including a current object, so a normal (non-refresh)
   927  		// plan would normally plan to create a new object _and_ destroy
   928  		// the deposed one, but refresh-only mode should prevent that.
   929  		s.SetResourceInstanceDeposed(addr, deposedKey, &states.ResourceInstanceObjectSrc{
   930  			AttrsJSON: []byte(`{"arg":"before"}`),
   931  			Status:    states.ObjectReady,
   932  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
   933  	})
   934  
   935  	p := simpleMockProvider()
   936  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   937  		Provider: providers.Schema{Block: simpleTestSchema()},
   938  		ResourceTypes: map[string]providers.Schema{
   939  			"test_object": {
   940  				Block: &configschema.Block{
   941  					Attributes: map[string]*configschema.Attribute{
   942  						"arg": {Type: cty.String, Optional: true},
   943  					},
   944  				},
   945  			},
   946  		},
   947  	}
   948  	p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
   949  		newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) {
   950  			if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) {
   951  				return cty.StringVal("current"), nil
   952  			}
   953  			return v, nil
   954  		})
   955  		if err != nil {
   956  			// shouldn't get here
   957  			t.Fatalf("ReadResourceFn transform failed")
   958  			return providers.ReadResourceResponse{}
   959  		}
   960  		return providers.ReadResourceResponse{
   961  			NewState: newVal,
   962  		}
   963  	}
   964  	p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
   965  		// We should've been given the prior state JSON as our input to upgrade.
   966  		if !bytes.Contains(req.RawStateJSON, []byte("before")) {
   967  			t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON)
   968  		}
   969  
   970  		// We'll put something different in "arg" as part of upgrading, just
   971  		// so that we can verify below that PrevRunState contains the upgraded
   972  		// (but NOT refreshed) version of the object.
   973  		resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{
   974  			"arg": cty.StringVal("upgraded"),
   975  		})
   976  		return resp
   977  	}
   978  
   979  	ctx := testContext2(t, &ContextOpts{
   980  		Providers: map[addrs.Provider]providers.Factory{
   981  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
   982  		},
   983  	})
   984  
   985  	plan, diags := ctx.Plan(m, state, &PlanOpts{
   986  		Mode: plans.RefreshOnlyMode,
   987  	})
   988  	if diags.HasErrors() {
   989  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
   990  	}
   991  
   992  	if !p.UpgradeResourceStateCalled {
   993  		t.Errorf("Provider's UpgradeResourceState wasn't called; should've been")
   994  	}
   995  	if !p.ReadResourceCalled {
   996  		t.Errorf("Provider's ReadResource wasn't called; should've been")
   997  	}
   998  
   999  	if got, want := len(plan.Changes.Resources), 0; got != want {
  1000  		t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources))
  1001  	}
  1002  
  1003  	if instState := plan.PriorState.ResourceInstance(addr); instState == nil {
  1004  		t.Errorf("%s has no prior state at all after plan", addr)
  1005  	} else {
  1006  		if obj := instState.Deposed[deposedKey]; obj == nil {
  1007  			t.Errorf("%s has no deposed object after plan", addr)
  1008  		} else if got, want := obj.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) {
  1009  			// Should've saved the result of refreshing
  1010  			t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
  1011  		}
  1012  	}
  1013  	if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil {
  1014  		t.Errorf("%s has no previous run state at all after plan", addr)
  1015  	} else {
  1016  		if obj := instState.Deposed[deposedKey]; obj == nil {
  1017  			t.Errorf("%s has no deposed object in the previous run state", addr)
  1018  		} else if got, want := obj.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) {
  1019  			// Should've saved the result of upgrading
  1020  			t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want)
  1021  		}
  1022  	}
  1023  
  1024  	// The output value should also have updated. If not, it's likely that we
  1025  	// skipped updating the working state to match the refreshed state when we
  1026  	// were evaluating the resource.
  1027  	if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil {
  1028  		t.Errorf("no change planned for output value 'out'")
  1029  	} else {
  1030  		outChange, err := outChangeSrc.Decode()
  1031  		if err != nil {
  1032  			t.Fatalf("failed to decode output value 'out': %s", err)
  1033  		}
  1034  		got := outChange.After
  1035  		want := cty.UnknownVal(cty.String)
  1036  		if !want.RawEquals(got) {
  1037  			t.Errorf("wrong value for output value 'out'\ngot:  %#v\nwant: %#v", got, want)
  1038  		}
  1039  	}
  1040  }
  1041  
  1042  func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) {
  1043  	m := testModuleInline(t, map[string]string{
  1044  		"child/main.tf": `
  1045  output "out" {
  1046    value = sensitive("xyz")
  1047  }`,
  1048  		"main.tf": `
  1049  module "child" {
  1050    source = "./child"
  1051  }
  1052  
  1053  output "root" {
  1054    value = module.child.out
  1055  }`,
  1056  	})
  1057  
  1058  	ctx := testContext2(t, &ContextOpts{})
  1059  
  1060  	_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
  1061  	if !diags.HasErrors() {
  1062  		t.Fatal("succeeded; want errors")
  1063  	}
  1064  	if got, want := diags.Err().Error(), "Output refers to sensitive values"; !strings.Contains(got, want) {
  1065  		t.Fatalf("wrong error:\ngot:  %s\nwant: message containing %q", got, want)
  1066  	}
  1067  }
  1068  
  1069  func TestContext2Plan_planDataSourceSensitiveNested(t *testing.T) {
  1070  	m := testModuleInline(t, map[string]string{
  1071  		"main.tf": `
  1072  resource "test_instance" "bar" {
  1073  }
  1074  
  1075  data "test_data_source" "foo" {
  1076    foo {
  1077      bar = test_instance.bar.sensitive
  1078    }
  1079  }
  1080  `,
  1081  	})
  1082  
  1083  	p := new(MockProvider)
  1084  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
  1085  		resp.PlannedState = cty.ObjectVal(map[string]cty.Value{
  1086  			"sensitive": cty.UnknownVal(cty.String),
  1087  		})
  1088  		return resp
  1089  	}
  1090  	p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
  1091  		ResourceTypes: map[string]*configschema.Block{
  1092  			"test_instance": {
  1093  				Attributes: map[string]*configschema.Attribute{
  1094  					"sensitive": {
  1095  						Type:      cty.String,
  1096  						Computed:  true,
  1097  						Sensitive: true,
  1098  					},
  1099  				},
  1100  			},
  1101  		},
  1102  		DataSources: map[string]*configschema.Block{
  1103  			"test_data_source": {
  1104  				Attributes: map[string]*configschema.Attribute{
  1105  					"id": {
  1106  						Type:     cty.String,
  1107  						Computed: true,
  1108  					},
  1109  				},
  1110  				BlockTypes: map[string]*configschema.NestedBlock{
  1111  					"foo": {
  1112  						Block: configschema.Block{
  1113  							Attributes: map[string]*configschema.Attribute{
  1114  								"bar": {Type: cty.String, Optional: true},
  1115  							},
  1116  						},
  1117  						Nesting: configschema.NestingSet,
  1118  					},
  1119  				},
  1120  			},
  1121  		},
  1122  	})
  1123  
  1124  	state := states.NewState()
  1125  	root := state.EnsureModule(addrs.RootModuleInstance)
  1126  	root.SetResourceInstanceCurrent(
  1127  		mustResourceInstanceAddr("data.test_data_source.foo").Resource,
  1128  		&states.ResourceInstanceObjectSrc{
  1129  			Status:    states.ObjectReady,
  1130  			AttrsJSON: []byte(`{"string":"data_id", "foo":[{"bar":"old"}]}`),
  1131  			AttrSensitivePaths: []cty.PathValueMarks{
  1132  				{
  1133  					Path:  cty.GetAttrPath("foo"),
  1134  					Marks: cty.NewValueMarks(marks.Sensitive),
  1135  				},
  1136  			},
  1137  		},
  1138  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
  1139  	)
  1140  	root.SetResourceInstanceCurrent(
  1141  		mustResourceInstanceAddr("test_instance.bar").Resource,
  1142  		&states.ResourceInstanceObjectSrc{
  1143  			Status:    states.ObjectReady,
  1144  			AttrsJSON: []byte(`{"sensitive":"old"}`),
  1145  			AttrSensitivePaths: []cty.PathValueMarks{
  1146  				{
  1147  					Path:  cty.GetAttrPath("sensitive"),
  1148  					Marks: cty.NewValueMarks(marks.Sensitive),
  1149  				},
  1150  			},
  1151  		},
  1152  		mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
  1153  	)
  1154  
  1155  	ctx := testContext2(t, &ContextOpts{
  1156  		Providers: map[addrs.Provider]providers.Factory{
  1157  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
  1158  		},
  1159  	})
  1160  
  1161  	plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
  1162  	assertNoErrors(t, diags)
  1163  
  1164  	for _, res := range plan.Changes.Resources {
  1165  		switch res.Addr.String() {
  1166  		case "test_instance.bar":
  1167  			if res.Action != plans.Update {
  1168  				t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
  1169  			}
  1170  		case "data.test_data_source.foo":
  1171  			if res.Action != plans.Read {
  1172  				t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
  1173  			}
  1174  		default:
  1175  			t.Fatalf("unexpected %s change for %s", res.Action, res.Addr)
  1176  		}
  1177  	}
  1178  }
  1179  
  1180  func TestContext2Plan_forceReplace(t *testing.T) {
  1181  	addrA := mustResourceInstanceAddr("test_object.a")
  1182  	addrB := mustResourceInstanceAddr("test_object.b")
  1183  	m := testModuleInline(t, map[string]string{
  1184  		"main.tf": `
  1185  			resource "test_object" "a" {
  1186  			}
  1187  			resource "test_object" "b" {
  1188  			}
  1189  		`,
  1190  	})
  1191  
  1192  	state := states.BuildState(func(s *states.SyncState) {
  1193  		s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
  1194  			AttrsJSON: []byte(`{}`),
  1195  			Status:    states.ObjectReady,
  1196  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
  1197  		s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{
  1198  			AttrsJSON: []byte(`{}`),
  1199  			Status:    states.ObjectReady,
  1200  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
  1201  	})
  1202  
  1203  	p := simpleMockProvider()
  1204  	ctx := testContext2(t, &ContextOpts{
  1205  		Providers: map[addrs.Provider]providers.Factory{
  1206  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
  1207  		},
  1208  	})
  1209  
  1210  	plan, diags := ctx.Plan(m, state, &PlanOpts{
  1211  		Mode: plans.NormalMode,
  1212  		ForceReplace: []addrs.AbsResourceInstance{
  1213  			addrA,
  1214  		},
  1215  	})
  1216  	if diags.HasErrors() {
  1217  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
  1218  	}
  1219  
  1220  	t.Run(addrA.String(), func(t *testing.T) {
  1221  		instPlan := plan.Changes.ResourceInstance(addrA)
  1222  		if instPlan == nil {
  1223  			t.Fatalf("no plan for %s at all", addrA)
  1224  		}
  1225  
  1226  		if got, want := instPlan.Action, plans.DeleteThenCreate; got != want {
  1227  			t.Errorf("wrong planned action\ngot:  %s\nwant: %s", got, want)
  1228  		}
  1229  		if got, want := instPlan.ActionReason, plans.ResourceInstanceReplaceByRequest; got != want {
  1230  			t.Errorf("wrong action reason\ngot:  %s\nwant: %s", got, want)
  1231  		}
  1232  	})
  1233  	t.Run(addrB.String(), func(t *testing.T) {
  1234  		instPlan := plan.Changes.ResourceInstance(addrB)
  1235  		if instPlan == nil {
  1236  			t.Fatalf("no plan for %s at all", addrB)
  1237  		}
  1238  
  1239  		if got, want := instPlan.Action, plans.NoOp; got != want {
  1240  			t.Errorf("wrong planned action\ngot:  %s\nwant: %s", got, want)
  1241  		}
  1242  		if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
  1243  			t.Errorf("wrong action reason\ngot:  %s\nwant: %s", got, want)
  1244  		}
  1245  	})
  1246  }
  1247  
  1248  func TestContext2Plan_forceReplaceIncompleteAddr(t *testing.T) {
  1249  	addr0 := mustResourceInstanceAddr("test_object.a[0]")
  1250  	addr1 := mustResourceInstanceAddr("test_object.a[1]")
  1251  	addrBare := mustResourceInstanceAddr("test_object.a")
  1252  	m := testModuleInline(t, map[string]string{
  1253  		"main.tf": `
  1254  			resource "test_object" "a" {
  1255  				count = 2
  1256  			}
  1257  		`,
  1258  	})
  1259  
  1260  	state := states.BuildState(func(s *states.SyncState) {
  1261  		s.SetResourceInstanceCurrent(addr0, &states.ResourceInstanceObjectSrc{
  1262  			AttrsJSON: []byte(`{}`),
  1263  			Status:    states.ObjectReady,
  1264  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
  1265  		s.SetResourceInstanceCurrent(addr1, &states.ResourceInstanceObjectSrc{
  1266  			AttrsJSON: []byte(`{}`),
  1267  			Status:    states.ObjectReady,
  1268  		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
  1269  	})
  1270  
  1271  	p := simpleMockProvider()
  1272  	ctx := testContext2(t, &ContextOpts{
  1273  		Providers: map[addrs.Provider]providers.Factory{
  1274  			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
  1275  		},
  1276  	})
  1277  
  1278  	plan, diags := ctx.Plan(m, state, &PlanOpts{
  1279  		Mode: plans.NormalMode,
  1280  		ForceReplace: []addrs.AbsResourceInstance{
  1281  			addrBare,
  1282  		},
  1283  	})
  1284  	if diags.HasErrors() {
  1285  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
  1286  	}
  1287  	diagsErr := diags.ErrWithWarnings()
  1288  	if diagsErr == nil {
  1289  		t.Fatalf("no warnings were returned")
  1290  	}
  1291  	if got, want := diagsErr.Error(), "Incompletely-matched force-replace resource instance"; !strings.Contains(got, want) {
  1292  		t.Errorf("missing expected warning\ngot:\n%s\n\nwant substring: %s", got, want)
  1293  	}
  1294  
  1295  	t.Run(addr0.String(), func(t *testing.T) {
  1296  		instPlan := plan.Changes.ResourceInstance(addr0)
  1297  		if instPlan == nil {
  1298  			t.Fatalf("no plan for %s at all", addr0)
  1299  		}
  1300  
  1301  		if got, want := instPlan.Action, plans.NoOp; got != want {
  1302  			t.Errorf("wrong planned action\ngot:  %s\nwant: %s", got, want)
  1303  		}
  1304  		if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
  1305  			t.Errorf("wrong action reason\ngot:  %s\nwant: %s", got, want)
  1306  		}
  1307  	})
  1308  	t.Run(addr1.String(), func(t *testing.T) {
  1309  		instPlan := plan.Changes.ResourceInstance(addr1)
  1310  		if instPlan == nil {
  1311  			t.Fatalf("no plan for %s at all", addr1)
  1312  		}
  1313  
  1314  		if got, want := instPlan.Action, plans.NoOp; got != want {
  1315  			t.Errorf("wrong planned action\ngot:  %s\nwant: %s", got, want)
  1316  		}
  1317  		if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
  1318  			t.Errorf("wrong action reason\ngot:  %s\nwant: %s", got, want)
  1319  		}
  1320  	})
  1321  }