985 lines
25 KiB
Go
985 lines
25 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 config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"agola.io/agola/internal/errors"
|
|
itypes "agola.io/agola/internal/services/types"
|
|
"agola.io/agola/internal/util"
|
|
"agola.io/agola/services/types"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
)
|
|
|
|
const (
|
|
maxConfigSize = 1024 * 1024 // 1MiB
|
|
maxRunNameLength = 100
|
|
maxTaskNameLength = 100
|
|
maxStepNameLength = 100
|
|
|
|
defaultWorkingDir = "~/project"
|
|
)
|
|
|
|
type ConfigFormat int
|
|
|
|
const (
|
|
// ConfigFormatJSON handles both json or yaml format (since json is a subset of yaml)
|
|
ConfigFormatJSON ConfigFormat = iota
|
|
ConfigFormatJsonnet
|
|
ConfigFormatStarlark
|
|
)
|
|
|
|
var (
|
|
regExpDelimiters = []string{"/", "#"}
|
|
)
|
|
|
|
type Config struct {
|
|
Runs []*Run `json:"runs"`
|
|
|
|
DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"`
|
|
}
|
|
|
|
type RuntimeType string
|
|
|
|
const (
|
|
RuntimeTypePod RuntimeType = "pod"
|
|
)
|
|
|
|
type DockerRegistryAuthType string
|
|
|
|
const (
|
|
DockerRegistryAuthTypeBasic DockerRegistryAuthType = "basic"
|
|
DockerRegistryAuthTypeEncodedAuth DockerRegistryAuthType = "encodedauth"
|
|
)
|
|
|
|
type DockerRegistryAuth struct {
|
|
Type DockerRegistryAuthType `json:"type"`
|
|
|
|
// basic auth
|
|
Username Value `json:"username"`
|
|
Password Value `json:"password"`
|
|
|
|
// encoded auth
|
|
Auth Value `json:"auth"`
|
|
|
|
// future auths like aws ecr auth
|
|
}
|
|
|
|
type Runtime struct {
|
|
Type RuntimeType `json:"type,omitempty"`
|
|
Arch types.Arch `json:"arch,omitempty"`
|
|
Containers []*Container `json:"containers,omitempty"`
|
|
}
|
|
|
|
type Container struct {
|
|
Image string `json:"image,omitempty"`
|
|
Environment map[string]Value `json:"environment,omitempty"`
|
|
User string `json:"user"`
|
|
Privileged bool `json:"privileged"`
|
|
Entrypoint string `json:"entrypoint"`
|
|
Volumes []Volume `json:"volumes"`
|
|
}
|
|
|
|
type Volume struct {
|
|
Path string `json:"path"`
|
|
|
|
TmpFS *VolumeTmpFS `json:"tmpfs"`
|
|
}
|
|
|
|
type VolumeTmpFS struct {
|
|
Size *resource.Quantity `json:"size"`
|
|
}
|
|
|
|
type Run struct {
|
|
Name string `json:"name"`
|
|
Tasks []*Task `json:"tasks"`
|
|
When *When `json:"when"`
|
|
DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"`
|
|
}
|
|
|
|
type Task struct {
|
|
Name string `json:"name"`
|
|
Runtime *Runtime `json:"runtime"`
|
|
Environment map[string]Value `json:"environment,omitempty"`
|
|
WorkingDir string `json:"working_dir"`
|
|
Shell string `json:"shell"`
|
|
User string `json:"user"`
|
|
Steps Steps `json:"steps"`
|
|
Depends Depends `json:"depends"`
|
|
IgnoreFailure bool `json:"ignore_failure"`
|
|
Approval bool `json:"approval"`
|
|
When *When `json:"when"`
|
|
DockerRegistriesAuth map[string]*DockerRegistryAuth `json:"docker_registries_auth"`
|
|
}
|
|
|
|
type DependCondition string
|
|
|
|
const (
|
|
DependConditionOnSuccess DependCondition = "on_success"
|
|
DependConditionOnFailure DependCondition = "on_failure"
|
|
DependConditionOnSkipped DependCondition = "on_skipped"
|
|
)
|
|
|
|
type Depends []*Depend
|
|
|
|
type Depend struct {
|
|
TaskName string `json:"task"`
|
|
Conditions []DependCondition `json:"conditions"`
|
|
}
|
|
|
|
type Step interface{}
|
|
|
|
type Steps []Step
|
|
|
|
type BaseStep struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
When *When `json:"when"`
|
|
}
|
|
|
|
type CloneStep struct {
|
|
BaseStep `json:",inline"`
|
|
Depth *int `json:"depth"`
|
|
RecurseSubmodules bool `json:"recurse_submodules"`
|
|
}
|
|
|
|
type RunStep struct {
|
|
BaseStep `json:",inline"`
|
|
Command string `json:"command"`
|
|
Environment map[string]Value `json:"environment,omitempty"`
|
|
WorkingDir string `json:"working_dir"`
|
|
Shell string `json:"shell"`
|
|
Tty *bool `json:"tty"`
|
|
}
|
|
|
|
type SaveToWorkspaceStep struct {
|
|
BaseStep `json:",inline"`
|
|
Contents []*SaveContent `json:"contents"`
|
|
}
|
|
|
|
type RestoreWorkspaceStep struct {
|
|
BaseStep `json:",inline"`
|
|
DestDir string `json:"dest_dir"`
|
|
}
|
|
|
|
type SaveCacheStep struct {
|
|
BaseStep `json:",inline"`
|
|
Key string `json:"key"`
|
|
Contents []*SaveContent `json:"contents"`
|
|
}
|
|
|
|
type RestoreCacheStep struct {
|
|
BaseStep `json:",inline"`
|
|
Keys []string `json:"keys"`
|
|
DestDir string `json:"dest_dir"`
|
|
}
|
|
|
|
type SaveContent struct {
|
|
SourceDir string `json:"source_dir"`
|
|
DestDir string `json:"dest_dir"`
|
|
Paths []string `json:"paths"`
|
|
}
|
|
|
|
func (s *Steps) UnmarshalJSON(b []byte) error {
|
|
var stepsRaw []json.RawMessage
|
|
if err := json.Unmarshal(b, &stepsRaw); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
steps := make(Steps, len(stepsRaw))
|
|
for i, stepRaw := range stepsRaw {
|
|
var step interface{}
|
|
|
|
var stepMap map[string]json.RawMessage
|
|
if err := json.Unmarshal(stepRaw, &stepMap); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
// handle default step definition using format { type: "steptype", other steps fields }
|
|
if _, ok := stepMap["type"]; ok {
|
|
var stepTypeI interface{}
|
|
if err := json.Unmarshal(stepMap["type"], &stepTypeI); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
stepType, ok := stepTypeI.(string)
|
|
if !ok {
|
|
return errors.Errorf("step type at index %d must be a string", i)
|
|
}
|
|
|
|
switch stepType {
|
|
case "clone":
|
|
var s CloneStep
|
|
if err := json.Unmarshal(stepRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "run":
|
|
var s RunStep
|
|
if err := json.Unmarshal(stepRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
if s.Tty == nil {
|
|
s.Tty = util.BoolP(true)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "save_to_workspace":
|
|
var s SaveToWorkspaceStep
|
|
if err := json.Unmarshal(stepRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "restore_workspace":
|
|
var s RestoreWorkspaceStep
|
|
if err := json.Unmarshal(stepRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "save_cache":
|
|
var s SaveCacheStep
|
|
if err := json.Unmarshal(stepRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "restore_cache":
|
|
var s RestoreCacheStep
|
|
if err := json.Unmarshal(stepRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
default:
|
|
return errors.Errorf("unknown step type: %s", stepType)
|
|
}
|
|
} else {
|
|
// handle simpler (for yaml not for json) steps definition using format "steptype": { stepSpecification }
|
|
if len(stepMap) > 1 {
|
|
return errors.Errorf("wrong steps description at index %d: more than one step name per list entry", i)
|
|
}
|
|
for stepType, stepSpecRaw := range stepMap {
|
|
var stepSpec interface{}
|
|
if err := json.Unmarshal(stepSpecRaw, &stepSpec); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
switch stepType {
|
|
case "clone":
|
|
var s CloneStep
|
|
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "run":
|
|
var s RunStep
|
|
switch stepSpec := stepSpec.(type) {
|
|
case string:
|
|
s.Command = stepSpec
|
|
default:
|
|
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "save_to_workspace":
|
|
var s SaveToWorkspaceStep
|
|
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "restore_workspace":
|
|
var s RestoreWorkspaceStep
|
|
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "save_cache":
|
|
var s SaveCacheStep
|
|
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
|
|
case "restore_cache":
|
|
var s RestoreCacheStep
|
|
if err := json.Unmarshal(stepSpecRaw, &s); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
s.Type = stepType
|
|
step = &s
|
|
default:
|
|
return errors.Errorf("unknown step type: %s", stepType)
|
|
}
|
|
}
|
|
}
|
|
|
|
steps[i] = step
|
|
}
|
|
|
|
*s = steps
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Depends) UnmarshalJSON(b []byte) error {
|
|
var dependsRaw []json.RawMessage
|
|
|
|
if err := json.Unmarshal(b, &dependsRaw); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
depends := make([]*Depend, len(dependsRaw))
|
|
for i, dependRaw := range dependsRaw {
|
|
var dependi interface{}
|
|
if err := json.Unmarshal(dependRaw, &dependi); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
var depend *Depend
|
|
isSimpler := false
|
|
switch de := dependi.(type) {
|
|
// handle simpler (for yaml) depends definition using format "taskname":
|
|
case string:
|
|
depend = &Depend{
|
|
TaskName: dependi.(string),
|
|
}
|
|
case map[string]interface{}:
|
|
if len(de) == 1 {
|
|
for _, v := range de {
|
|
switch v.(type) {
|
|
case []interface{}:
|
|
isSimpler = true
|
|
case string:
|
|
default:
|
|
return errors.Errorf("unsupported depend entry format")
|
|
}
|
|
}
|
|
}
|
|
if !isSimpler {
|
|
// handle default depends definition using format "task": "taskname", conditions: [ list of conditions ]
|
|
if err := json.Unmarshal(dependRaw, &depend); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
} else {
|
|
// handle simpler (for yaml) depends definition using format "taskname": [ list of conditions ]
|
|
if len(de) != 1 {
|
|
return errors.Errorf("unsupported depend entry format")
|
|
}
|
|
type deplist map[string][]DependCondition
|
|
var dl deplist
|
|
if err := json.Unmarshal(dependRaw, &dl); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
if len(dl) != 1 {
|
|
return errors.Errorf("unsupported depend entry format")
|
|
}
|
|
for k, v := range dl {
|
|
depend = &Depend{
|
|
TaskName: k,
|
|
Conditions: v,
|
|
}
|
|
}
|
|
}
|
|
|
|
default:
|
|
return errors.Errorf("unsupported depend entry format")
|
|
}
|
|
depends[i] = depend
|
|
}
|
|
|
|
*d = depends
|
|
|
|
return nil
|
|
}
|
|
|
|
type ValueType int
|
|
|
|
const (
|
|
ValueTypeString ValueType = iota
|
|
ValueTypeFromVariable
|
|
)
|
|
|
|
type Value struct {
|
|
Type ValueType
|
|
Value string
|
|
}
|
|
|
|
func (val *Value) UnmarshalJSON(b []byte) error {
|
|
var ival interface{}
|
|
if err := json.Unmarshal(b, &ival); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
switch valValue := ival.(type) {
|
|
case string:
|
|
val.Type = ValueTypeString
|
|
val.Value = valValue
|
|
case map[string]interface{}:
|
|
for k, v := range valValue {
|
|
if k == "from_variable" {
|
|
switch v.(type) {
|
|
case string:
|
|
default:
|
|
return errors.Errorf("unknown value format: %v", v)
|
|
}
|
|
val.Type = ValueTypeFromVariable
|
|
val.Value = v.(string)
|
|
}
|
|
}
|
|
default:
|
|
return errors.Errorf("unknown value format: %v", ival)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type When types.When
|
|
|
|
type when struct {
|
|
Branch interface{} `json:"branch"`
|
|
Tag interface{} `json:"tag"`
|
|
Ref interface{} `json:"ref"`
|
|
}
|
|
|
|
func (w *When) ToWhen() *types.When {
|
|
return (*types.When)(w)
|
|
}
|
|
|
|
func (w *When) UnmarshalJSON(b []byte) error {
|
|
var wi *when
|
|
if err := json.Unmarshal(b, &wi); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
var err error
|
|
|
|
if wi.Branch != nil {
|
|
w.Branch, err = parseWhenConditions(wi.Branch)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
if wi.Tag != nil {
|
|
w.Tag, err = parseWhenConditions(wi.Tag)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
if wi.Ref != nil {
|
|
w.Ref, err = parseWhenConditions(wi.Ref)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseWhenConditions(wi interface{}) (*types.WhenConditions, error) {
|
|
w := &types.WhenConditions{}
|
|
|
|
var err error
|
|
include := []string{}
|
|
exclude := []string{}
|
|
|
|
switch c := wi.(type) {
|
|
case string:
|
|
include = []string{c}
|
|
case []interface{}:
|
|
ss, err := parseSliceString(c)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
include = ss
|
|
case map[string]interface{}:
|
|
for k, v := range c {
|
|
switch k {
|
|
case "include":
|
|
include, err = parseStringOrSlice(v)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
case "exclude":
|
|
exclude, err = parseStringOrSlice(v)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
default:
|
|
return nil, errors.Errorf(`expected one of "include" or "exclude", got %s`, k)
|
|
}
|
|
}
|
|
default:
|
|
return nil, errors.Errorf("wrong when format")
|
|
}
|
|
|
|
w.Include, err = parseWhenConditionSlice(include)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
w.Exclude, err = parseWhenConditionSlice(exclude)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return w, nil
|
|
}
|
|
|
|
func parseWhenConditionSlice(conds []string) ([]types.WhenCondition, error) {
|
|
if len(conds) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
wcs := []types.WhenCondition{}
|
|
for _, cond := range conds {
|
|
wc, err := parseWhenCondition(cond)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
wcs = append(wcs, *wc)
|
|
}
|
|
|
|
return wcs, nil
|
|
}
|
|
|
|
func parseWhenCondition(s string) (*types.WhenCondition, error) {
|
|
isRegExp := false
|
|
if len(s) > 2 {
|
|
for _, d := range regExpDelimiters {
|
|
if strings.HasPrefix(s, d) && strings.HasSuffix(s, d) {
|
|
isRegExp = true
|
|
s = s[1 : len(s)-1]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
wc := &types.WhenCondition{Match: s}
|
|
|
|
if isRegExp {
|
|
if _, err := regexp.Compile(s); err != nil {
|
|
return nil, errors.Wrapf(err, "wrong regular expression")
|
|
}
|
|
wc.Type = types.WhenConditionTypeRegExp
|
|
} else {
|
|
wc.Type = types.WhenConditionTypeSimple
|
|
}
|
|
return wc, nil
|
|
}
|
|
|
|
func parseStringOrSlice(si interface{}) ([]string, error) {
|
|
ss := []string{}
|
|
switch c := si.(type) {
|
|
case string:
|
|
ss = []string{c}
|
|
case []interface{}:
|
|
var err error
|
|
ss, err = parseSliceString(c)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
return ss, nil
|
|
}
|
|
|
|
func parseSliceString(si []interface{}) ([]string, error) {
|
|
ss := []string{}
|
|
for _, v := range si {
|
|
switch s := v.(type) {
|
|
case string:
|
|
ss = append(ss, s)
|
|
default:
|
|
return nil, errors.Errorf("expected string")
|
|
}
|
|
}
|
|
return ss, nil
|
|
}
|
|
|
|
func (c *Config) Run(runName string) *Run {
|
|
for _, r := range c.Runs {
|
|
if r.Name == runName {
|
|
return r
|
|
}
|
|
}
|
|
panic(fmt.Sprintf("run %q doesn't exists", runName))
|
|
}
|
|
|
|
func (r *Run) Task(taskName string) *Task {
|
|
for _, t := range r.Tasks {
|
|
if t.Name == taskName {
|
|
return t
|
|
}
|
|
}
|
|
panic(fmt.Sprintf("task %q for run %q doesn't exists", taskName, r.Name))
|
|
}
|
|
|
|
var DefaultConfig = Config{}
|
|
|
|
// ConfigContext is the context to pass to the config generator. Fields are not marked as omitempty since
|
|
// we want to provide all of them with empty value if not existing in such context
|
|
// (i.e. pull_request_id will be an empty string when not a pull request)
|
|
type ConfigContext struct {
|
|
RefType itypes.RunRefType `json:"ref_type"`
|
|
Ref string `json:"ref"`
|
|
Branch string `json:"branch"`
|
|
Tag string `json:"tag"`
|
|
PullRequestID string `json:"pull_request_id"`
|
|
CommitSHA string `json:"commit_sha"`
|
|
}
|
|
|
|
func ParseConfig(configData []byte, format ConfigFormat, configContext *ConfigContext) (*Config, error) {
|
|
// TODO(sgotti) execute jsonnet and starlark executor in a
|
|
// separate process to avoid issues with malformat config that
|
|
// could lead to infinite executions and memory exhaustion
|
|
switch format {
|
|
case ConfigFormatJsonnet:
|
|
// Generate json from jsonnet
|
|
var err error
|
|
configData, err = execJsonnet(configData, configContext)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to execute jsonnet")
|
|
}
|
|
case ConfigFormatStarlark:
|
|
// Generate json from starlark
|
|
var err error
|
|
configData, err = execStarlark(configData, configContext)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to execute starlark")
|
|
}
|
|
}
|
|
|
|
if len(configData) > maxConfigSize {
|
|
return nil, errors.Errorf("config size is greater than allowed max config size: %d > %d", len(configData), maxConfigSize)
|
|
}
|
|
|
|
config := DefaultConfig
|
|
if err := yaml.Unmarshal(configData, &config); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to unmarshal config")
|
|
}
|
|
|
|
return &config, checkConfig(&config)
|
|
}
|
|
|
|
func checkConfig(config *Config) error {
|
|
if len(config.Runs) == 0 {
|
|
return errors.Errorf("no runs defined")
|
|
}
|
|
|
|
seenRuns := map[string]struct{}{}
|
|
for ri, run := range config.Runs {
|
|
if run == nil {
|
|
return errors.Errorf("run at index %d is empty", ri)
|
|
}
|
|
|
|
if run.Name == "" {
|
|
return errors.Errorf("run at index %d has empty name", ri)
|
|
}
|
|
|
|
if len(run.Name) > maxRunNameLength {
|
|
return errors.Errorf("run name %q too long", run.Name)
|
|
}
|
|
|
|
if _, ok := seenRuns[run.Name]; ok {
|
|
return errors.Errorf("duplicate run name: %s", run.Name)
|
|
}
|
|
seenRuns[run.Name] = struct{}{}
|
|
|
|
seenTasks := map[string]struct{}{}
|
|
for ti, task := range run.Tasks {
|
|
if task == nil {
|
|
return errors.Errorf("run %q: task at index %d is empty", run.Name, ti)
|
|
}
|
|
|
|
if task.Name == "" {
|
|
return errors.Errorf("run %q: task at index %d has empty name", run.Name, ti)
|
|
}
|
|
|
|
if len(task.Name) > maxTaskNameLength {
|
|
return errors.Errorf("task name %q too long", task.Name)
|
|
}
|
|
|
|
if _, ok := seenTasks[task.Name]; ok {
|
|
return errors.Errorf("duplicate task name: %s", task.Name)
|
|
}
|
|
seenTasks[task.Name] = struct{}{}
|
|
|
|
// check tasks runtime
|
|
if task.Runtime == nil {
|
|
return errors.Errorf("task %q: runtime is not defined", task.Name)
|
|
}
|
|
|
|
r := task.Runtime
|
|
if r.Type != "" {
|
|
if r.Type != RuntimeTypePod {
|
|
return errors.Errorf("task %q runtime: wrong type %q", task.Name, r.Type)
|
|
}
|
|
}
|
|
if len(r.Containers) == 0 {
|
|
return errors.Errorf("task %q runtime: at least one container must be defined", task.Name)
|
|
}
|
|
if r.Arch != "" {
|
|
if !types.IsValidArch(r.Arch) {
|
|
return errors.Errorf("task %q runtime: invalid arch %q", task.Name, r.Arch)
|
|
}
|
|
}
|
|
|
|
for _, container := range r.Containers {
|
|
for _, vol := range container.Volumes {
|
|
if vol.TmpFS == nil {
|
|
return errors.Errorf("no volume config specified")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check broken dependencies
|
|
for _, run := range config.Runs {
|
|
// collect all task names
|
|
allTasks := map[string]struct{}{}
|
|
for _, task := range run.Tasks {
|
|
allTasks[task.Name] = struct{}{}
|
|
}
|
|
|
|
for _, task := range run.Tasks {
|
|
for _, dep := range task.Depends {
|
|
if _, ok := allTasks[dep.TaskName]; !ok {
|
|
return errors.Errorf("run task %q needed by task %q doesn't exist", dep.TaskName, task.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check circular dependencies
|
|
for _, run := range config.Runs {
|
|
cerrs := &util.Errors{}
|
|
for _, task := range run.Tasks {
|
|
allParents := getAllTaskParents(run, task)
|
|
for _, parent := range allParents {
|
|
if parent.Name == task.Name {
|
|
// TODO(sgotti) get the parent that depends on task to report it
|
|
dep := []string{}
|
|
for _, parent := range allParents {
|
|
pparents := getTaskParents(run, parent)
|
|
for _, pparent := range pparents {
|
|
if pparent.Name == task.Name {
|
|
dep = append(dep, fmt.Sprintf("%q", parent.Name))
|
|
}
|
|
}
|
|
}
|
|
cerrs.Append(errors.Errorf("circular dependency between task %q and tasks %s", task.Name, strings.Join(dep, " ")))
|
|
}
|
|
}
|
|
}
|
|
if cerrs.IsErr() {
|
|
return cerrs
|
|
}
|
|
}
|
|
|
|
// check that the task and its parent don't have a common dependency
|
|
for _, run := range config.Runs {
|
|
for _, task := range run.Tasks {
|
|
parents := getTaskParents(run, task)
|
|
for _, parent := range parents {
|
|
allParentParents := getAllTaskParents(run, parent)
|
|
for _, p := range parents {
|
|
for _, pp := range allParentParents {
|
|
if p.Name == pp.Name {
|
|
return errors.Errorf("task %q and its dependency %q have both a dependency on task %q", task.Name, parent.Name, p.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check duplicate task dependencies
|
|
for _, run := range config.Runs {
|
|
for _, task := range run.Tasks {
|
|
// check duplicate dependencies in task
|
|
seenDependencies := map[string]struct{}{}
|
|
for _, dep := range task.Depends {
|
|
if _, ok := seenDependencies[dep.TaskName]; ok {
|
|
return errors.Errorf("duplicate task dependency: %s", task.Name)
|
|
}
|
|
seenDependencies[dep.TaskName] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, run := range config.Runs {
|
|
for _, task := range run.Tasks {
|
|
for i, s := range task.Steps {
|
|
switch step := s.(type) {
|
|
// TODO(sgotti) we could use the run step command as step name but when the
|
|
// command is very long or multi line it doesn't makes sense and will
|
|
// probably be quite unuseful/confusing from an UI point of view
|
|
case *CloneStep:
|
|
if step.Depth != nil && *step.Depth < 1 {
|
|
return errors.Errorf("depth value must be greater than 0 for clone step in task %q", task.Name)
|
|
}
|
|
case *RunStep:
|
|
if step.Command == "" {
|
|
return errors.Errorf("no command defined for step %d (run) in task %q", i, task.Name)
|
|
}
|
|
|
|
case *SaveCacheStep:
|
|
if step.Key == "" {
|
|
return errors.Errorf("no key defined for step %d (save_cache) in task %q", i, task.Name)
|
|
}
|
|
|
|
case *RestoreCacheStep:
|
|
if len(step.Keys) == 0 {
|
|
return errors.Errorf("no keys defined for step %d (restore_cache) in task %q", i, task.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set defaults
|
|
for _, registryAuth := range config.DockerRegistriesAuth {
|
|
if registryAuth.Type == "" {
|
|
registryAuth.Type = DockerRegistryAuthTypeBasic
|
|
}
|
|
}
|
|
|
|
for _, run := range config.Runs {
|
|
// set auth type to basic if not specified
|
|
for _, registryAuth := range run.DockerRegistriesAuth {
|
|
if registryAuth.Type == "" {
|
|
registryAuth.Type = DockerRegistryAuthTypeBasic
|
|
}
|
|
}
|
|
for _, task := range run.Tasks {
|
|
// set auth type to basic if not specified
|
|
for _, registryAuth := range task.DockerRegistriesAuth {
|
|
if registryAuth.Type == "" {
|
|
registryAuth.Type = DockerRegistryAuthTypeBasic
|
|
}
|
|
}
|
|
|
|
// set task default working dir
|
|
if task.WorkingDir == "" {
|
|
task.WorkingDir = defaultWorkingDir
|
|
}
|
|
|
|
// set task runtime type to pod if empty
|
|
r := task.Runtime
|
|
if r.Type == "" {
|
|
r.Type = RuntimeTypePod
|
|
}
|
|
|
|
// set steps defaults
|
|
for i, s := range task.Steps {
|
|
switch step := s.(type) {
|
|
// TODO(sgotti) we could use the run step command as step name but when the
|
|
// command is very long or multi line it doesn't makes sense and will
|
|
// probably be quite unuseful/confusing from an UI point of view
|
|
case *RunStep:
|
|
if step.Name == "" {
|
|
lines, err := util.CountLines(step.Command)
|
|
// if we failed to count the lines (shouldn't happen) or the number of lines is > 1 then a name is requred
|
|
if err != nil || lines > 1 {
|
|
return errors.Errorf("missing step name for step %d (run) in task %q, required since command is more than one line", i, task.Name)
|
|
}
|
|
len := len(step.Command)
|
|
if len > maxStepNameLength {
|
|
len = maxStepNameLength
|
|
}
|
|
step.Name = step.Command[:len]
|
|
}
|
|
// if tty is omitted its default is true
|
|
if step.Tty == nil {
|
|
step.Tty = util.BoolP(true)
|
|
}
|
|
case *SaveCacheStep:
|
|
for _, content := range step.Contents {
|
|
if len(content.Paths) == 0 {
|
|
// default to all files inside the sourceDir
|
|
content.Paths = []string{"**"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTaskParents returns direct parents of task.
|
|
func getTaskParents(run *Run, task *Task) []*Task {
|
|
parents := []*Task{}
|
|
for _, el := range run.Tasks {
|
|
isParent := false
|
|
for _, d := range task.Depends {
|
|
if d.TaskName == el.Name {
|
|
isParent = true
|
|
}
|
|
}
|
|
if isParent {
|
|
parents = append(parents, el)
|
|
}
|
|
}
|
|
return parents
|
|
}
|
|
|
|
// getAllTaskParents returns all the parents (both direct and ancestors) of a task.
|
|
// In case of circular dependency it won't loop forever but will also return
|
|
// the task as parent of itself
|
|
func getAllTaskParents(run *Run, task *Task) []*Task {
|
|
pMap := map[string]*Task{}
|
|
nextParents := getTaskParents(run, task)
|
|
|
|
for len(nextParents) > 0 {
|
|
parents := nextParents
|
|
nextParents = []*Task{}
|
|
for _, parent := range parents {
|
|
if _, ok := pMap[parent.Name]; ok {
|
|
continue
|
|
}
|
|
pMap[parent.Name] = parent
|
|
nextParents = append(nextParents, getTaskParents(run, parent)...)
|
|
}
|
|
}
|
|
|
|
parents := make([]*Task, 0, len(pMap))
|
|
for _, v := range pMap {
|
|
parents = append(parents, v)
|
|
}
|
|
return parents
|
|
}
|