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 }