mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-11 03:57:01 +00:00
support for organization runners
This commit is contained in:
@@ -31,6 +31,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, h.Body)
|
||||
}
|
||||
|
||||
// NewServer creates a fake server for running unit tests
|
||||
func NewServer() *httptest.Server {
|
||||
routes := map[string]handler{
|
||||
// For CreateRegistrationToken
|
||||
@@ -46,6 +47,18 @@ func NewServer() *httptest.Server {
|
||||
Status: http.StatusBadRequest,
|
||||
Body: "",
|
||||
},
|
||||
"/orgs/test/actions/runners/registration-token": handler{
|
||||
Status: http.StatusCreated,
|
||||
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
||||
},
|
||||
"/orgs/invalid/actions/runners/registration-token": handler{
|
||||
Status: http.StatusOK,
|
||||
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
||||
},
|
||||
"/orgs/error/actions/runners/registration-token": handler{
|
||||
Status: http.StatusBadRequest,
|
||||
Body: "",
|
||||
},
|
||||
|
||||
// For ListRunners
|
||||
"/repos/test/valid/actions/runners": handler{
|
||||
@@ -60,6 +73,18 @@ func NewServer() *httptest.Server {
|
||||
Status: http.StatusBadRequest,
|
||||
Body: "",
|
||||
},
|
||||
"/orgs/test/actions/runners": handler{
|
||||
Status: http.StatusOK,
|
||||
Body: RunnersListBody,
|
||||
},
|
||||
"/orgs/invalid/actions/runners": handler{
|
||||
Status: http.StatusNoContent,
|
||||
Body: "",
|
||||
},
|
||||
"/orgs/error/actions/runners": handler{
|
||||
Status: http.StatusBadRequest,
|
||||
Body: "",
|
||||
},
|
||||
|
||||
// For RemoveRunner
|
||||
"/repos/test/valid/actions/runners/1": handler{
|
||||
@@ -74,6 +99,18 @@ func NewServer() *httptest.Server {
|
||||
Status: http.StatusBadRequest,
|
||||
Body: "",
|
||||
},
|
||||
"/orgs/test/actions/runners/1": handler{
|
||||
Status: http.StatusNoContent,
|
||||
Body: "",
|
||||
},
|
||||
"/orgs/invalid/actions/runners/1": handler{
|
||||
Status: http.StatusOK,
|
||||
Body: "",
|
||||
},
|
||||
"/orgs/error/actions/runners/1": handler{
|
||||
Status: http.StatusBadRequest,
|
||||
Body: "",
|
||||
},
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -2,10 +2,8 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +12,7 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Client wraps GitHub client with some additional
|
||||
type Client struct {
|
||||
*github.Client
|
||||
regTokens map[string]*github.RegistrationToken
|
||||
@@ -34,7 +33,7 @@ func NewClient(appID, installationID int64, privateKeyPath string) (*Client, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewClient returns a client authenticated with personal access token.
|
||||
// NewClientWithAccessToken returns a client authenticated with personal access token.
|
||||
func NewClientWithAccessToken(token string) (*Client, error) {
|
||||
tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
@@ -48,22 +47,31 @@ func NewClientWithAccessToken(token string) (*Client, error) {
|
||||
}
|
||||
|
||||
// GetRegistrationToken returns a registration token tied with the name of repository and runner.
|
||||
func (c *Client) GetRegistrationToken(ctx context.Context, repository, name string) (*github.RegistrationToken, error) {
|
||||
func (c *Client) GetRegistrationToken(ctx context.Context, owner, repo, name string) (*github.RegistrationToken, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
owner, repo, err := splitOwnerAndRepo(repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
key := owner
|
||||
if len(repo) > 0 {
|
||||
key = fmt.Sprintf("%s/%s", repo, name)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s/%s", repo, name)
|
||||
var rt *github.RegistrationToken
|
||||
|
||||
rt, ok := c.regTokens[key]
|
||||
if ok && rt.GetExpiresAt().After(time.Now().Add(-10*time.Minute)) {
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
rt, res, err := c.Client.Actions.CreateRegistrationToken(ctx, owner, repo)
|
||||
var res *github.Response
|
||||
var err error
|
||||
|
||||
if len(repo) > 0 {
|
||||
rt, res, err = c.Client.Actions.CreateRegistrationToken(ctx, owner, repo)
|
||||
} else {
|
||||
rt, res, err = CreateOrganizationRegistrationToken(ctx, c, owner)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create registration token: %v", err)
|
||||
}
|
||||
@@ -81,13 +89,16 @@ func (c *Client) GetRegistrationToken(ctx context.Context, repository, name stri
|
||||
}
|
||||
|
||||
// RemoveRunner removes a runner with specified runner ID from repocitory.
|
||||
func (c *Client) RemoveRunner(ctx context.Context, repository string, runnerID int64) error {
|
||||
owner, repo, err := splitOwnerAndRepo(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
func (c *Client) RemoveRunner(ctx context.Context, owner, repo string, runnerID int64) error {
|
||||
var res *github.Response
|
||||
var err error
|
||||
|
||||
if len(repo) > 0 {
|
||||
res, err = c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID)
|
||||
} else {
|
||||
res, err = RemoveOrganizationRunner(ctx, c, owner, runnerID)
|
||||
}
|
||||
|
||||
res, err := c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove runner: %v", err)
|
||||
}
|
||||
@@ -99,18 +110,22 @@ func (c *Client) RemoveRunner(ctx context.Context, repository string, runnerID i
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRunners returns a list of runners of specified repository name.
|
||||
func (c *Client) ListRunners(ctx context.Context, repository string) ([]*github.Runner, error) {
|
||||
// ListRunners returns a list of runners of specified owner/repository name.
|
||||
func (c *Client) ListRunners(ctx context.Context, owner, repo string) ([]*github.Runner, error) {
|
||||
var runners []*github.Runner
|
||||
|
||||
owner, repo, err := splitOwnerAndRepo(repository)
|
||||
if err != nil {
|
||||
return runners, err
|
||||
}
|
||||
|
||||
opts := github.ListOptions{PerPage: 10}
|
||||
for {
|
||||
list, res, err := c.Client.Actions.ListRunners(ctx, owner, repo, &opts)
|
||||
list := &github.Runners{}
|
||||
var res *github.Response
|
||||
var err error
|
||||
|
||||
if len(repo) > 0 {
|
||||
list, res, err = c.Client.Actions.ListRunners(ctx, owner, repo, &opts)
|
||||
} else {
|
||||
list, res, err = ListOrganizationRunners(ctx, c, owner, &opts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return runners, fmt.Errorf("failed to remove runner: %v", err)
|
||||
}
|
||||
@@ -136,12 +151,3 @@ func (c *Client) cleanup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splitOwnerAndRepo splits specified repository name to the owner and repo name.
|
||||
func splitOwnerAndRepo(repo string) (string, string, error) {
|
||||
chunk := strings.Split(repo, "/")
|
||||
if len(chunk) != 2 {
|
||||
return "", "", errors.New("invalid repository name")
|
||||
}
|
||||
return chunk[0], chunk[1], nil
|
||||
}
|
||||
|
||||
95
github/github_beta.go
Normal file
95
github/github_beta.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package github
|
||||
|
||||
// this contains BETA API clients, that are currently not (yet) in go-github
|
||||
// once these functions have been added there, they can be removed from here
|
||||
// code was reused from https://github.com/google/go-github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/go-github/v31/github"
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// CreateOrganizationRegistrationToken creates a token that can be used to add a self-hosted runner on an organization.
|
||||
//
|
||||
// GitHub API docs: https://developer.github.com/v3/actions/self-hosted-runners/#create-a-registration-token-for-an-organization
|
||||
func CreateOrganizationRegistrationToken(ctx context.Context, client *Client, owner string) (*github.RegistrationToken, *github.Response, error) {
|
||||
u := fmt.Sprintf("orgs/%v/actions/runners/registration-token", owner)
|
||||
|
||||
req, err := client.NewRequest("POST", u, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
registrationToken := new(github.RegistrationToken)
|
||||
resp, err := client.Do(ctx, req, registrationToken)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return registrationToken, resp, nil
|
||||
}
|
||||
|
||||
// ListOrganizationRunners lists all the self-hosted runners for an organization.
|
||||
//
|
||||
// GitHub API docs: https://developer.github.com/v3/actions/self-hosted-runners/#list-self-hosted-runners-for-an-organization
|
||||
func ListOrganizationRunners(ctx context.Context, client *Client, owner string, opts *github.ListOptions) (*github.Runners, *github.Response, error) {
|
||||
u := fmt.Sprintf("orgs/%v/actions/runners", owner)
|
||||
u, err := addOptions(u, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := client.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
runners := &github.Runners{}
|
||||
resp, err := client.Do(ctx, req, &runners)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return runners, resp, nil
|
||||
}
|
||||
|
||||
// RemoveOrganizationRunner forces the removal of a self-hosted runner in a repository using the runner id.
|
||||
//
|
||||
// GitHub API docs: https://developer.github.com/v3/actions/self_hosted_runners/#remove-a-self-hosted-runner
|
||||
func RemoveOrganizationRunner(ctx context.Context, client *Client, owner string, runnerID int64) (*github.Response, error) {
|
||||
u := fmt.Sprintf("orgs/%v/actions/runners/%v", owner, runnerID)
|
||||
|
||||
req, err := client.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// addOptions adds the parameters in opt as URL query parameters to s. opt
|
||||
// must be a struct whose fields may contain "url" tags.
|
||||
func addOptions(s string, opts interface{}) (string, error) {
|
||||
v := reflect.ValueOf(opts)
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
qs, err := query.Values(opts)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
u.RawQuery = qs.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
@@ -36,18 +36,22 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestGetRegistrationToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
org string
|
||||
repo string
|
||||
token string
|
||||
err bool
|
||||
}{
|
||||
{repo: "test/valid", token: fake.RegistrationToken, err: false},
|
||||
{repo: "test/invalid", token: "", err: true},
|
||||
{repo: "test/error", token: "", err: true},
|
||||
{org: "test", repo: "valid", token: fake.RegistrationToken, err: false},
|
||||
{org: "test", repo: "invalid", token: "", err: true},
|
||||
{org: "test", repo: "error", token: "", err: true},
|
||||
{org: "test", repo: "", token: fake.RegistrationToken, err: false},
|
||||
{org: "invalid", repo: "", token: "", err: true},
|
||||
{org: "error", repo: "", token: "", err: true},
|
||||
}
|
||||
|
||||
client := newTestClient()
|
||||
for i, tt := range tests {
|
||||
rt, err := client.GetRegistrationToken(context.Background(), tt.repo, "test")
|
||||
rt, err := client.GetRegistrationToken(context.Background(), tt.org, tt.repo, "test")
|
||||
if !tt.err && err != nil {
|
||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||
}
|
||||
@@ -59,18 +63,22 @@ func TestGetRegistrationToken(t *testing.T) {
|
||||
|
||||
func TestListRunners(t *testing.T) {
|
||||
tests := []struct {
|
||||
org string
|
||||
repo string
|
||||
length int
|
||||
err bool
|
||||
}{
|
||||
{repo: "test/valid", length: 2, err: false},
|
||||
{repo: "test/invalid", length: 0, err: true},
|
||||
{repo: "test/error", length: 0, err: true},
|
||||
{org: "test", repo: "valid", length: 2, err: false},
|
||||
{org: "test", repo: "invalid", length: 0, err: true},
|
||||
{org: "test", repo: "error", length: 0, err: true},
|
||||
{org: "test", repo: "", length: 2, err: false},
|
||||
{org: "invalid", repo: "", length: 0, err: true},
|
||||
{org: "error", repo: "", length: 0, err: true},
|
||||
}
|
||||
|
||||
client := newTestClient()
|
||||
for i, tt := range tests {
|
||||
runners, err := client.ListRunners(context.Background(), tt.repo)
|
||||
runners, err := client.ListRunners(context.Background(), tt.org, tt.repo)
|
||||
if !tt.err && err != nil {
|
||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||
}
|
||||
@@ -82,17 +90,21 @@ func TestListRunners(t *testing.T) {
|
||||
|
||||
func TestRemoveRunner(t *testing.T) {
|
||||
tests := []struct {
|
||||
org string
|
||||
repo string
|
||||
err bool
|
||||
}{
|
||||
{repo: "test/valid", err: false},
|
||||
{repo: "test/invalid", err: true},
|
||||
{repo: "test/error", err: true},
|
||||
{org: "test", repo: "valid", err: false},
|
||||
{org: "test", repo: "invalid", err: true},
|
||||
{org: "test", repo: "error", err: true},
|
||||
{org: "test", repo: "", err: false},
|
||||
{org: "invalid", repo: "", err: true},
|
||||
{org: "error", repo: "", err: true},
|
||||
}
|
||||
|
||||
client := newTestClient()
|
||||
for i, tt := range tests {
|
||||
err := client.RemoveRunner(context.Background(), tt.repo, int64(1))
|
||||
err := client.RemoveRunner(context.Background(), tt.org, tt.repo, int64(1))
|
||||
if !tt.err && err != nil {
|
||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user