Merge pull request #44 from reiniertimmer/organization-runners
support for organization runners & labels
This commit is contained in:
commit
7df119e470
61
README.md
61
README.md
|
|
@ -66,9 +66,11 @@ $ kubectl create secret generic controller-manager \
|
|||
|
||||
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*.
|
||||
|
||||
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 +89,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 +133,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 +227,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.
|
||||
|
|
|
|||
|
|
@ -23,9 +23,16 @@ import (
|
|||
|
||||
// RunnerSpec defines the desired state of Runner
|
||||
type RunnerSpec struct {
|
||||
// +kubebuilder:validation:MinLength=3
|
||||
// +optional
|
||||
// +kubebuilder:validation:Pattern=`^[^/]+$`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
|
||||
// +optional
|
||||
// +kubebuilder:validation:Pattern=`^[^/]+/[^/]+$`
|
||||
Repository string `json:"repository"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
|
||||
// +optional
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
|
||||
// +optional
|
||||
Containers []corev1.Container `json:"containers,omitempty"`
|
||||
|
|
@ -76,15 +83,20 @@ 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,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Labels []string `json:"labels,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=".spec.labels",name=Labels,type=string
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string
|
||||
|
||||
// Runner is the Schema for the runners API
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4111,12 +4111,18 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
organization:
|
||||
pattern: ^[^/]+$
|
||||
type: string
|
||||
repository:
|
||||
minLength: 3
|
||||
pattern: ^[^/]+/[^/]+$
|
||||
type: string
|
||||
resources:
|
||||
|
|
@ -6708,8 +6714,6 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- repository
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
|
|
|
|||
|
|
@ -4111,12 +4111,18 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
organization:
|
||||
pattern: ^[^/]+$
|
||||
type: string
|
||||
repository:
|
||||
minLength: 3
|
||||
pattern: ^[^/]+/[^/]+$
|
||||
type: string
|
||||
resources:
|
||||
|
|
@ -6708,8 +6714,6 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- repository
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
|
|
|
|||
|
|
@ -9,9 +9,15 @@ metadata:
|
|||
name: runners.actions.summerwind.dev
|
||||
spec:
|
||||
additionalPrinterColumns:
|
||||
- JSONPath: .spec.organization
|
||||
name: Organization
|
||||
type: string
|
||||
- JSONPath: .spec.repository
|
||||
name: Repository
|
||||
type: string
|
||||
- JSONPath: .spec.labels
|
||||
name: Labels
|
||||
type: string
|
||||
- JSONPath: .status.phase
|
||||
name: Status
|
||||
type: string
|
||||
|
|
@ -3854,12 +3860,18 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
organization:
|
||||
pattern: ^[^/]+$
|
||||
type: string
|
||||
repository:
|
||||
minLength: 3
|
||||
pattern: ^[^/]+/[^/]+$
|
||||
type: string
|
||||
resources:
|
||||
|
|
@ -6292,8 +6304,6 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- repository
|
||||
type: object
|
||||
status:
|
||||
description: RunnerStatus defines the observed state of Runner
|
||||
|
|
@ -6305,17 +6315,23 @@ spec:
|
|||
reason:
|
||||
type: string
|
||||
registration:
|
||||
description: RunnerStatusRegistration contains runner registration status
|
||||
properties:
|
||||
expiresAt:
|
||||
format: date-time
|
||||
type: string
|
||||
labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
organization:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
required:
|
||||
- expiresAt
|
||||
- repository
|
||||
- token
|
||||
type: object
|
||||
required:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
|
@ -65,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)
|
||||
|
||||
|
|
@ -83,7 +90,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 +115,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")
|
||||
|
|
@ -117,9 +124,11 @@ 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,
|
||||
Labels: runner.Spec.Labels,
|
||||
Token: rt.GetToken(),
|
||||
ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time),
|
||||
}
|
||||
|
||||
if err := r.Status().Update(ctx, updated); err != nil {
|
||||
|
|
@ -212,8 +221,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 +239,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,10 +262,18 @@ 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,
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_LABELS",
|
||||
Value: strings.Join(runner.Spec.Labels, ","),
|
||||
},
|
||||
{
|
||||
Name: "RUNNER_TOKEN",
|
||||
Value: runner.Status.Registration.Token,
|
||||
|
|
@ -428,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +2,6 @@ package github
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
@ -14,6 +13,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 +34,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 +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, repository, 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()
|
||||
|
||||
owner, repo, err := splitOwnerAndRepo(repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s/%s", repo, name)
|
||||
key := getRegistrationKey(org, repo)
|
||||
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)
|
||||
owner, repo, err := getOwnerAndRepo(org, repo)
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -81,13 +84,15 @@ 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)
|
||||
func (c *Client) RemoveRunner(ctx context.Context, org, repo string, runnerID int64) error {
|
||||
owner, repo, err := getOwnerAndRepo(org, repo)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID)
|
||||
res, err := c.removeRunner(ctx, owner, repo, runnerID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove runner: %v", err)
|
||||
}
|
||||
|
|
@ -99,18 +104,20 @@ 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) {
|
||||
var runners []*github.Runner
|
||||
// ListRunners returns a list of runners of specified owner/repository name.
|
||||
func (c *Client) ListRunners(ctx context.Context, org, repo string) ([]*github.Runner, error) {
|
||||
owner, repo, err := getOwnerAndRepo(org, repo)
|
||||
|
||||
owner, repo, err := splitOwnerAndRepo(repository)
|
||||
if err != nil {
|
||||
return runners, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var runners []*github.Runner
|
||||
|
||||
opts := github.ListOptions{PerPage: 10}
|
||||
for {
|
||||
list, res, err := c.Client.Actions.ListRunners(ctx, owner, repo, &opts)
|
||||
list, res, err := c.listRunners(ctx, owner, repo, &opts)
|
||||
|
||||
if err != nil {
|
||||
return runners, fmt.Errorf("failed to remove runner: %v", err)
|
||||
}
|
||||
|
|
@ -137,11 +144,56 @@ func (c *Client) cleanup() {
|
|||
}
|
||||
}
|
||||
|
||||
// splitOwnerAndRepo splits specified repository name to the owner and repo name.
|
||||
// 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 "", "", errors.New("invalid repository name")
|
||||
return "", "", fmt.Errorf("invalid repository name: '%s'", repo)
|
||||
}
|
||||
return chunk[0], chunk[1], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
1
go.mod
1
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
|
||||
|
|
|
|||
1
go.sum
1
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=
|
||||
|
|
|
|||
|
|
@ -5,18 +5,28 @@ if [ -z "${RUNNER_NAME}" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${RUNNER_REPO}" ]; then
|
||||
echo "RUNNER_REPO must be set" 1>&2
|
||||
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 [ -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/${RUNNER_REPO}" --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
|
||||
|
|
|
|||
Loading…
Reference in New Issue