[ci skip] add Forgejo task pipeline for OpenClaw AI agent
Forgejo issues as a task queue for OpenClaw: - Forgejo OAuth2 with Authentik SSO, self-registration disabled - Webhook-triggered task processing (instant) + CronJob backup (5min poll) - Tasks processed via Mistral Large 3 (NVIDIA NIM API) - Results posted as issue comments, auto-labeled and closed - Comment follow-ups and reopened issues supported - n8n RBAC for OpenClaw pod exec (future workflow integration)
This commit is contained in:
parent
0d03037393
commit
efe0cdefc8
5 changed files with 842 additions and 0 deletions
|
|
@ -3,6 +3,11 @@ variable "tls_secret_name" {
|
|||
sensitive = true
|
||||
}
|
||||
variable "nfs_server" { type = string }
|
||||
variable "forgejo_authentik_client_id" { type = string }
|
||||
variable "forgejo_authentik_client_secret" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
|
||||
resource "kubernetes_namespace" "forgejo" {
|
||||
|
|
@ -66,6 +71,29 @@ resource "kubernetes_deployment" "forgejo" {
|
|||
name = "USER_GID"
|
||||
value = 1000
|
||||
}
|
||||
# Root URL for OAuth2 redirect callbacks
|
||||
env {
|
||||
name = "FORGEJO__server__ROOT_URL"
|
||||
value = "https://forgejo.viktorbarzin.me"
|
||||
}
|
||||
# Disable local registration — only allow OAuth2 (Authentik)
|
||||
env {
|
||||
name = "FORGEJO__service__DISABLE_REGISTRATION"
|
||||
value = "false"
|
||||
}
|
||||
env {
|
||||
name = "FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION"
|
||||
value = "true"
|
||||
}
|
||||
env {
|
||||
name = "FORGEJO__openid__ENABLE_OPENID_SIGNIN"
|
||||
value = "false"
|
||||
}
|
||||
# Allow webhook delivery to internal k8s services
|
||||
env {
|
||||
name = "FORGEJO__webhook__ALLOWED_HOST_LIST"
|
||||
value = "*.svc.cluster.local"
|
||||
}
|
||||
volume_mount {
|
||||
name = "data"
|
||||
mount_path = "/data"
|
||||
|
|
|
|||
|
|
@ -33,6 +33,49 @@ module "nfs_data" {
|
|||
nfs_path = "/mnt/main/n8n"
|
||||
}
|
||||
|
||||
# --- RBAC: Allow n8n to exec into OpenClaw pods for task execution ---
|
||||
|
||||
resource "kubernetes_service_account" "n8n" {
|
||||
metadata {
|
||||
name = "n8n"
|
||||
namespace = kubernetes_namespace.n8n.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_role" "n8n_openclaw_exec" {
|
||||
metadata {
|
||||
name = "n8n-openclaw-exec"
|
||||
namespace = "openclaw"
|
||||
}
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["pods"]
|
||||
verbs = ["get", "list"]
|
||||
}
|
||||
rule {
|
||||
api_groups = [""]
|
||||
resources = ["pods/exec"]
|
||||
verbs = ["create"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_role_binding" "n8n_openclaw_exec" {
|
||||
metadata {
|
||||
name = "n8n-openclaw-exec"
|
||||
namespace = "openclaw"
|
||||
}
|
||||
subject {
|
||||
kind = "ServiceAccount"
|
||||
name = kubernetes_service_account.n8n.metadata[0].name
|
||||
namespace = kubernetes_namespace.n8n.metadata[0].name
|
||||
}
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "Role"
|
||||
name = kubernetes_role.n8n_openclaw_exec.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "n8n" {
|
||||
metadata {
|
||||
name = "n8n"
|
||||
|
|
@ -56,6 +99,7 @@ resource "kubernetes_deployment" "n8n" {
|
|||
}
|
||||
}
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.n8n.metadata[0].name
|
||||
container {
|
||||
name = "n8n"
|
||||
image = "docker.n8n.io/n8nio/n8n"
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ variable "openclaw_telegram_bot_token" {
|
|||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "forgejo_api_token" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
|
||||
|
|
@ -660,6 +664,199 @@ module "ingress" {
|
|||
}
|
||||
}
|
||||
|
||||
# --- Webhook receiver: triggers task-processor Job on Forgejo issue events ---
|
||||
|
||||
resource "kubernetes_config_map" "task_webhook" {
|
||||
metadata {
|
||||
name = "task-webhook"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
}
|
||||
data = {
|
||||
"server.py" = <<-PYEOF
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import subprocess, time, json, os
|
||||
|
||||
BOT_USER = os.environ.get('FORGEJO_BOT_USER', 'viktor')
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
try:
|
||||
body = self.rfile.read(int(self.headers.get('Content-Length', 0)))
|
||||
data = json.loads(body)
|
||||
action = data.get('action', '')
|
||||
|
||||
# Trigger on: new issue, reopened issue, or new comment
|
||||
trigger = False
|
||||
if action in ('opened', 'reopened'):
|
||||
issue = data.get('issue', {})
|
||||
print(f"Issue #{issue.get('number','?')} {action}: {issue.get('title','?')}")
|
||||
trigger = True
|
||||
elif action == 'created' and 'comment' in data:
|
||||
comment = data.get('comment', {})
|
||||
commenter = comment.get('user', {}).get('login', '')
|
||||
# Skip comments from the bot itself to avoid loops
|
||||
if commenter != BOT_USER:
|
||||
issue = data.get('issue', {})
|
||||
print(f"Comment on #{issue.get('number','?')} by {commenter}")
|
||||
trigger = True
|
||||
else:
|
||||
print(f"Skipping own comment on #{data.get('issue',{}).get('number','?')}")
|
||||
|
||||
if trigger:
|
||||
job_name = f"task-processor-{int(time.time())}"
|
||||
subprocess.run([
|
||||
'kubectl', 'create', 'job', job_name,
|
||||
'--from=cronjob/task-processor',
|
||||
'-n', 'openclaw'
|
||||
], check=True)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
else:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true,"skipped":true}')
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
self.wfile.write(f'{{"error":"{e}"}}'.encode())
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"status":"ok"}')
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print(f"[webhook] {args[0]} {args[1]} {args[2]}")
|
||||
|
||||
print("Task webhook receiver listening on :8080")
|
||||
HTTPServer(('', 8080), Handler).serve_forever()
|
||||
PYEOF
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service_account" "task_webhook" {
|
||||
metadata {
|
||||
name = "task-webhook"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_role" "task_webhook" {
|
||||
metadata {
|
||||
name = "task-webhook-job-creator"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
}
|
||||
rule {
|
||||
api_groups = ["batch"]
|
||||
resources = ["jobs", "cronjobs"]
|
||||
verbs = ["get", "list", "create"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_role_binding" "task_webhook" {
|
||||
metadata {
|
||||
name = "task-webhook-job-creator"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
}
|
||||
subject {
|
||||
kind = "ServiceAccount"
|
||||
name = kubernetes_service_account.task_webhook.metadata[0].name
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
}
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "Role"
|
||||
name = kubernetes_role.task_webhook.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "task_webhook" {
|
||||
metadata {
|
||||
name = "task-webhook"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
labels = {
|
||||
app = "task-webhook"
|
||||
tier = local.tiers.aux
|
||||
}
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "task-webhook"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "task-webhook"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.task_webhook.metadata[0].name
|
||||
container {
|
||||
name = "webhook"
|
||||
image = "python:3-alpine"
|
||||
command = ["sh", "-c", "apk add --no-cache curl > /dev/null 2>&1 && curl -sfL https://dl.k8s.io/release/v1.34.2/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl && exec python3 -u /app/server.py"]
|
||||
port {
|
||||
container_port = 8080
|
||||
}
|
||||
volume_mount {
|
||||
name = "app"
|
||||
mount_path = "/app"
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "5m"
|
||||
memory = "32Mi"
|
||||
}
|
||||
limits = {
|
||||
cpu = "100m"
|
||||
memory = "64Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "app"
|
||||
config_map {
|
||||
name = kubernetes_config_map.task_webhook.metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "task_webhook" {
|
||||
metadata {
|
||||
name = "task-webhook"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
labels = {
|
||||
app = "task-webhook"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
selector = {
|
||||
app = "task-webhook"
|
||||
}
|
||||
port {
|
||||
port = 80
|
||||
target_port = 8080
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "task_webhook_ingress" {
|
||||
source = "../../modules/kubernetes/ingress_factory"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
name = "task-webhook"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
host = "task-webhook"
|
||||
port = 80
|
||||
}
|
||||
|
||||
# --- CronJob: Scheduled cluster health check ---
|
||||
|
||||
resource "kubernetes_service_account" "healthcheck" {
|
||||
|
|
@ -768,3 +965,84 @@ resource "kubernetes_cron_job_v1" "cluster_healthcheck" {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- CronJob: Task processor — polls Forgejo issues and triggers OpenClaw ---
|
||||
|
||||
resource "kubernetes_cron_job_v1" "task_processor" {
|
||||
metadata {
|
||||
name = "task-processor"
|
||||
namespace = kubernetes_namespace.openclaw.metadata[0].name
|
||||
labels = {
|
||||
app = "task-processor"
|
||||
tier = local.tiers.aux
|
||||
}
|
||||
}
|
||||
spec {
|
||||
schedule = "*/5 * * * *"
|
||||
concurrency_policy = "Forbid"
|
||||
failed_jobs_history_limit = 3
|
||||
successful_jobs_history_limit = 3
|
||||
|
||||
job_template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "task-processor"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
active_deadline_seconds = 600
|
||||
backoff_limit = 0
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "task-processor"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.healthcheck.metadata[0].name
|
||||
restart_policy = "Never"
|
||||
|
||||
container {
|
||||
name = "task-processor"
|
||||
image = "bitnami/kubectl:latest"
|
||||
command = ["bash", "-c", <<-EOF
|
||||
# Find the openclaw pod
|
||||
POD=$(kubectl get pods -n openclaw -l app=openclaw -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
|
||||
if [ -z "$POD" ]; then
|
||||
echo "ERROR: OpenClaw pod not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Executing task processor in pod $POD..."
|
||||
kubectl exec -n openclaw "$POD" -c openclaw -- \
|
||||
env FORGEJO_TOKEN="$FORGEJO_TOKEN" \
|
||||
OPENCLAW_TOKEN="$OPENCLAW_TOKEN" \
|
||||
OPENCLAW_URL="https://integrate.api.nvidia.com" \
|
||||
bash /workspace/infra/scripts/task-processor.sh
|
||||
EOF
|
||||
]
|
||||
|
||||
env {
|
||||
name = "FORGEJO_TOKEN"
|
||||
value = var.forgejo_api_token
|
||||
}
|
||||
env {
|
||||
name = "OPENCLAW_TOKEN"
|
||||
value = var.nvidia_api_key
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "50m"
|
||||
memory = "64Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "128Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue