github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/filestate/backend.go (about) 1 // Copyright 2016-2022, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package filestate 16 17 import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "net/url" 23 "os" 24 "path" 25 "path/filepath" 26 "regexp" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/gofrs/uuid" 32 33 user "github.com/tweekmonster/luser" 34 "gocloud.dev/blob" 35 _ "gocloud.dev/blob/azureblob" // driver for azblob:// 36 _ "gocloud.dev/blob/fileblob" // driver for file:// 37 "gocloud.dev/blob/gcsblob" // driver for gs:// 38 _ "gocloud.dev/blob/s3blob" // driver for s3:// 39 "gocloud.dev/gcerrors" 40 41 "github.com/pulumi/pulumi/pkg/v3/authhelpers" 42 "github.com/pulumi/pulumi/pkg/v3/backend" 43 "github.com/pulumi/pulumi/pkg/v3/backend/display" 44 "github.com/pulumi/pulumi/pkg/v3/engine" 45 "github.com/pulumi/pulumi/pkg/v3/operations" 46 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 47 "github.com/pulumi/pulumi/pkg/v3/resource/edit" 48 "github.com/pulumi/pulumi/pkg/v3/resource/stack" 49 "github.com/pulumi/pulumi/pkg/v3/util/validation" 50 "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" 51 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 52 "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" 53 sdkDisplay "github.com/pulumi/pulumi/sdk/v3/go/common/display" 54 "github.com/pulumi/pulumi/sdk/v3/go/common/encoding" 55 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" 56 "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" 57 "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" 58 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 59 "github.com/pulumi/pulumi/sdk/v3/go/common/util/result" 60 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 61 ) 62 63 // PulumiFilestateGzipEnvVar is an env var that must be truthy 64 // to enable gzip compression when using the filestate backend. 65 const PulumiFilestateGzipEnvVar = "PULUMI_SELF_MANAGED_STATE_GZIP" 66 67 // Backend extends the base backend interface with specific information about local backends. 68 type Backend interface { 69 backend.Backend 70 local() // at the moment, no local specific info, so just use a marker function. 71 } 72 73 type localBackend struct { 74 d diag.Sink 75 76 // originalURL is the URL provided when the localBackend was initialized, for example 77 // "file://~". url is a canonicalized version that should be used when persisting data. 78 // (For example, replacing ~ with the home directory, making an absolute path, etc.) 79 originalURL string 80 url string 81 82 bucket Bucket 83 mutex sync.Mutex 84 85 lockID string 86 87 gzip bool 88 } 89 90 type localBackendReference struct { 91 name tokens.Name 92 } 93 94 func (r localBackendReference) String() string { 95 return string(r.name) 96 } 97 98 func (r localBackendReference) Name() tokens.Name { 99 return r.name 100 } 101 102 func IsFileStateBackendURL(urlstr string) bool { 103 u, err := url.Parse(urlstr) 104 if err != nil { 105 return false 106 } 107 108 return blob.DefaultURLMux().ValidBucketScheme(u.Scheme) 109 } 110 111 const FilePathPrefix = "file://" 112 113 func New(d diag.Sink, originalURL string) (Backend, error) { 114 if !IsFileStateBackendURL(originalURL) { 115 return nil, fmt.Errorf("local URL %s has an illegal prefix; expected one of: %s", 116 originalURL, strings.Join(blob.DefaultURLMux().BucketSchemes(), ", ")) 117 } 118 119 u, err := massageBlobPath(originalURL) 120 if err != nil { 121 return nil, err 122 } 123 124 p, err := url.Parse(u) 125 if err != nil { 126 return nil, err 127 } 128 129 blobmux := blob.DefaultURLMux() 130 131 // for gcp we want to support additional credentials 132 // schemes on top of go-cloud's default credentials mux. 133 if p.Scheme == gcsblob.Scheme { 134 blobmux, err = authhelpers.GoogleCredentialsMux(context.TODO()) 135 if err != nil { 136 return nil, err 137 } 138 } 139 140 bucket, err := blobmux.OpenBucket(context.TODO(), u) 141 if err != nil { 142 return nil, fmt.Errorf("unable to open bucket %s: %w", u, err) 143 } 144 145 if !strings.HasPrefix(u, FilePathPrefix) { 146 bucketSubDir := strings.TrimLeft(p.Path, "/") 147 if bucketSubDir != "" { 148 if !strings.HasSuffix(bucketSubDir, "/") { 149 bucketSubDir += "/" 150 } 151 152 bucket = blob.PrefixedBucket(bucket, bucketSubDir) 153 } 154 } 155 156 isAcc, err := bucket.IsAccessible(context.TODO()) 157 if err != nil { 158 return nil, fmt.Errorf("unable to check if bucket %s is accessible: %w", u, err) 159 } 160 if !isAcc { 161 return nil, fmt.Errorf("bucket %s is not accessible", u) 162 } 163 164 // Allocate a unique lock ID for this backend instance. 165 lockID, err := uuid.NewV4() 166 if err != nil { 167 return nil, err 168 } 169 170 gzipCompression := cmdutil.IsTruthy(os.Getenv(PulumiFilestateGzipEnvVar)) 171 172 return &localBackend{ 173 d: d, 174 originalURL: originalURL, 175 url: u, 176 bucket: &wrappedBucket{bucket: bucket}, 177 lockID: lockID.String(), 178 gzip: gzipCompression, 179 }, nil 180 } 181 182 // massageBlobPath takes the path the user provided and converts it to an appropriate form go-cloud 183 // can support. Importantly, s3/azblob/gs paths should not be be touched. This will only affect 184 // file:// paths which have a few oddities around them that we want to ensure work properly. 185 func massageBlobPath(path string) (string, error) { 186 if !strings.HasPrefix(path, FilePathPrefix) { 187 // Not a file:// path. Keep this untouched and pass directly to gocloud. 188 return path, nil 189 } 190 191 // Strip off the "file://" portion so we can examine and determine what to do with the rest. 192 path = strings.TrimPrefix(path, FilePathPrefix) 193 194 // We need to specially handle ~. The shell doesn't take care of this for us, and later 195 // functions we run into can't handle this either. 196 // 197 // From https://stackoverflow.com/questions/17609732/expand-tilde-to-home-directory 198 if strings.HasPrefix(path, "~") { 199 usr, err := user.Current() 200 if err != nil { 201 return "", fmt.Errorf("Could not determine current user to resolve `file://~` path.: %w", err) 202 } 203 204 if path == "~" { 205 path = usr.HomeDir 206 } else { 207 path = filepath.Join(usr.HomeDir, path[2:]) 208 } 209 } 210 211 // For file:// backend, ensure a relative path is resolved. fileblob only supports absolute paths. 212 path, err := filepath.Abs(path) 213 if err != nil { 214 return "", fmt.Errorf("An IO error occurred while building the absolute path: %w", err) 215 } 216 217 // Using example from https://godoc.org/gocloud.dev/blob/fileblob#example-package--OpenBucket 218 // On Windows, convert "\" to "/" and add a leading "/". (See https://gocloud.dev/howto/blob/#local) 219 path = filepath.ToSlash(path) 220 if os.PathSeparator != '/' && !strings.HasPrefix(path, "/") { 221 path = "/" + path 222 } 223 224 return FilePathPrefix + path, nil 225 } 226 227 func Login(d diag.Sink, url string) (Backend, error) { 228 be, err := New(d, url) 229 if err != nil { 230 return nil, err 231 } 232 return be, workspace.StoreAccount(be.URL(), workspace.Account{}, true) 233 } 234 235 func (b *localBackend) local() {} 236 237 func (b *localBackend) Name() string { 238 name, err := os.Hostname() 239 contract.IgnoreError(err) 240 if name == "" { 241 name = "local" 242 } 243 return name 244 } 245 246 func (b *localBackend) URL() string { 247 return b.originalURL 248 } 249 250 func (b *localBackend) StateDir() string { 251 return workspace.BookkeepingDir 252 } 253 254 func (b *localBackend) GetPolicyPack(ctx context.Context, policyPack string, 255 d diag.Sink) (backend.PolicyPack, error) { 256 257 return nil, fmt.Errorf("File state backend does not support resource policy") 258 } 259 260 func (b *localBackend) ListPolicyGroups(ctx context.Context, orgName string, _ backend.ContinuationToken) ( 261 apitype.ListPolicyGroupsResponse, backend.ContinuationToken, error) { 262 return apitype.ListPolicyGroupsResponse{}, nil, fmt.Errorf("File state backend does not support resource policy") 263 } 264 265 func (b *localBackend) ListPolicyPacks(ctx context.Context, orgName string, _ backend.ContinuationToken) ( 266 apitype.ListPolicyPacksResponse, backend.ContinuationToken, error) { 267 return apitype.ListPolicyPacksResponse{}, nil, fmt.Errorf("File state backend does not support resource policy") 268 } 269 270 func (b *localBackend) SupportsTags() bool { 271 return false 272 } 273 274 func (b *localBackend) SupportsOrganizations() bool { 275 return false 276 } 277 278 func (b *localBackend) ParseStackReference(stackRefName string) (backend.StackReference, error) { 279 if err := b.ValidateStackName(stackRefName); err != nil { 280 return nil, err 281 } 282 return localBackendReference{name: tokens.Name(stackRefName)}, nil 283 } 284 285 // ValidateStackName verifies the stack name is valid for the local backend. We use the same rules as the 286 // httpstate backend. 287 func (b *localBackend) ValidateStackName(stackName string) error { 288 if strings.Contains(stackName, "/") { 289 return errors.New("stack names may not contain slashes") 290 } 291 292 validNameRegex := regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$") 293 if !validNameRegex.MatchString(stackName) { 294 return errors.New( 295 "stack names are limited to 100 characters and may only contain alphanumeric, hyphens, underscores, or periods") 296 } 297 298 return nil 299 } 300 301 func (b *localBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) { 302 // Local backends don't really have multiple projects, so just return false here. 303 return false, nil 304 } 305 306 func (b *localBackend) CreateStack(ctx context.Context, stackRef backend.StackReference, 307 opts interface{}) (backend.Stack, error) { 308 309 err := b.Lock(ctx, stackRef) 310 if err != nil { 311 return nil, err 312 } 313 defer b.Unlock(ctx, stackRef) 314 315 contract.Requiref(opts == nil, "opts", "local stacks do not support any options") 316 317 stackName := stackRef.Name() 318 if stackName == "" { 319 return nil, errors.New("invalid empty stack name") 320 } 321 322 if _, _, err := b.getStack(ctx, stackName); err == nil { 323 return nil, &backend.StackAlreadyExistsError{StackName: string(stackName)} 324 } 325 326 tags, err := backend.GetEnvironmentTagsForCurrentStack() 327 if err != nil { 328 return nil, fmt.Errorf("getting stack tags: %w", err) 329 } 330 if err = validation.ValidateStackProperties(string(stackName), tags); err != nil { 331 return nil, fmt.Errorf("validating stack properties: %w", err) 332 } 333 334 file, err := b.saveStack(stackName, nil, nil) 335 if err != nil { 336 return nil, err 337 } 338 339 stack := newStack(stackRef, file, nil, b) 340 fmt.Printf("Created stack '%s'\n", stack.Ref()) 341 342 return stack, nil 343 } 344 345 func (b *localBackend) GetStack(ctx context.Context, stackRef backend.StackReference) (backend.Stack, error) { 346 stackName := stackRef.Name() 347 snapshot, path, err := b.getStack(ctx, stackName) 348 349 switch { 350 case gcerrors.Code(err) == gcerrors.NotFound: 351 return nil, nil 352 case err != nil: 353 return nil, err 354 default: 355 return newStack(stackRef, path, snapshot, b), nil 356 } 357 } 358 359 func (b *localBackend) ListStacks( 360 ctx context.Context, _ backend.ListStacksFilter, _ backend.ContinuationToken) ( 361 []backend.StackSummary, backend.ContinuationToken, error) { 362 stacks, err := b.getLocalStacks() 363 if err != nil { 364 return nil, nil, err 365 } 366 367 // Note that the provided stack filter is not honored, since fields like 368 // organizations and tags aren't persisted in the local backend. 369 var results []backend.StackSummary 370 for _, stackName := range stacks { 371 chk, err := b.getCheckpoint(stackName) 372 if err != nil { 373 return nil, nil, err 374 } 375 stackRef, err := b.ParseStackReference(string(stackName)) 376 if err != nil { 377 return nil, nil, err 378 } 379 results = append(results, newLocalStackSummary(stackRef, chk)) 380 } 381 382 return results, nil, nil 383 } 384 385 func (b *localBackend) RemoveStack(ctx context.Context, stack backend.Stack, force bool) (bool, error) { 386 387 err := b.Lock(ctx, stack.Ref()) 388 if err != nil { 389 return false, err 390 } 391 defer b.Unlock(ctx, stack.Ref()) 392 393 stackName := stack.Ref().Name() 394 snapshot, _, err := b.getStack(ctx, stackName) 395 if err != nil { 396 return false, err 397 } 398 399 // Don't remove stacks that still have resources. 400 if !force && snapshot != nil && len(snapshot.Resources) > 0 { 401 return true, errors.New("refusing to remove stack because it still contains resources") 402 } 403 404 return false, b.removeStack(stackName) 405 } 406 407 func (b *localBackend) RenameStack(ctx context.Context, stack backend.Stack, 408 newName tokens.QName) (backend.StackReference, error) { 409 410 err := b.Lock(ctx, stack.Ref()) 411 if err != nil { 412 return nil, err 413 } 414 defer b.Unlock(ctx, stack.Ref()) 415 416 // Get the current state from the stack to be renamed. 417 stackName := stack.Ref().Name() 418 snap, _, err := b.getStack(ctx, stackName) 419 if err != nil { 420 return nil, err 421 } 422 423 // Ensure the new stack name is valid. 424 newRef, err := b.ParseStackReference(string(newName)) 425 if err != nil { 426 return nil, err 427 } 428 429 newStackName := newRef.Name() 430 431 // Ensure the destination stack does not already exist. 432 hasExisting, err := b.bucket.Exists(ctx, b.stackPath(newStackName)) 433 if err != nil { 434 return nil, err 435 } 436 if hasExisting { 437 return nil, fmt.Errorf("a stack named %s already exists", newName) 438 } 439 440 // If we have a snapshot, we need to rename the URNs inside it to use the new stack name. 441 if snap != nil { 442 if err = edit.RenameStack(snap, newStackName, ""); err != nil { 443 return nil, err 444 } 445 } 446 447 // Now save the snapshot with a new name (we pass nil to re-use the existing secrets manager from the snapshot). 448 if _, err = b.saveStack(newStackName, snap, nil); err != nil { 449 return nil, err 450 } 451 452 // To remove the old stack, just make a backup of the file and don't write out anything new. 453 file := b.stackPath(stackName) 454 backupTarget(b.bucket, file, false) 455 456 // And rename the history folder as well. 457 if err = b.renameHistory(stackName, newStackName); err != nil { 458 return nil, err 459 } 460 return newRef, err 461 } 462 463 func (b *localBackend) GetLatestConfiguration(ctx context.Context, 464 stack backend.Stack) (config.Map, error) { 465 466 hist, err := b.GetHistory(ctx, stack.Ref(), 1 /*pageSize*/, 1 /*page*/) 467 if err != nil { 468 return nil, err 469 } 470 if len(hist) == 0 { 471 return nil, backend.ErrNoPreviousDeployment 472 } 473 474 return hist[0].Config, nil 475 } 476 477 func (b *localBackend) PackPolicies( 478 ctx context.Context, policyPackRef backend.PolicyPackReference, 479 cancellationScopes backend.CancellationScopeSource, 480 callerEventsOpt chan<- engine.Event) result.Result { 481 482 return result.Error("File state backend does not support resource policy") 483 } 484 485 func (b *localBackend) Preview(ctx context.Context, stack backend.Stack, 486 op backend.UpdateOperation) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) { 487 488 // We can skip PreviewThenPromptThenExecute and just go straight to Execute. 489 opts := backend.ApplierOptions{ 490 DryRun: true, 491 ShowLink: true, 492 } 493 return b.apply(ctx, apitype.PreviewUpdate, stack, op, opts, nil /*events*/) 494 } 495 496 func (b *localBackend) Update(ctx context.Context, stack backend.Stack, 497 op backend.UpdateOperation) (sdkDisplay.ResourceChanges, result.Result) { 498 499 err := b.Lock(ctx, stack.Ref()) 500 if err != nil { 501 return nil, result.FromError(err) 502 } 503 defer b.Unlock(ctx, stack.Ref()) 504 505 return backend.PreviewThenPromptThenExecute(ctx, apitype.UpdateUpdate, stack, op, b.apply) 506 } 507 508 func (b *localBackend) Import(ctx context.Context, stack backend.Stack, 509 op backend.UpdateOperation, imports []deploy.Import) (sdkDisplay.ResourceChanges, result.Result) { 510 511 err := b.Lock(ctx, stack.Ref()) 512 if err != nil { 513 return nil, result.FromError(err) 514 } 515 defer b.Unlock(ctx, stack.Ref()) 516 517 op.Imports = imports 518 return backend.PreviewThenPromptThenExecute(ctx, apitype.ResourceImportUpdate, stack, op, b.apply) 519 } 520 521 func (b *localBackend) Refresh(ctx context.Context, stack backend.Stack, 522 op backend.UpdateOperation) (sdkDisplay.ResourceChanges, result.Result) { 523 524 err := b.Lock(ctx, stack.Ref()) 525 if err != nil { 526 return nil, result.FromError(err) 527 } 528 defer b.Unlock(ctx, stack.Ref()) 529 530 return backend.PreviewThenPromptThenExecute(ctx, apitype.RefreshUpdate, stack, op, b.apply) 531 } 532 533 func (b *localBackend) Destroy(ctx context.Context, stack backend.Stack, 534 op backend.UpdateOperation) (sdkDisplay.ResourceChanges, result.Result) { 535 536 err := b.Lock(ctx, stack.Ref()) 537 if err != nil { 538 return nil, result.FromError(err) 539 } 540 defer b.Unlock(ctx, stack.Ref()) 541 542 return backend.PreviewThenPromptThenExecute(ctx, apitype.DestroyUpdate, stack, op, b.apply) 543 } 544 545 func (b *localBackend) Query(ctx context.Context, op backend.QueryOperation) result.Result { 546 547 return b.query(ctx, op, nil /*events*/) 548 } 549 550 func (b *localBackend) Watch(ctx context.Context, stack backend.Stack, 551 op backend.UpdateOperation, paths []string) result.Result { 552 return backend.Watch(ctx, b, stack, op, b.apply, paths) 553 } 554 555 // apply actually performs the provided type of update on a locally hosted stack. 556 func (b *localBackend) apply( 557 ctx context.Context, kind apitype.UpdateKind, stack backend.Stack, 558 op backend.UpdateOperation, opts backend.ApplierOptions, 559 events chan<- engine.Event) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) { 560 561 stackRef := stack.Ref() 562 stackName := stackRef.Name() 563 actionLabel := backend.ActionLabel(kind, opts.DryRun) 564 565 if !(op.Opts.Display.JSONDisplay || op.Opts.Display.Type == display.DisplayWatch) { 566 // Print a banner so it's clear this is a local deployment. 567 fmt.Printf(op.Opts.Display.Color.Colorize( 568 colors.SpecHeadline+"%s (%s):"+colors.Reset+"\n"), actionLabel, stackRef) 569 } 570 571 // Start the update. 572 update, err := b.newUpdate(ctx, stackName, op) 573 if err != nil { 574 return nil, nil, result.FromError(err) 575 } 576 577 // Spawn a display loop to show events on the CLI. 578 displayEvents := make(chan engine.Event) 579 displayDone := make(chan bool) 580 go display.ShowEvents( 581 strings.ToLower(actionLabel), kind, stackName, op.Proj.Name, 582 displayEvents, displayDone, op.Opts.Display, opts.DryRun) 583 584 // Create a separate event channel for engine events that we'll pipe to both listening streams. 585 engineEvents := make(chan engine.Event) 586 587 scope := op.Scopes.NewScope(engineEvents, opts.DryRun) 588 eventsDone := make(chan bool) 589 go func() { 590 // Pull in all events from the engine and send them to the two listeners. 591 for e := range engineEvents { 592 displayEvents <- e 593 594 // If the caller also wants to see the events, stream them there also. 595 if events != nil { 596 events <- e 597 } 598 } 599 600 close(eventsDone) 601 }() 602 603 // Create the management machinery. 604 persister := b.newSnapshotPersister(stackName, op.SecretsManager) 605 manager := backend.NewSnapshotManager(persister, update.GetTarget().Snapshot) 606 engineCtx := &engine.Context{ 607 Cancel: scope.Context(), 608 Events: engineEvents, 609 SnapshotManager: manager, 610 BackendClient: backend.NewBackendClient(b), 611 } 612 613 // Perform the update 614 start := time.Now().Unix() 615 var plan *deploy.Plan 616 var changes sdkDisplay.ResourceChanges 617 var updateRes result.Result 618 switch kind { 619 case apitype.PreviewUpdate: 620 plan, changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, true) 621 case apitype.UpdateUpdate: 622 _, changes, updateRes = engine.Update(update, engineCtx, op.Opts.Engine, opts.DryRun) 623 case apitype.ResourceImportUpdate: 624 _, changes, updateRes = engine.Import(update, engineCtx, op.Opts.Engine, op.Imports, opts.DryRun) 625 case apitype.RefreshUpdate: 626 _, changes, updateRes = engine.Refresh(update, engineCtx, op.Opts.Engine, opts.DryRun) 627 case apitype.DestroyUpdate: 628 _, changes, updateRes = engine.Destroy(update, engineCtx, op.Opts.Engine, opts.DryRun) 629 default: 630 contract.Failf("Unrecognized update kind: %s", kind) 631 } 632 end := time.Now().Unix() 633 634 // Wait for the display to finish showing all the events. 635 <-displayDone 636 scope.Close() // Don't take any cancellations anymore, we're shutting down. 637 close(engineEvents) 638 contract.IgnoreClose(manager) 639 640 // Make sure the goroutine writing to displayEvents and events has exited before proceeding. 641 <-eventsDone 642 close(displayEvents) 643 644 // Save update results. 645 backendUpdateResult := backend.SucceededResult 646 if updateRes != nil { 647 backendUpdateResult = backend.FailedResult 648 } 649 info := backend.UpdateInfo{ 650 Kind: kind, 651 StartTime: start, 652 Message: op.M.Message, 653 Environment: op.M.Environment, 654 Config: update.GetTarget().Config, 655 Result: backendUpdateResult, 656 EndTime: end, 657 // IDEA: it would be nice to populate the *Deployment, so that addToHistory below doesn't need to 658 // rudely assume it knows where the checkpoint file is on disk as it makes a copy of it. This isn't 659 // trivial to achieve today given the event driven nature of plan-walking, however. 660 ResourceChanges: changes, 661 } 662 663 var saveErr error 664 var backupErr error 665 if !opts.DryRun { 666 saveErr = b.addToHistory(stackName, info) 667 backupErr = b.backupStack(stackName) 668 } 669 670 if updateRes != nil { 671 // We swallow saveErr and backupErr as they are less important than the updateErr. 672 return plan, changes, updateRes 673 } 674 675 if saveErr != nil { 676 // We swallow backupErr as it is less important than the saveErr. 677 return plan, changes, result.FromError(fmt.Errorf("saving update info: %w", saveErr)) 678 } 679 680 if backupErr != nil { 681 return plan, changes, result.FromError(fmt.Errorf("saving backup: %w", backupErr)) 682 } 683 684 // Make sure to print a link to the stack's checkpoint before exiting. 685 if !op.Opts.Display.SuppressPermalink && opts.ShowLink && !op.Opts.Display.JSONDisplay { 686 // Note we get a real signed link for aws/azure/gcp links. But no such option exists for 687 // file:// links so we manually create the link ourselves. 688 var link string 689 if strings.HasPrefix(b.url, FilePathPrefix) { 690 u, _ := url.Parse(b.url) 691 u.Path = filepath.ToSlash(path.Join(u.Path, b.stackPath(stackName))) 692 link = u.String() 693 } else { 694 link, err = b.bucket.SignedURL(context.TODO(), b.stackPath(stackName), nil) 695 if err != nil { 696 // set link to be empty to when there is an error to hide use of Permalinks 697 link = "" 698 699 // we log a warning here rather then returning an error to avoid exiting 700 // pulumi with an error code. 701 // printing a statefile perma link happens after all the providers have finished 702 // deploying the infrastructure, failing the pulumi update because there was a 703 // problem printing a statefile perma link can be missleading in automated CI environments. 704 cmdutil.Diag().Warningf(diag.Message("", "Unable to create signed url for current backend to "+ 705 "create a Permalink. Please visit https://www.pulumi.com/docs/troubleshooting/ "+ 706 "for more information\n")) 707 } 708 } 709 710 if link != "" { 711 fmt.Printf(op.Opts.Display.Color.Colorize( 712 colors.SpecHeadline+"Permalink: "+ 713 colors.Underline+colors.BrightBlue+"%s"+colors.Reset+"\n"), link) 714 } 715 } 716 717 return plan, changes, nil 718 } 719 720 // query executes a query program against the resource outputs of a locally hosted stack. 721 func (b *localBackend) query(ctx context.Context, op backend.QueryOperation, 722 callerEventsOpt chan<- engine.Event) result.Result { 723 724 return backend.RunQuery(ctx, b, op, callerEventsOpt, b.newQuery) 725 } 726 727 func (b *localBackend) GetHistory( 728 ctx context.Context, 729 stackRef backend.StackReference, 730 pageSize int, 731 page int) ([]backend.UpdateInfo, error) { 732 stackName := stackRef.Name() 733 updates, err := b.getHistory(stackName, pageSize, page) 734 if err != nil { 735 return nil, err 736 } 737 return updates, nil 738 } 739 740 func (b *localBackend) GetLogs(ctx context.Context, stack backend.Stack, cfg backend.StackConfiguration, 741 query operations.LogQuery) ([]operations.LogEntry, error) { 742 743 stackName := stack.Ref().Name() 744 target, err := b.getTarget(ctx, stackName, cfg.Config, cfg.Decrypter) 745 if err != nil { 746 return nil, err 747 } 748 749 return GetLogsForTarget(target, query) 750 } 751 752 // GetLogsForTarget fetches stack logs using the config, decrypter, and checkpoint in the given target. 753 func GetLogsForTarget(target *deploy.Target, query operations.LogQuery) ([]operations.LogEntry, error) { 754 contract.Assert(target != nil) 755 756 if target.Snapshot == nil { 757 // If the stack has not been deployed yet, return no logs. 758 return nil, nil 759 } 760 761 config, err := target.Config.Decrypt(target.Decrypter) 762 if err != nil { 763 return nil, err 764 } 765 766 components := operations.NewResourceTree(target.Snapshot.Resources) 767 ops := components.OperationsProvider(config) 768 logs, err := ops.GetLogs(query) 769 if logs == nil { 770 return nil, err 771 } 772 return *logs, err 773 } 774 775 func (b *localBackend) ExportDeployment(ctx context.Context, 776 stk backend.Stack) (*apitype.UntypedDeployment, error) { 777 778 stackName := stk.Ref().Name() 779 snap, _, err := b.getStack(ctx, stackName) 780 if err != nil { 781 return nil, err 782 } 783 784 if snap == nil { 785 snap = deploy.NewSnapshot(deploy.Manifest{}, nil, nil, nil) 786 } 787 788 sdep, err := stack.SerializeDeployment(snap, snap.SecretsManager /* showSecrsts */, false) 789 if err != nil { 790 return nil, fmt.Errorf("serializing deployment: %w", err) 791 } 792 793 data, err := encoding.JSON.Marshal(sdep) 794 if err != nil { 795 return nil, err 796 } 797 798 return &apitype.UntypedDeployment{ 799 Version: 3, 800 Deployment: json.RawMessage(data), 801 }, nil 802 } 803 804 func (b *localBackend) ImportDeployment(ctx context.Context, stk backend.Stack, 805 deployment *apitype.UntypedDeployment) error { 806 807 err := b.Lock(ctx, stk.Ref()) 808 if err != nil { 809 return err 810 } 811 defer b.Unlock(ctx, stk.Ref()) 812 813 stackName := stk.Ref().Name() 814 _, _, err = b.getStack(ctx, stackName) 815 if err != nil { 816 return err 817 } 818 819 snap, err := stack.DeserializeUntypedDeployment(ctx, deployment, stack.DefaultSecretsProvider) 820 if err != nil { 821 return err 822 } 823 824 _, err = b.saveStack(stackName, snap, snap.SecretsManager) 825 return err 826 } 827 828 func (b *localBackend) Logout() error { 829 return workspace.DeleteAccount(b.originalURL) 830 } 831 832 func (b *localBackend) LogoutAll() error { 833 return workspace.DeleteAllAccounts() 834 } 835 836 func (b *localBackend) CurrentUser() (string, []string, error) { 837 user, err := user.Current() 838 if err != nil { 839 return "", nil, err 840 } 841 return user.Username, nil, nil 842 } 843 844 func (b *localBackend) getLocalStacks() ([]tokens.Name, error) { 845 var stacks []tokens.Name 846 847 // Read the stack directory. 848 path := b.stackPath("") 849 850 files, err := listBucket(b.bucket, path) 851 if err != nil { 852 return nil, fmt.Errorf("error listing stacks: %w", err) 853 } 854 855 for _, file := range files { 856 // Ignore directories. 857 if file.IsDir { 858 continue 859 } 860 861 // Skip files without valid extensions (e.g., *.bak files). 862 stackfn := objectName(file) 863 ext := filepath.Ext(stackfn) 864 // But accept gzip compression 865 if ext == encoding.GZIPExt { 866 stackfn = strings.TrimSuffix(stackfn, encoding.GZIPExt) 867 ext = filepath.Ext(stackfn) 868 } 869 870 if _, has := encoding.Marshalers[ext]; !has { 871 continue 872 } 873 874 // Read in this stack's information. 875 name := tokens.Name(stackfn[:len(stackfn)-len(ext)]) 876 877 stacks = append(stacks, name) 878 } 879 880 return stacks, nil 881 } 882 883 // UpdateStackTags updates the stacks's tags, replacing all existing tags. 884 func (b *localBackend) UpdateStackTags(ctx context.Context, 885 stack backend.Stack, tags map[apitype.StackTagName]string) error { 886 887 // The local backend does not currently persist tags. 888 return errors.New("stack tags not supported in --local mode") 889 } 890 891 func (b *localBackend) CancelCurrentUpdate(ctx context.Context, stackRef backend.StackReference) error { 892 // Try to delete ALL the lock files 893 allFiles, err := listBucket(b.bucket, stackLockDir(stackRef.Name())) 894 if err != nil { 895 // Don't error if it just wasn't found 896 if gcerrors.Code(err) == gcerrors.NotFound { 897 return nil 898 } 899 return err 900 } 901 902 for _, file := range allFiles { 903 if file.IsDir { 904 continue 905 } 906 907 err := b.bucket.Delete(ctx, file.Key) 908 if err != nil { 909 // Race condition, don't error if the file was delete between us calling list and now 910 if gcerrors.Code(err) == gcerrors.NotFound { 911 return nil 912 } 913 return err 914 } 915 } 916 917 return nil 918 }