Support runner groups with selected visibility in webhooks autoscaler (#1012)

The current implementation doesn't support yet runner groups with custom visibility (e.g selected repositories only). If there are multiple runner groups with selected visibility - not all runner groups may be a potential target to be scaled up. Thus this PR introduces support to allow having runner groups with selected visibility. This requires to query GitHub API to find what are the potential runner groups that are linked to a specific repository (whether using visibility all or selected).

This also improves resolving the `scaleTargetKey` that are used to match an HRA based on the inputs of the `RunnerSet`/`RunnerDeployment` spec to better support for runner groups.

This requires to configure github auth in the webhook server, to keep backwards compatibility if github auth is not provided to the webhook server, this will assume all runner groups have no selected visibility and it will target any available runner group as before
This commit is contained in:
Felipe Galindo Sanchez 2021-12-19 01:29:44 -08:00 committed by GitHub
parent 0c34196d87
commit 4ebec38208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 341 additions and 82 deletions

View File

@ -54,6 +54,32 @@ spec:
key: github_webhook_secret_token
name: {{ include "actions-runner-controller-github-webhook-server.secretName" . }}
optional: true
{{- if .Values.authSecret.enabled }}
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
key: github_token
name: {{ include "actions-runner-controller.secretName" . }}
optional: true
- name: GITHUB_APP_ID
valueFrom:
secretKeyRef:
key: github_app_id
name: {{ include "actions-runner-controller.secretName" . }}
optional: true
- name: GITHUB_APP_INSTALLATION_ID
valueFrom:
secretKeyRef:
key: github_app_installation_id
name: {{ include "actions-runner-controller.secretName" . }}
optional: true
- name: GITHUB_APP_PRIVATE_KEY
valueFrom:
secretKeyRef:
key: github_app_private_key
name: {{ include "actions-runner-controller.secretName" . }}
optional: true
{{- end }}
{{- range $key, $val := .Values.githubWebhookServer.env }}
- name: {{ $key }}
value: {{ $val | quote }}

View File

@ -28,6 +28,8 @@ import (
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
"github.com/actions-runner-controller/actions-runner-controller/controllers"
"github.com/actions-runner-controller/actions-runner-controller/github"
"github.com/kelseyhightower/envconfig"
zaplib "go.uber.org/zap"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@ -76,8 +78,17 @@ func main() {
enableLeaderElection bool
syncPeriod time.Duration
logLevel string
ghClient *github.Client
)
var c github.Config
err = envconfig.Process("github", &c)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: processing environment variables: %v\n", err)
os.Exit(1)
}
webhookSecretTokenEnv = os.Getenv(webhookSecretTokenEnvName)
flag.StringVar(&webhookAddr, "webhook-addr", ":8000", "The address the metric endpoint binds to.")
@ -88,6 +99,11 @@ func main() {
flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change")
flag.StringVar(&logLevel, "log-level", logLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
flag.StringVar(&webhookSecretToken, "github-webhook-secret-token", "", "The personal access token of GitHub.")
flag.StringVar(&c.Token, "github-token", c.Token, "The personal access token of GitHub.")
flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.")
flag.Int64Var(&c.AppInstallationID, "github-app-installation-id", c.AppInstallationID, "The installation ID of GitHub App.")
flag.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
flag.Parse()
if webhookSecretToken == "" && webhookSecretTokenEnv != "" {
@ -121,6 +137,15 @@ func main() {
}
})
if len(c.Token) > 0 || (c.AppID > 0 && c.AppInstallationID > 0 && c.AppPrivateKey != "") {
ghClient, err = c.NewClient()
if err != nil {
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)
setupLog.Error(err, "unable to create controller", "controller", "Runner")
os.Exit(1)
}
}
ctrl.SetLogger(logger)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
@ -143,6 +168,7 @@ func main() {
Scheme: mgr.GetScheme(),
SecretKeyBytes: []byte(webhookSecretToken),
Namespace: watchNamespace,
GitHubClient: ghClient,
}
if err = hraGitHubWebhook.SetupWithManager(mgr); err != nil {

View File

@ -37,12 +37,14 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
"github.com/actions-runner-controller/actions-runner-controller/github"
)
const (
scaleTargetKey = "scaleTarget"
keyPrefixEnterprise = "enterprises/"
keyRunnerGroup = "/group/"
)
// HorizontalRunnerAutoscalerGitHubWebhook autoscales a HorizontalRunnerAutoscaler and the RunnerDeployment on each
@ -57,6 +59,9 @@ type HorizontalRunnerAutoscalerGitHubWebhook struct {
// the administrator is generated and specified in GitHub Web UI.
SecretKeyBytes []byte
// GitHub Client to discover runner groups assigned to a repository
GitHubClient *github.Client
// Namespace is the namespace to watch for HorizontalRunnerAutoscaler's to be
// scaled on Webhook.
// Set to empty for letting it watch for all namespaces.
@ -436,63 +441,30 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleTarget(ctx co
}
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) {
repositoryRunnerKey := owner + "/" + repo
if target, err := autoscaler.getScaleTarget(ctx, repositoryRunnerKey, f); err != nil {
log.Info("finding repository-wide runner", "repository", repositoryRunnerKey)
return nil, err
} else if target != nil {
log.Info("scale up target is repository-wide runners", "repository", repo)
return target, nil
scaleTarget := func(value string) (*ScaleTarget, error) {
return autoscaler.getScaleTarget(ctx, value, f)
}
if ownerType == "User" {
log.V(1).Info("no repository runner found", "organization", owner)
return nil, nil
}
if target, err := autoscaler.getScaleTarget(ctx, owner, f); err != nil {
log.Info("finding organizational runner", "organization", owner)
return nil, err
} else if target != nil {
log.Info("scale up target is organizational runners", "organization", owner)
return target, nil
}
if enterprise == "" {
log.V(1).Info("no repository runner or organizational runner found",
"repository", repositoryRunnerKey,
"organization", owner,
)
return nil, nil
}
if target, err := autoscaler.getScaleTarget(ctx, enterpriseKey(enterprise), f); err != nil {
log.Error(err, "finding enterprise runner", "enterprise", enterprise)
return nil, err
} else if target != nil {
log.Info("scale up target is enterprise runners", "enterprise", enterprise)
return target, nil
} else {
log.V(1).Info("no repository/organizational/enterprise runner found",
"repository", repositoryRunnerKey,
"organization", owner,
"enterprises", enterprise,
)
}
return nil, nil
return autoscaler.getScaleUpTargetWithFunction(ctx, log, repo, owner, ownerType, enterprise, scaleTarget)
}
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetForRepoOrOrg(
ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, labels []string,
) (*ScaleTarget, error) {
scaleTarget := func(value string) (*ScaleTarget, error) {
return autoscaler.getJobScaleTarget(ctx, value, labels)
}
return autoscaler.getScaleUpTargetWithFunction(ctx, log, repo, owner, ownerType, enterprise, scaleTarget)
}
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTargetWithFunction(
ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, scaleTarget func(value string) (*ScaleTarget, error)) (*ScaleTarget, error) {
repositoryRunnerKey := owner + "/" + repo
if target, err := autoscaler.getJobScaleTarget(ctx, repositoryRunnerKey, labels); err != nil {
log.Info("finding repository-wide runner", "repository", repositoryRunnerKey)
// Search for repository HRAs
if target, err := scaleTarget(repositoryRunnerKey); err != nil {
log.Error(err, "finding repository-wide runner", "repository", repositoryRunnerKey)
return nil, err
} else if target != nil {
log.Info("job scale up target is repository-wide runners", "repository", repo)
@ -500,34 +472,41 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetFo
}
if ownerType == "User" {
log.V(1).Info("no repository runner found", "organization", owner)
log.V(1).Info("user repositories not supported", "owner", owner)
return nil, nil
}
if target, err := autoscaler.getJobScaleTarget(ctx, owner, labels); err != nil {
log.Info("finding organizational runner", "organization", owner)
// Search for organization runner HRAs in default runner group
if target, err := scaleTarget(owner); err != nil {
log.Error(err, "finding organizational runner", "organization", owner)
return nil, err
} else if target != nil {
log.Info("job scale up target is organizational runners", "organization", owner)
return target, nil
}
if enterprise == "" {
log.V(1).Info("no repository runner or organizational runner found",
"repository", repositoryRunnerKey,
"organization", owner,
)
return nil, nil
}
if target, err := autoscaler.getJobScaleTarget(ctx, enterpriseKey(enterprise), labels); err != nil {
if enterprise != "" {
// Search for enterprise runner HRAs in default runner group
if target, err := scaleTarget(enterpriseKey(enterprise)); err != nil {
log.Error(err, "finding enterprise runner", "enterprise", enterprise)
return nil, err
} else if target != nil {
log.Info("scale up target is enterprise runners", "enterprise", enterprise)
log.Info("scale up target is default enterprise runners", "enterprise", enterprise)
return target, nil
} else {
}
}
// At this point there were no default organization/enterprise runners available to use, try now
// searching in runner groups
// We need to get the potential runner groups first to avoid spending API queries needless. Once/if GitHub improves an
// API to find related/linked runner groups from a specific repository this logic could be removed
availableEnterpriseGroups, availableOrganizationGroups, err := autoscaler.getPotentialGroupsFromHRAs(ctx, enterprise, owner)
if err != nil {
log.Error(err, "finding potential organization runner groups from HRAs", "organization", owner)
return nil, err
}
if len(availableEnterpriseGroups) == 0 && len(availableOrganizationGroups) == 0 {
log.V(1).Info("no repository/organizational/enterprise runner found",
"repository", repositoryRunnerKey,
"organization", owner,
@ -535,7 +514,101 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetFo
)
}
var enterpriseGroups []string
var organizationGroups []string
if autoscaler.GitHubClient != nil {
// Get available organization runner groups and enterprise runner groups for a repository
// These are the sum of runner groups with repository access = All repositories plus
// runner groups where owner/repo has access to
enterpriseGroups, organizationGroups, err = autoscaler.GitHubClient.GetRunnerGroupsFromRepository(ctx, owner, repositoryRunnerKey, availableEnterpriseGroups, availableOrganizationGroups)
log.V(1).Info("Searching in runner groups", "enterprise.groups", enterpriseGroups, "organization.groups", organizationGroups)
if err != nil {
log.Error(err, "Unable to find runner groups from repository", "organization", owner, "repository", repo)
return nil, nil
}
} else {
// For backwards compatibility if GitHub authentication is not configured, we assume all runner groups have
// visibility=all to honor the previous implementation, therefore any available enterprise/organization runner
// is a potential target for scaling
enterpriseGroups = availableEnterpriseGroups
organizationGroups = availableOrganizationGroups
}
for _, group := range organizationGroups {
if target, err := scaleTarget(organizationalRunnerGroupKey(owner, group)); err != nil {
log.Error(err, "finding organizational runner group", "organization", owner)
return nil, err
} else if target != nil {
log.Info(fmt.Sprintf("job scale up target is organizational runner group %s", target.Name), "organization", owner)
return target, nil
}
}
for _, group := range enterpriseGroups {
if target, err := scaleTarget(enterpriseRunnerGroupKey(enterprise, group)); err != nil {
log.Error(err, "finding enterprise runner group", "enterprise", owner)
return nil, err
} else if target != nil {
log.Info(fmt.Sprintf("job scale up target is enterprise runner group %s", target.Name), "enterprise", owner)
return target, nil
}
}
log.V(1).Info("no repository/organizational/enterprise runner found",
"repository", repositoryRunnerKey,
"organization", owner,
"enterprises", enterprise,
)
return nil, nil
}
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getPotentialGroupsFromHRAs(ctx context.Context, enterprise, org string) ([]string, []string, error) {
var enterpriseRunnerGroups []string
var orgRunnerGroups []string
ns := autoscaler.Namespace
var defaultListOpts []client.ListOption
if ns != "" {
defaultListOpts = append(defaultListOpts, client.InNamespace(ns))
}
opts := append([]client.ListOption{}, defaultListOpts...)
if autoscaler.Namespace != "" {
opts = append(opts, client.InNamespace(autoscaler.Namespace))
}
var hraList v1alpha1.HorizontalRunnerAutoscalerList
if err := autoscaler.List(ctx, &hraList, opts...); err != nil {
return orgRunnerGroups, enterpriseRunnerGroups, err
}
for _, hra := range hraList.Items {
switch hra.Spec.ScaleTargetRef.Kind {
case "RunnerSet":
var rs v1alpha1.RunnerSet
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
return orgRunnerGroups, enterpriseRunnerGroups, err
}
if rs.Spec.Organization == org && rs.Spec.Group != "" {
orgRunnerGroups = append(orgRunnerGroups, rs.Spec.Group)
}
if rs.Spec.Enterprise == enterprise && rs.Spec.Group != "" {
enterpriseRunnerGroups = append(enterpriseRunnerGroups, rs.Spec.Group)
}
case "RunnerDeployment", "":
var rd v1alpha1.RunnerDeployment
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
return orgRunnerGroups, enterpriseRunnerGroups, err
}
if rd.Spec.Template.Spec.Organization == org && rd.Spec.Template.Spec.Group != "" {
orgRunnerGroups = append(orgRunnerGroups, rd.Spec.Template.Spec.Group)
}
if rd.Spec.Template.Spec.Enterprise == enterprise && rd.Spec.Template.Spec.Group != "" {
enterpriseRunnerGroups = append(enterpriseRunnerGroups, rd.Spec.Template.Spec.Group)
}
}
}
return enterpriseRunnerGroups, orgRunnerGroups, nil
}
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleTarget(ctx context.Context, name string, labels []string) (*ScaleTarget, error) {
@ -582,6 +655,9 @@ HRA:
// Ensure that the RunnerSet-managed runners have all the labels requested by the workflow_job.
for _, l := range labels {
if l == "self-hosted" {
continue // label is automatically added to self-hosted runners
}
var matched bool
// ignore "self-hosted" label as all instance here are self-hosted
@ -613,6 +689,9 @@ HRA:
// Ensure that the RunnerDeployment-managed runners have all the labels requested by the workflow_job.
for _, l := range labels {
if l == "self-hosted" {
continue // label is automatically added to self-hosted runners
}
var matched bool
// ignore "self-hosted" label as all instance here are self-hosted
@ -724,31 +803,55 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr
switch hra.Spec.ScaleTargetRef.Kind {
case "", "RunnerDeployment":
var rd v1alpha1.RunnerDeployment
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerDeployment not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name))
return nil
}
keys := []string{rd.Spec.Template.Spec.Repository, rd.Spec.Template.Spec.Organization}
if enterprise := rd.Spec.Template.Spec.Enterprise; enterprise != "" {
keys = append(keys, enterpriseKey(enterprise))
keys := []string{}
if rd.Spec.Template.Spec.Repository != "" {
keys = append(keys, rd.Spec.Template.Spec.Repository) // Repository runners
}
if rd.Spec.Template.Spec.Organization != "" {
if group := rd.Spec.Template.Spec.Group; group != "" {
keys = append(keys, organizationalRunnerGroupKey(rd.Spec.Template.Spec.Organization, rd.Spec.Template.Spec.Group)) // Organization runner groups
} else {
keys = append(keys, rd.Spec.Template.Spec.Organization) // Organization runners
}
}
if enterprise := rd.Spec.Template.Spec.Enterprise; enterprise != "" {
if group := rd.Spec.Template.Spec.Group; group != "" {
keys = append(keys, enterpriseRunnerGroupKey(enterprise, rd.Spec.Template.Spec.Group)) // Enterprise runner groups
} else {
keys = append(keys, enterpriseKey(enterprise)) // Enterprise runners
}
}
autoscaler.Log.V(1).Info(fmt.Sprintf("HRA keys indexed for HRA %s: %v", hra.Name, keys))
return keys
case "RunnerSet":
var rs v1alpha1.RunnerSet
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerSet not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name))
return nil
}
keys := []string{rs.Spec.Repository, rs.Spec.Organization}
if enterprise := rs.Spec.Enterprise; enterprise != "" {
keys = append(keys, enterpriseKey(enterprise))
keys := []string{}
if rs.Spec.Repository != "" {
keys = append(keys, rs.Spec.Repository) // Repository runners
}
if rs.Spec.Organization != "" {
keys = append(keys, rs.Spec.Organization) // Organization runners
if group := rs.Spec.Group; group != "" {
keys = append(keys, organizationalRunnerGroupKey(rs.Spec.Organization, rs.Spec.Group)) // Organization runner groups
}
}
if enterprise := rs.Spec.Enterprise; enterprise != "" {
keys = append(keys, enterpriseKey(enterprise)) // Enterprise runners
if group := rs.Spec.Group; group != "" {
keys = append(keys, enterpriseRunnerGroupKey(enterprise, rs.Spec.Group)) // Enterprise runner groups
}
}
autoscaler.Log.V(1).Info(fmt.Sprintf("HRA keys indexed for HRA %s: %v", hra.Name, keys))
return keys
}
@ -766,3 +869,11 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr
func enterpriseKey(name string) string {
return keyPrefixEnterprise + name
}
func organizationalRunnerGroupKey(owner, group string) string {
return owner + keyRunnerGroup + group
}
func enterpriseRunnerGroupKey(enterprise, group string) string {
return keyPrefixEnterprise + enterprise + keyRunnerGroup + group
}

View File

@ -103,7 +103,7 @@ func (c *Client) GetRegistrationToken(ctx context.Context, enterprise, org, repo
return rt, nil
}
enterprise, owner, repo, err := getEnterpriseOrganisationAndRepo(enterprise, org, repo)
enterprise, owner, repo, err := getEnterpriseOrganizationAndRepo(enterprise, org, repo)
if err != nil {
return rt, err
@ -129,7 +129,7 @@ func (c *Client) GetRegistrationToken(ctx context.Context, enterprise, org, repo
// RemoveRunner removes a runner with specified runner ID from repository.
func (c *Client) RemoveRunner(ctx context.Context, enterprise, org, repo string, runnerID int64) error {
enterprise, owner, repo, err := getEnterpriseOrganisationAndRepo(enterprise, org, repo)
enterprise, owner, repo, err := getEnterpriseOrganizationAndRepo(enterprise, org, repo)
if err != nil {
return err
@ -150,7 +150,7 @@ func (c *Client) RemoveRunner(ctx context.Context, enterprise, org, repo string,
// ListRunners returns a list of runners of specified owner/repository name.
func (c *Client) ListRunners(ctx context.Context, enterprise, org, repo string) ([]*github.Runner, error) {
enterprise, owner, repo, err := getEnterpriseOrganisationAndRepo(enterprise, org, repo)
enterprise, owner, repo, err := getEnterpriseOrganizationAndRepo(enterprise, org, repo)
if err != nil {
return nil, err
@ -176,6 +176,93 @@ func (c *Client) ListRunners(ctx context.Context, enterprise, org, repo string)
return runners, nil
}
func (c *Client) GetRunnerGroupsFromRepository(ctx context.Context, org, repo string, potentialEnterpriseGroups []string, potentialOrgGroups []string) ([]string, []string, error) {
var enterpriseRunnerGroups []string
var orgRunnerGroups []string
if org != "" {
runnerGroups, err := c.getOrganizationRunnerGroups(ctx, org, repo)
if err != nil {
return enterpriseRunnerGroups, orgRunnerGroups, err
}
for _, runnerGroup := range runnerGroups {
if runnerGroup.GetInherited() { // enterprise runner groups
if !containsString(potentialEnterpriseGroups, runnerGroup.GetName()) {
continue
}
if runnerGroup.GetVisibility() == "all" {
enterpriseRunnerGroups = append(enterpriseRunnerGroups, runnerGroup.GetName())
} else {
hasAccess, err := c.hasRepoAccessToOrganizationRunnerGroup(ctx, org, runnerGroup.GetID(), repo)
if err != nil {
return enterpriseRunnerGroups, orgRunnerGroups, err
}
if hasAccess {
enterpriseRunnerGroups = append(enterpriseRunnerGroups, runnerGroup.GetName())
}
}
} else { // organization runner groups
if !containsString(potentialOrgGroups, runnerGroup.GetName()) {
continue
}
if runnerGroup.GetVisibility() == "all" {
orgRunnerGroups = append(orgRunnerGroups, runnerGroup.GetName())
} else {
hasAccess, err := c.hasRepoAccessToOrganizationRunnerGroup(ctx, org, runnerGroup.GetID(), repo)
if err != nil {
return enterpriseRunnerGroups, orgRunnerGroups, err
}
if hasAccess {
orgRunnerGroups = append(orgRunnerGroups, runnerGroup.GetName())
}
}
}
}
}
return enterpriseRunnerGroups, orgRunnerGroups, nil
}
func (c *Client) hasRepoAccessToOrganizationRunnerGroup(ctx context.Context, org string, runnerGroupId int64, repo string) (bool, error) {
opts := github.ListOptions{PerPage: 100}
for {
list, res, err := c.Client.Actions.ListRepositoryAccessRunnerGroup(ctx, org, runnerGroupId, &opts)
if err != nil {
return false, fmt.Errorf("failed to list repository access for runner group: %w", err)
}
for _, githubRepo := range list.Repositories {
if githubRepo.GetFullName() == repo {
return true, nil
}
}
if res.NextPage == 0 {
break
}
opts.Page = res.NextPage
}
return false, nil
}
func (c *Client) getOrganizationRunnerGroups(ctx context.Context, org, repo string) ([]*github.RunnerGroup, error) {
var runnerGroups []*github.RunnerGroup
opts := github.ListOptions{PerPage: 100}
for {
list, res, err := c.Client.Actions.ListOrganizationRunnerGroups(ctx, org, &opts)
if err != nil {
return runnerGroups, fmt.Errorf("failed to list organization runner groups: %w", err)
}
runnerGroups = append(runnerGroups, list.RunnerGroups...)
if res.NextPage == 0 {
break
}
opts.Page = res.NextPage
}
return runnerGroups, nil
}
// cleanup removes expired registration tokens.
func (c *Client) cleanup() {
c.mu.Lock()
@ -267,8 +354,8 @@ func (c *Client) listRepositoryWorkflowRuns(ctx context.Context, user string, re
return workflowRuns, nil
}
// Validates enterprise, organisation and repo arguments. Both are optional, but at least one should be specified
func getEnterpriseOrganisationAndRepo(enterprise, org, repo string) (string, string, string, error) {
// Validates enterprise, organization and repo arguments. Both are optional, but at least one should be specified
func getEnterpriseOrganizationAndRepo(enterprise, org, repo string) (string, string, string, error) {
if len(repo) > 0 {
owner, repository, err := splitOwnerAndRepo(repo)
return "", owner, repository, err
@ -345,3 +432,12 @@ func (r *Client) IsRunnerBusy(ctx context.Context, enterprise, org, repo, name s
return false, &RunnerNotFound{runnerName: name}
}
func containsString(list []string, value string) bool {
for _, item := range list {
if item == value {
return true
}
}
return false
}