mirror of
https://github.com/bapung/gitea-runner-operator.git
synced 2026-06-22 07:58:44 +00:00
documentation
This commit is contained in:
226
README.md
226
README.md
@@ -1,83 +1,185 @@
|
|||||||
# Overview
|
# Gitea Runner Operator
|
||||||
|
|
||||||
Operator to manage gitea Act runner on Kubernetes
|
A Kubernetes Operator to manage ephemeral Gitea Act runners. This operator automatically spawns runner pods based on the demand of queued jobs in your Gitea instance, ensuring efficient resource usage and isolation.
|
||||||
|
|
||||||
# How it works?
|
## Features
|
||||||
|
|
||||||
1. It installs a set of CRDs: `kind: RunnerGroup` in Kubernetes
|
- **Ephemeral Runners**: Each job gets a fresh runner which is destroyed after execution.
|
||||||
|
- **Multiple Scopes**: Support for `global`, `org`, `user`, and `repo` level runners.
|
||||||
|
- **Auto-Scaling**: Automatically scales runners up to a configured maximum based on queued jobs.
|
||||||
|
- **Label Matching**: matches Gitea job labels (e.g., `ubuntu-latest`) to runner capabilities.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Kubernetes Cluster**: v1.23+
|
||||||
|
- **Gitea**: v1.25.0+ (with Actions enabled)
|
||||||
|
|
||||||
|
## Installation (Helm Chart)
|
||||||
|
|
||||||
|
### Incoming
|
||||||
|
|
||||||
|
## Installation (Manual)
|
||||||
|
|
||||||
|
### 1. Deploy the Operator
|
||||||
|
|
||||||
|
You can deploy the operator using the provided manifests.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/bapung/gitea-runner-operator.git
|
||||||
|
cd gitea-runner-operator
|
||||||
|
|
||||||
|
# Install CRDs
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Deploy the controller to the cluster
|
||||||
|
make deploy IMG=ghcr.io/bapung/gitea-runner-operator:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Credentials Secret
|
||||||
|
|
||||||
|
Create a secret containing the Gitea Registration Token and an API Auth Token.
|
||||||
|
|
||||||
|
1. **Registration Token**: Get this from Gitea Admin -> Actions -> Runners -> Create new Runner (or Org/Repo settings).
|
||||||
|
2. **Auth Token**: Generate a token in Gitea User Settings -> Applications. It needs `read:repository`, `read:user` permissions.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: gitea-runner-secret
|
||||||
|
namespace: gitea-runner-operator-system
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
registrationToken: "<YOUR_REGISTRATION_TOKEN>"
|
||||||
|
authToken: "<YOUR_API_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The core resource is the `RunnerGroup`. Below are examples for different scopes.
|
||||||
|
|
||||||
|
### 1. Repository Scope
|
||||||
|
|
||||||
|
Spawns runners only for jobs in a specific repository.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: gitea.bpg.pw/v1alpha1
|
apiVersion: gitea.bpg.pw/v1alpha1
|
||||||
kind: RunnerGroup
|
kind: RunnerGroup
|
||||||
metadata:
|
metadata:
|
||||||
name: my-repo-runner-1
|
name: my-repo-runner
|
||||||
namespace: gitea-runner-system
|
namespace: gitea-runner-operator-system
|
||||||
spec:
|
spec:
|
||||||
scope: repo # valid options: global, org or user, repo
|
scope: repo
|
||||||
org: myorg # optional; ommited if scope == global; mutually exclusive with user
|
org: myorg
|
||||||
user: myusername # optional; ommited if scope == global; mutually exclusive with org
|
repo: myrepo
|
||||||
repo: myreponame # optional; ommited if scope == org || scope == global
|
giteaURL: https://gitea.example.com
|
||||||
gitea:
|
maxActiveRunners: 5
|
||||||
url: https://gitea.bpg.pw
|
|
||||||
labels:
|
labels:
|
||||||
- default
|
- "ubuntu-latest"
|
||||||
- app:infra
|
- "custom-label"
|
||||||
maxActiveRunners: 5 #
|
registrationToken:
|
||||||
registrationToken: # registration token for runner
|
|
||||||
secretRef:
|
secretRef:
|
||||||
name: gitea-runner-secret-0
|
name: gitea-runner-secret
|
||||||
key: registrationToken
|
key: registrationToken
|
||||||
authToken: # token to get list of job status
|
authToken:
|
||||||
secretRef:
|
secretRef:
|
||||||
name: gitea-runner-secret-0
|
name: gitea-runner-secret
|
||||||
key: authToken
|
key: authToken
|
||||||
```
|
```
|
||||||
|
|
||||||
2. The RunnerGroup controller will continuously watch for queued jobs based on its scope: `global`, `org`, or `repo`. If a new workflow run is detected with `status: queued`, based on the RunnerGroup's labels, the controller will spawn a new ephemeral runner as a Job.
|
### 2. Organization Scope
|
||||||
|
|
||||||
|
Spawns runners for any repository within the organization.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: batch/v1
|
apiVersion: gitea.bpg.pw/v1alpha1
|
||||||
kind: Job
|
kind: RunnerGroup
|
||||||
metadata:
|
metadata:
|
||||||
name: my-repo-runner-1-275f1b8f
|
name: my-org-runner
|
||||||
labels:
|
namespace: gitea-runner-operator-system
|
||||||
app: my-repo-runner-1
|
|
||||||
# tags to determine that this resource is managed by the Operator
|
|
||||||
spec:
|
spec:
|
||||||
# Optional: Automatically clean up the job after it finishes (e.g., 100 seconds)
|
scope: org
|
||||||
ttlSecondsAfterFinished: 600
|
org: myorg
|
||||||
template:
|
# repo is omitted
|
||||||
metadata:
|
giteaURL: https://gitea.example.com
|
||||||
labels:
|
maxActiveRunners: 10
|
||||||
app: act-my-repo-runner-1
|
# ... (tokens)
|
||||||
spec:
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1000
|
|
||||||
volumes:
|
|
||||||
- name: runner-data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: act-runner-vol
|
|
||||||
containers:
|
|
||||||
- name: runner
|
|
||||||
image: gitea/act_runner:nightly-dind-rootless
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: DOCKER_HOST
|
|
||||||
value: tcp://localhost:2376
|
|
||||||
- name: DOCKER_CERT_PATH
|
|
||||||
value: /certs/client
|
|
||||||
- name: DOCKER_TLS_VERIFY
|
|
||||||
value: "1"
|
|
||||||
- name: GITEA_INSTANCE_URL
|
|
||||||
value: https://gitea.bpg.pw
|
|
||||||
- name: GITEA_RUNNER_EPHEMERAL # always ephemeral
|
|
||||||
value: "1"
|
|
||||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: gitea-runner-secret-0
|
|
||||||
key: registrationToken
|
|
||||||
securityContext:
|
|
||||||
privileged: true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. User Scope
|
||||||
|
|
||||||
|
Spawns runners for any repository owned by the specified user.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: gitea.bpg.pw/v1alpha1
|
||||||
|
kind: RunnerGroup
|
||||||
|
metadata:
|
||||||
|
name: my-user-runner
|
||||||
|
namespace: gitea-runner-operator-system
|
||||||
|
spec:
|
||||||
|
scope: user
|
||||||
|
user: myusername
|
||||||
|
# org and repo are omitted
|
||||||
|
giteaURL: https://gitea.example.com
|
||||||
|
maxActiveRunners: 3
|
||||||
|
# ... (tokens)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Global Scope
|
||||||
|
|
||||||
|
Spawns runners for any job in the Gitea instance (Admin level).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: gitea.bpg.pw/v1alpha1
|
||||||
|
kind: RunnerGroup
|
||||||
|
metadata:
|
||||||
|
name: global-runner
|
||||||
|
namespace: gitea-runner-operator-system
|
||||||
|
spec:
|
||||||
|
scope: global
|
||||||
|
# org, user, and repo are omitted
|
||||||
|
giteaURL: https://gitea.example.com
|
||||||
|
maxActiveRunners: 20
|
||||||
|
# ... (tokens)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. The **Controller** polls the Gitea API (using the `authToken`) to check for queued jobs matching the scope and labels.
|
||||||
|
2. If a matching queued job is found, and the current active runner count is below `maxActiveRunners`, the Controller creates a Kubernetes `Job`.
|
||||||
|
3. The `Job` pod starts an `act_runner` instance, registers itself using the `registrationToken` (as ephemeral), picks up the job, executes it, and then terminates.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Runners are not starting
|
||||||
|
|
||||||
|
1. **Check Controller Logs**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs -n gitea-runner-operator-system -l control-plane=controller-manager -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for errors regarding API authentication or connectivity.
|
||||||
|
|
||||||
|
2. **Check Permissions**:
|
||||||
|
Ensure the `authToken` has sufficient permissions (`read:repository`, etc.) to query actions.
|
||||||
|
|
||||||
|
3. **Check Labels**:
|
||||||
|
Enable debug logging in the controller to see label matching logic. If your Gitea job requires `ubuntu-latest` but your RunnerGroup defines `centos`, it won't match.
|
||||||
|
|
||||||
|
### Docker Daemon Issues
|
||||||
|
|
||||||
|
The default runner image uses `dind-rootless`. This requires the pod to run with `privileged: true`. Ensure your cluster policies (PSP/PSA) allow privileged pods in the operator namespace.
|
||||||
|
|
||||||
|
## Roadmap / Wishlist
|
||||||
|
|
||||||
|
- Helm Chart
|
||||||
|
- Custom Runner Job Spec definition
|
||||||
|
- Push mode using Webhook trigger
|
||||||
|
|||||||
@@ -107,12 +107,17 @@ spec:
|
|||||||
description: Repo is required if scope is 'repo'
|
description: Repo is required if scope is 'repo'
|
||||||
type: string
|
type: string
|
||||||
scope:
|
scope:
|
||||||
description: Scope defines the scope of the runner (global, org, repo)
|
description: Scope defines the scope of the runner (global, org, user,
|
||||||
|
repo)
|
||||||
enum:
|
enum:
|
||||||
- global
|
- global
|
||||||
- org
|
- org
|
||||||
|
- user
|
||||||
- repo
|
- repo
|
||||||
type: string
|
type: string
|
||||||
|
user:
|
||||||
|
description: User is required if scope is 'user'
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- authToken
|
- authToken
|
||||||
- giteaURL
|
- giteaURL
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ spec:
|
|||||||
|
|
||||||
# Scope of the runners (global, org, or repo)
|
# Scope of the runners (global, org, or repo)
|
||||||
scope: "org"
|
scope: "org"
|
||||||
org: "bapung" # Required if scope is 'org' or 'repo'
|
#org: "bapungorg" # Required if scope is 'org' or 'repo'; cannot be used with user
|
||||||
user: "" # Required if scope is 'user' or 'repo'
|
user: "bapung" # Required if scope is 'user' or 'repo'; cannot be used with org
|
||||||
#repo: "dummy-service-workflow" # Required if scope is 'repo'
|
#repo: "dummy-service-workflow" # Required if scope is 'repo'
|
||||||
|
|
||||||
# Labels to identify this runner group
|
# Labels to identify this runner group
|
||||||
|
|||||||
@@ -25,11 +25,19 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
giteav1alpha1 "github.com/bapung/gitea-runner-operator/api/v1alpha1"
|
giteav1alpha1 "github.com/bapung/gitea-runner-operator/api/v1alpha1"
|
||||||
|
"github.com/bapung/gitea-runner-operator/internal/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type fakeGiteaClient struct{}
|
||||||
|
|
||||||
|
func (c *fakeGiteaClient) GetRunnerStats(ctx context.Context, giteaURL, authToken string, scope giteav1alpha1.RunnerGroupScope, org string, user string, repo string, labels []string) (*gitea.RunnerStats, error) {
|
||||||
|
return &gitea.RunnerStats{QueuedJobs: []gitea.ActionWorkflowJob{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ = Describe("RunnerGroup Controller", func() {
|
var _ = Describe("RunnerGroup Controller", func() {
|
||||||
Context("When reconciling a resource", func() {
|
Context("When reconciling a resource", func() {
|
||||||
const resourceName = "test-resource"
|
const resourceName = "test-resource"
|
||||||
@@ -43,6 +51,21 @@ var _ = Describe("RunnerGroup Controller", func() {
|
|||||||
runnergroup := &giteav1alpha1.RunnerGroup{}
|
runnergroup := &giteav1alpha1.RunnerGroup{}
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
|
By("creating the secret")
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "gitea-secret",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"token": []byte("dummy"),
|
||||||
|
"auth": []byte("dummy"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := k8sClient.Create(ctx, secret); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
Expect(err).To(Succeed())
|
||||||
|
}
|
||||||
|
|
||||||
By("creating the custom resource for the Kind RunnerGroup")
|
By("creating the custom resource for the Kind RunnerGroup")
|
||||||
err := k8sClient.Get(ctx, typeNamespacedName, runnergroup)
|
err := k8sClient.Get(ctx, typeNamespacedName, runnergroup)
|
||||||
if err != nil && errors.IsNotFound(err) {
|
if err != nil && errors.IsNotFound(err) {
|
||||||
@@ -51,7 +74,19 @@ var _ = Describe("RunnerGroup Controller", func() {
|
|||||||
Name: resourceName,
|
Name: resourceName,
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
},
|
},
|
||||||
// TODO(user): Specify other spec details if needed.
|
Spec: giteav1alpha1.RunnerGroupSpec{
|
||||||
|
Scope: giteav1alpha1.RunnerGroupScopeGlobal,
|
||||||
|
GiteaURL: "https://gitea.example.com",
|
||||||
|
MaxActiveRunners: 1,
|
||||||
|
RegistrationTokenRef: corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{Name: "gitea-secret"},
|
||||||
|
Key: "token",
|
||||||
|
},
|
||||||
|
AuthTokenRef: corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{Name: "gitea-secret"},
|
||||||
|
Key: "auth",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||||
}
|
}
|
||||||
@@ -71,6 +106,7 @@ var _ = Describe("RunnerGroup Controller", func() {
|
|||||||
controllerReconciler := &RunnerGroupReconciler{
|
controllerReconciler := &RunnerGroupReconciler{
|
||||||
Client: k8sClient,
|
Client: k8sClient,
|
||||||
Scheme: k8sClient.Scheme(),
|
Scheme: k8sClient.Scheme(),
|
||||||
|
GiteaClient: &fakeGiteaClient{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
|||||||
Reference in New Issue
Block a user