|
|
|
@ -31,19 +31,19 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
|
|
|
|
|
err := yaml.Unmarshal([]byte(content), &root)
|
|
|
|
|
if err != nil {
|
|
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
|
|
fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err),
|
|
|
|
|
fmt.Sprintf("YAML parsing error: %v", err),
|
|
|
|
|
0, 0, 0, 0)
|
|
|
|
|
return validationErr
|
|
|
|
|
}
|
|
|
|
|
if len(root.Content) == 0 {
|
|
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
|
|
"файл .gitlab-ci.yml пуст или не содержит допустимых данных",
|
|
|
|
|
".gitflame-ci.yml file is empty or does not contain valid data",
|
|
|
|
|
0, 0, 0, 0)
|
|
|
|
|
return validationErr
|
|
|
|
|
}
|
|
|
|
|
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
|
|
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
|
|
"некорректный формат YAML: ожидался корневой объект",
|
|
|
|
|
"invalid YAML format: a root object was expected",
|
|
|
|
|
0, 0, 0, 0)
|
|
|
|
|
return validationErr
|
|
|
|
|
}
|
|
|
|
@ -75,6 +75,7 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
|
|
|
|
|
validateStages(cfg, validationErr)
|
|
|
|
|
validateVariables(cfg, validationErr)
|
|
|
|
|
validateIncludeRules(cfg, validationErr)
|
|
|
|
|
checkFinalEmptyLine(content, validationErr)
|
|
|
|
|
|
|
|
|
|
if !validationErr.IsEmpty() {
|
|
|
|
|
return validationErr
|
|
|
|
@ -153,46 +154,123 @@ func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
// validateVariablesMap проверяет карту переменных varsMap, используя данные из YAML‑узла varsNode для получения позиций.
|
|
|
|
|
func validateVariablesMap(varsMap map[string]interface{}, varsNode *yaml.Node, ve *errors.ValidationError, posGetter func(varName string) (int, int)) {
|
|
|
|
|
allowedKeys := map[string]struct{}{
|
|
|
|
|
"value": {},
|
|
|
|
|
"description": {},
|
|
|
|
|
"expand": {},
|
|
|
|
|
"options": {},
|
|
|
|
|
}
|
|
|
|
|
for varName, val := range varsMap {
|
|
|
|
|
if isScalar(val) {
|
|
|
|
|
if len(varName) > MaxVariableNameLength {
|
|
|
|
|
ve.AddDetailWithSpan(errors.ErrVariableNameTooLong, fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName),
|
|
|
|
|
0, 0, 0, 0,
|
|
|
|
|
|
|
|
|
|
// Проходим по всем ключам, определённым в YAML‑узле
|
|
|
|
|
for i := 0; i < len(varsNode.Content)-1; i += 2 {
|
|
|
|
|
keyNode := varsNode.Content[i]
|
|
|
|
|
varName := keyNode.Value
|
|
|
|
|
// Получаем позицию для этой переменной через posGetter.
|
|
|
|
|
line, col := posGetter(varName)
|
|
|
|
|
|
|
|
|
|
// Проверяем длину имени переменной
|
|
|
|
|
if len(varName) > MaxVariableNameLength {
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrVariableNameTooLong,
|
|
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName),
|
|
|
|
|
line, col, line, col+len(varName),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Проверяем, что значение переменной присутствует и корректно
|
|
|
|
|
if val, exists := varsMap[varName]; exists {
|
|
|
|
|
if !isScalar(val) {
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrVariableInvalid,
|
|
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName),
|
|
|
|
|
line, col, line, col+len(varName),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} 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 nested, ok := val.(map[string]interface{}); ok {
|
|
|
|
|
var 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, ", ")),
|
|
|
|
|
line, col, line, col+len(varName),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(invalidKeys) > 0 {
|
|
|
|
|
ve.AddDetailWithSpan(errors.ErrVariablesInvalidKey,
|
|
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariablesInvalidKey], varName, strings.Join(invalidKeys, ", ")),
|
|
|
|
|
0, 0, 0, 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateJobVariables(job *Job, ve *errors.ValidationError) {
|
|
|
|
|
if job.VariablesNode == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if job.VariablesNode.Kind != yaml.MappingNode {
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrVariablesInvalid,
|
|
|
|
|
"job variables should be a map",
|
|
|
|
|
job.Line, job.Column, job.Line, job.Column+1,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
varsMap, ok := job.Variables.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrVariablesInvalid,
|
|
|
|
|
"job variables should be a map",
|
|
|
|
|
job.Line, job.Column, job.Line, job.Column+1,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
validateVariablesMap(varsMap, job.VariablesNode, ve, func(varName string) (int, int) {
|
|
|
|
|
// Ищем переменную в узле job.VariablesNode
|
|
|
|
|
for i := 0; i < len(job.VariablesNode.Content)-1; i += 2 {
|
|
|
|
|
keyNode := job.VariablesNode.Content[i]
|
|
|
|
|
if keyNode.Value == varName {
|
|
|
|
|
return keyNode.Line, keyNode.Column
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ve.AddDetailWithSpan(errors.ErrVariableInvalid,
|
|
|
|
|
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName),
|
|
|
|
|
0, 0, 0, 0)
|
|
|
|
|
}
|
|
|
|
|
return job.Line, job.Column // если не найдено, используем позицию job
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
|
|
if cfg.VariablesNode == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if cfg.VariablesNode.Kind != yaml.MappingNode {
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrVariablesInvalid,
|
|
|
|
|
errors.ErrorMessages[errors.ErrVariablesInvalid],
|
|
|
|
|
0, 0, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
varsMap, ok := cfg.Variables.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrVariablesInvalid,
|
|
|
|
|
errors.ErrorMessages[errors.ErrVariablesInvalid],
|
|
|
|
|
0, 0, 0, 0,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
validateVariablesMap(varsMap, cfg.VariablesNode, ve, func(varName string) (int, int) {
|
|
|
|
|
// Ищем в YAML узле переменной с именем varName
|
|
|
|
|
for i := 0; i < len(cfg.VariablesNode.Content)-1; i += 2 {
|
|
|
|
|
keyNode := cfg.VariablesNode.Content[i]
|
|
|
|
|
if keyNode.Value == varName {
|
|
|
|
|
return keyNode.Line, keyNode.Column
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0, 0
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isScalar(val interface{}) bool {
|
|
|
|
@ -335,6 +413,7 @@ func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
|
|
validateJobRules(job, ve)
|
|
|
|
|
validateJobWhen(job, ve)
|
|
|
|
|
validateJobOnly(job, ve)
|
|
|
|
|
validateJobVariables(job, ve)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if visibleCount == 0 {
|
|
|
|
@ -798,3 +877,20 @@ func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkFinalEmptyLine проверяет, что файл заканчивается пустой строкой.
|
|
|
|
|
func checkFinalEmptyLine(content string, ve *errors.ValidationError) {
|
|
|
|
|
// Если содержимое не заканчивается символом новой строки...
|
|
|
|
|
if !strings.HasSuffix(content, "\n") {
|
|
|
|
|
lines := strings.Split(content, "\n")
|
|
|
|
|
lastLineIndex := len(lines)
|
|
|
|
|
lastLineContent := lines[lastLineIndex-1]
|
|
|
|
|
lineNum := lastLineIndex
|
|
|
|
|
colNum := len(lastLineContent) + 1
|
|
|
|
|
ve.AddDetailWithSpan(
|
|
|
|
|
errors.ErrMissingFinalNewline,
|
|
|
|
|
errors.ErrorMessages[errors.ErrMissingFinalNewline],
|
|
|
|
|
lineNum, colNum, lineNum, colNum,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|