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:
@@ -0,0 +1 @@
|
||||
bin/
|
||||
@@ -0,0 +1,17 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: docker-build-api
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo: git.unkin.net/unkin/forgebot-api
|
||||
dockerfile: Dockerfile.api
|
||||
dry_run: true
|
||||
|
||||
- name: docker-build-operator
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo: git.unkin.net/unkin/forgebot-operator
|
||||
dockerfile: Dockerfile.operator
|
||||
dry_run: true
|
||||
@@ -0,0 +1,30 @@
|
||||
when:
|
||||
- event: tag
|
||||
ref: refs/tags/v*
|
||||
|
||||
steps:
|
||||
- name: docker-api
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: git.unkin.net
|
||||
repo: git.unkin.net/unkin/forgebot-api
|
||||
dockerfile: Dockerfile.api
|
||||
username: droneci
|
||||
password:
|
||||
from_secret: DRONECI_PASSWORD
|
||||
tags:
|
||||
- ${CI_COMMIT_TAG}
|
||||
- latest
|
||||
|
||||
- name: docker-operator
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: git.unkin.net
|
||||
repo: git.unkin.net/unkin/forgebot-operator
|
||||
dockerfile: Dockerfile.operator
|
||||
username: droneci
|
||||
password:
|
||||
from_secret: DRONECI_PASSWORD
|
||||
tags:
|
||||
- ${CI_COMMIT_TAG}
|
||||
- latest
|
||||
@@ -0,0 +1,9 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: pre-commit
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- test -z "$(gofmt -l .)"
|
||||
- go vet ./...
|
||||
@@ -0,0 +1,8 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- go test -race -count=1 ./pkg/... ./internal/... ./api/...
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o forgebot-api ./cmd/api
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
COPY --from=builder /build/forgebot-api /usr/local/bin/forgebot-api
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["forgebot-api"]
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o forgebot-operator ./cmd/operator
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
COPY --from=builder /build/forgebot-operator /usr/local/bin/forgebot-operator
|
||||
|
||||
ENTRYPOINT ["forgebot-operator"]
|
||||
@@ -0,0 +1,53 @@
|
||||
.PHONY: build test lint fmt generate docker-api docker-operator clean tidy
|
||||
|
||||
BINARY_API := bin/forgebot-api
|
||||
BINARY_OP := bin/forgebot-operator
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev")
|
||||
|
||||
build: tidy
|
||||
go build -ldflags="-s -w" -o $(BINARY_API) ./cmd/api
|
||||
go build -ldflags="-s -w" -o $(BINARY_OP) ./cmd/operator
|
||||
|
||||
test:
|
||||
go test -race -count=1 ./pkg/... ./internal/... ./api/...
|
||||
|
||||
lint:
|
||||
go vet ./...
|
||||
|
||||
fmt:
|
||||
gofmt -w .
|
||||
|
||||
generate:
|
||||
controller-gen object paths="./api/..."
|
||||
controller-gen crd paths="./api/..." output:crd:artifacts:config=config/crd/bases
|
||||
controller-gen rbac:roleName=forgebot-operator paths="./internal/controller/..." output:rbac:dir=config/rbac
|
||||
|
||||
docker-api:
|
||||
docker build -t forgebot-api:$(VERSION) -f Dockerfile.api .
|
||||
|
||||
docker-operator:
|
||||
docker build -t forgebot-operator:$(VERSION) -f Dockerfile.operator .
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
|
||||
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
|
||||
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
|
||||
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
|
||||
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
|
||||
|
||||
patch:
|
||||
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
|
||||
minor:
|
||||
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
|
||||
major:
|
||||
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/internal/apiserver"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := apiserver.LoadConfig()
|
||||
if err != nil {
|
||||
slog.Error("failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
srv, err := apiserver.New(cfg)
|
||||
if err != nil {
|
||||
slog.Error("failed to create server", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := srv.Run(ctx); err != nil {
|
||||
slog.Error("server exited with error", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
|
||||
forgebotv1alpha1 "git.unkin.net/unkin/forgebot/api/v1alpha1"
|
||||
"git.unkin.net/unkin/forgebot/internal/controller"
|
||||
)
|
||||
|
||||
var scheme = runtime.NewScheme()
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(forgebotv1alpha1.AddToScheme(scheme))
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var probeAddr string
|
||||
var leaderElect bool
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "metrics endpoint")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "health probe endpoint")
|
||||
flag.BoolVar(&leaderElect, "leader-elect", false, "enable leader election")
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseDevMode(false)))
|
||||
logger := ctrl.Log.WithName("setup")
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
},
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: leaderElect,
|
||||
LeaderElectionID: "forgebot-operator",
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "unable to create manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := controller.SetupAll(mgr); err != nil {
|
||||
logger.Error(err, "unable to setup controllers")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
logger.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
logger.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
logger.Error(err, "manager exited with error")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.17.3
|
||||
name: agentpools.forgebot.unkin.net
|
||||
spec:
|
||||
group: forgebot.unkin.net
|
||||
names:
|
||||
kind: AgentPool
|
||||
listKind: AgentPoolList
|
||||
plural: agentpools
|
||||
singular: agentpool
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.model
|
||||
name: Model
|
||||
type: string
|
||||
- jsonPath: .spec.maxConcurrent
|
||||
name: Max
|
||||
type: integer
|
||||
- jsonPath: .status.activeJobs
|
||||
name: Active
|
||||
type: integer
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
credentialSecretRef:
|
||||
description: |-
|
||||
LocalObjectReference contains enough information to let you locate the
|
||||
referenced object inside the same namespace.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
endpoint:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
maxConcurrent:
|
||||
type: integer
|
||||
model:
|
||||
type: string
|
||||
resources:
|
||||
description: ResourceRequirements describes the compute resource requirements.
|
||||
properties:
|
||||
claims:
|
||||
description: |-
|
||||
Claims lists the names of resources, defined in spec.resourceClaims,
|
||||
that are used by this container.
|
||||
|
||||
This field depends on the
|
||||
DynamicResourceAllocation feature gate.
|
||||
|
||||
This field is immutable. It can only be set for containers.
|
||||
items:
|
||||
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name must match the name of one entry in pod.spec.resourceClaims of
|
||||
the Pod where this field is used. It makes that resource available
|
||||
inside a container.
|
||||
type: string
|
||||
request:
|
||||
description: |-
|
||||
Request is the name chosen for a request in the referenced claim.
|
||||
If empty, everything from the claim is made available, otherwise
|
||||
only the result of this request.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
limits:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: |-
|
||||
Limits describes the maximum amount of compute resources allowed.
|
||||
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
type: object
|
||||
requests:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: |-
|
||||
Requests describes the minimum amount of compute resources required.
|
||||
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
|
||||
otherwise to an implementation-defined value. Requests cannot exceed Limits.
|
||||
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
type: object
|
||||
type: object
|
||||
serviceAccountName:
|
||||
type: string
|
||||
required:
|
||||
- credentialSecretRef
|
||||
- endpoint
|
||||
- image
|
||||
- maxConcurrent
|
||||
- model
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
activeJobs:
|
||||
type: integer
|
||||
lastActivity:
|
||||
type: string
|
||||
totalRun:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- activeJobs
|
||||
- totalRun
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.17.3
|
||||
name: agenttasks.forgebot.unkin.net
|
||||
spec:
|
||||
group: forgebot.unkin.net
|
||||
names:
|
||||
kind: AgentTask
|
||||
listKind: AgentTaskList
|
||||
plural: agenttasks
|
||||
singular: agenttask
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.command
|
||||
name: Command
|
||||
type: string
|
||||
- jsonPath: .spec.repository
|
||||
name: Repo
|
||||
type: string
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
context:
|
||||
properties:
|
||||
author:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
commentID:
|
||||
format: int64
|
||||
type: integer
|
||||
issueNumber:
|
||||
type: integer
|
||||
prNumber:
|
||||
type: integer
|
||||
required:
|
||||
- author
|
||||
type: object
|
||||
extraTools:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
parentTaskRef:
|
||||
type: string
|
||||
poolRef:
|
||||
type: string
|
||||
ref:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
skill:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
- context
|
||||
- poolRef
|
||||
- ref
|
||||
- repository
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
childTasks:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
endTime:
|
||||
format: date-time
|
||||
type: string
|
||||
jobName:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
phase:
|
||||
type: string
|
||||
startTime:
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.17.3
|
||||
name: providerqueues.forgebot.unkin.net
|
||||
spec:
|
||||
group: forgebot.unkin.net
|
||||
names:
|
||||
kind: ProviderQueue
|
||||
listKind: ProviderQueueList
|
||||
plural: providerqueues
|
||||
singular: providerqueue
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.provider
|
||||
name: Provider
|
||||
type: string
|
||||
- jsonPath: .spec.pollInterval
|
||||
name: Interval
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
credentialSecretRef:
|
||||
description: |-
|
||||
LocalObjectReference contains enough information to let you locate the
|
||||
referenced object inside the same namespace.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
endpoint:
|
||||
type: string
|
||||
pollInterval:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
required:
|
||||
- credentialSecretRef
|
||||
- endpoint
|
||||
- pollInterval
|
||||
- provider
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
lastError:
|
||||
type: string
|
||||
lastPoll:
|
||||
format: date-time
|
||||
type: string
|
||||
tasksCreated:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- tasksCreated
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.17.3
|
||||
name: repositorybindings.forgebot.unkin.net
|
||||
spec:
|
||||
group: forgebot.unkin.net
|
||||
names:
|
||||
kind: RepositoryBinding
|
||||
listKind: RepositoryBindingList
|
||||
plural: repositorybindings
|
||||
singular: repositorybinding
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.repository
|
||||
name: Repo
|
||||
type: string
|
||||
- jsonPath: .spec.agentPoolRef
|
||||
name: Pool
|
||||
type: string
|
||||
- jsonPath: .status.webhookRegistered
|
||||
name: Webhook
|
||||
type: boolean
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
agentPoolRef:
|
||||
type: string
|
||||
allowedCommands:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
allowedUsers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
providerQueueRef:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
skillMapping:
|
||||
items:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
skill:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
- skill
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- agentPoolRef
|
||||
- providerQueueRef
|
||||
- repository
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
lastError:
|
||||
type: string
|
||||
webhookRegistered:
|
||||
type: boolean
|
||||
required:
|
||||
- webhookRegistered
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
apiVersion: forgebot.unkin.net/v1alpha1
|
||||
kind: AgentPool
|
||||
metadata:
|
||||
name: default-pool
|
||||
namespace: forgebot
|
||||
spec:
|
||||
model: claude-sonnet-4-20250514
|
||||
endpoint: https://litellm.k8s.syd1.au.unkin.net
|
||||
maxConcurrent: 3
|
||||
image: git.unkin.net/unkin/agent-dev:latest
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 4Gi
|
||||
credentialSecretRef:
|
||||
name: litellm-api-key
|
||||
serviceAccountName: forgebot-agent
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
apiVersion: forgebot.unkin.net/v1alpha1
|
||||
kind: AgentTask
|
||||
metadata:
|
||||
name: example-review
|
||||
namespace: forgebot
|
||||
spec:
|
||||
poolRef: default-pool
|
||||
command: review
|
||||
skill: review
|
||||
repository: unkin/artifactapi
|
||||
ref: refs/pull/42/head
|
||||
context:
|
||||
prNumber: 42
|
||||
commentID: 12345
|
||||
body: "/review check for security issues"
|
||||
author: unkinben
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
apiVersion: forgebot.unkin.net/v1alpha1
|
||||
kind: ProviderQueue
|
||||
metadata:
|
||||
name: gitea-queue
|
||||
namespace: forgebot
|
||||
spec:
|
||||
provider: gitea
|
||||
endpoint: http://forgebot-api.forgebot.svc.cluster.local/api/v1
|
||||
pollInterval: 30s
|
||||
credentialSecretRef:
|
||||
name: forgebot-api-token
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
apiVersion: forgebot.unkin.net/v1alpha1
|
||||
kind: RepositoryBinding
|
||||
metadata:
|
||||
name: artifactapi
|
||||
namespace: forgebot
|
||||
spec:
|
||||
repository: unkin/artifactapi
|
||||
providerQueueRef: gitea-queue
|
||||
agentPoolRef: default-pool
|
||||
allowedUsers:
|
||||
- unkinben
|
||||
allowedCommands:
|
||||
- plan
|
||||
- review
|
||||
- implement
|
||||
- test
|
||||
- fix
|
||||
skillMapping:
|
||||
- command: plan
|
||||
skill: plan
|
||||
- command: review
|
||||
skill: review
|
||||
- command: implement
|
||||
skill: implement
|
||||
- command: test
|
||||
skill: test
|
||||
- command: fix
|
||||
skill: fix
|
||||
@@ -0,0 +1,78 @@
|
||||
module git.unkin.net/unkin/forgebot
|
||||
|
||||
go 1.25.9
|
||||
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.19.0
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
k8s.io/api v0.34.4
|
||||
k8s.io/apimachinery v0.34.4
|
||||
k8s.io/client-go v0.34.4
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
code.gitea.io/sdk/gitea v0.19.0 h1:8I6s1s4RHgzxiPHhOQdgim1RWIRcr0LVMbHBjBFXq4Y=
|
||||
code.gitea.io/sdk/gitea v0.19.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.4 h1:Z5hsoQcZ2yBjelb9j5JKzCVo9qv9XLkVm5llnqS4h+0=
|
||||
k8s.io/api v0.34.4/go.mod h1:6SaGYuGPkMqqCgg8rPG/OQoCrhgSEV+wWn9v21fDP3o=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.4 h1:C5SiSzLEMyWIk53sSbnk0WlOOyqv/MFnWvuc/d6M+xc=
|
||||
k8s.io/apimachinery v0.34.4/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.4 h1:IXhvzFdm0e897kXtLbeyMpAGzontcShJ/gi/XCCsOLc=
|
||||
k8s.io/client-go v0.34.4/go.mod h1:tXIVJTQabT5QRGlFdxZQFxrIhcGUPpKL5DAc4gSWTE8=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
@@ -0,0 +1,51 @@
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
DBHost string
|
||||
DBPort int
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
DBSSL string
|
||||
WebhookSecret string
|
||||
GiteaURL string
|
||||
GiteaToken string
|
||||
}
|
||||
|
||||
func (c *Config) DatabaseDSN() string {
|
||||
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||
c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName, c.DBSSL)
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
dbPort, err := strconv.Atoi(getenv("DBPORT", "5432"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid DBPORT: %w", err)
|
||||
}
|
||||
return &Config{
|
||||
ListenAddr: getenv("LISTEN_ADDR", ":8000"),
|
||||
DBHost: getenv("DBHOST", "localhost"),
|
||||
DBPort: dbPort,
|
||||
DBUser: getenv("DBUSER", "forgebot"),
|
||||
DBPass: getenv("DBPASS", ""),
|
||||
DBName: getenv("DBNAME", "forgebot"),
|
||||
DBSSL: getenv("DBSSL", "disable"),
|
||||
WebhookSecret: getenv("WEBHOOK_SECRET", ""),
|
||||
GiteaURL: getenv("GITEA_URL", "https://git.unkin.net"),
|
||||
GiteaToken: getenv("GITEA_TOKEN", ""),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/internal/database"
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewHealthHandler(db *database.DB) *HealthHandler {
|
||||
return &HealthHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
|
||||
status := "ok"
|
||||
code := http.StatusOK
|
||||
|
||||
if !h.db.Healthy(r.Context()) {
|
||||
status = "degraded"
|
||||
code = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": status})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/internal/database"
|
||||
"git.unkin.net/unkin/forgebot/internal/provider/gitea"
|
||||
"git.unkin.net/unkin/forgebot/pkg/models"
|
||||
)
|
||||
|
||||
type TasksHandler struct {
|
||||
db *database.DB
|
||||
provider *gitea.Client
|
||||
}
|
||||
|
||||
func NewTasksHandler(db *database.DB, provider *gitea.Client) *TasksHandler {
|
||||
return &TasksHandler{db: db, provider: provider}
|
||||
}
|
||||
|
||||
func (h *TasksHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.URL.Query().Get("status")
|
||||
repository := r.URL.Query().Get("repository")
|
||||
|
||||
tasks, err := h.db.ListTasks(r.Context(), status, repository)
|
||||
if err != nil {
|
||||
slog.Error("failed to list tasks", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if tasks == nil {
|
||||
tasks = []models.Task{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(tasks)
|
||||
}
|
||||
|
||||
func (h *TasksHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.CreateTaskRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Command == "" || req.Repository == "" {
|
||||
http.Error(w, "command and repository are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !models.ValidCommands[req.Command] {
|
||||
http.Error(w, "invalid command", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
task, err := h.db.CreateTask(r.Context(), req)
|
||||
if err != nil {
|
||||
slog.Error("failed to create task", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(task)
|
||||
}
|
||||
|
||||
func (h *TasksHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
task, err := h.db.GetTask(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "task not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(task)
|
||||
}
|
||||
|
||||
func (h *TasksHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var req models.UpdateTaskRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.UpdateTaskStatus(r.Context(), id, req); err != nil {
|
||||
slog.Error("failed to update task", "error", err, "id", id)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *TasksHandler) PostComment(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var req models.CommentRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
task, err := h.db.GetTask(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "task not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(task.Repository, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
http.Error(w, "invalid repository format", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
issueNum := task.IssueNumber
|
||||
if task.PRNumber > 0 {
|
||||
issueNum = task.PRNumber
|
||||
}
|
||||
|
||||
if err := h.provider.PostComment(parts[0], parts[1], issueNum, req.Body); err != nil {
|
||||
slog.Error("failed to post comment", "error", err, "task", id)
|
||||
http.Error(w, "failed to post comment", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/internal/database"
|
||||
"git.unkin.net/unkin/forgebot/internal/provider/gitea"
|
||||
"git.unkin.net/unkin/forgebot/pkg/models"
|
||||
)
|
||||
|
||||
type WebhookHandler struct {
|
||||
db *database.DB
|
||||
provider *gitea.Client
|
||||
webhookSecret string
|
||||
}
|
||||
|
||||
func NewWebhookHandler(db *database.DB, provider *gitea.Client, secret string) *WebhookHandler {
|
||||
return &WebhookHandler{
|
||||
db: db,
|
||||
provider: provider,
|
||||
webhookSecret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebhookHandler) HandleGitea(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
signature := r.Header.Get("X-Gitea-Signature")
|
||||
if !gitea.VerifySignature(body, h.webhookSecret, signature) {
|
||||
http.Error(w, "invalid signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
event, err := h.provider.ParseWebhook(body, h.webhookSecret)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse webhook", "error", err)
|
||||
http.Error(w, "failed to parse webhook", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if event == nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
commands := models.ParseCommands(event.Body)
|
||||
if len(commands) == 0 {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(event.Repository, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
http.Error(w, "invalid repository format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
task, err := h.db.CreateTask(r.Context(), models.CreateTaskRequest{
|
||||
Command: cmd.Name,
|
||||
Repository: event.Repository,
|
||||
Ref: event.Ref,
|
||||
IssueNumber: event.IssueNum,
|
||||
PRNumber: event.PRNum,
|
||||
CommentID: event.CommentID,
|
||||
Body: cmd.Args,
|
||||
Author: event.Author,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("failed to create task", "error", err, "command", cmd.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("task created from webhook",
|
||||
"id", task.ID,
|
||||
"command", cmd.Name,
|
||||
"repository", event.Repository,
|
||||
"author", event.Author,
|
||||
)
|
||||
|
||||
h.provider.AddReaction(parts[0], parts[1], event.CommentID, "eyes")
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/internal/apiserver/handlers"
|
||||
"git.unkin.net/unkin/forgebot/internal/database"
|
||||
"git.unkin.net/unkin/forgebot/internal/provider/gitea"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
cfg *Config
|
||||
router chi.Router
|
||||
db *database.DB
|
||||
provider *gitea.Client
|
||||
}
|
||||
|
||||
func New(cfg *Config) (*Server, error) {
|
||||
db, err := database.New(cfg.DatabaseDSN())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := gitea.NewClient(cfg.GiteaURL, cfg.GiteaToken)
|
||||
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
provider: provider,
|
||||
}
|
||||
s.router = s.routes()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.Timeout(60 * time.Second))
|
||||
|
||||
healthH := handlers.NewHealthHandler(s.db)
|
||||
webhookH := handlers.NewWebhookHandler(s.db, s.provider, s.cfg.WebhookSecret)
|
||||
tasksH := handlers.NewTasksHandler(s.db, s.provider)
|
||||
|
||||
r.Get("/health", healthH.Health)
|
||||
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.Post("/webhook/gitea", webhookH.HandleGitea)
|
||||
|
||||
r.Get("/tasks", tasksH.List)
|
||||
r.Post("/tasks", tasksH.Create)
|
||||
r.Get("/tasks/{id}", tasksH.Get)
|
||||
r.Patch("/tasks/{id}", tasksH.UpdateStatus)
|
||||
r.Post("/tasks/{id}/comment", tasksH.PostComment)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
httpServer := &http.Server{
|
||||
Addr: s.cfg.ListenAddr,
|
||||
Handler: s.router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 300 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
slog.Info("shutting down server")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = httpServer.Shutdown(shutdownCtx)
|
||||
}()
|
||||
|
||||
slog.Info("starting server", "addr", s.cfg.ListenAddr)
|
||||
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
forgebotv1alpha1 "git.unkin.net/unkin/forgebot/api/v1alpha1"
|
||||
)
|
||||
|
||||
type AgentPoolReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=agentpools,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=agentpools/status,verbs=get;update;patch
|
||||
|
||||
func (r *AgentPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var pool forgebotv1alpha1.AgentPool
|
||||
if err := r.Get(ctx, req.NamespacedName, &pool); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
var taskList forgebotv1alpha1.AgentTaskList
|
||||
if err := r.List(ctx, &taskList, client.InNamespace(req.Namespace)); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
active := 0
|
||||
for _, task := range taskList.Items {
|
||||
if task.Spec.PoolRef == pool.Name && task.Status.Phase == forgebotv1alpha1.TaskRunning {
|
||||
active++
|
||||
}
|
||||
}
|
||||
|
||||
if pool.Status.ActiveJobs != active {
|
||||
pool.Status.ActiveJobs = active
|
||||
if err := r.Status().Update(ctx, &pool); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("updated pool status", "pool", pool.Name, "active", active)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *AgentPoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&forgebotv1alpha1.AgentPool{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
forgebotv1alpha1 "git.unkin.net/unkin/forgebot/api/v1alpha1"
|
||||
)
|
||||
|
||||
type AgentTaskReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=agenttasks,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=agenttasks/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;delete
|
||||
|
||||
func (r *AgentTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var task forgebotv1alpha1.AgentTask
|
||||
if err := r.Get(ctx, req.NamespacedName, &task); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
switch task.Status.Phase {
|
||||
case forgebotv1alpha1.TaskPending, "":
|
||||
return r.handlePending(ctx, &task)
|
||||
case forgebotv1alpha1.TaskRunning:
|
||||
return r.handleRunning(ctx, &task)
|
||||
default:
|
||||
logger.V(1).Info("task in terminal state", "phase", task.Status.Phase)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AgentTaskReconciler) handlePending(ctx context.Context, task *forgebotv1alpha1.AgentTask) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var pool forgebotv1alpha1.AgentPool
|
||||
if err := r.Get(ctx, client.ObjectKey{Namespace: task.Namespace, Name: task.Spec.PoolRef}, &pool); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("get pool %s: %w", task.Spec.PoolRef, err)
|
||||
}
|
||||
|
||||
if pool.Status.ActiveJobs >= pool.Spec.MaxConcurrent {
|
||||
logger.Info("pool at capacity, requeueing", "pool", pool.Name, "active", pool.Status.ActiveJobs)
|
||||
return ctrl.Result{RequeueAfter: 10_000_000_000}, nil // 10s
|
||||
}
|
||||
|
||||
job := r.buildJob(task, &pool)
|
||||
if err := ctrl.SetControllerReference(task, job, r.Scheme); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if err := r.Create(ctx, job); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("create job: %w", err)
|
||||
}
|
||||
|
||||
now := metav1.Now()
|
||||
task.Status.Phase = forgebotv1alpha1.TaskRunning
|
||||
task.Status.JobName = job.Name
|
||||
task.Status.StartTime = &now
|
||||
if err := r.Status().Update(ctx, task); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("created job for task", "job", job.Name, "task", task.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *AgentTaskReconciler) handleRunning(ctx context.Context, task *forgebotv1alpha1.AgentTask) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var job batchv1.Job
|
||||
if err := r.Get(ctx, client.ObjectKey{Namespace: task.Namespace, Name: task.Status.JobName}, &job); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if job.Status.Succeeded > 0 {
|
||||
now := metav1.Now()
|
||||
task.Status.Phase = forgebotv1alpha1.TaskSucceeded
|
||||
task.Status.EndTime = &now
|
||||
if err := r.Status().Update(ctx, task); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("task succeeded", "task", task.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if job.Status.Failed > 0 {
|
||||
now := metav1.Now()
|
||||
task.Status.Phase = forgebotv1alpha1.TaskFailed
|
||||
task.Status.EndTime = &now
|
||||
task.Status.Message = "job failed"
|
||||
if err := r.Status().Update(ctx, task); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("task failed", "task", task.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: 15_000_000_000}, nil // 15s
|
||||
}
|
||||
|
||||
func (r *AgentTaskReconciler) buildJob(task *forgebotv1alpha1.AgentTask, pool *forgebotv1alpha1.AgentPool) *batchv1.Job {
|
||||
backoffLimit := int32(0)
|
||||
ttl := int32(3600)
|
||||
|
||||
env := []corev1.EnvVar{
|
||||
{Name: "FORGEBOT_REPO", Value: task.Spec.Repository},
|
||||
{Name: "FORGEBOT_REF", Value: task.Spec.Ref},
|
||||
{Name: "FORGEBOT_COMMAND", Value: task.Spec.Command},
|
||||
{Name: "FORGEBOT_SKILL", Value: task.Spec.Skill},
|
||||
{Name: "FORGEBOT_TASK_ID", Value: task.Name},
|
||||
{Name: "FORGEBOT_MODEL", Value: pool.Spec.Model},
|
||||
{Name: "FORGEBOT_BODY", Value: task.Spec.Context.Body},
|
||||
{Name: "FORGEBOT_AUTHOR", Value: task.Spec.Context.Author},
|
||||
{Name: "FORGEBOT_ISSUE_NUMBER", Value: fmt.Sprintf("%d", task.Spec.Context.IssueNumber)},
|
||||
{Name: "FORGEBOT_PR_NUMBER", Value: fmt.Sprintf("%d", task.Spec.Context.PRNumber)},
|
||||
{Name: "ANTHROPIC_BASE_URL", Value: pool.Spec.Endpoint},
|
||||
}
|
||||
|
||||
if pool.Spec.CredentialSecretRef.Name != "" {
|
||||
env = append(env, corev1.EnvVar{
|
||||
Name: "ANTHROPIC_API_KEY",
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
SecretKeyRef: &corev1.SecretKeySelector{
|
||||
LocalObjectReference: pool.Spec.CredentialSecretRef,
|
||||
Key: "api-key",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("forgebot-%s", task.Name),
|
||||
Namespace: task.Namespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "forgebot",
|
||||
"forgebot.unkin.net/task": task.Name,
|
||||
"forgebot.unkin.net/pool": pool.Name,
|
||||
"forgebot.unkin.net/command": task.Spec.Command,
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
BackoffLimit: &backoffLimit,
|
||||
TTLSecondsAfterFinished: &ttl,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "forgebot",
|
||||
"forgebot.unkin.net/task": task.Name,
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: pool.Spec.ServiceAccountName,
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "agent",
|
||||
Image: pool.Spec.Image,
|
||||
Env: env,
|
||||
Resources: pool.Spec.Resources,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AgentTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&forgebotv1alpha1.AgentTask{}).
|
||||
Owns(&batchv1.Job{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
forgebotv1alpha1 "git.unkin.net/unkin/forgebot/api/v1alpha1"
|
||||
"git.unkin.net/unkin/forgebot/pkg/models"
|
||||
)
|
||||
|
||||
type ProviderQueueReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=providerqueues,verbs=get;list;watch;update;patch
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=providerqueues/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=agenttasks,verbs=create
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=repositorybindings,verbs=get;list;watch
|
||||
|
||||
func (r *ProviderQueueReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var queue forgebotv1alpha1.ProviderQueue
|
||||
if err := r.Get(ctx, req.NamespacedName, &queue); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
pollInterval, err := time.ParseDuration(queue.Spec.PollInterval)
|
||||
if err != nil {
|
||||
pollInterval = 30 * time.Second
|
||||
}
|
||||
|
||||
httpClient := r.HTTPClient
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
}
|
||||
|
||||
resp, err := httpClient.Get(queue.Spec.Endpoint + "/tasks?status=pending")
|
||||
if err != nil {
|
||||
now := metav1.Now()
|
||||
queue.Status.LastPoll = &now
|
||||
queue.Status.LastError = err.Error()
|
||||
_ = r.Status().Update(ctx, &queue)
|
||||
logger.Error(err, "failed to poll API")
|
||||
return ctrl.Result{RequeueAfter: pollInterval}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ctrl.Result{RequeueAfter: pollInterval}, err
|
||||
}
|
||||
|
||||
var tasks []models.Task
|
||||
if err := json.Unmarshal(body, &tasks); err != nil {
|
||||
return ctrl.Result{RequeueAfter: pollInterval}, err
|
||||
}
|
||||
|
||||
var bindings forgebotv1alpha1.RepositoryBindingList
|
||||
if err := r.List(ctx, &bindings, client.InNamespace(req.Namespace)); err != nil {
|
||||
return ctrl.Result{RequeueAfter: pollInterval}, err
|
||||
}
|
||||
|
||||
bindingMap := map[string]*forgebotv1alpha1.RepositoryBinding{}
|
||||
for i := range bindings.Items {
|
||||
b := &bindings.Items[i]
|
||||
if b.Spec.ProviderQueueRef == queue.Name {
|
||||
bindingMap[b.Spec.Repository] = b
|
||||
}
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
binding, ok := bindingMap[task.Repository]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isAllowed(binding, task.Author, task.Command) {
|
||||
continue
|
||||
}
|
||||
|
||||
agentTask := &forgebotv1alpha1.AgentTask{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("task-%s", task.ID[:8]),
|
||||
Namespace: req.Namespace,
|
||||
},
|
||||
Spec: forgebotv1alpha1.AgentTaskSpec{
|
||||
PoolRef: binding.Spec.AgentPoolRef,
|
||||
Command: task.Command,
|
||||
Skill: resolveSkill(binding, task.Command),
|
||||
Repository: task.Repository,
|
||||
Ref: task.Ref,
|
||||
Context: forgebotv1alpha1.TaskContext{
|
||||
IssueNumber: task.IssueNumber,
|
||||
PRNumber: task.PRNumber,
|
||||
CommentID: task.CommentID,
|
||||
Body: task.Body,
|
||||
Author: task.Author,
|
||||
},
|
||||
ExtraTools: task.ExtraTools,
|
||||
ParentTaskRef: task.ParentTaskID,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, agentTask); err != nil {
|
||||
logger.Error(err, "failed to create AgentTask", "task", task.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
queue.Status.TasksCreated++
|
||||
logger.Info("created AgentTask", "task", agentTask.Name, "command", task.Command)
|
||||
}
|
||||
|
||||
now := metav1.Now()
|
||||
queue.Status.LastPoll = &now
|
||||
queue.Status.LastError = ""
|
||||
_ = r.Status().Update(ctx, &queue)
|
||||
|
||||
return ctrl.Result{RequeueAfter: pollInterval}, nil
|
||||
}
|
||||
|
||||
func isAllowed(binding *forgebotv1alpha1.RepositoryBinding, author, command string) bool {
|
||||
if len(binding.Spec.AllowedUsers) > 0 {
|
||||
found := false
|
||||
for _, u := range binding.Spec.AllowedUsers {
|
||||
if u == author {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(binding.Spec.AllowedCommands) > 0 {
|
||||
for _, c := range binding.Spec.AllowedCommands {
|
||||
if c == command {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func resolveSkill(binding *forgebotv1alpha1.RepositoryBinding, command string) string {
|
||||
for _, m := range binding.Spec.SkillMapping {
|
||||
if m.Command == command {
|
||||
return m.Skill
|
||||
}
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func (r *ProviderQueueReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&forgebotv1alpha1.ProviderQueue{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
forgebotv1alpha1 "git.unkin.net/unkin/forgebot/api/v1alpha1"
|
||||
)
|
||||
|
||||
type RepositoryBindingReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=repositorybindings,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=forgebot.unkin.net,resources=repositorybindings/status,verbs=get;update;patch
|
||||
|
||||
func (r *RepositoryBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
var binding forgebotv1alpha1.RepositoryBinding
|
||||
if err := r.Get(ctx, req.NamespacedName, &binding); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// TODO: Validate that referenced pool and queue exist
|
||||
// TODO: Register webhook with Gitea for this repository
|
||||
|
||||
logger.V(1).Info("reconciled binding", "repo", binding.Spec.Repository)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *RepositoryBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&forgebotv1alpha1.RepositoryBinding{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
)
|
||||
|
||||
func SetupAll(mgr ctrl.Manager) error {
|
||||
if err := (&AgentPoolReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := (&AgentTaskReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := (&ProviderQueueReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := (&RepositoryBindingReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package database
|
||||
|
||||
import "context"
|
||||
|
||||
func (db *DB) migrate() error {
|
||||
_, err := db.Pool.Exec(context.Background(), `
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
parent_task_id UUID REFERENCES tasks(id),
|
||||
command TEXT NOT NULL,
|
||||
skill TEXT NOT NULL DEFAULT '',
|
||||
repository TEXT NOT NULL,
|
||||
ref TEXT NOT NULL,
|
||||
issue_number INTEGER NOT NULL DEFAULT 0,
|
||||
pr_number INTEGER NOT NULL DEFAULT 0,
|
||||
comment_id BIGINT NOT NULL DEFAULT 0,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
author TEXT NOT NULL,
|
||||
extra_tools TEXT[] NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
pool_ref TEXT NOT NULL DEFAULT '',
|
||||
job_name TEXT NOT NULL DEFAULT '',
|
||||
result TEXT NOT NULL DEFAULT '',
|
||||
error_message TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_repository ON tasks(repository);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func New(dsn string) (*DB, error) {
|
||||
pool, err := pgxpool.New(context.Background(), dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to postgres: %w", err)
|
||||
}
|
||||
if err := pool.Ping(context.Background()); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("ping postgres: %w", err)
|
||||
}
|
||||
db := &DB{Pool: pool}
|
||||
if err := db.migrate(); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (db *DB) Close() {
|
||||
db.Pool.Close()
|
||||
}
|
||||
|
||||
func (db *DB) Healthy(ctx context.Context) bool {
|
||||
return db.Pool.Ping(ctx) == nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/pkg/models"
|
||||
)
|
||||
|
||||
func (db *DB) CreateTask(ctx context.Context, req models.CreateTaskRequest) (*models.Task, error) {
|
||||
task := &models.Task{
|
||||
Command: req.Command,
|
||||
Skill: req.Skill,
|
||||
Repository: req.Repository,
|
||||
Ref: req.Ref,
|
||||
IssueNumber: req.IssueNumber,
|
||||
PRNumber: req.PRNumber,
|
||||
CommentID: req.CommentID,
|
||||
Body: req.Body,
|
||||
Author: req.Author,
|
||||
ExtraTools: req.ExtraTools,
|
||||
ParentTaskID: req.ParentTaskID,
|
||||
PoolRef: req.PoolRef,
|
||||
Status: models.StatusPending,
|
||||
}
|
||||
if task.ExtraTools == nil {
|
||||
task.ExtraTools = []string{}
|
||||
}
|
||||
|
||||
err := db.Pool.QueryRow(ctx, `
|
||||
INSERT INTO tasks (
|
||||
parent_task_id, command, skill, repository, ref,
|
||||
issue_number, pr_number, comment_id, body, author,
|
||||
extra_tools, pool_ref
|
||||
) VALUES (
|
||||
NULLIF($1, ''), $2, $3, $4, $5,
|
||||
$6, $7, $8, $9, $10,
|
||||
$11, $12
|
||||
) RETURNING id, created_at`,
|
||||
task.ParentTaskID, task.Command, task.Skill, task.Repository, task.Ref,
|
||||
task.IssueNumber, task.PRNumber, task.CommentID, task.Body, task.Author,
|
||||
task.ExtraTools, task.PoolRef,
|
||||
).Scan(&task.ID, &task.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTask(ctx context.Context, id string) (*models.Task, error) {
|
||||
task := &models.Task{}
|
||||
var parentID *string
|
||||
var startedAt, completedAt *time.Time
|
||||
|
||||
err := db.Pool.QueryRow(ctx, `
|
||||
SELECT id, parent_task_id, command, skill, repository, ref,
|
||||
issue_number, pr_number, comment_id, body, author,
|
||||
extra_tools, status, pool_ref, job_name, result, error_message,
|
||||
created_at, started_at, completed_at
|
||||
FROM tasks WHERE id = $1`, id,
|
||||
).Scan(
|
||||
&task.ID, &parentID, &task.Command, &task.Skill, &task.Repository, &task.Ref,
|
||||
&task.IssueNumber, &task.PRNumber, &task.CommentID, &task.Body, &task.Author,
|
||||
&task.ExtraTools, &task.Status, &task.PoolRef, &task.JobName, &task.Result, &task.ErrorMessage,
|
||||
&task.CreatedAt, &startedAt, &completedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parentID != nil {
|
||||
task.ParentTaskID = *parentID
|
||||
}
|
||||
task.StartedAt = startedAt
|
||||
task.CompletedAt = completedAt
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (db *DB) ListPendingTasks(ctx context.Context) ([]models.Task, error) {
|
||||
return db.listTasksByStatus(ctx, string(models.StatusPending))
|
||||
}
|
||||
|
||||
func (db *DB) listTasksByStatus(ctx context.Context, status string) ([]models.Task, error) {
|
||||
rows, err := db.Pool.Query(ctx, `
|
||||
SELECT id, parent_task_id, command, skill, repository, ref,
|
||||
issue_number, pr_number, comment_id, body, author,
|
||||
extra_tools, status, pool_ref, job_name, result, error_message,
|
||||
created_at, started_at, completed_at
|
||||
FROM tasks WHERE status = $1
|
||||
ORDER BY created_at ASC`, status,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanTasks(rows)
|
||||
}
|
||||
|
||||
func (db *DB) ListTasks(ctx context.Context, status string, repository string) ([]models.Task, error) {
|
||||
query := `SELECT id, parent_task_id, command, skill, repository, ref,
|
||||
issue_number, pr_number, comment_id, body, author,
|
||||
extra_tools, status, pool_ref, job_name, result, error_message,
|
||||
created_at, started_at, completed_at
|
||||
FROM tasks WHERE 1=1`
|
||||
args := []any{}
|
||||
argIdx := 1
|
||||
|
||||
if status != "" {
|
||||
query += " AND status = $" + itoa(argIdx)
|
||||
args = append(args, status)
|
||||
argIdx++
|
||||
}
|
||||
if repository != "" {
|
||||
query += " AND repository = $" + itoa(argIdx)
|
||||
args = append(args, repository)
|
||||
argIdx++
|
||||
}
|
||||
query += " ORDER BY created_at DESC LIMIT 100"
|
||||
|
||||
rows, err := db.Pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanTasks(rows)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateTaskStatus(ctx context.Context, id string, req models.UpdateTaskRequest) error {
|
||||
if req.Status == models.StatusRunning {
|
||||
_, err := db.Pool.Exec(ctx, `
|
||||
UPDATE tasks SET status = $2, job_name = COALESCE(NULLIF($3, ''), job_name), started_at = NOW()
|
||||
WHERE id = $1`, id, req.Status, req.JobName)
|
||||
return err
|
||||
}
|
||||
if req.Status == models.StatusSucceeded || req.Status == models.StatusFailed {
|
||||
_, err := db.Pool.Exec(ctx, `
|
||||
UPDATE tasks SET status = $2, result = COALESCE(NULLIF($3, ''), result),
|
||||
error_message = COALESCE(NULLIF($4, ''), error_message), completed_at = NOW()
|
||||
WHERE id = $1`, id, req.Status, req.Message, req.ErrorMessage)
|
||||
return err
|
||||
}
|
||||
_, err := db.Pool.Exec(ctx, `UPDATE tasks SET status = $2 WHERE id = $1`, id, req.Status)
|
||||
return err
|
||||
}
|
||||
|
||||
func scanTasks(rows pgx.Rows) ([]models.Task, error) {
|
||||
var tasks []models.Task
|
||||
for rows.Next() {
|
||||
var task models.Task
|
||||
var parentID *string
|
||||
var startedAt, completedAt *time.Time
|
||||
|
||||
err := rows.Scan(
|
||||
&task.ID, &parentID, &task.Command, &task.Skill, &task.Repository, &task.Ref,
|
||||
&task.IssueNumber, &task.PRNumber, &task.CommentID, &task.Body, &task.Author,
|
||||
&task.ExtraTools, &task.Status, &task.PoolRef, &task.JobName, &task.Result, &task.ErrorMessage,
|
||||
&task.CreatedAt, &startedAt, &completedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parentID != nil {
|
||||
task.ParentTaskID = *parentID
|
||||
}
|
||||
task.StartedAt = startedAt
|
||||
task.CompletedAt = completedAt
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks, rows.Err()
|
||||
}
|
||||
|
||||
func itoa(i int) string {
|
||||
return string(rune('0'+i)) + ""
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
api *gitea.Client
|
||||
url string
|
||||
}
|
||||
|
||||
func NewClient(url, token string) *Client {
|
||||
client, _ := gitea.NewClient(url, gitea.SetToken(token))
|
||||
return &Client{
|
||||
api: client,
|
||||
url: url,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
sdk "code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
func (c *Client) PostComment(owner, repo string, issueOrPR int, body string) error {
|
||||
_, _, err := c.api.CreateIssueComment(owner, repo, int64(issueOrPR), sdk.CreateIssueCommentOption{
|
||||
Body: body,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) AddReaction(owner, repo string, commentID int64, reaction string) error {
|
||||
_, _, err := c.api.PostIssueCommentReaction(owner, repo, commentID, reaction)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/internal/provider"
|
||||
)
|
||||
|
||||
type webhookPayload struct {
|
||||
Action string `json:"action"`
|
||||
Comment *commentPayload `json:"comment,omitempty"`
|
||||
Issue *issuePayload `json:"issue,omitempty"`
|
||||
Repository *repoPayload `json:"repository"`
|
||||
PullRequest *prPayload `json:"pull_request,omitempty"`
|
||||
}
|
||||
|
||||
type commentPayload struct {
|
||||
ID int64 `json:"id"`
|
||||
Body string `json:"body"`
|
||||
User struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"user"`
|
||||
}
|
||||
|
||||
type issuePayload struct {
|
||||
Number int `json:"number"`
|
||||
PullRequest *struct{} `json:"pull_request,omitempty"`
|
||||
}
|
||||
|
||||
type prPayload struct {
|
||||
Number int `json:"number"`
|
||||
Head struct {
|
||||
Ref string `json:"ref"`
|
||||
} `json:"head"`
|
||||
}
|
||||
|
||||
type repoPayload struct {
|
||||
FullName string `json:"full_name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
}
|
||||
|
||||
func (c *Client) ParseWebhook(body []byte, secret string) (*provider.WebhookEvent, error) {
|
||||
var payload webhookPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal webhook: %w", err)
|
||||
}
|
||||
|
||||
if payload.Action != "created" || payload.Comment == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
event := &provider.WebhookEvent{
|
||||
Action: payload.Action,
|
||||
Repository: payload.Repository.FullName,
|
||||
CommentID: payload.Comment.ID,
|
||||
Body: payload.Comment.Body,
|
||||
Author: payload.Comment.User.Login,
|
||||
}
|
||||
|
||||
if payload.Issue != nil {
|
||||
if payload.Issue.PullRequest != nil {
|
||||
event.Type = "pull_request_comment"
|
||||
event.PRNum = payload.Issue.Number
|
||||
if payload.PullRequest != nil {
|
||||
event.Ref = payload.PullRequest.Head.Ref
|
||||
}
|
||||
} else {
|
||||
event.Type = "issue_comment"
|
||||
event.IssueNum = payload.Issue.Number
|
||||
}
|
||||
event.Ref = payload.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func VerifySignature(body []byte, secret, signature string) bool {
|
||||
if secret == "" {
|
||||
return true
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(body)
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package provider
|
||||
|
||||
type WebhookEvent struct {
|
||||
Type string
|
||||
Action string
|
||||
Repository string
|
||||
Ref string
|
||||
IssueNum int
|
||||
PRNum int
|
||||
CommentID int64
|
||||
Body string
|
||||
Author string
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
ParseWebhook(body []byte, secret string) (*WebhookEvent, error)
|
||||
PostComment(owner, repo string, issueOrPR int, body string) error
|
||||
AddReaction(owner, repo string, commentID int64, reaction string) error
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import "strings"
|
||||
|
||||
var ValidCommands = map[string]bool{
|
||||
"plan": true,
|
||||
"implement": true,
|
||||
"review": true,
|
||||
"test": true,
|
||||
"fix": true,
|
||||
"retry": true,
|
||||
"cancel": true,
|
||||
}
|
||||
|
||||
type ParsedCommand struct {
|
||||
Name string
|
||||
Args string
|
||||
}
|
||||
|
||||
func ParseCommands(body string) []ParsedCommand {
|
||||
var commands []ParsedCommand
|
||||
for _, line := range strings.Split(body, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "/") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line[1:], " ", 2)
|
||||
name := strings.ToLower(parts[0])
|
||||
if !ValidCommands[name] {
|
||||
continue
|
||||
}
|
||||
cmd := ParsedCommand{Name: name}
|
||||
if len(parts) > 1 {
|
||||
cmd.Args = strings.TrimSpace(parts[1])
|
||||
}
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
return commands
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
StatusPending TaskStatus = "pending"
|
||||
StatusRunning TaskStatus = "running"
|
||||
StatusSucceeded TaskStatus = "succeeded"
|
||||
StatusFailed TaskStatus = "failed"
|
||||
StatusCancelled TaskStatus = "cancelled"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
ParentTaskID string `json:"parentTaskId,omitempty"`
|
||||
Command string `json:"command"`
|
||||
Skill string `json:"skill,omitempty"`
|
||||
Repository string `json:"repository"`
|
||||
Ref string `json:"ref"`
|
||||
IssueNumber int `json:"issueNumber,omitempty"`
|
||||
PRNumber int `json:"prNumber,omitempty"`
|
||||
CommentID int64 `json:"commentId,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Author string `json:"author"`
|
||||
ExtraTools []string `json:"extraTools,omitempty"`
|
||||
Status TaskStatus `json:"status"`
|
||||
PoolRef string `json:"poolRef,omitempty"`
|
||||
JobName string `json:"jobName,omitempty"`
|
||||
Result string `json:"result,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
StartedAt *time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type CreateTaskRequest struct {
|
||||
Command string `json:"command"`
|
||||
Skill string `json:"skill,omitempty"`
|
||||
Repository string `json:"repository"`
|
||||
Ref string `json:"ref"`
|
||||
IssueNumber int `json:"issueNumber,omitempty"`
|
||||
PRNumber int `json:"prNumber,omitempty"`
|
||||
CommentID int64 `json:"commentId,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Author string `json:"author"`
|
||||
ExtraTools []string `json:"extraTools,omitempty"`
|
||||
ParentTaskID string `json:"parentTaskId,omitempty"`
|
||||
PoolRef string `json:"poolRef,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateTaskRequest struct {
|
||||
Status TaskStatus `json:"status,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
JobName string `json:"jobName,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
}
|
||||
|
||||
type CommentRequest struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
Reference in New Issue
Block a user