mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 11:41:27 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
390f2a62d9 | ||
|
|
1555651325 | ||
|
|
e7445e286f | ||
|
|
79655989d0 | ||
|
|
55323c3754 | ||
|
|
f80c3c1928 | ||
|
|
9a86812214 | ||
|
|
e889eaeb04 | ||
|
|
b96979888c | ||
|
|
7df119e470 | ||
|
|
966e0dca37 | ||
|
|
8c42b317ec | ||
|
|
9f57f52e36 | ||
|
|
8c5b776807 | ||
|
|
2567f6ee4e | ||
|
|
eca3cc7941 | ||
|
|
75d15ee91b | ||
|
|
fb35dd4131 |
107
README.md
107
README.md
@@ -6,11 +6,15 @@ This controller operates self-hosted runners for GitHub Actions on your Kubernet
|
|||||||
|
|
||||||
[GitHub Actions](https://github.com/features/actions) is a very useful tool for automating development. GitHub Actions jobs are run in the cloud by default, but you may want to run your jobs in your environment. [Self-hosted runner](https://github.com/actions/runner) can be used for such use cases, but requires the provisioning and configuration of a virtual machine instance. Instead if you already have a Kubernetes cluster, it makes more sense to run the self-hosted runner on top of it.
|
[GitHub Actions](https://github.com/features/actions) is a very useful tool for automating development. GitHub Actions jobs are run in the cloud by default, but you may want to run your jobs in your environment. [Self-hosted runner](https://github.com/actions/runner) can be used for such use cases, but requires the provisioning and configuration of a virtual machine instance. Instead if you already have a Kubernetes cluster, it makes more sense to run the self-hosted runner on top of it.
|
||||||
|
|
||||||
*actions-runner-controller* makes that possible. Just create a *Runner* resource on your Kubernetes, and it will run and operate the self-hosted runner for the specified repository. Combined with Kubernetes RBAC, you can also build simple Self-hosted runners as a Service.
|
**actions-runner-controller** makes that possible. Just create a *Runner* resource on your Kubernetes, and it will run and operate the self-hosted runner for the specified repository. Combined with Kubernetes RBAC, you can also build simple Self-hosted runners as a Service.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
First, install *actions-runner-controller* with a manifest file. This will create *actions-runner-system* namespace in your Kubernetes and deploy the required resources.
|
actions-runner-controller uses [cert-manager](https://cert-manager.io/docs/installation/kubernetes/) for certificate management of Admission Webhook. Make sure you have already installed cert-manager before you install. The installation instructions for cert-manager can be found below.
|
||||||
|
|
||||||
|
- [Installing cert-manager on Kubernetes](https://cert-manager.io/docs/installation/kubernetes/)
|
||||||
|
|
||||||
|
Install the custom resource and actions-runner-controller itself. This will create actions-runner-system namespace in your Kubernetes and deploy the required resources.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml
|
$ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml
|
||||||
@@ -18,7 +22,7 @@ $ kubectl apply -f https://github.com/summerwind/actions-runner-controller/relea
|
|||||||
|
|
||||||
## Setting up authentication with GitHub API
|
## Setting up authentication with GitHub API
|
||||||
|
|
||||||
There are two ways for _actions-runner-controller_ to authenticate with the the GitHub API:
|
There are two ways for actions-runner-controller to authenticate with the the GitHub API:
|
||||||
|
|
||||||
1. Using GitHub App.
|
1. Using GitHub App.
|
||||||
2. Using Personal Access Token.
|
2. Using Personal Access Token.
|
||||||
@@ -66,9 +70,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*.
|
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)
|
- [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 runners one by one with `Runner`.
|
||||||
- Manage a set of runners with `RunnerDeployment`.
|
- 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.
|
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.
|
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
|
### RunnerDeployments
|
||||||
|
|
||||||
There are `RunnerReplicaSet` and `RunnerDeployment` that corresponds to `ReplicaSet` and `Deployment` but for `Runner`.
|
There are `RunnerReplicaSet` and `RunnerDeployment` that corresponds to `ReplicaSet` and `Deployment` but for `Runner`.
|
||||||
@@ -162,7 +184,6 @@ You can see that 2 runners have been created as specified by `replicas: 2`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
$ kubectl get runners
|
$ kubectl get runners
|
||||||
NAME REPOSITORY STATUS
|
|
||||||
NAME REPOSITORY STATUS
|
NAME REPOSITORY STATUS
|
||||||
example-runnerdeploy2475h595fr mumoshu/actions-runner-controller-ci Running
|
example-runnerdeploy2475h595fr mumoshu/actions-runner-controller-ci Running
|
||||||
example-runnerdeploy2475ht2qbr mumoshu/actions-runner-controller-ci Running
|
example-runnerdeploy2475ht2qbr mumoshu/actions-runner-controller-ci Running
|
||||||
@@ -209,3 +230,75 @@ spec:
|
|||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 0
|
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.
|
||||||
|
|
||||||
|
## Softeware installed in the runner image
|
||||||
|
|
||||||
|
The GitHub hosted runners include a large amount of pre-installed software packages. For Ubuntu 18.04, this list can be found at https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md
|
||||||
|
|
||||||
|
The container image is based on Ubuntu 18.04, but it does not contain all of the software installed on the GitHub runners. It contains the following subset of packages from the GitHub runners:
|
||||||
|
|
||||||
|
* Basic CLI packages
|
||||||
|
* git (2.26)
|
||||||
|
* docker
|
||||||
|
* build-essentials
|
||||||
|
|
||||||
|
The virtual environments from GitHub contain a lot more software packages (different versions of Java, Node.js, Golang, .NET, etc) which are not provided in the runner image. Most of these have dedicated setup actions which allow the tools to be installed on-demand in a workflow, for example: `actions/setup-java` or `actions/setup-node`
|
||||||
|
|
||||||
|
If there is a need to include packages in the runner image for which there is no setup action, then this can be achieved by building a custom container image for the runner. The easiest way is to start with the `summerwind/actions-runner` image and installing the extra dependencies directly in the docker image:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
FROM summerwind/actions-runner:v2.169.1
|
||||||
|
|
||||||
|
RUN sudo apt update -y \
|
||||||
|
&& apt install YOUR_PACKAGE
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then configure the runner to use a custom docker image by configuring the `image` field of a `Runner` or `RunnerDeployment`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: actions.summerwind.dev/v1alpha1
|
||||||
|
kind: Runner
|
||||||
|
metadata:
|
||||||
|
name: custom-runner
|
||||||
|
spec:
|
||||||
|
repository: summerwind/actions-runner-controller
|
||||||
|
image: YOUR_CUSTOM_DOCKER_IMAGE
|
||||||
|
```
|
||||||
|
|||||||
@@ -17,15 +17,24 @@ limitations under the License.
|
|||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunnerSpec defines the desired state of Runner
|
// RunnerSpec defines the desired state of Runner
|
||||||
type RunnerSpec struct {
|
type RunnerSpec struct {
|
||||||
// +kubebuilder:validation:MinLength=3
|
// +optional
|
||||||
|
// +kubebuilder:validation:Pattern=`^[^/]+$`
|
||||||
|
Organization string `json:"organization,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
// +kubebuilder:validation:Pattern=`^[^/]+/[^/]+$`
|
// +kubebuilder:validation:Pattern=`^[^/]+/[^/]+$`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
|
||||||
// +optional
|
// +optional
|
||||||
Containers []corev1.Container `json:"containers,omitempty"`
|
Containers []corev1.Container `json:"containers,omitempty"`
|
||||||
@@ -68,6 +77,19 @@ type RunnerSpec struct {
|
|||||||
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
|
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateRepository validates repository field.
|
||||||
|
func (rs *RunnerSpec) ValidateRepository() error {
|
||||||
|
// Organization and repository are both exclusive.
|
||||||
|
if len(rs.Organization) == 0 && len(rs.Repository) == 0 {
|
||||||
|
return errors.New("Spec needs organization or repository")
|
||||||
|
}
|
||||||
|
if len(rs.Organization) > 0 && len(rs.Repository) > 0 {
|
||||||
|
return errors.New("Spec cannot have both organization and repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RunnerStatus defines the observed state of Runner
|
// RunnerStatus defines the observed state of Runner
|
||||||
type RunnerStatus struct {
|
type RunnerStatus struct {
|
||||||
Registration RunnerStatusRegistration `json:"registration"`
|
Registration RunnerStatusRegistration `json:"registration"`
|
||||||
@@ -76,15 +98,20 @@ type RunnerStatus struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunnerStatusRegistration contains runner registration status
|
||||||
type RunnerStatusRegistration struct {
|
type RunnerStatusRegistration struct {
|
||||||
Repository string `json:"repository"`
|
Organization string `json:"organization,omitempty"`
|
||||||
Token string `json:"token"`
|
Repository string `json:"repository,omitempty"`
|
||||||
ExpiresAt metav1.Time `json:"expiresAt"`
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt metav1.Time `json:"expiresAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
// +kubebuilder:subresource:status
|
// +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.repository",name=Repository,type=string
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".spec.labels",name=Labels,type=string
|
||||||
// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string
|
// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string
|
||||||
|
|
||||||
// Runner is the Schema for the runners API
|
// Runner is the Schema for the runners API
|
||||||
|
|||||||
84
api/v1alpha1/runner_webhook.go
Normal file
84
api/v1alpha1/runner_webhook.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
// log is for logging in this package.
|
||||||
|
var runnerLog = logf.Log.WithName("runner-resource")
|
||||||
|
|
||||||
|
func (r *Runner) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewWebhookManagedBy(mgr).
|
||||||
|
For(r).
|
||||||
|
Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=mutate.runner.actions.summerwind.dev
|
||||||
|
|
||||||
|
var _ webhook.Defaulter = &Runner{}
|
||||||
|
|
||||||
|
// Default implements webhook.Defaulter so a webhook will be registered for the type
|
||||||
|
func (r *Runner) Default() {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runner,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runners,versions=v1alpha1,name=validate.runner.actions.summerwind.dev
|
||||||
|
|
||||||
|
var _ webhook.Validator = &Runner{}
|
||||||
|
|
||||||
|
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *Runner) ValidateCreate() error {
|
||||||
|
runnerLog.Info("validate resource to be created", "name", r.Name)
|
||||||
|
return r.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *Runner) ValidateUpdate(old runtime.Object) error {
|
||||||
|
runnerLog.Info("validate resource to be updated", "name", r.Name)
|
||||||
|
return r.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *Runner) ValidateDelete() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates resource spec.
|
||||||
|
func (r *Runner) Validate() error {
|
||||||
|
var (
|
||||||
|
errList field.ErrorList
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = r.Spec.ValidateRepository()
|
||||||
|
if err != nil {
|
||||||
|
errList = append(errList, field.Invalid(field.NewPath("spec", "repository"), r.Spec.Repository, err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errList) > 0 {
|
||||||
|
return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
84
api/v1alpha1/runnerdeployment_webhook.go
Normal file
84
api/v1alpha1/runnerdeployment_webhook.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
// log is for logging in this package.
|
||||||
|
var runenrDeploymentLog = logf.Log.WithName("runnerdeployment-resource")
|
||||||
|
|
||||||
|
func (r *RunnerDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewWebhookManagedBy(mgr).
|
||||||
|
For(r).
|
||||||
|
Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=mutate.runnerdeployment.actions.summerwind.dev
|
||||||
|
|
||||||
|
var _ webhook.Defaulter = &RunnerDeployment{}
|
||||||
|
|
||||||
|
// Default implements webhook.Defaulter so a webhook will be registered for the type
|
||||||
|
func (r *RunnerDeployment) Default() {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerdeployment,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerdeployments,versions=v1alpha1,name=validate.runnerdeployment.actions.summerwind.dev
|
||||||
|
|
||||||
|
var _ webhook.Validator = &RunnerDeployment{}
|
||||||
|
|
||||||
|
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *RunnerDeployment) ValidateCreate() error {
|
||||||
|
runenrDeploymentLog.Info("validate resource to be created", "name", r.Name)
|
||||||
|
return r.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *RunnerDeployment) ValidateUpdate(old runtime.Object) error {
|
||||||
|
runenrDeploymentLog.Info("validate resource to be updated", "name", r.Name)
|
||||||
|
return r.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *RunnerDeployment) ValidateDelete() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates resource spec.
|
||||||
|
func (r *RunnerDeployment) Validate() error {
|
||||||
|
var (
|
||||||
|
errList field.ErrorList
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = r.Spec.Template.Spec.ValidateRepository()
|
||||||
|
if err != nil {
|
||||||
|
errList = append(errList, field.Invalid(field.NewPath("spec", "template", "spec", "repository"), r.Spec.Template.Spec.Repository, err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errList) > 0 {
|
||||||
|
return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
84
api/v1alpha1/runnerreplicaset_webhook.go
Normal file
84
api/v1alpha1/runnerreplicaset_webhook.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
// log is for logging in this package.
|
||||||
|
var runnerReplicaSetLog = logf.Log.WithName("runnerreplicaset-resource")
|
||||||
|
|
||||||
|
func (r *RunnerReplicaSet) SetupWebhookWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewWebhookManagedBy(mgr).
|
||||||
|
For(r).
|
||||||
|
Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:webhook:path=/mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=true,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=mutate.runnerreplicaset.actions.summerwind.dev
|
||||||
|
|
||||||
|
var _ webhook.Defaulter = &RunnerReplicaSet{}
|
||||||
|
|
||||||
|
// Default implements webhook.Defaulter so a webhook will be registered for the type
|
||||||
|
func (r *RunnerReplicaSet) Default() {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:webhook:path=/validate-actions-summerwind-dev-v1alpha1-runnerreplicaset,verbs=create;update,mutating=false,failurePolicy=fail,groups=actions.summerwind.dev,resources=runnerreplicasets,versions=v1alpha1,name=validate.runnerreplicaset.actions.summerwind.dev
|
||||||
|
|
||||||
|
var _ webhook.Validator = &RunnerReplicaSet{}
|
||||||
|
|
||||||
|
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *RunnerReplicaSet) ValidateCreate() error {
|
||||||
|
runnerReplicaSetLog.Info("validate resource to be created", "name", r.Name)
|
||||||
|
return r.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *RunnerReplicaSet) ValidateUpdate(old runtime.Object) error {
|
||||||
|
runnerReplicaSetLog.Info("validate resource to be updated", "name", r.Name)
|
||||||
|
return r.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
|
||||||
|
func (r *RunnerReplicaSet) ValidateDelete() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates resource spec.
|
||||||
|
func (r *RunnerReplicaSet) Validate() error {
|
||||||
|
var (
|
||||||
|
errList field.ErrorList
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = r.Spec.Template.Spec.ValidateRepository()
|
||||||
|
if err != nil {
|
||||||
|
errList = append(errList, field.Invalid(field.NewPath("spec", "template", "spec", "repository"), r.Spec.Template.Spec.Repository, err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errList) > 0 {
|
||||||
|
return apierrors.NewInvalid(r.GroupVersionKind().GroupKind(), r.Name, errList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ package v1alpha1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
@@ -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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) {
|
func (in *RunnerSpec) DeepCopyInto(out *RunnerSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.Labels != nil {
|
||||||
|
in, out := &in.Labels, &out.Labels
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
if in.Containers != nil {
|
if in.Containers != nil {
|
||||||
in, out := &in.Containers, &out.Containers
|
in, out := &in.Containers, &out.Containers
|
||||||
*out = make([]v1.Container, len(*in))
|
*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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *RunnerStatusRegistration) DeepCopyInto(out *RunnerStatusRegistration) {
|
func (in *RunnerStatusRegistration) DeepCopyInto(out *RunnerStatusRegistration) {
|
||||||
*out = *in
|
*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)
|
in.ExpiresAt.DeepCopyInto(&out.ExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4111,12 +4111,18 @@ spec:
|
|||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
labels:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
organization:
|
||||||
|
pattern: ^[^/]+$
|
||||||
|
type: string
|
||||||
repository:
|
repository:
|
||||||
minLength: 3
|
|
||||||
pattern: ^[^/]+/[^/]+$
|
pattern: ^[^/]+/[^/]+$
|
||||||
type: string
|
type: string
|
||||||
resources:
|
resources:
|
||||||
@@ -6708,8 +6714,6 @@ spec:
|
|||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
required:
|
|
||||||
- repository
|
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -4111,12 +4111,18 @@ spec:
|
|||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
labels:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
organization:
|
||||||
|
pattern: ^[^/]+$
|
||||||
|
type: string
|
||||||
repository:
|
repository:
|
||||||
minLength: 3
|
|
||||||
pattern: ^[^/]+/[^/]+$
|
pattern: ^[^/]+/[^/]+$
|
||||||
type: string
|
type: string
|
||||||
resources:
|
resources:
|
||||||
@@ -6708,8 +6714,6 @@ spec:
|
|||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
required:
|
|
||||||
- repository
|
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -9,9 +9,15 @@ metadata:
|
|||||||
name: runners.actions.summerwind.dev
|
name: runners.actions.summerwind.dev
|
||||||
spec:
|
spec:
|
||||||
additionalPrinterColumns:
|
additionalPrinterColumns:
|
||||||
|
- JSONPath: .spec.organization
|
||||||
|
name: Organization
|
||||||
|
type: string
|
||||||
- JSONPath: .spec.repository
|
- JSONPath: .spec.repository
|
||||||
name: Repository
|
name: Repository
|
||||||
type: string
|
type: string
|
||||||
|
- JSONPath: .spec.labels
|
||||||
|
name: Labels
|
||||||
|
type: string
|
||||||
- JSONPath: .status.phase
|
- JSONPath: .status.phase
|
||||||
name: Status
|
name: Status
|
||||||
type: string
|
type: string
|
||||||
@@ -3854,12 +3860,18 @@ spec:
|
|||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
labels:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
organization:
|
||||||
|
pattern: ^[^/]+$
|
||||||
|
type: string
|
||||||
repository:
|
repository:
|
||||||
minLength: 3
|
|
||||||
pattern: ^[^/]+/[^/]+$
|
pattern: ^[^/]+/[^/]+$
|
||||||
type: string
|
type: string
|
||||||
resources:
|
resources:
|
||||||
@@ -6292,8 +6304,6 @@ spec:
|
|||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
required:
|
|
||||||
- repository
|
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: RunnerStatus defines the observed state of Runner
|
description: RunnerStatus defines the observed state of Runner
|
||||||
@@ -6305,17 +6315,23 @@ spec:
|
|||||||
reason:
|
reason:
|
||||||
type: string
|
type: string
|
||||||
registration:
|
registration:
|
||||||
|
description: RunnerStatusRegistration contains runner registration status
|
||||||
properties:
|
properties:
|
||||||
expiresAt:
|
expiresAt:
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
|
labels:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
organization:
|
||||||
|
type: string
|
||||||
repository:
|
repository:
|
||||||
type: string
|
type: string
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- expiresAt
|
- expiresAt
|
||||||
- repository
|
|
||||||
- token
|
- token
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ bases:
|
|||||||
- ../rbac
|
- ../rbac
|
||||||
- ../manager
|
- ../manager
|
||||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml
|
||||||
#- ../webhook
|
- ../webhook
|
||||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
|
||||||
#- ../certmanager
|
- ../certmanager
|
||||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||||
#- ../prometheus
|
#- ../prometheus
|
||||||
|
|
||||||
@@ -36,39 +36,39 @@ patchesStrategicMerge:
|
|||||||
#- manager_prometheus_metrics_patch.yaml
|
#- manager_prometheus_metrics_patch.yaml
|
||||||
|
|
||||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml
|
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml
|
||||||
#- manager_webhook_patch.yaml
|
- manager_webhook_patch.yaml
|
||||||
|
|
||||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
|
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
|
||||||
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
|
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
|
||||||
# 'CERTMANAGER' needs to be enabled to use ca injection
|
# 'CERTMANAGER' needs to be enabled to use ca injection
|
||||||
#- webhookcainjection_patch.yaml
|
- webhookcainjection_patch.yaml
|
||||||
|
|
||||||
# the following config is for teaching kustomize how to do var substitution
|
# the following config is for teaching kustomize how to do var substitution
|
||||||
vars:
|
vars:
|
||||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
||||||
#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
|
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
|
||||||
# objref:
|
objref:
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1alpha2
|
version: v1alpha2
|
||||||
# name: serving-cert # this name should match the one in certificate.yaml
|
name: serving-cert # this name should match the one in certificate.yaml
|
||||||
# fieldref:
|
fieldref:
|
||||||
# fieldpath: metadata.namespace
|
fieldpath: metadata.namespace
|
||||||
#- name: CERTIFICATE_NAME
|
- name: CERTIFICATE_NAME
|
||||||
# objref:
|
objref:
|
||||||
# kind: Certificate
|
kind: Certificate
|
||||||
# group: cert-manager.io
|
group: cert-manager.io
|
||||||
# version: v1alpha2
|
version: v1alpha2
|
||||||
# name: serving-cert # this name should match the one in certificate.yaml
|
name: serving-cert # this name should match the one in certificate.yaml
|
||||||
#- name: SERVICE_NAMESPACE # namespace of the service
|
- name: SERVICE_NAMESPACE # namespace of the service
|
||||||
# objref:
|
objref:
|
||||||
# kind: Service
|
kind: Service
|
||||||
# version: v1
|
version: v1
|
||||||
# name: webhook-service
|
name: webhook-service
|
||||||
# fieldref:
|
fieldref:
|
||||||
# fieldpath: metadata.namespace
|
fieldpath: metadata.namespace
|
||||||
#- name: SERVICE_NAME
|
- name: SERVICE_NAME
|
||||||
# objref:
|
objref:
|
||||||
# kind: Service
|
kind: Service
|
||||||
# version: v1
|
version: v1
|
||||||
# name: webhook-service
|
name: webhook-service
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||||
|
kind: MutatingWebhookConfiguration
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: mutating-webhook-configuration
|
||||||
|
webhooks:
|
||||||
|
- clientConfig:
|
||||||
|
caBundle: Cg==
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /mutate-actions-summerwind-dev-v1alpha1-runner
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: mutate.runner.actions.summerwind.dev
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- runners
|
||||||
|
- clientConfig:
|
||||||
|
caBundle: Cg==
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /mutate-actions-summerwind-dev-v1alpha1-runnerdeployment
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: mutate.runnerdeployment.actions.summerwind.dev
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- runnerdeployments
|
||||||
|
- clientConfig:
|
||||||
|
caBundle: Cg==
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /mutate-actions-summerwind-dev-v1alpha1-runnerreplicaset
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: mutate.runnerreplicaset.actions.summerwind.dev
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- runnerreplicasets
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||||
|
kind: ValidatingWebhookConfiguration
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: validating-webhook-configuration
|
||||||
|
webhooks:
|
||||||
|
- clientConfig:
|
||||||
|
caBundle: Cg==
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /validate-actions-summerwind-dev-v1alpha1-runner
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: validate.runner.actions.summerwind.dev
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- runners
|
||||||
|
- clientConfig:
|
||||||
|
caBundle: Cg==
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /validate-actions-summerwind-dev-v1alpha1-runnerdeployment
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: validate.runnerdeployment.actions.summerwind.dev
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- runnerdeployments
|
||||||
|
- clientConfig:
|
||||||
|
caBundle: Cg==
|
||||||
|
service:
|
||||||
|
name: webhook-service
|
||||||
|
namespace: system
|
||||||
|
path: /validate-actions-summerwind-dev-v1alpha1-runnerreplicaset
|
||||||
|
failurePolicy: Fail
|
||||||
|
name: validate.runnerreplicaset.actions.summerwind.dev
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
apiVersions:
|
||||||
|
- v1alpha1
|
||||||
|
operations:
|
||||||
|
- CREATE
|
||||||
|
- UPDATE
|
||||||
|
resources:
|
||||||
|
- runnerreplicasets
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"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)
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := runner.Validate()
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Failed to validate runner spec", "error", err.Error())
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
if runner.ObjectMeta.DeletionTimestamp.IsZero() {
|
if runner.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers)
|
finalizers, added := addFinalizer(runner.ObjectMeta.Finalizers)
|
||||||
|
|
||||||
@@ -83,14 +90,18 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
|||||||
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers)
|
finalizers, removed := removeFinalizer(runner.ObjectMeta.Finalizers)
|
||||||
|
|
||||||
if removed {
|
if removed {
|
||||||
ok, err := r.unregisterRunner(ctx, runner.Spec.Repository, runner.Name)
|
if len(runner.Status.Registration.Token) > 0 {
|
||||||
if err != nil {
|
ok, err := r.unregisterRunner(ctx, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
||||||
log.Error(err, "Failed to unregister runner")
|
if err != nil {
|
||||||
return ctrl.Result{}, err
|
log.Error(err, "Failed to unregister runner")
|
||||||
}
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.V(1).Info("Runner no longer exists on GitHub")
|
log.V(1).Info("Runner no longer exists on GitHub")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.V(1).Info("Runner was never registered on GitHub")
|
||||||
}
|
}
|
||||||
|
|
||||||
newRunner := runner.DeepCopy()
|
newRunner := runner.DeepCopy()
|
||||||
@@ -101,14 +112,14 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
|||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Removed runner from GitHub", "repository", runner.Spec.Repository)
|
log.Info("Removed runner from GitHub", "repository", runner.Spec.Repository, "organization", runner.Spec.Organization)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctrl.Result{}, nil
|
return ctrl.Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !runner.IsRegisterable() {
|
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 {
|
if err != nil {
|
||||||
r.Recorder.Event(&runner, corev1.EventTypeWarning, "FailedUpdateRegistrationToken", "Updating registration token failed")
|
r.Recorder.Event(&runner, corev1.EventTypeWarning, "FailedUpdateRegistrationToken", "Updating registration token failed")
|
||||||
log.Error(err, "Failed to get new registration token")
|
log.Error(err, "Failed to get new registration token")
|
||||||
@@ -117,9 +128,11 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
|||||||
|
|
||||||
updated := runner.DeepCopy()
|
updated := runner.DeepCopy()
|
||||||
updated.Status.Registration = v1alpha1.RunnerStatusRegistration{
|
updated.Status.Registration = v1alpha1.RunnerStatusRegistration{
|
||||||
Repository: runner.Spec.Repository,
|
Organization: runner.Spec.Organization,
|
||||||
Token: rt.GetToken(),
|
Repository: runner.Spec.Repository,
|
||||||
ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time),
|
Labels: runner.Spec.Labels,
|
||||||
|
Token: rt.GetToken(),
|
||||||
|
ExpiresAt: metav1.NewTime(rt.GetExpiresAt().Time),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Status().Update(ctx, updated); err != nil {
|
if err := r.Status().Update(ctx, updated); err != nil {
|
||||||
@@ -212,8 +225,8 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
|||||||
return ctrl.Result{}, nil
|
return ctrl.Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RunnerReconciler) unregisterRunner(ctx context.Context, repo, name string) (bool, error) {
|
func (r *RunnerReconciler) unregisterRunner(ctx context.Context, org, repo, name string) (bool, error) {
|
||||||
runners, err := r.GitHubClient.ListRunners(ctx, repo)
|
runners, err := r.GitHubClient.ListRunners(ctx, org, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -230,7 +243,7 @@ func (r *RunnerReconciler) unregisterRunner(ctx context.Context, repo, name stri
|
|||||||
return false, nil
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,10 +266,18 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
|||||||
Name: "RUNNER_NAME",
|
Name: "RUNNER_NAME",
|
||||||
Value: runner.Name,
|
Value: runner.Name,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "RUNNER_ORG",
|
||||||
|
Value: runner.Spec.Organization,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "RUNNER_REPO",
|
Name: "RUNNER_REPO",
|
||||||
Value: runner.Spec.Repository,
|
Value: runner.Spec.Repository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "RUNNER_LABELS",
|
||||||
|
Value: strings.Join(runner.Spec.Labels, ","),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "RUNNER_TOKEN",
|
Name: "RUNNER_TOKEN",
|
||||||
Value: runner.Status.Registration.Token,
|
Value: runner.Status.Registration.Token,
|
||||||
@@ -339,7 +360,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(runner.Spec.Volumes) != 0 {
|
if len(runner.Spec.Volumes) != 0 {
|
||||||
pod.Spec.Volumes = append(runner.Spec.Volumes, runner.Spec.Volumes...)
|
pod.Spec.Volumes = append(pod.Spec.Volumes, runner.Spec.Volumes...)
|
||||||
}
|
}
|
||||||
if len(runner.Spec.InitContainers) != 0 {
|
if len(runner.Spec.InitContainers) != 0 {
|
||||||
pod.Spec.InitContainers = append(pod.Spec.InitContainers, runner.Spec.InitContainers...)
|
pod.Spec.InitContainers = append(pod.Spec.InitContainers, runner.Spec.InitContainers...)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
fmt.Fprintf(w, h.Body)
|
fmt.Fprintf(w, h.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewServer creates a fake server for running unit tests
|
||||||
func NewServer() *httptest.Server {
|
func NewServer() *httptest.Server {
|
||||||
routes := map[string]handler{
|
routes := map[string]handler{
|
||||||
// For CreateRegistrationToken
|
// For CreateRegistrationToken
|
||||||
@@ -46,6 +47,18 @@ func NewServer() *httptest.Server {
|
|||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
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
|
// For ListRunners
|
||||||
"/repos/test/valid/actions/runners": handler{
|
"/repos/test/valid/actions/runners": handler{
|
||||||
@@ -60,6 +73,18 @@ func NewServer() *httptest.Server {
|
|||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
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
|
// For RemoveRunner
|
||||||
"/repos/test/valid/actions/runners/1": handler{
|
"/repos/test/valid/actions/runners/1": handler{
|
||||||
@@ -74,6 +99,18 @@ func NewServer() *httptest.Server {
|
|||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
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()
|
mux := http.NewServeMux()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package github
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Client wraps GitHub client with some additional
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*github.Client
|
*github.Client
|
||||||
regTokens map[string]*github.RegistrationToken
|
regTokens map[string]*github.RegistrationToken
|
||||||
@@ -34,7 +34,7 @@ func NewClient(appID, installationID int64, privateKeyPath string) (*Client, err
|
|||||||
}, nil
|
}, 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) {
|
func NewClientWithAccessToken(token string) (*Client, error) {
|
||||||
tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
|
tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
|
||||||
&oauth2.Token{AccessToken: token},
|
&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.
|
// 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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
owner, repo, err := splitOwnerAndRepo(repository)
|
key := getRegistrationKey(org, repo)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fmt.Sprintf("%s/%s", repo, name)
|
|
||||||
rt, ok := c.regTokens[key]
|
rt, ok := c.regTokens[key]
|
||||||
|
|
||||||
if ok && rt.GetExpiresAt().After(time.Now().Add(-10*time.Minute)) {
|
if ok && rt.GetExpiresAt().After(time.Now().Add(-10*time.Minute)) {
|
||||||
return rt, nil
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create registration token: %v", err)
|
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.
|
// RemoveRunner removes a runner with specified runner ID from repocitory.
|
||||||
func (c *Client) RemoveRunner(ctx context.Context, repository string, runnerID int64) error {
|
func (c *Client) RemoveRunner(ctx context.Context, org, repo string, runnerID int64) error {
|
||||||
owner, repo, err := splitOwnerAndRepo(repository)
|
owner, repo, err := getOwnerAndRepo(org, repo)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID)
|
res, err := c.removeRunner(ctx, owner, repo, runnerID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove runner: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRunners returns a list of runners of specified repository name.
|
// ListRunners returns a list of runners of specified owner/repository name.
|
||||||
func (c *Client) ListRunners(ctx context.Context, repository string) ([]*github.Runner, error) {
|
func (c *Client) ListRunners(ctx context.Context, org, repo string) ([]*github.Runner, error) {
|
||||||
var runners []*github.Runner
|
owner, repo, err := getOwnerAndRepo(org, repo)
|
||||||
|
|
||||||
owner, repo, err := splitOwnerAndRepo(repository)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return runners, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runners []*github.Runner
|
||||||
|
|
||||||
opts := github.ListOptions{PerPage: 10}
|
opts := github.ListOptions{PerPage: 10}
|
||||||
for {
|
for {
|
||||||
list, res, err := c.Client.Actions.ListRunners(ctx, owner, repo, &opts)
|
list, res, err := c.listRunners(ctx, owner, repo, &opts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return runners, fmt.Errorf("failed to remove runner: %v", err)
|
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) {
|
func splitOwnerAndRepo(repo string) (string, string, error) {
|
||||||
chunk := strings.Split(repo, "/")
|
chunk := strings.Split(repo, "/")
|
||||||
if len(chunk) != 2 {
|
if len(chunk) != 2 {
|
||||||
return "", "", errors.New("invalid repository name")
|
return "", "", fmt.Errorf("invalid repository name: '%s'", repo)
|
||||||
}
|
}
|
||||||
return chunk[0], chunk[1], nil
|
return chunk[0], chunk[1], nil
|
||||||
}
|
}
|
||||||
|
|||||||
95
github/github_beta.go
Normal file
95
github/github_beta.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package github
|
||||||
|
|
||||||
|
// this contains BETA API clients, that are currently not (yet) in go-github
|
||||||
|
// once these functions have been added there, they can be removed from here
|
||||||
|
// code was reused from https://github.com/google/go-github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v31/github"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateOrganizationRegistrationToken creates a token that can be used to add a self-hosted runner on an organization.
|
||||||
|
//
|
||||||
|
// GitHub API docs: https://developer.github.com/v3/actions/self-hosted-runners/#create-a-registration-token-for-an-organization
|
||||||
|
func CreateOrganizationRegistrationToken(ctx context.Context, client *Client, owner string) (*github.RegistrationToken, *github.Response, error) {
|
||||||
|
u := fmt.Sprintf("orgs/%v/actions/runners/registration-token", owner)
|
||||||
|
|
||||||
|
req, err := client.NewRequest("POST", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
registrationToken := new(github.RegistrationToken)
|
||||||
|
resp, err := client.Do(ctx, req, registrationToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return registrationToken, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListOrganizationRunners lists all the self-hosted runners for an organization.
|
||||||
|
//
|
||||||
|
// GitHub API docs: https://developer.github.com/v3/actions/self-hosted-runners/#list-self-hosted-runners-for-an-organization
|
||||||
|
func ListOrganizationRunners(ctx context.Context, client *Client, owner string, opts *github.ListOptions) (*github.Runners, *github.Response, error) {
|
||||||
|
u := fmt.Sprintf("orgs/%v/actions/runners", owner)
|
||||||
|
u, err := addOptions(u, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := client.NewRequest("GET", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
runners := &github.Runners{}
|
||||||
|
resp, err := client.Do(ctx, req, &runners)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return runners, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOrganizationRunner forces the removal of a self-hosted runner in a repository using the runner id.
|
||||||
|
//
|
||||||
|
// GitHub API docs: https://developer.github.com/v3/actions/self_hosted_runners/#remove-a-self-hosted-runner
|
||||||
|
func RemoveOrganizationRunner(ctx context.Context, client *Client, owner string, runnerID int64) (*github.Response, error) {
|
||||||
|
u := fmt.Sprintf("orgs/%v/actions/runners/%v", owner, runnerID)
|
||||||
|
|
||||||
|
req, err := client.NewRequest("DELETE", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Do(ctx, req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addOptions adds the parameters in opt as URL query parameters to s. opt
|
||||||
|
// must be a struct whose fields may contain "url" tags.
|
||||||
|
func addOptions(s string, opts interface{}) (string, error) {
|
||||||
|
v := reflect.ValueOf(opts)
|
||||||
|
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qs, err := query.Values(opts)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = qs.Encode()
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
@@ -36,18 +36,22 @@ func TestMain(m *testing.M) {
|
|||||||
|
|
||||||
func TestGetRegistrationToken(t *testing.T) {
|
func TestGetRegistrationToken(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
org string
|
||||||
repo string
|
repo string
|
||||||
token string
|
token string
|
||||||
err bool
|
err bool
|
||||||
}{
|
}{
|
||||||
{repo: "test/valid", token: fake.RegistrationToken, err: false},
|
{org: "test", repo: "valid", token: fake.RegistrationToken, err: false},
|
||||||
{repo: "test/invalid", token: "", err: true},
|
{org: "test", repo: "invalid", token: "", err: true},
|
||||||
{repo: "test/error", 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()
|
client := newTestClient()
|
||||||
for i, tt := range tests {
|
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 {
|
if !tt.err && err != nil {
|
||||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -59,18 +63,22 @@ func TestGetRegistrationToken(t *testing.T) {
|
|||||||
|
|
||||||
func TestListRunners(t *testing.T) {
|
func TestListRunners(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
org string
|
||||||
repo string
|
repo string
|
||||||
length int
|
length int
|
||||||
err bool
|
err bool
|
||||||
}{
|
}{
|
||||||
{repo: "test/valid", length: 2, err: false},
|
{org: "test", repo: "valid", length: 2, err: false},
|
||||||
{repo: "test/invalid", length: 0, err: true},
|
{org: "test", repo: "invalid", length: 0, err: true},
|
||||||
{repo: "test/error", 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()
|
client := newTestClient()
|
||||||
for i, tt := range tests {
|
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 {
|
if !tt.err && err != nil {
|
||||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -82,17 +90,21 @@ func TestListRunners(t *testing.T) {
|
|||||||
|
|
||||||
func TestRemoveRunner(t *testing.T) {
|
func TestRemoveRunner(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
org string
|
||||||
repo string
|
repo string
|
||||||
err bool
|
err bool
|
||||||
}{
|
}{
|
||||||
{repo: "test/valid", err: false},
|
{org: "test", repo: "valid", err: false},
|
||||||
{repo: "test/invalid", err: true},
|
{org: "test", repo: "invalid", err: true},
|
||||||
{repo: "test/error", 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()
|
client := newTestClient()
|
||||||
for i, tt := range tests {
|
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 {
|
if !tt.err && err != nil {
|
||||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
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/davecgh/go-spew v1.1.1
|
||||||
github.com/go-logr/logr v0.1.0
|
github.com/go-logr/logr v0.1.0
|
||||||
github.com/google/go-github/v31 v31.0.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/ginkgo v1.8.0
|
||||||
github.com/onsi/gomega v1.5.0
|
github.com/onsi/gomega v1.5.0
|
||||||
github.com/stretchr/testify v1.4.0 // indirect
|
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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
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-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 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
|
||||||
github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
|
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=
|
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
|
||||||
|
|||||||
13
main.go
13
main.go
@@ -174,6 +174,19 @@ func main() {
|
|||||||
setupLog.Error(err, "unable to create controller", "controller", "RunnerDeployment")
|
setupLog.Error(err, "unable to create controller", "controller", "RunnerDeployment")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = (&actionsv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create webhook", "webhook", "Runner")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err = (&actionsv1alpha1.RunnerDeployment{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create webhook", "webhook", "RunnerDeployment")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err = (&actionsv1alpha1.RunnerReplicaSet{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create webhook", "webhook", "RunnerReplicaSet")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
// +kubebuilder:scaffold:builder
|
// +kubebuilder:scaffold:builder
|
||||||
|
|
||||||
setupLog.Info("starting manager")
|
setupLog.Info("starting manager")
|
||||||
|
|||||||
@@ -3,9 +3,40 @@ FROM ubuntu:18.04
|
|||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG DOCKER_VERSION
|
ARG DOCKER_VERSION
|
||||||
|
|
||||||
RUN apt update \
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
&& apt install sudo curl ca-certificates -y --no-install-recommends \
|
RUN apt update -y \
|
||||||
&& curl -L -o docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz \
|
&& apt install -y software-properties-common \
|
||||||
|
&& add-apt-repository -y ppa:git-core/ppa \
|
||||||
|
&& apt update -y \
|
||||||
|
&& apt install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
dnsutils \
|
||||||
|
ftp \
|
||||||
|
git \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
|
jq \
|
||||||
|
libunwind8 \
|
||||||
|
locales \
|
||||||
|
netcat \
|
||||||
|
openssh-client \
|
||||||
|
parallel \
|
||||||
|
rsync \
|
||||||
|
shellcheck \
|
||||||
|
sudo \
|
||||||
|
telnet \
|
||||||
|
time \
|
||||||
|
tzdata \
|
||||||
|
unzip \
|
||||||
|
upx \
|
||||||
|
wget \
|
||||||
|
zip \
|
||||||
|
zstd \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -L -o docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz \
|
||||||
&& tar zxvf docker.tgz \
|
&& tar zxvf docker.tgz \
|
||||||
&& install -o root -g root -m 755 docker/docker /usr/local/bin/docker \
|
&& install -o root -g root -m 755 docker/docker /usr/local/bin/docker \
|
||||||
&& rm -rf docker docker.tgz \
|
&& rm -rf docker docker.tgz \
|
||||||
@@ -20,7 +51,8 @@ RUN mkdir -p /runner \
|
|||||||
&& curl -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
|
&& curl -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
|
||||||
&& tar xzf ./runner.tar.gz \
|
&& tar xzf ./runner.tar.gz \
|
||||||
&& rm runner.tar.gz \
|
&& rm runner.tar.gz \
|
||||||
&& ./bin/installdependencies.sh
|
&& ./bin/installdependencies.sh \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY entrypoint.sh /runner
|
COPY entrypoint.sh /runner
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,28 @@ if [ -z "${RUNNER_NAME}" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${RUNNER_REPO}" ]; then
|
if [ -n "${RUNNER_ORG}" -a -n "${RUNNER_REPO}" ]; then
|
||||||
echo "RUNNER_REPO must be set" 1>&2
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "${RUNNER_LABELS}" ]; then
|
||||||
|
LABEL_ARG="--labels ${RUNNER_LABELS}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "${RUNNER_TOKEN}" ]; then
|
if [ -z "${RUNNER_TOKEN}" ]; then
|
||||||
echo "RUNNER_TOKEN must be set" 1>&2
|
echo "RUNNER_TOKEN must be set" 1>&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd /runner
|
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
|
unset RUNNER_NAME RUNNER_REPO RUNNER_TOKEN
|
||||||
exec ./run.sh --once
|
exec ./run.sh --once
|
||||||
|
|||||||
Reference in New Issue
Block a user