pulumi/pkg/workspace/workspace.go
Matt Ellis a704750714 Remove Workspace dependency on diag.Sink
It was only being used for two cases where we would issue warnings for
cases where the file system casing did not match expected casing. I
think it's probably better if we don't try to be smart here and just
treat these cases the same as if the file had not existed. Removing
the dependncy on diag also makes it a little clearer that this stuff
should be pulled out from the engine.
2017-10-02 15:25:22 -07:00

165 lines
5 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package workspace
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
homedir "github.com/mitchellh/go-homedir"
"github.com/pulumi/pulumi/pkg/encoding"
"github.com/pulumi/pulumi/pkg/tokens"
)
// W offers functionality for interacting with Lumi workspaces. A workspace influences compilation; for example, it
// can specify default versions of dependencies, easing the process of working with multiple projects.
type W interface {
Path() string // the base path of the current workspace.
Root() string // the root path of the current workspace.
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
DetectPackage() (string, error) // locates the nearest project file in the directory hierarchy.
Save() error // saves any modifications to the workspace.
}
// New creates a new workspace from the given starting path.
func New(path string) (W, error) {
// First normalize the path to an absolute one.
var err error
path, err = filepath.Abs(path)
if err != nil {
return nil, err
}
home, err := homedir.Dir()
if err != nil {
return nil, err
}
ws := workspace{
path: path,
home: home,
}
// Perform our I/O: memoize the root directory and load up any settings before returning.
if err := ws.init(); err != nil {
return nil, err
}
return &ws, nil
}
type workspace struct {
path string // the path at which the workspace was constructed.
home string // the home directory to use for this workspace.
root string // the root of the workspace.
settings Settings // an optional bag of workspace-wide settings.
}
// init finds the root of the workspace, caches it for fast lookups, and loads up any workspace settings.
func (w *workspace) init() error {
if w.root == "" {
// Detect the root of the workspace and cache it.
root := pathDir(w.path)
Search:
for {
files, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for _, file := range files {
// A lumi directory delimits the root of the workspace.
lumidir := filepath.Join(root, file.Name())
if IsLumiDir(lumidir) {
glog.V(3).Infof("Lumi workspace detected; setting root to %v", root)
w.root = root // remember the root.
w.settings, err = w.readSettings() // load up optional settings.
if err != nil {
return err
}
break Search
}
}
// If neither succeeded, keep looking in our parent directory.
if root = filepath.Dir(root); isTop(root) {
// We reached the top of the filesystem. Just set root back to the path and stop.
glog.V(3).Infof("No Lumi workspace found; defaulting to current path %v", w.root)
w.root = w.path
break
}
}
}
return nil
}
func (w *workspace) Path() string { return w.path }
func (w *workspace) Root() string { return w.root }
func (w *workspace) Settings() *Settings { return &w.settings }
func (w *workspace) DetectPackage() (string, error) {
return DetectPackage(w.path)
}
// 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)
}
// Save persists any in-memory changes made to the workspace.
func (w *workspace) Save() error {
// For now, the only changes to commit are the settings file changes.
return w.saveSettings()
}
// settingsFile returns the settings file location for this workspace.
func (w *workspace) settingsFile(ext string) string {
return filepath.Join(w.root, Dir, SettingsFile+ext)
}
// readSettings loads a settings file from the workspace, probing for all available extensions.
func (w *workspace) readSettings() (Settings, error) {
// Attempt to load the raw bytes from all available extensions.
var settings Settings
for _, ext := range encoding.Exts {
// See if the file exists.
path := w.settingsFile(ext)
b, err := ioutil.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
continue // try the next extension
}
return settings, err
}
// If it does, go ahead and decode it.
m := encoding.Marshalers[ext]
if err := m.Unmarshal(b, &settings); err != nil {
return settings, err
}
}
return settings, nil
}
// saveSettings saves the settings into a file for this workspace, committing any in-memory changes that have been made.
// IDEA: right now, we only support JSON. It'd be ideal if we supported YAML too (and it would be quite easy).
func (w *workspace) saveSettings() error {
m := encoding.Default()
settings := w.Settings()
b, err := m.Marshal(settings)
if err != nil {
return err
}
path := w.settingsFile(encoding.DefaultExt())
return ioutil.WriteFile(path, b, 0644)
}