70eeddb719
Currently we are using different `When` types for every service and convert between them. This is a good approach if we want to keep isolated all the services (like if we were using different repos for every service instead of the current monorepo). But currently, since When is identical between all the services, simplify this by using a common When type.
440 lines
11 KiB
Go
440 lines
11 KiB
Go
// Copyright 2019 Sorint.lab
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package runconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"agola.io/agola/internal/config"
|
|
"agola.io/agola/internal/util"
|
|
rstypes "agola.io/agola/services/runservice/types"
|
|
"agola.io/agola/services/types"
|
|
|
|
errors "golang.org/x/xerrors"
|
|
)
|
|
|
|
func genRuntime(c *config.Config, ce *config.Runtime, variables map[string]string) *rstypes.Runtime {
|
|
containers := []*rstypes.Container{}
|
|
for _, cc := range ce.Containers {
|
|
env := genEnv(cc.Environment, variables)
|
|
container := &rstypes.Container{
|
|
Image: cc.Image,
|
|
Environment: env,
|
|
User: cc.User,
|
|
Privileged: cc.Privileged,
|
|
Entrypoint: cc.Entrypoint,
|
|
}
|
|
|
|
containers = append(containers, container)
|
|
}
|
|
|
|
return &rstypes.Runtime{
|
|
Type: rstypes.RuntimeType(ce.Type),
|
|
Arch: ce.Arch,
|
|
Containers: containers,
|
|
}
|
|
}
|
|
|
|
func stepFromConfigStep(csi interface{}, variables map[string]string) interface{} {
|
|
switch cs := csi.(type) {
|
|
case *config.CloneStep:
|
|
// transform a "clone" step in a "run" step command
|
|
rs := &config.RunStep{}
|
|
|
|
rs.Type = "run"
|
|
rs.Name = "Clone repository and checkout code"
|
|
rs.Command = `
|
|
set -x
|
|
|
|
mkdir ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
touch ~/.ssh/id_rsa
|
|
chmod 600 ~/.ssh/id_rsa
|
|
touch ~/.ssh/known_hosts
|
|
chmod 600 ~/.ssh/known_hosts
|
|
|
|
# Add public ssh host key
|
|
if [ -n "$AGOLA_SSHHOSTKEY" ]; then
|
|
echo "$AGOLA_SSHHOSTKEY" >> ~/.ssh/known_hosts
|
|
fi
|
|
|
|
# Add repository deploy key
|
|
(cat <<EOF > ~/.ssh/id_rsa
|
|
$AGOLA_SSHPRIVKEY
|
|
EOF
|
|
)
|
|
|
|
STRICT_HOST_KEY_CHECKING="yes"
|
|
|
|
if [ -n "$AGOLA_SKIPSSHHOSTKEYCHECK" ]; then
|
|
# Disable git host key verification
|
|
STRICT_HOST_KEY_CHECKING="no"
|
|
fi
|
|
|
|
(cat <<EOF > ~/.ssh/config
|
|
Host $AGOLA_GIT_HOST
|
|
HostName $AGOLA_GIT_HOST
|
|
Port $AGOLA_GIT_PORT
|
|
StrictHostKeyChecking ${STRICT_HOST_KEY_CHECKING}
|
|
EOF
|
|
)
|
|
|
|
git clone $AGOLA_REPOSITORY_URL .
|
|
git fetch origin $AGOLA_GIT_REF
|
|
|
|
if [ -n "$AGOLA_GIT_COMMITSHA" ]; then
|
|
git checkout $AGOLA_GIT_COMMITSHA
|
|
else
|
|
git checkout FETCH_HEAD
|
|
fi
|
|
`
|
|
|
|
return rs
|
|
|
|
case *config.RunStep:
|
|
rs := &rstypes.RunStep{}
|
|
|
|
env := genEnv(cs.Environment, variables)
|
|
|
|
rs.Type = cs.Type
|
|
rs.Name = cs.Name
|
|
rs.Command = cs.Command
|
|
rs.Environment = env
|
|
rs.WorkingDir = cs.WorkingDir
|
|
rs.Shell = cs.Shell
|
|
rs.User = cs.User
|
|
return rs
|
|
|
|
case *config.SaveToWorkspaceStep:
|
|
sws := &rstypes.SaveToWorkspaceStep{}
|
|
|
|
sws.Type = cs.Type
|
|
sws.Name = cs.Name
|
|
|
|
sws.Contents = make([]rstypes.SaveContent, len(cs.Contents))
|
|
for i, csc := range cs.Contents {
|
|
sc := rstypes.SaveContent{}
|
|
sc.SourceDir = csc.SourceDir
|
|
sc.DestDir = csc.DestDir
|
|
sc.Paths = csc.Paths
|
|
|
|
sws.Contents[i] = sc
|
|
}
|
|
return sws
|
|
|
|
case *config.RestoreWorkspaceStep:
|
|
rws := &rstypes.RestoreWorkspaceStep{}
|
|
rws.Name = cs.Name
|
|
rws.Type = cs.Type
|
|
rws.DestDir = cs.DestDir
|
|
|
|
return rws
|
|
|
|
case *config.SaveCacheStep:
|
|
sws := &rstypes.SaveCacheStep{}
|
|
|
|
sws.Type = cs.Type
|
|
sws.Name = cs.Name
|
|
sws.Key = cs.Key
|
|
|
|
sws.Contents = make([]rstypes.SaveContent, len(cs.Contents))
|
|
for i, csc := range cs.Contents {
|
|
sc := rstypes.SaveContent{}
|
|
sc.SourceDir = csc.SourceDir
|
|
sc.DestDir = csc.DestDir
|
|
sc.Paths = csc.Paths
|
|
|
|
sws.Contents[i] = sc
|
|
}
|
|
return sws
|
|
|
|
case *config.RestoreCacheStep:
|
|
rws := &rstypes.RestoreCacheStep{}
|
|
rws.Name = cs.Name
|
|
rws.Type = cs.Type
|
|
rws.Keys = cs.Keys
|
|
rws.DestDir = cs.DestDir
|
|
|
|
return rws
|
|
|
|
default:
|
|
panic(fmt.Errorf("unknown config step type: %s", util.Dump(cs)))
|
|
}
|
|
}
|
|
|
|
// GenRunConfigTasks generates a run config tasks from a run in the config, expanding all the references to tasks
|
|
// this functions assumes that the config is already checked for possible errors (i.e referenced task must exits)
|
|
func GenRunConfigTasks(uuid util.UUIDGenerator, c *config.Config, runName string, variables map[string]string, branch, tag, ref string) map[string]*rstypes.RunConfigTask {
|
|
cr := c.Run(runName)
|
|
|
|
rcts := map[string]*rstypes.RunConfigTask{}
|
|
|
|
for _, ct := range cr.Tasks {
|
|
include := types.MatchWhen(ct.When.ToWhen(), branch, tag, ref)
|
|
|
|
steps := make(rstypes.Steps, len(ct.Steps))
|
|
for i, cpts := range ct.Steps {
|
|
steps[i] = stepFromConfigStep(cpts, variables)
|
|
}
|
|
|
|
tEnv := genEnv(ct.Environment, variables)
|
|
|
|
t := &rstypes.RunConfigTask{
|
|
ID: uuid.New(ct.Name).String(),
|
|
Name: ct.Name,
|
|
Runtime: genRuntime(c, ct.Runtime, variables),
|
|
Environment: tEnv,
|
|
WorkingDir: ct.WorkingDir,
|
|
Shell: ct.Shell,
|
|
User: ct.User,
|
|
Steps: steps,
|
|
IgnoreFailure: ct.IgnoreFailure,
|
|
Skip: !include,
|
|
NeedsApproval: ct.Approval,
|
|
DockerRegistriesAuth: make(map[string]rstypes.DockerRegistryAuth),
|
|
}
|
|
|
|
if c.DockerRegistriesAuth != nil {
|
|
for regname, auth := range c.DockerRegistriesAuth {
|
|
t.DockerRegistriesAuth[regname] = rstypes.DockerRegistryAuth{
|
|
Type: rstypes.DockerRegistryAuthType(auth.Type),
|
|
Username: genValue(auth.Username, variables),
|
|
Password: genValue(auth.Password, variables),
|
|
}
|
|
}
|
|
}
|
|
|
|
// override with per run docker registry auth
|
|
if cr.DockerRegistriesAuth != nil {
|
|
for regname, auth := range cr.DockerRegistriesAuth {
|
|
t.DockerRegistriesAuth[regname] = rstypes.DockerRegistryAuth{
|
|
Type: rstypes.DockerRegistryAuthType(auth.Type),
|
|
Username: genValue(auth.Username, variables),
|
|
Password: genValue(auth.Password, variables),
|
|
}
|
|
}
|
|
}
|
|
|
|
// override with per task docker registry auth
|
|
if ct.DockerRegistriesAuth != nil {
|
|
for regname, auth := range ct.DockerRegistriesAuth {
|
|
t.DockerRegistriesAuth[regname] = rstypes.DockerRegistryAuth{
|
|
Type: rstypes.DockerRegistryAuthType(auth.Type),
|
|
Username: genValue(auth.Username, variables),
|
|
Password: genValue(auth.Password, variables),
|
|
}
|
|
}
|
|
}
|
|
|
|
rcts[t.ID] = t
|
|
}
|
|
|
|
// populate depends, needs to be done after having created all the tasks so we can resolve their id
|
|
for _, rct := range rcts {
|
|
ct := cr.Task(rct.Name)
|
|
|
|
depends := make(map[string]*rstypes.RunConfigTaskDepend, len(ct.Depends))
|
|
for _, d := range ct.Depends {
|
|
conditions := make([]rstypes.RunConfigTaskDependCondition, len(d.Conditions))
|
|
// when no conditions are defined default to on_success
|
|
if len(d.Conditions) == 0 {
|
|
conditions = append(conditions, rstypes.RunConfigTaskDependConditionOnSuccess)
|
|
} else {
|
|
for ic, c := range d.Conditions {
|
|
var condition rstypes.RunConfigTaskDependCondition
|
|
switch c {
|
|
case config.DependConditionOnSuccess:
|
|
condition = rstypes.RunConfigTaskDependConditionOnSuccess
|
|
case config.DependConditionOnFailure:
|
|
condition = rstypes.RunConfigTaskDependConditionOnFailure
|
|
}
|
|
conditions[ic] = condition
|
|
}
|
|
}
|
|
|
|
drct := getRunConfigTaskByName(rcts, d.TaskName)
|
|
depends[drct.ID] = &rstypes.RunConfigTaskDepend{
|
|
TaskID: drct.ID,
|
|
Conditions: conditions,
|
|
}
|
|
}
|
|
|
|
rct.Depends = depends
|
|
}
|
|
|
|
return rcts
|
|
}
|
|
|
|
func getRunConfigTaskByName(rcts map[string]*rstypes.RunConfigTask, name string) *rstypes.RunConfigTask {
|
|
for _, rct := range rcts {
|
|
if rct.Name == name {
|
|
return rct
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckRunConfigTasks(rcts map[string]*rstypes.RunConfigTask) error {
|
|
// check circular dependencies
|
|
cerrs := &util.Errors{}
|
|
for _, t := range rcts {
|
|
allParents := GetAllParents(rcts, t)
|
|
for _, parent := range allParents {
|
|
if parent.ID == t.ID {
|
|
// TODO(sgotti) get the parent that depends on task to report it
|
|
dep := []string{}
|
|
for _, parent := range allParents {
|
|
pparents := GetParents(rcts, parent)
|
|
for _, pparent := range pparents {
|
|
if pparent.ID == t.ID {
|
|
dep = append(dep, fmt.Sprintf("%q", parent.Name))
|
|
}
|
|
}
|
|
}
|
|
cerrs.Append(errors.Errorf("circular dependency between task %q and tasks %s", t.Name, strings.Join(dep, " ")))
|
|
}
|
|
}
|
|
}
|
|
if cerrs.IsErr() {
|
|
return cerrs
|
|
}
|
|
|
|
// check that the task and its parent don't have a common dependency
|
|
for _, t := range rcts {
|
|
parents := GetParents(rcts, t)
|
|
for _, parent := range parents {
|
|
allParentParents := GetAllParents(rcts, parent)
|
|
for _, p := range parents {
|
|
for _, pp := range allParentParents {
|
|
if p.ID == pp.ID {
|
|
return errors.Errorf("task %q and its parent %q have both a dependency on task %q", t.Name, parent.Name, p.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GenTasksLevels(rcts map[string]*rstypes.RunConfigTask) error {
|
|
// reset all task level
|
|
for _, t := range rcts {
|
|
t.Level = -1
|
|
}
|
|
|
|
level := 0
|
|
for {
|
|
c := 0
|
|
for _, t := range rcts {
|
|
// skip tasks with the level already set
|
|
if t.Level != -1 {
|
|
continue
|
|
}
|
|
|
|
parents := GetParents(rcts, t)
|
|
ok := true
|
|
for _, p := range parents {
|
|
// * skip if the parent doesn't have a level yet
|
|
// * skip if the parent has a level equal than the current one (this happens when
|
|
// we have just set a level to a task in this same level loop)
|
|
if p.Level == -1 || p.Level >= level {
|
|
ok = false
|
|
}
|
|
}
|
|
if ok {
|
|
t.Level = level
|
|
c++
|
|
}
|
|
}
|
|
|
|
// if no tasks were updated in this level we can stop here
|
|
if c == 0 {
|
|
break
|
|
}
|
|
level++
|
|
}
|
|
for _, t := range rcts {
|
|
if t.Level == -1 {
|
|
return errors.Errorf("circular dependency detected")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetParents returns direct parents of task.
|
|
func GetParents(rcts map[string]*rstypes.RunConfigTask, task *rstypes.RunConfigTask) []*rstypes.RunConfigTask {
|
|
parents := []*rstypes.RunConfigTask{}
|
|
for _, t := range rcts {
|
|
if _, ok := task.Depends[t.ID]; ok {
|
|
parents = append(parents, t)
|
|
}
|
|
}
|
|
return parents
|
|
}
|
|
|
|
// GetAllParents returns all the parents (both direct and ancestors) of task.
|
|
// In case of circular dependency it won't loop forever but will also return
|
|
// task as parent of itself
|
|
func GetAllParents(rcts map[string]*rstypes.RunConfigTask, task *rstypes.RunConfigTask) []*rstypes.RunConfigTask {
|
|
pMap := map[string]*rstypes.RunConfigTask{}
|
|
nextParents := GetParents(rcts, task)
|
|
|
|
for len(nextParents) > 0 {
|
|
parents := nextParents
|
|
nextParents = []*rstypes.RunConfigTask{}
|
|
for _, parent := range parents {
|
|
if _, ok := pMap[parent.ID]; ok {
|
|
continue
|
|
}
|
|
pMap[parent.ID] = parent
|
|
nextParents = append(nextParents, GetParents(rcts, parent)...)
|
|
}
|
|
}
|
|
|
|
parents := make([]*rstypes.RunConfigTask, 0, len(pMap))
|
|
for _, v := range pMap {
|
|
parents = append(parents, v)
|
|
}
|
|
return parents
|
|
}
|
|
|
|
func GetParentDependConditions(t, pt *rstypes.RunConfigTask) []rstypes.RunConfigTaskDependCondition {
|
|
if dt, ok := t.Depends[pt.ID]; ok {
|
|
return dt.Conditions
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func genEnv(cenv map[string]config.Value, variables map[string]string) map[string]string {
|
|
env := map[string]string{}
|
|
for envName, envVar := range cenv {
|
|
env[envName] = genValue(envVar, variables)
|
|
}
|
|
return env
|
|
}
|
|
|
|
func genValue(val config.Value, variables map[string]string) string {
|
|
switch val.Type {
|
|
case config.ValueTypeString:
|
|
return val.Value
|
|
case config.ValueTypeFromVariable:
|
|
return variables[val.Value]
|
|
default:
|
|
panic(fmt.Errorf("wrong value type: %q", val.Value))
|
|
}
|
|
}
|