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.
185 lines
5.5 KiB
Go
185 lines
5.5 KiB
Go
package gitlabcivalidator
|
|
|
|
import (
|
|
"fmt"
|
|
"gopkg.in/yaml.v3"
|
|
"tea.gitpark.ru/Azaki/CI_VALIDATOR/errors"
|
|
)
|
|
|
|
// ParseGitLabCIConfig parses the YAML and returns the GitLabCIConfig structure.
|
|
func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConfig, error) {
|
|
var config GitLabCIConfig
|
|
var root yaml.Node
|
|
|
|
// Parse YAML into the root node.
|
|
err := yaml.Unmarshal(data, &root)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("YAML syntax error: %w", err)
|
|
}
|
|
if len(root.Content) == 0 {
|
|
return nil, fmt.Errorf(".gitlab-ci.yml file is empty or does not contain valid data")
|
|
}
|
|
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
|
|
return nil, fmt.Errorf("invalid YAML format: expected a root object")
|
|
}
|
|
mainNode := root.Content[0]
|
|
|
|
// Decode known top-level keys into config.
|
|
err = mainNode.Decode(&config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode GitLab CI configuration: %w", err)
|
|
}
|
|
|
|
// Allowed top-level keys that are NOT jobs.
|
|
allowedKeys := map[string]struct{}{
|
|
"default": {},
|
|
"include": {},
|
|
"before_script": {},
|
|
"image": {},
|
|
"services": {},
|
|
"after_script": {},
|
|
"variables": {},
|
|
"stages": {},
|
|
"cache": {},
|
|
"workflow": {},
|
|
"changes": {},
|
|
"paths": {},
|
|
}
|
|
|
|
// Initialize config.Jobs if not already set.
|
|
if config.Jobs == nil {
|
|
config.Jobs = make(map[string]*Job)
|
|
}
|
|
|
|
// Now, iterate over the top-level mapping keys.
|
|
if mainNode.Kind == yaml.MappingNode {
|
|
for i := 0; i < len(mainNode.Content)-1; i += 2 {
|
|
keyNode := mainNode.Content[i]
|
|
valNode := mainNode.Content[i+1]
|
|
|
|
if keyNode.Value == "variables" {
|
|
config.VariablesNode = valNode
|
|
}
|
|
// If the key is not one of the allowed top-level keys, treat it as a job.
|
|
if _, ok := allowedKeys[keyNode.Value]; !ok {
|
|
// Create a new Job.
|
|
job := &Job{
|
|
Name: keyNode.Value,
|
|
Line: keyNode.Line,
|
|
Column: keyNode.Column,
|
|
DependencyLines: make(map[string]struct{ Line, Column int }),
|
|
}
|
|
// Even if the job value is not a mapping node, add the job to config for further validation.
|
|
if valNode.Kind == yaml.MappingNode {
|
|
// Optionally, you can validate unknown keys inside the job mapping here.
|
|
// For example:
|
|
validateJobMapping(valNode, ve)
|
|
// Decode the job mapping.
|
|
err = valNode.Decode(job)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode job '%s': %w", job.Name, err)
|
|
}
|
|
// Process inner fields manually (e.g. dependencies, needs, only, etc.)
|
|
for d := 0; d < len(valNode.Content)-1; d += 2 {
|
|
depKey := valNode.Content[d]
|
|
depVal := valNode.Content[d+1]
|
|
switch depKey.Value {
|
|
case "dependencies":
|
|
if 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}
|
|
}
|
|
}
|
|
case "needs":
|
|
if depVal.Kind == yaml.SequenceNode {
|
|
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
|
|
}{needNode.Line, needNode.Column}
|
|
}
|
|
}
|
|
case "only":
|
|
if depVal.Kind == yaml.ScalarNode {
|
|
job.Only = depVal.Value
|
|
} else if depVal.Kind == yaml.SequenceNode {
|
|
var arr []string
|
|
for k := 0; k < len(depVal.Content); k++ {
|
|
arr = append(arr, depVal.Content[k].Value)
|
|
}
|
|
job.Only = arr
|
|
}
|
|
job.OnlyLine = depKey.Line
|
|
job.OnlyColumn = depKey.Column
|
|
case "variables":
|
|
// Заполняем поля переменных для job.
|
|
job.VariablesNode = depVal
|
|
var vars map[string]interface{}
|
|
if err := depVal.Decode(&vars); err != nil {
|
|
return nil, fmt.Errorf("failed to decode variables for job '%s': %w", job.Name, err)
|
|
}
|
|
job.Variables = vars
|
|
}
|
|
}
|
|
} else {
|
|
// If the job value is not a mapping, leave job fields as default.
|
|
}
|
|
// Save the job in config.
|
|
config.Jobs[job.Name] = job
|
|
}
|
|
}
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// findUnknownRootKeysWithSpan scans the mainNode for unknown keys and returns their information.
|
|
func findUnknownRootKeysWithSpan(mainNode *yaml.Node) []errors.UnknownKeyError {
|
|
allowedKeys := map[string]struct{}{
|
|
"default": {},
|
|
"include": {},
|
|
//"before_script": {},
|
|
"image": {},
|
|
"services": {},
|
|
"after_script": {},
|
|
"variables": {},
|
|
"stages": {},
|
|
"cache": {},
|
|
"workflow": {},
|
|
"jobs": {},
|
|
"changes": {},
|
|
"paths": {},
|
|
}
|
|
|
|
var unknowns []errors.UnknownKeyError
|
|
if mainNode.Kind != yaml.MappingNode {
|
|
return unknowns
|
|
}
|
|
|
|
for i := 0; i < len(mainNode.Content)-1; i += 2 {
|
|
keyNode := mainNode.Content[i]
|
|
if _, ok := allowedKeys[keyNode.Value]; !ok && keyNode.Value == "before_script" {
|
|
endLine, endCol := keyNode.Line, keyNode.Column+len(keyNode.Value)
|
|
unknowns = append(unknowns, errors.UnknownKeyError{
|
|
Key: keyNode.Value,
|
|
Line: keyNode.Line,
|
|
Column: keyNode.Column,
|
|
EndLine: endLine,
|
|
EndColumn: endCol,
|
|
})
|
|
}
|
|
}
|
|
return unknowns
|
|
}
|