Move a bunch of code around
Move most of the guts of `lumi` into the newly created `engine` package.
This commit is contained in:
parent
dcc549d9ec
commit
a6eabdc34b
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
291
cmd/lumi/env.go
291
cmd/lumi/env.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
639
cmd/lumi/plan.go
639
cmd/lumi/plan.go
|
@ -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
70
pkg/engine/compiler.go
Normal 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
|
||||
}
|
24
pkg/engine/config_delete.go
Normal file
24
pkg/engine/config_delete.go
Normal 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
25
pkg/engine/config_get.go
Normal 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
25
pkg/engine/config_list.go
Normal 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
28
pkg/engine/config_set.go
Normal 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
178
pkg/engine/deploy.go
Normal 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
17
pkg/engine/destroy.go
Normal 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
238
pkg/engine/env.go
Normal 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
12
pkg/engine/env_current.go
Normal 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
49
pkg/engine/env_info.go
Normal 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
8
pkg/engine/env_init.go
Normal 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
63
pkg/engine/env_list.go
Normal 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
25
pkg/engine/env_remove.go
Normal 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
8
pkg/engine/env_select.go
Normal 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
100
pkg/engine/pack.go
Normal 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
87
pkg/engine/pack_eval.go
Normal 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
399
pkg/engine/pack_info.go
Normal 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
19
pkg/engine/pack_verify.go
Normal 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
642
pkg/engine/plan.go
Normal 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] + "- " }
|
Loading…
Reference in a new issue