blob: 01c79f99522b7beb38e253cd7bad4a66f2bf64a4 [file] [log] [blame]
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
}