mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 19:50:30 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35caf436d4 | ||
|
|
a136714723 | ||
|
|
fde8df608b | ||
|
|
4733edc20d | ||
|
|
3818e584ec | ||
|
|
50487bbb54 | ||
|
|
e2164f9946 | ||
|
|
bdc1279e9e | ||
|
|
3223480bc0 | ||
|
|
e642632a50 | ||
|
|
3c3077a11c | ||
|
|
e10637ce35 | ||
|
|
ae30648985 | ||
|
|
be0850a582 | ||
|
|
a995597111 | ||
|
|
ba8f61141b | ||
|
|
c0914743b0 | ||
|
|
eca6917c6a |
42
README.md
42
README.md
@@ -199,13 +199,25 @@ In the below example, `actions-runner` checks for pending workflow runs for each
|
|||||||
apiVersion: actions.summerwind.dev/v1alpha1
|
apiVersion: actions.summerwind.dev/v1alpha1
|
||||||
kind: RunnerDeployment
|
kind: RunnerDeployment
|
||||||
metadata:
|
metadata:
|
||||||
name: summerwind-actions-runner-controller
|
name: example-runner-deployment
|
||||||
spec:
|
spec:
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 3
|
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
repository: summerwind/actions-runner-controller
|
repository: summerwind/actions-runner-controller
|
||||||
|
---
|
||||||
|
apiVersion: actions.summerwind.dev/v1alpha1
|
||||||
|
kind: HorizontalRunnerAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: example-runner-deployment-autoscaler
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
name: example-runner-deployment
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||||
|
repositoryNames:
|
||||||
|
- summerwind/actions-runner-controller
|
||||||
```
|
```
|
||||||
|
|
||||||
Please also note that the sync period is set to 10 minutes by default and it's configurable via `--sync-period` flag.
|
Please also note that the sync period is set to 10 minutes by default and it's configurable via `--sync-period` flag.
|
||||||
@@ -217,14 +229,26 @@ By default, it doesn't scale down until the grace period of 10 minutes passes af
|
|||||||
apiVersion: actions.summerwind.dev/v1alpha1
|
apiVersion: actions.summerwind.dev/v1alpha1
|
||||||
kind: RunnerDeployment
|
kind: RunnerDeployment
|
||||||
metadata:
|
metadata:
|
||||||
name: summerwind-actions-runner-controller
|
name: example-runner-deployment
|
||||||
spec:
|
spec:
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 3
|
|
||||||
scaleDownDelaySecondsAfterScaleUp: 1m
|
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
repository: summerwind/actions-runner-controller
|
repository: summerwind/actions-runner-controller
|
||||||
|
---
|
||||||
|
apiVersion: actions.summerwind.dev/v1alpha1
|
||||||
|
kind: HorizontalRunnerAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: example-runner-deployment-autoscaler
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
name: example-runner-deployment
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
scaleDownDelaySecondsAfterScaleOut: 60
|
||||||
|
metrics:
|
||||||
|
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||||
|
repositoryNames:
|
||||||
|
- summerwind/actions-runner-controller
|
||||||
```
|
```
|
||||||
|
|
||||||
## Additional tweaks
|
## Additional tweaks
|
||||||
@@ -250,8 +274,8 @@ spec:
|
|||||||
operator: Exists
|
operator: Exists
|
||||||
|
|
||||||
repository: mumoshu/actions-runner-controller-ci
|
repository: mumoshu/actions-runner-controller-ci
|
||||||
ImagePullPolicy: Always
|
|
||||||
image: custom-image/actions-runner:latest
|
image: custom-image/actions-runner:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: "4.0"
|
cpu: "4.0"
|
||||||
@@ -317,7 +341,7 @@ The container image is based on Ubuntu 18.04, but it does not contain all of the
|
|||||||
* docker
|
* docker
|
||||||
* build-essentials
|
* 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`
|
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:
|
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:
|
||||||
|
|
||||||
|
|||||||
102
api/v1alpha1/horizontalrunnerautoscaler_types.go
Normal file
102
api/v1alpha1/horizontalrunnerautoscaler_types.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HorizontalRunnerAutoscalerSpec defines the desired state of HorizontalRunnerAutoscaler
|
||||||
|
type HorizontalRunnerAutoscalerSpec struct {
|
||||||
|
// ScaleTargetRef sis the reference to scaled resource like RunnerDeployment
|
||||||
|
ScaleTargetRef ScaleTargetRef `json:"scaleTargetRef,omitempty"`
|
||||||
|
|
||||||
|
// MinReplicas is the minimum number of replicas the deployment is allowed to scale
|
||||||
|
// +optional
|
||||||
|
MinReplicas *int `json:"minReplicas,omitempty"`
|
||||||
|
|
||||||
|
// MinReplicas is the maximum number of replicas the deployment is allowed to scale
|
||||||
|
// +optional
|
||||||
|
MaxReplicas *int `json:"maxReplicas,omitempty"`
|
||||||
|
|
||||||
|
// ScaleDownDelaySecondsAfterScaleUp is the approximate delay for a scale down followed by a scale up
|
||||||
|
// Used to prevent flapping (down->up->down->... loop)
|
||||||
|
// +optional
|
||||||
|
ScaleDownDelaySecondsAfterScaleUp *int `json:"scaleDownDelaySecondsAfterScaleOut,omitempty"`
|
||||||
|
|
||||||
|
// Metrics is the collection of various metric targets to calculate desired number of runners
|
||||||
|
// +optional
|
||||||
|
Metrics []MetricSpec `json:"metrics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScaleTargetRef struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricSpec struct {
|
||||||
|
// Type is the type of metric to be used for autoscaling.
|
||||||
|
// The only supported Type is TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// RepositoryNames is the list of repository names to be used for calculating the metric.
|
||||||
|
// For example, a repository name is the REPO part of `github.com/USER/REPO`.
|
||||||
|
// +optional
|
||||||
|
RepositoryNames []string `json:"repositoryNames,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HorizontalRunnerAutoscalerStatus struct {
|
||||||
|
// ObservedGeneration is the most recent generation observed for the target. It corresponds to e.g.
|
||||||
|
// RunnerDeployment's generation, which is updated on mutation by the API Server.
|
||||||
|
// +optional
|
||||||
|
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||||
|
|
||||||
|
// DesiredReplicas is the total number of desired, non-terminated and latest pods to be set for the primary RunnerSet
|
||||||
|
// This doesn't include outdated pods while upgrading the deployment and replacing the runnerset.
|
||||||
|
// +optional
|
||||||
|
DesiredReplicas *int `json:"desiredReplicas,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
LastSuccessfulScaleOutTime *metav1.Time `json:"lastSuccessfulScaleOutTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".spec.minReplicas",name=Min,type=number
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".spec.maxReplicas",name=Max,type=number
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.desiredReplicas",name=Desired,type=number
|
||||||
|
|
||||||
|
// HorizontalRunnerAutoscaler is the Schema for the horizontalrunnerautoscaler API
|
||||||
|
type HorizontalRunnerAutoscaler struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec HorizontalRunnerAutoscalerSpec `json:"spec,omitempty"`
|
||||||
|
Status HorizontalRunnerAutoscalerStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// HorizontalRunnerAutoscalerList contains a list of HorizontalRunnerAutoscaler
|
||||||
|
type HorizontalRunnerAutoscalerList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []HorizontalRunnerAutoscaler `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&HorizontalRunnerAutoscaler{}, &HorizontalRunnerAutoscalerList{})
|
||||||
|
}
|
||||||
@@ -48,6 +48,8 @@ type RunnerSpec struct {
|
|||||||
// +optional
|
// +optional
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
// +optional
|
// +optional
|
||||||
|
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
|
||||||
|
// +optional
|
||||||
Env []corev1.EnvVar `json:"env,omitempty"`
|
Env []corev1.EnvVar `json:"env,omitempty"`
|
||||||
|
|
||||||
// +optional
|
// +optional
|
||||||
|
|||||||
@@ -20,24 +20,15 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AutoscalingMetricTypeTotalNumberOfQueuedAndInProgressWorkflowRuns = "TotalNumberOfQueuedAndInProgressWorkflowRuns"
|
||||||
|
)
|
||||||
|
|
||||||
// RunnerReplicaSetSpec defines the desired state of RunnerDeployment
|
// RunnerReplicaSetSpec defines the desired state of RunnerDeployment
|
||||||
type RunnerDeploymentSpec struct {
|
type RunnerDeploymentSpec struct {
|
||||||
// +optional
|
// +optional
|
||||||
Replicas *int `json:"replicas,omitempty"`
|
Replicas *int `json:"replicas,omitempty"`
|
||||||
|
|
||||||
// MinReplicas is the minimum number of replicas the deployment is allowed to scale
|
|
||||||
// +optional
|
|
||||||
MinReplicas *int `json:"minReplicas,omitempty"`
|
|
||||||
|
|
||||||
// MinReplicas is the maximum number of replicas the deployment is allowed to scale
|
|
||||||
// +optional
|
|
||||||
MaxReplicas *int `json:"maxReplicas,omitempty"`
|
|
||||||
|
|
||||||
// ScaleDownDelaySecondsAfterScaleUp is the approximate delay for a scale down followed by a scale up
|
|
||||||
// Used to prevent flapping (down->up->down->... loop)
|
|
||||||
// +optional
|
|
||||||
ScaleDownDelaySecondsAfterScaleUp *int `json:"scaleDownDelaySecondsAfterScaleOut,omitempty"`
|
|
||||||
|
|
||||||
Template RunnerTemplate `json:"template"`
|
Template RunnerTemplate `json:"template"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +40,6 @@ type RunnerDeploymentStatus struct {
|
|||||||
// This doesn't include outdated pods while upgrading the deployment and replacing the runnerset.
|
// This doesn't include outdated pods while upgrading the deployment and replacing the runnerset.
|
||||||
// +optional
|
// +optional
|
||||||
Replicas *int `json:"desiredReplicas,omitempty"`
|
Replicas *int `json:"desiredReplicas,omitempty"`
|
||||||
|
|
||||||
// +optional
|
|
||||||
LastSuccessfulScaleOutTime *metav1.Time `json:"lastSuccessfulScaleOutTime,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|||||||
@@ -25,6 +25,147 @@ import (
|
|||||||
"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.
|
||||||
|
func (in *HorizontalRunnerAutoscaler) DeepCopyInto(out *HorizontalRunnerAutoscaler) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalRunnerAutoscaler.
|
||||||
|
func (in *HorizontalRunnerAutoscaler) DeepCopy() *HorizontalRunnerAutoscaler {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(HorizontalRunnerAutoscaler)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *HorizontalRunnerAutoscaler) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *HorizontalRunnerAutoscalerList) DeepCopyInto(out *HorizontalRunnerAutoscalerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]HorizontalRunnerAutoscaler, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalRunnerAutoscalerList.
|
||||||
|
func (in *HorizontalRunnerAutoscalerList) DeepCopy() *HorizontalRunnerAutoscalerList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(HorizontalRunnerAutoscalerList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *HorizontalRunnerAutoscalerList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *HorizontalRunnerAutoscalerSpec) DeepCopyInto(out *HorizontalRunnerAutoscalerSpec) {
|
||||||
|
*out = *in
|
||||||
|
out.ScaleTargetRef = in.ScaleTargetRef
|
||||||
|
if in.MinReplicas != nil {
|
||||||
|
in, out := &in.MinReplicas, &out.MinReplicas
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.MaxReplicas != nil {
|
||||||
|
in, out := &in.MaxReplicas, &out.MaxReplicas
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.ScaleDownDelaySecondsAfterScaleUp != nil {
|
||||||
|
in, out := &in.ScaleDownDelaySecondsAfterScaleUp, &out.ScaleDownDelaySecondsAfterScaleUp
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.Metrics != nil {
|
||||||
|
in, out := &in.Metrics, &out.Metrics
|
||||||
|
*out = make([]MetricSpec, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalRunnerAutoscalerSpec.
|
||||||
|
func (in *HorizontalRunnerAutoscalerSpec) DeepCopy() *HorizontalRunnerAutoscalerSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(HorizontalRunnerAutoscalerSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *HorizontalRunnerAutoscalerStatus) DeepCopyInto(out *HorizontalRunnerAutoscalerStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.DesiredReplicas != nil {
|
||||||
|
in, out := &in.DesiredReplicas, &out.DesiredReplicas
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.LastSuccessfulScaleOutTime != nil {
|
||||||
|
in, out := &in.LastSuccessfulScaleOutTime, &out.LastSuccessfulScaleOutTime
|
||||||
|
*out = (*in).DeepCopy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalRunnerAutoscalerStatus.
|
||||||
|
func (in *HorizontalRunnerAutoscalerStatus) DeepCopy() *HorizontalRunnerAutoscalerStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(HorizontalRunnerAutoscalerStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MetricSpec) DeepCopyInto(out *MetricSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.RepositoryNames != nil {
|
||||||
|
in, out := &in.RepositoryNames, &out.RepositoryNames
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricSpec.
|
||||||
|
func (in *MetricSpec) DeepCopy() *MetricSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MetricSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// 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 *Runner) DeepCopyInto(out *Runner) {
|
func (in *Runner) DeepCopyInto(out *Runner) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@@ -119,21 +260,6 @@ func (in *RunnerDeploymentSpec) DeepCopyInto(out *RunnerDeploymentSpec) {
|
|||||||
*out = new(int)
|
*out = new(int)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
if in.MinReplicas != nil {
|
|
||||||
in, out := &in.MinReplicas, &out.MinReplicas
|
|
||||||
*out = new(int)
|
|
||||||
**out = **in
|
|
||||||
}
|
|
||||||
if in.MaxReplicas != nil {
|
|
||||||
in, out := &in.MaxReplicas, &out.MaxReplicas
|
|
||||||
*out = new(int)
|
|
||||||
**out = **in
|
|
||||||
}
|
|
||||||
if in.ScaleDownDelaySecondsAfterScaleUp != nil {
|
|
||||||
in, out := &in.ScaleDownDelaySecondsAfterScaleUp, &out.ScaleDownDelaySecondsAfterScaleUp
|
|
||||||
*out = new(int)
|
|
||||||
**out = **in
|
|
||||||
}
|
|
||||||
in.Template.DeepCopyInto(&out.Template)
|
in.Template.DeepCopyInto(&out.Template)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +281,6 @@ func (in *RunnerDeploymentStatus) DeepCopyInto(out *RunnerDeploymentStatus) {
|
|||||||
*out = new(int)
|
*out = new(int)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
if in.LastSuccessfulScaleOutTime != nil {
|
|
||||||
in, out := &in.LastSuccessfulScaleOutTime, &out.LastSuccessfulScaleOutTime
|
|
||||||
*out = (*in).DeepCopy()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerDeploymentStatus.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnerDeploymentStatus.
|
||||||
@@ -467,3 +589,18 @@ func (in *RunnerTemplate) DeepCopy() *RunnerTemplate {
|
|||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ScaleTargetRef) DeepCopyInto(out *ScaleTargetRef) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScaleTargetRef.
|
||||||
|
func (in *ScaleTargetRef) DeepCopy() *ScaleTargetRef {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ScaleTargetRef)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1beta1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.2.4
|
||||||
|
creationTimestamp: null
|
||||||
|
name: horizontalrunnerautoscalers.actions.summerwind.dev
|
||||||
|
spec:
|
||||||
|
additionalPrinterColumns:
|
||||||
|
- JSONPath: .spec.minReplicas
|
||||||
|
name: Min
|
||||||
|
type: number
|
||||||
|
- JSONPath: .spec.maxReplicas
|
||||||
|
name: Max
|
||||||
|
type: number
|
||||||
|
- JSONPath: .status.desiredReplicas
|
||||||
|
name: Desired
|
||||||
|
type: number
|
||||||
|
group: actions.summerwind.dev
|
||||||
|
names:
|
||||||
|
kind: HorizontalRunnerAutoscaler
|
||||||
|
listKind: HorizontalRunnerAutoscalerList
|
||||||
|
plural: horizontalrunnerautoscalers
|
||||||
|
singular: horizontalrunnerautoscaler
|
||||||
|
scope: Namespaced
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
|
validation:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: HorizontalRunnerAutoscaler is the Schema for the horizontalrunnerautoscaler
|
||||||
|
API
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: 'APIVersion defines the versioned schema of this representation
|
||||||
|
of an object. Servers should convert recognized schemas to the latest
|
||||||
|
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: 'Kind is a string value representing the REST resource this
|
||||||
|
object represents. Servers may infer this from the endpoint the client
|
||||||
|
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: HorizontalRunnerAutoscalerSpec defines the desired state of
|
||||||
|
HorizontalRunnerAutoscaler
|
||||||
|
properties:
|
||||||
|
maxReplicas:
|
||||||
|
description: MinReplicas is the maximum number of replicas the deployment
|
||||||
|
is allowed to scale
|
||||||
|
type: integer
|
||||||
|
metrics:
|
||||||
|
description: Metrics is the collection of various metric targets to
|
||||||
|
calculate desired number of runners
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
repositoryNames:
|
||||||
|
description: RepositoryNames is the list of repository names to
|
||||||
|
be used for calculating the metric. For example, a repository
|
||||||
|
name is the REPO part of `github.com/USER/REPO`.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
description: Type is the type of metric to be used for autoscaling.
|
||||||
|
The only supported Type is TotalNumberOfQueuedAndInProgressWorkflowRuns
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
minReplicas:
|
||||||
|
description: MinReplicas is the minimum number of replicas the deployment
|
||||||
|
is allowed to scale
|
||||||
|
type: integer
|
||||||
|
scaleDownDelaySecondsAfterScaleOut:
|
||||||
|
description: ScaleDownDelaySecondsAfterScaleUp is the approximate delay
|
||||||
|
for a scale down followed by a scale up Used to prevent flapping (down->up->down->...
|
||||||
|
loop)
|
||||||
|
type: integer
|
||||||
|
scaleTargetRef:
|
||||||
|
description: ScaleTargetRef sis the reference to scaled resource like
|
||||||
|
RunnerDeployment
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
properties:
|
||||||
|
desiredReplicas:
|
||||||
|
description: DesiredReplicas is the total number of desired, non-terminated
|
||||||
|
and latest pods to be set for the primary RunnerSet This doesn't include
|
||||||
|
outdated pods while upgrading the deployment and replacing the runnerset.
|
||||||
|
type: integer
|
||||||
|
lastSuccessfulScaleOutTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: ObservedGeneration is the most recent generation observed
|
||||||
|
for the target. It corresponds to e.g. RunnerDeployment's generation,
|
||||||
|
which is updated on mutation by the API Server.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
version: v1alpha1
|
||||||
|
versions:
|
||||||
|
- name: v1alpha1
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
status:
|
||||||
|
acceptedNames:
|
||||||
|
kind: ""
|
||||||
|
plural: ""
|
||||||
|
conditions: []
|
||||||
|
storedVersions: []
|
||||||
@@ -46,21 +46,8 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
description: RunnerReplicaSetSpec defines the desired state of RunnerDeployment
|
description: RunnerReplicaSetSpec defines the desired state of RunnerDeployment
|
||||||
properties:
|
properties:
|
||||||
maxReplicas:
|
|
||||||
description: MinReplicas is the maximum number of replicas the deployment
|
|
||||||
is allowed to scale
|
|
||||||
type: integer
|
|
||||||
minReplicas:
|
|
||||||
description: MinReplicas is the minimum number of replicas the deployment
|
|
||||||
is allowed to scale
|
|
||||||
type: integer
|
|
||||||
replicas:
|
replicas:
|
||||||
type: integer
|
type: integer
|
||||||
scaleDownDelaySecondsAfterScaleOut:
|
|
||||||
description: ScaleDownDelaySecondsAfterScaleUp is the approximate delay
|
|
||||||
for a scale down followed by a scale up Used to prevent flapping (down->up->down->...
|
|
||||||
loop)
|
|
||||||
type: integer
|
|
||||||
template:
|
template:
|
||||||
properties:
|
properties:
|
||||||
metadata:
|
metadata:
|
||||||
@@ -3019,6 +3006,10 @@ spec:
|
|||||||
type: array
|
type: array
|
||||||
image:
|
image:
|
||||||
type: string
|
type: string
|
||||||
|
imagePullPolicy:
|
||||||
|
description: PullPolicy describes a policy for if/when to pull
|
||||||
|
a container image
|
||||||
|
type: string
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
items:
|
items:
|
||||||
description: LocalObjectReference contains enough information
|
description: LocalObjectReference contains enough information
|
||||||
@@ -6741,9 +6732,6 @@ spec:
|
|||||||
and latest pods to be set for the primary RunnerSet This doesn't include
|
and latest pods to be set for the primary RunnerSet This doesn't include
|
||||||
outdated pods while upgrading the deployment and replacing the runnerset.
|
outdated pods while upgrading the deployment and replacing the runnerset.
|
||||||
type: integer
|
type: integer
|
||||||
lastSuccessfulScaleOutTime:
|
|
||||||
format: date-time
|
|
||||||
type: string
|
|
||||||
readyReplicas:
|
readyReplicas:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -3006,6 +3006,10 @@ spec:
|
|||||||
type: array
|
type: array
|
||||||
image:
|
image:
|
||||||
type: string
|
type: string
|
||||||
|
imagePullPolicy:
|
||||||
|
description: PullPolicy describes a policy for if/when to pull
|
||||||
|
a container image
|
||||||
|
type: string
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
items:
|
items:
|
||||||
description: LocalObjectReference contains enough information
|
description: LocalObjectReference contains enough information
|
||||||
|
|||||||
@@ -2815,6 +2815,10 @@ spec:
|
|||||||
type: array
|
type: array
|
||||||
image:
|
image:
|
||||||
type: string
|
type: string
|
||||||
|
imagePullPolicy:
|
||||||
|
description: PullPolicy describes a policy for if/when to pull a container
|
||||||
|
image
|
||||||
|
type: string
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
items:
|
items:
|
||||||
description: LocalObjectReference contains enough information to let
|
description: LocalObjectReference contains enough information to let
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ resources:
|
|||||||
- bases/actions.summerwind.dev_runners.yaml
|
- bases/actions.summerwind.dev_runners.yaml
|
||||||
- bases/actions.summerwind.dev_runnerreplicasets.yaml
|
- bases/actions.summerwind.dev_runnerreplicasets.yaml
|
||||||
- bases/actions.summerwind.dev_runnerdeployments.yaml
|
- bases/actions.summerwind.dev_runnerdeployments.yaml
|
||||||
|
- bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml
|
||||||
# +kubebuilder:scaffold:crdkustomizeresource
|
# +kubebuilder:scaffold:crdkustomizeresource
|
||||||
|
|
||||||
patchesStrategicMerge:
|
patchesStrategicMerge:
|
||||||
|
|||||||
@@ -6,6 +6,26 @@ metadata:
|
|||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
name: manager-role
|
name: manager-role
|
||||||
rules:
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
resources:
|
||||||
|
- horizontalrunnerautoscalers
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.summerwind.dev
|
||||||
|
resources:
|
||||||
|
- horizontalrunnerautoscalers/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- actions.summerwind.dev
|
- actions.summerwind.dev
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -2,66 +2,78 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotSupported struct {
|
func (r *HorizontalRunnerAutoscalerReconciler) determineDesiredReplicas(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
|
||||||
}
|
if hra.Spec.MinReplicas == nil {
|
||||||
|
return nil, fmt.Errorf("horizontalrunnerautoscaler %s/%s is missing minReplicas", hra.Namespace, hra.Name)
|
||||||
var _ error = NotSupported{}
|
} else if hra.Spec.MaxReplicas == nil {
|
||||||
|
return nil, fmt.Errorf("horizontalrunnerautoscaler %s/%s is missing maxReplicas", hra.Namespace, hra.Name)
|
||||||
func (e NotSupported) Error() string {
|
|
||||||
return "Autoscaling is currently supported only when spec.repository is set"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RunnerDeploymentReconciler) determineDesiredReplicas(rd v1alpha1.RunnerDeployment) (*int, error) {
|
|
||||||
if rd.Spec.Replicas != nil {
|
|
||||||
return nil, fmt.Errorf("bug: determineDesiredReplicas should not be called for deplomeny with specific replicas")
|
|
||||||
} else if rd.Spec.MinReplicas == nil {
|
|
||||||
return nil, fmt.Errorf("runnerdeployment %s/%s is missing minReplicas", rd.Namespace, rd.Name)
|
|
||||||
} else if rd.Spec.MaxReplicas == nil {
|
|
||||||
return nil, fmt.Errorf("runnerdeployment %s/%s is missing maxReplicas", rd.Namespace, rd.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var replicas int
|
var repos [][]string
|
||||||
|
|
||||||
repoID := rd.Spec.Template.Spec.Repository
|
repoID := rd.Spec.Template.Spec.Repository
|
||||||
if repoID == "" {
|
if repoID == "" {
|
||||||
return nil, NotSupported{}
|
orgName := rd.Spec.Template.Spec.Organization
|
||||||
}
|
if orgName == "" {
|
||||||
|
return nil, fmt.Errorf("asserting runner deployment spec to detect bug: spec.template.organization should not be empty on this code path")
|
||||||
|
}
|
||||||
|
|
||||||
repo := strings.Split(repoID, "/")
|
metrics := hra.Spec.Metrics
|
||||||
user, repoName := repo[0], repo[1]
|
|
||||||
list, _, err := r.GitHubClient.Actions.ListRepositoryWorkflowRuns(context.TODO(), user, repoName, nil)
|
if len(metrics) == 0 {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("validating autoscaling metrics: one or more metrics is required")
|
||||||
return nil, err
|
} else if tpe := metrics[0].Type; tpe != v1alpha1.AutoscalingMetricTypeTotalNumberOfQueuedAndInProgressWorkflowRuns {
|
||||||
|
return nil, fmt.Errorf("validting autoscaling metrics: unsupported metric type %q: only supported value is %s", tpe, v1alpha1.AutoscalingMetricTypeTotalNumberOfQueuedAndInProgressWorkflowRuns)
|
||||||
|
} else if len(metrics[0].RepositoryNames) == 0 {
|
||||||
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].repositoryNames is required and must have one more more entries for organizational runner deployment")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repoName := range metrics[0].RepositoryNames {
|
||||||
|
repos = append(repos, []string{orgName, repoName})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
repo := strings.Split(repoID, "/")
|
||||||
|
|
||||||
|
repos = append(repos, repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
var total, inProgress, queued, completed, unknown int
|
var total, inProgress, queued, completed, unknown int
|
||||||
|
|
||||||
for _, r := range list.WorkflowRuns {
|
for _, repo := range repos {
|
||||||
total++
|
user, repoName := repo[0], repo[1]
|
||||||
|
list, _, err := r.GitHubClient.Actions.ListRepositoryWorkflowRuns(context.TODO(), user, repoName, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// In May 2020, there are only 3 statuses.
|
for _, r := range list.WorkflowRuns {
|
||||||
// Follow the below links for more details:
|
total++
|
||||||
// - https://developer.github.com/v3/actions/workflow-runs/#list-repository-workflow-runs
|
|
||||||
// - https://developer.github.com/v3/checks/runs/#create-a-check-run
|
// In May 2020, there are only 3 statuses.
|
||||||
switch r.GetStatus() {
|
// Follow the below links for more details:
|
||||||
case "completed":
|
// - https://developer.github.com/v3/actions/workflow-runs/#list-repository-workflow-runs
|
||||||
completed++
|
// - https://developer.github.com/v3/checks/runs/#create-a-check-run
|
||||||
case "in_progress":
|
switch r.GetStatus() {
|
||||||
inProgress++
|
case "completed":
|
||||||
case "queued":
|
completed++
|
||||||
queued++
|
case "in_progress":
|
||||||
default:
|
inProgress++
|
||||||
unknown++
|
case "queued":
|
||||||
|
queued++
|
||||||
|
default:
|
||||||
|
unknown++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
minReplicas := *rd.Spec.MinReplicas
|
minReplicas := *hra.Spec.MinReplicas
|
||||||
maxReplicas := *rd.Spec.MaxReplicas
|
maxReplicas := *hra.Spec.MaxReplicas
|
||||||
necessaryReplicas := queued + inProgress
|
necessaryReplicas := queued + inProgress
|
||||||
|
|
||||||
var desiredReplicas int
|
var desiredReplicas int
|
||||||
@@ -75,7 +87,7 @@ func (r *RunnerDeploymentReconciler) determineDesiredReplicas(rd v1alpha1.Runner
|
|||||||
}
|
}
|
||||||
|
|
||||||
rd.Status.Replicas = &desiredReplicas
|
rd.Status.Replicas = &desiredReplicas
|
||||||
replicas = desiredReplicas
|
replicas := desiredReplicas
|
||||||
|
|
||||||
r.Log.V(1).Info(
|
r.Log.V(1).Info(
|
||||||
"Calculated desired replicas",
|
"Calculated desired replicas",
|
||||||
|
|||||||
@@ -115,23 +115,12 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// fixed at 3
|
// fixed at 3
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
fixed: intPtr(3),
|
|
||||||
want: 3,
|
|
||||||
},
|
|
||||||
// org runner, fixed at 3
|
|
||||||
{
|
|
||||||
org: "test",
|
|
||||||
fixed: intPtr(3),
|
|
||||||
want: 3,
|
|
||||||
},
|
|
||||||
// org runner, 1 demanded, min at 1
|
|
||||||
{
|
|
||||||
org: "test",
|
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
fixed: intPtr(3),
|
||||||
err: "Autoscaling is currently supported only when spec.repository is set",
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +140,7 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
client := newGithubClient(server)
|
client := newGithubClient(server)
|
||||||
|
|
||||||
r := &RunnerDeploymentReconciler{
|
h := &HorizontalRunnerAutoscalerReconciler{
|
||||||
Log: log,
|
Log: log,
|
||||||
GitHubClient: client,
|
GitHubClient: client,
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
@@ -159,23 +148,34 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
|
|
||||||
rd := v1alpha1.RunnerDeployment{
|
rd := v1alpha1.RunnerDeployment{
|
||||||
TypeMeta: metav1.TypeMeta{},
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "testrd",
|
||||||
|
},
|
||||||
Spec: v1alpha1.RunnerDeploymentSpec{
|
Spec: v1alpha1.RunnerDeploymentSpec{
|
||||||
Template: v1alpha1.RunnerTemplate{
|
Template: v1alpha1.RunnerTemplate{
|
||||||
Spec: v1alpha1.RunnerSpec{
|
Spec: v1alpha1.RunnerSpec{
|
||||||
Repository: tc.repo,
|
Repository: tc.repo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Replicas: tc.fixed,
|
Replicas: tc.fixed,
|
||||||
|
},
|
||||||
|
Status: v1alpha1.RunnerDeploymentStatus{
|
||||||
|
Replicas: tc.sReplicas,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hra := v1alpha1.HorizontalRunnerAutoscaler{
|
||||||
|
Spec: v1alpha1.HorizontalRunnerAutoscalerSpec{
|
||||||
MaxReplicas: tc.max,
|
MaxReplicas: tc.max,
|
||||||
MinReplicas: tc.min,
|
MinReplicas: tc.min,
|
||||||
},
|
},
|
||||||
Status: v1alpha1.RunnerDeploymentStatus{
|
Status: v1alpha1.HorizontalRunnerAutoscalerStatus{
|
||||||
Replicas: tc.sReplicas,
|
DesiredReplicas: tc.sReplicas,
|
||||||
LastSuccessfulScaleOutTime: tc.sTime,
|
LastSuccessfulScaleOutTime: tc.sTime,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rs, err := r.newRunnerReplicaSetWithAutoscaling(rd)
|
got, err := h.computeReplicas(rd, hra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if tc.err == "" {
|
if tc.err == "" {
|
||||||
t.Fatalf("unexpected error: expected none, got %v", err)
|
t.Fatalf("unexpected error: expected none, got %v", err)
|
||||||
@@ -185,8 +185,6 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
got := rs.Spec.Replicas
|
|
||||||
|
|
||||||
if got == nil {
|
if got == nil {
|
||||||
t.Fatalf("unexpected value of rs.Spec.Replicas: nil")
|
t.Fatalf("unexpected value of rs.Spec.Replicas: nil")
|
||||||
}
|
}
|
||||||
@@ -197,3 +195,205 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
|
||||||
|
intPtr := func(v int) *int {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
metav1Now := metav1.Now()
|
||||||
|
testcases := []struct {
|
||||||
|
repos []string
|
||||||
|
org string
|
||||||
|
fixed *int
|
||||||
|
max *int
|
||||||
|
min *int
|
||||||
|
sReplicas *int
|
||||||
|
sTime *metav1.Time
|
||||||
|
workflowRuns string
|
||||||
|
want int
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
// 3 demanded, max at 3
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(2),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(2),
|
||||||
|
max: intPtr(3),
|
||||||
|
sReplicas: intPtr(3),
|
||||||
|
sTime: &metav1Now,
|
||||||
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
// 3 demanded, max at 2
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(2),
|
||||||
|
max: intPtr(2),
|
||||||
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
// 2 demanded, min at 2
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(2),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
// 1 demanded, min at 2
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(2),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
// 1 demanded, min at 2
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(2),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
// 1 demanded, min at 1
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(1),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
// 1 demanded, min at 1
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
min: intPtr(1),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
// fixed at 3
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
fixed: intPtr(1),
|
||||||
|
min: intPtr(1),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
// org runner, fixed at 3
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
repos: []string{"valid"},
|
||||||
|
fixed: intPtr(1),
|
||||||
|
min: intPtr(1),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
// org runner, 1 demanded, min at 1, no repos
|
||||||
|
{
|
||||||
|
org: "test",
|
||||||
|
min: intPtr(1),
|
||||||
|
max: intPtr(3),
|
||||||
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
err: "validating autoscaling metrics: spec.autoscaling.metrics[].repositoryNames is required and must have one more more entries for organizational runner deployment",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range testcases {
|
||||||
|
tc := testcases[i]
|
||||||
|
|
||||||
|
log := zap.New(func(o *zap.Options) {
|
||||||
|
o.Development = true
|
||||||
|
})
|
||||||
|
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
_ = clientgoscheme.AddToScheme(scheme)
|
||||||
|
_ = v1alpha1.AddToScheme(scheme)
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||||
|
server := fake.NewServer(fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns))
|
||||||
|
defer server.Close()
|
||||||
|
client := newGithubClient(server)
|
||||||
|
|
||||||
|
h := &HorizontalRunnerAutoscalerReconciler{
|
||||||
|
Log: log,
|
||||||
|
Scheme: scheme,
|
||||||
|
GitHubClient: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := v1alpha1.RunnerDeployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "testrd",
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.RunnerDeploymentSpec{
|
||||||
|
Template: v1alpha1.RunnerTemplate{
|
||||||
|
Spec: v1alpha1.RunnerSpec{
|
||||||
|
Organization: tc.org,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: tc.fixed,
|
||||||
|
},
|
||||||
|
Status: v1alpha1.RunnerDeploymentStatus{
|
||||||
|
Replicas: tc.sReplicas,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hra := v1alpha1.HorizontalRunnerAutoscaler{
|
||||||
|
Spec: v1alpha1.HorizontalRunnerAutoscalerSpec{
|
||||||
|
ScaleTargetRef: v1alpha1.ScaleTargetRef{
|
||||||
|
Name: "testrd",
|
||||||
|
},
|
||||||
|
MaxReplicas: tc.max,
|
||||||
|
MinReplicas: tc.min,
|
||||||
|
Metrics: []v1alpha1.MetricSpec{
|
||||||
|
{
|
||||||
|
Type: v1alpha1.AutoscalingMetricTypeTotalNumberOfQueuedAndInProgressWorkflowRuns,
|
||||||
|
RepositoryNames: tc.repos,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: v1alpha1.HorizontalRunnerAutoscalerStatus{
|
||||||
|
DesiredReplicas: tc.sReplicas,
|
||||||
|
LastSuccessfulScaleOutTime: tc.sTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := h.computeReplicas(rd, hra)
|
||||||
|
if err != nil {
|
||||||
|
if tc.err == "" {
|
||||||
|
t.Fatalf("unexpected error: expected none, got %v", err)
|
||||||
|
} else if err.Error() != tc.err {
|
||||||
|
t.Fatalf("unexpected error: expected %v, got %v", tc.err, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if got == nil {
|
||||||
|
t.Fatalf("unexpected value of rs.Spec.Replicas: nil, wanted %v", tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *got != tc.want {
|
||||||
|
t.Errorf("%d: incorrect desired replicas: want %d, got %d", i, tc.want, *got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
167
controllers/horizontalrunnerautoscaler_controller.go
Normal file
167
controllers/horizontalrunnerautoscaler_controller.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
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 controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/summerwind/actions-runner-controller/github"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/client-go/tools/record"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultScaleDownDelay = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// HorizontalRunnerAutoscalerReconciler reconciles a HorizontalRunnerAutoscaler object
|
||||||
|
type HorizontalRunnerAutoscalerReconciler struct {
|
||||||
|
client.Client
|
||||||
|
GitHubClient *github.Client
|
||||||
|
Log logr.Logger
|
||||||
|
Recorder record.EventRecorder
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments,verbs=get;list;watch;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=horizontalrunnerautoscalers,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=horizontalrunnerautoscalers/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||||
|
|
||||||
|
func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
log := r.Log.WithValues("horizontalrunnerautoscaler", req.NamespacedName)
|
||||||
|
|
||||||
|
var hra v1alpha1.HorizontalRunnerAutoscaler
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &hra); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hra.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rd v1alpha1.RunnerDeployment
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{
|
||||||
|
Namespace: req.Namespace,
|
||||||
|
Name: hra.Spec.ScaleTargetRef.Name,
|
||||||
|
}, &rd); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rd.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
replicas, err := r.computeReplicas(rd, hra)
|
||||||
|
if err != nil {
|
||||||
|
r.Recorder.Event(&hra, corev1.EventTypeNormal, "RunnerAutoscalingFailure", err.Error())
|
||||||
|
|
||||||
|
log.Error(err, "Could not compute replicas")
|
||||||
|
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultReplicas = 1
|
||||||
|
|
||||||
|
currentDesiredReplicas := getIntOrDefault(rd.Spec.Replicas, defaultReplicas)
|
||||||
|
newDesiredReplicas := getIntOrDefault(replicas, defaultReplicas)
|
||||||
|
|
||||||
|
// Please add more conditions that we can in-place update the newest runnerreplicaset without disruption
|
||||||
|
if currentDesiredReplicas != newDesiredReplicas {
|
||||||
|
copy := rd.DeepCopy()
|
||||||
|
copy.Spec.Replicas = &newDesiredReplicas
|
||||||
|
|
||||||
|
if err := r.Client.Update(ctx, copy); err != nil {
|
||||||
|
log.Error(err, "Failed to update runnerderployment resource")
|
||||||
|
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hra.Status.DesiredReplicas == nil || *hra.Status.DesiredReplicas != *replicas {
|
||||||
|
updated := hra.DeepCopy()
|
||||||
|
|
||||||
|
if (hra.Status.DesiredReplicas == nil && *replicas > 1) ||
|
||||||
|
(hra.Status.DesiredReplicas != nil && *replicas > *hra.Status.DesiredReplicas) {
|
||||||
|
|
||||||
|
updated.Status.LastSuccessfulScaleOutTime = &metav1.Time{Time: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.Status.DesiredReplicas = replicas
|
||||||
|
|
||||||
|
if err := r.Status().Update(ctx, updated); err != nil {
|
||||||
|
log.Error(err, "Failed to update horizontalrunnerautoscaler status")
|
||||||
|
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HorizontalRunnerAutoscalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
r.Recorder = mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller")
|
||||||
|
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.HorizontalRunnerAutoscaler{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HorizontalRunnerAutoscalerReconciler) computeReplicas(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
|
||||||
|
var computedReplicas *int
|
||||||
|
|
||||||
|
replicas, err := r.determineDesiredReplicas(rd, hra)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var scaleDownDelay time.Duration
|
||||||
|
|
||||||
|
if hra.Spec.ScaleDownDelaySecondsAfterScaleUp != nil {
|
||||||
|
scaleDownDelay = time.Duration(*hra.Spec.ScaleDownDelaySecondsAfterScaleUp) * time.Second
|
||||||
|
} else {
|
||||||
|
scaleDownDelay = DefaultScaleDownDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if hra.Status.DesiredReplicas == nil ||
|
||||||
|
*hra.Status.DesiredReplicas < *replicas ||
|
||||||
|
hra.Status.LastSuccessfulScaleOutTime == nil ||
|
||||||
|
hra.Status.LastSuccessfulScaleOutTime.Add(scaleDownDelay).Before(now) {
|
||||||
|
|
||||||
|
computedReplicas = replicas
|
||||||
|
} else {
|
||||||
|
computedReplicas = hra.Status.DesiredReplicas
|
||||||
|
}
|
||||||
|
|
||||||
|
return computedReplicas, nil
|
||||||
|
}
|
||||||
308
controllers/integration_test.go
Normal file
308
controllers/integration_test.go
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/summerwind/actions-runner-controller/github/fake"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testEnvironment struct {
|
||||||
|
Namespace *corev1.Namespace
|
||||||
|
Responses *fake.FixedResponses
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
workflowRunsFor3Replicas = `{"total_count": 5, "workflow_runs":[{"status":"queued"}, {"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`
|
||||||
|
workflowRunsFor1Replicas = `{"total_count": 6, "workflow_runs":[{"status":"queued"}, {"status":"completed"}, {"status":"completed"}, {"status":"completed"}, {"status":"completed"}]}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupIntegrationTest will set up a testing environment.
|
||||||
|
// This includes:
|
||||||
|
// * creating a Namespace to be used during the test
|
||||||
|
// * starting all the reconcilers
|
||||||
|
// * stopping all the reconcilers after the test ends
|
||||||
|
// Call this function at the start of each of your tests.
|
||||||
|
func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
||||||
|
var stopCh chan struct{}
|
||||||
|
ns := &corev1.Namespace{}
|
||||||
|
|
||||||
|
responses := &fake.FixedResponses{}
|
||||||
|
responses.ListRepositoryWorkflowRuns = &fake.Handler{
|
||||||
|
Status: 200,
|
||||||
|
Body: workflowRunsFor3Replicas,
|
||||||
|
}
|
||||||
|
server := fake.NewServer(fake.WithFixedResponses(responses))
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
stopCh = make(chan struct{})
|
||||||
|
*ns = corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{})
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
||||||
|
|
||||||
|
replicasetController := &RunnerReplicaSetReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: scheme.Scheme,
|
||||||
|
Log: logf.Log,
|
||||||
|
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
||||||
|
}
|
||||||
|
err = replicasetController.SetupWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
|
deploymentsController := &RunnerDeploymentReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: scheme.Scheme,
|
||||||
|
Log: logf.Log,
|
||||||
|
Recorder: mgr.GetEventRecorderFor("runnerdeployment-controller"),
|
||||||
|
}
|
||||||
|
err = deploymentsController.SetupWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
|
client := newGithubClient(server)
|
||||||
|
|
||||||
|
autoscalerController := &HorizontalRunnerAutoscalerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: scheme.Scheme,
|
||||||
|
Log: logf.Log,
|
||||||
|
GitHubClient: client,
|
||||||
|
Recorder: mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller"),
|
||||||
|
}
|
||||||
|
err = autoscalerController.SetupWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := mgr.Start(stopCh)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
close(stopCh)
|
||||||
|
|
||||||
|
server.Close()
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace")
|
||||||
|
})
|
||||||
|
|
||||||
|
return &testEnvironment{Namespace: ns, Responses: responses}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Context("Inside of a new namespace", func() {
|
||||||
|
ctx := context.TODO()
|
||||||
|
env := SetupIntegrationTest(ctx)
|
||||||
|
ns := env.Namespace
|
||||||
|
responses := env.Responses
|
||||||
|
|
||||||
|
Describe("when no existing resources exist", func() {
|
||||||
|
|
||||||
|
It("should create and scale runners", func() {
|
||||||
|
name := "example-runnerdeploy"
|
||||||
|
|
||||||
|
{
|
||||||
|
rs := &actionsv1alpha1.RunnerDeployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
||||||
|
Replicas: intPtr(1),
|
||||||
|
Template: actionsv1alpha1.RunnerTemplate{
|
||||||
|
Spec: actionsv1alpha1.RunnerSpec{
|
||||||
|
Repository: "test/valid",
|
||||||
|
Image: "bar",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{Name: "FOO", Value: "FOOVALUE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, rs)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create test RunnerDeployment resource")
|
||||||
|
|
||||||
|
runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}}
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerSets.Items)
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(1))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSets.Items) == 0 {
|
||||||
|
logf.Log.Info("No runnerreplicasets exist yet")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return *runnerSets.Items[0].Spec.Replicas
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// We wrap the update in the Eventually block to avoid the below error that occurs due to concurrent modification
|
||||||
|
// made by the controller to update .Status.AvailableReplicas and .Status.ReadyReplicas
|
||||||
|
// Operation cannot be fulfilled on runnersets.actions.summerwind.dev "example-runnerset": the object has been modified; please apply your changes to the latest version and try again
|
||||||
|
Eventually(func() error {
|
||||||
|
var rd actionsv1alpha1.RunnerDeployment
|
||||||
|
|
||||||
|
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: name}, &rd)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get test RunnerDeployment resource")
|
||||||
|
|
||||||
|
rd.Spec.Replicas = intPtr(2)
|
||||||
|
|
||||||
|
return k8sClient.Update(ctx, &rd)
|
||||||
|
},
|
||||||
|
time.Second*1, time.Millisecond*500).Should(BeNil())
|
||||||
|
|
||||||
|
runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}}
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerSets.Items)
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(1))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *runnerSets.Items[0].Spec.Replicas
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 3 replicas
|
||||||
|
{
|
||||||
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
||||||
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
MinReplicas: intPtr(1),
|
||||||
|
MaxReplicas: intPtr(3),
|
||||||
|
ScaleDownDelaySecondsAfterScaleUp: nil,
|
||||||
|
Metrics: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, hra)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create test HorizontalRunnerAutoscaler resource")
|
||||||
|
|
||||||
|
runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}}
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerSets.Items)
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(1))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSets.Items) == 0 {
|
||||||
|
logf.Log.Info("No runnerreplicasets exist yet")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return *runnerSets.Items[0].Spec.Replicas
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-down to 1 replica
|
||||||
|
{
|
||||||
|
responses.ListRepositoryWorkflowRuns.Body = workflowRunsFor1Replicas
|
||||||
|
|
||||||
|
var hra actionsv1alpha1.HorizontalRunnerAutoscaler
|
||||||
|
|
||||||
|
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: name}, &hra)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource")
|
||||||
|
|
||||||
|
hra.Annotations = map[string]string{
|
||||||
|
"force-update": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Update(ctx, &hra)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource")
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() int {
|
||||||
|
var runnerSets actionsv1alpha1.RunnerReplicaSetList
|
||||||
|
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSets.Items) == 0 {
|
||||||
|
logf.Log.Info("No runnerreplicasets exist yet")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return *runnerSets.Items[0].Spec.Replicas
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -261,6 +261,11 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
|||||||
runnerImage = r.RunnerImage
|
runnerImage = r.RunnerImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runnerImagePullPolicy := runner.Spec.ImagePullPolicy
|
||||||
|
if runnerImagePullPolicy == "" {
|
||||||
|
runnerImagePullPolicy = corev1.PullAlways
|
||||||
|
}
|
||||||
|
|
||||||
env := []corev1.EnvVar{
|
env := []corev1.EnvVar{
|
||||||
{
|
{
|
||||||
Name: "RUNNER_NAME",
|
Name: "RUNNER_NAME",
|
||||||
@@ -298,7 +303,7 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
|||||||
{
|
{
|
||||||
Name: containerName,
|
Name: containerName,
|
||||||
Image: runnerImage,
|
Image: runnerImage,
|
||||||
ImagePullPolicy: "Always",
|
ImagePullPolicy: runnerImagePullPolicy,
|
||||||
Env: env,
|
Env: env,
|
||||||
EnvFrom: runner.Spec.EnvFrom,
|
EnvFrom: runner.Spec.EnvFrom,
|
||||||
VolumeMounts: []corev1.VolumeMount{
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/summerwind/actions-runner-controller/github"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -49,10 +48,9 @@ const (
|
|||||||
// RunnerDeploymentReconciler reconciles a Runner object
|
// RunnerDeploymentReconciler reconciles a Runner object
|
||||||
type RunnerDeploymentReconciler struct {
|
type RunnerDeploymentReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
GitHubClient *github.Client
|
Log logr.Logger
|
||||||
Log logr.Logger
|
Recorder record.EventRecorder
|
||||||
Recorder record.EventRecorder
|
Scheme *runtime.Scheme
|
||||||
Scheme *runtime.Scheme
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments,verbs=get;list;watch;create;update;patch;delete
|
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments,verbs=get;list;watch;create;update;patch;delete
|
||||||
@@ -97,11 +95,9 @@ func (r *RunnerDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
oldSets = myRunnerReplicaSets[1:]
|
oldSets = myRunnerReplicaSets[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
desiredRS, err := r.newRunnerReplicaSetWithAutoscaling(rd)
|
desiredRS, err := r.newRunnerReplicaSet(rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(NotSupported); ok {
|
r.Recorder.Event(&rd, corev1.EventTypeNormal, "RunnerAutoscalingFailure", err.Error())
|
||||||
r.Recorder.Event(&rd, corev1.EventTypeNormal, "RunnerReplicaSetAutoScaleNotSupported", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error(err, "Could not create runnerreplicaset")
|
log.Error(err, "Could not create runnerreplicaset")
|
||||||
|
|
||||||
@@ -195,12 +191,6 @@ func (r *RunnerDeploymentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
updated := rd.DeepCopy()
|
updated := rd.DeepCopy()
|
||||||
updated.Status.Replicas = desiredRS.Spec.Replicas
|
updated.Status.Replicas = desiredRS.Spec.Replicas
|
||||||
|
|
||||||
if (rd.Status.Replicas == nil && *desiredRS.Spec.Replicas > 1) ||
|
|
||||||
(rd.Status.Replicas != nil && *desiredRS.Spec.Replicas > *rd.Status.Replicas) {
|
|
||||||
|
|
||||||
updated.Status.LastSuccessfulScaleOutTime = &metav1.Time{Time: time.Now()}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.Status().Update(ctx, updated); err != nil {
|
if err := r.Status().Update(ctx, updated); err != nil {
|
||||||
log.Error(err, "Failed to update runnerdeployment status")
|
log.Error(err, "Failed to update runnerdeployment status")
|
||||||
|
|
||||||
@@ -265,7 +255,7 @@ func CloneAndAddLabel(labels map[string]string, labelKey, labelValue string) map
|
|||||||
return newLabels
|
return newLabels
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RunnerDeploymentReconciler) newRunnerReplicaSet(rd v1alpha1.RunnerDeployment, computedReplicas *int) (*v1alpha1.RunnerReplicaSet, error) {
|
func (r *RunnerDeploymentReconciler) newRunnerReplicaSet(rd v1alpha1.RunnerDeployment) (*v1alpha1.RunnerReplicaSet, error) {
|
||||||
newRSTemplate := *rd.Spec.Template.DeepCopy()
|
newRSTemplate := *rd.Spec.Template.DeepCopy()
|
||||||
templateHash := ComputeHash(&newRSTemplate)
|
templateHash := ComputeHash(&newRSTemplate)
|
||||||
// Add template hash label to selector.
|
// Add template hash label to selector.
|
||||||
@@ -286,10 +276,6 @@ func (r *RunnerDeploymentReconciler) newRunnerReplicaSet(rd v1alpha1.RunnerDeplo
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if computedReplicas != nil {
|
|
||||||
rs.Spec.Replicas = computedReplicas
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctrl.SetControllerReference(&rd, &rs, r.Scheme); err != nil {
|
if err := ctrl.SetControllerReference(&rd, &rs, r.Scheme); err != nil {
|
||||||
return &rs, err
|
return &rs, err
|
||||||
}
|
}
|
||||||
@@ -321,36 +307,3 @@ func (r *RunnerDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|||||||
Owns(&v1alpha1.RunnerReplicaSet{}).
|
Owns(&v1alpha1.RunnerReplicaSet{}).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RunnerDeploymentReconciler) newRunnerReplicaSetWithAutoscaling(rd v1alpha1.RunnerDeployment) (*v1alpha1.RunnerReplicaSet, error) {
|
|
||||||
var computedReplicas *int
|
|
||||||
|
|
||||||
if rd.Spec.Replicas == nil {
|
|
||||||
replicas, err := r.determineDesiredReplicas(rd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var scaleDownDelay time.Duration
|
|
||||||
|
|
||||||
if rd.Spec.ScaleDownDelaySecondsAfterScaleUp != nil {
|
|
||||||
scaleDownDelay = time.Duration(*rd.Spec.ScaleDownDelaySecondsAfterScaleUp) * time.Second
|
|
||||||
} else {
|
|
||||||
scaleDownDelay = 10 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if rd.Status.Replicas == nil ||
|
|
||||||
*rd.Status.Replicas < *replicas ||
|
|
||||||
rd.Status.LastSuccessfulScaleOutTime == nil ||
|
|
||||||
rd.Status.LastSuccessfulScaleOutTime.Add(scaleDownDelay).Before(now) {
|
|
||||||
|
|
||||||
computedReplicas = replicas
|
|
||||||
} else {
|
|
||||||
computedReplicas = rd.Status.Replicas
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.newRunnerReplicaSet(rd, computedReplicas)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,111 +21,116 @@ const (
|
|||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
type handler struct {
|
type Handler struct {
|
||||||
Status int
|
Status int
|
||||||
Body string
|
Body string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
w.WriteHeader(h.Status)
|
w.WriteHeader(h.Status)
|
||||||
fmt.Fprintf(w, h.Body)
|
fmt.Fprintf(w, h.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
*FixedResponses
|
||||||
|
}
|
||||||
|
|
||||||
// NewServer creates a fake server for running unit tests
|
// NewServer creates a fake server for running unit tests
|
||||||
func NewServer(opts ...Option) *httptest.Server {
|
func NewServer(opts ...Option) *httptest.Server {
|
||||||
var responses FixedResponses
|
config := ServerConfig{
|
||||||
|
FixedResponses: &FixedResponses{},
|
||||||
for _, o := range opts {
|
|
||||||
o(&responses)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routes := map[string]handler{
|
for _, o := range opts {
|
||||||
|
o(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := map[string]*Handler{
|
||||||
// For CreateRegistrationToken
|
// For CreateRegistrationToken
|
||||||
"/repos/test/valid/actions/runners/registration-token": handler{
|
"/repos/test/valid/actions/runners/registration-token": &Handler{
|
||||||
Status: http.StatusCreated,
|
Status: http.StatusCreated,
|
||||||
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
||||||
},
|
},
|
||||||
"/repos/test/invalid/actions/runners/registration-token": handler{
|
"/repos/test/invalid/actions/runners/registration-token": &Handler{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
||||||
},
|
},
|
||||||
"/repos/test/error/actions/runners/registration-token": handler{
|
"/repos/test/error/actions/runners/registration-token": &Handler{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/orgs/test/actions/runners/registration-token": handler{
|
"/orgs/test/actions/runners/registration-token": &Handler{
|
||||||
Status: http.StatusCreated,
|
Status: http.StatusCreated,
|
||||||
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
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{
|
"/orgs/invalid/actions/runners/registration-token": &Handler{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Body: fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)),
|
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{
|
"/orgs/error/actions/runners/registration-token": &Handler{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
// For ListRunners
|
// For ListRunners
|
||||||
"/repos/test/valid/actions/runners": handler{
|
"/repos/test/valid/actions/runners": &Handler{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Body: RunnersListBody,
|
Body: RunnersListBody,
|
||||||
},
|
},
|
||||||
"/repos/test/invalid/actions/runners": handler{
|
"/repos/test/invalid/actions/runners": &Handler{
|
||||||
Status: http.StatusNoContent,
|
Status: http.StatusNoContent,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/repos/test/error/actions/runners": handler{
|
"/repos/test/error/actions/runners": &Handler{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/orgs/test/actions/runners": handler{
|
"/orgs/test/actions/runners": &Handler{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Body: RunnersListBody,
|
Body: RunnersListBody,
|
||||||
},
|
},
|
||||||
"/orgs/invalid/actions/runners": handler{
|
"/orgs/invalid/actions/runners": &Handler{
|
||||||
Status: http.StatusNoContent,
|
Status: http.StatusNoContent,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/orgs/error/actions/runners": handler{
|
"/orgs/error/actions/runners": &Handler{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
// For RemoveRunner
|
// For RemoveRunner
|
||||||
"/repos/test/valid/actions/runners/1": handler{
|
"/repos/test/valid/actions/runners/1": &Handler{
|
||||||
Status: http.StatusNoContent,
|
Status: http.StatusNoContent,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/repos/test/invalid/actions/runners/1": handler{
|
"/repos/test/invalid/actions/runners/1": &Handler{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/repos/test/error/actions/runners/1": handler{
|
"/repos/test/error/actions/runners/1": &Handler{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/orgs/test/actions/runners/1": handler{
|
"/orgs/test/actions/runners/1": &Handler{
|
||||||
Status: http.StatusNoContent,
|
Status: http.StatusNoContent,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/orgs/invalid/actions/runners/1": handler{
|
"/orgs/invalid/actions/runners/1": &Handler{
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
"/orgs/error/actions/runners/1": handler{
|
"/orgs/error/actions/runners/1": &Handler{
|
||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
// For auto-scaling based on the number of queued(pending) workflow runs
|
// For auto-scaling based on the number of queued(pending) workflow runs
|
||||||
"/repos/test/valid/actions/runs": responses.listRepositoryWorkflowRuns.handler(),
|
"/repos/test/valid/actions/runs": config.FixedResponses.ListRepositoryWorkflowRuns,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
for path, handler := range routes {
|
for path, handler := range routes {
|
||||||
h := handler
|
mux.Handle(path, handler)
|
||||||
mux.Handle(path, &h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return httptest.NewServer(mux)
|
return httptest.NewServer(mux)
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
package fake
|
package fake
|
||||||
|
|
||||||
type FixedResponses struct {
|
type FixedResponses struct {
|
||||||
listRepositoryWorkflowRuns FixedResponse
|
ListRepositoryWorkflowRuns *Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
type FixedResponse struct {
|
type Option func(*ServerConfig)
|
||||||
Status int
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r FixedResponse) handler() handler {
|
|
||||||
return handler{
|
|
||||||
Status: r.Status,
|
|
||||||
Body: r.Body,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option func(responses *FixedResponses)
|
|
||||||
|
|
||||||
func WithListRepositoryWorkflowRunsResponse(status int, body string) Option {
|
func WithListRepositoryWorkflowRunsResponse(status int, body string) Option {
|
||||||
return func(r *FixedResponses) {
|
return func(c *ServerConfig) {
|
||||||
r.listRepositoryWorkflowRuns = FixedResponse{
|
c.FixedResponses.ListRepositoryWorkflowRuns = &Handler{
|
||||||
Status: status,
|
Status: status,
|
||||||
Body: body,
|
Body: body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithFixedResponses(responses *FixedResponses) Option {
|
||||||
|
return func(c *ServerConfig) {
|
||||||
|
c.FixedResponses = responses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
19
main.go
19
main.go
@@ -169,10 +169,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runnerDeploymentReconciler := &controllers.RunnerDeploymentReconciler{
|
runnerDeploymentReconciler := &controllers.RunnerDeploymentReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Log: ctrl.Log.WithName("controllers").WithName("RunnerDeployment"),
|
Log: ctrl.Log.WithName("controllers").WithName("RunnerDeployment"),
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
GitHubClient: ghClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = runnerDeploymentReconciler.SetupWithManager(mgr); err != nil {
|
if err = runnerDeploymentReconciler.SetupWithManager(mgr); err != nil {
|
||||||
@@ -180,6 +179,18 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
horizontalRunnerAutoscaler := &controllers.HorizontalRunnerAutoscalerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Log: ctrl.Log.WithName("controllers").WithName("HorizontalRunnerAutoscaler"),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
GitHubClient: ghClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = horizontalRunnerAutoscaler.SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create controller", "controller", "HorizontalRunnerAutoscaler")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if err = (&actionsv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil {
|
if err = (&actionsv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
setupLog.Error(err, "unable to create webhook", "webhook", "Runner")
|
setupLog.Error(err, "unable to create webhook", "webhook", "Runner")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
NAME ?= summerwind/actions-runner
|
NAME ?= summerwind/actions-runner
|
||||||
|
|
||||||
RUNNER_VERSION ?= 2.267.0
|
RUNNER_VERSION ?= 2.272.0
|
||||||
DOCKER_VERSION ?= 19.03.8
|
DOCKER_VERSION ?= 19.03.12
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${NAME}:latest -t ${NAME}:v${RUNNER_VERSION} .
|
docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${NAME}:latest -t ${NAME}:v${RUNNER_VERSION} .
|
||||||
|
|||||||
Reference in New Issue
Block a user