pulumi/pkg/workspace/workspace.go
2018-12-06 14:04:01 -08:00

190 lines
4.7 KiB
Go

// 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 workspace
import (
// nolint: gosec
"crypto/sha1"
"encoding/hex"
"encoding/json"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/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)
var 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.Require(w != nil, "w")
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, errors.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, 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), 0700)
if err != nil {
return err
}
b, err := json.MarshalIndent(pw.settings, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(settingsFile, b, 0600)
}
func (pw *projectWorkspace) readSettings() error {
settingsPath := pw.settingsPath()
b, err := ioutil.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 err
}
pw.settings = &settings
return nil
}
func (pw *projectWorkspace) settingsPath() string {
user, err := user.Current()
contract.AssertNoErrorf(err, "could not get current user")
uniqueFileName := string(pw.name) + "-" + sha1HexString(pw.project) + "-" + WorkspaceFile
return filepath.Join(user.HomeDir, BookkeepingDir, WorkspaceDir, uniqueFileName)
}
// 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.AssertNoError(err)
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.Replace(string(nm), tokens.QNameDelimiter, "-", -1)
}