blob: a5c38ac75c361fbc78ef650295cfbf5d39165110 [file] [log] [blame]
package roller
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/flynn/json5"
"go.skia.org/infra/autoroll/go/codereview"
arb_notifier "go.skia.org/infra/autoroll/go/notifier"
"go.skia.org/infra/autoroll/go/repo_manager"
"go.skia.org/infra/go/human"
"go.skia.org/infra/go/notifier"
"go.skia.org/infra/go/util"
)
const (
// Throttling parameters.
DEFAULT_SAFETY_THROTTLE_ATTEMPT_COUNT = 3
DEFAULT_SAFETY_THROTTLE_TIME_WINDOW = 30 * time.Minute
ROLLER_TYPE_ASSET = "asset"
ROLLER_TYPE_AFDO = "afdo"
ROLLER_TYPE_ANDROID = "android"
ROLLER_TYPE_COPY = "copy"
ROLLER_TYPE_DEPS = "deps"
ROLLER_TYPE_DEPS_NO_CHECKOUT = "noCheckoutDEPS"
ROLLER_TYPE_FUCHSIA_SDK = "fuchsiaSDK"
ROLLER_TYPE_GITHUB = "github"
ROLLER_TYPE_GITHUB_DEPS = "githubDEPS"
ROLLER_TYPE_GOOGLE3 = "google3"
ROLLER_TYPE_INVALID = "INVALID"
ROLLER_TYPE_MANIFEST = "manifest"
)
var (
SAFETY_THROTTLE_CONFIG_DEFAULT = &ThrottleConfig{
AttemptCount: DEFAULT_SAFETY_THROTTLE_ATTEMPT_COUNT,
TimeWindow: DEFAULT_SAFETY_THROTTLE_TIME_WINDOW,
}
)
// ThrottleConfig determines the throttling behavior for the roller.
type ThrottleConfig struct {
AttemptCount int64
TimeWindow time.Duration
}
// Intermediate struct used for JSON [un]marshaling.
type throttleConfigJSON struct {
AttemptCount int64 `json:"attemptCount"`
TimeWindow string `json:"timeWindow"`
}
// See documentation for json.Marshaler interface.
func (c *ThrottleConfig) MarshalJSON() ([]byte, error) {
tc := throttleConfigJSON{
AttemptCount: c.AttemptCount,
TimeWindow: strings.TrimSpace(human.Duration(c.TimeWindow)),
}
return json.Marshal(&tc)
}
// See documentation for json.Unmarshaler interface.
func (c *ThrottleConfig) UnmarshalJSON(b []byte) error {
var tc throttleConfigJSON
if err := json5.Unmarshal(b, &tc); err != nil {
return err
}
dur, err := human.ParseDuration(tc.TimeWindow)
if err != nil {
return err
}
c.AttemptCount = tc.AttemptCount
c.TimeWindow = dur
return nil
}
// Dummy repo manager config used to indicate a Google3 roller.
type Google3FakeRepoManagerConfig struct {
// Branch of the child repo to roll.
ChildBranch string `json:"childBranch"`
// URL of the child repo.
ChildRepo string `json:"childRepo"`
}
// See documentation for util.Validator interface.
func (c *Google3FakeRepoManagerConfig) Validate() error {
if c.ChildBranch == "" {
return errors.New("ChildBranch is required.")
}
if c.ChildRepo == "" {
return errors.New("ChildRepo is required.")
}
return nil
}
// KubernetesConfig contains configuration information for an AutoRoller running
// in Kubernetes.
type KubernetesConfig struct {
// Requested number of CPUs.
CPU string `json:"cpu"`
// Requested memory, eg. "2Gi"
Memory string `json:"memory"`
// Requested disk size, eg. "200Gi"
Disk string `json:"disk"`
}
// Validate the KubernetesConfig.
func (c *KubernetesConfig) Validate() error {
// KubernetesConfig is optional for now.
if c == nil {
return nil
}
if c.CPU == "" {
return errors.New("CPU is required.")
}
if c.Memory == "" {
return errors.New("Memory is required.")
}
if c.Disk == "" {
return errors.New("Disk is required.")
}
return nil
}
// AutoRollerConfig contains configuration information for an AutoRoller.
type AutoRollerConfig struct {
// Required Fields.
// User friendly name of the child repo.
ChildName string `json:"childName"`
// List of email addresses of contacts for this roller, used for sending
// PSAs, asking questions, etc.
Contacts []string `json:"contacts"`
// If true, the roller is only visible to Googlers.
IsInternal bool `json:"isInternal"`
// User friendly name of the parent repo.
ParentName string `json:"parentName"`
// URL of the waterfall/status display for the parent repo.
ParentWaterfall string `json:"parentWaterfall"`
// Name of the roller, used for database keys.
RollerName string `json:"rollerName"`
// Full email address of the service account for this roller.
ServiceAccount string `json:"serviceAccount"`
// Email addresses to CC on rolls, or URL from which to obtain those
// email addresses.
Sheriff []string `json:"sheriff"`
// Backup email addresses to CC on rolls, in case obtaining the email
// addresses from the URL fails. Only required if a URL is specified
// for Sheriff.
SheriffBackup []string `json:"sheriffBackup,omitempty"`
// Code review settings.
Gerrit *codereview.GerritConfig `json:"gerrit,omitempty"`
Github *codereview.GithubConfig `json:"github,omitempty"`
Google3Review *codereview.Google3Config `json:"google3Review,omitempty"`
// RepoManager configs. Exactly one must be provided.
AFDORepoManager *repo_manager.AFDORepoManagerConfig `json:"afdoRepoManager,omitempty"`
AndroidRepoManager *repo_manager.AndroidRepoManagerConfig `json:"androidRepoManager,omitempty"`
AssetRepoManager *repo_manager.AssetRepoManagerConfig `json:"assetRepoManager,omitempty"`
CopyRepoManager *repo_manager.CopyRepoManagerConfig `json:"copyRepoManager,omitempty"`
DEPSRepoManager *repo_manager.DEPSRepoManagerConfig `json:"depsRepoManager,omitempty"`
FuchsiaSDKAndroidRepoManager *repo_manager.FuchsiaSDKAndroidRepoManagerConfig `json:"fuchsiaSDKAndroidRepoManager,omitempty"`
FuchsiaSDKRepoManager *repo_manager.FuchsiaSDKRepoManagerConfig `json:"fuchsiaSDKRepoManager,omitempty"`
GithubRepoManager *repo_manager.GithubRepoManagerConfig `json:"githubRepoManager,omitempty"`
GithubDEPSRepoManager *repo_manager.GithubDEPSRepoManagerConfig `json:"githubDEPSRepoManager,omitempty"`
Google3RepoManager *Google3FakeRepoManagerConfig `json:"google3,omitempty"`
ManifestRepoManager *repo_manager.ManifestRepoManagerConfig `json:"manifestRepoManager,omitempty"`
NoCheckoutDEPSRepoManager *repo_manager.NoCheckoutDEPSRepoManagerConfig `json:"noCheckoutDEPSRepoManager,omitempty"`
// Kubernetes config.
// TODO(borenet): Optional right now, but will eventually be required.
Kubernetes *KubernetesConfig `json:"kubernetes"`
// Optional Fields.
// Comma-separated list of trybots to add to roll CLs, in addition to
// the default set of commit queue trybots.
CqExtraTrybots []string `json:"cqExtraTrybots,omitempty"`
// Limit to one successful roll within this time period.
MaxRollFrequency string `json:"maxRollFrequency,omitempty"`
// Any extra notification systems to be used for this roller.
Notifiers []*notifier.Config `json:"notifiers,omitempty"`
// Throttling configuration to prevent uploading too many CLs within
// too short a time period.
SafetyThrottle *ThrottleConfig `json:"safetyThrottle,omitempty"`
// Private.
rollerType string // Set by RollerType().
}
// Validate the config.
func (c *AutoRollerConfig) Validate() error {
if c.ChildName == "" {
return errors.New("ChildName is required.")
}
if len(c.Contacts) < 1 {
return errors.New("At least one contact is required.")
}
if c.ParentName == "" {
return errors.New("ParentName is required.")
}
if c.ParentWaterfall == "" {
return errors.New("ParentWaterfall is required.")
}
if c.RollerName == "" {
return errors.New("RollerName is required.")
}
// TODO(borenet): Make ServiceAccount required for all rollers once
// they're moved to k8s.
if c.ServiceAccount == "" && c.Kubernetes != nil {
return errors.New("ServiceAccount is required.")
}
if c.Sheriff == nil || len(c.Sheriff) == 0 {
return errors.New("Sheriff is required.")
}
cr := []util.Validator{}
if c.Gerrit != nil {
cr = append(cr, c.Gerrit)
}
if c.Github != nil {
cr = append(cr, c.Github)
}
if c.Google3Review != nil {
cr = append(cr, c.Google3Review)
}
if len(cr) != 1 {
return errors.New("Exactly one of Gerrit, Github, or Google3Review is required.")
}
if err := cr[0].Validate(); err != nil {
return err
}
rm := []util.Validator{}
if c.AFDORepoManager != nil {
rm = append(rm, c.AFDORepoManager)
}
if c.AndroidRepoManager != nil {
rm = append(rm, c.AndroidRepoManager)
}
if c.AssetRepoManager != nil {
rm = append(rm, c.AssetRepoManager)
}
if c.CopyRepoManager != nil {
rm = append(rm, c.CopyRepoManager)
}
if c.DEPSRepoManager != nil {
rm = append(rm, c.DEPSRepoManager)
}
if c.FuchsiaSDKAndroidRepoManager != nil {
rm = append(rm, c.FuchsiaSDKAndroidRepoManager)
}
if c.FuchsiaSDKRepoManager != nil {
rm = append(rm, c.FuchsiaSDKRepoManager)
}
if c.GithubRepoManager != nil {
rm = append(rm, c.GithubRepoManager)
}
if c.GithubDEPSRepoManager != nil {
rm = append(rm, c.GithubDEPSRepoManager)
}
if c.Google3RepoManager != nil {
rm = append(rm, c.Google3RepoManager)
}
if c.ManifestRepoManager != nil {
rm = append(rm, c.ManifestRepoManager)
}
if c.NoCheckoutDEPSRepoManager != nil {
rm = append(rm, c.NoCheckoutDEPSRepoManager)
}
if len(rm) != 1 {
return fmt.Errorf("Exactly one repo manager must be supplied, but got %d", len(rm))
}
if err := rm[0].Validate(); err != nil {
return err
}
if err := c.Kubernetes.Validate(); err != nil {
return fmt.Errorf("KubernetesConfig validation failed: %s", err)
}
// Verify that the notifier configs are valid.
_, err := arb_notifier.New(context.Background(), "fake", "fake", "fake", nil, nil, c.Notifiers)
return err
}
// Return the "type" of this roller.
func (c *AutoRollerConfig) RollerType() string {
if c.rollerType == "" {
if c.AFDORepoManager != nil {
c.rollerType = ROLLER_TYPE_AFDO
} else if c.AndroidRepoManager != nil {
c.rollerType = ROLLER_TYPE_ANDROID
} else if c.AssetRepoManager != nil {
c.rollerType = ROLLER_TYPE_ASSET
} else if c.CopyRepoManager != nil {
c.rollerType = ROLLER_TYPE_COPY
} else if c.DEPSRepoManager != nil {
c.rollerType = ROLLER_TYPE_DEPS
} else if c.FuchsiaSDKRepoManager != nil {
c.rollerType = ROLLER_TYPE_FUCHSIA_SDK
} else if c.GithubRepoManager != nil {
c.rollerType = ROLLER_TYPE_GITHUB
} else if c.GithubDEPSRepoManager != nil {
c.rollerType = ROLLER_TYPE_GITHUB_DEPS
} else if c.Google3RepoManager != nil {
c.rollerType = ROLLER_TYPE_GOOGLE3
} else if c.ManifestRepoManager != nil {
c.rollerType = ROLLER_TYPE_MANIFEST
} else if c.NoCheckoutDEPSRepoManager != nil {
c.rollerType = ROLLER_TYPE_DEPS_NO_CHECKOUT
} else {
c.rollerType = ROLLER_TYPE_INVALID
}
}
return c.rollerType
}
// Return the code review config for the roller.
func (c *AutoRollerConfig) CodeReview() codereview.CodeReviewConfig {
if c.Github != nil {
return c.Github
}
if c.Google3Review != nil {
return c.Google3Review
}
return c.Gerrit
}