rdev/internal/projects/registry.go
jordan 0960b17eb2 feat: Implement v0.2-v0.4 (workspaces, git, API)
v0.2 - Real Workspaces:
- Project-specific claudebox StatefulSets (pantheon, aeries)
- Init containers for git clone via SSH
- Deploy key secrets template
- Project ConfigMaps for CLAUDE.md

v0.3 - Git Integration:
- Dockerfile with rdev-bot git identity
- openssh-client for SSH operations
- Image version bump to v0.3.0

v0.4 - API Server:
- Go REST API with chi router
- Endpoints: /projects, /claude, /shell, /git, /events
- SSE streaming for real-time output
- OpenAPI docs via Scalar at /docs
- Kubernetes RBAC for pod exec
- Executor and project registry packages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 21:07:00 -07:00

149 lines
3.2 KiB
Go

// Package projects provides a registry of claudebox projects.
package projects
import (
"context"
"fmt"
"os/exec"
"strings"
"sync"
)
// Project represents a claudebox project.
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PodName string `json:"pod"`
Status string `json:"status"`
Workspace string `json:"workspace,omitempty"`
}
// Registry manages the list of available projects.
type Registry struct {
namespace string
projects map[string]*Project
mu sync.RWMutex
}
// NewRegistry creates a new project registry.
func NewRegistry(namespace string) *Registry {
r := &Registry{
namespace: namespace,
projects: make(map[string]*Project),
}
// Initialize with known projects
// In the future, this could discover projects from K8s labels
r.projects["pantheon"] = &Project{
ID: "pantheon",
Name: "Pantheon",
Description: "Go API backend",
PodName: "claudebox-pantheon-0",
Status: "unknown",
Workspace: "/workspace",
}
r.projects["aeries"] = &Project{
ID: "aeries",
Name: "Aeries",
Description: "Note community platform",
PodName: "claudebox-aeries-0",
Status: "unknown",
Workspace: "/workspace",
}
return r
}
// List returns all projects.
func (r *Registry) List() []*Project {
r.mu.RLock()
defer r.mu.RUnlock()
projects := make([]*Project, 0, len(r.projects))
for _, p := range r.projects {
projects = append(projects, p)
}
return projects
}
// Get returns a project by ID.
func (r *Registry) Get(id string) (*Project, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
p, ok := r.projects[id]
return p, ok
}
// Exists checks if a project exists.
func (r *Registry) Exists(id string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, ok := r.projects[id]
return ok
}
// RefreshStatus updates the status of all projects from K8s.
func (r *Registry) RefreshStatus(ctx context.Context) error {
r.mu.Lock()
defer r.mu.Unlock()
for _, p := range r.projects {
status, err := getPodStatus(ctx, r.namespace, p.PodName)
if err != nil {
p.Status = "error"
continue
}
p.Status = status
}
return nil
}
// getPodStatus queries the status of a pod.
func getPodStatus(ctx context.Context, namespace, podName string) (string, error) {
cmd := exec.CommandContext(ctx, "kubectl",
"get", "pod", podName,
"-n", namespace,
"-o", "jsonpath={.status.phase}",
)
output, err := cmd.Output()
if err != nil {
// Check if pod doesn't exist
if strings.Contains(err.Error(), "not found") {
return "not_found", nil
}
return "unknown", fmt.Errorf("get pod status: %w", err)
}
phase := strings.ToLower(strings.TrimSpace(string(output)))
switch phase {
case "running":
return "running", nil
case "pending":
return "pending", nil
case "succeeded":
return "completed", nil
case "failed":
return "failed", nil
default:
return phase, nil
}
}
// Register adds a new project to the registry.
func (r *Registry) Register(p *Project) {
r.mu.Lock()
defer r.mu.Unlock()
r.projects[p.ID] = p
}
// Unregister removes a project from the registry.
func (r *Registry) Unregister(id string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.projects, id)
}