diff --git a/.github/workflows/gha-publish-chart.yaml b/.github/workflows/gha-publish-chart.yaml index 2b92f0a3..7d9e8e34 100644 --- a/.github/workflows/gha-publish-chart.yaml +++ b/.github/workflows/gha-publish-chart.yaml @@ -103,6 +103,7 @@ jobs: - name: Build & push controller image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: + context: . file: Dockerfile platforms: linux/amd64,linux/arm64 build-args: VERSION=${{ inputs.release_tag_name }} diff --git a/.github/workflows/gha-validate-chart.yaml b/.github/workflows/gha-validate-chart.yaml index cfcad8fc..ffb1e583 100644 --- a/.github/workflows/gha-validate-chart.yaml +++ b/.github/workflows/gha-validate-chart.yaml @@ -65,82 +65,63 @@ jobs: echo "$changed" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - name: Install helm-unittest - if: | - contains(steps.list-changed.outputs.changed_charts, 'charts/gha-runner-scale-set-controller-experimental') || - contains(steps.list-changed.outputs.changed_charts, 'charts/gha-runner-scale-set-experimental') - run: | - helm plugin install https://github.com/helm-unittest/helm-unittest.git - - - name: Run helm-unittest (gha-runner-scale-set-controller-experimental) - if: contains(steps.list-changed.outputs.changed_charts, 'charts/gha-runner-scale-set-controller-experimental') - run: | - helm unittest ./charts/gha-runner-scale-set-controller-experimental/ - - - name: Run helm-unittest (gha-runner-scale-set-experimental) - if: contains(steps.list-changed.outputs.changed_charts, 'charts/gha-runner-scale-set-experimental') - run: | - helm unittest ./charts/gha-runner-scale-set-experimental/ - - name: Run chart-testing (lint) run: | ct lint --config charts/.ci/ct-config-gha.yaml - - name: Set up docker buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - if: steps.list-changed.outputs.changed == 'true' - with: - version: latest - - - name: Build controller image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f - if: steps.list-changed.outputs.changed == 'true' - with: - file: Dockerfile - platforms: linux/amd64 - load: true - build-args: | - DOCKER_IMAGE_NAME=test-arc - VERSION=dev - tags: | - test-arc:dev - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Create kind cluster - uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc - if: steps.list-changed.outputs.changed == 'true' - with: - cluster_name: chart-testing - - - name: Load image into cluster - if: steps.list-changed.outputs.changed == 'true' - run: | - export DOCKER_IMAGE_NAME=test-arc - export VERSION=dev - export IMG_RESULT=load - make docker-buildx - kind load docker-image test-arc:dev --name chart-testing - - - name: Run chart-testing (install) - if: steps.list-changed.outputs.changed == 'true' - run: | - ct install --config charts/.ci/ct-config-gha.yaml test-chart: name: Test Chart runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 + with: + version: ${{ env.HELM_VERSION }} + + # python is a requirement for the chart-testing action below (supports yamllint among other tests) + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Set up chart-testing + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f + + - name: Run chart-testing (list-changed) + id: list-changed + run: | + ct version + changed=$(ct list-changed --config charts/.ci/ct-config-gha.yaml) + if [[ -n "$changed" ]]; then + echo "changed=true" >> $GITHUB_OUTPUT + fi + echo "changed_charts<> $GITHUB_OUTPUT + echo "$changed" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Install helm-unittest + run: | + helm plugin install https://github.com/helm-unittest/helm-unittest.git + + - name: Run helm-unittest (gha-runner-scale-set-controller-experimental) + run: | + helm unittest ./charts/gha-runner-scale-set-controller-experimental/ + + - name: Run helm-unittest (gha-runner-scale-set-experimental) + run: | + helm unittest ./charts/gha-runner-scale-set-experimental/ + - uses: actions/setup-go@v6 with: go-version-file: "go.mod" cache: false + - name: Test gha-runner-scale-set run: go test ./charts/gha-runner-scale-set/... + - name: Test gha-runner-scale-set-controller run: go test ./charts/gha-runner-scale-set-controller/... - - name: Test gha-runner-scale-set-experimental - run: go test ./charts/gha-runner-scale-set-experimental/... - - name: Test gha-runner-scale-set-controller-experimental - run: go test ./charts/gha-runner-scale-set-controller-experimental/... diff --git a/Dockerfile b/Dockerfile index a11010cf..c7c255a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM --platform=$BUILDPLATFORM golang:1.26.2 AS builder +FROM --platform=$BUILDPLATFORM golang:1.26.3 AS builder WORKDIR /workspace diff --git a/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go b/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go index 81308fa1..e24d7dab 100644 --- a/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go +++ b/apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go @@ -37,7 +37,7 @@ import ( // +kubebuilder:printcolumn:JSONPath=".spec.minRunners",name=Minimum Runners,type=integer // +kubebuilder:printcolumn:JSONPath=".spec.maxRunners",name=Maximum Runners,type=integer // +kubebuilder:printcolumn:JSONPath=".status.currentRunners",name=Current Runners,type=integer -// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Phase,type=string +// +kubebuilder:printcolumn:JSONPath=".status.state",name=State,type=string // +kubebuilder:printcolumn:JSONPath=".status.pendingEphemeralRunners",name=Pending Runners,type=integer // +kubebuilder:printcolumn:JSONPath=".status.runningEphemeralRunners",name=Running Runners,type=integer // +kubebuilder:printcolumn:JSONPath=".status.finishedEphemeralRunners",name=Finished Runners,type=integer diff --git a/apis/actions.github.com/v1alpha1/ephemeralrunner_types.go b/apis/actions.github.com/v1alpha1/ephemeralrunner_types.go index 155108d8..f9b4df52 100644 --- a/apis/actions.github.com/v1alpha1/ephemeralrunner_types.go +++ b/apis/actions.github.com/v1alpha1/ephemeralrunner_types.go @@ -29,7 +29,7 @@ const EphemeralRunnerContainerName = "runner" // +kubebuilder:subresource:status // +kubebuilder:printcolumn:JSONPath=".spec.githubConfigUrl",name="GitHub Config URL",type=string // +kubebuilder:printcolumn:JSONPath=".status.runnerId",name=RunnerId,type=number -// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Phase,type=string +// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string // +kubebuilder:printcolumn:JSONPath=".status.jobRepositoryName",name=JobRepository,type=string // +kubebuilder:printcolumn:JSONPath=".status.jobWorkflowRef",name=JobWorkflowRef,type=string // +kubebuilder:printcolumn:JSONPath=".status.workflowRunId",name=WorkflowRunId,type=number diff --git a/apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go b/apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go index 299b7850..229bb252 100644 --- a/apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go +++ b/apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go @@ -64,7 +64,6 @@ const ( // +kubebuilder:printcolumn:JSONPath=".status.runningEphemeralRunners",name=Running Runners,type=integer // +kubebuilder:printcolumn:JSONPath=".status.finishedEphemeralRunners",name=Finished Runners,type=integer // +kubebuilder:printcolumn:JSONPath=".status.deletingEphemeralRunners",name=Deleting Runners,type=integer -// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Phase,type=string // EphemeralRunnerSet is the Schema for the ephemeralrunnersets API type EphemeralRunnerSet struct { diff --git a/charts/.ci/ct-config-gha.yaml b/charts/.ci/ct-config-gha.yaml index b0a15a37..095e6573 100644 --- a/charts/.ci/ct-config-gha.yaml +++ b/charts/.ci/ct-config-gha.yaml @@ -8,4 +8,6 @@ check-version-increment: false # Disable checking that the chart version has bee charts: - charts/gha-runner-scale-set-controller - charts/gha-runner-scale-set + - charts/gha-runner-scale-set-controller-experimental + - charts/gha-runner-scale-set-experimental skip-clean-up: true diff --git a/charts/gha-runner-scale-set-controller-experimental/Chart.yaml b/charts/gha-runner-scale-set-controller-experimental/Chart.yaml index 8af1d81b..317a4fd4 100644 --- a/charts/gha-runner-scale-set-controller-experimental/Chart.yaml +++ b/charts/gha-runner-scale-set-controller-experimental/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: "0.14.1" +version: "0.14.2" # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.14.1" +appVersion: "0.14.2" home: https://github.com/actions/actions-runner-controller diff --git a/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_autoscalingrunnersets.yaml b/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_autoscalingrunnersets.yaml index bf1b43c8..191f8194 100644 --- a/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_autoscalingrunnersets.yaml +++ b/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_autoscalingrunnersets.yaml @@ -24,8 +24,8 @@ spec: - jsonPath: .status.currentRunners name: Current Runners type: integer - - jsonPath: .status.phase - name: Phase + - jsonPath: .status.state + name: State type: string - jsonPath: .status.pendingEphemeralRunners name: Pending Runners diff --git a/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunners.yaml b/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunners.yaml index 455f7828..9c870fe6 100644 --- a/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunners.yaml +++ b/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunners.yaml @@ -22,7 +22,7 @@ spec: name: RunnerId type: number - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .status.jobRepositoryName name: JobRepository diff --git a/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunnersets.yaml b/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunnersets.yaml index 15289d8b..01209775 100644 --- a/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunnersets.yaml +++ b/charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunnersets.yaml @@ -33,9 +33,6 @@ spec: - jsonPath: .status.deletingEphemeralRunners name: Deleting Runners type: integer - - jsonPath: .status.phase - name: Phase - type: string name: v1alpha1 schema: openAPIV3Schema: diff --git a/charts/gha-runner-scale-set-controller-experimental/templates/_controller_template.tpl b/charts/gha-runner-scale-set-controller-experimental/templates/_controller_template.tpl index 44a17b3c..18cd6ccf 100644 --- a/charts/gha-runner-scale-set-controller-experimental/templates/_controller_template.tpl +++ b/charts/gha-runner-scale-set-controller-experimental/templates/_controller_template.tpl @@ -73,6 +73,11 @@ args: {{- with .Values.controller.manager.config.k8sClientRateLimiterBurst }} - "--k8s-client-rate-limiter-burst={{ . }}" {{- end }} +{{- with .Values.controller.manager.config.rateLimiter }} +{{- with .name }} + - "--workqueue-rate-limiter={{ . }}" +{{- end }} +{{- end }} {{- with .Values.controller.manager.config.healthProbeBindAddress }} - "--health-probe-bind-address={{ . }}" {{- end }} diff --git a/charts/gha-runner-scale-set-controller-experimental/tests/controller_deployment_rate_limiter_test.yaml b/charts/gha-runner-scale-set-controller-experimental/tests/controller_deployment_rate_limiter_test.yaml new file mode 100644 index 00000000..30039ed3 --- /dev/null +++ b/charts/gha-runner-scale-set-controller-experimental/tests/controller_deployment_rate_limiter_test.yaml @@ -0,0 +1,66 @@ +suite: "Controller Deployment rate limiter" +templates: + - deployment.yaml +tests: + - it: should omit workqueue-rate-limiter flag by default + release: + name: "test-arc" + namespace: "test-ns" + asserts: + - notContains: + path: spec.template.spec.containers[0].args + content: "--workqueue-rate-limiter=bucket_rate_limiter" + - notContains: + path: spec.template.spec.containers[0].args + content: "--workqueue-rate-limiter=typed_rate_limiter" + + - it: should include workqueue-rate-limiter flag when bucket_rate_limiter is configured + set: + controller: + manager: + config: + rateLimiter: + name: "bucket_rate_limiter" + release: + name: "test-arc" + namespace: "test-ns" + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: "--workqueue-rate-limiter=bucket_rate_limiter" + + - it: should include workqueue-rate-limiter flag when typed_rate_limiter is configured + set: + controller: + manager: + config: + rateLimiter: + name: "typed_rate_limiter" + release: + name: "test-arc" + namespace: "test-ns" + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: "--workqueue-rate-limiter=typed_rate_limiter" + + - it: should render both config and extraArgs in deterministic order + set: + controller: + manager: + config: + rateLimiter: + name: "bucket_rate_limiter" + container: + extraArgs: + - "--workqueue-rate-limiter=typed_rate_limiter" + release: + name: "test-arc" + namespace: "test-ns" + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: "--workqueue-rate-limiter=bucket_rate_limiter" + - contains: + path: spec.template.spec.containers[0].args + content: "--workqueue-rate-limiter=typed_rate_limiter" diff --git a/charts/gha-runner-scale-set-controller-experimental/tests/template_test.go b/charts/gha-runner-scale-set-controller-experimental/tests/template_test.go deleted file mode 100644 index 80870ec3..00000000 --- a/charts/gha-runner-scale-set-controller-experimental/tests/template_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package tests - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/gruntwork-io/terratest/modules/helm" - "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/gruntwork-io/terratest/modules/logger" - "github.com/gruntwork-io/terratest/modules/random" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v2" - appsv1 "k8s.io/api/apps/v1" -) - -type Chart struct { - Version string `yaml:"version"` - AppVersion string `yaml:"appVersion"` -} - -func TestTemplate_RenderedDeployment_UsesChartMetadataLabels(t *testing.T) { - t.Parallel() - - helmChartPath, err := filepath.Abs("../../gha-runner-scale-set-controller-experimental") - require.NoError(t, err) - - chartContent, err := os.ReadFile(filepath.Join(helmChartPath, "Chart.yaml")) - require.NoError(t, err) - - chart := new(Chart) - err = yaml.Unmarshal(chartContent, chart) - require.NoError(t, err) - - releaseName := "test-arc" - namespaceName := "test-" + strings.ToLower(random.UniqueId()) - - options := &helm.Options{ - Logger: logger.Discard, - KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), - } - - output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/deployment.yaml"}) - - var deployment appsv1.Deployment - helm.UnmarshalK8SYaml(t, output, &deployment) - - assert.Equal(t, "gha-rs-controller-"+chart.Version, deployment.Labels["helm.sh/chart"]) - assert.Equal(t, chart.AppVersion, deployment.Labels["app.kubernetes.io/version"]) -} diff --git a/charts/gha-runner-scale-set-controller-experimental/values.yaml b/charts/gha-runner-scale-set-controller-experimental/values.yaml index 75bcc314..a06d86c6 100644 --- a/charts/gha-runner-scale-set-controller-experimental/values.yaml +++ b/charts/gha-runner-scale-set-controller-experimental/values.yaml @@ -44,6 +44,13 @@ controller: k8sClientRateLimiterQPS: null k8sClientRateLimiterBurst: null + ## Workqueue rate limiter configuration. + ## By default, controller-runtime uses a combined rate limiter with both a per-item + ## exponential backoff and an overall token bucket (10 QPS, 100 bucket size). + ## Valid names: "bucket_rate_limiter" (default), "typed_rate_limiter" (per-item only, no global token bucket). + # rateLimiter: + # name: "bucket_rate_limiter" + # The address the health probe endpoint binds to. Disabled if empty/null. # When set, liveness and readiness probes are added to the controller pod. # healthProbeBindAddress: ":8081" diff --git a/charts/gha-runner-scale-set-controller/Chart.yaml b/charts/gha-runner-scale-set-controller/Chart.yaml index 159ba11f..b68accce 100644 --- a/charts/gha-runner-scale-set-controller/Chart.yaml +++ b/charts/gha-runner-scale-set-controller/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.14.1 +version: 0.14.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.14.1" +appVersion: "0.14.2" home: https://github.com/actions/actions-runner-controller diff --git a/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml b/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml index bf1b43c8..191f8194 100644 --- a/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml +++ b/charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml @@ -24,8 +24,8 @@ spec: - jsonPath: .status.currentRunners name: Current Runners type: integer - - jsonPath: .status.phase - name: Phase + - jsonPath: .status.state + name: State type: string - jsonPath: .status.pendingEphemeralRunners name: Pending Runners diff --git a/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunners.yaml b/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunners.yaml index 455f7828..9c870fe6 100644 --- a/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunners.yaml +++ b/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunners.yaml @@ -22,7 +22,7 @@ spec: name: RunnerId type: number - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .status.jobRepositoryName name: JobRepository diff --git a/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunnersets.yaml b/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunnersets.yaml index 15289d8b..01209775 100644 --- a/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunnersets.yaml +++ b/charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunnersets.yaml @@ -33,9 +33,6 @@ spec: - jsonPath: .status.deletingEphemeralRunners name: Deleting Runners type: integer - - jsonPath: .status.phase - name: Phase - type: string name: v1alpha1 schema: openAPIV3Schema: diff --git a/charts/gha-runner-scale-set-experimental/Chart.yaml b/charts/gha-runner-scale-set-experimental/Chart.yaml index 46c6007b..252d33ee 100644 --- a/charts/gha-runner-scale-set-experimental/Chart.yaml +++ b/charts/gha-runner-scale-set-experimental/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: "0.14.1" +version: "0.14.2" # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.14.1" +appVersion: "0.14.2" home: https://github.com/actions/actions-runner-controller diff --git a/charts/gha-runner-scale-set-experimental/tests/template_test.go b/charts/gha-runner-scale-set-experimental/tests/template_test.go deleted file mode 100644 index 2a1aa615..00000000 --- a/charts/gha-runner-scale-set-experimental/tests/template_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package tests - -import ( - "os" - "path/filepath" - "strings" - "testing" - - v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" - "github.com/gruntwork-io/terratest/modules/helm" - "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/gruntwork-io/terratest/modules/logger" - "github.com/gruntwork-io/terratest/modules/random" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v2" -) - -type Chart struct { - Version string `yaml:"version"` - AppVersion string `yaml:"appVersion"` -} - -func TestTemplate_RenderedAutoscalingRunnerSet_UsesChartMetadataLabels(t *testing.T) { - t.Parallel() - - helmChartPath, err := filepath.Abs("../../gha-runner-scale-set-experimental") - require.NoError(t, err) - - chartContent, err := os.ReadFile(filepath.Join(helmChartPath, "Chart.yaml")) - require.NoError(t, err) - - chart := new(Chart) - err = yaml.Unmarshal(chartContent, chart) - require.NoError(t, err) - - releaseName := "test-runners" - namespaceName := "test-" + strings.ToLower(random.UniqueId()) - - options := &helm.Options{ - Logger: logger.Discard, - SetValues: map[string]string{ - "scaleset.name": "test", - "auth.url": "https://github.com/actions", - "auth.githubToken": "gh_token12345", - "controllerServiceAccount.name": "arc", - "controllerServiceAccount.namespace": "arc-system", - }, - KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), - } - - output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnserset.yaml"}) - - var autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet - helm.UnmarshalK8SYaml(t, output, &autoscalingRunnerSet) - - assert.Equal(t, "gha-rs-"+chart.Version, autoscalingRunnerSet.Labels["helm.sh/chart"]) - assert.Equal(t, chart.AppVersion, autoscalingRunnerSet.Labels["app.kubernetes.io/version"]) - assert.Equal(t, "gha-rs-"+chart.Version, autoscalingRunnerSet.Spec.Template.Labels["helm.sh/chart"]) - assert.Equal(t, chart.AppVersion, autoscalingRunnerSet.Spec.Template.Labels["app.kubernetes.io/version"]) -} diff --git a/charts/gha-runner-scale-set/Chart.yaml b/charts/gha-runner-scale-set/Chart.yaml index a783121b..18cc3c3e 100644 --- a/charts/gha-runner-scale-set/Chart.yaml +++ b/charts/gha-runner-scale-set/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.14.1 +version: 0.14.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.14.1" +appVersion: "0.14.2" home: https://github.com/actions/actions-runner-controller diff --git a/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml b/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml index bf1b43c8..191f8194 100644 --- a/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml +++ b/config/crd/bases/actions.github.com_autoscalingrunnersets.yaml @@ -24,8 +24,8 @@ spec: - jsonPath: .status.currentRunners name: Current Runners type: integer - - jsonPath: .status.phase - name: Phase + - jsonPath: .status.state + name: State type: string - jsonPath: .status.pendingEphemeralRunners name: Pending Runners diff --git a/config/crd/bases/actions.github.com_ephemeralrunners.yaml b/config/crd/bases/actions.github.com_ephemeralrunners.yaml index 455f7828..9c870fe6 100644 --- a/config/crd/bases/actions.github.com_ephemeralrunners.yaml +++ b/config/crd/bases/actions.github.com_ephemeralrunners.yaml @@ -22,7 +22,7 @@ spec: name: RunnerId type: number - jsonPath: .status.phase - name: Phase + name: Status type: string - jsonPath: .status.jobRepositoryName name: JobRepository diff --git a/config/crd/bases/actions.github.com_ephemeralrunnersets.yaml b/config/crd/bases/actions.github.com_ephemeralrunnersets.yaml index 15289d8b..01209775 100644 --- a/config/crd/bases/actions.github.com_ephemeralrunnersets.yaml +++ b/config/crd/bases/actions.github.com_ephemeralrunnersets.yaml @@ -33,9 +33,6 @@ spec: - jsonPath: .status.deletingEphemeralRunners name: Deleting Runners type: integer - - jsonPath: .status.phase - name: Phase - type: string name: v1alpha1 schema: openAPIV3Schema: diff --git a/controllers/actions.github.com/autoscalingrunnerset_controller.go b/controllers/actions.github.com/autoscalingrunnerset_controller.go index f31bfd39..228465ec 100644 --- a/controllers/actions.github.com/autoscalingrunnerset_controller.go +++ b/controllers/actions.github.com/autoscalingrunnerset_controller.go @@ -138,14 +138,16 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl if !v1alpha1.IsVersionAllowed(autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], build.Version) { if err := r.Delete(ctx, autoscalingRunnerSet); err != nil { - log.Error(err, "Failed to delete autoscaling runner set on version mismatch", + log.Error( + err, "Failed to delete autoscaling runner set on version mismatch", "buildVersion", build.Version, "autoscalingRunnerSetVersion", autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], ) return ctrl.Result{}, nil } - log.Info("Autoscaling runner set version doesn't match the build version. Deleting the resource.", + log.Info( + "Autoscaling runner set version doesn't match the build version. Deleting the resource.", "buildVersion", build.Version, "autoscalingRunnerSetVersion", autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion], ) @@ -270,12 +272,12 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl // Our listener pod is out of date, so we need to delete it to get a new recreate. listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash] listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash() - if listenerFound && (listenerValuesHashChanged || listenerSpecHashChanged) { + if listenerFound && (listenerValuesHashChanged || + listenerSpecHashChanged || + latestRunnerSet == nil || + listener.Spec.EphemeralRunnerSetName != latestRunnerSet.Name) { log.Info("RunnerScaleSetListener is out of date. Deleting it so that it is recreated", "name", listener.Name) if err := r.Delete(ctx, listener); err != nil { - if kerrors.IsNotFound(err) { - return ctrl.Result{}, nil - } log.Error(err, "Failed to delete AutoscalingListener resource") return ctrl.Result{}, err } @@ -473,6 +475,7 @@ func (r *AutoscalingRunnerSetReconciler) removeFinalizersFromDependentResources( } func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) { + original := autoscalingRunnerSet.DeepCopy() logger.Info("Creating a new runner scale set") actionsClient, err := r.GetActionsService(ctx, autoscalingRunnerSet) if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 { @@ -537,7 +540,8 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex RunnerSetting: scaleset.RunnerSetting{ DisableUpdate: true, }, - }) + }, + ) if err != nil { logger.Error(err, "Failed to create a new runner scale set on Actions service") return ctrl.Result{}, err @@ -556,15 +560,16 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex autoscalingRunnerSet.Labels = map[string]string{} } + autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName] = runnerScaleSet.Name + autoscalingRunnerSet.Annotations[runnerScaleSetIDAnnotationKey] = strconv.Itoa(runnerScaleSet.ID) + autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName] = runnerScaleSet.RunnerGroupName + if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, autoscalingRunnerSet.Labels); err != nil { // should never happen + logger.Error(err, "Failed to apply GitHub URL labels") + return ctrl.Result{}, err + } + logger.Info("Adding runner scale set ID, name and runner group name as an annotation and url labels") - if err = patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) { - obj.Annotations[AnnotationKeyGitHubRunnerScaleSetName] = runnerScaleSet.Name - obj.Annotations[runnerScaleSetIDAnnotationKey] = strconv.Itoa(runnerScaleSet.ID) - obj.Annotations[AnnotationKeyGitHubRunnerGroupName] = runnerScaleSet.RunnerGroupName - if err := applyGitHubURLLabels(obj.Spec.GitHubConfigUrl, obj.Labels); err != nil { // should never happen - logger.Error(err, "Failed to apply GitHub URL labels") - } - }); err != nil { + if err = r.Patch(ctx, autoscalingRunnerSet, client.MergeFrom(original)); err != nil { logger.Error(err, "Failed to add runner scale set ID, name and runner group name as an annotation") return ctrl.Result{}, err } diff --git a/controllers/actions.github.com/autoscalingrunnerset_controller_test.go b/controllers/actions.github.com/autoscalingrunnerset_controller_test.go index a2d9a6b2..1592fc43 100644 --- a/controllers/actions.github.com/autoscalingrunnerset_controller_test.go +++ b/controllers/actions.github.com/autoscalingrunnerset_controller_test.go @@ -85,13 +85,19 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return &scaleset.RunnerGroup{ID: 1, Name: groupName}, nil }), scalefake.WithGetRunnerScaleSet(nil, nil), - scalefake.WithCreateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil), + scalefake.WithCreateRunnerScaleSetFunc(func(ctx context.Context, rs *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) { + // Return a RunnerScaleSet with name matching the requesting ARS + runnerGroupMapLock.RLock() + groupName := runnerGroupMap[rs.RunnerGroupID] + runnerGroupMapLock.RUnlock() + return &scaleset.RunnerScaleSet{ID: 1, Name: rs.Name, RunnerGroupID: rs.RunnerGroupID, RunnerGroupName: groupName}, nil + }), scalefake.WithUpdateRunnerScaleSetFunc(func(ctx context.Context, scaleSetID int, rs *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) { // Return a RunnerScaleSet with the group name corresponding to the runner group ID runnerGroupMapLock.RLock() groupName := runnerGroupMap[rs.RunnerGroupID] runnerGroupMapLock.RUnlock() - return &scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: rs.RunnerGroupID, RunnerGroupName: groupName}, nil + return &scaleset.RunnerScaleSet{ID: 1, Name: rs.Name, RunnerGroupID: rs.RunnerGroupID, RunnerGroupName: groupName}, nil }), scalefake.WithDeleteRunnerScaleSet(nil), ), @@ -153,7 +159,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return created.Finalizers[0], nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer") + autoscalingRunnerSetTestInterval, + ).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer") // Check if runner scale set is created on service Eventually( @@ -202,7 +209,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return fmt.Sprintf("%s/%s", created.Labels[LabelKeyGitHubOrganization], created.Labels[LabelKeyGitHubRepository]), nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("owner/repo"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's label") + autoscalingRunnerSetTestInterval, + ).Should(BeEquivalentTo("owner/repo"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's label") // Check if ephemeral runner set is created Eventually( @@ -216,7 +224,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return len(runnerSetList.Items), nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created") + autoscalingRunnerSetTestInterval, + ).Should(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created") // Check if listener is created Eventually( @@ -224,7 +233,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener)) }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "Listener should be created") // Check if status is updated runnerSetList := new(v1alpha1.EphemeralRunnerSetList) @@ -242,7 +252,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener)) }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "Listener should be created") // Delete the AutoScalingRunnerSet err := k8sClient.Delete(ctx, autoscalingRunnerSet) @@ -259,7 +270,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return fmt.Errorf("listener is not deleted") }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be deleted") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "Listener should be deleted") // Check if all the EphemeralRunnerSet is deleted Eventually( @@ -277,7 +289,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "All EphemeralRunnerSet should be deleted") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "All EphemeralRunnerSet should be deleted") // Check if the AutoScalingRunnerSet is deleted Eventually( @@ -290,10 +303,144 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return fmt.Errorf("AutoScalingRunnerSet is not deleted") }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "AutoScalingRunnerSet should be deleted") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "AutoScalingRunnerSet should be deleted") }) }) + It("should not churn listener when already referencing latest ERS (no-op stability)", func() { + min := 1 + max := 10 + testARSName := "test-asrs-no-churn" + testARSNamespace := autoscalingNS.Name + + testARS := &v1alpha1.AutoscalingRunnerSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: testARSName, + Namespace: testARSNamespace, + Labels: map[string]string{ + LabelKeyKubernetesVersion: buildVersion, + }, + }, + Spec: v1alpha1.AutoscalingRunnerSetSpec{ + GitHubConfigUrl: "https://github.com/owner/repo", + GitHubConfigSecret: configSecret.Name, + MaxRunners: &max, + MinRunners: &min, + RunnerGroup: "testgroup", + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "runner", + Image: "ghcr.io/actions/runner", + }, + }, + }, + }, + }, + } + + err := k8sClient.Create(ctx, testARS) + Expect(err).NotTo(HaveOccurred(), "failed to create test AutoScalingRunnerSet") + + var latestERSName string + Eventually( + func() (string, error) { + runnerSetList := new(v1alpha1.EphemeralRunnerSetList) + err := k8sClient.List(ctx, runnerSetList, client.InNamespace(testARSNamespace)) + if err != nil { + return "", err + } + + // Filter to only ERS owned by our test ARS + var ownedByTestARS []v1alpha1.EphemeralRunnerSet + for _, ers := range runnerSetList.Items { + for _, owner := range ers.OwnerReferences { + if owner.UID == testARS.UID { + ownedByTestARS = append(ownedByTestARS, ers) + break + } + } + } + + if len(ownedByTestARS) != 1 { + return "", fmt.Errorf("expected 1 EphemeralRunnerSet owned by test ARS, got %d", len(ownedByTestARS)) + } + + return ownedByTestARS[0].Name, nil + }, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(Not(BeEmpty()), "ERS should be created") + + // Capture the latest ERS name + runnerSetList := new(v1alpha1.EphemeralRunnerSetList) + err = k8sClient.List(ctx, runnerSetList, client.InNamespace(testARSNamespace)) + Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet") + + var ownedByTestARS []v1alpha1.EphemeralRunnerSet + for _, ers := range runnerSetList.Items { + for _, owner := range ers.OwnerReferences { + if owner.UID == testARS.UID { + ownedByTestARS = append(ownedByTestARS, ers) + break + } + } + } + Expect(len(ownedByTestARS)).To(Equal(1), "should have exactly 1 EphemeralRunnerSet owned by test ARS") + latestERSName = ownedByTestARS[0].Name + + listener := new(v1alpha1.AutoscalingListener) + Eventually( + func() (string, error) { + err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(testARS), Namespace: testARSNamespace}, listener) + if err != nil { + return "", err + } + return listener.Spec.EphemeralRunnerSetName, nil + }, + autoscalingRunnerSetTestTimeout, + autoscalingRunnerSetTestInterval, + ).Should(Equal(latestERSName), "listener should reference the latest ERS") + + // Capture listener identity (UID and ResourceVersion) + err = k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(testARS), Namespace: testARSNamespace}, listener) + Expect(err).NotTo(HaveOccurred(), "failed to get listener") + originalUID := listener.UID + originalResourceVersion := listener.ResourceVersion + + Consistently( + func() (types.UID, error) { + currentListener := new(v1alpha1.AutoscalingListener) + err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(testARS), Namespace: testARSNamespace}, currentListener) + if err != nil { + return "", err + } + return currentListener.UID, nil + }, + time.Second*5, + autoscalingRunnerSetTestInterval, + ).Should(Equal(originalUID), "listener UID should remain unchanged (no recreation)") + + Consistently( + func() (string, error) { + currentListener := new(v1alpha1.AutoscalingListener) + err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(testARS), Namespace: testARSNamespace}, currentListener) + if err != nil { + return "", err + } + return currentListener.ResourceVersion, nil + }, + time.Second*5, + autoscalingRunnerSetTestInterval, + ).Should(Equal(originalResourceVersion), "listener ResourceVersion should remain unchanged (no updates)") + + err = k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(testARS), Namespace: testARSNamespace}, listener) + Expect(err).NotTo(HaveOccurred(), "failed to get listener") + Expect(listener.Spec.EphemeralRunnerSetName).To(Equal(latestERSName), "listener should still reference latest ERS") + }) + Context("When updating a new AutoScalingRunnerSet", func() { It("It should re-create EphemeralRunnerSet and Listener as needed when updating AutoScalingRunnerSet", func() { // Wait till the listener is created @@ -303,7 +450,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener) }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "Listener should be created") runnerSetList := new(v1alpha1.EphemeralRunnerSetList) err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace)) @@ -339,7 +487,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return runnerSetList.Items[0].Annotations[annotationKeyRunnerSpecHash], nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(runnerSet.Annotations[annotationKeyRunnerSpecHash]), "New EphemeralRunnerSet should be created") + autoscalingRunnerSetTestInterval, + ).ShouldNot(BeEquivalentTo(runnerSet.Annotations[annotationKeyRunnerSpecHash]), "New EphemeralRunnerSet should be created") // We should create a new listener Eventually( @@ -353,7 +502,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return listener.Spec.EphemeralRunnerSetName, nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(runnerSet.Name), "New Listener should be created") + autoscalingRunnerSetTestInterval, + ).ShouldNot(BeEquivalentTo(runnerSet.Name), "New Listener should be created") // Only update the Spec for the AutoScalingListener // This should trigger re-creation of the Listener only @@ -389,7 +539,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return string(runnerSetList.Items[0].UID), nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created") + autoscalingRunnerSetTestInterval, + ).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created") // We should only re-create a new listener Eventually( @@ -403,7 +554,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return string(listener.UID), nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created") + autoscalingRunnerSetTestInterval, + ).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created") // Only update the values hash for the autoscaling runner set // This should trigger re-creation of the Listener only @@ -438,7 +590,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return string(runnerSetList.Items[0].UID), nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created") + autoscalingRunnerSetTestInterval, + ).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created") // We should only re-create a new listener Eventually( @@ -452,7 +605,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return string(listener.UID), nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created") + autoscalingRunnerSetTestInterval, + ).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created") }) It("It should update RunnerScaleSet's runner group on service when it changes", func() { @@ -463,7 +617,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener)) }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created") + autoscalingRunnerSetTestInterval, + ).Should(Succeed(), "Listener should be created") patched := autoscalingRunnerSet.DeepCopy() patched.Spec.RunnerGroup = "testgroup2" @@ -485,7 +640,8 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { return updated.Annotations[AnnotationKeyGitHubRunnerGroupName], nil }, autoscalingRunnerSetTestTimeout, - autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("testgroup2"), "AutoScalingRunnerSet should have the new runner group in its annotation") + autoscalingRunnerSetTestInterval, + ).Should(BeEquivalentTo("testgroup2"), "AutoScalingRunnerSet should have the new runner group in its annotation") // delete the annotation and it should be re-added patched = autoscalingRunnerSet.DeepCopy() @@ -642,13 +798,14 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() { ).Should(BeTrue(), "AutoscalingRunnerSet should be created") runnerSetList := new(v1alpha1.EphemeralRunnerSetList) - Eventually(func() (int, error) { - err := k8sClient.List(ctx, runnerSetList, client.InNamespace(ars.Namespace)) - if err != nil { - return 0, err - } - return len(runnerSetList.Items), nil - }, + Eventually( + func() (int, error) { + err := k8sClient.List(ctx, runnerSetList, client.InNamespace(ars.Namespace)) + if err != nil { + return 0, err + } + return len(runnerSetList.Items), nil + }, autoscalingRunnerSetTestTimeout, autoscalingRunnerSetTestInterval, ).Should(BeEquivalentTo(1), "Failed to fetch runner set list") diff --git a/controllers/actions.github.com/multiclient/fake/client.go b/controllers/actions.github.com/multiclient/fake/client.go index d8f62f59..737092a8 100644 --- a/controllers/actions.github.com/multiclient/fake/client.go +++ b/controllers/actions.github.com/multiclient/fake/client.go @@ -102,6 +102,13 @@ func WithSystemInfo(info scaleset.SystemInfo) ClientOption { } } +// WithCreateRunnerScaleSetFunc configures a function to handle CreateRunnerScaleSet calls dynamically +func WithCreateRunnerScaleSetFunc(fn func(context.Context, *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)) ClientOption { + return func(c *Client) { + c.createRunnerScaleSetFunc = fn + } +} + // WithUpdateRunnerScaleSetFunc configures a function to handle UpdateRunnerScaleSet calls dynamically func WithUpdateRunnerScaleSetFunc(fn func(context.Context, int, *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)) ClientOption { return func(c *Client) { @@ -112,6 +119,7 @@ func WithUpdateRunnerScaleSetFunc(fn func(context.Context, int, *scaleset.Runner // Client implements multiclient.Client interface for testing type Client struct { systemInfo scaleset.SystemInfo + createRunnerScaleSetFunc func(context.Context, *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) updateRunnerScaleSetFunc func(context.Context, int, *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) getRunnerScaleSetResult struct { @@ -215,6 +223,9 @@ func (c *Client) GetRunnerScaleSetByID(ctx context.Context, runnerScaleSetID int } func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) { + if c.createRunnerScaleSetFunc != nil { + return c.createRunnerScaleSetFunc(ctx, runnerScaleSet) + } return c.createRunnerScaleSetResult.RunnerScaleSet, c.createRunnerScaleSetResult.err } diff --git a/docs/gha-runner-scale-set-controller/README.md b/docs/gha-runner-scale-set-controller/README.md index 92736a63..a0f009d9 100644 --- a/docs/gha-runner-scale-set-controller/README.md +++ b/docs/gha-runner-scale-set-controller/README.md @@ -43,6 +43,25 @@ You can follow [this troubleshooting guide](https://docs.github.com/en/actions/h ## Changelog +### 0.14.2 + +1. Fix orphan no-permission ServiceAccount in kubernetes-novolume mode [#4455](https://github.com/actions/actions-runner-controller/pull/4455) +1. Updates: runner to v2.334.0 [#4467](https://github.com/actions/actions-runner-controller/pull/4467) +1. Add option to disable workqueue bucket rate limiter [#4451](https://github.com/actions/actions-runner-controller/pull/4451) +1. Add a flag for enabling pprof on the controller manager [#4449](https://github.com/actions/actions-runner-controller/pull/4449) +1. Add health and readiness probes to controller manager [#4459](https://github.com/actions/actions-runner-controller/pull/4459) +1. Fix empty GVK in OwnerReferences for modern controllers [#4475](https://github.com/actions/actions-runner-controller/pull/4475) +1. Fix: Detect init container failure in EphemeralRunner controller [#4457](https://github.com/actions/actions-runner-controller/pull/4457) +1. Bump the actions group with 3 updates [#4483](https://github.com/actions/actions-runner-controller/pull/4483) +1. Render empty arrays for kubernetes-novolume volumes fields [#4461](https://github.com/actions/actions-runner-controller/pull/4461) +1. Fix secret reconciliation updates for the listener pod [#4492](https://github.com/actions/actions-runner-controller/pull/4492) +1. Fix job execution duration when runner assign time is not set [#4472](https://github.com/actions/actions-runner-controller/pull/4472) +1. Update CODEOWNERS [#4495](https://github.com/actions/actions-runner-controller/pull/4495) +1. Bump Go to 1.26.2 to fix critical security vulnerabilities [#4491](https://github.com/actions/actions-runner-controller/pull/4491) +1. Fix helm chart validation workflow [#4479](https://github.com/actions/actions-runner-controller/pull/4479) +1. Port rate limiter to experimental charts [#4478](https://github.com/actions/actions-runner-controller/pull/4478) +1. Bump Go to 1.26.3 [#4504](https://github.com/actions/actions-runner-controller/pull/4504) + ### 0.14.1 1. Fix null field for resource metadata fields in experimental chart [#4419](https://github.com/actions/actions-runner-controller/pull/4419) diff --git a/github/actions/actions.go b/github/actions/actions.go index 50f23213..7a93c457 100644 --- a/github/actions/actions.go +++ b/github/actions/actions.go @@ -85,7 +85,7 @@ func (c *GitHubConfig) GitHubAPIURL(path string) *url.URL { result.Host = fmt.Sprintf("api.%s", c.ConfigURL.Host) result.Path = "" - if strings.EqualFold("www.github.com", c.ConfigURL.Host) { + if c.ConfigURL.Host == "www.github.com" { // re-routing www.github.com to api.github.com result.Host = "api.github.com" } @@ -102,8 +102,8 @@ func isHostedGitHubURL(u *url.URL) bool { return false } - return strings.EqualFold(u.Host, "github.com") || - strings.EqualFold(u.Host, "www.github.com") || - strings.EqualFold(u.Host, "github.localhost") || + return u.Host == "github.com" || + u.Host == "www.github.com" || + u.Host == "github.localhost" || strings.HasSuffix(u.Host, ".ghe.com") } diff --git a/go.mod b/go.mod index 1611b4b9..4cf62aee 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/actions/actions-runner-controller -go 1.26.2 +go 1.26.3 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 - github.com/actions/scaleset v0.3.0 + github.com/actions/scaleset v0.4.0 github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/evanphx/json-patch v5.9.11+incompatible diff --git a/go.sum b/go.sum index 8b1ad837..8a6ea558 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/actions-runner-controller/httpcache v0.2.0 h1:hCNvYuVPJ2xxYBymqBvH0hS github.com/actions-runner-controller/httpcache v0.2.0/go.mod h1:JLu9/2M/btPz1Zu/vTZ71XzukQHn2YeISPmJoM5exBI= github.com/actions/scaleset v0.3.0 h1:y5/ClYLJXFuGCikzILOOPhaCShAcL6K0mnUtjDKFxVw= github.com/actions/scaleset v0.3.0/go.mod h1:2L2I6rggFWV+zprDet6y7y7Vkm3HPudaup78eSc79Uo= +github.com/actions/scaleset v0.4.0 h1:691GC2AkHb3ZGjfNvatboYoRS7CLr3+4VcZk/6w9IbM= +github.com/actions/scaleset v0.4.0/go.mod h1:2L2I6rggFWV+zprDet6y7y7Vkm3HPudaup78eSc79Uo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 102d3d60..b56a5749 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -783,26 +783,30 @@ func (e *env) installActionsRunnerController(t *testing.T, repo, tag, testID, ch } if e.useApp { - varEnv = append(varEnv, + varEnv = append( + varEnv, "ACCEPTANCE_TEST_SECRET_TYPE=app", "APP_ID="+e.appID, "APP_INSTALLATION_ID="+e.appInstallationID, "APP_PRIVATE_KEY_FILE="+e.appPrivateKeyFile, ) } else { - varEnv = append(varEnv, + varEnv = append( + varEnv, "ACCEPTANCE_TEST_SECRET_TYPE=token", "GITHUB_TOKEN="+e.githubToken, ) } if e.logFormat != "" { - varEnv = append(varEnv, + varEnv = append( + varEnv, "LOG_FORMAT="+e.logFormat, ) } - varEnv = append(varEnv, + varEnv = append( + varEnv, "GITHUB_WEBHOOK_SERVER_ENV_NAME="+c.GithubWebhookServerEnvName, "GITHUB_WEBHOOK_SERVER_ENV_VALUE="+c.GithubWebhookServerEnvValue, ) @@ -909,20 +913,24 @@ func (e *env) do(t *testing.T, op string, kind DeployKind, testID string, env .. } if e.dockerdWithinRunnerContainer { - varEnv = append(varEnv, + varEnv = append( + varEnv, "RUNNER_DOCKERD_WITHIN_RUNNER_CONTAINER=true", ) if e.rootlessDocker { - varEnv = append(varEnv, + varEnv = append( + varEnv, "RUNNER_NAME="+e.vars.runnerRootlessDindImageRepo, ) } else { - varEnv = append(varEnv, + varEnv = append( + varEnv, "RUNNER_NAME="+e.vars.runnerDindImageRepo, ) } } else { - varEnv = append(varEnv, + varEnv = append( + varEnv, "RUNNER_DOCKERD_WITHIN_RUNNER_CONTAINER=false", "RUNNER_NAME="+e.vars.runnerImageRepo, ) @@ -1062,14 +1070,16 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam if !kubernetesContainerMode { if kind == RunnerDeployments { - steps = append(steps, + steps = append( + steps, testing.Step{ Run: sudo + "mkdir -p \"${RUNNER_TOOL_CACHE}\" \"${HOME}/.cache\"", }, ) if useSudo { - steps = append(steps, + steps = append( + steps, testing.Step{ // This might be the easiest way to handle permissions without use of securityContext // https://stackoverflow.com/questions/50156124/kubernetes-nfs-persistent-volumes-permission-denied#comment107483717_53186320 @@ -1080,7 +1090,8 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam } if useSudo { - steps = append(steps, + steps = append( + steps, testing.Step{ // This might be the easiest way to handle permissions without use of securityContext // https://stackoverflow.com/questions/50156124/kubernetes-nfs-persistent-volumes-permission-denied#comment107483717_53186320 @@ -1102,18 +1113,20 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam ) } - steps = append(steps, + steps = append( + steps, testing.Step{ Uses: "actions/setup-go@v3", With: &testing.With{ - GoVersion: "1.26.1", + GoVersion: "1.26.3", }, }, ) // Ensure both the alias and the full command work after // https://github.com/actions/actions-runner-controller/pull/2326 - steps = append(steps, + steps = append( + steps, testing.Step{ Run: "docker-compose version", }, @@ -1123,7 +1136,8 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam ) } - steps = append(steps, + steps = append( + steps, testing.Step{ Run: "go version", }, @@ -1166,19 +1180,21 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam if useCustomDockerContext { setupBuildXActionWith.Endpoint = "mycontext" - steps = append(steps, testing.Step{ - // https://github.com/docker/buildx/issues/413#issuecomment-710660155 - // To prevent setup-buildx-action from failing with: - // error: could not create a builder instance with TLS data loaded from environment. Please use `docker context create ` to create a context for current environment and then create a builder instance with `docker buildx create ` - Run: "docker context create mycontext", - }, + steps = append( + steps, testing.Step{ + // https://github.com/docker/buildx/issues/413#issuecomment-710660155 + // To prevent setup-buildx-action from failing with: + // error: could not create a builder instance with TLS data loaded from environment. Please use `docker context create ` to create a context for current environment and then create a builder instance with `docker buildx create ` + Run: "docker context create mycontext", + }, testing.Step{ Run: "docker context use mycontext", }, ) } - steps = append(steps, + steps = append( + steps, testing.Step{ Name: "Set up Docker Buildx", Uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2", @@ -1203,7 +1219,8 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam ) if useSudo { - steps = append(steps, + steps = append( + steps, testing.Step{ // https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#local-cache // See https://github.com/moby/buildkit/issues/1896 for why this is needed @@ -1218,7 +1235,8 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam if useSudo { if kind == RunnerDeployments { - steps = append(steps, + steps = append( + steps, testing.Step{ // https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#local-cache // See https://github.com/moby/buildkit/issues/1896 for why this is needed @@ -1232,7 +1250,8 @@ func installActionsWorkflow(t *testing.T, testName, runnerLabel, testResultCMNam } } - steps = append(steps, + steps = append( + steps, testing.Step{ Uses: "azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f", With: &testing.With{