diff --git a/charts/actions-runner-controller/templates/githubwebhook.deployment.yaml b/charts/actions-runner-controller/templates/githubwebhook.deployment.yaml index 008aa842..5cc350e0 100644 --- a/charts/actions-runner-controller/templates/githubwebhook.deployment.yaml +++ b/charts/actions-runner-controller/templates/githubwebhook.deployment.yaml @@ -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 }} diff --git a/cmd/githubwebhookserver/main.go b/cmd/githubwebhookserver/main.go index fbf3d0a8..b440fd54 100644 --- a/cmd/githubwebhookserver/main.go +++ b/cmd/githubwebhookserver/main.go @@ -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 { diff --git a/controllers/horizontal_runner_autoscaler_webhook.go b/controllers/horizontal_runner_autoscaler_webhook.go index 2532399f..2bd3930a 100644 --- a/controllers/horizontal_runner_autoscaler_webhook.go +++ b/controllers/horizontal_runner_autoscaler_webhook.go @@ -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 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 default enterprise runners", "enterprise", enterprise) + return target, nil + } } - if target, err := autoscaler.getJobScaleTarget(ctx, enterpriseKey(enterprise), labels); err != nil { - log.Error(err, "finding enterprise runner", "enterprise", enterprise) + // 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 - } else if target != nil { - log.Info("scale up target is enterprise runners", "enterprise", enterprise) - return target, nil - } else { + } + if len(availableEnterpriseGroups) == 0 && len(availableOrganizationGroups) == 0 { log.V(1).Info("no repository/organizational/enterprise runner found", "repository", repositoryRunnerKey, "organization", owner, @@ -535,9 +514,103 @@ 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) { hras, err := autoscaler.findHRAsByKey(ctx, name) if err != nil { @@ -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 +} diff --git a/github/github.go b/github/github.go index dc95f6fb..3ad428f6 100644 --- a/github/github.go +++ b/github/github.go @@ -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 +}