mirror of
https://github.com/actions-runner-controller/actions-runner-controller.git
synced 2026-10-01 04:41:31 +02:00
Fix experimental charts and upgrade Helm tooling (#4687)
This commit is contained in:
@@ -40,7 +40,7 @@ on:
|
||||
default: false
|
||||
|
||||
env:
|
||||
HELM_VERSION: v3.8.0
|
||||
HELM_VERSION: v4.2.2
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
@@ -18,7 +18,8 @@ on:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
KUBE_SCORE_VERSION: 1.16.1
|
||||
HELM_VERSION: v3.19.4
|
||||
HELM_VERSION: v4.2.2
|
||||
HELM_UNITTEST_VERSION: 1.1.2
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -105,7 +106,17 @@ jobs:
|
||||
|
||||
- name: Install helm-unittest
|
||||
run: |
|
||||
helm plugin install https://github.com/helm-unittest/helm-unittest.git
|
||||
# Pin the upstream signing key independently of the release download.
|
||||
curl --fail --silent --show-error --location \
|
||||
https://raw.githubusercontent.com/helm-unittest/helm-unittest/33c48cac798e465deda9a66c8e6c07c0973cf53d/public-key.asc \
|
||||
--output "${RUNNER_TEMP}/helm-unittest-key.asc"
|
||||
gpg --batch --yes --dearmor \
|
||||
--output "${RUNNER_TEMP}/helm-unittest-keyring.gpg" \
|
||||
"${RUNNER_TEMP}/helm-unittest-key.asc"
|
||||
helm plugin install \
|
||||
"https://github.com/helm-unittest/helm-unittest/releases/download/v${HELM_UNITTEST_VERSION}/unittest-${HELM_UNITTEST_VERSION}.tgz" \
|
||||
--verify \
|
||||
--keyring "${RUNNER_TEMP}/helm-unittest-keyring.gpg"
|
||||
|
||||
- name: Run helm-unittest (gha-runner-scale-set-controller-experimental)
|
||||
run: |
|
||||
@@ -125,3 +136,6 @@ jobs:
|
||||
|
||||
- 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/...
|
||||
|
||||
@@ -5,16 +5,21 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- ".github/workflows/go.yaml"
|
||||
- "charts/gha-runner-scale-set*/**"
|
||||
- "**.go"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/go.yaml"
|
||||
- "charts/gha-runner-scale-set*/**"
|
||||
- "**.go"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
|
||||
env:
|
||||
HELM_VERSION: v4.2.2
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -86,6 +91,10 @@ jobs:
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310
|
||||
with:
|
||||
version: ${{ env.HELM_VERSION }}
|
||||
- run: make manifests
|
||||
- name: Check diff
|
||||
run: git diff --exit-code
|
||||
|
||||
@@ -101,6 +101,11 @@ NAME=$DOCKER_USER/actions-runner make \
|
||||
A set of example pipelines (./acceptance/pipelines) are provided in this repository which you can use to validate your runners are working as expected.
|
||||
When raising a PR please run the relevant suites to prove your change hasn't broken anything.
|
||||
|
||||
Go chart tests and controller chart-contract tests require the `helm` CLI on `PATH`.
|
||||
Install the version listed under [Helm Version Changes](#helm-version-changes)
|
||||
before running `go test ./...` or `make test`. The make targets provision envtest,
|
||||
not Helm; the helm-unittest plugin is not required for Go tests.
|
||||
|
||||
#### Running Ginkgo Tests
|
||||
You can run the integration test suite that is written in Ginkgo with:
|
||||
|
||||
@@ -201,6 +206,13 @@ Send PR, add issue number to description
|
||||
In general we ask you not to bump the version in your PR.
|
||||
The maintainers will manage releases and publishing new charts.
|
||||
|
||||
The Go test job and scale-set chart validation and publishing workflows use
|
||||
Helm CLI v4.2.2.
|
||||
Keep their `HELM_VERSION` pins aligned when updating the CLI. Validation uses
|
||||
helm-unittest v1.1.2 from its signed release archive; the workflow verifies it
|
||||
with the pinned upstream signing key. Legacy ARC workflows retain their separate
|
||||
Helm version. These CLI pins do not change chart versions or require Helm 4 for users.
|
||||
|
||||
## Testing Controller Built from a Pull Request
|
||||
|
||||
We always appreciate your help in testing open pull requests by deploying custom builds of actions-runner-controller onto your own environment, so that we are extra sure we didn't break anything.
|
||||
|
||||
+4
-1
@@ -97,9 +97,12 @@ args:
|
||||
{{- end }}
|
||||
{{- $ports := list -}}
|
||||
{{- if .Values.controller.metrics }}
|
||||
{{- $metricsPort := dict "containerPort" ((regexReplaceAll ":([0-9]+)" .Values.controller.metrics.controllerManagerAddr "${1}") | int) "protocol" "TCP" "name" "metrics" -}}
|
||||
{{- $port := include "gha-controller.metrics-port" .Values.controller.metrics.controllerManagerAddr -}}
|
||||
{{- if $port }}
|
||||
{{- $metricsPort := dict "containerPort" (int $port) "protocol" "TCP" "name" "metrics" -}}
|
||||
{{- $ports = append $ports $metricsPort -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.controller.manager.container.extraPorts }}
|
||||
{{- if kindIs "slice" . }}
|
||||
{{- $ports = concat $ports . -}}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
{{/*
|
||||
The bind address must retain its host in --metrics-addr, but containerPort only
|
||||
accepts a numeric port. "0" disables the server and must not create a port.
|
||||
*/}}
|
||||
{{- define "gha-controller.metrics-port" -}}
|
||||
{{- $address := . -}}
|
||||
{{- $error := "controller.metrics.controllerManagerAddr must be \"0\" or a host:port address with a numeric port between 1 and 65535 (IPv6 hosts must be bracketed)" -}}
|
||||
{{- if ne (toString $address) "0" -}}
|
||||
{{- if not (kindIs "string" $address) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- if not (regexMatch `^(\[[^]]+\]|[^:]*):[0-9]+$` $address) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- $portString := regexFind "[0-9]+$" $address -}}
|
||||
{{- $port := atoi $portString -}}
|
||||
{{- if or (lt $port 1) (gt $port 65535) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- $host := trimSuffix (printf ":%s" $portString) $address -}}
|
||||
{{- if hasPrefix "[" $host -}}
|
||||
{{- $ip := trimSuffix "]" (trimPrefix "[" $host) -}}
|
||||
{{- $zone := splitList "%" $ip -}}
|
||||
{{- if gt (len $zone) 2 -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- if eq (len $zone) 2 -}}
|
||||
{{- if not (regexMatch "^[A-Za-z0-9_.-]+$" (index $zone 1)) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $ip = index $zone 0 -}}
|
||||
{{- if contains "." $ip -}}
|
||||
{{- $ipv4 := last (splitList ":" $ip) -}}
|
||||
{{- include "gha-controller.validate-ipv4" (dict "ip" $ipv4 "error" $error) -}}
|
||||
{{- $ip = printf "%s0:0" (trimSuffix $ipv4 $ip) -}}
|
||||
{{- end -}}
|
||||
{{- if or (not (regexMatch "^[0-9A-Fa-f:]+$" $ip)) (contains ":::" $ip) (and (hasPrefix ":" $ip) (not (hasPrefix "::" $ip))) (and (hasSuffix ":" $ip) (not (hasSuffix "::" $ip))) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- $halves := splitList "::" $ip -}}
|
||||
{{- $groups := splitList ":" $ip -}}
|
||||
{{- $count := 0 -}}
|
||||
{{- range $groups -}}
|
||||
{{- if ne . "" -}}
|
||||
{{- if not (regexMatch "^[0-9A-Fa-f]{1,4}$" .) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- $count = add1 $count -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if eq (len $halves) 1 -}}
|
||||
{{- if or (ne $count 8) (hasPrefix ":" $ip) (hasSuffix ":" $ip) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- else if or (ne (len $halves) 2) (ge $count 8) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- else if ne $host "" -}}
|
||||
{{- if or (gt (len $host) 253) (not (regexMatch `^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*\.?$` $host)) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- range splitList "." $host -}}
|
||||
{{- if gt (len .) 63 -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if regexMatch `^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$` $host -}}
|
||||
{{- include "gha-controller.validate-ipv4" (dict "ip" $host "error" $error) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $port -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gha-controller.validate-ipv4" -}}
|
||||
{{- $error := .error -}}
|
||||
{{- $parts := splitList "." .ip -}}
|
||||
{{- if ne (len $parts) 4 -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- range $parts -}}
|
||||
{{- if or (not (regexMatch "^(0|[1-9][0-9]{0,2})$" .)) (gt (int .) 255) -}}
|
||||
{{- fail $error -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
suite: Controller metrics addresses
|
||||
templates:
|
||||
- deployment.yaml
|
||||
set:
|
||||
controller.metrics.listenerAddr: ":9090"
|
||||
controller.metrics.listenerEndpoint: /metrics
|
||||
tests:
|
||||
- it: extracts the IPv4 bind port without changing the address
|
||||
set:
|
||||
controller.metrics.controllerManagerAddr: "127.0.0.1:8080"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].args
|
||||
content: "--metrics-addr=127.0.0.1:8080"
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].ports
|
||||
value: [{name: metrics, containerPort: 8080, protocol: TCP}]
|
||||
- it: extracts the bracketed IPv6 bind port
|
||||
set:
|
||||
controller.metrics.controllerManagerAddr: "[::]:8080"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].args
|
||||
content: "--metrics-addr=[::]:8080"
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].ports
|
||||
value: [{name: metrics, containerPort: 8080, protocol: TCP}]
|
||||
- it: disables only controller metrics and retains extra ports
|
||||
set:
|
||||
controller.metrics.controllerManagerAddr: "0"
|
||||
controller.manager.container.extraPorts:
|
||||
- {name: custom, containerPort: 9000, protocol: TCP}
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].args
|
||||
content: "--metrics-addr=0"
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].args
|
||||
content: "--listener-metrics-addr=:9090"
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].ports
|
||||
value: [{name: custom, containerPort: 9000, protocol: TCP}]
|
||||
- it: rejects a non-numeric port explicitly
|
||||
set:
|
||||
controller.metrics.controllerManagerAddr: "localhost:http"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: 'controller.metrics.controllerManagerAddr must be "0" or a host:port address with a numeric port between 1 and 65535 (IPv6 hosts must be bracketed)'
|
||||
@@ -126,8 +126,10 @@ controller:
|
||||
volumeMounts: []
|
||||
|
||||
# Metrics configuration. If omitted, metrics are disabled.
|
||||
# controllerManagerAddr accepts host:port (e.g. ":8080", "127.0.0.1:8080",
|
||||
# "[::]:8080", or "localhost:8080"), with a numeric port from 1 to 65535.
|
||||
# Set it to "0" to disable controller metrics independently of listener metrics.
|
||||
# metrics:
|
||||
# controllerManagerAddr: ":8080"
|
||||
# listenerAddr: ":8080"
|
||||
# listenerEndpoint: "/metrics"
|
||||
|
||||
|
||||
@@ -115,14 +115,20 @@ spec:
|
||||
{{- end }}
|
||||
command:
|
||||
- "/manager"
|
||||
{{- if or .Values.metrics .Values.pprof.addr }}
|
||||
{{- $controllerMetricsEnabled := false }}
|
||||
{{- with .Values.metrics }}
|
||||
{{- $controllerMetricsEnabled = ne (toString .controllerManagerAddr) "0" }}
|
||||
{{- end }}
|
||||
{{- if or $controllerMetricsEnabled .Values.pprof.addr }}
|
||||
ports:
|
||||
{{- end }}
|
||||
{{- if $controllerMetricsEnabled }}
|
||||
{{- with .Values.metrics }}
|
||||
- containerPort: {{ required "Values.metrics.controllerManagerAddr must end with a numeric port" (regexFind "[0-9]+$" .controllerManagerAddr) }}
|
||||
protocol: TCP
|
||||
name: metrics
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.pprof.addr }}
|
||||
- containerPort: {{ required "Values.pprof.addr must end with a numeric port" (regexFind "[0-9]+$" .Values.pprof.addr) }}
|
||||
protocol: TCP
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gruntwork-io/terratest/modules/helm"
|
||||
"github.com/gruntwork-io/terratest/modules/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestControllerMetricsAddress(t *testing.T) {
|
||||
for _, chartName := range []string{"gha-runner-scale-set-controller", "gha-runner-scale-set-controller-experimental"} {
|
||||
t.Run(chartName, func(t *testing.T) {
|
||||
chart, err := filepath.Abs("../../" + chartName)
|
||||
require.NoError(t, err)
|
||||
for _, tc := range []struct {
|
||||
address string
|
||||
port int32
|
||||
}{
|
||||
{":8080", 8080}, {"127.0.0.1:8080", 8080}, {"[::]:8080", 8080},
|
||||
{"[2001:db8::1]:9090", 9090}, {"[::ffff:127.0.0.1]:8080", 8080},
|
||||
{"[fe80::1%eth0]:8080", 8080}, {"localhost:8080", 8080},
|
||||
{"metrics.example.com:8080", 8080}, {":1", 1}, {":65535", 65535}, {"0", 0},
|
||||
{"", 0},
|
||||
} {
|
||||
t.Run(tc.address, func(t *testing.T) {
|
||||
values := ""
|
||||
if tc.address != "" {
|
||||
values = fmt.Sprintf("metrics:\n controllerManagerAddr: %q\n listenerAddr: ':9090'\n listenerEndpoint: /metrics\n", tc.address)
|
||||
if chartName == "gha-runner-scale-set-controller-experimental" {
|
||||
values = "controller:\n" + indentMetricsValues(values)
|
||||
}
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "values.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(values), 0600))
|
||||
output := helm.RenderTemplateContext(t, t.Context(), &helm.Options{
|
||||
Logger: logger.Discard, ValuesFiles: []string{path},
|
||||
}, chart, "metrics", []string{"templates/deployment.yaml"})
|
||||
var deployment appsv1.Deployment
|
||||
require.NoError(t, yaml.UnmarshalStrict([]byte(output), &deployment))
|
||||
c := deployment.Spec.Template.Spec.Containers[0]
|
||||
if tc.port == 0 {
|
||||
assert.Empty(t, c.Ports)
|
||||
assert.Contains(t, c.Args, "--metrics-addr=0")
|
||||
} else {
|
||||
assert.Equal(t, []corev1.ContainerPort{{Name: "metrics", ContainerPort: tc.port, Protocol: corev1.ProtocolTCP}}, c.Ports)
|
||||
assert.Contains(t, c.Args, "--metrics-addr="+tc.address)
|
||||
}
|
||||
if tc.address != "" {
|
||||
assert.Contains(t, c.Args, "--listener-metrics-addr=:9090")
|
||||
assert.Contains(t, c.Args, "--listener-metrics-endpoint=/metrics")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func indentMetricsValues(values string) string {
|
||||
result := ""
|
||||
for _, line := range strings.Split(strings.TrimSuffix(values, "\n"), "\n") {
|
||||
result += " " + line + "\n"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestExperimentalControllerRejectsInvalidMetricsAddress(t *testing.T) {
|
||||
chart, err := filepath.Abs("../../gha-runner-scale-set-controller-experimental")
|
||||
require.NoError(t, err)
|
||||
for _, address := range []string{
|
||||
"", "8080", ":0", ":65536", ":-1", ":http", ":1.5", ":999999999999999999999",
|
||||
"127.0.0.1", "http://localhost:8080", "::1:8080", "[::]:http", "[::1:8080",
|
||||
"[not-an-ip]:8080", "[::::]:8080", "[1:2:3]:8080", "[:1::]:8080", "[::1:]:8080",
|
||||
"[::1::2]:8080", "[1:2:3:4:5:6:7:8::]:8080", "host name:8080", "bad/host:8080",
|
||||
"999.999.999.999:8080",
|
||||
} {
|
||||
t.Run(address, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "values.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(fmt.Sprintf("controller:\n metrics:\n controllerManagerAddr: %q\n listenerAddr: ':9090'\n listenerEndpoint: /metrics\n", address)), 0600))
|
||||
_, err := helm.RenderTemplateContextE(t, t.Context(), &helm.Options{
|
||||
Logger: logger.Discard, ValuesFiles: []string{path},
|
||||
}, chart, "metrics", []string{"templates/deployment.yaml"})
|
||||
require.ErrorContains(t, err, "controller.metrics.controllerManagerAddr")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalControllerMetricsDecimalPort(t *testing.T) {
|
||||
chart, err := filepath.Abs("../../gha-runner-scale-set-controller-experimental")
|
||||
require.NoError(t, err)
|
||||
for address, port := range map[string]int32{":08080": 8080, "localhost:008080": 8080, ":010": 10} {
|
||||
t.Run(address, func(t *testing.T) {
|
||||
output := helm.RenderTemplateContext(t, t.Context(), &helm.Options{
|
||||
Logger: logger.Discard,
|
||||
SetValues: map[string]string{
|
||||
"controller.metrics.controllerManagerAddr": address,
|
||||
"controller.metrics.listenerAddr": ":9090",
|
||||
"controller.metrics.listenerEndpoint": "/metrics",
|
||||
},
|
||||
}, chart, "metrics", []string{"templates/deployment.yaml"})
|
||||
var deployment appsv1.Deployment
|
||||
require.NoError(t, yaml.UnmarshalStrict([]byte(output), &deployment))
|
||||
assert.Equal(t, port, deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort)
|
||||
assert.Contains(t, deployment.Spec.Template.Spec.Containers[0].Args, "--metrics-addr="+address)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerMetricsDisabledKeepsPprof(t *testing.T) {
|
||||
chart, err := filepath.Abs("..")
|
||||
require.NoError(t, err)
|
||||
output := helm.RenderTemplateContext(t, t.Context(), &helm.Options{
|
||||
Logger: logger.Discard,
|
||||
SetValues: map[string]string{
|
||||
"metrics.controllerManagerAddr": "0",
|
||||
"metrics.listenerAddr": ":9090",
|
||||
"metrics.listenerEndpoint": "/metrics",
|
||||
"pprof.addr": ":6060",
|
||||
},
|
||||
}, chart, "metrics", []string{"templates/deployment.yaml"})
|
||||
var deployment appsv1.Deployment
|
||||
require.NoError(t, yaml.UnmarshalStrict([]byte(output), &deployment))
|
||||
c := deployment.Spec.Template.Spec.Containers[0]
|
||||
assert.Equal(t, []corev1.ContainerPort{{Name: "pprof", ContainerPort: 6060, Protocol: corev1.ProtocolTCP}}, c.Ports)
|
||||
assert.Contains(t, c.Args, "--metrics-addr=0")
|
||||
assert.Contains(t, c.Args, "--listener-metrics-addr=:9090")
|
||||
assert.Contains(t, c.Args, "--pprof-addr=:6060")
|
||||
}
|
||||
@@ -93,6 +93,8 @@ priorityClassName: ""
|
||||
## This will disable metrics.
|
||||
##
|
||||
## To enable metrics, uncomment the following lines.
|
||||
## Set controllerManagerAddr to "0" to disable only controller metrics while
|
||||
## keeping listener metrics enabled; no controller metrics containerPort is emitted.
|
||||
# metrics:
|
||||
# controllerManagerAddr: ":8080"
|
||||
# listenerAddr: ":8080"
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
{{- define "runner-mode-dind.runner-container" -}}
|
||||
name: runner
|
||||
image: {{ include "runner.image" . | quote }}
|
||||
command: {{ include "runner.command" . }}
|
||||
env:
|
||||
- {{ include "runner-mode-dind.env-docker-host" . | nindent 4 }}
|
||||
- {{ include "runner-mode-dind.env-wait-for-docker-timeout" . | nindent 4 }}
|
||||
{{/* TODO:: Should we skip DOCKER_HOST and RUNNER_WAIT_FOR_DOCKER_IN_SECONDS? */}}
|
||||
{{- with .Values.runner.env }}
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{ include "githubServerTLS.envItems" (dict "root" $ "existingEnv" (.Values.runner.env | default list)) | nindent 2 }}
|
||||
volumeMounts:
|
||||
- name: work
|
||||
mountPath: /home/runner/_work
|
||||
- name: dind-sock
|
||||
mountPath: {{ include "runner-mode-dind.sock-mount-dir" . | quote }}
|
||||
{{ include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (list)) | nindent 2 }}
|
||||
{{- $env := list (include "runner-mode-dind.env-docker-host" . | fromYaml) (include "runner-mode-dind.env-wait-for-docker-timeout" . | fromYaml) -}}
|
||||
{{- $mounts := list (dict "name" "work" "mountPath" "/home/runner/_work") (dict "name" "dind-sock" "mountPath" (include "runner-mode-dind.sock-mount-dir" .)) -}}
|
||||
{{- include "runner-container.render" (dict "root" . "env" $env "volumeMounts" $mounts "legacyEnv" .Values.runner.env) -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "runner-mode-dind.dind-container" -}}
|
||||
@@ -143,7 +129,10 @@ value: {{ $dockerSock | quote }}
|
||||
|
||||
{{- define "runner-mode-dind.env-wait-for-docker-timeout" -}}
|
||||
{{- $dind := .Values.runner.dind | default dict -}}
|
||||
{{- $waitForDockerInSeconds := $dind.waitForDockerInSeconds | default 120 -}}
|
||||
{{- $waitForDockerInSeconds := 120 -}}
|
||||
{{- if hasKey $dind "waitForDockerInSeconds" -}}
|
||||
{{- $waitForDockerInSeconds = $dind.waitForDockerInSeconds -}}
|
||||
{{- end -}}
|
||||
{{- if not (or (kindIs "int" $waitForDockerInSeconds) (kindIs "int64" $waitForDockerInSeconds) (kindIs "float64" $waitForDockerInSeconds)) -}}
|
||||
{{- fail "runner.dind.waitForDockerInSeconds must be a number" -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -5,30 +5,5 @@ Container spec that is expanded for the runner container
|
||||
{{- if not .Values.runner.container }}
|
||||
{{ fail "You must provide a runner container specification in values.runner.container" }}
|
||||
{{- end }}
|
||||
name: runner
|
||||
image: {{ .Values.runner.container.image | default "ghcr.io/actions/actions-runner:latest" }}
|
||||
command: {{ toJson (default (list "/home/runner/run.sh") .Values.runner.container.command) }}
|
||||
|
||||
{{ $tlsEnvItems := include "githubServerTLS.envItems" (dict "root" $ "existingEnv" (.Values.runner.container.env | default list)) }}
|
||||
{{ if or .Values.runner.container.env $tlsEnvItems }}
|
||||
env:
|
||||
{{- with .Values.runner.container.env }}
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{ $tlsEnvItems | nindent 2 }}
|
||||
{{ end }}
|
||||
|
||||
{{ $tlsVolumeMountItem := include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (.Values.runner.container.volumeMounts | default list)) }}
|
||||
{{ if or .Values.runner.container.volumeMounts $tlsVolumeMountItem }}
|
||||
volumeMounts:
|
||||
{{- with .Values.runner.container.volumeMounts }}
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{ $tlsVolumeMountItem | nindent 2 }}
|
||||
{{ end }}
|
||||
|
||||
{{ $extra := omit .Values.runner.container "name" "image" "command" "env" "volumeMounts" }}
|
||||
{{- if not (empty $extra) -}}
|
||||
{{ toYaml $extra }}
|
||||
{{- end -}}
|
||||
{{- include "runner-container.render" (dict "root" .) -}}
|
||||
{{- end }}
|
||||
@@ -20,15 +20,6 @@
|
||||
{{- end -}}
|
||||
{{- $hasExtension := or (not (empty $extensionRef)) (not (empty $extensionYamlStr)) -}}
|
||||
{{- $hookTemplatePath := printf "%s/hook-template.yaml" (dir $hookPath) -}}
|
||||
{{- $setHookTemplateEnv := true -}}
|
||||
{{- $userEnv := (.Values.runner.env | default list) -}}
|
||||
{{- if kindIs "slice" $userEnv -}}
|
||||
{{- range $userEnv -}}
|
||||
{{- if and (kindIs "map" .) (eq ((index . "name") | default "") "ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE") -}}
|
||||
{{- $setHookTemplateEnv = false -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "string" $hookPath) -}}
|
||||
{{- fail "runner.kubernetesMode.hookPath must be a string" -}}
|
||||
{{- end -}}
|
||||
@@ -48,41 +39,20 @@
|
||||
{{- if not (kindIs "bool" $requireJobContainer) -}}
|
||||
{{- fail "runner.kubernetesMode.requireJobContainer must be a bool" -}}
|
||||
{{- end -}}
|
||||
name: runner
|
||||
image: {{ include "runner.image" . | quote }}
|
||||
command: {{ include "runner.command" . }}
|
||||
|
||||
{{ $tlsEnvItems := include "githubServerTLS.envItems" (dict "root" $ "existingEnv" (.Values.runner.env | default list)) }}
|
||||
env:
|
||||
- name: ACTIONS_RUNNER_CONTAINER_HOOKS
|
||||
value: {{ $hookPath | quote }}
|
||||
- name: ACTIONS_RUNNER_POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER
|
||||
value: {{ ternary "true" "false" $requireJobContainer | quote }}
|
||||
{{- if not $requireJobContainer -}}
|
||||
{{- printf "# WARNING: runner.kubernetesMode.requireJobContainer is set to false. This means that the runner container will be used to execute jobs, which may lead to security risks if the runner is compromised. It is recommended to set runner.kubernetesMode.requireJobContainer to true in production environments." }}
|
||||
{{- end -}}
|
||||
{{- if and $hasExtension $setHookTemplateEnv }}
|
||||
- name: ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE
|
||||
value: {{ $hookTemplatePath | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.runner.env }}
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{ $tlsEnvItems | nindent 2 }}
|
||||
volumeMounts:
|
||||
- name: work
|
||||
mountPath: /home/runner/_work
|
||||
{{- if $hasExtension }}
|
||||
- name: hook-extension
|
||||
mountPath: {{ $hookTemplatePath | quote }}
|
||||
subPath: extension
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{ include "githubServerTLS.volumeMountItem" (dict "root" $ "existingVolumeMounts" (list)) | nindent 2 }}
|
||||
{{- $env := list
|
||||
(dict "name" "ACTIONS_RUNNER_CONTAINER_HOOKS" "value" $hookPath)
|
||||
(dict "name" "ACTIONS_RUNNER_POD_NAME" "valueFrom" (dict "fieldRef" (dict "fieldPath" "metadata.name")))
|
||||
(dict "name" "ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER" "value" (ternary "true" "false" $requireJobContainer))
|
||||
-}}
|
||||
{{- $mounts := list (dict "name" "work" "mountPath" "/home/runner/_work") -}}
|
||||
{{- if $hasExtension -}}
|
||||
{{- $env = append $env (dict "name" "ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE" "value" $hookTemplatePath) -}}
|
||||
{{- $mounts = append $mounts (dict "name" "hook-extension" "mountPath" $hookTemplatePath "subPath" "extension" "readOnly" true) -}}
|
||||
{{- end -}}
|
||||
{{- if not $requireJobContainer }}
|
||||
# WARNING: Jobs may execute directly in the runner container when requireJobContainer is false.
|
||||
{{ end -}}
|
||||
{{- include "runner-container.render" (dict "root" . "env" $env "volumeMounts" $mounts "legacyEnv" .Values.runner.env) -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "runner-mode-kubernetes.pod-volumes" -}}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{{/*
|
||||
Merge list entries by their Kubernetes identity, replacing whole entries rather
|
||||
than merging maps (an EnvVar must not acquire both value and valueFrom).
|
||||
User entries precede defaults, as in the standard runner chart.
|
||||
*/}}
|
||||
{{- define "runner-container.merge-list" -}}
|
||||
{{- $key := .key -}}
|
||||
{{- $path := .path -}}
|
||||
{{- $seen := dict -}}
|
||||
{{- $out := list -}}
|
||||
{{- range $group := list .overrides .defaults -}}
|
||||
{{- $groupSeen := dict -}}
|
||||
{{- range $group -}}
|
||||
{{- if not (kindIs "map" .) -}}
|
||||
{{- fail (printf "%s must contain objects with a %s" $path $key) -}}
|
||||
{{- end -}}
|
||||
{{- $identity := index . $key -}}
|
||||
{{- if or (not (kindIs "string" $identity)) (empty $identity) -}}
|
||||
{{- fail (printf "%s entries must have a non-empty %s" $path $key) -}}
|
||||
{{- end -}}
|
||||
{{- if hasKey $groupSeen $identity -}}
|
||||
{{- fail (printf "%s contains duplicate %s %q" $path $key $identity) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $groupSeen $identity true -}}
|
||||
{{- if not (hasKey $seen $identity) -}}
|
||||
{{- $out = append $out . -}}
|
||||
{{- $_ := set $seen $identity true -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
All modes share the documented runner.container customization surface.
|
||||
Keep Values immutable: mode and TLS defaults are built separately and only a
|
||||
deep copy of the user's container is modified.
|
||||
*/}}
|
||||
{{- define "runner-container.render" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $container := $root.Values.runner.container | default dict -}}
|
||||
{{- if not (kindIs "map" $container) -}}
|
||||
{{- fail "runner.container must be a map/object" -}}
|
||||
{{- end -}}
|
||||
{{- range $field := list "env" "volumeMounts" "args" -}}
|
||||
{{- if and (hasKey $container $field) (not (kindIs "slice" (index $container $field))) -}}
|
||||
{{- fail (printf "runner.container.%s must be a list" $field) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $field := list "resources" "securityContext" -}}
|
||||
{{- if and (hasKey $container $field) (not (kindIs "map" (index $container $field))) -}}
|
||||
{{- fail (printf "runner.container.%s must be a map/object" $field) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if hasKey $container "volumes" -}}
|
||||
{{- fail "runner.container.volumes is not supported; use runner.pod.spec.volumes" -}}
|
||||
{{- end -}}
|
||||
{{- $out := deepCopy (omit $container "name" "image" "command" "env" "volumeMounts") -}}
|
||||
{{- $_ := set $out "name" "runner" -}}
|
||||
{{- $_ := set $out "image" (include "runner.image" $root) -}}
|
||||
{{- $_ := set $out "command" (include "runner.command" $root | fromJsonArray) -}}
|
||||
{{- $env := $container.env | default list -}}
|
||||
{{- if .legacyEnv -}}
|
||||
{{- if not (kindIs "slice" .legacyEnv) -}}
|
||||
{{- fail "runner.env must be a list; use runner.container.env" -}}
|
||||
{{- end -}}
|
||||
{{- $env = include "runner-container.merge-list" (dict "key" "name" "path" "runner.container.env" "overrides" $env "defaults" .legacyEnv) | fromYamlArray -}}
|
||||
{{- end -}}
|
||||
{{- $env = include "runner-container.merge-list" (dict "key" "name" "path" "runner.container.env" "overrides" $env "defaults" (.env | default list)) | fromYamlArray -}}
|
||||
{{- $tlsEnv := include "githubServerTLS.envItems" (dict "root" $root "existingEnv" $env) | fromYamlArray -}}
|
||||
{{- $env = concat $env $tlsEnv -}}
|
||||
{{- if $env -}}
|
||||
{{- $_ := set $out "env" $env -}}
|
||||
{{- end -}}
|
||||
{{- $mounts := include "runner-container.merge-list" (dict "key" "mountPath" "path" "runner.container.volumeMounts" "overrides" ($container.volumeMounts | default list) "defaults" (.volumeMounts | default list)) | fromYamlArray -}}
|
||||
{{- $tlsMounts := include "githubServerTLS.volumeMountItem" (dict "root" $root "existingVolumeMounts" $mounts) | fromYamlArray -}}
|
||||
{{- $mounts = include "runner-container.merge-list" (dict "key" "mountPath" "path" "runner.container.volumeMounts" "overrides" $mounts "defaults" $tlsMounts) | fromYamlArray -}}
|
||||
{{- if $mounts -}}
|
||||
{{- $_ := set $out "volumeMounts" $mounts -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{- define "secret-resolution.type" -}}
|
||||
{{- $config := .Values.secretResolution -}}
|
||||
{{- include "assert-map" (dict "value" $config "path" ".Values.secretResolution") -}}
|
||||
{{- if not $config -}}
|
||||
kubernetes
|
||||
{{- else -}}
|
||||
{{- $type := $config.type -}}
|
||||
{{- if not (kindIs "string" $type) -}}
|
||||
{{- fail (printf "Unsupported keyVault type: %v" $type) -}}
|
||||
{{- end -}}
|
||||
{{- if not (has $type (list "kubernetes" "azureKeyVault")) -}}
|
||||
{{- fail (printf "Unsupported keyVault type: %s" $type) -}}
|
||||
{{- end -}}
|
||||
{{- $type -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -5,7 +5,7 @@
|
||||
{{- $dind := (index $runner "dind" | default dict) }}
|
||||
{{- $kubeDefaults := (index $kubeMode "default" | default true) }}
|
||||
{{- $kubeServiceAccountName := (index $kubeMode "serviceAccountName" | default "") }}
|
||||
{{- $usesKubernetesSecrets := or (not .Values.secretResolution) (eq .Values.secretResolution.type "kubernetes") }}
|
||||
{{- $usesKubernetesSecrets := eq (include "secret-resolution.type" .) "kubernetes" }}
|
||||
{{- /* All listener configuration lives under .Values.listener */ -}}
|
||||
{{- if hasKey .Values "listenerMetrics" }}
|
||||
{{- fail ".Values.listenerMetrics has moved to .Values.listener.metrics" }}
|
||||
@@ -156,22 +156,20 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.secretResolution (ne .Values.secretResolution.type "kubernetes") }}
|
||||
{{- if not $usesKubernetesSecrets }}
|
||||
vaultConfig:
|
||||
type: {{ .Values.secretResolution.type }}
|
||||
type: azure_key_vault
|
||||
{{- if .Values.secretResolution.proxy }}
|
||||
proxy: {{- toYaml .Values.secretResolution.proxy | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.secretResolution.type "azureKeyVault" }}
|
||||
{{- if hasKey (.Values.secretResolution.azureKeyVault | default dict) "secretKey" }}
|
||||
{{- fail "secretResolution.azureKeyVault.secretKey is not supported; use auth.secretName to select the vault secret" }}
|
||||
{{- end }}
|
||||
azureKeyVault:
|
||||
url: {{ .Values.secretResolution.azureKeyVault.url }}
|
||||
tenantId: {{ .Values.secretResolution.azureKeyVault.tenantId }}
|
||||
clientId: {{ .Values.secretResolution.azureKeyVault.clientId }}
|
||||
certificatePath: {{ .Values.secretResolution.azureKeyVault.certificatePath }}
|
||||
secretKey: {{ .Values.secretResolution.azureKeyVault.secretKey }}
|
||||
{{- else }}
|
||||
{{- fail (printf "Unsupported keyVault type: %s" .Values.secretResolution.type) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.proxy }}
|
||||
@@ -310,6 +308,9 @@ spec:
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "autoscaling-runner-set.template-service-account" . | quote }}
|
||||
{{- if not (hasKey $runnerPodSpec "restartPolicy") }}
|
||||
restartPolicy: Never
|
||||
{{- end }}
|
||||
{{- if $hasInitContainers }}
|
||||
initContainers:
|
||||
{{- if and (eq $runnerMode "dind") $dind.copyRunnerExternals }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{- include "validate-all-metadata" . }}
|
||||
{{- $usesKubernetesSecrets := or (not .Values.secretResolution) (eq .Values.secretResolution.type "kubernetes") -}}
|
||||
{{- $usesKubernetesSecrets := eq (include "secret-resolution.type" .) "kubernetes" -}}
|
||||
|
||||
{{- if and (not $usesKubernetesSecrets) (empty .Values.auth.secretName) -}}
|
||||
{{- fail ".Values.auth.secretName is required when .Values.secretResolution.type is not \"kubernetes\"" -}}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
suite: Runner container customization and restart policy
|
||||
templates:
|
||||
- autoscalingrunnserset.yaml
|
||||
set:
|
||||
auth.url: https://github.com/example
|
||||
auth.secretName: existing-auth
|
||||
controllerServiceAccount.name: controller
|
||||
controllerServiceAccount.namespace: arc-system
|
||||
runner.container.env:
|
||||
- {name: CUSTOM_MARKER, value: expected}
|
||||
runner.container.resources.requests: {cpu: 250m, memory: 512Mi}
|
||||
runner.container.securityContext: {runAsUser: 1000, runAsGroup: 0, privileged: false}
|
||||
runner.container.volumeMounts:
|
||||
- {name: custom, mountPath: /custom}
|
||||
runner.pod.spec.volumes:
|
||||
- {name: custom, emptyDir: {}}
|
||||
tests:
|
||||
- it: preserves manual container customization and defaults to Never
|
||||
set:
|
||||
runner.mode: ""
|
||||
asserts: &container-contract
|
||||
- equal:
|
||||
path: spec.template.spec.restartPolicy
|
||||
value: Never
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content: {name: CUSTOM_MARKER, value: expected}
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].resources.requests
|
||||
value: {cpu: 250m, memory: 512Mi}
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].securityContext
|
||||
value: {runAsUser: 1000, runAsGroup: 0, privileged: false}
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content: {name: custom, mountPath: /custom}
|
||||
- it: preserves dind container customization and defaults to Never
|
||||
set:
|
||||
runner.mode: dind
|
||||
asserts: *container-contract
|
||||
- it: preserves kubernetes container customization and defaults to Never
|
||||
set:
|
||||
runner.mode: kubernetes
|
||||
asserts: *container-contract
|
||||
- it: preserves a manual restart policy override
|
||||
set:
|
||||
runner.mode: ""
|
||||
runner.pod.spec.restartPolicy: OnFailure
|
||||
asserts: &restart-override
|
||||
- equal:
|
||||
path: spec.template.spec.restartPolicy
|
||||
value: OnFailure
|
||||
- it: preserves a dind restart policy override
|
||||
set:
|
||||
runner.mode: dind
|
||||
runner.pod.spec.restartPolicy: OnFailure
|
||||
asserts: *restart-override
|
||||
- it: preserves a kubernetes restart policy override
|
||||
set:
|
||||
runner.mode: kubernetes
|
||||
runner.pod.spec.restartPolicy: OnFailure
|
||||
asserts: *restart-override
|
||||
+18
-5
@@ -23,6 +23,7 @@ tests:
|
||||
scaleset.name: "test"
|
||||
auth.url: "https://github.com/org"
|
||||
auth.githubToken: "gh_token12345"
|
||||
auth.secretName: "azure-auth"
|
||||
controllerServiceAccount.name: "arc"
|
||||
controllerServiceAccount.namespace: "arc-system"
|
||||
secretResolution:
|
||||
@@ -32,14 +33,16 @@ tests:
|
||||
tenantId: "tenant-123"
|
||||
clientId: "client-456"
|
||||
certificatePath: "/etc/certs/akv.pem"
|
||||
secretKey: "secret-key-name"
|
||||
release:
|
||||
name: "test-name"
|
||||
namespace: "test-namespace"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.vaultConfig.type
|
||||
value: azureKeyVault
|
||||
value: azure_key_vault
|
||||
- equal:
|
||||
path: spec.githubConfigSecret
|
||||
value: azure-auth
|
||||
- equal:
|
||||
path: spec.vaultConfig.azureKeyVault.url
|
||||
value: "https://myvault.vault.azure.net"
|
||||
@@ -52,9 +55,8 @@ tests:
|
||||
- equal:
|
||||
path: spec.vaultConfig.azureKeyVault.certificatePath
|
||||
value: "/etc/certs/akv.pem"
|
||||
- equal:
|
||||
- notExists:
|
||||
path: spec.vaultConfig.azureKeyVault.secretKey
|
||||
value: "secret-key-name"
|
||||
|
||||
- it: should render vaultConfig proxy when configured
|
||||
set:
|
||||
@@ -76,7 +78,6 @@ tests:
|
||||
tenantId: "tenant-123"
|
||||
clientId: "client-456"
|
||||
certificatePath: "/etc/certs/akv.pem"
|
||||
secretKey: "secret-key-name"
|
||||
release:
|
||||
name: "test-name"
|
||||
namespace: "test-namespace"
|
||||
@@ -108,3 +109,15 @@ tests:
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "Unsupported keyVault type: hashicorpVault"
|
||||
|
||||
- it: rejects the unused secretKey rather than silently changing the selector
|
||||
set:
|
||||
auth.url: https://github.com/example
|
||||
auth.secretName: azure-auth
|
||||
controllerServiceAccount.name: controller
|
||||
controllerServiceAccount.namespace: arc-system
|
||||
secretResolution.type: azureKeyVault
|
||||
secretResolution.azureKeyVault.secretKey: ignored
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "secretResolution.azureKeyVault.secretKey is not supported; use auth.secretName to select the vault secret"
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
|
||||
"github.com/actions/actions-runner-controller/vault"
|
||||
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
|
||||
"github.com/gruntwork-io/terratest/modules/helm"
|
||||
"github.com/gruntwork-io/terratest/modules/k8s"
|
||||
"github.com/gruntwork-io/terratest/modules/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const contractValues = `
|
||||
auth:
|
||||
url: https://github.com/example
|
||||
secretName: existing-auth
|
||||
controllerServiceAccount:
|
||||
name: controller
|
||||
namespace: arc-system
|
||||
`
|
||||
|
||||
func renderContract(t *testing.T, values string, set map[string]string) (string, error) {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "values.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(contractValues+values), 0600))
|
||||
chart, err := filepath.Abs("..")
|
||||
require.NoError(t, err)
|
||||
return helm.RenderTemplateContextE(t, t.Context(), &helm.Options{
|
||||
Logger: logger.Discard,
|
||||
ValuesFiles: []string{path},
|
||||
SetValues: set,
|
||||
KubectlOptions: k8s.NewKubectlOptions("", "", "runners"),
|
||||
}, chart, "contract", []string{"templates/autoscalingrunnserset.yaml"})
|
||||
}
|
||||
|
||||
func runnerSetContract(t *testing.T, values string, set map[string]string) *actionsv1alpha1.AutoscalingRunnerSet {
|
||||
t.Helper()
|
||||
output, err := renderContract(t, values, set)
|
||||
require.NoError(t, err)
|
||||
var ars actionsv1alpha1.AutoscalingRunnerSet
|
||||
require.NoError(t, yaml.UnmarshalStrict([]byte(output), &ars))
|
||||
return &ars
|
||||
}
|
||||
|
||||
func TestExperimentalAzureVaultDispatch(t *testing.T) {
|
||||
certificate := filepath.Join(t.TempDir(), "missing.pem")
|
||||
output, err := renderContract(t, fmt.Sprintf(`
|
||||
secretResolution:
|
||||
type: azureKeyVault
|
||||
azureKeyVault:
|
||||
url: https://example.vault.azure.net
|
||||
tenantId: tenant
|
||||
clientId: client
|
||||
certificatePath: %q
|
||||
`, certificate), nil)
|
||||
require.NoError(t, err)
|
||||
var ars actionsv1alpha1.AutoscalingRunnerSet
|
||||
assert.NoError(t, yaml.UnmarshalStrict([]byte(output), &ars))
|
||||
require.NotNil(t, ars.Spec.VaultConfig)
|
||||
assert.Equal(t, vault.VaultTypeAzureKeyVault, ars.Spec.VaultConfig.Type)
|
||||
assert.NoError(t, ars.Spec.VaultConfig.Type.Validate())
|
||||
assert.Equal(t, "tenant", ars.Spec.VaultConfig.AzureKeyVault.TenantID)
|
||||
assert.Equal(t, "client", ars.Spec.VaultConfig.AzureKeyVault.ClientID)
|
||||
assert.Equal(t, certificate, ars.Spec.VaultConfig.AzureKeyVault.CertificatePath)
|
||||
assert.Equal(t, "existing-auth", ars.Spec.GitHubConfigSecret)
|
||||
assert.Equal(t, "existing-auth", ars.GitHubConfigSecret(), "the selector passed to the vault resolver")
|
||||
|
||||
// A missing local certificate proves dispatch reached Azure without any network or credentials.
|
||||
resolver := secretresolver.New(fake.NewClientBuilder().Build(), nil)
|
||||
_, err = resolver.GetAppConfig(t.Context(), &ars)
|
||||
require.ErrorContains(t, err, "failed to create Azure Key Vault client")
|
||||
assert.ErrorContains(t, err, "cert path")
|
||||
assert.ErrorContains(t, err, "does not exist")
|
||||
assert.NotContains(t, err.Error(), "unknown vault type")
|
||||
|
||||
validCertificate, err := filepath.Abs("../../../vault/azurekeyvault/testdata/server.crt")
|
||||
require.NoError(t, err)
|
||||
valid := runnerSetContract(t, fmt.Sprintf(`
|
||||
secretResolution:
|
||||
type: azureKeyVault
|
||||
azureKeyVault:
|
||||
url: https://example.vault.azure.net
|
||||
tenantId: tenant
|
||||
clientId: client
|
||||
certificatePath: %q
|
||||
`, validCertificate), nil)
|
||||
config := valid.Spec.VaultConfig.AzureKeyVault
|
||||
azureConfig := azurekeyvault.Config{
|
||||
URL: config.URL, TenantID: config.TenantID, ClientID: config.ClientID, CertificatePath: config.CertificatePath,
|
||||
}
|
||||
assert.NoError(t, azureConfig.Validate())
|
||||
|
||||
for _, discriminator := range []string{"azure_key_vault", "hashicorpVault", "", "true"} {
|
||||
t.Run("unsupported/"+discriminator, func(t *testing.T) {
|
||||
_, err := renderContract(t, fmt.Sprintf("secretResolution:\n type: %q\n", discriminator), nil)
|
||||
require.ErrorContains(t, err, "Unsupported keyVault type")
|
||||
})
|
||||
}
|
||||
for _, discriminator := range []string{"true", "42", "[]", "{}"} {
|
||||
t.Run("invalid-type/"+discriminator, func(t *testing.T) {
|
||||
_, err := renderContract(t, "secretResolution:\n type: "+discriminator+"\n", nil)
|
||||
require.ErrorContains(t, err, "Unsupported keyVault type")
|
||||
})
|
||||
}
|
||||
_, err = renderContract(t, "secretResolution:\n type: azureKeyVault\n azureKeyVault:\n secretKey: ignored\n", nil)
|
||||
require.ErrorContains(t, err, "secretResolution.azureKeyVault.secretKey is not supported; use auth.secretName")
|
||||
assert.Nil(t, runnerSetContract(t, "", nil).Spec.VaultConfig)
|
||||
}
|
||||
|
||||
const customizedRunner = `
|
||||
runner:
|
||||
container:
|
||||
name: ignored
|
||||
image: example.com/custom-runner:v1
|
||||
command: ["/custom/run"]
|
||||
args: ["--custom"]
|
||||
imagePullPolicy: Always
|
||||
stdin: false
|
||||
tty: false
|
||||
env:
|
||||
- {name: CUSTOM_MARKER, value: expected}
|
||||
resources:
|
||||
requests: {cpu: 250m, memory: 512Mi}
|
||||
limits: {cpu: "1", memory: 1Gi}
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 0
|
||||
privileged: false
|
||||
allowPrivilegeEscalation: false
|
||||
volumeMounts:
|
||||
- {name: custom, mountPath: /custom, readOnly: false}
|
||||
pod:
|
||||
spec:
|
||||
volumes:
|
||||
- {name: custom, emptyDir: {}}
|
||||
`
|
||||
|
||||
func TestExperimentalRunnerCustomization(t *testing.T) {
|
||||
for _, mode := range []string{"", "dind", "kubernetes"} {
|
||||
for _, namespace := range []string{"runners", "custom-namespace"} {
|
||||
t.Run(mode+"/"+namespace, func(t *testing.T) {
|
||||
set := map[string]string{"runner.mode": mode, "namespaceOverride": namespace}
|
||||
ars := runnerSetContract(t, customizedRunner, set)
|
||||
assert.Equal(t, namespace, ars.Namespace)
|
||||
require.NotEmpty(t, ars.Spec.Template.Spec.Containers)
|
||||
c := ars.Spec.Template.Spec.Containers[0]
|
||||
assert.Equal(t, "runner", c.Name)
|
||||
assert.Equal(t, "example.com/custom-runner:v1", c.Image)
|
||||
assert.Equal(t, []string{"/custom/run"}, c.Command)
|
||||
assert.Equal(t, []string{"--custom"}, c.Args)
|
||||
assert.Equal(t, corev1.PullAlways, c.ImagePullPolicy)
|
||||
assert.Contains(t, c.Env, corev1.EnvVar{Name: "CUSTOM_MARKER", Value: "expected"})
|
||||
assert.Equal(t, "250m", c.Resources.Requests.Cpu().String())
|
||||
assert.Equal(t, "512Mi", c.Resources.Requests.Memory().String())
|
||||
assert.Equal(t, "1", c.Resources.Limits.Cpu().String())
|
||||
assert.Equal(t, "1Gi", c.Resources.Limits.Memory().String())
|
||||
require.NotNil(t, c.SecurityContext)
|
||||
assert.Equal(t, int64(1000), *c.SecurityContext.RunAsUser)
|
||||
assert.Equal(t, int64(0), *c.SecurityContext.RunAsGroup)
|
||||
assert.False(t, *c.SecurityContext.Privileged)
|
||||
assert.False(t, *c.SecurityContext.AllowPrivilegeEscalation)
|
||||
assert.Contains(t, c.VolumeMounts, corev1.VolumeMount{Name: "custom", MountPath: "/custom"})
|
||||
assertUniqueContainerLists(t, c)
|
||||
|
||||
defaults := runnerSetContract(t, "", set).Spec.Template.Spec.Containers[0]
|
||||
assert.Equal(t, "ghcr.io/actions/actions-runner:latest", defaults.Image)
|
||||
assert.Equal(t, []string{"/home/runner/run.sh"}, defaults.Command)
|
||||
for _, env := range defaults.Env {
|
||||
assert.Contains(t, c.Env, env)
|
||||
}
|
||||
for _, mount := range defaults.VolumeMounts {
|
||||
assert.Contains(t, c.VolumeMounts, mount)
|
||||
}
|
||||
if mode == "dind" {
|
||||
assert.Contains(t, c.Env, corev1.EnvVar{Name: "DOCKER_HOST", Value: "unix:///var/run/docker.sock"})
|
||||
assert.Equal(t, "dind", ars.Spec.Template.Spec.InitContainers[1].Name)
|
||||
assert.Equal(t, corev1.ContainerRestartPolicyAlways, *ars.Spec.Template.Spec.InitContainers[1].RestartPolicy)
|
||||
}
|
||||
if mode == "kubernetes" {
|
||||
assert.Contains(t, c.Env, corev1.EnvVar{Name: "ACTIONS_RUNNER_CONTAINER_HOOKS", Value: "/home/runner/k8s/index.js"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertUniqueContainerLists(t *testing.T, c corev1.Container) {
|
||||
t.Helper()
|
||||
envs, mounts := map[string]bool{}, map[string]bool{}
|
||||
for _, env := range c.Env {
|
||||
assert.False(t, envs[env.Name], "duplicate env %s", env.Name)
|
||||
envs[env.Name] = true
|
||||
assert.False(t, env.Value != "" && env.ValueFrom != nil, "value and valueFrom on %s", env.Name)
|
||||
}
|
||||
for _, mount := range c.VolumeMounts {
|
||||
assert.False(t, mounts[mount.MountPath], "duplicate mountPath %s", mount.MountPath)
|
||||
mounts[mount.MountPath] = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalRunnerOverrides(t *testing.T) {
|
||||
for _, mode := range []string{"", "dind", "kubernetes"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
ars := runnerSetContract(t, `
|
||||
githubServerTLS:
|
||||
runnerMountPath: /certs
|
||||
certificateFrom:
|
||||
configMapKeyRef: {name: ca, key: ca.crt}
|
||||
runner:
|
||||
kubernetesMode:
|
||||
extensionRef: hooks
|
||||
requireJobContainer: false
|
||||
dind:
|
||||
waitForDockerInSeconds: 0
|
||||
container:
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
valueFrom:
|
||||
secretKeyRef: {name: docker-host, key: address}
|
||||
- {name: ACTIONS_RUNNER_POD_NAME, value: custom-pod}
|
||||
- {name: ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE, value: /custom/hooks.yaml}
|
||||
- {name: NODE_EXTRA_CA_CERTS, value: /custom/ca.crt}
|
||||
- {name: RUNNER_UPDATE_CA_CERTS, value: "0"}
|
||||
volumeMounts:
|
||||
- {name: work, mountPath: /home/runner/_work, readOnly: false}
|
||||
- {name: hook-extension, mountPath: /home/runner/k8s/hook-template.yaml, readOnly: false}
|
||||
- {name: github-server-tls-cert, mountPath: /custom/certs, readOnly: false}
|
||||
`, map[string]string{"runner.mode": mode})
|
||||
c := ars.Spec.Template.Spec.Containers[0]
|
||||
assertUniqueContainerLists(t, c)
|
||||
envs := map[string]corev1.EnvVar{}
|
||||
for _, env := range c.Env {
|
||||
envs[env.Name] = env
|
||||
}
|
||||
assert.Equal(t, "", envs["DOCKER_HOST"].Value)
|
||||
require.NotNil(t, envs["DOCKER_HOST"].ValueFrom)
|
||||
assert.Equal(t, "docker-host", envs["DOCKER_HOST"].ValueFrom.SecretKeyRef.Name)
|
||||
assert.Equal(t, "custom-pod", envs["ACTIONS_RUNNER_POD_NAME"].Value)
|
||||
assert.Nil(t, envs["ACTIONS_RUNNER_POD_NAME"].ValueFrom)
|
||||
assert.Equal(t, "/custom/hooks.yaml", envs["ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE"].Value)
|
||||
assert.Equal(t, "/custom/ca.crt", envs["NODE_EXTRA_CA_CERTS"].Value)
|
||||
assert.Equal(t, "0", envs["RUNNER_UPDATE_CA_CERTS"].Value)
|
||||
assert.Contains(t, c.VolumeMounts, corev1.VolumeMount{Name: "hook-extension", MountPath: "/home/runner/k8s/hook-template.yaml"})
|
||||
assert.Contains(t, c.VolumeMounts, corev1.VolumeMount{Name: "github-server-tls-cert", MountPath: "/custom/certs"})
|
||||
if mode == "dind" {
|
||||
assert.Equal(t, "0", envs["RUNNER_WAIT_FOR_DOCKER_IN_SECONDS"].Value)
|
||||
assert.Contains(t, c.VolumeMounts, corev1.VolumeMount{Name: "dind-sock", MountPath: "/var/run"})
|
||||
}
|
||||
if mode == "kubernetes" {
|
||||
assert.Equal(t, "false", envs["ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER"].Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalRunnerRestartPolicy(t *testing.T) {
|
||||
for _, mode := range []string{"", "dind", "kubernetes"} {
|
||||
for _, policy := range []string{"", "Never", "OnFailure", "Always"} {
|
||||
t.Run(mode+"/"+policy, func(t *testing.T) {
|
||||
set := map[string]string{"runner.mode": mode}
|
||||
want := corev1.RestartPolicyNever
|
||||
if policy != "" {
|
||||
set["runner.pod.spec.restartPolicy"] = policy
|
||||
want = corev1.RestartPolicy(policy)
|
||||
}
|
||||
ars := runnerSetContract(t, "", set)
|
||||
assert.Equal(t, want, ars.Spec.Template.Spec.RestartPolicy)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalRunnerIncludesDoNotMutateValues(t *testing.T) {
|
||||
chart := filepath.Join(t.TempDir(), "chart")
|
||||
require.NoError(t, os.CopyFS(chart, os.DirFS("..")))
|
||||
probe := `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: probe
|
||||
data:
|
||||
{{- $before := toJson .Values }}
|
||||
{{- $first := include "runner-mode-dind.runner-container" . }}
|
||||
{{- $second := include "runner-mode-kubernetes.runner-container" . }}
|
||||
unchanged: {{ eq $before (toJson .Values) | quote }}
|
||||
repeatable: {{ eq $first (include "runner-mode-dind.runner-container" .) | quote }}
|
||||
kubernetesRepeatable: {{ eq $second (include "runner-mode-kubernetes.runner-container" .) | quote }}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(chart, "templates/probe.yaml"), []byte(probe), 0600))
|
||||
values := filepath.Join(t.TempDir(), "values.yaml")
|
||||
require.NoError(t, os.WriteFile(values, []byte(contractValues+customizedRunner), 0600))
|
||||
output := helm.RenderTemplateContext(t, t.Context(), &helm.Options{
|
||||
Logger: logger.Discard, ValuesFiles: []string{values},
|
||||
}, chart, "contract", []string{"templates/probe.yaml"})
|
||||
var probeConfig corev1.ConfigMap
|
||||
require.NoError(t, yaml.UnmarshalStrict([]byte(output), &probeConfig))
|
||||
for key, value := range probeConfig.Data {
|
||||
assert.Equal(t, "true", value, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalRunnerRejectsMalformedLists(t *testing.T) {
|
||||
for _, mode := range []string{"", "dind", "kubernetes"} {
|
||||
for _, field := range []string{"env", "volumeMounts"} {
|
||||
for _, value := range []string{"false", "0", `"invalid"`, "{}"} {
|
||||
t.Run(strings.Join([]string{mode, field, value}, "/"), func(t *testing.T) {
|
||||
_, err := renderContract(t, fmt.Sprintf("runner:\n container:\n %s: %s\n", field, value), map[string]string{"runner.mode": mode})
|
||||
require.ErrorContains(t, err, "runner.container."+field+" must be a list")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalRunnerLegacyEnvPrecedence(t *testing.T) {
|
||||
for _, mode := range []string{"dind", "kubernetes"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
ars := runnerSetContract(t, `
|
||||
runner:
|
||||
env:
|
||||
- {name: LEGACY_ONLY, value: retained}
|
||||
- {name: OVERRIDE, value: old}
|
||||
- name: DOCKER_HOST
|
||||
valueFrom:
|
||||
secretKeyRef: {name: docker-host, key: address}
|
||||
container:
|
||||
env:
|
||||
- {name: OVERRIDE, value: new}
|
||||
- {name: DOCKER_HOST, value: "unix:///custom/docker.sock"}
|
||||
`, map[string]string{"runner.mode": mode})
|
||||
c := ars.Spec.Template.Spec.Containers[0]
|
||||
assertUniqueContainerLists(t, c)
|
||||
assert.Contains(t, c.Env, corev1.EnvVar{Name: "LEGACY_ONLY", Value: "retained"})
|
||||
assert.Contains(t, c.Env, corev1.EnvVar{Name: "OVERRIDE", Value: "new"})
|
||||
assert.Contains(t, c.Env, corev1.EnvVar{Name: "DOCKER_HOST", Value: "unix:///custom/docker.sock"})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalRunnerRejectsDuplicateIdentities(t *testing.T) {
|
||||
for _, mode := range []string{"", "dind", "kubernetes"} {
|
||||
for field, list := range map[string]string{
|
||||
"env": "[{name: DUP, value: first}, {name: DUP, value: second}]",
|
||||
"volumeMounts": "[{name: a, mountPath: /same}, {name: b, mountPath: /same}]",
|
||||
} {
|
||||
t.Run(mode+"/"+field, func(t *testing.T) {
|
||||
_, err := renderContract(t, "runner:\n container:\n "+field+": "+list+"\n", map[string]string{"runner.mode": mode})
|
||||
require.ErrorContains(t, err, "runner.container."+field+" contains duplicate")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ secretResolution:
|
||||
# Name of the secret resolver to use.
|
||||
# Available values:
|
||||
# - "kubernetes" - use Kubernetes secrets
|
||||
# - "azureKeyVault" - use Azure Key Vault
|
||||
# - "azureKeyVault" - use Azure Key Vault (rendered as the API type "azure_key_vault")
|
||||
type: "kubernetes"
|
||||
## Proxy settings when type is NOT "kubernetes"
|
||||
# proxy:
|
||||
@@ -55,12 +55,15 @@ secretResolution:
|
||||
# - example.com
|
||||
# - example.org
|
||||
|
||||
## Configuration for Azure Key Vault integration
|
||||
## Configuration for Azure Key Vault integration.
|
||||
## auth.secretName selects the Azure secret containing the GitHub app/token configuration.
|
||||
## azureKeyVault.secretKey is not supported; it was never used by the API/resolver
|
||||
## and is now rejected rather than silently ignored. Set auth.secretName explicitly.
|
||||
# azureKeyVault:
|
||||
# url: ""
|
||||
# client_id: ""
|
||||
# tenant_id: ""
|
||||
# certificate_path: ""
|
||||
# clientId: ""
|
||||
# tenantId: ""
|
||||
# certificatePath: ""
|
||||
|
||||
## Proxy can be used to define proxy settings that will be used by the
|
||||
## controller, the listener and the runner of this scale set.
|
||||
@@ -226,6 +229,7 @@ runner:
|
||||
# - spec.containers: appended after the generated "runner" container (name "runner" is reserved)
|
||||
# - spec.initContainers: appended after any generated initContainers (e.g. dind mode)
|
||||
# - spec.volumes: appended after generated volumes
|
||||
# - spec.restartPolicy: defaults to Never; an explicit Kubernetes restart policy is preserved
|
||||
#
|
||||
# Note: serviceAccountName is managed by the chart and cannot be overridden via runner.pod.spec.
|
||||
spec:
|
||||
@@ -234,7 +238,15 @@ runner:
|
||||
initContainers: []
|
||||
volumes: []
|
||||
|
||||
# container field is applied to the container named "runner". You cannot override the name of the runner container
|
||||
# Applied to the container named "runner" in every mode; its name cannot be overridden.
|
||||
# image and command use the defaults below when omitted. Other container fields
|
||||
# (e.g. resources, securityContext, args) are passed through, including false/zero values.
|
||||
# env entries replace mode defaults by name; volumeMounts replace them by mountPath.
|
||||
# Replacements are whole entries, not merged maps. Unspecified mode defaults remain.
|
||||
# TLS defaults also respect an existing mount named github-server-tls-cert.
|
||||
# Duplicate env names or mountPaths within a list are rejected.
|
||||
# Overrides of Docker/hook env or mounts must remain compatible with the selected mode.
|
||||
# In dind/kubernetes mode, the older runner.env is still read, but container.env takes precedence.
|
||||
container:
|
||||
image: "ghcr.io/actions/actions-runner:latest"
|
||||
command: ["/home/runner/run.sh"]
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package actionsgithubcom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func renderAdmissionChart(ctx context.Context, chart, template, namespace, values string) []byte {
|
||||
GinkgoHelper()
|
||||
cmd := exec.CommandContext(ctx, "helm", "template", "chart-contract",
|
||||
filepath.Join("../../charts", chart), "--namespace", namespace,
|
||||
"--show-only", "templates/"+template, "--values", "-")
|
||||
cmd.Stdin = strings.NewReader(values)
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).NotTo(HaveOccurred(), string(output))
|
||||
return output
|
||||
}
|
||||
|
||||
var _ = Describe("Experimental chart contracts", func() {
|
||||
var namespace string
|
||||
BeforeEach(func(ctx SpecContext) {
|
||||
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "chart-contract-"}}
|
||||
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
|
||||
namespace = ns.Name
|
||||
DeferCleanup(func(ctx SpecContext) {
|
||||
Expect(k8sClient.Delete(ctx, ns)).To(Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
for _, mode := range []string{"", "dind", "kubernetes"} {
|
||||
for _, policy := range []string{"", "Never", "OnFailure", "Always"} {
|
||||
It(fmt.Sprintf("admits runner mode %q restartPolicy %q through ResourceBuilder", mode, policy), func(ctx SpecContext) {
|
||||
values := fmt.Sprintf(`
|
||||
auth:
|
||||
url: https://github.com/example
|
||||
secretName: existing-auth
|
||||
controllerServiceAccount:
|
||||
name: controller
|
||||
namespace: arc-system
|
||||
runner:
|
||||
mode: %q
|
||||
container:
|
||||
env:
|
||||
- {name: CUSTOM_MARKER, value: expected}
|
||||
- {name: ACTIONS_RUNNER_POD_NAME, value: custom-pod}
|
||||
- name: DOCKER_HOST
|
||||
valueFrom:
|
||||
secretKeyRef: {name: docker-host, key: address}
|
||||
resources:
|
||||
requests: {cpu: 250m, memory: 512Mi}
|
||||
securityContext: {runAsUser: 1000, runAsGroup: 0, allowPrivilegeEscalation: false}
|
||||
volumeMounts:
|
||||
- {name: custom, mountPath: /custom}
|
||||
pod:
|
||||
spec:
|
||||
volumes:
|
||||
- {name: custom, emptyDir: {}}
|
||||
`, mode)
|
||||
want := corev1.RestartPolicyNever
|
||||
if policy != "" {
|
||||
values += " restartPolicy: " + policy + "\n"
|
||||
want = corev1.RestartPolicy(policy)
|
||||
}
|
||||
var ars actionsv1alpha1.AutoscalingRunnerSet
|
||||
Expect(yaml.UnmarshalStrict(renderAdmissionChart(ctx, "gha-runner-scale-set-experimental", "autoscalingrunnserset.yaml", namespace, values), &ars)).To(Succeed())
|
||||
Expect(k8sClient.Create(ctx, &ars)).To(Succeed())
|
||||
admitted := &actionsv1alpha1.AutoscalingRunnerSet{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&ars), admitted)).To(Succeed())
|
||||
Expect(admitted.Spec.ListenerConfig).To(Equal(ars.Spec.ListenerConfig))
|
||||
|
||||
cache := NewResourceCache()
|
||||
builder := ResourceBuilder{ResourceCache: &cache}
|
||||
ars.Annotations[runnerScaleSetIDAnnotationKey] = "1"
|
||||
ers, err := builder.newEphemeralRunnerSet(&ars)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient.Create(ctx, ers)).To(Succeed())
|
||||
runner, err := builder.newEphemeralRunner(ers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient.Create(ctx, runner)).To(Succeed())
|
||||
pod, err := builder.newEphemeralRunnerPod(runner, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "jit"}})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient.Create(ctx, &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: pod.Spec.ServiceAccountName, Namespace: namespace,
|
||||
}})).To(Succeed())
|
||||
Expect(k8sClient.Create(ctx, pod)).To(Succeed())
|
||||
var live corev1.Pod
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(pod), &live)).To(Succeed())
|
||||
Expect(live.Spec.RestartPolicy).To(Equal(want))
|
||||
Expect(ars.Spec.Template.Spec.RestartPolicy).To(Equal(want))
|
||||
Expect(live.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{Name: "CUSTOM_MARKER", Value: "expected"}))
|
||||
Expect(live.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{Name: "ACTIONS_RUNNER_POD_NAME", Value: "custom-pod"}))
|
||||
Expect(live.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{
|
||||
Name: "DOCKER_HOST",
|
||||
ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: "docker-host"}, Key: "address",
|
||||
}},
|
||||
}))
|
||||
Expect(live.Spec.Containers[0].Resources.Requests.Cpu().String()).To(Equal("250m"))
|
||||
Expect(live.Spec.Containers[0].VolumeMounts).To(ContainElement(corev1.VolumeMount{Name: "custom", MountPath: "/custom"}))
|
||||
if mode == "dind" {
|
||||
Expect(*live.Spec.InitContainers[1].RestartPolicy).To(Equal(corev1.ContainerRestartPolicyAlways))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, chart := range []string{"gha-runner-scale-set-controller", "gha-runner-scale-set-controller-experimental"} {
|
||||
for _, address := range []string{"", ":8080", "127.0.0.1:8080", "[::]:8080", "localhost:8080", ":65535", "0"} {
|
||||
It(fmt.Sprintf("admits %s metrics %q", chart, address), func(ctx SpecContext) {
|
||||
values := ""
|
||||
if address != "" {
|
||||
values = fmt.Sprintf("metrics:\n controllerManagerAddr: %q\n listenerAddr: ':9090'\n listenerEndpoint: /metrics\n", address)
|
||||
if strings.HasSuffix(chart, "-experimental") {
|
||||
values = "controller:\n " + strings.ReplaceAll(strings.TrimSuffix(values, "\n"), "\n", "\n ") + "\n"
|
||||
}
|
||||
}
|
||||
var deployment appsv1.Deployment
|
||||
Expect(yaml.UnmarshalStrict(renderAdmissionChart(ctx, chart, "deployment.yaml", namespace, values), &deployment)).To(Succeed())
|
||||
Expect(k8sClient.Create(ctx, &deployment)).To(Succeed())
|
||||
var live appsv1.Deployment
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&deployment), &live)).To(Succeed())
|
||||
if address == "" || address == "0" {
|
||||
Expect(live.Spec.Template.Spec.Containers[0].Ports).To(BeEmpty())
|
||||
Expect(live.Spec.Template.Spec.Containers[0].Args).To(ContainElement("--metrics-addr=0"))
|
||||
} else {
|
||||
Expect(live.Spec.Template.Spec.Containers[0].Ports).To(HaveLen(1))
|
||||
Expect(live.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort).To(BeNumerically(">", 0))
|
||||
Expect(live.Spec.Template.Spec.Containers[0].Args).To(ContainElement("--metrics-addr=" + address))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user