pulumi/pkg/workspace/paths.go
joeduffy 3d74eac67d Make major commands more pleasant
This change eliminates the need to constantly type in the environment
name when performing major commands like configuration, planning, and
deployment.  It's probably due to my age, however, I keep fat-fingering
simple commands in front of investors and I am embarrassed!

In the new model, there is a notion of a "current environment", and
I have modeled it kinda sorta just like Git's notion of "current branch."

By default, the current environment is set when you `init` something.
Otherwise, there is the `coco env select <env>` command to change it.
(Running this command w/out a new <env> will show you the current one.)

The major commands `config`, `plan`, `deploy`, and `destroy` will prefer
to use the current environment, unless it is overridden by using the
--env flag.  All of the `coco env <cmd> <env>` commands still require the
explicit passing of an environment which seems reasonable since they are,
after all, about manipulating environments.

As part of this, I've overhauled the aging workspace settings cruft,
which had fallen into disrepair since the initial prototype.
2017-03-21 19:23:32 -07:00

172 lines
5.5 KiB
Go

// Copyright 2017 Pulumi, Inc. All rights reserved.
package workspace
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pulumi/coconut/pkg/compiler/errors"
"github.com/pulumi/coconut/pkg/diag"
"github.com/pulumi/coconut/pkg/encoding"
"github.com/pulumi/coconut/pkg/tokens"
)
const ProjectFile = "Coconut" // the base name of a Project.
const Dir = ".coconut" // the default name of the CocoPack output directory.
const PackFile = "Cocopack" // the base name of a compiled CocoPack.
const BinDir = "bin" // the default name of the CocoPack binary output directory.
const EnvDir = "env" // the default name of the CocoPack environment directory.
const DepDir = "packs" // the directory in which dependencies exist, either local or global.
const SettingsFile = "workspace" // the base name of a markup file for shared settings in a workspace.
const InstallRootEnvvar = "COCONUT" // the envvar describing where Coconut has been installed.
const InstallRootLibdir = "lib" // the directory in which the Coconut standard library exists.
const DefaultInstallRoot = "/usr/local/coconut" // where Coconut is installed by default.
// InstallRoot returns Coconut's installation location. This is controlled my the COCOROOT envvar.
func InstallRoot() string {
// TODO: support Windows.
root := os.Getenv(InstallRootEnvvar)
if root == "" {
return DefaultInstallRoot
}
return root
}
// EnvPath returns a path to the given environment's default location.
func EnvPath(env tokens.QName) string {
path := filepath.Join(Dir, EnvDir)
if env != "" {
path = filepath.Join(path, qnamePath(env)+encoding.Exts[0])
}
return path
}
// isTop returns true if the path represents the top of the filesystem.
func isTop(path string) bool {
return os.IsPathSeparator(path[len(path)-1])
}
// pathDir returns the nearest directory to the given path (identity if a directory; parent otherwise).
func pathDir(path string) string {
// It's possible that the path is a file (e.g., a Coconut.yaml file); if so, we want the directory.
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return path
}
return filepath.Dir(path)
}
// DetectPackage locates the closest package from the given path, searching "upwards" in the directory hierarchy. If no
// Project is found, an empty path is returned. If problems are detected, they are logged to the diag.Sink.
func DetectPackage(path string, d diag.Sink) (string, error) {
// It's possible the target is already the file we seek; if so, return right away.
if IsProject(path, d) {
return path, nil
}
curr := pathDir(path)
for {
stop := false
// Enumerate the current path's files, checking each to see if it's a Project.
files, err := ioutil.ReadDir(curr)
if err != nil {
return "", err
}
// See if there's a compiled package in the expected location.
pack := filepath.Join(Dir, BinDir, PackFile)
for _, ext := range encoding.Exts {
packfile := pack + ext
if IsPack(packfile, d) {
return packfile, nil
}
}
// Now look for individual projects.
for _, file := range files {
name := file.Name()
path := filepath.Join(curr, name)
if IsProject(path, d) {
return path, nil
} else if IsCocoDir(path) {
// If we hit a workspace, stop looking.
stop = true
}
}
// If we encountered a stop condition, break out of the loop.
if stop {
break
}
// If neither succeeded, keep looking in our parent directory.
curr = filepath.Dir(curr)
if isTop(curr) {
break
}
}
return "", nil
}
// IsCocoDir returns true if the target is a Coconut directory.
func IsCocoDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir() && info.Name() == Dir
}
// IsProject returns true if the path references what appears to be a valid project. If problems are detected -- like
// an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
func IsProject(path string, d diag.Sink) bool {
return isMarkupFile(path, ProjectFile, d)
}
// IsPack returns true if the path references what appears to be a valid package. If problems are detected -- like
// an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
func IsPack(path string, d diag.Sink) bool {
return isMarkupFile(path, PackFile, d)
}
// IsSettings returns true if the path references what appears to be a valid settings file. If problems are detected --
// like an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
func IsSettings(path string, d diag.Sink) bool {
return isMarkupFile(path, SettingsFile, d)
}
func isMarkupFile(path string, expect string, d diag.Sink) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
// Missing files and directories can't be markup files.
return false
}
// Ensure the base name is expected.
name := info.Name()
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
if base != expect {
if d != nil && strings.EqualFold(base, expect) {
// If the strings aren't equal, but case-insensitively match, issue a warning.
d.Warningf(errors.WarningIllegalMarkupFileCasing.AtFile(name), expect)
}
return false
}
// Check all supported extensions.
for _, mext := range encoding.Exts {
if name == expect+mext {
return true
}
}
// If we got here, it means the base name matched, but not the extension. Warn and return.
if d != nil {
d.Warningf(errors.WarningIllegalMarkupFileExt.AtFile(name), expect, ext)
}
return false
}