Unify a bit of command logic, and hoist some failure modes

This commit is contained in:
joeduffy 2017-02-27 14:13:27 -08:00
parent 73babc13a0
commit 371a847eb9
4 changed files with 75 additions and 65 deletions

View file

@ -58,8 +58,44 @@ func sink() diag.Sink {
return snk return snk
} }
func initHuskCmd(cmd *cobra.Command, args []string) *huskCmdInfo {
// Create a new context for the plan operations.
ctx := resource.NewContext(sink())
// Read in the name of the husk to use.
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "fatal: missing required husk name\n")
os.Exit(-1)
}
husk := tokens.QName(args[0])
// Read in the deployment information, bailing if an IO error occurs.
dep, old := readHusk(ctx, husk)
if dep == nil {
contract.Assert(!ctx.Diag.Success())
return nil // failure reading the husk information.
}
return &huskCmdInfo{
ctx: ctx,
husk: husk,
dep: dep,
old: old,
args: args[1:],
orig: args,
}
}
type huskCmdInfo struct {
ctx *resource.Context // the resulting context
husk tokens.QName // the husk name
dep *resource.Deployment // the husk's deployment record
old resource.Snapshot // the husk's latest deployment snapshot
args []string // the rest of the args after extracting the husk name
orig []string // the original args before extracting the husk name
}
// create just creates a new husk without deploying anything into it. // create just creates a new husk without deploying anything into it.
func create(cmd *cobra.Command, args []string, husk tokens.QName) { func create(husk tokens.QName) {
if success := saveHusk(husk, nil, "", false); success { if success := saveHusk(husk, nil, "", false); success {
fmt.Printf("Coconut husk '%v' initialized; ready for deployments (see `coco husk deploy`)\n", husk) fmt.Printf("Coconut husk '%v' initialized; ready for deployments (see `coco husk deploy`)\n", husk)
} }
@ -121,44 +157,35 @@ type compileResult struct {
} }
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan. // plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
func plan(cmd *cobra.Command, args []string, husk tokens.QName, delete bool) *planResult { func plan(cmd *cobra.Command, info *huskCmdInfo, delete bool) *planResult {
// Create a new context for the plan operations.
ctx := resource.NewContext(sink())
// Read in the deployment information, bailing if an IO error occurs.
dep, old := readHusk(ctx, husk)
if dep == nil {
contract.Assert(!ctx.Diag.Success())
return nil // failure reading the husk information.
}
// If deleting, there is no need to create a new snapshot; otherwise, we will need to compile the package. // If deleting, there is no need to create a new snapshot; otherwise, we will need to compile the package.
var new resource.Snapshot var new resource.Snapshot
var result *compileResult var result *compileResult
if !delete { if !delete {
// First, compile; if that yields errors or an empty heap, exit early. // First, compile; if that yields errors or an empty heap, exit early.
if result = compile(cmd, args); result == nil || result.Heap == nil { if result = compile(cmd, info.args); result == nil || result.Heap == nil {
return nil return nil
} }
// Create a resource snapshot from the compiled/evaluated object graph. // Create a resource snapshot from the compiled/evaluated object graph.
var err error var err error
new, err = resource.NewGraphSnapshot(ctx, husk, result.Pkg.Tok, result.C.Ctx().Opts.Args, result.Heap) new, err = resource.NewGraphSnapshot(
info.ctx, info.husk, result.Pkg.Tok, result.C.Ctx().Opts.Args, result.Heap)
if err != nil { if err != nil {
result.C.Diag().Errorf(errors.ErrorCantCreateSnapshot, err) result.C.Diag().Errorf(errors.ErrorCantCreateSnapshot, err)
return nil return nil
} else if !ctx.Diag.Success() { } else if !info.ctx.Diag.Success() {
return nil return nil
} }
} }
// Generate a plan; this API handles all interesting cases (create, update, delete). // Generate a plan; this API handles all interesting cases (create, update, delete).
plan := resource.NewPlan(ctx, old, new) plan := resource.NewPlan(info.ctx, info.old, new)
return &planResult{ return &planResult{
compileResult: result, compileResult: result,
Ctx: ctx, Ctx: info.ctx,
Husk: husk, Husk: info.husk,
Old: old, Old: info.old,
New: new, New: new,
Plan: plan, Plan: plan,
} }
@ -173,11 +200,11 @@ type planResult struct {
Plan resource.Plan Plan resource.Plan
} }
func apply(cmd *cobra.Command, args []string, husk tokens.QName, opts applyOptions) { func apply(cmd *cobra.Command, info *huskCmdInfo, opts applyOptions) {
if result := plan(cmd, args, husk, opts.Delete); result != nil { if result := plan(cmd, info, opts.Delete); result != nil {
// If we are doing an empty update, say so. // If we are doing an empty update, say so.
if result.Plan.Empty() && !opts.Delete { if result.Plan.Empty() && !opts.Delete {
sink().Infof(diag.Message("nothing to do -- resources are up to date")) info.ctx.Diag.Infof(diag.Message("nothing to do -- resources are up to date"))
} }
// Now based on whether a dry run was specified, or not, either print or perform the planned operations. // Now based on whether a dry run was specified, or not, either print or perform the planned operations.
@ -186,7 +213,7 @@ func apply(cmd *cobra.Command, args []string, husk tokens.QName, opts applyOptio
if opts.Output == "" || opts.Output == "-" { if opts.Output == "" || opts.Output == "-" {
printPlan(result.Plan, opts.ShowUnchanged, opts.Summary) printPlan(result.Plan, opts.ShowUnchanged, opts.Summary)
} else { } else {
saveHusk(husk, result.New, opts.Output, true /*overwrite*/) saveHusk(info.husk, result.New, opts.Output, true /*overwrite*/)
} }
} else { } else {
// If show unchanged was requested, print them first. // If show unchanged was requested, print them first.
@ -204,7 +231,7 @@ func apply(cmd *cobra.Command, args []string, husk tokens.QName, opts applyOptio
// TODO: we want richer diagnostics in the event that a plan apply fails. For instance, we want to // TODO: we want richer diagnostics in the event that a plan apply fails. For instance, we want to
// know precisely what step failed, we want to know whether it was catastrophic, etc. We also // know precisely what step failed, we want to know whether it was catastrophic, etc. We also
// probably want to plumb diag.Sink through apply so it can issue its own rich diagnostics. // probably want to plumb diag.Sink through apply so it can issue its own rich diagnostics.
sink().Errorf(errors.ErrorPlanApplyFailed, err) info.ctx.Diag.Errorf(errors.ErrorPlanApplyFailed, err)
} }
// Print out the total number of steps performed (and their kinds), if any succeeded. // Print out the total number of steps performed (and their kinds), if any succeeded.

View file

@ -3,12 +3,7 @@
package cmd package cmd
import ( import (
"fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/coconut/pkg/tokens"
) )
func newHuskDeployCmd() *cobra.Command { func newHuskDeployCmd() *cobra.Command {
@ -31,20 +26,15 @@ func newHuskDeployCmd() *cobra.Command {
"By default, the Nut to execute is loaded from the current directory. Optionally, an\n" + "By default, the Nut to execute is loaded from the current directory. Optionally, an\n" +
"explicit path can be provided using the [nut-file] argument.", "explicit path can be provided using the [nut-file] argument.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Read in the name of the husk to use. if info := initHuskCmd(cmd, args); info != nil {
if len(args) == 0 { apply(cmd, info, applyOptions{
fmt.Fprintf(os.Stderr, "fatal: missing required husk name\n") Delete: false,
os.Exit(-1) DryRun: dryRun,
ShowUnchanged: showUnchanged,
Summary: summary,
Output: output,
})
} }
husk := tokens.QName(args[0])
apply(cmd, args[1:], husk, applyOptions{
Delete: false,
DryRun: dryRun,
ShowUnchanged: showUnchanged,
Summary: summary,
Output: output,
})
}, },
} }

View file

@ -8,8 +8,6 @@ import (
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/pulumi/coconut/pkg/tokens"
) )
func newHuskDestroyCmd() *cobra.Command { func newHuskDestroyCmd() *cobra.Command {
@ -28,28 +26,23 @@ func newHuskDestroyCmd() *cobra.Command {
"Warning: although old snapshots can be used to recreate an environment, this command\n" + "Warning: although old snapshots can be used to recreate an environment, this command\n" +
"is generally irreversable and should be used with great care.", "is generally irreversable and should be used with great care.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Read in the name of the husk to use. if info := initHuskCmd(cmd, args); info != nil {
if len(args) == 0 { if !dryRun && !yes {
fmt.Fprintf(os.Stderr, "fatal: missing required husk name\n") fmt.Printf("This will permanently delete all resources in the '%v' husk!\n", info.husk)
os.Exit(-1) fmt.Printf("Please confirm that this is what you'd like to do by typing (\"yes\"): ")
} reader := bufio.NewReader(os.Stdin)
if line, _ := reader.ReadString('\n'); line != "yes\n" {
husk := tokens.QName(args[0]) fmt.Fprintf(os.Stderr, "Confirmation declined -- exiting without destroying the husk\n")
if !yes { os.Exit(-1)
fmt.Printf("This will permanently delete all resources in the '%v' husk!\n", husk) }
fmt.Printf("Please confirm that this is what you'd like to do by typing (\"yes\"): ")
reader := bufio.NewReader(os.Stdin)
if line, _ := reader.ReadString('\n'); line != "yes\n" {
fmt.Fprintf(os.Stderr, "Confirmation declined -- exiting without destroying the husk\n")
os.Exit(-1)
} }
}
apply(cmd, args[1:], husk, applyOptions{ apply(cmd, info, applyOptions{
Delete: true, Delete: true,
DryRun: dryRun, DryRun: dryRun,
Summary: summary, Summary: summary,
}) })
}
}, },
} }

View file

@ -27,7 +27,7 @@ func newHuskInitCmd() *cobra.Command {
} }
husk := tokens.QName(args[0]) husk := tokens.QName(args[0])
create(cmd, args[1:], husk) create(husk)
}, },
} }
} }