grim/convey

Bump the version for release
v0.14.0-alpha3
2018-02-20, Gary Kramlich
166a6d1979fa
Bump the version for release
// Convey
// Copyright 2016-2018 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 (
"bytes"
"fmt"
"regexp"
"text/template"
"bitbucket.org/rw_grim/convey/color"
"bitbucket.org/rw_grim/convey/config"
"bitbucket.org/rw_grim/convey/logging"
"bitbucket.org/rw_grim/convey/plans"
"bitbucket.org/rw_grim/convey/state"
)
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}}
# 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}}"{{if .Always}} style="bold,filled,rounded"{{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}}" style="dashed,bold"]
{{.Normalized}}_start -> {{range .Plans}}{{.Normalized}}_start
{{.Normalized}}_finish -> {{end}} {{.Normalized}}_finish
{{end}}
}
`
var normalizeRegex *regexp.Regexp
type (
// Graphviz is a Runner that will create a graphviz output for the config
// that has been loaded.
Graphviz struct{}
graphvizTask struct {
Normalized string
Name string
}
graphvizStage struct {
Normalized string
Name string
Enabled bool
Concurrent bool
Always bool
Tasks []string
FallThroughs []graphvizStage
}
graphvizPlan struct {
Normalized string
Name string
Color string
Stages []graphvizStage
}
graphvizMetaPlan struct {
Normalized string
Name string
Color string
Plans []graphvizPlan
}
)
func normalize(prefix, str string) string {
if normalizeRegex == nil {
normalizeRegex = regexp.MustCompile("[^a-zA-Z0-9_]")
}
return prefix + "_" + normalizeRegex.ReplaceAllString(str, "_")
}
func (g *Graphviz) getTasks(cfg *config.Config) []graphvizTask {
tasks := []graphvizTask{}
for name := range cfg.Tasks {
task := graphvizTask{
Normalized: normalize("task", name),
Name: name,
}
tasks = append(tasks, task)
}
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 {
gvizPlan := graphvizPlan{
Normalized: normalize("plan", name),
Name: name,
Color: color.X11(name),
Stages: []graphvizStage{},
}
for idx, stage := range plan.Stages {
gvizStage := 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 {
gvizStage.Tasks = append(gvizStage.Tasks, normalize("task", task))
}
gvizPlan.Stages = append(gvizPlan.Stages, gvizStage)
// if this always runs, if it's not the first add it to the previous stages' fallthroughs
if stage.Always && idx > 0 {
for i := 0; i < idx; i++ {
gvizPlan.Stages[i].FallThroughs = append(gvizPlan.Stages[idx-1].FallThroughs, gvizStage)
}
}
}
return gvizPlan
}
func (g *Graphviz) getPlans(cfg *config.Config) []graphvizPlan {
plans := []graphvizPlan{}
for name, plan := range cfg.Plans {
plans = append(plans, g.plan(name, plan))
}
return plans
}
// Run runs the graphviz runner.
func (g *Graphviz) Run(cfg *config.Config, plans []string, env []string, st *state.State) int {
logger := logging.NewAdapter("graphviz")
params := map[string]interface{}{
"Tasks": g.getTasks(cfg),
"Plans": g.getPlans(cfg),
"MetaPlans": g.getMetaPlans(cfg),
}
// Load the template
tmpl, err := template.New("graphviz").Parse(graphvizTemplate)
if err != nil {
if nErr := logger.Fatalf("error: %s", err); nErr != nil {
fmt.Printf("error reporting error: %s\n", nErr)
}
return 1
}
// Run the template
output := new(bytes.Buffer)
err = tmpl.Execute(output, params)
if err != nil {
if nErr := logger.Fatalf("error: %s", err); nErr != nil {
fmt.Printf("error reporting error: %s\n", nErr)
}
return 1
}
// Output the template
fmt.Print(output)
return 0
}