support for organization runners

This commit is contained in:
Reinier Timmer 2020-04-23 16:36:40 +02:00
parent d1429beaa6
commit fb35dd4131
12 changed files with 236 additions and 61 deletions

View File

@ -24,7 +24,11 @@ import (
// RunnerSpec defines the desired state of Runner
type RunnerSpec struct {
// +kubebuilder:validation:MinLength=3
// +kubebuilder:validation:Pattern=`^[^/]+/[^/]+$`
// +kubebuilder:validation:Pattern=`^[^/]+$`
Organization string `json:"organization"`
// +optional
// +kubebuilder:validation:Pattern=`^[^/]*$`
Repository string `json:"repository"`
// +optional

View File

@ -4115,9 +4115,12 @@ spec:
additionalProperties:
type: string
type: object
repository:
organization:
minLength: 3
pattern: ^[^/]+/[^/]+$
pattern: ^[^/]+$
type: string
repository:
pattern: ^[^/]*$
type: string
resources:
description: ResourceRequirements describes the compute resource
@ -6709,7 +6712,7 @@ spec:
type: object
type: array
required:
- repository
- organization
type: object
type: object
required:

View File

@ -4115,9 +4115,12 @@ spec:
additionalProperties:
type: string
type: object
repository:
organization:
minLength: 3
pattern: ^[^/]+/[^/]+$
pattern: ^[^/]+$
type: string
repository:
pattern: ^[^/]*$
type: string
resources:
description: ResourceRequirements describes the compute resource
@ -6709,7 +6712,7 @@ spec:
type: object
type: array
required:
- repository
- organization
type: object
type: object
required:

View File

@ -3858,9 +3858,12 @@ spec:
additionalProperties:
type: string
type: object
repository:
organization:
minLength: 3
pattern: ^[^/]+/[^/]+$
pattern: ^[^/]+$
type: string
repository:
pattern: ^[^/]*$
type: string
resources:
description: ResourceRequirements describes the compute resource requirements.
@ -6293,7 +6296,7 @@ spec:
type: object
type: array
required:
- repository
- organization
type: object
status:
description: RunnerStatus defines the observed state of Runner

View File

@ -83,7 +83,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers)
if removed {
ok, err := r.unregisterRunner(ctx, runner.Spec.Repository, runner.Name)
ok, err := r.unregisterRunner(ctx, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
if err != nil {
log.Error(err, "Failed to unregister runner")
return ctrl.Result{}, err
@ -108,7 +108,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
if !runner.IsRegisterable() {
rt, err := r.GitHubClient.GetRegistrationToken(ctx, runner.Spec.Repository, runner.Name)
rt, err := r.GitHubClient.GetRegistrationToken(ctx, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
if err != nil {
r.Recorder.Event(&runner, corev1.EventTypeWarning, "FailedUpdateRegistrationToken", "Updating registration token failed")
log.Error(err, "Failed to get new registration token")
@ -212,8 +212,8 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
return ctrl.Result{}, nil
}
func (r *RunnerReconciler) unregisterRunner(ctx context.Context, repo, name string) (bool, error) {
runners, err := r.GitHubClient.ListRunners(ctx, repo)
func (r *RunnerReconciler) unregisterRunner(ctx context.Context, org, repo, name string) (bool, error) {
runners, err := r.GitHubClient.ListRunners(ctx, org, repo)
if err != nil {
return false, err
}
@ -230,7 +230,7 @@ func (r *RunnerReconciler) unregisterRunner(ctx context.Context, repo, name stri
return false, nil
}
if err := r.GitHubClient.RemoveRunner(ctx, repo, id); err != nil {
if err := r.GitHubClient.RemoveRunner(ctx, org, repo, id); err != nil {
return false, err
}
@ -253,6 +253,10 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
Name: "RUNNER_NAME",
Value: runner.Name,
},
{
Name: "RUNNER_ORG",
Value: runner.Spec.Organization,
},
{
Name: "RUNNER_REPO",
Value: runner.Spec.Repository,

View File

@ -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()

View File

@ -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
View 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
}

View File

@ -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)
}

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/go-logr/logr v0.1.0
github.com/google/go-github/v31 v31.0.0
github.com/google/go-querystring v1.0.0
github.com/onsi/ginkgo v1.8.0
github.com/onsi/gomega v1.5.0
github.com/stretchr/testify v1.4.0 // indirect

1
go.sum
View File

@ -116,6 +116,7 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=

View File

@ -5,18 +5,24 @@ if [ -z "${RUNNER_NAME}" ]; then
exit 1
fi
if [ -z "${RUNNER_REPO}" ]; then
echo "RUNNER_REPO must be set" 1>&2
if [ -z "${RUNNER_ORG}" ]; then
echo "RUNNER_ORG must be set" 1>&2
exit 1
fi
ATTACH="${RUNNER_ORG}"
if [ ! -z "${RUNNER_REPO}" ]; then
ATTACH="${RUNNER_ORG}/${RUNNER_REPO}"
fi
if [ -z "${RUNNER_TOKEN}" ]; then
echo "RUNNER_TOKEN must be set" 1>&2
exit 1
fi
cd /runner
./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "https://github.com/${RUNNER_REPO}" --token "${RUNNER_TOKEN}"
./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "https://github.com/${ATTACH}" --token "${RUNNER_TOKEN}"
unset RUNNER_NAME RUNNER_REPO RUNNER_TOKEN
exec ./run.sh --once