| package config |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| "strings" |
| |
| "go.skia.org/infra/go/allowed" |
| "go.skia.org/infra/go/gerrit" |
| "go.skia.org/infra/go/gitiles" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/skcq/go/codereview" |
| "go.skia.org/infra/task_scheduler/go/specs" |
| ) |
| |
| const ( |
| SkCQCfgPath = "infra/skcq.json" |
| ) |
| |
| // ConfigNotFoundError is returned when a cfg is not found in the repo+branch. |
| type ConfigNotFoundError struct { |
| configPath string |
| repo string |
| branch string |
| } |
| |
| func (e *ConfigNotFoundError) Error() string { |
| return fmt.Sprintf("Config %s not found in %s/%s", e.configPath, e.repo, e.branch) |
| } |
| |
| func IsNotFound(err error) bool { |
| _, ok := err.(*ConfigNotFoundError) |
| return ok |
| } |
| |
| // CannotModifyCfgsOnTheFlyError is returned when the owner of the change does |
| // not have permission to modify the cfg. |
| type CannotModifyCfgsOnTheFlyError struct { |
| issueID int64 |
| issueOwner string |
| } |
| |
| func (e *CannotModifyCfgsOnTheFlyError) Error() string { |
| return fmt.Sprintf("Config was modified in %d but the owner %s does not have permission to run it", e.issueID, e.issueOwner) |
| } |
| |
| func IsCannotModifyCfgsOnTheFly(err error) bool { |
| _, ok := err.(*CannotModifyCfgsOnTheFlyError) |
| return ok |
| } |
| |
| // ConfigReader is an interface to read configs for SkCQ. Useful for testing. |
| type ConfigReader interface { |
| // GetSkCQCfg reads the SkCQ cfg file from CL's ref if it was modified, else |
| // it reads it from HEAD. |
| GetSkCQCfg(ctx context.Context) (*SkCQCfg, error) |
| |
| // GetTasksCfg reads the Tasks.json file from CL's ref if it was modified, else |
| // it reads it from HEAD. |
| GetTasksCfg(ctx context.Context, tasksJSONPath string) (*specs.TasksCfg, error) |
| |
| // GetAuthorsFileContents reads the AUTHORS file from CL's ref if it was modified, |
| // else it reads it from HEAD. |
| GetAuthorsFileContents(ctx context.Context, authorsPath string) (string, error) |
| } |
| |
| // GitilesConfigReader is an implementation of ConfigReader interface. |
| type GitilesConfigReader struct { |
| gitilesRepo gitiles.GitilesRepo |
| ci *gerrit.ChangeInfo |
| cr codereview.CodeReview |
| changedFiles []string |
| canModifyCfgsOnTheFly allowed.Allow |
| } |
| |
| // NewGitilesConfigReader returns an instance of GitilesConfigReader. |
| func NewGitilesConfigReader(ctx context.Context, httpClient *http.Client, ci *gerrit.ChangeInfo, cr codereview.CodeReview, canModifyCfgsOnTheFly allowed.Allow) (*GitilesConfigReader, error) { |
| gitilesRepo := gitiles.NewRepo(cr.GetRepoUrl(ci), httpClient) |
| changedFiles, err := cr.GetFileNames(ctx, ci) |
| if err != nil { |
| return nil, skerr.Fmt("Not able to get changed files for %d: %s", ci.Issue, err) |
| } |
| return &GitilesConfigReader{ |
| gitilesRepo: gitilesRepo, |
| cr: cr, |
| ci: ci, |
| changedFiles: changedFiles, |
| canModifyCfgsOnTheFly: canModifyCfgsOnTheFly, |
| }, nil |
| } |
| |
| // GetSkCQCfg implements the ConfigReader interface. |
| func (gc *GitilesConfigReader) GetSkCQCfg(ctx context.Context) (*SkCQCfg, error) { |
| // If SkCQ cfg is in list of changed files then use that. Else use from HEAD. |
| contents, modifiedInCL, err := gc.getFileContents(ctx, SkCQCfgPath) |
| if err != nil { |
| return nil, err |
| } |
| if modifiedInCL && !gc.canModifyCfgsOnTheFly.Member(gc.ci.Owner.Email) { |
| return nil, &CannotModifyCfgsOnTheFlyError{ |
| issueID: gc.ci.Issue, |
| issueOwner: gc.ci.Owner.Email, |
| } |
| } |
| cfg, err := ParseSkCQCfg(contents) |
| if err != nil { |
| return nil, skerr.Fmt("Error when parsing SkCQ cfg: %s", err) |
| } |
| if err := cfg.Validate(); err != nil { |
| return nil, skerr.Wrapf(err, "Error validating SkCQ cfg") |
| } |
| return cfg, nil |
| } |
| |
| // GetTasksCfg implements the ConfigReader interface. |
| func (gc *GitilesConfigReader) GetTasksCfg(ctx context.Context, tasksJSONPath string) (*specs.TasksCfg, error) { |
| // If tasks.json is in list of changed files then use that. Else use from HEAD. |
| contents, _, err := gc.getFileContents(ctx, tasksJSONPath) |
| if err != nil { |
| return nil, err |
| } |
| cfg, err := specs.ParseTasksCfg(contents) |
| if err != nil { |
| return nil, skerr.Fmt("Error when parsing tasks.json cfg: %s", err) |
| } |
| return cfg, nil |
| } |
| |
| // GetAuthorsFileContents implements the ConfigReader interface. |
| func (gc *GitilesConfigReader) GetAuthorsFileContents(ctx context.Context, authorsPath string) (string, error) { |
| // If AUTHORS is in list of changed files then use that. Else use from HEAD. |
| contents, _, err := gc.getFileContents(ctx, authorsPath) |
| if err != nil { |
| return "", err |
| } |
| return contents, nil |
| } |
| |
| // getFileContents checks to see if the CL has modified the file and returns those contents. |
| // If the file has not been modified then it returns the file contents from HEAD. |
| func (gc *GitilesConfigReader) getFileContents(ctx context.Context, cfgPath string) (string, bool, error) { |
| ref := gc.ci.Branch |
| modifiedInCL := false |
| for _, f := range gc.changedFiles { |
| if f == cfgPath { |
| ref = gc.cr.GetChangeRef(gc.ci) |
| sklog.Infof("[%d] Has modified %s. Using the modified version from %s.", gc.ci.Issue, cfgPath, ref) |
| modifiedInCL = true |
| break |
| } |
| } |
| contents, err := gc.gitilesRepo.ReadFileAtRef(ctx, cfgPath, ref) |
| if err != nil { |
| if strings.Contains(err.Error(), "NOT_FOUND") { |
| return "", modifiedInCL, &ConfigNotFoundError{ |
| configPath: cfgPath, |
| repo: gc.ci.Project, |
| branch: gc.ci.Branch, |
| } |
| } |
| return "", modifiedInCL, skerr.Fmt("Failed to read %s: %s", cfgPath, err) |
| } |
| if !modifiedInCL { |
| sklog.Infof("[%d] has not modified %s. Using the version from HEAD.", gc.ci.Issue, cfgPath) |
| } |
| return string(contents), modifiedInCL, nil |
| } |