Files
actions-runner-controller/simulator/runnergroups.go
Felipe Galindo Sanchez d0d316252e Option to consider runner group visibility on scale based on webhook (#1062)
This will work on GHES but GitHub Enterprise Cloud due to excessive GitHub API calls required.
More work is needed, like adding a cache layer to the GitHub client, to make it usable on GitHub Enterprise Cloud.

Fixes additional cases from https://github.com/actions-runner-controller/actions-runner-controller/pull/1012

If GitHub auth is provided in the webhooks controller then runner groups with custom visibility are supported. Otherwise, all runner groups will be assumed to be visible to all repositories

`getScaleUpTargetWithFunction()` will check if there is an HRA available with the following flow:

1. Search for **repository** HRAs - if so it ends here
2. Get available HRAs in k8s
3. Compute visible runner groups
  a. If GitHub auth is provided - get all the runner groups that are visible to the repository of the incoming webhook using GitHub API calls.  
  b. If GitHub auth is not provided - assume all runner groups are visible to all repositories
4. Search for **default organization** runners (a.k.a runners from organization's visible default runner group) with matching labels
5. Search for **default enterprise** runners (a.k.a runners from enterprise's visible default runner group) with matching labels
6. Search for **custom organization runner groups** with matching labels
7. Search for **custom enterprise runner groups** with matching labels

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
2022-02-16 19:08:56 +09:00

181 lines
3.8 KiB
Go

package simulator
import (
"fmt"
"sort"
"github.com/google/go-github/v39/github"
)
type RunnerGroupScope int
const (
Organization RunnerGroupScope = iota
Enterprise
)
func (s RunnerGroupScope) String() string {
switch s {
case Organization:
return "Organization"
case Enterprise:
return "Enterprise"
default:
panic(fmt.Sprintf("unimplemented RunnerGroupScope: %v", int(s)))
}
}
type RunnerGroupKind int
const (
Default RunnerGroupKind = iota
Custom
)
func (s RunnerGroupKind) String() string {
switch s {
case Default:
return "Default"
case Custom:
return "Custom"
default:
panic(fmt.Sprintf("unimplemented RunnerGroupKind: %v", int(s)))
}
}
func NewRunnerGroupFromGitHub(g *github.RunnerGroup) RunnerGroup {
var name string
if !g.GetDefault() {
name = g.GetName()
}
var scope RunnerGroupScope
if g.GetInherited() {
scope = Enterprise
} else {
scope = Organization
}
return newRunnerGroup(scope, name)
}
func NewRunnerGroupFromProperties(enterprise, organization, group string) RunnerGroup {
var scope RunnerGroupScope
if enterprise != "" {
scope = Enterprise
} else {
scope = Organization
}
return newRunnerGroup(scope, group)
}
// newRunnerGroup creates a new RunnerGroup instance from the provided arguments.
// There's a convention that an empty name implies a default runner group.
func newRunnerGroup(scope RunnerGroupScope, name string) RunnerGroup {
if name == "" {
return RunnerGroup{
Scope: scope,
Kind: Default,
Name: "",
}
}
return RunnerGroup{
Scope: scope,
Kind: Custom,
Name: name,
}
}
type RunnerGroup struct {
Scope RunnerGroupScope
Kind RunnerGroupKind
Name string
}
// VisibleRunnerGroups is a set of enterprise and organization runner groups
// that are visible to a GitHub repository.
// GitHub Actions chooses one of such visible group on which the workflow job is scheduled.
// ARC chooses the same group as Actions as the scale target.
type VisibleRunnerGroups struct {
// sortedGroups is a pointer to a mutable list of RunnerGroups that contains all the runner sortedGroups
// that are visible to the repository, including organization sortedGroups defined at the organization level,
// and enterprise sortedGroups that are inherited down to the organization.
sortedGroups []RunnerGroup
}
func NewVisibleRunnerGroups() *VisibleRunnerGroups {
return &VisibleRunnerGroups{}
}
func (g *VisibleRunnerGroups) IsEmpty() bool {
return len(g.sortedGroups) == 0
}
func (r *VisibleRunnerGroups) Includes(ref RunnerGroup) bool {
for _, r := range r.sortedGroups {
if r.Scope == ref.Scope && r.Kind == ref.Kind && r.Name == ref.Name {
return true
}
}
return false
}
// Add adds a runner group into VisibleRunnerGroups
// at a certain position in the list so that
// Traverse can return runner groups in order of higher precedence to lower precedence.
func (g *VisibleRunnerGroups) Add(rg RunnerGroup) error {
n := len(g.sortedGroups)
i := sort.Search(n, func(i int) bool {
data := g.sortedGroups[i]
if rg.Kind > data.Kind {
return false
} else if rg.Kind < data.Kind {
return true
}
if rg.Scope > data.Scope {
return false
} else if rg.Scope < data.Scope {
return true
}
return false
})
g.insert(rg, i)
return nil
}
func (g *VisibleRunnerGroups) insert(rg RunnerGroup, i int) {
var result []RunnerGroup
result = append(result, g.sortedGroups[:i]...)
result = append(result, rg)
result = append(result, g.sortedGroups[i:]...)
g.sortedGroups = result
}
// Traverse traverses all the runner groups visible to a repository
// in order of higher precedence to lower precedence.
func (g *VisibleRunnerGroups) Traverse(f func(RunnerGroup) (bool, error)) error {
for _, rg := range g.sortedGroups {
ok, err := f(rg)
if err != nil {
return err
}
if ok {
return nil
}
}
return nil
}