Skip to content
Open
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
269 changes: 269 additions & 0 deletions .claude/context/backend-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,275 @@ if !found || err != nil {
- `types/session.go` - Type definitions
- `server/server.go` - Server setup, token redaction

## Exception: Public API Gateway Service

The `components/public-api/` service is a **stateless HTTP gateway** that does NOT follow the standard backend patterns above. This is intentional:

- **No K8s Clients**: Does NOT use `GetK8sClientsForRequest()` or access Kubernetes directly
- **No RBAC Permissions**: ServiceAccount has NO RoleBindings
- **Token Forwarding Only**: Proxies requests to backend with user's token in `Authorization` header
- **Backend Validates**: All K8s operations and RBAC enforcement happen in the backend service

The public-api is a thin shim layer that extracts/validates tokens, extracts project context, validates input parameters (prevents injection attacks), and forwards requests with proper authorization headers.

## Package Organization

**Backend Structure** (`components/backend/`):

```
backend/
β”œβ”€β”€ handlers/ # HTTP handlers grouped by resource
β”‚ β”œβ”€β”€ sessions.go # AgenticSession CRUD + lifecycle
β”‚ β”œβ”€β”€ projects.go # Project management
β”‚ β”œβ”€β”€ rfe.go # RFE workflows
β”‚ β”œβ”€β”€ helpers.go # Shared utilities (StringPtr, etc.)
β”‚ └── middleware.go # Auth, validation, RBAC
β”œβ”€β”€ types/ # Type definitions (no business logic)
β”‚ β”œβ”€β”€ session.go
β”‚ β”œβ”€β”€ project.go
β”‚ └── common.go
β”œβ”€β”€ server/ # Server setup, CORS, middleware
β”œβ”€β”€ k8s/ # K8s resource templates
β”œβ”€β”€ git/, github/ # External integrations
β”œβ”€β”€ websocket/ # Real-time messaging
β”œβ”€β”€ routes.go # HTTP route registration
└── main.go # Wiring, dependency injection
```

**Operator Structure** (`components/operator/`):

```
operator/
β”œβ”€β”€ internal/
β”‚ β”œβ”€β”€ config/ # K8s client init, config loading
β”‚ β”œβ”€β”€ types/ # GVR definitions, resource helpers
β”‚ β”œβ”€β”€ handlers/ # Watch handlers (sessions, namespaces, projectsettings)
β”‚ └── services/ # Reusable services (PVC provisioning, etc.)
└── main.go # Watch coordination
```

**Rules**:

- Handlers contain HTTP/watch logic ONLY
- Types are pure data structures
- Business logic in separate service packages
- No cyclic dependencies between packages

## Resource Management

**OwnerReferences Pattern**:

```go
ownerRef := v1.OwnerReference{
APIVersion: obj.GetAPIVersion(),
Kind: obj.GetKind(),
Name: obj.GetName(),
UID: obj.GetUID(),
Controller: boolPtr(true),
// BlockOwnerDeletion: intentionally omitted (permission issues)
}

job := &batchv1.Job{
ObjectMeta: v1.ObjectMeta{
Name: jobName,
Namespace: namespace,
OwnerReferences: []v1.OwnerReference{ownerRef},
},
}
```

**Cleanup Patterns**:

```go
policy := v1.DeletePropagationBackground
err := K8sClient.BatchV1().Jobs(ns).Delete(ctx, jobName, v1.DeleteOptions{
PropagationPolicy: &policy,
})
if err != nil && !errors.IsNotFound(err) {
log.Printf("Failed to delete job: %v", err)
return err
}
```

## API Design Patterns

**Project-Scoped Endpoints**:

```go
r.GET("/api/projects/:projectName/agentic-sessions", ValidateProjectContext(), ListSessions)
r.POST("/api/projects/:projectName/agentic-sessions", ValidateProjectContext(), CreateSession)
r.GET("/api/projects/:projectName/agentic-sessions/:sessionName", ValidateProjectContext(), GetSession)
```

**Middleware Chain** (order matters):

```go
r.Use(gin.Recovery())
r.Use(gin.LoggerWithFormatter(customRedactingFormatter))
r.Use(cors.New(corsConfig))
r.Use(forwardedIdentityMiddleware())
r.Use(ValidateProjectContext())
```

**Response Patterns**:

```go
c.JSON(http.StatusOK, gin.H{"items": sessions})
c.JSON(http.StatusCreated, gin.H{"message": "Session created", "name": name, "uid": uid})
c.Status(http.StatusNoContent)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
```

## Operator Patterns

**Watch Loop with Reconnection**:

```go
func WatchAgenticSessions() {
gvr := types.GetAgenticSessionResource()
for {
watcher, err := config.DynamicClient.Resource(gvr).Watch(ctx, v1.ListOptions{})
if err != nil {
log.Printf("Failed to create watcher: %v", err)
time.Sleep(5 * time.Second)
continue
}
for event := range watcher.ResultChan() {
switch event.Type {
case watch.Added, watch.Modified:
obj := event.Object.(*unstructured.Unstructured)
handleEvent(obj)
case watch.Deleted:
// Handle cleanup
}
}
watcher.Stop()
time.Sleep(2 * time.Second)
}
}
```

**Reconciliation Pattern**:

```go
func handleEvent(obj *unstructured.Unstructured) error {
name := obj.GetName()
namespace := obj.GetNamespace()

currentObj, err := getDynamicClient().Get(ctx, name, namespace)
if errors.IsNotFound(err) {
return nil
}

status, found, _ := unstructured.NestedMap(currentObj.Object, "status")
phase := getPhaseOrDefault(status, "Pending")
if phase != "Pending" {
return nil
}

if _, err := getResource(name); err == nil {
return nil
}

createResource(...)
updateStatus(namespace, name, map[string]interface{}{"phase": "Creating"})
return nil
}
```

**Status Updates** (use UpdateStatus subresource):

```go
func updateAgenticSessionStatus(namespace, name string, updates map[string]interface{}) error {
gvr := types.GetAgenticSessionResource()
obj, err := config.DynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, v1.GetOptions{})
if errors.IsNotFound(err) {
return nil
}
if obj.Object["status"] == nil {
obj.Object["status"] = make(map[string]interface{})
}
status := obj.Object["status"].(map[string]interface{})
for k, v := range updates {
status[k] = v
}
_, err = config.DynamicClient.Resource(gvr).Namespace(namespace).UpdateStatus(ctx, obj, v1.UpdateOptions{})
if errors.IsNotFound(err) {
return nil
}
return err
}
```

**Goroutine Monitoring**:

```go
go monitorJob(jobName, sessionName, namespace)

func monitorJob(jobName, sessionName, namespace string) {
for {
time.Sleep(5 * time.Second)
if _, err := getSession(namespace, sessionName); errors.IsNotFound(err) {
return
}
job, err := K8sClient.BatchV1().Jobs(namespace).Get(ctx, jobName, v1.GetOptions{})
if errors.IsNotFound(err) {
return
}
if job.Status.Succeeded > 0 {
updateStatus(namespace, sessionName, map[string]interface{}{
"phase": "Completed",
"completionTime": time.Now().Format(time.RFC3339),
})
cleanup(namespace, jobName)
return
}
}
}
```

## Common Mistakes to Avoid

**Backend**:

- Using service account client for user operations (always use user token)
- Not checking if user-scoped client creation succeeded
- Logging full token values (use `len(token)` instead)
- Not validating project access in middleware
- Type assertions without checking: `val := obj["key"].(string)` (use `val, ok := ...`)
- Not setting OwnerReferences (causes resource leaks)
- Treating IsNotFound as fatal error during cleanup
- Exposing internal error details to API responses (use generic messages)

**Operator**:

- Not reconnecting watch on channel close
- Processing events without verifying resource still exists
- Updating status on main object instead of /status subresource
- Not checking current phase before reconciliation (causes duplicate resources)
- Creating resources without idempotency checks
- Goroutine leaks (not exiting monitor when resource deleted)
- Using `panic()` in watch/reconciliation loops
- Not setting SecurityContext on Job pods

## Reference Files

**Backend**:

- `components/backend/handlers/sessions.go` - Complete session lifecycle, user/SA client usage
- `components/backend/handlers/middleware.go` - Auth patterns, token extraction, RBAC
- `components/backend/handlers/helpers.go` - Utility functions (StringPtr, BoolPtr)
- `components/backend/types/common.go` - Type definitions
- `components/backend/server/server.go` - Server setup, middleware chain, token redaction
- `components/backend/routes.go` - HTTP route definitions and registration

**Operator**:

- `components/operator/internal/handlers/sessions.go` - Watch loop, reconciliation, status updates
- `components/operator/internal/config/config.go` - K8s client initialization
- `components/operator/internal/types/resources.go` - GVR definitions
- `components/operator/internal/services/infrastructure.go` - Reusable services

## Recent Issues & Learnings

- **2024-11-15:** Fixed token leak in logs - never log raw tokens
Expand Down
Loading
Loading