├── .github └── workflows │ ├── pull_request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api └── v1 │ ├── groupversion_info.go │ ├── runner_types.go │ └── zz_generated.deepcopy.go ├── bin └── runner.go ├── examples ├── kustomization.yaml ├── kustomizeconfig.yaml └── runner.yaml ├── go.mod ├── go.sum ├── internal └── controllers │ └── runner_controller.go ├── main.go ├── manifests ├── NODEPORT ├── cluster_role.yaml ├── cluster_role_binding.yaml ├── crd │ └── github-actions-runner.kaidotdev.github.io_runners.yaml ├── deployment.yaml ├── kustomization.yaml ├── kustomizeconfig.yaml ├── pod_disruption_budget.yaml ├── role.yaml ├── role_binding.yaml ├── service.yaml ├── service_account.yaml └── stateful_set.yaml ├── patches ├── deployment.yaml ├── kustomization.yaml └── namespace.yaml └── skaffold.yaml /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PullRequest 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | lint: 8 | name: Lint 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Set up Go 1.22 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: 1.22 15 | id: go 16 | - name: Check out code 17 | uses: actions/checkout@v4 18 | - name: Lint 19 | run: make lint 20 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | on: [push] 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - name: Set up Go 1.22 9 | uses: actions/setup-go@v4 10 | with: 11 | go-version: 1.22 12 | id: go 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | - name: Cache go mod download 16 | uses: actions/cache@v3 17 | with: 18 | path: /home/runner/go/pkg/mod 19 | key: ${{ runner.os }}-gomod-${{ hashFiles(format('{0}{1}', github.workspace, '/go.sum')) }} 20 | restore-keys: | 21 | ${{ runner.os }}-gomod- 22 | - name: Cache go build 23 | uses: actions/cache@v3 24 | with: 25 | path: /home/runner/.cache/go-build 26 | key: ${{ runner.os }}-go-${{ hashFiles(format('{0}{1}', github.workspace, '/**/*.go')) }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | - name: Test 30 | run: go test ./... -race -bench . -benchmem -trimpath 31 | 32 | publish: 33 | name: Publish 34 | runs-on: ubuntu-22.04 35 | needs: [test] 36 | env: 37 | OWNER: kaidotdev 38 | REPOSITORY_NAME: github-actions-runner-controller 39 | IMAGE_NAME: github-actions-runner-controller 40 | steps: 41 | - name: Check out code 42 | uses: actions/checkout@v4 43 | - name: Cache docker build 44 | id: docker-cache 45 | uses: actions/cache@v3 46 | with: 47 | path: /home/runner/.cache/docker-build 48 | key: ${{ runner.os }}-docker-${{ hashFiles(format('{0}{1}', github.workspace, '/Dockerfile')) }}-${{ hashFiles(format('{0}{1}', github.workspace, '/go.sum')) }}-${{ hashFiles(format('{0}{1}', github.workspace, '/**/*.go')) }} 49 | restore-keys: | 50 | ${{ runner.os }}-docker- 51 | - uses: docker/setup-qemu-action@v3 52 | - uses: docker/setup-buildx-action@v3 53 | - name: Publish 54 | run: | 55 | IMAGE_PATH=ghcr.io/${OWNER}/${IMAGE_NAME} 56 | TAG=${GITHUB_REF##*/} 57 | opt='' 58 | [ -d /home/runner/.cache/docker-build ] && opt='--cache-from type=local,src=/home/runner/.cache/docker-build' 59 | docker login ghcr.io -u $OWNER -p ${{ secrets.GITHUB_TOKEN }} 60 | docker buildx build --output type=docker,name=$IMAGE_PATH:$TAG,push=false ${opt} --cache-to type=local,mode=max,dest=/home/runner/.cache/docker-build . 61 | docker push $IMAGE_PATH:$TAG 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | create: 4 | tags: 5 | - v*.*.* 6 | jobs: 7 | publish: 8 | name: Publish 9 | runs-on: ubuntu-22.04 10 | env: 11 | OWNER: kaidotdev 12 | REPOSITORY_NAME: github-actions-runner-controller 13 | IMAGE_NAME: github-actions-runner-controller 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: 1.22 21 | - name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v6 23 | with: 24 | version: latest 25 | args: release --clean 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | - name: Cache docker build 29 | id: cache 30 | uses: actions/cache@v3 31 | with: 32 | path: /home/runner/.cache/docker-build 33 | key: ${{ runner.os }}-docker-${{ hashFiles(format('{0}{1}', github.workspace, '/Dockerfile')) }}-${{ hashFiles(format('{0}{1}', github.workspace, '/go.sum')) }}-${{ hashFiles(format('{0}{1}', github.workspace, '/**/*.go')) }} 34 | restore-keys: | 35 | ${{ runner.os }}-docker- 36 | - uses: docker/setup-qemu-action@v3 37 | - uses: docker/setup-buildx-action@v3 38 | - name: Publish 39 | run: | 40 | IMAGE_PATH=ghcr.io/${OWNER}/${IMAGE_NAME} 41 | TAG=${GITHUB_REF##*/} 42 | opt='' 43 | if [ -d /home/runner/.cache/docker-build ]; then 44 | opt='--cache-from type=local,src=/home/runner/.cache/docker-build' 45 | else 46 | opt='--cache-from type=registry,ref=$IMAGE_PATH:master' 47 | fi 48 | docker login ghcr.io -u $OWNER -p ${{ secrets.GITHUB_TOKEN }} 49 | docker buildx build --output type=docker,name=$IMAGE_PATH:$TAG,push=false ${opt} --cache-to type=local,mode=max,dest=/home/runner/.cache/docker-build . 50 | docker push $IMAGE_PATH:$TAG 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaidotdev/github-actions-runner-controller/bfa37f4d42932ef583c3039a125a43501258dcbb/.gitignore -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: bin/runner.go 3 | binary: runner 4 | env: 5 | - CGO_ENABLED=0 6 | goos: 7 | - linux 8 | goarch: 9 | - amd64 10 | ldflags: 11 | - -s -w 12 | 13 | archives: 14 | - format: binary 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.4 2 | 3 | FROM golang:1.22-bullseye AS builder 4 | 5 | ENV CGO_ENABLED=0 6 | 7 | WORKDIR /opt/builder 8 | 9 | COPY go.mod go.sum /opt/builder/ 10 | RUN --mount=type=cache,target=/go/pkg/mod go mod download 11 | 12 | COPY main.go /opt/builder/main.go 13 | COPY api /opt/builder/api 14 | COPY internal /opt/builder/internal 15 | 16 | ARG LD_FLAGS="-s -w" 17 | RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go build -trimpath -o /usr/local/bin/main -ldflags="${LD_FLAGS}" /opt/builder/main.go 18 | 19 | FROM gcr.io/distroless/static:nonroot 20 | COPY --link --from=builder /usr/local/bin/main /usr/local/bin/github-actions-runner-controller 21 | 22 | USER 65532 23 | 24 | ENTRYPOINT ["/usr/local/bin/github-actions-runner-controller"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kai Aihara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: gen 4 | gen: ## Generate from controller-gen 5 | @go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0 6 | @$(shell go env GOPATH)/bin/controller-gen paths="./..." object crd:crdVersions=v1 output:crd:artifacts:config=manifests/crd 7 | 8 | .PHONY: test 9 | test: ## Test 10 | @go test ./... -race -bench . -benchmem -trimpath -cover 11 | 12 | .PHONY: lint 13 | lint: ## Lint 14 | @go install golang.org/x/tools/cmd/goimports@latest 15 | @for d in $(shell go list -f {{.Dir}} ./...); do $(shell go env GOPATH)/bin/goimports -w $$d/*.go; done 16 | 17 | .PHONY: dev 18 | dev: ## Run skaffold 19 | @skaffold dev 20 | 21 | .PHONY: help 22 | help: ## Show help 23 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHubActionsRunnerController 2 | 3 | GitHubActionsRunnerController is Kubernetes Custom Controller that runs self-hosted runner of GitHub Actions. 4 | 5 | ## Installation 6 | 7 | ```shell 8 | $ kubectl apply -k manifests 9 | ``` 10 | 11 | ## Usage 12 | 13 | Applying an `examples` manifest runs self-hosted runner of GitHub Actions. 14 | 15 | **`TOKEN` must have `administration` permission of a target repository to use `POST /repos/:owner/:repo/actions/runners/registration-token` endpoint.** 16 | 17 | ```shell 18 | $ echo -n "" > examples/TOKEN 19 | $ kubectl apply -k examples 20 | ``` 21 | 22 | ![runners](https://github.com/kaidotdev/github-actions-runner-controller/wiki/images/runners.png) 23 | 24 | The runner is based on an image that defined at `Runner` manifest. 25 | Its image is rebuilt as an image for Runner using [GoogleContainerTools/kaniko](https://github.com/GoogleContainerTools/kaniko) by github-actions-runner-controller, and it is distributed via local docker registry. 26 | 27 | ```shell 28 | $ cat examples/runner.yaml 29 | apiVersion: github-actions-runner.kaidotdev.github.io/v1 30 | kind: Runner 31 | metadata: 32 | name: example 33 | spec: 34 | image: ubuntu:18.04 35 | repository: kaidotdev/github-actions-runner-controller 36 | tokenSecretKeyRef: 37 | name: credentials 38 | key: TOKEN 39 | 40 | # This shows the image is pulling from the local docker registry 41 | $ kubectl get pod -l app=example -o jsonpath='{$.items[*].metadata.name}: {$.items[*].spec.containers[0].image}' 42 | example-6dd7c8974c-4sgjv: 127.0.0.1:31994/f601e6d⏎ 43 | 44 | # This shows the image is based on ubuntu:18.04 45 | $ kubectl exec -it example-6dd7c8974c-4sgjv cat /etc/os-release 46 | NAME="Ubuntu" 47 | VERSION="18.04.4 LTS (Bionic Beaver)" 48 | ID=ubuntu 49 | ID_LIKE=debian 50 | PRETTY_NAME="Ubuntu 18.04.4 LTS" 51 | VERSION_ID="18.04" 52 | HOME_URL="https://www.ubuntu.com/" 53 | SUPPORT_URL="https://help.ubuntu.com/" 54 | BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" 55 | PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" 56 | VERSION_CODENAME=bionic 57 | UBUNTU_CODENAME=bionic 58 | ``` 59 | 60 | You can pass additional information to runner pod via `builderContainerSpec`, `runnerContainerSpec`, and `template`. 61 | 62 | ```yaml 63 | apiVersion: github-actions-runner.kaidotdev.github.io/v1 64 | kind: Runner 65 | metadata: 66 | name: example 67 | spec: 68 | image: ubuntu:18.04 69 | repository: kaidotdev/github-actions-runner-controller 70 | tokenSecretKeyRef: 71 | name: credentials 72 | key: TOKEN 73 | builderContainerSpec: 74 | resource: 75 | requests: 76 | cpu: 1000m 77 | runnerContainerSpec: 78 | env: 79 | - name: FOO 80 | valueFrom: 81 | fieldRef: 82 | fieldPath: metadata.name 83 | - name: BAR 84 | value: bar 85 | template: 86 | metadata: 87 | labels: 88 | version: v1 89 | annotations: 90 | # `--enable-runner-metrics` is required to scrape from prometheus 91 | prometheus.io/scrape: "true" 92 | prometheus.io/schema: "http" 93 | prometheus.io/port: "9090" 94 | prometheus.io/path: "/metrics" 95 | sidecar.istio.io/inject: "false" 96 | ``` 97 | 98 | Therefore, when combined with [DirectXMan12/k8s-prometheus-adapter](https://github.com/DirectXMan12/k8s-prometheus-adapter), it is possible to scale according to runner metrics using HPA. 99 | 100 | ```yaml 101 | - seriesQuery: 'github_actions_runs{status="queued"}' 102 | resources: 103 | overrides: 104 | namespace: 105 | resource: namespace 106 | pod: 107 | resource: pod 108 | name: 109 | matches: "^(.*)$" 110 | as: "${1}_queued" 111 | metricsQuery: <<.Series>>{<<.LabelMatchers>>} 112 | ``` 113 | 114 | ```yaml 115 | apiVersion: autoscaling/v2beta2 116 | kind: HorizontalPodAutoscaler 117 | metadata: 118 | name: example-runner 119 | spec: 120 | maxReplicas: 5 121 | minReplicas: 1 122 | scaleTargetRef: 123 | apiVersion: apps/v1 124 | kind: Deployment 125 | name: example 126 | metrics: 127 | - type: Pods 128 | pods: 129 | metric: 130 | name: github_actions_runs_queued 131 | target: 132 | type: AverageValue 133 | averageValue: 3 134 | ``` 135 | 136 | See CRD for other available fields and detailed descriptions: [github-actions-runner.kaidotdev.github.io_runners.yaml](https://github.com/kaidotdev/github-actions-runner-controller/blob/master/manifests/crd/github-actions-runner.kaidotdev.github.io_runners.yaml) 137 | 138 | ### GitHub Apps 139 | 140 | You can use GitHub Apps to authenticate the runner. 141 | 142 | ```sh 143 | kubectl create secret generic credentials --from-literal=github_app_id="" --from-literal=github_app_installation_id="" --from-file=github_app_private_key="" 144 | cat < k8s.io/client-go v0.29.3 79 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= 9 | github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 10 | github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= 11 | github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 12 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 13 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 14 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 15 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 18 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 19 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 20 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 21 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 22 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 23 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 24 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 25 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 26 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 27 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 28 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 29 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 30 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 31 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 32 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 33 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 34 | github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= 35 | github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= 36 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 38 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 39 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 40 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 41 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 42 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 43 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 44 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 47 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 48 | github.com/google/goexpect v0.0.0-20191001010744-5b6988669ffa h1:PMkmJA8ju9DjqAJjIzrBdrmhuuPsoNnNLYgKQBopWL0= 49 | github.com/google/goexpect v0.0.0-20191001010744-5b6988669ffa/go.mod h1:qtE5aAEkt0vOSA84DBh8aJsz6riL8ONfqfULY7lBjqc= 50 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 51 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 52 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 53 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= 54 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 55 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 56 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 57 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 58 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 59 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 60 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 61 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 62 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 63 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 64 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 65 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 66 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 67 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 68 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 69 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 70 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 71 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 72 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 73 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 76 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 77 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 78 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 79 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 80 | github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= 81 | github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= 82 | github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= 83 | github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 84 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 85 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 86 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 87 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 91 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 92 | github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= 93 | github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= 94 | github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= 95 | github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= 96 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= 97 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= 98 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 99 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 100 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 101 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 102 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 103 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 104 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 105 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 106 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 107 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 108 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 109 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 110 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 111 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 112 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 113 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 114 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 115 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 116 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 117 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 118 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 119 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 120 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 121 | golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw= 122 | golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 123 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 124 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 125 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 126 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 127 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 128 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 129 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 130 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 131 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 132 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 133 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 134 | golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= 135 | golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 136 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 148 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 149 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 150 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 151 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 152 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 153 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 154 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 155 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 156 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 157 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 158 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 159 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 160 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 161 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 162 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 163 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 164 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 165 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 166 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 167 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 168 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 169 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 171 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 172 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 173 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 174 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 175 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 176 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 177 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 178 | google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= 179 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 180 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 181 | google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= 182 | google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= 183 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 184 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 185 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 186 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 187 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 188 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 189 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 190 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 191 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 192 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 193 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 194 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 195 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 196 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 197 | k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= 198 | k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= 199 | k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI= 200 | k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc= 201 | k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= 202 | k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= 203 | k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= 204 | k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= 205 | k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo= 206 | k8s.io/component-base v0.29.3/go.mod h1:Yuj33XXjuOk2BAaHsIGHhCKZQAgYKhqIxIjIr2UXYio= 207 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 208 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 209 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 210 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 211 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= 212 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 213 | sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= 214 | sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= 215 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 216 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 217 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 218 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 219 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 220 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 221 | -------------------------------------------------------------------------------- /internal/controllers/runner_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/sha256" 7 | "crypto/x509" 8 | "encoding/json" 9 | "encoding/pem" 10 | "fmt" 11 | "net/http" 12 | "reflect" 13 | "strings" 14 | "time" 15 | 16 | garV1 "github-actions-runner-controller/api/v1" 17 | 18 | dockerref "github.com/docker/distribution/reference" 19 | "github.com/go-logr/logr" 20 | "github.com/golang-jwt/jwt/v5" 21 | "golang.org/x/xerrors" 22 | appsV1 "k8s.io/api/apps/v1" 23 | coreV1 "k8s.io/api/core/v1" 24 | v1 "k8s.io/api/core/v1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/api/resource" 27 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/util/intstr" 30 | "k8s.io/client-go/tools/record" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | "sigs.k8s.io/controller-runtime/pkg/controller" 34 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 35 | "sigs.k8s.io/controller-runtime/pkg/predicate" 36 | ) 37 | 38 | const ( 39 | ownerKey = ".metadata.controller" 40 | optimisticLockErrorMsg = "the object has been modified; please apply your changes to the latest version and try again" 41 | expiresAtAnnotation = "github-actions-runner.kaidotio.github.io/expiresAt" 42 | ) 43 | 44 | type RunnerReconciler struct { 45 | client.Client 46 | Log logr.Logger 47 | Scheme *runtime.Scheme 48 | Recorder record.EventRecorder 49 | PushRegistryHost string 50 | PullRegistryHost string 51 | EnableRunnerMetrics bool 52 | ExporterImage string 53 | GitHubAppClientId string 54 | GitHubAppInstallationId string 55 | GitHubAppPrivateKey string 56 | KanikoImage string 57 | BinaryVersion string 58 | RunnerVersion string 59 | Disableupdate bool 60 | } 61 | 62 | func (r *RunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 63 | var requeueAfter time.Duration 64 | 65 | runner := &garV1.Runner{} 66 | logger := r.Log.WithValues("runner", req.NamespacedName) 67 | if err := r.Get(ctx, req.NamespacedName, runner); err != nil { 68 | if apierrors.IsNotFound(err) { 69 | return ctrl.Result{}, nil 70 | } 71 | return ctrl.Result{}, err 72 | } 73 | 74 | if err := r.cleanupOwnedResources(ctx, runner); err != nil { 75 | return ctrl.Result{}, err 76 | } 77 | 78 | if runner.Spec.TokenSecretKeyRef == nil && r.GitHubAppClientId != "" && r.GitHubAppInstallationId != "" && r.GitHubAppPrivateKey != "" { 79 | var tokenSecret v1.Secret 80 | if err := r.Client.Get( 81 | ctx, 82 | client.ObjectKey{ 83 | Name: req.Name, 84 | Namespace: req.Namespace, 85 | }, 86 | &tokenSecret, 87 | ); apierrors.IsNotFound(err) { 88 | tokenSecret, err := r.createTokenSecret(runner) 89 | if err != nil { 90 | return ctrl.Result{}, err 91 | } 92 | if err := controllerutil.SetControllerReference(runner, tokenSecret, r.Scheme); err != nil { 93 | return ctrl.Result{}, err 94 | } 95 | if err := r.Create(ctx, tokenSecret); err != nil { 96 | return ctrl.Result{}, err 97 | } 98 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulCreated", "Created token secret: %q", tokenSecret.Name) 99 | logger.V(1).Info("create", "secret", tokenSecret) 100 | 101 | expire, err := time.Parse(time.RFC3339, tokenSecret.Annotations[expiresAtAnnotation]) 102 | if err != nil { 103 | return ctrl.Result{}, err 104 | } 105 | requeueAfter = expire.Sub(time.Now()) - time.Minute 106 | } else if err != nil { 107 | return ctrl.Result{}, err 108 | } else { 109 | expectedTokenSecret, err := r.createTokenSecret(runner) 110 | if err != nil { 111 | return ctrl.Result{}, err 112 | } 113 | if !reflect.DeepEqual(tokenSecret.Data, expectedTokenSecret.Data) || 114 | !reflect.DeepEqual(tokenSecret.StringData, expectedTokenSecret.StringData) { 115 | tokenSecret.Annotations = expectedTokenSecret.Annotations 116 | tokenSecret.Data = expectedTokenSecret.Data 117 | tokenSecret.StringData = expectedTokenSecret.StringData 118 | 119 | if err := r.Update(ctx, &tokenSecret); err != nil { 120 | return ctrl.Result{}, err 121 | } 122 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulUpdated", "Updated token secret: %q", tokenSecret.Name) 123 | logger.V(1).Info("update", "secret", tokenSecret) 124 | 125 | expire, err := time.Parse(time.RFC3339, tokenSecret.Annotations[expiresAtAnnotation]) 126 | if err != nil { 127 | return ctrl.Result{}, err 128 | } 129 | requeueAfter = expire.Sub(time.Now()) - time.Minute 130 | } 131 | } 132 | 133 | runner.Spec.TokenSecretKeyRef = &coreV1.SecretKeySelector{ 134 | LocalObjectReference: coreV1.LocalObjectReference{ 135 | Name: req.Name, 136 | }, 137 | Key: "GITHUB_TOKEN", 138 | } 139 | } 140 | 141 | var workspaceConfigMap v1.ConfigMap 142 | if err := r.Client.Get( 143 | ctx, 144 | client.ObjectKey{ 145 | Name: req.Name + "-workspace", 146 | Namespace: req.Namespace, 147 | }, 148 | &workspaceConfigMap, 149 | ); apierrors.IsNotFound(err) { 150 | workspaceConfigMap = *r.buildWorkspaceConfigMap(runner) 151 | if err := controllerutil.SetControllerReference(runner, &workspaceConfigMap, r.Scheme); err != nil { 152 | return ctrl.Result{}, err 153 | } 154 | if err := r.Create(ctx, &workspaceConfigMap); err != nil { 155 | return ctrl.Result{}, err 156 | } 157 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulCreated", "Created workspace config map: %q", workspaceConfigMap.Name) 158 | logger.V(1).Info("create", "config map", workspaceConfigMap) 159 | } else if err != nil { 160 | return ctrl.Result{}, err 161 | } else { 162 | expectedWorkspaceConfigMap := r.buildWorkspaceConfigMap(runner) 163 | if !reflect.DeepEqual(workspaceConfigMap.Data, expectedWorkspaceConfigMap.Data) || 164 | !reflect.DeepEqual(workspaceConfigMap.BinaryData, expectedWorkspaceConfigMap.BinaryData) { 165 | workspaceConfigMap.Data = expectedWorkspaceConfigMap.Data 166 | workspaceConfigMap.BinaryData = expectedWorkspaceConfigMap.BinaryData 167 | 168 | if err := r.Update(ctx, &workspaceConfigMap); err != nil { 169 | return ctrl.Result{}, err 170 | } 171 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulUpdated", "Updated config map: %q", workspaceConfigMap.Name) 172 | logger.V(1).Info("update", "config map", workspaceConfigMap) 173 | } 174 | } 175 | 176 | var deployment appsV1.Deployment 177 | if err := r.Client.Get( 178 | ctx, 179 | client.ObjectKey{ 180 | Name: req.Name + "-runner", 181 | Namespace: req.Namespace, 182 | }, 183 | &deployment, 184 | ); apierrors.IsNotFound(err) { 185 | deployment = *r.buildDeployment(runner) 186 | if err := controllerutil.SetControllerReference(runner, &deployment, r.Scheme); err != nil { 187 | return ctrl.Result{}, err 188 | } 189 | if err := r.Create(ctx, &deployment); err != nil { 190 | return ctrl.Result{}, err 191 | } 192 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulCreated", "Created deployment: %q", deployment.Name) 193 | logger.V(1).Info("create", "deployment", deployment) 194 | } else if err != nil { 195 | return ctrl.Result{}, err 196 | } else { 197 | expectedDeployment := r.buildDeployment(runner) 198 | if !reflect.DeepEqual(deployment.Spec.Template, expectedDeployment.Spec.Template) { 199 | deployment.Spec.Template = expectedDeployment.Spec.Template 200 | 201 | if err := r.Update(ctx, &deployment); err != nil { 202 | if strings.Contains(err.Error(), optimisticLockErrorMsg) { 203 | return ctrl.Result{RequeueAfter: time.Second}, nil 204 | } 205 | return ctrl.Result{}, err 206 | } 207 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulUpdated", "Updated deployment: %q", deployment.Name) 208 | logger.V(1).Info("update", "deployment", deployment) 209 | } 210 | } 211 | 212 | return ctrl.Result{RequeueAfter: requeueAfter}, nil 213 | } 214 | 215 | func (r *RunnerReconciler) buildRepositoryName(runner *garV1.Runner) string { 216 | named, err := dockerref.ParseNormalizedNamed(runner.Spec.Image) 217 | if err != nil { 218 | return fmt.Sprintf("%x", sha256.Sum256([]byte(runner.Spec.Image+r.BinaryVersion+r.RunnerVersion)))[:7] 219 | } 220 | trimmed := dockerref.TrimNamed(named).String() 221 | return fmt.Sprintf("%x", sha256.Sum256([]byte(trimmed+r.BinaryVersion+r.RunnerVersion)))[:7] 222 | } 223 | 224 | func (r *RunnerReconciler) buildBuilderContainer(runner *garV1.Runner) v1.Container { 225 | if runner.Spec.BuilderContainerSpec.Resources.Limits == nil { 226 | runner.Spec.BuilderContainerSpec.Resources.Limits = make(v1.ResourceList) 227 | } 228 | if runner.Spec.BuilderContainerSpec.Resources.Limits.Memory().IsZero() { 229 | runner.Spec.BuilderContainerSpec.Resources.Limits[v1.ResourceMemory] = resource.MustParse("4Gi") 230 | } 231 | return v1.Container{ 232 | Name: "kaniko", 233 | Image: r.KanikoImage, 234 | ImagePullPolicy: v1.PullIfNotPresent, 235 | Args: []string{ 236 | "--dockerfile=Dockerfile", 237 | "--context=dir:///workspace", 238 | "--cache=true", 239 | "--compressed-caching=false", 240 | fmt.Sprintf("--destination=%s/%s", r.PushRegistryHost, r.buildRepositoryName(runner)), 241 | }, 242 | EnvFrom: runner.Spec.BuilderContainerSpec.EnvFrom, 243 | Env: runner.Spec.BuilderContainerSpec.Env, 244 | VolumeMounts: append([]v1.VolumeMount{ 245 | { 246 | Name: "workspace", 247 | MountPath: "/workspace/Dockerfile", 248 | SubPath: "Dockerfile", 249 | ReadOnly: true, 250 | }, 251 | }, runner.Spec.BuilderContainerSpec.VolumeMounts...), 252 | Resources: runner.Spec.BuilderContainerSpec.Resources, 253 | TerminationMessagePath: coreV1.TerminationMessagePathDefault, 254 | TerminationMessagePolicy: coreV1.TerminationMessageReadFile, 255 | } 256 | } 257 | 258 | func (r *RunnerReconciler) buildRunnerContainer(runner *garV1.Runner) v1.Container { 259 | args := []string{ 260 | "--without-install", 261 | "--repository=$(REPOSITORY)", 262 | "--hostname=$(HOSTNAME)", 263 | } 264 | env := runner.Spec.RunnerContainerSpec.Env 265 | envFrom := runner.Spec.RunnerContainerSpec.EnvFrom 266 | 267 | env = append(env, []coreV1.EnvVar{ 268 | { 269 | Name: "REPOSITORY", 270 | Value: runner.Spec.Repository, 271 | }, 272 | { 273 | Name: "HOSTNAME", 274 | ValueFrom: &coreV1.EnvVarSource{ 275 | FieldRef: &coreV1.ObjectFieldSelector{ 276 | APIVersion: "v1", 277 | FieldPath: "metadata.name", 278 | }, 279 | }, 280 | }, 281 | }...) 282 | 283 | if runner.Spec.TokenSecretKeyRef != nil { 284 | args = append(args, "--token=$(TOKEN)") 285 | env = append(env, coreV1.EnvVar{ 286 | Name: "TOKEN", 287 | ValueFrom: &coreV1.EnvVarSource{ 288 | SecretKeyRef: runner.Spec.TokenSecretKeyRef, 289 | }, 290 | }) 291 | } 292 | 293 | if runner.Spec.AppSecretRef != nil { 294 | args = append(args, []string{ 295 | "--github-app-id=$(github_app_id)", 296 | "--github-app-installation-id=$(github_app_installation_id)", 297 | "--github-app-private-key=$(github_app_private_key)", 298 | }...) 299 | envFrom = append(envFrom, coreV1.EnvFromSource{ 300 | SecretRef: runner.Spec.AppSecretRef, 301 | }) 302 | } 303 | 304 | c := v1.Container{ 305 | Name: "runner", 306 | SecurityContext: &v1.SecurityContext{ 307 | Privileged: func(b bool) *bool { return &b }(false), 308 | ReadOnlyRootFilesystem: func(b bool) *bool { return &b }(false), 309 | RunAsUser: func(i int64) *int64 { return &i }(60000), 310 | RunAsNonRoot: func(b bool) *bool { return &b }(true), 311 | SeccompProfile: &coreV1.SeccompProfile{ 312 | Type: coreV1.SeccompProfileTypeRuntimeDefault, 313 | }, 314 | }, 315 | Image: fmt.Sprintf("%s/%s", r.PullRegistryHost, r.buildRepositoryName(runner)), 316 | ImagePullPolicy: v1.PullAlways, 317 | Args: args, 318 | EnvFrom: envFrom, 319 | Env: env, 320 | Resources: runner.Spec.RunnerContainerSpec.Resources, 321 | VolumeMounts: runner.Spec.RunnerContainerSpec.VolumeMounts, 322 | TerminationMessagePath: coreV1.TerminationMessagePathDefault, 323 | TerminationMessagePolicy: coreV1.TerminationMessageReadFile, 324 | } 325 | if r.Disableupdate { 326 | c.Args = append(c.Args, "--disableupdate") 327 | } 328 | return c 329 | } 330 | 331 | func (r *RunnerReconciler) buildExporterContainer(runner *garV1.Runner) v1.Container { 332 | return v1.Container{ 333 | Name: "exporter", 334 | Image: r.ExporterImage, 335 | ImagePullPolicy: v1.PullAlways, 336 | Args: []string{ 337 | "server", 338 | "--api-address=0.0.0.0:8000", 339 | "--monitor-address=0.0.0.0:9090", 340 | "--repository=$(REPOSITORY)", 341 | "--token=$(TOKEN)", 342 | }, 343 | Env: []coreV1.EnvVar{ 344 | { 345 | Name: "REPOSITORY", 346 | Value: runner.Spec.Repository, 347 | }, 348 | { 349 | Name: "TOKEN", 350 | ValueFrom: &coreV1.EnvVarSource{ 351 | SecretKeyRef: runner.Spec.TokenSecretKeyRef, 352 | }, 353 | }, 354 | }, 355 | Ports: []coreV1.ContainerPort{ 356 | { 357 | ContainerPort: 9090, 358 | Protocol: "TCP", 359 | }, 360 | }, 361 | TerminationMessagePath: coreV1.TerminationMessagePathDefault, 362 | TerminationMessagePolicy: coreV1.TerminationMessageReadFile, 363 | } 364 | } 365 | 366 | func (r *RunnerReconciler) buildDeployment(runner *garV1.Runner) *appsV1.Deployment { 367 | containers := []v1.Container{ 368 | r.buildRunnerContainer(runner), 369 | } 370 | 371 | if r.EnableRunnerMetrics { 372 | containers = append(containers, r.buildExporterContainer(runner)) 373 | } 374 | 375 | appLabel := runner.Name + "-runner" 376 | labels := map[string]string{ 377 | "app": appLabel, 378 | } 379 | for k, v := range runner.Spec.Template.ObjectMeta.Labels { 380 | labels[k] = v 381 | } 382 | runner.Spec.Template.ObjectMeta.Labels = labels 383 | annotations := map[string]string{ 384 | "image": runner.Spec.Image, 385 | } 386 | for k, v := range runner.Spec.Template.ObjectMeta.Annotations { 387 | annotations[k] = v 388 | } 389 | runner.Spec.Template.ObjectMeta.Annotations = annotations 390 | return &appsV1.Deployment{ 391 | ObjectMeta: metaV1.ObjectMeta{ 392 | Name: runner.Name + "-runner", 393 | Namespace: runner.Namespace, 394 | }, 395 | Spec: appsV1.DeploymentSpec{ 396 | Selector: &metaV1.LabelSelector{ 397 | MatchLabels: map[string]string{ 398 | "app": appLabel, 399 | }, 400 | }, 401 | Replicas: func(i int32) *int32 { 402 | return &i 403 | }(1), 404 | Strategy: appsV1.DeploymentStrategy{ 405 | Type: appsV1.RollingUpdateDeploymentStrategyType, 406 | RollingUpdate: &appsV1.RollingUpdateDeployment{ 407 | MaxSurge: &intstr.IntOrString{ 408 | Type: intstr.String, 409 | StrVal: "25%", 410 | }, 411 | MaxUnavailable: &intstr.IntOrString{ 412 | Type: intstr.Int, 413 | IntVal: 1, 414 | }, 415 | }, 416 | }, 417 | Template: v1.PodTemplateSpec{ 418 | ObjectMeta: runner.Spec.Template.ObjectMeta, 419 | Spec: v1.PodSpec{ 420 | Affinity: &v1.Affinity{ 421 | PodAntiAffinity: &v1.PodAntiAffinity{ 422 | PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{ 423 | { 424 | Weight: 100, 425 | PodAffinityTerm: v1.PodAffinityTerm{ 426 | LabelSelector: &metaV1.LabelSelector{ 427 | MatchLabels: map[string]string{ 428 | "app": appLabel, 429 | }, 430 | }, 431 | TopologyKey: "kubernetes.io/hostname", 432 | }, 433 | }, 434 | }, 435 | }, 436 | }, 437 | InitContainers: []v1.Container{ 438 | r.buildBuilderContainer(runner), 439 | }, 440 | Containers: containers, 441 | Volumes: append([]v1.Volume{ 442 | { 443 | Name: "workspace", 444 | VolumeSource: v1.VolumeSource{ 445 | ConfigMap: &v1.ConfigMapVolumeSource{ 446 | LocalObjectReference: v1.LocalObjectReference{ 447 | Name: runner.Name + "-workspace", 448 | }, 449 | DefaultMode: func(i int32) *int32 { 450 | return &i 451 | }(420), 452 | }, 453 | }, 454 | }, 455 | }, runner.Spec.Template.Spec.Volumes...), 456 | RestartPolicy: coreV1.RestartPolicyAlways, 457 | TerminationGracePeriodSeconds: func(i int64) *int64 { 458 | return &i 459 | }(30), 460 | DNSPolicy: coreV1.DNSClusterFirst, 461 | SecurityContext: &coreV1.PodSecurityContext{ 462 | SeccompProfile: &coreV1.SeccompProfile{ 463 | Type: coreV1.SeccompProfileTypeRuntimeDefault, 464 | }, 465 | }, 466 | SchedulerName: coreV1.DefaultSchedulerName, 467 | }, 468 | }, 469 | }, 470 | } 471 | } 472 | 473 | func (r *RunnerReconciler) buildWorkspaceConfigMap(runner *garV1.Runner) *v1.ConfigMap { 474 | return &v1.ConfigMap{ 475 | ObjectMeta: metaV1.ObjectMeta{ 476 | Name: runner.Name + "-workspace", 477 | Namespace: runner.Namespace, 478 | }, 479 | Data: map[string]string{ 480 | "Dockerfile": fmt.Sprintf(` 481 | FROM %s 482 | USER root 483 | ENV DEBIAN_FRONTEND=noninteractive 484 | RUN (command -v apt && apt update && apt install -y ca-certificates iputils-ping tar sudo git) || \ 485 | (command -v apt-get && apt-get update && apt-get install -y --no-install-recommends ca-certificates iputils-ping tar sudo git) || \ 486 | (command -v dnf && dnf install -y ca-certificates iputils tar sudo git) || \ 487 | (command -v yum && yum install -y ca-certificates iputils tar sudo git) || \ 488 | (command -v zypper && zypper install -n ca-certificates iputils tar sudo git-core) || \ 489 | (echo "Unknown OS version" && exit 1) 490 | 491 | ADD https://github.com/kaidotdev/github-actions-runner-controller/releases/download/v%s/runner_%s_linux_amd64 /usr/local/bin/runner 492 | RUN chmod +x /usr/local/bin/runner 493 | 494 | RUN echo 'runner::60000:60000::/home/runner:/bin/sh' >> /etc/passwd 495 | RUN echo 'runner::60000:' >> /etc/group 496 | RUN mkdir -p /home/runner && chown -R runner:runner /home/runner 497 | 498 | RUN echo "runner:!:0:0:99999:7:::" >> /etc/shadow 499 | RUN echo "runner ALL=(ALL) NOPASSWD: ALL" | sudo EDITOR='tee -a' visudo 500 | 501 | WORKDIR /home/runner 502 | 503 | RUN /usr/local/bin/runner --only-install --runner-version %s 504 | 505 | USER 60000 506 | 507 | ENTRYPOINT ["/usr/local/bin/runner"] 508 | `, runner.Spec.Image, r.BinaryVersion, r.BinaryVersion, r.RunnerVersion), 509 | }, 510 | } 511 | } 512 | 513 | func (r *RunnerReconciler) createTokenSecret(runner *garV1.Runner) (*v1.Secret, error) { 514 | body := struct { 515 | Repositories []string `json:"repositories"` 516 | RepositoryIds []int `json:"repository_ids"` 517 | Permissions map[string]string `json:"permissions"` 518 | }{} 519 | 520 | accessToken := struct { 521 | Token string `json:"token"` 522 | ExpiresAt string `json:"expires_at"` 523 | }{} 524 | 525 | err, jwtToken := signJwt(r.GitHubAppPrivateKey, r.GitHubAppClientId) 526 | if err != nil { 527 | return nil, xerrors.Errorf("failed to sign jwt: %w", err) 528 | } 529 | 530 | body.Repositories = []string{strings.SplitN(runner.Spec.Repository, "/", 2)[1]} 531 | body.Permissions = map[string]string{ 532 | "actions": "read", 533 | "administration": "write", 534 | "metadata": "read", 535 | } 536 | b, err := json.Marshal(body) 537 | if err != nil { 538 | return nil, xerrors.Errorf("failed to marshal body: %w", err) 539 | } 540 | 541 | accessTokenRequest, err := http.NewRequest("POST", fmt.Sprintf("https://api.github.com/app/installations/%s/access_tokens", r.GitHubAppInstallationId), bytes.NewReader(b)) 542 | if err != nil { 543 | return nil, xerrors.Errorf("failed to create request: %w", err) 544 | } 545 | 546 | accessTokenRequest.Header.Set("Accept", "application/vnd.github+json") 547 | accessTokenRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *jwtToken)) 548 | accessTokenRequest.Header.Set("X-GitHub-Api-Version", "2022-11-28") 549 | accessTokenResponse, err := http.DefaultClient.Do(accessTokenRequest) 550 | if err != nil { 551 | return nil, xerrors.Errorf("failed to do request: %w", err) 552 | } 553 | defer func() { 554 | _ = accessTokenResponse.Body.Close() 555 | }() 556 | 557 | if accessTokenResponse.StatusCode != http.StatusCreated { 558 | return nil, xerrors.Errorf("failed to get access token: %d", accessTokenResponse.StatusCode) 559 | } 560 | 561 | if err := json.NewDecoder(accessTokenResponse.Body).Decode(&accessToken); err != nil { 562 | return nil, xerrors.Errorf("failed to decode access token: %w", err) 563 | } 564 | 565 | return &v1.Secret{ 566 | ObjectMeta: metaV1.ObjectMeta{ 567 | Name: runner.Name, 568 | Namespace: runner.Namespace, 569 | Annotations: map[string]string{ 570 | expiresAtAnnotation: accessToken.ExpiresAt, 571 | }, 572 | }, 573 | StringData: map[string]string{ 574 | "GITHUB_TOKEN": accessToken.Token, 575 | }, 576 | }, nil 577 | } 578 | 579 | func signJwt(privateKey string, clientId string) (error, *string) { 580 | block, _ := pem.Decode([]byte(privateKey)) 581 | if block == nil { 582 | return xerrors.New("failed to decode private key"), nil 583 | } 584 | 585 | rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 586 | if err != nil { 587 | return xerrors.Errorf("failed to parse private key: %w", err), nil 588 | } 589 | 590 | now := time.Now() 591 | claims := jwt.MapClaims{ 592 | "iat": now.Unix(), 593 | "exp": now.Add(time.Minute * 10).Unix(), 594 | "iss": clientId, 595 | } 596 | 597 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 598 | jwtToken, err := token.SignedString(rsaPrivateKey) 599 | if err != nil { 600 | return xerrors.Errorf("failed to sign token: %w", err), nil 601 | } 602 | return nil, &jwtToken 603 | } 604 | 605 | func (r *RunnerReconciler) cleanupOwnedResources(ctx context.Context, runner *garV1.Runner) error { 606 | var configMaps v1.ConfigMapList 607 | if err := r.List( 608 | ctx, 609 | &configMaps, 610 | client.InNamespace(runner.Namespace), 611 | client.MatchingFields{ownerKey: runner.Name}, 612 | ); err != nil { 613 | return err 614 | } 615 | 616 | for _, configMap := range configMaps.Items { 617 | configMap := configMap 618 | 619 | if configMap.Name == runner.Name+"-workspace" { 620 | continue 621 | } 622 | 623 | if err := r.Client.Delete(ctx, &configMap); err != nil { 624 | return err 625 | } 626 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulDeleted", "Deleted config map: %q", configMap.Name) 627 | } 628 | 629 | var deployments appsV1.DeploymentList 630 | if err := r.List( 631 | ctx, 632 | &deployments, 633 | client.InNamespace(runner.Namespace), 634 | client.MatchingFields{ownerKey: runner.Name}, 635 | ); err != nil { 636 | return err 637 | } 638 | 639 | for _, deployment := range deployments.Items { 640 | deployment := deployment 641 | 642 | if deployment.Name == runner.Name+"-runner" { 643 | continue 644 | } 645 | 646 | if err := r.Client.Delete(ctx, &deployment); err != nil { 647 | return err 648 | } 649 | r.Recorder.Eventf(runner, coreV1.EventTypeNormal, "SuccessfulDeleted", "Deleted deployment: %q", deployment.Name) 650 | } 651 | 652 | return nil 653 | } 654 | 655 | func (r *RunnerReconciler) SetupWithManager(mgr ctrl.Manager) error { 656 | ctx := context.Background() 657 | if err := mgr.GetFieldIndexer().IndexField(ctx, &v1.ConfigMap{}, ownerKey, func(rawObj client.Object) []string { 658 | configMap := rawObj.(*v1.ConfigMap) 659 | owner := metaV1.GetControllerOf(configMap) 660 | if owner == nil { 661 | return nil 662 | } 663 | if owner.Kind != "Runner" { 664 | return nil 665 | } 666 | 667 | return []string{owner.Name} 668 | }); err != nil { 669 | return err 670 | } 671 | 672 | if err := mgr.GetFieldIndexer().IndexField(ctx, &appsV1.Deployment{}, ownerKey, func(rawObj client.Object) []string { 673 | deployment := rawObj.(*appsV1.Deployment) 674 | owner := metaV1.GetControllerOf(deployment) 675 | if owner == nil { 676 | return nil 677 | } 678 | if owner.Kind != "Runner" { 679 | return nil 680 | } 681 | 682 | return []string{owner.Name} 683 | }); err != nil { 684 | return err 685 | } 686 | 687 | return ctrl.NewControllerManagedBy(mgr). 688 | For(&garV1.Runner{}). 689 | Owns(&v1.ConfigMap{}). 690 | Owns(&appsV1.Deployment{}). 691 | WithEventFilter(predicate.GenerationChangedPredicate{}). 692 | WithOptions(controller.Options{MaxConcurrentReconciles: 1}). 693 | Complete(r) 694 | } 695 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | garV1 "github-actions-runner-controller/api/v1" 7 | "github-actions-runner-controller/internal/controllers" 8 | "os" 9 | 10 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 11 | // to ensure that exec-entrypoint and run can make use of them. 12 | _ "k8s.io/client-go/plugin/pkg/client/auth" 13 | 14 | "k8s.io/apimachinery/pkg/runtime" 15 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 16 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 17 | "k8s.io/klog/v2" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | "sigs.k8s.io/controller-runtime/pkg/healthz" 20 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 21 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 22 | "sigs.k8s.io/controller-runtime/pkg/webhook" 23 | // +kubebuilder:scaffold:imports 24 | ) 25 | 26 | var ( 27 | scheme = runtime.NewScheme() 28 | ) 29 | 30 | func init() { 31 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 32 | utilruntime.Must(garV1.AddToScheme(scheme)) 33 | } 34 | 35 | func main() { 36 | var metricsAddr string 37 | var secureMetrics bool 38 | var enableHTTP2 bool 39 | var probeAddr string 40 | var enableLeaderElection bool 41 | var pushRegistryHost string 42 | var pullRegistryHost string 43 | var enableRunnerMetrics bool 44 | var exporterImage string 45 | var githubAppClientId string 46 | var githubAppInstallationId string 47 | var githubAppPrivateKey string 48 | var kanikoImage string 49 | var binaryVersion string 50 | var runnerVersion string 51 | var disableupdate bool 52 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 53 | flag.BoolVar(&secureMetrics, "metrics-secure", false, "If set the metrics endpoint is served securely") 54 | flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") 55 | flag.StringVar(&probeAddr, "health-probe-bind-address", "0.0.0.0:8081", "The address the probe endpoint binds to.") 56 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 57 | "Enable leader election for controller manager.") 58 | flag.StringVar(&pushRegistryHost, "push-registry-host", "ghcr.io/kaidotdev/github-actions-runner-controller", "Host of Docker Registry used as push destination.") 59 | flag.StringVar(&pullRegistryHost, "pull-registry-host", "ghcr.io/kaidotdev/github-actions-runner-controller", "Host of Docker Registry used as pull source.") 60 | flag.BoolVar(&enableRunnerMetrics, "enable-runner-metrics", false, "Enable to expose runner metrics using prometheus exporter.") 61 | flag.StringVar(&exporterImage, "exporter-image", "ghcr.io/kaidotdev/github-actions-exporter/github-actions-exporter:v0.1.1", "Docker Image of exporter used by exporter container") 62 | flag.StringVar(&githubAppClientId, "github-app-client-id", "", "GitHub App Client ID") 63 | flag.StringVar(&githubAppInstallationId, "github-app-installation-id", "", "GitHub App Installation ID") 64 | flag.StringVar(&githubAppPrivateKey, "github-app-private-key", "", "GitHub App Private Key") 65 | flag.StringVar(&kanikoImage, "kaniko-image", "gcr.io/kaniko-project/executor:v1.23.0", "Docker Image of kaniko used by builder container") 66 | flag.StringVar(&binaryVersion, "binary-version", "0.4.5", "Version of own runner binary") 67 | flag.StringVar(&runnerVersion, "runner-version", "2.321.0", "Version of GitHub Actions runner") 68 | flag.BoolVar(&disableupdate, "disableupdate", false, "Disable self-hosted runner automatic update to the latest released version") 69 | opts := zap.Options{} 70 | opts.BindFlags(flag.CommandLine) 71 | klog.InitFlags(flag.CommandLine) 72 | flag.Parse() 73 | 74 | zapLogger := zap.New(zap.UseFlagOptions(&opts)) 75 | klog.SetLogger(zapLogger) 76 | ctrl.SetLogger(zapLogger) 77 | 78 | entrypointLogger := ctrl.Log.WithName("entrypoint") 79 | 80 | // if the enable-http2 flag is false (the default), http/2 should be disabled 81 | // due to its vulnerabilities. More specifically, disabling http/2 will 82 | // prevent from being vulnerable to the HTTP/2 Stream Cancelation and 83 | // Rapid Reset CVEs. For more information see: 84 | // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 85 | // - https://github.com/advisories/GHSA-4374-p667-p6c8 86 | disableHTTP2 := func(c *tls.Config) { 87 | entrypointLogger.Info("disabling http/2") 88 | c.NextProtos = []string{"http/1.1"} 89 | } 90 | 91 | tlsOpts := []func(*tls.Config){} 92 | if !enableHTTP2 { 93 | tlsOpts = append(tlsOpts, disableHTTP2) 94 | } 95 | 96 | webhookServer := webhook.NewServer(webhook.Options{ 97 | TLSOpts: tlsOpts, 98 | }) 99 | m, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 100 | Scheme: scheme, 101 | Metrics: metricsserver.Options{ 102 | BindAddress: metricsAddr, 103 | SecureServing: secureMetrics, 104 | TLSOpts: tlsOpts, 105 | }, 106 | WebhookServer: webhookServer, 107 | HealthProbeBindAddress: probeAddr, 108 | LeaderElection: enableLeaderElection, 109 | LeaderElectionID: "github-actions-runner-controller", 110 | }) 111 | if err != nil { 112 | entrypointLogger.Error(err, "unable to create manager") 113 | os.Exit(1) 114 | } 115 | 116 | if err := (&controllers.RunnerReconciler{ 117 | Client: m.GetClient(), 118 | Scheme: m.GetScheme(), 119 | Log: ctrl.Log.WithName("controllers").WithName("Runner"), 120 | Recorder: m.GetEventRecorderFor("github-actions-runner-controller"), 121 | PushRegistryHost: pushRegistryHost, 122 | PullRegistryHost: pullRegistryHost, 123 | EnableRunnerMetrics: enableRunnerMetrics, 124 | ExporterImage: exporterImage, 125 | GitHubAppClientId: githubAppClientId, 126 | GitHubAppInstallationId: githubAppInstallationId, 127 | GitHubAppPrivateKey: githubAppPrivateKey, KanikoImage: kanikoImage, 128 | BinaryVersion: binaryVersion, 129 | RunnerVersion: runnerVersion, 130 | Disableupdate: disableupdate, 131 | }).SetupWithManager(m); err != nil { 132 | entrypointLogger.Error(err, "unable to create controller", "controller", "Runner") 133 | os.Exit(1) 134 | } 135 | 136 | if err := m.AddHealthzCheck("healthz", healthz.Ping); err != nil { 137 | entrypointLogger.Error(err, "unable to set up health check") 138 | os.Exit(1) 139 | } 140 | if err := m.AddReadyzCheck("readyz", healthz.Ping); err != nil { 141 | entrypointLogger.Error(err, "unable to set up ready check") 142 | os.Exit(1) 143 | } 144 | 145 | entrypointLogger.Info("starting manager") 146 | if err := m.Start(ctrl.SetupSignalHandler()); err != nil { 147 | entrypointLogger.Error(err, "problem running manager") 148 | os.Exit(1) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /manifests/NODEPORT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaidotdev/github-actions-runner-controller/bfa37f4d42932ef583c3039a125a43501258dcbb/manifests/NODEPORT -------------------------------------------------------------------------------- /manifests/cluster_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: github-actions-runner-controller 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: 9 | - secrets 10 | verbs: 11 | - create 12 | - delete 13 | - get 14 | - list 15 | - patch 16 | - update 17 | - watch 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - configmaps 22 | verbs: 23 | - create 24 | - delete 25 | - get 26 | - list 27 | - patch 28 | - update 29 | - watch 30 | - apiGroups: 31 | - apps 32 | resources: 33 | - deployments 34 | verbs: 35 | - create 36 | - delete 37 | - get 38 | - list 39 | - patch 40 | - update 41 | - watch 42 | - apiGroups: 43 | - apps 44 | resources: 45 | - deployments/status 46 | verbs: 47 | - get 48 | - apiGroups: 49 | - github-actions-runner.kaidotdev.github.io 50 | resources: 51 | - runners 52 | verbs: 53 | - create 54 | - delete 55 | - get 56 | - list 57 | - patch 58 | - update 59 | - watch 60 | - apiGroups: 61 | - github-actions-runner.kaidotdev.github.io 62 | resources: 63 | - runners/status 64 | verbs: 65 | - get 66 | - patch 67 | - update 68 | - apiGroups: 69 | - "" 70 | resources: 71 | - events 72 | verbs: 73 | - create 74 | - get 75 | - list 76 | - patch 77 | -------------------------------------------------------------------------------- /manifests/cluster_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: github-actions-runner-controller 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: github-actions-runner-controller 9 | subjects: 10 | - kind: ServiceAccount 11 | name: github-actions-runner-controller 12 | -------------------------------------------------------------------------------- /manifests/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: github-actions-runner-controller 5 | spec: 6 | replicas: 4 7 | strategy: 8 | type: RollingUpdate 9 | rollingUpdate: 10 | maxSurge: 25% 11 | maxUnavailable: 1 12 | selector: 13 | matchLabels: 14 | app: github-actions-runner-controller 15 | template: 16 | metadata: 17 | labels: 18 | app: github-actions-runner-controller 19 | spec: 20 | serviceAccountName: github-actions-runner-controller 21 | securityContext: 22 | sysctls: 23 | # https://github.com/kubernetes/kubernetes/pull/54896 24 | #- name: net.core.somaxconn 25 | # value: "65535" 26 | - name: net.ipv4.ip_local_port_range 27 | value: "10000 65535" 28 | affinity: 29 | podAntiAffinity: 30 | preferredDuringSchedulingIgnoredDuringExecution: 31 | - podAffinityTerm: 32 | labelSelector: 33 | matchExpressions: 34 | - key: app 35 | operator: In 36 | values: 37 | - github-actions-runner-controller 38 | topologyKey: kubernetes.io/hostname 39 | weight: 100 40 | initContainers: 41 | - name: fetch-nodeport 42 | image: bitnami/kubectl:1.16.3 43 | command: 44 | - sh 45 | args: 46 | - -c 47 | - kubectl patch configmap/$(CONFIGMAP_NAME) -n $(NAMESPACE) -p '{"data":{"NODEPORT":"'`kubectl get service $(SERVICE_NAME) -n $(NAMESPACE) -o jsonpath='{$.spec.ports[0].nodePort}'`'"}}' 48 | env: 49 | - name: SERVICE_NAME 50 | value: $(SERVICE_NAME) 51 | - name: NAMESPACE 52 | valueFrom: 53 | fieldRef: 54 | fieldPath: metadata.namespace 55 | - name: CONFIGMAP_NAME 56 | value: $(CONFIGMAP_NAME) 57 | containers: 58 | - name: controller 59 | image: ghcr.io/kaidotdev/github-actions-runner-controller:v0.3.19 60 | imagePullPolicy: Always 61 | args: 62 | - --metrics-addr=0.0.0.0:8080 63 | - --enable-leader-election 64 | - --push-registry-host=$(SERVICE_NAME)-0.$(SERVICE_NAME).$(NAMESPACE).svc.cluster.local:5000 65 | - --pull-registry-host=127.0.0.1:$(NODEPORT) 66 | - --enable-runner-metrics 67 | env: 68 | - name: SERVICE_NAME 69 | value: $(SERVICE_NAME) 70 | - name: NAMESPACE 71 | valueFrom: 72 | fieldRef: 73 | fieldPath: metadata.namespace 74 | - name: NODEPORT 75 | valueFrom: 76 | configMapKeyRef: 77 | name: metadata 78 | key: NODEPORT 79 | ports: 80 | - containerPort: 8080 81 | -------------------------------------------------------------------------------- /manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: default 2 | 3 | resources: 4 | - crd/github-actions-runner.kaidotdev.github.io_runners.yaml 5 | # +kubebuilder:scaffold:crdkustomizeresource 6 | - cluster_role.yaml 7 | - cluster_role_binding.yaml 8 | - deployment.yaml 9 | - pod_disruption_budget.yaml 10 | - role.yaml 11 | - role_binding.yaml 12 | - service.yaml 13 | - service_account.yaml 14 | - stateful_set.yaml 15 | 16 | configMapGenerator: 17 | - name: metadata 18 | files: 19 | - NODEPORT 20 | 21 | configurations: 22 | - kustomizeconfig.yaml 23 | 24 | vars: 25 | - name: SERVICE_NAME 26 | objref: 27 | apiVersion: v1 28 | kind: Service 29 | name: github-actions-runner-controller-registry 30 | fieldref: 31 | fieldpath: metadata.name 32 | - name: CONFIGMAP_NAME 33 | objref: 34 | apiVersion: v1 35 | kind: ConfigMap 36 | name: metadata 37 | fieldref: 38 | fieldpath: metadata.name 39 | -------------------------------------------------------------------------------- /manifests/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | namereference: 2 | - kind: Service 3 | version: v1 4 | fieldSpecs: 5 | - path: rules/resourceNames 6 | kind: Role 7 | - path: rules/resourceNames 8 | kind: ClusterRole 9 | - kind: ConfigMap 10 | version: v1 11 | fieldSpecs: 12 | - path: rules/resourceNames 13 | kind: Role 14 | - path: rules/resourceNames 15 | kind: ClusterRole 16 | -------------------------------------------------------------------------------- /manifests/pod_disruption_budget.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: github-actions-runner-controller 5 | spec: 6 | maxUnavailable: 1 7 | selector: 8 | matchLabels: 9 | app: github-actions-runner-controller 10 | -------------------------------------------------------------------------------- /manifests/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: github-actions-runner-controller-leader-election 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: 9 | - configmaps 10 | verbs: 11 | - get 12 | - list 13 | - watch 14 | - create 15 | - update 16 | - patch 17 | - delete 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - configmaps/status 22 | verbs: 23 | - get 24 | - update 25 | - patch 26 | - apiGroups: 27 | - "" 28 | resources: 29 | - events 30 | verbs: 31 | - create 32 | - apiGroups: 33 | - coordination.k8s.io 34 | resources: 35 | - leases 36 | resourceNames: 37 | - github-actions-runner-controller 38 | verbs: 39 | - get 40 | - update 41 | - patch 42 | - apiGroups: 43 | - coordination.k8s.io 44 | resources: 45 | - leases 46 | verbs: 47 | - create 48 | --- 49 | apiVersion: rbac.authorization.k8s.io/v1 50 | kind: Role 51 | metadata: 52 | name: github-actions-runner-controller 53 | rules: 54 | - apiGroups: 55 | - "" 56 | resources: 57 | - services 58 | resourceNames: 59 | - github-actions-runner-controller-registry 60 | verbs: 61 | - get 62 | - apiGroups: 63 | - "" 64 | resources: 65 | - configmaps 66 | resourceNames: 67 | - metadata 68 | verbs: 69 | - patch 70 | -------------------------------------------------------------------------------- /manifests/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: github-actions-runner-controller-leader-election 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: github-actions-runner-controller-leader-election 9 | subjects: 10 | - kind: ServiceAccount 11 | name: github-actions-runner-controller 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: RoleBinding 15 | metadata: 16 | name: github-actions-runner-controller 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: Role 20 | name: github-actions-runner-controller 21 | subjects: 22 | - kind: ServiceAccount 23 | name: github-actions-runner-controller 24 | -------------------------------------------------------------------------------- /manifests/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: github-actions-runner-controller-registry 5 | spec: 6 | type: NodePort 7 | selector: 8 | app: github-actions-runner-controller-registry 9 | ports: 10 | - port: 5000 11 | protocol: TCP 12 | targetPort: 5000 13 | -------------------------------------------------------------------------------- /manifests/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: github-actions-runner-controller 5 | -------------------------------------------------------------------------------- /manifests/stateful_set.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: github-actions-runner-controller-registry 5 | spec: 6 | serviceName: github-actions-runner-controller-registry 7 | replicas: 1 8 | updateStrategy: 9 | type: RollingUpdate 10 | selector: 11 | matchLabels: 12 | app: github-actions-runner-controller-registry 13 | template: 14 | metadata: 15 | labels: 16 | app: github-actions-runner-controller-registry 17 | spec: 18 | containers: 19 | - name: registry 20 | image: registry:2 21 | imagePullPolicy: Always 22 | ports: 23 | - containerPort: 5000 24 | volumeMounts: 25 | - name: data 26 | mountPath: /var/lib/registry 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: data 30 | spec: 31 | accessModes: 32 | - ReadWriteOnce 33 | resources: 34 | requests: 35 | storage: 10Gi 36 | -------------------------------------------------------------------------------- /patches/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: github-actions-runner-controller 5 | spec: 6 | replicas: 1 7 | strategy: 8 | type: RollingUpdate 9 | rollingUpdate: 10 | maxSurge: 0 11 | maxUnavailable: 1 12 | template: 13 | spec: 14 | containers: 15 | - name: controller 16 | image: github-actions-runner-controller 17 | imagePullPolicy: Never 18 | -------------------------------------------------------------------------------- /patches/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: github-actions-runner-controller 2 | 3 | bases: 4 | - ../manifests 5 | 6 | resources: 7 | - namespace.yaml 8 | 9 | patchesStrategicMerge: 10 | - deployment.yaml 11 | -------------------------------------------------------------------------------- /patches/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: github-actions-runner-controller 5 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v1beta12 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: github-actions-runner-controller 6 | context: . 7 | local: 8 | useBuildkit: true 9 | deploy: 10 | kustomize: 11 | path: patches 12 | # kubectl: 13 | # manifests: 14 | # - patches 15 | # flags: 16 | # apply: 17 | # - -k 18 | --------------------------------------------------------------------------------