mirror of
https://github.com/bapung/gitea-runner-operator.git
synced 2026-06-21 23:48:43 +00:00
add user scope
This commit is contained in:
@@ -30,18 +30,23 @@ type RunnerGroupScope string
|
||||
const (
|
||||
RunnerGroupScopeGlobal RunnerGroupScope = "global"
|
||||
RunnerGroupScopeOrg RunnerGroupScope = "org"
|
||||
RunnerGroupScopeUser RunnerGroupScope = "user"
|
||||
RunnerGroupScopeRepo RunnerGroupScope = "repo"
|
||||
)
|
||||
|
||||
type RunnerGroupSpec struct {
|
||||
// Scope defines the scope of the runner (global, org, repo)
|
||||
// +kubebuilder:validation:Enum=global;org;repo
|
||||
// Scope defines the scope of the runner (global, org, user, repo)
|
||||
// +kubebuilder:validation:Enum=global;org;user;repo
|
||||
Scope RunnerScope `json:"scope"`
|
||||
|
||||
// Org is required if scope is 'org'
|
||||
// +optional
|
||||
Org string `json:"org,omitempty"`
|
||||
|
||||
// User is required if scope is 'user'
|
||||
// +optional
|
||||
User string `json:"user,omitempty"`
|
||||
|
||||
// Repo is required if scope is 'repo'
|
||||
// +optional
|
||||
Repo string `json:"repo,omitempty"`
|
||||
@@ -49,7 +54,8 @@ type RunnerGroupSpec struct {
|
||||
// GiteaURL is the base URL of the Gitea instance
|
||||
GiteaURL string `json:"giteaURL"`
|
||||
|
||||
// Labels to assign to the runner
|
||||
// Labels to assign to the runner.
|
||||
// Defaults (e.g. ubuntu-latest) are merged automatically by the controller.
|
||||
// +optional
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
|
||||
@@ -79,154 +85,103 @@ type RunnerGroupStatus struct {
|
||||
|
||||
## 4. Controller Implementation (`internal/controller/runnergroup_controller.go`)
|
||||
|
||||
The controller handles the reconciliation loop.
|
||||
The controller handles the reconciliation loop and manages the lifecycle of ephemeral runners.
|
||||
|
||||
### 4.1 RBAC Permissions
|
||||
### 4.1 Struct Definition
|
||||
|
||||
Add markers to generate RBAC roles:
|
||||
The reconciler includes a thread-safe map to cache spawned jobs and prevent duplicate scheduling.
|
||||
|
||||
```go
|
||||
// +kubebuilder:rbac:groups=gitea.bpg.pw,resources=runnergroups,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=gitea.bpg.pw,resources=runnergroups/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
|
||||
type RunnerGroupReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
GiteaClient gitea.Client
|
||||
SpawnedJobsCache sync.Map // Stores [int64]time.Time (JobID -> SpawnTime)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Reconcile Logic
|
||||
|
||||
The `Reconcile` function should follow this flow:
|
||||
The `Reconcile` function follows this flow:
|
||||
|
||||
1. **Fetch RunnerGroup**: Get the `RunnerGroup` CR instance. If not found, ignore (deleted).
|
||||
2. **List Jobs**: List all `batchv1.Job` resources in the same namespace that are owned by this RunnerGroup.
|
||||
- Filter by label `gitea.bpg.pw/runnergroup-name=<runnergroup-name>`.
|
||||
3. **Update Status**: Update `status.activeRunners` with the count of non-completed jobs.
|
||||
4. **Capacity Check**:
|
||||
- If `activeRunners >= spec.maxActiveRunners`, stop and requeue.
|
||||
5. **Poll Gitea**:
|
||||
- Retrieve the Auth Token from the Secret referenced in `spec.authToken`.
|
||||
- Instantiate a Gitea API Client.
|
||||
- Query for queued workflow runs matching the scope and labels.
|
||||
6. **Scale Up**:
|
||||
- Calculate `needed = count(queued_jobs)`.
|
||||
- Calculate `available_slots = spec.maxActiveRunners - activeRunners`.
|
||||
- `to_spawn = min(needed, available_slots)`.
|
||||
- Loop `to_spawn` times:
|
||||
- Create a new `batchv1.Job`.
|
||||
7. **Requeue**: Return `ctrl.Result{RequeueAfter: 10 * time.Second}` to ensure continuous polling.
|
||||
1. **Fetch RunnerGroup**: Get the `RunnerGroup` CR instance.
|
||||
2. **List Jobs**: List all `batchv1.Job` resources owned by this CR to calculate `activeRunners`.
|
||||
3. **Update Status**: Update `status.activeRunners`.
|
||||
4. **Capacity Check**: Stop scaling if `activeRunners >= spec.maxActiveRunners`.
|
||||
5. **Label Calculation**: Call `getEffectiveLabels` to merge `spec.labels` with hardcoded Gitea defaults (e.g., `ubuntu-latest:docker://node:16-bullseye`).
|
||||
6. **Poll Gitea**:
|
||||
- Retrieve Auth Token.
|
||||
- Call `GiteaClient.GetRunnerStats` with the effective labels.
|
||||
- This returns a list of `QueuedJobs`.
|
||||
7. **Scale Up & Deduplication**:
|
||||
- Iterate through `stats.QueuedJobs`.
|
||||
- **Check Cache**: If Job ID exists in `SpawnedJobsCache`:
|
||||
- If TTL (< 5 min) is valid: **Skip** (already handled).
|
||||
- If TTL expired: **Retry** (assume previous runner failed).
|
||||
- If Job ID not in cache or expired:
|
||||
- Check `availableSlots`.
|
||||
- Retrieve Registration Token (if not yet fetched).
|
||||
- **Spawn Job**: Create `batchv1.Job`.
|
||||
- **Update Cache**: Store Job ID in `SpawnedJobsCache`.
|
||||
- Decrement `availableSlots`.
|
||||
8. **Cache Cleanup**: Remove IDs from `SpawnedJobsCache` if they are not present in the latest `QueuedJobs` list from Gitea.
|
||||
9. **Requeue**: Return `ctrl.Result{RequeueAfter: 10 * time.Second}`.
|
||||
|
||||
### 4.3 Job Construction
|
||||
### 4.3 Helper Functions
|
||||
|
||||
Helper function to create the Job object:
|
||||
#### getEffectiveLabels
|
||||
|
||||
```go
|
||||
func (r *RunnerGroupReconciler) constructJobForRunnerGroup(runnerGroup *giteav1alpha1.RunnerGroup, registrationToken string) (*batchv1.Job, error) {
|
||||
// Generate random suffix for name
|
||||
name := fmt.Sprintf("%s-%s", runnerGroup.Name, randString(5))
|
||||
Merges user-defined labels with Gitea defaults. If a user defines `ubuntu-latest`, it overrides the default `ubuntu-latest:docker://...`.
|
||||
|
||||
// Construct Env Vars
|
||||
envVars := []corev1.EnvVar{
|
||||
{Name: "GITEA_INSTANCE_URL", Value: runnerGroup.Spec.GiteaURL},
|
||||
{Name: "GITEA_RUNNER_REGISTRATION_TOKEN", Value: registrationToken},
|
||||
{Name: "GITEA_RUNNER_EPHEMERAL", Value: "true"},
|
||||
{Name: "DOCKER_HOST", Value: "tcp://localhost:2376"},
|
||||
// ... other envs from README
|
||||
}
|
||||
#### constructJobForRunnerGroup
|
||||
|
||||
if len(runnerGroup.Spec.Labels) > 0 {
|
||||
labelsStr := strings.Join(runnerGroup.Spec.Labels, ",")
|
||||
envVars = append(envVars, corev1.EnvVar{Name: "GITEA_RUNNER_LABELS", Value: labelsStr})
|
||||
}
|
||||
Creates the Job object with:
|
||||
|
||||
// Construct Job
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: runnerGroup.Namespace,
|
||||
Labels: map[string]string{
|
||||
"app": runnerGroup.Name,
|
||||
"gitea.bpg.pw/runnergroup-name": runnerGroup.Name,
|
||||
"gitea.bpg.pw/managed-by": "gitea-runner-operator",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
TTLSecondsAfterFinished: pointer.Int32(600),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: corev1.RestartPolicyOnFailure,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: "gitea/act_runner:nightly-dind-rootless",
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: pointer.Bool(true)},
|
||||
Env: envVars,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{Name: "runner-data", MountPath: "/data"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "runner-data",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
||||
ClaimName: "act-runner-vol", // Note: Consider making this configurable or EmptyDir
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set Controller Reference
|
||||
if err := ctrl.SetControllerReference(runnerGroup, job, r.Scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
```
|
||||
- **Name**: `{runnergroup-name}-{random-suffix}`
|
||||
- **Env**:
|
||||
- `GITEA_RUNNER_NAME`: Set to the Job name.
|
||||
- `GITEA_RUNNER_LABELS`: Comma-separated effective labels.
|
||||
- Standard runner envs (`GITEA_INSTANCE_URL`, etc).
|
||||
|
||||
## 5. Gitea Client (`internal/gitea/client.go`)
|
||||
|
||||
A simple HTTP client wrapper to interact with Gitea.
|
||||
A specialized client to interact with Gitea's Actions API.
|
||||
|
||||
### 5.1 Interface
|
||||
|
||||
```go
|
||||
type RunnerStats struct {
|
||||
QueuedJobs []ActionWorkflowJob
|
||||
Running int
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
GetQueuedRuns(ctx context.Context, scope RunnerGroupScope, owner, repo string, labels []string) (int, error)
|
||||
GetRunnerStats(ctx context.Context, giteaURL, authToken string, scope RunnerGroupScope, org, repo string, labels []string) (*RunnerStats, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Implementation Details
|
||||
### 5.2 Logic
|
||||
|
||||
- **Endpoint**: `/api/v1/repos/{owner}/{repo}/actions/runs`
|
||||
- **Query Params**: `status=queued`
|
||||
- **Filtering**:
|
||||
- The API might return all queued runs.
|
||||
- The client must filter these runs locally to ensure they match the `labels` defined in the RunnerGroup CR.
|
||||
- _Note_: Gitea API might not support filtering by labels directly in the list endpoint, so client-side filtering is necessary.
|
||||
1. **Endpoints**:
|
||||
- Repo/Org/Global: Uses `/actions/jobs` endpoints.
|
||||
- User: Fetches repos via `/users/{user}/repos`, then queries `/actions/jobs` for each repo.
|
||||
2. **Fetching**:
|
||||
- Fetches jobs with `status=queued`, `waiting`, `pending`.
|
||||
- Handles pagination (fetches all pages).
|
||||
3. **Filtering**:
|
||||
- Iterates through fetched jobs.
|
||||
- **Matches Labels**: Checks if the job's required labels are a subset of the runner's supported labels (effective labels).
|
||||
- Supports exact match (`linux` == `linux`)
|
||||
- Supports schema match (`ubuntu-latest` matches `ubuntu-latest:docker://...`)
|
||||
- Returns only matching jobs in `QueuedJobs`.
|
||||
|
||||
## 6. Configuration & Deployment
|
||||
## 6. Testing Strategy
|
||||
|
||||
### 6.1 Dockerfile
|
||||
|
||||
Standard Operator SDK Dockerfile. Ensure the base image is minimal (e.g., `gcr.io/distroless/static:nonroot`).
|
||||
|
||||
### 6.2 Kustomize
|
||||
|
||||
Update `config/default/kustomization.yaml` to include the CRD and RBAC configurations.
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
1. **Unit Tests**:
|
||||
- Test `constructJobForRunnerGroup` to ensure Env vars and Labels are set correctly.
|
||||
- Test Gitea Client response parsing.
|
||||
2. **Integration Tests (EnvTest)**:
|
||||
- Spin up a local k8s control plane.
|
||||
- Create a `RunnerGroup` CR.
|
||||
- Verify the controller creates a `Job` when the mocked Gitea client returns queued jobs.
|
||||
- Verify the controller respects `MaxActiveRunners`.
|
||||
1. **Unit Tests (`internal/gitea/client_test.go`)**:
|
||||
- Mock Gitea API server.
|
||||
- Verify `GetRunnerStats` correctly parses JSON and handles pagination.
|
||||
- Verify label matching logic (subset, schema matching).
|
||||
2. **Controller Tests**:
|
||||
- Verify `SpawnedJobsCache` prevents double scheduling.
|
||||
- Verify TTL logic allows retries for stuck jobs.
|
||||
- Verify `getEffectiveLabels` merging logic.
|
||||
|
||||
Reference in New Issue
Block a user