-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
apply.go
310 lines (265 loc) 路 10.3 KB
/
apply.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
// Copyright 2016-2018, 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 backend
import (
"bytes"
"context"
"fmt"
"os"
"strings"
survey "github.com/AlecAivazis/survey/v2"
surveycore "github.com/AlecAivazis/survey/v2/core"
"github.com/pulumi/pulumi/pkg/v3/backend/display"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
sdkDisplay "github.com/pulumi/pulumi/sdk/v3/go/common/display"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
)
// ApplierOptions is a bag of configuration settings for an Applier.
type ApplierOptions struct {
// DryRun indicates if the update should not change any resource state and instead just preview changes.
DryRun bool
// ShowLink indicates if a link to the update persisted result can be displayed.
ShowLink bool
}
// Applier applies the changes specified by this update operation against the target stack.
type Applier func(ctx context.Context, kind apitype.UpdateKind, stack Stack, op UpdateOperation,
opts ApplierOptions, events chan<- engine.Event) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result)
func ActionLabel(kind apitype.UpdateKind, dryRun bool) string {
v := updateTextMap[kind]
contract.Assert(v.previewText != "")
contract.Assert(v.text != "")
if dryRun {
return "Previewing " + v.previewText
}
return v.text
}
var updateTextMap = map[apitype.UpdateKind]struct {
previewText string
text string
}{
apitype.PreviewUpdate: {"update", "Previewing"},
apitype.UpdateUpdate: {"update", "Updating"},
apitype.RefreshUpdate: {"refresh", "Refreshing"},
apitype.DestroyUpdate: {"destroy", "Destroying"},
apitype.StackImportUpdate: {"stack import", "Importing"},
apitype.ResourceImportUpdate: {"import", "Importing"},
}
type response string
const (
yes response = "yes"
yesPlan response = "[experimental] yes, using Update Plans (https://pulumi.com/updateplans)"
no response = "no"
details response = "details"
)
func PreviewThenPrompt(ctx context.Context, kind apitype.UpdateKind, stack Stack,
op UpdateOperation, apply Applier) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) {
// create a channel to hear about the update events from the engine. this will be used so that
// we can build up the diff display in case the user asks to see the details of the diff
// Note that eventsChannel is not closed in a `defer`. It is generally unsafe to do so, since defers run during
// panics and we can't know whether or not we were in the middle of writing to this channel when the panic occurred.
//
// Instead of using a `defer`, we manually close `eventsChannel` on every exit of this function.
eventsChannel := make(chan engine.Event)
var events []engine.Event
go func() {
// pull the events from the channel and store them locally
for e := range eventsChannel {
if e.Type == engine.ResourcePreEvent ||
e.Type == engine.ResourceOutputsEvent ||
e.Type == engine.SummaryEvent {
events = append(events, e)
}
}
}()
// Perform the update operations, passing true for dryRun, so that we get a preview.
// We perform the preview (DryRun), but don't display the cloud link since the
// thing the user cares about would be the link to the actual update if they
// confirm the prompt.
opts := ApplierOptions{
DryRun: true,
ShowLink: true,
}
plan, changes, res := apply(ctx, kind, stack, op, opts, eventsChannel)
if res != nil {
close(eventsChannel)
return plan, changes, res
}
// If there are no changes, or we're auto-approving or just previewing, we can skip the confirmation prompt.
if op.Opts.AutoApprove || kind == apitype.PreviewUpdate {
close(eventsChannel)
return plan, changes, nil
}
infoPrefix := "\b" + op.Opts.Display.Color.Colorize(colors.SpecWarning+"info: "+colors.Reset)
if kind != apitype.UpdateUpdate {
// If not an update, we can skip displaying warnings
} else if countResources(events) == 0 {
// This is an update and there are no resources being CREATED
fmt.Print(infoPrefix, "There are no resources in your stack (other than the stack resource).\n\n")
}
// Otherwise, ensure the user wants to proceed.
res, plan = confirmBeforeUpdating(kind, stack, events, plan, op.Opts)
close(eventsChannel)
return plan, changes, res
}
// confirmBeforeUpdating asks the user whether to proceed. A nil error means yes.
func confirmBeforeUpdating(kind apitype.UpdateKind, stack Stack,
events []engine.Event, plan *deploy.Plan, opts UpdateOptions) (result.Result, *deploy.Plan) {
for {
var response string
surveycore.DisableColor = true
surveyIcons := survey.WithIcons(func(icons *survey.IconSet) {
icons.Question = survey.Icon{}
icons.SelectFocus = survey.Icon{Text: opts.Display.Color.Colorize(colors.BrightGreen + ">" + colors.Reset)}
})
choices := []string{string(yes), string(no)}
// If we have a new plan but didn't start with a plan we can prompt to use the new plan.
// If we're in experimental mode we don't add this because "yes" will also use the plan
if plan != nil && opts.Engine.Plan == nil && !opts.Engine.Experimental {
choices = append([]string{string(yesPlan)}, choices...)
}
// For non-previews, we can also offer a detailed summary.
if !opts.SkipPreview {
choices = append(choices, string(details))
}
var previewWarning string
if opts.SkipPreview {
previewWarning = colors.SpecWarning + " without a preview" + colors.Bold
}
// Create a prompt. If this is a refresh, we'll add some extra text so it's clear we aren't updating resources.
prompt := "\b" + opts.Display.Color.Colorize(
colors.SpecPrompt+fmt.Sprintf("Do you want to perform this %s%s?",
updateTextMap[kind].previewText, previewWarning)+colors.Reset)
if kind == apitype.RefreshUpdate {
prompt += "\n" +
opts.Display.Color.Colorize(colors.SpecImportant+
"No resources will be modified as part of this refresh; just your stack's state will be.\n"+
colors.Reset)
}
// Now prompt the user for a yes, no, or details, and then proceed accordingly.
if err := survey.AskOne(&survey.Select{
Message: prompt,
Options: choices,
Default: string(no),
}, &response, surveyIcons); err != nil {
return result.FromError(fmt.Errorf("confirmation cancelled, not proceeding with the %s: %w", kind, err)), nil
}
if response == string(no) {
fmt.Printf("confirmation declined, not proceeding with the %s\n", kind)
return result.Bail(), nil
}
if response == string(yes) {
// If we're in experimental mode always use the plan
if opts.Engine.Experimental {
return nil, plan
}
return nil, nil
}
if response == string(yesPlan) {
return nil, plan
}
if response == string(details) {
diff := createDiff(kind, events, opts.Display)
_, err := os.Stdout.WriteString(diff + "\n")
contract.IgnoreError(err)
continue
}
}
}
func PreviewThenPromptThenExecute(ctx context.Context, kind apitype.UpdateKind, stack Stack,
op UpdateOperation, apply Applier) (sdkDisplay.ResourceChanges, result.Result) {
// Preview the operation to the user and ask them if they want to proceed.
if !op.Opts.SkipPreview {
// We want to run the preview with the given plan and then run the full update with the initial plan as well,
// but because plans are mutated as they're checked we need to clone it here.
// We want to use the original plan because a program could be non-deterministic and have a plan of
// operations P0, the update preview could return P1, and then the actual update could run P2, were P1 < P2 < P0.
var originalPlan *deploy.Plan
if op.Opts.Engine.Plan != nil {
originalPlan = op.Opts.Engine.Plan.Clone()
}
plan, changes, res := PreviewThenPrompt(ctx, kind, stack, op, apply)
if res != nil || kind == apitype.PreviewUpdate {
return changes, res
}
// If we had an original plan use it, else if prompt said to use the plan from Preview then use the
// newly generated plan
if originalPlan != nil {
op.Opts.Engine.Plan = originalPlan
} else if plan != nil {
op.Opts.Engine.Plan = plan
} else {
op.Opts.Engine.Plan = nil
}
}
// Perform the change (!DryRun) and show the cloud link to the result.
// We don't care about the events it issues, so just pass a nil channel along.
opts := ApplierOptions{
DryRun: false,
ShowLink: true,
}
// No need to generate a plan at this stage, there's no way for the system or user to extract the plan
// after here.
op.Opts.Engine.GeneratePlan = false
_, changes, res := apply(ctx, kind, stack, op, opts, nil /*events*/)
return changes, res
}
func countResources(events []engine.Event) int {
count := 0
for _, e := range events {
if e.Type != engine.ResourcePreEvent {
continue
}
p, ok := e.Payload().(engine.ResourcePreEventPayload)
if !ok {
continue
}
if p.Metadata.Type.String() == "pulumi:pulumi:Stack" {
continue
}
count++
}
return count
}
func createDiff(updateKind apitype.UpdateKind, events []engine.Event, displayOpts display.Options) string {
buff := &bytes.Buffer{}
seen := make(map[resource.URN]engine.StepEventMetadata)
displayOpts.SummaryDiff = true
outputEventsDiff := make([]string, 0)
for _, e := range events {
if e.Type == engine.SummaryEvent {
continue
}
msg := display.RenderDiffEvent(e, seen, displayOpts)
if msg == "" {
continue
}
// display output events last
if e.Type == engine.ResourceOutputsEvent {
outputEventsDiff = append(outputEventsDiff, msg)
continue
}
_, err := buff.WriteString(msg)
contract.IgnoreError(err)
}
for _, msg := range outputEventsDiff {
_, err := buff.WriteString(msg)
contract.IgnoreError(err)
}
return strings.TrimSpace(buff.String())
}