From ebe7d060cba64ee4135c46de1dc3fe22f2d4d269 Mon Sep 17 00:00:00 2001 From: Tingluo Huang Date: Sat, 23 Apr 2022 21:54:40 -0400 Subject: [PATCH] Find runner groups that visible to repository using a single API call. (#1324) The [ListRunnerGroup API](https://docs.github.com/en/rest/reference/actions#list-self-hosted-runner-groups-for-an-organization) now add a new query parameter `visible_to_repository`. We were doing `N+1` lookup when trying to find which runner group can be used for job from a certain repository. - List all runner groups - Loop through all groups to check repository access for each of them via [API](https://docs.github.com/en/rest/reference/actions#list-repository-access-to-a-self-hosted-runner-group-in-an-organization) The new query parameter `visible_to_repository` should allow us to get the runner groups with access in one call. Limitation: - The new query parameter is only supported in GitHub.com, which means anyone who uses ARC in GitHub Enterprise Server won't get this. - I am working on a PR to update `go-github` library to support the new parameter, but it will take a few weeks for a newer `go-github` to be released, so in the meantime, I am duplicating the implementation in ARC as well to support the new query parameter. --- github/github.go | 59 +++++++++++++++++++++++++++++ simulator/runnergroup_visibility.go | 51 ++++++++++++++++--------- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/github/github.go b/github/github.go index b037d8ba..835d1e3c 100644 --- a/github/github.go +++ b/github/github.go @@ -265,6 +265,29 @@ func (c *Client) ListOrganizationRunnerGroups(ctx context.Context, org string) ( return runnerGroups, nil } +// ListOrganizationRunnerGroupsForRepository returns all the runner groups defined in the organization and +// inherited to the organization from an enterprise. +// We can remove this when google/go-github library is updated to support this. +func (c *Client) ListOrganizationRunnerGroupsForRepository(ctx context.Context, org, repo string) ([]*github.RunnerGroup, error) { + var runnerGroups []*github.RunnerGroup + + opts := github.ListOptions{PerPage: 100} + for { + list, res, err := c.listOrganizationRunnerGroupsVisibleToRepo(ctx, org, repo, &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 +} + func (c *Client) ListRunnerGroupRepositoryAccesses(ctx context.Context, org string, runnerGroupId int64) ([]*github.Repository, error) { var repos []*github.Repository @@ -286,6 +309,42 @@ func (c *Client) ListRunnerGroupRepositoryAccesses(ctx context.Context, org stri return repos, nil } +// listOrganizationRunnerGroupsVisibleToRepo lists all self-hosted runner groups configured in an organization which can be used by the repository. +// +// GitHub API docs: https://docs.github.com/en/rest/reference/actions#list-self-hosted-runner-groups-for-an-organization +func (c *Client) listOrganizationRunnerGroupsVisibleToRepo(ctx context.Context, org, repo string, opts *github.ListOptions) (*github.RunnerGroups, *github.Response, error) { + repoName := repo + parts := strings.Split(repo, "/") + if len(parts) == 2 { + repoName = parts[1] + } + + u := fmt.Sprintf("orgs/%v/actions/runner-groups?visible_to_repository=%v", org, repoName) + + if opts != nil { + if opts.PerPage > 0 { + u = fmt.Sprintf("%v&per_page=%v", u, opts.PerPage) + } + + if opts.Page > 0 { + u = fmt.Sprintf("%v&page=%v", u, opts.Page) + } + } + + req, err := c.Client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + groups := &github.RunnerGroups{} + resp, err := c.Client.Do(ctx, req, &groups) + if err != nil { + return nil, resp, err + } + + return groups, resp, nil +} + // cleanup removes expired registration tokens. func (c *Client) cleanup() { c.mu.Lock() diff --git a/simulator/runnergroup_visibility.go b/simulator/runnergroup_visibility.go index 43d33cff..bad0d465 100644 --- a/simulator/runnergroup_visibility.go +++ b/simulator/runnergroup_visibility.go @@ -18,30 +18,47 @@ func (c *Simulator) GetRunnerGroupsVisibleToRepository(ctx context.Context, org, panic(fmt.Sprintf("BUG: owner should not be empty in this context. repo=%v", repo)) } - runnerGroups, err := c.Client.ListOrganizationRunnerGroups(ctx, org) - if err != nil { - return visible, err - } - - for _, runnerGroup := range runnerGroups { - ref := NewRunnerGroupFromGitHub(runnerGroup) - - if !managed.Includes(ref) { - continue + if c.Client.GithubBaseURL == "https://github.com/" { + runnerGroups, err := c.Client.ListOrganizationRunnerGroupsForRepository(ctx, org, repo) + if err != nil { + return visible, err } - if runnerGroup.GetVisibility() != "all" { - hasAccess, err := c.hasRepoAccessToOrganizationRunnerGroup(ctx, org, runnerGroup.GetID(), repo) - if err != nil { - return visible, err - } + for _, runnerGroup := range runnerGroups { + ref := NewRunnerGroupFromGitHub(runnerGroup) - if !hasAccess { + if !managed.Includes(ref) { continue } + + visible.Add(ref) + } + } else { + runnerGroups, err := c.Client.ListOrganizationRunnerGroups(ctx, org) + if err != nil { + return visible, err } - visible.Add(ref) + for _, runnerGroup := range runnerGroups { + ref := NewRunnerGroupFromGitHub(runnerGroup) + + if !managed.Includes(ref) { + continue + } + + if runnerGroup.GetVisibility() != "all" { + hasAccess, err := c.hasRepoAccessToOrganizationRunnerGroup(ctx, org, runnerGroup.GetID(), repo) + if err != nil { + return visible, err + } + + if !hasAccess { + continue + } + } + + visible.Add(ref) + } } return visible, nil