2016-11-16 20:09:45 +01:00
|
|
|
// Copyright 2016 Marapongo, Inc. All rights reserved.
|
|
|
|
|
2016-11-17 17:52:54 +01:00
|
|
|
package core
|
2016-11-16 20:09:45 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/marapongo/mu/pkg/ast"
|
|
|
|
"github.com/marapongo/mu/pkg/diag"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Visitor unifies all visitation patterns under a single interface.
|
|
|
|
type Visitor interface {
|
|
|
|
Phase
|
|
|
|
VisitMetadata(doc *diag.Document, kind string, meta *ast.Metadata)
|
|
|
|
VisitStack(doc *diag.Document, stack *ast.Stack)
|
|
|
|
VisitParameter(doc *diag.Document, name string, param *ast.Parameter)
|
2016-11-16 22:11:58 +01:00
|
|
|
VisitDependency(doc *diag.Document, name ast.Name, dep *ast.Dependency)
|
2016-11-17 02:30:03 +01:00
|
|
|
VisitServices(doc *diag.Document, svcs *ast.Services)
|
|
|
|
VisitService(doc *diag.Document, name ast.Name, public bool, svc *ast.Service)
|
Add cloud target and architecture detection
This change implements most of the cloud target and architecture detection
logic, along with associated verification and a bunch of new error messages.
There are two settings for picking a cloud destination:
* Architecture: this specifies the combination of cloud (e.g., AWS, GCP, etc)
plus scheduler (e.g., none, Swarm, ECS, etc).
* Target: a named, preconfigured entity that includes both an Architecture and
an assortment of extra default configuration options.
The general idea here is that you can preconfigure a set of Targets for
named environments like "prod", "stage", etc. Those can either exist in a
single Mufile, or the Mucluster file if they are shared amongst multiple
Mufiles. This can be specified at the command line as such:
$ mu build --target=stage
Furthermore, a given environment may be annointed the default, so that
$ mu build
selects that environment without needing to say so explicitly.
It is also possible to specify an architecture at the command line for
scenarios where you aren't intending to target an existing named environment.
This is good for "anonymous" testing scenarios or even just running locally:
$ mu build --arch=aws
$ mu build --arch=aws:ecs
$ mu build --arch=local:kubernetes
$ .. and so on ..
This change does little more than plumb these settings around, verify them,
etc., however it sets us up to actually start dispating to the right backend.
2016-11-17 19:30:37 +01:00
|
|
|
VisitTarget(doc *diag.Document, name string, target *ast.Target)
|
2016-11-16 20:09:45 +01:00
|
|
|
}
|
2016-11-16 20:45:41 +01:00
|
|
|
|
|
|
|
// NewInOrderVisitor wraps another Visitor and walks the tree in a deterministic order, deferring to another set of
|
|
|
|
// Visitor objects for pre- and/or post-actions. Either pre or post may be nil.
|
|
|
|
func NewInOrderVisitor(pre Visitor, post Visitor) Visitor {
|
|
|
|
return &inOrderVisitor{pre, post}
|
|
|
|
}
|
|
|
|
|
Add cloud target and architecture detection
This change implements most of the cloud target and architecture detection
logic, along with associated verification and a bunch of new error messages.
There are two settings for picking a cloud destination:
* Architecture: this specifies the combination of cloud (e.g., AWS, GCP, etc)
plus scheduler (e.g., none, Swarm, ECS, etc).
* Target: a named, preconfigured entity that includes both an Architecture and
an assortment of extra default configuration options.
The general idea here is that you can preconfigure a set of Targets for
named environments like "prod", "stage", etc. Those can either exist in a
single Mufile, or the Mucluster file if they are shared amongst multiple
Mufiles. This can be specified at the command line as such:
$ mu build --target=stage
Furthermore, a given environment may be annointed the default, so that
$ mu build
selects that environment without needing to say so explicitly.
It is also possible to specify an architecture at the command line for
scenarios where you aren't intending to target an existing named environment.
This is good for "anonymous" testing scenarios or even just running locally:
$ mu build --arch=aws
$ mu build --arch=aws:ecs
$ mu build --arch=local:kubernetes
$ .. and so on ..
This change does little more than plumb these settings around, verify them,
etc., however it sets us up to actually start dispating to the right backend.
2016-11-17 19:30:37 +01:00
|
|
|
// inOrderVisitor simply implements the Visitor pattern as specified above.
|
|
|
|
//
|
|
|
|
// Note that we need to iterate all maps in a stable order (since Go's are unordered by default). Sadly, this
|
|
|
|
// is rather verbose due to Go's lack of generics, reflectionless Keys() functions, and so on.
|
2016-11-16 20:45:41 +01:00
|
|
|
type inOrderVisitor struct {
|
|
|
|
pre Visitor
|
|
|
|
post Visitor
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *inOrderVisitor) Diag() diag.Sink {
|
|
|
|
if v.pre != nil {
|
|
|
|
return v.pre.Diag()
|
|
|
|
}
|
|
|
|
if v.post != nil {
|
|
|
|
return v.post.Diag()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *inOrderVisitor) VisitMetadata(doc *diag.Document, kind string, meta *ast.Metadata) {
|
|
|
|
if v.pre != nil {
|
|
|
|
v.pre.VisitMetadata(doc, kind, meta)
|
|
|
|
}
|
Add cloud target and architecture detection
This change implements most of the cloud target and architecture detection
logic, along with associated verification and a bunch of new error messages.
There are two settings for picking a cloud destination:
* Architecture: this specifies the combination of cloud (e.g., AWS, GCP, etc)
plus scheduler (e.g., none, Swarm, ECS, etc).
* Target: a named, preconfigured entity that includes both an Architecture and
an assortment of extra default configuration options.
The general idea here is that you can preconfigure a set of Targets for
named environments like "prod", "stage", etc. Those can either exist in a
single Mufile, or the Mucluster file if they are shared amongst multiple
Mufiles. This can be specified at the command line as such:
$ mu build --target=stage
Furthermore, a given environment may be annointed the default, so that
$ mu build
selects that environment without needing to say so explicitly.
It is also possible to specify an architecture at the command line for
scenarios where you aren't intending to target an existing named environment.
This is good for "anonymous" testing scenarios or even just running locally:
$ mu build --arch=aws
$ mu build --arch=aws:ecs
$ mu build --arch=local:kubernetes
$ .. and so on ..
This change does little more than plumb these settings around, verify them,
etc., however it sets us up to actually start dispating to the right backend.
2016-11-17 19:30:37 +01:00
|
|
|
|
2016-11-19 00:57:07 +01:00
|
|
|
for _, name := range ast.StableTargets(meta.Targets) {
|
Add cloud target and architecture detection
This change implements most of the cloud target and architecture detection
logic, along with associated verification and a bunch of new error messages.
There are two settings for picking a cloud destination:
* Architecture: this specifies the combination of cloud (e.g., AWS, GCP, etc)
plus scheduler (e.g., none, Swarm, ECS, etc).
* Target: a named, preconfigured entity that includes both an Architecture and
an assortment of extra default configuration options.
The general idea here is that you can preconfigure a set of Targets for
named environments like "prod", "stage", etc. Those can either exist in a
single Mufile, or the Mucluster file if they are shared amongst multiple
Mufiles. This can be specified at the command line as such:
$ mu build --target=stage
Furthermore, a given environment may be annointed the default, so that
$ mu build
selects that environment without needing to say so explicitly.
It is also possible to specify an architecture at the command line for
scenarios where you aren't intending to target an existing named environment.
This is good for "anonymous" testing scenarios or even just running locally:
$ mu build --arch=aws
$ mu build --arch=aws:ecs
$ mu build --arch=local:kubernetes
$ .. and so on ..
This change does little more than plumb these settings around, verify them,
etc., however it sets us up to actually start dispating to the right backend.
2016-11-17 19:30:37 +01:00
|
|
|
target := meta.Targets[name]
|
|
|
|
v.VisitTarget(doc, name, &target)
|
|
|
|
// Copy the targeteter back in case it was updated.
|
|
|
|
meta.Targets[name] = target
|
|
|
|
}
|
|
|
|
|
2016-11-16 20:45:41 +01:00
|
|
|
if v.post != nil {
|
|
|
|
v.post.VisitMetadata(doc, kind, meta)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *inOrderVisitor) VisitStack(doc *diag.Document, stack *ast.Stack) {
|
|
|
|
if v.pre != nil {
|
|
|
|
v.pre.VisitStack(doc, stack)
|
|
|
|
}
|
|
|
|
|
|
|
|
v.VisitMetadata(doc, "Stack", &stack.Metadata)
|
|
|
|
|
2016-11-19 00:57:07 +01:00
|
|
|
for _, name := range ast.StableParameters(stack.Parameters) {
|
2016-11-17 02:00:58 +01:00
|
|
|
param := stack.Parameters[name]
|
|
|
|
v.VisitParameter(doc, name, ¶m)
|
|
|
|
// Copy the parameter back in case it was updated.
|
|
|
|
stack.Parameters[name] = param
|
2016-11-16 20:45:41 +01:00
|
|
|
}
|
|
|
|
|
2016-11-19 00:57:07 +01:00
|
|
|
for _, name := range ast.StableDependencies(stack.Dependencies) {
|
2016-11-16 22:11:58 +01:00
|
|
|
aname := ast.Name(name)
|
|
|
|
dep := stack.Dependencies[aname]
|
|
|
|
v.VisitDependency(doc, aname, &dep)
|
2016-11-17 02:00:58 +01:00
|
|
|
// Copy the dependency back in case it was updated.
|
|
|
|
stack.Dependencies[aname] = dep
|
2016-11-16 20:45:41 +01:00
|
|
|
}
|
|
|
|
|
2016-11-17 02:30:03 +01:00
|
|
|
v.VisitServices(doc, &stack.Services)
|
|
|
|
|
2016-11-16 20:45:41 +01:00
|
|
|
if v.post != nil {
|
|
|
|
v.post.VisitStack(doc, stack)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *inOrderVisitor) VisitParameter(doc *diag.Document, name string, param *ast.Parameter) {
|
|
|
|
if v.pre != nil {
|
|
|
|
v.pre.VisitParameter(doc, name, param)
|
|
|
|
}
|
|
|
|
if v.post != nil {
|
|
|
|
v.post.VisitParameter(doc, name, param)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-17 02:30:03 +01:00
|
|
|
func (v *inOrderVisitor) VisitDependency(doc *diag.Document, name ast.Name, dep *ast.Dependency) {
|
2016-11-16 20:45:41 +01:00
|
|
|
if v.pre != nil {
|
2016-11-17 02:30:03 +01:00
|
|
|
v.pre.VisitDependency(doc, name, dep)
|
2016-11-16 20:45:41 +01:00
|
|
|
}
|
|
|
|
if v.post != nil {
|
2016-11-17 02:30:03 +01:00
|
|
|
v.post.VisitDependency(doc, name, dep)
|
2016-11-16 20:45:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-17 02:30:03 +01:00
|
|
|
func (v *inOrderVisitor) VisitServices(doc *diag.Document, svcs *ast.Services) {
|
Retain unrecognized service properties
During unmarshaling, the default behavior of the stock Golang JSON marshaler,
and consequently the YAML one we used which mimics its behavior, is to toss away
unrecognized properties. This isn't what we want for two reasons:
First, we want to issue errors/warnings on unrecognized fields to aid in diagnostics;
we will set aside some extensible section for 3rd parties to use. This is not
addressed in this change, however.
Second, and more pertinent, is that we need to retain unrecognized fields for certain
types like services, which are extensible by default.
Until golang/go#6213 is addressed -- imminent, it seems -- we will have to do a
somewhat hacky workaround to this problem. This change contains what I consider to
be the "least bad" in that we won't introduce a lot of performance overhead, and
just have to deal with the slight annoyance of the ast.Services node type containing
both Public/Private *and* PublicUntyped/PrivateUntyped fields alongside one another.
The marshaler dumps property bags into the *Untyped fields, and the parsetree analyzer
expands them out into a structured ast.Service type. Subsequent passes can then
ignore the *Untyped fields altogether.
Note that this would cause some marshaling funkiness if we ever wanted to remarshal
the mutated ASTs back into JSON/YAML. Since we don't do that right now, however, I've
not made any attempt to keep the two pairs in synch. Post-parsetree analyzer, we
literally just forget about the *Untyped guys.
2016-11-19 18:01:23 +01:00
|
|
|
if v.pre != nil {
|
|
|
|
v.pre.VisitServices(doc, svcs)
|
|
|
|
}
|
|
|
|
|
2016-11-19 00:57:07 +01:00
|
|
|
for _, name := range ast.StableServices(svcs.Public) {
|
2016-11-17 02:30:03 +01:00
|
|
|
aname := ast.Name(name)
|
|
|
|
public := svcs.Public[aname]
|
|
|
|
v.VisitService(doc, aname, true, &public)
|
|
|
|
// Copy the public service back in case it was updated.
|
|
|
|
svcs.Public[aname] = public
|
|
|
|
}
|
|
|
|
|
2016-11-19 00:57:07 +01:00
|
|
|
for _, name := range ast.StableServices(svcs.Private) {
|
2016-11-17 02:30:03 +01:00
|
|
|
aname := ast.Name(name)
|
|
|
|
private := svcs.Private[aname]
|
|
|
|
v.VisitService(doc, aname, false, &private)
|
|
|
|
// Copy the private service back in case it was updated.
|
|
|
|
svcs.Private[aname] = private
|
|
|
|
}
|
Retain unrecognized service properties
During unmarshaling, the default behavior of the stock Golang JSON marshaler,
and consequently the YAML one we used which mimics its behavior, is to toss away
unrecognized properties. This isn't what we want for two reasons:
First, we want to issue errors/warnings on unrecognized fields to aid in diagnostics;
we will set aside some extensible section for 3rd parties to use. This is not
addressed in this change, however.
Second, and more pertinent, is that we need to retain unrecognized fields for certain
types like services, which are extensible by default.
Until golang/go#6213 is addressed -- imminent, it seems -- we will have to do a
somewhat hacky workaround to this problem. This change contains what I consider to
be the "least bad" in that we won't introduce a lot of performance overhead, and
just have to deal with the slight annoyance of the ast.Services node type containing
both Public/Private *and* PublicUntyped/PrivateUntyped fields alongside one another.
The marshaler dumps property bags into the *Untyped fields, and the parsetree analyzer
expands them out into a structured ast.Service type. Subsequent passes can then
ignore the *Untyped fields altogether.
Note that this would cause some marshaling funkiness if we ever wanted to remarshal
the mutated ASTs back into JSON/YAML. Since we don't do that right now, however, I've
not made any attempt to keep the two pairs in synch. Post-parsetree analyzer, we
literally just forget about the *Untyped guys.
2016-11-19 18:01:23 +01:00
|
|
|
|
|
|
|
if v.post != nil {
|
|
|
|
v.post.VisitServices(doc, svcs)
|
|
|
|
}
|
2016-11-17 02:30:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (v *inOrderVisitor) VisitService(doc *diag.Document, name ast.Name, public bool, svc *ast.Service) {
|
2016-11-16 20:45:41 +01:00
|
|
|
if v.pre != nil {
|
2016-11-17 02:30:03 +01:00
|
|
|
v.pre.VisitService(doc, name, public, svc)
|
2016-11-16 20:45:41 +01:00
|
|
|
}
|
|
|
|
if v.post != nil {
|
2016-11-17 02:30:03 +01:00
|
|
|
v.post.VisitService(doc, name, public, svc)
|
2016-11-16 20:45:41 +01:00
|
|
|
}
|
|
|
|
}
|
Add cloud target and architecture detection
This change implements most of the cloud target and architecture detection
logic, along with associated verification and a bunch of new error messages.
There are two settings for picking a cloud destination:
* Architecture: this specifies the combination of cloud (e.g., AWS, GCP, etc)
plus scheduler (e.g., none, Swarm, ECS, etc).
* Target: a named, preconfigured entity that includes both an Architecture and
an assortment of extra default configuration options.
The general idea here is that you can preconfigure a set of Targets for
named environments like "prod", "stage", etc. Those can either exist in a
single Mufile, or the Mucluster file if they are shared amongst multiple
Mufiles. This can be specified at the command line as such:
$ mu build --target=stage
Furthermore, a given environment may be annointed the default, so that
$ mu build
selects that environment without needing to say so explicitly.
It is also possible to specify an architecture at the command line for
scenarios where you aren't intending to target an existing named environment.
This is good for "anonymous" testing scenarios or even just running locally:
$ mu build --arch=aws
$ mu build --arch=aws:ecs
$ mu build --arch=local:kubernetes
$ .. and so on ..
This change does little more than plumb these settings around, verify them,
etc., however it sets us up to actually start dispating to the right backend.
2016-11-17 19:30:37 +01:00
|
|
|
|
|
|
|
func (v *inOrderVisitor) VisitTarget(doc *diag.Document, name string, target *ast.Target) {
|
|
|
|
if v.pre != nil {
|
|
|
|
v.pre.VisitTarget(doc, name, target)
|
|
|
|
}
|
|
|
|
if v.post != nil {
|
|
|
|
v.post.VisitTarget(doc, name, target)
|
|
|
|
}
|
|
|
|
}
|