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

     1  //nolint:revive
     2  package lifecycletest
     3  
     4  import (
     5  	"context"
     6  	"reflect"
     7  	"sync"
     8  	"testing"
     9  
    10  	"github.com/mitchellh/copystructure"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    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/providers"
    17  	"github.com/pulumi/pulumi/pkg/v3/util/cancel"
    18  	"github.com/pulumi/pulumi/sdk/v3/go/common/display"
    19  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    20  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
    21  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    22  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    23  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
    24  	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
    25  )
    26  
    27  type updateInfo struct {
    28  	project workspace.Project
    29  	target  deploy.Target
    30  }
    31  
    32  func (u *updateInfo) GetRoot() string {
    33  	return ""
    34  }
    35  
    36  func (u *updateInfo) GetProject() *workspace.Project {
    37  	return &u.project
    38  }
    39  
    40  func (u *updateInfo) GetTarget() *deploy.Target {
    41  	return &u.target
    42  }
    43  
    44  func ImportOp(imports []deploy.Import) TestOp {
    45  	return TestOp(func(info UpdateInfo, ctx *Context, opts UpdateOptions,
    46  		dryRun bool) (*deploy.Plan, display.ResourceChanges, result.Result) {
    47  
    48  		return Import(info, ctx, opts, imports, dryRun)
    49  	})
    50  }
    51  
    52  type TestOp func(UpdateInfo, *Context, UpdateOptions, bool) (*deploy.Plan, display.ResourceChanges, result.Result)
    53  
    54  type ValidateFunc func(project workspace.Project, target deploy.Target, entries JournalEntries,
    55  	events []Event, res result.Result) result.Result
    56  
    57  func (op TestOp) Plan(project workspace.Project, target deploy.Target, opts UpdateOptions,
    58  	backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Plan, result.Result) {
    59  
    60  	plan, _, res := op.runWithContext(context.Background(), project, target, opts, true, backendClient, validate)
    61  	return plan, res
    62  }
    63  
    64  func (op TestOp) Run(project workspace.Project, target deploy.Target, opts UpdateOptions,
    65  	dryRun bool, backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
    66  
    67  	return op.RunWithContext(context.Background(), project, target, opts, dryRun, backendClient, validate)
    68  }
    69  
    70  func (op TestOp) RunWithContext(
    71  	callerCtx context.Context, project workspace.Project,
    72  	target deploy.Target, opts UpdateOptions, dryRun bool,
    73  	backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Snapshot, result.Result) {
    74  
    75  	_, snap, res := op.runWithContext(callerCtx, project, target, opts, dryRun, backendClient, validate)
    76  	return snap, res
    77  }
    78  
    79  func (op TestOp) runWithContext(
    80  	callerCtx context.Context, project workspace.Project,
    81  	target deploy.Target, opts UpdateOptions, dryRun bool,
    82  	backendClient deploy.BackendClient, validate ValidateFunc) (*deploy.Plan, *deploy.Snapshot, result.Result) {
    83  
    84  	// Create an appropriate update info and context.
    85  	info := &updateInfo{project: project, target: target}
    86  
    87  	cancelCtx, cancelSrc := cancel.NewContext(context.Background())
    88  	done := make(chan bool)
    89  	defer close(done)
    90  	go func() {
    91  		select {
    92  		case <-callerCtx.Done():
    93  			cancelSrc.Cancel()
    94  		case <-done:
    95  		}
    96  	}()
    97  
    98  	events := make(chan Event)
    99  	journal := NewJournal()
   100  
   101  	ctx := &Context{
   102  		Cancel:          cancelCtx,
   103  		Events:          events,
   104  		SnapshotManager: journal,
   105  		BackendClient:   backendClient,
   106  	}
   107  
   108  	// Begin draining events.
   109  	var wg sync.WaitGroup
   110  	var firedEvents []Event
   111  	wg.Add(1)
   112  	go func() {
   113  		for e := range events {
   114  			firedEvents = append(firedEvents, e)
   115  		}
   116  		wg.Done()
   117  	}()
   118  
   119  	// Run the step and its validator.
   120  	plan, _, res := op(info, ctx, opts, dryRun)
   121  	close(events)
   122  	wg.Wait()
   123  	contract.IgnoreClose(journal)
   124  
   125  	if validate != nil {
   126  		res = validate(project, target, journal.Entries(), firedEvents, res)
   127  	}
   128  	if dryRun {
   129  		return plan, nil, res
   130  	}
   131  
   132  	snap, err := journal.Snap(target.Snapshot)
   133  	if res == nil && err != nil {
   134  		res = result.FromError(err)
   135  	} else if res == nil && snap != nil {
   136  		res = result.WrapIfNonNil(snap.VerifyIntegrity())
   137  	}
   138  	return nil, snap, res
   139  }
   140  
   141  type TestStep struct {
   142  	Op            TestOp
   143  	ExpectFailure bool
   144  	SkipPreview   bool
   145  	Validate      ValidateFunc
   146  }
   147  
   148  func (t *TestStep) ValidateAnd(f ValidateFunc) {
   149  	o := t.Validate
   150  	t.Validate = func(project workspace.Project, target deploy.Target, entries JournalEntries,
   151  		events []Event, res result.Result) result.Result {
   152  		r := o(project, target, entries, events, res)
   153  		if r != nil {
   154  			return r
   155  		}
   156  		return f(project, target, entries, events, res)
   157  	}
   158  }
   159  
   160  type TestPlan struct {
   161  	Project        string
   162  	Stack          string
   163  	Runtime        string
   164  	RuntimeOptions map[string]interface{}
   165  	Config         config.Map
   166  	Decrypter      config.Decrypter
   167  	BackendClient  deploy.BackendClient
   168  	Options        UpdateOptions
   169  	Steps          []TestStep
   170  }
   171  
   172  // nolint: goconst
   173  func (p *TestPlan) getNames() (stack tokens.Name, project tokens.PackageName, runtime string) {
   174  	project = tokens.PackageName(p.Project)
   175  	if project == "" {
   176  		project = "test"
   177  	}
   178  	runtime = p.Runtime
   179  	if runtime == "" {
   180  		runtime = "test"
   181  	}
   182  	stack = tokens.Name(p.Stack)
   183  	if stack == "" {
   184  		stack = "test"
   185  	}
   186  	return stack, project, runtime
   187  }
   188  
   189  func (p *TestPlan) NewURN(typ tokens.Type, name string, parent resource.URN) resource.URN {
   190  	stack, project, _ := p.getNames()
   191  	var pt tokens.Type
   192  	if parent != "" {
   193  		pt = parent.Type()
   194  	}
   195  	return resource.NewURN(stack.Q(), project, pt, typ, tokens.QName(name))
   196  }
   197  
   198  func (p *TestPlan) NewProviderURN(pkg tokens.Package, name string, parent resource.URN) resource.URN {
   199  	return p.NewURN(providers.MakeProviderType(pkg), name, parent)
   200  }
   201  
   202  func (p *TestPlan) GetProject() workspace.Project {
   203  	_, projectName, runtime := p.getNames()
   204  
   205  	return workspace.Project{
   206  		Name:    projectName,
   207  		Runtime: workspace.NewProjectRuntimeInfo(runtime, p.RuntimeOptions),
   208  	}
   209  }
   210  
   211  func (p *TestPlan) GetTarget(t *testing.T, snapshot *deploy.Snapshot) deploy.Target {
   212  	stack, _, _ := p.getNames()
   213  
   214  	cfg := p.Config
   215  	if cfg == nil {
   216  		cfg = config.Map{}
   217  	}
   218  
   219  	return deploy.Target{
   220  		Name:      stack,
   221  		Config:    cfg,
   222  		Decrypter: p.Decrypter,
   223  		// note: it's really important that the preview and update operate on different snapshots.  the engine can and
   224  		// does mutate the snapshot in-place, even in previews, and sharing a snapshot between preview and update can
   225  		// cause state changes from the preview to persist even when doing an update.
   226  		Snapshot: CloneSnapshot(t, snapshot),
   227  	}
   228  }
   229  
   230  func assertIsErrorOrBailResult(t *testing.T, res result.Result) {
   231  	assert.NotNil(t, res)
   232  }
   233  
   234  // CloneSnapshot makes a deep copy of the given snapshot and returns a pointer to the clone.
   235  func CloneSnapshot(t *testing.T, snap *deploy.Snapshot) *deploy.Snapshot {
   236  	t.Helper()
   237  	if snap != nil {
   238  		copiedSnap := copystructure.Must(copystructure.Copy(*snap)).(deploy.Snapshot)
   239  		assert.True(t, reflect.DeepEqual(*snap, copiedSnap))
   240  		return &copiedSnap
   241  	}
   242  
   243  	return snap
   244  }
   245  
   246  func (p *TestPlan) Run(t *testing.T, snapshot *deploy.Snapshot) *deploy.Snapshot {
   247  	project := p.GetProject()
   248  	snap := snapshot
   249  	for _, step := range p.Steps {
   250  		// note: it's really important that the preview and update operate on different snapshots.  the engine can and
   251  		// does mutate the snapshot in-place, even in previews, and sharing a snapshot between preview and update can
   252  		// cause state changes from the preview to persist even when doing an update.
   253  		// GetTarget ALWAYS clones the snapshot, so the previewTarget.Snapshot != target.Snapshot
   254  		if !step.SkipPreview {
   255  			previewTarget := p.GetTarget(t, snap)
   256  			// Don't run validate on the preview step
   257  			_, res := step.Op.Run(project, previewTarget, p.Options, true, p.BackendClient, nil)
   258  			if step.ExpectFailure {
   259  				assertIsErrorOrBailResult(t, res)
   260  				continue
   261  			}
   262  
   263  			assert.Nil(t, res)
   264  		}
   265  
   266  		var res result.Result
   267  		target := p.GetTarget(t, snap)
   268  		snap, res = step.Op.Run(project, target, p.Options, false, p.BackendClient, step.Validate)
   269  		if step.ExpectFailure {
   270  			assertIsErrorOrBailResult(t, res)
   271  			continue
   272  		}
   273  
   274  		if res != nil {
   275  			if res.IsBail() {
   276  				t.Logf("Got unexpected bail result")
   277  				t.FailNow()
   278  			} else {
   279  				t.Logf("Got unexpected error result: %v", res.Error())
   280  				t.FailNow()
   281  			}
   282  		}
   283  
   284  		assert.Nil(t, res)
   285  	}
   286  
   287  	return snap
   288  }
   289  
   290  // resCount is the expected number of resources registered during this test.
   291  func MakeBasicLifecycleSteps(t *testing.T, resCount int) []TestStep {
   292  	return []TestStep{
   293  		// Initial update
   294  		{
   295  			Op: Update,
   296  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   297  				_ []Event, res result.Result) result.Result {
   298  
   299  				// Should see only creates or reads.
   300  				for _, entry := range entries {
   301  					op := entry.Step.Op()
   302  					assert.True(t, op == deploy.OpCreate || op == deploy.OpRead)
   303  				}
   304  				snap, err := entries.Snap(target.Snapshot)
   305  				require.NoError(t, err)
   306  				assert.Len(t, snap.Resources, resCount)
   307  				return res
   308  			},
   309  		},
   310  		// No-op refresh
   311  		{
   312  			Op: Refresh,
   313  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   314  				_ []Event, res result.Result) result.Result {
   315  
   316  				// Should see only refresh-sames.
   317  				for _, entry := range entries {
   318  					assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
   319  					assert.Equal(t, deploy.OpSame, entry.Step.(*deploy.RefreshStep).ResultOp())
   320  				}
   321  				snap, err := entries.Snap(target.Snapshot)
   322  				require.NoError(t, err)
   323  				assert.Len(t, snap.Resources, resCount)
   324  				return res
   325  			},
   326  		},
   327  		// No-op update
   328  		{
   329  			Op: Update,
   330  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   331  				_ []Event, res result.Result) result.Result {
   332  
   333  				// Should see only sames.
   334  				for _, entry := range entries {
   335  					op := entry.Step.Op()
   336  					assert.True(t, op == deploy.OpSame || op == deploy.OpRead)
   337  				}
   338  				snap, err := entries.Snap(target.Snapshot)
   339  				require.NoError(t, err)
   340  				assert.Len(t, snap.Resources, resCount)
   341  				return res
   342  			},
   343  		},
   344  		// No-op refresh
   345  		{
   346  			Op: Refresh,
   347  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   348  				_ []Event, res result.Result) result.Result {
   349  
   350  				// Should see only refresh-sames.
   351  				for _, entry := range entries {
   352  					assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
   353  					assert.Equal(t, deploy.OpSame, entry.Step.(*deploy.RefreshStep).ResultOp())
   354  				}
   355  				snap, err := entries.Snap(target.Snapshot)
   356  				require.NoError(t, err)
   357  				assert.Len(t, snap.Resources, resCount)
   358  				return res
   359  			},
   360  		},
   361  		// Destroy
   362  		{
   363  			Op: Destroy,
   364  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   365  				_ []Event, res result.Result) result.Result {
   366  
   367  				// Should see only deletes.
   368  				for _, entry := range entries {
   369  					switch entry.Step.Op() {
   370  					case deploy.OpDelete, deploy.OpReadDiscard:
   371  						// ok
   372  					default:
   373  						assert.Fail(t, "expected OpDelete or OpReadDiscard")
   374  					}
   375  				}
   376  				snap, err := entries.Snap(target.Snapshot)
   377  				require.NoError(t, err)
   378  				assert.Len(t, snap.Resources, 0)
   379  				return res
   380  			},
   381  		},
   382  		// No-op refresh
   383  		{
   384  			Op: Refresh,
   385  			Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
   386  				_ []Event, res result.Result) result.Result {
   387  
   388  				assert.Len(t, entries, 0)
   389  				snap, err := entries.Snap(target.Snapshot)
   390  				require.NoError(t, err)
   391  				assert.Len(t, snap.Resources, 0)
   392  				return res
   393  			},
   394  		},
   395  	}
   396  }