Initial scaffold: API service, K8s operator, and CRDs

Forgebot is a K8s operator + API service for dispatching AI agent
jobs from git forge commands. Includes:

- CRDs: AgentPool, AgentTask, ProviderQueue, RepositoryBinding
- API server with webhook handler, task queue, and comment proxy
- Operator controllers for task scheduling and job management
- Gitea provider with webhook parsing and signature verification
- PostgreSQL database with auto-migration
- Woodpecker CI pipelines and multi-stage Dockerfiles
This commit is contained in:
2026-06-08 22:49:18 +10:00
parent fd1a4956ed
commit 49d514c050
46 changed files with 3139 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type AgentPoolSpec struct {
Model string `json:"model"`
Endpoint string `json:"endpoint"`
MaxConcurrent int `json:"maxConcurrent"`
Image string `json:"image"`
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
CredentialSecretRef corev1.LocalObjectReference `json:"credentialSecretRef"`
ServiceAccountName string `json:"serviceAccountName,omitempty"`
}
type AgentPoolStatus struct {
ActiveJobs int `json:"activeJobs"`
TotalRun int64 `json:"totalRun"`
LastActivity string `json:"lastActivity,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Model",type=string,JSONPath=`.spec.model`
// +kubebuilder:printcolumn:name="Max",type=integer,JSONPath=`.spec.maxConcurrent`
// +kubebuilder:printcolumn:name="Active",type=integer,JSONPath=`.status.activeJobs`
type AgentPool struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec AgentPoolSpec `json:"spec,omitempty"`
Status AgentPoolStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type AgentPoolList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []AgentPool `json:"items"`
}
func init() {
SchemeBuilder.Register(&AgentPool{}, &AgentPoolList{})
}
+67
View File
@@ -0,0 +1,67 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type TaskPhase string
const (
TaskPending TaskPhase = "Pending"
TaskRunning TaskPhase = "Running"
TaskSucceeded TaskPhase = "Succeeded"
TaskFailed TaskPhase = "Failed"
)
type TaskContext struct {
IssueNumber int `json:"issueNumber,omitempty"`
PRNumber int `json:"prNumber,omitempty"`
CommentID int64 `json:"commentID,omitempty"`
Body string `json:"body,omitempty"`
Author string `json:"author"`
}
type AgentTaskSpec struct {
PoolRef string `json:"poolRef"`
Command string `json:"command"`
Skill string `json:"skill,omitempty"`
Repository string `json:"repository"`
Ref string `json:"ref"`
Context TaskContext `json:"context"`
ExtraTools []string `json:"extraTools,omitempty"`
ParentTaskRef string `json:"parentTaskRef,omitempty"`
}
type AgentTaskStatus struct {
Phase TaskPhase `json:"phase,omitempty"`
JobName string `json:"jobName,omitempty"`
StartTime *metav1.Time `json:"startTime,omitempty"`
EndTime *metav1.Time `json:"endTime,omitempty"`
Message string `json:"message,omitempty"`
ChildTasks []string `json:"childTasks,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Command",type=string,JSONPath=`.spec.command`
// +kubebuilder:printcolumn:name="Repo",type=string,JSONPath=`.spec.repository`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
type AgentTask struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec AgentTaskSpec `json:"spec,omitempty"`
Status AgentTaskStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type AgentTaskList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []AgentTask `json:"items"`
}
func init() {
SchemeBuilder.Register(&AgentTask{}, &AgentTaskList{})
}
+4
View File
@@ -0,0 +1,4 @@
// Package v1alpha1 contains API Schema definitions for forgebot v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=forgebot.unkin.net
package v1alpha1
+12
View File
@@ -0,0 +1,12 @@
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
GroupVersion = schema.GroupVersion{Group: "forgebot.unkin.net", Version: "v1alpha1"}
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
AddToScheme = SchemeBuilder.AddToScheme
)
+42
View File
@@ -0,0 +1,42 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type ProviderQueueSpec struct {
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
PollInterval string `json:"pollInterval"`
CredentialSecretRef corev1.LocalObjectReference `json:"credentialSecretRef"`
}
type ProviderQueueStatus struct {
LastPoll *metav1.Time `json:"lastPoll,omitempty"`
TasksCreated int64 `json:"tasksCreated"`
LastError string `json:"lastError,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.provider`
// +kubebuilder:printcolumn:name="Interval",type=string,JSONPath=`.spec.pollInterval`
type ProviderQueue struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ProviderQueueSpec `json:"spec,omitempty"`
Status ProviderQueueStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type ProviderQueueList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ProviderQueue `json:"items"`
}
func init() {
SchemeBuilder.Register(&ProviderQueue{}, &ProviderQueueList{})
}
+48
View File
@@ -0,0 +1,48 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type SkillMapping struct {
Command string `json:"command"`
Skill string `json:"skill"`
}
type RepositoryBindingSpec struct {
Repository string `json:"repository"`
ProviderQueueRef string `json:"providerQueueRef"`
AgentPoolRef string `json:"agentPoolRef"`
AllowedUsers []string `json:"allowedUsers,omitempty"`
AllowedCommands []string `json:"allowedCommands,omitempty"`
SkillMapping []SkillMapping `json:"skillMapping,omitempty"`
}
type RepositoryBindingStatus struct {
WebhookRegistered bool `json:"webhookRegistered"`
LastError string `json:"lastError,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Repo",type=string,JSONPath=`.spec.repository`
// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.agentPoolRef`
// +kubebuilder:printcolumn:name="Webhook",type=boolean,JSONPath=`.status.webhookRegistered`
type RepositoryBinding struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec RepositoryBindingSpec `json:"spec,omitempty"`
Status RepositoryBindingStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type RepositoryBindingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []RepositoryBinding `json:"items"`
}
func init() {
SchemeBuilder.Register(&RepositoryBinding{}, &RepositoryBindingList{})
}
+436
View File
@@ -0,0 +1,436 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentPool) DeepCopyInto(out *AgentPool) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPool.
func (in *AgentPool) DeepCopy() *AgentPool {
if in == nil {
return nil
}
out := new(AgentPool)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AgentPool) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentPoolList) DeepCopyInto(out *AgentPoolList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]AgentPool, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPoolList.
func (in *AgentPoolList) DeepCopy() *AgentPoolList {
if in == nil {
return nil
}
out := new(AgentPoolList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AgentPoolList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentPoolSpec) DeepCopyInto(out *AgentPoolSpec) {
*out = *in
in.Resources.DeepCopyInto(&out.Resources)
out.CredentialSecretRef = in.CredentialSecretRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPoolSpec.
func (in *AgentPoolSpec) DeepCopy() *AgentPoolSpec {
if in == nil {
return nil
}
out := new(AgentPoolSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentPoolStatus) DeepCopyInto(out *AgentPoolStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentPoolStatus.
func (in *AgentPoolStatus) DeepCopy() *AgentPoolStatus {
if in == nil {
return nil
}
out := new(AgentPoolStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentTask) DeepCopyInto(out *AgentTask) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentTask.
func (in *AgentTask) DeepCopy() *AgentTask {
if in == nil {
return nil
}
out := new(AgentTask)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AgentTask) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentTaskList) DeepCopyInto(out *AgentTaskList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]AgentTask, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentTaskList.
func (in *AgentTaskList) DeepCopy() *AgentTaskList {
if in == nil {
return nil
}
out := new(AgentTaskList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AgentTaskList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentTaskSpec) DeepCopyInto(out *AgentTaskSpec) {
*out = *in
out.Context = in.Context
if in.ExtraTools != nil {
in, out := &in.ExtraTools, &out.ExtraTools
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentTaskSpec.
func (in *AgentTaskSpec) DeepCopy() *AgentTaskSpec {
if in == nil {
return nil
}
out := new(AgentTaskSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AgentTaskStatus) DeepCopyInto(out *AgentTaskStatus) {
*out = *in
if in.StartTime != nil {
in, out := &in.StartTime, &out.StartTime
*out = (*in).DeepCopy()
}
if in.EndTime != nil {
in, out := &in.EndTime, &out.EndTime
*out = (*in).DeepCopy()
}
if in.ChildTasks != nil {
in, out := &in.ChildTasks, &out.ChildTasks
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentTaskStatus.
func (in *AgentTaskStatus) DeepCopy() *AgentTaskStatus {
if in == nil {
return nil
}
out := new(AgentTaskStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderQueue) DeepCopyInto(out *ProviderQueue) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderQueue.
func (in *ProviderQueue) DeepCopy() *ProviderQueue {
if in == nil {
return nil
}
out := new(ProviderQueue)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProviderQueue) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderQueueList) DeepCopyInto(out *ProviderQueueList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ProviderQueue, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderQueueList.
func (in *ProviderQueueList) DeepCopy() *ProviderQueueList {
if in == nil {
return nil
}
out := new(ProviderQueueList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProviderQueueList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderQueueSpec) DeepCopyInto(out *ProviderQueueSpec) {
*out = *in
out.CredentialSecretRef = in.CredentialSecretRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderQueueSpec.
func (in *ProviderQueueSpec) DeepCopy() *ProviderQueueSpec {
if in == nil {
return nil
}
out := new(ProviderQueueSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderQueueStatus) DeepCopyInto(out *ProviderQueueStatus) {
*out = *in
if in.LastPoll != nil {
in, out := &in.LastPoll, &out.LastPoll
*out = (*in).DeepCopy()
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderQueueStatus.
func (in *ProviderQueueStatus) DeepCopy() *ProviderQueueStatus {
if in == nil {
return nil
}
out := new(ProviderQueueStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RepositoryBinding) DeepCopyInto(out *RepositoryBinding) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryBinding.
func (in *RepositoryBinding) DeepCopy() *RepositoryBinding {
if in == nil {
return nil
}
out := new(RepositoryBinding)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RepositoryBinding) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RepositoryBindingList) DeepCopyInto(out *RepositoryBindingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]RepositoryBinding, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryBindingList.
func (in *RepositoryBindingList) DeepCopy() *RepositoryBindingList {
if in == nil {
return nil
}
out := new(RepositoryBindingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RepositoryBindingList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RepositoryBindingSpec) DeepCopyInto(out *RepositoryBindingSpec) {
*out = *in
if in.AllowedUsers != nil {
in, out := &in.AllowedUsers, &out.AllowedUsers
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.AllowedCommands != nil {
in, out := &in.AllowedCommands, &out.AllowedCommands
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.SkillMapping != nil {
in, out := &in.SkillMapping, &out.SkillMapping
*out = make([]SkillMapping, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryBindingSpec.
func (in *RepositoryBindingSpec) DeepCopy() *RepositoryBindingSpec {
if in == nil {
return nil
}
out := new(RepositoryBindingSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RepositoryBindingStatus) DeepCopyInto(out *RepositoryBindingStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryBindingStatus.
func (in *RepositoryBindingStatus) DeepCopy() *RepositoryBindingStatus {
if in == nil {
return nil
}
out := new(RepositoryBindingStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SkillMapping) DeepCopyInto(out *SkillMapping) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillMapping.
func (in *SkillMapping) DeepCopy() *SkillMapping {
if in == nil {
return nil
}
out := new(SkillMapping)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TaskContext) DeepCopyInto(out *TaskContext) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TaskContext.
func (in *TaskContext) DeepCopy() *TaskContext {
if in == nil {
return nil
}
out := new(TaskContext)
in.DeepCopyInto(out)
return out
}