// Package taskworker reconciles assigned Gitea tasks against an executor backend. package taskworker import ( "context" "errors" "strconv" "git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskidentity" "git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskscheduler" ) // Phase is observed from Kubernetes or OpenSandbox, never stored by the worker. type Phase string const ( PhasePending Phase = "pending" PhaseRunning Phase = "running" PhaseSucceeded Phase = "succeeded" PhaseFailed Phase = "failed" ) // Executor is the backend resource discovered by assignment ID. type Executor struct { Name string IdentityTarget string Phase Phase } // Metadata is persisted on the backend resource. Labels remain query-safe; // annotations retain complete identity context. type Metadata struct { Labels map[string]string Annotations map[string]string } // Backend is implemented by the native Pod and OpenSandbox adapters. // Every method must be idempotent. type Backend interface { Find(context.Context, string) (*Executor, error) Create(context.Context, taskscheduler.Assignment, Metadata) (*Executor, error) BindIdentity(context.Context, *Executor, taskidentity.Identity) error Delete(context.Context, *Executor) error } // TaskState uses Gitea as the authority for whether an assigned task has // already reached a terminal state. type TaskState interface { Terminal(context.Context, int64) (bool, error) Report(context.Context, int64, Phase) error } // Worker has no correctness-critical in-memory state. Handle may be called // again for the same assignment after any operation. type Worker struct { Backend Backend Tasks TaskState } // Handle performs one reconciliation. Done means the queue message may be // acknowledged. A false result should remain pending and be reconciled again. func (w Worker) Handle(ctx context.Context, assignment taskscheduler.Assignment) (done bool, err error) { if w.Backend == nil || w.Tasks == nil { return false, errors.New("backend and Gitea task state are required") } if assignment.ID == "" || assignment.Task == nil { return false, errors.New("valid assignment is required") } terminal, err := w.Tasks.Terminal(ctx, assignment.Task.GetId()) if err != nil { return false, err } executor, err := w.Backend.Find(ctx, assignment.ID) if err != nil { return false, err } if terminal { if executor != nil { if err := w.Backend.Delete(ctx, executor); err != nil { return false, err } } return true, nil } if executor == nil { executor, err = w.Backend.Create(ctx, assignment, BackendMetadata(assignment)) if err != nil { return false, err } } if executor.IdentityTarget == "" { return false, nil } if err := w.Backend.BindIdentity(ctx, executor, assignment.Identity); err != nil { return false, err } switch executor.Phase { case PhaseSucceeded, PhaseFailed: // Reporting first closes the delete-before-ack crash window: after a // restart Gitea prevents this assignment from executing a second time. if err := w.Tasks.Report(ctx, assignment.Task.GetId(), executor.Phase); err != nil { return false, err } if err := w.Backend.Delete(ctx, executor); err != nil { return false, err } return true, nil default: return false, nil } } // BackendMetadata is the shared metadata contract for Pods and OpenSandbox. func BackendMetadata(assignment taskscheduler.Assignment) Metadata { return Metadata{ Labels: map[string]string{ "ci.ddupan.top/assignment-id": assignment.ID, "ci.ddupan.top/task-id": strconv.FormatInt(assignment.Task.GetId(), 10), }, Annotations: map[string]string{ "ci.ddupan.top/repository": assignment.Identity.Repository, "ci.ddupan.top/job-key": assignment.Identity.Task, "ci.ddupan.top/spiffe-id": assignment.Identity.SPIFFEID, }, } }