diff --git a/cmd/apply.go b/cmd/apply.go index c8eec5b4d..1a3cec293 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -4,9 +4,12 @@ package cmd import ( "github.com/spf13/cobra" + + "github.com/marapongo/mu/pkg/compiler/errors" ) func newApplyCmd() *cobra.Command { + var delete bool var cmd = &cobra.Command{ Use: "apply [blueprint] [-- [args]]", Short: "Apply a deployment plan from a Mu blueprint", @@ -23,10 +26,21 @@ func newApplyCmd() *cobra.Command { "By default, a blueprint package is loaded from the current directory. Optionally,\n" + "a path to a blueprint elsewhere can be provided as the [blueprint] argument.", Run: func(cmd *cobra.Command, args []string) { + if comp, plan := plan(cmd, args, delete); plan != nil { + if _, err, _ := plan.Apply(); err != nil { + // 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 + // probably want to plumb diag.Sink through apply so it can issue its own rich diagnostics. + comp.Diag().Errorf(errors.ErrorPlanApplyFailed, err) + } + } }, } // TODO: options; most importantly, what to compare the blueprint against. + cmd.PersistentFlags().BoolVar( + &delete, "delete", false, + "Delete the entirety of the blueprint's resources") return cmd } diff --git a/cmd/compile_shared.go b/cmd/compile_shared.go index 3115cce4e..093388030 100644 --- a/cmd/compile_shared.go +++ b/cmd/compile_shared.go @@ -9,14 +9,16 @@ import ( "github.com/marapongo/mu/pkg/compiler" "github.com/marapongo/mu/pkg/compiler/core" + "github.com/marapongo/mu/pkg/compiler/errors" + "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/graph" + "github.com/marapongo/mu/pkg/resource" "github.com/marapongo/mu/pkg/util/cmdutil" - "github.com/marapongo/mu/pkg/util/contract" ) // compile just uses the standard logic to parse arguments, options, and to locate/compile a package. It returns the // MuGL graph that is produced, or nil if an error occurred (in which case, we would expect non-0 errors). -func compile(cmd *cobra.Command, args []string) graph.Graph { +func compile(cmd *cobra.Command, args []string) (compiler.Compiler, graph.Graph) { // If there's a --, we need to separate out the command args from the stack args. flags := cmd.Flags() dashdash := flags.ArgsLenAtDash() @@ -26,23 +28,29 @@ func compile(cmd *cobra.Command, args []string) graph.Graph { args = args[0:dashdash] } + // A func to lazily allocate a sink to be used if we can't create a compiler. + d := func() diag.Sink { return core.DefaultSink("") } + // Create a compiler options object and map any flags and arguments to settings on it. opts := core.DefaultOptions() opts.Args = dashdashArgsToMap(packArgs) // In the case of an argument, load that specific package and new up a compiler based on its base path. // Otherwise, use the default workspace and package logic (which consults the current working directory). + var comp compiler.Compiler var mugl graph.Graph if len(args) == 0 { - comp, err := compiler.Newwd(opts) + var err error + comp, err = compiler.Newwd(opts) if err != nil { - contract.Failf("fatal: %v", err) + // Create a temporary diagnostics sink so that we can issue an error and bail out. + d().Errorf(errors.ErrorCantCreateCompiler, err) + return nil, nil } mugl = comp.Compile() } else { fn := args[0] if pkg := cmdutil.ReadPackageFromArg(fn); pkg != nil { - var comp compiler.Compiler var err error if fn == "-" { comp, err = compiler.Newwd(opts) @@ -50,10 +58,41 @@ func compile(cmd *cobra.Command, args []string) graph.Graph { comp, err = compiler.New(filepath.Dir(fn), opts) } if err != nil { - contract.Failf("fatal: %v", err) + d().Errorf(errors.ErrorCantReadPackage, fn, err) + return nil, nil } mugl = comp.CompilePackage(pkg) } } - return mugl + return comp, mugl +} + +// plan just uses the standard logic to parse arguments, options, and to create a plan for a package. +func plan(cmd *cobra.Command, args []string, delete bool) (compiler.Compiler, resource.Plan) { + // Perform the compilation and, if non-nil is returned, create a plan and print it. + comp, mugl := compile(cmd, args) + if mugl != nil { + // TODO: fetch the old plan for purposes of diffing. + rs, err := resource.NewSnapshot(mugl) // create a resource snapshot from the object graph. + if err != nil { + comp.Diag().Errorf(errors.ErrorCantCreateSnapshot, err) + return comp, nil + } + + // Create a new context for the plan operations. + ctx := resource.NewContext() + + var plan resource.Plan + if delete { + // Generate a plan for deleting the entire snapshot. + plan = resource.NewDeletePlan(ctx, rs) + } else { + // Generate a plan for creating the resources from scratch. + plan = resource.NewCreatePlan(ctx, rs) + } + + return comp, plan + } + + return comp, nil } diff --git a/cmd/eval.go b/cmd/eval.go index 5bc6c7020..c311b1a31 100644 --- a/cmd/eval.go +++ b/cmd/eval.go @@ -31,7 +31,7 @@ func newEvalCmd() *cobra.Command { "a path to a blueprint elsewhere can be provided as the [blueprint] argument.", Run: func(cmd *cobra.Command, args []string) { // Perform the compilation and, if non-nil is returned, output the graph. - if mugl := compile(cmd, args); mugl != nil { + if _, mugl := compile(cmd, args); mugl != nil { // Serialize that MuGL graph so that it's suitable for printing/serializing. if dotOutput { // Convert the output to a DOT file. diff --git a/cmd/plan.go b/cmd/plan.go index 29c087a12..9cca6cd5c 100644 --- a/cmd/plan.go +++ b/cmd/plan.go @@ -5,7 +5,6 @@ package cmd import ( "bytes" "fmt" - "os" "strconv" "github.com/spf13/cobra" @@ -33,28 +32,7 @@ func newPlanCmd() *cobra.Command { "By default, a blueprint package is loaded from the current directory. Optionally,\n" + "a path to a blueprint elsewhere can be provided as the [blueprint] argument.", Run: func(cmd *cobra.Command, args []string) { - // Perform the compilation and, if non-nil is returned, create a plan and print it. - if mugl := compile(cmd, args); mugl != nil { - // TODO: fetch the old plan for purposes of diffing. - rs, err := resource.NewSnapshot(mugl) // create a resource snapshot from the object graph. - if err != nil { - fmt.Fprintf(os.Stderr, "fatal: %v\n", err) - os.Exit(-1) - } - - // Create a new context for the plan operations. - ctx := resource.NewContext() - - var plan resource.Plan - if delete { - // Generate a plan for deleting the entire snapshot. - plan = resource.NewDeletePlan(ctx, rs) - } else { - // Generate a plan for creating the resources from scratch. - plan = resource.NewCreatePlan(ctx, rs) - } - - // Finally just pretty-print out the plan. + if _, plan := plan(cmd, args, delete); plan != nil { printPlan(plan) } }, diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index c28fcee5e..c34d312e3 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -59,10 +59,7 @@ func New(path string, opts *core.Options) (Compiler, error) { } d := opts.Diag if d == nil { - d = diag.DefaultSink(diag.FormatOptions{ - Pwd: path, // ensure output paths are relative to the current path. - Colors: true, // turn on colorization of warnings/errors. - }) + d = core.DefaultSink(path) } // Now create a new context to share amongst the compiler and workspace. diff --git a/pkg/compiler/core/opts.go b/pkg/compiler/core/opts.go index d45f49e42..54e52349c 100644 --- a/pkg/compiler/core/opts.go +++ b/pkg/compiler/core/opts.go @@ -20,3 +20,11 @@ type Options struct { func DefaultOptions() *Options { return &Options{} } + +// DefaultOptionsSink returns the default preconfigured diagnostics sink. +func DefaultSink(path string) diag.Sink { + return diag.DefaultSink(diag.FormatOptions{ + Pwd: path, // ensure output paths are relative to the current path. + Colors: true, // turn on colorization of warnings/errors. + }) +} diff --git a/pkg/compiler/errors/planapply.go b/pkg/compiler/errors/planapply.go new file mode 100644 index 000000000..ca6f4d600 --- /dev/null +++ b/pkg/compiler/errors/planapply.go @@ -0,0 +1,11 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package errors + +// Plan and apply errors are in the [2000,3000) range. +var ( + ErrorCantCreateCompiler = newError(2000, "An error occurred during compiler construction: %v") + ErrorCantReadPackage = newError(2001, "An error occurred while reading the package '%v': %v") + ErrorCantCreateSnapshot = newError(2002, "Illegal MuGL structure detected; cannot create a snapshot: %v") + ErrorPlanApplyFailed = newError(2003, "Plan apply failed: %v") +)