|
|
package gitlabcivalidator
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
"gitflame.ru/CI_VLDR/errors"
|
|
|
"gopkg.in/yaml.v3"
|
|
|
"strings"
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
// computeEndPosition возвращает конечные координаты для фрагмента, предполагая, что текст находится на одной строке.
|
|
|
func computeEndPosition(startLine, startColumn int, text string) (endLine, endColumn int) {
|
|
|
// Если текст пустой, выделим хотя бы один символ
|
|
|
if len(text) == 0 {
|
|
|
return startLine, startColumn + 1
|
|
|
}
|
|
|
return startLine, startColumn + len(text)
|
|
|
}
|
|
|
|
|
|
// ValidateGitLabCIFile – главная точка входа в библиотеку.
|
|
|
func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
|
|
|
validationErr := &errors.ValidationError{}
|
|
|
|
|
|
if len(content) == 0 {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrEmptyFile, errors.ErrorMessages[errors.ErrEmptyFile], 0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
// Повторно распарсить YAML, чтобы получить узел и позиционную информацию
|
|
|
var root yaml.Node
|
|
|
err := yaml.Unmarshal([]byte(content), &root)
|
|
|
if err != nil {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err),
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
if len(root.Content) == 0 {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
"файл .gitlab-ci.yml пуст или не содержит допустимых данных",
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
"некорректный формат YAML: ожидался корневой объект",
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
// Получаем корневой узел
|
|
|
mainNode := root.Content[0]
|
|
|
// Проверяем неизвестные ключи и добавляем ошибки в объект валидации
|
|
|
unknowns := findUnknownRootKeysWithSpan(mainNode)
|
|
|
for _, unk := range unknowns {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrUnknownRootKey,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrUnknownRootKey], unk.Key),
|
|
|
unk.Line, unk.Column, unk.EndLine, unk.EndColumn)
|
|
|
}
|
|
|
|
|
|
// Парсим YAML в структуру конфигурации
|
|
|
cfg, err := ParseGitLabCIConfig([]byte(content), validationErr)
|
|
|
if err != nil {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrYamlSyntax], err),
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
validateRootObject(cfg, validationErr)
|
|
|
validateJobs(cfg, validationErr)
|
|
|
validateChanges(cfg, validationErr)
|
|
|
validatePaths(cfg, validationErr)
|
|
|
validateService(cfg, validationErr)
|
|
|
validateStages(cfg, validationErr)
|
|
|
validateVariables(cfg, validationErr)
|
|
|
validateIncludeRules(cfg, validationErr)
|
|
|
|
|
|
if !validationErr.IsEmpty() {
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func checkSHAExistsInBranchesOrTags(sha string) bool {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
func validateRootObject(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
validateBeforeScript(cfg.BeforeScript, ve)
|
|
|
}
|
|
|
|
|
|
func validateBeforeScript(before interface{}, ve *errors.ValidationError) {
|
|
|
if before == nil {
|
|
|
return
|
|
|
}
|
|
|
if _, ok := before.(string); ok {
|
|
|
return
|
|
|
}
|
|
|
if arr, ok := before.([]interface{}); ok {
|
|
|
if !validateNestedStringArray(arr, 1, 10) {
|
|
|
ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid,
|
|
|
errors.ErrorMessages[errors.ErrBeforeScriptInvalid],
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid,
|
|
|
errors.ErrorMessages[errors.ErrBeforeScriptInvalid],
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateNestedStringArray(arr []interface{}, currentLevel, maxLevel int) bool {
|
|
|
if currentLevel > maxLevel {
|
|
|
return false
|
|
|
}
|
|
|
for _, item := range arr {
|
|
|
switch item.(type) {
|
|
|
case string:
|
|
|
case []interface{}:
|
|
|
if !validateNestedStringArray(item.([]interface{}), currentLevel+1, maxLevel) {
|
|
|
return false
|
|
|
}
|
|
|
default:
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
func validateService(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Services == nil {
|
|
|
return
|
|
|
}
|
|
|
switch cfg.Services.(type) {
|
|
|
case string:
|
|
|
case map[string]interface{}:
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrServiceInvalid,
|
|
|
errors.ErrorMessages[errors.ErrServiceInvalid],
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
for i, st := range cfg.Stages {
|
|
|
if strings.TrimSpace(st) == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrStageInvalid,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrStageInvalid], i),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Variables == nil {
|
|
|
return
|
|
|
}
|
|
|
varsMap, ok := cfg.Variables.(map[string]interface{})
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrVariablesInvalid, errors.ErrorMessages[errors.ErrVariablesInvalid], 0, 0, 0, 0)
|
|
|
return
|
|
|
}
|
|
|
allowedKeys := map[string]struct{}{
|
|
|
"value": {},
|
|
|
"description": {},
|
|
|
"expand": {},
|
|
|
"options": {},
|
|
|
}
|
|
|
for varName, val := range varsMap {
|
|
|
if isScalar(val) {
|
|
|
// ok
|
|
|
} else if nested, ok := val.(map[string]interface{}); ok {
|
|
|
invalidKeys := []string{}
|
|
|
for k := range nested {
|
|
|
if _, allowed := allowedKeys[k]; !allowed {
|
|
|
invalidKeys = append(invalidKeys, k)
|
|
|
}
|
|
|
}
|
|
|
if len(invalidKeys) > 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrVariablesInvalidKey,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariablesInvalidKey], varName, strings.Join(invalidKeys, ", ")),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrVariableInvalid,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func isScalar(val interface{}) bool {
|
|
|
switch val.(type) {
|
|
|
case string, bool, int, int64, float64, float32:
|
|
|
return true
|
|
|
default:
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Jobs == nil || len(cfg.Jobs) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrMissingJobs, errors.ErrorMessages[errors.ErrMissingJobs], 0, 0, 0, 0)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
stagesAllowed := make(map[string]struct{})
|
|
|
for _, st := range cfg.Stages {
|
|
|
stagesAllowed[st] = struct{}{}
|
|
|
}
|
|
|
|
|
|
visibleCount := 0
|
|
|
dependencyGraph := make(map[string][]string)
|
|
|
|
|
|
for _, job := range cfg.Jobs {
|
|
|
// (a) Check that the job name is not empty.
|
|
|
if job.Name == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrJobNameBlank,
|
|
|
errors.ErrorMessages[errors.ErrJobNameBlank],
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
continue
|
|
|
}
|
|
|
if job.Name[0] != '.' {
|
|
|
visibleCount++
|
|
|
if job.Stage == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrMissingStage,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrMissingStage], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// (d) Check that at least one of script is present.
|
|
|
if job.Script == nil && job.Run == nil && job.Trigger == nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrMissingScript,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrMissingScript], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
|
|
|
// (e) Check that the specified stage is allowed.
|
|
|
if job.Stage != "" {
|
|
|
if _, ok := stagesAllowed[job.Stage]; !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrJobStageNotExist,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrJobStageNotExist], job.Name, job.Stage, cfg.Stages),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// (f) Validate dependencies.
|
|
|
dependencyGraph[job.Name] = append(dependencyGraph[job.Name], job.Dependencies...)
|
|
|
for _, dep := range job.Dependencies {
|
|
|
depPos, _ := job.DependencyLines[dep]
|
|
|
if _, ok := cfg.Jobs[dep]; !ok {
|
|
|
endLine, endCol := computeEndPosition(depPos.Line, depPos.Column, dep)
|
|
|
ve.AddDetailWithSpan(errors.ErrUndefinedDependency,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrUndefinedDependency],
|
|
|
job.Name, job.Line, job.Column, dep, depPos.Line, depPos.Column, endCol),
|
|
|
job.Line, job.Column, endLine, endCol)
|
|
|
} else {
|
|
|
if !checkStageOrder(cfg, dep, job.Name) {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidStageOrder,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidStageOrder],
|
|
|
job.Name, dep, job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// (g) Validate the 'needs' block.
|
|
|
if len(job.Needs) > 0 {
|
|
|
needNames := make(map[string]struct{})
|
|
|
for _, nd := range job.Needs {
|
|
|
// Check for duplicates.
|
|
|
if _, exist := needNames[nd]; exist {
|
|
|
ndStartLine, ndStartCol := job.NeedLines[nd].Line, job.NeedLines[nd].Column
|
|
|
ndEndLine, ndEndCol := computeEndPosition(ndStartLine, ndStartCol, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrDuplicateNeeds,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrDuplicateNeeds], job.Name, nd),
|
|
|
ndStartLine, ndStartCol, ndEndLine, ndEndCol)
|
|
|
} else {
|
|
|
needNames[nd] = struct{}{}
|
|
|
}
|
|
|
|
|
|
// Check for empty need name.
|
|
|
if nd == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrJobNameBlank,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrJobNameBlank], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
|
|
|
// Check maximum length.
|
|
|
if len(nd) > MaxJobNameLength {
|
|
|
ndStartLine, ndStartCol := job.NeedLines[nd].Line, job.NeedLines[nd].Column
|
|
|
ndEndLine, ndEndCol := computeEndPosition(ndStartLine, ndStartCol, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrNeedNameTooLong,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrNeedNameTooLong], job.Name, nd, MaxJobNameLength),
|
|
|
ndStartLine, ndStartCol, ndEndLine, ndEndCol)
|
|
|
}
|
|
|
|
|
|
// Check that the referenced job in 'needs' exists.
|
|
|
needPos, posExists := job.NeedLines[nd]
|
|
|
if !posExists {
|
|
|
needPos = struct{ Line, Column int }{Line: job.Line, Column: job.Column}
|
|
|
}
|
|
|
if _, ok := cfg.Jobs[nd]; !ok {
|
|
|
ndEndLine, ndEndCol := computeEndPosition(needPos.Line, needPos.Column, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrUndefinedNeed,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrUndefinedNeed], job.Name, needPos.Line, needPos.Column, ndEndCol, nd),
|
|
|
needPos.Line, needPos.Column, ndEndLine, ndEndCol)
|
|
|
} else if !checkStageOrder(cfg, nd, job.Name) {
|
|
|
ndEndLine, ndEndCol := computeEndPosition(needPos.Line, needPos.Column, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidStagesOrder,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidStagesOrder],
|
|
|
job.Name, job.Line, job.Column, nd, needPos.Line, needPos.Column, ndEndCol, job.Name),
|
|
|
needPos.Line, needPos.Column, ndEndLine, ndEndCol)
|
|
|
}
|
|
|
|
|
|
dependencyGraph[job.Name] = append(dependencyGraph[job.Name], nd)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
validateJobArtifacts(job, ve)
|
|
|
validateJobDelayedParameters(job, ve)
|
|
|
validateJobDependenciesNeedsConsistency(job, ve)
|
|
|
validateJobRules(job, ve)
|
|
|
validateJobWhen(job, ve)
|
|
|
validateJobOnly(job, ve)
|
|
|
}
|
|
|
|
|
|
if visibleCount == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrNoVisibleJob,
|
|
|
errors.ErrorMessages[errors.ErrNoVisibleJob],
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
|
|
|
cycle := detectCycles(dependencyGraph)
|
|
|
if len(cycle) > 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrCyclicDependency,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrCyclicDependency], cycle),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobArtifacts(job *Job, ve *errors.ValidationError) {
|
|
|
if job.Artifacts != nil {
|
|
|
if len(job.Artifacts.Paths) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrArtifactsPathsBlank,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrArtifactsPathsBlank], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func checkStageOrder(cfg *GitLabCIConfig, jobDep, jobName string) bool {
|
|
|
depStage := cfg.Jobs[jobDep].Stage
|
|
|
jobStage := cfg.Jobs[jobName].Stage
|
|
|
if depStage == "" || jobStage == "" {
|
|
|
return true
|
|
|
}
|
|
|
allStages := cfg.Stages
|
|
|
depIndex, jobIndex := -1, -1
|
|
|
for i, st := range allStages {
|
|
|
if st == depStage {
|
|
|
depIndex = i
|
|
|
}
|
|
|
if st == jobStage {
|
|
|
jobIndex = i
|
|
|
}
|
|
|
}
|
|
|
if depIndex == -1 || jobIndex == -1 {
|
|
|
return true
|
|
|
}
|
|
|
return depIndex <= jobIndex
|
|
|
}
|
|
|
|
|
|
func validateChanges(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Changes == nil {
|
|
|
return
|
|
|
}
|
|
|
switch v := cfg.Changes.(type) {
|
|
|
case []interface{}:
|
|
|
if len(v) > 50 {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesTooManyEntries, errors.ErrorMessages[errors.ErrChangesTooManyEntries], 0, 0, 0, 0)
|
|
|
}
|
|
|
for _, item := range v {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, errors.ErrorMessages[errors.ErrChangesNotArrayOfStrings], 0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
case map[string]interface{}:
|
|
|
paths, ok := v["paths"]
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesMissingPaths, errors.ErrorMessages[errors.ErrChangesMissingPaths], 0, 0, 0, 0)
|
|
|
} else {
|
|
|
arr, ok := paths.([]interface{})
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, errors.ErrorMessages[errors.ErrChangesNotArrayOfStrings], 0, 0, 0, 0)
|
|
|
} else {
|
|
|
if len(arr) > 50 {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesTooManyEntries, errors.ErrorMessages[errors.ErrChangesTooManyEntries], 0, 0, 0, 0)
|
|
|
}
|
|
|
for _, item := range arr {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, errors.ErrorMessages[errors.ErrPathsNotArrayOfStrings], 0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
case string:
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesInvalidType, errors.ErrorMessages[errors.ErrChangesInvalidType], 0, 0, 0, 0)
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesInvalidType, errors.ErrorMessages[errors.ErrChangesInvalidType], 0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validatePaths(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Paths == nil {
|
|
|
return
|
|
|
}
|
|
|
arr, ok := cfg.Paths.([]interface{})
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrPathsNotArrayOfStrings, errors.ErrorMessages[errors.ErrPathsNotArrayOfStrings], 0, 0, 0, 0)
|
|
|
return
|
|
|
}
|
|
|
for _, item := range arr {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrPathsNotArrayOfStrings, errors.ErrorMessages[errors.ErrPathsNotArrayOfStrings], 0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobDelayedParameters(job *Job, ve *errors.ValidationError) {
|
|
|
if job.When == "delayed" {
|
|
|
if job.StartIn == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInMissing,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrStartInMissing], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
} else {
|
|
|
d, err := time.ParseDuration(job.StartIn)
|
|
|
if err != nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInInvalid,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrStartInInvalid], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
} else if d > 7*24*time.Hour {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInTooLong,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrStartInTooLong], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
if job.StartIn != "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInMustBeBlank,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrStartInMustBeBlank], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobDependenciesNeedsConsistency(job *Job, ve *errors.ValidationError) {
|
|
|
if len(job.Dependencies) > 0 && len(job.Needs) > 0 {
|
|
|
needNames := make(map[string]struct{})
|
|
|
for _, nd := range job.Needs {
|
|
|
needNames[nd] = struct{}{}
|
|
|
}
|
|
|
for _, dep := range job.Dependencies {
|
|
|
if _, ok := needNames[dep]; !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrDependencyNotInNeeds,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrDependencyNotInNeeds], job.Name, dep),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobRules(job *Job, ve *errors.ValidationError) {
|
|
|
switch r := job.Rules.(type) {
|
|
|
case map[string]interface{}:
|
|
|
validateRule(r, job, ve)
|
|
|
case []interface{}:
|
|
|
for _, item := range r {
|
|
|
if ruleMap, ok := item.(map[string]interface{}); ok {
|
|
|
validateRule(ruleMap, job, ve)
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidRulesFormat,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidRulesFormat], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobOnly(job *Job, ve *errors.ValidationError) {
|
|
|
if job.Only == nil {
|
|
|
return
|
|
|
}
|
|
|
switch v := job.Only.(type) {
|
|
|
case string:
|
|
|
if strings.TrimSpace(v) == "" {
|
|
|
endLine, endCol := computeEndPosition(job.OnlyLine, job.OnlyColumn, v)
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, endLine, endCol)
|
|
|
}
|
|
|
case []interface{}:
|
|
|
if len(v) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, job.OnlyLine, job.OnlyColumn+1)
|
|
|
} else {
|
|
|
for _, item := range v {
|
|
|
if s, ok := item.(string); ok {
|
|
|
if strings.TrimSpace(s) == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, job.OnlyLine, job.OnlyColumn+1)
|
|
|
break
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, job.OnlyLine, job.OnlyColumn+1)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
case []string:
|
|
|
if len(v) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, job.OnlyLine, job.OnlyColumn+1)
|
|
|
} else {
|
|
|
for _, s := range v {
|
|
|
if strings.TrimSpace(s) == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, job.OnlyLine, job.OnlyColumn+1)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidOnly,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidOnly], job.Name),
|
|
|
job.OnlyLine, job.OnlyColumn, job.OnlyLine, job.OnlyColumn+1)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// validateMappingKeys validates that all keys in a YAML mapping node are within the allowed set.
|
|
|
// fieldName is used for error messages.
|
|
|
func validateMappingKeys(node *yaml.Node, allowedKeys map[string]struct{}, fieldName string, ve *errors.ValidationError) {
|
|
|
if node.Kind != yaml.MappingNode {
|
|
|
return
|
|
|
}
|
|
|
for i := 0; i < len(node.Content)-1; i += 2 {
|
|
|
keyNode := node.Content[i]
|
|
|
if _, ok := allowedKeys[keyNode.Value]; !ok {
|
|
|
endLine, endCol := keyNode.Line, keyNode.Column+len(keyNode.Value)
|
|
|
ve.AddDetailWithSpan(errors.ErrUnknownKey,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrUnknownKey], keyNode.Value, fieldName),
|
|
|
keyNode.Line, keyNode.Column, endLine, endCol)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobMapping(jobNode *yaml.Node, ve *errors.ValidationError) {
|
|
|
allowedKeys := map[string]struct{}{
|
|
|
"stage": {},
|
|
|
"script": {},
|
|
|
"run": {},
|
|
|
"trigger": {},
|
|
|
"needs": {},
|
|
|
"dependencies": {},
|
|
|
"rules": {},
|
|
|
"artifacts": {},
|
|
|
"when": {},
|
|
|
"start_in": {},
|
|
|
"only": {},
|
|
|
"except": {},
|
|
|
// add any additional allowed keys for a job...
|
|
|
}
|
|
|
validateMappingKeys(jobNode, allowedKeys, "job", ve)
|
|
|
}
|
|
|
|
|
|
func validateRule(rule map[string]interface{}, job *Job, ve *errors.ValidationError) {
|
|
|
// Validate "if" field, if present.
|
|
|
if ifVal, exists := rule["if"]; exists {
|
|
|
if ifStr, ok := ifVal.(string); ok {
|
|
|
if !validateExpressionSyntax(ifStr) {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidExpressionSyntax,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidExpressionSyntax], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidExpressionSyntax,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidExpressionSyntax], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Validate "when" field, if present.
|
|
|
if whenVal, exists := rule["when"]; exists {
|
|
|
if whenStr, ok := whenVal.(string); ok {
|
|
|
allowedWhen := map[string]struct{}{
|
|
|
"on_success": {},
|
|
|
"always": {},
|
|
|
"on_failure": {},
|
|
|
"never": {},
|
|
|
"delayed": {},
|
|
|
}
|
|
|
if _, ok := allowedWhen[whenStr]; !ok {
|
|
|
endLine, endCol := computeEndPosition(job.Line, job.Column, whenStr)
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidWhen,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidWhen], job.Name, whenStr),
|
|
|
job.Line, job.Column, endLine, endCol)
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidWhen,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidWhen], job.Name, ""),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Validate "changes" field, if present.
|
|
|
if changesVal, exists := rule["changes"]; exists {
|
|
|
if changesVal == nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidChanges,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidChanges], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
} else {
|
|
|
switch v := changesVal.(type) {
|
|
|
case string:
|
|
|
if strings.TrimSpace(v) == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidChanges,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidChanges], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
case []interface{}:
|
|
|
if len(v) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidChanges,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidChanges], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
} else {
|
|
|
for _, item := range v {
|
|
|
if s, ok := item.(string); ok {
|
|
|
if strings.TrimSpace(s) == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidChanges,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidChanges], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
break
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidChanges,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidChanges], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidChanges,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidChanges], job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Validate allowed keys in the rule.
|
|
|
allowed := map[string]struct{}{
|
|
|
"if": {},
|
|
|
"when": {},
|
|
|
"start_in": {},
|
|
|
"allow_failure": {},
|
|
|
"changes": {},
|
|
|
"exists": {},
|
|
|
}
|
|
|
for key := range rule {
|
|
|
if _, ok := allowed[key]; !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrUnknownRulesKey,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrUnknownRulesKey], job.Name, key),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Name))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobWhen(job *Job, ve *errors.ValidationError) {
|
|
|
if job.When == "" {
|
|
|
return
|
|
|
}
|
|
|
allowed := map[string]struct{}{
|
|
|
"on_success": {},
|
|
|
"always": {},
|
|
|
"on_failure": {},
|
|
|
"never": {},
|
|
|
"delayed": {},
|
|
|
}
|
|
|
if _, ok := allowed[job.When]; !ok {
|
|
|
endLine, endCol := computeEndPosition(job.Line, job.Column, job.When)
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidWhen,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrInvalidWhen], job.Name, job.When),
|
|
|
job.Line, job.Column, endLine, endCol)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateExpressionSyntax(expr string) bool {
|
|
|
return expr != ""
|
|
|
}
|
|
|
|
|
|
func detectCycles(dependencyGraph map[string][]string) []string {
|
|
|
visited := make(map[string]bool)
|
|
|
recStack := make(map[string]bool)
|
|
|
var cycle []string
|
|
|
|
|
|
var dfs func(string) bool
|
|
|
dfs = func(node string) bool {
|
|
|
if recStack[node] {
|
|
|
cycle = append(cycle, node)
|
|
|
return true
|
|
|
}
|
|
|
if visited[node] {
|
|
|
return false
|
|
|
}
|
|
|
visited[node] = true
|
|
|
recStack[node] = true
|
|
|
|
|
|
for _, neighbor := range dependencyGraph[node] {
|
|
|
if dfs(neighbor) {
|
|
|
cycle = append(cycle, node)
|
|
|
return true
|
|
|
}
|
|
|
}
|
|
|
recStack[node] = false
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
for node := range dependencyGraph {
|
|
|
if !visited[node] {
|
|
|
if dfs(node) {
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return cycle
|
|
|
}
|
|
|
|
|
|
func validateIncludeRules(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Include == nil {
|
|
|
return
|
|
|
}
|
|
|
switch v := cfg.Include.(type) {
|
|
|
case map[string]interface{}:
|
|
|
validateIncludeRuleMap(v, ve)
|
|
|
case []interface{}:
|
|
|
for _, item := range v {
|
|
|
if m, ok := item.(map[string]interface{}); ok {
|
|
|
validateIncludeRuleMap(m, ve)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError) {
|
|
|
keysToCheck := []string{"exists", "changes"}
|
|
|
for _, key := range keysToCheck {
|
|
|
if val, exists := m[key]; exists {
|
|
|
switch val.(type) {
|
|
|
case string:
|
|
|
// ok
|
|
|
case []interface{}:
|
|
|
for _, item := range val.([]interface{}) {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrIncludeRulesInvalid,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrIncludeRulesInvalid], key),
|
|
|
0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrIncludeRulesInvalid,
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrIncludeRulesInvalid], key),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|