grim/convey

closing merged branch
expand-list
2017-10-03, Gary Kramlich
345a52ef04c6
closing merged branch
/*
* 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 convey
import (
"fmt"
"sort"
"strings"
"github.com/go-yaml/yaml"
toposort "github.com/philopon/go-toposort"
cConfig "bitbucket.org/rw_grim/convey/config"
"bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/plans"
"bitbucket.org/rw_grim/convey/state"
"bitbucket.org/rw_grim/convey/tasks"
cYaml "bitbucket.org/rw_grim/convey/yaml"
)
type options struct {
DefaultPlan string `yaml:"default-plan"`
SSHIdentities cYaml.StringOrSlice `yaml:"ssh-identities"`
}
type config struct {
Extends string `yaml:"extends"`
Tasks map[string]yaml.MapSlice `yaml:"tasks"`
Plans map[string]plans.Plan `yaml:"plans"`
MetaPlans map[string]plans.MetaPlan `yaml:"meta-plans"`
Environment cYaml.StringOrSlice `yaml:"environment"`
Options options `yaml:"options"`
}
type override struct {
Environment cYaml.StringOrSlice `yaml:"environment"`
Options options `yaml:"options"`
}
type Loader struct {
defaultPlan string
depth int
fileLoader func(string, *Loader) (*cConfig.Config, error)
}
func (c *Loader) Load(path, base string, data []byte) (*cConfig.Config, error) {
// load the raw config
cfg := config{}
err := yaml.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
// see if we're extending something - partially load a base
// config object if so that we'll modify; otherwise use an
// empty base config object that we'll set in a similar way
var baseConfig *cConfig.Config
if cfg.Extends != "" {
if c.depth >= MaxExtendsDepth {
return nil, ErrMaxExtendsDepth
}
c.depth++
baseConfig, err = c.loadFile(cfg.Extends)
c.depth--
// We can safely ignore no plans and no tasks errors here
// as we're ensured to also get a valid base config back.
// This is a bit of a strange idiom, but is still used in
// places like io.Reader (return non-zero n on error).
if err != nil && err != ErrNoPlans && err != ErrNoTasks {
return nil, err
}
} else {
baseConfig = &cConfig.Config{
Tasks: map[string]tasks.Task{},
Plans: map[string]plans.Plan{},
MetaPlans: map[string]plans.MetaPlan{},
SSHIdentities: []string{},
}
}
// turn the raw tasks into real tasks
realTasks, err := loadTasks(path, cfg.Tasks)
if err != nil {
return nil, err
}
// iterate through each plan and do any cleanup we need to
for _, plan := range cfg.Plans {
// set stage names for any that are missing them
for idx := range plan.Stages {
if plan.Stages[idx].Name == "" {
plan.Stages[idx].Name = fmt.Sprintf("stage-%d", idx)
}
}
}
// store the default plan in the loader
c.defaultPlan = cfg.Options.DefaultPlan
// tasks, plans, and metaplans are all name => * maps, and
// if we're extending something we want to inherit all of these
// EXCEPT for things that we are explicitly overriding by using
// the same name. Do a shallow merge here
for name, task := range realTasks {
baseConfig.Tasks[name] = task
}
// If the plan has the merge attribute set, try to overwrite
// stages of a plan that is already declared with the same name.
// If no such plan exists, raise an error (you're trying to
// overwrite a stage of nothing, which is almost certainly an error.
for name, plan := range cfg.Plans {
if plan.Merge {
base, ok := baseConfig.Plans[name]
if !ok {
return nil, fmt.Errorf("cannot merge with unknown plan '%s'", name)
}
base, err := mergePlan(base, plan, name)
if err != nil {
return nil, err
}
baseConfig.Plans[name] = base
} else {
baseConfig.Plans[name] = plan
}
}
for name, metaPlan := range cfg.MetaPlans {
baseConfig.MetaPlans[name] = metaPlan
}
baseConfig.Environment = environment.Merge(baseConfig.Environment, cfg.Environment)
// Check if the default plan is being overridden
if cfg.Options.DefaultPlan != "" {
c.defaultPlan = cfg.Options.DefaultPlan
}
// don't clobber ssh-identities with an empty list
if len(cfg.Options.SSHIdentities) > 0 {
baseConfig.SSHIdentities = cfg.Options.SSHIdentities
}
// Now that we have all of the tasks defined up to this point, any task
// that needs a reference to another task via the ResolvableTask interface
// is passed the entire map. We need to make sure that tasks are resolved
// in the correct order. This is a problem, for example, if the extend
// intrinsic clones a task that is not fully resolved.
if err := c.resolve(baseConfig); err != nil {
return nil, err
}
// Return the base config at this point, but maybe possibly return an
// error that we're missing something in order to proceed. This gets
// kind of weird: if we're calling Load recursively through LoadFile,
// as is the case with extending a config file, we're going to ignore
// these errors but need the actual config (we may define tasks and
// plans in an extending config).
if len(baseConfig.Tasks) == 0 {
return baseConfig, ErrNoTasks
}
if len(baseConfig.Plans) == 0 {
return baseConfig, ErrNoPlans
}
return baseConfig, nil
}
func (c *Loader) resolve(baseConfig *cConfig.Config) error {
// Extract resolvable tasks from config tasks
resolvables := map[string]tasks.ResolvableTask{}
for name, task := range baseConfig.Tasks {
if rc, ok := task.(tasks.ResolvableTask); ok {
resolvables[name] = rc
}
}
graph := toposort.NewGraph(len(resolvables))
// Add all nodes to graph before edges, as it won't count
// the in/out degree correctly if we add an edge before one
// of the terminating nodes.
for name, rc := range resolvables {
graph.AddNode(name)
for _, dependency := range rc.Dependencies() {
if _, ok := baseConfig.Tasks[name]; !ok {
return fmt.Errorf("Unknown task dependency '%s'", dependency)
}
graph.AddNode(dependency)
}
}
// Add all edges
for name, rc := range resolvables {
for _, dependency := range rc.Dependencies() {
graph.AddEdge(dependency, name)
}
}
// Sort and resolve all tasks
order, ok := graph.Toposort()
if !ok {
for _, name := range order {
delete(resolvables, name)
}
remaining := []string{}
for k := range resolvables {
remaining = append(remaining, k)
}
sort.Strings(remaining)
return fmt.Errorf("The following tasks are part of a dependency cycle: %s", strings.Join(remaining, ", "))
}
for _, name := range order {
rc, ok := resolvables[name]
if !ok {
continue
}
if err := rc.Resolve(baseConfig.Tasks); err != nil {
return fmt.Errorf("%s: %s", name, err.Error())
}
}
return nil
}
func (c *Loader) loadFile(path string) (*cConfig.Config, error) {
if c.fileLoader == nil {
return cConfig.LoadFile(path, c)
}
return c.fileLoader(path, c)
}
func (c *Loader) LoadOverride(path, base string, data []byte, config *cConfig.Config) {
var overrideData override
err := yaml.Unmarshal(data, &overrideData)
if err != nil {
return
}
config.Environment = environment.Merge(config.Environment, overrideData.Environment)
// Check if the default plan is being overridden
if overrideData.Options.DefaultPlan != "" {
c.defaultPlan = overrideData.Options.DefaultPlan
}
// if there are ssh-identities in the override they need to replace the
// ones in the normal config file.
if len(overrideData.Options.SSHIdentities) > 0 {
config.SSHIdentities = overrideData.Options.SSHIdentities
}
}
func (l *Loader) Filenames() []string {
return []string{"convey.yml", "convey.yaml"}
}
func (l *Loader) OverrideSuffix() string {
return "-override"
}
func (l *Loader) DefaultPlan() string {
if l.defaultPlan != "" {
return l.defaultPlan
}
return "default"
}
func (l *Loader) ResolvePlanName(plan string, cfg *cConfig.Config, st *state.State) string {
return plan
}