Merge branch 'master' into master
This commit is contained in:
		
						commit
						9bd284fb7b
					
				|  | @ -18,7 +18,7 @@ on: | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
| env: | env: | ||||||
|   KUBE_SCORE_VERSION: 1.16.1 |   KUBE_SCORE_VERSION: 1.16.1 | ||||||
|   HELM_VERSION: v3.8.0 |   HELM_VERSION: v3.17.0 | ||||||
| 
 | 
 | ||||||
| permissions: | permissions: | ||||||
|   contents: read |   contents: read | ||||||
|  | @ -46,22 +46,6 @@ jobs: | ||||||
|         with: |         with: | ||||||
|           version: ${{ env.HELM_VERSION }} |           version: ${{ env.HELM_VERSION }} | ||||||
| 
 | 
 | ||||||
|       - name: Set up kube-score |  | ||||||
|         run: | |  | ||||||
|           wget https://github.com/zegl/kube-score/releases/download/v${{ env.KUBE_SCORE_VERSION }}/kube-score_${{ env.KUBE_SCORE_VERSION }}_linux_amd64 -O kube-score |  | ||||||
|           chmod 755 kube-score |  | ||||||
| 
 |  | ||||||
|       - name: Kube-score generated manifests |  | ||||||
|         run: helm template  --values charts/.ci/values-kube-score.yaml charts/* | ./kube-score score - |  | ||||||
|               --ignore-test pod-networkpolicy |  | ||||||
|               --ignore-test deployment-has-poddisruptionbudget |  | ||||||
|               --ignore-test deployment-has-host-podantiaffinity |  | ||||||
|               --ignore-test container-security-context |  | ||||||
|               --ignore-test pod-probes |  | ||||||
|               --ignore-test container-image-tag |  | ||||||
|               --enable-optional-test container-security-context-privileged |  | ||||||
|               --enable-optional-test container-security-context-readonlyrootfilesystem |  | ||||||
| 
 |  | ||||||
|       # python is a requirement for the chart-testing action below (supports yamllint among other tests) |       # python is a requirement for the chart-testing action below (supports yamllint among other tests) | ||||||
|       - uses: actions/setup-python@v5 |       - uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|  |  | ||||||
|  | @ -37,7 +37,6 @@ RUN --mount=target=. \ | ||||||
|   --mount=type=cache,mode=0777,target=${GOCACHE} \ |   --mount=type=cache,mode=0777,target=${GOCACHE} \ | ||||||
|   export GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} && \ |   export GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} && \ | ||||||
|   go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/manager main.go && \ |   go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/manager main.go && \ | ||||||
|   go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/github-runnerscaleset-listener ./cmd/githubrunnerscalesetlistener && \ |  | ||||||
|   go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/ghalistener ./cmd/ghalistener && \ |   go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}' -X 'github.com/actions/actions-runner-controller/build.CommitSHA=${COMMIT_SHA}'" -o /out/ghalistener ./cmd/ghalistener && \ | ||||||
|   go build -trimpath -ldflags="-s -w" -o /out/github-webhook-server ./cmd/githubwebhookserver && \ |   go build -trimpath -ldflags="-s -w" -o /out/github-webhook-server ./cmd/githubwebhookserver && \ | ||||||
|   go build -trimpath -ldflags="-s -w" -o /out/actions-metrics-server ./cmd/actionsmetricsserver && \ |   go build -trimpath -ldflags="-s -w" -o /out/actions-metrics-server ./cmd/actionsmetricsserver && \ | ||||||
|  | @ -52,7 +51,6 @@ WORKDIR / | ||||||
| COPY --from=builder /out/manager . | COPY --from=builder /out/manager . | ||||||
| COPY --from=builder /out/github-webhook-server . | COPY --from=builder /out/github-webhook-server . | ||||||
| COPY --from=builder /out/actions-metrics-server . | COPY --from=builder /out/actions-metrics-server . | ||||||
| COPY --from=builder /out/github-runnerscaleset-listener . |  | ||||||
| COPY --from=builder /out/ghalistener . | COPY --from=builder /out/ghalistener . | ||||||
| COPY --from=builder /out/sleep . | COPY --from=builder /out/sleep . | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										2
									
								
								Makefile
								
								
								
								
							|  | @ -87,7 +87,7 @@ test-with-deps: kube-apiserver etcd kubectl | ||||||
| # Build manager binary
 | # Build manager binary
 | ||||||
| manager: generate fmt vet | manager: generate fmt vet | ||||||
| 	go build -o bin/manager main.go | 	go build -o bin/manager main.go | ||||||
| 	go build -o bin/github-runnerscaleset-listener ./cmd/githubrunnerscalesetlistener | 	go build -o bin/github-runnerscaleset-listener ./cmd/ghalistener | ||||||
| 
 | 
 | ||||||
| # Run against the configured Kubernetes cluster in ~/.kube/config
 | # Run against the configured Kubernetes cluster in ~/.kube/config
 | ||||||
| run: generate fmt vet manifests | run: generate fmt vet manifests | ||||||
|  |  | ||||||
|  | @ -1,5 +1,3 @@ | ||||||
| Thank you for installing {{ .Chart.Name }}. | Thank you for installing {{ .Chart.Name }}. | ||||||
| 
 | 
 | ||||||
| Your release is named {{ .Release.Name }}. | Your release is named {{ .Release.Name }}. | ||||||
| 
 |  | ||||||
| WARNING: Older version of the listener (githubrunnerscalesetlistener) is deprecated and will be removed in the future gha-runner-scale-set-0.10.0 release. If you are using environment variable override to force the old listener, please remove the environment variable and use the new listener (ghalistener) instead. |  | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.autoscalingRunnerSet) }} | ||||||
| apiVersion: actions.github.com/v1alpha1 | apiVersion: actions.github.com/v1alpha1 | ||||||
| kind: AutoscalingRunnerSet | kind: AutoscalingRunnerSet | ||||||
| metadata: | metadata: | ||||||
|  | @ -10,9 +11,25 @@ metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.scale-set-name" . }} |   name: {{ include "gha-runner-scale-set.scale-set-name" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   namespace: {{ include "gha-runner-scale-set.namespace" . }} | ||||||
|   labels: |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.autoscalingRunnerSet.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     app.kubernetes.io/component: "autoscaling-runner-set" |     app.kubernetes.io/component: "autoscaling-runner-set" | ||||||
|     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|   annotations: |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.autoscalingRunnerSet.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     actions.github.com/values-hash: {{ toJson .Values | sha256sum | trunc 63 }} |     actions.github.com/values-hash: {{ toJson .Values | sha256sum | trunc 63 }} | ||||||
|     {{- $containerMode := .Values.containerMode }} |     {{- $containerMode := .Values.containerMode }} | ||||||
|     {{- if not (kindIs "string" .Values.githubConfigSecret) }} |     {{- if not (kindIs "string" .Values.githubConfigSecret) }} | ||||||
|  |  | ||||||
|  | @ -1,11 +1,29 @@ | ||||||
| {{- if not (kindIs "string" .Values.githubConfigSecret) }} | {{- if not (kindIs "string" .Values.githubConfigSecret) }} | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.githubConfigSecret) }} | ||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
| kind: Secret | kind: Secret | ||||||
| metadata: | metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.githubsecret" . }} |   name: {{ include "gha-runner-scale-set.githubsecret" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   namespace: {{ include "gha-runner-scale-set.namespace" . }} | ||||||
|   labels: |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.githubConfigSecret.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|  |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.githubConfigSecret.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|   finalizers: |   finalizers: | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
| data: | data: | ||||||
|  |  | ||||||
|  | @ -1,12 +1,28 @@ | ||||||
| {{- $containerMode := .Values.containerMode }} | {{- $containerMode := .Values.containerMode }} | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.kubernetesModeRole) }} | ||||||
| {{- if and (eq $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | {{- if and (eq $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | ||||||
| # default permission for runner pod service account in kubernetes mode (container hook) | # default permission for runner pod service account in kubernetes mode (container hook) | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| kind: Role | kind: Role | ||||||
| metadata: | metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.kubeModeRoleName" . }} |   name: {{ include "gha-runner-scale-set.kubeModeRoleName" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |     {{- toYaml . | nindent 4 }} | ||||||
|   finalizers: |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.kubernetesModeRole.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|  |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.kubernetesModeRole.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
| rules: | rules: | ||||||
| - apiGroups: [""] | - apiGroups: [""] | ||||||
|  |  | ||||||
|  | @ -1,10 +1,31 @@ | ||||||
| {{- $containerMode := .Values.containerMode }} | {{- $containerMode := .Values.containerMode }} | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.kubernetesModeRoleBinding) }} | ||||||
| {{- if and (eq $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | {{- if and (eq $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| kind: RoleBinding | kind: RoleBinding | ||||||
| metadata: | metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.kubeModeRoleBindingName" . }} |   name: {{ include "gha-runner-scale-set.kubeModeRoleBindingName" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   namespace: {{ include "gha-runner-scale-set.namespace" . }} | ||||||
|  |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.kubernetesModeRoleBinding.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|  | 
 | ||||||
|  |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.kubernetesModeRoleBinding.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|   finalizers: |   finalizers: | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
| roleRef: | roleRef: | ||||||
|  |  | ||||||
|  | @ -1,18 +1,32 @@ | ||||||
| {{- $containerMode := .Values.containerMode }} | {{- $containerMode := .Values.containerMode }} | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.kubernetesModeServiceAccount) }} | ||||||
| {{- if and (eq $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | {{- if and (eq $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | ||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
| kind: ServiceAccount | kind: ServiceAccount | ||||||
| metadata: | metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.kubeModeServiceAccountName" . }} |   name: {{ include "gha-runner-scale-set.kubeModeServiceAccountName" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   {{- if or .Values.annotations $hasCustomResourceMeta }} | ||||||
|   {{- if .Values.containerMode.kubernetesModeServiceAccount }} |  | ||||||
|   {{- with .Values.containerMode.kubernetesModeServiceAccount.annotations }} |  | ||||||
|   annotations: |   annotations: | ||||||
|   {{- toYaml . | nindent 4 }} |     {{- with .Values.annotations }} | ||||||
|   {{- end }} |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.kubernetesModeServiceAccount.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|   {{- end }} |   {{- end }} | ||||||
|  |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.kubernetesModeServiceAccount.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|  | 
 | ||||||
|   finalizers: |   finalizers: | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
|   labels: |  | ||||||
|     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} |  | ||||||
| {{- end }} | {{- end }} | ||||||
|  |  | ||||||
|  | @ -1,11 +1,29 @@ | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.managerRole) }} | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| kind: Role | kind: Role | ||||||
| metadata: | metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.managerRoleName" . }} |   name: {{ include "gha-runner-scale-set.managerRoleName" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   namespace: {{ include "gha-runner-scale-set.namespace" . }} | ||||||
|   labels: |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.managerRole.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|     app.kubernetes.io/component: manager-role |     app.kubernetes.io/component: manager-role | ||||||
|  |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.managerRole.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|   finalizers: |   finalizers: | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
| rules: | rules: | ||||||
|  |  | ||||||
|  | @ -1,11 +1,29 @@ | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.managerRoleBinding) }} | ||||||
| apiVersion: rbac.authorization.k8s.io/v1 | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
| kind: RoleBinding | kind: RoleBinding | ||||||
| metadata: | metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.managerRoleBindingName" . }} |   name: {{ include "gha-runner-scale-set.managerRoleBindingName" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   namespace: {{ include "gha-runner-scale-set.namespace" . }} | ||||||
|   labels: |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.managerRoleBinding.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|     app.kubernetes.io/component: manager-role-binding |     app.kubernetes.io/component: manager-role-binding | ||||||
|  |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.managerRoleBinding.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|   finalizers: |   finalizers: | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
| roleRef: | roleRef: | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | {{- $hasCustomResourceMeta := (and .Values.resourceMeta .Values.resourceMeta.noPermissionServiceAccount) }} | ||||||
| {{- $containerMode := .Values.containerMode }} | {{- $containerMode := .Values.containerMode }} | ||||||
| {{- if and (ne $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | {{- if and (ne $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} | ||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
|  | @ -6,7 +7,24 @@ metadata: | ||||||
|   name: {{ include "gha-runner-scale-set.noPermissionServiceAccountName" . }} |   name: {{ include "gha-runner-scale-set.noPermissionServiceAccountName" . }} | ||||||
|   namespace: {{ include "gha-runner-scale-set.namespace" . }} |   namespace: {{ include "gha-runner-scale-set.namespace" . }} | ||||||
|   labels: |   labels: | ||||||
|  |     {{- with .Values.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.noPermissionServiceAccount.labels }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} |     {{- include "gha-runner-scale-set.labels" . | nindent 4 }} | ||||||
|  |   annotations: | ||||||
|  |     {{- with .Values.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- if $hasCustomResourceMeta }} | ||||||
|  |     {{- with .Values.resourceMeta.noPermissionServiceAccount.annotations }} | ||||||
|  |     {{- toYaml . | nindent 4 }} | ||||||
|  |     {{- end }} | ||||||
|  |     {{- end }} | ||||||
|   finalizers: |   finalizers: | ||||||
|     - actions.github.com/cleanup-protection |     - actions.github.com/cleanup-protection | ||||||
| {{- end }} | {{- end }} | ||||||
|  |  | ||||||
|  | @ -743,37 +743,6 @@ func TestTemplateRenderedAutoScalingRunnerSet_DinD_ExtraInitContainers(t *testin | ||||||
| 	assert.Equal(t, "ls", ars.Spec.Template.Spec.InitContainers[2].Command[0], "InitContainers[2] Command[0] should be ls") | 	assert.Equal(t, "ls", ars.Spec.Template.Spec.InitContainers[2].Command[0], "InitContainers[2] Command[0] should be ls") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestTemplateRenderedKubernetesModeServiceAccountAnnotations(t *testing.T) { |  | ||||||
| 	t.Parallel() |  | ||||||
| 
 |  | ||||||
| 	// Path to the helm chart we will test
 |  | ||||||
| 	helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	testValuesPath, err := filepath.Abs("../tests/values_kubernetes_mode_service_account_annotations.yaml") |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	releaseName := "test-runners" |  | ||||||
| 	namespaceName := "test-" + strings.ToLower(random.UniqueId()) |  | ||||||
| 
 |  | ||||||
| 	options := &helm.Options{ |  | ||||||
| 		Logger: logger.Discard, |  | ||||||
| 		SetValues: map[string]string{ |  | ||||||
| 			"controllerServiceAccount.name":      "arc", |  | ||||||
| 			"controllerServiceAccount.namespace": "arc-system", |  | ||||||
| 		}, |  | ||||||
| 		ValuesFiles:    []string{testValuesPath}, |  | ||||||
| 		KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_serviceaccount.yaml"}) |  | ||||||
| 
 |  | ||||||
| 	var sa corev1.ServiceAccount |  | ||||||
| 	helm.UnmarshalK8SYaml(t, output, &sa) |  | ||||||
| 
 |  | ||||||
| 	assert.Equal(t, "arn:aws:iam::123456789012:role/sample-role", sa.Annotations["eks.amazonaws.com/role-arn"], "Annotations should be arn:aws:iam::123456789012:role/sample-role") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestTemplateRenderedAutoScalingRunnerSet_DinD_ExtraVolumes(t *testing.T) { | func TestTemplateRenderedAutoScalingRunnerSet_DinD_ExtraVolumes(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 
 | 
 | ||||||
|  | @ -2145,6 +2114,209 @@ func TestAutoscalingRunnerSetAnnotationValuesHash(t *testing.T) { | ||||||
| 	assert.LessOrEqual(t, len(secondHash), 63) | 	assert.LessOrEqual(t, len(secondHash), 63) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestCustomLabels(t *testing.T) { | ||||||
|  | 	t.Parallel() | ||||||
|  | 
 | ||||||
|  | 	// Path to the helm chart we will test
 | ||||||
|  | 	helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	releaseName := "test-runners" | ||||||
|  | 	namespaceName := "test-" + strings.ToLower(random.UniqueId()) | ||||||
|  | 
 | ||||||
|  | 	options := &helm.Options{ | ||||||
|  | 		Logger: logger.Discard, | ||||||
|  | 		SetValues: map[string]string{ | ||||||
|  | 			"githubConfigUrl":                                              "https://github.com/actions", | ||||||
|  | 			"githubConfigSecret.github_token":                              "gh_token12345", | ||||||
|  | 			"controllerServiceAccount.name":                                "arc", | ||||||
|  | 			"containerMode.type":                                           "kubernetes", | ||||||
|  | 			"controllerServiceAccount.namespace":                           "arc-system", | ||||||
|  | 			`labels.argocd\.argoproj\.io/sync-wave`:                        `"1"`, | ||||||
|  | 			`labels.app\.kubernetes\.io/part-of`:                           "no-override", // this shouldn't be overwritten
 | ||||||
|  | 			"resourceMeta.autoscalingRunnerSet.labels.ars-custom":          "ars-custom-value", | ||||||
|  | 			"resourceMeta.githubConfigSecret.labels.gh-custom":             "gh-custom-value", | ||||||
|  | 			"resourceMeta.kubernetesModeRole.labels.kmr-custom":            "kmr-custom-value", | ||||||
|  | 			"resourceMeta.kubernetesModeRoleBinding.labels.kmrb-custom":    "kmrb-custom-value", | ||||||
|  | 			"resourceMeta.kubernetesModeServiceAccount.labels.kmsa-custom": "kmsa-custom-value", | ||||||
|  | 			"resourceMeta.managerRole.labels.mr-custom":                    "mr-custom-value", | ||||||
|  | 			"resourceMeta.managerRoleBinding.labels.mrb-custom":            "mrb-custom-value", | ||||||
|  | 		}, | ||||||
|  | 		KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/githubsecret.yaml"}) | ||||||
|  | 
 | ||||||
|  | 	const targetLabel = "argocd.argoproj.io/sync-wave" | ||||||
|  | 	const wantCustomValue = `"1"` | ||||||
|  | 	const reservedLabel = "app.kubernetes.io/part-of" | ||||||
|  | 	const wantReservedValue = "gha-rs" | ||||||
|  | 
 | ||||||
|  | 	var githubSecret corev1.Secret | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &githubSecret) | ||||||
|  | 	assert.Equal(t, wantCustomValue, githubSecret.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, githubSecret.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "gh-custom-value", githubSecret.Labels["gh-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_role.yaml"}) | ||||||
|  | 	var role rbacv1.Role | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &role) | ||||||
|  | 	assert.Equal(t, wantCustomValue, role.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, role.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "kmr-custom-value", role.Labels["kmr-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_role_binding.yaml"}) | ||||||
|  | 	var roleBinding rbacv1.RoleBinding | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &roleBinding) | ||||||
|  | 	assert.Equal(t, wantCustomValue, roleBinding.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, roleBinding.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "kmrb-custom-value", roleBinding.Labels["kmrb-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}) | ||||||
|  | 	var ars v1alpha1.AutoscalingRunnerSet | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &ars) | ||||||
|  | 	assert.Equal(t, wantCustomValue, ars.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, ars.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "ars-custom-value", ars.Labels["ars-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_serviceaccount.yaml"}) | ||||||
|  | 	var serviceAccount corev1.ServiceAccount | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &serviceAccount) | ||||||
|  | 	assert.Equal(t, wantCustomValue, serviceAccount.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, serviceAccount.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "kmsa-custom-value", serviceAccount.Labels["kmsa-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/manager_role.yaml"}) | ||||||
|  | 	var managerRole rbacv1.Role | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &managerRole) | ||||||
|  | 	assert.Equal(t, wantCustomValue, managerRole.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, managerRole.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "mr-custom-value", managerRole.Labels["mr-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/manager_role_binding.yaml"}) | ||||||
|  | 	var managerRoleBinding rbacv1.RoleBinding | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &managerRoleBinding) | ||||||
|  | 	assert.Equal(t, wantCustomValue, managerRoleBinding.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, managerRoleBinding.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "mrb-custom-value", managerRoleBinding.Labels["mrb-custom"]) | ||||||
|  | 
 | ||||||
|  | 	options = &helm.Options{ | ||||||
|  | 		Logger: logger.Discard, | ||||||
|  | 		SetValues: map[string]string{ | ||||||
|  | 			"githubConfigUrl":                                            "https://github.com/actions", | ||||||
|  | 			"githubConfigSecret.github_token":                            "gh_token12345", | ||||||
|  | 			"controllerServiceAccount.name":                              "arc", | ||||||
|  | 			"controllerServiceAccount.namespace":                         "arc-system", | ||||||
|  | 			`labels.argocd\.argoproj\.io/sync-wave`:                      `"1"`, | ||||||
|  | 			"resourceMeta.noPermissionServiceAccount.labels.npsa-custom": "npsa-custom-value", | ||||||
|  | 		}, | ||||||
|  | 		KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/no_permission_serviceaccount.yaml"}) | ||||||
|  | 	var noPermissionServiceAccount corev1.ServiceAccount | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &noPermissionServiceAccount) | ||||||
|  | 	assert.Equal(t, wantCustomValue, noPermissionServiceAccount.Labels[targetLabel]) | ||||||
|  | 	assert.Equal(t, wantReservedValue, noPermissionServiceAccount.Labels[reservedLabel]) | ||||||
|  | 	assert.Equal(t, "npsa-custom-value", noPermissionServiceAccount.Labels["npsa-custom"]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestCustomAnnotations(t *testing.T) { | ||||||
|  | 	t.Parallel() | ||||||
|  | 
 | ||||||
|  | 	// Path to the helm chart we will test
 | ||||||
|  | 	helmChartPath, err := filepath.Abs("../../gha-runner-scale-set") | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	releaseName := "test-runners" | ||||||
|  | 	namespaceName := "test-" + strings.ToLower(random.UniqueId()) | ||||||
|  | 
 | ||||||
|  | 	options := &helm.Options{ | ||||||
|  | 		Logger: logger.Discard, | ||||||
|  | 		SetValues: map[string]string{ | ||||||
|  | 			"githubConfigUrl":                                                   "https://github.com/actions", | ||||||
|  | 			"githubConfigSecret.github_token":                                   "gh_token12345", | ||||||
|  | 			"containerMode.type":                                                "kubernetes", | ||||||
|  | 			"controllerServiceAccount.name":                                     "arc", | ||||||
|  | 			"controllerServiceAccount.namespace":                                "arc-system", | ||||||
|  | 			`annotations.argocd\.argoproj\.io/sync-wave`:                        `"1"`, | ||||||
|  | 			"resourceMeta.autoscalingRunnerSet.annotations.ars-custom":          "ars-custom-value", | ||||||
|  | 			"resourceMeta.githubConfigSecret.annotations.gh-custom":             "gh-custom-value", | ||||||
|  | 			"resourceMeta.kubernetesModeRole.annotations.kmr-custom":            "kmr-custom-value", | ||||||
|  | 			"resourceMeta.kubernetesModeRoleBinding.annotations.kmrb-custom":    "kmrb-custom-value", | ||||||
|  | 			"resourceMeta.kubernetesModeServiceAccount.annotations.kmsa-custom": "kmsa-custom-value", | ||||||
|  | 			"resourceMeta.managerRole.annotations.mr-custom":                    "mr-custom-value", | ||||||
|  | 			"resourceMeta.managerRoleBinding.annotations.mrb-custom":            "mrb-custom-value", | ||||||
|  | 		}, | ||||||
|  | 		KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const targetAnnotations = "argocd.argoproj.io/sync-wave" | ||||||
|  | 	const wantCustomValue = `"1"` | ||||||
|  | 
 | ||||||
|  | 	output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/githubsecret.yaml"}) | ||||||
|  | 
 | ||||||
|  | 	var githubSecret corev1.Secret | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &githubSecret) | ||||||
|  | 	assert.Equal(t, wantCustomValue, githubSecret.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "gh-custom-value", githubSecret.Annotations["gh-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_role.yaml"}) | ||||||
|  | 	var role rbacv1.Role | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &role) | ||||||
|  | 	assert.Equal(t, wantCustomValue, role.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "kmr-custom-value", role.Annotations["kmr-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_role_binding.yaml"}) | ||||||
|  | 	var roleBinding rbacv1.RoleBinding | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &roleBinding) | ||||||
|  | 	assert.Equal(t, wantCustomValue, roleBinding.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "kmrb-custom-value", roleBinding.Annotations["kmrb-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"}) | ||||||
|  | 	var ars v1alpha1.AutoscalingRunnerSet | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &ars) | ||||||
|  | 	assert.Equal(t, wantCustomValue, ars.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "ars-custom-value", ars.Annotations["ars-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/kube_mode_serviceaccount.yaml"}) | ||||||
|  | 	var serviceAccount corev1.ServiceAccount | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &serviceAccount) | ||||||
|  | 	assert.Equal(t, wantCustomValue, serviceAccount.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "kmsa-custom-value", serviceAccount.Annotations["kmsa-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/manager_role.yaml"}) | ||||||
|  | 	var managerRole rbacv1.Role | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &managerRole) | ||||||
|  | 	assert.Equal(t, wantCustomValue, managerRole.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "mr-custom-value", managerRole.Annotations["mr-custom"]) | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/manager_role_binding.yaml"}) | ||||||
|  | 	var managerRoleBinding rbacv1.RoleBinding | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &managerRoleBinding) | ||||||
|  | 	assert.Equal(t, wantCustomValue, managerRoleBinding.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "mrb-custom-value", managerRoleBinding.Annotations["mrb-custom"]) | ||||||
|  | 
 | ||||||
|  | 	options = &helm.Options{ | ||||||
|  | 		Logger: logger.Discard, | ||||||
|  | 		SetValues: map[string]string{ | ||||||
|  | 			"githubConfigUrl":                                                 "https://github.com/actions", | ||||||
|  | 			"githubConfigSecret.github_token":                                 "gh_token12345", | ||||||
|  | 			"controllerServiceAccount.name":                                   "arc", | ||||||
|  | 			"controllerServiceAccount.namespace":                              "arc-system", | ||||||
|  | 			`annotations.argocd\.argoproj\.io/sync-wave`:                      `"1"`, | ||||||
|  | 			"resourceMeta.noPermissionServiceAccount.annotations.npsa-custom": "npsa-custom-value", | ||||||
|  | 		}, | ||||||
|  | 		KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/no_permission_serviceaccount.yaml"}) | ||||||
|  | 	var noPermissionServiceAccount corev1.ServiceAccount | ||||||
|  | 	helm.UnmarshalK8SYaml(t, output, &noPermissionServiceAccount) | ||||||
|  | 	assert.Equal(t, wantCustomValue, noPermissionServiceAccount.Annotations[targetAnnotations]) | ||||||
|  | 	assert.Equal(t, "npsa-custom-value", noPermissionServiceAccount.Annotations["npsa-custom"]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestNamespaceOverride(t *testing.T) { | func TestNamespaceOverride(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| githubConfigUrl: https://github.com/actions/actions-runner-controller |  | ||||||
| githubConfigSecret: |  | ||||||
|   github_token: test |  | ||||||
| containerMode: |  | ||||||
|   type: kubernetes |  | ||||||
|   kubernetesModeServiceAccount: |  | ||||||
|     annotations: |  | ||||||
|       eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/sample-role |  | ||||||
|  | @ -4,15 +4,15 @@ githubConfigUrl: "" | ||||||
| 
 | 
 | ||||||
| ## githubConfigSecret is the k8s secret information to use when authenticating via the GitHub API. | ## githubConfigSecret is the k8s secret information to use when authenticating via the GitHub API. | ||||||
| ## You can choose to supply: | ## You can choose to supply: | ||||||
| ##   A) a PAT token,  | ##   A) a PAT token, | ||||||
| ##   B) a GitHub App, or  | ##   B) a GitHub App, or | ||||||
| ##   C) a pre-defined Kubernetes secret. | ##   C) a pre-defined Kubernetes secret. | ||||||
| ## The syntax for each of these variations is documented below. | ## The syntax for each of these variations is documented below. | ||||||
| ## (Variation A) When using a PAT token, the syntax is as follows: | ## (Variation A) When using a PAT token, the syntax is as follows: | ||||||
| githubConfigSecret: | githubConfigSecret: | ||||||
|   # Example:   |   # Example: | ||||||
|   # github_token: "ghp_sampleSampleSampleSampleSampleSample" |   # github_token: "ghp_sampleSampleSampleSampleSampleSample" | ||||||
|   github_token: ""   |   github_token: "" | ||||||
| # | # | ||||||
| ## (Variation B) When using a GitHub App, the syntax is as follows: | ## (Variation B) When using a GitHub App, the syntax is as follows: | ||||||
| # githubConfigSecret: | # githubConfigSecret: | ||||||
|  | @ -100,8 +100,7 @@ githubConfigSecret: | ||||||
| #     resources: | #     resources: | ||||||
| #       requests: | #       requests: | ||||||
| #         storage: 1Gi | #         storage: 1Gi | ||||||
| #   kubernetesModeServiceAccount: | # | ||||||
| #     annotations: |  | ||||||
| 
 | 
 | ||||||
| ## listenerTemplate is the PodSpec for each listener Pod | ## listenerTemplate is the PodSpec for each listener Pod | ||||||
| ## For reference: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec | ## For reference: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec | ||||||
|  | @ -219,3 +218,63 @@ template: | ||||||
| 
 | 
 | ||||||
| # Overrides the default `.Release.Namespace` for all resources in this chart. | # Overrides the default `.Release.Namespace` for all resources in this chart. | ||||||
| namespaceOverride: "" | namespaceOverride: "" | ||||||
|  | 
 | ||||||
|  | ## Optional annotations and labels applied to all resources created by helm installation | ||||||
|  | ## | ||||||
|  | ## Annotations applied to all resources created by this helm chart. Annotations will not override the default ones, so make sure | ||||||
|  | ## the custom annotation is not reserved. | ||||||
|  | # annotations: | ||||||
|  | #   key: value | ||||||
|  | ## | ||||||
|  | ## Labels applied to all resources created by this helm chart. Labels will not override the default ones, so make sure | ||||||
|  | ## the custom label is not reserved. | ||||||
|  | # labels: | ||||||
|  | #   key: value | ||||||
|  | 
 | ||||||
|  | ## If you want more fine-grained control over annotations applied to particular resource created by this chart, | ||||||
|  | ## you can use `resourceMeta`. | ||||||
|  | ## Order of applying labels and annotations is: | ||||||
|  | ## 1. Apply labels/annotations globally, using `annotations` and `labels` field | ||||||
|  | ## 2. Apply `resourceMeta` labels/annotations | ||||||
|  | ## 3. Apply reserved labels/annotations | ||||||
|  | # resourceMeta: | ||||||
|  | #   autoscalingRunnerSet: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   githubConfigSecret: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   kubernetesModeRole: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   kubernetesModeRoleBinding: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   kubernetesModeServiceAccount: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   managerRole: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   managerRoleBinding: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  | #   noPermissionServiceAccount: | ||||||
|  | #     labels: | ||||||
|  | #       key: value | ||||||
|  | #     annotations: | ||||||
|  | #       key: value | ||||||
|  |  | ||||||
|  | @ -16,22 +16,22 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type Config struct { | type Config struct { | ||||||
| 	ConfigureUrl                string `json:"configureUrl"` | 	ConfigureUrl                string `json:"configure_url"` | ||||||
| 	AppID                       int64  `json:"appID"` | 	AppID                       int64  `json:"app_id"` | ||||||
| 	AppInstallationID           int64  `json:"appInstallationID"` | 	AppInstallationID           int64  `json:"app_installation_id"` | ||||||
| 	AppPrivateKey               string `json:"appPrivateKey"` | 	AppPrivateKey               string `json:"app_private_key"` | ||||||
| 	Token                       string `json:"token"` | 	Token                       string `json:"token"` | ||||||
| 	EphemeralRunnerSetNamespace string `json:"ephemeralRunnerSetNamespace"` | 	EphemeralRunnerSetNamespace string `json:"ephemeral_runner_set_namespace"` | ||||||
| 	EphemeralRunnerSetName      string `json:"ephemeralRunnerSetName"` | 	EphemeralRunnerSetName      string `json:"ephemeral_runner_set_name"` | ||||||
| 	MaxRunners                  int    `json:"maxRunners"` | 	MaxRunners                  int    `json:"max_runners"` | ||||||
| 	MinRunners                  int    `json:"minRunners"` | 	MinRunners                  int    `json:"min_runners"` | ||||||
| 	RunnerScaleSetId            int    `json:"runnerScaleSetId"` | 	RunnerScaleSetId            int    `json:"runner_scale_set_id"` | ||||||
| 	RunnerScaleSetName          string `json:"runnerScaleSetName"` | 	RunnerScaleSetName          string `json:"runner_scale_set_name"` | ||||||
| 	ServerRootCA                string `json:"serverRootCA"` | 	ServerRootCA                string `json:"server_root_ca"` | ||||||
| 	LogLevel                    string `json:"logLevel"` | 	LogLevel                    string `json:"log_level"` | ||||||
| 	LogFormat                   string `json:"logFormat"` | 	LogFormat                   string `json:"log_format"` | ||||||
| 	MetricsAddr                 string `json:"metricsAddr"` | 	MetricsAddr                 string `json:"metrics_addr"` | ||||||
| 	MetricsEndpoint             string `json:"metricsEndpoint"` | 	MetricsEndpoint             string `json:"metrics_endpoint"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Read(path string) (Config, error) { | func Read(path string) (Config, error) { | ||||||
|  |  | ||||||
|  | @ -1,129 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" |  | ||||||
| 	jsonpatch "github.com/evanphx/json-patch" |  | ||||||
| 	"github.com/go-logr/logr" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	"k8s.io/client-go/kubernetes" |  | ||||||
| 	"k8s.io/client-go/rest" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type AutoScalerKubernetesManager struct { |  | ||||||
| 	*kubernetes.Clientset |  | ||||||
| 
 |  | ||||||
| 	logger logr.Logger |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewKubernetesManager(logger *logr.Logger) (*AutoScalerKubernetesManager, error) { |  | ||||||
| 	conf, err := rest.InClusterConfig() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	kubeClient, err := kubernetes.NewForConfig(conf) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var manager = &AutoScalerKubernetesManager{ |  | ||||||
| 		Clientset: kubeClient, |  | ||||||
| 		logger:    logger.WithName("KubernetesManager"), |  | ||||||
| 	} |  | ||||||
| 	return manager, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (k *AutoScalerKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace, resourceName string, runnerCount int) error { |  | ||||||
| 	original := &v1alpha1.EphemeralRunnerSet{ |  | ||||||
| 		Spec: v1alpha1.EphemeralRunnerSetSpec{ |  | ||||||
| 			Replicas: -1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	originalJson, err := json.Marshal(original) |  | ||||||
| 	if err != nil { |  | ||||||
| 		k.logger.Error(err, "could not marshal empty ephemeral runner set") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	patch := &v1alpha1.EphemeralRunnerSet{ |  | ||||||
| 		Spec: v1alpha1.EphemeralRunnerSetSpec{ |  | ||||||
| 			Replicas: runnerCount, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	patchJson, err := json.Marshal(patch) |  | ||||||
| 	if err != nil { |  | ||||||
| 		k.logger.Error(err, "could not marshal patch ephemeral runner set") |  | ||||||
| 	} |  | ||||||
| 	mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchJson) |  | ||||||
| 	if err != nil { |  | ||||||
| 		k.logger.Error(err, "could not create merge patch json for ephemeral runner set") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	k.logger.Info("Created merge patch json for EphemeralRunnerSet update", "json", string(mergePatch)) |  | ||||||
| 
 |  | ||||||
| 	patchedEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{} |  | ||||||
| 	err = k.RESTClient(). |  | ||||||
| 		Patch(types.MergePatchType). |  | ||||||
| 		Prefix("apis", "actions.github.com", "v1alpha1"). |  | ||||||
| 		Namespace(namespace). |  | ||||||
| 		Resource("EphemeralRunnerSets"). |  | ||||||
| 		Name(resourceName). |  | ||||||
| 		Body([]byte(mergePatch)). |  | ||||||
| 		Do(ctx). |  | ||||||
| 		Into(patchedEphemeralRunnerSet) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("could not patch ephemeral runner set , patch JSON: %s, error: %w", string(mergePatch), err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	k.logger.Info("Ephemeral runner set scaled.", "namespace", namespace, "name", resourceName, "replicas", patchedEphemeralRunnerSet.Spec.Replicas) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (k *AutoScalerKubernetesManager) UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName string, workflowRunId, jobRequestId int64) error { |  | ||||||
| 	original := &v1alpha1.EphemeralRunner{} |  | ||||||
| 	originalJson, err := json.Marshal(original) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("could not marshal empty ephemeral runner, error: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	patch := &v1alpha1.EphemeralRunner{ |  | ||||||
| 		Status: v1alpha1.EphemeralRunnerStatus{ |  | ||||||
| 			JobRequestId:      jobRequestId, |  | ||||||
| 			JobRepositoryName: fmt.Sprintf("%s/%s", ownerName, repositoryName), |  | ||||||
| 			WorkflowRunId:     workflowRunId, |  | ||||||
| 			JobWorkflowRef:    jobWorkflowRef, |  | ||||||
| 			JobDisplayName:    jobDisplayName, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	patchedJson, err := json.Marshal(patch) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("could not marshal patched ephemeral runner, error: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchedJson) |  | ||||||
| 	if err != nil { |  | ||||||
| 		k.logger.Error(err, "could not create merge patch json for ephemeral runner") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	k.logger.Info("Created merge patch json for EphemeralRunner status update", "json", string(mergePatch)) |  | ||||||
| 
 |  | ||||||
| 	patchedStatus := &v1alpha1.EphemeralRunner{} |  | ||||||
| 	err = k.RESTClient(). |  | ||||||
| 		Patch(types.MergePatchType). |  | ||||||
| 		Prefix("apis", "actions.github.com", "v1alpha1"). |  | ||||||
| 		Namespace(namespace). |  | ||||||
| 		Resource("EphemeralRunners"). |  | ||||||
| 		Name(resourceName). |  | ||||||
| 		SubResource("status"). |  | ||||||
| 		Body(mergePatch). |  | ||||||
| 		Do(ctx). |  | ||||||
| 		Into(patchedStatus) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("could not patch ephemeral runner status, patch JSON: %s, error: %w", string(mergePatch), err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -1,191 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"math/rand" |  | ||||||
| 	"net/http" |  | ||||||
| 	"os" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/go-logr/logr" |  | ||||||
| 	"github.com/google/uuid" |  | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	sessionCreationMaxRetryCount = 10 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type devContextKey bool |  | ||||||
| 
 |  | ||||||
| var testIgnoreSleep devContextKey = true |  | ||||||
| 
 |  | ||||||
| type AutoScalerClient struct { |  | ||||||
| 	client actions.SessionService |  | ||||||
| 	logger logr.Logger |  | ||||||
| 
 |  | ||||||
| 	lastMessageId  int64 |  | ||||||
| 	initialMessage *actions.RunnerScaleSetMessage |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewAutoScalerClient( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	client actions.ActionsService, |  | ||||||
| 	logger *logr.Logger, |  | ||||||
| 	runnerScaleSetId int, |  | ||||||
| 	options ...func(*AutoScalerClient), |  | ||||||
| ) (*AutoScalerClient, error) { |  | ||||||
| 	listener := AutoScalerClient{ |  | ||||||
| 		logger: logger.WithName("auto_scaler"), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	session, initialMessage, err := createSession(ctx, &listener.logger, client, runnerScaleSetId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("fail to create session. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	listener.lastMessageId = 0 |  | ||||||
| 	listener.initialMessage = initialMessage |  | ||||||
| 	listener.client = newSessionClient(client, logger, session) |  | ||||||
| 
 |  | ||||||
| 	for _, option := range options { |  | ||||||
| 		option(&listener) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return &listener, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func createSession(ctx context.Context, logger *logr.Logger, client actions.ActionsService, runnerScaleSetId int) (*actions.RunnerScaleSetSession, *actions.RunnerScaleSetMessage, error) { |  | ||||||
| 	hostName, err := os.Hostname() |  | ||||||
| 	if err != nil { |  | ||||||
| 		hostName = uuid.New().String() |  | ||||||
| 		logger.Info("could not get hostname, fail back to a random string.", "fallback", hostName) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var runnerScaleSetSession *actions.RunnerScaleSetSession |  | ||||||
| 	var retryCount int |  | ||||||
| 	for { |  | ||||||
| 		runnerScaleSetSession, err = client.CreateMessageSession(ctx, runnerScaleSetId, hostName) |  | ||||||
| 		if err == nil { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		clientSideError := &actions.HttpClientSideError{} |  | ||||||
| 		if errors.As(err, &clientSideError) && clientSideError.Code != http.StatusConflict { |  | ||||||
| 			logger.Info("unable to create message session. The error indicates something is wrong on the client side, won't make any retry.") |  | ||||||
| 			return nil, nil, fmt.Errorf("create message session http request failed. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		retryCount++ |  | ||||||
| 		if retryCount >= sessionCreationMaxRetryCount { |  | ||||||
| 			return nil, nil, fmt.Errorf("create message session failed since it exceed %d retry limit. %w", sessionCreationMaxRetryCount, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		logger.Info("unable to create message session. Will try again in 30 seconds", "error", err.Error()) |  | ||||||
| 		if ok := ctx.Value(testIgnoreSleep); ok == nil { |  | ||||||
| 			time.Sleep(getRandomDuration(30, 45)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	statistics, _ := json.Marshal(runnerScaleSetSession.Statistics) |  | ||||||
| 	logger.Info("current runner scale set statistics.", "statistics", string(statistics)) |  | ||||||
| 
 |  | ||||||
| 	if runnerScaleSetSession.Statistics.TotalAvailableJobs > 0 || runnerScaleSetSession.Statistics.TotalAssignedJobs > 0 { |  | ||||||
| 		acquirableJobs, err := client.GetAcquirableJobs(ctx, runnerScaleSetId) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, nil, fmt.Errorf("get acquirable jobs failed. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		acquirableJobsJson, err := json.Marshal(acquirableJobs.Jobs) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, nil, fmt.Errorf("marshal acquirable jobs failed. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		initialMessage := &actions.RunnerScaleSetMessage{ |  | ||||||
| 			MessageId:   0, |  | ||||||
| 			MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 			Statistics:  runnerScaleSetSession.Statistics, |  | ||||||
| 			Body:        string(acquirableJobsJson), |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return runnerScaleSetSession, initialMessage, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	initialMessage := &actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   0, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics:  runnerScaleSetSession.Statistics, |  | ||||||
| 		Body:        "", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return runnerScaleSetSession, initialMessage, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *AutoScalerClient) Close() error { |  | ||||||
| 	m.logger.Info("closing.") |  | ||||||
| 	return m.client.Close() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *AutoScalerClient) GetRunnerScaleSetMessage(ctx context.Context, handler func(msg *actions.RunnerScaleSetMessage) error, maxCapacity int) error { |  | ||||||
| 	if m.initialMessage != nil { |  | ||||||
| 		err := handler(m.initialMessage) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("fail to process initial message. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		m.initialMessage = nil |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for { |  | ||||||
| 		message, err := m.client.GetMessage(ctx, m.lastMessageId, maxCapacity) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("get message failed from refreshing client. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if message == nil { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		err = handler(message) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("handle message failed. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		m.lastMessageId = message.MessageId |  | ||||||
| 
 |  | ||||||
| 		return m.deleteMessage(ctx, message.MessageId) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *AutoScalerClient) deleteMessage(ctx context.Context, messageId int64) error { |  | ||||||
| 	err := m.client.DeleteMessage(ctx, messageId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("delete message failed from refreshing client. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.logger.Info("deleted message.", "messageId", messageId) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *AutoScalerClient) AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error { |  | ||||||
| 	m.logger.Info("acquiring jobs.", "request count", len(requestIds), "requestIds", fmt.Sprint(requestIds)) |  | ||||||
| 	if len(requestIds) == 0 { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ids, err := m.client.AcquireJobs(ctx, requestIds) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("acquire jobs failed from refreshing client. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.logger.Info("acquired jobs.", "requested", len(requestIds), "acquired", len(ids)) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getRandomDuration(minSeconds, maxSeconds int) time.Duration { |  | ||||||
| 	return time.Duration(rand.Intn(maxSeconds-minSeconds)+minSeconds) * time.Second |  | ||||||
| } |  | ||||||
|  | @ -1,735 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/actions/actions-runner-controller/logging" |  | ||||||
| 	"github.com/google/uuid" |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/mock" |  | ||||||
| 	"github.com/stretchr/testify/require" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestCreateSession(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 	assert.Equal(t, session, session, "Session is not correct") |  | ||||||
| 	assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestCreateSession_CreateInitMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 			TotalAssignedJobs:  5, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{ |  | ||||||
| 		Count: 1, |  | ||||||
| 		Jobs: []actions.AcquirableJob{ |  | ||||||
| 			{ |  | ||||||
| 				RunnerRequestId: 1, |  | ||||||
| 				OwnerName:       "owner", |  | ||||||
| 				RepositoryName:  "repo", |  | ||||||
| 				AcquireJobUrl:   "https://github.com", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 	assert.Equal(t, session, session, "Session is not correct") |  | ||||||
| 	assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.initialMessage.MessageId, "Initial message id should be 0") |  | ||||||
| 	assert.Equal(t, "RunnerScaleSetJobMessages", asClient.initialMessage.MessageType, "Initial message type should be RunnerScaleSetJobMessages") |  | ||||||
| 	assert.Equal(t, 5, asClient.initialMessage.Statistics.TotalAssignedJobs, "Initial message total assigned jobs should be 5") |  | ||||||
| 	assert.Equal(t, 1, asClient.initialMessage.Statistics.TotalAvailableJobs, "Initial message total available jobs should be 1") |  | ||||||
| 	assert.Equal(t, "[{\"acquireJobUrl\":\"https://github.com\",\"messageType\":\"\",\"runnerRequestId\":1,\"repositoryName\":\"repo\",\"ownerName\":\"owner\",\"jobWorkflowRef\":\"\",\"eventName\":\"\",\"requestLabels\":null}]", asClient.initialMessage.Body, "Initial message body is not correct") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestCreateSession_CreateInitMessageWithOnlyAssignedJobs(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAssignedJobs: 5, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{ |  | ||||||
| 		Count: 0, |  | ||||||
| 		Jobs:  []actions.AcquirableJob{}, |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 	assert.Equal(t, session, session, "Session is not correct") |  | ||||||
| 	assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.initialMessage.MessageId, "Initial message id should be 0") |  | ||||||
| 	assert.Equal(t, "RunnerScaleSetJobMessages", asClient.initialMessage.MessageType, "Initial message type should be RunnerScaleSetJobMessages") |  | ||||||
| 	assert.Equal(t, 5, asClient.initialMessage.Statistics.TotalAssignedJobs, "Initial message total assigned jobs should be 5") |  | ||||||
| 	assert.Equal(t, 0, asClient.initialMessage.Statistics.TotalAvailableJobs, "Initial message total available jobs should be 0") |  | ||||||
| 	assert.Equal(t, "[]", asClient.initialMessage.Body, "Initial message body is not correct") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestCreateSession_CreateInitMessageFailed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 			TotalAssignedJobs:  5, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(nil, fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "get acquirable jobs failed. error", "Unexpected error") |  | ||||||
| 	assert.Nil(t, asClient, "Client should be nil") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestCreateSession_RetrySessionConflict(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.WithValue(context.Background(), testIgnoreSleep, true) |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{ |  | ||||||
| 		Code: 409, |  | ||||||
| 	}).Once() |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil).Once() |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 	assert.Equal(t, session, session, "Session is not correct") |  | ||||||
| 	assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestCreateSession_RetrySessionConflict_RunOutOfRetry(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.WithValue(context.Background(), testIgnoreSleep, true) |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{ |  | ||||||
| 		Code: 409, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	assert.Error(t, err, "Error should be returned") |  | ||||||
| 	assert.Nil(t, asClient, "AutoScaler should be nil") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertNumberOfCalls(t, "CreateMessageSession", sessionCreationMaxRetryCount), "CreateMessageSession should be called 10 times") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestCreateSession_NotRetryOnGeneralException(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.WithValue(context.Background(), testIgnoreSleep, true) |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{ |  | ||||||
| 		Code: 403, |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 
 |  | ||||||
| 	assert.Error(t, err, "Error should be returned") |  | ||||||
| 	assert.Nil(t, asClient, "AutoScaler should be nil") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertNumberOfCalls(t, "CreateMessageSession", 1), "CreateMessageSession should be called 1 time and not retry on generic error") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteSession(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("Close").Return(nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.Close() |  | ||||||
| 	assert.NoError(t, err, "Error deleting session") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteSession_Failed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("Close").Return(fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.Close() |  | ||||||
| 	assert.Error(t, err, "Error should be returned") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetRunnerScaleSetMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("GetMessage", ctx, int64(0), mock.Anything).Return(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "test", |  | ||||||
| 		Body:        "test", |  | ||||||
| 	}, nil) |  | ||||||
| 	mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Error getting message") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Initial message") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Error getting message") |  | ||||||
| 	assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetRunnerScaleSetMessage_HandleFailed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("GetMessage", ctx, int64(0), mock.Anything).Return(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "test", |  | ||||||
| 		Body:        "test", |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	// read initial message
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Error getting message") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return fmt.Errorf("error") |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "handle message failed. error", "Error getting message") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should not be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetRunnerScaleSetMessage_HandleInitialMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 			TotalAssignedJobs:  2, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything, mock.Anything).Return(session, nil) |  | ||||||
| 	mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{ |  | ||||||
| 		Count: 1, |  | ||||||
| 		Jobs: []actions.AcquirableJob{ |  | ||||||
| 			{ |  | ||||||
| 				RunnerRequestId: 1, |  | ||||||
| 				OwnerName:       "owner", |  | ||||||
| 				RepositoryName:  "repo", |  | ||||||
| 				AcquireJobUrl:   "https://github.com", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 	require.NotNil(t, asClient.initialMessage, "Initial message should be set") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Error getting message") |  | ||||||
| 	assert.Nil(t, asClient.initialMessage, "Initial message should be nil") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetRunnerScaleSetMessage_HandleInitialMessageFailed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 			TotalAssignedJobs:  2, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{ |  | ||||||
| 		Count: 1, |  | ||||||
| 		Jobs: []actions.AcquirableJob{ |  | ||||||
| 			{ |  | ||||||
| 				RunnerRequestId: 1, |  | ||||||
| 				OwnerName:       "owner", |  | ||||||
| 				RepositoryName:  "repo", |  | ||||||
| 				AcquireJobUrl:   "https://github.com", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 	require.NotNil(t, asClient.initialMessage, "Initial message should be set") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return fmt.Errorf("error") |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "fail to process initial message. error", "Error getting message") |  | ||||||
| 	assert.NotNil(t, asClient.initialMessage, "Initial message should be nil") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetRunnerScaleSetMessage_RetryUntilGetMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("GetMessage", ctx, int64(0), mock.Anything).Return(nil, nil).Times(3) |  | ||||||
| 	mockSessionClient.On("GetMessage", ctx, int64(0), mock.Anything).Return(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "test", |  | ||||||
| 		Body:        "test", |  | ||||||
| 	}, nil).Once() |  | ||||||
| 	mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 	assert.NoError(t, err, "Error getting initial message") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Error getting message") |  | ||||||
| 	assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetRunnerScaleSetMessage_ErrorOnGetMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("GetMessage", ctx, int64(0), mock.Anything).Return(nil, fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	// process initial message
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 	assert.NoError(t, err, "Error getting initial message") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		return fmt.Errorf("Should not be called") |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "get message failed from refreshing client. error", "Error should be returned") |  | ||||||
| 	assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteRunnerScaleSetMessage_Error(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("GetMessage", ctx, int64(0), mock.Anything).Return(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "test", |  | ||||||
| 		Body:        "test", |  | ||||||
| 	}, nil) |  | ||||||
| 	mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 	assert.NoError(t, err, "Error getting initial message") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error { |  | ||||||
| 		logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body) |  | ||||||
| 		return nil |  | ||||||
| 	}, 10) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "delete message failed from refreshing client. error", "Error getting message") |  | ||||||
| 	assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobsForRunnerScaleSet(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("AcquireJobs", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1, 2, 3}, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{1, 2, 3}) |  | ||||||
| 	assert.NoError(t, err, "Error acquiring jobs") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobsForRunnerScaleSet_SkipEmptyList(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{}) |  | ||||||
| 	assert.NoError(t, err, "Error acquiring jobs") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobsForRunnerScaleSet_Failed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	mockSessionClient := &actions.MockSessionService{} |  | ||||||
| 	logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil) |  | ||||||
| 	mockSessionClient.On("AcquireJobs", ctx, mock.Anything).Return(nil, fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) { |  | ||||||
| 		asc.client = mockSessionClient |  | ||||||
| 	}) |  | ||||||
| 	require.NoError(t, err, "Error creating autoscaler client") |  | ||||||
| 
 |  | ||||||
| 	err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{1, 2, 3}) |  | ||||||
| 	assert.ErrorContains(t, err, "acquire jobs failed from refreshing client. error", "Expect error acquiring jobs") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
|  | @ -1,246 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/cmd/githubrunnerscalesetlistener/config" |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/go-logr/logr" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type ScaleSettings struct { |  | ||||||
| 	Namespace    string |  | ||||||
| 	ResourceName string |  | ||||||
| 	MinRunners   int |  | ||||||
| 	MaxRunners   int |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Service struct { |  | ||||||
| 	ctx                context.Context |  | ||||||
| 	logger             logr.Logger |  | ||||||
| 	rsClient           RunnerScaleSetClient |  | ||||||
| 	kubeManager        KubernetesManager |  | ||||||
| 	settings           *ScaleSettings |  | ||||||
| 	currentRunnerCount int |  | ||||||
| 	metricsExporter    metricsExporter |  | ||||||
| 	errs               []error |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func WithPrometheusMetrics(conf config.Config) func(*Service) { |  | ||||||
| 	return func(svc *Service) { |  | ||||||
| 		parsedURL, err := actions.ParseGitHubConfigFromURL(conf.ConfigureUrl) |  | ||||||
| 		if err != nil { |  | ||||||
| 			svc.errs = append(svc.errs, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		svc.metricsExporter.withBaseLabels(baseLabels{ |  | ||||||
| 			scaleSetName:      conf.EphemeralRunnerSetName, |  | ||||||
| 			scaleSetNamespace: conf.EphemeralRunnerSetNamespace, |  | ||||||
| 			enterprise:        parsedURL.Enterprise, |  | ||||||
| 			organization:      parsedURL.Organization, |  | ||||||
| 			repository:        parsedURL.Repository, |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func WithLogger(logger logr.Logger) func(*Service) { |  | ||||||
| 	return func(s *Service) { |  | ||||||
| 		s.logger = logger.WithName("service") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewService( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	rsClient RunnerScaleSetClient, |  | ||||||
| 	manager KubernetesManager, |  | ||||||
| 	settings *ScaleSettings, |  | ||||||
| 	options ...func(*Service), |  | ||||||
| ) (*Service, error) { |  | ||||||
| 	s := &Service{ |  | ||||||
| 		ctx:                ctx, |  | ||||||
| 		rsClient:           rsClient, |  | ||||||
| 		kubeManager:        manager, |  | ||||||
| 		settings:           settings, |  | ||||||
| 		currentRunnerCount: -1, // force patch on startup
 |  | ||||||
| 		logger:             logr.FromContextOrDiscard(ctx), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, option := range options { |  | ||||||
| 		option(s) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(s.errs) > 0 { |  | ||||||
| 		return nil, errors.Join(s.errs...) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return s, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Service) Start() error { |  | ||||||
| 	s.metricsExporter.publishStatic(s.settings.MaxRunners, s.settings.MinRunners) |  | ||||||
| 	for { |  | ||||||
| 		s.logger.Info("waiting for message...") |  | ||||||
| 		select { |  | ||||||
| 		case <-s.ctx.Done(): |  | ||||||
| 			s.logger.Info("service is stopped.") |  | ||||||
| 			return nil |  | ||||||
| 		default: |  | ||||||
| 			err := s.rsClient.GetRunnerScaleSetMessage(s.ctx, s.processMessage, s.settings.MaxRunners) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return fmt.Errorf("could not get and process message. %w", err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Service) processMessage(message *actions.RunnerScaleSetMessage) error { |  | ||||||
| 	s.logger.Info("process message.", "messageId", message.MessageId, "messageType", message.MessageType) |  | ||||||
| 	if message.Statistics == nil { |  | ||||||
| 		return fmt.Errorf("can't process message with empty statistics") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	s.logger.Info("current runner scale set statistics.", |  | ||||||
| 		"available jobs", message.Statistics.TotalAvailableJobs, |  | ||||||
| 		"acquired jobs", message.Statistics.TotalAcquiredJobs, |  | ||||||
| 		"assigned jobs", message.Statistics.TotalAssignedJobs, |  | ||||||
| 		"running jobs", message.Statistics.TotalRunningJobs, |  | ||||||
| 		"registered runners", message.Statistics.TotalRegisteredRunners, |  | ||||||
| 		"busy runners", message.Statistics.TotalBusyRunners, |  | ||||||
| 		"idle runners", message.Statistics.TotalIdleRunners) |  | ||||||
| 
 |  | ||||||
| 	s.metricsExporter.publishStatistics(message.Statistics) |  | ||||||
| 
 |  | ||||||
| 	if message.MessageType != "RunnerScaleSetJobMessages" { |  | ||||||
| 		s.logger.Info("skip message with unknown message type.", "messageType", message.MessageType) |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if message.MessageId == 0 && message.Body == "" { // initial message with statistics only
 |  | ||||||
| 		return s.scaleForAssignedJobCount(message.Statistics.TotalAssignedJobs) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var batchedMessages []json.RawMessage |  | ||||||
| 	if err := json.NewDecoder(strings.NewReader(message.Body)).Decode(&batchedMessages); err != nil { |  | ||||||
| 		return fmt.Errorf("could not decode job messages. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	s.logger.Info("process batched runner scale set job messages.", "messageId", message.MessageId, "batchSize", len(batchedMessages)) |  | ||||||
| 
 |  | ||||||
| 	var availableJobs []int64 |  | ||||||
| 	for _, message := range batchedMessages { |  | ||||||
| 		var messageType actions.JobMessageType |  | ||||||
| 		if err := json.Unmarshal(message, &messageType); err != nil { |  | ||||||
| 			return fmt.Errorf("could not decode job message type. %w", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		switch messageType.MessageType { |  | ||||||
| 		case "JobAvailable": |  | ||||||
| 			var jobAvailable actions.JobAvailable |  | ||||||
| 			if err := json.Unmarshal(message, &jobAvailable); err != nil { |  | ||||||
| 				return fmt.Errorf("could not decode job available message. %w", err) |  | ||||||
| 			} |  | ||||||
| 			s.logger.Info( |  | ||||||
| 				"job available message received.", |  | ||||||
| 				"RequestId", |  | ||||||
| 				jobAvailable.RunnerRequestId, |  | ||||||
| 			) |  | ||||||
| 			availableJobs = append(availableJobs, jobAvailable.RunnerRequestId) |  | ||||||
| 		case "JobAssigned": |  | ||||||
| 			var jobAssigned actions.JobAssigned |  | ||||||
| 			if err := json.Unmarshal(message, &jobAssigned); err != nil { |  | ||||||
| 				return fmt.Errorf("could not decode job assigned message. %w", err) |  | ||||||
| 			} |  | ||||||
| 			s.logger.Info( |  | ||||||
| 				"job assigned message received.", |  | ||||||
| 				"RequestId", |  | ||||||
| 				jobAssigned.RunnerRequestId, |  | ||||||
| 			) |  | ||||||
| 			// s.metricsExporter.publishJobAssigned(&jobAssigned)
 |  | ||||||
| 		case "JobStarted": |  | ||||||
| 			var jobStarted actions.JobStarted |  | ||||||
| 			if err := json.Unmarshal(message, &jobStarted); err != nil { |  | ||||||
| 				return fmt.Errorf("could not decode job started message. %w", err) |  | ||||||
| 			} |  | ||||||
| 			s.logger.Info( |  | ||||||
| 				"job started message received.", |  | ||||||
| 				"RequestId", |  | ||||||
| 				jobStarted.RunnerRequestId, |  | ||||||
| 				"RunnerId", |  | ||||||
| 				jobStarted.RunnerId, |  | ||||||
| 			) |  | ||||||
| 			s.metricsExporter.publishJobStarted(&jobStarted) |  | ||||||
| 			s.updateJobInfoForRunner(jobStarted) |  | ||||||
| 		case "JobCompleted": |  | ||||||
| 			var jobCompleted actions.JobCompleted |  | ||||||
| 			if err := json.Unmarshal(message, &jobCompleted); err != nil { |  | ||||||
| 				return fmt.Errorf("could not decode job completed message. %w", err) |  | ||||||
| 			} |  | ||||||
| 			s.logger.Info( |  | ||||||
| 				"job completed message received.", |  | ||||||
| 				"RequestId", |  | ||||||
| 				jobCompleted.RunnerRequestId, |  | ||||||
| 				"Result", |  | ||||||
| 				jobCompleted.Result, |  | ||||||
| 				"RunnerId", |  | ||||||
| 				jobCompleted.RunnerId, |  | ||||||
| 				"RunnerName", |  | ||||||
| 				jobCompleted.RunnerName, |  | ||||||
| 			) |  | ||||||
| 			s.metricsExporter.publishJobCompleted(&jobCompleted) |  | ||||||
| 		default: |  | ||||||
| 			s.logger.Info("unknown job message type.", "messageType", messageType.MessageType) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := s.rsClient.AcquireJobsForRunnerScaleSet(s.ctx, availableJobs) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("could not acquire jobs. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return s.scaleForAssignedJobCount(message.Statistics.TotalAssignedJobs) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Service) scaleForAssignedJobCount(count int) error { |  | ||||||
| 	// Max runners should always be set by the resource builder either to the configured value,
 |  | ||||||
| 	// or the maximum int32 (resourcebuilder.newAutoScalingListener()).
 |  | ||||||
| 	targetRunnerCount := min(s.settings.MinRunners+count, s.settings.MaxRunners) |  | ||||||
| 	s.metricsExporter.publishDesiredRunners(targetRunnerCount) |  | ||||||
| 	if targetRunnerCount != s.currentRunnerCount { |  | ||||||
| 		s.logger.Info("try scale runner request up/down base on assigned job count", |  | ||||||
| 			"assigned job", count, |  | ||||||
| 			"decision", targetRunnerCount, |  | ||||||
| 			"min", s.settings.MinRunners, |  | ||||||
| 			"max", s.settings.MaxRunners, |  | ||||||
| 			"currentRunnerCount", s.currentRunnerCount, |  | ||||||
| 		) |  | ||||||
| 		err := s.kubeManager.ScaleEphemeralRunnerSet(s.ctx, s.settings.Namespace, s.settings.ResourceName, targetRunnerCount) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("could not scale ephemeral runner set (%s/%s). %w", s.settings.Namespace, s.settings.ResourceName, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		s.currentRunnerCount = targetRunnerCount |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // updateJobInfoForRunner updates the ephemeral runner with the job info and this is best effort since the info is only for better telemetry
 |  | ||||||
| func (s *Service) updateJobInfoForRunner(jobInfo actions.JobStarted) { |  | ||||||
| 	s.logger.Info("update job info for runner", |  | ||||||
| 		"runnerName", jobInfo.RunnerName, |  | ||||||
| 		"ownerName", jobInfo.OwnerName, |  | ||||||
| 		"repoName", jobInfo.RepositoryName, |  | ||||||
| 		"workflowRef", jobInfo.JobWorkflowRef, |  | ||||||
| 		"workflowRunId", jobInfo.WorkflowRunId, |  | ||||||
| 		"jobDisplayName", jobInfo.JobDisplayName, |  | ||||||
| 		"requestId", jobInfo.RunnerRequestId, |  | ||||||
| 	) |  | ||||||
| 	err := s.kubeManager.UpdateEphemeralRunnerWithJobInfo(s.ctx, s.settings.Namespace, jobInfo.RunnerName, jobInfo.OwnerName, jobInfo.RepositoryName, jobInfo.JobWorkflowRef, jobInfo.JobDisplayName, jobInfo.WorkflowRunId, jobInfo.RunnerRequestId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		s.logger.Error(err, "could not update ephemeral runner with job info", "runnerName", jobInfo.RunnerName, "requestId", jobInfo.RunnerRequestId) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,684 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/actions/actions-runner-controller/logging" |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/mock" |  | ||||||
| 	"github.com/stretchr/testify/require" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestNewService(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	assert.Equal(t, logger, service.logger) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestStart(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything, mock.Anything).Run(func(mock.Arguments) { cancel() }).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.Start() |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestStart_ScaleToMinRunners(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   5, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", ctx, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { |  | ||||||
| 		_ = service.scaleForAssignedJobCount(5) |  | ||||||
| 	}).Return(nil) |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.Start() |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 
 |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestStart_ScaleToMinRunnersFailed(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   5, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	c := mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(fmt.Errorf("error")).Once() |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", ctx, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { |  | ||||||
| 		_ = service.scaleForAssignedJobCount(5) |  | ||||||
| 	}).Return(c.ReturnArguments.Get(0)) |  | ||||||
| 
 |  | ||||||
| 	err = service.Start() |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "could not get and process message", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestStart_GetMultipleMessages(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything, mock.Anything).Return(nil).Times(5) |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.Start() |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestStart_ErrorOnMessage(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything, mock.Anything).Return(nil).Times(2) |  | ||||||
| 	mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything, mock.Anything).Return(fmt.Errorf("error")).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.Start() |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "could not get and process message. error", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_NoStatistic(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "test", |  | ||||||
| 		Body:        "test", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "can't process message with empty statistics", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_IgnoreUnknownMessageType(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "unknown", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 		}, |  | ||||||
| 		Body: "[]", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_InvalidBatchMessageJson(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 		}, |  | ||||||
| 		Body: "invalid json", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "could not decode job messages", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_InvalidJobMessageJson(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 		}, |  | ||||||
| 		Body: "[\"something\", \"test\"]", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "could not decode job message type", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_MultipleMessages(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   1, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 3 && ids[1] == 4 })).Return(nil).Once() |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 3).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAssignedJobs:  2, |  | ||||||
| 			TotalAvailableJobs: 2, |  | ||||||
| 		}, |  | ||||||
| 		Body: "[{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 3},{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 4},{\"messageType\":\"JobAssigned\", \"runnerRequestId\": 2}, {\"messageType\":\"JobCompleted\", \"runnerRequestId\": 1, \"result\":\"succeed\"},{\"messageType\":\"unknown\"}]", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_AcquireJobsFailed(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 })).Return(fmt.Errorf("error")).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAssignedJobs:  1, |  | ||||||
| 			TotalAvailableJobs: 1, |  | ||||||
| 		}, |  | ||||||
| 		Body: "[{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 1}]", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "could not acquire jobs. error", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestScaleForAssignedJobCount_DeDupScale(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   0, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.scaleForAssignedJobCount(2) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(2) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(2) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(2) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.Equal(t, 2, service.currentRunnerCount, "Unexpected runner count") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestScaleForAssignedJobCount_ScaleWithinMinMax(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   1, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 1).Return(nil).Once() |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 4).Return(nil).Once() |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(nil).Once() |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(nil).Once() |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.scaleForAssignedJobCount(0) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(3) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(5) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(1) |  | ||||||
| 	require.NoError(t, err, "Unexpected error") |  | ||||||
| 	err = service.scaleForAssignedJobCount(10) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.Equal(t, 5, service.currentRunnerCount, "Unexpected runner count") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestScaleForAssignedJobCount_ScaleFailed(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   1, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 3).Return(fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	err = service.scaleForAssignedJobCount(2) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "could not scale ephemeral runner set (namespace/resource). error", "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_JobStartedMessage(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   1, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	service.currentRunnerCount = 1 |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager.On( |  | ||||||
| 		"UpdateEphemeralRunnerWithJobInfo", |  | ||||||
| 		ctx, |  | ||||||
| 		service.settings.Namespace, |  | ||||||
| 		"runner1", |  | ||||||
| 		"owner1", |  | ||||||
| 		"repo1", |  | ||||||
| 		".github/workflows/ci.yaml", |  | ||||||
| 		"job1", |  | ||||||
| 		int64(100), |  | ||||||
| 		int64(3), |  | ||||||
| 	).Run( |  | ||||||
| 		func(_ mock.Arguments) { cancel() }, |  | ||||||
| 	).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return len(ids) == 0 })).Return(nil).Once() |  | ||||||
| 	mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(nil) |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAssignedJobs:  1, |  | ||||||
| 			TotalAvailableJobs: 0, |  | ||||||
| 		}, |  | ||||||
| 		Body: "[{\"messageType\":\"JobStarted\", \"runnerRequestId\": 3, \"runnerId\": 1, \"runnerName\": \"runner1\", \"ownerName\": \"owner1\", \"repositoryName\": \"repo1\", \"jobWorkflowRef\": \".github/workflows/ci.yaml\", \"jobDisplayName\": \"job1\", \"workflowRunId\": 100 }]", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessMessage_JobStartedMessageIgnoreRunnerUpdateError(t *testing.T) { |  | ||||||
| 	mockRsClient := &MockRunnerScaleSetClient{} |  | ||||||
| 	mockKubeManager := &MockKubernetesManager{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	defer cancel() |  | ||||||
| 	service, err := NewService( |  | ||||||
| 		ctx, |  | ||||||
| 		mockRsClient, |  | ||||||
| 		mockKubeManager, |  | ||||||
| 		&ScaleSettings{ |  | ||||||
| 			Namespace:    "namespace", |  | ||||||
| 			ResourceName: "resource", |  | ||||||
| 			MinRunners:   1, |  | ||||||
| 			MaxRunners:   5, |  | ||||||
| 		}, |  | ||||||
| 		func(s *Service) { |  | ||||||
| 			s.logger = logger |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	service.currentRunnerCount = 1 |  | ||||||
| 
 |  | ||||||
| 	mockKubeManager.On("UpdateEphemeralRunnerWithJobInfo", ctx, service.settings.Namespace, "runner1", "owner1", "repo1", ".github/workflows/ci.yaml", "job1", int64(100), int64(3)).Run(func(args mock.Arguments) { cancel() }).Return(fmt.Errorf("error")).Once() |  | ||||||
| 	mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return len(ids) == 0 })).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	err = service.processMessage(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "RunnerScaleSetJobMessages", |  | ||||||
| 		Statistics: &actions.RunnerScaleSetStatistic{ |  | ||||||
| 			TotalAssignedJobs:  0, |  | ||||||
| 			TotalAvailableJobs: 0, |  | ||||||
| 		}, |  | ||||||
| 		Body: "[{\"messageType\":\"JobStarted\", \"runnerRequestId\": 3, \"runnerId\": 1, \"runnerName\": \"runner1\", \"ownerName\": \"owner1\", \"repositoryName\": \"repo1\", \"jobWorkflowRef\": \".github/workflows/ci.yaml\", \"jobDisplayName\": \"job1\", \"workflowRunId\": 100 }]", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Unexpected error") |  | ||||||
| 	assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| 	assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
|  | @ -1,76 +0,0 @@ | ||||||
| package config |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Config struct { |  | ||||||
| 	ConfigureUrl                string `json:"configureUrl"` |  | ||||||
| 	AppID                       int64  `json:"appID"` |  | ||||||
| 	AppInstallationID           int64  `json:"appInstallationID"` |  | ||||||
| 	AppPrivateKey               string `json:"appPrivateKey"` |  | ||||||
| 	Token                       string `json:"token"` |  | ||||||
| 	EphemeralRunnerSetNamespace string `json:"ephemeralRunnerSetNamespace"` |  | ||||||
| 	EphemeralRunnerSetName      string `json:"ephemeralRunnerSetName"` |  | ||||||
| 	MaxRunners                  int    `json:"maxRunners"` |  | ||||||
| 	MinRunners                  int    `json:"minRunners"` |  | ||||||
| 	RunnerScaleSetId            int    `json:"runnerScaleSetId"` |  | ||||||
| 	RunnerScaleSetName          string `json:"runnerScaleSetName"` |  | ||||||
| 	ServerRootCA                string `json:"serverRootCA"` |  | ||||||
| 	LogLevel                    string `json:"logLevel"` |  | ||||||
| 	LogFormat                   string `json:"logFormat"` |  | ||||||
| 	MetricsAddr                 string `json:"metricsAddr"` |  | ||||||
| 	MetricsEndpoint             string `json:"metricsEndpoint"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func Read(path string) (Config, error) { |  | ||||||
| 	f, err := os.Open(path) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return Config{}, err |  | ||||||
| 	} |  | ||||||
| 	defer f.Close() |  | ||||||
| 
 |  | ||||||
| 	var config Config |  | ||||||
| 	if err := json.NewDecoder(f).Decode(&config); err != nil { |  | ||||||
| 		return Config{}, fmt.Errorf("failed to decode config: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := config.validate(); err != nil { |  | ||||||
| 		return Config{}, fmt.Errorf("failed to validate config: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return config, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (c *Config) validate() error { |  | ||||||
| 	if len(c.ConfigureUrl) == 0 { |  | ||||||
| 		return fmt.Errorf("GitHubConfigUrl is not provided") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(c.EphemeralRunnerSetNamespace) == 0 || len(c.EphemeralRunnerSetName) == 0 { |  | ||||||
| 		return fmt.Errorf("EphemeralRunnerSetNamespace '%s' or EphemeralRunnerSetName '%s' is missing", c.EphemeralRunnerSetNamespace, c.EphemeralRunnerSetName) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if c.RunnerScaleSetId == 0 { |  | ||||||
| 		return fmt.Errorf("RunnerScaleSetId '%d' is missing", c.RunnerScaleSetId) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if c.MaxRunners < c.MinRunners { |  | ||||||
| 		return fmt.Errorf("MinRunners '%d' cannot be greater than MaxRunners '%d'", c.MinRunners, c.MaxRunners) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	hasToken := len(c.Token) > 0 |  | ||||||
| 	hasPrivateKeyConfig := c.AppID > 0 && c.AppPrivateKey != "" |  | ||||||
| 
 |  | ||||||
| 	if !hasToken && !hasPrivateKeyConfig { |  | ||||||
| 		return fmt.Errorf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if hasToken && hasPrivateKeyConfig { |  | ||||||
| 		return fmt.Errorf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -1,92 +0,0 @@ | ||||||
| package config |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestConfigValidationMinMax(t *testing.T) { |  | ||||||
| 	config := &Config{ |  | ||||||
| 		ConfigureUrl:                "github.com/some_org/some_repo", |  | ||||||
| 		EphemeralRunnerSetNamespace: "namespace", |  | ||||||
| 		EphemeralRunnerSetName:      "deployment", |  | ||||||
| 		RunnerScaleSetId:            1, |  | ||||||
| 		MinRunners:                  5, |  | ||||||
| 		MaxRunners:                  2, |  | ||||||
| 		Token:                       "token", |  | ||||||
| 	} |  | ||||||
| 	err := config.validate() |  | ||||||
| 	assert.ErrorContains(t, err, "MinRunners '5' cannot be greater than MaxRunners '2", "Expected error about MinRunners > MaxRunners") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestConfigValidationMissingToken(t *testing.T) { |  | ||||||
| 	config := &Config{ |  | ||||||
| 		ConfigureUrl:                "github.com/some_org/some_repo", |  | ||||||
| 		EphemeralRunnerSetNamespace: "namespace", |  | ||||||
| 		EphemeralRunnerSetName:      "deployment", |  | ||||||
| 		RunnerScaleSetId:            1, |  | ||||||
| 	} |  | ||||||
| 	err := config.validate() |  | ||||||
| 	expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey)) |  | ||||||
| 	assert.ErrorContains(t, err, expectedError, "Expected error about missing auth") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestConfigValidationAppKey(t *testing.T) { |  | ||||||
| 	config := &Config{ |  | ||||||
| 		AppID:                       1, |  | ||||||
| 		AppInstallationID:           10, |  | ||||||
| 		ConfigureUrl:                "github.com/some_org/some_repo", |  | ||||||
| 		EphemeralRunnerSetNamespace: "namespace", |  | ||||||
| 		EphemeralRunnerSetName:      "deployment", |  | ||||||
| 		RunnerScaleSetId:            1, |  | ||||||
| 	} |  | ||||||
| 	err := config.validate() |  | ||||||
| 	expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey)) |  | ||||||
| 	assert.ErrorContains(t, err, expectedError, "Expected error about missing auth") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) { |  | ||||||
| 	config := &Config{ |  | ||||||
| 		AppID:                       1, |  | ||||||
| 		AppInstallationID:           10, |  | ||||||
| 		AppPrivateKey:               "asdf", |  | ||||||
| 		Token:                       "asdf", |  | ||||||
| 		ConfigureUrl:                "github.com/some_org/some_repo", |  | ||||||
| 		EphemeralRunnerSetNamespace: "namespace", |  | ||||||
| 		EphemeralRunnerSetName:      "deployment", |  | ||||||
| 		RunnerScaleSetId:            1, |  | ||||||
| 	} |  | ||||||
| 	err := config.validate() |  | ||||||
| 	expectedError := fmt.Sprintf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey)) |  | ||||||
| 	assert.ErrorContains(t, err, expectedError, "Expected error about missing auth") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestConfigValidation(t *testing.T) { |  | ||||||
| 	config := &Config{ |  | ||||||
| 		ConfigureUrl:                "https://github.com/actions", |  | ||||||
| 		EphemeralRunnerSetNamespace: "namespace", |  | ||||||
| 		EphemeralRunnerSetName:      "deployment", |  | ||||||
| 		RunnerScaleSetId:            1, |  | ||||||
| 		MinRunners:                  1, |  | ||||||
| 		MaxRunners:                  5, |  | ||||||
| 		Token:                       "asdf", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := config.validate() |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err, "Expected no error") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestConfigValidationConfigUrl(t *testing.T) { |  | ||||||
| 	config := &Config{ |  | ||||||
| 		EphemeralRunnerSetNamespace: "namespace", |  | ||||||
| 		EphemeralRunnerSetName:      "deployment", |  | ||||||
| 		RunnerScaleSetId:            1, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := config.validate() |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl") |  | ||||||
| } |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| //go:generate mockery --inpackage --name=KubernetesManager
 |  | ||||||
| type KubernetesManager interface { |  | ||||||
| 	ScaleEphemeralRunnerSet(ctx context.Context, namespace, resourceName string, runnerCount int) error |  | ||||||
| 
 |  | ||||||
| 	UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName string, jobRequestId, workflowRunId int64) error |  | ||||||
| } |  | ||||||
|  | @ -1,244 +0,0 @@ | ||||||
| /* |  | ||||||
| Copyright 2021 The actions-runner-controller authors. |  | ||||||
| 
 |  | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| you may not use this file except in compliance with the License. |  | ||||||
| You may obtain a copy of the License at |  | ||||||
| 
 |  | ||||||
|     http://www.apache.org/licenses/LICENSE-2.0
 |  | ||||||
| 
 |  | ||||||
| Unless required by applicable law or agreed to in writing, software |  | ||||||
| distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| See the License for the specific language governing permissions and |  | ||||||
| limitations under the License. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/x509" |  | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" |  | ||||||
| 	"os" |  | ||||||
| 	"os/signal" |  | ||||||
| 	"syscall" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/build" |  | ||||||
| 	"github.com/actions/actions-runner-controller/cmd/githubrunnerscalesetlistener/config" |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/actions/actions-runner-controller/logging" |  | ||||||
| 	"github.com/go-logr/logr" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promhttp" |  | ||||||
| 	"golang.org/x/net/http/httpproxy" |  | ||||||
| 	"golang.org/x/sync/errgroup" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	configPath, ok := os.LookupEnv("LISTENER_CONFIG_PATH") |  | ||||||
| 	if !ok { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "Error: LISTENER_CONFIG_PATH environment variable is not set\n") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	rc, err := config.Read(configPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "Error: reading config from path(%q): %v\n", configPath, err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logLevel := string(logging.LogLevelDebug) |  | ||||||
| 	if rc.LogLevel != "" { |  | ||||||
| 		logLevel = rc.LogLevel |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logFormat := string(logging.LogFormatText) |  | ||||||
| 	if rc.LogFormat != "" { |  | ||||||
| 		logFormat = rc.LogFormat |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logger, err := logging.NewLogger(logLevel, logFormat) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "Error: creating logger: %v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) |  | ||||||
| 	defer stop() |  | ||||||
| 
 |  | ||||||
| 	g, ctx := errgroup.WithContext(ctx) |  | ||||||
| 
 |  | ||||||
| 	g.Go(func() error { |  | ||||||
| 		opts := runOptions{ |  | ||||||
| 			serviceOptions: []func(*Service){ |  | ||||||
| 				WithLogger(logger), |  | ||||||
| 			}, |  | ||||||
| 		} |  | ||||||
| 		opts.serviceOptions = append(opts.serviceOptions, WithPrometheusMetrics(rc)) |  | ||||||
| 
 |  | ||||||
| 		return run(ctx, rc, logger, opts) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	if len(rc.MetricsAddr) != 0 { |  | ||||||
| 		g.Go(func() error { |  | ||||||
| 			metricsServer := metricsServer{ |  | ||||||
| 				rc:     rc, |  | ||||||
| 				logger: logger, |  | ||||||
| 			} |  | ||||||
| 			g.Go(func() error { |  | ||||||
| 				<-ctx.Done() |  | ||||||
| 				return metricsServer.shutdown() |  | ||||||
| 			}) |  | ||||||
| 			return metricsServer.listenAndServe() |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := g.Wait(); err != nil { |  | ||||||
| 		logger.Error(err, "Error encountered") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type metricsServer struct { |  | ||||||
| 	rc     config.Config |  | ||||||
| 	logger logr.Logger |  | ||||||
| 	srv    *http.Server |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *metricsServer) shutdown() error { |  | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |  | ||||||
| 	defer cancel() |  | ||||||
| 	return s.srv.Shutdown(ctx) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *metricsServer) listenAndServe() error { |  | ||||||
| 	reg := prometheus.NewRegistry() |  | ||||||
| 	reg.MustRegister( |  | ||||||
| 		// availableJobs,
 |  | ||||||
| 		// acquiredJobs,
 |  | ||||||
| 		assignedJobs, |  | ||||||
| 		runningJobs, |  | ||||||
| 		registeredRunners, |  | ||||||
| 		busyRunners, |  | ||||||
| 		minRunners, |  | ||||||
| 		maxRunners, |  | ||||||
| 		desiredRunners, |  | ||||||
| 		idleRunners, |  | ||||||
| 		startedJobsTotal, |  | ||||||
| 		completedJobsTotal, |  | ||||||
| 		// jobQueueDurationSeconds,
 |  | ||||||
| 		jobStartupDurationSeconds, |  | ||||||
| 		jobExecutionDurationSeconds, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	mux := http.NewServeMux() |  | ||||||
| 	mux.Handle( |  | ||||||
| 		s.rc.MetricsEndpoint, |  | ||||||
| 		promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}), |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	s.srv = &http.Server{ |  | ||||||
| 		Addr:    s.rc.MetricsAddr, |  | ||||||
| 		Handler: mux, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	s.logger.Info("Starting metrics server", "address", s.srv.Addr) |  | ||||||
| 	return s.srv.ListenAndServe() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type runOptions struct { |  | ||||||
| 	serviceOptions []func(*Service) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func run(ctx context.Context, rc config.Config, logger logr.Logger, opts runOptions) error { |  | ||||||
| 	// Create root context and hook with sigint and sigterm
 |  | ||||||
| 	creds := &actions.ActionsAuth{} |  | ||||||
| 	if rc.Token != "" { |  | ||||||
| 		creds.Token = rc.Token |  | ||||||
| 	} else { |  | ||||||
| 		creds.AppCreds = &actions.GitHubAppAuth{ |  | ||||||
| 			AppID:             rc.AppID, |  | ||||||
| 			AppInstallationID: rc.AppInstallationID, |  | ||||||
| 			AppPrivateKey:     rc.AppPrivateKey, |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	actionsServiceClient, err := newActionsClientFromConfig( |  | ||||||
| 		rc, |  | ||||||
| 		creds, |  | ||||||
| 		actions.WithLogger(logger), |  | ||||||
| 	) |  | ||||||
| 	actionsServiceClient.SetUserAgent(actions.UserAgentInfo{ |  | ||||||
| 		Version:    build.Version, |  | ||||||
| 		CommitSHA:  build.CommitSHA, |  | ||||||
| 		ScaleSetID: rc.RunnerScaleSetId, |  | ||||||
| 		HasProxy:   hasProxy(), |  | ||||||
| 		Subsystem:  "githubrunnerscalesetlistener", |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create an Actions Service client: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Create message listener
 |  | ||||||
| 	autoScalerClient, err := NewAutoScalerClient(ctx, actionsServiceClient, &logger, rc.RunnerScaleSetId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create a message listener: %w", err) |  | ||||||
| 	} |  | ||||||
| 	defer autoScalerClient.Close() |  | ||||||
| 
 |  | ||||||
| 	// Create kube manager and scale controller
 |  | ||||||
| 	kubeManager, err := NewKubernetesManager(&logger) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create kubernetes manager: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	scaleSettings := &ScaleSettings{ |  | ||||||
| 		Namespace:    rc.EphemeralRunnerSetNamespace, |  | ||||||
| 		ResourceName: rc.EphemeralRunnerSetName, |  | ||||||
| 		MaxRunners:   rc.MaxRunners, |  | ||||||
| 		MinRunners:   rc.MinRunners, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	service, err := NewService(ctx, autoScalerClient, kubeManager, scaleSettings, opts.serviceOptions...) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create new service: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Start listening for messages
 |  | ||||||
| 	if err = service.Start(); err != nil { |  | ||||||
| 		return fmt.Errorf("failed to start message queue listener: %w", err) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func newActionsClientFromConfig(config config.Config, creds *actions.ActionsAuth, options ...actions.ClientOption) (*actions.Client, error) { |  | ||||||
| 	if config.ServerRootCA != "" { |  | ||||||
| 		systemPool, err := x509.SystemCertPool() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("failed to load system cert pool: %w", err) |  | ||||||
| 		} |  | ||||||
| 		pool := systemPool.Clone() |  | ||||||
| 		ok := pool.AppendCertsFromPEM([]byte(config.ServerRootCA)) |  | ||||||
| 		if !ok { |  | ||||||
| 			return nil, fmt.Errorf("failed to parse root certificate") |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		options = append(options, actions.WithRootCAs(pool)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	proxyFunc := httpproxy.FromEnvironment().ProxyFunc() |  | ||||||
| 	options = append(options, actions.WithProxy(func(req *http.Request) (*url.URL, error) { |  | ||||||
| 		return proxyFunc(req.URL) |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	return actions.NewClient(config.ConfigureUrl, creds, options...) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func hasProxy() bool { |  | ||||||
| 	proxyFunc := httpproxy.FromEnvironment().ProxyFunc() |  | ||||||
| 	return proxyFunc != nil |  | ||||||
| } |  | ||||||
|  | @ -1,169 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/tls" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/require" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/cmd/githubrunnerscalesetlistener/config" |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions/testserver" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestCustomerServerRootCA(t *testing.T) { |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	certsFolder := filepath.Join( |  | ||||||
| 		"../../", |  | ||||||
| 		"github", |  | ||||||
| 		"actions", |  | ||||||
| 		"testdata", |  | ||||||
| 	) |  | ||||||
| 	certPath := filepath.Join(certsFolder, "server.crt") |  | ||||||
| 	keyPath := filepath.Join(certsFolder, "server.key") |  | ||||||
| 
 |  | ||||||
| 	serverCalledSuccessfully := false |  | ||||||
| 
 |  | ||||||
| 	server := testserver.NewUnstarted(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		serverCalledSuccessfully = true |  | ||||||
| 		w.WriteHeader(http.StatusOK) |  | ||||||
| 		w.Write([]byte(`{"count": 0}`)) |  | ||||||
| 	})) |  | ||||||
| 	cert, err := tls.LoadX509KeyPair(certPath, keyPath) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} |  | ||||||
| 	server.StartTLS() |  | ||||||
| 
 |  | ||||||
| 	var certsString string |  | ||||||
| 	rootCA, err := os.ReadFile(filepath.Join(certsFolder, "rootCA.crt")) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	certsString = string(rootCA) |  | ||||||
| 
 |  | ||||||
| 	intermediate, err := os.ReadFile(filepath.Join(certsFolder, "intermediate.pem")) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	certsString = certsString + string(intermediate) |  | ||||||
| 
 |  | ||||||
| 	config := config.Config{ |  | ||||||
| 		ConfigureUrl: server.ConfigURLForOrg("myorg"), |  | ||||||
| 		ServerRootCA: certsString, |  | ||||||
| 	} |  | ||||||
| 	creds := &actions.ActionsAuth{ |  | ||||||
| 		Token: "token", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	client, err := newActionsClientFromConfig(config, creds) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	_, err = client.GetRunnerScaleSet(ctx, 1, "test") |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	assert.True(t, serverCalledSuccessfully) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProxySettings(t *testing.T) { |  | ||||||
| 	t.Run("http", func(t *testing.T) { |  | ||||||
| 		wentThroughProxy := false |  | ||||||
| 
 |  | ||||||
| 		proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 			wentThroughProxy = true |  | ||||||
| 		})) |  | ||||||
| 		t.Cleanup(func() { |  | ||||||
| 			proxy.Close() |  | ||||||
| 		}) |  | ||||||
| 
 |  | ||||||
| 		prevProxy := os.Getenv("http_proxy") |  | ||||||
| 		os.Setenv("http_proxy", proxy.URL) |  | ||||||
| 		defer os.Setenv("http_proxy", prevProxy) |  | ||||||
| 
 |  | ||||||
| 		config := config.Config{ |  | ||||||
| 			ConfigureUrl: "https://github.com/org/repo", |  | ||||||
| 		} |  | ||||||
| 		creds := &actions.ActionsAuth{ |  | ||||||
| 			Token: "token", |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		client, err := newActionsClientFromConfig(config, creds) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		_, err = client.Do(req) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		assert.True(t, wentThroughProxy) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	t.Run("https", func(t *testing.T) { |  | ||||||
| 		wentThroughProxy := false |  | ||||||
| 
 |  | ||||||
| 		proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 			wentThroughProxy = true |  | ||||||
| 		})) |  | ||||||
| 		t.Cleanup(func() { |  | ||||||
| 			proxy.Close() |  | ||||||
| 		}) |  | ||||||
| 
 |  | ||||||
| 		prevProxy := os.Getenv("https_proxy") |  | ||||||
| 		os.Setenv("https_proxy", proxy.URL) |  | ||||||
| 		defer os.Setenv("https_proxy", prevProxy) |  | ||||||
| 
 |  | ||||||
| 		config := config.Config{ |  | ||||||
| 			ConfigureUrl: "https://github.com/org/repo", |  | ||||||
| 		} |  | ||||||
| 		creds := &actions.ActionsAuth{ |  | ||||||
| 			Token: "token", |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		client, err := newActionsClientFromConfig(config, creds, actions.WithRetryMax(0)) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		_, err = client.Do(req) |  | ||||||
| 		// proxy doesn't support https
 |  | ||||||
| 		assert.Error(t, err) |  | ||||||
| 		assert.True(t, wentThroughProxy) |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	t.Run("no_proxy", func(t *testing.T) { |  | ||||||
| 		wentThroughProxy := false |  | ||||||
| 
 |  | ||||||
| 		proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 			wentThroughProxy = true |  | ||||||
| 		})) |  | ||||||
| 		t.Cleanup(func() { |  | ||||||
| 			proxy.Close() |  | ||||||
| 		}) |  | ||||||
| 
 |  | ||||||
| 		prevProxy := os.Getenv("http_proxy") |  | ||||||
| 		os.Setenv("http_proxy", proxy.URL) |  | ||||||
| 		defer os.Setenv("http_proxy", prevProxy) |  | ||||||
| 
 |  | ||||||
| 		prevNoProxy := os.Getenv("no_proxy") |  | ||||||
| 		os.Setenv("no_proxy", "example.com") |  | ||||||
| 		defer os.Setenv("no_proxy", prevNoProxy) |  | ||||||
| 
 |  | ||||||
| 		config := config.Config{ |  | ||||||
| 			ConfigureUrl: "https://github.com/org/repo", |  | ||||||
| 		} |  | ||||||
| 		creds := &actions.ActionsAuth{ |  | ||||||
| 			Token: "token", |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		client, err := newActionsClientFromConfig(config, creds) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 		_, err = client.Do(req) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		assert.False(t, wentThroughProxy) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| //go:generate mockery --inpackage --name=RunnerScaleSetClient
 |  | ||||||
| type RunnerScaleSetClient interface { |  | ||||||
| 	GetRunnerScaleSetMessage(ctx context.Context, handler func(msg *actions.RunnerScaleSetMessage) error, maxCapacity int) error |  | ||||||
| 	AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error |  | ||||||
| } |  | ||||||
|  | @ -1,343 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // label names
 |  | ||||||
| const ( |  | ||||||
| 	labelKeyRunnerScaleSetName      = "name" |  | ||||||
| 	labelKeyRunnerScaleSetNamespace = "namespace" |  | ||||||
| 	labelKeyEnterprise              = "enterprise" |  | ||||||
| 	labelKeyOrganization            = "organization" |  | ||||||
| 	labelKeyRepository              = "repository" |  | ||||||
| 	labelKeyJobName                 = "job_name" |  | ||||||
| 	labelKeyJobWorkflowRef          = "job_workflow_ref" |  | ||||||
| 	labelKeyEventName               = "event_name" |  | ||||||
| 	labelKeyJobResult               = "job_result" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const githubScaleSetSubsystem = "gha" |  | ||||||
| 
 |  | ||||||
| // labels
 |  | ||||||
| var ( |  | ||||||
| 	scaleSetLabels = []string{ |  | ||||||
| 		labelKeyRunnerScaleSetName, |  | ||||||
| 		labelKeyRepository, |  | ||||||
| 		labelKeyOrganization, |  | ||||||
| 		labelKeyEnterprise, |  | ||||||
| 		labelKeyRunnerScaleSetNamespace, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	jobLabels = []string{ |  | ||||||
| 		labelKeyRepository, |  | ||||||
| 		labelKeyOrganization, |  | ||||||
| 		labelKeyEnterprise, |  | ||||||
| 		labelKeyJobName, |  | ||||||
| 		labelKeyJobWorkflowRef, |  | ||||||
| 		labelKeyEventName, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	completedJobsTotalLabels   = append(jobLabels, labelKeyJobResult) |  | ||||||
| 	jobExecutionDurationLabels = append(jobLabels, labelKeyJobResult) |  | ||||||
| 	startedJobsTotalLabels     = jobLabels |  | ||||||
| 	jobStartupDurationLabels   = []string{ |  | ||||||
| 		labelKeyRepository, |  | ||||||
| 		labelKeyOrganization, |  | ||||||
| 		labelKeyEnterprise, |  | ||||||
| 		labelKeyEventName, |  | ||||||
| 	} |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // metrics
 |  | ||||||
| var ( |  | ||||||
| 	// availableJobs = prometheus.NewGaugeVec(
 |  | ||||||
| 	// 	prometheus.GaugeOpts{
 |  | ||||||
| 	// 		Subsystem: githubScaleSetSubsystem,
 |  | ||||||
| 	// 		Name:      "available_jobs",
 |  | ||||||
| 	// 		Help:      "Number of jobs with `runs-on` matching the runner scale set name. Jobs are not yet assigned to the runner scale set.",
 |  | ||||||
| 	// 	},
 |  | ||||||
| 	// 	scaleSetLabels,
 |  | ||||||
| 	// )
 |  | ||||||
| 	//
 |  | ||||||
| 	// acquiredJobs = prometheus.NewGaugeVec(
 |  | ||||||
| 	// 	prometheus.GaugeOpts{
 |  | ||||||
| 	// 		Subsystem: githubScaleSetSubsystem,
 |  | ||||||
| 	// 		Name:      "acquired_jobs",
 |  | ||||||
| 	// 		Help:      "Number of jobs acquired by the scale set.",
 |  | ||||||
| 	// 	},
 |  | ||||||
| 	// 	scaleSetLabels,
 |  | ||||||
| 	// )
 |  | ||||||
| 
 |  | ||||||
| 	assignedJobs = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "assigned_jobs", |  | ||||||
| 			Help:      "Number of jobs assigned to this scale set.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	runningJobs = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "running_jobs", |  | ||||||
| 			Help:      "Number of jobs running (or about to be run).", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	registeredRunners = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "registered_runners", |  | ||||||
| 			Help:      "Number of runners registered by the scale set.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	busyRunners = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "busy_runners", |  | ||||||
| 			Help:      "Number of registered runners running a job.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	minRunners = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "min_runners", |  | ||||||
| 			Help:      "Minimum number of runners.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	maxRunners = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "max_runners", |  | ||||||
| 			Help:      "Maximum number of runners.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	desiredRunners = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "desired_runners", |  | ||||||
| 			Help:      "Number of runners desired by the scale set.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	idleRunners = prometheus.NewGaugeVec( |  | ||||||
| 		prometheus.GaugeOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "idle_runners", |  | ||||||
| 			Help:      "Number of registered runners not running a job.", |  | ||||||
| 		}, |  | ||||||
| 		scaleSetLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	startedJobsTotal = prometheus.NewCounterVec( |  | ||||||
| 		prometheus.CounterOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "started_jobs_total", |  | ||||||
| 			Help:      "Total number of jobs started.", |  | ||||||
| 		}, |  | ||||||
| 		startedJobsTotalLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	completedJobsTotal = prometheus.NewCounterVec( |  | ||||||
| 		prometheus.CounterOpts{ |  | ||||||
| 			Name:      "completed_jobs_total", |  | ||||||
| 			Help:      "Total number of jobs completed.", |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 		}, |  | ||||||
| 		completedJobsTotalLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	// jobQueueDurationSeconds = prometheus.NewHistogramVec(
 |  | ||||||
| 	// 	prometheus.HistogramOpts{
 |  | ||||||
| 	// 		Subsystem: githubScaleSetSubsystem,
 |  | ||||||
| 	// 		Name:      "job_queue_duration_seconds",
 |  | ||||||
| 	// 		Help:      "Time spent waiting for workflow jobs to get assigned to the scale set after queueing (in seconds).",
 |  | ||||||
| 	// 		Buckets:   runtimeBuckets,
 |  | ||||||
| 	// 	},
 |  | ||||||
| 	// 	jobLabels,
 |  | ||||||
| 	// )
 |  | ||||||
| 
 |  | ||||||
| 	jobStartupDurationSeconds = prometheus.NewHistogramVec( |  | ||||||
| 		prometheus.HistogramOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "job_startup_duration_seconds", |  | ||||||
| 			Help:      "Time spent waiting for workflow job to get started on the runner owned by the scale set (in seconds).", |  | ||||||
| 			Buckets:   runtimeBuckets, |  | ||||||
| 		}, |  | ||||||
| 		jobStartupDurationLabels, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	jobExecutionDurationSeconds = prometheus.NewHistogramVec( |  | ||||||
| 		prometheus.HistogramOpts{ |  | ||||||
| 			Subsystem: githubScaleSetSubsystem, |  | ||||||
| 			Name:      "job_execution_duration_seconds", |  | ||||||
| 			Help:      "Time spent executing workflow jobs by the scale set (in seconds).", |  | ||||||
| 			Buckets:   runtimeBuckets, |  | ||||||
| 		}, |  | ||||||
| 		jobExecutionDurationLabels, |  | ||||||
| 	) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var runtimeBuckets []float64 = []float64{ |  | ||||||
| 	0.01, |  | ||||||
| 	0.05, |  | ||||||
| 	0.1, |  | ||||||
| 	0.5, |  | ||||||
| 	1, |  | ||||||
| 	2, |  | ||||||
| 	3, |  | ||||||
| 	4, |  | ||||||
| 	5, |  | ||||||
| 	6, |  | ||||||
| 	7, |  | ||||||
| 	8, |  | ||||||
| 	9, |  | ||||||
| 	10, |  | ||||||
| 	12, |  | ||||||
| 	15, |  | ||||||
| 	18, |  | ||||||
| 	20, |  | ||||||
| 	25, |  | ||||||
| 	30, |  | ||||||
| 	40, |  | ||||||
| 	50, |  | ||||||
| 	60, |  | ||||||
| 	70, |  | ||||||
| 	80, |  | ||||||
| 	90, |  | ||||||
| 	100, |  | ||||||
| 	110, |  | ||||||
| 	120, |  | ||||||
| 	150, |  | ||||||
| 	180, |  | ||||||
| 	210, |  | ||||||
| 	240, |  | ||||||
| 	300, |  | ||||||
| 	360, |  | ||||||
| 	420, |  | ||||||
| 	480, |  | ||||||
| 	540, |  | ||||||
| 	600, |  | ||||||
| 	900, |  | ||||||
| 	1200, |  | ||||||
| 	1800, |  | ||||||
| 	2400, |  | ||||||
| 	3000, |  | ||||||
| 	3600, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type metricsExporter struct { |  | ||||||
| 	// Initialized during creation.
 |  | ||||||
| 	baseLabels |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type baseLabels struct { |  | ||||||
| 	scaleSetName      string |  | ||||||
| 	scaleSetNamespace string |  | ||||||
| 	enterprise        string |  | ||||||
| 	organization      string |  | ||||||
| 	repository        string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (b *baseLabels) jobLabels(jobBase *actions.JobMessageBase) prometheus.Labels { |  | ||||||
| 	return prometheus.Labels{ |  | ||||||
| 		labelKeyEnterprise:     b.enterprise, |  | ||||||
| 		labelKeyOrganization:   b.organization, |  | ||||||
| 		labelKeyRepository:     b.repository, |  | ||||||
| 		labelKeyJobName:        jobBase.JobDisplayName, |  | ||||||
| 		labelKeyJobWorkflowRef: jobBase.JobWorkflowRef, |  | ||||||
| 		labelKeyEventName:      jobBase.EventName, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (b *baseLabels) scaleSetLabels() prometheus.Labels { |  | ||||||
| 	return prometheus.Labels{ |  | ||||||
| 		labelKeyRunnerScaleSetName:      b.scaleSetName, |  | ||||||
| 		labelKeyRunnerScaleSetNamespace: b.scaleSetNamespace, |  | ||||||
| 		labelKeyEnterprise:              b.enterprise, |  | ||||||
| 		labelKeyOrganization:            b.organization, |  | ||||||
| 		labelKeyRepository:              b.repository, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (b *baseLabels) completedJobLabels(msg *actions.JobCompleted) prometheus.Labels { |  | ||||||
| 	l := b.jobLabels(&msg.JobMessageBase) |  | ||||||
| 	l[labelKeyJobResult] = msg.Result |  | ||||||
| 	return l |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (b *baseLabels) startedJobLabels(msg *actions.JobStarted) prometheus.Labels { |  | ||||||
| 	l := b.jobLabels(&msg.JobMessageBase) |  | ||||||
| 	return l |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (b *baseLabels) jobStartupDurationLabels(msg *actions.JobStarted) prometheus.Labels { |  | ||||||
| 	return prometheus.Labels{ |  | ||||||
| 		labelKeyEnterprise:   b.enterprise, |  | ||||||
| 		labelKeyOrganization: b.organization, |  | ||||||
| 		labelKeyRepository:   b.repository, |  | ||||||
| 		labelKeyEventName:    msg.EventName, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *metricsExporter) withBaseLabels(base baseLabels) { |  | ||||||
| 	m.baseLabels = base |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *metricsExporter) publishStatic(max, min int) { |  | ||||||
| 	l := m.scaleSetLabels() |  | ||||||
| 	maxRunners.With(l).Set(float64(max)) |  | ||||||
| 	minRunners.With(l).Set(float64(min)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *metricsExporter) publishStatistics(stats *actions.RunnerScaleSetStatistic) { |  | ||||||
| 	l := m.scaleSetLabels() |  | ||||||
| 
 |  | ||||||
| 	// availableJobs.With(l).Set(float64(stats.TotalAvailableJobs))
 |  | ||||||
| 	// acquiredJobs.With(l).Set(float64(stats.TotalAcquiredJobs))
 |  | ||||||
| 	assignedJobs.With(l).Set(float64(stats.TotalAssignedJobs)) |  | ||||||
| 	runningJobs.With(l).Set(float64(stats.TotalRunningJobs)) |  | ||||||
| 	registeredRunners.With(l).Set(float64(stats.TotalRegisteredRunners)) |  | ||||||
| 	busyRunners.With(l).Set(float64(stats.TotalBusyRunners)) |  | ||||||
| 	idleRunners.With(l).Set(float64(stats.TotalIdleRunners)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *metricsExporter) publishJobStarted(msg *actions.JobStarted) { |  | ||||||
| 	l := m.startedJobLabels(msg) |  | ||||||
| 	startedJobsTotal.With(l).Inc() |  | ||||||
| 
 |  | ||||||
| 	l = m.jobStartupDurationLabels(msg) |  | ||||||
| 	startupDuration := msg.JobMessageBase.RunnerAssignTime.Unix() - msg.JobMessageBase.ScaleSetAssignTime.Unix() |  | ||||||
| 	jobStartupDurationSeconds.With(l).Observe(float64(startupDuration)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // func (m *metricsExporter) publishJobAssigned(msg *actions.JobAssigned) {
 |  | ||||||
| // 	l := m.jobLabels(&msg.JobMessageBase)
 |  | ||||||
| // 	queueDuration := msg.JobMessageBase.ScaleSetAssignTime.Unix() - msg.JobMessageBase.QueueTime.Unix()
 |  | ||||||
| // 	jobQueueDurationSeconds.With(l).Observe(float64(queueDuration))
 |  | ||||||
| // }
 |  | ||||||
| 
 |  | ||||||
| func (m *metricsExporter) publishJobCompleted(msg *actions.JobCompleted) { |  | ||||||
| 	l := m.completedJobLabels(msg) |  | ||||||
| 	completedJobsTotal.With(l).Inc() |  | ||||||
| 
 |  | ||||||
| 	executionDuration := msg.JobMessageBase.FinishTime.Unix() - msg.JobMessageBase.RunnerAssignTime.Unix() |  | ||||||
| 	jobExecutionDurationSeconds.With(l).Observe(float64(executionDuration)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *metricsExporter) publishDesiredRunners(count int) { |  | ||||||
| 	desiredRunners.With(m.scaleSetLabels()).Set(float64(count)) |  | ||||||
| } |  | ||||||
|  | @ -1,56 +0,0 @@ | ||||||
| // Code generated by mockery v2.36.1. DO NOT EDIT.
 |  | ||||||
| 
 |  | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	context "context" |  | ||||||
| 
 |  | ||||||
| 	mock "github.com/stretchr/testify/mock" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // MockKubernetesManager is an autogenerated mock type for the KubernetesManager type
 |  | ||||||
| type MockKubernetesManager struct { |  | ||||||
| 	mock.Mock |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ScaleEphemeralRunnerSet provides a mock function with given fields: ctx, namespace, resourceName, runnerCount
 |  | ||||||
| func (_m *MockKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace string, resourceName string, runnerCount int) error { |  | ||||||
| 	ret := _m.Called(ctx, namespace, resourceName, runnerCount) |  | ||||||
| 
 |  | ||||||
| 	var r0 error |  | ||||||
| 	if rf, ok := ret.Get(0).(func(context.Context, string, string, int) error); ok { |  | ||||||
| 		r0 = rf(ctx, namespace, resourceName, runnerCount) |  | ||||||
| 	} else { |  | ||||||
| 		r0 = ret.Error(0) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return r0 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // UpdateEphemeralRunnerWithJobInfo provides a mock function with given fields: ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId
 |  | ||||||
| func (_m *MockKubernetesManager) UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace string, resourceName string, ownerName string, repositoryName string, jobWorkflowRef string, jobDisplayName string, jobRequestId int64, workflowRunId int64) error { |  | ||||||
| 	ret := _m.Called(ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId) |  | ||||||
| 
 |  | ||||||
| 	var r0 error |  | ||||||
| 	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, int64, int64) error); ok { |  | ||||||
| 		r0 = rf(ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId) |  | ||||||
| 	} else { |  | ||||||
| 		r0 = ret.Error(0) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return r0 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewMockKubernetesManager creates a new instance of MockKubernetesManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
 |  | ||||||
| // The first argument is typically a *testing.T value.
 |  | ||||||
| func NewMockKubernetesManager(t interface { |  | ||||||
| 	mock.TestingT |  | ||||||
| 	Cleanup(func()) |  | ||||||
| }) *MockKubernetesManager { |  | ||||||
| 	mock := &MockKubernetesManager{} |  | ||||||
| 	mock.Mock.Test(t) |  | ||||||
| 
 |  | ||||||
| 	t.Cleanup(func() { mock.AssertExpectations(t) }) |  | ||||||
| 
 |  | ||||||
| 	return mock |  | ||||||
| } |  | ||||||
|  | @ -1,58 +0,0 @@ | ||||||
| // Code generated by mockery v2.36.1. DO NOT EDIT.
 |  | ||||||
| 
 |  | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	context "context" |  | ||||||
| 
 |  | ||||||
| 	actions "github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 
 |  | ||||||
| 	mock "github.com/stretchr/testify/mock" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // MockRunnerScaleSetClient is an autogenerated mock type for the RunnerScaleSetClient type
 |  | ||||||
| type MockRunnerScaleSetClient struct { |  | ||||||
| 	mock.Mock |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // AcquireJobsForRunnerScaleSet provides a mock function with given fields: ctx, requestIds
 |  | ||||||
| func (_m *MockRunnerScaleSetClient) AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error { |  | ||||||
| 	ret := _m.Called(ctx, requestIds) |  | ||||||
| 
 |  | ||||||
| 	var r0 error |  | ||||||
| 	if rf, ok := ret.Get(0).(func(context.Context, []int64) error); ok { |  | ||||||
| 		r0 = rf(ctx, requestIds) |  | ||||||
| 	} else { |  | ||||||
| 		r0 = ret.Error(0) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return r0 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetRunnerScaleSetMessage provides a mock function with given fields: ctx, handler, maxCapacity
 |  | ||||||
| func (_m *MockRunnerScaleSetClient) GetRunnerScaleSetMessage(ctx context.Context, handler func(*actions.RunnerScaleSetMessage) error, maxCapacity int) error { |  | ||||||
| 	ret := _m.Called(ctx, handler, maxCapacity) |  | ||||||
| 
 |  | ||||||
| 	var r0 error |  | ||||||
| 	if rf, ok := ret.Get(0).(func(context.Context, func(*actions.RunnerScaleSetMessage) error, int) error); ok { |  | ||||||
| 		r0 = rf(ctx, handler, maxCapacity) |  | ||||||
| 	} else { |  | ||||||
| 		r0 = ret.Error(0) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return r0 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewMockRunnerScaleSetClient creates a new instance of MockRunnerScaleSetClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
 |  | ||||||
| // The first argument is typically a *testing.T value.
 |  | ||||||
| func NewMockRunnerScaleSetClient(t interface { |  | ||||||
| 	mock.TestingT |  | ||||||
| 	Cleanup(func()) |  | ||||||
| }) *MockRunnerScaleSetClient { |  | ||||||
| 	mock := &MockRunnerScaleSetClient{} |  | ||||||
| 	mock.Mock.Test(t) |  | ||||||
| 
 |  | ||||||
| 	t.Cleanup(func() { mock.AssertExpectations(t) }) |  | ||||||
| 
 |  | ||||||
| 	return mock |  | ||||||
| } |  | ||||||
|  | @ -1,127 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/go-logr/logr" |  | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type SessionRefreshingClient struct { |  | ||||||
| 	client  actions.ActionsService |  | ||||||
| 	logger  logr.Logger |  | ||||||
| 	session *actions.RunnerScaleSetSession |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func newSessionClient(client actions.ActionsService, logger *logr.Logger, session *actions.RunnerScaleSetSession) *SessionRefreshingClient { |  | ||||||
| 	return &SessionRefreshingClient{ |  | ||||||
| 		client:  client, |  | ||||||
| 		session: session, |  | ||||||
| 		logger:  logger.WithName("refreshing_client"), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *SessionRefreshingClient) GetMessage(ctx context.Context, lastMessageId int64, maxCapacity int) (*actions.RunnerScaleSetMessage, error) { |  | ||||||
| 	if maxCapacity < 0 { |  | ||||||
| 		return nil, fmt.Errorf("maxCapacity must be greater than or equal to 0") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	message, err := m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId, maxCapacity) |  | ||||||
| 	if err == nil { |  | ||||||
| 		return message, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	expiredError := &actions.MessageQueueTokenExpiredError{} |  | ||||||
| 	if !errors.As(err, &expiredError) { |  | ||||||
| 		return nil, fmt.Errorf("get message failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.logger.Info("message queue token is expired during GetNextMessage, refreshing...") |  | ||||||
| 	session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("refresh message session failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.session = session |  | ||||||
| 	message, err = m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId, maxCapacity) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("delete message failed after refresh message session. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return message, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *SessionRefreshingClient) DeleteMessage(ctx context.Context, messageId int64) error { |  | ||||||
| 	err := m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId) |  | ||||||
| 	if err == nil { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	expiredError := &actions.MessageQueueTokenExpiredError{} |  | ||||||
| 	if !errors.As(err, &expiredError) { |  | ||||||
| 		return fmt.Errorf("delete message failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.logger.Info("message queue token is expired during DeleteMessage, refreshing...") |  | ||||||
| 	session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("refresh message session failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.session = session |  | ||||||
| 	err = m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("delete message failed after refresh message session. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *SessionRefreshingClient) AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error) { |  | ||||||
| 	ids, err := m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds) |  | ||||||
| 	if err == nil { |  | ||||||
| 		return ids, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	expiredError := &actions.MessageQueueTokenExpiredError{} |  | ||||||
| 	if !errors.As(err, &expiredError) { |  | ||||||
| 		return nil, fmt.Errorf("acquire jobs failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.logger.Info("message queue token is expired during AcquireJobs, refreshing...") |  | ||||||
| 	session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("refresh message session failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.session = session |  | ||||||
| 	ids, err = m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("acquire jobs failed after refresh message session. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return ids, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *SessionRefreshingClient) Close() error { |  | ||||||
| 	if m.session == nil { |  | ||||||
| 		m.logger.Info("session is already deleted. (no-op)") |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ctxWithTimeout, cancel := context.WithTimeout(context.Background(), time.Second*30) |  | ||||||
| 	defer cancel() |  | ||||||
| 
 |  | ||||||
| 	m.logger.Info("deleting session.") |  | ||||||
| 	err := m.client.DeleteMessageSession(ctxWithTimeout, m.session.RunnerScaleSet.Id, m.session.SessionId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("delete message session failed. %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m.session = nil |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -1,421 +0,0 @@ | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" |  | ||||||
| 	"github.com/actions/actions-runner-controller/logging" |  | ||||||
| 	"github.com/google/uuid" |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/mock" |  | ||||||
| 	"github.com/stretchr/testify/require" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestGetMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0), 10).Return(nil, nil).Once() |  | ||||||
| 	mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0), 10).Return(&actions.RunnerScaleSetMessage{MessageId: 1}, nil).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	msg, err := client.GetMessage(ctx, 0, 10) |  | ||||||
| 	require.NoError(t, err, "GetMessage should not return an error") |  | ||||||
| 
 |  | ||||||
| 	assert.Nil(t, msg, "GetMessage should return nil message") |  | ||||||
| 
 |  | ||||||
| 	msg, err = client.GetMessage(ctx, 0, 10) |  | ||||||
| 	require.NoError(t, err, "GetMessage should not return an error") |  | ||||||
| 
 |  | ||||||
| 	assert.Equal(t, int64(1), msg.MessageId, "GetMessage should return a message with id 1") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteMessage(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	err := client.DeleteMessage(ctx, int64(1)) |  | ||||||
| 	assert.NoError(t, err, "DeleteMessage should not return an error") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobs(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1}, nil) |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3}) |  | ||||||
| 	assert.NoError(t, err, "AcquireJobs should not return an error") |  | ||||||
| 	assert.Equal(t, []int64{1}, ids, "AcquireJobs should return a slice with one id") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestClose(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("DeleteMessageSession", mock.Anything, 1, &sessionId).Return(nil).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	err := client.Close() |  | ||||||
| 	assert.NoError(t, err, "DeleteMessageSession should not return an error") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetMessage_Error(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0), 10).Return(nil, fmt.Errorf("error")).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	msg, err := client.GetMessage(ctx, 0, 10) |  | ||||||
| 	assert.ErrorContains(t, err, "get message failed. error", "GetMessage should return an error") |  | ||||||
| 	assert.Nil(t, msg, "GetMessage should return nil message") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteMessage_SessionError(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(fmt.Errorf("error")).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	err := client.DeleteMessage(ctx, int64(1)) |  | ||||||
| 	assert.ErrorContains(t, err, "delete message failed. error", "DeleteMessage should return an error") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobs_Error(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, fmt.Errorf("error")).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 
 |  | ||||||
| 	ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3}) |  | ||||||
| 	assert.ErrorContains(t, err, "acquire jobs failed. error", "AcquireJobs should return an error") |  | ||||||
| 	assert.Nil(t, ids, "AcquireJobs should return nil ids") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetMessage_RefreshToken(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0), 10).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once() |  | ||||||
| 	mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, "token2", int64(0), 10).Return(&actions.RunnerScaleSetMessage{ |  | ||||||
| 		MessageId:   1, |  | ||||||
| 		MessageType: "test", |  | ||||||
| 		Body:        "test", |  | ||||||
| 	}, nil).Once() |  | ||||||
| 	mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token2", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	}, nil).Once() |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 	msg, err := client.GetMessage(ctx, 0, 10) |  | ||||||
| 	assert.NoError(t, err, "Error getting message") |  | ||||||
| 	assert.Equal(t, int64(1), msg.MessageId, "message id should be updated") |  | ||||||
| 	assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteMessage_RefreshSessionToken(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(&actions.MessageQueueTokenExpiredError{}).Once() |  | ||||||
| 	mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, "token2", int64(1)).Return(nil).Once() |  | ||||||
| 	mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token2", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 	err := client.DeleteMessage(ctx, 1) |  | ||||||
| 	assert.NoError(t, err, "Error delete message") |  | ||||||
| 	assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobs_RefreshToken(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("AcquireJobs", ctx, mock.Anything, session.MessageQueueAccessToken, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once() |  | ||||||
| 	mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token2", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1, 2, 3}, nil) |  | ||||||
| 	mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token2", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	}, nil) |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 	ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3}) |  | ||||||
| 	assert.NoError(t, err, "Error acquiring jobs") |  | ||||||
| 	assert.Equal(t, []int64{1, 2, 3}, ids, "Job ids should be returned") |  | ||||||
| 	assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetMessage_RefreshToken_Failed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0), 10).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once() |  | ||||||
| 	mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 	msg, err := client.GetMessage(ctx, 0, 10) |  | ||||||
| 	assert.ErrorContains(t, err, "refresh message session failed. error", "Error should be returned") |  | ||||||
| 	assert.Nil(t, msg, "Message should be nil") |  | ||||||
| 	assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDeleteMessage_RefreshToken_Failed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(&actions.MessageQueueTokenExpiredError{}).Once() |  | ||||||
| 	mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 	err := client.DeleteMessage(ctx, 1) |  | ||||||
| 
 |  | ||||||
| 	assert.ErrorContains(t, err, "refresh message session failed. error", "Error getting message") |  | ||||||
| 	assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAcquireJobs_RefreshToken_Failed(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessionId := uuid.New() |  | ||||||
| 	session := &actions.RunnerScaleSetSession{ |  | ||||||
| 		SessionId:               &sessionId, |  | ||||||
| 		OwnerName:               "owner", |  | ||||||
| 		MessageQueueUrl:         "https://github.com", |  | ||||||
| 		MessageQueueAccessToken: "token", |  | ||||||
| 		RunnerScaleSet: &actions.RunnerScaleSet{ |  | ||||||
| 			Id: 1, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mockActionsClient.On("AcquireJobs", ctx, mock.Anything, session.MessageQueueAccessToken, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once() |  | ||||||
| 	mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error")) |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, session) |  | ||||||
| 	ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3}) |  | ||||||
| 	assert.ErrorContains(t, err, "refresh message session failed. error", "Expect error refreshing message session") |  | ||||||
| 	assert.Nil(t, ids, "Job ids should be nil") |  | ||||||
| 	assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestClose_Skip(t *testing.T) { |  | ||||||
| 	mockActionsClient := &actions.MockActionsService{} |  | ||||||
| 	logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText) |  | ||||||
| 	logger = logger.WithName(t.Name()) |  | ||||||
| 	require.NoError(t, log_err, "Error creating logger") |  | ||||||
| 
 |  | ||||||
| 	client := newSessionClient(mockActionsClient, &logger, nil) |  | ||||||
| 	err := client.Close() |  | ||||||
| 	require.NoError(t, err, "Error closing session client") |  | ||||||
| 	assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met") |  | ||||||
| } |  | ||||||
|  | @ -284,15 +284,14 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au | ||||||
| 		if listenerPod.ObjectMeta.DeletionTimestamp.IsZero() { | 		if listenerPod.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			logger.Info("Deleting the listener pod") | 			logger.Info("Deleting the listener pod") | ||||||
| 			if err := r.Delete(ctx, listenerPod); err != nil { | 			if err := r.Delete(ctx, listenerPod); err != nil { | ||||||
| 				return false, fmt.Errorf("failed to delete listener pod: %v", err) | 				return false, fmt.Errorf("failed to delete listener pod: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case kerrors.IsNotFound(err): | ||||||
| 		return false, fmt.Errorf("failed to get listener pods: %v", err) |  | ||||||
| 
 |  | ||||||
| 	default: // NOT FOUND
 |  | ||||||
| 		_ = r.publishRunningListener(autoscalingListener, false) // If error is returned, we never published metrics so it is safe to ignore
 | 		_ = r.publishRunningListener(autoscalingListener, false) // If error is returned, we never published metrics so it is safe to ignore
 | ||||||
|  | 	default: | ||||||
|  | 		return false, fmt.Errorf("failed to get listener pods: %w", err) | ||||||
| 	} | 	} | ||||||
| 	logger.Info("Listener pod is deleted") | 	logger.Info("Listener pod is deleted") | ||||||
| 
 | 
 | ||||||
|  | @ -303,12 +302,12 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au | ||||||
| 		if secret.ObjectMeta.DeletionTimestamp.IsZero() { | 		if secret.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			logger.Info("Deleting the listener config secret") | 			logger.Info("Deleting the listener config secret") | ||||||
| 			if err := r.Delete(ctx, &secret); err != nil { | 			if err := r.Delete(ctx, &secret); err != nil { | ||||||
| 				return false, fmt.Errorf("failed to delete listener config secret: %v", err) | 				return false, fmt.Errorf("failed to delete listener config secret: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case !kerrors.IsNotFound(err): | ||||||
| 		return false, fmt.Errorf("failed to get listener config secret: %v", err) | 		return false, fmt.Errorf("failed to get listener config secret: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if autoscalingListener.Spec.Proxy != nil { | 	if autoscalingListener.Spec.Proxy != nil { | ||||||
|  | @ -320,12 +319,12 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au | ||||||
| 			if proxySecret.ObjectMeta.DeletionTimestamp.IsZero() { | 			if proxySecret.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 				logger.Info("Deleting the listener proxy secret") | 				logger.Info("Deleting the listener proxy secret") | ||||||
| 				if err := r.Delete(ctx, proxySecret); err != nil { | 				if err := r.Delete(ctx, proxySecret); err != nil { | ||||||
| 					return false, fmt.Errorf("failed to delete listener proxy secret: %v", err) | 					return false, fmt.Errorf("failed to delete listener proxy secret: %w", err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			return false, nil | 			return false, nil | ||||||
| 		case err != nil && !kerrors.IsNotFound(err): | 		case !kerrors.IsNotFound(err): | ||||||
| 			return false, fmt.Errorf("failed to get listener proxy secret: %v", err) | 			return false, fmt.Errorf("failed to get listener proxy secret: %w", err) | ||||||
| 		} | 		} | ||||||
| 		logger.Info("Listener proxy secret is deleted") | 		logger.Info("Listener proxy secret is deleted") | ||||||
| 	} | 	} | ||||||
|  | @ -337,12 +336,12 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au | ||||||
| 		if listenerRoleBinding.ObjectMeta.DeletionTimestamp.IsZero() { | 		if listenerRoleBinding.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			logger.Info("Deleting the listener role binding") | 			logger.Info("Deleting the listener role binding") | ||||||
| 			if err := r.Delete(ctx, listenerRoleBinding); err != nil { | 			if err := r.Delete(ctx, listenerRoleBinding); err != nil { | ||||||
| 				return false, fmt.Errorf("failed to delete listener role binding: %v", err) | 				return false, fmt.Errorf("failed to delete listener role binding: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case !kerrors.IsNotFound(err): | ||||||
| 		return false, fmt.Errorf("failed to get listener role binding: %v", err) | 		return false, fmt.Errorf("failed to get listener role binding: %w", err) | ||||||
| 	} | 	} | ||||||
| 	logger.Info("Listener role binding is deleted") | 	logger.Info("Listener role binding is deleted") | ||||||
| 
 | 
 | ||||||
|  | @ -353,12 +352,12 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au | ||||||
| 		if listenerRole.ObjectMeta.DeletionTimestamp.IsZero() { | 		if listenerRole.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			logger.Info("Deleting the listener role") | 			logger.Info("Deleting the listener role") | ||||||
| 			if err := r.Delete(ctx, listenerRole); err != nil { | 			if err := r.Delete(ctx, listenerRole); err != nil { | ||||||
| 				return false, fmt.Errorf("failed to delete listener role: %v", err) | 				return false, fmt.Errorf("failed to delete listener role: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case !kerrors.IsNotFound(err): | ||||||
| 		return false, fmt.Errorf("failed to get listener role: %v", err) | 		return false, fmt.Errorf("failed to get listener role: %w", err) | ||||||
| 	} | 	} | ||||||
| 	logger.Info("Listener role is deleted") | 	logger.Info("Listener role is deleted") | ||||||
| 
 | 
 | ||||||
|  | @ -370,12 +369,12 @@ func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, au | ||||||
| 		if listenerSa.ObjectMeta.DeletionTimestamp.IsZero() { | 		if listenerSa.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			logger.Info("Deleting the listener service account") | 			logger.Info("Deleting the listener service account") | ||||||
| 			if err := r.Delete(ctx, listenerSa); err != nil { | 			if err := r.Delete(ctx, listenerSa); err != nil { | ||||||
| 				return false, fmt.Errorf("failed to delete listener service account: %v", err) | 				return false, fmt.Errorf("failed to delete listener service account: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case !kerrors.IsNotFound(err): | ||||||
| 		return false, fmt.Errorf("failed to get listener service account: %v", err) | 		return false, fmt.Errorf("failed to get listener service account: %w", err) | ||||||
| 	} | 	} | ||||||
| 	logger.Info("Listener service account is deleted") | 	logger.Info("Listener service account is deleted") | ||||||
| 
 | 
 | ||||||
|  | @ -447,7 +446,7 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a | ||||||
| 		var err error | 		var err error | ||||||
| 		cert, err = r.certificate(ctx, autoscalingRunnerSet, autoscalingListener) | 		cert, err = r.certificate(ctx, autoscalingRunnerSet, autoscalingListener) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return ctrl.Result{}, fmt.Errorf("failed to create certificate env var for listener: %v", err) | 			return ctrl.Result{}, fmt.Errorf("failed to create certificate env var for listener: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import ( | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
| 
 | 
 | ||||||
| 	listenerconfig "github.com/actions/actions-runner-controller/cmd/githubrunnerscalesetlistener/config" | 	listenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config" | ||||||
| 	. "github.com/onsi/ginkgo/v2" | 	. "github.com/onsi/ginkgo/v2" | ||||||
| 	. "github.com/onsi/gomega" | 	. "github.com/onsi/gomega" | ||||||
| 	kerrors "k8s.io/apimachinery/pkg/api/errors" | 	kerrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  |  | ||||||
|  | @ -335,12 +335,12 @@ func (r *AutoscalingRunnerSetReconciler) cleanupListener(ctx context.Context, au | ||||||
| 		if listener.ObjectMeta.DeletionTimestamp.IsZero() { | 		if listener.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			logger.Info("Deleting the listener") | 			logger.Info("Deleting the listener") | ||||||
| 			if err := r.Delete(ctx, &listener); err != nil { | 			if err := r.Delete(ctx, &listener); err != nil { | ||||||
| 				return false, fmt.Errorf("failed to delete listener: %v", err) | 				return false, fmt.Errorf("failed to delete listener: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case !kerrors.IsNotFound(err): | ||||||
| 		return false, fmt.Errorf("failed to get listener: %v", err) | 		return false, fmt.Errorf("failed to get listener: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	logger.Info("Listener is deleted") | 	logger.Info("Listener is deleted") | ||||||
|  | @ -351,7 +351,7 @@ func (r *AutoscalingRunnerSetReconciler) cleanupEphemeralRunnerSets(ctx context. | ||||||
| 	logger.Info("Cleaning up ephemeral runner sets") | 	logger.Info("Cleaning up ephemeral runner sets") | ||||||
| 	runnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet) | 	runnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, fmt.Errorf("failed to list ephemeral runner sets: %v", err) | 		return false, fmt.Errorf("failed to list ephemeral runner sets: %w", err) | ||||||
| 	} | 	} | ||||||
| 	if runnerSets.empty() { | 	if runnerSets.empty() { | ||||||
| 		logger.Info("All ephemeral runner sets are deleted") | 		logger.Info("All ephemeral runner sets are deleted") | ||||||
|  | @ -360,7 +360,7 @@ func (r *AutoscalingRunnerSetReconciler) cleanupEphemeralRunnerSets(ctx context. | ||||||
| 
 | 
 | ||||||
| 	logger.Info("Deleting all ephemeral runner sets", "count", runnerSets.count()) | 	logger.Info("Deleting all ephemeral runner sets", "count", runnerSets.count()) | ||||||
| 	if err := r.deleteEphemeralRunnerSets(ctx, runnerSets.all(), logger); err != nil { | 	if err := r.deleteEphemeralRunnerSets(ctx, runnerSets.all(), logger); err != nil { | ||||||
| 		return false, fmt.Errorf("failed to delete ephemeral runner sets: %v", err) | 		return false, fmt.Errorf("failed to delete ephemeral runner sets: %w", err) | ||||||
| 	} | 	} | ||||||
| 	return false, nil | 	return false, nil | ||||||
| } | } | ||||||
|  | @ -375,7 +375,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteEphemeralRunnerSets(ctx context.C | ||||||
| 		} | 		} | ||||||
| 		logger.Info("Deleting ephemeral runner set", "name", rs.Name) | 		logger.Info("Deleting ephemeral runner set", "name", rs.Name) | ||||||
| 		if err := r.Delete(ctx, rs); err != nil { | 		if err := r.Delete(ctx, rs); err != nil { | ||||||
| 			return fmt.Errorf("failed to delete EphemeralRunnerSet resource: %v", err) | 			return fmt.Errorf("failed to delete EphemeralRunnerSet resource: %w", err) | ||||||
| 		} | 		} | ||||||
| 		logger.Info("Deleted ephemeral runner set", "name", rs.Name) | 		logger.Info("Deleted ephemeral runner set", "name", rs.Name) | ||||||
| 	} | 	} | ||||||
|  | @ -670,7 +670,7 @@ func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(c | ||||||
| func (r *AutoscalingRunnerSetReconciler) listEphemeralRunnerSets(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*EphemeralRunnerSets, error) { | func (r *AutoscalingRunnerSetReconciler) listEphemeralRunnerSets(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*EphemeralRunnerSets, error) { | ||||||
| 	list := new(v1alpha1.EphemeralRunnerSetList) | 	list := new(v1alpha1.EphemeralRunnerSetList) | ||||||
| 	if err := r.List(ctx, list, client.InNamespace(autoscalingRunnerSet.Namespace), client.MatchingFields{resourceOwnerKey: autoscalingRunnerSet.Name}); err != nil { | 	if err := r.List(ctx, list, client.InNamespace(autoscalingRunnerSet.Namespace), client.MatchingFields{resourceOwnerKey: autoscalingRunnerSet.Name}); err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to list ephemeral runner sets: %v", err) | 		return nil, fmt.Errorf("failed to list ephemeral runner sets: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return &EphemeralRunnerSets{list: list}, nil | 	return &EphemeralRunnerSets{list: list}, nil | ||||||
|  | @ -814,7 +814,7 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeKubernetesModeRol | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from container mode kubernetes role binding", "name", roleBindingName) | 		c.logger.Info("Removed finalizer from container mode kubernetes role binding", "name", roleBindingName) | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case !kerrors.IsNotFound(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch kubernetes mode role binding: %w", err) | 		c.err = fmt.Errorf("failed to fetch kubernetes mode role binding: %w", err) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
|  | @ -856,11 +856,11 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeKubernetesModeRol | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from container mode kubernetes role") | 		c.logger.Info("Removed finalizer from container mode kubernetes role") | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case kerrors.IsNotFound(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch kubernetes mode role: %w", err) | 		c.logger.Info("Container mode kubernetes role has already been deleted", "name", roleName) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
| 		c.logger.Info("Container mode kubernetes role has already been deleted", "name", roleName) | 		c.err = fmt.Errorf("failed to fetch kubernetes mode role: %w", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -899,11 +899,11 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeKubernetesModeSer | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from container mode kubernetes service account") | 		c.logger.Info("Removed finalizer from container mode kubernetes service account") | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case kerrors.IsNotFound(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch kubernetes mode service account: %w", err) | 		c.logger.Info("Container mode kubernetes service account has already been deleted", "name", serviceAccountName) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
| 		c.logger.Info("Container mode kubernetes service account has already been deleted", "name", serviceAccountName) | 		c.err = fmt.Errorf("failed to fetch kubernetes mode service account: %w", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -942,11 +942,11 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeNoPermissionServi | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from no permission service account", "name", serviceAccountName) | 		c.logger.Info("Removed finalizer from no permission service account", "name", serviceAccountName) | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case kerrors.IsNotFound(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch service account: %w", err) | 		c.logger.Info("No permission service account has already been deleted", "name", serviceAccountName) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
| 		c.logger.Info("No permission service account has already been deleted", "name", serviceAccountName) | 		c.err = fmt.Errorf("failed to fetch service account: %w", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -985,11 +985,11 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeGitHubSecretFinal | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from GitHub secret", "name", githubSecretName) | 		c.logger.Info("Removed finalizer from GitHub secret", "name", githubSecretName) | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err) && !kerrors.IsForbidden(err): | 	case kerrors.IsNotFound(err) || kerrors.IsForbidden(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch GitHub secret: %w", err) | 		c.logger.Info("GitHub secret has already been deleted", "name", githubSecretName) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
| 		c.logger.Info("GitHub secret has already been deleted", "name", githubSecretName) | 		c.err = fmt.Errorf("failed to fetch GitHub secret: %w", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -1028,11 +1028,11 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeManagerRoleBindin | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from manager role binding", "name", managerRoleBindingName) | 		c.logger.Info("Removed finalizer from manager role binding", "name", managerRoleBindingName) | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case kerrors.IsNotFound(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch manager role binding: %w", err) | 		c.logger.Info("Manager role binding has already been deleted", "name", managerRoleBindingName) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
| 		c.logger.Info("Manager role binding has already been deleted", "name", managerRoleBindingName) | 		c.err = fmt.Errorf("failed to fetch manager role binding: %w", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -1071,11 +1071,11 @@ func (c *autoscalingRunnerSetFinalizerDependencyCleaner) removeManagerRoleFinali | ||||||
| 		} | 		} | ||||||
| 		c.logger.Info("Removed finalizer from manager role", "name", managerRoleName) | 		c.logger.Info("Removed finalizer from manager role", "name", managerRoleName) | ||||||
| 		return | 		return | ||||||
| 	case err != nil && !kerrors.IsNotFound(err): | 	case kerrors.IsNotFound(err): | ||||||
| 		c.err = fmt.Errorf("failed to fetch manager role: %w", err) | 		c.logger.Info("Manager role has already been deleted", "name", managerRoleName) | ||||||
| 		return | 		return | ||||||
| 	default: | 	default: | ||||||
| 		c.logger.Info("Manager role has already been deleted", "name", managerRoleName) | 		c.err = fmt.Errorf("failed to fetch manager role: %w", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -344,7 +344,7 @@ func (r *EphemeralRunnerReconciler) cleanupResources(ctx context.Context, epheme | ||||||
| 		if pod.ObjectMeta.DeletionTimestamp.IsZero() { | 		if pod.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			log.Info("Deleting the runner pod") | 			log.Info("Deleting the runner pod") | ||||||
| 			if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) { | 			if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) { | ||||||
| 				return false, fmt.Errorf("failed to delete pod: %v", err) | 				return false, fmt.Errorf("failed to delete pod: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
|  | @ -361,7 +361,7 @@ func (r *EphemeralRunnerReconciler) cleanupResources(ctx context.Context, epheme | ||||||
| 		if secret.ObjectMeta.DeletionTimestamp.IsZero() { | 		if secret.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 			log.Info("Deleting the jitconfig secret") | 			log.Info("Deleting the jitconfig secret") | ||||||
| 			if err := r.Delete(ctx, secret); err != nil && !kerrors.IsNotFound(err) { | 			if err := r.Delete(ctx, secret); err != nil && !kerrors.IsNotFound(err) { | ||||||
| 				return false, fmt.Errorf("failed to delete secret: %v", err) | 				return false, fmt.Errorf("failed to delete secret: %w", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, nil | 		return false, nil | ||||||
|  | @ -377,7 +377,7 @@ func (r *EphemeralRunnerReconciler) cleanupContainerHooksResources(ctx context.C | ||||||
| 	log.Info("Cleaning up runner linked pods") | 	log.Info("Cleaning up runner linked pods") | ||||||
| 	done, err = r.cleanupRunnerLinkedPods(ctx, ephemeralRunner, log) | 	done, err = r.cleanupRunnerLinkedPods(ctx, ephemeralRunner, log) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, fmt.Errorf("failed to clean up runner linked pods: %v", err) | 		return false, fmt.Errorf("failed to clean up runner linked pods: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !done { | 	if !done { | ||||||
|  | @ -402,7 +402,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedPods(ctx context.Context, | ||||||
| 	var runnerLinkedPodList corev1.PodList | 	var runnerLinkedPodList corev1.PodList | ||||||
| 	err = r.List(ctx, &runnerLinkedPodList, client.InNamespace(ephemeralRunner.Namespace), runnerLinedLabels) | 	err = r.List(ctx, &runnerLinkedPodList, client.InNamespace(ephemeralRunner.Namespace), runnerLinedLabels) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, fmt.Errorf("failed to list runner-linked pods: %v", err) | 		return false, fmt.Errorf("failed to list runner-linked pods: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(runnerLinkedPodList.Items) == 0 { | 	if len(runnerLinkedPodList.Items) == 0 { | ||||||
|  | @ -421,7 +421,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedPods(ctx context.Context, | ||||||
| 
 | 
 | ||||||
| 		log.Info("Deleting container hooks runner-linked pod", "name", linkedPod.Name) | 		log.Info("Deleting container hooks runner-linked pod", "name", linkedPod.Name) | ||||||
| 		if err := r.Delete(ctx, linkedPod); err != nil && !kerrors.IsNotFound(err) { | 		if err := r.Delete(ctx, linkedPod); err != nil && !kerrors.IsNotFound(err) { | ||||||
| 			errs = append(errs, fmt.Errorf("failed to delete runner linked pod %q: %v", linkedPod.Name, err)) | 			errs = append(errs, fmt.Errorf("failed to delete runner linked pod %q: %w", linkedPod.Name, err)) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -456,7 +456,7 @@ func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedSecrets(ctx context.Conte | ||||||
| 
 | 
 | ||||||
| 		log.Info("Deleting container hooks runner-linked secret", "name", s.Name) | 		log.Info("Deleting container hooks runner-linked secret", "name", s.Name) | ||||||
| 		if err := r.Delete(ctx, s); err != nil && !kerrors.IsNotFound(err) { | 		if err := r.Delete(ctx, s); err != nil && !kerrors.IsNotFound(err) { | ||||||
| 			errs = append(errs, fmt.Errorf("failed to delete runner linked secret %q: %v", s.Name, err)) | 			errs = append(errs, fmt.Errorf("failed to delete runner linked secret %q: %w", s.Name, err)) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -470,12 +470,12 @@ func (r *EphemeralRunnerReconciler) markAsFailed(ctx context.Context, ephemeralR | ||||||
| 		obj.Status.Reason = reason | 		obj.Status.Reason = reason | ||||||
| 		obj.Status.Message = errMessage | 		obj.Status.Message = errMessage | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %v", err) | 		return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Removing the runner from the service") | 	log.Info("Removing the runner from the service") | ||||||
| 	if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil { | 	if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil { | ||||||
| 		return fmt.Errorf("failed to remove the runner from service: %v", err) | 		return fmt.Errorf("failed to remove the runner from service: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("EphemeralRunner is marked as Failed and deleted from the service") | 	log.Info("EphemeralRunner is marked as Failed and deleted from the service") | ||||||
|  | @ -487,7 +487,7 @@ func (r *EphemeralRunnerReconciler) markAsFinished(ctx context.Context, ephemera | ||||||
| 	if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) { | 	if err := patchSubResource(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) { | ||||||
| 		obj.Status.Phase = corev1.PodSucceeded | 		obj.Status.Phase = corev1.PodSucceeded | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		return fmt.Errorf("failed to update ephemeral runner with status finished: %v", err) | 		return fmt.Errorf("failed to update ephemeral runner with status finished: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("EphemeralRunner status is marked as Finished") | 	log.Info("EphemeralRunner status is marked as Finished") | ||||||
|  | @ -500,7 +500,7 @@ func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephem | ||||||
| 	if pod.ObjectMeta.DeletionTimestamp.IsZero() { | 	if pod.ObjectMeta.DeletionTimestamp.IsZero() { | ||||||
| 		log.Info("Deleting the ephemeral runner pod", "podId", pod.UID) | 		log.Info("Deleting the ephemeral runner pod", "podId", pod.UID) | ||||||
| 		if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) { | 		if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) { | ||||||
| 			return fmt.Errorf("failed to delete pod with status failed: %v", err) | 			return fmt.Errorf("failed to delete pod with status failed: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -514,7 +514,7 @@ func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephem | ||||||
| 		obj.Status.Reason = pod.Status.Reason | 		obj.Status.Reason = pod.Status.Reason | ||||||
| 		obj.Status.Message = pod.Status.Message | 		obj.Status.Message = pod.Status.Message | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		return fmt.Errorf("failed to update ephemeral runner status: failed attempts: %v", err) | 		return fmt.Errorf("failed to update ephemeral runner status: failed attempts: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("EphemeralRunner pod is deleted and status is updated with failure count") | 	log.Info("EphemeralRunner pod is deleted and status is updated with failure count") | ||||||
|  | @ -528,7 +528,7 @@ func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Con | ||||||
| 	log.Info("Creating ephemeral runner JIT config") | 	log.Info("Creating ephemeral runner JIT config") | ||||||
| 	actionsClient, err := r.actionsClientFor(ctx, ephemeralRunner) | 	actionsClient, err := r.actionsClientFor(ctx, ephemeralRunner) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return &ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %v", err) | 		return &ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	jitSettings := &actions.RunnerScaleSetJitRunnerSetting{ | 	jitSettings := &actions.RunnerScaleSetJitRunnerSetting{ | ||||||
|  | @ -546,12 +546,12 @@ func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Con | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		actionsError := &actions.ActionsError{} | 		actionsError := &actions.ActionsError{} | ||||||
| 		if !errors.As(err, &actionsError) { | 		if !errors.As(err, &actionsError) { | ||||||
| 			return &ctrl.Result{}, fmt.Errorf("failed to generate JIT config with generic error: %v", err) | 			return &ctrl.Result{}, fmt.Errorf("failed to generate JIT config with generic error: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if actionsError.StatusCode != http.StatusConflict || | 		if actionsError.StatusCode != http.StatusConflict || | ||||||
| 			!actionsError.IsException("AgentExistsException") { | 			!actionsError.IsException("AgentExistsException") { | ||||||
| 			return &ctrl.Result{}, fmt.Errorf("failed to generate JIT config with Actions service error: %v", err) | 			return &ctrl.Result{}, fmt.Errorf("failed to generate JIT config with Actions service error: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// If the runner with the name we want already exists it means:
 | 		// If the runner with the name we want already exists it means:
 | ||||||
|  | @ -564,7 +564,7 @@ func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Con | ||||||
| 		log.Info("Getting runner jit config failed with conflict error, trying to get the runner by name", "runnerName", ephemeralRunner.Name) | 		log.Info("Getting runner jit config failed with conflict error, trying to get the runner by name", "runnerName", ephemeralRunner.Name) | ||||||
| 		existingRunner, err := actionsClient.GetRunnerByName(ctx, ephemeralRunner.Name) | 		existingRunner, err := actionsClient.GetRunnerByName(ctx, ephemeralRunner.Name) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return &ctrl.Result{}, fmt.Errorf("failed to get runner by name: %v", err) | 			return &ctrl.Result{}, fmt.Errorf("failed to get runner by name: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if existingRunner == nil { | 		if existingRunner == nil { | ||||||
|  | @ -577,7 +577,7 @@ func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Con | ||||||
| 			log.Info("Removing the runner with the same name") | 			log.Info("Removing the runner with the same name") | ||||||
| 			err := actionsClient.RemoveRunner(ctx, int64(existingRunner.Id)) | 			err := actionsClient.RemoveRunner(ctx, int64(existingRunner.Id)) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return &ctrl.Result{}, fmt.Errorf("failed to remove runner from the service: %v", err) | 				return &ctrl.Result{}, fmt.Errorf("failed to remove runner from the service: %w", err) | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			log.Info("Removed the runner with the same name, re-queuing the reconciliation") | 			log.Info("Removed the runner with the same name, re-queuing the reconciliation") | ||||||
|  | @ -586,7 +586,7 @@ func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Con | ||||||
| 
 | 
 | ||||||
| 		// TODO: Do we want to mark the ephemeral runner as failed, and let EphemeralRunnerSet to clean it up, so we can recover from this situation?
 | 		// TODO: Do we want to mark the ephemeral runner as failed, and let EphemeralRunnerSet to clean it up, so we can recover from this situation?
 | ||||||
| 		// The situation is that the EphemeralRunner's name is already used by something else to register a runner, and we can't take the control back.
 | 		// The situation is that the EphemeralRunner's name is already used by something else to register a runner, and we can't take the control back.
 | ||||||
| 		return &ctrl.Result{}, fmt.Errorf("runner with the same name but doesn't belong to this RunnerScaleSet: %v", err) | 		return &ctrl.Result{}, fmt.Errorf("runner with the same name but doesn't belong to this RunnerScaleSet: %w", err) | ||||||
| 	} | 	} | ||||||
| 	log.Info("Created ephemeral runner JIT config", "runnerId", jitConfig.Runner.Id) | 	log.Info("Created ephemeral runner JIT config", "runnerId", jitConfig.Runner.Id) | ||||||
| 
 | 
 | ||||||
|  | @ -597,7 +597,7 @@ func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Con | ||||||
| 		obj.Status.RunnerJITConfig = jitConfig.EncodedJITConfig | 		obj.Status.RunnerJITConfig = jitConfig.EncodedJITConfig | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return &ctrl.Result{}, fmt.Errorf("failed to update runner status for RunnerId/RunnerName/RunnerJITConfig: %v", err) | 		return &ctrl.Result{}, fmt.Errorf("failed to update runner status for RunnerId/RunnerName/RunnerJITConfig: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// We want to continue without a requeue for faster pod creation.
 | 	// We want to continue without a requeue for faster pod creation.
 | ||||||
|  | @ -691,12 +691,12 @@ func (r *EphemeralRunnerReconciler) createSecret(ctx context.Context, runner *v1 | ||||||
| 	jitSecret := r.ResourceBuilder.newEphemeralRunnerJitSecret(runner) | 	jitSecret := r.ResourceBuilder.newEphemeralRunnerJitSecret(runner) | ||||||
| 
 | 
 | ||||||
| 	if err := ctrl.SetControllerReference(runner, jitSecret, r.Scheme); err != nil { | 	if err := ctrl.SetControllerReference(runner, jitSecret, r.Scheme); err != nil { | ||||||
| 		return &ctrl.Result{}, fmt.Errorf("failed to set controller reference: %v", err) | 		return &ctrl.Result{}, fmt.Errorf("failed to set controller reference: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Created new secret spec for ephemeral runner") | 	log.Info("Created new secret spec for ephemeral runner") | ||||||
| 	if err := r.Create(ctx, jitSecret); err != nil { | 	if err := r.Create(ctx, jitSecret); err != nil { | ||||||
| 		return &ctrl.Result{}, fmt.Errorf("failed to create jit secret: %v", err) | 		return &ctrl.Result{}, fmt.Errorf("failed to create jit secret: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Created ephemeral runner secret", "secretName", jitSecret.Name) | 	log.Info("Created ephemeral runner secret", "secretName", jitSecret.Name) | ||||||
|  | @ -743,7 +743,7 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context, | ||||||
| 		obj.Status.Message = pod.Status.Message | 		obj.Status.Message = pod.Status.Message | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("failed to update runner status for Phase/Reason/Message/Ready: %v", err) | 		return fmt.Errorf("failed to update runner status for Phase/Reason/Message/Ready: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Updated ephemeral runner status") | 	log.Info("Updated ephemeral runner status") | ||||||
|  | @ -835,7 +835,7 @@ func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Conte | ||||||
| 
 | 
 | ||||||
| 		if actionsError.StatusCode != http.StatusNotFound || | 		if actionsError.StatusCode != http.StatusNotFound || | ||||||
| 			!actionsError.IsException("AgentNotFoundException") { | 			!actionsError.IsException("AgentNotFoundException") { | ||||||
| 			return false, fmt.Errorf("failed to check if runner exists in GitHub service: %v", err) | 			return false, fmt.Errorf("failed to check if runner exists in GitHub service: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		log.Info("Runner does not exist in GitHub service", "runnerId", runner.Status.RunnerId) | 		log.Info("Runner does not exist in GitHub service", "runnerId", runner.Status.RunnerId) | ||||||
|  | @ -849,7 +849,7 @@ func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Conte | ||||||
| func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error { | func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error { | ||||||
| 	client, err := r.actionsClientFor(ctx, ephemeralRunner) | 	client, err := r.actionsClientFor(ctx, ephemeralRunner) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("failed to get actions client for runner: %v", err) | 		return fmt.Errorf("failed to get actions client for runner: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Removing runner from the service", "runnerId", ephemeralRunner.Status.RunnerId) | 	log.Info("Removing runner from the service", "runnerId", ephemeralRunner.Status.RunnerId) | ||||||
|  |  | ||||||
|  | @ -275,7 +275,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpProxySecret(ctx context.Context, e | ||||||
| 	proxySecret.Name = proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet) | 	proxySecret.Name = proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet) | ||||||
| 
 | 
 | ||||||
| 	if err := r.Delete(ctx, proxySecret); err != nil && !kerrors.IsNotFound(err) { | 	if err := r.Delete(ctx, proxySecret); err != nil && !kerrors.IsNotFound(err) { | ||||||
| 		return fmt.Errorf("failed to delete proxy secret: %v", err) | 		return fmt.Errorf("failed to delete proxy secret: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Deleted proxy secret") | 	log.Info("Deleted proxy secret") | ||||||
|  | @ -287,7 +287,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte | ||||||
| 	ephemeralRunnerList := new(v1alpha1.EphemeralRunnerList) | 	ephemeralRunnerList := new(v1alpha1.EphemeralRunnerList) | ||||||
| 	err := r.List(ctx, ephemeralRunnerList, client.InNamespace(ephemeralRunnerSet.Namespace), client.MatchingFields{resourceOwnerKey: ephemeralRunnerSet.Name}) | 	err := r.List(ctx, ephemeralRunnerList, client.InNamespace(ephemeralRunnerSet.Namespace), client.MatchingFields{resourceOwnerKey: ephemeralRunnerSet.Name}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, fmt.Errorf("failed to list child ephemeral runners: %v", err) | 		return false, fmt.Errorf("failed to list child ephemeral runners: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Info("Actual Ephemeral runner counts", "count", len(ephemeralRunnerList.Items)) | 	log.Info("Actual Ephemeral runner counts", "count", len(ephemeralRunnerList.Items)) | ||||||
|  | @ -441,7 +441,7 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co | ||||||
| 	} | 	} | ||||||
| 	actionsClient, err := r.actionsClientFor(ctx, ephemeralRunnerSet) | 	actionsClient, err := r.actionsClientFor(ctx, ephemeralRunnerSet) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %v", err) | 		return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %w", err) | ||||||
| 	} | 	} | ||||||
| 	var errs []error | 	var errs []error | ||||||
| 	deletedCount := 0 | 	deletedCount := 0 | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" | 	"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" | ||||||
| 	"github.com/actions/actions-runner-controller/build" | 	"github.com/actions/actions-runner-controller/build" | ||||||
| 	listenerconfig "github.com/actions/actions-runner-controller/cmd/githubrunnerscalesetlistener/config" | 	listenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config" | ||||||
| 	"github.com/actions/actions-runner-controller/github/actions" | 	"github.com/actions/actions-runner-controller/github/actions" | ||||||
| 	"github.com/actions/actions-runner-controller/hash" | 	"github.com/actions/actions-runner-controller/hash" | ||||||
| 	"github.com/actions/actions-runner-controller/logging" | 	"github.com/actions/actions-runner-controller/logging" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue