|
|
package gitlabcivalidator
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
"gopkg.in/yaml.v3"
|
|
|
)
|
|
|
|
|
|
// ParseGitLabCIConfig разбирает YAML и возвращает структуру GitLabCIConfig.
|
|
|
func ParseGitLabCIConfig(data []byte) (*GitLabCIConfig, error) {
|
|
|
var config GitLabCIConfig
|
|
|
var root yaml.Node
|
|
|
|
|
|
// Парсим YAML в узел root
|
|
|
err := yaml.Unmarshal(data, &root)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("ошибка синтаксического анализа YAML: %w", err)
|
|
|
}
|
|
|
|
|
|
// Если root пустой, значит YAML-файл был пуст или некорректен
|
|
|
if len(root.Content) == 0 {
|
|
|
return nil, fmt.Errorf("файл .gitlab-ci.yml пуст или не содержит допустимых данных")
|
|
|
}
|
|
|
|
|
|
// Ожидаем, что корневой узел — это YAML-документ
|
|
|
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
|
|
|
return nil, fmt.Errorf("некорректный формат YAML: ожидался корневой объект")
|
|
|
}
|
|
|
|
|
|
// Основной объект — первый дочерний элемент
|
|
|
mainNode := root.Content[0]
|
|
|
|
|
|
// Здесь выполняем проверку ключей
|
|
|
unknownKeys := findUnknownRootKeys(mainNode)
|
|
|
if len(unknownKeys) > 0 {
|
|
|
return nil, fmt.Errorf("contains unknown keys: %v", unknownKeys)
|
|
|
}
|
|
|
|
|
|
// Декодируем root-объект в структуру
|
|
|
err = mainNode.Decode(&config)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("не удалось декодировать конфигурацию GitLab CI: %w", err)
|
|
|
}
|
|
|
|
|
|
// Ищем узел `jobs` вручную
|
|
|
for i := 0; i < len(mainNode.Content)-1; i += 2 {
|
|
|
keyNode := mainNode.Content[i]
|
|
|
valNode := mainNode.Content[i+1]
|
|
|
|
|
|
if keyNode.Value == "jobs" && valNode.Kind == yaml.MappingNode {
|
|
|
// `jobs` найден, разбираем его вручную
|
|
|
config.Jobs = make(map[string]*Job)
|
|
|
|
|
|
for j := 0; j < len(valNode.Content)-1; j += 2 {
|
|
|
jobKeyNode := valNode.Content[j] // Имя job'а
|
|
|
jobValNode := valNode.Content[j+1] // Значение job'а (массив или объект)
|
|
|
|
|
|
if jobValNode.Kind == yaml.MappingNode {
|
|
|
job := &Job{
|
|
|
Name: jobKeyNode.Value,
|
|
|
Line: jobKeyNode.Line,
|
|
|
Column: jobKeyNode.Column,
|
|
|
DependencyLines: make(map[string]struct{ Line, Column int }),
|
|
|
}
|
|
|
|
|
|
// Декодируем содержимое job
|
|
|
err := jobValNode.Decode(job)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("не удалось декодировать job '%s': %w", job.Name, err)
|
|
|
}
|
|
|
|
|
|
for d := 0; d < len(jobValNode.Content)-1; d += 2 {
|
|
|
depKey := jobValNode.Content[d]
|
|
|
depVal := jobValNode.Content[d+1]
|
|
|
|
|
|
if depKey.Value == "dependencies" && depVal.Kind == yaml.SequenceNode {
|
|
|
for k := 0; k < len(depVal.Content); k++ {
|
|
|
depNode := depVal.Content[k]
|
|
|
job.Dependencies = append(job.Dependencies, depNode.Value)
|
|
|
job.DependencyLines[depNode.Value] = struct {
|
|
|
Line int
|
|
|
Column int
|
|
|
}{depNode.Line, depNode.Column}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if depKey.Value == "needs" && depVal.Kind == yaml.SequenceNode {
|
|
|
// Инициализируем NeedLines, если ещё не инициализировано.
|
|
|
job.NeedLines = make(map[string]struct {
|
|
|
Line int
|
|
|
Column int
|
|
|
})
|
|
|
for k := 0; k < len(depVal.Content); k++ {
|
|
|
needNode := depVal.Content[k]
|
|
|
job.Needs = append(job.Needs, needNode.Value)
|
|
|
job.NeedLines[needNode.Value] = struct {
|
|
|
Line int
|
|
|
Column int
|
|
|
}{
|
|
|
Line: needNode.Line,
|
|
|
Column: needNode.Column,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Сохраняем job в конфигурации
|
|
|
config.Jobs[job.Name] = job
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return &config, nil
|
|
|
}
|
|
|
|
|
|
func findUnknownRootKeys(mainNode *yaml.Node) []string {
|
|
|
// Разрешённые ключи корневого объекта
|
|
|
allowedKeys := map[string]struct{}{
|
|
|
"default": {},
|
|
|
"include": {},
|
|
|
"before_script": {},
|
|
|
"image": {},
|
|
|
"services": {},
|
|
|
"after_script": {},
|
|
|
"variables": {},
|
|
|
"stages": {},
|
|
|
"cache": {},
|
|
|
"workflow": {},
|
|
|
"jobs": {},
|
|
|
"changes": {},
|
|
|
"paths": {},
|
|
|
}
|
|
|
|
|
|
var unknownKeys []string
|
|
|
if mainNode.Kind != yaml.MappingNode {
|
|
|
return unknownKeys
|
|
|
}
|
|
|
|
|
|
// В YAML Mapping узле ключи находятся в нечетных индексах
|
|
|
for i := 0; i < len(mainNode.Content)-1; i += 2 {
|
|
|
keyNode := mainNode.Content[i]
|
|
|
if _, ok := allowedKeys[keyNode.Value]; !ok {
|
|
|
unknownKeys = append(unknownKeys, keyNode.Value)
|
|
|
}
|
|
|
}
|
|
|
return unknownKeys
|
|
|
}
|