helpers/gitlab_ci_yaml_parser/parser.go (314 lines of code) (raw):

package gitlab_ci_yaml_parser import ( "errors" "fmt" "io/ioutil" "strconv" "strings" "gitlab.com/gitlab-org/gitlab-ci-multi-runner/common" "gopkg.in/yaml.v2" ) type GitLabCiYamlParser struct { filename string jobName string config DataBag jobConfig DataBag } func (c *GitLabCiYamlParser) parseFile() (err error) { data, err := ioutil.ReadFile(c.filename) if err != nil { return err } config := make(DataBag) err = yaml.Unmarshal(data, config) if err != nil { return err } err = config.Sanitize() if err != nil { return err } c.config = config return } func (c *GitLabCiYamlParser) loadJob() (err error) { jobConfig, ok := c.config.GetSubOptions(c.jobName) if !ok { return fmt.Errorf("no job named %q", c.jobName) } c.jobConfig = jobConfig return } func (c *GitLabCiYamlParser) prepareJobInfo(job *common.JobResponse) (err error) { job.JobInfo = common.JobInfo{ Name: c.jobName, } if stage, ok := c.jobConfig.GetString("stage"); ok { job.JobInfo.Stage = stage } else { job.JobInfo.Stage = "test" } return } func (c *GitLabCiYamlParser) getCommands(commands interface{}) (common.StepScript, error) { if lines, ok := commands.([]interface{}); ok { var steps common.StepScript for _, line := range lines { if lineText, ok := line.(string); ok { steps = append(steps, lineText) } else { return common.StepScript{}, errors.New("unsupported script") } } return steps, nil } else if text, ok := commands.(string); ok { return common.StepScript(strings.Split(text, "\n")), nil } else if commands != nil { return common.StepScript{}, errors.New("unsupported script") } return common.StepScript{}, nil } func (c *GitLabCiYamlParser) prepareSteps(job *common.JobResponse) (err error) { if c.jobConfig["script"] == nil { err = fmt.Errorf("missing 'script' for job") return } var scriptCommands, afterScriptCommands common.StepScript // get before_script beforeScript, err := c.getCommands(c.config["before_script"]) if err != nil { return } // get job before_script jobBeforeScript, err := c.getCommands(c.jobConfig["before_script"]) if err != nil { return } if len(jobBeforeScript) < 1 { scriptCommands = beforeScript } else { scriptCommands = jobBeforeScript } // get script script, err := c.getCommands(c.jobConfig["script"]) if err != nil { return } scriptCommands = append(scriptCommands, script...) afterScriptCommands, err = c.getCommands(c.jobConfig["after_script"]) if err != nil { return } job.Steps = common.Steps{ common.Step{ Name: common.StepNameScript, Script: scriptCommands, Timeout: 3600, When: common.StepWhenOnSuccess, AllowFailure: false, }, common.Step{ Name: common.StepNameAfterScript, Script: afterScriptCommands, Timeout: 3600, When: common.StepWhenAlways, AllowFailure: false, }, } return } func (c *GitLabCiYamlParser) buildDefaultVariables(job *common.JobResponse) (defaultVariables common.JobVariables, err error) { defaultVariables = common.JobVariables{ {Key: "CI", Value: "true", Public: true, Internal: true, File: false}, {Key: "GITLAB_CI", Value: "true", Public: true, Internal: true, File: false}, {Key: "CI_SERVER_NAME", Value: "GitLab CI", Public: true, Internal: true, File: false}, {Key: "CI_SERVER_VERSION", Value: "", Public: true, Internal: true, File: false}, {Key: "CI_SERVER_REVISION", Value: "", Public: true, Internal: true, File: false}, {Key: "CI_PROJECT_ID", Value: strconv.Itoa(job.JobInfo.ProjectID), Public: true, Internal: true, File: false}, {Key: "CI_JOB_ID", Value: strconv.Itoa(job.ID), Public: true, Internal: true, File: false}, {Key: "CI_JOB_NAME", Value: job.JobInfo.Name, Public: true, Internal: true, File: false}, {Key: "CI_JOB_STAGE", Value: job.JobInfo.Stage, Public: true, Internal: true, File: false}, {Key: "CI_JOB_TOKEN", Value: job.Token, Public: true, Internal: true, File: false}, {Key: "CI_REPOSITORY_URL", Value: job.GitInfo.RepoURL, Public: true, Internal: true, File: false}, {Key: "CI_COMMIT_REF", Value: job.GitInfo.Sha, Public: true, Internal: true, File: false}, {Key: "CI_COMMIT_BEFORE_SHA", Value: job.GitInfo.BeforeSha, Public: true, Internal: true, File: false}, {Key: "CI_COMMIT_REF_NAME", Value: job.GitInfo.Ref, Public: true, Internal: true, File: false}, } return } func (c *GitLabCiYamlParser) buildVariables(configVariables interface{}) (buildVariables common.JobVariables, err error) { if variables, ok := configVariables.(map[string]interface{}); ok { for key, value := range variables { if valueText, ok := value.(string); ok { buildVariables = append(buildVariables, common.JobVariable{ Key: key, Value: valueText, Public: true, }) } else { err = fmt.Errorf("invalid value for variable %q", key) } } } else if configVariables != nil { err = errors.New("unsupported variables") } return } func (c *GitLabCiYamlParser) prepareVariables(job *common.JobResponse) (err error) { job.Variables = common.JobVariables{} defaultVariables, err := c.buildDefaultVariables(job) if err != nil { return } job.Variables = append(job.Variables, defaultVariables...) globalVariables, err := c.buildVariables(c.config["variables"]) if err != nil { return } job.Variables = append(job.Variables, globalVariables...) jobVariables, err := c.buildVariables(c.jobConfig["variables"]) if err != nil { return } job.Variables = append(job.Variables, jobVariables...) return } func (c *GitLabCiYamlParser) prepareImage(job *common.JobResponse) (err error) { job.Image = common.Image{} if imageName, ok := c.jobConfig.GetString("image"); ok { job.Image.Name = imageName return } if imageDefinition, ok := c.jobConfig.GetSubOptions("image"); ok { job.Image.Name, _ = imageDefinition.GetString("name") job.Image.Entrypoint, _ = imageDefinition.GetStringSlice("entrypoint") return } if imageName, ok := c.config.GetString("image"); ok { job.Image.Name = imageName return } if imageDefinition, ok := c.config.GetSubOptions("image"); ok { job.Image.Name, _ = imageDefinition.GetString("name") job.Image.Entrypoint, _ = imageDefinition.GetStringSlice("entrypoint") return } return } func parseExtendedServiceDefinitionMap(serviceDefinition map[interface{}]interface{}) (image common.Image) { service := make(DataBag) for key, value := range serviceDefinition { service[key.(string)] = value } image.Name, _ = service.GetString("name") image.Alias, _ = service.GetString("alias") image.Command, _ = service.GetStringSlice("command") image.Entrypoint, _ = service.GetStringSlice("entrypoint") return } func (c *GitLabCiYamlParser) prepareServices(job *common.JobResponse) (err error) { job.Services = common.Services{} if servicesMap, ok := getOptions("services", c.jobConfig, c.config); ok { for _, service := range servicesMap { if serviceName, ok := service.(string); ok { job.Services = append(job.Services, common.Image{ Name: serviceName, }) continue } if serviceDefinition, ok := service.(map[interface{}]interface{}); ok { job.Services = append(job.Services, parseExtendedServiceDefinitionMap(serviceDefinition)) } } } return } func (c *GitLabCiYamlParser) prepareArtifacts(job *common.JobResponse) (err error) { var ok bool artifactsMap := getOptionsMap("artifacts", c.jobConfig, c.config) artifactsPaths, _ := artifactsMap.GetSlice("paths") paths := common.ArtifactPaths{} for _, path := range artifactsPaths { paths = append(paths, path.(string)) } var artifactsName string if artifactsName, ok = artifactsMap.GetString("name"); !ok { artifactsName = "" } var artifactsUntracked interface{} if artifactsUntracked, ok = artifactsMap.Get("untracked"); !ok { artifactsUntracked = false } var artifactsWhen string if artifactsWhen, ok = artifactsMap.GetString("when"); !ok { artifactsWhen = string(common.ArtifactWhenOnSuccess) } var artifactsExpireIn string if artifactsExpireIn, ok = artifactsMap.GetString("expireIn"); !ok { artifactsExpireIn = "" } job.Artifacts = make(common.Artifacts, 1) job.Artifacts[0] = common.Artifact{ Name: artifactsName, Untracked: artifactsUntracked.(bool), Paths: paths, When: common.ArtifactWhen(artifactsWhen), ExpireIn: artifactsExpireIn, } return } func (c *GitLabCiYamlParser) prepareCache(job *common.JobResponse) (err error) { var ok bool cacheMap := getOptionsMap("cache", c.jobConfig, c.config) cachePaths, _ := cacheMap.GetSlice("paths") paths := common.ArtifactPaths{} for _, path := range cachePaths { paths = append(paths, path.(string)) } var cacheKey string if cacheKey, ok = cacheMap.GetString("key"); !ok { cacheKey = "" } var cacheUntracked interface{} if cacheUntracked, ok = cacheMap.Get("untracked"); !ok { cacheUntracked = false } job.Cache = make(common.Caches, 1) job.Cache[0] = common.Cache{ Key: cacheKey, Untracked: cacheUntracked.(bool), Paths: paths, } return } func (c *GitLabCiYamlParser) ParseYaml(job *common.JobResponse) (err error) { err = c.parseFile() if err != nil { return err } err = c.loadJob() if err != nil { return err } parsers := []struct { method func(job *common.JobResponse) error }{ {c.prepareJobInfo}, {c.prepareSteps}, {c.prepareVariables}, {c.prepareImage}, {c.prepareServices}, {c.prepareArtifacts}, {c.prepareCache}, } for _, parser := range parsers { err = parser.method(job) if err != nil { return err } } return nil } func NewGitLabCiYamlParser(jobName string) *GitLabCiYamlParser { return &GitLabCiYamlParser{ filename: ".gitlab-ci.yml", jobName: jobName, } }