mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-11 03:57:01 +00:00
Compare commits
39 Commits
actions-ru
...
actions-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
450e384c4c | ||
|
|
e9eef04993 | ||
|
|
598dd1d9fe | ||
|
|
9890a90e69 | ||
|
|
9da123ae5e | ||
|
|
4d4137aa28 | ||
|
|
022007078e | ||
|
|
31e5e61155 | ||
|
|
1d1453c5f2 | ||
|
|
e44e53b88e | ||
|
|
398791241e | ||
|
|
991535e567 | ||
|
|
2d7fbbfb68 | ||
|
|
dd0b9f3e95 | ||
|
|
7cb2bc84c8 | ||
|
|
b0e74bebab | ||
|
|
dfbe53dcca | ||
|
|
ebc3970b84 | ||
|
|
1ddcf6946a | ||
|
|
cfbaad38c8 | ||
|
|
67f6de010b | ||
|
|
2db608879a | ||
|
|
2c4a6ca90b | ||
|
|
829bf20449 | ||
|
|
be13322816 | ||
|
|
7f4a76a39b | ||
|
|
0fce761686 | ||
|
|
c88ff44518 | ||
|
|
2fdf35ac9d | ||
|
|
6cce3fefc5 | ||
|
|
eb2eaf8130 | ||
|
|
7bf712d0d4 | ||
|
|
7d024a6c05 | ||
|
|
434823bcb3 | ||
|
|
35d047db01 | ||
|
|
f1db6af1c5 | ||
|
|
4f3f2fb60d | ||
|
|
2623140c9a | ||
|
|
1db9d9d574 |
34
.github/workflows/build-and-release-runners.yml
vendored
34
.github/workflows/build-and-release-runners.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
paths:
|
paths:
|
||||||
- 'runner/**'
|
- 'runner/**'
|
||||||
- .github/workflows/build-runner.yml
|
- .github/workflows/build-and-release-runners.yml
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -15,10 +15,8 @@ on:
|
|||||||
- runner/Dockerfile
|
- runner/Dockerfile
|
||||||
- runner/dindrunner.Dockerfile
|
- runner/dindrunner.Dockerfile
|
||||||
- runner/entrypoint.sh
|
- runner/entrypoint.sh
|
||||||
- .github/workflows/build-runner.yml
|
- .github/workflows/build-and-release-runners.yml
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
name: Runner
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -31,7 +29,7 @@ jobs:
|
|||||||
- name: actions-runner-dind
|
- name: actions-runner-dind
|
||||||
dockerfile: dindrunner.Dockerfile
|
dockerfile: dindrunner.Dockerfile
|
||||||
env:
|
env:
|
||||||
RUNNER_VERSION: 2.276.1
|
RUNNER_VERSION: 2.277.1
|
||||||
DOCKER_VERSION: 19.03.12
|
DOCKER_VERSION: 19.03.12
|
||||||
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
DOCKERHUB_USERNAME: ${{ github.repository_owner }}
|
||||||
steps:
|
steps:
|
||||||
@@ -52,36 +50,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' }}
|
||||||
with:
|
with:
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
|
||||||
# Considered unstable builds
|
- name: Build and Push
|
||||||
# Mutable (no sha) and immutable (include sha) tags are created, see Issue 285 and PR 286 for why
|
|
||||||
- name: Build and push canary builds
|
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: ./runner
|
context: ./runner
|
||||||
file: ./runner/${{ matrix.dockerfile }}
|
file: ./runner/${{ matrix.dockerfile }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' && github.event_name != 'release' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
build-args: |
|
|
||||||
RUNNER_VERSION=${{ env.RUNNER_VERSION }}
|
|
||||||
DOCKER_VERSION=${{ env.DOCKER_VERSION }}
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKERHUB_USERNAME }}/${{ matrix.name }}:v${{ env.RUNNER_VERSION }}-canary
|
|
||||||
${{ env.DOCKERHUB_USERNAME }}/${{ matrix.name }}:v${{ env.RUNNER_VERSION }}-canary-${{ steps.vars.outputs.sha_short }}
|
|
||||||
|
|
||||||
# Considered stable builds
|
|
||||||
# Mutable (no sha) and immutable (include sha) tags are created, see Issue 285 and PR 286 for why
|
|
||||||
- name: Build and push release builds
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./runner
|
|
||||||
file: ./runner/${{ matrix.dockerfile }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: ${{ github.event_name == 'release' }}
|
|
||||||
build-args: |
|
build-args: |
|
||||||
RUNNER_VERSION=${{ env.RUNNER_VERSION }}
|
RUNNER_VERSION=${{ env.RUNNER_VERSION }}
|
||||||
DOCKER_VERSION=${{ env.DOCKER_VERSION }}
|
DOCKER_VERSION=${{ env.DOCKER_VERSION }}
|
||||||
|
|||||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -57,6 +57,7 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:latest
|
||||||
${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:${{ env.VERSION }}
|
${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:${{ env.VERSION }}
|
||||||
${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:${{ env.VERSION }}-${{ steps.vars.outputs.sha_short }}
|
${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:${{ env.VERSION }}-${{ steps.vars.outputs.sha_short }}
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/test.yaml
vendored
1
.github/workflows/test.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'runner/**'
|
- 'runner/**'
|
||||||
|
- .github/workflows/build-and-release-runners.yml
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
6
.github/workflows/wip.yml
vendored
6
.github/workflows/wip.yml
vendored
@@ -30,11 +30,13 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
# Considered unstable builds
|
||||||
|
# See Issue #285, PR #286, and PR #323 for more information
|
||||||
- name: Build and Push
|
- name: Build and Push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:latest
|
tags: |
|
||||||
|
${{ env.DOCKERHUB_USERNAME }}/actions-runner-controller:canary
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# actions-runner-controller
|
# actions-runner-controller
|
||||||
|
|
||||||
|
[](https://github.com/jonico/awesome-runners)
|
||||||
|
|
||||||
This controller operates self-hosted runners for GitHub Actions on your Kubernetes cluster.
|
This controller operates self-hosted runners for GitHub Actions on your Kubernetes cluster.
|
||||||
|
|
||||||
ToC:
|
ToC:
|
||||||
@@ -393,7 +395,7 @@ spec:
|
|||||||
```
|
```
|
||||||
|
|
||||||
With the above example, the webhook server scales `myrunners` by `1` replica for 5 minutes on each `check_run` event
|
With the above example, the webhook server scales `myrunners` by `1` replica for 5 minutes on each `check_run` event
|
||||||
with the type of `created` and the status of `queued` received.
|
with the type of `created` and the status of `queued` received.
|
||||||
|
|
||||||
The primary benefit of autoscaling on Webhook compared to the standard autoscaling is that this one allows you to
|
The primary benefit of autoscaling on Webhook compared to the standard autoscaling is that this one allows you to
|
||||||
immediately add "resource slack" for future GitHub Actions job runs.
|
immediately add "resource slack" for future GitHub Actions job runs.
|
||||||
@@ -529,14 +531,14 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: "2.0"
|
cpu: "2.0"
|
||||||
memory: "4Gi"
|
memory: "4Gi"
|
||||||
|
|
||||||
# Timeout after a node crashed or became unreachable to evict your pods somewhere else (default 5mins)
|
# Timeout after a node crashed or became unreachable to evict your pods somewhere else (default 5mins)
|
||||||
tolerations:
|
tolerations:
|
||||||
- key: "node.kubernetes.io/unreachable"
|
- key: "node.kubernetes.io/unreachable"
|
||||||
operator: "Exists"
|
operator: "Exists"
|
||||||
effect: "NoExecute"
|
effect: "NoExecute"
|
||||||
tolerationSeconds: 10
|
tolerationSeconds: 10
|
||||||
|
|
||||||
# If set to false, there are no privileged container and you cannot use docker.
|
# If set to false, there are no privileged container and you cannot use docker.
|
||||||
dockerEnabled: false
|
dockerEnabled: false
|
||||||
# If set to true, runner pod container only 1 container that's expected to be able to run docker, too.
|
# If set to true, runner pod container only 1 container that's expected to be able to run docker, too.
|
||||||
|
|||||||
@@ -126,6 +126,16 @@ type MetricSpec struct {
|
|||||||
// to determine how many pods should be removed.
|
// to determine how many pods should be removed.
|
||||||
// +optional
|
// +optional
|
||||||
ScaleDownFactor string `json:"scaleDownFactor,omitempty"`
|
ScaleDownFactor string `json:"scaleDownFactor,omitempty"`
|
||||||
|
|
||||||
|
// ScaleUpAdjustment is the number of runners added on scale-up.
|
||||||
|
// You can only specify either ScaleUpFactor or ScaleUpAdjustment.
|
||||||
|
// +optional
|
||||||
|
ScaleUpAdjustment int `json:"scaleUpAdjustment,omitempty"`
|
||||||
|
|
||||||
|
// ScaleDownAdjustment is the number of runners removed on scale-down.
|
||||||
|
// You can only specify either ScaleDownFactor or ScaleDownAdjustment.
|
||||||
|
// +optional
|
||||||
|
ScaleDownAdjustment int `json:"scaleDownAdjustment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HorizontalRunnerAutoscalerStatus struct {
|
type HorizontalRunnerAutoscalerStatus struct {
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.4.0
|
version: 0.6.0
|
||||||
|
|
||||||
home: https://github.com/summerwind/actions-runner-controller
|
home: https://github.com/summerwind/actions-runner-controller
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/summerwind/actions-runner-controller
|
- https://github.com/summerwind/actions-runner-controller
|
||||||
|
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: summerwind
|
- name: summerwind
|
||||||
email: contact@summerwind.jp
|
email: contact@summerwind.jp
|
||||||
url: https://github.com/summerwind
|
url: https://github.com/summerwind
|
||||||
- name: funkypenguin
|
- name: funkypenguin
|
||||||
email: davidy@funkypenguin.co.nz
|
email: davidy@funkypenguin.co.nz
|
||||||
url: https://www.funkypenguin.co.nz
|
url: https://www.funkypenguin.co.nz
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ spec:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
scaleDownAdjustment:
|
||||||
|
description: ScaleDownAdjustment is the number of runners removed
|
||||||
|
on scale-down. You can only specify either ScaleDownFactor or
|
||||||
|
ScaleDownAdjustment.
|
||||||
|
type: integer
|
||||||
scaleDownFactor:
|
scaleDownFactor:
|
||||||
description: ScaleDownFactor is the multiplicative factor applied
|
description: ScaleDownFactor is the multiplicative factor applied
|
||||||
to the current number of runners used to determine how many
|
to the current number of runners used to determine how many
|
||||||
@@ -87,6 +92,10 @@ spec:
|
|||||||
description: ScaleDownThreshold is the percentage of busy runners
|
description: ScaleDownThreshold is the percentage of busy runners
|
||||||
less than which will trigger the hpa to scale the runners down.
|
less than which will trigger the hpa to scale the runners down.
|
||||||
type: string
|
type: string
|
||||||
|
scaleUpAdjustment:
|
||||||
|
description: ScaleUpAdjustment is the number of runners added
|
||||||
|
on scale-up. You can only specify either ScaleUpFactor or ScaleUpAdjustment.
|
||||||
|
type: integer
|
||||||
scaleUpFactor:
|
scaleUpFactor:
|
||||||
description: ScaleUpFactor is the multiplicative factor applied
|
description: ScaleUpFactor is the multiplicative factor applied
|
||||||
to the current number of runners used to determine how many
|
to the current number of runners used to determine how many
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
1. Get the application URL by running these commands:
|
1. Get the application URL by running these commands:
|
||||||
{{- if .Values.ingress.enabled }}
|
{{- if .Values.githubWebhookServer.ingress.enabled }}
|
||||||
{{- range $host := .Values.ingress.hosts }}
|
{{- range $host := .Values.githubWebhookServer.ingress.hosts }}
|
||||||
{{- range .paths }}
|
{{- range .paths }}
|
||||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
|
http{{ if $.Values.githubWebhookServer.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- else if contains "NodePort" .Values.service.type }}
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ Create the name of the service account to use
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "actions-runner-controller-github-webhook-server.secretName" -}}
|
||||||
|
{{- default (include "actions-runner-controller-github-webhook-server.fullname" .) .Values.githubWebhookServer.secret.name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
{{- define "actions-runner-controller-github-webhook-server.roleName" -}}
|
{{- define "actions-runner-controller-github-webhook-server.roleName" -}}
|
||||||
{{- include "actions-runner-controller-github-webhook-server.fullname" . }}
|
{{- include "actions-runner-controller-github-webhook-server.fullname" . }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ Create the name of the service account to use
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "actions-runner-controller.secretName" -}}
|
||||||
|
{{- default (include "actions-runner-controller.fullname" .) .Values.authSecret.name -}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
{{- define "actions-runner-controller.leaderElectionRoleName" -}}
|
{{- define "actions-runner-controller.leaderElectionRoleName" -}}
|
||||||
{{- include "actions-runner-controller.fullname" . }}-leader-election
|
{{- include "actions-runner-controller.fullname" . }}-leader-election
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@@ -85,11 +89,11 @@ Create the name of the service account to use
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- define "actions-runner-controller.webhookServiceName" -}}
|
{{- define "actions-runner-controller.webhookServiceName" -}}
|
||||||
{{- include "actions-runner-controller.fullname" . }}-webhook
|
{{- include "actions-runner-controller.fullname" . | trunc 55 }}-webhook
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- define "actions-runner-controller.authProxyServiceName" -}}
|
{{- define "actions-runner-controller.authProxyServiceName" -}}
|
||||||
{{- include "actions-runner-controller.fullname" . }}-metrics-service
|
{{- include "actions-runner-controller.fullname" . | trunc 47 }}-metrics-service
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- define "actions-runner-controller.selfsignedIssuerName" -}}
|
{{- define "actions-runner-controller.selfsignedIssuerName" -}}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
{{- include "actions-runner-controller.selectorLabels" . | nindent 6 }}
|
{{- include "actions-runner-controller.selectorLabels" . | nindent 6 }}
|
||||||
@@ -41,19 +42,19 @@ spec:
|
|||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
key: github_token
|
key: github_token
|
||||||
name: controller-manager
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
optional: true
|
optional: true
|
||||||
- name: GITHUB_APP_ID
|
- name: GITHUB_APP_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
key: github_app_id
|
key: github_app_id
|
||||||
name: controller-manager
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
optional: true
|
optional: true
|
||||||
- name: GITHUB_APP_INSTALLATION_ID
|
- name: GITHUB_APP_INSTALLATION_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
key: github_app_installation_id
|
key: github_app_installation_id
|
||||||
name: controller-manager
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
optional: true
|
optional: true
|
||||||
- name: GITHUB_APP_PRIVATE_KEY
|
- name: GITHUB_APP_PRIVATE_KEY
|
||||||
value: /etc/actions-runner-controller/github_app_private_key
|
value: /etc/actions-runner-controller/github_app_private_key
|
||||||
@@ -61,7 +62,7 @@ spec:
|
|||||||
- name: {{ $key }}
|
- name: {{ $key }}
|
||||||
value: {{ $val | quote }}
|
value: {{ $val | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default (cat "v" .Chart.AppVersion | replace " " "") }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
name: manager
|
name: manager
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
@@ -71,13 +72,13 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: "/etc/actions-runner-controller"
|
- mountPath: "/etc/actions-runner-controller"
|
||||||
name: controller-manager
|
name: secret
|
||||||
readOnly: true
|
readOnly: true
|
||||||
- mountPath: /tmp
|
- mountPath: /tmp
|
||||||
name: tmp
|
name: tmp
|
||||||
- mountPath: /tmp/k8s-webhook-server/serving-certs
|
- mountPath: /tmp/k8s-webhook-server/serving-certs
|
||||||
name: cert
|
name: cert
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@@ -93,14 +94,14 @@ spec:
|
|||||||
- containerPort: 8443
|
- containerPort: 8443
|
||||||
name: https
|
name: https
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
terminationGracePeriodSeconds: 10
|
terminationGracePeriodSeconds: 10
|
||||||
volumes:
|
volumes:
|
||||||
- name: controller-manager
|
- name: secret
|
||||||
secret:
|
secret:
|
||||||
secretName: controller-manager
|
secretName: {{ include "actions-runner-controller.secretName" . }}
|
||||||
- name: cert
|
- name: cert
|
||||||
secret:
|
secret:
|
||||||
defaultMode: 420
|
defaultMode: 420
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
|
replicas: {{ .Values.githubWebhookServer.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
{{- include "actions-runner-controller-github-webhook-server.selectorLabels" . | nindent 6 }}
|
{{- include "actions-runner-controller-github-webhook-server.selectorLabels" . | nindent 6 }}
|
||||||
@@ -32,7 +33,6 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- args:
|
- args:
|
||||||
- "--metrics-addr=127.0.0.1:8080"
|
- "--metrics-addr=127.0.0.1:8080"
|
||||||
- "--enable-leader-election"
|
|
||||||
- "--sync-period={{ .Values.githubWebhookServer.syncPeriod }}"
|
- "--sync-period={{ .Values.githubWebhookServer.syncPeriod }}"
|
||||||
command:
|
command:
|
||||||
- "/github-webhook-server"
|
- "/github-webhook-server"
|
||||||
@@ -41,18 +41,18 @@ spec:
|
|||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
key: github_webhook_secret_token
|
key: github_webhook_secret_token
|
||||||
name: github-webhook-server
|
name: {{- include "actions-runner-controller-github-webhook-server.secretName" . }}
|
||||||
optional: true
|
optional: true
|
||||||
{{- range $key, $val := .Values.githubWebhookServer.env }}
|
{{- range $key, $val := .Values.githubWebhookServer.env }}
|
||||||
- name: {{ $key }}
|
- name: {{ $key }}
|
||||||
value: {{ $val | quote }}
|
value: {{ $val | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
image: "{{ .Values.githubWebhookServer.image.repository }}:{{ .Values.githubWebhookServer.image.tag | default (cat "v" .Chart.AppVersion | replace " " "") }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
name: github-webhook-server
|
name: github-webhook-server
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
name: github-webhook-server
|
name: http
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.githubWebhookServer.resources | nindent 12 }}
|
{{- toYaml .Values.githubWebhookServer.resources | nindent 12 }}
|
||||||
@@ -70,14 +70,10 @@ spec:
|
|||||||
- containerPort: 8443
|
- containerPort: 8443
|
||||||
name: https
|
name: https
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
terminationGracePeriodSeconds: 10
|
terminationGracePeriodSeconds: 10
|
||||||
volumes:
|
|
||||||
- name: github-webhook-server
|
|
||||||
secret:
|
|
||||||
secretName: github-webhook-server
|
|
||||||
{{- with .Values.githubWebhookServer.nodeSelector }}
|
{{- with .Values.githubWebhookServer.nodeSelector }}
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{{- if .Values.githubWebhookServer.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "actions-runner-controller-github-webhook-server.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.githubWebhookServer.service.port -}}
|
||||||
|
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
{{- else -}}
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
{{- end }}
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
labels:
|
||||||
|
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.githubWebhookServer.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.githubWebhookServer.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.githubWebhookServer.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.githubWebhookServer.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
backend:
|
||||||
|
serviceName: {{ $fullName }}
|
||||||
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{{- if .Values.githubWebhookServer.enabled }}
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: {{ include "actions-runner-controller-github-webhook-server.roleName" . }}
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: {{ include "actions-runner-controller-github-webhook-server.roleName" . }}
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: {{ include "actions-runner-controller-github-webhook-server.serviceAccountName" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
{{- end }}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{{- if .Values.githubWebhookServer.enabled }}
|
{{- if .Values.githubWebhookServer.enabled }}
|
||||||
{{- if .Values.githubWebhookServer.secret.enabled }}
|
{{- if .Values.githubWebhookServer.secret.create }}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: github-webhook-server
|
name: {{- include "actions-runner-controller-github-webhook-server.secretName" . }}
|
||||||
namespace: {{ .Release.Namespace }}
|
namespace: {{ .Release.Namespace }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
{{- range $k, $v := .Values.githubWebhookServer.secret }}
|
{{- if .Values.githubWebhookServer.secret.github_webhook_secret_token }}
|
||||||
{{ $k }}: {{ $v | toString | b64enc }}
|
github_webhook_secret_token: {{ .Values.githubWebhookServer.secret.github_webhook_secret_token | toString | b64enc }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
{{- if or .Values.authSecret.enabled }}
|
{{- if .Values.authSecret.create }}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: controller-manager
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
namespace: {{ .Release.Namespace }}
|
namespace: {{ .Release.Namespace }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
{{- include "actions-runner-controller.labels" . | nindent 4 }}
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
{{- range $k, $v := .Values.authSecret }}
|
{{- if .Values.authSecret.github_app_id }}
|
||||||
{{ $k }}: {{ $v | toString | b64enc }}
|
github_app_id: {{ .Values.authSecret.github_app_id | toString | b64enc }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- if .Values.authSecret.github_app_installation_id }}
|
||||||
|
github_app_installation_id: {{ .Values.authSecret.github_app_installation_id | toString | b64enc }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.authSecret.github_app_private_key }}
|
||||||
|
github_app_private_key: {{ .Values.authSecret.github_app_private_key | toString | b64enc }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.authSecret.github_token }}
|
||||||
|
github_token: {{ .Values.authSecret.github_token | toString | b64enc }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
{{- if semverCompare ">=1.16-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
{{- else -}}
|
||||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||||
|
{{- end }}
|
||||||
kind: MutatingWebhookConfiguration
|
kind: MutatingWebhookConfiguration
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
@@ -64,7 +68,11 @@ webhooks:
|
|||||||
- runnerreplicasets
|
- runnerreplicasets
|
||||||
|
|
||||||
---
|
---
|
||||||
|
{{- if semverCompare ">=1.16-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: admissionregistration.k8s.io/v1
|
||||||
|
{{- else -}}
|
||||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||||
|
{{- end }}
|
||||||
kind: ValidatingWebhookConfiguration
|
kind: ValidatingWebhookConfiguration
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ syncPeriod: 10m
|
|||||||
# Only 1 authentication method can be deployed at a time
|
# Only 1 authentication method can be deployed at a time
|
||||||
# Uncomment the configuration you are applying and fill in the details
|
# Uncomment the configuration you are applying and fill in the details
|
||||||
authSecret:
|
authSecret:
|
||||||
enabled: false
|
create: true
|
||||||
|
name: "controller-manager"
|
||||||
### GitHub Apps Configuration
|
### GitHub Apps Configuration
|
||||||
#github_app_id: ""
|
#github_app_id: ""
|
||||||
#github_app_installation_id: ""
|
#github_app_installation_id: ""
|
||||||
@@ -21,15 +22,14 @@ authSecret:
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
repository: summerwind/actions-runner-controller
|
repository: summerwind/actions-runner-controller
|
||||||
# Overrides the manager image tag whose default is the chart appVersion if the tag key is commented out
|
tag: "v0.17.0"
|
||||||
tag: "latest"
|
|
||||||
dindSidecarRepositoryAndTag: "docker:dind"
|
dindSidecarRepositoryAndTag: "docker:dind"
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
kube_rbac_proxy:
|
kube_rbac_proxy:
|
||||||
image:
|
image:
|
||||||
repository: gcr.io/kubebuilder/kube-rbac-proxy
|
repository: quay.io/brancz/kube-rbac-proxy
|
||||||
tag: v0.4.1
|
tag: v0.8.0
|
||||||
|
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
@@ -46,10 +46,12 @@ serviceAccount:
|
|||||||
|
|
||||||
podAnnotations: {}
|
podAnnotations: {}
|
||||||
|
|
||||||
podSecurityContext: {}
|
podSecurityContext:
|
||||||
|
{}
|
||||||
# fsGroup: 2000
|
# fsGroup: 2000
|
||||||
|
|
||||||
securityContext: {}
|
securityContext:
|
||||||
|
{}
|
||||||
# capabilities:
|
# capabilities:
|
||||||
# drop:
|
# drop:
|
||||||
# - ALL
|
# - ALL
|
||||||
@@ -61,20 +63,8 @@ service:
|
|||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 443
|
port: 443
|
||||||
|
|
||||||
ingress:
|
resources:
|
||||||
enabled: false
|
{}
|
||||||
annotations: {}
|
|
||||||
# kubernetes.io/ingress.class: nginx
|
|
||||||
# kubernetes.io/tls-acme: "true"
|
|
||||||
hosts:
|
|
||||||
- host: chart-example.local
|
|
||||||
paths: []
|
|
||||||
tls: []
|
|
||||||
# - secretName: chart-example-tls
|
|
||||||
# hosts:
|
|
||||||
# - chart-example.local
|
|
||||||
|
|
||||||
resources: {}
|
|
||||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
# choice for the user. This also increases chances charts run on environments with little
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
@@ -104,7 +94,8 @@ affinity: {}
|
|||||||
# PriorityClass: system-cluster-critical
|
# PriorityClass: system-cluster-critical
|
||||||
priorityClassName: ""
|
priorityClassName: ""
|
||||||
|
|
||||||
env: {}
|
env:
|
||||||
|
{}
|
||||||
# http_proxy: "proxy.com:8080"
|
# http_proxy: "proxy.com:8080"
|
||||||
# https_proxy: "proxy.com:8080"
|
# https_proxy: "proxy.com:8080"
|
||||||
# no_proxy: ""
|
# no_proxy: ""
|
||||||
@@ -115,14 +106,10 @@ githubWebhookServer:
|
|||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
syncPeriod: 10m
|
syncPeriod: 10m
|
||||||
secret:
|
secret:
|
||||||
enabled: false
|
create: true
|
||||||
|
name: "github-webhook-server"
|
||||||
### GitHub Webhook Configuration
|
### GitHub Webhook Configuration
|
||||||
#github_webhook_secret_token: ""
|
#github_webhook_secret_token: ""
|
||||||
image:
|
|
||||||
repository: summerwind/actions-runner-controller
|
|
||||||
# Overrides the manager image tag whose default is the chart appVersion if the tag key is commented out
|
|
||||||
tag: "latest"
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
fullnameOverride: ""
|
fullnameOverride: ""
|
||||||
@@ -144,10 +131,23 @@ githubWebhookServer:
|
|||||||
affinity: {}
|
affinity: {}
|
||||||
priorityClassName: ""
|
priorityClassName: ""
|
||||||
service:
|
service:
|
||||||
type: NodePort
|
type: ClusterIP
|
||||||
ports:
|
ports:
|
||||||
- port: 80
|
- port: 80
|
||||||
targetPort: 8000
|
targetPort: http
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
#nodePort: someFixedPortForUseWithTerraformCdkCfnEtc
|
#nodePort: someFixedPortForUseWithTerraformCdkCfnEtc
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
annotations:
|
||||||
|
{}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths: []
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ spec:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
scaleDownAdjustment:
|
||||||
|
description: ScaleDownAdjustment is the number of runners removed
|
||||||
|
on scale-down. You can only specify either ScaleDownFactor or
|
||||||
|
ScaleDownAdjustment.
|
||||||
|
type: integer
|
||||||
scaleDownFactor:
|
scaleDownFactor:
|
||||||
description: ScaleDownFactor is the multiplicative factor applied
|
description: ScaleDownFactor is the multiplicative factor applied
|
||||||
to the current number of runners used to determine how many
|
to the current number of runners used to determine how many
|
||||||
@@ -87,6 +92,10 @@ spec:
|
|||||||
description: ScaleDownThreshold is the percentage of busy runners
|
description: ScaleDownThreshold is the percentage of busy runners
|
||||||
less than which will trigger the hpa to scale the runners down.
|
less than which will trigger the hpa to scale the runners down.
|
||||||
type: string
|
type: string
|
||||||
|
scaleUpAdjustment:
|
||||||
|
description: ScaleUpAdjustment is the number of runners added
|
||||||
|
on scale-up. You can only specify either ScaleUpFactor or ScaleUpAdjustment.
|
||||||
|
type: integer
|
||||||
scaleUpFactor:
|
scaleUpFactor:
|
||||||
description: ScaleUpFactor is the multiplicative factor applied
|
description: ScaleUpFactor is the multiplicative factor applied
|
||||||
to the current number of runners used to determine how many
|
to the current number of runners used to determine how many
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: kube-rbac-proxy
|
- name: kube-rbac-proxy
|
||||||
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1
|
image: quay.io/brancz/kube-rbac-proxy:v0.8.0
|
||||||
args:
|
args:
|
||||||
- "--secure-listen-address=0.0.0.0:8443"
|
- "--secure-listen-address=0.0.0.0:8443"
|
||||||
- "--upstream=http://127.0.0.1:8080/"
|
- "--upstream=http://127.0.0.1:8080/"
|
||||||
|
|||||||
@@ -189,6 +189,9 @@ func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByQueuedAndInPro
|
|||||||
"workflow_runs_in_progress", inProgress,
|
"workflow_runs_in_progress", inProgress,
|
||||||
"workflow_runs_queued", queued,
|
"workflow_runs_queued", queued,
|
||||||
"workflow_runs_unknown", unknown,
|
"workflow_runs_unknown", unknown,
|
||||||
|
"namespace", hra.Namespace,
|
||||||
|
"runner_deployment", rd.Name,
|
||||||
|
"horizontal_runner_autoscaler", hra.Name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return &replicas, nil
|
return &replicas, nil
|
||||||
@@ -196,7 +199,6 @@ func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByQueuedAndInPro
|
|||||||
|
|
||||||
func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByPercentageRunnersBusy(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
|
func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByPercentageRunnersBusy(rd v1alpha1.RunnerDeployment, hra v1alpha1.HorizontalRunnerAutoscaler) (*int, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
orgName := rd.Spec.Template.Spec.Organization
|
|
||||||
minReplicas := *hra.Spec.MinReplicas
|
minReplicas := *hra.Spec.MinReplicas
|
||||||
maxReplicas := *hra.Spec.MaxReplicas
|
maxReplicas := *hra.Spec.MaxReplicas
|
||||||
metrics := hra.Spec.Metrics[0]
|
metrics := hra.Spec.Metrics[0]
|
||||||
@@ -220,14 +222,34 @@ func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByPercentageRunn
|
|||||||
|
|
||||||
scaleDownThreshold = sdt
|
scaleDownThreshold = sdt
|
||||||
}
|
}
|
||||||
if metrics.ScaleUpFactor != "" {
|
|
||||||
|
scaleUpAdjustment := metrics.ScaleUpAdjustment
|
||||||
|
if scaleUpAdjustment != 0 {
|
||||||
|
if metrics.ScaleUpAdjustment < 0 {
|
||||||
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].scaleUpAdjustment cannot be lower than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if metrics.ScaleUpFactor != "" {
|
||||||
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[]: scaleUpAdjustment and scaleUpFactor cannot be specified together")
|
||||||
|
}
|
||||||
|
} else if metrics.ScaleUpFactor != "" {
|
||||||
suf, err := strconv.ParseFloat(metrics.ScaleUpFactor, 64)
|
suf, err := strconv.ParseFloat(metrics.ScaleUpFactor, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].scaleUpFactor cannot be parsed into a float64")
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].scaleUpFactor cannot be parsed into a float64")
|
||||||
}
|
}
|
||||||
scaleUpFactor = suf
|
scaleUpFactor = suf
|
||||||
}
|
}
|
||||||
if metrics.ScaleDownFactor != "" {
|
|
||||||
|
scaleDownAdjustment := metrics.ScaleDownAdjustment
|
||||||
|
if scaleDownAdjustment != 0 {
|
||||||
|
if metrics.ScaleDownAdjustment < 0 {
|
||||||
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].scaleDownAdjustment cannot be lower than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if metrics.ScaleDownFactor != "" {
|
||||||
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[]: scaleDownAdjustment and scaleDownFactor cannot be specified together")
|
||||||
|
}
|
||||||
|
} else if metrics.ScaleDownFactor != "" {
|
||||||
sdf, err := strconv.ParseFloat(metrics.ScaleDownFactor, 64)
|
sdf, err := strconv.ParseFloat(metrics.ScaleDownFactor, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].scaleDownFactor cannot be parsed into a float64")
|
return nil, errors.New("validating autoscaling metrics: spec.autoscaling.metrics[].scaleDownFactor cannot be parsed into a float64")
|
||||||
@@ -245,8 +267,18 @@ func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByPercentageRunn
|
|||||||
runnerMap[items.Name] = struct{}{}
|
runnerMap[items.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
enterprise = rd.Spec.Template.Spec.Enterprise
|
||||||
|
organization = rd.Spec.Template.Spec.Organization
|
||||||
|
repository = rd.Spec.Template.Spec.Repository
|
||||||
|
)
|
||||||
|
|
||||||
// ListRunners will return all runners managed by GitHub - not restricted to ns
|
// ListRunners will return all runners managed by GitHub - not restricted to ns
|
||||||
runners, err := r.GitHubClient.ListRunners(ctx, "", orgName, "")
|
runners, err := r.GitHubClient.ListRunners(
|
||||||
|
ctx,
|
||||||
|
enterprise,
|
||||||
|
organization,
|
||||||
|
repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -261,9 +293,17 @@ func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByPercentageRunn
|
|||||||
var desiredReplicas int
|
var desiredReplicas int
|
||||||
fractionBusy := float64(numRunnersBusy) / float64(numRunners)
|
fractionBusy := float64(numRunnersBusy) / float64(numRunners)
|
||||||
if fractionBusy >= scaleUpThreshold {
|
if fractionBusy >= scaleUpThreshold {
|
||||||
desiredReplicas = int(math.Ceil(float64(numRunners) * scaleUpFactor))
|
if scaleUpAdjustment > 0 {
|
||||||
|
desiredReplicas = numRunners + scaleUpAdjustment
|
||||||
|
} else {
|
||||||
|
desiredReplicas = int(math.Ceil(float64(numRunners) * scaleUpFactor))
|
||||||
|
}
|
||||||
} else if fractionBusy < scaleDownThreshold {
|
} else if fractionBusy < scaleDownThreshold {
|
||||||
desiredReplicas = int(float64(numRunners) * scaleDownFactor)
|
if scaleDownAdjustment > 0 {
|
||||||
|
desiredReplicas = numRunners - scaleDownAdjustment
|
||||||
|
} else {
|
||||||
|
desiredReplicas = int(float64(numRunners) * scaleDownFactor)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
desiredReplicas = *rd.Spec.Replicas
|
desiredReplicas = *rd.Spec.Replicas
|
||||||
}
|
}
|
||||||
@@ -282,6 +322,12 @@ func (r *HorizontalRunnerAutoscalerReconciler) calculateReplicasByPercentageRunn
|
|||||||
"current_replicas", rd.Spec.Replicas,
|
"current_replicas", rd.Spec.Replicas,
|
||||||
"num_runners", numRunners,
|
"num_runners", numRunners,
|
||||||
"num_runners_busy", numRunnersBusy,
|
"num_runners_busy", numRunnersBusy,
|
||||||
|
"namespace", hra.Namespace,
|
||||||
|
"runner_deployment", rd.Name,
|
||||||
|
"horizontal_runner_autoscaler", hra.Name,
|
||||||
|
"enterprise", enterprise,
|
||||||
|
"organization", organization,
|
||||||
|
"repository", repository,
|
||||||
)
|
)
|
||||||
|
|
||||||
rd.Status.Replicas = &desiredReplicas
|
rd.Status.Replicas = &desiredReplicas
|
||||||
|
|||||||
@@ -40,14 +40,18 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
|
|
||||||
metav1Now := metav1.Now()
|
metav1Now := metav1.Now()
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
repo string
|
repo string
|
||||||
org string
|
org string
|
||||||
fixed *int
|
fixed *int
|
||||||
max *int
|
max *int
|
||||||
min *int
|
min *int
|
||||||
sReplicas *int
|
sReplicas *int
|
||||||
sTime *metav1.Time
|
sTime *metav1.Time
|
||||||
workflowRuns string
|
|
||||||
|
workflowRuns string
|
||||||
|
workflowRuns_queued string
|
||||||
|
workflowRuns_in_progress string
|
||||||
|
|
||||||
workflowJobs map[int]string
|
workflowJobs map[int]string
|
||||||
want int
|
want int
|
||||||
err string
|
err string
|
||||||
@@ -55,87 +59,107 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
// Legacy functionality
|
// Legacy functionality
|
||||||
// 3 demanded, max at 3
|
// 3 demanded, max at 3
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
|
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
sReplicas: intPtr(3),
|
sReplicas: intPtr(3),
|
||||||
sTime: &metav1Now,
|
sTime: &metav1Now,
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
// 3 demanded, max at 2
|
// 3 demanded, max at 2
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(2),
|
max: intPtr(2),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 2 demanded, min at 2
|
// 2 demanded, min at 2
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 2
|
// 1 demanded, min at 2
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 2
|
// 1 demanded, min at 2
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 1
|
// 1 demanded, min at 1
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
||||||
want: 1,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
want: 1,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 1
|
// 1 demanded, min at 1
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 1,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 1,
|
||||||
},
|
},
|
||||||
// fixed at 3
|
// fixed at 3
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
fixed: intPtr(3),
|
fixed: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 3, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Job-level autoscaling
|
// Job-level autoscaling
|
||||||
// 5 requested from 3 workflows
|
// 5 requested from 3 workflows
|
||||||
{
|
{
|
||||||
repo: "test/valid",
|
repo: "test/valid",
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(10),
|
max: intPtr(10),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"id": 1, "status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 2, "workflow_runs":[{"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}]}"`,
|
||||||
workflowJobs: map[int]string{
|
workflowJobs: map[int]string{
|
||||||
1: `{"jobs": [{"status":"queued"}, {"status":"queued"}]}`,
|
1: `{"jobs": [{"status":"queued"}, {"status":"queued"}]}`,
|
||||||
2: `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}`,
|
2: `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}`,
|
||||||
@@ -158,7 +182,7 @@ func TestDetermineDesiredReplicas_RepositoryRunner(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||||
server := fake.NewServer(
|
server := fake.NewServer(
|
||||||
fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns),
|
fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns, tc.workflowRuns_queued, tc.workflowRuns_in_progress),
|
||||||
fake.WithListWorkflowJobsResponse(200, tc.workflowJobs),
|
fake.WithListWorkflowJobsResponse(200, tc.workflowJobs),
|
||||||
fake.WithListRunnersResponse(200, fake.RunnersListBody),
|
fake.WithListRunnersResponse(200, fake.RunnersListBody),
|
||||||
)
|
)
|
||||||
@@ -228,129 +252,157 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
|
|||||||
|
|
||||||
metav1Now := metav1.Now()
|
metav1Now := metav1.Now()
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
repos []string
|
repos []string
|
||||||
org string
|
org string
|
||||||
fixed *int
|
fixed *int
|
||||||
max *int
|
max *int
|
||||||
min *int
|
min *int
|
||||||
sReplicas *int
|
sReplicas *int
|
||||||
sTime *metav1.Time
|
sTime *metav1.Time
|
||||||
workflowRuns string
|
|
||||||
|
workflowRuns string
|
||||||
|
workflowRuns_queued string
|
||||||
|
workflowRuns_in_progress string
|
||||||
|
|
||||||
workflowJobs map[int]string
|
workflowJobs map[int]string
|
||||||
want int
|
want int
|
||||||
err string
|
err string
|
||||||
}{
|
}{
|
||||||
// 3 demanded, max at 3
|
// 3 demanded, max at 3
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
|
// 2 demanded, max at 3, currently 3, delay scaling down due to grace period
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
sReplicas: intPtr(3),
|
sReplicas: intPtr(3),
|
||||||
sTime: &metav1Now,
|
sTime: &metav1Now,
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
// 3 demanded, max at 2
|
// 3 demanded, max at 2
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(2),
|
max: intPtr(2),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 2 demanded, min at 2
|
// 2 demanded, min at 2
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 3, "workflow_runs":[{"status":"queued"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 2
|
// 1 demanded, min at 2
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 2
|
// 1 demanded, min at 2
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 2,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 2,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 1
|
// 1 demanded, min at 1
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"completed"}]}"`,
|
||||||
want: 1,
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
want: 1,
|
||||||
},
|
},
|
||||||
// 1 demanded, min at 1
|
// 1 demanded, min at 1
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 1,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
want: 1,
|
||||||
},
|
},
|
||||||
// fixed at 3
|
// fixed at 3
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
fixed: intPtr(1),
|
fixed: intPtr(1),
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 3, "workflow_runs":[{"status":"in_progress"},{"status":"in_progress"},{"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
// org runner, fixed at 3
|
// org runner, fixed at 3
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
fixed: intPtr(1),
|
fixed: intPtr(1),
|
||||||
min: intPtr(1),
|
min: intPtr(1),
|
||||||
max: intPtr(3),
|
max: intPtr(3),
|
||||||
workflowRuns: `{"total_count": 2, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"status":"in_progress"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
want: 3,
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 3, "workflow_runs":[{"status":"in_progress"},{"status":"in_progress"},{"status":"in_progress"}]}"`,
|
||||||
|
want: 3,
|
||||||
},
|
},
|
||||||
// org runner, 1 demanded, min at 1, no repos
|
// org runner, 1 demanded, min at 1, no repos
|
||||||
{
|
{
|
||||||
org: "test",
|
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"}]}"`,
|
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",
|
workflowRuns_queued: `{"total_count": 0, "workflow_runs":[]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`,
|
||||||
|
err: "validating autoscaling metrics: spec.autoscaling.metrics[].repositoryNames is required and must have one more more entries for organizational runner deployment",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Job-level autoscaling
|
// Job-level autoscaling
|
||||||
// 5 requested from 3 workflows
|
// 5 requested from 3 workflows
|
||||||
{
|
{
|
||||||
org: "test",
|
org: "test",
|
||||||
repos: []string{"valid"},
|
repos: []string{"valid"},
|
||||||
min: intPtr(2),
|
min: intPtr(2),
|
||||||
max: intPtr(10),
|
max: intPtr(10),
|
||||||
workflowRuns: `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
|
workflowRuns: `{"total_count": 4, "workflow_runs":[{"id": 1, "status":"queued"}, {"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
|
workflowRuns_queued: `{"total_count": 1, "workflow_runs":[{"id": 1, "status":"queued"}]}"`,
|
||||||
|
workflowRuns_in_progress: `{"total_count": 2, "workflow_runs":[{"id": 2, "status":"in_progress"}, {"id": 3, "status":"in_progress"}, {"status":"completed"}]}"`,
|
||||||
workflowJobs: map[int]string{
|
workflowJobs: map[int]string{
|
||||||
1: `{"jobs": [{"status":"queued"}, {"status":"queued"}]}`,
|
1: `{"jobs": [{"status":"queued"}, {"status":"queued"}]}`,
|
||||||
2: `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}`,
|
2: `{"jobs": [{"status": "in_progress"}, {"status":"completed"}]}`,
|
||||||
@@ -373,7 +425,7 @@ func TestDetermineDesiredReplicas_OrganizationalRunner(t *testing.T) {
|
|||||||
|
|
||||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||||
server := fake.NewServer(
|
server := fake.NewServer(
|
||||||
fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns),
|
fake.WithListRepositoryWorkflowRunsResponse(200, tc.workflowRuns, tc.workflowRuns_queued, tc.workflowRuns_in_progress),
|
||||||
fake.WithListWorkflowJobsResponse(200, tc.workflowJobs),
|
fake.WithListWorkflowJobsResponse(200, tc.workflowJobs),
|
||||||
fake.WithListRunnersResponse(200, fake.RunnersListBody),
|
fake.WithListRunnersResponse(200, fake.RunnersListBody),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
@@ -56,6 +57,7 @@ type HorizontalRunnerAutoscalerGitHubWebhook struct {
|
|||||||
// scaled on Webhook.
|
// scaled on Webhook.
|
||||||
// Set to empty for letting it watch for all namespaces.
|
// Set to empty for letting it watch for all namespaces.
|
||||||
WatchNamespace string
|
WatchNamespace string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
||||||
@@ -126,28 +128,38 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
|||||||
|
|
||||||
var target *ScaleTarget
|
var target *ScaleTarget
|
||||||
|
|
||||||
autoscaler.Log.Info("processing webhook event", "eventType", webhookType)
|
log := autoscaler.Log.WithValues(
|
||||||
|
"event", webhookType,
|
||||||
|
"hookID", r.Header.Get("X-GitHub-Hook-ID"),
|
||||||
|
"delivery", r.Header.Get("X-GitHub-Delivery"),
|
||||||
|
)
|
||||||
|
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *gogithub.PushEvent:
|
case *gogithub.PushEvent:
|
||||||
target, err = autoscaler.getScaleUpTarget(
|
target, err = autoscaler.getScaleUpTarget(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
*e.Repo.Name,
|
log,
|
||||||
*e.Repo.Organization,
|
e.Repo.GetName(),
|
||||||
|
e.Repo.Owner.GetLogin(),
|
||||||
|
e.Repo.Owner.GetType(),
|
||||||
autoscaler.MatchPushEvent(e),
|
autoscaler.MatchPushEvent(e),
|
||||||
)
|
)
|
||||||
case *gogithub.PullRequestEvent:
|
case *gogithub.PullRequestEvent:
|
||||||
target, err = autoscaler.getScaleUpTarget(
|
target, err = autoscaler.getScaleUpTarget(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
*e.Repo.Name,
|
log,
|
||||||
*e.Repo.Organization.Name,
|
e.Repo.GetName(),
|
||||||
|
e.Repo.Owner.GetLogin(),
|
||||||
|
e.Repo.Owner.GetType(),
|
||||||
autoscaler.MatchPullRequestEvent(e),
|
autoscaler.MatchPullRequestEvent(e),
|
||||||
)
|
)
|
||||||
case *gogithub.CheckRunEvent:
|
case *gogithub.CheckRunEvent:
|
||||||
target, err = autoscaler.getScaleUpTarget(
|
target, err = autoscaler.getScaleUpTarget(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
*e.Repo.Name,
|
log,
|
||||||
*e.Org.Name,
|
e.Repo.GetName(),
|
||||||
|
e.Repo.Owner.GetLogin(),
|
||||||
|
e.Repo.Owner.GetType(),
|
||||||
autoscaler.MatchCheckRunEvent(e),
|
autoscaler.MatchCheckRunEvent(e),
|
||||||
)
|
)
|
||||||
case *gogithub.PingEvent:
|
case *gogithub.PingEvent:
|
||||||
@@ -158,20 +170,20 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
|||||||
msg := "pong"
|
msg := "pong"
|
||||||
|
|
||||||
if written, err := w.Write([]byte(msg)); err != nil {
|
if written, err := w.Write([]byte(msg)); err != nil {
|
||||||
autoscaler.Log.Error(err, "failed writing http response", "msg", msg, "written", written)
|
log.Error(err, "failed writing http response", "msg", msg, "written", written)
|
||||||
}
|
}
|
||||||
|
|
||||||
autoscaler.Log.Info("received ping event")
|
log.Info("received ping event")
|
||||||
|
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
autoscaler.Log.Info("unknown event type", "eventType", webhookType)
|
log.Info("unknown event type", "eventType", webhookType)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
autoscaler.Log.Error(err, "handling check_run event")
|
log.Error(err, "handling check_run event")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,21 +191,21 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
|||||||
if target == nil {
|
if target == nil {
|
||||||
msg := "no horizontalrunnerautoscaler to scale for this github event"
|
msg := "no horizontalrunnerautoscaler to scale for this github event"
|
||||||
|
|
||||||
autoscaler.Log.Info(msg, "eventType", webhookType)
|
log.Info(msg, "eventType", webhookType)
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
if written, err := w.Write([]byte(msg)); err != nil {
|
if written, err := w.Write([]byte(msg)); err != nil {
|
||||||
autoscaler.Log.Error(err, "failed writing http response", "msg", msg, "written", written)
|
log.Error(err, "failed writing http response", "msg", msg, "written", written)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := autoscaler.tryScaleUp(context.TODO(), target); err != nil {
|
if err := autoscaler.tryScaleUp(context.TODO(), target); err != nil {
|
||||||
autoscaler.Log.Error(err, "could not scale up")
|
log.Error(err, "could not scale up")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -207,7 +219,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons
|
|||||||
autoscaler.Log.Info(msg)
|
autoscaler.Log.Info(msg)
|
||||||
|
|
||||||
if written, err := w.Write([]byte(msg)); err != nil {
|
if written, err := w.Write([]byte(msg)); err != nil {
|
||||||
autoscaler.Log.Error(err, "failed writing http response", "msg", msg, "written", written)
|
log.Error(err, "failed writing http response", "msg", msg, "written", written)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +238,10 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) findHRAsByKey(ctx con
|
|||||||
opts := append([]client.ListOption{}, defaultListOpts...)
|
opts := append([]client.ListOption{}, defaultListOpts...)
|
||||||
opts = append(opts, client.MatchingFields{scaleTargetKey: value})
|
opts = append(opts, client.MatchingFields{scaleTargetKey: value})
|
||||||
|
|
||||||
|
if autoscaler.WatchNamespace != "" {
|
||||||
|
opts = append(opts, client.InNamespace(autoscaler.WatchNamespace))
|
||||||
|
}
|
||||||
|
|
||||||
var hraList v1alpha1.HorizontalRunnerAutoscalerList
|
var hraList v1alpha1.HorizontalRunnerAutoscalerList
|
||||||
|
|
||||||
if err := autoscaler.List(ctx, &hraList, opts...); err != nil {
|
if err := autoscaler.List(ctx, &hraList, opts...); err != nil {
|
||||||
@@ -294,28 +310,59 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleTarget(ctx co
|
|||||||
|
|
||||||
targets := autoscaler.searchScaleTargets(hras, f)
|
targets := autoscaler.searchScaleTargets(hras, f)
|
||||||
|
|
||||||
if len(targets) != 1 {
|
n := len(targets)
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 1 {
|
||||||
|
var scaleTargetIDs []string
|
||||||
|
|
||||||
|
for _, t := range targets {
|
||||||
|
scaleTargetIDs = append(scaleTargetIDs, t.HorizontalRunnerAutoscaler.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
autoscaler.Log.Info(
|
||||||
|
"Found too many scale targets: "+
|
||||||
|
"It must be exactly one to avoid ambiguity. "+
|
||||||
|
"Either set WatchNamespace for the webhook-based autoscaler to let it only find HRAs in the namespace, "+
|
||||||
|
"or update Repository or Organization fields in your RunnerDeployment resources to fix the ambiguity.",
|
||||||
|
"scaleTargets", strings.Join(scaleTargetIDs, ","))
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &targets[0], nil
|
return &targets[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, repoNameFromWebhook, orgNameFromWebhook string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, log logr.Logger, repo, owner, ownerType string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) {
|
||||||
if target, err := autoscaler.getScaleTarget(ctx, repoNameFromWebhook, f); err != nil {
|
repositoryRunnerKey := owner + "/" + repo
|
||||||
|
|
||||||
|
if target, err := autoscaler.getScaleTarget(ctx, repositoryRunnerKey, f); err != nil {
|
||||||
|
autoscaler.Log.Info("finding repository-wide runner", "repository", repositoryRunnerKey)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if target != nil {
|
} else if target != nil {
|
||||||
autoscaler.Log.Info("scale up target is repository-wide runners", "repository", repoNameFromWebhook)
|
autoscaler.Log.Info("scale up target is repository-wide runners", "repository", repo)
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if target, err := autoscaler.getScaleTarget(ctx, orgNameFromWebhook, f); err != nil {
|
if ownerType == "User" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if target, err := autoscaler.getScaleTarget(ctx, owner, f); err != nil {
|
||||||
|
log.Info("finding organizational runner", "organization", owner)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if target != nil {
|
} else if target != nil {
|
||||||
autoscaler.Log.Info("scale up target is organizational runners", "repository", orgNameFromWebhook)
|
log.Info("scale up target is organizational runners", "organization", owner)
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info(
|
||||||
|
"Scale target not found. If this is unexpected, ensure that there is exactly one repository-wide or organizational runner deployment that matches this webhook event",
|
||||||
|
)
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +381,9 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScaleUp(ctx contex
|
|||||||
amount = target.ScaleUpTrigger.Amount
|
amount = target.ScaleUpTrigger.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
copy.Spec.CapacityReservations = append(copy.Spec.CapacityReservations, v1alpha1.CapacityReservation{
|
capacityReservations := getValidCapacityReservations(copy)
|
||||||
|
|
||||||
|
copy.Spec.CapacityReservations = append(capacityReservations, v1alpha1.CapacityReservation{
|
||||||
ExpirationTime: metav1.Time{Time: time.Now().Add(target.ScaleUpTrigger.Duration.Duration)},
|
ExpirationTime: metav1.Time{Time: time.Now().Add(target.ScaleUpTrigger.Duration.Duration)},
|
||||||
Replicas: amount,
|
Replicas: amount,
|
||||||
})
|
})
|
||||||
@@ -348,8 +397,27 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScaleUp(ctx contex
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getValidCapacityReservations(autoscaler *v1alpha1.HorizontalRunnerAutoscaler) []v1alpha1.CapacityReservation {
|
||||||
|
var capacityReservations []v1alpha1.CapacityReservation
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for _, reservation := range autoscaler.Spec.CapacityReservations {
|
||||||
|
if reservation.ExpirationTime.Time.After(now) {
|
||||||
|
capacityReservations = append(capacityReservations, reservation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return capacityReservations
|
||||||
|
}
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr ctrl.Manager) error {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
autoscaler.Recorder = mgr.GetEventRecorderFor("webhookbasedautoscaler")
|
name := "webhookbasedautoscaler"
|
||||||
|
if autoscaler.Name != "" {
|
||||||
|
name = autoscaler.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
autoscaler.Recorder = mgr.GetEventRecorderFor(name)
|
||||||
|
|
||||||
if err := mgr.GetFieldIndexer().IndexField(&v1alpha1.HorizontalRunnerAutoscaler{}, scaleTargetKey, func(rawObj runtime.Object) []string {
|
if err := mgr.GetFieldIndexer().IndexField(&v1alpha1.HorizontalRunnerAutoscaler{}, scaleTargetKey, func(rawObj runtime.Object) []string {
|
||||||
hra := rawObj.(*v1alpha1.HorizontalRunnerAutoscaler)
|
hra := rawObj.(*v1alpha1.HorizontalRunnerAutoscaler)
|
||||||
@@ -371,5 +439,6 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr
|
|||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&v1alpha1.HorizontalRunnerAutoscaler{}).
|
For(&v1alpha1.HorizontalRunnerAutoscaler{}).
|
||||||
|
Named(name).
|
||||||
Complete(autoscaler)
|
Complete(autoscaler)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import (
|
|||||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -27,21 +30,37 @@ func init() {
|
|||||||
_ = actionsv1alpha1.AddToScheme(sc)
|
_ = actionsv1alpha1.AddToScheme(sc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhookCheckRun(t *testing.T) {
|
func TestOrgWebhookCheckRun(t *testing.T) {
|
||||||
|
f, err := os.Open("testdata/org_webhook_check_run_payload.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not open the fixture: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
var e github.CheckRunEvent
|
||||||
|
if err := json.NewDecoder(f).Decode(&e); err != nil {
|
||||||
|
t.Fatalf("invalid json: %s", err)
|
||||||
|
}
|
||||||
testServer(t,
|
testServer(t,
|
||||||
"check_run",
|
"check_run",
|
||||||
&github.CheckRunEvent{
|
&e,
|
||||||
CheckRun: &github.CheckRun{
|
200,
|
||||||
Status: github.String("queued"),
|
"no horizontalrunnerautoscaler to scale for this github event",
|
||||||
},
|
)
|
||||||
Repo: &github.Repository{
|
}
|
||||||
Name: github.String("myorg/myrepo"),
|
|
||||||
},
|
func TestRepoWebhookCheckRun(t *testing.T) {
|
||||||
Org: &github.Organization{
|
f, err := os.Open("testdata/repo_webhook_check_run_payload.json")
|
||||||
Name: github.String("myorg"),
|
if err != nil {
|
||||||
},
|
t.Fatalf("could not open the fixture: %s", err)
|
||||||
Action: github.String("created"),
|
}
|
||||||
},
|
defer f.Close()
|
||||||
|
var e github.CheckRunEvent
|
||||||
|
if err := json.NewDecoder(f).Decode(&e); err != nil {
|
||||||
|
t.Fatalf("invalid json: %s", err)
|
||||||
|
}
|
||||||
|
testServer(t,
|
||||||
|
"check_run",
|
||||||
|
&e,
|
||||||
200,
|
200,
|
||||||
"no horizontalrunnerautoscaler to scale for this github event",
|
"no horizontalrunnerautoscaler to scale for this github event",
|
||||||
)
|
)
|
||||||
@@ -94,6 +113,43 @@ func TestWebhookPing(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetValidCapacityReservations(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
||||||
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
||||||
|
CapacityReservations: []actionsv1alpha1.CapacityReservation{
|
||||||
|
{
|
||||||
|
ExpirationTime: metav1.Time{Time: now.Add(-time.Second)},
|
||||||
|
Replicas: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExpirationTime: metav1.Time{Time: now},
|
||||||
|
Replicas: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExpirationTime: metav1.Time{Time: now.Add(time.Second)},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
revs := getValidCapacityReservations(hra)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
|
||||||
|
for _, r := range revs {
|
||||||
|
count += r.Replicas
|
||||||
|
}
|
||||||
|
|
||||||
|
want := 3
|
||||||
|
|
||||||
|
if count != want {
|
||||||
|
t.Errorf("want %d, got %d", want, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func installTestLogger(webhook *HorizontalRunnerAutoscalerGitHubWebhook) *bytes.Buffer {
|
func installTestLogger(webhook *HorizontalRunnerAutoscalerGitHubWebhook) *bytes.Buffer {
|
||||||
logs := &bytes.Buffer{}
|
logs := &bytes.Buffer{}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type HorizontalRunnerAutoscalerReconciler struct {
|
|||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
|
|
||||||
CacheDuration time.Duration
|
CacheDuration time.Duration
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments,verbs=get;list;watch;update;patch
|
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerdeployments,verbs=get;list;watch;update;patch
|
||||||
@@ -131,16 +132,16 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl
|
|||||||
|
|
||||||
var updated *v1alpha1.HorizontalRunnerAutoscaler
|
var updated *v1alpha1.HorizontalRunnerAutoscaler
|
||||||
|
|
||||||
if hra.Status.DesiredReplicas == nil || *hra.Status.DesiredReplicas != *replicas {
|
if hra.Status.DesiredReplicas == nil || *hra.Status.DesiredReplicas != newDesiredReplicas {
|
||||||
updated = hra.DeepCopy()
|
updated = hra.DeepCopy()
|
||||||
|
|
||||||
if (hra.Status.DesiredReplicas == nil && *replicas > 1) ||
|
if (hra.Status.DesiredReplicas == nil && newDesiredReplicas > 1) ||
|
||||||
(hra.Status.DesiredReplicas != nil && *replicas > *hra.Status.DesiredReplicas) {
|
(hra.Status.DesiredReplicas != nil && newDesiredReplicas > *hra.Status.DesiredReplicas) {
|
||||||
|
|
||||||
updated.Status.LastSuccessfulScaleOutTime = &metav1.Time{Time: time.Now()}
|
updated.Status.LastSuccessfulScaleOutTime = &metav1.Time{Time: time.Now()}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated.Status.DesiredReplicas = replicas
|
updated.Status.DesiredReplicas = &newDesiredReplicas
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicasFromCache == nil {
|
if replicasFromCache == nil {
|
||||||
@@ -164,7 +165,7 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl
|
|||||||
cacheDuration = 10 * time.Minute
|
cacheDuration = 10 * time.Minute
|
||||||
}
|
}
|
||||||
|
|
||||||
updated.Status.CacheEntries = append(updated.Status.CacheEntries, v1alpha1.CacheEntry{
|
updated.Status.CacheEntries = append(cacheEntries, v1alpha1.CacheEntry{
|
||||||
Key: v1alpha1.CacheEntryKeyDesiredReplicas,
|
Key: v1alpha1.CacheEntryKeyDesiredReplicas,
|
||||||
Value: *replicas,
|
Value: *replicas,
|
||||||
ExpirationTime: metav1.Time{Time: time.Now().Add(cacheDuration)},
|
ExpirationTime: metav1.Time{Time: time.Now().Add(cacheDuration)},
|
||||||
@@ -183,10 +184,16 @@ func (r *HorizontalRunnerAutoscalerReconciler) Reconcile(req ctrl.Request) (ctrl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *HorizontalRunnerAutoscalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *HorizontalRunnerAutoscalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
r.Recorder = mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller")
|
name := "horizontalrunnerautoscaler-controller"
|
||||||
|
if r.Name != "" {
|
||||||
|
name = r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&v1alpha1.HorizontalRunnerAutoscaler{}).
|
For(&v1alpha1.HorizontalRunnerAutoscaler{}).
|
||||||
|
Named(name).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/google/go-github/v33/github"
|
"github.com/google/go-github/v33/github"
|
||||||
github3 "github.com/google/go-github/v33/github"
|
|
||||||
github2 "github.com/summerwind/actions-runner-controller/github"
|
github2 "github.com/summerwind/actions-runner-controller/github"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,19 +29,22 @@ import (
|
|||||||
type testEnvironment struct {
|
type testEnvironment struct {
|
||||||
Namespace *corev1.Namespace
|
Namespace *corev1.Namespace
|
||||||
Responses *fake.FixedResponses
|
Responses *fake.FixedResponses
|
||||||
|
|
||||||
|
webhookServer *httptest.Server
|
||||||
|
ghClient *github2.Client
|
||||||
|
fakeRunnerList *fake.RunnersList
|
||||||
|
fakeGithubServer *httptest.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
workflowRunsFor3Replicas = `{"total_count": 5, "workflow_runs":[{"status":"queued"}, {"status":"queued"}, {"status":"in_progress"}, {"status":"in_progress"}, {"status":"completed"}]}"`
|
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"}]}"`
|
workflowRunsFor3Replicas_queued = `{"total_count": 2, "workflow_runs":[{"status":"queued"}, {"status":"queued"}]}"`
|
||||||
|
workflowRunsFor3Replicas_in_progress = `{"total_count": 1, "workflow_runs":[{"status":"in_progress"}]}"`
|
||||||
|
workflowRunsFor1Replicas = `{"total_count": 6, "workflow_runs":[{"status":"queued"}, {"status":"completed"}, {"status":"completed"}, {"status":"completed"}, {"status":"completed"}]}"`
|
||||||
|
workflowRunsFor1Replicas_queued = `{"total_count": 1, "workflow_runs":[{"status":"queued"}]}"`
|
||||||
|
workflowRunsFor1Replicas_in_progress = `{"total_count": 0, "workflow_runs":[]}"`
|
||||||
)
|
)
|
||||||
|
|
||||||
var webhookServer *httptest.Server
|
|
||||||
|
|
||||||
var ghClient *github2.Client
|
|
||||||
|
|
||||||
var fakeRunnerList *fake.RunnersList
|
|
||||||
|
|
||||||
// SetupIntegrationTest will set up a testing environment.
|
// SetupIntegrationTest will set up a testing environment.
|
||||||
// This includes:
|
// This includes:
|
||||||
// * creating a Namespace to be used during the test
|
// * creating a Namespace to be used during the test
|
||||||
@@ -51,15 +55,11 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
|||||||
var stopCh chan struct{}
|
var stopCh chan struct{}
|
||||||
ns := &corev1.Namespace{}
|
ns := &corev1.Namespace{}
|
||||||
|
|
||||||
responses := &fake.FixedResponses{}
|
env := &testEnvironment{
|
||||||
responses.ListRunners = fake.DefaultListRunnersHandler()
|
Namespace: ns,
|
||||||
responses.ListRepositoryWorkflowRuns = &fake.Handler{
|
webhookServer: nil,
|
||||||
Status: 200,
|
ghClient: nil,
|
||||||
Body: workflowRunsFor3Replicas,
|
|
||||||
}
|
}
|
||||||
fakeRunnerList = fake.NewRunnersList()
|
|
||||||
responses.ListRunners = fakeRunnerList.HandleList()
|
|
||||||
fakeGithubServer := fake.NewServer(fake.WithFixedResponses(responses))
|
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
stopCh = make(chan struct{})
|
stopCh = make(chan struct{})
|
||||||
@@ -73,14 +73,36 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
|||||||
mgr, err := ctrl.NewManager(cfg, ctrl.Options{})
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{})
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
||||||
|
|
||||||
ghClient = newGithubClient(fakeGithubServer)
|
responses := &fake.FixedResponses{}
|
||||||
|
responses.ListRunners = fake.DefaultListRunnersHandler()
|
||||||
|
responses.ListRepositoryWorkflowRuns = &fake.Handler{
|
||||||
|
Status: 200,
|
||||||
|
Body: workflowRunsFor3Replicas,
|
||||||
|
Statuses: map[string]string{
|
||||||
|
"queued": workflowRunsFor3Replicas_queued,
|
||||||
|
"in_progress": workflowRunsFor3Replicas_in_progress,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fakeRunnerList := fake.NewRunnersList()
|
||||||
|
responses.ListRunners = fakeRunnerList.HandleList()
|
||||||
|
fakeGithubServer := fake.NewServer(fake.WithFixedResponses(responses))
|
||||||
|
|
||||||
|
env.Responses = responses
|
||||||
|
env.fakeRunnerList = fakeRunnerList
|
||||||
|
env.fakeGithubServer = fakeGithubServer
|
||||||
|
env.ghClient = newGithubClient(fakeGithubServer)
|
||||||
|
|
||||||
|
controllerName := func(name string) string {
|
||||||
|
return fmt.Sprintf("%s%s", ns.Name, name)
|
||||||
|
}
|
||||||
|
|
||||||
replicasetController := &RunnerReplicaSetReconciler{
|
replicasetController := &RunnerReplicaSetReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: scheme.Scheme,
|
Scheme: scheme.Scheme,
|
||||||
Log: logf.Log,
|
Log: logf.Log,
|
||||||
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
||||||
GitHubClient: ghClient,
|
GitHubClient: env.ghClient,
|
||||||
|
Name: controllerName("runnerreplicaset"),
|
||||||
}
|
}
|
||||||
err = replicasetController.SetupWithManager(mgr)
|
err = replicasetController.SetupWithManager(mgr)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
@@ -90,28 +112,30 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
|||||||
Scheme: scheme.Scheme,
|
Scheme: scheme.Scheme,
|
||||||
Log: logf.Log,
|
Log: logf.Log,
|
||||||
Recorder: mgr.GetEventRecorderFor("runnerdeployment-controller"),
|
Recorder: mgr.GetEventRecorderFor("runnerdeployment-controller"),
|
||||||
|
Name: controllerName("runnnerdeployment"),
|
||||||
}
|
}
|
||||||
err = deploymentsController.SetupWithManager(mgr)
|
err = deploymentsController.SetupWithManager(mgr)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
client := newGithubClient(fakeGithubServer)
|
|
||||||
|
|
||||||
autoscalerController := &HorizontalRunnerAutoscalerReconciler{
|
autoscalerController := &HorizontalRunnerAutoscalerReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: scheme.Scheme,
|
Scheme: scheme.Scheme,
|
||||||
Log: logf.Log,
|
Log: logf.Log,
|
||||||
GitHubClient: client,
|
GitHubClient: env.ghClient,
|
||||||
Recorder: mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller"),
|
Recorder: mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller"),
|
||||||
CacheDuration: 1 * time.Second,
|
CacheDuration: 1 * time.Second,
|
||||||
|
Name: controllerName("horizontalrunnerautoscaler"),
|
||||||
}
|
}
|
||||||
err = autoscalerController.SetupWithManager(mgr)
|
err = autoscalerController.SetupWithManager(mgr)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
autoscalerWebhook := &HorizontalRunnerAutoscalerGitHubWebhook{
|
autoscalerWebhook := &HorizontalRunnerAutoscalerGitHubWebhook{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: scheme.Scheme,
|
Scheme: scheme.Scheme,
|
||||||
Log: logf.Log,
|
Log: logf.Log,
|
||||||
Recorder: mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller"),
|
Recorder: mgr.GetEventRecorderFor("horizontalrunnerautoscaler-controller"),
|
||||||
|
Name: controllerName("horizontalrunnerautoscalergithubwebhook"),
|
||||||
|
WatchNamespace: ns.Name,
|
||||||
}
|
}
|
||||||
err = autoscalerWebhook.SetupWithManager(mgr)
|
err = autoscalerWebhook.SetupWithManager(mgr)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to setup autoscaler webhook")
|
Expect(err).NotTo(HaveOccurred(), "failed to setup autoscaler webhook")
|
||||||
@@ -119,7 +143,7 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", autoscalerWebhook.Handle)
|
mux.HandleFunc("/", autoscalerWebhook.Handle)
|
||||||
|
|
||||||
webhookServer = httptest.NewServer(mux)
|
env.webhookServer = httptest.NewServer(mux)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer GinkgoRecover()
|
defer GinkgoRecover()
|
||||||
@@ -132,29 +156,28 @@ func SetupIntegrationTest(ctx context.Context) *testEnvironment {
|
|||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
close(stopCh)
|
close(stopCh)
|
||||||
|
|
||||||
fakeGithubServer.Close()
|
env.fakeGithubServer.Close()
|
||||||
webhookServer.Close()
|
env.webhookServer.Close()
|
||||||
|
|
||||||
err := k8sClient.Delete(ctx, ns)
|
err := k8sClient.Delete(ctx, ns)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace")
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace")
|
||||||
})
|
})
|
||||||
|
|
||||||
return &testEnvironment{Namespace: ns, Responses: responses}
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
env := SetupIntegrationTest(ctx)
|
env := SetupIntegrationTest(ctx)
|
||||||
ns := env.Namespace
|
ns := env.Namespace
|
||||||
responses := env.Responses
|
|
||||||
|
|
||||||
Describe("when no existing resources exist", func() {
|
Describe("when no existing resources exist", func() {
|
||||||
|
|
||||||
It("should create and scale runners", func() {
|
It("should create and scale organization's repository runners on pull_request event", func() {
|
||||||
name := "example-runnerdeploy"
|
name := "example-runnerdeploy"
|
||||||
|
|
||||||
{
|
{
|
||||||
rs := &actionsv1alpha1.RunnerDeployment{
|
rd := &actionsv1alpha1.RunnerDeployment{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
Namespace: ns.Name,
|
Namespace: ns.Name,
|
||||||
@@ -174,80 +197,17 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := k8sClient.Create(ctx, rs)
|
ExpectCreate(ctx, rd, "test RunnerDeployment")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to create test RunnerDeployment resource")
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
|
||||||
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
|
ExpectRunnerDeploymentEventuallyUpdates(ctx, ns.Name, name, func(rd *actionsv1alpha1.RunnerDeployment) {
|
||||||
// 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)
|
rd.Spec.Replicas = intPtr(2)
|
||||||
|
})
|
||||||
return k8sClient.Update(ctx, &rd)
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
},
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 2)
|
||||||
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
|
// Scale-up to 3 replicas
|
||||||
@@ -280,68 +240,23 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := k8sClient.Create(ctx, hra)
|
ExpectCreate(ctx, hra, "test HorizontalRunnerAutoscaler")
|
||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to create test HorizontalRunnerAutoscaler resource")
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var runnerList actionsv1alpha1.RunnerList
|
env.ExpectRegisteredNumberCountEventuallyEquals(3, "count of fake runners after HRA creation")
|
||||||
|
|
||||||
err := k8sClient.List(ctx, &runnerList, client.InNamespace(ns.Name))
|
|
||||||
if err != nil {
|
|
||||||
logf.Log.Error(err, "list runners")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, r := range runnerList.Items {
|
|
||||||
fakeRunnerList.Add(&github3.Runner{
|
|
||||||
ID: github.Int64(int64(i)),
|
|
||||||
Name: github.String(r.Name),
|
|
||||||
OS: github.String("linux"),
|
|
||||||
Status: github.String("online"),
|
|
||||||
Busy: github.Bool(false),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
rs, err := ghClient.ListRunners(context.Background(), "", "", "test/valid")
|
|
||||||
Expect(err).NotTo(HaveOccurred(), "verifying list fake runners response")
|
|
||||||
Expect(len(rs)).To(Equal(3), "count of fake list runners")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale-down to 1 replica
|
// Scale-down to 1 replica
|
||||||
{
|
{
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
responses.ListRepositoryWorkflowRuns.Body = workflowRunsFor1Replicas
|
env.Responses.ListRepositoryWorkflowRuns.Body = workflowRunsFor1Replicas
|
||||||
|
env.Responses.ListRepositoryWorkflowRuns.Statuses["queued"] = workflowRunsFor1Replicas_queued
|
||||||
|
env.Responses.ListRepositoryWorkflowRuns.Statuses["in_progress"] = workflowRunsFor1Replicas_in_progress
|
||||||
|
|
||||||
var hra actionsv1alpha1.HorizontalRunnerAutoscaler
|
var hra actionsv1alpha1.HorizontalRunnerAutoscaler
|
||||||
|
|
||||||
@@ -357,77 +272,523 @@ var _ = Context("INTEGRATION: Inside of a new namespace", func() {
|
|||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource")
|
Expect(err).NotTo(HaveOccurred(), "failed to get test HorizontalRunnerAutoscaler resource")
|
||||||
|
|
||||||
Eventually(
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1, "runners after HRA force update for scale-down")
|
||||||
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), "runners after HRA force update for scale-down")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scale-up to 2 replicas on first pull_request create webhook event
|
||||||
{
|
{
|
||||||
resp, err := sendWebhook(webhookServer, "pull_request", &github.PullRequestEvent{
|
env.SendOrgPullRequestEvent("test", "valid", "main", "created")
|
||||||
PullRequest: &github.PullRequest{
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook")
|
||||||
Base: &github.PullRequestBranch{
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 2, "runners after first webhook event")
|
||||||
Ref: github.String("main"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Repo: &github.Repository{
|
|
||||||
Name: github.String("test/valid"),
|
|
||||||
Organization: &github.Organization{
|
|
||||||
Name: github.String("test"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: github.String("created"),
|
|
||||||
})
|
|
||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to send pull_request event")
|
|
||||||
|
|
||||||
Expect(resp.StatusCode).To(Equal(200))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale-up to 2 replicas
|
// Scale-up to 3 replicas on second pull_request create webhook event
|
||||||
{
|
{
|
||||||
runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}}
|
env.SendOrgPullRequestEvent("test", "valid", "main", "created")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3, "runners after second webhook event")
|
||||||
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), "runner sets after webhook")
|
|
||||||
|
|
||||||
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(2), "runners after webhook")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("should create and scale organization's repository runners on check_run event", func() {
|
||||||
|
name := "example-runnerdeploy"
|
||||||
|
|
||||||
|
{
|
||||||
|
rd := &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",
|
||||||
|
Group: "baz",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{Name: "FOO", Value: "FOOVALUE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectCreate(ctx, rd, "test RunnerDeployment")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(1, "count of fake list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 3 replicas by the default TotalNumberOfQueuedAndInProgressWorkflowRuns-based scaling
|
||||||
|
// See workflowRunsFor3Replicas_queued and workflowRunsFor3Replicas_in_progress for GitHub List-Runners API responses
|
||||||
|
// used while testing.
|
||||||
|
{
|
||||||
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
||||||
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
MinReplicas: intPtr(1),
|
||||||
|
MaxReplicas: intPtr(5),
|
||||||
|
ScaleDownDelaySecondsAfterScaleUp: intPtr(1),
|
||||||
|
Metrics: nil,
|
||||||
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
||||||
|
{
|
||||||
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
||||||
|
CheckRun: &actionsv1alpha1.CheckRunSpec{
|
||||||
|
Types: []string{"created"},
|
||||||
|
Status: "pending",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Amount: 1,
|
||||||
|
Duration: metav1.Duration{Duration: time.Minute},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectCreate(ctx, hra, "test HorizontalRunnerAutoscaler")
|
||||||
|
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(3, "count of fake list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 4 replicas on first check_run create webhook event
|
||||||
|
{
|
||||||
|
env.SendOrgCheckRunEvent("test", "valid", "pending", "created")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 4, "runners after first webhook event")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(4, "count of fake list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 5 replicas on second check_run create webhook event
|
||||||
|
{
|
||||||
|
env.SendOrgCheckRunEvent("test", "valid", "pending", "created")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 5, "runners after second webhook event")
|
||||||
|
}
|
||||||
|
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(5, "count of fake list runners")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should create and scale user's repository runners on pull_request event", func() {
|
||||||
|
name := "example-runnerdeploy"
|
||||||
|
|
||||||
|
{
|
||||||
|
rd := &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",
|
||||||
|
Group: "baz",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{Name: "FOO", Value: "FOOVALUE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectCreate(ctx, rd, "test RunnerDeployment")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ExpectRunnerDeploymentEventuallyUpdates(ctx, ns.Name, name, func(rd *actionsv1alpha1.RunnerDeployment) {
|
||||||
|
rd.Spec.Replicas = intPtr(2)
|
||||||
|
})
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 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: intPtr(1),
|
||||||
|
Metrics: nil,
|
||||||
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
||||||
|
{
|
||||||
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
||||||
|
PullRequest: &actionsv1alpha1.PullRequestSpec{
|
||||||
|
Types: []string{"created"},
|
||||||
|
Branches: []string{"main"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Amount: 1,
|
||||||
|
Duration: metav1.Duration{Duration: time.Minute},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectCreate(ctx, hra, "test HorizontalRunnerAutoscaler")
|
||||||
|
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(3, "count of fake runners after HRA creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-down to 1 replica
|
||||||
|
{
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
env.Responses.ListRepositoryWorkflowRuns.Body = workflowRunsFor1Replicas
|
||||||
|
env.Responses.ListRepositoryWorkflowRuns.Statuses["queued"] = workflowRunsFor1Replicas_queued
|
||||||
|
env.Responses.ListRepositoryWorkflowRuns.Statuses["in_progress"] = workflowRunsFor1Replicas_in_progress
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1, "runners after HRA force update for scale-down")
|
||||||
|
ExpectHRADesiredReplicasEquals(ctx, ns.Name, name, 1, "runner deployment desired replicas")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 2 replicas on first pull_request create webhook event
|
||||||
|
{
|
||||||
|
env.SendUserPullRequestEvent("test", "valid", "main", "created")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 2, "runners after first webhook event")
|
||||||
|
ExpectHRADesiredReplicasEquals(ctx, ns.Name, name, 2, "runner deployment desired replicas")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 3 replicas on second pull_request create webhook event
|
||||||
|
{
|
||||||
|
env.SendUserPullRequestEvent("test", "valid", "main", "created")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3, "runners after second webhook event")
|
||||||
|
ExpectHRADesiredReplicasEquals(ctx, ns.Name, name, 3, "runner deployment desired replicas")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should create and scale user's repository runners on check_run event", func() {
|
||||||
|
name := "example-runnerdeploy"
|
||||||
|
|
||||||
|
{
|
||||||
|
rd := &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",
|
||||||
|
Group: "baz",
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{Name: "FOO", Value: "FOOVALUE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectCreate(ctx, rd, "test RunnerDeployment")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(1, "count of fake list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 3 replicas by the default TotalNumberOfQueuedAndInProgressWorkflowRuns-based scaling
|
||||||
|
// See workflowRunsFor3Replicas_queued and workflowRunsFor3Replicas_in_progress for GitHub List-Runners API responses
|
||||||
|
// used while testing.
|
||||||
|
{
|
||||||
|
hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
|
||||||
|
ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
MinReplicas: intPtr(1),
|
||||||
|
MaxReplicas: intPtr(5),
|
||||||
|
ScaleDownDelaySecondsAfterScaleUp: intPtr(1),
|
||||||
|
Metrics: nil,
|
||||||
|
ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
|
||||||
|
{
|
||||||
|
GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
|
||||||
|
CheckRun: &actionsv1alpha1.CheckRunSpec{
|
||||||
|
Types: []string{"created"},
|
||||||
|
Status: "pending",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Amount: 1,
|
||||||
|
Duration: metav1.Duration{Duration: time.Minute},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectCreate(ctx, hra, "test HorizontalRunnerAutoscaler")
|
||||||
|
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1)
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(3, "count of fake list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 4 replicas on first check_run create webhook event
|
||||||
|
{
|
||||||
|
env.SendUserCheckRunEvent("test", "valid", "pending", "created")
|
||||||
|
ExpectRunnerSetsCountEventuallyEquals(ctx, ns.Name, 1, "runner sets after webhook")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 4, "runners after first webhook event")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(4, "count of fake list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-up to 5 replicas on second check_run create webhook event
|
||||||
|
{
|
||||||
|
env.SendUserCheckRunEvent("test", "valid", "pending", "created")
|
||||||
|
ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx, ns.Name, 5, "runners after second webhook event")
|
||||||
|
}
|
||||||
|
|
||||||
|
env.ExpectRegisteredNumberCountEventuallyEquals(5, "count of fake list runners")
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func ExpectHRADesiredReplicasEquals(ctx context.Context, ns, name string, desired int, optionalDescriptions ...interface{}) {
|
||||||
|
var rd actionsv1alpha1.HorizontalRunnerAutoscaler
|
||||||
|
|
||||||
|
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, &rd)
|
||||||
|
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to get test HRA resource")
|
||||||
|
|
||||||
|
replicas := rd.Status.DesiredReplicas
|
||||||
|
|
||||||
|
ExpectWithOffset(1, *replicas).To(Equal(desired), optionalDescriptions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnvironment) ExpectRegisteredNumberCountEventuallyEquals(want int, optionalDescriptions ...interface{}) {
|
||||||
|
EventuallyWithOffset(
|
||||||
|
1,
|
||||||
|
func() int {
|
||||||
|
env.SyncRunnerRegistrations()
|
||||||
|
|
||||||
|
rs, err := env.ghClient.ListRunners(context.Background(), "", "", "test/valid")
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "verifying list fake runners response")
|
||||||
|
|
||||||
|
return len(rs)
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(Equal(want), optionalDescriptions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnvironment) SendOrgPullRequestEvent(org, repo, branch, action string) {
|
||||||
|
resp, err := sendWebhook(env.webhookServer, "pull_request", &github.PullRequestEvent{
|
||||||
|
PullRequest: &github.PullRequest{
|
||||||
|
Base: &github.PullRequestBranch{
|
||||||
|
Ref: github.String(branch),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Repo: &github.Repository{
|
||||||
|
Name: github.String(repo),
|
||||||
|
Owner: &github.User{
|
||||||
|
Login: github.String(org),
|
||||||
|
Type: github.String("Organization"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: github.String(action),
|
||||||
|
})
|
||||||
|
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to send pull_request event")
|
||||||
|
|
||||||
|
ExpectWithOffset(1, resp.StatusCode).To(Equal(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnvironment) SendOrgCheckRunEvent(org, repo, status, action string) {
|
||||||
|
resp, err := sendWebhook(env.webhookServer, "check_run", &github.CheckRunEvent{
|
||||||
|
CheckRun: &github.CheckRun{
|
||||||
|
Status: github.String(status),
|
||||||
|
},
|
||||||
|
Org: &github.Organization{
|
||||||
|
Login: github.String(org),
|
||||||
|
},
|
||||||
|
Repo: &github.Repository{
|
||||||
|
Name: github.String(repo),
|
||||||
|
Owner: &github.User{
|
||||||
|
Login: github.String(org),
|
||||||
|
Type: github.String("Organization"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: github.String(action),
|
||||||
|
})
|
||||||
|
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to send check_run event")
|
||||||
|
|
||||||
|
ExpectWithOffset(1, resp.StatusCode).To(Equal(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnvironment) SendUserPullRequestEvent(owner, repo, branch, action string) {
|
||||||
|
resp, err := sendWebhook(env.webhookServer, "pull_request", &github.PullRequestEvent{
|
||||||
|
PullRequest: &github.PullRequest{
|
||||||
|
Base: &github.PullRequestBranch{
|
||||||
|
Ref: github.String(branch),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Repo: &github.Repository{
|
||||||
|
Name: github.String(repo),
|
||||||
|
Owner: &github.User{
|
||||||
|
Login: github.String(owner),
|
||||||
|
Type: github.String("User"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: github.String(action),
|
||||||
|
})
|
||||||
|
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to send pull_request event")
|
||||||
|
|
||||||
|
ExpectWithOffset(1, resp.StatusCode).To(Equal(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnvironment) SendUserCheckRunEvent(owner, repo, status, action string) {
|
||||||
|
resp, err := sendWebhook(env.webhookServer, "check_run", &github.CheckRunEvent{
|
||||||
|
CheckRun: &github.CheckRun{
|
||||||
|
Status: github.String(status),
|
||||||
|
},
|
||||||
|
Repo: &github.Repository{
|
||||||
|
Name: github.String(repo),
|
||||||
|
Owner: &github.User{
|
||||||
|
Login: github.String(owner),
|
||||||
|
Type: github.String("User"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: github.String(action),
|
||||||
|
})
|
||||||
|
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to send check_run event")
|
||||||
|
|
||||||
|
ExpectWithOffset(1, resp.StatusCode).To(Equal(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (env *testEnvironment) SyncRunnerRegistrations() {
|
||||||
|
var runnerList actionsv1alpha1.RunnerList
|
||||||
|
|
||||||
|
err := k8sClient.List(context.TODO(), &runnerList, client.InNamespace(env.Namespace.Name))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runners")
|
||||||
|
}
|
||||||
|
|
||||||
|
env.fakeRunnerList.Sync(runnerList.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectCreate(ctx context.Context, rd runtime.Object, s string) {
|
||||||
|
err := k8sClient.Create(ctx, rd)
|
||||||
|
|
||||||
|
ExpectWithOffset(1, err).NotTo(HaveOccurred(), fmt.Sprintf("failed to create %s resource", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectRunnerDeploymentEventuallyUpdates(ctx context.Context, ns string, name string, f func(rd *actionsv1alpha1.RunnerDeployment)) {
|
||||||
|
// 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
|
||||||
|
EventuallyWithOffset(
|
||||||
|
1,
|
||||||
|
func() error {
|
||||||
|
var rd actionsv1alpha1.RunnerDeployment
|
||||||
|
|
||||||
|
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, &rd)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get test RunnerDeployment resource")
|
||||||
|
|
||||||
|
f(&rd)
|
||||||
|
|
||||||
|
return k8sClient.Update(ctx, &rd)
|
||||||
|
},
|
||||||
|
time.Second*1, time.Millisecond*500).Should(BeNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectRunnerSetsCountEventuallyEquals(ctx context.Context, ns string, count int, optionalDescription ...interface{}) {
|
||||||
|
runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}}
|
||||||
|
|
||||||
|
EventuallyWithOffset(
|
||||||
|
1,
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerSets.Items)
|
||||||
|
},
|
||||||
|
time.Second*10, time.Millisecond*500).Should(BeEquivalentTo(count), optionalDescription...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectRunnerSetsManagedReplicasCountEventuallyEquals(ctx context.Context, ns string, count int, optionalDescription ...interface{}) {
|
||||||
|
runnerSets := actionsv1alpha1.RunnerReplicaSetList{Items: []actionsv1alpha1.RunnerReplicaSet{}}
|
||||||
|
|
||||||
|
EventuallyWithOffset(
|
||||||
|
1,
|
||||||
|
func() int {
|
||||||
|
err := k8sClient.List(ctx, &runnerSets, client.InNamespace(ns))
|
||||||
|
if err != nil {
|
||||||
|
logf.Log.Error(err, "list runner sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSets.Items) == 0 {
|
||||||
|
logf.Log.Info("No runnerreplicasets exist yet")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSets.Items) != 1 {
|
||||||
|
logf.Log.Info("Too many runnerreplicasets exist", "runnerSets", runnerSets)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return *runnerSets.Items[0].Spec.Replicas
|
||||||
|
},
|
||||||
|
time.Second*5, time.Millisecond*500).Should(BeEquivalentTo(count), optionalDescription...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -244,60 +244,85 @@ func (r *RunnerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
|||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
notRegistered := false
|
// all checks done below only decide whether a restart is needed
|
||||||
|
// if a restart was already decided before, there is no need for the checks
|
||||||
|
// saving API calls and scary log messages
|
||||||
|
if !restart {
|
||||||
|
|
||||||
runnerBusy, err := r.GitHubClient.IsRunnerBusy(ctx, runner.Spec.Enterprise, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
notRegistered := false
|
||||||
if err != nil {
|
offline := false
|
||||||
var e *github.RunnerNotFound
|
|
||||||
if errors.As(err, &e) {
|
|
||||||
log.Error(err, "Failed to check if runner is busy. Probably this runner has never been successfully registered to GitHub.")
|
|
||||||
|
|
||||||
notRegistered = true
|
runnerBusy, err := r.GitHubClient.IsRunnerBusy(ctx, runner.Spec.Enterprise, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
||||||
} else {
|
if err != nil {
|
||||||
var e *gogithub.RateLimitError
|
var notFoundException *github.RunnerNotFound
|
||||||
if errors.As(err, &e) {
|
var offlineException *github.RunnerOffline
|
||||||
// We log the underlying error when we failed calling GitHub API to list or unregisters,
|
if errors.As(err, ¬FoundException) {
|
||||||
// or the runner is still busy.
|
log.V(1).Info("Failed to check if runner is busy. Either this runner has never been successfully registered to GitHub or it still needs more time.", "runnerName", runner.Name)
|
||||||
log.Error(
|
|
||||||
err,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Failed to check if runner is busy due to Github API rate limit. Retrying in %s to avoid excessive GitHub API calls",
|
|
||||||
retryDelayOnGitHubAPIRateLimitError,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return ctrl.Result{RequeueAfter: retryDelayOnGitHubAPIRateLimitError}, err
|
notRegistered = true
|
||||||
|
} else if errors.As(err, &offlineException) {
|
||||||
|
log.V(1).Info("GitHub runner appears to be offline, waiting for runner to get online ...", "runnerName", runner.Name)
|
||||||
|
offline = true
|
||||||
|
} else {
|
||||||
|
var e *gogithub.RateLimitError
|
||||||
|
if errors.As(err, &e) {
|
||||||
|
// We log the underlying error when we failed calling GitHub API to list or unregisters,
|
||||||
|
// or the runner is still busy.
|
||||||
|
log.Error(
|
||||||
|
err,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Failed to check if runner is busy due to Github API rate limit. Retrying in %s to avoid excessive GitHub API calls",
|
||||||
|
retryDelayOnGitHubAPIRateLimitError,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ctrl.Result{RequeueAfter: retryDelayOnGitHubAPIRateLimitError}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctrl.Result{}, err
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// See the `newPod` function called above for more information
|
// See the `newPod` function called above for more information
|
||||||
// about when this hash changes.
|
// about when this hash changes.
|
||||||
curHash := pod.Labels[LabelKeyPodTemplateHash]
|
curHash := pod.Labels[LabelKeyPodTemplateHash]
|
||||||
newHash := newPod.Labels[LabelKeyPodTemplateHash]
|
newHash := newPod.Labels[LabelKeyPodTemplateHash]
|
||||||
|
|
||||||
if !runnerBusy && curHash != newHash {
|
if !runnerBusy && curHash != newHash {
|
||||||
restart = true
|
restart = true
|
||||||
}
|
}
|
||||||
|
|
||||||
registrationTimeout := 10 * time.Minute
|
registrationTimeout := 10 * time.Minute
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
registrationDidTimeout := currentTime.Sub(pod.CreationTimestamp.Add(registrationTimeout)) > 0
|
registrationDidTimeout := currentTime.Sub(pod.CreationTimestamp.Add(registrationTimeout)) > 0
|
||||||
|
|
||||||
if notRegistered && registrationDidTimeout {
|
if notRegistered && registrationDidTimeout {
|
||||||
log.Info(
|
log.Info(
|
||||||
"Runner failed to register itself to GitHub in timely manner. "+
|
"Runner failed to register itself to GitHub in timely manner. "+
|
||||||
"Recreating the pod to see if it resolves the issue. "+
|
"Recreating the pod to see if it resolves the issue. "+
|
||||||
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
||||||
"See https://github.com/summerwind/actions-runner-controller/issues/288",
|
"See https://github.com/summerwind/actions-runner-controller/issues/288",
|
||||||
"podCreationTimestamp", pod.CreationTimestamp,
|
"podCreationTimestamp", pod.CreationTimestamp,
|
||||||
"currentTime", currentTime,
|
"currentTime", currentTime,
|
||||||
"configuredRegistrationTimeout", registrationTimeout,
|
"configuredRegistrationTimeout", registrationTimeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
restart = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if offline && registrationDidTimeout {
|
||||||
|
log.Info(
|
||||||
|
"Already existing GitHub runner still appears offline . "+
|
||||||
|
"Recreating the pod to see if it resolves the issue. "+
|
||||||
|
"CAUTION: If you see this a lot, you should investigate the root cause. ",
|
||||||
|
"podCreationTimestamp", pod.CreationTimestamp,
|
||||||
|
"currentTime", currentTime,
|
||||||
|
"configuredRegistrationTimeout", registrationTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
restart = true
|
||||||
|
}
|
||||||
|
|
||||||
restart = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't do anything if there's no need to restart the runner
|
// Don't do anything if there's no need to restart the runner
|
||||||
@@ -655,11 +680,14 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
r.Recorder = mgr.GetEventRecorderFor("runner-controller")
|
name := "runner-controller"
|
||||||
|
|
||||||
|
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&v1alpha1.Runner{}).
|
For(&v1alpha1.Runner{}).
|
||||||
Owns(&corev1.Pod{}).
|
Owns(&corev1.Pod{}).
|
||||||
|
Named(name).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,11 @@ const (
|
|||||||
// RunnerDeploymentReconciler reconciles a Runner object
|
// RunnerDeploymentReconciler reconciles a Runner object
|
||||||
type RunnerDeploymentReconciler struct {
|
type RunnerDeploymentReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
Log logr.Logger
|
Log logr.Logger
|
||||||
Recorder record.EventRecorder
|
Recorder record.EventRecorder
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
|
CommonRunnerLabels []string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// +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
|
||||||
@@ -262,6 +264,10 @@ func (r *RunnerDeploymentReconciler) newRunnerReplicaSet(rd v1alpha1.RunnerDeplo
|
|||||||
// Add template hash label to selector.
|
// Add template hash label to selector.
|
||||||
labels := CloneAndAddLabel(rd.Spec.Template.Labels, LabelKeyRunnerTemplateHash, templateHash)
|
labels := CloneAndAddLabel(rd.Spec.Template.Labels, LabelKeyRunnerTemplateHash, templateHash)
|
||||||
|
|
||||||
|
for _, l := range r.CommonRunnerLabels {
|
||||||
|
newRSTemplate.Spec.Labels = append(newRSTemplate.Spec.Labels, l)
|
||||||
|
}
|
||||||
|
|
||||||
newRSTemplate.Labels = labels
|
newRSTemplate.Labels = labels
|
||||||
|
|
||||||
rs := v1alpha1.RunnerReplicaSet{
|
rs := v1alpha1.RunnerReplicaSet{
|
||||||
@@ -285,7 +291,12 @@ func (r *RunnerDeploymentReconciler) newRunnerReplicaSet(rd v1alpha1.RunnerDeplo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RunnerDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *RunnerDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
r.Recorder = mgr.GetEventRecorderFor("runnerdeployment-controller")
|
name := "runnerdeployment-controller"
|
||||||
|
if r.Name != "" {
|
||||||
|
name = r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||||
|
|
||||||
if err := mgr.GetFieldIndexer().IndexField(&v1alpha1.RunnerReplicaSet{}, runnerSetOwnerKey, func(rawObj runtime.Object) []string {
|
if err := mgr.GetFieldIndexer().IndexField(&v1alpha1.RunnerReplicaSet{}, runnerSetOwnerKey, func(rawObj runtime.Object) []string {
|
||||||
runnerSet := rawObj.(*v1alpha1.RunnerReplicaSet)
|
runnerSet := rawObj.(*v1alpha1.RunnerReplicaSet)
|
||||||
@@ -306,5 +317,6 @@ func (r *RunnerDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&v1alpha1.RunnerDeployment{}).
|
For(&v1alpha1.RunnerDeployment{}).
|
||||||
Owns(&v1alpha1.RunnerReplicaSet{}).
|
Owns(&v1alpha1.RunnerReplicaSet{}).
|
||||||
|
Named(name).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@@ -18,6 +21,40 @@ import (
|
|||||||
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNewRunnerReplicaSet(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
if err := actionsv1alpha1.AddToScheme(scheme); err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &RunnerDeploymentReconciler{
|
||||||
|
CommonRunnerLabels: []string{"dev"},
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
rd := actionsv1alpha1.RunnerDeployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "example",
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.RunnerDeploymentSpec{
|
||||||
|
Template: actionsv1alpha1.RunnerTemplate{
|
||||||
|
Spec: actionsv1alpha1.RunnerSpec{
|
||||||
|
Labels: []string{"project1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, err := r.newRunnerReplicaSet(rd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"project1", "dev"}
|
||||||
|
if d := cmp.Diff(want, rs.Spec.Template.Spec.Labels); d != "" {
|
||||||
|
t.Errorf("%s", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetupDeploymentTest will set up a testing environment.
|
// SetupDeploymentTest will set up a testing environment.
|
||||||
// This includes:
|
// This includes:
|
||||||
// * creating a Namespace to be used during the test
|
// * creating a Namespace to be used during the test
|
||||||
@@ -45,6 +82,7 @@ func SetupDeploymentTest(ctx context.Context) *corev1.Namespace {
|
|||||||
Scheme: scheme.Scheme,
|
Scheme: scheme.Scheme,
|
||||||
Log: logf.Log,
|
Log: logf.Log,
|
||||||
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
||||||
|
Name: "runnerdeployment-" + ns.Name,
|
||||||
}
|
}
|
||||||
err = controller.SetupWithManager(mgr)
|
err = controller.SetupWithManager(mgr)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
gogithub "github.com/google/go-github/v33/github"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
gogithub "github.com/google/go-github/v33/github"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@@ -44,6 +45,7 @@ type RunnerReplicaSetReconciler struct {
|
|||||||
Recorder record.EventRecorder
|
Recorder record.EventRecorder
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
GitHubClient *github.Client
|
GitHubClient *github.Client
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerreplicasets,verbs=get;list;watch;create;update;patch;delete
|
// +kubebuilder:rbac:groups=actions.summerwind.dev,resources=runnerreplicasets,verbs=get;list;watch;create;update;patch;delete
|
||||||
@@ -108,11 +110,15 @@ func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
busy, err := r.GitHubClient.IsRunnerBusy(ctx, runner.Spec.Enterprise, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
busy, err := r.GitHubClient.IsRunnerBusy(ctx, runner.Spec.Enterprise, runner.Spec.Organization, runner.Spec.Repository, runner.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
notRegistered := false
|
notRegistered := false
|
||||||
|
offline := false
|
||||||
|
|
||||||
var e *github.RunnerNotFound
|
var notFoundException *github.RunnerNotFound
|
||||||
if errors.As(err, &e) {
|
var offlineException *github.RunnerOffline
|
||||||
log.Error(err, "Failed to check if runner is busy. Probably this runner has never been successfully registered to GitHub, and therefore we prioritize it for deletion", "runnerName", runner.Name)
|
if errors.As(err, ¬FoundException) {
|
||||||
|
log.V(1).Info("Failed to check if runner is busy. Either this runner has never been successfully registered to GitHub or it still needs more time.", "runnerName", runner.Name)
|
||||||
notRegistered = true
|
notRegistered = true
|
||||||
|
} else if errors.As(err, &offlineException) {
|
||||||
|
offline = true
|
||||||
} else {
|
} else {
|
||||||
var e *gogithub.RateLimitError
|
var e *gogithub.RateLimitError
|
||||||
if errors.As(err, &e) {
|
if errors.As(err, &e) {
|
||||||
@@ -139,7 +145,7 @@ func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
if notRegistered && registrationDidTimeout {
|
if notRegistered && registrationDidTimeout {
|
||||||
log.Info(
|
log.Info(
|
||||||
"Runner failed to register itself to GitHub in timely manner. "+
|
"Runner failed to register itself to GitHub in timely manner. "+
|
||||||
"Recreating the pod to see if it resolves the issue. "+
|
"Marking the runner for scale down. "+
|
||||||
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
"CAUTION: If you see this a lot, you should investigate the root cause. "+
|
||||||
"See https://github.com/summerwind/actions-runner-controller/issues/288",
|
"See https://github.com/summerwind/actions-runner-controller/issues/288",
|
||||||
"runnerCreationTimestamp", runner.CreationTimestamp,
|
"runnerCreationTimestamp", runner.CreationTimestamp,
|
||||||
@@ -149,6 +155,11 @@ func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
|
|
||||||
notBusy = append(notBusy, runner)
|
notBusy = append(notBusy, runner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// offline runners should always be a great target for scale down
|
||||||
|
if offline {
|
||||||
|
notBusy = append(notBusy, runner)
|
||||||
|
}
|
||||||
} else if !busy {
|
} else if !busy {
|
||||||
notBusy = append(notBusy, runner)
|
notBusy = append(notBusy, runner)
|
||||||
}
|
}
|
||||||
@@ -165,7 +176,7 @@ func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Recorder.Event(&rs, corev1.EventTypeNormal, "RunnerDeleted", fmt.Sprintf("Deleted runner '%s'", myRunners[i].Name))
|
r.Recorder.Event(&rs, corev1.EventTypeNormal, "RunnerDeleted", fmt.Sprintf("Deleted runner '%s'", notBusy[i].Name))
|
||||||
log.Info("Deleted runner", "runnerreplicaset", rs.ObjectMeta.Name)
|
log.Info("Deleted runner", "runnerreplicaset", rs.ObjectMeta.Name)
|
||||||
}
|
}
|
||||||
} else if desired > available {
|
} else if desired > available {
|
||||||
@@ -193,8 +204,10 @@ func (r *RunnerReplicaSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e
|
|||||||
updated.Status.ReadyReplicas = ready
|
updated.Status.ReadyReplicas = ready
|
||||||
|
|
||||||
if err := r.Status().Update(ctx, updated); err != nil {
|
if err := r.Status().Update(ctx, updated); err != nil {
|
||||||
log.Error(err, "Failed to update runner status")
|
log.Info("Failed to update status. Retrying immediately", "error", err.Error())
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{
|
||||||
|
Requeue: true,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +234,16 @@ func (r *RunnerReplicaSetReconciler) newRunner(rs v1alpha1.RunnerReplicaSet) (v1
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RunnerReplicaSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *RunnerReplicaSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
r.Recorder = mgr.GetEventRecorderFor("runnerreplicaset-controller")
|
name := "runnerreplicaset-controller"
|
||||||
|
if r.Name != "" {
|
||||||
|
name = r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Recorder = mgr.GetEventRecorderFor(name)
|
||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&v1alpha1.RunnerReplicaSet{}).
|
For(&v1alpha1.RunnerReplicaSet{}).
|
||||||
Owns(&v1alpha1.Runner{}).
|
Owns(&v1alpha1.Runner{}).
|
||||||
|
Named(name).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func SetupTest(ctx context.Context) *corev1.Namespace {
|
|||||||
Log: logf.Log,
|
Log: logf.Log,
|
||||||
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
Recorder: mgr.GetEventRecorderFor("runnerreplicaset-controller"),
|
||||||
GitHubClient: ghClient,
|
GitHubClient: ghClient,
|
||||||
|
Name: "runnerreplicaset-" + ns.Name,
|
||||||
}
|
}
|
||||||
err = controller.SetupWithManager(mgr)
|
err = controller.SetupWithManager(mgr)
|
||||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|||||||
373
controllers/testdata/org_webhook_check_run_payload.json
vendored
Normal file
373
controllers/testdata/org_webhook_check_run_payload.json
vendored
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
{
|
||||||
|
"action": "created",
|
||||||
|
"check_run": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"head_sha": "1234567890123456789012345678901234567890",
|
||||||
|
"external_id": "92058b04-f16a-5035-546c-cae3ad5e2f5f",
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/check-runs/123467890",
|
||||||
|
"html_url": "https://github.com/MYORG/MYREPO/runs/123467890",
|
||||||
|
"details_url": "https://github.com/MYORG/MYREPO/runs/123467890",
|
||||||
|
"status": "queued",
|
||||||
|
"conclusion": null,
|
||||||
|
"started_at": "2021-02-18T06:16:31Z",
|
||||||
|
"completed_at": null,
|
||||||
|
"output": {
|
||||||
|
"title": null,
|
||||||
|
"summary": null,
|
||||||
|
"text": null,
|
||||||
|
"annotations_count": 0,
|
||||||
|
"annotations_url": "https://api.github.com/repos/MYORG/MYREPO/check-runs/123467890/annotations"
|
||||||
|
},
|
||||||
|
"name": "validate",
|
||||||
|
"check_suite": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"head_branch": "MYNAME/actions-runner-controller-webhook",
|
||||||
|
"head_sha": "1234567890123456789012345678901234567890",
|
||||||
|
"status": "queued",
|
||||||
|
"conclusion": null,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/check-suites/1234567890",
|
||||||
|
"before": "1234567890123456789012345678901234567890",
|
||||||
|
"after": "1234567890123456789012345678901234567890",
|
||||||
|
"pull_requests": [
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/pulls/2033",
|
||||||
|
"id": 1234567890,
|
||||||
|
"number": 1234567890,
|
||||||
|
"head": {
|
||||||
|
"ref": "feature",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"ref": "master",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"app": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"slug": "github-actions",
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"owner": {
|
||||||
|
"login": "github",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/123467890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/github",
|
||||||
|
"html_url": "https://github.com/github",
|
||||||
|
"followers_url": "https://api.github.com/users/github/followers",
|
||||||
|
"following_url": "https://api.github.com/users/github/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/github/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/github/repos",
|
||||||
|
"events_url": "https://api.github.com/users/github/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/github/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"name": "GitHub Actions",
|
||||||
|
"description": "Automate your workflow from idea to production",
|
||||||
|
"external_url": "https://help.github.com/en/actions",
|
||||||
|
"html_url": "https://github.com/apps/github-actions",
|
||||||
|
"created_at": "2018-07-30T09:30:17Z",
|
||||||
|
"updated_at": "2019-12-10T19:04:12Z",
|
||||||
|
"permissions": {
|
||||||
|
"actions": "write",
|
||||||
|
"checks": "write",
|
||||||
|
"contents": "write",
|
||||||
|
"deployments": "write",
|
||||||
|
"issues": "write",
|
||||||
|
"metadata": "read",
|
||||||
|
"organization_packages": "write",
|
||||||
|
"packages": "write",
|
||||||
|
"pages": "write",
|
||||||
|
"pull_requests": "write",
|
||||||
|
"repository_hooks": "write",
|
||||||
|
"repository_projects": "write",
|
||||||
|
"security_events": "write",
|
||||||
|
"statuses": "write",
|
||||||
|
"vulnerability_alerts": "read"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"check_run",
|
||||||
|
"check_suite",
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"deployment",
|
||||||
|
"deployment_status",
|
||||||
|
"fork",
|
||||||
|
"gollum",
|
||||||
|
"issues",
|
||||||
|
"issue_comment",
|
||||||
|
"label",
|
||||||
|
"milestone",
|
||||||
|
"page_build",
|
||||||
|
"project",
|
||||||
|
"project_card",
|
||||||
|
"project_column",
|
||||||
|
"public",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_review_comment",
|
||||||
|
"push",
|
||||||
|
"registry_package",
|
||||||
|
"release",
|
||||||
|
"repository",
|
||||||
|
"repository_dispatch",
|
||||||
|
"status",
|
||||||
|
"watch",
|
||||||
|
"workflow_dispatch",
|
||||||
|
"workflow_run"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"created_at": "2021-02-18T06:15:32Z",
|
||||||
|
"updated_at": "2021-02-18T06:16:31Z"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"slug": "github-actions",
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"owner": {
|
||||||
|
"login": "github",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/github",
|
||||||
|
"html_url": "https://github.com/github",
|
||||||
|
"followers_url": "https://api.github.com/users/github/followers",
|
||||||
|
"following_url": "https://api.github.com/users/github/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/github/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/github/repos",
|
||||||
|
"events_url": "https://api.github.com/users/github/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/github/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"name": "GitHub Actions",
|
||||||
|
"description": "Automate your workflow from idea to production",
|
||||||
|
"external_url": "https://help.github.com/en/actions",
|
||||||
|
"html_url": "https://github.com/apps/github-actions",
|
||||||
|
"created_at": "2018-07-30T09:30:17Z",
|
||||||
|
"updated_at": "2019-12-10T19:04:12Z",
|
||||||
|
"permissions": {
|
||||||
|
"actions": "write",
|
||||||
|
"checks": "write",
|
||||||
|
"contents": "write",
|
||||||
|
"deployments": "write",
|
||||||
|
"issues": "write",
|
||||||
|
"metadata": "read",
|
||||||
|
"organization_packages": "write",
|
||||||
|
"packages": "write",
|
||||||
|
"pages": "write",
|
||||||
|
"pull_requests": "write",
|
||||||
|
"repository_hooks": "write",
|
||||||
|
"repository_projects": "write",
|
||||||
|
"security_events": "write",
|
||||||
|
"statuses": "write",
|
||||||
|
"vulnerability_alerts": "read"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"check_run",
|
||||||
|
"check_suite",
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"deployment",
|
||||||
|
"deployment_status",
|
||||||
|
"fork",
|
||||||
|
"gollum",
|
||||||
|
"issues",
|
||||||
|
"issue_comment",
|
||||||
|
"label",
|
||||||
|
"milestone",
|
||||||
|
"page_build",
|
||||||
|
"project",
|
||||||
|
"project_card",
|
||||||
|
"project_column",
|
||||||
|
"public",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_review_comment",
|
||||||
|
"push",
|
||||||
|
"registry_package",
|
||||||
|
"release",
|
||||||
|
"repository",
|
||||||
|
"repository_dispatch",
|
||||||
|
"status",
|
||||||
|
"watch",
|
||||||
|
"workflow_dispatch",
|
||||||
|
"workflow_run"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pull_requests": [
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/pulls/1234567890",
|
||||||
|
"id": 1234567890,
|
||||||
|
"number": 1234567890,
|
||||||
|
"head": {
|
||||||
|
"ref": "feature",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"ref": "master",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"name": "MYREPO",
|
||||||
|
"full_name": "MYORG/MYREPO",
|
||||||
|
"private": true,
|
||||||
|
"owner": {
|
||||||
|
"login": "MYORG",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/MYORG",
|
||||||
|
"html_url": "https://github.com/MYORG",
|
||||||
|
"followers_url": "https://api.github.com/users/MYORG/followers",
|
||||||
|
"following_url": "https://api.github.com/users/MYORG/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/MYORG/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/MYORG/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/MYORG/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/MYORG/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/MYORG/repos",
|
||||||
|
"events_url": "https://api.github.com/users/MYORG/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/MYORG/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"html_url": "https://github.com/MYORG/MYREPO",
|
||||||
|
"description": "MYREPO",
|
||||||
|
"fork": false,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"forks_url": "https://api.github.com/repos/MYORG/MYREPO/forks",
|
||||||
|
"keys_url": "https://api.github.com/repos/MYORG/MYREPO/keys{/key_id}",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/MYORG/MYREPO/collaborators{/collaborator}",
|
||||||
|
"teams_url": "https://api.github.com/repos/MYORG/MYREPO/teams",
|
||||||
|
"hooks_url": "https://api.github.com/repos/MYORG/MYREPO/hooks",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/MYORG/MYREPO/issues/events{/number}",
|
||||||
|
"events_url": "https://api.github.com/repos/MYORG/MYREPO/events",
|
||||||
|
"assignees_url": "https://api.github.com/repos/MYORG/MYREPO/assignees{/user}",
|
||||||
|
"branches_url": "https://api.github.com/repos/MYORG/MYREPO/branches{/branch}",
|
||||||
|
"tags_url": "https://api.github.com/repos/MYORG/MYREPO/tags",
|
||||||
|
"blobs_url": "https://api.github.com/repos/MYORG/MYREPO/git/blobs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/MYORG/MYREPO/git/tags{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/MYORG/MYREPO/git/refs{/sha}",
|
||||||
|
"trees_url": "https://api.github.com/repos/MYORG/MYREPO/git/trees{/sha}",
|
||||||
|
"statuses_url": "https://api.github.com/repos/MYORG/MYREPO/statuses/{sha}",
|
||||||
|
"languages_url": "https://api.github.com/repos/MYORG/MYREPO/languages",
|
||||||
|
"stargazers_url": "https://api.github.com/repos/MYORG/MYREPO/stargazers",
|
||||||
|
"contributors_url": "https://api.github.com/repos/MYORG/MYREPO/contributors",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/MYORG/MYREPO/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/MYORG/MYREPO/subscription",
|
||||||
|
"commits_url": "https://api.github.com/repos/MYORG/MYREPO/commits{/sha}",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/MYORG/MYREPO/git/commits{/sha}",
|
||||||
|
"comments_url": "https://api.github.com/repos/MYORG/MYREPO/comments{/number}",
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/MYORG/MYREPO/issues/comments{/number}",
|
||||||
|
"contents_url": "https://api.github.com/repos/MYORG/MYREPO/contents/{+path}",
|
||||||
|
"compare_url": "https://api.github.com/repos/MYORG/MYREPO/compare/{base}...{head}",
|
||||||
|
"merges_url": "https://api.github.com/repos/MYORG/MYREPO/merges",
|
||||||
|
"archive_url": "https://api.github.com/repos/MYORG/MYREPO/{archive_format}{/ref}",
|
||||||
|
"downloads_url": "https://api.github.com/repos/MYORG/MYREPO/downloads",
|
||||||
|
"issues_url": "https://api.github.com/repos/MYORG/MYREPO/issues{/number}",
|
||||||
|
"pulls_url": "https://api.github.com/repos/MYORG/MYREPO/pulls{/number}",
|
||||||
|
"milestones_url": "https://api.github.com/repos/MYORG/MYREPO/milestones{/number}",
|
||||||
|
"notifications_url": "https://api.github.com/repos/MYORG/MYREPO/notifications{?since,all,participating}",
|
||||||
|
"labels_url": "https://api.github.com/repos/MYORG/MYREPO/labels{/name}",
|
||||||
|
"releases_url": "https://api.github.com/repos/MYORG/MYREPO/releases{/id}",
|
||||||
|
"deployments_url": "https://api.github.com/repos/MYORG/MYREPO/deployments",
|
||||||
|
"created_at": "2017-08-10T02:21:10Z",
|
||||||
|
"updated_at": "2021-02-18T04:40:55Z",
|
||||||
|
"pushed_at": "2021-02-18T06:15:30Z",
|
||||||
|
"git_url": "git://github.com/MYORG/MYREPO.git",
|
||||||
|
"ssh_url": "git@github.com:MYORG/MYREPO.git",
|
||||||
|
"clone_url": "https://github.com/MYORG/MYREPO.git",
|
||||||
|
"svn_url": "https://github.com/MYORG/MYREPO",
|
||||||
|
"homepage": null,
|
||||||
|
"size": 30782,
|
||||||
|
"stargazers_count": 2,
|
||||||
|
"watchers_count": 2,
|
||||||
|
"language": "Shell",
|
||||||
|
"has_issues": false,
|
||||||
|
"has_projects": true,
|
||||||
|
"has_downloads": true,
|
||||||
|
"has_wiki": false,
|
||||||
|
"has_pages": false,
|
||||||
|
"forks_count": 0,
|
||||||
|
"mirror_url": null,
|
||||||
|
"archived": false,
|
||||||
|
"disabled": false,
|
||||||
|
"open_issues_count": 6,
|
||||||
|
"license": null,
|
||||||
|
"forks": 0,
|
||||||
|
"open_issues": 6,
|
||||||
|
"watchers": 2,
|
||||||
|
"default_branch": "master"
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"login": "MYORG",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"url": "https://api.github.com/orgs/MYORG",
|
||||||
|
"repos_url": "https://api.github.com/orgs/MYORG/repos",
|
||||||
|
"events_url": "https://api.github.com/orgs/MYORG/events",
|
||||||
|
"hooks_url": "https://api.github.com/orgs/MYORG/hooks",
|
||||||
|
"issues_url": "https://api.github.com/orgs/MYORG/issues",
|
||||||
|
"members_url": "https://api.github.com/orgs/MYORG/members{/member}",
|
||||||
|
"public_members_url": "https://api.github.com/orgs/MYORG/public_members{/member}",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"sender": {
|
||||||
|
"login": "MYNAME",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/MYNAME",
|
||||||
|
"html_url": "https://github.com/MYNAME",
|
||||||
|
"followers_url": "https://api.github.com/users/MYNAME/followers",
|
||||||
|
"following_url": "https://api.github.com/users/MYNAME/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/MYNAME/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/MYNAME/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/MYNAME/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/MYNAME/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/MYNAME/repos",
|
||||||
|
"events_url": "https://api.github.com/users/MYNAME/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/MYNAME/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
}
|
||||||
|
}
|
||||||
360
controllers/testdata/repo_webhook_check_run_payload.json
vendored
Normal file
360
controllers/testdata/repo_webhook_check_run_payload.json
vendored
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
{
|
||||||
|
"action": "completed",
|
||||||
|
"check_run": {
|
||||||
|
"id": 1949438388,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"head_sha": "1234567890123456789012345678901234567890",
|
||||||
|
"external_id": "ca395085-040a-526b-2ce8-bdc85f692774",
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/check-runs/123467890",
|
||||||
|
"html_url": "https://github.com/MYORG/MYREPO/runs/123467890",
|
||||||
|
"details_url": "https://github.com/MYORG/MYREPO/runs/123467890",
|
||||||
|
"status": "queued",
|
||||||
|
"conclusion": null,
|
||||||
|
"started_at": "2021-02-18T06:16:31Z",
|
||||||
|
"completed_at": null,
|
||||||
|
"output": {
|
||||||
|
"title": null,
|
||||||
|
"summary": null,
|
||||||
|
"text": null,
|
||||||
|
"annotations_count": 0,
|
||||||
|
"annotations_url": "https://api.github.com/repos/MYORG/MYREPO/check-runs/123467890/annotations"
|
||||||
|
},
|
||||||
|
"name": "build",
|
||||||
|
"name": "validate",
|
||||||
|
"check_suite": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"head_branch": "MYNAME/actions-runner-controller-webhook",
|
||||||
|
"head_sha": "1234567890123456789012345678901234567890",
|
||||||
|
"status": "queued",
|
||||||
|
"conclusion": null,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/check-suites/1234567890",
|
||||||
|
"before": "1234567890123456789012345678901234567890",
|
||||||
|
"after": "1234567890123456789012345678901234567890",
|
||||||
|
"pull_requests": [
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/pulls/2033",
|
||||||
|
"id": 1234567890,
|
||||||
|
"number": 1234567890,
|
||||||
|
"head": {
|
||||||
|
"ref": "feature",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"ref": "master",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"app": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"slug": "github-actions",
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"owner": {
|
||||||
|
"login": "github",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/123467890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/github",
|
||||||
|
"html_url": "https://github.com/github",
|
||||||
|
"followers_url": "https://api.github.com/users/github/followers",
|
||||||
|
"following_url": "https://api.github.com/users/github/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/github/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/github/repos",
|
||||||
|
"events_url": "https://api.github.com/users/github/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/github/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"name": "GitHub Actions",
|
||||||
|
"description": "Automate your workflow from idea to production",
|
||||||
|
"external_url": "https://help.github.com/en/actions",
|
||||||
|
"html_url": "https://github.com/apps/github-actions",
|
||||||
|
"created_at": "2018-07-30T09:30:17Z",
|
||||||
|
"updated_at": "2019-12-10T19:04:12Z",
|
||||||
|
"permissions": {
|
||||||
|
"actions": "write",
|
||||||
|
"checks": "write",
|
||||||
|
"contents": "write",
|
||||||
|
"deployments": "write",
|
||||||
|
"issues": "write",
|
||||||
|
"metadata": "read",
|
||||||
|
"organization_packages": "write",
|
||||||
|
"packages": "write",
|
||||||
|
"pages": "write",
|
||||||
|
"pull_requests": "write",
|
||||||
|
"repository_hooks": "write",
|
||||||
|
"repository_projects": "write",
|
||||||
|
"security_events": "write",
|
||||||
|
"statuses": "write",
|
||||||
|
"vulnerability_alerts": "read"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"check_run",
|
||||||
|
"check_suite",
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"deployment",
|
||||||
|
"deployment_status",
|
||||||
|
"fork",
|
||||||
|
"gollum",
|
||||||
|
"issues",
|
||||||
|
"issue_comment",
|
||||||
|
"label",
|
||||||
|
"milestone",
|
||||||
|
"page_build",
|
||||||
|
"project",
|
||||||
|
"project_card",
|
||||||
|
"project_column",
|
||||||
|
"public",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_review_comment",
|
||||||
|
"push",
|
||||||
|
"registry_package",
|
||||||
|
"release",
|
||||||
|
"repository",
|
||||||
|
"repository_dispatch",
|
||||||
|
"status",
|
||||||
|
"watch",
|
||||||
|
"workflow_dispatch",
|
||||||
|
"workflow_run"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"created_at": "2021-02-18T06:15:32Z",
|
||||||
|
"updated_at": "2021-02-18T06:16:31Z"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"slug": "github-actions",
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"owner": {
|
||||||
|
"login": "github",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/github",
|
||||||
|
"html_url": "https://github.com/github",
|
||||||
|
"followers_url": "https://api.github.com/users/github/followers",
|
||||||
|
"following_url": "https://api.github.com/users/github/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/github/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/github/repos",
|
||||||
|
"events_url": "https://api.github.com/users/github/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/github/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"name": "GitHub Actions",
|
||||||
|
"description": "Automate your workflow from idea to production",
|
||||||
|
"external_url": "https://help.github.com/en/actions",
|
||||||
|
"html_url": "https://github.com/apps/github-actions",
|
||||||
|
"created_at": "2018-07-30T09:30:17Z",
|
||||||
|
"updated_at": "2019-12-10T19:04:12Z",
|
||||||
|
"permissions": {
|
||||||
|
"actions": "write",
|
||||||
|
"checks": "write",
|
||||||
|
"contents": "write",
|
||||||
|
"deployments": "write",
|
||||||
|
"issues": "write",
|
||||||
|
"metadata": "read",
|
||||||
|
"organization_packages": "write",
|
||||||
|
"packages": "write",
|
||||||
|
"pages": "write",
|
||||||
|
"pull_requests": "write",
|
||||||
|
"repository_hooks": "write",
|
||||||
|
"repository_projects": "write",
|
||||||
|
"security_events": "write",
|
||||||
|
"statuses": "write",
|
||||||
|
"vulnerability_alerts": "read"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"check_run",
|
||||||
|
"check_suite",
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"deployment",
|
||||||
|
"deployment_status",
|
||||||
|
"fork",
|
||||||
|
"gollum",
|
||||||
|
"issues",
|
||||||
|
"issue_comment",
|
||||||
|
"label",
|
||||||
|
"milestone",
|
||||||
|
"page_build",
|
||||||
|
"project",
|
||||||
|
"project_card",
|
||||||
|
"project_column",
|
||||||
|
"public",
|
||||||
|
"pull_request",
|
||||||
|
"pull_request_review",
|
||||||
|
"pull_request_review_comment",
|
||||||
|
"push",
|
||||||
|
"registry_package",
|
||||||
|
"release",
|
||||||
|
"repository",
|
||||||
|
"repository_dispatch",
|
||||||
|
"status",
|
||||||
|
"watch",
|
||||||
|
"workflow_dispatch",
|
||||||
|
"workflow_run"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pull_requests": [
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO/pulls/1234567890",
|
||||||
|
"id": 1234567890,
|
||||||
|
"number": 1234567890,
|
||||||
|
"head": {
|
||||||
|
"ref": "feature",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"ref": "master",
|
||||||
|
"sha": "1234567890123456789012345678901234567890",
|
||||||
|
"repo": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"url": "https://api.github.com/repos/MYORG/MYREPO",
|
||||||
|
"name": "MYREPO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"name": "MYREPO",
|
||||||
|
"full_name": "MYORG/MYREPO",
|
||||||
|
"private": true,
|
||||||
|
"owner": {
|
||||||
|
"login": "MYUSER",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/MYUSER",
|
||||||
|
"html_url": "https://github.com/MYUSER",
|
||||||
|
"followers_url": "https://api.github.com/users/MYUSER/followers",
|
||||||
|
"following_url": "https://api.github.com/users/MYUSER/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/MYUSER/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/MYUSER/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/MYUSER/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/MYUSER/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/MYUSER/repos",
|
||||||
|
"events_url": "https://api.github.com/users/MYUSER/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/MYUSER/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"html_url": "https://github.com/MYUSER/MYREPO",
|
||||||
|
"description": null,
|
||||||
|
"fork": false,
|
||||||
|
"url": "https://api.github.com/repos/MYUSER/MYREPO",
|
||||||
|
"forks_url": "https://api.github.com/repos/MYUSER/MYREPO/forks",
|
||||||
|
"keys_url": "https://api.github.com/repos/MYUSER/MYREPO/keys{/key_id}",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/MYUSER/MYREPO/collaborators{/collaborator}",
|
||||||
|
"teams_url": "https://api.github.com/repos/MYUSER/MYREPO/teams",
|
||||||
|
"hooks_url": "https://api.github.com/repos/MYUSER/MYREPO/hooks",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/MYUSER/MYREPO/issues/events{/number}",
|
||||||
|
"events_url": "https://api.github.com/repos/MYUSER/MYREPO/events",
|
||||||
|
"assignees_url": "https://api.github.com/repos/MYUSER/MYREPO/assignees{/user}",
|
||||||
|
"branches_url": "https://api.github.com/repos/MYUSER/MYREPO/branches{/branch}",
|
||||||
|
"tags_url": "https://api.github.com/repos/MYUSER/MYREPO/tags",
|
||||||
|
"blobs_url": "https://api.github.com/repos/MYUSER/MYREPO/git/blobs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/MYUSER/MYREPO/git/tags{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/MYUSER/MYREPO/git/refs{/sha}",
|
||||||
|
"trees_url": "https://api.github.com/repos/MYUSER/MYREPO/git/trees{/sha}",
|
||||||
|
"statuses_url": "https://api.github.com/repos/MYUSER/MYREPO/statuses/{sha}",
|
||||||
|
"languages_url": "https://api.github.com/repos/MYUSER/MYREPO/languages",
|
||||||
|
"stargazers_url": "https://api.github.com/repos/MYUSER/MYREPO/stargazers",
|
||||||
|
"contributors_url": "https://api.github.com/repos/MYUSER/MYREPO/contributors",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/MYUSER/MYREPO/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/MYUSER/MYREPO/subscription",
|
||||||
|
"commits_url": "https://api.github.com/repos/MYUSER/MYREPO/commits{/sha}",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/MYUSER/MYREPO/git/commits{/sha}",
|
||||||
|
"comments_url": "https://api.github.com/repos/MYUSER/MYREPO/comments{/number}",
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/MYUSER/MYREPO/issues/comments{/number}",
|
||||||
|
"contents_url": "https://api.github.com/repos/MYUSER/MYREPO/contents/{+path}",
|
||||||
|
"compare_url": "https://api.github.com/repos/MYUSER/MYREPO/compare/{base}...{head}",
|
||||||
|
"merges_url": "https://api.github.com/repos/MYUSER/MYREPO/merges",
|
||||||
|
"archive_url": "https://api.github.com/repos/MYUSER/MYREPO/{archive_format}{/ref}",
|
||||||
|
"downloads_url": "https://api.github.com/repos/MYUSER/MYREPO/downloads",
|
||||||
|
"issues_url": "https://api.github.com/repos/MYUSER/MYREPO/issues{/number}",
|
||||||
|
"pulls_url": "https://api.github.com/repos/MYUSER/MYREPO/pulls{/number}",
|
||||||
|
"milestones_url": "https://api.github.com/repos/MYUSER/MYREPO/milestones{/number}",
|
||||||
|
"notifications_url": "https://api.github.com/repos/MYUSER/MYREPO/notifications{?since,all,participating}",
|
||||||
|
"labels_url": "https://api.github.com/repos/MYUSER/MYREPO/labels{/name}",
|
||||||
|
"releases_url": "https://api.github.com/repos/MYUSER/MYREPO/releases{/id}",
|
||||||
|
"deployments_url": "https://api.github.com/repos/MYUSER/MYREPO/deployments",
|
||||||
|
"created_at": "2021-02-18T06:16:31Z",
|
||||||
|
"updated_at": "2021-02-18T06:16:31Z",
|
||||||
|
"pushed_at": "2021-02-18T06:16:31Z",
|
||||||
|
"git_url": "git://github.com/MYUSER/MYREPO.git",
|
||||||
|
"ssh_url": "git@github.com:MYUSER/MYREPO.git",
|
||||||
|
"clone_url": "https://github.com/MYUSER/MYREPO.git",
|
||||||
|
"svn_url": "https://github.com/MYUSER/MYREPO",
|
||||||
|
"homepage": null,
|
||||||
|
"size": 4,
|
||||||
|
"stargazers_count": 0,
|
||||||
|
"watchers_count": 0,
|
||||||
|
"language": null,
|
||||||
|
"has_issues": true,
|
||||||
|
"has_projects": true,
|
||||||
|
"has_downloads": true,
|
||||||
|
"has_wiki": true,
|
||||||
|
"has_pages": false,
|
||||||
|
"forks_count": 0,
|
||||||
|
"mirror_url": null,
|
||||||
|
"archived": false,
|
||||||
|
"disabled": false,
|
||||||
|
"open_issues_count": 0,
|
||||||
|
"license": null,
|
||||||
|
"forks": 0,
|
||||||
|
"open_issues": 0,
|
||||||
|
"watchers": 0,
|
||||||
|
"default_branch": "main"
|
||||||
|
},
|
||||||
|
"sender": {
|
||||||
|
"login": "MYUSER",
|
||||||
|
"id": 1234567890,
|
||||||
|
"node_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1234567890?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/MYUSER",
|
||||||
|
"html_url": "https://github.com/MYUSER",
|
||||||
|
"followers_url": "https://api.github.com/users/MYUSER/followers",
|
||||||
|
"following_url": "https://api.github.com/users/MYUSER/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/MYUSER/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/MYUSER/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/MYUSER/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/MYUSER/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/MYUSER/repos",
|
||||||
|
"events_url": "https://api.github.com/users/MYUSER/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/MYUSER/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,10 +37,21 @@ func (h *ListRunnersHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
type Handler struct {
|
type Handler struct {
|
||||||
Status int
|
Status int
|
||||||
Body string
|
Body string
|
||||||
|
|
||||||
|
Statuses map[string]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)
|
||||||
|
|
||||||
|
status := req.URL.Query().Get("status")
|
||||||
|
if h.Statuses != nil {
|
||||||
|
if body, ok := h.Statuses[status]; ok {
|
||||||
|
fmt.Fprintf(w, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, h.Body)
|
fmt.Fprintf(w, h.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +113,18 @@ func NewServer(opts ...Option) *httptest.Server {
|
|||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
|
"/enterprises/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)),
|
||||||
|
},
|
||||||
|
"/enterprises/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)),
|
||||||
|
},
|
||||||
|
"/enterprises/error/actions/runners/registration-token": &Handler{
|
||||||
|
Status: http.StatusBadRequest,
|
||||||
|
Body: "",
|
||||||
|
},
|
||||||
|
|
||||||
// For ListRunners
|
// For ListRunners
|
||||||
"/repos/test/valid/actions/runners": config.FixedResponses.ListRunners,
|
"/repos/test/valid/actions/runners": config.FixedResponses.ListRunners,
|
||||||
@@ -125,6 +148,18 @@ func NewServer(opts ...Option) *httptest.Server {
|
|||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
|
"/enterprises/test/actions/runners": &Handler{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Body: RunnersListBody,
|
||||||
|
},
|
||||||
|
"/enterprises/invalid/actions/runners": &Handler{
|
||||||
|
Status: http.StatusNoContent,
|
||||||
|
Body: "",
|
||||||
|
},
|
||||||
|
"/enterprises/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{
|
||||||
@@ -151,6 +186,18 @@ func NewServer(opts ...Option) *httptest.Server {
|
|||||||
Status: http.StatusBadRequest,
|
Status: http.StatusBadRequest,
|
||||||
Body: "",
|
Body: "",
|
||||||
},
|
},
|
||||||
|
"/enterprises/test/actions/runners/1": &Handler{
|
||||||
|
Status: http.StatusNoContent,
|
||||||
|
Body: "",
|
||||||
|
},
|
||||||
|
"/enterprises/invalid/actions/runners/1": &Handler{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Body: "",
|
||||||
|
},
|
||||||
|
"/enterprises/error/actions/runners/1": &Handler{
|
||||||
|
Status: http.StatusBadRequest,
|
||||||
|
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": config.FixedResponses.ListRepositoryWorkflowRuns,
|
"/repos/test/valid/actions/runs": config.FixedResponses.ListRepositoryWorkflowRuns,
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ type FixedResponses struct {
|
|||||||
|
|
||||||
type Option func(*ServerConfig)
|
type Option func(*ServerConfig)
|
||||||
|
|
||||||
func WithListRepositoryWorkflowRunsResponse(status int, body string) Option {
|
func WithListRepositoryWorkflowRunsResponse(status int, body, queued, in_progress string) Option {
|
||||||
return func(c *ServerConfig) {
|
return func(c *ServerConfig) {
|
||||||
c.FixedResponses.ListRepositoryWorkflowRuns = &Handler{
|
c.FixedResponses.ListRepositoryWorkflowRuns = &Handler{
|
||||||
Status: status,
|
Status: status,
|
||||||
Body: body,
|
Body: body,
|
||||||
|
Statuses: map[string]string{
|
||||||
|
"queued": queued,
|
||||||
|
"in_progress": in_progress,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package fake
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/summerwind/actions-runner-controller/api/v1alpha1"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -64,6 +65,20 @@ func (r *RunnersList) handleRemove() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RunnersList) Sync(runners []v1alpha1.Runner) {
|
||||||
|
r.runners = nil
|
||||||
|
|
||||||
|
for i, want := range runners {
|
||||||
|
r.Add(&github.Runner{
|
||||||
|
ID: github.Int64(int64(i)),
|
||||||
|
Name: github.String(want.Name),
|
||||||
|
OS: github.String("linux"),
|
||||||
|
Status: github.String("online"),
|
||||||
|
Busy: github.Bool(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func exists(runners []*github.Runner, runner *github.Runner) bool {
|
func exists(runners []*github.Runner, runner *github.Runner) bool {
|
||||||
for _, r := range runners {
|
for _, r := range runners {
|
||||||
if *r.Name == *runner.Name {
|
if *r.Name == *runner.Name {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bradleyfalzon/ghinstallation"
|
"github.com/bradleyfalzon/ghinstallation"
|
||||||
"github.com/google/go-github/v33/github"
|
"github.com/google/go-github/v33/github"
|
||||||
|
"github.com/summerwind/actions-runner-controller/github/metrics"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,15 +35,9 @@ type Client struct {
|
|||||||
|
|
||||||
// NewClient creates a Github Client
|
// NewClient creates a Github Client
|
||||||
func (c *Config) NewClient() (*Client, error) {
|
func (c *Config) NewClient() (*Client, error) {
|
||||||
var (
|
var transport http.RoundTripper
|
||||||
httpClient *http.Client
|
|
||||||
client *github.Client
|
|
||||||
)
|
|
||||||
githubBaseURL := "https://github.com/"
|
|
||||||
if len(c.Token) > 0 {
|
if len(c.Token) > 0 {
|
||||||
httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
|
transport = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token})).Transport
|
||||||
&oauth2.Token{AccessToken: c.Token},
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.AppID, c.AppInstallationID, c.AppPrivateKey)
|
tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.AppID, c.AppInstallationID, c.AppPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,9 +50,13 @@ func (c *Config) NewClient() (*Client, error) {
|
|||||||
}
|
}
|
||||||
tr.BaseURL = githubAPIURL
|
tr.BaseURL = githubAPIURL
|
||||||
}
|
}
|
||||||
httpClient = &http.Client{Transport: tr}
|
transport = tr
|
||||||
}
|
}
|
||||||
|
transport = metrics.Transport{Transport: transport}
|
||||||
|
httpClient := &http.Client{Transport: transport}
|
||||||
|
|
||||||
|
var client *github.Client
|
||||||
|
var githubBaseURL string
|
||||||
if len(c.EnterpriseURL) > 0 {
|
if len(c.EnterpriseURL) > 0 {
|
||||||
var err error
|
var err error
|
||||||
client, err = github.NewEnterpriseClient(c.EnterpriseURL, c.EnterpriseURL, httpClient)
|
client, err = github.NewEnterpriseClient(c.EnterpriseURL, c.EnterpriseURL, httpClient)
|
||||||
@@ -67,6 +66,7 @@ func (c *Config) NewClient() (*Client, error) {
|
|||||||
githubBaseURL = fmt.Sprintf("%s://%s%s", client.BaseURL.Scheme, client.BaseURL.Host, strings.TrimSuffix(client.BaseURL.Path, "api/v3/"))
|
githubBaseURL = fmt.Sprintf("%s://%s%s", client.BaseURL.Scheme, client.BaseURL.Host, strings.TrimSuffix(client.BaseURL.Path, "api/v3/"))
|
||||||
} else {
|
} else {
|
||||||
client = github.NewClient(httpClient)
|
client = github.NewClient(httpClient)
|
||||||
|
githubBaseURL = "https://github.com/"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
@@ -82,10 +82,13 @@ func (c *Client) GetRegistrationToken(ctx context.Context, enterprise, org, repo
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
key := getRegistrationKey(org, repo)
|
key := getRegistrationKey(org, repo, enterprise)
|
||||||
rt, ok := c.regTokens[key]
|
rt, ok := c.regTokens[key]
|
||||||
|
|
||||||
if ok && rt.GetExpiresAt().After(time.Now()) {
|
// we like to give runners a chance that are just starting up and may miss the expiration date by a bit
|
||||||
|
runnerStartupTimeout := 3 * time.Minute
|
||||||
|
|
||||||
|
if ok && rt.GetExpiresAt().After(time.Now().Add(runnerStartupTimeout)) {
|
||||||
return rt, nil
|
return rt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,12 +213,34 @@ func (c *Client) listRunners(ctx context.Context, enterprise, org, repo string,
|
|||||||
func (c *Client) ListRepositoryWorkflowRuns(ctx context.Context, user string, repoName string) ([]*github.WorkflowRun, error) {
|
func (c *Client) ListRepositoryWorkflowRuns(ctx context.Context, user string, repoName string) ([]*github.WorkflowRun, error) {
|
||||||
c.Client.Actions.ListRepositoryWorkflowRuns(ctx, user, repoName, nil)
|
c.Client.Actions.ListRepositoryWorkflowRuns(ctx, user, repoName, nil)
|
||||||
|
|
||||||
|
queued, err := c.listRepositoryWorkflowRuns(ctx, user, repoName, "queued")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing queued workflow runs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress, err := c.listRepositoryWorkflowRuns(ctx, user, repoName, "in_progress")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing in_progress workflow runs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var workflowRuns []*github.WorkflowRun
|
||||||
|
|
||||||
|
workflowRuns = append(workflowRuns, queued...)
|
||||||
|
workflowRuns = append(workflowRuns, inProgress...)
|
||||||
|
|
||||||
|
return workflowRuns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) listRepositoryWorkflowRuns(ctx context.Context, user string, repoName, status string) ([]*github.WorkflowRun, error) {
|
||||||
|
c.Client.Actions.ListRepositoryWorkflowRuns(ctx, user, repoName, nil)
|
||||||
|
|
||||||
var workflowRuns []*github.WorkflowRun
|
var workflowRuns []*github.WorkflowRun
|
||||||
|
|
||||||
opts := github.ListWorkflowRunsOptions{
|
opts := github.ListWorkflowRunsOptions{
|
||||||
ListOptions: github.ListOptions{
|
ListOptions: github.ListOptions{
|
||||||
PerPage: 100,
|
PerPage: 100,
|
||||||
},
|
},
|
||||||
|
Status: status,
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -250,11 +275,8 @@ func getEnterpriseOrganisationAndRepo(enterprise, org, repo string) (string, str
|
|||||||
return "", "", "", fmt.Errorf("enterprise, organization and repository are all empty")
|
return "", "", "", fmt.Errorf("enterprise, organization and repository are all empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRegistrationKey(org, repo string) string {
|
func getRegistrationKey(org, repo, enterprise string) string {
|
||||||
if len(org) > 0 {
|
return fmt.Sprintf("org=%s,repo=%s,enterprise=%s", org, repo, enterprise)
|
||||||
return org
|
|
||||||
}
|
|
||||||
return repo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitOwnerAndRepo(repo string) (string, string, error) {
|
func splitOwnerAndRepo(repo string) (string, string, error) {
|
||||||
@@ -291,6 +313,14 @@ func (e *RunnerNotFound) Error() string {
|
|||||||
return fmt.Sprintf("runner %q not found", e.runnerName)
|
return fmt.Sprintf("runner %q not found", e.runnerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RunnerOffline struct {
|
||||||
|
runnerName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RunnerOffline) Error() string {
|
||||||
|
return fmt.Sprintf("runner %q offline", e.runnerName)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Client) IsRunnerBusy(ctx context.Context, enterprise, org, repo, name string) (bool, error) {
|
func (r *Client) IsRunnerBusy(ctx context.Context, enterprise, org, repo, name string) (bool, error) {
|
||||||
runners, err := r.ListRunners(ctx, enterprise, org, repo)
|
runners, err := r.ListRunners(ctx, enterprise, org, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -299,6 +329,9 @@ func (r *Client) IsRunnerBusy(ctx context.Context, enterprise, org, repo, name s
|
|||||||
|
|
||||||
for _, runner := range runners {
|
for _, runner := range runners {
|
||||||
if runner.GetName() == name {
|
if runner.GetName() == name {
|
||||||
|
if runner.GetStatus() == "offline" {
|
||||||
|
return false, &RunnerOffline{runnerName: name}
|
||||||
|
}
|
||||||
return runner.GetBusy(), nil
|
return runner.GetBusy(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
github/metrics/transport.go
Normal file
63
github/metrics/transport.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Package metrics provides monitoring of the GitHub related metrics.
|
||||||
|
//
|
||||||
|
// This depends on the metrics exporter of kubebuilder.
|
||||||
|
// See https://book.kubebuilder.io/reference/metrics.html for details.
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
metrics.Registry.MustRegister(metricRateLimit, metricRateLimitRemaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
|
||||||
|
metricRateLimit = prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "github_rate_limit",
|
||||||
|
Help: "The maximum number of requests you're permitted to make per hour",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
metricRateLimitRemaining = prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "github_rate_limit_remaining",
|
||||||
|
Help: "The number of requests remaining in the current rate limit window",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
|
||||||
|
headerRateLimit = "X-RateLimit-Limit"
|
||||||
|
headerRateLimitRemaining = "X-RateLimit-Remaining"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transport wraps a transport with metrics monitoring
|
||||||
|
type Transport struct {
|
||||||
|
Transport http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := t.Transport.RoundTrip(req)
|
||||||
|
if resp != nil {
|
||||||
|
parseResponse(resp)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponse(resp *http.Response) {
|
||||||
|
rateLimit, err := strconv.Atoi(resp.Header.Get(headerRateLimit))
|
||||||
|
if err == nil {
|
||||||
|
metricRateLimit.Set(float64(rateLimit))
|
||||||
|
}
|
||||||
|
rateLimitRemaining, err := strconv.Atoi(resp.Header.Get(headerRateLimitRemaining))
|
||||||
|
if err == nil {
|
||||||
|
metricRateLimitRemaining.Set(float64(rateLimitRemaining))
|
||||||
|
}
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -6,11 +6,13 @@ require (
|
|||||||
github.com/bradleyfalzon/ghinstallation v1.1.1
|
github.com/bradleyfalzon/ghinstallation v1.1.1
|
||||||
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-cmp v0.3.1
|
||||||
github.com/google/go-github/v33 v33.0.1-0.20210204004227-319dcffb518a
|
github.com/google/go-github/v33 v33.0.1-0.20210204004227-319dcffb518a
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/kelseyhightower/envconfig v1.4.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/prometheus/client_golang v0.9.2
|
||||||
github.com/stretchr/testify v1.4.0 // indirect
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||||
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
||||||
|
|||||||
28
main.go
28
main.go
@@ -20,6 +20,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kelseyhightower/envconfig"
|
"github.com/kelseyhightower/envconfig"
|
||||||
@@ -62,6 +63,8 @@ func main() {
|
|||||||
|
|
||||||
runnerImage string
|
runnerImage string
|
||||||
dockerImage string
|
dockerImage string
|
||||||
|
|
||||||
|
commonRunnerLabels commaSeparatedStringSlice
|
||||||
)
|
)
|
||||||
|
|
||||||
var c github.Config
|
var c github.Config
|
||||||
@@ -80,6 +83,7 @@ func main() {
|
|||||||
flag.Int64Var(&c.AppInstallationID, "github-app-installation-id", c.AppInstallationID, "The installation ID of GitHub App.")
|
flag.Int64Var(&c.AppInstallationID, "github-app-installation-id", c.AppInstallationID, "The installation ID of GitHub App.")
|
||||||
flag.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
|
flag.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
|
||||||
flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change")
|
flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change")
|
||||||
|
flag.Var(&commonRunnerLabels, "common-runner-labels", "Runner labels in the K1=V1,K2=V2,... format that are inherited all the runners created by the controller. See https://github.com/summerwind/actions-runner-controller/issues/321 for more information")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logger := zap.New(func(o *zap.Options) {
|
logger := zap.New(func(o *zap.Options) {
|
||||||
@@ -133,9 +137,10 @@ 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(),
|
||||||
|
CommonRunnerLabels: commonRunnerLabels,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = runnerDeploymentReconciler.SetupWithManager(mgr); err != nil {
|
if err = runnerDeploymentReconciler.SetupWithManager(mgr); err != nil {
|
||||||
@@ -176,3 +181,20 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type commaSeparatedStringSlice []string
|
||||||
|
|
||||||
|
func (s *commaSeparatedStringSlice) String() string {
|
||||||
|
return fmt.Sprintf("%v", *s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *commaSeparatedStringSlice) Set(value string) error {
|
||||||
|
for _, v := range strings.Split(value, ",") {
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = append(*s, v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
|||||||
|
|
||||||
RUN echo AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache > .env \
|
RUN echo AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache > .env \
|
||||||
&& mkdir /opt/hostedtoolcache \
|
&& mkdir /opt/hostedtoolcache \
|
||||||
&& chgrp runner /opt/hostedtoolcache \
|
&& chgrp docker /opt/hostedtoolcache \
|
||||||
&& chmod g+rwx /opt/hostedtoolcache
|
&& chmod g+rwx /opt/hostedtoolcache
|
||||||
|
|
||||||
COPY entrypoint.sh /
|
COPY entrypoint.sh /
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
|
|||||||
|
|
||||||
RUN echo AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache > /runner.env \
|
RUN echo AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache > /runner.env \
|
||||||
&& mkdir /opt/hostedtoolcache \
|
&& mkdir /opt/hostedtoolcache \
|
||||||
&& chgrp runner /opt/hostedtoolcache \
|
&& chgrp docker /opt/hostedtoolcache \
|
||||||
&& chmod g+rwx /opt/hostedtoolcache
|
&& chmod g+rwx /opt/hostedtoolcache
|
||||||
|
|
||||||
COPY modprobe startup.sh /usr/local/bin/
|
COPY modprobe startup.sh /usr/local/bin/
|
||||||
|
|||||||
Reference in New Issue
Block a user