github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/refresh_test.go (about)

     1  package lifecycletest
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"strconv"
     8  	"testing"
     9  
    10  	"github.com/blang/semver"
    11  	combinations "github.com/mxschmitt/golang-combinations"
    12  	"github.com/stretchr/testify/assert"
    13  
    14  	. "github.com/pulumi/pulumi/pkg/v3/engine"
    15  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    16  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
    17  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
    18  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    19  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
    20  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
    21  	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
    22  )
    23  
    24  func TestParallelRefresh(t *testing.T) {
    25  	t.Parallel()
    26  
    27  	loaders := []*deploytest.ProviderLoader{
    28  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
    29  			return &deploytest.Provider{}, nil
    30  		}),
    31  	}
    32  
    33  	// Create a program that registers four resources, each of which depends on the resource that immediately precedes
    34  	// it.
    35  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
    36  		resA, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
    37  		assert.NoError(t, err)
    38  
    39  		resB, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
    40  			Dependencies: []resource.URN{resA},
    41  		})
    42  		assert.NoError(t, err)
    43  
    44  		resC, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
    45  			Dependencies: []resource.URN{resB},
    46  		})
    47  		assert.NoError(t, err)
    48  
    49  		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resD", true, deploytest.ResourceOptions{
    50  			Dependencies: []resource.URN{resC},
    51  		})
    52  		assert.NoError(t, err)
    53  
    54  		return nil
    55  	})
    56  	host := deploytest.NewPluginHost(nil, nil, program, loaders...)
    57  
    58  	p := &TestPlan{
    59  		Options: UpdateOptions{Parallel: 4, Host: host},
    60  	}
    61  
    62  	p.Steps = []TestStep{{Op: Update}}
    63  	snap := p.Run(t, nil)
    64  
    65  	assert.Len(t, snap.Resources, 5)
    66  	assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
    67  	assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
    68  	assert.Equal(t, string(snap.Resources[2].URN.Name()), "resB")
    69  	assert.Equal(t, string(snap.Resources[3].URN.Name()), "resC")
    70  	assert.Equal(t, string(snap.Resources[4].URN.Name()), "resD")
    71  
    72  	p.Steps = []TestStep{{Op: Refresh}}
    73  	snap = p.Run(t, snap)
    74  
    75  	assert.Len(t, snap.Resources, 5)
    76  	assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
    77  	assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
    78  	assert.Equal(t, string(snap.Resources[2].URN.Name()), "resB")
    79  	assert.Equal(t, string(snap.Resources[3].URN.Name()), "resC")
    80  	assert.Equal(t, string(snap.Resources[4].URN.Name()), "resD")
    81  }
    82  
    83  func TestExternalRefresh(t *testing.T) {
    84  	t.Parallel()
    85  
    86  	loaders := []*deploytest.ProviderLoader{
    87  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
    88  			return &deploytest.Provider{}, nil
    89  		}),
    90  	}
    91  
    92  	// Our program reads a resource and exits.
    93  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
    94  		_, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "")
    95  		if !assert.NoError(t, err) {
    96  			t.FailNow()
    97  		}
    98  
    99  		return nil
   100  	})
   101  	host := deploytest.NewPluginHost(nil, nil, program, loaders...)
   102  	p := &TestPlan{
   103  		Options: UpdateOptions{Host: host},
   104  		Steps:   []TestStep{{Op: Update}},
   105  	}
   106  
   107  	// The read should place "resA" in the snapshot with the "External" bit set.
   108  	snap := p.Run(t, nil)
   109  	assert.Len(t, snap.Resources, 2)
   110  	assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
   111  	assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
   112  	assert.True(t, snap.Resources[1].External)
   113  
   114  	p = &TestPlan{
   115  		Options: UpdateOptions{Host: host},
   116  		Steps:   []TestStep{{Op: Refresh}},
   117  	}
   118  
   119  	snap = p.Run(t, snap)
   120  	// A refresh should leave "resA" as it is in the snapshot. The External bit should still be set.
   121  	assert.Len(t, snap.Resources, 2)
   122  	assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider
   123  	assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
   124  	assert.True(t, snap.Resources[1].External)
   125  }
   126  
   127  func TestRefreshInitFailure(t *testing.T) {
   128  	t.Parallel()
   129  
   130  	p := &TestPlan{}
   131  
   132  	provURN := p.NewProviderURN("pkgA", "default", "")
   133  	resURN := p.NewURN("pkgA:m:typA", "resA", "")
   134  	res2URN := p.NewURN("pkgA:m:typA", "resB", "")
   135  
   136  	res2Outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
   137  
   138  	//
   139  	// Refresh will persist any initialization errors that are returned by `Read`. This provider
   140  	// will error out or not based on the value of `refreshShouldFail`.
   141  	//
   142  	refreshShouldFail := false
   143  
   144  	//
   145  	// Set up test environment to use `readFailProvider` as the underlying resource provider.
   146  	//
   147  	loaders := []*deploytest.ProviderLoader{
   148  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   149  			return &deploytest.Provider{
   150  				ReadF: func(
   151  					urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
   152  				) (plugin.ReadResult, resource.Status, error) {
   153  					if refreshShouldFail && urn == resURN {
   154  						err := &plugin.InitError{
   155  							Reasons: []string{"Refresh reports continued to fail to initialize"},
   156  						}
   157  						return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusPartialFailure, err
   158  					} else if urn == res2URN {
   159  						return plugin.ReadResult{Outputs: res2Outputs}, resource.StatusOK, nil
   160  					}
   161  					return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusOK, nil
   162  				},
   163  			}, nil
   164  		}),
   165  	}
   166  
   167  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   168  		_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
   169  		assert.NoError(t, err)
   170  		return nil
   171  	})
   172  	host := deploytest.NewPluginHost(nil, nil, program, loaders...)
   173  
   174  	p.Options.Host = host
   175  
   176  	//
   177  	// Create an old snapshot with a single initialization failure.
   178  	//
   179  	old := &deploy.Snapshot{
   180  		Resources: []*resource.State{
   181  			{
   182  				Type:       resURN.Type(),
   183  				URN:        resURN,
   184  				Custom:     true,
   185  				ID:         "0",
   186  				Inputs:     resource.PropertyMap{},
   187  				Outputs:    resource.PropertyMap{},
   188  				InitErrors: []string{"Resource failed to initialize"},
   189  			},
   190  			{
   191  				Type:    res2URN.Type(),
   192  				URN:     res2URN,
   193  				Custom:  true,
   194  				ID:      "1",
   195  				Inputs:  resource.PropertyMap{},
   196  				Outputs: resource.PropertyMap{},
   197  			},
   198  		},
   199  	}
   200  
   201  	//
   202  	// Refresh DOES NOT fail, causing the initialization error to disappear.
   203  	//
   204  	p.Steps = []TestStep{{Op: Refresh}}
   205  	snap := p.Run(t, old)
   206  
   207  	for _, resource := range snap.Resources {
   208  		switch urn := resource.URN; urn {
   209  		case provURN:
   210  			// break
   211  		case resURN:
   212  			assert.Empty(t, resource.InitErrors)
   213  		case res2URN:
   214  			assert.Equal(t, res2Outputs, resource.Outputs)
   215  		default:
   216  			t.Fatalf("unexpected resource %v", urn)
   217  		}
   218  	}
   219  
   220  	//
   221  	// Refresh again, see the resource is in a partial state of failure, but the refresh operation
   222  	// DOES NOT fail. The initialization error is still persisted.
   223  	//
   224  	refreshShouldFail = true
   225  	p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
   226  	snap = p.Run(t, old)
   227  	for _, resource := range snap.Resources {
   228  		switch urn := resource.URN; urn {
   229  		case provURN:
   230  			// break
   231  		case resURN:
   232  			assert.Equal(t, []string{"Refresh reports continued to fail to initialize"}, resource.InitErrors)
   233  		case res2URN:
   234  			assert.Equal(t, res2Outputs, resource.Outputs)
   235  		default:
   236  			t.Fatalf("unexpected resource %v", urn)
   237  		}
   238  	}
   239  }
   240  
   241  // Test that tests that Refresh can detect that resources have been deleted and removes them
   242  // from the snapshot.
   243  func TestRefreshWithDelete(t *testing.T) {
   244  	t.Parallel()
   245  
   246  	//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
   247  	for _, parallelFactor := range []int{1, 4} {
   248  		parallelFactor := parallelFactor
   249  		t.Run(fmt.Sprintf("parallel-%d", parallelFactor), func(t *testing.T) {
   250  			t.Parallel()
   251  
   252  			loaders := []*deploytest.ProviderLoader{
   253  				deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   254  					return &deploytest.Provider{
   255  						ReadF: func(
   256  							urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
   257  						) (plugin.ReadResult, resource.Status, error) {
   258  							// This thing doesn't exist. Returning nil from Read should trigger
   259  							// the engine to delete it from the snapshot.
   260  							return plugin.ReadResult{}, resource.StatusOK, nil
   261  						},
   262  					}, nil
   263  				}),
   264  			}
   265  
   266  			program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   267  				_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
   268  				assert.NoError(t, err)
   269  				return err
   270  			})
   271  
   272  			host := deploytest.NewPluginHost(nil, nil, program, loaders...)
   273  			p := &TestPlan{Options: UpdateOptions{Host: host, Parallel: parallelFactor}}
   274  
   275  			p.Steps = []TestStep{{Op: Update}}
   276  			snap := p.Run(t, nil)
   277  
   278  			p.Steps = []TestStep{{Op: Refresh}}
   279  			snap = p.Run(t, snap)
   280  
   281  			// Refresh succeeds and records that the resource in the snapshot doesn't exist anymore
   282  			provURN := p.NewProviderURN("pkgA", "default", "")
   283  			assert.Len(t, snap.Resources, 1)
   284  			assert.Equal(t, provURN, snap.Resources[0].URN)
   285  		})
   286  	}
   287  }
   288  
   289  // Tests that dependencies are correctly rewritten when refresh removes deleted resources.
   290  func TestRefreshDeleteDependencies(t *testing.T) {
   291  	t.Parallel()
   292  
   293  	names := []string{"resA", "resB", "resC"}
   294  
   295  	// Try refreshing a stack with every combination of the three above resources as a target to
   296  	// refresh.
   297  	subsets := combinations.All(names)
   298  
   299  	// combinations.All doesn't return the empty set.  So explicitly test that case (i.e. test no
   300  	// targets specified)
   301  	validateRefreshDeleteCombination(t, names, []string{})
   302  
   303  	for _, subset := range subsets {
   304  		validateRefreshDeleteCombination(t, names, subset)
   305  	}
   306  }
   307  
   308  // Looks up the provider ID in newResources and sets "Provider" to reference that in every resource in oldResources.
   309  func setProviderRef(t *testing.T, oldResources, newResources []*resource.State, provURN resource.URN) {
   310  	for _, r := range newResources {
   311  		if r.URN == provURN {
   312  			provRef, err := providers.NewReference(r.URN, r.ID)
   313  			assert.NoError(t, err)
   314  			for i := range oldResources {
   315  				oldResources[i].Provider = provRef.String()
   316  			}
   317  			break
   318  		}
   319  	}
   320  }
   321  
   322  func validateRefreshDeleteCombination(t *testing.T, names []string, targets []string) {
   323  	p := &TestPlan{}
   324  
   325  	const resType = "pkgA:m:typA"
   326  
   327  	urnA := p.NewURN(resType, names[0], "")
   328  	urnB := p.NewURN(resType, names[1], "")
   329  	urnC := p.NewURN(resType, names[2], "")
   330  	urns := []resource.URN{urnA, urnB, urnC}
   331  
   332  	refreshTargets := []resource.URN{}
   333  
   334  	t.Logf("Refreshing targets: %v", targets)
   335  	for _, target := range targets {
   336  		refreshTargets = append(refreshTargets, pickURN(t, urns, names, target))
   337  	}
   338  
   339  	p.Options.RefreshTargets = deploy.NewUrnTargetsFromUrns(refreshTargets)
   340  
   341  	newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
   342  		return &resource.State{
   343  			Type:         urn.Type(),
   344  			URN:          urn,
   345  			Custom:       true,
   346  			Delete:       delete,
   347  			ID:           id,
   348  			Inputs:       resource.PropertyMap{},
   349  			Outputs:      resource.PropertyMap{},
   350  			Dependencies: dependencies,
   351  		}
   352  	}
   353  
   354  	oldResources := []*resource.State{
   355  		newResource(urnA, "0", false),
   356  		newResource(urnB, "1", false, urnA),
   357  		newResource(urnC, "2", false, urnA, urnB),
   358  		newResource(urnA, "3", true),
   359  		newResource(urnA, "4", true),
   360  		newResource(urnC, "5", true, urnA, urnB),
   361  	}
   362  
   363  	old := &deploy.Snapshot{
   364  		Resources: oldResources,
   365  	}
   366  
   367  	loaders := []*deploytest.ProviderLoader{
   368  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   369  			return &deploytest.Provider{
   370  				ReadF: func(urn resource.URN, id resource.ID,
   371  					inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
   372  
   373  					switch id {
   374  					case "0", "4":
   375  						// We want to delete resources A::0 and A::4.
   376  						return plugin.ReadResult{}, resource.StatusOK, nil
   377  					default:
   378  						return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
   379  					}
   380  				},
   381  			}, nil
   382  		}),
   383  	}
   384  
   385  	p.Options.Host = deploytest.NewPluginHost(nil, nil, nil, loaders...)
   386  
   387  	p.Steps = []TestStep{
   388  		{
   389  			Op: Refresh,
   390  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   391  				_ []Event, res result.Result) result.Result {
   392  
   393  				// Should see only refreshes.
   394  				for _, entry := range entries {
   395  					if len(refreshTargets) > 0 {
   396  						// should only see changes to urns we explicitly asked to change
   397  						assert.Containsf(t, refreshTargets, entry.Step.URN(),
   398  							"Refreshed a resource that wasn't a target: %v", entry.Step.URN())
   399  					}
   400  
   401  					assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
   402  				}
   403  
   404  				return res
   405  			},
   406  		},
   407  	}
   408  
   409  	snap := p.Run(t, old)
   410  
   411  	provURN := p.NewProviderURN("pkgA", "default", "")
   412  
   413  	// The new resources will have had their default provider urn filled in. We fill this in on
   414  	// the old resources here as well so that the equal checks below pass
   415  	setProviderRef(t, oldResources, snap.Resources, provURN)
   416  
   417  	for _, r := range snap.Resources {
   418  		switch urn := r.URN; urn {
   419  		case provURN:
   420  			continue
   421  		case urnA, urnB, urnC:
   422  			// break
   423  		default:
   424  			t.Fatalf("unexpected resource %v", urn)
   425  		}
   426  
   427  		if len(refreshTargets) == 0 || containsURN(refreshTargets, urnA) {
   428  			// 'A' was deleted, so we should see the impact downstream.
   429  
   430  			switch r.ID {
   431  			case "1":
   432  				// A::0 was deleted, so B's dependency list should be empty.
   433  				assert.Equal(t, urnB, r.URN)
   434  				assert.Empty(t, r.Dependencies)
   435  			case "2":
   436  				// A::0 was deleted, so C's dependency list should only contain B.
   437  				assert.Equal(t, urnC, r.URN)
   438  				assert.Equal(t, []resource.URN{urnB}, r.Dependencies)
   439  			case "3":
   440  				// A::3 should not have changed.
   441  				assert.Equal(t, oldResources[3], r)
   442  			case "5":
   443  				// A::4 was deleted but A::3 was still refernceable by C, so C should not have changed.
   444  				assert.Equal(t, oldResources[5], r)
   445  			default:
   446  				t.Fatalf("Unexpected changed resource when refreshing %v: %v::%v", refreshTargets, r.URN, r.ID)
   447  			}
   448  		} else {
   449  			// A was not deleted. So nothing should be impacted.
   450  			id, err := strconv.Atoi(r.ID.String())
   451  			assert.NoError(t, err)
   452  			assert.Equal(t, oldResources[id], r)
   453  		}
   454  	}
   455  }
   456  
   457  func containsURN(urns []resource.URN, urn resource.URN) bool {
   458  	for _, val := range urns {
   459  		if val == urn {
   460  			return true
   461  		}
   462  	}
   463  
   464  	return false
   465  }
   466  
   467  // Tests basic refresh functionality.
   468  func TestRefreshBasics(t *testing.T) {
   469  	t.Parallel()
   470  
   471  	names := []string{"resA", "resB", "resC"}
   472  
   473  	// Try refreshing a stack with every combination of the three above resources as a target to
   474  	// refresh.
   475  	subsets := combinations.All(names)
   476  
   477  	// combinations.All doesn't return the empty set.  So explicitly test that case (i.e. test no
   478  	// targets specified)
   479  	//validateRefreshBasicsCombination(t, names, []string{})
   480  
   481  	for _, subset := range subsets {
   482  		validateRefreshBasicsCombination(t, names, subset)
   483  	}
   484  }
   485  
   486  func validateRefreshBasicsCombination(t *testing.T, names []string, targets []string) {
   487  	p := &TestPlan{}
   488  
   489  	const resType = "pkgA:m:typA"
   490  
   491  	urnA := p.NewURN(resType, names[0], "")
   492  	urnB := p.NewURN(resType, names[1], "")
   493  	urnC := p.NewURN(resType, names[2], "")
   494  	urns := []resource.URN{urnA, urnB, urnC}
   495  
   496  	refreshTargets := []resource.URN{}
   497  
   498  	for _, target := range targets {
   499  		refreshTargets = append(p.Options.RefreshTargets.Literals(), pickURN(t, urns, names, target))
   500  	}
   501  
   502  	p.Options.RefreshTargets = deploy.NewUrnTargetsFromUrns(refreshTargets)
   503  
   504  	newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
   505  		return &resource.State{
   506  			Type:         urn.Type(),
   507  			URN:          urn,
   508  			Custom:       true,
   509  			Delete:       delete,
   510  			ID:           id,
   511  			Inputs:       resource.PropertyMap{},
   512  			Outputs:      resource.PropertyMap{},
   513  			Dependencies: dependencies,
   514  		}
   515  	}
   516  
   517  	oldResources := []*resource.State{
   518  		newResource(urnA, "0", false),
   519  		newResource(urnB, "1", false, urnA),
   520  		newResource(urnC, "2", false, urnA, urnB),
   521  		newResource(urnA, "3", true),
   522  		newResource(urnA, "4", true),
   523  		newResource(urnC, "5", true, urnA, urnB),
   524  	}
   525  
   526  	newStates := map[resource.ID]plugin.ReadResult{
   527  		// A::0 and A::3 will have no changes.
   528  		"0": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}},
   529  		"3": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}},
   530  
   531  		// B::1 and A::4 will have changes. The latter will also have input changes.
   532  		"1": {Outputs: resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, Inputs: resource.PropertyMap{}},
   533  		"4": {
   534  			Outputs: resource.PropertyMap{"baz": resource.NewStringProperty("qux")},
   535  			Inputs:  resource.PropertyMap{"oof": resource.NewStringProperty("zab")},
   536  		},
   537  
   538  		// C::2 and C::5 will be deleted.
   539  		"2": {},
   540  		"5": {},
   541  	}
   542  
   543  	old := &deploy.Snapshot{
   544  		Resources: oldResources,
   545  	}
   546  
   547  	loaders := []*deploytest.ProviderLoader{
   548  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   549  			return &deploytest.Provider{
   550  				ReadF: func(urn resource.URN, id resource.ID,
   551  					inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
   552  
   553  					new, hasNewState := newStates[id]
   554  					assert.True(t, hasNewState)
   555  					return new, resource.StatusOK, nil
   556  				},
   557  			}, nil
   558  		}),
   559  	}
   560  
   561  	p.Options.Host = deploytest.NewPluginHost(nil, nil, nil, loaders...)
   562  
   563  	p.Steps = []TestStep{{
   564  		Op: Refresh,
   565  		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   566  			_ []Event, res result.Result) result.Result {
   567  
   568  			// Should see only refreshes.
   569  			for _, entry := range entries {
   570  				if len(refreshTargets) > 0 {
   571  					// should only see changes to urns we explicitly asked to change
   572  					assert.Containsf(t, refreshTargets, entry.Step.URN(),
   573  						"Refreshed a resource that wasn't a target: %v", entry.Step.URN())
   574  				}
   575  
   576  				assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
   577  				resultOp := entry.Step.(*deploy.RefreshStep).ResultOp()
   578  
   579  				old := entry.Step.Old()
   580  				if !old.Custom || providers.IsProviderType(old.Type) {
   581  					// Component and provider resources should never change.
   582  					assert.Equal(t, deploy.OpSame, resultOp)
   583  					continue
   584  				}
   585  
   586  				expected, new := newStates[old.ID], entry.Step.New()
   587  				if expected.Outputs == nil {
   588  					// If the resource was deleted, we want the result op to be an OpDelete.
   589  					assert.Nil(t, new)
   590  					assert.Equal(t, deploy.OpDelete, resultOp)
   591  				} else {
   592  					// If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want
   593  					// an OpSame.
   594  					if reflect.DeepEqual(old.Outputs, expected.Outputs) {
   595  						assert.Equal(t, deploy.OpSame, resultOp)
   596  					} else {
   597  						assert.Equal(t, deploy.OpUpdate, resultOp)
   598  					}
   599  
   600  					// Only the inputs and outputs should have changed (if anything changed).
   601  					old.Inputs = expected.Inputs
   602  					old.Outputs = expected.Outputs
   603  					assert.Equal(t, old, new)
   604  				}
   605  			}
   606  			return res
   607  		},
   608  	}}
   609  	snap := p.Run(t, old)
   610  
   611  	provURN := p.NewProviderURN("pkgA", "default", "")
   612  
   613  	// The new resources will have had their default provider urn filled in. We fill this in on
   614  	// the old resources here as well so that the equal checks below pass
   615  	setProviderRef(t, oldResources, snap.Resources, provURN)
   616  
   617  	for _, r := range snap.Resources {
   618  		switch urn := r.URN; urn {
   619  		case provURN:
   620  			continue
   621  		case urnA, urnB, urnC:
   622  			// break
   623  		default:
   624  			t.Fatalf("unexpected resource %v", urn)
   625  		}
   626  
   627  		// The only resources left in the checkpoint should be those that were not deleted by the refresh.
   628  		expected := newStates[r.ID]
   629  		assert.NotNil(t, expected)
   630  
   631  		idx, err := strconv.ParseInt(string(r.ID), 0, 0)
   632  		assert.NoError(t, err)
   633  
   634  		targetedForRefresh := false
   635  		for _, targetUrn := range refreshTargets {
   636  			if targetUrn == r.URN {
   637  				targetedForRefresh = true
   638  			}
   639  		}
   640  
   641  		// If targeted for refresh the new resources should be equal to the old resources + the new inputs and outputs
   642  		old := oldResources[int(idx)]
   643  		if targetedForRefresh {
   644  			old.Inputs = expected.Inputs
   645  			old.Outputs = expected.Outputs
   646  		}
   647  		assert.Equal(t, old, r)
   648  	}
   649  }
   650  
   651  // Tests that an interrupted refresh leaves behind an expected state.
   652  func TestCanceledRefresh(t *testing.T) {
   653  	t.Parallel()
   654  
   655  	p := &TestPlan{}
   656  
   657  	const resType = "pkgA:m:typA"
   658  
   659  	urnA := p.NewURN(resType, "resA", "")
   660  	urnB := p.NewURN(resType, "resB", "")
   661  	urnC := p.NewURN(resType, "resC", "")
   662  
   663  	newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
   664  		return &resource.State{
   665  			Type:         urn.Type(),
   666  			URN:          urn,
   667  			Custom:       true,
   668  			Delete:       delete,
   669  			ID:           id,
   670  			Inputs:       resource.PropertyMap{},
   671  			Outputs:      resource.PropertyMap{},
   672  			Dependencies: dependencies,
   673  		}
   674  	}
   675  
   676  	oldResources := []*resource.State{
   677  		newResource(urnA, "0", false),
   678  		newResource(urnB, "1", false),
   679  		newResource(urnC, "2", false),
   680  	}
   681  
   682  	newStates := map[resource.ID]resource.PropertyMap{
   683  		// A::0 and B::1 will have changes; D::3 will be deleted.
   684  		"0": {"foo": resource.NewStringProperty("bar")},
   685  		"1": {"baz": resource.NewStringProperty("qux")},
   686  		"2": nil,
   687  	}
   688  
   689  	old := &deploy.Snapshot{
   690  		Resources: oldResources,
   691  	}
   692  
   693  	// Set up a cancelable context for the refresh operation.
   694  	ctx, cancel := context.WithCancel(context.Background())
   695  
   696  	// Serialize all refreshes s.t. we can cancel after the first is issued.
   697  	refreshes, cancelled := make(chan resource.ID), make(chan bool)
   698  	go func() {
   699  		<-refreshes
   700  		cancel()
   701  	}()
   702  
   703  	loaders := []*deploytest.ProviderLoader{
   704  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   705  			return &deploytest.Provider{
   706  				ReadF: func(urn resource.URN, id resource.ID,
   707  					inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
   708  
   709  					refreshes <- id
   710  					<-cancelled
   711  
   712  					new, hasNewState := newStates[id]
   713  					assert.True(t, hasNewState)
   714  					return plugin.ReadResult{Outputs: new}, resource.StatusOK, nil
   715  				},
   716  				CancelF: func() error {
   717  					close(cancelled)
   718  					return nil
   719  				},
   720  			}, nil
   721  		}),
   722  	}
   723  
   724  	refreshed := make(map[resource.ID]bool)
   725  	op := TestOp(Refresh)
   726  	options := UpdateOptions{
   727  		Parallel: 1,
   728  		Host:     deploytest.NewPluginHost(nil, nil, nil, loaders...),
   729  	}
   730  	project, target := p.GetProject(), p.GetTarget(t, old)
   731  	validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
   732  		_ []Event, res result.Result) result.Result {
   733  
   734  		for _, entry := range entries {
   735  			assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
   736  			resultOp := entry.Step.(*deploy.RefreshStep).ResultOp()
   737  
   738  			old := entry.Step.Old()
   739  			if !old.Custom || providers.IsProviderType(old.Type) {
   740  				// Component and provider resources should never change.
   741  				assert.Equal(t, deploy.OpSame, resultOp)
   742  				continue
   743  			}
   744  
   745  			refreshed[old.ID] = true
   746  
   747  			expected, new := newStates[old.ID], entry.Step.New()
   748  			if expected == nil {
   749  				// If the resource was deleted, we want the result op to be an OpDelete.
   750  				assert.Nil(t, new)
   751  				assert.Equal(t, deploy.OpDelete, resultOp)
   752  			} else {
   753  				// If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want
   754  				// an OpSame.
   755  				if reflect.DeepEqual(old.Outputs, expected) {
   756  					assert.Equal(t, deploy.OpSame, resultOp)
   757  				} else {
   758  					assert.Equal(t, deploy.OpUpdate, resultOp)
   759  				}
   760  
   761  				// Only the outputs should have changed (if anything changed).
   762  				old.Outputs = expected
   763  				assert.Equal(t, old, new)
   764  			}
   765  		}
   766  		return res
   767  	}
   768  
   769  	snap, res := op.RunWithContext(ctx, project, target, options, false, nil, validate)
   770  	assertIsErrorOrBailResult(t, res)
   771  	assert.Equal(t, 1, len(refreshed))
   772  
   773  	provURN := p.NewProviderURN("pkgA", "default", "")
   774  
   775  	// The new resources will have had their default provider urn filled in. We fill this in on
   776  	// the old resources here as well so that the equal checks below pass
   777  	setProviderRef(t, oldResources, snap.Resources, provURN)
   778  
   779  	for _, r := range snap.Resources {
   780  		switch urn := r.URN; urn {
   781  		case provURN:
   782  			continue
   783  		case urnA, urnB, urnC:
   784  			// break
   785  		default:
   786  			t.Fatalf("unexpected resource %v", urn)
   787  		}
   788  
   789  		idx, err := strconv.ParseInt(string(r.ID), 0, 0)
   790  		assert.NoError(t, err)
   791  
   792  		if refreshed[r.ID] {
   793  			// The refreshed resource should have its new state.
   794  			expected := newStates[r.ID]
   795  			if expected == nil {
   796  				assert.Fail(t, "refreshed resource was not deleted")
   797  			} else {
   798  				old := oldResources[int(idx)]
   799  				old.Outputs = expected
   800  				assert.Equal(t, old, r)
   801  			}
   802  		} else {
   803  			// Any resources that were not refreshed should retain their original state.
   804  			old := oldResources[int(idx)]
   805  			assert.Equal(t, old, r)
   806  		}
   807  	}
   808  }
   809  
   810  func TestRefreshStepWillPersistUpdatedIDs(t *testing.T) {
   811  	t.Parallel()
   812  
   813  	p := &TestPlan{}
   814  
   815  	provURN := p.NewProviderURN("pkgA", "default", "")
   816  	resURN := p.NewURN("pkgA:m:typA", "resA", "")
   817  	idBefore := resource.ID("myid")
   818  	idAfter := resource.ID("mynewid")
   819  	outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
   820  
   821  	loaders := []*deploytest.ProviderLoader{
   822  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   823  			return &deploytest.Provider{
   824  				ReadF: func(
   825  					urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
   826  				) (plugin.ReadResult, resource.Status, error) {
   827  					return plugin.ReadResult{ID: idAfter, Outputs: outputs, Inputs: resource.PropertyMap{}}, resource.StatusOK, nil
   828  				},
   829  			}, nil
   830  		}),
   831  	}
   832  
   833  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   834  		_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
   835  		assert.NoError(t, err)
   836  		return nil
   837  	})
   838  	host := deploytest.NewPluginHost(nil, nil, program, loaders...)
   839  
   840  	p.Options.Host = host
   841  
   842  	old := &deploy.Snapshot{
   843  		Resources: []*resource.State{
   844  			{
   845  				Type:       resURN.Type(),
   846  				URN:        resURN,
   847  				Custom:     true,
   848  				ID:         idBefore,
   849  				Inputs:     resource.PropertyMap{},
   850  				Outputs:    outputs,
   851  				InitErrors: []string{"Resource failed to initialize"},
   852  			},
   853  		},
   854  	}
   855  
   856  	p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
   857  	snap := p.Run(t, old)
   858  
   859  	for _, resource := range snap.Resources {
   860  		switch urn := resource.URN; urn {
   861  		case provURN:
   862  			// break
   863  		case resURN:
   864  			assert.Empty(t, resource.InitErrors)
   865  			assert.Equal(t, idAfter, resource.ID)
   866  		default:
   867  			t.Fatalf("unexpected resource %v", urn)
   868  		}
   869  	}
   870  }
   871  
   872  // TestRefreshUpdateWithDeletedResource validates that the engine handles a deleted resource without error on an
   873  // update with refresh.
   874  func TestRefreshUpdateWithDeletedResource(t *testing.T) {
   875  	t.Parallel()
   876  
   877  	p := &TestPlan{}
   878  
   879  	resURN := p.NewURN("pkgA:m:typA", "resA", "")
   880  	idBefore := resource.ID("myid")
   881  
   882  	loaders := []*deploytest.ProviderLoader{
   883  		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
   884  			return &deploytest.Provider{
   885  				ReadF: func(
   886  					urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
   887  				) (plugin.ReadResult, resource.Status, error) {
   888  					return plugin.ReadResult{}, resource.StatusOK, nil
   889  				},
   890  			}, nil
   891  		}),
   892  	}
   893  
   894  	program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
   895  		return nil
   896  	})
   897  	host := deploytest.NewPluginHost(nil, nil, program, loaders...)
   898  
   899  	p.Options.Host = host
   900  	p.Options.Refresh = true
   901  
   902  	old := &deploy.Snapshot{
   903  		Resources: []*resource.State{
   904  			{
   905  				Type:    resURN.Type(),
   906  				URN:     resURN,
   907  				Custom:  true,
   908  				ID:      idBefore,
   909  				Inputs:  resource.PropertyMap{},
   910  				Outputs: resource.PropertyMap{},
   911  			},
   912  		},
   913  	}
   914  
   915  	p.Steps = []TestStep{{Op: Update}}
   916  	snap := p.Run(t, old)
   917  	assert.Equal(t, 0, len(snap.Resources))
   918  }