Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Dockerfile.router
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Multi-stage build for agentcube-router
FROM golang:1.24.9-alpine AS builder

# Build arguments for multi-architecture support
ARG TARGETOS=linux
ARG TARGETARCH

WORKDIR /workspace

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY cmd/ cmd/
COPY pkg/ pkg/
COPY client-go/ client-go/

# Build with dynamic architecture support
# Supports amd64, arm64, arm/v7, etc.
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -o agentcube-router ./cmd/router

# Runtime image
FROM alpine:3.19

RUN apk --no-cache add ca-certificates

WORKDIR /app

# Copy binary from builder
COPY --from=builder /workspace/agentcube-router .

# Run as non-root user
RUN adduser -D -u 1000 router
USER router

EXPOSE 8080

ENTRYPOINT ["/app/agentcube-router"]
CMD ["--port=8080", "--debug"]
64 changes: 62 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ build-test-tunnel: ## Build test-tunnel tool
@echo "Building test-tunnel..."
go build -o bin/test-tunnel ./cmd/test-tunnel

build-all: build build-agentd build-test-tunnel ## Build all binaries
build-router: generate ## Build agentcube-router binary
@echo "Building agentcube-router..."
go build -o bin/agentcube-router ./cmd/router

build-all: build build-agentd build-test-tunnel build-router ## Build all binaries

# Run server (development mode)
run:
Expand All @@ -99,11 +103,18 @@ run-local:
--ssh-username=sandbox \
--ssh-port=22

# Run router (development mode)
run-router:
@echo "Running agentcube-router..."
go run ./cmd/router/main.go \
--port=8080 \
--debug

# Clean build artifacts
clean:
@echo "Cleaning..."
rm -rf bin/
rm -f agentcube-apiserver agentd
rm -f agentcube-apiserver agentd agentcube-router

# Install dependencies
deps:
Expand Down Expand Up @@ -143,6 +154,7 @@ install: build

# Docker image variables
APISERVER_IMAGE ?= agentcube-apiserver:latest
ROUTER_IMAGE ?= agentcube-router:latest
IMAGE_REGISTRY ?= ""

# Docker and Kubernetes targets
Expand Down Expand Up @@ -192,6 +204,54 @@ kind-load:
@echo "Loading image to kind..."
kind load docker-image $(APISERVER_IMAGE)

# Router Docker targets
docker-build-router:
@echo "Building Router Docker image..."
docker build -f Dockerfile.router -t $(ROUTER_IMAGE) .

# Multi-architecture build for router (supports amd64, arm64)
docker-buildx-router:
@echo "Building multi-architecture Router Docker image..."
docker buildx build -f Dockerfile.router --platform linux/amd64,linux/arm64 -t $(ROUTER_IMAGE) .

# Multi-architecture build and push for router
docker-buildx-push-router:
@if [ -z "$(IMAGE_REGISTRY)" ]; then \
echo "Error: IMAGE_REGISTRY not set. Usage: make docker-buildx-push-router IMAGE_REGISTRY=your-registry.com"; \
exit 1; \
fi
@echo "Building and pushing multi-architecture Router Docker image to $(IMAGE_REGISTRY)/$(ROUTER_IMAGE)..."
docker buildx build -f Dockerfile.router --platform linux/amd64,linux/arm64 \
-t $(IMAGE_REGISTRY)/$(ROUTER_IMAGE) \
--push .

docker-push-router: docker-build-router
@if [ -z "$(IMAGE_REGISTRY)" ]; then \
echo "Error: IMAGE_REGISTRY not set. Usage: make docker-push-router IMAGE_REGISTRY=your-registry.com"; \
exit 1; \
fi
@echo "Tagging and pushing Router Docker image to $(IMAGE_REGISTRY)/$(ROUTER_IMAGE)..."
docker tag $(ROUTER_IMAGE) $(IMAGE_REGISTRY)/$(ROUTER_IMAGE)
docker push $(IMAGE_REGISTRY)/$(ROUTER_IMAGE)

# Load router image to kind cluster
kind-load-router:
@echo "Loading router image to kind..."
kind load docker-image $(ROUTER_IMAGE)

# Deploy router to Kubernetes
k8s-deploy-router:
@echo "Deploying router to Kubernetes..."
kubectl apply -f k8s/agentcube-router.yaml

k8s-delete-router:
@echo "Deleting router from Kubernetes..."
kubectl delete -f k8s/agentcube-router.yaml

k8s-logs-router:
@echo "Showing router logs..."
kubectl logs -n agentcube -l app=agentcube-router -f

# Sandbox image targets
SANDBOX_IMAGE ?= sandbox:latest

Expand Down
76 changes: 76 additions & 0 deletions cmd/router/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"context"
"flag"
"log"
"os"
"os/signal"
"syscall"

"github.com/volcano-sh/agentcube/pkg/router"
)

func main() {
var (
port = flag.String("port", "8080", "Router API server port")
enableTLS = flag.Bool("enable-tls", false, "Enable TLS (HTTPS)")
tlsCert = flag.String("tls-cert", "", "Path to TLS certificate file")
tlsKey = flag.String("tls-key", "", "Path to TLS key file")
debug = flag.Bool("debug", false, "Enable debug mode")
maxConcurrentRequests = flag.Int("max-concurrent-requests", 1000, "Maximum number of concurrent requests")
requestTimeout = flag.Int("request-timeout", 30, "Request timeout in seconds")
maxIdleConns = flag.Int("max-idle-conns", 100, "Maximum number of idle connections")
maxConnsPerHost = flag.Int("max-conns-per-host", 10, "Maximum number of connections per host")
)

// Parse command line flags
flag.Parse()

// Create Router API server configuration
config := &router.Config{
Port: *port,
Debug: *debug,
EnableTLS: *enableTLS,
TLSCert: *tlsCert,
TLSKey: *tlsKey,
MaxConcurrentRequests: *maxConcurrentRequests,
RequestTimeout: *requestTimeout,
MaxIdleConns: *maxIdleConns,
MaxConnsPerHost: *maxConnsPerHost,
}

// Create Router API server
server, err := router.NewServer(config)
if err != nil {
log.Fatalf("Failed to create Router API server: %v", err)
}

// Setup signal handling with context cancellation
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

// Start Router API server in goroutine
errCh := make(chan error, 1)
go func() {
log.Printf("Starting agentcube Router server on port %s", *port)
if err := server.Start(ctx); err != nil {
errCh <- err
}
close(errCh)
}()

// Wait for signal or error
select {
case <-ctx.Done():
log.Println("Received shutdown signal, shutting down gracefully...")
// Cancel the context to trigger server shutdown
cancel()
// Wait for server goroutine to exit after graceful shutdown is complete
<-errCh
case err := <-errCh:
log.Fatalf("Server error: %v", err)
}

log.Println("Router server stopped")
}
184 changes: 184 additions & 0 deletions docs/design/router-proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Router Submodule Design Document

## 1. Overview

Agent-router is a high-performance proxy that receives user HTTP requests and forwards them to the corresponding Sandbox. It acts as a reverse proxy and gateway, focusing on efficient request routing while delegating session and sandbox creating to the SessionManager component.

**Key Responsibilities:**

- HTTP request routing and forwarding
- Connection pooling and concurrency control
- Integration with external cache for session activity tracking

## 2. Architecture Design

### 2.1 Overall Architecture Flow

```mermaid
graph TB
Client[Client] --> Router[Router API Server]
Router --> SessionMgr[SessionManager Interface]
Router --> Sandbox1[Sandbox 1]
Router --> Sandbox2[Sandbox 2]
Router --> SandboxN[Sandbox N]
SessionMgr --> Cache

subgraph "Router Core Components"
Router
SessionMgr
end

subgraph "Sandbox Cluster"
Sandbox1
Sandbox2
SandboxN
end
```

### 2.2 Request Routing Flow

```mermaid
sequenceDiagram
actor Client
box Router
participant Apiserver
participant SessionManager
end
participant Cache
participant WorkloadManager
participant Sandbox

Client->>Apiserver: invoke request
activate Apiserver
Apiserver->>SessionManager: GetSandboxBySession
activate SessionManager
alt session-id is empty
SessionManager->>WorkloadManager: CreateSandbox
WorkloadManager->>SessionManager: Sandbox
else session-id is not empty
SessionManager->>Cache: GetSandboxBySessionID
Cache->>SessionManager: Sandbox
end
SessionManager->>Apiserver: Sandbox
deactivate SessionManager

alt Sandbox is empty
Apiserver->>Client: Bad request
else Sandbox is not empty
Apiserver->>Sandbox: Forward request
Apiserver->>Cache: Update sandbox last activte time
Sandbox->>Apiserver: invoke result
Apiserver->>Cache: Update sandbox last activte time
Apiserver->>Client: invoke result
end
deactivate Apiserver
```

## 3. Detailed Design

### 3.1 API Endpoints

The Router uses the Gin framework to provide HTTP services with the following endpoints:

#### Invocation Endpoints (With Concurrency Limiting)

1. **Agent Runtime Invocation**
```
POST /v1/namespaces/{namespace}/agent-runtimes/{name}/invocations/*path
```
- Headers: `x-agentcube-session-id` (optional, creates new session if empty)
- Forwards request to AgentRuntime sandbox
- Response includes `x-agentcube-session-id` header

2. **Code Interpreter Invocation**
```
POST /v1/namespaces/{namespace}/code-interpreters/{name}/invocations/*path
```
- Headers: `x-agentcube-session-id` (optional, creates new session if empty)
- Forwards request to CodeInterpreter sandbox
- Response includes `x-agentcube-session-id` header

#### Health Check Endpoints (No Authentication, No Concurrency Limit)

1. **Liveness Probe**
```
GET /health/live
```
Returns: `{"status": "alive"}`

2. **Readiness Probe**
```
GET /health/ready
```
Returns: `{"status": "ready"}` if SessionManager is available
Returns: `503 Service Unavailable` if SessionManager is not available

### 3.4 Request Handling Flow

**Invocation Request Processing:**

1. **Extract Session ID**: Read `x-agentcube-session-id` from request header
2. **Get Sandbox Info**: Call `SessionManager.GetSandboxBySession()`
- If session ID is empty: Creates new sandbox via Workload Manager
- If session ID exists: Retrieves sandbox from Redis
3. **Select Endpoint**: Match request path with sandbox entry points
- Finds entry point with matching path prefix
- Falls back to first entry point if no match
- Adds protocol prefix if not present (e.g., `http://`)
4. **Update Activity**: Record session activity in Redis (before and after forwarding)
5. **Forward Request**: Use reverse proxy to forward to sandbox
- Preserves original request method, headers, and body
- Sets `X-Forwarded-Host` and `X-Forwarded-Proto` headers
- Uses connection pooling for efficiency
6. **Return Response**: Forward sandbox response to client
- Always includes `x-agentcube-session-id` in response header
- Preserves original response status and body

**Error Handling:**
- Invalid session ID → `400 Bad Request`
- No entry points → `404 Not Found`
- Invalid endpoint → `500 Internal Server Error`
- Connection refused → `502 Bad Gateway` (SANDBOX_UNREACHABLE)
- Timeout → `504 Gateway Timeout` (SANDBOX_TIMEOUT)
- Server overload → `429 Too Many Requests` (SERVER_OVERLOADED)

### 3.5 Concurrency Control

**Semaphore-Based Limiting:**
- Uses buffered channel as semaphore to limit concurrent requests
- Default limit: 1000 concurrent requests
- Applied only to invocation endpoints (not health checks)
- Returns `429 Too Many Requests` when limit exceeded

**Connection Pooling:**
- Reusable HTTP transport for all reverse proxy operations
- Configurable idle connections and connections per host
- HTTP/2 support enabled by default
- No idle connection timeout (persistent connections)

## 4. HTTP Response Handling

### 4.1 Success Responses

| Status Code | Scenario | Response Headers | Response Body |
|-------------|----------|------------------|---------------|
| 200 OK | Request processed successfully | `x-agentcube-session-id: <session-id>` | Original response from Sandbox |
| 201 Created | Resource created successfully | `x-agentcube-session-id: <session-id>` | Created resource information |
| 202 Accepted | Async request accepted | `x-agentcube-session-id: <session-id>` | Task status information |

### 4.2 Client Error Responses

| Status Code | Scenario | Response Body Example |
|-------------|----------|----------------------|
| 400 Bad Request | Invalid session ID | `{"error": "Invalid session id <session-id>", "code": "BadRequest"}` |
| 404 Not Found | No entry points found for sandbox | `{"error": "no entry points found for sandbox", "code": "Service not found"}` |
| 429 Too Many Requests | Server overloaded (concurrent request limit exceeded) | `{"error": "server overloaded, please try again later", "code": "SERVER_OVERLOADED"}` |

### 4.3 Server Error Responses

| Status Code | Scenario | Response Body Example |
|-------------|----------|----------------------|
| 500 Internal Server Error | Invalid endpoint | `{"error": "internal server error", "code": "INTERNAL_ERROR"}` |
| 502 Bad Gateway | Sandbox connection failed | `{"error": "sandbox unreachable", "code": "SANDBOX_UNREACHABLE"}` |
| 503 Service Unavailable | SessionManager unavailable | `{"error": "session manager not available", "status": "not ready"}` |
| 504 Gateway Timeout | Sandbox response timeout | `{"error": "sandbox timeout", "code": "SANDBOX_TIMEOUT"}` |
Loading
Loading