From 977b16b2cc05ed0a5b2f317f02c465b84cb61496 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 25 Feb 2017 09:24:52 -0800 Subject: [PATCH] Add basic targeting capability This change partially implements pulumi/coconut#94, by adding the ability to name targets during creation and reuse those names during deletion and update. This simplifies the management of deployment records, checkpoints, and snapshots. I've opted to call these things "husks" (perhaps going overboard with joy after our recent renaming). The basic idea is that for any executable Nut that will be deployed, you have a nutpack/ directory whose layout looks roughly as follows: nutpack/ bin/ Nutpack.json ... any other compiled artifacts ... husks/ ... one snapshot per husk ... For example, if we had a stage and prod husk, we would have: nutpack/ bin/... husks/ prod.json stage.json In the prod.json and stage.json files, we'd have the most recent deployment record for that environment. These would presumably get checked in and versioned along with the overall Nut, so that we can use Git history for rollbacks, etc. The create, update, and delete commands look in the right place for these files automatically, so you don't need to manually supply them. --- cmd/create.go | 23 ++-- cmd/delete.go | 17 ++- cmd/describe.go | 32 +++-- cmd/shared.go | 114 +++++++++--------- cmd/update.go | 21 ++-- examples/scenarios/aws/ec2instance/.gitignore | 1 + .../aws/ec2instance/mu_package/.gitignore | 2 - .../aws/ec2instance/mu_package/README.md | 6 - .../ec2instance/mu_package/targets/.gitignore | 2 - .../scenarios/aws/ec2instance/tsconfig.json | 2 +- pkg/resource/moniker.go | 2 +- pkg/resource/serialize.go | 20 +-- pkg/resource/snapshot.go | 23 ++-- pkg/workspace/paths.go | 57 +++++---- pkg/workspace/workspace.go | 5 + 15 files changed, 174 insertions(+), 153 deletions(-) delete mode 100644 examples/scenarios/aws/ec2instance/mu_package/.gitignore delete mode 100644 examples/scenarios/aws/ec2instance/mu_package/README.md delete mode 100644 examples/scenarios/aws/ec2instance/mu_package/targets/.gitignore diff --git a/cmd/create.go b/cmd/create.go index fbc8b5856..330f9e540 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -11,19 +11,20 @@ func newCreateCmd() *cobra.Command { var summary bool var output string var cmd = &cobra.Command{ - Use: "create [blueprint] [-- [args]]", - Short: "Create a new environment and its resources", - Long: "Create a new environment and its resources.\n" + + Use: "create husk-name [nut-file] [-- [args]]", + Short: "Create a new husk (target) with a given name and fresh resources", + Long: "Create a new husk (target) with a given name and fresh resources.\n" + "\n" + - "This command creates a new environment and its resources. These resources are\n" + - "the result of compiling and evaluating a Nut blueprint, and then extracting all\n" + - "resource allocations from its CocoGL graph. This command results in a full snapshot\n" + - "of the environment's resource state, so that it may be updated incrementally later on.\n" + + "This command creates a new husk (target) and its resources, with the given name. These\n" + + "resources are computed by compiling and evaluating an executable Nut, and then extracting\n" + + "resource allocations from its resulting object graph. This command saves full snapshot\n" + + "of the husk's final resource state, so that it may be updated incrementally later on.\n" + "\n" + - "By default, the Nut blueprint is loaded from the current directory. Optionally,\n" + - "a path to a Nut elsewhere can be provided as the [blueprint] argument.", + "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.", Run: func(cmd *cobra.Command, args []string) { - apply(cmd, args, "", applyOptions{ + apply(cmd, args, applyOptions{ + Create: true, Delete: false, DryRun: dryRun, Summary: summary, @@ -40,7 +41,7 @@ func newCreateCmd() *cobra.Command { "Only display summarization of resources and plan operations") cmd.PersistentFlags().StringVarP( &output, "output", "o", "", - "Serialize the resulting snapshot to a specific file, instead of the standard location") + "Serialize the resulting husk snapshot to a specific file, instead of the standard location") return cmd } diff --git a/cmd/delete.go b/cmd/delete.go index fcb2e2800..cb94e611b 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -10,14 +10,19 @@ func newDeleteCmd() *cobra.Command { var dryRun bool var summary bool var cmd = &cobra.Command{ - Use: "delete [snapshot]", - Short: "Delete an existing environment and its resources", - Long: "Delete an existing environment and its resources.\n" + + Use: "delete husk-name", + Short: "Delete an existing husk (target) and its resources", + Long: "Delete an existing husk (target) and its resources.\n" + "\n" + - "This command deletes an entire existing environment whose state is represented by the\n" + - "existing snapshot file. After running to completion, this environment will be gone.", + "This command deletes an entire existing husk by name. The current state is loaded\n" + + "from the associated snapshot file in the workspace. After running to completion,\n" + + "this environment and all of its associated state will be gone.\n" + + "\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.", Run: func(cmd *cobra.Command, args []string) { - applyExisting(cmd, args, applyOptions{ + apply(cmd, args, applyOptions{ + Create: false, Delete: true, DryRun: dryRun, Summary: summary, diff --git a/cmd/describe.go b/cmd/describe.go index b7b660a21..115f8096e 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -4,6 +4,7 @@ package cmd import ( "fmt" + "os" "strings" "unicode" @@ -14,6 +15,7 @@ import ( "github.com/pulumi/coconut/pkg/tokens" "github.com/pulumi/coconut/pkg/util/cmdutil" "github.com/pulumi/coconut/pkg/util/contract" + "github.com/pulumi/coconut/pkg/workspace" ) func newDescribeCmd() *cobra.Command { @@ -22,8 +24,8 @@ func newDescribeCmd() *cobra.Command { var printSymbols bool var printExportedSymbols bool var cmd = &cobra.Command{ - Use: "describe [packages...]", - Short: "Describe a Nut", + Use: "describe [nuts...]", + Short: "Describe one or more Nuts", Long: "Describe prints package, symbol, and IL information from one or more Nuts.", Run: func(cmd *cobra.Command, args []string) { // If printAll is true, flip all the flags. @@ -33,13 +35,27 @@ func newDescribeCmd() *cobra.Command { printExportedSymbols = true } - // Enumerate the list of packages, deserialize them, and print information. - for _, arg := range args { - pkg := cmdutil.ReadPackageFromArg(arg) - if pkg == nil { - break + if len(args) == 0 { + // No package specified, just load from the current directory. + pwd, _ := os.Getwd() + pkgpath, err := workspace.DetectPackage(pwd, sink()) + if err != nil { + fmt.Fprintf(os.Stderr, "fatal: could not find a nut: %v", err) + os.Exit(-1) + } + + if pkg := cmdutil.ReadPackage(pkgpath); pkg != nil { + printPackage(pkg, printSymbols, printExportedSymbols, printIL) + } + } else { + // Enumerate the list of packages, deserialize them, and print information. + for _, arg := range args { + pkg := cmdutil.ReadPackageFromArg(arg) + if pkg == nil { + break + } + printPackage(pkg, printSymbols, printExportedSymbols, printIL) } - printPackage(pkg, printSymbols, printExportedSymbols, printIL) } }, } diff --git a/cmd/shared.go b/cmd/shared.go index 93146ac2a..39289d678 100644 --- a/cmd/shared.go +++ b/cmd/shared.go @@ -95,14 +95,14 @@ type compileResult struct { } // plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan. -func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planResult { +func plan(cmd *cobra.Command, args []string, husk tokens.QName, create bool, delete bool) *planResult { // Create a new context for the plan operations. ctx := resource.NewContext(sink()) // If we are using an existing snapshot, read in that file (bailing if an IO error occurs). - var existing resource.Snapshot - if existfn != "" { - if existing = readSnapshot(ctx, existfn); existing == nil { + var old resource.Snapshot + if !create { + if old = readSnapshot(ctx, husk); old == nil { return nil } } @@ -112,15 +112,14 @@ func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planR return &planResult{ compileResult: nil, Ctx: ctx, - Nutpoint: existfn, - Existing: existing, - Snap: nil, - Plan: resource.NewDeletePlan(ctx, existing), + Husk: husk, + Old: old, + New: nil, + Plan: resource.NewDeletePlan(ctx, old), } } else if result := compile(cmd, args); result != nil && result.Heap != nil { // Create a resource snapshot from the compiled/evaluated object graph. - ns := resource.Namespace("no_namespace") // TODO[pulumi/coconut#94]: support for targets/namespaces. - snap, err := resource.NewGraphSnapshot(ctx, ns, result.Pkg.Name, result.C.Ctx().Opts.Args, result.Heap) + snap, err := resource.NewGraphSnapshot(ctx, husk, result.Pkg.Name, result.C.Ctx().Opts.Args, result.Heap) if err != nil { result.C.Diag().Errorf(errors.ErrorCantCreateSnapshot, err) return nil @@ -129,19 +128,20 @@ func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planR } var plan resource.Plan - if existing == nil { + if create { // Generate a plan for creating the resources from scratch. plan = resource.NewCreatePlan(ctx, snap) } else { // Generate a plan for updating existing resources to the new snapshot. - plan = resource.NewUpdatePlan(ctx, existing, snap) + contract.Assert(old != nil) + plan = resource.NewUpdatePlan(ctx, old, snap) } return &planResult{ compileResult: result, Ctx: ctx, - Nutpoint: existfn, - Existing: existing, - Snap: snap, + Husk: husk, + Old: old, + New: snap, Plan: plan, } } @@ -151,15 +151,25 @@ func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planR type planResult struct { *compileResult - Ctx *resource.Context - Nutpoint string // the file from which the existing snapshot was loaded (if any). - Existing resource.Snapshot // the existing snapshot (if any). - Snap resource.Snapshot // the new snapshot for this plan (if any). - Plan resource.Plan + Ctx *resource.Context + Husk tokens.QName // the husk name. + Old resource.Snapshot // the existing snapshot (if any). + New resource.Snapshot // the new snapshot for this plan (if any). + Plan resource.Plan } -func apply(cmd *cobra.Command, args []string, existing string, opts applyOptions) { - if result := plan(cmd, args, existing, opts.Delete); result != nil { +func apply(cmd *cobra.Command, args []string, opts applyOptions) { + // Read in the name of the husk to use. + var husk tokens.QName + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "fatal: missing required husk name\n") + os.Exit(-1) + } else { + husk = tokens.QName(args[0]) + args = args[1:] + } + + if result := plan(cmd, args, husk, opts.Create, opts.Delete); result != nil { if result.Plan.Empty() { sink().Infof(diag.Message("nothing to do -- resources are up to date")) } else if opts.DryRun { @@ -167,7 +177,7 @@ func apply(cmd *cobra.Command, args []string, existing string, opts applyOptions if opts.Output == "" || opts.Output == "-" { printPlan(result.Plan, opts.Summary) } else { - saveSnapshot(result.Snap, opts.Output) + saveSnapshot(husk, result.New, opts.Output) } } else { // Create an object to track progress and perform the actual operations. @@ -206,53 +216,38 @@ func apply(cmd *cobra.Command, args []string, existing string, opts applyOptions fmt.Printf(colors.Colorize(s)) // Now save the updated snapshot to the specified output file, if any, or the standard location otherwise. - // TODO: perform partial updates if we weren't able to perform the entire planned set of operations. + // TODO: save partial updates if we weren't able to perform the entire planned set of operations. if opts.Delete { - contract.Assert(result.Nutpoint != "") - deleteSnapshot(result.Nutpoint) + deleteSnapshot(result.Husk) } else { - out := opts.Output - if out == "" { - out = result.Nutpoint // try overwriting the existing file. - } - if out == "" { - out = workspace.Nutpoint // use the default file name. - } - contract.Assert(result.Snap != nil) - saveSnapshot(result.Snap, out) + contract.Assert(result.New != nil) + saveSnapshot(result.Husk, result.New, opts.Output) } } } } -func applyExisting(cmd *cobra.Command, args []string, opts applyOptions) { - // Read in the snapshot argument. - // TODO: if not supplied, auto-detect the current one. - if len(args) == 0 { - fmt.Fprintf(os.Stderr, "fatal: missing required snapshot argument\n") - os.Exit(-1) - } - - apply(cmd, args[1:], args[0], opts) -} - // backupSnapshot 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 backupSnapshot(file string) { contract.Require(file != "", "file") - // TODO: consider multiple backups (.bak.bak.bak...etc). os.Rename(file, file+".bak") // ignore errors. + // TODO: consider multiple backups (.bak.bak.bak...etc). } // deleteSnapshot removes an existing snapshot file, leaving behind a backup. -func deleteSnapshot(file string) { - contract.Require(file != "", "file") +func deleteSnapshot(husk tokens.QName) { + contract.Require(husk != "", "husk") // Just make a backup of the file and don't write out anything new. + file := workspace.HuskPath(husk) backupSnapshot(file) } // readSnapshot reads in an existing snapshot file, issuing an error and returning nil if something goes awry. -func readSnapshot(ctx *resource.Context, file string) resource.Snapshot { +func readSnapshot(ctx *resource.Context, husk tokens.QName) resource.Snapshot { + contract.Require(husk != "", "husk") + file := workspace.HuskPath(husk) + // Detect the encoding of the file so we can do our initial unmarshaling. m, ext := encoding.Detect(file) if m == nil { @@ -291,10 +286,13 @@ func readSnapshot(ctx *resource.Context, file string) resource.Snapshot { return resource.DeserializeSnapshot(ctx, &snap) } -// saveSnapshot saves a new CocoGL snapshot at the given location, backing up any existing ones. -func saveSnapshot(snap resource.Snapshot, file string) { +// saveSnapshot saves a new snapshot at the given location, backing up any existing ones. +func saveSnapshot(husk tokens.QName, snap resource.Snapshot, file string) { contract.Require(snap != nil, "snap") - contract.Require(file != "", "file") + contract.Require(husk != "", "husk") + if file == "" { + file = workspace.HuskPath(husk) + } // Make a serializable CocoGL data structure and then use the encoder to encode it. m, ext := encoding.Detect(file) @@ -313,15 +311,21 @@ func saveSnapshot(snap resource.Snapshot, file string) { // Back up the existing file if it already exists. backupSnapshot(file) - // And now write out the new snapshot file, overwriting that location. - if err = ioutil.WriteFile(file, b, 0644); err != nil { + // Ensure the directory exists. + if err = os.MkdirAll(filepath.Dir(file), 0744); err != nil { sink().Errorf(errors.ErrorIO, err) + } else { + // And now write out the new snapshot file, overwriting that location. + if err = ioutil.WriteFile(file, b, 0644); err != nil { + sink().Errorf(errors.ErrorIO, err) + } } } } } type applyOptions struct { + Create bool // true if we are creating resources. Delete bool // true if we are deleting resources. DryRun bool // true if we should just print the plan without performing it. Summary bool // true if we should only summarize resources and operations. diff --git a/cmd/update.go b/cmd/update.go index 330a617a7..821e869a3 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -11,21 +11,22 @@ func newUpdateCmd() *cobra.Command { var summary bool var output string var cmd = &cobra.Command{ - Use: "update [snapshot] [blueprint] [-- [args]]", - Short: "Update an existing environment and its resources", - Long: "Update an existing environment and its resources.\n" + + Use: "update husk-name [nut-file] [-- [args]]", + Short: "Update an existing husk (target) and its resources", + Long: "Update an existing husk (target) and its resources.\n" + "\n" + - "This command updates an existing environment whose state is represented by the\n" + + "This command updates an existing husk environment whose state is represented by the\n" + "existing snapshot file. The new desired state is computed by compiling and evaluating\n" + - "a Nut blueprint, and extracting all resource allocations from its CocoGL graph.\n" + - "This is then compared against the existing state to determine what operations must take\n" + + "an executable Nut, and extracting all resource allocations from its resulting object graph.\n" + + "This graph is compared against the existing state to determine what operations must take\n" + "place to achieve the desired state. This command results in a full snapshot of the\n" + "environment's new resource state, so that it may be updated incrementally again later.\n" + "\n" + - "By default, the Nut blueprint is loaded from the current directory. Optionally,\n" + - "a path to a Nut elsewhere can be provided as the [blueprint] argument.", + "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.", Run: func(cmd *cobra.Command, args []string) { - applyExisting(cmd, args, applyOptions{ + apply(cmd, args, applyOptions{ + Create: false, Delete: false, DryRun: dryRun, Summary: summary, @@ -42,7 +43,7 @@ func newUpdateCmd() *cobra.Command { "Only display summarization of resources and plan operations") cmd.PersistentFlags().StringVarP( &output, "output", "o", "", - "Serialize the resulting snapshot to a specific file, instead of overwriting the existing one") + "Serialize the resulting husk snapshot to a specific file, instead of overwriting the existing one") return cmd } diff --git a/examples/scenarios/aws/ec2instance/.gitignore b/examples/scenarios/aws/ec2instance/.gitignore index 6dbf1d900..37953e513 100644 --- a/examples/scenarios/aws/ec2instance/.gitignore +++ b/examples/scenarios/aws/ec2instance/.gitignore @@ -1,3 +1,4 @@ bin/ node_modules/ +nutpack/ diff --git a/examples/scenarios/aws/ec2instance/mu_package/.gitignore b/examples/scenarios/aws/ec2instance/mu_package/.gitignore deleted file mode 100644 index 81325b3d8..000000000 --- a/examples/scenarios/aws/ec2instance/mu_package/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/bin/ - diff --git a/examples/scenarios/aws/ec2instance/mu_package/README.md b/examples/scenarios/aws/ec2instance/mu_package/README.md deleted file mode 100644 index 591a5ccce..000000000 --- a/examples/scenarios/aws/ec2instance/mu_package/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# ec2instance Mu Package - -This is the auto-generated directory structure for the "ec2instance" Mu package. - -The compiled package can be found underneath `bin/`, while all known deployment targets are in `targets/`. - diff --git a/examples/scenarios/aws/ec2instance/mu_package/targets/.gitignore b/examples/scenarios/aws/ec2instance/mu_package/targets/.gitignore deleted file mode 100644 index 07c43328d..000000000 --- a/examples/scenarios/aws/ec2instance/mu_package/targets/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/*.bak - diff --git a/examples/scenarios/aws/ec2instance/tsconfig.json b/examples/scenarios/aws/ec2instance/tsconfig.json index 3086b63ab..7955f6e26 100644 --- a/examples/scenarios/aws/ec2instance/tsconfig.json +++ b/examples/scenarios/aws/ec2instance/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "outDir": "bin", + "outDir": "nutpack/bin", "target": "es6", "module": "commonjs", "moduleResolution": "node", diff --git a/pkg/resource/moniker.go b/pkg/resource/moniker.go index 9a0053456..b074792e1 100644 --- a/pkg/resource/moniker.go +++ b/pkg/resource/moniker.go @@ -30,7 +30,7 @@ type Moniker string const MonikerDelimiter = "::" // the delimiter between elements of the moniker. // NewMoniker creates a unique moniker for the given object. -func NewMoniker(ns Namespace, alloc tokens.Module, t tokens.Type, name tokens.QName) Moniker { +func NewMoniker(ns tokens.QName, alloc tokens.Module, t tokens.Type, name tokens.QName) Moniker { return Moniker( string(ns) + MonikerDelimiter + string(alloc) + diff --git a/pkg/resource/serialize.go b/pkg/resource/serialize.go index 65b8d7e38..5bc9f1fe9 100644 --- a/pkg/resource/serialize.go +++ b/pkg/resource/serialize.go @@ -15,7 +15,7 @@ import ( // SerializedSnapshot is a serializable, flattened CocoGL graph structure, specifically for snapshots. It is similar // to the actual Snapshot interface, except that it flattens and rearranges a few data structures for serializability. type SerializedSnapshot struct { - Target Namespace `json:"target"` // the target environment name. + Husk tokens.QName `json:"husk"` // the target environment name. Package tokens.PackageName `json:"package"` // the package which created this graph. Args *core.Args `json:"args,omitempty"` // the blueprint args for graph creation. Refs *string `json:"refs,omitempty"` // the ref alias, if any (`#ref` by default). @@ -66,7 +66,7 @@ func SerializeSnapshot(snap Snapshot, reftag string) *SerializedSnapshot { } return &SerializedSnapshot{ - Target: snap.Ns(), + Husk: snap.Husk(), Package: snap.Pkg(), // TODO: eventually, this should carry version metadata too. Args: argsp, Refs: refp, @@ -149,20 +149,20 @@ func SerializeProperty(prop PropertyValue, reftag string) (interface{}, bool) { } // DeserializeSnapshot takes a serialized CocoGL snapshot data structure and returns its associated snapshot. -func DeserializeSnapshot(ctx *Context, mugl *SerializedSnapshot) Snapshot { +func DeserializeSnapshot(ctx *Context, ser *SerializedSnapshot) Snapshot { // Determine the reftag to use. var reftag string - if mugl.Refs == nil { + if ser.Refs == nil { reftag = DefaultSnapshotReftag } else { - reftag = *mugl.Refs + reftag = *ser.Refs } // For every serialized resource vertex, create a SerializedResource out of it. var resources []Resource - if mugl.Resources != nil { + if ser.Resources != nil { // TODO: we need to enumerate resources in the specific order in which they were emitted. - for _, kvp := range mugl.Resources.Iter() { + for _, kvp := range ser.Resources.Iter() { // Deserialize the resources, if they exist. res := kvp.Value var props PropertyMap @@ -183,11 +183,11 @@ func DeserializeSnapshot(ctx *Context, mugl *SerializedSnapshot) Snapshot { } var args core.Args - if mugl.Args != nil { - args = *mugl.Args + if ser.Args != nil { + args = *ser.Args } - return NewSnapshot(ctx, mugl.Target, mugl.Package, args, resources) + return NewSnapshot(ctx, ser.Husk, ser.Package, args, resources) } func DeserializeProperties(props SerializedPropertyMap, reftag string) PropertyMap { diff --git a/pkg/resource/snapshot.go b/pkg/resource/snapshot.go index e60aa6612..3c1fdeb5d 100644 --- a/pkg/resource/snapshot.go +++ b/pkg/resource/snapshot.go @@ -14,14 +14,12 @@ import ( "github.com/pulumi/coconut/pkg/util/contract" ) -type Namespace tokens.QName // a namespace is the target for a deployment. - // Snapshot is a view of a collection of resources in an environment at a point in time. It describes resources; their // IDs, names, and properties; their dependencies; and more. A snapshot is a diffable entity and can be used to create // or apply an infrastructure deployment plan in order to make reality match the snapshot state. type Snapshot interface { Ctx() *Context // fetches the context for this snapshot. - Ns() Namespace // the namespace being deployed into. + Husk() tokens.QName // the husk/namespace target being deployed into. Pkg() tokens.PackageName // the package from which this snapshot came. Args() core.Args // the arguments used to compile this package. Resources() []Resource // a topologically sorted list of resources (based on dependencies). @@ -32,14 +30,15 @@ type Snapshot interface { // NewSnapshot creates a snapshot from the given arguments. Note that resources must be in topologically-sorted // dependency order, otherwise undefined behavior will result from using the resulting snapshot object. -func NewSnapshot(ctx *Context, ns Namespace, pkg tokens.PackageName, args core.Args, resources []Resource) Snapshot { - return &snapshot{ctx, ns, pkg, args, resources} +func NewSnapshot(ctx *Context, husk tokens.QName, pkg tokens.PackageName, + args core.Args, resources []Resource) Snapshot { + return &snapshot{ctx, husk, pkg, args, resources} } // NewGraphSnapshot takes an object graph and produces a resource snapshot from it. It understands how to name // resources based on their position within the graph and how to identify and record dependencies. This function can // fail dynamically if the input graph did not satisfy the preconditions for resource graphs (like that it is a DAG). -func NewGraphSnapshot(ctx *Context, ns Namespace, pkg tokens.PackageName, args core.Args, +func NewGraphSnapshot(ctx *Context, husk tokens.QName, pkg tokens.PackageName, args core.Args, heap *heapstate.Heap) (Snapshot, error) { // Topologically sort the entire heapstate (in dependency order) and extract just the resource objects. @@ -50,24 +49,24 @@ func NewGraphSnapshot(ctx *Context, ns Namespace, pkg tokens.PackageName, args c // Next, name all resources, create their monikers and objects, and maps that we will use. Note that we must do // this in DAG order (guaranteed by our topological sort above), so that referenced monikers are available. - resources, err := createResources(ctx, ns, heap, resobjs) + resources, err := createResources(ctx, husk, heap, resobjs) if err != nil { return nil, err } - return NewSnapshot(ctx, ns, pkg, args, resources), nil + return NewSnapshot(ctx, husk, pkg, args, resources), nil } type snapshot struct { ctx *Context // the context shared by all operations in this snapshot. - ns Namespace // the namespace being deployed into. + husk tokens.QName // the husk/namespace target being deployed into. pkg tokens.PackageName // the package from which this snapshot came. args core.Args // the arguments used to compile this package. resources []Resource // the topologically sorted linearized list of resources. } func (s *snapshot) Ctx() *Context { return s.ctx } -func (s *snapshot) Ns() Namespace { return s.ns } +func (s *snapshot) Husk() tokens.QName { return s.husk } func (s *snapshot) Pkg() tokens.PackageName { return s.pkg } func (s *snapshot) Args() core.Args { return s.args } func (s *snapshot) Resources() []Resource { return s.resources } @@ -82,7 +81,7 @@ func (s *snapshot) ResourceByObject(obj *rt.Object) Resource { return s.ctx.ObjR // createResources uses a graph to create monikers and resource objects for every resource within. It // returns two maps for further use: a map of vertex to its new resource object, and a map of vertex to its moniker. -func createResources(ctx *Context, ns Namespace, heap *heapstate.Heap, resobjs []*rt.Object) ([]Resource, error) { +func createResources(ctx *Context, husk tokens.QName, heap *heapstate.Heap, resobjs []*rt.Object) ([]Resource, error) { var resources []Resource for _, resobj := range resobjs { // Create an object resource without a moniker. @@ -101,7 +100,7 @@ func createResources(ctx *Context, ns Namespace, heap *heapstate.Heap, resobjs [ // Now compute a unique moniker for this object and ensure we haven't had any collisions. alloc := heap.Alloc(resobj) - moniker := NewMoniker(ns, alloc.Mod.Tok, t, name) + moniker := NewMoniker(husk, alloc.Mod.Tok, t, name) glog.V(7).Infof("Resource moniker computed: %v", moniker) if _, exists := ctx.MksRes[moniker]; exists { // If this moniker is already in use, issue an error, ignore this one, and break. The break is necessary diff --git a/pkg/workspace/paths.go b/pkg/workspace/paths.go index 260063f6b..088d3f1e2 100644 --- a/pkg/workspace/paths.go +++ b/pkg/workspace/paths.go @@ -11,35 +11,24 @@ import ( "github.com/pulumi/coconut/pkg/compiler/errors" "github.com/pulumi/coconut/pkg/diag" "github.com/pulumi/coconut/pkg/encoding" + "github.com/pulumi/coconut/pkg/tokens" ) -// Nutfile is the base name of a Nutfile. -const Nutfile = "Nut" +const Nutfile = "Nut" // the base name of a Nutfile. +const Nutpack = "Nutpack" // the base name of a compiled NutPack. +const NutpackOutDir = "nutpack" // the default name of the NutPack output directory. +const NutpackBinDir = "bin" // the default name of the NutPack binary output directory. +const NutpackHusksDir = "husks" // the default name of the NutPack husks directory. +const Nutspace = "Coconut" // the base name of a markup file for shared settings in a workspace. +const Nutdeps = ".Nuts" // the directory in which dependencies exist, either local or global. -// Nutpack is the base name of a compiled Nut package. -const Nutpack = "Nutpack" - -// Nutpoint is the base name of a Nut's CocoGL graph file (checkpoint). -const Nutpoint = "Nutpoint" - -// Nutspace is the base name of a markup file containing settings shared amongst a workspace. -const Nutspace = "Nutspace" - -// Nutdeps is the directory in which dependency modules exist, either local to a workspace, or globally. -const Nutdeps = ".Nuts" - -// InstallRootEnvvar is the envvar describing where Coconut has been installed. -const InstallRootEnvvar = "COCOROOT" - -// InstallRootLibdir is the directory in which the Coconut standard library exists. -const InstallRootLibdir = "lib" - -// DefaultInstallRoot is where Coconut is installed by default, if the envvar is missing. -// TODO: support Windows. -const DefaultInstallRoot = "/usr/local/coconut" +const InstallRootEnvvar = "COCOROOT" // the envvar describing where Coconut has been installed. +const InstallRootLibdir = "lib" // the directory in which the Coconut standard library exists. +const DefaultInstallRoot = "/usr/local/coconut" // where Coconut is installed by default. // InstallRoot returns Coconut's installation location. This is controlled my the COCOROOT envvar. func InstallRoot() string { + // TODO: support Windows. root := os.Getenv(InstallRootEnvvar) if root == "" { return DefaultInstallRoot @@ -47,6 +36,11 @@ func InstallRoot() string { return root } +// HuskPath returns a path to the given husk's default location. +func HuskPath(husk tokens.QName) string { + return filepath.Join(NutpackOutDir, NutpackHusksDir, qnamePath(husk)+encoding.Exts[0]) +} + // isTop returns true if the path represents the top of the filesystem. func isTop(path string) bool { return os.IsPathSeparator(path[len(path)-1]) @@ -79,6 +73,17 @@ func DetectPackage(path string, d diag.Sink) (string, error) { if err != nil { return "", err } + + // See if there's a compiled Nutpack in the expected location. + pack := filepath.Join(NutpackOutDir, NutpackBinDir, Nutpack) + for _, ext := range encoding.Exts { + packfile := pack + ext + if IsNutpack(packfile, d) { + return packfile, nil + } + } + + // Now look for individual Nutfiles. for _, file := range files { name := file.Name() path := filepath.Join(curr, name) @@ -117,12 +122,6 @@ func IsNutpack(path string, d diag.Sink) bool { return isMarkupFile(path, Nutpack, d) } -// IsNutpoint returns true if the path references what appears to be a valid CocoGL file. If problems are detected -- -// like an incorrect extension -- they are logged to the provided diag.Sink (if non-nil). -func IsNutpoint(path string, d diag.Sink) bool { - return isMarkupFile(path, Nutpoint, d) -} - // IsNutspace returns true if the path references what appears to be a valid Nutspace file. If problems are detected -- // like an incorrect extension -- they are logged to the provided diag.Sink (if non-nil). func IsNutspace(path string, d diag.Sink) bool { diff --git a/pkg/workspace/workspace.go b/pkg/workspace/workspace.go index d9ad947e8..3651fecef 100644 --- a/pkg/workspace/workspace.go +++ b/pkg/workspace/workspace.go @@ -183,6 +183,11 @@ func namePath(nm tokens.Name) string { return stringNamePath(string(nm)) } +// qnamePath just cleans a name and makes sure it's appropriate to use as a path. +func qnamePath(nm tokens.QName) string { + return stringNamePath(string(nm)) +} + // packageNamePath just cleans a package name and makes sure it's appropriate to use as a path. func packageNamePath(nm tokens.PackageName) string { return stringNamePath(string(nm))