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>
149 lines
3.2 KiB
Go
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)
|
|
}
|