grim/convey
Clone
Summary
Browse
Changes
Graph
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
}