mirror of https://github.com/pulumi/pulumi.git
211 lines
5.5 KiB
Go
211 lines
5.5 KiB
Go
// 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 workspace
|
|
|
|
import (
|
|
//nolint:gosec
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
)
|
|
|
|
// W offers functionality for interacting with Pulumi workspaces.
|
|
type W interface {
|
|
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
|
|
Save() error // saves any modifications to the workspace.
|
|
}
|
|
|
|
type projectWorkspace struct {
|
|
name tokens.PackageName // the package this workspace is associated with.
|
|
project string // the path to the Pulumi.[yaml|json] file for this project.
|
|
settings *Settings // settings for this workspace.
|
|
}
|
|
|
|
var (
|
|
cache = make(map[string]W)
|
|
cacheMutex sync.RWMutex
|
|
)
|
|
|
|
func loadFromCache(key string) (W, bool) {
|
|
cacheMutex.RLock()
|
|
defer cacheMutex.RUnlock()
|
|
|
|
w, ok := cache[key]
|
|
return w, ok
|
|
}
|
|
|
|
func upsertIntoCache(key string, w W) {
|
|
contract.Requiref(w != nil, "w", "cannot be nil")
|
|
|
|
cacheMutex.Lock()
|
|
defer cacheMutex.Unlock()
|
|
|
|
cache[key] = w
|
|
}
|
|
|
|
// New creates a new workspace using the current working directory.
|
|
func New() (W, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewFrom(cwd)
|
|
}
|
|
|
|
// NewFrom creates a new Pulumi workspace in the given directory. Requires a Pulumi.yaml file be present in the
|
|
// folder hierarchy between dir and the .pulumi folder.
|
|
func NewFrom(dir string) (W, error) {
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dir = absDir
|
|
|
|
if w, ok := loadFromCache(dir); ok {
|
|
return w, nil
|
|
}
|
|
|
|
path, err := DetectProjectPathFrom(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if path == "" {
|
|
return nil, fmt.Errorf("no Pulumi.yaml project file found (searching upwards from %s). If you have not "+
|
|
"created a project yet, use `pulumi new` to do so", dir)
|
|
}
|
|
|
|
proj, err := LoadProject(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w := &projectWorkspace{
|
|
name: proj.Name,
|
|
project: path,
|
|
}
|
|
|
|
err = w.readSettings()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read workspace settings: %w", err)
|
|
}
|
|
|
|
upsertIntoCache(dir, w)
|
|
return w, nil
|
|
}
|
|
|
|
func (pw *projectWorkspace) Settings() *Settings {
|
|
return pw.settings
|
|
}
|
|
|
|
func (pw *projectWorkspace) Save() error {
|
|
settingsFile := pw.settingsPath()
|
|
|
|
// If the settings file is empty, don't write an new one, and delete the old one if present. Since we put workspaces
|
|
// under ~/.pulumi/workspaces, cleaning them out when possible prevents us from littering a bunch of files in the
|
|
// home directory.
|
|
if pw.settings.IsEmpty() {
|
|
err := os.Remove(settingsFile)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := os.MkdirAll(filepath.Dir(settingsFile), 0o700)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b, err := json.MarshalIndent(pw.settings, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return atomicWriteFile(settingsFile, b)
|
|
}
|
|
|
|
// atomicWriteFile provides a rename based atomic write through a temporary file.
|
|
func atomicWriteFile(path string, b []byte) error {
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temporary file %s: %w", path, err)
|
|
}
|
|
defer func() { contract.Ignore(os.Remove(tmp.Name())) }()
|
|
|
|
if err = tmp.Chmod(0o600); err != nil {
|
|
return fmt.Errorf("failed to set temporary file permission: %w", err)
|
|
}
|
|
if _, err = tmp.Write(b); err != nil {
|
|
return fmt.Errorf("failed to write to temporary file: %w", err)
|
|
}
|
|
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 {
|
|
settingsPath := pw.settingsPath()
|
|
|
|
b, err := os.ReadFile(settingsPath)
|
|
if err != nil && os.IsNotExist(err) {
|
|
// not an error to not have an existing settings file.
|
|
pw.settings = &Settings{}
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
var settings Settings
|
|
|
|
err = json.Unmarshal(b, &settings)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse file %s: %w", settingsPath, err)
|
|
}
|
|
|
|
pw.settings = &settings
|
|
return nil
|
|
}
|
|
|
|
func (pw *projectWorkspace) settingsPath() string {
|
|
uniqueFileName := string(pw.name) + "-" + sha1HexString(pw.project) + "-" + WorkspaceFile
|
|
path, err := GetPulumiPath(WorkspaceDir, uniqueFileName)
|
|
contract.AssertNoErrorf(err, "could not get workspace path")
|
|
return path
|
|
}
|
|
|
|
// sha1HexString returns a hex string of the sha1 hash of value.
|
|
func sha1HexString(value string) string {
|
|
//nolint:gosec
|
|
h := sha1.New()
|
|
_, err := h.Write([]byte(value))
|
|
contract.AssertNoErrorf(err, "error hashing string")
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// qnameFileName takes a qname and cleans it for use as a filename (by replacing tokens.QNameDelimter with a dash)
|
|
func qnameFileName(nm tokens.QName) string {
|
|
return strings.ReplaceAll(string(nm), tokens.QNameDelimiter, "-")
|
|
}
|