You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ci_validator/gitlabcivalidator/validator.go

789 lines
25 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)
}
}
}
}