changeset 304:9de876d04a05

Add support for meta-plans. closes #82
author Gary Kramlich <grim@reaperworld.com>
date Mon, 24 Apr 2017 00:15:09 -0500
parents d3b0c59359d7
children cfa2230015f6
files ChangeLog README.md config/config.go config/convey.go convey.yml main.go plans/metaplans.go runners/graphviz.go runners/listmetaplans.go
diffstat 9 files changed, 247 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Thu Apr 20 18:41:01 2017 -0500
+++ b/ChangeLog	Mon Apr 24 00:15:09 2017 -0500
@@ -1,6 +1,7 @@
 0.5.0:
   * Added support for disabling stages (fixes #33)
   * Added support for aliasing the default plan (fixes #56)
+  * Added support for metaplans (fixes #82)
   * Updated the graphviz colors by removing the yellow colors and added a
     bunch more colors.  (Fixes #77)
 
--- a/README.md	Thu Apr 20 18:41:01 2017 -0500
+++ b/README.md	Mon Apr 24 00:15:09 2017 -0500
@@ -39,6 +39,7 @@
       -S, --force-sequential       Don't run anything concurrently
       -g, --graphviz               Output a graphviz diagram of the config file
       -P, --list-plans             List the plans that are available
+      -M, --list-meta-plans        List the meta plans that are available
       -L, --list-tasks             List the supported tasks
       -m, --memory=MEMORY          The ammount of memor to give the run task
           --ssh-agent              Enabled ssh-agent support for tasks
@@ -84,7 +85,7 @@
 
 # Configuration
 
-Configuration is done via a file named `convey.yaml`.  This file defines the tasks as well as the plans.  There are four top level-items: default-plan, environment, plans, and tasks.
+Configuration is done via a file named `convey.yaml`.  This file defines the tasks as well as the plans.  There are five top level-items: default-plan, environment, meta-plans, plans, and tasks.
 
 `default-plan` is the name of the plan that should be ran by default.
 
@@ -376,7 +377,7 @@
 
 ### Plan
 
-Plans themselves are pretty straight forward.  They group a `stages` with an optional `environment`.
+Plans themselves are pretty straight forward.  They group `stages` with an optional `environment`.
 
 ### Attributes
 
@@ -404,6 +405,35 @@
 
 ----
 
+## Meta Plans
+
+Meta plans are used to group plans together and run them in serial.  This is also possible via the command line by specifying multiple plans to run.
+
+There is currently no support for nesting meta plans.
+
+### Example
+
+    meta-plans:
+      world:
+        plans:
+          - plan1
+          - plan2
+          - plan3
+
+----
+
+### Meta Plan
+
+Meta plans contain a single attribute which is the list of `plans` to run. 
+
+### Attributes
+
+| Name        | Required | Description |
+| ----------- | -------- | ----------- |
+| plans | Yes         | A list of `plans` to run in the order they should be run in. |
+
+----
+
 # Config Loaders
 
 Convey supports multiple different types of configs that it can load.  Currently supported loaders are convey and bitbucket.\
--- a/config/config.go	Thu Apr 20 18:41:01 2017 -0500
+++ b/config/config.go	Mon Apr 24 00:15:09 2017 -0500
@@ -25,5 +25,6 @@
 type Config struct {
 	Tasks       map[string]tasks.Task
 	Plans       map[string]plans.Plan
+	MetaPlans   map[string]plans.MetaPlan
 	Environment []string
 }
--- a/config/convey.go	Thu Apr 20 18:41:01 2017 -0500
+++ b/config/convey.go	Mon Apr 24 00:15:09 2017 -0500
@@ -28,10 +28,11 @@
 )
 
 type conveyConfig struct {
-	DefaultPlan string                   `yaml:"default-plan"`
-	Tasks       map[string]yaml.MapSlice `yaml:"tasks"`
-	Plans       map[string]plans.Plan    `yaml:"plans"`
-	Environment []string                 `yaml:"environment"`
+	DefaultPlan string                    `yaml:"default-plan"`
+	Tasks       map[string]yaml.MapSlice  `yaml:"tasks"`
+	Plans       map[string]plans.Plan     `yaml:"plans"`
+	MetaPlans   map[string]plans.MetaPlan `yaml:"meta-plans"`
+	Environment []string                  `yaml:"environment"`
 }
 
 type conveyOverride struct {
@@ -81,6 +82,7 @@
 	realConfig := &Config{
 		Environment: config.Environment,
 		Plans:       config.Plans,
+		MetaPlans:   config.MetaPlans,
 		Tasks:       realTasks,
 	}
 
--- a/convey.yml	Thu Apr 20 18:41:01 2017 -0500
+++ b/convey.yml	Mon Apr 24 00:15:09 2017 -0500
@@ -200,3 +200,12 @@
     stages:
       - name: clean
         tasks: [clean]
+meta-plans:
+  everything:
+    plans:
+      - images
+      - default
+  release:
+    plans:
+      - default
+      - deploy
--- a/main.go	Thu Apr 20 18:41:01 2017 -0500
+++ b/main.go	Mon Apr 24 00:15:09 2017 -0500
@@ -48,6 +48,7 @@
 	graphviz        = app.Flag("graphviz", "Output a graphviz diagram of the config file").Short('g').Default("False").Bool()
 	keep            = app.Flag("keep", "Keep the workspace volume").Short('k').Hidden().Default("False").Bool()
 	listTasks       = app.Flag("list-tasks", "List the supported tasks").Short('L').Default("false").Bool()
+	listMetaPlans   = app.Flag("list-meta-plans", "List the meta plans that are available").Short('M').Default("false").Bool()
 	listPlans       = app.Flag("list-plans", "List the plans that are available").Short('P').Default("false").Bool()
 	memory          = app.Flag("memory", "The ammount of memor to give the run task").Short('m').String()
 	showConfig      = app.Flag("show-config", "Show a dump of the config file").Short('C').Hidden().Default("false").Bool()
@@ -117,6 +118,8 @@
 	var runner runners.Runner
 	if *graphviz {
 		runner = &runners.Graphviz{}
+	} else if *listMetaPlans {
+		runner = &runners.ListMetaPlans{}
 	} else if *listPlans {
 		runner = &runners.ListPlans{}
 	} else if *listTasks {
@@ -130,7 +133,11 @@
 	// resolve the plan name with the load and options
 	realPlans := []string{}
 	for _, planName := range *planNames {
-		realPlans = append(realPlans, loader.ResolvePlanName(planName, cfg, opts))
+		if metaPlan, found := cfg.MetaPlans[planName]; found {
+			realPlans = append(realPlans, metaPlan.Plans...)
+		} else {
+			realPlans = append(realPlans, loader.ResolvePlanName(planName, cfg, opts))
+		}
 	}
 
 	if len(realPlans) == 0 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plans/metaplans.go	Mon Apr 24 00:15:09 2017 -0500
@@ -0,0 +1,43 @@
+/*
+ * Convey
+ * Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package plans
+
+import (
+	"fmt"
+)
+
+type MetaPlan struct {
+	Plans []string `yaml:"plans"`
+}
+
+func (m *MetaPlan) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type rawMetaPlan MetaPlan
+	raw := rawMetaPlan{}
+
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	if len(raw.Plans) == 0 {
+		return fmt.Errorf("no plans specified")
+	}
+
+	*m = MetaPlan(raw)
+
+	return nil
+}
--- a/runners/graphviz.go	Thu Apr 20 18:41:01 2017 -0500
+++ b/runners/graphviz.go	Mon Apr 24 00:15:09 2017 -0500
@@ -27,24 +27,52 @@
 	"bitbucket.org/rw_grim/convey/config"
 	"bitbucket.org/rw_grim/convey/logging"
 	"bitbucket.org/rw_grim/convey/options"
+	"bitbucket.org/rw_grim/convey/plans"
 )
 
 var graphvizTemplate = `
 digraph {
 	graph[rankdir="LR"]
+
+	node[style="filled,rounded" shape="rect"]
+
+	# add the meta plans
+	node[shape="rect" fillcolor="plum"]
+{{range .MetaPlans}}	{{.Normalized}}_start[label="{{.Name}} start"]
+	{{.Normalized}}_finish[label="{{.Name}} finish"]
+{{end}}
+
+	# add a start and finish node for each plan
+	node[shape="component" fillcolor="palegreen"]
+{{range .Plans}}	{{.Normalized}}_start[label="{{.Name}} start"]
+	{{.Normalized}}_finish[label="{{.Name}} finish"]
+{{end}}
+
+	# add the tasks
 	node[style="filled,rounded" shape="rect" fillcolor="powderblue"]
-
 {{range .Tasks}}	{{.Normalized}}[label="{{.Name}}"]
 {{end}}
-{{range .Plans}}	edge[color="{{.Color}}"]{{$planNormalized := .Normalized }}
-	{{.Normalized}}_start[label="{{.Name}}" fillcolor="palegreen" shape="rect"]
-	{{range .Stages}}{{$planNormalized}}_{{.Normalized}} [label="{{.Name}}" shape="octagon" fillcolor="{{if .Enabled}}lightpink1{{else}}gray{{end}}"]
-	{{end}}
-	{{.Normalized}}_start{{range .Stages}} -> {{$planNormalized}}_{{.Normalized}}{{if .Concurrent}} -> { {{range .Tasks}} {{.}} {{end}} }{{else}}{{range .Tasks}} -> {{.}}{{end}}{{end}}{{end}}
-	{{/*add the fall throughs */}}
-{{- range .Stages}}{{$stage := .}}{{range .FallThroughs}}{{$planNormalized}}_{{$stage.Normalized}} -> {{$planNormalized}}_{{.Normalized}} [style="dotted"]
+
+{{range .Plans}}
+
+	# add the stages for {{.Name}}
+	edge[color="{{.Color}}"]{{$planNormalized := .Normalized }}
+	{{- range .Stages}}
+	{{$planNormalized}}_{{.Normalized}} [label="{{.Name}}" shape="octagon" fillcolor="{{if .Enabled}}lightpink1{{else}}gray{{end}}"]{{end}}
+
+	# {{.Name}} happy path
+	{{.Normalized}}_start{{range .Stages}} -> {{$planNormalized}}_{{.Normalized}}{{if .Concurrent}} -> { {{range .Tasks}} {{.}} {{end}} }{{else}}{{range .Tasks}} -> {{.}}{{end}}{{end}}{{end}} -> {{.Normalized}}_finish
+	# {{.Name}} unhappy path
+	{{- range .Stages}}{{$stage := .}}{{range .FallThroughs}}{{$planNormalized}}_{{$stage.Normalized}} -> {{$planNormalized}}_{{.Normalized}} [style="dotted"]
 	{{end}}{{end -}}
 {{- end}}
+
+{{range .MetaPlans}}
+	# metaplan {{.Name}}
+	edge[color="{{.Color}}"]
+	{{.Normalized}}_start -> {{range .Plans}}{{.Normalized}}_start
+	{{.Normalized}}_finish -> {{end}} {{.Normalized}}_finish
+{{end}}
 }
 `
 
@@ -74,14 +102,21 @@
 		Color      string
 		Stages     []GraphvizStage
 	}
+
+	GraphvizMetaPlan struct {
+		Normalized string
+		Name       string
+		Color      string
+		Plans      []GraphvizPlan
+	}
 )
 
-func normalize(str string) string {
+func normalize(prefix, str string) string {
 	if normalizeRegex == nil {
 		normalizeRegex = regexp.MustCompile("[^a-zA-Z0-9_]")
 	}
 
-	return normalizeRegex.ReplaceAllString(str, "_")
+	return prefix + "_" + normalizeRegex.ReplaceAllString(str, "_")
 }
 
 func (g *Graphviz) getTasks(cfg *config.Config) []GraphvizTask {
@@ -89,7 +124,7 @@
 
 	for name, _ := range cfg.Tasks {
 		task := GraphvizTask{
-			Normalized: normalize(name),
+			Normalized: normalize("task", name),
 			Name:       name,
 		}
 		tasks = append(tasks, task)
@@ -98,40 +133,66 @@
 	return tasks
 }
 
+func (g *Graphviz) getMetaPlans(cfg *config.Config) []GraphvizMetaPlan {
+	metaPlans := []GraphvizMetaPlan{}
+
+	for name, cMetaPlan := range cfg.MetaPlans {
+		metaPlan := GraphvizMetaPlan{
+			Normalized: normalize("metaplan", name),
+			Name:       name,
+			Color:      color.X11(name),
+		}
+
+		for _, name := range cMetaPlan.Plans {
+			if plan, found := cfg.Plans[name]; found {
+				metaPlan.Plans = append(metaPlan.Plans, g.plan(name, plan))
+			}
+		}
+
+		metaPlans = append(metaPlans, metaPlan)
+	}
+
+	return metaPlans
+}
+
+func (g Graphviz) plan(name string, plan plans.Plan) GraphvizPlan {
+	graphvizPlan := GraphvizPlan{
+		Normalized: normalize("plan", name),
+		Name:       name,
+		Color:      color.X11(name),
+		Stages:     []GraphvizStage{},
+	}
+
+	for idx, stage := range plan.Stages {
+		graphvizStage := GraphvizStage{
+			Normalized: normalize("stage", stage.Name),
+			Name:       stage.Name,
+			Enabled:    stage.Enabled,
+			Concurrent: stage.Concurrent,
+			Always:     stage.Always,
+			Tasks:      []string{},
+		}
+
+		for _, task := range stage.Tasks {
+			graphvizStage.Tasks = append(graphvizStage.Tasks, normalize("task", task))
+		}
+
+		graphvizPlan.Stages = append(graphvizPlan.Stages, graphvizStage)
+
+		// if this always runs, if it's not the first add it to the previous stage's fallthroughs
+		if stage.Always && idx > 0 {
+			graphvizPlan.Stages[idx-1].FallThroughs = append(graphvizPlan.Stages[idx-1].FallThroughs, graphvizStage)
+		}
+	}
+
+	return graphvizPlan
+}
+
 func (g *Graphviz) getPlans(cfg *config.Config) []GraphvizPlan {
 	plans := []GraphvizPlan{}
 
 	for name, plan := range cfg.Plans {
-		graphvizPlan := GraphvizPlan{
-			Normalized: normalize(name),
-			Name:       name,
-			Color:      color.X11(name),
-			Stages:     []GraphvizStage{},
-		}
-
-		for idx, stage := range plan.Stages {
-			graphvizStage := GraphvizStage{
-				Normalized: normalize(stage.Name),
-				Name:       stage.Name,
-				Enabled:    stage.Enabled,
-				Concurrent: stage.Concurrent,
-				Always:     stage.Always,
-				Tasks:      []string{},
-			}
-
-			for _, task := range stage.Tasks {
-				graphvizStage.Tasks = append(graphvizStage.Tasks, normalize(task))
-			}
-
-			graphvizPlan.Stages = append(graphvizPlan.Stages, graphvizStage)
-
-			// if this always runs, if it's not the first add it to the previous stage's fallthroughs
-			if stage.Always && idx > 0 {
-				graphvizPlan.Stages[idx-1].FallThroughs = append(graphvizPlan.Stages[idx-1].FallThroughs, graphvizStage)
-			}
-		}
-
-		plans = append(plans, graphvizPlan)
+		plans = append(plans, g.plan(name, plan))
 	}
 
 	return plans
@@ -141,8 +202,9 @@
 	logger := logging.NewAdapter("graphviz")
 
 	params := map[string]interface{}{
-		"Tasks": g.getTasks(cfg),
-		"Plans": g.getPlans(cfg),
+		"Tasks":     g.getTasks(cfg),
+		"Plans":     g.getPlans(cfg),
+		"MetaPlans": g.getMetaPlans(cfg),
 	}
 
 	// Load the template
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/listmetaplans.go	Mon Apr 24 00:15:09 2017 -0500
@@ -0,0 +1,42 @@
+/*
+ * Convey
+ * Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package runners
+
+import (
+	"fmt"
+
+	"bitbucket.org/rw_grim/convey/config"
+	"bitbucket.org/rw_grim/convey/options"
+)
+
+type ListMetaPlans struct{}
+
+func (l *ListMetaPlans) Run(cfg *config.Config, plans []string, env []string, opts *options.Options) int {
+	for metaPlanName, metaPlan := range cfg.MetaPlans {
+		fmt.Printf("%s\n", metaPlanName)
+		if len(metaPlan.Plans) == 0 {
+			fmt.Printf("  (no plans)\n")
+		} else {
+			for _, plan := range metaPlan.Plans {
+				fmt.Printf("  %s\n", plan)
+			}
+		}
+	}
+
+	return 0
+}