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
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/resource"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
)
@ -21,14 +17,14 @@ func newConfigCmd() *cobra.Command {
Short: "Query, set, replace, or unset configuration values",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return ListConfig(env)
return engine.ListConfig(env)
} else if len(args) == 1 && !unset {
return GetConfig(env, args[0])
return engine.GetConfig(env, args[0])
} 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
}
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
import (
"bytes"
"fmt"
"time"
"github.com/spf13/cobra"
"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/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
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" +
"explicit path can be provided using the [package] argument.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
return Deploy(DeployOptions{
return engine.Deploy(engine.DeployOptions{
Environment: env,
Package: pkgargFromArgs(args),
Debug: debug,
@ -94,166 +85,3 @@ func newDeployCmd() *cobra.Command {
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 (
"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"
)
@ -30,7 +30,7 @@ func newDestroyCmd() *cobra.Command {
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
if dryRun || yes ||
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
@ -55,17 +55,3 @@ func newDestroyCmd() *cobra.Command {
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
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path/filepath"
goerr "github.com/pkg/errors"
"github.com/spf13/cobra"
"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/engine"
"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 {
@ -37,7 +21,7 @@ func newEnvCmd() *cobra.Command {
"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",
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
}
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/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
)
@ -26,12 +26,7 @@ func newEnvInitCmd() *cobra.Command {
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
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"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/workspace"
)
func newEnvLsCmd() *cobra.Command {
@ -24,56 +15,7 @@ func newEnvLsCmd() *cobra.Command {
Aliases: []string{"list"},
Short: "List all known environments",
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 (
"github.com/pkg/errors"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
)
@ -33,7 +33,7 @@ func newEnvRmCmd() *cobra.Command {
// Ensure the user really wants to do this.
if yes ||
confirmPrompt("This will permanently remove the '%v' environment!", envName) {
return RemoveEnv(envName, force)
return engine.RemoveEnv(envName, force)
}
return nil
@ -49,22 +49,3 @@ func newEnvRmCmd() *cobra.Command {
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
import (
"fmt"
"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"
)
@ -24,22 +22,10 @@ func newEnvSelectCmd() *cobra.Command {
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
// Read in the name of the environment to switch to.
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
import (
"bufio"
"fmt"
"os"
"github.com/golang/glog"
"github.com/spf13/cobra"
"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/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
// NewLumiCmd creates a new Lumi Cmd instance.
@ -56,60 +54,15 @@ func pkgargFromArgs(args []string) string {
return ""
}
// 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)
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
}
// 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
return true
}

View file

@ -3,19 +3,7 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"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 {
@ -31,88 +19,3 @@ func newPackCmd() *cobra.Command {
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
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"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"
"github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"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.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
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
}
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
import (
"fmt"
"os"
"strings"
"unicode"
"github.com/spf13/cobra"
"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/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
func newPackInfoCmd() *cobra.Command {
@ -36,7 +28,7 @@ func newPackInfoCmd() *cobra.Command {
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
}
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
import (
"errors"
"github.com/pulumi/pulumi-fabric/pkg/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"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" +
"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 {
return PackVerify(pkgargFromArgs(args))
return engine.PackVerify(pkgargFromArgs(args))
}),
}
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
import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"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/engine"
"github.com/pulumi/pulumi-fabric/pkg/util/cmdutil"
"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 {
contract.Assertf(!dotOutput, "TODO[pulumi/pulumi-fabric#235]: DOT files not yet supported")
return Plan(PlanOptions{
return engine.Plan(engine.PlanOptions{
Package: pkgargFromArgs(args),
Debug: debug,
Environment: env,
@ -93,626 +81,3 @@ func newPlanCmd() *cobra.Command {
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] + "- " }