-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
step_generator.go
1925 lines (1709 loc) 路 77 KB
/
step_generator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2016-2021, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deploy
import (
cryptorand "crypto/rand"
"fmt"
"strings"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/v3/resource/graph"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
)
// stepGenerator is responsible for turning resource events into steps that can be fed to the deployment executor.
// It does this by consulting the deployment and calculating the appropriate step action based on the requested goal
// state and the existing state of the world.
type stepGenerator struct {
deployment *Deployment // the deployment to which this step generator belongs
opts Options // options for this step generator
updateTargetsOpt UrnTargets // the set of resources to update; resources not in this set will be same'd
replaceTargetsOpt UrnTargets // the set of resoures to replace
// signals that one or more errors have been reported to the user, and the deployment should terminate
// in error. This primarily allows `preview` to aggregate many policy violation events and
// report them all at once.
sawError bool
urns map[resource.URN]bool // set of URNs discovered for this deployment
reads map[resource.URN]bool // set of URNs read for this deployment
deletes map[resource.URN]bool // set of URNs deleted in this deployment
replaces map[resource.URN]bool // set of URNs replaced in this deployment
updates map[resource.URN]bool // set of URNs updated in this deployment
creates map[resource.URN]bool // set of URNs created in this deployment
sames map[resource.URN]bool // set of URNs that were not changed in this deployment
// set of URNs that would have been created, but were filtered out because the user didn't
// specify them with --target
skippedCreates map[resource.URN]bool
pendingDeletes map[*resource.State]bool // set of resources (not URNs!) that are pending deletion
providers map[resource.URN]*resource.State // URN map of providers that we have seen so far.
// a map from URN to a list of property keys that caused the replacement of a dependent resource during a
// delete-before-replace.
dependentReplaceKeys map[resource.URN][]resource.PropertyKey
// a map from old names (aliased URNs) to the new URN that aliased to them.
aliased map[resource.URN]resource.URN
// a map from current URN of the resource to the old URN that it was aliased from.
aliases map[resource.URN]resource.URN
}
func (sg *stepGenerator) isTargetedUpdate() bool {
return sg.updateTargetsOpt.IsConstrained() || sg.replaceTargetsOpt.IsConstrained()
}
// isTargetedForUpdate returns if `res` is targeted for update. The function accommodates
// `--target-dependents`. `targetDependentsForUpdate` should probably be called if this function
// returns true.
func (sg *stepGenerator) isTargetedForUpdate(res *resource.State) bool {
if sg.updateTargetsOpt.Contains(res.URN) {
return true
} else if !sg.opts.TargetDependents {
return false
}
if res.Provider != "" {
res, err := providers.ParseReference(res.Provider)
contract.AssertNoError(err)
if sg.updateTargetsOpt.Contains(res.URN()) {
return true
}
}
if res.Parent != "" {
if sg.updateTargetsOpt.Contains(res.Parent) {
return true
}
}
for _, dep := range res.Dependencies {
if dep != "" && sg.updateTargetsOpt.Contains(dep) {
return true
}
}
return false
}
func (sg *stepGenerator) isTargetedReplace(urn resource.URN) bool {
return sg.replaceTargetsOpt.IsConstrained() && sg.replaceTargetsOpt.Contains(urn)
}
func (sg *stepGenerator) Errored() bool {
return sg.sawError
}
// checkParent checks that the parent given is valid for the given resource type, and returns a default parent
// if there is one.
func (sg *stepGenerator) checkParent(parent resource.URN, resourceType tokens.Type) (resource.URN, result.Result) {
// Some goal settings are based on the parent settings so make sure our parent is correct.
if resourceType == resource.RootStackType {
// The RootStack must not have a parent set
if parent != "" {
return "", result.Errorf("root stack resource can not have a parent (tried to set it to %v)", parent)
}
} else {
// For other resources they may or may not have a parent.
//
// TODO(fraser): I think every resource but the RootStack should have a parent, however currently a
// number of our tests do not create a RootStack resource, feels odd that it's possible for the engine
// to run without a RootStack resource. I feel this ought to be fixed by making the engine always
// create the RootStack before running the user program, however that leaves some questions of what to
// do if we ever support changing any of the settings (such as the provider map) on the RootStack
// resource. For now we set it to the root stack if we can find it, but we don't error on blank parents
// If it is set check the parent exists.
if parent != "" {
// The parent for this resource hasn't been registered yet. That's an error and we can't continue.
if _, hasParent := sg.urns[parent]; !hasParent {
return "", result.Errorf("could not find parent resource %v", parent)
}
} else {
// Else try and set it to the root stack
// TODO: It looks like this currently has some issues with state ordering (see
// https://github.com/pulumi/pulumi/issues/10950). Best I can guess is the stack resource is
// hitting the step generator and so saving it's URN to sg.urns and issuing a Create step but not
// actually getting to writing it's state to the snapshot. Then in parallel with this something
// else is causing a pulumi:providers:pulumi default provider to be created, this picks up the
// stack URN from sg.urns and so sets it's parent automatically, but then races the step executor
// to write itself to state before the stack resource manages to. Long term we want to ensure
// there's always a stack resource present, and so that all resources (except the stack) have a
// parent (this will save us some work in each SDK), but for now lets just turn this support off.
//for urn := range sg.urns {
// if urn.Type() == resource.RootStackType {
// return urn, nil
// }
//}
}
}
return parent, nil
}
// generateURN generates a URN for a new resource and confirms we haven't seen it before in this deployment.
func (sg *stepGenerator) generateURN(
parent resource.URN, ty tokens.Type, name tokens.QName) (resource.URN, result.Result) {
// Generate a URN for this new resource, confirm we haven't seen it before in this deployment.
urn := sg.deployment.generateURN(parent, ty, name)
if sg.urns[urn] {
// TODO[pulumi/pulumi-framework#19]: improve this error message!
sg.deployment.Diag().Errorf(diag.GetDuplicateResourceURNError(urn), urn)
return "", result.Bail()
}
sg.urns[urn] = true
return urn, nil
}
// GenerateReadSteps is responsible for producing one or more steps required to service
// a ReadResourceEvent coming from the language host.
func (sg *stepGenerator) GenerateReadSteps(event ReadResourceEvent) ([]Step, result.Result) {
// Some event settings are based on the parent settings so make sure our parent is correct.
parent, res := sg.checkParent(event.Parent(), event.Type())
if res != nil {
return nil, res
}
urn, res := sg.generateURN(parent, event.Type(), event.Name())
if res != nil {
return nil, res
}
newState := resource.NewState(event.Type(),
urn,
true, /*custom*/
false, /*delete*/
event.ID(),
event.Properties(),
make(resource.PropertyMap), /* outputs */
parent,
false, /*protect*/
true, /*external*/
event.Dependencies(),
nil, /* initErrors */
event.Provider(),
nil, /* propertyDependencies */
false, /* deleteBeforeCreate */
event.AdditionalSecretOutputs(),
nil, /* aliases */
nil, /* customTimeouts */
"", /* importID */
false, /* retainOnDelete */
"", /* deletedWith */
)
old, hasOld := sg.deployment.Olds()[urn]
if newState.ID == "" {
return nil, result.Errorf("Expected an ID for %v", urn)
}
// If the snapshot has an old resource for this URN and it's not external, we're going
// to have to delete the old resource and conceptually replace it with the resource we
// are about to read.
//
// We accomplish this through the "read-replacement" step, which atomically reads a resource
// and marks the resource it is replacing as pending deletion.
//
// In the event that the new "read" resource's ID matches the existing resource,
// we do not need to delete the resource - we know exactly what resource we are going
// to get from the read.
//
// This operation is tentatively called "relinquish" - it semantically represents the
// release of a resource from the management of Pulumi.
if hasOld && !old.External && old.ID != event.ID() {
logging.V(7).Infof(
"stepGenerator.GenerateReadSteps(...): replacing existing resource %s, ids don't match", urn)
sg.replaces[urn] = true
return []Step{
NewReadReplacementStep(sg.deployment, event, old, newState),
NewReplaceStep(sg.deployment, old, newState, nil, nil, nil, true),
}, nil
}
if bool(logging.V(7)) && hasOld && old.ID == event.ID() {
logging.V(7).Infof("stepGenerator.GenerateReadSteps(...): recognized relinquish of resource %s", urn)
}
sg.reads[urn] = true
return []Step{
NewReadStep(sg.deployment, event, old, newState),
}, nil
}
// GenerateSteps produces one or more steps required to achieve the goal state specified by the
// incoming RegisterResourceEvent.
//
// If the given resource is a custom resource, the step generator will invoke Diff and Check on the
// provider associated with that resource. If those fail, an error is returned.
func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, result.Result) {
steps, res := sg.generateSteps(event)
if res != nil {
contract.Assert(len(steps) == 0)
return nil, res
}
// Check each proposed step against the relevant resource plan, if any
for _, s := range steps {
logging.V(5).Infof("Checking step %s for %s", s.Op(), s.URN())
if sg.deployment.plan != nil {
if resourcePlan, ok := sg.deployment.plan.ResourcePlans[s.URN()]; ok {
if len(resourcePlan.Ops) == 0 {
return nil, result.Errorf("%v is not allowed by the plan: no more steps were expected for this resource", s.Op())
}
constraint := resourcePlan.Ops[0]
// We remove the Op from the list before doing the constraint check.
// This is because we look at Ops at the end to see if any expected operations didn't attempt to happen.
// This op has been attempted, it just might fail its constraint.
resourcePlan.Ops = resourcePlan.Ops[1:]
if !ConstrainedTo(s.Op(), constraint) {
return nil, result.Errorf("%v is not allowed by the plan: this resource is constrained to %v", s.Op(), constraint)
}
} else {
if !ConstrainedTo(s.Op(), OpSame) {
return nil, result.Errorf("%v is not allowed by the plan: no steps were expected for this resource", s.Op())
}
}
}
// If we're generating plans add the operation to the plan being generated
if sg.opts.GeneratePlan {
// Resource plan might be aliased
urn, isAliased := sg.aliased[s.URN()]
if !isAliased {
urn = s.URN()
}
resourcePlan, ok := sg.deployment.newPlans.get(urn)
if !ok {
return nil, result.Errorf("Expected a new resource plan for %v", urn)
}
resourcePlan.Ops = append(resourcePlan.Ops, s.Op())
}
}
if !sg.isTargetedUpdate() {
return steps, nil
}
// We got a set of steps to perform during a targeted update. If any of the steps are not same steps and depend on
// creates we skipped because they were not in the --target list, issue an error that that the create was necessary
// and that the user must target the resource to create.
for _, step := range steps {
if step.Op() == OpSame || step.New() == nil {
continue
}
for _, urn := range step.New().Dependencies {
if sg.skippedCreates[urn] {
// Targets were specified, but didn't include this resource to create. And a
// resource we are producing a step for does depend on this created resource.
// Give a particular error in that case to let them know. Also mark that we're
// in an error state so that we eventually will error out of the entire
// application run.
d := diag.GetResourceWillBeCreatedButWasNotSpecifiedInTargetList(step.URN())
sg.deployment.Diag().Errorf(d, step.URN(), urn)
sg.sawError = true
if !sg.deployment.preview {
// In preview we keep going so that the user will hear about all the problems and can then
// fix up their command once (as opposed to adding a target, rerunning, adding a target,
// rerunning, etc. etc.).
//
// Doing a normal run. We should not proceed here at all. We don't want to create
// something the user didn't ask for.
return nil, result.Bail()
}
// Remove the resource from the list of skipped creates so that we do not issue duplicate diagnostics.
delete(sg.skippedCreates, urn)
}
}
}
return steps, nil
}
func (sg *stepGenerator) collapseAliasToUrn(goal *resource.Goal, alias resource.Alias) resource.URN {
if alias.URN != "" {
return alias.URN
}
n := alias.Name
if n == "" {
n = string(goal.Name)
}
t := alias.Type
if t == "" {
t = string(goal.Type)
}
var parentType tokens.Type
// If alias.NoParent is true then parentType is blank, else we need to look if a parent URN is given
if !alias.NoParent() {
parentURN := alias.Parent
if parentURN == "" {
parentURN = goal.Parent
}
if parentURN != "" && parentURN.Type() != resource.RootStackType {
// Skip empty parents and don't use the root stack type; otherwise, use the full qualified type.
parentType = parentURN.QualifiedType()
}
}
project := alias.Project
if project == "" {
project = sg.deployment.source.Project().String()
}
stack := alias.Stack
if stack == "" {
stack = sg.deployment.Target().Name.String()
}
return resource.NewURN(tokens.QName(stack), tokens.PackageName(project), parentType, tokens.Type(t), tokens.QName(n))
}
// inheritedChildAlias computes the alias that should be applied to a child based on an alias applied to it's
// parent. This may involve changing the name of the resource in cases where the resource has a named derived
// from the name of the parent, and the parent name changed.
func (sg *stepGenerator) inheritedChildAlias(
childType tokens.Type,
childName, parentName tokens.QName,
parentAlias resource.URN) resource.URN {
// If the child name has the parent name as a prefix, then we make the assumption that
// it was constructed from the convention of using '{name}-details' as the name of the
// child resource. To ensure this is aliased correctly, we must then also replace the
// parent aliases name in the prefix of the child resource name.
//
// For example:
// * name: "newapp-function"
// * options.parent.__name: "newapp"
// * parentAlias: "urn:pulumi:stackname::projectname::awsx:ec2:Vpc::app"
// * parentAliasName: "app"
// * aliasName: "app-function"
// * childAlias: "urn:pulumi:stackname::projectname::aws:s3/bucket:Bucket::app-function"
aliasName := childName
if strings.HasPrefix(childName.String(), parentName.String()) {
aliasName = tokens.QName(
parentAlias.Name().String() +
strings.TrimPrefix(childName.String(), parentName.String()))
}
return resource.NewURN(
sg.deployment.Target().Name.Q(),
sg.deployment.source.Project(),
parentAlias.QualifiedType(),
childType,
aliasName)
}
func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, result.Result) {
var invalid bool // will be set to true if this object fails validation.
goal := event.Goal()
// Some goal settings are based on the parent settings so make sure our parent is correct.
parent, res := sg.checkParent(goal.Parent, goal.Type)
if res != nil {
return nil, res
}
goal.Parent = parent
urn, res := sg.generateURN(goal.Parent, goal.Type, goal.Name)
if res != nil {
return nil, res
}
// Generate the aliases for this resource
aliases := make(map[resource.URN]struct{}, 0)
for _, alias := range goal.Aliases {
urn := sg.collapseAliasToUrn(goal, alias)
aliases[urn] = struct{}{}
}
// Now multiply out any aliases our parent had.
if goal.Parent != "" {
if parentAlias, has := sg.aliases[goal.Parent]; has {
aliases[sg.inheritedChildAlias(goal.Type, goal.Name, goal.Parent.Name(), parentAlias)] = struct{}{}
for _, alias := range goal.Aliases {
childAlias := sg.collapseAliasToUrn(goal, alias)
aliasedChildType := childAlias.Type()
aliasedChildName := childAlias.Name()
inheritedAlias := sg.inheritedChildAlias(aliasedChildType, aliasedChildName, goal.Parent.Name(), parentAlias)
aliases[inheritedAlias] = struct{}{}
}
}
}
if previousAliasURN, alreadyAliased := sg.aliased[urn]; alreadyAliased {
// This resource is claiming to be X but we've already seen another resource claim that via aliases
invalid = true
sg.deployment.Diag().Errorf(diag.GetDuplicateResourceAliasedError(urn), urn, previousAliasURN)
}
// Check for an old resource so that we can figure out if this is a create, delete, etc., and/or
// to diff. We look up first by URN and then by any provided aliases. If it is found using an
// alias, record that alias so that we do not delete the aliased resource later.
var oldInputs resource.PropertyMap
var oldOutputs resource.PropertyMap
var old *resource.State
var hasOld bool
var alias []resource.Alias
aliases[urn] = struct{}{}
for urnOrAlias := range aliases {
old, hasOld = sg.deployment.Olds()[urnOrAlias]
if hasOld {
oldInputs = old.Inputs
oldOutputs = old.Outputs
if urnOrAlias != urn {
if _, alreadySeen := sg.urns[urnOrAlias]; alreadySeen {
// This resource is claiming to X but we've already seen that urn created
invalid = true
sg.deployment.Diag().Errorf(diag.GetDuplicateResourceAliasError(urn), urnOrAlias, urn, urn)
}
if previousAliasURN, alreadyAliased := sg.aliased[urnOrAlias]; alreadyAliased {
// This resource is claiming to be X but we've already seen another resource claim that
invalid = true
sg.deployment.Diag().Errorf(diag.GetDuplicateResourceAliasError(urn), urnOrAlias, urn, previousAliasURN)
}
sg.aliased[urnOrAlias] = urn
// register the alias with the provider registry
sg.deployment.providers.RegisterAlias(urn, urnOrAlias)
// NOTE: we save the URN of the existing resource so that the snapshotter can replace references to the
// existing resource with the URN of the newly-registered resource. We do not need to save any of the
// resource's other possible aliases.
alias = []resource.Alias{{URN: urnOrAlias}}
// Save the alias actually being used so we can look it up later if anything has this as a parent
sg.aliases[urn] = urnOrAlias
}
break
}
}
// Create the desired inputs from the goal state
inputs := goal.Properties
if hasOld {
// Set inputs back to their old values (if any) for any "ignored" properties
processedInputs, res := processIgnoreChanges(inputs, oldInputs, goal.IgnoreChanges)
if res != nil {
return nil, res
}
inputs = processedInputs
}
aliasUrns := make([]resource.URN, len(alias))
for i, a := range alias {
aliasUrns[i] = a.URN
}
// Produce a new state object that we'll build up as operations are performed. Ultimately, this is what will
// get serialized into the checkpoint file.
new := resource.NewState(goal.Type, urn, goal.Custom, false, "", inputs, nil, goal.Parent, goal.Protect, false,
goal.Dependencies, goal.InitErrors, goal.Provider, goal.PropertyDependencies, false,
goal.AdditionalSecretOutputs, aliasUrns, &goal.CustomTimeouts, "", goal.RetainOnDelete, goal.DeletedWith)
// Mark the URN/resource as having been seen. So we can run analyzers on all resources seen, as well as
// lookup providers for calculating replacement of resources that use the provider.
sg.deployment.goals.set(urn, goal)
if providers.IsProviderType(goal.Type) {
sg.providers[urn] = new
}
// Fetch the provider for this resource.
prov, res := sg.loadResourceProvider(urn, goal.Custom, goal.Provider, goal.Type)
if res != nil {
return nil, res
}
// We only allow unknown property values to be exposed to the provider if we are performing an update preview.
allowUnknowns := sg.deployment.preview
// We may be re-creating this resource if it got deleted earlier in the execution of this deployment.
_, recreating := sg.deletes[urn]
// We may be creating this resource if it previously existed in the snapshot as an External resource
wasExternal := hasOld && old.External
// If we have a plan for this resource we need to feed the saved seed to Check to remove non-determinism
var randomSeed []byte
if sg.deployment.plan != nil {
if resourcePlan, ok := sg.deployment.plan.ResourcePlans[urn]; ok {
randomSeed = resourcePlan.Seed
}
}
// If the above didn't set the seed, generate a new random one. If we're running with plans but this
// resource was missing a seed then if the seed is used later checks will fail.
if randomSeed == nil {
randomSeed = make([]byte, 32)
n, err := cryptorand.Read(randomSeed)
contract.AssertNoError(err)
contract.Assert(n == len(randomSeed))
}
// If the goal contains an ID, this may be an import. An import occurs if there is no old resource or if the old
// resource's ID does not match the ID in the goal state.
var oldImportID resource.ID
if hasOld {
oldImportID = old.ID
// If the old resource has an ImportID, look at that rather than the ID, since some resources use a different
// format of identifier for the import input than the ID property.
if old.ImportID != "" {
oldImportID = old.ImportID
}
}
isImport := goal.Custom && goal.ID != "" && (!hasOld || old.External || oldImportID != goal.ID)
if isImport {
// TODO(seqnum) Not sure how sequence numbers should interact with imports
// Write the ID of the resource to import into the new state and return an ImportStep or an
// ImportReplacementStep
new.ID = goal.ID
new.ImportID = goal.ID
// If we're generating plans create a plan, Imports have no diff, just a goal state
if sg.opts.GeneratePlan {
newResourcePlan := &ResourcePlan{
Seed: randomSeed,
Goal: NewGoalPlan(nil, goal)}
sg.deployment.newPlans.set(urn, newResourcePlan)
}
if isReplace := hasOld && !recreating; isReplace {
return []Step{
NewImportReplacementStep(sg.deployment, event, old, new, goal.IgnoreChanges, randomSeed),
NewReplaceStep(sg.deployment, old, new, nil, nil, nil, true),
}, nil
}
return []Step{NewImportStep(sg.deployment, event, new, goal.IgnoreChanges, randomSeed)}, nil
}
// Ensure the provider is okay with this resource and fetch the inputs to pass to subsequent methods.
var err error
if prov != nil {
var failures []plugin.CheckFailure
// If we are re-creating this resource because it was deleted earlier, the old inputs are now
// invalid (they got deleted) so don't consider them. Similarly, if the old resource was External,
// don't consider those inputs since Pulumi does not own them. Finally, if the resource has been
// targeted for replacement, ignore its old state.
if recreating || wasExternal || sg.isTargetedReplace(urn) || !hasOld {
inputs, failures, err = prov.Check(urn, nil, goal.Properties, allowUnknowns, randomSeed)
} else {
inputs, failures, err = prov.Check(urn, oldInputs, inputs, allowUnknowns, randomSeed)
}
if err != nil {
return nil, result.FromError(err)
} else if issueCheckErrors(sg.deployment, new, urn, failures) {
invalid = true
}
new.Inputs = inputs
}
// If the resource is valid and we're generating plans then generate a plan
if !invalid && sg.opts.GeneratePlan {
if recreating || wasExternal || sg.isTargetedReplace(urn) || !hasOld {
oldInputs = nil
}
inputDiff := oldInputs.Diff(inputs)
// Generate the output goal plan, if we're recreating this it should already exist
if recreating {
plan, ok := sg.deployment.newPlans.get(urn)
if !ok {
return nil, result.FromError(fmt.Errorf("no plan for resource %v", urn))
}
// The plan will have had it's Ops already partially filled in for the delete operation, but we
// now have the information needed to fill in Seed and Goal.
plan.Seed = randomSeed
plan.Goal = NewGoalPlan(inputDiff, goal)
} else {
newResourcePlan := &ResourcePlan{
Seed: randomSeed,
Goal: NewGoalPlan(inputDiff, goal)}
sg.deployment.newPlans.set(urn, newResourcePlan)
}
}
// If there is a plan for this resource, validate that the program goal conforms to the plan.
// If theres no plan for this resource check that nothing has been changed.
// We don't check plans if the resource is invalid, it's going to fail anyway.
if !invalid && sg.deployment.plan != nil {
resourcePlan, ok := sg.deployment.plan.ResourcePlans[urn]
if !ok {
if old == nil {
// We could error here, but we'll trigger an error later on anyway that Create isn't valid here
} else if err := checkMissingPlan(old, inputs, goal); err != nil {
return nil, result.FromError(fmt.Errorf("resource %s violates plan: %w", urn, err))
}
} else {
if err := resourcePlan.checkGoal(oldInputs, inputs, goal); err != nil {
return nil, result.FromError(fmt.Errorf("resource %s violates plan: %w", urn, err))
}
}
}
// Send the resource off to any Analyzers before being operated on.
analyzers := sg.deployment.ctx.Host.ListAnalyzers()
for _, analyzer := range analyzers {
r := plugin.AnalyzerResource{
URN: new.URN,
Type: new.Type,
Name: new.URN.Name(),
Properties: inputs,
Options: plugin.AnalyzerResourceOptions{
Protect: new.Protect,
IgnoreChanges: goal.IgnoreChanges,
DeleteBeforeReplace: goal.DeleteBeforeReplace,
AdditionalSecretOutputs: new.AdditionalSecretOutputs,
Aliases: new.GetAliases(),
CustomTimeouts: new.CustomTimeouts,
},
}
providerResource := sg.getProviderResource(new.URN, new.Provider)
if providerResource != nil {
r.Provider = &plugin.AnalyzerProviderResource{
URN: providerResource.URN,
Type: providerResource.Type,
Name: providerResource.URN.Name(),
Properties: providerResource.Inputs,
}
}
diagnostics, err := analyzer.Analyze(r)
if err != nil {
return nil, result.FromError(err)
}
for _, d := range diagnostics {
if d.EnforcementLevel == apitype.Mandatory {
if !sg.deployment.preview {
invalid = true
}
sg.sawError = true
}
// For now, we always use the URN we have here rather than a URN specified with the diagnostic.
sg.opts.Events.OnPolicyViolation(new.URN, d)
}
}
// If the resource isn't valid, don't proceed any further.
if invalid {
return nil, result.Bail()
}
// There are four cases we need to consider when figuring out what to do with this resource.
//
// Case 1: recreating
// In this case, we have seen a resource with this URN before and we have already issued a
// delete step for it. This happens when the engine has to delete a resource before it has
// enough information about whether that resource still exists. A concrete example is
// when a resource depends on a resource that is delete-before-replace: the engine must first
// delete the dependent resource before depending the DBR resource, but the engine can't know
// yet whether the dependent resource is being replaced or deleted.
//
// In this case, we are seeing the resource again after deleting it, so it must be a replacement.
//
// Logically, recreating implies hasOld, since in order to delete something it must have
// already existed.
contract.Assert(!recreating || hasOld)
if recreating {
logging.V(7).Infof("Planner decided to re-create replaced resource '%v' deleted due to dependent DBR", urn)
// Unmark this resource as deleted, we now know it's being replaced instead.
delete(sg.deletes, urn)
sg.replaces[urn] = true
keys := sg.dependentReplaceKeys[urn]
return []Step{
NewReplaceStep(sg.deployment, old, new, nil, nil, nil, false),
NewCreateReplacementStep(sg.deployment, event, old, new, keys, nil, nil, false),
}, nil
}
// Case 2: wasExternal
// In this case, the resource we are operating upon exists in the old snapshot, but it
// was "external" - Pulumi does not own its lifecycle. Conceptually, this operation is
// akin to "taking ownership" of a resource that we did not previously control.
//
// Since we are not allowed to manipulate the existing resource, we must create a resource
// to take its place. Since this is technically a replacement operation, we pend deletion of
// read until the end of the deployment.
if wasExternal {
logging.V(7).Infof("Planner recognized '%s' as old external resource, creating instead", urn)
sg.creates[urn] = true
if err != nil {
return nil, result.FromError(err)
}
return []Step{
NewCreateReplacementStep(sg.deployment, event, old, new, nil, nil, nil, true),
NewReplaceStep(sg.deployment, old, new, nil, nil, nil, true),
}, nil
}
isTargeted := sg.isTargetedForUpdate(new)
if isTargeted {
sg.updateTargetsOpt.addLiteral(urn)
}
// Case 3: hasOld
// In this case, the resource we are operating upon now exists in the old snapshot.
// It must be an update or a replace. Which operation we do depends on the the specific change made to the
// resource's properties:
//
// - if the user has requested that only specific resources be updated, and this resource is
// not in that set, do no 'Diff' and just treat the resource as 'same' (i.e. unchanged).
//
// - If the resource's provider reference changed, the resource must be replaced. This behavior is founded upon
// the assumption that providers are recreated iff their configuration changed in such a way that they are no
// longer able to manage existing resources.
//
// - Otherwise, we invoke the resource's provider's `Diff` method. If this method indicates that the resource must
// be replaced, we do so. If it does not, we update the resource in place.
if hasOld {
contract.Assert(old != nil)
// If the user requested only specific resources to update, and this resource was not in
// that set, then do nothing but create a SameStep for it.
if !isTargeted {
logging.V(7).Infof(
"Planner decided not to update '%v' due to not being in target group (same) (inputs=%v)", urn, new.Inputs)
} else {
updateSteps, res := sg.generateStepsFromDiff(
event, urn, old, new, oldInputs, oldOutputs, inputs, prov, goal, randomSeed)
if res != nil {
return nil, res
}
if len(updateSteps) > 0 {
// 'Diff' produced update steps. We're done at this point.
return updateSteps, nil
}
// Diff didn't produce any steps for this resource. Fall through and indicate that it
// is same/unchanged.
logging.V(7).Infof("Planner decided not to update '%v' after diff (same) (inputs=%v)", urn, new.Inputs)
}
// No need to update anything, the properties didn't change.
sg.sames[urn] = true
return []Step{NewSameStep(sg.deployment, event, old, new)}, nil
}
// Case 4: Not Case 1, 2, or 3
// If a resource isn't being recreated and it's not being updated or replaced,
// it's just being created.
// We're in the create stage now. In a normal run just issue a 'create step'. If, however, the
// user is doing a run with `--target`s, then we need to operate specially here.
//
// 1. If the user did include this resource urn in the --target list, then we can proceed
// normally and issue a create step for this.
//
// 2. However, if they did not include the resource in the --target list, then we want to flat
// out ignore it (just like we ignore updates to resource not in the --target list). This has
// interesting implications though. Specifically, what to do if a prop from this resource is
// then actually needed by a property we *are* doing a targeted create/update for.
//
// In that case, we want to error to force the user to be explicit about wanting this resource
// to be created. However, we can't issue the error until later on when the resource is
// referenced. So, to support this we create a special "same" step here for this resource. That
// "same" step has a bit on it letting us know that it is for this case. If we then later see a
// resource that depends on this resource, we will issue an error letting the user know.
//
// We will also not record this non-created resource into the checkpoint as it doesn't actually
// exist.
if !isTargeted && !providers.IsProviderType(goal.Type) {
sg.sames[urn] = true
sg.skippedCreates[urn] = true
return []Step{NewSkippedCreateStep(sg.deployment, event, new)}, nil
}
sg.creates[urn] = true
logging.V(7).Infof("Planner decided to create '%v' (inputs=%v)", urn, new.Inputs)
return []Step{NewCreateStep(sg.deployment, event, new)}, nil
}
func (sg *stepGenerator) generateStepsFromDiff(
event RegisterResourceEvent, urn resource.URN, old, new *resource.State,
oldInputs, oldOutputs, inputs resource.PropertyMap,
prov plugin.Provider, goal *resource.Goal, randomSeed []byte) ([]Step, result.Result) {
// We only allow unknown property values to be exposed to the provider if we are performing an update preview.
allowUnknowns := sg.deployment.preview
diff, err := sg.diff(urn, old, new, oldInputs, oldOutputs, inputs, prov, allowUnknowns, goal.IgnoreChanges)
// If the plugin indicated that the diff is unavailable, assume that the resource will be updated and
// report the message contained in the error.
if _, ok := err.(plugin.DiffUnavailableError); ok {
diff = plugin.DiffResult{Changes: plugin.DiffSome}
sg.deployment.ctx.Diag.Warningf(diag.RawMessage(urn, err.Error()))
} else if err != nil {
return nil, result.FromError(err)
}
// Ensure that we received a sensible response.
if diff.Changes != plugin.DiffNone && diff.Changes != plugin.DiffSome {
return nil, result.Errorf(
"unrecognized diff state for %s: %d", urn, diff.Changes)
}
hasInitErrors := len(old.InitErrors) > 0
// Update the diff to apply any replaceOnChanges annotations and to include initErrors in the diff.
diff, err = applyReplaceOnChanges(diff, goal.ReplaceOnChanges, hasInitErrors)
if err != nil {
return nil, result.FromError(err)
}
// If there were changes check for a replacement vs. an in-place update.
if diff.Changes == plugin.DiffSome {
if diff.Replace() {
// If this resource is protected we can't replace it because that entails a delete
// Note that we do allow unprotecting and replacing to happen in a single update
// cycle, we don't look at old.Protect here.
if new.Protect && old.Protect {
message := fmt.Sprintf("unable to replace resource %q\n"+
"as it is currently marked for protection. To unprotect the resource, "+
"remove the `protect` flag from the resource in your Pulumi "+
"program and run `pulumi up`", urn)
sg.deployment.ctx.Diag.Errorf(diag.StreamMessage(urn, message, 0))
sg.sawError = true
return nil, result.Bail()
}
// If the goal state specified an ID, issue an error: the replacement will change the ID, and is
// therefore incompatible with the goal state.
if goal.ID != "" {
const message = "previously-imported resources that still specify an ID may not be replaced; " +
"please remove the `import` declaration from your program"
if sg.deployment.preview {
sg.deployment.ctx.Diag.Warningf(diag.StreamMessage(urn, message, 0))
} else {
return nil, result.Errorf(message)
}
}
sg.replaces[urn] = true
// If we are going to perform a replacement, we need to recompute the default values. The above logic
// had assumed that we were going to carry them over from the old resource, which is no longer true.
//
// Note that if we're performing a targeted replace, we already have the correct inputs.
if prov != nil && !sg.isTargetedReplace(urn) {
var failures []plugin.CheckFailure
inputs, failures, err = prov.Check(urn, nil, goal.Properties, allowUnknowns, randomSeed)
if err != nil {
return nil, result.FromError(err)
} else if issueCheckErrors(sg.deployment, new, urn, failures) {
return nil, result.Bail()
}
new.Inputs = inputs
}
if logging.V(7) {
logging.V(7).Infof("Planner decided to replace '%v' (oldprops=%v inputs=%v replaceKeys=%v)",
urn, oldInputs, new.Inputs, diff.ReplaceKeys)
}
// We have two approaches to performing replacements:
//
// * CreateBeforeDelete: the default mode first creates a new instance of the resource, then
// updates all dependent resources to point to the new one, and finally after all of that,
// deletes the old resource. This ensures minimal downtime.
//
// * DeleteBeforeCreate: this mode can be used for resources that cannot be tolerate having
// side-by-side old and new instances alive at once. This first deletes the resource and
// then creates the new one. This may result in downtime, so is less preferred. Note that
// until pulumi/pulumi#624 is resolved, we cannot safely perform this operation on resources
// that have dependent resources (we try to delete the resource while they refer to it).
//
// The provider is responsible for requesting which of these two modes to use. The user can override
// the provider's decision by setting the `deleteBeforeReplace` field of `ResourceOptions` to either
// `true` or `false`.
deleteBeforeReplace := diff.DeleteBeforeReplace
if goal.DeleteBeforeReplace != nil {
deleteBeforeReplace = *goal.DeleteBeforeReplace
}
if deleteBeforeReplace {
logging.V(7).Infof("Planner decided to delete-before-replacement for resource '%v'", urn)
contract.Assert(sg.deployment.depGraph != nil)
// DeleteBeforeCreate implies that we must immediately delete the resource. For correctness,
// we must also eagerly delete all resources that depend directly or indirectly on the resource
// being replaced and would be replaced by a change to the relevant dependency.
//
// To do this, we'll utilize the dependency information contained in the snapshot if it is
// trustworthy, which is interpreted by the DependencyGraph type.
var steps []Step
if sg.opts.TrustDependencies {
toReplace, res := sg.calculateDependentReplacements(old)
if res != nil {
return nil, res
}
// Deletions must occur in reverse dependency order, and `deps` is returned in dependency
// order, so we iterate in reverse.
for i := len(toReplace) - 1; i >= 0; i-- {
dependentResource := toReplace[i].res
// If we already deleted this resource due to some other DBR, don't do it again.
if sg.pendingDeletes[dependentResource] {
continue
}
// If we're generating plans create a plan for this delete
if sg.opts.GeneratePlan {
if _, ok := sg.deployment.newPlans.get(dependentResource.URN); !ok {
// We haven't see this resource before, create a new
// resource plan for it with no goal (because it's going to be a delete)
resourcePlan := &ResourcePlan{}
sg.deployment.newPlans.set(dependentResource.URN, resourcePlan)
}
}
sg.dependentReplaceKeys[dependentResource.URN] = toReplace[i].keys
logging.V(7).Infof("Planner decided to delete '%v' due to dependence on condemned resource '%v'",
dependentResource.URN, urn)
// This resource might already be pending-delete
if dependentResource.Delete {
steps = append(steps, NewDeleteStep(sg.deployment, sg.deletes, dependentResource))
} else {
steps = append(steps, NewDeleteReplacementStep(sg.deployment, sg.deletes, dependentResource, true))
}