From fb35dd41314d2ff979d6d5f32c852ce13ba99282 Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Thu, 23 Apr 2020 16:36:40 +0200 Subject: [PATCH 1/8] support for organization runners --- api/v1alpha1/runner_types.go | 6 +- ...ions.summerwind.dev_runnerdeployments.yaml | 9 +- ...ions.summerwind.dev_runnerreplicasets.yaml | 9 +- .../bases/actions.summerwind.dev_runners.yaml | 9 +- controllers/runner_controller.go | 14 ++- github/fake/fake.go | 37 ++++++++ github/github.go | 68 +++++++------ github/github_beta.go | 95 +++++++++++++++++++ github/github_test.go | 36 ++++--- go.mod | 1 + go.sum | 1 + runner/entrypoint.sh | 12 ++- 12 files changed, 236 insertions(+), 61 deletions(-) create mode 100644 github/github_beta.go diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 6c275a4e..c8266a62 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -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 diff --git a/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml b/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml index e1b53f38..3158dbdc 100644 --- a/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml +++ b/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml @@ -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: diff --git a/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml b/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml index 6ef86604..fdc49396 100644 --- a/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml +++ b/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml @@ -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: diff --git a/config/crd/bases/actions.summerwind.dev_runners.yaml b/config/crd/bases/actions.summerwind.dev_runners.yaml index 0681d35b..ccb0a965 100644 --- a/config/crd/bases/actions.summerwind.dev_runners.yaml +++ b/config/crd/bases/actions.summerwind.dev_runners.yaml @@ -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 diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 293297e8..9454e751 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -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, diff --git a/github/fake/fake.go b/github/fake/fake.go index a9ed3667..28bf9046 100644 --- a/github/fake/fake.go +++ b/github/fake/fake.go @@ -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() diff --git a/github/github.go b/github/github.go index 401eee4f..6bcd5518 100644 --- a/github/github.go +++ b/github/github.go @@ -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 -} diff --git a/github/github_beta.go b/github/github_beta.go new file mode 100644 index 00000000..f690b35f --- /dev/null +++ b/github/github_beta.go @@ -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 +} diff --git a/github/github_test.go b/github/github_test.go index bd048efa..8cfbd33c 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -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) } diff --git a/go.mod b/go.mod index 342f99a2..a81bb556 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9c79b86d..adb85fd2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh index 5bef0a63..ca59a7cf 100755 --- a/runner/entrypoint.sh +++ b/runner/entrypoint.sh @@ -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 From 75d15ee91bb8aa1cebe214e42195c2501228d4d4 Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Fri, 24 Apr 2020 07:17:09 +0200 Subject: [PATCH 2/8] backwards compatibility of dockerfile --- runner/entrypoint.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh index ca59a7cf..32d4ae31 100755 --- a/runner/entrypoint.sh +++ b/runner/entrypoint.sh @@ -5,15 +5,15 @@ if [ -z "${RUNNER_NAME}" ]; then exit 1 fi -if [ -z "${RUNNER_ORG}" ]; then - echo "RUNNER_ORG must be set" 1>&2 - exit 1 -fi - -ATTACH="${RUNNER_ORG}" - -if [ ! -z "${RUNNER_REPO}" ]; then +if [ -n "${RUNNER_ORG}" -a -n "${RUNNER_REPO}" ]; then ATTACH="${RUNNER_ORG}/${RUNNER_REPO}" +elif [ -n "${RUNNER_ORG}" ]; then + ATTACH="${RUNNER_ORG}" +elif [ -n "${RUNNER_REPO}" ]; then + ATTACH="${RUNNER_REPO}" +else + echo "At least one of RUNNER_ORG or RUNNER_REPO must be set" 1>&2 + exit 1 fi if [ -z "${RUNNER_TOKEN}" ]; then From eca3cc79413933c3e2659a5577d801b539dcf685 Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Fri, 24 Apr 2020 09:50:41 +0200 Subject: [PATCH 3/8] add organization info to runner status --- api/v1alpha1/runner_types.go | 9 ++++++--- config/crd/bases/actions.summerwind.dev_runners.yaml | 8 +++++++- controllers/runner_controller.go | 7 ++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index c8266a62..bb681393 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -80,14 +80,17 @@ type RunnerStatus struct { Message string `json:"message"` } +// RunnerStatusRegistration contains runner registration status type RunnerStatusRegistration struct { - Repository string `json:"repository"` - Token string `json:"token"` - ExpiresAt metav1.Time `json:"expiresAt"` + Organization string `json:"organization"` + Repository string `json:"repository,omitempty"` + Token string `json:"token"` + ExpiresAt metav1.Time `json:"expiresAt"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=".spec.organization",name=Organization,type=string // +kubebuilder:printcolumn:JSONPath=".spec.repository",name=Repository,type=string // +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string diff --git a/config/crd/bases/actions.summerwind.dev_runners.yaml b/config/crd/bases/actions.summerwind.dev_runners.yaml index ccb0a965..13f05915 100644 --- a/config/crd/bases/actions.summerwind.dev_runners.yaml +++ b/config/crd/bases/actions.summerwind.dev_runners.yaml @@ -9,6 +9,9 @@ metadata: name: runners.actions.summerwind.dev spec: additionalPrinterColumns: + - JSONPath: .spec.organization + name: Organization + type: string - JSONPath: .spec.repository name: Repository type: string @@ -6308,17 +6311,20 @@ spec: reason: type: string registration: + description: RunnerStatusRegistration contains runner registration status properties: expiresAt: format: date-time type: string + organization: + type: string repository: type: string token: type: string required: - expiresAt - - repository + - organization - token type: object required: diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 9454e751..92e06c49 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -117,9 +117,10 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { updated := runner.DeepCopy() updated.Status.Registration = v1alpha1.RunnerStatusRegistration{ - Repository: runner.Spec.Repository, - Token: rt.GetToken(), - ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time), + Organization: runner.Spec.Organization, + Repository: runner.Spec.Repository, + Token: rt.GetToken(), + ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time), } if err := r.Status().Update(ctx, updated); err != nil { From 2567f6ee4e4664906cb0faa4951b502233b86ffd Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Fri, 24 Apr 2020 10:03:39 +0200 Subject: [PATCH 4/8] omit empty repository from runner spec --- api/v1alpha1/runner_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index bb681393..3ea55925 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -29,7 +29,7 @@ type RunnerSpec struct { // +optional // +kubebuilder:validation:Pattern=`^[^/]*$` - Repository string `json:"repository"` + Repository string `json:"repository,omitempty"` // +optional Containers []corev1.Container `json:"containers,omitempty"` From 8c5b776807a7264f88890666ca2be13ce080fe69 Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Fri, 24 Apr 2020 11:29:52 +0200 Subject: [PATCH 5/8] support runner labels --- api/v1alpha1/runner_types.go | 5 +++++ api/v1alpha1/zz_generated.deepcopy.go | 10 ++++++++++ .../actions.summerwind.dev_runnerdeployments.yaml | 4 ++++ .../actions.summerwind.dev_runnerreplicasets.yaml | 4 ++++ config/crd/bases/actions.summerwind.dev_runners.yaml | 11 +++++++++++ controllers/runner_controller.go | 6 ++++++ runner/entrypoint.sh | 6 +++++- 7 files changed, 45 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 3ea55925..3a9f2ffe 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -31,6 +31,9 @@ type RunnerSpec struct { // +kubebuilder:validation:Pattern=`^[^/]*$` Repository string `json:"repository,omitempty"` + // +optional + Labels []string `json:"labels,omitempty"` + // +optional Containers []corev1.Container `json:"containers,omitempty"` // +optional @@ -84,6 +87,7 @@ type RunnerStatus struct { type RunnerStatusRegistration struct { Organization string `json:"organization"` Repository string `json:"repository,omitempty"` + Labels []string `json:"labels,omitempty"` Token string `json:"token"` ExpiresAt metav1.Time `json:"expiresAt"` } @@ -92,6 +96,7 @@ type RunnerStatusRegistration struct { // +kubebuilder:subresource:status // +kubebuilder:printcolumn:JSONPath=".spec.organization",name=Organization,type=string // +kubebuilder:printcolumn:JSONPath=".spec.repository",name=Repository,type=string +// +kubebuilder:printcolumn:JSONPath=".spec.labels",name=Labels,type=string // +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string // Runner is the Schema for the runners API diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0b700e1c..029f7219 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -277,6 +277,11 @@ func (in *RunnerReplicaSetStatus) DeepCopy() *RunnerReplicaSetStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) { *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Containers != nil { in, out := &in.Containers, &out.Containers *out = make([]v1.Container, len(*in)) @@ -404,6 +409,11 @@ func (in *RunnerStatus) DeepCopy() *RunnerStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RunnerStatusRegistration) DeepCopyInto(out *RunnerStatusRegistration) { *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } in.ExpiresAt.DeepCopyInto(&out.ExpiresAt) } diff --git a/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml b/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml index 3158dbdc..5bca12b6 100644 --- a/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml +++ b/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml @@ -4111,6 +4111,10 @@ spec: - name type: object type: array + labels: + items: + type: string + type: array nodeSelector: additionalProperties: type: string diff --git a/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml b/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml index fdc49396..193b1c33 100644 --- a/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml +++ b/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml @@ -4111,6 +4111,10 @@ spec: - name type: object type: array + labels: + items: + type: string + type: array nodeSelector: additionalProperties: type: string diff --git a/config/crd/bases/actions.summerwind.dev_runners.yaml b/config/crd/bases/actions.summerwind.dev_runners.yaml index 13f05915..2de67d7d 100644 --- a/config/crd/bases/actions.summerwind.dev_runners.yaml +++ b/config/crd/bases/actions.summerwind.dev_runners.yaml @@ -15,6 +15,9 @@ spec: - JSONPath: .spec.repository name: Repository type: string + - JSONPath: .spec.labels + name: Labels + type: string - JSONPath: .status.phase name: Status type: string @@ -3857,6 +3860,10 @@ spec: - name type: object type: array + labels: + items: + type: string + type: array nodeSelector: additionalProperties: type: string @@ -6316,6 +6323,10 @@ spec: expiresAt: format: date-time type: string + labels: + items: + type: string + type: array organization: type: string repository: diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index 92e06c49..be6863ed 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "strings" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/errors" @@ -119,6 +120,7 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { updated.Status.Registration = v1alpha1.RunnerStatusRegistration{ Organization: runner.Spec.Organization, Repository: runner.Spec.Repository, + Labels: runner.Spec.Labels, Token: rt.GetToken(), ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time), } @@ -262,6 +264,10 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) { Name: "RUNNER_REPO", Value: runner.Spec.Repository, }, + { + Name: "RUNNER_LABELS", + Value: strings.Join(runner.Spec.Labels, ","), + }, { Name: "RUNNER_TOKEN", Value: runner.Status.Registration.Token, diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh index 32d4ae31..56cb7281 100755 --- a/runner/entrypoint.sh +++ b/runner/entrypoint.sh @@ -16,13 +16,17 @@ else exit 1 fi +if [ -n "${RUNNER_LABELS}" ]; then + LABEL_ARG="--labels ${RUNNER_LABELS}" +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/${ATTACH}" --token "${RUNNER_TOKEN}" +./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "https://github.com/${ATTACH}" --token "${RUNNER_TOKEN}" ${LABEL_ARG} unset RUNNER_NAME RUNNER_REPO RUNNER_TOKEN exec ./run.sh --once From 9f57f52e369df516881fc43bc60e21950ea1e8e7 Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Tue, 28 Apr 2020 07:24:37 +0200 Subject: [PATCH 6/8] organization and repository are now exclusive --- api/v1alpha1/runner_types.go | 8 +- ...ions.summerwind.dev_runnerdeployments.yaml | 5 +- ...ions.summerwind.dev_runnerreplicasets.yaml | 5 +- .../bases/actions.summerwind.dev_runners.yaml | 6 +- controllers/runner_controller.go | 18 +++ github/github.go | 108 +++++++++++++----- 6 files changed, 102 insertions(+), 48 deletions(-) diff --git a/api/v1alpha1/runner_types.go b/api/v1alpha1/runner_types.go index 3a9f2ffe..252b5c39 100644 --- a/api/v1alpha1/runner_types.go +++ b/api/v1alpha1/runner_types.go @@ -23,12 +23,12 @@ import ( // RunnerSpec defines the desired state of Runner type RunnerSpec struct { - // +kubebuilder:validation:MinLength=3 + // +optional // +kubebuilder:validation:Pattern=`^[^/]+$` - Organization string `json:"organization"` + Organization string `json:"organization,omitempty"` // +optional - // +kubebuilder:validation:Pattern=`^[^/]*$` + // +kubebuilder:validation:Pattern=`^[^/]+/[^/]+$` Repository string `json:"repository,omitempty"` // +optional @@ -85,7 +85,7 @@ type RunnerStatus struct { // RunnerStatusRegistration contains runner registration status type RunnerStatusRegistration struct { - Organization string `json:"organization"` + Organization string `json:"organization,omitempty"` Repository string `json:"repository,omitempty"` Labels []string `json:"labels,omitempty"` Token string `json:"token"` diff --git a/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml b/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml index 5bca12b6..da31694c 100644 --- a/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml +++ b/config/crd/bases/actions.summerwind.dev_runnerdeployments.yaml @@ -4120,11 +4120,10 @@ spec: type: string type: object organization: - minLength: 3 pattern: ^[^/]+$ type: string repository: - pattern: ^[^/]*$ + pattern: ^[^/]+/[^/]+$ type: string resources: description: ResourceRequirements describes the compute resource @@ -6715,8 +6714,6 @@ spec: - name type: object type: array - required: - - organization type: object type: object required: diff --git a/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml b/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml index 193b1c33..06216a1e 100644 --- a/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml +++ b/config/crd/bases/actions.summerwind.dev_runnerreplicasets.yaml @@ -4120,11 +4120,10 @@ spec: type: string type: object organization: - minLength: 3 pattern: ^[^/]+$ type: string repository: - pattern: ^[^/]*$ + pattern: ^[^/]+/[^/]+$ type: string resources: description: ResourceRequirements describes the compute resource @@ -6715,8 +6714,6 @@ spec: - name type: object type: array - required: - - organization type: object type: object required: diff --git a/config/crd/bases/actions.summerwind.dev_runners.yaml b/config/crd/bases/actions.summerwind.dev_runners.yaml index 2de67d7d..4450d325 100644 --- a/config/crd/bases/actions.summerwind.dev_runners.yaml +++ b/config/crd/bases/actions.summerwind.dev_runners.yaml @@ -3869,11 +3869,10 @@ spec: type: string type: object organization: - minLength: 3 pattern: ^[^/]+$ type: string repository: - pattern: ^[^/]*$ + pattern: ^[^/]+/[^/]+$ type: string resources: description: ResourceRequirements describes the compute resource requirements. @@ -6305,8 +6304,6 @@ spec: - name type: object type: array - required: - - organization type: object status: description: RunnerStatus defines the observed state of Runner @@ -6335,7 +6332,6 @@ spec: type: string required: - expiresAt - - organization - token type: object required: diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index be6863ed..7cb9fd14 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -66,6 +66,12 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, client.IgnoreNotFound(err) } + err := validateRunnerSpec(&runner.Spec) + if err != nil { + log.Info("Failed to validate runner spec", "error", err.Error()) + return ctrl.Result{}, nil + } + if runner.ObjectMeta.DeletionTimestamp.IsZero() { finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers) @@ -439,3 +445,15 @@ func removeFinalizer(finalizers []string) ([]string, bool) { return result, removed } + +// organization & repository are both exclusive - however this cannot be checked with kubebuilder +// therefore have an additional check here to log an error in case spec is invalid +func validateRunnerSpec(spec *v1alpha1.RunnerSpec) error { + if len(spec.Organization) == 0 && len(spec.Repository) == 0 { + return fmt.Errorf("RunnerSpec needs organization or repository") + } + if len(spec.Organization) > 0 && len(spec.Repository) > 0 { + return fmt.Errorf("RunnerSpec cannot have both organization and repository") + } + return nil +} diff --git a/github/github.go b/github/github.go index 6bcd5518..a4de3ac1 100644 --- a/github/github.go +++ b/github/github.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "sync" "time" @@ -47,31 +48,25 @@ 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, owner, repo, name string) (*github.RegistrationToken, error) { +func (c *Client) GetRegistrationToken(ctx context.Context, org, repo, name string) (*github.RegistrationToken, error) { c.mu.Lock() defer c.mu.Unlock() - key := owner - if len(repo) > 0 { - key = fmt.Sprintf("%s/%s", repo, name) - } - - var rt *github.RegistrationToken - + key := getRegistrationKey(org, repo) rt, ok := c.regTokens[key] + if ok && rt.GetExpiresAt().After(time.Now().Add(-10*time.Minute)) { return rt, nil } - var res *github.Response - var err error + owner, repo, err := getOwnerAndRepo(org, repo) - 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 rt, err } + rt, res, err := c.createRegistrationToken(ctx, owner, repo) + if err != nil { return nil, fmt.Errorf("failed to create registration token: %v", err) } @@ -89,16 +84,15 @@ func (c *Client) GetRegistrationToken(ctx context.Context, owner, repo, name str } // RemoveRunner removes a runner with specified runner ID from repocitory. -func (c *Client) RemoveRunner(ctx context.Context, owner, repo string, runnerID int64) error { - var res *github.Response - var err error +func (c *Client) RemoveRunner(ctx context.Context, org, repo string, runnerID int64) error { + owner, repo, err := getOwnerAndRepo(org, repo) - if len(repo) > 0 { - res, err = c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID) - } else { - res, err = RemoveOrganizationRunner(ctx, c, owner, runnerID) + if err != nil { + return err } + res, err := c.removeRunner(ctx, owner, repo, runnerID) + if err != nil { return fmt.Errorf("failed to remove runner: %v", err) } @@ -111,20 +105,18 @@ func (c *Client) RemoveRunner(ctx context.Context, owner, repo string, runnerID } // ListRunners returns a list of runners of specified owner/repository name. -func (c *Client) ListRunners(ctx context.Context, owner, repo string) ([]*github.Runner, error) { +func (c *Client) ListRunners(ctx context.Context, org, repo string) ([]*github.Runner, error) { + owner, repo, err := getOwnerAndRepo(org, repo) + + if err != nil { + return nil, err + } + var runners []*github.Runner opts := github.ListOptions{PerPage: 10} for { - 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) - } + list, res, err := c.listRunners(ctx, owner, repo, &opts) if err != nil { return runners, fmt.Errorf("failed to remove runner: %v", err) @@ -151,3 +143,57 @@ func (c *Client) cleanup() { } } } + +// wrappers for github functions (switch between organization/repository mode) +// so the calling functions don't need to switch and their code is a bit cleaner + +func (c *Client) createRegistrationToken(ctx context.Context, owner, repo string) (*github.RegistrationToken, *github.Response, error) { + if len(repo) > 0 { + return c.Client.Actions.CreateRegistrationToken(ctx, owner, repo) + } else { + return CreateOrganizationRegistrationToken(ctx, c, owner) + } +} + +func (c *Client) removeRunner(ctx context.Context, owner, repo string, runnerID int64) (*github.Response, error) { + if len(repo) > 0 { + return c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID) + } else { + return RemoveOrganizationRunner(ctx, c, owner, runnerID) + } +} + +func (c *Client) listRunners(ctx context.Context, owner, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { + if len(repo) > 0 { + return c.Client.Actions.ListRunners(ctx, owner, repo, opts) + } else { + return ListOrganizationRunners(ctx, c, owner, opts) + } +} + +// Validates owner and repo arguments. Both are optional, but at least one should be specified +func getOwnerAndRepo(org, repo string) (string, string, error) { + if len(repo) > 0 { + return splitOwnerAndRepo(repo) + } + if len(org) > 0 { + return org, "", nil + } + return "", "", fmt.Errorf("organization and repository are both empty") +} + +func getRegistrationKey(org, repo string) string { + if len(org) > 0 { + return org + } else { + return repo + } +} + +func splitOwnerAndRepo(repo string) (string, string, error) { + chunk := strings.Split(repo, "/") + if len(chunk) != 2 { + return "", "", fmt.Errorf("invalid repository name: '%s'", repo) + } + return chunk[0], chunk[1], nil +} From 8c42b317ec7288a327cddb11b42124beef0bf762 Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Tue, 28 Apr 2020 10:56:26 +0200 Subject: [PATCH 7/8] updated documentation --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2176cc6b..a7182a1c 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,17 @@ $ kubectl create secret generic controller-manager \ ### Using Personal Access Token +<<<<<<< HEAD From an account that has `admin` privileges for the repository, create a [personal access token](https://github.com/settings/tokens) with `repo` scope. This token is used to register a self-hosted runner by *actions-runner-controller*. +======= +Next, from an account that has `admin` privileges for the repository, create a [personal access token](https://github.com/settings/tokens). +>>>>>>> updated documentation -To use a Personal Access Token, you must issue the token with an account that has `admin` privileges. +Self-hosted runners in GitHub can either be connected to a single repository, or to a GitHub organization (so they are available to all repositories in the organization). This token is used to register a self-hosted runner by *actions-runner-controller*. -Open the Create Token page from the following link, grant the `repo` scope, and press the "Generate Token" button at the bottom of the page to create the token. +For adding a runner to a repository, the token should have `repo` scope. If the runner should be added to an organization, the token should have `admin:org` scope. Note that to use a Personal Access Token, you must issue the token with an account that has `admin` privileges (on the repository and/or the organization). + +Open the Create Token page from the following link, grant the `repo` and/or `admin:org` scope, and press the "Generate Token" button at the bottom of the page to create the token. - [Create personal access token](https://github.com/settings/tokens/new) @@ -87,7 +93,7 @@ There are two ways to use this controller: - Manage runners one by one with `Runner`. - Manage a set of runners with `RunnerDeployment`. -### Runners +### Repository runners To launch a single self-hosted runner, you need to create a manifest file includes *Runner* resource as follows. This example launches a self-hosted runner with name *example-runner* for the *summerwind/actions-runner-controller* repository. @@ -131,6 +137,22 @@ The runner you created has been registered to your repository. Now your can use your self-hosted runner. See the [official documentation](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-self-hosted-runners-in-a-workflow) on how to run a job with it. +### Organization Runners + +To add the runner to an organization, you only need to replace the `repository` field with `organization`, so the runner will register itself to the organization. + +``` +# runner.yaml +apiVersion: actions.summerwind.dev/v1alpha1 +kind: Runner +metadata: + name: example-org-runner +spec: + organization: your-organization-name +``` + +Now you can see the runner on the organization level (if you have organization owner permissions). + ### RunnerDeployments There are `RunnerReplicaSet` and `RunnerDeployment` that corresponds to `ReplicaSet` and `Deployment` but for `Runner`. @@ -209,3 +231,40 @@ spec: securityContext: runAsUser: 0 ``` + +## Runner labels + +To run a workflow job on a self-hosted runner, you can use the following syntax in your workflow: + +```yaml +jobs: + release: + runs-on: self-hosted +``` + +When you have multiple kinds of self-hosted runners, you can distinguish between them using labels. In order to do so, you can specify one or more labels in your `Runner` or `RunnerDeployment` spec. + +```yaml +# runnerdeployment.yaml +apiVersion: actions.summerwind.dev/v1alpha1 +kind: RunnerDeployment +metadata: + name: custom-runner +spec: + replicas: 1 + template: + spec: + repository: summerwind/actions-runner-controller + labels: + - custom-runner +``` + +Once this spec is applied, you can observe the labels for your runner from the repository or organization in the GitHub settings page for the repository or organization. You can now select a specific runner from your workflow by using the label in `runs-on`: + +```yaml +jobs: + release: + runs-on: custom-runner +``` + +Note that if you specify `self-hosted` in your worlflow, then this will run your job on _any_ self-hosted runner, regardless of the labels that they have. From 966e0dca372c61896eec96f6a29e6e91776b89fa Mon Sep 17 00:00:00 2001 From: Reinier Timmer Date: Tue, 28 Apr 2020 11:24:59 +0200 Subject: [PATCH 8/8] fix merge conflict on README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index a7182a1c..d2c96ab5 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,7 @@ $ kubectl create secret generic controller-manager \ ### Using Personal Access Token -<<<<<<< HEAD From an account that has `admin` privileges for the repository, create a [personal access token](https://github.com/settings/tokens) with `repo` scope. This token is used to register a self-hosted runner by *actions-runner-controller*. -======= -Next, from an account that has `admin` privileges for the repository, create a [personal access token](https://github.com/settings/tokens). ->>>>>>> updated documentation Self-hosted runners in GitHub can either be connected to a single repository, or to a GitHub organization (so they are available to all repositories in the organization). This token is used to register a self-hosted runner by *actions-runner-controller*.