From fbeac6fc1036cc8081057b1071f884291575bb9e Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Thu, 11 Nov 2021 14:58:39 -0800 Subject: [PATCH] Improve corrupt workspace settings experience (#8393) * Improve corrupt workspace settings experience This improvement comes in two parts. 1. The error message for a corrupt workspace settings file now clearly indicates both the file and the parsing error. 2. Writing the workspace settings is now atomic. This prevents corruption from multiple concurrent calls. * Use builtin atomic write * Use builtin ioutil.TempFile * Change tmp file dir --- sdk/go/common/workspace/workspace.go | 31 ++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/sdk/go/common/workspace/workspace.go b/sdk/go/common/workspace/workspace.go index 6df82440e..0aaeedd5e 100644 --- a/sdk/go/common/workspace/workspace.go +++ b/sdk/go/common/workspace/workspace.go @@ -1,4 +1,4 @@ -// Copyright 2016-2018, Pulumi Corporation. +// 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. @@ -19,6 +19,7 @@ import ( "crypto/sha1" "encoding/hex" "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -105,7 +106,7 @@ func NewFrom(dir string) (W, error) { err = w.readSettings() if err != nil { - return nil, err + return nil, fmt.Errorf("unable to read workspace settings: %w", err) } upsertIntoCache(dir, w) @@ -139,8 +140,30 @@ func (pw *projectWorkspace) Save() error { if err != nil { return err } + return atomicWriteFile(settingsFile, b) +} - return ioutil.WriteFile(settingsFile, b, 0600) +// atomicWriteFile provides a rename based atomic write through a temporary file. +func atomicWriteFile(path string, b []byte) error { + tmp, err := ioutil.TempFile(filepath.Dir(path), filepath.Base(path)) + if err != nil { + return errors.Wrapf(err, "failed to create temporary file %s", path) + } + defer func() { contract.Ignore(os.Remove(tmp.Name())) }() + + if err = tmp.Chmod(0600); err != nil { + return errors.Wrap(err, "failed to set temporary file permission") + } + if _, err = tmp.Write(b); err != nil { + return errors.Wrap(err, "failed to write to temporary file") + } + if err = tmp.Sync(); err != nil { + return err + } + if err = tmp.Close(); err != nil { + return err + } + return os.Rename(tmp.Name(), path) } func (pw *projectWorkspace) readSettings() error { @@ -159,7 +182,7 @@ func (pw *projectWorkspace) readSettings() error { err = json.Unmarshal(b, &settings) if err != nil { - return err + return errors.Wrapf(err, "could not parse file %s", settingsPath) } pw.settings = &settings