2020-08-19 20:13:42 +02:00
|
|
|
package auto
|
|
|
|
|
|
|
|
import (
|
2020-08-22 07:20:32 +02:00
|
|
|
"context"
|
2020-08-20 06:23:53 +02:00
|
|
|
"encoding/json"
|
2020-08-19 20:13:42 +02:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2020-08-21 18:49:46 +02:00
|
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
|
2020-08-19 20:13:42 +02:00
|
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
|
|
|
)
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// LocalWorkspace is a default implemenatation of workspace of the Workspace interface.
|
|
|
|
// A Workspace is the execution context containing a single Pulumi project, a program,
|
|
|
|
// and multiple stacks. Workspaces are used to manage the execution environment,
|
|
|
|
// providing various utilities such as plugin installation, environment configuration
|
|
|
|
// ($PULUMI_HOME), and creation, deletion, and listing of Stacks.
|
|
|
|
// LocalWorkspace relies on pulumi.yaml and pulumi.<stack>.yaml as the intermediate format
|
|
|
|
// for Project and Stack settings. Modifying ProjectSettings will
|
|
|
|
// alter the Workspace pulumi.yaml file, and setting config on a Stack will modify the pulumi.<stack>.yaml file.
|
|
|
|
// This is identical to the behavior of Pulumi CLI driven workspaces.
|
2020-08-19 20:13:42 +02:00
|
|
|
type LocalWorkspace struct {
|
|
|
|
workDir string
|
|
|
|
pulumiHome *string
|
|
|
|
program pulumi.RunFunc
|
|
|
|
}
|
|
|
|
|
|
|
|
var settingsExtensions = []string{".yaml", ".yml", ".json"}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// ProjectSettings returns the settings object for the current project if any
|
|
|
|
// LocalWorkspace reads settings from the pulumi.yaml in the workspace.
|
|
|
|
// A workspace can contain only a single project at a time.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) ProjectSettings(ctx context.Context) (*workspace.Project, error) {
|
2020-08-19 20:13:42 +02:00
|
|
|
for _, ext := range settingsExtensions {
|
2020-08-21 18:49:46 +02:00
|
|
|
projectPath := filepath.Join(l.WorkDir(), fmt.Sprintf("Pulumi%s", ext))
|
|
|
|
if _, err := os.Stat(projectPath); err == nil {
|
2020-08-19 20:13:42 +02:00
|
|
|
proj, err := workspace.LoadProject(projectPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "found project settings, but failed to load")
|
|
|
|
}
|
|
|
|
return proj, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("unable to find project settings in workspace")
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// WriteProjectSettings overwrites the settings object in the current project.
|
|
|
|
// There can only be a single project per workspace. Fails is new project name does not match old.
|
|
|
|
// LocalWorkspace writes this value to a pulumi.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) WriteProjectSettings(ctx context.Context, settings *workspace.Project) error {
|
2020-08-21 18:49:46 +02:00
|
|
|
pulumiYamlPath := filepath.Join(l.WorkDir(), "Pulumi.yaml")
|
2020-08-19 20:13:42 +02:00
|
|
|
return settings.Save(pulumiYamlPath)
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// StackSettings returns the settings object for the stack matching the specified fullyQualifiedStackName if any.
|
|
|
|
// LocalWorkspace reads this from a pulumi.<stack>.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) StackSettings(ctx context.Context, fqsn string) (*workspace.ProjectStack, error) {
|
2020-08-25 20:16:54 +02:00
|
|
|
name, err := getStackFromFQSN(fqsn)
|
2020-08-19 20:13:42 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to load stack settings, invalid stack name")
|
|
|
|
}
|
|
|
|
for _, ext := range settingsExtensions {
|
|
|
|
stackPath := filepath.Join(l.WorkDir(), fmt.Sprintf("pulumi.%s%s", name, ext))
|
|
|
|
if _, err := os.Stat(stackPath); err != nil {
|
|
|
|
proj, err := workspace.LoadProjectStack(stackPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "found stack settings, but failed to load")
|
|
|
|
}
|
|
|
|
return proj, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.Errorf("unable to find stack settings in workspace for %s", fqsn)
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// WriteStackSettings overwrites the settings object for the stack matching the specified fullyQualifiedStackName.
|
|
|
|
// LocalWorkspace writes this value to a pulumi.<stack>.yaml file in Workspace.WorkDir()
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) WriteStackSettings(
|
|
|
|
ctx context.Context,
|
|
|
|
fqsn string,
|
|
|
|
settings *workspace.ProjectStack,
|
|
|
|
) error {
|
2020-08-25 20:16:54 +02:00
|
|
|
name, err := getStackFromFQSN(fqsn)
|
2020-08-19 20:13:42 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to save stack settings, invalid stack name")
|
|
|
|
}
|
|
|
|
stackYamlPath := filepath.Join(l.WorkDir(), fmt.Sprintf("pulumi.%s.yaml:", name))
|
|
|
|
err = settings.Save(stackYamlPath)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "failed to save stack setttings for %s", fqsn)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SerializeArgsForOp is hook to provide additional args to every CLI commands before they are executed.
|
|
|
|
// Provided with fullyQualifiedStackName,
|
|
|
|
// returns a list of args to append to an invoked command ["--config=...", ]
|
|
|
|
// LocalWorkspace does not utilize this extensibility point.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) SerializeArgsForOp(ctx context.Context, fqsn string) ([]string, error) {
|
2020-08-19 20:13:42 +02:00
|
|
|
// not utilized for LocalWorkspace
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// PostOpCallback is a hook executed after every command. Called with the fullyQualifiedStackName.
|
|
|
|
// An extensibility point to perform workspace cleanup (CLI operations may create/modify a pulumi.stack.yaml)
|
|
|
|
// LocalWorkspace does not utilize this extensibility point.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) PostOpCallback(ctx context.Context, fqsn string) error {
|
2020-08-19 20:13:42 +02:00
|
|
|
// not utilized for LocalWorkspace
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// GetConfig returns the value associated with the specified fullyQualifiedStackName and key,
|
|
|
|
// scoped to the current workspace. LocalWorkspace reads this config from the matching pulumi.stack.yaml file.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) GetConfig(ctx context.Context, fqsn string, key string) (ConfigValue, error) {
|
2020-08-20 06:23:53 +02:00
|
|
|
var val ConfigValue
|
2020-08-22 07:20:32 +02:00
|
|
|
err := l.SelectStack(ctx, fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return val, errors.Wrapf(err, "could not get config, unable to select stack %s", fqsn)
|
|
|
|
}
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "get", key, "--show-secrets", "--json")
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return val, errors.Wrap(newAutoError(err, stdout, stderr, errCode), "unable read config")
|
|
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(stdout), &val)
|
|
|
|
if err != nil {
|
|
|
|
return val, errors.Wrap(err, "unable to unmarshal config value")
|
|
|
|
}
|
2020-08-19 20:13:42 +02:00
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// GetAllConfig returns the config map for the specified fullyQualifiedStackName, scoped to the current workspace.
|
|
|
|
// LocalWorkspace reads this config from the matching pulumi.stack.yaml file.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) GetAllConfig(ctx context.Context, fqsn string) (ConfigMap, error) {
|
2020-08-20 06:23:53 +02:00
|
|
|
var val ConfigMap
|
2020-08-22 07:20:32 +02:00
|
|
|
err := l.SelectStack(ctx, fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return val, errors.Wrapf(err, "could not get config, unable to select stack %s", fqsn)
|
|
|
|
}
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "--show-secrets", "--json")
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return val, errors.Wrap(newAutoError(err, stdout, stderr, errCode), "unable read config")
|
|
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(stdout), &val)
|
|
|
|
if err != nil {
|
|
|
|
return val, errors.Wrap(err, "unable to unmarshal config value")
|
|
|
|
}
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SetConfig sets the specified KVP on the provided fullyQualifiedStackName.
|
|
|
|
// LocalWorkspace writes this value to the matching pulumi.<stack>.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) SetConfig(ctx context.Context, fqsn string, key string, val ConfigValue) error {
|
|
|
|
err := l.SelectStack(ctx, fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not set config, unable to select stack %s", fqsn)
|
|
|
|
}
|
|
|
|
|
|
|
|
secretArg := "--plaintext"
|
|
|
|
if val.Secret {
|
|
|
|
secretArg = "--secret"
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "set", key, val.Value, secretArg)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "unable set config")
|
|
|
|
}
|
|
|
|
return nil
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SetAllConfig sets all values in the provided config map for the specified fullyQualifiedStackName.
|
|
|
|
// LocalWorkspace writes the config to the matching pulumi.<stack>.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) SetAllConfig(ctx context.Context, fqsn string, config ConfigMap) error {
|
2020-08-20 06:23:53 +02:00
|
|
|
for k, v := range config {
|
2020-08-22 07:20:32 +02:00
|
|
|
err := l.SetConfig(ctx, fqsn, k, v)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2020-08-19 20:13:42 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// RemoveConfig removes the specified KVP on the provided fullyQualifiedStackName.
|
|
|
|
// It will remove any matching values in the pulumi.<stack>.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) RemoveConfig(ctx context.Context, fqsn string, key string) error {
|
|
|
|
err := l.SelectStack(ctx, fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not remove config, unable to select stack %s", fqsn)
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "rm", key)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "could not remove config")
|
|
|
|
}
|
2020-08-19 20:13:42 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// RemoveAllConfig removes all values in the provided key list for the specified fullyQualifiedStackName
|
|
|
|
// It will remove any matching values in the pulumi.<stack>.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) RemoveAllConfig(ctx context.Context, fqsn string, keys []string) error {
|
2020-08-20 06:23:53 +02:00
|
|
|
for _, k := range keys {
|
2020-08-22 07:20:32 +02:00
|
|
|
err := l.RemoveConfig(ctx, fqsn, k)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// RefreshConfig gets and sets the config map used with the last Update for Stack matching fullyQualifiedStackName.
|
|
|
|
// It will overwrite all configuration in the pulumi.<stack>.yaml file in Workspace.WorkDir().
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) RefreshConfig(ctx context.Context, fqsn string) (ConfigMap, error) {
|
|
|
|
err := l.SelectStack(ctx, fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "could not refresh config, unable to select stack %s", fqsn)
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "refresh", "--force")
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(newAutoError(err, stdout, stderr, errCode), "could not refresh config")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
cfg, err := l.GetAllConfig(ctx, fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not fetch config after refresh")
|
|
|
|
}
|
|
|
|
return cfg, nil
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// WorkDir returns the working directory to run Pulumi CLI commands.
|
|
|
|
// LocalWorkspace expects that this directory contains a Pulumi.yaml file.
|
|
|
|
// For "Inline" Pulumi programs created from NewStackInlineSource, a pulumi.yaml
|
|
|
|
// is created on behalf of the user if none is specified.
|
2020-08-19 20:13:42 +02:00
|
|
|
func (l *LocalWorkspace) WorkDir() string {
|
|
|
|
return l.workDir
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// PulumiHome returns the directory override for CLI metadata if set.
|
2020-08-19 20:13:42 +02:00
|
|
|
func (l *LocalWorkspace) PulumiHome() *string {
|
|
|
|
return l.pulumiHome
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// WhoAmI returns the currently authenticated user
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) WhoAmI(ctx context.Context) (string, error) {
|
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "whoami")
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrap(newAutoError(err, stdout, stderr, errCode), "could not determine authenticated user")
|
|
|
|
}
|
|
|
|
return strings.TrimSpace(stdout), nil
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// Stack returns a summary of the currently selected stack, if any.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) Stack(ctx context.Context) (*StackSummary, error) {
|
|
|
|
stacks, err := l.ListStacks(ctx)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not determine selected stack")
|
|
|
|
}
|
|
|
|
for _, s := range stacks {
|
|
|
|
if s.Current {
|
|
|
|
return &s, nil
|
|
|
|
}
|
|
|
|
}
|
2020-08-19 20:13:42 +02:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// CreateStack creates and sets a new stack with the fullyQualifiedStackName, failing if one already exists.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) CreateStack(ctx context.Context, fqsn string) error {
|
2020-08-20 06:23:53 +02:00
|
|
|
err := ValidateFullyQualifiedStackName(fqsn)
|
|
|
|
if err != nil {
|
2020-08-21 04:37:39 +02:00
|
|
|
return errors.Wrap(err, "failed to create stack")
|
2020-08-20 06:23:53 +02:00
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "init", fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
2020-08-21 04:37:39 +02:00
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "failed to create stack")
|
2020-08-20 06:23:53 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 04:37:39 +02:00
|
|
|
return nil
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SelectStack selects and sets an existing stack matching the fullyQualifiedStackName, failing if none exists.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) SelectStack(ctx context.Context, fqsn string) error {
|
2020-08-20 06:23:53 +02:00
|
|
|
err := ValidateFullyQualifiedStackName(fqsn)
|
|
|
|
if err != nil {
|
2020-08-21 04:37:39 +02:00
|
|
|
return errors.Wrap(err, "failed to select stack")
|
2020-08-20 06:23:53 +02:00
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "select", fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
2020-08-21 04:37:39 +02:00
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "failed to select stack")
|
2020-08-20 06:23:53 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 04:37:39 +02:00
|
|
|
return nil
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// RemoveStack deletes the stack and all associated configuration and history.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) RemoveStack(ctx context.Context, fqsn string) error {
|
2020-08-20 06:23:53 +02:00
|
|
|
err := ValidateFullyQualifiedStackName(fqsn)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to remove stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "rm", "--yes", fqsn)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "failed to remove stack")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// ListStacks returns all Stacks created under the current Project.
|
|
|
|
// This queries underlying backend and may return stacks not present in the Workspace (as pulumi.<stack>.yaml files).
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) ListStacks(ctx context.Context) ([]StackSummary, error) {
|
|
|
|
user, err := l.WhoAmI(ctx)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not list stacks")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
proj, err := l.ProjectSettings(ctx)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not list stacks")
|
|
|
|
}
|
|
|
|
|
|
|
|
var stacks []StackSummary
|
2020-08-22 07:20:32 +02:00
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "ls", "--json")
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return stacks, errors.Wrap(newAutoError(err, stdout, stderr, errCode), "could not list stacks")
|
|
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(stdout), &stacks)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "unable to unmarshal config value")
|
|
|
|
}
|
|
|
|
for _, s := range stacks {
|
|
|
|
nameParts := strings.Split(s.Name, "/")
|
|
|
|
if len(nameParts) == 1 {
|
|
|
|
s.Name = fmt.Sprintf("%s/%s/%s", user, proj.Name.String(), s.Name)
|
|
|
|
} else {
|
|
|
|
s.Name = fmt.Sprintf("%s/%s/%s", nameParts[0], proj.Name.String(), nameParts[1])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return stacks, nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// InstallPlugin acquires the plugin matching the specified name and version
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) InstallPlugin(ctx context.Context, name string, version string) error {
|
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "plugin", "install", "resource", name, version)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "failed to install plugin")
|
|
|
|
}
|
2020-08-19 20:13:42 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// RemovePlugin deletes the plugin matching the specified name and verision
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) RemovePlugin(ctx context.Context, name string, version string) error {
|
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "plugin", "rm", "resource", name, version)
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(newAutoError(err, stdout, stderr, errCode), "failed to remove plugin")
|
|
|
|
}
|
2020-08-19 20:13:42 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// ListPlugins lists all installed plugins.
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) ListPlugins(ctx context.Context) ([]workspace.PluginInfo, error) {
|
|
|
|
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "plugin", "ls", "--json")
|
2020-08-20 06:23:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(newAutoError(err, stdout, stderr, errCode), "could not list list")
|
|
|
|
}
|
|
|
|
var plugins []workspace.PluginInfo
|
|
|
|
err = json.Unmarshal([]byte(stdout), &plugins)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "unable to unmarshal plugin response")
|
|
|
|
}
|
|
|
|
return plugins, nil
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// Program returns the program `pulumi.RunFunc` to be used for Preview/Update if any.
|
|
|
|
// If none is specified, the stack will refer to Project Settings for this information.
|
2020-08-21 04:37:39 +02:00
|
|
|
func (l *LocalWorkspace) Program() pulumi.RunFunc {
|
|
|
|
return l.program
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SetProgram sets the program associated with the Workspace to the specified `pulumi.RunFunc`
|
2020-08-21 04:37:39 +02:00
|
|
|
func (l *LocalWorkspace) SetProgram(fn pulumi.RunFunc) {
|
|
|
|
l.program = fn
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
func (l *LocalWorkspace) runPulumiCmdSync(
|
|
|
|
ctx context.Context,
|
|
|
|
args ...string,
|
|
|
|
) (string, string, int, error) {
|
2020-08-19 20:13:42 +02:00
|
|
|
var env []string
|
|
|
|
if l.PulumiHome() != nil {
|
2020-08-25 20:16:54 +02:00
|
|
|
homeEnv := fmt.Sprintf("%s=%s", pulumiHomeEnv, *l.PulumiHome())
|
2020-08-22 01:22:45 +02:00
|
|
|
env = append(env, homeEnv)
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
2020-08-22 07:20:32 +02:00
|
|
|
return runPulumiCommandSync(ctx, l.WorkDir(), env, args...)
|
2020-08-19 20:13:42 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// NewLocalWorkspace creates an configures a LocalWorkspace. LocalWorkspaceOptions can be used to
|
|
|
|
// configure things like the working directory, the program to execute, and to seed the directory with source code
|
|
|
|
// from a git repository.
|
2020-08-22 07:20:32 +02:00
|
|
|
func NewLocalWorkspace(ctx context.Context, opts ...LocalWorkspaceOption) (Workspace, error) {
|
2020-08-19 20:13:42 +02:00
|
|
|
lwOpts := &localWorkspaceOptions{}
|
|
|
|
// for merging options, last specified value wins
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt.applyLocalWorkspaceOption(lwOpts)
|
|
|
|
}
|
|
|
|
|
|
|
|
var workDir string
|
|
|
|
|
|
|
|
if lwOpts.WorkDir != "" {
|
|
|
|
workDir = lwOpts.WorkDir
|
|
|
|
} else {
|
|
|
|
dir, err := ioutil.TempDir("", "pulumi_auto")
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "unable to create tmp directory for workspace")
|
|
|
|
}
|
|
|
|
workDir = dir
|
|
|
|
}
|
|
|
|
|
|
|
|
if lwOpts.Repo != nil {
|
|
|
|
// now do the git clone
|
2020-08-22 07:20:32 +02:00
|
|
|
projDir, err := setupGitRepo(ctx, workDir, lwOpts.Repo)
|
2020-08-19 20:13:42 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to create workspace, unable to enlist in git repo")
|
|
|
|
}
|
|
|
|
workDir = projDir
|
|
|
|
}
|
|
|
|
|
|
|
|
var program pulumi.RunFunc
|
|
|
|
if lwOpts.Program != nil {
|
|
|
|
program = lwOpts.Program
|
|
|
|
}
|
|
|
|
|
|
|
|
var pulumiHome *string
|
|
|
|
if lwOpts.PulumiHome != nil {
|
|
|
|
pulumiHome = lwOpts.PulumiHome
|
|
|
|
}
|
|
|
|
|
|
|
|
l := &LocalWorkspace{
|
|
|
|
workDir: workDir,
|
|
|
|
program: program,
|
|
|
|
pulumiHome: pulumiHome,
|
|
|
|
}
|
|
|
|
|
|
|
|
if lwOpts.Project != nil {
|
2020-08-22 07:20:32 +02:00
|
|
|
err := l.WriteProjectSettings(ctx, lwOpts.Project)
|
2020-08-19 20:13:42 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to create workspace, unable to save project settings")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-22 01:22:45 +02:00
|
|
|
for fqsn := range lwOpts.Stacks {
|
|
|
|
s := lwOpts.Stacks[fqsn]
|
2020-08-22 07:20:32 +02:00
|
|
|
err := l.WriteStackSettings(ctx, fqsn, &s)
|
2020-08-19 20:13:42 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to create workspace")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return l, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type localWorkspaceOptions struct {
|
|
|
|
// WorkDir is the directory to execute commands from and store state.
|
|
|
|
// Defaults to a tmp dir.
|
|
|
|
WorkDir string
|
|
|
|
// Program is the Pulumi Program to execute. If none is supplied,
|
|
|
|
// the program identified in $WORKDIR/pulumi.yaml will be used instead.
|
|
|
|
Program pulumi.RunFunc
|
|
|
|
// PulumiHome overrides the metadata directory for pulumi commands
|
|
|
|
PulumiHome *string
|
|
|
|
// Project is the project settings for the workspace
|
|
|
|
Project *workspace.Project
|
|
|
|
// Stacks is a map of [fqsn -> stack settings objects] to seed the workspace
|
|
|
|
Stacks map[string]workspace.ProjectStack
|
|
|
|
// Repo is a git repo with a Pulumi Project to clone into the WorkDir.
|
|
|
|
Repo *GitRepo
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// LocalWorkspaceOption is used to customize and configure a LocalWorkspace at initialization time.
|
|
|
|
// See Workdir, Program, PulumiHome, Project, Stacks, and Repo for concrete options.
|
2020-08-19 20:13:42 +02:00
|
|
|
type LocalWorkspaceOption interface {
|
|
|
|
applyLocalWorkspaceOption(*localWorkspaceOptions)
|
|
|
|
}
|
|
|
|
|
|
|
|
type localWorkspaceOption func(*localWorkspaceOptions)
|
|
|
|
|
|
|
|
func (o localWorkspaceOption) applyLocalWorkspaceOption(opts *localWorkspaceOptions) {
|
|
|
|
o(opts)
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// GitRepo contains info to acquire and setup a Pulumi program from a git repository.
|
2020-08-19 20:13:42 +02:00
|
|
|
type GitRepo struct {
|
|
|
|
// URL to clone git repo
|
|
|
|
URL string
|
|
|
|
// Optional path relative to the repo root specifying location of the pulumi program.
|
|
|
|
// Specifying this option will update the Worspace's WorkDir accordingly.
|
|
|
|
ProjectPath string
|
|
|
|
// Optional branch to checkout
|
|
|
|
Branch string
|
|
|
|
// Optional commit to checkout
|
|
|
|
CommitHash string
|
|
|
|
// Optional function to execute after enlisting in the specified repo.
|
|
|
|
Setup SetupFn
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetupFn is a function to execute after enlisting in a git repo.
|
|
|
|
// It is called with a PATH containing the pulumi program post-enlistment.
|
2020-08-22 07:20:32 +02:00
|
|
|
type SetupFn func(context.Context, string) error
|
2020-08-19 20:13:42 +02:00
|
|
|
|
|
|
|
// WorkDir is the directory to execute commands from and store state.
|
|
|
|
func WorkDir(workDir string) LocalWorkspaceOption {
|
|
|
|
return localWorkspaceOption(func(lo *localWorkspaceOptions) {
|
|
|
|
lo.WorkDir = workDir
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-08-21 04:37:39 +02:00
|
|
|
// Program is the Pulumi Program to execute. If none is supplied,
|
|
|
|
// the program identified in $WORKDIR/pulumi.yaml will be used instead.
|
2020-08-19 20:13:42 +02:00
|
|
|
func Program(program pulumi.RunFunc) LocalWorkspaceOption {
|
|
|
|
return localWorkspaceOption(func(lo *localWorkspaceOptions) {
|
|
|
|
lo.Program = program
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// PulumiHome overrides the metadata directory for pulumi commands
|
|
|
|
func PulumiHome(dir string) LocalWorkspaceOption {
|
|
|
|
return localWorkspaceOption(func(lo *localWorkspaceOptions) {
|
|
|
|
lo.PulumiHome = &dir
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Project sets project settings for the workspace
|
|
|
|
func Project(settings workspace.Project) LocalWorkspaceOption {
|
|
|
|
return localWorkspaceOption(func(lo *localWorkspaceOptions) {
|
|
|
|
lo.Project = &settings
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stacks is a list of stack settings objects to seed the workspace
|
|
|
|
func Stacks(settings map[string]workspace.ProjectStack) LocalWorkspaceOption {
|
|
|
|
return localWorkspaceOption(func(lo *localWorkspaceOptions) {
|
|
|
|
lo.Stacks = settings
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-08-21 18:49:46 +02:00
|
|
|
// Repo is a git repo with a Pulumi Project to clone into the WorkDir.
|
|
|
|
func Repo(gitRepo GitRepo) LocalWorkspaceOption {
|
2020-08-19 20:13:42 +02:00
|
|
|
return localWorkspaceOption(func(lo *localWorkspaceOptions) {
|
|
|
|
lo.Repo = &gitRepo
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// ValidateFullyQualifiedStackName validates that the fqsn is in the form "org/project/name"
|
2020-08-19 20:13:42 +02:00
|
|
|
func ValidateFullyQualifiedStackName(fqsn string) error {
|
|
|
|
parts := strings.Split(fqsn, "/")
|
|
|
|
if len(parts) != 3 {
|
|
|
|
return errors.Errorf(
|
|
|
|
"invalid fully qualified stack name: %s, expected in the form 'org/project/stack'",
|
|
|
|
fqsn,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2020-08-21 18:49:46 +02:00
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// NewStackLocalSource creates a Stack backed by a LocalWorkspace created on behalf of the user,
|
|
|
|
// from the specified WorkDir. This Workspace will pick up
|
|
|
|
// any available Settings files (pulumi.yaml, pulumi.<stack>.yaml).
|
2020-08-22 07:20:32 +02:00
|
|
|
func NewStackLocalSource(ctx context.Context, fqsn, workDir string, opts ...LocalWorkspaceOption) (Stack, error) {
|
2020-08-21 18:49:46 +02:00
|
|
|
opts = append(opts, WorkDir(workDir))
|
2020-08-22 07:20:32 +02:00
|
|
|
w, err := NewLocalWorkspace(ctx, opts...)
|
2020-08-21 18:49:46 +02:00
|
|
|
var stack Stack
|
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to create stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
return NewStack(ctx, fqsn, w)
|
2020-08-21 18:49:46 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SelectStackLocalSource selects an existing Stack backed by a LocalWorkspace created on behalf of the user,
|
|
|
|
// from the specified WorkDir. This Workspace will pick up
|
|
|
|
// any available Settings files (pulumi.yaml, pulumi.<stack>.yaml).
|
2020-08-22 07:20:32 +02:00
|
|
|
func SelectStackLocalSource(ctx context.Context, fqsn, workDir string, opts ...LocalWorkspaceOption) (Stack, error) {
|
2020-08-21 18:49:46 +02:00
|
|
|
opts = append(opts, WorkDir(workDir))
|
2020-08-22 07:20:32 +02:00
|
|
|
w, err := NewLocalWorkspace(ctx, opts...)
|
2020-08-21 18:49:46 +02:00
|
|
|
var stack Stack
|
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to select stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
return SelectStack(ctx, fqsn, w)
|
2020-08-21 18:49:46 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// NewStackRemoteSource creates a Stack backed by a LocalWorkspace created on behalf of the user,
|
|
|
|
// with source code cloned from the specified GitRepo. This Workspace will pick up
|
|
|
|
// any available Settings files (pulumi.yaml, pulumi.<stack>.yaml) that are cloned into the Workspace.
|
|
|
|
// Unless a WorkDir option is specified, the GitRepo will be clone into a new temporary directory provided by the OS.
|
2020-08-22 07:20:32 +02:00
|
|
|
func NewStackRemoteSource(ctx context.Context, fqsn string, repo GitRepo, opts ...LocalWorkspaceOption) (Stack, error) {
|
2020-08-21 18:49:46 +02:00
|
|
|
opts = append(opts, Repo(repo))
|
2020-08-22 07:20:32 +02:00
|
|
|
w, err := NewLocalWorkspace(ctx, opts...)
|
2020-08-21 18:49:46 +02:00
|
|
|
var stack Stack
|
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to create stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
return NewStack(ctx, fqsn, w)
|
2020-08-21 18:49:46 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SelectStackRemoteSource selects an existing Stack backed by a LocalWorkspace created on behalf of the user,
|
|
|
|
// with source code cloned from the specified GitRepo. This Workspace will pick up
|
|
|
|
// any available Settings files (pulumi.yaml, pulumi.<stack>.yaml) that are cloned into the Workspace.
|
|
|
|
// Unless a WorkDir option is specified, the GitRepo will be clone into a new temporary directory provided by the OS.
|
2020-08-22 21:01:55 +02:00
|
|
|
func SelectStackRemoteSource(
|
|
|
|
ctx context.Context,
|
|
|
|
fqsn string, repo GitRepo,
|
|
|
|
opts ...LocalWorkspaceOption,
|
|
|
|
) (Stack, error) {
|
2020-08-21 18:49:46 +02:00
|
|
|
opts = append(opts, Repo(repo))
|
2020-08-22 07:20:32 +02:00
|
|
|
w, err := NewLocalWorkspace(ctx, opts...)
|
2020-08-21 18:49:46 +02:00
|
|
|
var stack Stack
|
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to select stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
return SelectStack(ctx, fqsn, w)
|
2020-08-21 18:49:46 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// NewStackInlineSource creates a Stack backed by a LocalWorkspace created on behalf of the user,
|
|
|
|
// with the specified program. If no Project option is specified, default project settings will be created
|
|
|
|
// on behalf of the user. Similarly, unless a WorkDir option is specified, the working directory will default
|
|
|
|
// to a new temporary directory provided by the OS.
|
2020-08-21 18:49:46 +02:00
|
|
|
func NewStackInlineSource(
|
2020-08-22 07:20:32 +02:00
|
|
|
ctx context.Context,
|
2020-08-21 18:49:46 +02:00
|
|
|
fqsn string,
|
|
|
|
program pulumi.RunFunc,
|
|
|
|
opts ...LocalWorkspaceOption,
|
|
|
|
) (Stack, error) {
|
|
|
|
var stack Stack
|
|
|
|
opts = append(opts, Program(program))
|
|
|
|
proj, err := defaultInlineProject(fqsn)
|
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to create stack")
|
|
|
|
}
|
|
|
|
// as we implictly create project on behalf of the user, prepend to opts in case the user specifies one
|
|
|
|
opts = append([]LocalWorkspaceOption{Project(proj)}, opts...)
|
2020-08-22 07:20:32 +02:00
|
|
|
w, err := NewLocalWorkspace(ctx, opts...)
|
2020-08-21 18:49:46 +02:00
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to create stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
return NewStack(ctx, fqsn, w)
|
2020-08-21 18:49:46 +02:00
|
|
|
}
|
|
|
|
|
2020-08-25 20:16:54 +02:00
|
|
|
// SelectStackInlineSource selects an existing Stack backed by a new LocalWorkspace created on behalf of the user,
|
|
|
|
// with the specified program. If no Project option is specified, default project settings will be created
|
|
|
|
// on behalf of the user. Similarly, unless a WorkDir option is specified, the working directory will default
|
|
|
|
// to a new temporary directory provided by the OS.
|
2020-08-21 18:49:46 +02:00
|
|
|
func SelectStackInlineSource(
|
2020-08-22 07:20:32 +02:00
|
|
|
ctx context.Context,
|
2020-08-21 18:49:46 +02:00
|
|
|
fqsn string,
|
|
|
|
program pulumi.RunFunc,
|
|
|
|
opts ...LocalWorkspaceOption,
|
|
|
|
) (Stack, error) {
|
|
|
|
var stack Stack
|
|
|
|
opts = append(opts, Program(program))
|
|
|
|
proj, err := defaultInlineProject(fqsn)
|
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to select stack")
|
|
|
|
}
|
|
|
|
// as we implictly create project on behalf of the user, prepend to opts in case the user specifies one
|
|
|
|
opts = append([]LocalWorkspaceOption{Project(proj)}, opts...)
|
2020-08-22 07:20:32 +02:00
|
|
|
w, err := NewLocalWorkspace(ctx, opts...)
|
2020-08-21 18:49:46 +02:00
|
|
|
if err != nil {
|
|
|
|
return stack, errors.Wrap(err, "failed to select stack")
|
|
|
|
}
|
|
|
|
|
2020-08-22 07:20:32 +02:00
|
|
|
return SelectStack(ctx, fqsn, w)
|
2020-08-21 18:49:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func defaultInlineProject(fqsn string) (workspace.Project, error) {
|
|
|
|
var proj workspace.Project
|
|
|
|
err := ValidateFullyQualifiedStackName(fqsn)
|
|
|
|
if err != nil {
|
|
|
|
return proj, err
|
|
|
|
}
|
|
|
|
pName := strings.Split(fqsn, "/")[1]
|
|
|
|
proj = workspace.Project{
|
|
|
|
Name: tokens.PackageName(pName),
|
|
|
|
Runtime: workspace.NewProjectRuntimeInfo("go", nil),
|
|
|
|
}
|
|
|
|
|
|
|
|
return proj, nil
|
|
|
|
}
|
2020-08-25 20:16:54 +02:00
|
|
|
|
|
|
|
func getStackFromFQSN(fqsn string) (string, error) {
|
|
|
|
if err := ValidateFullyQualifiedStackName(fqsn); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strings.Split(fqsn, "/")[2], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
const pulumiHomeEnv = "PULUMI_HOME"
|