This PR removes three command line parameters from Cloud-enabled Pulumi commands (`update` and `stack init`). Previously we required users to pass in `--organization`, `--repository`, and `--project`. But with the recent "Pulumi repository" changes, we can now get that from the Pulumi workspace. And the project name from the `Pulumi.yaml`. This PR also fixes a bugs that block the Cloud-enabled CLI path: `update` was getting the stack name via `explicitOrCurrent`, but that fails if the current stack (e.g. the one just initialized in the cloud) doesn't exist on the local disk. As for better handling of "current stack" and and Cloud-enabled commands, https://github.com/pulumi/pulumi/pull/493 and the PR to enable `stack select`, `stack rm`, and `stack ls` do a better job of handling situations like this.
296 lines
7 KiB
Go
296 lines
7 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
cryptorand "crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/pulumi/pulumi/pkg/util/fsutil"
|
|
|
|
"github.com/pulumi/pulumi/pkg/pack"
|
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
|
"golang.org/x/crypto/pbkdf2"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/pulumi/pulumi/pkg/diag"
|
|
"github.com/pulumi/pulumi/pkg/diag/colors"
|
|
"github.com/pulumi/pulumi/pkg/engine"
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
"github.com/pulumi/pulumi/pkg/workspace"
|
|
|
|
git "gopkg.in/src-d/go-git.v4"
|
|
)
|
|
|
|
// newWorkspace creates a new workspace using the current working directory.
|
|
func newWorkspace() (workspace.W, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return workspace.NewProjectWorkspace(cwd)
|
|
}
|
|
|
|
// explicitOrCurrent returns an stack name after ensuring the stack exists. When a empty
|
|
// stack name is passed, the "current" ambient stack is returned
|
|
func explicitOrCurrent(name string) (tokens.QName, error) {
|
|
if name == "" {
|
|
return getCurrentStack()
|
|
}
|
|
|
|
_, _, _, err := getStack(tokens.QName(name))
|
|
return tokens.QName(name), err
|
|
}
|
|
|
|
// getCurrentStack reads the current stack.
|
|
func getCurrentStack() (tokens.QName, error) {
|
|
w, err := newWorkspace()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
stack := w.Settings().Stack
|
|
if stack == "" {
|
|
return "", errors.New("no current stack detected; please use `pulumi stack init` to create one")
|
|
}
|
|
return stack, nil
|
|
}
|
|
|
|
// setCurrentStack changes the current stack to the given stack name.
|
|
func setCurrentStack(name tokens.QName) error {
|
|
// Switch the current workspace to that stack.
|
|
w, err := newWorkspace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Settings().Stack = name
|
|
return w.Save()
|
|
}
|
|
|
|
// displayEvents reads events from the `events` channel until it is closed, displaying each event as it comes in.
|
|
// Once all events have been read from the channel and displayed, it closes the `done` channel so the caller can
|
|
// await all the events being written.
|
|
func displayEvents(events <-chan engine.Event, done chan bool, debug bool) {
|
|
defer func() {
|
|
done <- true
|
|
}()
|
|
|
|
Outer:
|
|
for event := range events {
|
|
switch event.Type {
|
|
case engine.CancelEvent:
|
|
break Outer
|
|
case engine.StdoutColorEvent:
|
|
fmt.Print(colors.ColorizeText(event.Payload.(string)))
|
|
case engine.DiagEvent:
|
|
payload := event.Payload.(engine.DiagEventPayload)
|
|
var out io.Writer
|
|
out = os.Stdout
|
|
|
|
if payload.Severity == diag.Error || payload.Severity == diag.Warning {
|
|
out = os.Stderr
|
|
}
|
|
if payload.Severity == diag.Debug && !debug {
|
|
out = ioutil.Discard
|
|
}
|
|
msg := payload.Message
|
|
if payload.UseColor {
|
|
msg = colors.ColorizeText(msg)
|
|
}
|
|
fmt.Fprintf(out, msg)
|
|
default:
|
|
contract.Failf("unknown event type '%s'", event.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getPackage() (*pack.Package, error) {
|
|
pkgPath, err := getPackageFilePath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pkg, err := pack.Load(pkgPath)
|
|
|
|
return pkg, err
|
|
}
|
|
|
|
func savePackage(pkg *pack.Package) error {
|
|
pkgPath, err := getPackageFilePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return pack.Save(pkgPath, pkg)
|
|
}
|
|
|
|
func getPackageFilePath() (string, error) {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pkgPath, err := workspace.DetectPackage(dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if pkgPath == "" {
|
|
return "", errors.Errorf("could not find Pulumi.yaml (searching upwards from %s)", dir)
|
|
}
|
|
|
|
return pkgPath, nil
|
|
}
|
|
|
|
func readConsoleNoEchoWithPrompt(prompt string) (string, error) {
|
|
if prompt != "" {
|
|
fmt.Printf("%s: ", prompt)
|
|
}
|
|
|
|
return readConsoleNoEcho()
|
|
}
|
|
|
|
func readPassPhrase(prompt string) (string, error) {
|
|
if phrase, has := os.LookupEnv("PULUMI_CONFIG_PASSPHRASE"); has {
|
|
return phrase, nil
|
|
}
|
|
|
|
return readConsoleNoEchoWithPrompt(prompt)
|
|
}
|
|
|
|
func getSymmetricCrypter() (config.ValueEncrypterDecrypter, error) {
|
|
pkg, err := getPackage()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if pkg.EncryptionSalt != "" {
|
|
phrase, phraseErr := readPassPhrase("passphrase")
|
|
if phraseErr != nil {
|
|
return nil, phraseErr
|
|
}
|
|
|
|
return symmetricCrypterFromPhraseAndState(phrase, pkg.EncryptionSalt)
|
|
}
|
|
|
|
phrase, err := readPassPhrase("passphrase")
|
|
if err != nil {
|
|
return nil, err
|
|
|
|
}
|
|
|
|
confirm, err := readPassPhrase("passphrase (confirm)")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if phrase != confirm {
|
|
return nil, errors.New("passphrases do not match")
|
|
}
|
|
|
|
salt := make([]byte, 8)
|
|
|
|
_, err = cryptorand.Read(salt)
|
|
contract.Assertf(err == nil, "could not read from system random")
|
|
|
|
c := symmetricCrypter{key: keyFromPassPhrase(phrase, salt, aes256GCMKeyBytes)}
|
|
|
|
// Encrypt a message and store it with the salt so we can test if the password is correct later
|
|
msg, err := c.EncryptValue("pulumi")
|
|
contract.Assert(err == nil)
|
|
|
|
pkg.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
|
|
|
|
err = savePackage(pkg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func keyFromPassPhrase(phrase string, salt []byte, keyLength int) []byte {
|
|
// 1,000,000 iterations was chosen because it took a little over a second on an i7-7700HQ Quad Core procesor
|
|
return pbkdf2.Key([]byte(phrase), salt, 1000000, keyLength, sha256.New)
|
|
}
|
|
|
|
func hasSecureValue(config map[tokens.ModuleMember]config.Value) bool {
|
|
for _, v := range config {
|
|
if v.Secure() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func detectOwnerAndName(dir string) (string, string, error) {
|
|
owner, repo, err := getGitHubProjectForOrigin(dir)
|
|
if err == nil {
|
|
return owner, repo, nil
|
|
}
|
|
|
|
user, err := user.Current()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return user.Username, filepath.Base(dir), nil
|
|
}
|
|
|
|
func getGitHubProjectForOrigin(dir string) (string, string, error) {
|
|
|
|
gitRoot, err := fsutil.WalkUp(dir, func(s string) bool { return filepath.Base(s) == ".git" }, nil)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "could not detect git repository")
|
|
}
|
|
if gitRoot == "" {
|
|
return "", "", errors.Errorf("could not locate git repository starting at: %s", dir)
|
|
}
|
|
|
|
repo, err := git.NewFilesystemRepository(gitRoot)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
remote, err := repo.Remote("origin")
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "could not read origin information")
|
|
}
|
|
|
|
remoteURL := remote.Config().URL
|
|
project := ""
|
|
|
|
const GitHubSSHPrefix = "git@github.com:"
|
|
const GitHubHTTPSPrefix = "https://github.com/"
|
|
const GitHubRepositorySuffix = ".git"
|
|
|
|
if strings.HasPrefix(remoteURL, GitHubSSHPrefix) {
|
|
project = trimGitRemoteURL(remoteURL, GitHubSSHPrefix, GitHubRepositorySuffix)
|
|
} else if strings.HasPrefix(remoteURL, GitHubHTTPSPrefix) {
|
|
project = trimGitRemoteURL(remoteURL, GitHubHTTPSPrefix, GitHubRepositorySuffix)
|
|
}
|
|
|
|
split := strings.Split(project, "/")
|
|
|
|
if len(split) != 2 {
|
|
return "", "", errors.Errorf("could not detect GitHub project from url: %v", remote)
|
|
}
|
|
|
|
return split[0], split[1], nil
|
|
}
|
|
|
|
func trimGitRemoteURL(url string, prefix string, suffix string) string {
|
|
return strings.TrimSuffix(strings.TrimPrefix(url, prefix), suffix)
|
|
}
|