// 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 diy import ( "context" "encoding/json" "errors" "fmt" "os" "os/user" "path" "path/filepath" "time" "github.com/pulumi/pulumi/pkg/v3/backend" "github.com/pulumi/pulumi/sdk/v3/go/common/diag" "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/fsutil" "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" ) type lockContent struct { Pid int `json:"pid"` Username string `json:"username"` Hostname string `json:"hostname"` Timestamp time.Time `json:"timestamp"` } func newLockContent() (*lockContent, error) { u, err := user.Current() if err != nil { return nil, err } hostname, err := os.Hostname() if err != nil { return nil, err } return &lockContent{ Pid: os.Getpid(), Username: u.Username, Hostname: hostname, Timestamp: time.Now(), }, nil } // checkForLock looks for any existing locks for this stack, and returns a helpful diagnostic if there is one. func (b *diyBackend) checkForLock(ctx context.Context, stackRef backend.StackReference) error { stackName := stackRef.FullyQualifiedName() allFiles, err := listBucket(ctx, b.bucket, stackLockDir(stackName)) if err != nil { return err } // lockPath may return a path with backslashes (\) on Windows. // We need to convert it to a slash path (/) to compare it to // the keys in the bucket which are always slash paths. wantLock := filepath.ToSlash(b.lockPath(stackRef)) var lockKeys []string for _, file := range allFiles { if file.IsDir { continue } if file.Key != wantLock { lockKeys = append(lockKeys, file.Key) } } if len(lockKeys) > 0 { errorString := fmt.Sprintf("the stack is currently locked by %v lock(s). Either wait for the other "+ "process(es) to end or delete the lock file with `pulumi cancel`.", len(lockKeys)) for _, lock := range lockKeys { content, err := b.bucket.ReadAll(ctx, lock) if err != nil { return err } l := &lockContent{} err = json.Unmarshal(content, &l) if err != nil { return err } errorString += fmt.Sprintf("\n %v: created by %v@%v (pid %v) at %v", b.url+"/"+lock, l.Username, l.Hostname, l.Pid, l.Timestamp.Format(time.RFC3339), ) } return errors.New(errorString) } return nil } func (b *diyBackend) Lock(ctx context.Context, stackRef backend.StackReference) error { // err := b.checkForLock(ctx, stackRef) if err != nil { return err } lockContent, err := newLockContent() if err != nil { return err } content, err := json.Marshal(lockContent) if err != nil { return err } err = b.bucket.WriteAll(ctx, b.lockPath(stackRef), content, nil) if err != nil { return err } err = b.checkForLock(ctx, stackRef) if err != nil { b.Unlock(ctx, stackRef) return err } return nil } func (b *diyBackend) Unlock(ctx context.Context, stackRef backend.StackReference) { err := b.bucket.Delete(ctx, b.lockPath(stackRef)) if err != nil { b.d.Errorf( diag.Message("", "there was a problem deleting the lock at %v, manual clean up may be required: %v"), path.Join(b.url, b.lockPath(stackRef)), err) } } func lockDir() string { return path.Join(workspace.BookkeepingDir, workspace.LockDir) } func stackLockDir(stack tokens.QName) string { contract.Requiref(stack != "", "stack", "must not be empty") return path.Join(lockDir(), fsutil.QnamePath(stack)) } func (b *diyBackend) lockPath(stackRef backend.StackReference) string { contract.Requiref(stackRef != nil, "stack", "must not be nil") return path.Join(stackLockDir(stackRef.FullyQualifiedName()), b.lockID+".json") }