Move a bunch of code around

Move most of the guts of `lumi` into the newly created `engine`
package.
This commit is contained in:
Matt Ellis 2017-08-22 16:56:15 -07:00
parent dcc549d9ec
commit a6eabdc34b
33 changed files with 2060 additions and 1954 deletions

View file

@ -3,13 +3,9 @@
package main package main
import ( import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/resource" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
) )
@ -21,14 +17,14 @@ func newConfigCmd() *cobra.Command {
Short: "Query, set, replace, or unset configuration values", Short: "Query, set, replace, or unset configuration values",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return ListConfig(env) return engine.ListConfig(env)
} else if len(args) == 1 && !unset { } else if len(args) == 1 && !unset {
return GetConfig(env, args[0]) return engine.GetConfig(env, args[0])
} else if len(args) == 1 { } else if len(args) == 1 {
return DeleteConfig(env, args[0]) return engine.DeleteConfig(env, args[0])
} }
return SetConfig(env, args[0], args[1]) return engine.SetConfig(env, args[0], args[1])
}), }),
} }
@ -41,77 +37,3 @@ func newConfigCmd() *cobra.Command {
return cmd return cmd
} }
func ListConfig(envName string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config != nil {
fmt.Printf("%-32s %-32s\n", "KEY", "VALUE")
for _, key := range info.Target.Config.StableKeys() {
v := info.Target.Config[key]
// TODO[pulumi/pulumi-fabric#113]: print complex values.
fmt.Printf("%-32s %-32s\n", key, v)
}
}
return nil
}
func GetConfig(envName string, key string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config != nil {
if v, has := config[tokens.Token(key)]; has {
fmt.Printf("%v\n", v)
return nil
}
}
return errors.Errorf("configuration key '%v' not found for environment '%v'", key, info.Target.Name)
}
func SetConfig(envName string, key string, value string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config == nil {
config = make(resource.ConfigMap)
info.Target.Config = config
}
config[tokens.Token(key)] = value
if !saveEnv(info.Target, info.Snapshot, "", true) {
return errors.Errorf("could not save configuration value")
}
return nil
}
func DeleteConfig(envName string, key string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config != nil {
delete(config, tokens.Token(key))
if !saveEnv(info.Target, info.Snapshot, "", true) {
return errors.Errorf("could not save configuration value")
}
}
return nil
}

View file

@ -3,19 +3,10 @@
package main package main
import ( import (
"bytes"
"fmt"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
) )
func newDeployCmd() *cobra.Command { func newDeployCmd() *cobra.Command {
@ -45,7 +36,7 @@ func newDeployCmd() *cobra.Command {
"By default, the package to execute is loaded from the current directory. Optionally, an\n" + "By default, the package to execute is loaded from the current directory. Optionally, an\n" +
"explicit path can be provided using the [package] argument.", "explicit path can be provided using the [package] argument.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
return Deploy(DeployOptions{ return engine.Deploy(engine.DeployOptions{
Environment: env, Environment: env,
Package: pkgargFromArgs(args), Package: pkgargFromArgs(args),
Debug: debug, Debug: debug,
@ -94,166 +85,3 @@ func newDeployCmd() *cobra.Command {
return cmd return cmd
} }
type DeployOptions struct {
Environment string // the environment we are deploying into
Package string // the package we are deploying (or "" to use the default)
Debug bool // true to enable resource debugging output.
DryRun bool // true if we should just print the plan without performing it.
Analyzers []string // an optional set of analyzers to run as part of this deployment.
ShowConfig bool // true to show the configuration variables being used.
ShowReads bool // true to show the read-only steps in the plan.
ShowReplacementSteps bool // true to show the replacement steps in the plan.
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
Output string // the place to store the output, if any.
}
func Deploy(opts DeployOptions) error {
info, err := initEnvCmdName(tokens.QName(opts.Environment), opts.Package)
if err != nil {
return err
}
return deployLatest(info, deployOptions{
Debug: opts.Debug,
Destroy: false,
DryRun: opts.DryRun,
Analyzers: opts.Analyzers,
ShowConfig: opts.ShowConfig,
ShowReads: opts.ShowReads,
ShowReplacementSteps: opts.ShowReplacementSteps,
ShowSames: opts.ShowSames,
Summary: opts.Summary,
Output: opts.Output,
})
}
type deployOptions struct {
Debug bool // true to enable resource debugging output.
Create bool // true if we are creating resources.
Destroy bool // true if we are destroying the environment.
DryRun bool // true if we should just print the plan without performing it.
Analyzers []string // an optional set of analyzers to run as part of this deployment.
ShowConfig bool // true to show the configuration variables being used.
ShowReads bool // true to show the read-only steps in the plan.
ShowReplacementSteps bool // true to show the replacement steps in the plan.
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
DOT bool // true if we should print the DOT file for this plan.
Output string // the place to store the output, if any.
}
func deployLatest(info *envCmdInfo, opts deployOptions) error {
result, err := plan(info, opts)
if err != nil {
return err
}
if result != nil {
defer contract.IgnoreClose(result)
if opts.DryRun {
// If a dry run, just print the plan, don't actually carry out the deployment.
if err := printPlan(result, opts); err != nil {
return err
}
} else {
// Otherwise, we will actually deploy the latest bits.
var header bytes.Buffer
printPrelude(&header, result, opts, false)
header.WriteString(fmt.Sprintf("%vDeploying changes:%v\n", colors.SpecUnimportant, colors.Reset))
fmt.Print(colors.Colorize(&header))
// Create an object to track progress and perform the actual operations.
start := time.Now()
progress := newProgress(opts)
summary, _, _, err := result.Plan.Apply(progress)
contract.Assert(summary != nil)
// Print a summary.
var footer bytes.Buffer
// Print out the total number of steps performed (and their kinds), the duration, and any summary info.
if c := printChangeSummary(&footer, progress.Ops, false); c != 0 {
footer.WriteString(fmt.Sprintf("%vDeployment duration: %v%v\n",
colors.SpecUnimportant, time.Since(start), colors.Reset))
}
if progress.MaybeCorrupt {
footer.WriteString(fmt.Sprintf(
"%vA catastrophic error occurred; resources states may be unknown%v\n",
colors.SpecAttention, colors.Reset))
}
// Now save the updated snapshot to the specified output file, if any, or the standard location otherwise.
// Note that if a failure has occurred, the Apply routine above will have returned a safe checkpoint.
targ := result.Info.Target
saveEnv(targ, summary.Snap(), opts.Output, true /*overwrite*/)
fmt.Print(colors.Colorize(&footer))
return err
}
}
return nil
}
// deployProgress pretty-prints the plan application process as it goes.
type deployProgress struct {
Steps int
Ops map[deploy.StepOp]int
MaybeCorrupt bool
Opts deployOptions
}
func newProgress(opts deployOptions) *deployProgress {
return &deployProgress{
Steps: 0,
Ops: make(map[deploy.StepOp]int),
Opts: opts,
}
}
func (prog *deployProgress) Before(step deploy.Step) {
if shouldShow(step, prog.Opts) {
var b bytes.Buffer
printStep(&b, step, prog.Opts.Summary, false, "")
fmt.Print(colors.Colorize(&b))
}
}
func (prog *deployProgress) After(step deploy.Step, status resource.Status, err error) {
stepop := step.Op()
if err != nil {
// Issue a true, bonafide error.
cmdutil.Diag().Errorf(errors.ErrorPlanApplyFailed, err)
// Print the state of the resource; we don't issue the error, because the deploy above will do that.
var b bytes.Buffer
stepnum := prog.Steps + 1
b.WriteString(fmt.Sprintf("Step #%v failed [%v]: ", stepnum, stepop))
switch status {
case resource.StatusOK:
b.WriteString(colors.SpecNote)
b.WriteString("provider successfully recovered from this failure")
case resource.StatusUnknown:
b.WriteString(colors.SpecAttention)
b.WriteString("this failure was catastrophic and the provider cannot guarantee recovery")
prog.MaybeCorrupt = true
default:
contract.Failf("Unrecognized resource state: %v", status)
}
b.WriteString(colors.Reset)
b.WriteString("\n")
fmt.Print(colors.Colorize(&b))
} else {
// Increment the counters.
if step.Logical() {
prog.Steps++
prog.Ops[stepop]++
}
// Print out any output properties that got created as a result of this operation.
if shouldShow(step, prog.Opts) && !prog.Opts.Summary {
var b bytes.Buffer
printResourceOutputProperties(&b, step, "")
fmt.Print(colors.Colorize(&b))
}
}
}

View file

@ -5,7 +5,7 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/tokens" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
) )
@ -30,7 +30,7 @@ func newDestroyCmd() *cobra.Command {
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
if dryRun || yes || if dryRun || yes ||
confirmPrompt("This will permanently destroy all resources in the '%v' environment!", env) { confirmPrompt("This will permanently destroy all resources in the '%v' environment!", env) {
return Destroy(env, pkgargFromArgs(args), dryRun, debug, summary) return engine.Destroy(env, pkgargFromArgs(args), dryRun, debug, summary)
} }
return nil return nil
@ -55,17 +55,3 @@ func newDestroyCmd() *cobra.Command {
return cmd return cmd
} }
func Destroy(envName string, pkgarg string, dryRun bool, debug bool, summary bool) error {
info, err := initEnvCmdName(tokens.QName(envName), pkgarg)
if err != nil {
return err
}
return deployLatest(info, deployOptions{
Debug: debug,
Destroy: true,
DryRun: dryRun,
Summary: summary,
})
}

View file

@ -3,26 +3,10 @@
package main package main
import ( import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
goerr "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/resource/environment"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/pulumi/pulumi-fabric/pkg/util/mapper"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
) )
func newEnvCmd() *cobra.Command { func newEnvCmd() *cobra.Command {
@ -37,7 +21,7 @@ func newEnvCmd() *cobra.Command {
"Each environment has a configuration and deployment history associated with it, stored in\n" + "Each environment has a configuration and deployment history associated with it, stored in\n" +
"the workspace, in addition to a full checkpoint of the last known good deployment.\n", "the workspace, in addition to a full checkpoint of the last known good deployment.\n",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
return EnvInfo(showIDs, showURNs) return engine.EnvInfo(showIDs, showURNs)
}), }),
} }
@ -53,274 +37,3 @@ func newEnvCmd() *cobra.Command {
return cmd return cmd
} }
func EnvInfo(showIDs bool, showURNs bool) error {
curr := getCurrentEnv()
if curr == "" {
return goerr.New("no current environment; either `lumi env init` or `lumi env select` one")
}
fmt.Printf("Current environment is %v\n", curr)
fmt.Printf(" (use `lumi env select` to change environments; `lumi env ls` lists known ones)\n")
target, snapshot, checkpoint := readEnv(curr)
if checkpoint == nil {
return goerr.Errorf("could not read environment information")
}
if checkpoint.Latest != nil {
fmt.Printf("Last deployment at %v\n", checkpoint.Latest.Time)
if checkpoint.Latest.Info != nil {
fmt.Printf("Additional deployment info: %v\n", checkpoint.Latest.Info)
}
}
if target.Config != nil && len(target.Config) > 0 {
fmt.Printf("%v configuration variables set (see `lumi config` for details)\n", len(target.Config))
}
if snapshot == nil || len(snapshot.Resources) == 0 {
fmt.Printf("No resources currently in this environment\n")
} else {
fmt.Printf("%v resources currently in this environment:\n", len(snapshot.Resources))
fmt.Printf("\n")
fmt.Printf("%-48s %s\n", "TYPE", "NAME")
for _, res := range snapshot.Resources {
fmt.Printf("%-48s %s\n", res.Type(), res.URN().Name())
// If the ID and/or URN is requested, show it on the following line. It would be nice to do this
// on a single line, but they can get quite lengthy and so this formatting makes more sense.
if showIDs {
fmt.Printf("\tID: %s\n", res.ID)
}
if showURNs {
fmt.Printf("\tURN: %s\n", res.URN())
}
}
}
return nil
}
func initEnvCmd(name string, pkgarg string) (*envCmdInfo, error) {
return initEnvCmdName(tokens.QName(name), pkgarg)
}
func initEnvCmdName(name tokens.QName, pkgarg string) (*envCmdInfo, error) {
// If the name is blank, use the default.
if name == "" {
name = getCurrentEnv()
}
if name == "" {
return nil, goerr.Errorf("missing environment name (and no default found)")
}
// Read in the deployment information, bailing if an IO error occurs.
target, snapshot, checkpoint := readEnv(name)
if checkpoint == nil {
return nil, goerr.Errorf("could not read environment information")
}
contract.Assert(target != nil)
contract.Assert(checkpoint != nil)
return &envCmdInfo{
Target: target,
Checkpoint: checkpoint,
Snapshot: snapshot,
PackageArg: pkgarg,
}, nil
}
type envCmdInfo struct {
Target *deploy.Target // the target environment.
Checkpoint *environment.Checkpoint // the full serialized checkpoint from which this came.
Snapshot *deploy.Snapshot // the environment's latest deployment snapshot
PackageArg string // an optional path to a package to pass to the compiler
}
func confirmPrompt(msg string, name string) bool {
prompt := fmt.Sprintf(msg, name)
fmt.Print(
colors.ColorizeText(fmt.Sprintf("%v%v%v\n", colors.SpecAttention, prompt, colors.Reset)))
fmt.Printf("Please confirm that this is what you'd like to do by typing (\"%v\"): ", name)
reader := bufio.NewReader(os.Stdin)
if line, _ := reader.ReadString('\n'); line != name+"\n" {
fmt.Fprintf(os.Stderr, "Confirmation declined -- exiting without doing anything\n")
return false
}
return true
}
// createEnv just creates a new empty environment without deploying anything into it.
func createEnv(name tokens.QName) {
env := &deploy.Target{Name: name}
if success := saveEnv(env, nil, "", false); success {
fmt.Printf("Environment '%v' initialized; see `lumi deploy` to deploy into it\n", name)
setCurrentEnv(name, false)
}
}
// newWorkspace creates a new workspace using the current working directory.
func newWorkspace() (workspace.W, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
ctx := core.NewContext(pwd, nil, &core.Options{})
return workspace.New(ctx)
}
// getCurrentEnv reads the current environment.
func getCurrentEnv() tokens.QName {
var name tokens.QName
w, err := newWorkspace()
if err == nil {
name = w.Settings().Env
}
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
}
return name
}
// setCurrentEnv changes the current environment to the given environment name, issuing an error if it doesn't exist.
func setCurrentEnv(name tokens.QName, verify bool) {
if verify {
if _, _, checkpoint := readEnv(name); checkpoint == nil {
return // no environment by this name exists, bail out.
}
}
// Switch the current workspace to that environment.
w, err := newWorkspace()
if err == nil {
w.Settings().Env = name
err = w.Save()
}
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
}
}
// removeTarget permanently deletes the environment's information from the local workstation.
func removeTarget(env *deploy.Target) {
deleteTarget(env)
msg := fmt.Sprintf("%sEnvironment '%s' has been removed!%s\n",
colors.SpecAttention, env.Name, colors.Reset)
fmt.Print(colors.ColorizeText(msg))
}
// backupTarget makes a backup of an existing file, in preparation for writing a new one. Instead of a copy, it
// simply renames the file, which is simpler, more efficient, etc.
func backupTarget(file string) {
contract.Require(file != "", "file")
err := os.Rename(file, file+".bak")
contract.IgnoreError(err) // ignore errors.
// IDEA: consider multiple backups (.bak.bak.bak...etc).
}
// deleteTarget removes an existing snapshot file, leaving behind a backup.
func deleteTarget(env *deploy.Target) {
contract.Require(env != nil, "env")
// Just make a backup of the file and don't write out anything new.
file := workspace.EnvPath(env.Name)
backupTarget(file)
}
// readEnv reads in an existing snapshot file, issuing an error and returning nil if something goes awry.
func readEnv(name tokens.QName) (*deploy.Target, *deploy.Snapshot, *environment.Checkpoint) {
contract.Require(name != "", "name")
file := workspace.EnvPath(name)
// Detect the encoding of the file so we can do our initial unmarshaling.
m, ext := encoding.Detect(file)
if m == nil {
cmdutil.Diag().Errorf(errors.ErrorIllegalMarkupExtension, ext)
return nil, nil, nil
}
// Now read the whole file into a byte blob.
b, err := ioutil.ReadFile(file)
if err != nil {
if os.IsNotExist(err) {
cmdutil.Diag().Errorf(errors.ErrorInvalidEnvName, name)
} else {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
}
return nil, nil, nil
}
// Unmarshal the contents into a checkpoint structure.
var checkpoint environment.Checkpoint
if err = m.Unmarshal(b, &checkpoint); err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, file, err)
return nil, nil, nil
}
// Next, use the mapping infrastructure to validate the contents.
// IDEA: we can eliminate this redundant unmarshaling once Go supports strict unmarshaling.
var obj map[string]interface{}
if err = m.Unmarshal(b, &obj); err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, file, err)
return nil, nil, nil
}
if obj["latest"] != nil {
if latest, islatest := obj["latest"].(map[string]interface{}); islatest {
delete(latest, "resources") // remove the resources, since they require custom marshaling.
}
}
md := mapper.New(nil)
var ignore environment.Checkpoint // just for errors.
if err = md.Decode(obj, &ignore); err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, file, err)
return nil, nil, nil
}
target, snapshot := environment.DeserializeCheckpoint(&checkpoint)
contract.Assert(target != nil)
return target, snapshot, &checkpoint
}
// saveEnv saves a new snapshot at the given location, backing up any existing ones.
func saveEnv(env *deploy.Target, snap *deploy.Snapshot, file string, existok bool) bool {
contract.Require(env != nil, "env")
if file == "" {
file = workspace.EnvPath(env.Name)
}
// Make a serializable LumiGL data structure and then use the encoder to encode it.
m, ext := encoding.Detect(file)
if m == nil {
cmdutil.Diag().Errorf(errors.ErrorIllegalMarkupExtension, ext)
return false
}
if filepath.Ext(file) == "" {
file = file + ext
}
dep := environment.SerializeCheckpoint(env, snap)
b, err := m.Marshal(dep)
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
return false
}
// If it's not ok for the file to already exist, ensure that it doesn't.
if !existok {
if _, staterr := os.Stat(file); staterr == nil {
cmdutil.Diag().Errorf(errors.ErrorIO, goerr.Errorf("file '%v' already exists", file))
return false
}
}
// Back up the existing file if it already exists.
backupTarget(file)
// Ensure the directory exists.
if err = os.MkdirAll(filepath.Dir(file), 0700); err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
return false
}
// And now write out the new snapshot file, overwriting that location.
if err = ioutil.WriteFile(file, b, 0600); err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
return false
}
return true
}

View file

@ -7,7 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/tokens" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
) )
@ -26,12 +26,7 @@ func newEnvInitCmd() *cobra.Command {
return errors.New("missing required environment name") return errors.New("missing required environment name")
} }
return InitEnv(args[0]) return engine.InitEnv(args[0])
}), }),
} }
} }
func InitEnv(name string) error {
createEnv(tokens.QName(name))
return nil
}

View file

@ -3,19 +3,10 @@
package main package main
import ( import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/encoding" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
) )
func newEnvLsCmd() *cobra.Command { func newEnvLsCmd() *cobra.Command {
@ -24,56 +15,7 @@ func newEnvLsCmd() *cobra.Command {
Aliases: []string{"list"}, Aliases: []string{"list"},
Short: "List all known environments", Short: "List all known environments",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
return ListEnvs() return engine.ListEnvs()
}), }),
} }
} }
func ListEnvs() error {
// Read the environment directory.
path := workspace.EnvPath("")
files, err := ioutil.ReadDir(path)
if err != nil && !os.IsNotExist(err) {
return errors.Errorf("could not read environments: %v", err)
}
fmt.Printf("%-20s %-48s %-12s\n", "NAME", "LAST DEPLOYMENT", "RESOURCE COUNT")
curr := getCurrentEnv()
for _, file := range files {
// Ignore directories.
if file.IsDir() {
continue
}
// Skip files without valid extensions (e.g., *.bak files).
envfn := file.Name()
ext := filepath.Ext(envfn)
if _, has := encoding.Marshalers[ext]; !has {
continue
}
// Read in this environment's information.
name := tokens.QName(envfn[:len(envfn)-len(ext)])
target, snapshot, checkpoint := readEnv(name)
if checkpoint == nil {
continue // failure reading the environment information.
}
// Now print out the name, last deployment time (if any), and resources (if any).
lastDeploy := "n/a"
resourceCount := "n/a"
if checkpoint.Latest != nil {
lastDeploy = checkpoint.Latest.Time.String()
}
if snapshot != nil {
resourceCount = strconv.Itoa(len(snapshot.Resources))
}
display := target.Name
if display == curr {
display += "*" // fancify the current environment.
}
fmt.Printf("%-20s %-48s %-12s\n", display, lastDeploy, resourceCount)
}
return nil
}

View file

@ -4,9 +4,9 @@ package main
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
) )
@ -33,7 +33,7 @@ func newEnvRmCmd() *cobra.Command {
// Ensure the user really wants to do this. // Ensure the user really wants to do this.
if yes || if yes ||
confirmPrompt("This will permanently remove the '%v' environment!", envName) { confirmPrompt("This will permanently remove the '%v' environment!", envName) {
return RemoveEnv(envName, force) return engine.RemoveEnv(envName, force)
} }
return nil return nil
@ -49,22 +49,3 @@ func newEnvRmCmd() *cobra.Command {
return cmd return cmd
} }
func RemoveEnv(envName string, force bool) error {
contract.Assert(envName != "")
info, err := initEnvCmd(envName, "")
if err != nil {
return err
}
// Don't remove environments that still have resources.
if !force && info.Snapshot != nil && len(info.Snapshot.Resources) > 0 {
return errors.Errorf(
"'%v' still has resources; removal rejected; pass --force to override", info.Target.Name)
}
removeTarget(info.Target)
return nil
}

View file

@ -3,11 +3,9 @@
package main package main
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/tokens" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
) )
@ -24,22 +22,10 @@ func newEnvSelectCmd() *cobra.Command {
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
// Read in the name of the environment to switch to. // Read in the name of the environment to switch to.
if len(args) == 0 { if len(args) == 0 {
return GetCurrentEnv() return engine.GetCurrentEnv()
} }
return SelectEnv(args[0]) return engine.SelectEnv(args[0])
}), }),
} }
} }
func GetCurrentEnv() error {
if name := getCurrentEnv(); name != "" {
fmt.Println(name)
}
return nil
}
func SelectEnv(envName string) error {
setCurrentEnv(tokens.QName(envName), true)
return nil
}

View file

@ -3,17 +3,15 @@
package main package main
import ( import (
"bufio"
"fmt"
"os"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/compiler" "github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/compiler/binder"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors"
"github.com/pulumi/pulumi-fabric/pkg/compiler/symbols"
"github.com/pulumi/pulumi-fabric/pkg/pack"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
) )
// NewLumiCmd creates a new Lumi Cmd instance. // NewLumiCmd creates a new Lumi Cmd instance.
@ -56,60 +54,15 @@ func pkgargFromArgs(args []string) string {
return "" return ""
} }
// TODO[pulumi/pulumi-fabric#88]: enable arguments to flow to the package itself. In that case, we want to split the func confirmPrompt(msg string, name string) bool {
// arguments at the --, if any, so we can still pass arguments to the compiler itself in these cases. prompt := fmt.Sprintf(msg, name)
func prepareCompiler(pkgarg string) (compiler.Compiler, *pack.Package) { fmt.Print(
// Create a compiler options object and map any flags and arguments to settings on it. colors.ColorizeText(fmt.Sprintf("%v%v%v\n", colors.SpecAttention, prompt, colors.Reset)))
opts := core.DefaultOptions() fmt.Printf("Please confirm that this is what you'd like to do by typing (\"%v\"): ", name)
reader := bufio.NewReader(os.Stdin)
// If a package argument is present, try to load that package (either via stdin or a path). if line, _ := reader.ReadString('\n'); line != name+"\n" {
var pkg *pack.Package fmt.Fprintf(os.Stderr, "Confirmation declined -- exiting without doing anything\n")
var root string return false
if pkgarg != "" {
pkg, root = readPackageFromArg(pkgarg)
} }
return true
// Now create a compiler object based on whether we loaded a package or just have a root to deal with.
var comp compiler.Compiler
var err error
if root == "" {
comp, err = compiler.Newwd(opts)
} else {
comp, err = compiler.New(root, opts)
}
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantCreateCompiler, err)
}
return comp, pkg
}
// compile just uses the standard logic to parse arguments, options, and to locate/compile a package. It returns the
// compilation result, or nil if an error occurred (in which case, we would expect diagnostics to have been output).
func compile(pkgarg string) *compileResult {
// Prepare the compiler info and, provided it succeeds, perform the compilation.
if comp, pkg := prepareCompiler(pkgarg); comp != nil {
var b binder.Binder
var pkgsym *symbols.Package
if pkg == nil {
b, pkgsym = comp.Compile()
} else {
b, pkgsym = comp.CompilePackage(pkg)
}
contract.Assert(b != nil)
contract.Assert(pkgsym != nil)
return &compileResult{
C: comp,
B: b,
Pkg: pkgsym,
}
}
return nil
}
type compileResult struct {
C compiler.Compiler
B binder.Binder
Pkg *symbols.Package
} }

View file

@ -3,19 +3,7 @@
package main package main
import ( import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/pack"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
) )
func newPackCmd() *cobra.Command { func newPackCmd() *cobra.Command {
@ -31,88 +19,3 @@ func newPackCmd() *cobra.Command {
return cmd return cmd
} }
// detectPackage returns a package given the path, or returns an error if one could not be located.
func detectPackage(path string) (*pack.Package, error) {
pkgpath, err := workspace.DetectPackage(path, cmdutil.Diag())
if err != nil {
return nil, errors.Errorf("could not locate a package to load: %v", err)
} else if pkgpath == "" {
return nil, errors.Errorf("no package found at: %v", err)
}
pkg, _ := readPackage(pkgpath)
contract.Assert(pkg != nil)
return pkg, nil
}
// readPackageFromArg reads a package from an argument value. It can be "-" to request reading from Stdin, and is
// interpreted as a path otherwise. If an error occurs, it is printed to Stderr, and the returned value will be nil.
// In addition to the package, a root directory is returned that the compiler should be formed over, if any.
func readPackageFromArg(arg string) (*pack.Package, string) {
// If the arg is simply "-", read from stdin.
if arg == "-" {
return readPackageFromStdin(), ""
}
// Read the package from a file.
return readPackage(arg)
}
// readPackageFromStdin attempts to read a package from Stdin; if an error occurs, it will be printed to Stderr, and
// the returned value will be nil.
func readPackageFromStdin() *pack.Package {
// If stdin, read the package from text, and then create a compiler using the working directory.
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not read from stdin\n")
fmt.Fprintf(os.Stderr, " %v\n", err)
return nil
}
return DecodePackage(encoding.Marshalers[".json"], b, "stdin")
}
// readPackage attempts to read a package from the given path; if an error occurs, it will be printed to Stderr, and
// the returned value will be nil. If the path is a directory, nil is returned.
func readPackage(path string) (*pack.Package, string) {
// If it's a directory, bail early.
info, err := os.Stat(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not read path '%v': %v\n", path, err)
return nil, ""
}
if info.IsDir() {
return nil, path
}
// Lookup the marshaler for this format.
ext := filepath.Ext(path)
m, has := encoding.Marshalers[ext]
if !has {
fmt.Fprintf(os.Stderr, "error: no marshaler found for file format '%v'\n", ext)
return nil, ""
}
// Read the contents.
b, err := ioutil.ReadFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: a problem occurred when reading file '%v'\n", path)
fmt.Fprintf(os.Stderr, " %v\n", err)
return nil, ""
}
return DecodePackage(m, b, path), filepath.Dir(path)
}
// DecodePackage turns a byte array into a package using the given marshaler. If an error occurs, it is printed to
// Stderr, and the returned package value will be nil.
func DecodePackage(m encoding.Marshaler, b []byte, path string) *pack.Package {
// Unmarshal the contents into a fresh package.
pkg, err := encoding.Decode(m, b)
if err != nil {
fmt.Fprintf(os.Stderr, "error: a problem occurred when unmarshaling file '%v'\n", path)
fmt.Fprintf(os.Stderr, " %v\n", err)
return nil
}
return pkg
}

View file

@ -3,15 +3,9 @@
package main package main
import ( import (
"fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/eval"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract" "github.com/pulumi/pulumi-fabric/pkg/util/contract"
) )
@ -33,7 +27,7 @@ func newPackEvalCmd() *cobra.Command {
"a path to a package elsewhere can be provided as the [package] argument.", "a path to a package elsewhere can be provided as the [package] argument.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
contract.Assertf(!dotOutput, "TODO[pulumi/pulumi-fabric#235]: DOT files not yet supported") contract.Assertf(!dotOutput, "TODO[pulumi/pulumi-fabric#235]: DOT files not yet supported")
return PackEval(configEnv, args) return engine.PackEval(configEnv, args)
}), }),
} }
@ -46,71 +40,3 @@ func newPackEvalCmd() *cobra.Command {
return cmd return cmd
} }
func PackEval(configEnv string, args []string) error {
// First, load and compile the package.
result := compile(pkgargFromArgs(args))
if result == nil {
return nil
}
// Now fire up an interpreter so we can run the program.
e := eval.New(result.B.Ctx(), nil)
// If configuration was requested, load it up and populate the object state.
if configEnv != "" {
envInfo, err := initEnvCmdName(tokens.QName(configEnv), pkgargFromArgs(args))
if err != nil {
return err
}
if err := deploy.InitEvalConfig(result.B.Ctx(), e, envInfo.Target.Config); err != nil {
return err
}
}
// Finally, execute the entire program, and serialize the return value (if any).
packArgs := dashdashArgsToMap(args)
if obj, _ := e.EvaluatePackage(result.Pkg, packArgs); obj != nil {
fmt.Print(obj)
}
return nil
}
// dashdashArgsToMap is a simple args parser that places incoming key/value pairs into a map. These are then used
// during package compilation as inputs to the main entrypoint function.
// IDEA: this is fairly rudimentary; we eventually want to support arrays, maps, and complex types.
func dashdashArgsToMap(args []string) core.Args {
mapped := make(core.Args)
for i := 0; i < len(args); i++ {
arg := args[i]
// Eat - or -- at the start.
if arg[0] == '-' {
arg = arg[1:]
if arg[0] == '-' {
arg = arg[1:]
}
}
// Now find a k=v, and split the k/v part.
if eq := strings.IndexByte(arg, '='); eq != -1 {
// For --k=v, simply store v underneath k's entry.
mapped[tokens.Name(arg[:eq])] = arg[eq+1:]
} else {
if i+1 < len(args) && args[i+1][0] != '-' {
// If the next arg doesn't start with '-' (i.e., another flag) use its value.
mapped[tokens.Name(arg)] = args[i+1]
i++
} else if arg[0:3] == "no-" {
// For --no-k style args, strip off the no- prefix and store false underneath k.
mapped[tokens.Name(arg[3:])] = false
} else {
// For all other --k args, assume this is a boolean flag, and set the value of k to true.
mapped[tokens.Name(arg)] = true
}
}
}
return mapped
}

View file

@ -3,18 +3,10 @@
package main package main
import ( import (
"fmt"
"os"
"strings"
"unicode"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/compiler/ast" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/pack"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
) )
func newPackInfoCmd() *cobra.Command { func newPackInfoCmd() *cobra.Command {
@ -36,7 +28,7 @@ func newPackInfoCmd() *cobra.Command {
printExportedSymbols = true printExportedSymbols = true
} }
return PackInfo(printExportedSymbols, printIL, printSymbols, args) return engine.PackInfo(printExportedSymbols, printIL, printSymbols, args)
}), }),
} }
@ -55,389 +47,3 @@ func newPackInfoCmd() *cobra.Command {
return cmd return cmd
} }
func PackInfo(printExportedSymbols bool, printIL bool, printSymbols bool, args []string) error {
var pkg *pack.Package
var err error
if len(args) == 0 {
// No package specified, just load from the current directory.
pwd, locerr := os.Getwd()
if locerr != nil {
return locerr
}
if pkg, err = detectPackage(pwd); err != nil {
return err
}
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
} else {
// Enumerate the list of packages, deserialize them, and print information.
var path string
for _, arg := range args {
pkg, path = readPackageFromArg(arg)
if pkg == nil {
if pkg, err = detectPackage(path); err != nil {
return err
}
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
}
}
}
return nil
}
func printComment(pc *string, indent string) {
// Prints a comment header using the given indentation, wrapping at 100 lines.
if pc != nil {
prefix := "// "
maxlen := 100 - len(indent)
// For every tab, chew up 3 more chars (so each one is 4 chars wide).
for _, i := range indent {
if i == '\t' {
maxlen -= 3
}
}
maxlen -= len(prefix)
if maxlen < 40 {
maxlen = 40
}
c := make([]rune, 0)
for _, r := range *pc {
c = append(c, r)
}
for len(c) > 0 {
fmt.Print(indent + prefix)
// Now, try to split the comment as close to maxlen-3 chars as possible (taking into account indent+"// "),
// but don't split words -- only split at whitespace characters if we can help it.
six := maxlen
for {
if len(c) <= six {
six = len(c)
break
} else if unicode.IsSpace(c[six]) {
// It's a space, set six to the first non-space character beforehand, and eix to the first
// non-space character afterwards.
for six > 0 && unicode.IsSpace(c[six-1]) {
six--
}
break
} else if six == 0 {
// We hit the start of the string and didn't find any spaces. Start over and try to find the
// first space *beyond* the start point (instead of *before*) and use that.
six = maxlen + 1
for six < len(c) && !unicode.IsSpace(c[six]) {
six++
}
break
}
// We need to keep searching, back up one and try again.
six--
}
// Print what we've got thus far, plus a newline.
fmt.Printf("%v\n", string(c[:six]))
// Now find the first non-space character beyond the split point and use that for the remainder.
eix := six
for eix < len(c) && unicode.IsSpace(c[eix]) {
eix++
}
c = c[eix:]
}
}
}
// printPackage pretty-prints the package metadata.
func printPackage(pkg *pack.Package, printSymbols bool, printExports bool, printIL bool) {
printComment(pkg.Description, "")
fmt.Printf("package \"%v\" {\n", pkg.Name)
if pkg.Author != nil {
fmt.Printf("%vauthor \"%v\"\n", tab, *pkg.Author)
}
if pkg.Website != nil {
fmt.Printf("%vwebsite \"%v\"\n", tab, *pkg.Website)
}
if pkg.License != nil {
fmt.Printf("%vlicense \"%v\"\n", tab, *pkg.License)
}
// Print the dependencies:
fmt.Printf("%vdependencies [", tab)
if pkg.Dependencies != nil && len(*pkg.Dependencies) > 0 {
fmt.Printf("\n")
for _, dep := range pack.StableDependencies(*pkg.Dependencies) {
fmt.Printf("%v%v: \"%v\"\n", tab+tab, dep, (*pkg.Dependencies)[dep])
}
fmt.Printf("%v", tab)
}
fmt.Printf("]\n")
// Print the modules (just names by default, or full symbols and/or IL if requested).
printModules(pkg, printSymbols, printExports, printIL, tab)
fmt.Printf("}\n")
}
func printModules(pkg *pack.Package, printSymbols bool, printExports bool, printIL bool, indent string) {
if pkg.Modules != nil {
pkgtok := tokens.NewPackageToken(pkg.Name)
for _, name := range ast.StableModules(*pkg.Modules) {
mod := (*pkg.Modules)[name]
modtok := tokens.NewModuleToken(pkgtok, name)
// Print the name.
fmt.Printf("%vmodule \"%v\" {", indent, name)
// Now, if requested, print the tokens.
if printSymbols || printExports {
if mod.Exports != nil || mod.Members != nil {
fmt.Printf("\n")
exports := make(map[tokens.Token]bool)
if mod.Exports != nil {
// Print the exports.
fmt.Printf("%vexports [", indent+tab)
if mod.Exports != nil && len(*mod.Exports) > 0 {
fmt.Printf("\n")
for _, exp := range ast.StableModuleExports(*mod.Exports) {
ref := (*mod.Exports)[exp].Referent.Tok
fmt.Printf("%v\"%v\" -> \"%v\"\n", indent+tab+tab, exp, ref)
exports[ref] = true
}
fmt.Printf("%v", indent+tab)
}
fmt.Printf("]\n")
}
if mod.Members != nil {
// Print the members.
for _, member := range ast.StableModuleMembers(*mod.Members) {
memtok := tokens.NewModuleMemberToken(modtok, member)
printModuleMember(memtok, (*mod.Members)[member], printExports, exports, indent+tab)
}
fmt.Printf("%v", indent)
}
}
} else {
// Print a "..." so that it's clear we're omitting information, versus the module being empty.
fmt.Printf("...")
}
fmt.Printf("}\n")
}
}
}
func printModuleMember(tok tokens.ModuleMember, member ast.ModuleMember,
exportOnly bool, exports map[tokens.Token]bool, indent string) {
printComment(member.GetDescription(), indent)
if !exportOnly || exports[tokens.Token(tok)] {
switch member.GetKind() {
case ast.ClassKind:
printClass(tokens.Type(tok), member.(*ast.Class), exportOnly, indent)
case ast.ModulePropertyKind:
printModuleProperty(tok, member.(*ast.ModuleProperty), indent)
case ast.ModuleMethodKind:
printModuleMethod(tok, member.(*ast.ModuleMethod), indent)
default:
contract.Failf("Unexpected ModuleMember kind: %v (tok %v)\n", member.GetKind(), tok)
}
}
}
func printClass(tok tokens.Type, class *ast.Class, exportOnly bool, indent string) {
fmt.Printf("%vclass \"%v\"", indent, tok.Name())
var mods []string
if class.Sealed != nil && *class.Sealed {
mods = append(mods, "sealed")
}
if class.Abstract != nil && *class.Abstract {
mods = append(mods, "abstract")
}
if class.Record != nil && *class.Record {
mods = append(mods, "record")
}
if class.Interface != nil && *class.Interface {
mods = append(mods, "interface")
}
if class.Attributes != nil {
for _, att := range *class.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
fmt.Print(modString(mods))
if class.Extends != nil {
fmt.Printf("\n%vextends %v", indent+tab+tab, string(class.Extends.Tok))
}
if class.Implements != nil {
for _, impl := range *class.Implements {
fmt.Printf("\n%vimplements %v", indent+tab+tab, string(impl.Tok))
}
}
fmt.Printf(" {")
if class.Members != nil {
fmt.Printf("\n")
for _, member := range ast.StableClassMembers(*class.Members) {
memtok := tokens.NewClassMemberToken(tok, member)
printClassMember(memtok, (*class.Members)[member], exportOnly, indent+tab)
}
fmt.Print(indent)
}
fmt.Printf("}\n")
}
func printClassMember(tok tokens.ClassMember, member ast.ClassMember, exportOnly bool, indent string) {
printComment(member.GetDescription(), indent)
acc := member.GetAccess()
if !exportOnly || (acc != nil && *acc == tokens.PublicAccessibility) {
switch member.GetKind() {
case ast.ClassPropertyKind:
printClassProperty(tok.Name(), member.(*ast.ClassProperty), indent)
case ast.ClassMethodKind:
printClassMethod(tok.Name(), member.(*ast.ClassMethod), indent)
default:
contract.Failf("Unexpected ClassMember kind: %v\n", member.GetKind())
}
}
}
func printClassProperty(name tokens.ClassMemberName, prop *ast.ClassProperty, indent string) {
var mods []string
if prop.Access != nil {
mods = append(mods, string(*prop.Access))
}
if prop.Static != nil && *prop.Static {
mods = append(mods, "static")
}
if prop.Readonly != nil && *prop.Readonly {
mods = append(mods, "readonly")
}
if prop.Attributes != nil {
for _, att := range *prop.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
fmt.Printf("%vproperty \"%v\"%v", indent, name, modString(mods))
if prop.Type != nil {
fmt.Printf(": %v", prop.Type.Tok)
}
if prop.Getter != nil || prop.Setter != nil {
fmt.Printf(" {\n")
if prop.Getter != nil {
printClassMethod(tokens.ClassMemberName("get"), prop.Getter, indent+" ")
}
if prop.Setter != nil {
printClassMethod(tokens.ClassMemberName("set"), prop.Setter, indent+" ")
}
fmt.Printf("%v}\n", indent)
} else {
fmt.Printf("\n")
}
}
func printClassMethod(name tokens.ClassMemberName, meth *ast.ClassMethod, indent string) {
var mods []string
if meth.Access != nil {
mods = append(mods, string(*meth.Access))
}
if meth.Static != nil && *meth.Static {
mods = append(mods, "static")
}
if meth.Sealed != nil && *meth.Sealed {
mods = append(mods, "sealed")
}
if meth.Abstract != nil && *meth.Abstract {
mods = append(mods, "abstract")
}
if meth.Attributes != nil {
for _, att := range *meth.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
fmt.Printf("%vmethod \"%v\"%v: %v\n", indent, name, modString(mods), funcSig(meth))
}
func printModuleMethod(tok tokens.ModuleMember, meth *ast.ModuleMethod, indent string) {
fmt.Printf("%vmethod \"%v\": %v\n", indent, tok.Name(), funcSig(meth))
}
func printModuleProperty(tok tokens.ModuleMember, prop *ast.ModuleProperty, indent string) {
var mods []string
if prop.Readonly != nil && *prop.Readonly {
mods = append(mods, "readonly")
}
fmt.Printf("%vproperty \"%v\"%v", indent, tok.Name(), modString(mods))
if prop.Type != nil {
fmt.Printf(": %v", prop.Type.Tok)
}
fmt.Printf("\n")
}
func modString(mods []string) string {
if len(mods) == 0 {
return ""
}
s := " ["
for i, mod := range mods {
if i > 0 {
s += ", "
}
s += mod
}
s += "]"
return s
}
// spaces returns a string with the given number of spaces.
func spaces(num int) string {
return strings.Repeat(" ", num)
}
// tab is a tab represented as spaces, since some consoles have ridiculously wide true tabs.
var tab = spaces(4)
func funcSig(fun ast.Function) string {
sig := "("
// To create a signature, first concatenate the parameters.
params := fun.GetParameters()
if params != nil {
for i, param := range *params {
if i > 0 {
sig += ", "
}
sig += string(param.Name.Ident)
var mods []string
if param.Attributes != nil {
for _, att := range *param.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
sig += modString(mods)
if param.Type != nil {
sig += ": " + string(param.Type.Tok)
}
}
}
sig += ")"
// And then the return type, if present.
ret := fun.GetReturnType()
if ret != nil {
sig += ": " + string(ret.Tok)
}
return sig
}

View file

@ -3,8 +3,7 @@
package main package main
import ( import (
"errors" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -23,25 +22,9 @@ func newPackVerifyCmd() *cobra.Command {
"errors anywhere it doesn't obey them. This is generally useful for tools developers\n" + "errors anywhere it doesn't obey them. This is generally useful for tools developers\n" +
"and can ensure that code does not fail at runtime, when such invariants are checked.", "and can ensure that code does not fail at runtime, when such invariants are checked.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
return PackVerify(pkgargFromArgs(args)) return engine.PackVerify(pkgargFromArgs(args))
}), }),
} }
return cmd return cmd
} }
func PackVerify(pkgarg string) error {
// Prepare the compiler info and, provided it succeeds, perform the verification.
if comp, pkg := prepareCompiler(pkgarg); comp != nil {
// Now perform the compilation and extract the heap snapshot.
if pkg == nil && !comp.Verify() {
return errors.New("verification failed")
} else if pkg != nil && !comp.VerifyPackage(pkg) {
return errors.New("verification failed")
}
return nil
}
return errors.New("could not create prepare compiler")
}

View file

@ -3,21 +3,9 @@
package main package main
import ( import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/diag" "github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/resource/plugin"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil" "github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract" "github.com/pulumi/pulumi-fabric/pkg/util/contract"
) )
@ -49,7 +37,7 @@ func newPlanCmd() *cobra.Command {
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
contract.Assertf(!dotOutput, "TODO[pulumi/pulumi-fabric#235]: DOT files not yet supported") contract.Assertf(!dotOutput, "TODO[pulumi/pulumi-fabric#235]: DOT files not yet supported")
return Plan(PlanOptions{ return engine.Plan(engine.PlanOptions{
Package: pkgargFromArgs(args), Package: pkgargFromArgs(args),
Debug: debug, Debug: debug,
Environment: env, Environment: env,
@ -93,626 +81,3 @@ func newPlanCmd() *cobra.Command {
return cmd return cmd
} }
type PlanOptions struct {
Package string // the package to compute the plan for
Debug bool // true to enable resource debugging output.
Environment string // the environment to use when planning
Analyzers []string // an optional set of analyzers to run as part of this deployment.
ShowConfig bool // true to show the configuration variables being used.
ShowReads bool // true to show the read-only steps in the plan.
ShowReplacementSteps bool // true to show the replacement steps in the plan.
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
}
func Plan(opts PlanOptions) error {
info, err := initEnvCmdName(tokens.QName(opts.Environment), opts.Package)
if err != nil {
return err
}
deployOpts := deployOptions{
Debug: opts.Debug,
Destroy: false,
DryRun: true,
Analyzers: opts.Analyzers,
ShowConfig: opts.ShowConfig,
ShowReads: opts.ShowReads,
ShowReplacementSteps: opts.ShowReplacementSteps,
ShowSames: opts.ShowSames,
Summary: opts.Summary,
}
result, err := plan(info, deployOpts)
if err != nil {
return err
}
if result != nil {
defer contract.IgnoreClose(result)
if err := printPlan(result, deployOpts); err != nil {
return err
}
}
return nil
}
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
func plan(info *envCmdInfo, opts deployOptions) (*planResult, error) {
contract.Assert(info != nil)
contract.Assert(info.Target != nil)
// Initialize the diagnostics logger with the right stuff.
cmdutil.InitDiag(diag.FormatOptions{
Colors: true,
Debug: opts.Debug,
})
// Create a context for plugins.
ctx, err := plugin.NewContext(cmdutil.Diag(), nil)
if err != nil {
return nil, err
}
// First, compile the package, in preparatin for interpreting it and creating resources.
result := compile(info.PackageArg)
if result == nil || !result.B.Ctx().Diag.Success() {
return nil, fmt.Errorf("Errors during compilation: %v", result.B.Ctx().Diag.Errors())
}
// If that succeeded, create a new source that will perform interpretation of the compiled program.
// TODO[pulumi/pulumi-fabric#88]: we are passing `nil` as the arguments map; we need to allow a way to pass these.
source := deploy.NewEvalSource(ctx, result.B.Ctx(), result.Pkg, nil, info.Target.Config, opts.Destroy)
// If there are any analyzers in the project file, add them.
var analyzers []tokens.QName
if as := result.Pkg.Node.Analyzers; as != nil {
for _, a := range *as {
analyzers = append(analyzers, a)
}
}
// Append any analyzers from the command line.
for _, a := range opts.Analyzers {
analyzers = append(analyzers, tokens.QName(a))
}
// Generate a plan; this API handles all interesting cases (create, update, delete).
plan := deploy.NewPlan(ctx, info.Target, info.Snapshot, source, analyzers)
return &planResult{
Ctx: ctx,
Info: info,
Plan: plan,
}, nil
}
type planResult struct {
Ctx *plugin.Context // the context containing plugins and their state.
Info *envCmdInfo // plan command information.
Plan *deploy.Plan // the plan created by this command.
}
func (res *planResult) Close() error {
return res.Ctx.Close()
}
func printPlan(result *planResult, opts deployOptions) error {
// First print config/unchanged/etc. if necessary.
var prelude bytes.Buffer
printPrelude(&prelude, result, opts, true)
// Now walk the plan's steps and and pretty-print them out.
prelude.WriteString(fmt.Sprintf("%vPlanning changes:%v\n", colors.SpecUnimportant, colors.Reset))
fmt.Print(colors.Colorize(&prelude))
iter, err := result.Plan.Iterate()
if err != nil {
return errors.Errorf("An error occurred while preparing the plan: %v", err)
}
defer contract.IgnoreClose(iter)
step, err := iter.Next()
if err != nil {
return errors.Errorf("An error occurred while enumerating the plan: %v", err)
}
var summary bytes.Buffer
counts := make(map[deploy.StepOp]int)
for step != nil {
var err error
// Perform the pre-step.
if err = step.Pre(); err != nil {
return errors.Errorf("An error occurred preparing the plan: %v", err)
}
// Print this step information (resource and all its properties).
// IDEA: it would be nice if, in the output, we showed the dependencies a la `git log --graph`.
if shouldShow(step, opts) {
printStep(&summary, step, opts.Summary, true, "")
}
// Be sure to skip the step so that in-memory state updates are performed.
if err = step.Skip(); err != nil {
return errors.Errorf("An error occurred while advancing the plan: %v", err)
}
// Track the operation if shown and/or if it is a logically meaningful operation.
if step.Logical() {
counts[step.Op()]++
}
if step, err = iter.Next(); err != nil {
return errors.Errorf("An error occurred while viewing the plan: %v", err)
}
}
// Print a summary of operation counts.
printChangeSummary(&summary, counts, true)
fmt.Print(colors.Colorize(&summary))
return nil
}
// shouldShow returns true if a step should show in the output.
func shouldShow(step deploy.Step, opts deployOptions) bool {
// For certain operations, whether they are tracked is controlled by flags (to cut down on superfluous output).
if _, isrd := step.(deploy.ReadStep); isrd {
return opts.ShowReads
} else if step.Op() == deploy.OpSame {
return opts.ShowSames
} else if step.Op() == deploy.OpCreateReplacement || step.Op() == deploy.OpDeleteReplaced {
return opts.ShowReplacementSteps
} else if step.Op() == deploy.OpReplace {
return !opts.ShowReplacementSteps
}
return true
}
func printPrelude(b *bytes.Buffer, result *planResult, opts deployOptions, planning bool) {
// If there are configuration variables, show them.
if opts.ShowConfig {
printConfig(b, result.Info.Target.Config)
}
}
func printConfig(b *bytes.Buffer, config resource.ConfigMap) {
b.WriteString(fmt.Sprintf("%vConfiguration:%v\n", colors.SpecUnimportant, colors.Reset))
if config != nil {
var toks []string
for tok := range config {
toks = append(toks, string(tok))
}
sort.Strings(toks)
for _, tok := range toks {
b.WriteString(fmt.Sprintf("%v%v: %v\n", detailsIndent, tok, config[tokens.Token(tok)]))
}
}
}
func printChangeSummary(b *bytes.Buffer, counts map[deploy.StepOp]int, plan bool) int {
changes := 0
for op, c := range counts {
if op != deploy.OpSame {
changes += c
}
}
var kind string
if plan {
kind = "planned"
} else {
kind = "deployed"
}
var changesLabel string
if changes == 0 {
kind = "required"
changesLabel = "no"
} else {
changesLabel = strconv.Itoa(changes)
}
b.WriteString(fmt.Sprintf("%vinfo%v: %v %v %v:\n",
colors.SpecInfo, colors.Reset, changesLabel, plural("change", changes), kind))
var planTo string
var pastTense string
if plan {
planTo = "to "
} else {
pastTense = "d"
}
// Now summarize all of the changes; we print sames a little differently.
for _, op := range deploy.StepOps {
if op != deploy.OpSame {
if c := counts[op]; c > 0 {
b.WriteString(fmt.Sprintf(" %v%v %v %v%v%v%v\n",
op.Prefix(), c, plural("resource", c), planTo, op, pastTense, colors.Reset))
}
}
}
if c := counts[deploy.OpSame]; c > 0 {
b.WriteString(fmt.Sprintf(" %v %v unchanged\n", c, plural("resource", c)))
}
return changes
}
func plural(s string, c int) string {
if c != 1 {
s += "s"
}
return s
}
const detailsIndent = " " // 4 spaces, plus 2 for "+ ", "- ", and " " leaders
func printStep(b *bytes.Buffer, step deploy.Step, summary bool, planning bool, indent string) {
// First print out the operation's prefix.
b.WriteString(step.Op().Prefix())
// Next, print the resource type (since it is easy on the eyes and can be quickly identified).
printStepHeader(b, step)
b.WriteString(step.Op().Suffix())
// Next print the resource URN, properties, etc.
if mut, ismut := step.(deploy.MutatingStep); ismut {
var replaces []resource.PropertyKey
if step.Op() == deploy.OpCreateReplacement {
replaces = step.(*deploy.CreateStep).Keys()
} else if step.Op() == deploy.OpReplace {
replaces = step.(*deploy.ReplaceStep).Keys()
}
printResourceProperties(b, mut.URN(), mut.Old(), mut.New(), replaces, summary, planning, indent)
} else if rd, isrd := step.(deploy.ReadStep); isrd {
for _, res := range rd.Resources() {
printResourceProperties(b, "", nil, res.State(), nil, summary, planning, indent)
}
} else {
contract.Failf("Expected each step to either be mutating or read-only")
}
// Finally make sure to reset the color.
b.WriteString(colors.Reset)
}
func printStepHeader(b *bytes.Buffer, step deploy.Step) {
b.WriteString(fmt.Sprintf("%s: (%s)\n", string(step.Type()), step.Op()))
}
func printResourceProperties(b *bytes.Buffer, urn resource.URN, old *resource.State, new *resource.State,
replaces []resource.PropertyKey, summary bool, planning bool, indent string) {
indent += detailsIndent
// Print out the URN and, if present, the ID, as "pseudo-properties".
var id resource.ID
if old != nil {
id = old.ID
}
if id != "" {
b.WriteString(fmt.Sprintf("%s[id=%s]\n", indent, string(id)))
}
if urn != "" {
b.WriteString(fmt.Sprintf("%s[urn=%s]\n", indent, urn))
}
if !summary {
// Print all of the properties associated with this resource.
if old == nil && new != nil {
printObject(b, new.AllInputs(), planning, indent)
} else if new == nil && old != nil {
printObject(b, old.AllInputs(), planning, indent)
} else {
printOldNewDiffs(b, old.AllInputs(), new.AllInputs(), replaces, planning, indent)
}
}
}
func maxKey(keys []resource.PropertyKey) int {
maxkey := 0
for _, k := range keys {
if len(k) > maxkey {
maxkey = len(k)
}
}
return maxkey
}
func printObject(b *bytes.Buffer, props resource.PropertyMap, planning bool, indent string) {
// Compute the maximum with of property keys so we can justify everything.
keys := props.StableKeys()
maxkey := maxKey(keys)
// Now print out the values intelligently based on the type.
for _, k := range keys {
if v := props[k]; shouldPrintPropertyValue(v, planning) {
printPropertyTitle(b, k, maxkey, indent)
printPropertyValue(b, v, planning, indent)
}
}
}
// printResourceOutputProperties prints only those properties that either differ from the input properties or, if
// there is an old snapshot of the resource, differ from the prior old snapshot's output properties.
func printResourceOutputProperties(b *bytes.Buffer, step deploy.Step, indent string) {
// Only certain kinds of steps have output properties associated with them.
mut := step.(deploy.MutatingStep)
if mut == nil ||
(step.Op() != deploy.OpCreate &&
step.Op() != deploy.OpCreateReplacement &&
step.Op() != deploy.OpUpdate) {
return
}
indent += detailsIndent
b.WriteString(step.Op().Color())
b.WriteString(step.Op().Suffix())
// First fetch all the relevant property maps that we may consult.
newins := mut.New().Inputs
newouts := mut.New().Outputs
var oldouts resource.PropertyMap
if old := mut.Old(); old != nil {
oldouts = old.Outputs
}
// Now sort the keys and enumerate each output property in a deterministic order.
firstout := true
keys := newouts.StableKeys()
maxkey := maxKey(keys)
for _, k := range keys {
newout := newouts[k]
// Print this property if it is printable, and one of these cases
// 1) new ins has it and it's different;
// 2) new ins doesn't have it, but old outs does, and it's different;
// 3) neither old outs nor new ins contain it;
if shouldPrintPropertyValue(newout, true) {
var print bool
if newin, has := newins[k]; has {
print = (newout.Diff(newin) != nil) // case 1
} else if oldouts != nil {
if oldout, has := oldouts[k]; has {
print = (newout.Diff(oldout) != nil) // case 2
} else {
print = true // case 3
}
} else {
print = true // also case 3
}
if print {
if firstout {
b.WriteString(fmt.Sprintf("%v---outputs:---\n", indent))
firstout = false
}
printPropertyTitle(b, k, maxkey, indent)
printPropertyValue(b, newout, false, indent)
}
}
}
b.WriteString(colors.Reset)
}
func shouldPrintPropertyValue(v resource.PropertyValue, outs bool) bool {
if v.IsNull() {
// by default, don't print nulls (they just clutter up the output)
return false
}
if v.IsOutput() && !outs {
// also don't show output properties until the outs parameter tells us to.
return false
}
return true
}
func printPropertyTitle(b *bytes.Buffer, k resource.PropertyKey, align int, indent string) {
b.WriteString(fmt.Sprintf("%s%-"+strconv.Itoa(align)+"s: ", indent, k))
}
func printPropertyValue(b *bytes.Buffer, v resource.PropertyValue, planning bool, indent string) {
if v.IsNull() {
b.WriteString("<null>")
} else if v.IsBool() {
b.WriteString(fmt.Sprintf("%t", v.BoolValue()))
} else if v.IsNumber() {
b.WriteString(fmt.Sprintf("%v", v.NumberValue()))
} else if v.IsString() {
b.WriteString(fmt.Sprintf("%q", v.StringValue()))
} else if v.IsArray() {
arr := v.ArrayValue()
if len(arr) == 0 {
b.WriteString("[]")
} else {
b.WriteString(fmt.Sprintf("[\n"))
for i, elem := range arr {
newIndent := printArrayElemHeader(b, i, indent)
printPropertyValue(b, elem, planning, newIndent)
}
b.WriteString(fmt.Sprintf("%s]", indent))
}
} else if v.IsAsset() {
a := v.AssetValue()
if text, has := a.GetText(); has {
b.WriteString("asset {\n")
// pretty print the text, line by line, with proper breaks.
lines := strings.Split(text, "\n")
for _, line := range lines {
b.WriteString(fmt.Sprintf("%v \"%v\"\n", indent, line))
}
b.WriteString(fmt.Sprintf("%v}", indent))
} else if path, has := a.GetPath(); has {
b.WriteString(fmt.Sprintf("asset { file://%v }", path))
} else {
contract.Assert(a.IsURI())
b.WriteString(fmt.Sprintf("asset { %v }", a.URI))
}
} else if v.IsArchive() {
a := v.ArchiveValue()
if assets, has := a.GetAssets(); has {
b.WriteString("archive {\n")
var names []string
for name := range assets {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
b.WriteString(fmt.Sprintf("%v \"%v\": ", indent, name))
printPropertyValue(b, resource.NewAssetProperty(assets[name]), planning, indent+" ")
}
b.WriteString(fmt.Sprintf("%v}", indent))
} else if path, has := a.GetPath(); has {
b.WriteString(fmt.Sprintf("archive { file://%v }", path))
} else {
contract.Assert(a.IsURI())
b.WriteString(fmt.Sprintf("archive { %v }", a.URI))
}
} else if v.IsComputed() || v.IsOutput() {
b.WriteString(v.TypeString())
} else {
contract.Assert(v.IsObject())
obj := v.ObjectValue()
if len(obj) == 0 {
b.WriteString("{}")
} else {
b.WriteString("{\n")
printObject(b, obj, planning, indent+" ")
b.WriteString(fmt.Sprintf("%s}", indent))
}
}
b.WriteString("\n")
}
func getArrayElemHeader(b *bytes.Buffer, i int, indent string) (string, string) {
prefix := fmt.Sprintf(" %s[%d]: ", indent, i)
return prefix, fmt.Sprintf("%-"+strconv.Itoa(len(prefix))+"s", "")
}
func printArrayElemHeader(b *bytes.Buffer, i int, indent string) string {
prefix, newIndent := getArrayElemHeader(b, i, indent)
b.WriteString(prefix)
return newIndent
}
func printOldNewDiffs(b *bytes.Buffer, olds resource.PropertyMap, news resource.PropertyMap,
replaces []resource.PropertyKey, planning bool, indent string) {
// Get the full diff structure between the two, and print it (recursively).
if diff := olds.Diff(news); diff != nil {
printObjectDiff(b, *diff, replaces, false, planning, indent)
} else {
printObject(b, news, planning, indent)
}
}
func printObjectDiff(b *bytes.Buffer, diff resource.ObjectDiff,
replaces []resource.PropertyKey, causedReplace bool, planning bool, indent string) {
contract.Assert(len(indent) > 2)
// Compute the maximum with of property keys so we can justify everything.
keys := diff.Keys()
maxkey := maxKey(keys)
// If a list of what causes a resource to get replaced exist, create a handy map.
var replaceMap map[resource.PropertyKey]bool
if len(replaces) > 0 {
replaceMap = make(map[resource.PropertyKey]bool)
for _, k := range replaces {
replaceMap[k] = true
}
}
// To print an object diff, enumerate the keys in stable order, and print each property independently.
for _, k := range keys {
title := func(id string) { printPropertyTitle(b, k, maxkey, id) }
if add, isadd := diff.Adds[k]; isadd {
if shouldPrintPropertyValue(add, planning) {
b.WriteString(colors.SpecCreate)
title(addIndent(indent))
printPropertyValue(b, add, planning, addIndent(indent))
b.WriteString(colors.Reset)
}
} else if delete, isdelete := diff.Deletes[k]; isdelete {
if shouldPrintPropertyValue(delete, planning) {
b.WriteString(colors.SpecDelete)
title(deleteIndent(indent))
printPropertyValue(b, delete, planning, deleteIndent(indent))
b.WriteString(colors.Reset)
}
} else if update, isupdate := diff.Updates[k]; isupdate {
if !causedReplace && replaceMap != nil {
causedReplace = replaceMap[k]
}
printPropertyValueDiff(b, title, update, causedReplace, planning, indent)
} else if same := diff.Sames[k]; shouldPrintPropertyValue(same, planning) {
title(indent)
printPropertyValue(b, diff.Sames[k], planning, indent)
}
}
}
func printPropertyValueDiff(b *bytes.Buffer, title func(string), diff resource.ValueDiff,
causedReplace bool, planning bool, indent string) {
contract.Assert(len(indent) > 2)
if diff.Array != nil {
title(indent)
b.WriteString("[\n")
a := diff.Array
for i := 0; i < a.Len(); i++ {
_, newIndent := getArrayElemHeader(b, i, indent)
titleFunc := func(id string) { printArrayElemHeader(b, i, id) }
if add, isadd := a.Adds[i]; isadd {
b.WriteString(deploy.OpCreate.Color())
titleFunc(addIndent(indent))
printPropertyValue(b, add, planning, addIndent(newIndent))
b.WriteString(colors.Reset)
} else if delete, isdelete := a.Deletes[i]; isdelete {
b.WriteString(deploy.OpDelete.Color())
titleFunc(deleteIndent(indent))
printPropertyValue(b, delete, planning, deleteIndent(newIndent))
b.WriteString(colors.Reset)
} else if update, isupdate := a.Updates[i]; isupdate {
printPropertyValueDiff(b, title, update, causedReplace, planning, indent)
} else {
titleFunc(indent)
printPropertyValue(b, a.Sames[i], planning, newIndent)
}
}
b.WriteString(fmt.Sprintf("%s]\n", indent))
} else if diff.Object != nil {
title(indent)
b.WriteString("{\n")
printObjectDiff(b, *diff.Object, nil, causedReplace, planning, indent+" ")
b.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
// If we ended up here, the two values either differ by type, or they have different primitive values. We will
// simply emit a deletion line followed by an addition line.
if shouldPrintPropertyValue(diff.Old, false) {
var color string
if causedReplace {
color = deploy.OpDelete.Color() // this property triggered replacement; color as a delete
} else {
color = deploy.OpUpdate.Color()
}
b.WriteString(color)
title(deleteIndent(indent))
printPropertyValue(b, diff.Old, planning, deleteIndent(indent))
b.WriteString(colors.Reset)
}
if shouldPrintPropertyValue(diff.New, false) {
var color string
if causedReplace {
color = deploy.OpCreate.Color() // this property triggered replacement; color as a create
} else {
color = deploy.OpUpdate.Color()
}
b.WriteString(color)
title(addIndent(indent))
printPropertyValue(b, diff.New, planning, addIndent(indent))
b.WriteString(colors.Reset)
}
}
}
func addIndent(indent string) string { return indent[:len(indent)-2] + "+ " }
func deleteIndent(indent string) string { return indent[:len(indent)-2] + "- " }

70
pkg/engine/compiler.go Normal file
View file

@ -0,0 +1,70 @@
package engine
import (
"github.com/pulumi/pulumi-fabric/pkg/compiler"
"github.com/pulumi/pulumi-fabric/pkg/compiler/binder"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors"
"github.com/pulumi/pulumi-fabric/pkg/compiler/symbols"
"github.com/pulumi/pulumi-fabric/pkg/pack"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
// TODO[pulumi/pulumi-fabric#88]: enable arguments to flow to the package itself. In that case, we want to split the
// arguments at the --, if any, so we can still pass arguments to the compiler itself in these cases.
func prepareCompiler(pkgarg string) (compiler.Compiler, *pack.Package) {
// Create a compiler options object and map any flags and arguments to settings on it.
opts := core.DefaultOptions()
// If a package argument is present, try to load that package (either via stdin or a path).
var pkg *pack.Package
var root string
if pkgarg != "" {
pkg, root = readPackageFromArg(pkgarg)
}
// Now create a compiler object based on whether we loaded a package or just have a root to deal with.
var comp compiler.Compiler
var err error
if root == "" {
comp, err = compiler.Newwd(opts)
} else {
comp, err = compiler.New(root, opts)
}
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantCreateCompiler, err)
}
return comp, pkg
}
// compile just uses the standard logic to parse arguments, options, and to locate/compile a package. It returns the
// compilation result, or nil if an error occurred (in which case, we would expect diagnostics to have been output).
func compile(pkgarg string) *compileResult {
// Prepare the compiler info and, provided it succeeds, perform the compilation.
if comp, pkg := prepareCompiler(pkgarg); comp != nil {
var b binder.Binder
var pkgsym *symbols.Package
if pkg == nil {
b, pkgsym = comp.Compile()
} else {
b, pkgsym = comp.CompilePackage(pkg)
}
contract.Assert(b != nil)
contract.Assert(pkgsym != nil)
return &compileResult{
C: comp,
B: b,
Pkg: pkgsym,
}
}
return nil
}
type compileResult struct {
C compiler.Compiler
B binder.Binder
Pkg *symbols.Package
}

View file

@ -0,0 +1,24 @@
package engine
import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
)
func DeleteConfig(envName string, key string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config != nil {
delete(config, tokens.Token(key))
if !saveEnv(info.Target, info.Snapshot, "", true) {
return errors.Errorf("could not save configuration value")
}
}
return nil
}

25
pkg/engine/config_get.go Normal file
View file

@ -0,0 +1,25 @@
package engine
import (
"fmt"
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
)
func GetConfig(envName string, key string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config != nil {
if v, has := config[tokens.Token(key)]; has {
fmt.Printf("%v\n", v)
return nil
}
}
return errors.Errorf("configuration key '%v' not found for environment '%v'", key, info.Target.Name)
}

25
pkg/engine/config_list.go Normal file
View file

@ -0,0 +1,25 @@
package engine
import (
"fmt"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
)
func ListConfig(envName string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config != nil {
fmt.Printf("%-32s %-32s\n", "KEY", "VALUE")
for _, key := range info.Target.Config.StableKeys() {
v := info.Target.Config[key]
// TODO[pulumi/pulumi-fabric#113]: print complex values.
fmt.Printf("%-32s %-32s\n", key, v)
}
}
return nil
}

28
pkg/engine/config_set.go Normal file
View file

@ -0,0 +1,28 @@
package engine
import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
)
func SetConfig(envName string, key string, value string) error {
info, err := initEnvCmdName(tokens.QName(envName), "")
if err != nil {
return err
}
config := info.Target.Config
if config == nil {
config = make(resource.ConfigMap)
info.Target.Config = config
}
config[tokens.Token(key)] = value
if !saveEnv(info.Target, info.Snapshot, "", true) {
return errors.Errorf("could not save configuration value")
}
return nil
}

178
pkg/engine/deploy.go Normal file
View file

@ -0,0 +1,178 @@
package engine
import (
"bytes"
"fmt"
"time"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
type DeployOptions struct {
Environment string // the environment we are deploying into
Package string // the package we are deploying (or "" to use the default)
Debug bool // true to enable resource debugging output.
DryRun bool // true if we should just print the plan without performing it.
Analyzers []string // an optional set of analyzers to run as part of this deployment.
ShowConfig bool // true to show the configuration variables being used.
ShowReads bool // true to show the read-only steps in the plan.
ShowReplacementSteps bool // true to show the replacement steps in the plan.
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
Output string // the place to store the output, if any.
}
func Deploy(opts DeployOptions) error {
info, err := initEnvCmdName(tokens.QName(opts.Environment), opts.Package)
if err != nil {
return err
}
return deployLatest(info, deployOptions{
Debug: opts.Debug,
Destroy: false,
DryRun: opts.DryRun,
Analyzers: opts.Analyzers,
ShowConfig: opts.ShowConfig,
ShowReads: opts.ShowReads,
ShowReplacementSteps: opts.ShowReplacementSteps,
ShowSames: opts.ShowSames,
Summary: opts.Summary,
Output: opts.Output,
})
}
type deployOptions struct {
Debug bool // true to enable resource debugging output.
Create bool // true if we are creating resources.
Destroy bool // true if we are destroying the environment.
DryRun bool // true if we should just print the plan without performing it.
Analyzers []string // an optional set of analyzers to run as part of this deployment.
ShowConfig bool // true to show the configuration variables being used.
ShowReads bool // true to show the read-only steps in the plan.
ShowReplacementSteps bool // true to show the replacement steps in the plan.
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
DOT bool // true if we should print the DOT file for this plan.
Output string // the place to store the output, if any.
}
func deployLatest(info *envCmdInfo, opts deployOptions) error {
result, err := plan(info, opts)
if err != nil {
return err
}
if result != nil {
defer contract.IgnoreClose(result)
if opts.DryRun {
// If a dry run, just print the plan, don't actually carry out the deployment.
if err := printPlan(result, opts); err != nil {
return err
}
} else {
// Otherwise, we will actually deploy the latest bits.
var header bytes.Buffer
printPrelude(&header, result, opts, false)
header.WriteString(fmt.Sprintf("%vDeploying changes:%v\n", colors.SpecUnimportant, colors.Reset))
fmt.Print(colors.Colorize(&header))
// Create an object to track progress and perform the actual operations.
start := time.Now()
progress := newProgress(opts)
summary, _, _, err := result.Plan.Apply(progress)
contract.Assert(summary != nil)
// Print a summary.
var footer bytes.Buffer
// Print out the total number of steps performed (and their kinds), the duration, and any summary info.
if c := printChangeSummary(&footer, progress.Ops, false); c != 0 {
footer.WriteString(fmt.Sprintf("%vDeployment duration: %v%v\n",
colors.SpecUnimportant, time.Since(start), colors.Reset))
}
if progress.MaybeCorrupt {
footer.WriteString(fmt.Sprintf(
"%vA catastrophic error occurred; resources states may be unknown%v\n",
colors.SpecAttention, colors.Reset))
}
// Now save the updated snapshot to the specified output file, if any, or the standard location otherwise.
// Note that if a failure has occurred, the Apply routine above will have returned a safe checkpoint.
targ := result.Info.Target
saveEnv(targ, summary.Snap(), opts.Output, true /*overwrite*/)
fmt.Print(colors.Colorize(&footer))
return err
}
}
return nil
}
// deployProgress pretty-prints the plan application process as it goes.
type deployProgress struct {
Steps int
Ops map[deploy.StepOp]int
MaybeCorrupt bool
Opts deployOptions
}
func newProgress(opts deployOptions) *deployProgress {
return &deployProgress{
Steps: 0,
Ops: make(map[deploy.StepOp]int),
Opts: opts,
}
}
func (prog *deployProgress) Before(step deploy.Step) {
if shouldShow(step, prog.Opts) {
var b bytes.Buffer
printStep(&b, step, prog.Opts.Summary, false, "")
fmt.Print(colors.Colorize(&b))
}
}
func (prog *deployProgress) After(step deploy.Step, status resource.Status, err error) {
stepop := step.Op()
if err != nil {
// Issue a true, bonafide error.
cmdutil.Diag().Errorf(errors.ErrorPlanApplyFailed, err)
// Print the state of the resource; we don't issue the error, because the deploy above will do that.
var b bytes.Buffer
stepnum := prog.Steps + 1
b.WriteString(fmt.Sprintf("Step #%v failed [%v]: ", stepnum, stepop))
switch status {
case resource.StatusOK:
b.WriteString(colors.SpecNote)
b.WriteString("provider successfully recovered from this failure")
case resource.StatusUnknown:
b.WriteString(colors.SpecAttention)
b.WriteString("this failure was catastrophic and the provider cannot guarantee recovery")
prog.MaybeCorrupt = true
default:
contract.Failf("Unrecognized resource state: %v", status)
}
b.WriteString(colors.Reset)
b.WriteString("\n")
fmt.Print(colors.Colorize(&b))
} else {
// Increment the counters.
if step.Logical() {
prog.Steps++
prog.Ops[stepop]++
}
// Print out any output properties that got created as a result of this operation.
if shouldShow(step, prog.Opts) && !prog.Opts.Summary {
var b bytes.Buffer
printResourceOutputProperties(&b, step, "")
fmt.Print(colors.Colorize(&b))
}
}
}

17
pkg/engine/destroy.go Normal file
View file

@ -0,0 +1,17 @@
package engine
import "github.com/pulumi/pulumi-fabric/pkg/tokens"
func Destroy(envName string, pkgarg string, dryRun bool, debug bool, summary bool) error {
info, err := initEnvCmdName(tokens.QName(envName), pkgarg)
if err != nil {
return err
}
return deployLatest(info, deployOptions{
Debug: debug,
Destroy: true,
DryRun: dryRun,
Summary: summary,
})
}

238
pkg/engine/env.go Normal file
View file

@ -0,0 +1,238 @@
package engine
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
goerr "github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/resource/environment"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/pulumi/pulumi-fabric/pkg/util/mapper"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
)
func initEnvCmd(name string, pkgarg string) (*envCmdInfo, error) {
return initEnvCmdName(tokens.QName(name), pkgarg)
}
func initEnvCmdName(name tokens.QName, pkgarg string) (*envCmdInfo, error) {
// If the name is blank, use the default.
if name == "" {
name = getCurrentEnv()
}
if name == "" {
return nil, goerr.Errorf("missing environment name (and no default found)")
}
// Read in the deployment information, bailing if an IO error occurs.
target, snapshot, checkpoint := readEnv(name)
if checkpoint == nil {
return nil, goerr.Errorf("could not read environment information")
}
contract.Assert(target != nil)
contract.Assert(checkpoint != nil)
return &envCmdInfo{
Target: target,
Checkpoint: checkpoint,
Snapshot: snapshot,
PackageArg: pkgarg,
}, nil
}
type envCmdInfo struct {
Target *deploy.Target // the target environment.
Checkpoint *environment.Checkpoint // the full serialized checkpoint from which this came.
Snapshot *deploy.Snapshot // the environment's latest deployment snapshot
PackageArg string // an optional path to a package to pass to the compiler
}
// createEnv just creates a new empty environment without deploying anything into it.
func createEnv(name tokens.QName) {
env := &deploy.Target{Name: name}
if success := saveEnv(env, nil, "", false); success {
fmt.Printf("Environment '%v' initialized; see `lumi deploy` to deploy into it\n", name)
setCurrentEnv(name, false)
}
}
// newWorkspace creates a new workspace using the current working directory.
func newWorkspace() (workspace.W, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
ctx := core.NewContext(pwd, nil, &core.Options{})
return workspace.New(ctx)
}
// getCurrentEnv reads the current environment.
func getCurrentEnv() tokens.QName {
var name tokens.QName
w, err := newWorkspace()
if err == nil {
name = w.Settings().Env
}
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
}
return name
}
// setCurrentEnv changes the current environment to the given environment name, issuing an error if it doesn't exist.
func setCurrentEnv(name tokens.QName, verify bool) {
if verify {
if _, _, checkpoint := readEnv(name); checkpoint == nil {
return // no environment by this name exists, bail out.
}
}
// Switch the current workspace to that environment.
w, err := newWorkspace()
if err == nil {
w.Settings().Env = name
err = w.Save()
}
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
}
}
// removeTarget permanently deletes the environment's information from the local workstation.
func removeTarget(env *deploy.Target) {
deleteTarget(env)
msg := fmt.Sprintf("%sEnvironment '%s' has been removed!%s\n",
colors.SpecAttention, env.Name, colors.Reset)
fmt.Print(colors.ColorizeText(msg))
}
// backupTarget makes a backup of an existing file, in preparation for writing a new one. Instead of a copy, it
// simply renames the file, which is simpler, more efficient, etc.
func backupTarget(file string) {
contract.Require(file != "", "file")
err := os.Rename(file, file+".bak")
contract.IgnoreError(err) // ignore errors.
// IDEA: consider multiple backups (.bak.bak.bak...etc).
}
// deleteTarget removes an existing snapshot file, leaving behind a backup.
func deleteTarget(env *deploy.Target) {
contract.Require(env != nil, "env")
// Just make a backup of the file and don't write out anything new.
file := workspace.EnvPath(env.Name)
backupTarget(file)
}
// readEnv reads in an existing snapshot file, issuing an error and returning nil if something goes awry.
func readEnv(name tokens.QName) (*deploy.Target, *deploy.Snapshot, *environment.Checkpoint) {
contract.Require(name != "", "name")
file := workspace.EnvPath(name)
// Detect the encoding of the file so we can do our initial unmarshaling.
m, ext := encoding.Detect(file)
if m == nil {
cmdutil.Diag().Errorf(errors.ErrorIllegalMarkupExtension, ext)
return nil, nil, nil
}
// Now read the whole file into a byte blob.
b, err := ioutil.ReadFile(file)
if err != nil {
if os.IsNotExist(err) {
cmdutil.Diag().Errorf(errors.ErrorInvalidEnvName, name)
} else {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
}
return nil, nil, nil
}
// Unmarshal the contents into a checkpoint structure.
var checkpoint environment.Checkpoint
if err = m.Unmarshal(b, &checkpoint); err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, file, err)
return nil, nil, nil
}
// Next, use the mapping infrastructure to validate the contents.
// IDEA: we can eliminate this redundant unmarshaling once Go supports strict unmarshaling.
var obj map[string]interface{}
if err = m.Unmarshal(b, &obj); err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, file, err)
return nil, nil, nil
}
if obj["latest"] != nil {
if latest, islatest := obj["latest"].(map[string]interface{}); islatest {
delete(latest, "resources") // remove the resources, since they require custom marshaling.
}
}
md := mapper.New(nil)
var ignore environment.Checkpoint // just for errors.
if err = md.Decode(obj, &ignore); err != nil {
cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, file, err)
return nil, nil, nil
}
target, snapshot := environment.DeserializeCheckpoint(&checkpoint)
contract.Assert(target != nil)
return target, snapshot, &checkpoint
}
// saveEnv saves a new snapshot at the given location, backing up any existing ones.
func saveEnv(env *deploy.Target, snap *deploy.Snapshot, file string, existok bool) bool {
contract.Require(env != nil, "env")
if file == "" {
file = workspace.EnvPath(env.Name)
}
// Make a serializable LumiGL data structure and then use the encoder to encode it.
m, ext := encoding.Detect(file)
if m == nil {
cmdutil.Diag().Errorf(errors.ErrorIllegalMarkupExtension, ext)
return false
}
if filepath.Ext(file) == "" {
file = file + ext
}
dep := environment.SerializeCheckpoint(env, snap)
b, err := m.Marshal(dep)
if err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
return false
}
// If it's not ok for the file to already exist, ensure that it doesn't.
if !existok {
if _, staterr := os.Stat(file); staterr == nil {
cmdutil.Diag().Errorf(errors.ErrorIO, goerr.Errorf("file '%v' already exists", file))
return false
}
}
// Back up the existing file if it already exists.
backupTarget(file)
// Ensure the directory exists.
if err = os.MkdirAll(filepath.Dir(file), 0700); err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
return false
}
// And now write out the new snapshot file, overwriting that location.
if err = ioutil.WriteFile(file, b, 0600); err != nil {
cmdutil.Diag().Errorf(errors.ErrorIO, err)
return false
}
return true
}

12
pkg/engine/env_current.go Normal file
View file

@ -0,0 +1,12 @@
package engine
import (
"fmt"
)
func GetCurrentEnv() error {
if name := getCurrentEnv(); name != "" {
fmt.Println(name)
}
return nil
}

49
pkg/engine/env_info.go Normal file
View file

@ -0,0 +1,49 @@
package engine
import (
"fmt"
"github.com/pkg/errors"
)
func EnvInfo(showIDs bool, showURNs bool) error {
curr := getCurrentEnv()
if curr == "" {
return errors.New("no current environment; either `lumi env init` or `lumi env select` one")
}
fmt.Printf("Current environment is %v\n", curr)
fmt.Printf(" (use `lumi env select` to change environments; `lumi env ls` lists known ones)\n")
target, snapshot, checkpoint := readEnv(curr)
if checkpoint == nil {
return errors.Errorf("could not read environment information")
}
if checkpoint.Latest != nil {
fmt.Printf("Last deployment at %v\n", checkpoint.Latest.Time)
if checkpoint.Latest.Info != nil {
fmt.Printf("Additional deployment info: %v\n", checkpoint.Latest.Info)
}
}
if target.Config != nil && len(target.Config) > 0 {
fmt.Printf("%v configuration variables set (see `lumi config` for details)\n", len(target.Config))
}
if snapshot == nil || len(snapshot.Resources) == 0 {
fmt.Printf("No resources currently in this environment\n")
} else {
fmt.Printf("%v resources currently in this environment:\n", len(snapshot.Resources))
fmt.Printf("\n")
fmt.Printf("%-48s %s\n", "TYPE", "NAME")
for _, res := range snapshot.Resources {
fmt.Printf("%-48s %s\n", res.Type(), res.URN().Name())
// If the ID and/or URN is requested, show it on the following line. It would be nice to do this
// on a single line, but they can get quite lengthy and so this formatting makes more sense.
if showIDs {
fmt.Printf("\tID: %s\n", res.ID)
}
if showURNs {
fmt.Printf("\tURN: %s\n", res.URN())
}
}
}
return nil
}

8
pkg/engine/env_init.go Normal file
View file

@ -0,0 +1,8 @@
package engine
import "github.com/pulumi/pulumi-fabric/pkg/tokens"
func InitEnv(name string) error {
createEnv(tokens.QName(name))
return nil
}

63
pkg/engine/env_list.go Normal file
View file

@ -0,0 +1,63 @@
package engine
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
)
func ListEnvs() error {
// Read the environment directory.
path := workspace.EnvPath("")
files, err := ioutil.ReadDir(path)
if err != nil && !os.IsNotExist(err) {
return errors.Errorf("could not read environments: %v", err)
}
fmt.Printf("%-20s %-48s %-12s\n", "NAME", "LAST DEPLOYMENT", "RESOURCE COUNT")
curr := getCurrentEnv()
for _, file := range files {
// Ignore directories.
if file.IsDir() {
continue
}
// Skip files without valid extensions (e.g., *.bak files).
envfn := file.Name()
ext := filepath.Ext(envfn)
if _, has := encoding.Marshalers[ext]; !has {
continue
}
// Read in this environment's information.
name := tokens.QName(envfn[:len(envfn)-len(ext)])
target, snapshot, checkpoint := readEnv(name)
if checkpoint == nil {
continue // failure reading the environment information.
}
// Now print out the name, last deployment time (if any), and resources (if any).
lastDeploy := "n/a"
resourceCount := "n/a"
if checkpoint.Latest != nil {
lastDeploy = checkpoint.Latest.Time.String()
}
if snapshot != nil {
resourceCount = strconv.Itoa(len(snapshot.Resources))
}
display := target.Name
if display == curr {
display += "*" // fancify the current environment.
}
fmt.Printf("%-20s %-48s %-12s\n", display, lastDeploy, resourceCount)
}
return nil
}

25
pkg/engine/env_remove.go Normal file
View file

@ -0,0 +1,25 @@
package engine
import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
func RemoveEnv(envName string, force bool) error {
contract.Assert(envName != "")
info, err := initEnvCmd(envName, "")
if err != nil {
return err
}
// Don't remove environments that still have resources.
if !force && info.Snapshot != nil && len(info.Snapshot.Resources) > 0 {
return errors.Errorf(
"'%v' still has resources; removal rejected; pass --force to override", info.Target.Name)
}
removeTarget(info.Target)
return nil
}

8
pkg/engine/env_select.go Normal file
View file

@ -0,0 +1,8 @@
package engine
import "github.com/pulumi/pulumi-fabric/pkg/tokens"
func SelectEnv(envName string) error {
setCurrentEnv(tokens.QName(envName), true)
return nil
}

100
pkg/engine/pack.go Normal file
View file

@ -0,0 +1,100 @@
package engine
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/pack"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
)
// detectPackage returns a package given the path, or returns an error if one could not be located.
func detectPackage(path string) (*pack.Package, error) {
pkgpath, err := workspace.DetectPackage(path, cmdutil.Diag())
if err != nil {
return nil, errors.Errorf("could not locate a package to load: %v", err)
} else if pkgpath == "" {
return nil, errors.Errorf("no package found at: %v", err)
}
pkg, _ := readPackage(pkgpath)
contract.Assert(pkg != nil)
return pkg, nil
}
// readPackageFromArg reads a package from an argument value. It can be "-" to request reading from Stdin, and is
// interpreted as a path otherwise. If an error occurs, it is printed to Stderr, and the returned value will be nil.
// In addition to the package, a root directory is returned that the compiler should be formed over, if any.
func readPackageFromArg(arg string) (*pack.Package, string) {
// If the arg is simply "-", read from stdin.
if arg == "-" {
return readPackageFromStdin(), ""
}
// Read the package from a file.
return readPackage(arg)
}
// readPackageFromStdin attempts to read a package from Stdin; if an error occurs, it will be printed to Stderr, and
// the returned value will be nil.
func readPackageFromStdin() *pack.Package {
// If stdin, read the package from text, and then create a compiler using the working directory.
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not read from stdin\n")
fmt.Fprintf(os.Stderr, " %v\n", err)
return nil
}
return DecodePackage(encoding.Marshalers[".json"], b, "stdin")
}
// readPackage attempts to read a package from the given path; if an error occurs, it will be printed to Stderr, and
// the returned value will be nil. If the path is a directory, nil is returned.
func readPackage(path string) (*pack.Package, string) {
// If it's a directory, bail early.
info, err := os.Stat(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not read path '%v': %v\n", path, err)
return nil, ""
}
if info.IsDir() {
return nil, path
}
// Lookup the marshaler for this format.
ext := filepath.Ext(path)
m, has := encoding.Marshalers[ext]
if !has {
fmt.Fprintf(os.Stderr, "error: no marshaler found for file format '%v'\n", ext)
return nil, ""
}
// Read the contents.
b, err := ioutil.ReadFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: a problem occurred when reading file '%v'\n", path)
fmt.Fprintf(os.Stderr, " %v\n", err)
return nil, ""
}
return DecodePackage(m, b, path), filepath.Dir(path)
}
// DecodePackage turns a byte array into a package using the given marshaler. If an error occurs, it is printed to
// Stderr, and the returned package value will be nil.
func DecodePackage(m encoding.Marshaler, b []byte, path string) *pack.Package {
// Unmarshal the contents into a fresh package.
pkg, err := encoding.Decode(m, b)
if err != nil {
fmt.Fprintf(os.Stderr, "error: a problem occurred when unmarshaling file '%v'\n", path)
fmt.Fprintf(os.Stderr, " %v\n", err)
return nil
}
return pkg
}

87
pkg/engine/pack_eval.go Normal file
View file

@ -0,0 +1,87 @@
package engine
import (
"fmt"
"strings"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core"
"github.com/pulumi/pulumi-fabric/pkg/eval"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
)
func PackEval(configEnv string, args []string) error {
// First, load and compile the package.
result := compile(pkgargFromArgs(args))
if result == nil {
return nil
}
// Now fire up an interpreter so we can run the program.
e := eval.New(result.B.Ctx(), nil)
// If configuration was requested, load it up and populate the object state.
if configEnv != "" {
envInfo, err := initEnvCmdName(tokens.QName(configEnv), pkgargFromArgs(args))
if err != nil {
return err
}
if err := deploy.InitEvalConfig(result.B.Ctx(), e, envInfo.Target.Config); err != nil {
return err
}
}
// Finally, execute the entire program, and serialize the return value (if any).
packArgs := dashdashArgsToMap(args)
if obj, _ := e.EvaluatePackage(result.Pkg, packArgs); obj != nil {
fmt.Print(obj)
}
return nil
}
func pkgargFromArgs(args []string) string {
if len(args) == 0 {
return ""
}
return args[0]
}
// dashdashArgsToMap is a simple args parser that places incoming key/value pairs into a map. These are then used
// during package compilation as inputs to the main entrypoint function.
// IDEA: this is fairly rudimentary; we eventually want to support arrays, maps, and complex types.
func dashdashArgsToMap(args []string) core.Args {
mapped := make(core.Args)
for i := 0; i < len(args); i++ {
arg := args[i]
// Eat - or -- at the start.
if arg[0] == '-' {
arg = arg[1:]
if arg[0] == '-' {
arg = arg[1:]
}
}
// Now find a k=v, and split the k/v part.
if eq := strings.IndexByte(arg, '='); eq != -1 {
// For --k=v, simply store v underneath k's entry.
mapped[tokens.Name(arg[:eq])] = arg[eq+1:]
} else {
if i+1 < len(args) && args[i+1][0] != '-' {
// If the next arg doesn't start with '-' (i.e., another flag) use its value.
mapped[tokens.Name(arg)] = args[i+1]
i++
} else if arg[0:3] == "no-" {
// For --no-k style args, strip off the no- prefix and store false underneath k.
mapped[tokens.Name(arg[3:])] = false
} else {
// For all other --k args, assume this is a boolean flag, and set the value of k to true.
mapped[tokens.Name(arg)] = true
}
}
}
return mapped
}

399
pkg/engine/pack_info.go Normal file
View file

@ -0,0 +1,399 @@
package engine
import (
"fmt"
"os"
"strings"
"unicode"
"github.com/pulumi/pulumi-fabric/pkg/compiler/ast"
"github.com/pulumi/pulumi-fabric/pkg/pack"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
func PackInfo(printExportedSymbols bool, printIL bool, printSymbols bool, args []string) error {
var pkg *pack.Package
var err error
if len(args) == 0 {
// No package specified, just load from the current directory.
pwd, locerr := os.Getwd()
if locerr != nil {
return locerr
}
if pkg, err = detectPackage(pwd); err != nil {
return err
}
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
} else {
// Enumerate the list of packages, deserialize them, and print information.
var path string
for _, arg := range args {
pkg, path = readPackageFromArg(arg)
if pkg == nil {
if pkg, err = detectPackage(path); err != nil {
return err
}
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
}
}
}
return nil
}
func printComment(pc *string, indent string) {
// Prints a comment header using the given indentation, wrapping at 100 lines.
if pc != nil {
prefix := "// "
maxlen := 100 - len(indent)
// For every tab, chew up 3 more chars (so each one is 4 chars wide).
for _, i := range indent {
if i == '\t' {
maxlen -= 3
}
}
maxlen -= len(prefix)
if maxlen < 40 {
maxlen = 40
}
c := make([]rune, 0)
for _, r := range *pc {
c = append(c, r)
}
for len(c) > 0 {
fmt.Print(indent + prefix)
// Now, try to split the comment as close to maxlen-3 chars as possible (taking into account indent+"// "),
// but don't split words -- only split at whitespace characters if we can help it.
six := maxlen
for {
if len(c) <= six {
six = len(c)
break
} else if unicode.IsSpace(c[six]) {
// It's a space, set six to the first non-space character beforehand, and eix to the first
// non-space character afterwards.
for six > 0 && unicode.IsSpace(c[six-1]) {
six--
}
break
} else if six == 0 {
// We hit the start of the string and didn't find any spaces. Start over and try to find the
// first space *beyond* the start point (instead of *before*) and use that.
six = maxlen + 1
for six < len(c) && !unicode.IsSpace(c[six]) {
six++
}
break
}
// We need to keep searching, back up one and try again.
six--
}
// Print what we've got thus far, plus a newline.
fmt.Printf("%v\n", string(c[:six]))
// Now find the first non-space character beyond the split point and use that for the remainder.
eix := six
for eix < len(c) && unicode.IsSpace(c[eix]) {
eix++
}
c = c[eix:]
}
}
}
// printPackage pretty-prints the package metadata.
func printPackage(pkg *pack.Package, printSymbols bool, printExports bool, printIL bool) {
printComment(pkg.Description, "")
fmt.Printf("package \"%v\" {\n", pkg.Name)
if pkg.Author != nil {
fmt.Printf("%vauthor \"%v\"\n", tab, *pkg.Author)
}
if pkg.Website != nil {
fmt.Printf("%vwebsite \"%v\"\n", tab, *pkg.Website)
}
if pkg.License != nil {
fmt.Printf("%vlicense \"%v\"\n", tab, *pkg.License)
}
// Print the dependencies:
fmt.Printf("%vdependencies [", tab)
if pkg.Dependencies != nil && len(*pkg.Dependencies) > 0 {
fmt.Printf("\n")
for _, dep := range pack.StableDependencies(*pkg.Dependencies) {
fmt.Printf("%v%v: \"%v\"\n", tab+tab, dep, (*pkg.Dependencies)[dep])
}
fmt.Printf("%v", tab)
}
fmt.Printf("]\n")
// Print the modules (just names by default, or full symbols and/or IL if requested).
printModules(pkg, printSymbols, printExports, printIL, tab)
fmt.Printf("}\n")
}
func printModules(pkg *pack.Package, printSymbols bool, printExports bool, printIL bool, indent string) {
if pkg.Modules != nil {
pkgtok := tokens.NewPackageToken(pkg.Name)
for _, name := range ast.StableModules(*pkg.Modules) {
mod := (*pkg.Modules)[name]
modtok := tokens.NewModuleToken(pkgtok, name)
// Print the name.
fmt.Printf("%vmodule \"%v\" {", indent, name)
// Now, if requested, print the tokens.
if printSymbols || printExports {
if mod.Exports != nil || mod.Members != nil {
fmt.Printf("\n")
exports := make(map[tokens.Token]bool)
if mod.Exports != nil {
// Print the exports.
fmt.Printf("%vexports [", indent+tab)
if mod.Exports != nil && len(*mod.Exports) > 0 {
fmt.Printf("\n")
for _, exp := range ast.StableModuleExports(*mod.Exports) {
ref := (*mod.Exports)[exp].Referent.Tok
fmt.Printf("%v\"%v\" -> \"%v\"\n", indent+tab+tab, exp, ref)
exports[ref] = true
}
fmt.Printf("%v", indent+tab)
}
fmt.Printf("]\n")
}
if mod.Members != nil {
// Print the members.
for _, member := range ast.StableModuleMembers(*mod.Members) {
memtok := tokens.NewModuleMemberToken(modtok, member)
printModuleMember(memtok, (*mod.Members)[member], printExports, exports, indent+tab)
}
fmt.Printf("%v", indent)
}
}
} else {
// Print a "..." so that it's clear we're omitting information, versus the module being empty.
fmt.Printf("...")
}
fmt.Printf("}\n")
}
}
}
func printModuleMember(tok tokens.ModuleMember, member ast.ModuleMember,
exportOnly bool, exports map[tokens.Token]bool, indent string) {
printComment(member.GetDescription(), indent)
if !exportOnly || exports[tokens.Token(tok)] {
switch member.GetKind() {
case ast.ClassKind:
printClass(tokens.Type(tok), member.(*ast.Class), exportOnly, indent)
case ast.ModulePropertyKind:
printModuleProperty(tok, member.(*ast.ModuleProperty), indent)
case ast.ModuleMethodKind:
printModuleMethod(tok, member.(*ast.ModuleMethod), indent)
default:
contract.Failf("Unexpected ModuleMember kind: %v (tok %v)\n", member.GetKind(), tok)
}
}
}
func printClass(tok tokens.Type, class *ast.Class, exportOnly bool, indent string) {
fmt.Printf("%vclass \"%v\"", indent, tok.Name())
var mods []string
if class.Sealed != nil && *class.Sealed {
mods = append(mods, "sealed")
}
if class.Abstract != nil && *class.Abstract {
mods = append(mods, "abstract")
}
if class.Record != nil && *class.Record {
mods = append(mods, "record")
}
if class.Interface != nil && *class.Interface {
mods = append(mods, "interface")
}
if class.Attributes != nil {
for _, att := range *class.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
fmt.Print(modString(mods))
if class.Extends != nil {
fmt.Printf("\n%vextends %v", indent+tab+tab, string(class.Extends.Tok))
}
if class.Implements != nil {
for _, impl := range *class.Implements {
fmt.Printf("\n%vimplements %v", indent+tab+tab, string(impl.Tok))
}
}
fmt.Printf(" {")
if class.Members != nil {
fmt.Printf("\n")
for _, member := range ast.StableClassMembers(*class.Members) {
memtok := tokens.NewClassMemberToken(tok, member)
printClassMember(memtok, (*class.Members)[member], exportOnly, indent+tab)
}
fmt.Print(indent)
}
fmt.Printf("}\n")
}
func printClassMember(tok tokens.ClassMember, member ast.ClassMember, exportOnly bool, indent string) {
printComment(member.GetDescription(), indent)
acc := member.GetAccess()
if !exportOnly || (acc != nil && *acc == tokens.PublicAccessibility) {
switch member.GetKind() {
case ast.ClassPropertyKind:
printClassProperty(tok.Name(), member.(*ast.ClassProperty), indent)
case ast.ClassMethodKind:
printClassMethod(tok.Name(), member.(*ast.ClassMethod), indent)
default:
contract.Failf("Unexpected ClassMember kind: %v\n", member.GetKind())
}
}
}
func printClassProperty(name tokens.ClassMemberName, prop *ast.ClassProperty, indent string) {
var mods []string
if prop.Access != nil {
mods = append(mods, string(*prop.Access))
}
if prop.Static != nil && *prop.Static {
mods = append(mods, "static")
}
if prop.Readonly != nil && *prop.Readonly {
mods = append(mods, "readonly")
}
if prop.Attributes != nil {
for _, att := range *prop.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
fmt.Printf("%vproperty \"%v\"%v", indent, name, modString(mods))
if prop.Type != nil {
fmt.Printf(": %v", prop.Type.Tok)
}
if prop.Getter != nil || prop.Setter != nil {
fmt.Printf(" {\n")
if prop.Getter != nil {
printClassMethod(tokens.ClassMemberName("get"), prop.Getter, indent+" ")
}
if prop.Setter != nil {
printClassMethod(tokens.ClassMemberName("set"), prop.Setter, indent+" ")
}
fmt.Printf("%v}\n", indent)
} else {
fmt.Printf("\n")
}
}
func printClassMethod(name tokens.ClassMemberName, meth *ast.ClassMethod, indent string) {
var mods []string
if meth.Access != nil {
mods = append(mods, string(*meth.Access))
}
if meth.Static != nil && *meth.Static {
mods = append(mods, "static")
}
if meth.Sealed != nil && *meth.Sealed {
mods = append(mods, "sealed")
}
if meth.Abstract != nil && *meth.Abstract {
mods = append(mods, "abstract")
}
if meth.Attributes != nil {
for _, att := range *meth.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
fmt.Printf("%vmethod \"%v\"%v: %v\n", indent, name, modString(mods), funcSig(meth))
}
func printModuleMethod(tok tokens.ModuleMember, meth *ast.ModuleMethod, indent string) {
fmt.Printf("%vmethod \"%v\": %v\n", indent, tok.Name(), funcSig(meth))
}
func printModuleProperty(tok tokens.ModuleMember, prop *ast.ModuleProperty, indent string) {
var mods []string
if prop.Readonly != nil && *prop.Readonly {
mods = append(mods, "readonly")
}
fmt.Printf("%vproperty \"%v\"%v", indent, tok.Name(), modString(mods))
if prop.Type != nil {
fmt.Printf(": %v", prop.Type.Tok)
}
fmt.Printf("\n")
}
func modString(mods []string) string {
if len(mods) == 0 {
return ""
}
s := " ["
for i, mod := range mods {
if i > 0 {
s += ", "
}
s += mod
}
s += "]"
return s
}
// spaces returns a string with the given number of spaces.
func spaces(num int) string {
return strings.Repeat(" ", num)
}
// tab is a tab represented as spaces, since some consoles have ridiculously wide true tabs.
var tab = spaces(4)
func funcSig(fun ast.Function) string {
sig := "("
// To create a signature, first concatenate the parameters.
params := fun.GetParameters()
if params != nil {
for i, param := range *params {
if i > 0 {
sig += ", "
}
sig += string(param.Name.Ident)
var mods []string
if param.Attributes != nil {
for _, att := range *param.Attributes {
mods = append(mods, "@"+att.Decorator.Tok.String())
}
}
sig += modString(mods)
if param.Type != nil {
sig += ": " + string(param.Type.Tok)
}
}
}
sig += ")"
// And then the return type, if present.
ret := fun.GetReturnType()
if ret != nil {
sig += ": " + string(ret.Tok)
}
return sig
}

19
pkg/engine/pack_verify.go Normal file
View file

@ -0,0 +1,19 @@
package engine
import "github.com/pkg/errors"
func PackVerify(pkgarg string) error {
// Prepare the compiler info and, provided it succeeds, perform the verification.
if comp, pkg := prepareCompiler(pkgarg); comp != nil {
// Now perform the compilation and extract the heap snapshot.
if pkg == nil && !comp.Verify() {
return errors.New("verification failed")
} else if pkg != nil && !comp.VerifyPackage(pkg) {
return errors.New("verification failed")
}
return nil
}
return errors.New("could not create prepare compiler")
}

642
pkg/engine/plan.go Normal file
View file

@ -0,0 +1,642 @@
package engine
import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/diag"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/resource/plugin"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
type PlanOptions struct {
Package string // the package to compute the plan for
Debug bool // true to enable resource debugging output.
Environment string // the environment to use when planning
Analyzers []string // an optional set of analyzers to run as part of this deployment.
ShowConfig bool // true to show the configuration variables being used.
ShowReads bool // true to show the read-only steps in the plan.
ShowReplacementSteps bool // true to show the replacement steps in the plan.
ShowSames bool // true to show the resources that aren't updated, in addition to those that are.
Summary bool // true if we should only summarize resources and operations.
}
func Plan(opts PlanOptions) error {
info, err := initEnvCmdName(tokens.QName(opts.Environment), opts.Package)
if err != nil {
return err
}
deployOpts := deployOptions{
Debug: opts.Debug,
Destroy: false,
DryRun: true,
Analyzers: opts.Analyzers,
ShowConfig: opts.ShowConfig,
ShowReads: opts.ShowReads,
ShowReplacementSteps: opts.ShowReplacementSteps,
ShowSames: opts.ShowSames,
Summary: opts.Summary,
}
result, err := plan(info, deployOpts)
if err != nil {
return err
}
if result != nil {
defer contract.IgnoreClose(result)
if err := printPlan(result, deployOpts); err != nil {
return err
}
}
return nil
}
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
func plan(info *envCmdInfo, opts deployOptions) (*planResult, error) {
contract.Assert(info != nil)
contract.Assert(info.Target != nil)
// Initialize the diagnostics logger with the right stuff.
cmdutil.InitDiag(diag.FormatOptions{
Colors: true,
Debug: opts.Debug,
})
// Create a context for plugins.
ctx, err := plugin.NewContext(cmdutil.Diag(), nil)
if err != nil {
return nil, err
}
// First, compile the package, in preparatin for interpreting it and creating resources.
result := compile(info.PackageArg)
if result == nil || !result.B.Ctx().Diag.Success() {
return nil, fmt.Errorf("Errors during compilation: %v", result.B.Ctx().Diag.Errors())
}
// If that succeeded, create a new source that will perform interpretation of the compiled program.
// TODO[pulumi/pulumi-fabric#88]: we are passing `nil` as the arguments map; we need to allow a way to pass these.
source := deploy.NewEvalSource(ctx, result.B.Ctx(), result.Pkg, nil, info.Target.Config, opts.Destroy)
// If there are any analyzers in the project file, add them.
var analyzers []tokens.QName
if as := result.Pkg.Node.Analyzers; as != nil {
for _, a := range *as {
analyzers = append(analyzers, a)
}
}
// Append any analyzers from the command line.
for _, a := range opts.Analyzers {
analyzers = append(analyzers, tokens.QName(a))
}
// Generate a plan; this API handles all interesting cases (create, update, delete).
plan := deploy.NewPlan(ctx, info.Target, info.Snapshot, source, analyzers)
return &planResult{
Ctx: ctx,
Info: info,
Plan: plan,
}, nil
}
type planResult struct {
Ctx *plugin.Context // the context containing plugins and their state.
Info *envCmdInfo // plan command information.
Plan *deploy.Plan // the plan created by this command.
}
func (res *planResult) Close() error {
return res.Ctx.Close()
}
func printPlan(result *planResult, opts deployOptions) error {
// First print config/unchanged/etc. if necessary.
var prelude bytes.Buffer
printPrelude(&prelude, result, opts, true)
// Now walk the plan's steps and and pretty-print them out.
prelude.WriteString(fmt.Sprintf("%vPlanning changes:%v\n", colors.SpecUnimportant, colors.Reset))
fmt.Print(colors.Colorize(&prelude))
iter, err := result.Plan.Iterate()
if err != nil {
return errors.Errorf("An error occurred while preparing the plan: %v", err)
}
defer contract.IgnoreClose(iter)
step, err := iter.Next()
if err != nil {
return errors.Errorf("An error occurred while enumerating the plan: %v", err)
}
var summary bytes.Buffer
counts := make(map[deploy.StepOp]int)
for step != nil {
var err error
// Perform the pre-step.
if err = step.Pre(); err != nil {
return errors.Errorf("An error occurred preparing the plan: %v", err)
}
// Print this step information (resource and all its properties).
// IDEA: it would be nice if, in the output, we showed the dependencies a la `git log --graph`.
if shouldShow(step, opts) {
printStep(&summary, step, opts.Summary, true, "")
}
// Be sure to skip the step so that in-memory state updates are performed.
if err = step.Skip(); err != nil {
return errors.Errorf("An error occurred while advancing the plan: %v", err)
}
// Track the operation if shown and/or if it is a logically meaningful operation.
if step.Logical() {
counts[step.Op()]++
}
if step, err = iter.Next(); err != nil {
return errors.Errorf("An error occurred while viewing the plan: %v", err)
}
}
// Print a summary of operation counts.
printChangeSummary(&summary, counts, true)
fmt.Print(colors.Colorize(&summary))
return nil
}
// shouldShow returns true if a step should show in the output.
func shouldShow(step deploy.Step, opts deployOptions) bool {
// For certain operations, whether they are tracked is controlled by flags (to cut down on superfluous output).
if _, isrd := step.(deploy.ReadStep); isrd {
return opts.ShowReads
} else if step.Op() == deploy.OpSame {
return opts.ShowSames
} else if step.Op() == deploy.OpCreateReplacement || step.Op() == deploy.OpDeleteReplaced {
return opts.ShowReplacementSteps
} else if step.Op() == deploy.OpReplace {
return !opts.ShowReplacementSteps
}
return true
}
func printPrelude(b *bytes.Buffer, result *planResult, opts deployOptions, planning bool) {
// If there are configuration variables, show them.
if opts.ShowConfig {
printConfig(b, result.Info.Target.Config)
}
}
func printConfig(b *bytes.Buffer, config resource.ConfigMap) {
b.WriteString(fmt.Sprintf("%vConfiguration:%v\n", colors.SpecUnimportant, colors.Reset))
if config != nil {
var toks []string
for tok := range config {
toks = append(toks, string(tok))
}
sort.Strings(toks)
for _, tok := range toks {
b.WriteString(fmt.Sprintf("%v%v: %v\n", detailsIndent, tok, config[tokens.Token(tok)]))
}
}
}
func printChangeSummary(b *bytes.Buffer, counts map[deploy.StepOp]int, plan bool) int {
changes := 0
for op, c := range counts {
if op != deploy.OpSame {
changes += c
}
}
var kind string
if plan {
kind = "planned"
} else {
kind = "deployed"
}
var changesLabel string
if changes == 0 {
kind = "required"
changesLabel = "no"
} else {
changesLabel = strconv.Itoa(changes)
}
b.WriteString(fmt.Sprintf("%vinfo%v: %v %v %v:\n",
colors.SpecInfo, colors.Reset, changesLabel, plural("change", changes), kind))
var planTo string
var pastTense string
if plan {
planTo = "to "
} else {
pastTense = "d"
}
// Now summarize all of the changes; we print sames a little differently.
for _, op := range deploy.StepOps {
if op != deploy.OpSame {
if c := counts[op]; c > 0 {
b.WriteString(fmt.Sprintf(" %v%v %v %v%v%v%v\n",
op.Prefix(), c, plural("resource", c), planTo, op, pastTense, colors.Reset))
}
}
}
if c := counts[deploy.OpSame]; c > 0 {
b.WriteString(fmt.Sprintf(" %v %v unchanged\n", c, plural("resource", c)))
}
return changes
}
func plural(s string, c int) string {
if c != 1 {
s += "s"
}
return s
}
const detailsIndent = " " // 4 spaces, plus 2 for "+ ", "- ", and " " leaders
func printStep(b *bytes.Buffer, step deploy.Step, summary bool, planning bool, indent string) {
// First print out the operation's prefix.
b.WriteString(step.Op().Prefix())
// Next, print the resource type (since it is easy on the eyes and can be quickly identified).
printStepHeader(b, step)
b.WriteString(step.Op().Suffix())
// Next print the resource URN, properties, etc.
if mut, ismut := step.(deploy.MutatingStep); ismut {
var replaces []resource.PropertyKey
if step.Op() == deploy.OpCreateReplacement {
replaces = step.(*deploy.CreateStep).Keys()
} else if step.Op() == deploy.OpReplace {
replaces = step.(*deploy.ReplaceStep).Keys()
}
printResourceProperties(b, mut.URN(), mut.Old(), mut.New(), replaces, summary, planning, indent)
} else if rd, isrd := step.(deploy.ReadStep); isrd {
for _, res := range rd.Resources() {
printResourceProperties(b, "", nil, res.State(), nil, summary, planning, indent)
}
} else {
contract.Failf("Expected each step to either be mutating or read-only")
}
// Finally make sure to reset the color.
b.WriteString(colors.Reset)
}
func printStepHeader(b *bytes.Buffer, step deploy.Step) {
b.WriteString(fmt.Sprintf("%s: (%s)\n", string(step.Type()), step.Op()))
}
func printResourceProperties(b *bytes.Buffer, urn resource.URN, old *resource.State, new *resource.State,
replaces []resource.PropertyKey, summary bool, planning bool, indent string) {
indent += detailsIndent
// Print out the URN and, if present, the ID, as "pseudo-properties".
var id resource.ID
if old != nil {
id = old.ID
}
if id != "" {
b.WriteString(fmt.Sprintf("%s[id=%s]\n", indent, string(id)))
}
if urn != "" {
b.WriteString(fmt.Sprintf("%s[urn=%s]\n", indent, urn))
}
if !summary {
// Print all of the properties associated with this resource.
if old == nil && new != nil {
printObject(b, new.AllInputs(), planning, indent)
} else if new == nil && old != nil {
printObject(b, old.AllInputs(), planning, indent)
} else {
printOldNewDiffs(b, old.AllInputs(), new.AllInputs(), replaces, planning, indent)
}
}
}
func maxKey(keys []resource.PropertyKey) int {
maxkey := 0
for _, k := range keys {
if len(k) > maxkey {
maxkey = len(k)
}
}
return maxkey
}
func printObject(b *bytes.Buffer, props resource.PropertyMap, planning bool, indent string) {
// Compute the maximum with of property keys so we can justify everything.
keys := props.StableKeys()
maxkey := maxKey(keys)
// Now print out the values intelligently based on the type.
for _, k := range keys {
if v := props[k]; shouldPrintPropertyValue(v, planning) {
printPropertyTitle(b, k, maxkey, indent)
printPropertyValue(b, v, planning, indent)
}
}
}
// printResourceOutputProperties prints only those properties that either differ from the input properties or, if
// there is an old snapshot of the resource, differ from the prior old snapshot's output properties.
func printResourceOutputProperties(b *bytes.Buffer, step deploy.Step, indent string) {
// Only certain kinds of steps have output properties associated with them.
mut := step.(deploy.MutatingStep)
if mut == nil ||
(step.Op() != deploy.OpCreate &&
step.Op() != deploy.OpCreateReplacement &&
step.Op() != deploy.OpUpdate) {
return
}
indent += detailsIndent
b.WriteString(step.Op().Color())
b.WriteString(step.Op().Suffix())
// First fetch all the relevant property maps that we may consult.
newins := mut.New().Inputs
newouts := mut.New().Outputs
var oldouts resource.PropertyMap
if old := mut.Old(); old != nil {
oldouts = old.Outputs
}
// Now sort the keys and enumerate each output property in a deterministic order.
firstout := true
keys := newouts.StableKeys()
maxkey := maxKey(keys)
for _, k := range keys {
newout := newouts[k]
// Print this property if it is printable, and one of these cases
// 1) new ins has it and it's different;
// 2) new ins doesn't have it, but old outs does, and it's different;
// 3) neither old outs nor new ins contain it;
if shouldPrintPropertyValue(newout, true) {
var print bool
if newin, has := newins[k]; has {
print = (newout.Diff(newin) != nil) // case 1
} else if oldouts != nil {
if oldout, has := oldouts[k]; has {
print = (newout.Diff(oldout) != nil) // case 2
} else {
print = true // case 3
}
} else {
print = true // also case 3
}
if print {
if firstout {
b.WriteString(fmt.Sprintf("%v---outputs:---\n", indent))
firstout = false
}
printPropertyTitle(b, k, maxkey, indent)
printPropertyValue(b, newout, false, indent)
}
}
}
b.WriteString(colors.Reset)
}
func shouldPrintPropertyValue(v resource.PropertyValue, outs bool) bool {
if v.IsNull() {
// by default, don't print nulls (they just clutter up the output)
return false
}
if v.IsOutput() && !outs {
// also don't show output properties until the outs parameter tells us to.
return false
}
return true
}
func printPropertyTitle(b *bytes.Buffer, k resource.PropertyKey, align int, indent string) {
b.WriteString(fmt.Sprintf("%s%-"+strconv.Itoa(align)+"s: ", indent, k))
}
func printPropertyValue(b *bytes.Buffer, v resource.PropertyValue, planning bool, indent string) {
if v.IsNull() {
b.WriteString("<null>")
} else if v.IsBool() {
b.WriteString(fmt.Sprintf("%t", v.BoolValue()))
} else if v.IsNumber() {
b.WriteString(fmt.Sprintf("%v", v.NumberValue()))
} else if v.IsString() {
b.WriteString(fmt.Sprintf("%q", v.StringValue()))
} else if v.IsArray() {
arr := v.ArrayValue()
if len(arr) == 0 {
b.WriteString("[]")
} else {
b.WriteString(fmt.Sprintf("[\n"))
for i, elem := range arr {
newIndent := printArrayElemHeader(b, i, indent)
printPropertyValue(b, elem, planning, newIndent)
}
b.WriteString(fmt.Sprintf("%s]", indent))
}
} else if v.IsAsset() {
a := v.AssetValue()
if text, has := a.GetText(); has {
b.WriteString("asset {\n")
// pretty print the text, line by line, with proper breaks.
lines := strings.Split(text, "\n")
for _, line := range lines {
b.WriteString(fmt.Sprintf("%v \"%v\"\n", indent, line))
}
b.WriteString(fmt.Sprintf("%v}", indent))
} else if path, has := a.GetPath(); has {
b.WriteString(fmt.Sprintf("asset { file://%v }", path))
} else {
contract.Assert(a.IsURI())
b.WriteString(fmt.Sprintf("asset { %v }", a.URI))
}
} else if v.IsArchive() {
a := v.ArchiveValue()
if assets, has := a.GetAssets(); has {
b.WriteString("archive {\n")
var names []string
for name := range assets {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
b.WriteString(fmt.Sprintf("%v \"%v\": ", indent, name))
printPropertyValue(b, resource.NewAssetProperty(assets[name]), planning, indent+" ")
}
b.WriteString(fmt.Sprintf("%v}", indent))
} else if path, has := a.GetPath(); has {
b.WriteString(fmt.Sprintf("archive { file://%v }", path))
} else {
contract.Assert(a.IsURI())
b.WriteString(fmt.Sprintf("archive { %v }", a.URI))
}
} else if v.IsComputed() || v.IsOutput() {
b.WriteString(v.TypeString())
} else {
contract.Assert(v.IsObject())
obj := v.ObjectValue()
if len(obj) == 0 {
b.WriteString("{}")
} else {
b.WriteString("{\n")
printObject(b, obj, planning, indent+" ")
b.WriteString(fmt.Sprintf("%s}", indent))
}
}
b.WriteString("\n")
}
func getArrayElemHeader(b *bytes.Buffer, i int, indent string) (string, string) {
prefix := fmt.Sprintf(" %s[%d]: ", indent, i)
return prefix, fmt.Sprintf("%-"+strconv.Itoa(len(prefix))+"s", "")
}
func printArrayElemHeader(b *bytes.Buffer, i int, indent string) string {
prefix, newIndent := getArrayElemHeader(b, i, indent)
b.WriteString(prefix)
return newIndent
}
func printOldNewDiffs(b *bytes.Buffer, olds resource.PropertyMap, news resource.PropertyMap,
replaces []resource.PropertyKey, planning bool, indent string) {
// Get the full diff structure between the two, and print it (recursively).
if diff := olds.Diff(news); diff != nil {
printObjectDiff(b, *diff, replaces, false, planning, indent)
} else {
printObject(b, news, planning, indent)
}
}
func printObjectDiff(b *bytes.Buffer, diff resource.ObjectDiff,
replaces []resource.PropertyKey, causedReplace bool, planning bool, indent string) {
contract.Assert(len(indent) > 2)
// Compute the maximum with of property keys so we can justify everything.
keys := diff.Keys()
maxkey := maxKey(keys)
// If a list of what causes a resource to get replaced exist, create a handy map.
var replaceMap map[resource.PropertyKey]bool
if len(replaces) > 0 {
replaceMap = make(map[resource.PropertyKey]bool)
for _, k := range replaces {
replaceMap[k] = true
}
}
// To print an object diff, enumerate the keys in stable order, and print each property independently.
for _, k := range keys {
title := func(id string) { printPropertyTitle(b, k, maxkey, id) }
if add, isadd := diff.Adds[k]; isadd {
if shouldPrintPropertyValue(add, planning) {
b.WriteString(colors.SpecCreate)
title(addIndent(indent))
printPropertyValue(b, add, planning, addIndent(indent))
b.WriteString(colors.Reset)
}
} else if delete, isdelete := diff.Deletes[k]; isdelete {
if shouldPrintPropertyValue(delete, planning) {
b.WriteString(colors.SpecDelete)
title(deleteIndent(indent))
printPropertyValue(b, delete, planning, deleteIndent(indent))
b.WriteString(colors.Reset)
}
} else if update, isupdate := diff.Updates[k]; isupdate {
if !causedReplace && replaceMap != nil {
causedReplace = replaceMap[k]
}
printPropertyValueDiff(b, title, update, causedReplace, planning, indent)
} else if same := diff.Sames[k]; shouldPrintPropertyValue(same, planning) {
title(indent)
printPropertyValue(b, diff.Sames[k], planning, indent)
}
}
}
func printPropertyValueDiff(b *bytes.Buffer, title func(string), diff resource.ValueDiff,
causedReplace bool, planning bool, indent string) {
contract.Assert(len(indent) > 2)
if diff.Array != nil {
title(indent)
b.WriteString("[\n")
a := diff.Array
for i := 0; i < a.Len(); i++ {
_, newIndent := getArrayElemHeader(b, i, indent)
titleFunc := func(id string) { printArrayElemHeader(b, i, id) }
if add, isadd := a.Adds[i]; isadd {
b.WriteString(deploy.OpCreate.Color())
titleFunc(addIndent(indent))
printPropertyValue(b, add, planning, addIndent(newIndent))
b.WriteString(colors.Reset)
} else if delete, isdelete := a.Deletes[i]; isdelete {
b.WriteString(deploy.OpDelete.Color())
titleFunc(deleteIndent(indent))
printPropertyValue(b, delete, planning, deleteIndent(newIndent))
b.WriteString(colors.Reset)
} else if update, isupdate := a.Updates[i]; isupdate {
printPropertyValueDiff(b, title, update, causedReplace, planning, indent)
} else {
titleFunc(indent)
printPropertyValue(b, a.Sames[i], planning, newIndent)
}
}
b.WriteString(fmt.Sprintf("%s]\n", indent))
} else if diff.Object != nil {
title(indent)
b.WriteString("{\n")
printObjectDiff(b, *diff.Object, nil, causedReplace, planning, indent+" ")
b.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
// If we ended up here, the two values either differ by type, or they have different primitive values. We will
// simply emit a deletion line followed by an addition line.
if shouldPrintPropertyValue(diff.Old, false) {
var color string
if causedReplace {
color = deploy.OpDelete.Color() // this property triggered replacement; color as a delete
} else {
color = deploy.OpUpdate.Color()
}
b.WriteString(color)
title(deleteIndent(indent))
printPropertyValue(b, diff.Old, planning, deleteIndent(indent))
b.WriteString(colors.Reset)
}
if shouldPrintPropertyValue(diff.New, false) {
var color string
if causedReplace {
color = deploy.OpCreate.Color() // this property triggered replacement; color as a create
} else {
color = deploy.OpUpdate.Color()
}
b.WriteString(color)
title(addIndent(indent))
printPropertyValue(b, diff.New, planning, addIndent(indent))
b.WriteString(colors.Reset)
}
}
}
func addIndent(indent string) string { return indent[:len(indent)-2] + "+ " }
func deleteIndent(indent string) string { return indent[:len(indent)-2] + "- " }