This change updates our configuration model to make it simpler to understand by removing some features and changing how things are persisted in files. Notable changes: - We've removed the notion of "workspace" vs "project" config. Now, configuration is always stored in a file next to `Pulumi.yaml` named `Pulumi.<stack-name>.yaml` (the same file we'd use for an other stack specific information we would need to persist in the future). - We've removed the notion of project wide configuration. Every new stack gets a completely empty set of configuration and there's no way to share common values across stacks, instead the common value has to be set on each stack. We retain some of the old code for the configuration system so we can support upgrading a project in place. That will happen with the next change. This change fixes some issues and allows us to close some others (since they are no longer possible). Fixes #866 Closes #872 Closes #731
193 lines
5.2 KiB
Go
193 lines
5.2 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package workspace
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
|
"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.
|
|
Repository() *Repository // returns the repository this project belongs to.
|
|
StackPath(stack tokens.QName) string // returns the path to store stack information.
|
|
BackupDirectory() (string, error) // returns the directory to store backup stack files.
|
|
HistoryDirectory(stack tokens.QName) string // returns the directory to store a stack's history information.
|
|
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.
|
|
repo *Repository // the repo this workspace is associated with.
|
|
}
|
|
|
|
// 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) {
|
|
repo, err := GetRepository(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path, err := DetectProjectPathFrom(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if path == "" {
|
|
return nil, errors.New("no Pulumi.yaml project file found")
|
|
}
|
|
|
|
proj, err := LoadProject(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w := projectWorkspace{
|
|
name: proj.Name,
|
|
project: path,
|
|
repo: repo,
|
|
}
|
|
|
|
err = w.readSettings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if w.settings.ConfigDeprecated == nil {
|
|
w.settings.ConfigDeprecated = make(map[tokens.QName]config.Map)
|
|
}
|
|
|
|
return &w, nil
|
|
}
|
|
|
|
func (pw *projectWorkspace) Settings() *Settings {
|
|
return pw.settings
|
|
}
|
|
|
|
func (pw *projectWorkspace) Repository() *Repository {
|
|
return pw.repo
|
|
}
|
|
|
|
func (pw *projectWorkspace) Save() error {
|
|
// let's remove all the empty entries from the config array
|
|
for k, v := range pw.settings.ConfigDeprecated {
|
|
if len(v) == 0 {
|
|
delete(pw.settings.ConfigDeprecated, k)
|
|
}
|
|
}
|
|
|
|
settingsFile := pw.settingsPath()
|
|
|
|
// ensure the path exists
|
|
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) StackPath(stack tokens.QName) string {
|
|
path := filepath.Join(pw.Repository().Root, StackDir, pw.name.String())
|
|
if stack != "" {
|
|
path = filepath.Join(path, qnamePath(stack)+".json")
|
|
}
|
|
return path
|
|
}
|
|
|
|
func (pw *projectWorkspace) BackupDirectory() (string, error) {
|
|
user, err := user.Current()
|
|
if user == nil || err != nil {
|
|
return "", errors.New("failed to get current user")
|
|
}
|
|
|
|
projectDir := filepath.Dir(pw.project)
|
|
projectBackupDirName := filepath.Base(projectDir) + "-" + sha1HexString(projectDir)
|
|
|
|
return filepath.Join(user.HomeDir, BookkeepingDir, BackupDir, projectBackupDirName), nil
|
|
}
|
|
|
|
func (pw *projectWorkspace) HistoryDirectory(stack tokens.QName) string {
|
|
path := filepath.Join(pw.Repository().Root, HistoryDir, pw.name.String())
|
|
if stack != "" {
|
|
return filepath.Join(path, qnamePath(stack))
|
|
}
|
|
return path
|
|
}
|
|
|
|
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 {
|
|
return filepath.Join(pw.Repository().Root, WorkspaceDir, pw.name.String(), WorkspaceFile)
|
|
}
|
|
|
|
// sha1HexString returns a hex string of the sha1 hash of value.
|
|
func sha1HexString(value string) string {
|
|
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)
|
|
}
|
|
|
|
// qnamePath just cleans a name and makes sure it's appropriate to use as a path.
|
|
func qnamePath(nm tokens.QName) string {
|
|
return stringNamePath(string(nm))
|
|
}
|
|
|
|
// stringNamePart cleans a string component of a name and makes sure it's appropriate to use as a path.
|
|
func stringNamePath(nm string) string {
|
|
return strings.Replace(nm, tokens.QNameDelimiter, string(os.PathSeparator), -1)
|
|
}
|