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>
This commit is contained in:
parent
4a042a8b71
commit
0960b17eb2
6
.gitignore
vendored
6
.gitignore
vendored
@ -17,3 +17,9 @@ Thumbs.db
|
||||
# Build artifacts
|
||||
*.tar
|
||||
*.gz
|
||||
rdev-api
|
||||
|
||||
# Deploy keys (generated, never commit)
|
||||
*-deploy-key
|
||||
*-deploy-key.pub
|
||||
*-deploy-key.b64
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@ -1,5 +1,5 @@
|
||||
# rdev claudebox - Claude Code in a container
|
||||
# v0.1 - Base case
|
||||
# v0.3 - Git integration
|
||||
|
||||
FROM ubuntu:22.04
|
||||
|
||||
@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js 20 (required for Claude Code CLI)
|
||||
@ -25,9 +26,18 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
# Install Claude Code CLI
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Configure git for rdev-bot identity
|
||||
RUN git config --global user.name "rdev-bot" \
|
||||
&& git config --global user.email "rdev@orchard9.ai" \
|
||||
&& git config --global init.defaultBranch main \
|
||||
&& git config --global push.autoSetupRemote true
|
||||
|
||||
# Create workspace directory
|
||||
RUN mkdir -p /workspace
|
||||
|
||||
# Create SSH directory with correct permissions
|
||||
RUN mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /workspace
|
||||
|
||||
|
||||
50
Dockerfile.api
Normal file
50
Dockerfile.api
Normal file
@ -0,0 +1,50 @@
|
||||
# rdev-api - Go API server for controlling claudebox pods
|
||||
# v0.4 - API Server
|
||||
|
||||
# Build stage
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install git for go mod download
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# Copy go mod files first for layer caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o rdev-api ./cmd/rdev-api
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.19
|
||||
|
||||
# Install kubectl for exec into pods
|
||||
RUN apk add --no-cache ca-certificates curl \
|
||||
&& curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
&& chmod +x kubectl \
|
||||
&& mv kubectl /usr/local/bin/
|
||||
|
||||
# Create non-root user
|
||||
RUN adduser -D -g '' appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/rdev-api .
|
||||
|
||||
# Use non-root user
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# Run the server
|
||||
ENTRYPOINT ["./rdev-api"]
|
||||
51
PLAN.md
51
PLAN.md
@ -65,16 +65,22 @@ kubectl exec -it -n rdev claudebox-0 -- claude "say hello"
|
||||
---
|
||||
|
||||
### v0.2 - Real Workspaces
|
||||
**Status**: Planned
|
||||
**Status**: Ready (pending deploy key setup)
|
||||
|
||||
Mount actual project repos (pantheon, aeries) into dedicated claudebox pods.
|
||||
|
||||
**Deliverables**:
|
||||
- [ ] `claudebox-pantheon` StatefulSet with pantheon repo
|
||||
- [ ] `claudebox-aeries` StatefulSet with aeries repo
|
||||
- [ ] Init container that clones repo on first start
|
||||
- [ ] Git SSH deploy keys as Kubernetes secrets
|
||||
- [ ] Project-specific CLAUDE.md mounted via ConfigMap
|
||||
- [x] `claudebox-pantheon` StatefulSet with pantheon repo
|
||||
- [x] `claudebox-aeries` StatefulSet with aeries repo
|
||||
- [x] Init container that clones repo on first start
|
||||
- [x] Git SSH deploy keys as Kubernetes secrets (template)
|
||||
- [x] Project-specific CLAUDE.md mounted via ConfigMap
|
||||
|
||||
**Manual Steps Required**:
|
||||
1. Generate deploy keys: `./scripts/generate-deploy-key.sh <project>`
|
||||
2. Add public keys to GitHub repo settings
|
||||
3. Update `secrets.yaml` with base64-encoded private keys
|
||||
4. Deploy: `kubectl apply -k deployments/k8s/base`
|
||||
|
||||
**Architecture**:
|
||||
```yaml
|
||||
@ -116,15 +122,20 @@ kubectl exec -n rdev claudebox-pantheon-0 -- git -C /workspace status
|
||||
---
|
||||
|
||||
### v0.3 - Git Integration
|
||||
**Status**: Planned
|
||||
**Status**: Ready (requires image rebuild)
|
||||
|
||||
Pods can commit and push changes back to GitHub.
|
||||
|
||||
**Deliverables**:
|
||||
- [ ] SSH keys mounted at `/root/.ssh/`
|
||||
- [ ] Git config (user.name, user.email) set in container
|
||||
- [ ] known_hosts includes github.com
|
||||
- [ ] Test push from inside pod
|
||||
- [x] SSH keys mounted at `/root/.ssh/` (done in v0.2)
|
||||
- [x] Git config (user.name, user.email) set in container
|
||||
- [x] known_hosts includes github.com (done in v0.2)
|
||||
- [x] Image updated to v0.3.0 with git identity
|
||||
|
||||
**Deploy Steps**:
|
||||
1. Build image: `./scripts/build-push.sh v0.3.0`
|
||||
2. Apply manifests: `kubectl apply -k deployments/k8s/base`
|
||||
3. Restart pods: `kubectl rollout restart statefulset -n rdev --all`
|
||||
|
||||
**Git Config** (via ConfigMap or Dockerfile):
|
||||
```bash
|
||||
@ -160,16 +171,22 @@ kubectl exec -n rdev claudebox-pantheon-0 -- bash -c "
|
||||
---
|
||||
|
||||
### v0.4 - API Server
|
||||
**Status**: Planned
|
||||
**Status**: Ready (requires image build)
|
||||
|
||||
Go API server for controlling claudebox pods.
|
||||
|
||||
**Deliverables**:
|
||||
- [ ] `rdev-api` Go service
|
||||
- [ ] REST endpoints for claude, shell, git commands
|
||||
- [ ] SSE endpoint for streaming output
|
||||
- [ ] Kubernetes RBAC for pod exec
|
||||
- [ ] Project registry (which pods exist)
|
||||
- [x] `rdev-api` Go service
|
||||
- [x] REST endpoints for claude, shell, git commands
|
||||
- [x] SSE endpoint for streaming output
|
||||
- [x] Kubernetes RBAC for pod exec
|
||||
- [x] Project registry (which pods exist)
|
||||
|
||||
**Deploy Steps**:
|
||||
1. Build images: `./scripts/build-push.sh v0.4.0`
|
||||
2. Apply manifests: `kubectl apply -k deployments/k8s/base`
|
||||
3. Test: `kubectl port-forward -n rdev svc/rdev-api 8080:8080`
|
||||
4. Visit: `http://localhost:8080/docs`
|
||||
|
||||
**API Endpoints**:
|
||||
|
||||
|
||||
160
deployments/k8s/base/claudebox-aeries.yaml
Normal file
160
deployments/k8s/base/claudebox-aeries.yaml
Normal file
@ -0,0 +1,160 @@
|
||||
# claudebox-aeries - Claude Code pod for the Aeries project
|
||||
# v0.2 - Real workspace with init container repo clone
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: claudebox-aeries
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
spec:
|
||||
serviceName: claudebox-aeries
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: claudebox-aeries
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: claudebox-aeries
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
spec:
|
||||
# Init container clones repo if workspace is empty
|
||||
initContainers:
|
||||
- name: git-clone
|
||||
image: ghcr.io/orchard9/rdev-claudebox:v0.3.0
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
|
||||
# Setup SSH for GitHub
|
||||
mkdir -p /root/.ssh
|
||||
cp /ssh-keys/id_ed25519 /root/.ssh/id_ed25519
|
||||
chmod 600 /root/.ssh/id_ed25519
|
||||
cp /ssh-keys/known_hosts /root/.ssh/known_hosts
|
||||
chmod 644 /root/.ssh/known_hosts
|
||||
|
||||
# Clone or fetch
|
||||
if [ ! -d /workspace/.git ]; then
|
||||
echo "Cloning aeries repository..."
|
||||
git clone git@github.com:orchard9/aeries.git /workspace
|
||||
echo "Clone complete."
|
||||
else
|
||||
echo "Repository exists, fetching latest..."
|
||||
cd /workspace
|
||||
git fetch origin
|
||||
echo "Fetch complete."
|
||||
fi
|
||||
|
||||
# Show status
|
||||
cd /workspace
|
||||
git log -1 --oneline
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: ssh-keys
|
||||
mountPath: /ssh-keys
|
||||
readOnly: true
|
||||
|
||||
containers:
|
||||
- name: claudebox
|
||||
image: ghcr.io/orchard9/rdev-claudebox:v0.3.0
|
||||
imagePullPolicy: Always
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
|
||||
volumeMounts:
|
||||
# Workspace with cloned repo
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
|
||||
# Claude config directory (persistent for auth)
|
||||
- name: claude-config
|
||||
mountPath: /root/.claude
|
||||
|
||||
# SSH keys for git operations
|
||||
- name: ssh-keys
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
|
||||
# Project-specific CLAUDE.md
|
||||
- name: project-config
|
||||
mountPath: /workspace/CLAUDE.md
|
||||
subPath: CLAUDE.md
|
||||
|
||||
# Simple liveness check - container is running
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- cat
|
||||
- /healthcheck.sh
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 60
|
||||
|
||||
# Readiness - claude CLI is available
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- claude
|
||||
- --version
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
|
||||
volumes:
|
||||
- name: workspace
|
||||
persistentVolumeClaim:
|
||||
claimName: claudebox-aeries-workspace
|
||||
|
||||
- name: claude-config
|
||||
persistentVolumeClaim:
|
||||
claimName: claudebox-aeries-claude-config
|
||||
|
||||
- name: ssh-keys
|
||||
secret:
|
||||
secretName: github-deploy-key-aeries
|
||||
defaultMode: 0600
|
||||
items:
|
||||
- key: id_ed25519
|
||||
path: id_ed25519
|
||||
- key: known_hosts
|
||||
path: known_hosts
|
||||
|
||||
- name: project-config
|
||||
configMap:
|
||||
name: claudebox-aeries-config
|
||||
|
||||
# Pull from GitHub Container Registry
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
---
|
||||
# Headless service for StatefulSet
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: claudebox-aeries
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
spec:
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: claudebox-aeries
|
||||
ports:
|
||||
- port: 8080
|
||||
name: http
|
||||
160
deployments/k8s/base/claudebox-pantheon.yaml
Normal file
160
deployments/k8s/base/claudebox-pantheon.yaml
Normal file
@ -0,0 +1,160 @@
|
||||
# claudebox-pantheon - Claude Code pod for the Pantheon project
|
||||
# v0.2 - Real workspace with init container repo clone
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: claudebox-pantheon
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
spec:
|
||||
serviceName: claudebox-pantheon
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: claudebox-pantheon
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: claudebox-pantheon
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
spec:
|
||||
# Init container clones repo if workspace is empty
|
||||
initContainers:
|
||||
- name: git-clone
|
||||
image: ghcr.io/orchard9/rdev-claudebox:v0.3.0
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
|
||||
# Setup SSH for GitHub
|
||||
mkdir -p /root/.ssh
|
||||
cp /ssh-keys/id_ed25519 /root/.ssh/id_ed25519
|
||||
chmod 600 /root/.ssh/id_ed25519
|
||||
cp /ssh-keys/known_hosts /root/.ssh/known_hosts
|
||||
chmod 644 /root/.ssh/known_hosts
|
||||
|
||||
# Clone or fetch
|
||||
if [ ! -d /workspace/.git ]; then
|
||||
echo "Cloning pantheon repository..."
|
||||
git clone git@github.com:orchard9/pantheon.git /workspace
|
||||
echo "Clone complete."
|
||||
else
|
||||
echo "Repository exists, fetching latest..."
|
||||
cd /workspace
|
||||
git fetch origin
|
||||
echo "Fetch complete."
|
||||
fi
|
||||
|
||||
# Show status
|
||||
cd /workspace
|
||||
git log -1 --oneline
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: ssh-keys
|
||||
mountPath: /ssh-keys
|
||||
readOnly: true
|
||||
|
||||
containers:
|
||||
- name: claudebox
|
||||
image: ghcr.io/orchard9/rdev-claudebox:v0.3.0
|
||||
imagePullPolicy: Always
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
|
||||
volumeMounts:
|
||||
# Workspace with cloned repo
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
|
||||
# Claude config directory (persistent for auth)
|
||||
- name: claude-config
|
||||
mountPath: /root/.claude
|
||||
|
||||
# SSH keys for git operations
|
||||
- name: ssh-keys
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
|
||||
# Project-specific CLAUDE.md
|
||||
- name: project-config
|
||||
mountPath: /workspace/CLAUDE.md
|
||||
subPath: CLAUDE.md
|
||||
|
||||
# Simple liveness check - container is running
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- cat
|
||||
- /healthcheck.sh
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 60
|
||||
|
||||
# Readiness - claude CLI is available
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- claude
|
||||
- --version
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
|
||||
volumes:
|
||||
- name: workspace
|
||||
persistentVolumeClaim:
|
||||
claimName: claudebox-pantheon-workspace
|
||||
|
||||
- name: claude-config
|
||||
persistentVolumeClaim:
|
||||
claimName: claudebox-pantheon-claude-config
|
||||
|
||||
- name: ssh-keys
|
||||
secret:
|
||||
secretName: github-deploy-key-pantheon
|
||||
defaultMode: 0600
|
||||
items:
|
||||
- key: id_ed25519
|
||||
path: id_ed25519
|
||||
- key: known_hosts
|
||||
path: known_hosts
|
||||
|
||||
- name: project-config
|
||||
configMap:
|
||||
name: claudebox-pantheon-config
|
||||
|
||||
# Pull from GitHub Container Registry
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
---
|
||||
# Headless service for StatefulSet
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: claudebox-pantheon
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
spec:
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: claudebox-pantheon
|
||||
ports:
|
||||
- port: 8080
|
||||
name: http
|
||||
@ -21,7 +21,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: claudebox
|
||||
image: ghcr.io/orchard9/rdev-claudebox:v0.1.0
|
||||
image: ghcr.io/orchard9/rdev-claudebox:v0.3.0
|
||||
imagePullPolicy: Always
|
||||
|
||||
resources:
|
||||
|
||||
99
deployments/k8s/base/configmaps.yaml
Normal file
99
deployments/k8s/base/configmaps.yaml
Normal file
@ -0,0 +1,99 @@
|
||||
# ConfigMaps for project-specific CLAUDE.md files
|
||||
# v0.2 - Project configuration
|
||||
#
|
||||
# These provide Claude Code with project-specific instructions.
|
||||
# The CLAUDE.md is mounted at /workspace/CLAUDE.md in each claudebox.
|
||||
#
|
||||
# Note: If the repo already has a CLAUDE.md, this will override it.
|
||||
# Consider using the repo's CLAUDE.md directly or merging them.
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: claudebox-pantheon-config
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
data:
|
||||
CLAUDE.md: |
|
||||
# Pantheon
|
||||
|
||||
Go API backend for the Orchard9 platform.
|
||||
|
||||
## Environment
|
||||
|
||||
This is running in a remote claudebox pod on k3s (rdev).
|
||||
- Workspace: /workspace (persistent, git-managed)
|
||||
- Claude config: /root/.claude (persistent auth)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build ./...
|
||||
|
||||
# Test
|
||||
go test ./...
|
||||
|
||||
# Run locally (if applicable)
|
||||
go run ./cmd/api
|
||||
```
|
||||
|
||||
## Git
|
||||
|
||||
SSH keys are mounted for git operations.
|
||||
- Fetch: `git fetch origin`
|
||||
- Pull: `git pull origin main`
|
||||
- Push: `git push origin HEAD` (v0.3+)
|
||||
|
||||
## Constraints
|
||||
|
||||
- This is a REMOTE environment - no local filesystem access
|
||||
- Changes persist across pod restarts
|
||||
- Commits are attributed to rdev-bot
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: claudebox-aeries-config
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
data:
|
||||
CLAUDE.md: |
|
||||
# Aeries
|
||||
|
||||
Orchard9 Aeries project.
|
||||
|
||||
## Environment
|
||||
|
||||
This is running in a remote claudebox pod on k3s (rdev).
|
||||
- Workspace: /workspace (persistent, git-managed)
|
||||
- Claude config: /root/.claude (persistent auth)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Build (adjust for your project)
|
||||
# go build ./...
|
||||
# npm run build
|
||||
# etc.
|
||||
```
|
||||
|
||||
## Git
|
||||
|
||||
SSH keys are mounted for git operations.
|
||||
- Fetch: `git fetch origin`
|
||||
- Pull: `git pull origin main`
|
||||
- Push: `git push origin HEAD` (v0.3+)
|
||||
|
||||
## Constraints
|
||||
|
||||
- This is a REMOTE environment - no local filesystem access
|
||||
- Changes persist across pod restarts
|
||||
- Commits are attributed to rdev-bot
|
||||
@ -5,9 +5,23 @@ namespace: rdev
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
|
||||
# v0.1 - Generic claudebox (for testing/dev)
|
||||
- pvc.yaml
|
||||
- claudebox.yaml
|
||||
|
||||
# v0.2 - Project-specific claudeboxes
|
||||
- pvc-pantheon.yaml
|
||||
- pvc-aeries.yaml
|
||||
- configmaps.yaml
|
||||
- secrets.yaml
|
||||
- claudebox-pantheon.yaml
|
||||
- claudebox-aeries.yaml
|
||||
|
||||
# v0.4 - API Server
|
||||
- rbac.yaml
|
||||
- rdev-api.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
app.kubernetes.io/part-of: rdev
|
||||
|
||||
36
deployments/k8s/base/pvc-aeries.yaml
Normal file
36
deployments/k8s/base/pvc-aeries.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
# PVCs for claudebox-aeries
|
||||
# v0.2 - Real workspace storage
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: claudebox-aeries-workspace
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: claudebox-aeries-claude-config
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
36
deployments/k8s/base/pvc-pantheon.yaml
Normal file
36
deployments/k8s/base/pvc-pantheon.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
# PVCs for claudebox-pantheon
|
||||
# v0.2 - Real workspace storage
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: claudebox-pantheon-workspace
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: claudebox-pantheon-claude-config
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
52
deployments/k8s/base/rbac.yaml
Normal file
52
deployments/k8s/base/rbac.yaml
Normal file
@ -0,0 +1,52 @@
|
||||
# RBAC for rdev-api to exec into claudebox pods
|
||||
# v0.4 - API Server
|
||||
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: rdev-api
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: rdev-api
|
||||
app.kubernetes.io/part-of: rdev
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: rdev-api
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: rdev-api
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rules:
|
||||
# List and get pods (for project discovery and status)
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Execute commands in pods
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/exec"]
|
||||
verbs: ["create"]
|
||||
|
||||
# Read pod logs (for debugging)
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log"]
|
||||
verbs: ["get"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: rdev-api
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: rdev-api
|
||||
app.kubernetes.io/part-of: rdev
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: rdev-api
|
||||
namespace: rdev
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: rdev-api
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
81
deployments/k8s/base/rdev-api.yaml
Normal file
81
deployments/k8s/base/rdev-api.yaml
Normal file
@ -0,0 +1,81 @@
|
||||
# rdev-api - Go REST API for controlling claudebox pods
|
||||
# v0.4 - API Server
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rdev-api
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: rdev-api
|
||||
app.kubernetes.io/part-of: rdev
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: rdev-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: rdev-api
|
||||
app.kubernetes.io/name: rdev-api
|
||||
app.kubernetes.io/part-of: rdev
|
||||
spec:
|
||||
serviceAccountName: rdev-api
|
||||
containers:
|
||||
- name: rdev-api
|
||||
image: ghcr.io/orchard9/rdev-api:v0.4.0
|
||||
imagePullPolicy: Always
|
||||
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 30
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
---
|
||||
# Service for rdev-api
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: rdev-api
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: rdev-api
|
||||
app.kubernetes.io/part-of: rdev
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: rdev-api
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: http
|
||||
name: http
|
||||
50
deployments/k8s/base/secrets.yaml
Normal file
50
deployments/k8s/base/secrets.yaml
Normal file
@ -0,0 +1,50 @@
|
||||
# GitHub Deploy Key Secrets for rdev
|
||||
# v0.2 - SSH keys for repo cloning
|
||||
#
|
||||
# INSTRUCTIONS:
|
||||
# 1. Generate deploy keys: ./scripts/generate-deploy-key.sh pantheon
|
||||
# 2. Add PUBLIC key to GitHub repo Settings -> Deploy Keys
|
||||
# 3. Replace placeholder values below with base64-encoded PRIVATE key
|
||||
# 4. Apply: kubectl apply -f secrets.yaml
|
||||
#
|
||||
# To encode: cat pantheon-deploy-key | base64 -w0
|
||||
# To decode and verify: echo "<base64>" | base64 -d
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: github-deploy-key-pantheon
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-pantheon
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: pantheon
|
||||
type: Opaque
|
||||
data:
|
||||
# Replace with base64-encoded private key
|
||||
# Generate with: ssh-keygen -t ed25519 -f pantheon-deploy-key -N ""
|
||||
# Encode with: cat pantheon-deploy-key | base64 -w0
|
||||
id_ed25519: REPLACE_WITH_BASE64_ENCODED_PRIVATE_KEY
|
||||
|
||||
# GitHub's SSH host key (pre-populated)
|
||||
# ssh-keyscan github.com 2>/dev/null | base64 -w0
|
||||
known_hosts: Z2l0aHViLmNvbSBzc2gtZWQyNTUxOSBBQUFBQzNOemFDMWxaREkxTlRFNUFBQUFJT01xcW5rVnpybTBTZEc2VU9vcUtMc2FiZ0g1Qzlva1dpMGRoMmw5R0tKbApnaXRodWIuY29tIGVjZHNhLXNoYTItbmlzdHAyNTYgQUFBQUUyVmpaSE5oTFhOb1lUSXRibWx6ZEhBeU5UWUFBQUFJYm1semRIQXlOVFlBQUFCQkJFbUtTRU5qUUVlek9teGtaTXk3b3BLZ3dGQjlua3Q1WVJyWU1qTnVHNU44N3VSUW81dDRRYkZGelVaYUpVQjd4TmtjYVFTNmlIbW5TazdNOU9tZUR2PT0KZ2l0aHViLmNvbSBzc2gtcnNhIEFBQUFCM056YUMxeWMyRUFBQUFEQVFBQkFBQUJnUUNqN25kTnhRb3dnY1FuanNoY0xycVBFaWlwaG50K1ZUVHZEUCtsSFhaZFhMRThWVUxDS0lLYjloZk5qM0FXSm1RTHBDb0Qzc1F2TWtGNUxXR1RMSFRVM25MSjViZi8wbG5wOGV5ZXhVNkpzR1dSUUFLTnlENjkzQjVVR2xXVlM1VjFqUEg1M3BZVllWUVB6WnlkeGpPUVFLeHk5ZkdoaVFGbGcza3RoZFdSRE5oNy9SRHp4SEZEZmRYYm5uSnZ4WVQ0Y1FVWWJ0SmFTQ0pWcU9aOVlUbG13bTJBUXZaM3IxZEJkZzVRcWN1SW53bzR1NXBhQUpObnpiTXBudGtzVXpWNEorUFN5OE9LSzRPc0tUc0I0RlNjS0VOSmRlMTlYTGFCUHJiNTZpUHhCS0tSMGJNK2NPdnhKelhhZWJORktjR2k4eVJLaGw0T0hlYkhCWDh4eFpZNWMwdWdpcTlSb29QaUtPelJERE1lekdhK0c4MDg1OVF2TkdPK3pZM3RNeHJIM1crT21uYU5keVN6dkpPUktjZEEwejNGU1huUk5jbnZpVlg0c3lGaWdhOUxGZjZ0ZDBhRy8xUFEwVjRCYzFQNXNHdTZBQUFBZUg0em5YNStNNTErUUpWZGorR2NMdTMwcE91U0E1cVZOQ0FodXl6RklBWWlhbjBFWUlnUlE3TmxYdz0K
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: github-deploy-key-aeries
|
||||
namespace: rdev
|
||||
labels:
|
||||
app.kubernetes.io/name: claudebox-aeries
|
||||
app.kubernetes.io/part-of: rdev
|
||||
rdev.orchard9.ai/project: aeries
|
||||
type: Opaque
|
||||
data:
|
||||
# Replace with base64-encoded private key
|
||||
# Generate with: ssh-keygen -t ed25519 -f aeries-deploy-key -N ""
|
||||
# Encode with: cat aeries-deploy-key | base64 -w0
|
||||
id_ed25519: REPLACE_WITH_BASE64_ENCODED_PRIVATE_KEY
|
||||
|
||||
# GitHub's SSH host key (pre-populated)
|
||||
known_hosts: Z2l0aHViLmNvbSBzc2gtZWQyNTUxOSBBQUFBQzNOemFDMWxaREkxTlRFNUFBQUFJT01xcW5rVnpybTBTZEc2VU9vcUtMc2FiZ0g1Qzlva1dpMGRoMmw5R0tKbApnaXRodWIuY29tIGVjZHNhLXNoYTItbmlzdHAyNTYgQUFBQUUyVmpaSE5oTFhOb1lUSXRibWx6ZEhBeU5UWUFBQUFJYm1semRIQXlOVFlBQUFCQkJFbUtTRU5qUUVlek9teGtaTXk3b3BLZ3dGQjlua3Q1WVJyWU1qTnVHNU44N3VSUW81dDRRYkZGelVaYUpVQjd4TmtjYVFTNmlIbW5TazdNOU9tZUR2PT0KZ2l0aHViLmNvbSBzc2gtcnNhIEFBQUFCM056YUMxeWMyRUFBQUFEQVFBQkFBQUJnUUNqN25kTnhRb3dnY1FuanNoY0xycVBFaWlwaG50K1ZUVHZEUCtsSFhaZFhMRThWVUxDS0lLYjloZk5qM0FXSm1RTHBDb0Qzc1F2TWtGNUxXR1RMSFRVM25MSjViZi8wbG5wOGV5ZXhVNkpzR1dSUUFLTnlENjkzQjVVR2xXVlM1VjFqUEg1M3BZVllWUVB6WnlkeGpPUVFLeHk5ZkdoaVFGbGcza3RoZFdSRE5oNy9SRHp4SEZEZmRYYm5uSnZ4WVQ0Y1FVWWJ0SmFTQ0pWcU9aOVlUbG13bTJBUXZaM3IxZEJkZzVRcWN1SW53bzR1NXBhQUpObnpiTXBudGtzVXpWNEorUFN5OE9LSzRPc0tUc0I0RlNjS0VOSmRlMTlYTGFCUHJiNTZpUHhCS0tSMGJNK2NPdnhKelhhZWJORktjR2k4eVJLaGw0T0hlYkhCWDh4eFpZNWMwdWdpcTlSb29QaUtPelJERE1lekdhK0c4MDg1OVF2TkdPK3pZM3RNeHJIM1crT21uYU5keVN6dkpPUktjZEEwejNGU1huUk5jbnZpVlg0c3lGaWdhOUxGZjZ0ZDBhRy8xUFEwVjRCYzFQNXNHdTZBQUFBZUg0em5YNStNNTErUUpWZGorR2NMdTMwcE91U0E1cVZOQ0FodXl6RklBWWlhbjBFWUlnUlE3TmxYdz0K
|
||||
212
history/v0.2.0.md
Normal file
212
history/v0.2.0.md
Normal file
@ -0,0 +1,212 @@
|
||||
# rdev v0.2.0 - Real Workspaces
|
||||
|
||||
**Date**: 2026-01-24
|
||||
**Status**: Ready for deployment (pending deploy keys)
|
||||
|
||||
## Summary
|
||||
|
||||
Project-specific claudebox pods with real GitHub repository workspaces. Each project gets its own StatefulSet with an init container that clones the repo on first start.
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Project-Specific StatefulSets
|
||||
|
||||
Two new claudebox deployments:
|
||||
- `claudebox-pantheon` - For the pantheon project
|
||||
- `claudebox-aeries` - For the aeries project
|
||||
|
||||
Each has:
|
||||
- Init container that clones repo via SSH
|
||||
- Persistent workspace (20Gi)
|
||||
- Persistent Claude config (1Gi)
|
||||
- SSH deploy keys for GitHub access
|
||||
- Project-specific CLAUDE.md via ConfigMap
|
||||
|
||||
### Init Container Logic
|
||||
|
||||
```bash
|
||||
# Setup SSH
|
||||
mkdir -p /root/.ssh
|
||||
cp /ssh-keys/id_ed25519 /root/.ssh/id_ed25519
|
||||
chmod 600 /root/.ssh/id_ed25519
|
||||
|
||||
# Clone or fetch
|
||||
if [ ! -d /workspace/.git ]; then
|
||||
git clone git@github.com:orchard9/${PROJECT}.git /workspace
|
||||
else
|
||||
cd /workspace && git fetch origin
|
||||
fi
|
||||
```
|
||||
|
||||
### Files Created
|
||||
|
||||
```
|
||||
deployments/k8s/base/
|
||||
├── claudebox-pantheon.yaml # Pantheon StatefulSet
|
||||
├── claudebox-aeries.yaml # Aeries StatefulSet
|
||||
├── pvc-pantheon.yaml # Pantheon PVCs
|
||||
├── pvc-aeries.yaml # Aeries PVCs
|
||||
├── configmaps.yaml # Project CLAUDE.md files
|
||||
└── secrets.yaml # Deploy key secrets (template)
|
||||
|
||||
scripts/
|
||||
└── generate-deploy-key.sh # Deploy key generation helper
|
||||
```
|
||||
|
||||
### Kubernetes Resources
|
||||
|
||||
| Resource | Type | Project |
|
||||
|----------|------|---------|
|
||||
| claudebox-pantheon | StatefulSet | pantheon |
|
||||
| claudebox-aeries | StatefulSet | aeries |
|
||||
| claudebox-pantheon-workspace | PVC (20Gi) | pantheon |
|
||||
| claudebox-pantheon-claude-config | PVC (1Gi) | pantheon |
|
||||
| claudebox-aeries-workspace | PVC (20Gi) | aeries |
|
||||
| claudebox-aeries-claude-config | PVC (1Gi) | aeries |
|
||||
| claudebox-pantheon-config | ConfigMap | pantheon |
|
||||
| claudebox-aeries-config | ConfigMap | aeries |
|
||||
| github-deploy-key-pantheon | Secret | pantheon |
|
||||
| github-deploy-key-aeries | Secret | aeries |
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
### 1. Generate Deploy Keys
|
||||
|
||||
```bash
|
||||
cd /path/to/rdev
|
||||
|
||||
# Generate key for pantheon
|
||||
./scripts/generate-deploy-key.sh pantheon
|
||||
|
||||
# Generate key for aeries
|
||||
./scripts/generate-deploy-key.sh aeries
|
||||
```
|
||||
|
||||
### 2. Add Public Keys to GitHub
|
||||
|
||||
For each project:
|
||||
1. Go to `https://github.com/orchard9/<project>/settings/keys`
|
||||
2. Click "Add deploy key"
|
||||
3. Title: `rdev-<project>`
|
||||
4. Paste contents of `<project>-deploy-key.pub`
|
||||
5. Check "Allow write access" (needed for v0.3)
|
||||
|
||||
### 3. Update Secrets
|
||||
|
||||
Edit `deployments/k8s/base/secrets.yaml`:
|
||||
- Replace `REPLACE_WITH_BASE64_ENCODED_PRIVATE_KEY` with contents of `<project>-deploy-key.b64`
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml
|
||||
kubectl apply -k deployments/k8s/base
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
|
||||
```bash
|
||||
# Check pods
|
||||
kubectl get pods -n rdev
|
||||
# Should show: claudebox-pantheon-0, claudebox-aeries-0
|
||||
|
||||
# Check init container logs
|
||||
kubectl logs -n rdev claudebox-pantheon-0 -c git-clone
|
||||
|
||||
# Verify repo was cloned
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- ls /workspace
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- git -C /workspace status
|
||||
|
||||
# Authenticate Claude (first time only)
|
||||
kubectl exec -it -n rdev claudebox-pantheon-0 -- claude
|
||||
```
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### 1. Reuse claudebox image for init container
|
||||
- Same image has git installed
|
||||
- No need for separate alpine/git image
|
||||
- Simpler to maintain
|
||||
|
||||
### 2. SSH Deploy Keys vs HTTPS PAT
|
||||
- SSH deploy keys are per-repo (more secure)
|
||||
- Aligns with v0.3 requirements (push access)
|
||||
- GitHub deploy keys can have write access
|
||||
|
||||
### 3. ConfigMap for CLAUDE.md
|
||||
- Allows customizing Claude behavior per project
|
||||
- Mounted at /workspace/CLAUDE.md
|
||||
- Can be updated without rebuilding image
|
||||
|
||||
### 4. Keep generic claudebox from v0.1
|
||||
- Useful for testing/development
|
||||
- No project-specific config required
|
||||
- Can be used as template
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
rdev namespace
|
||||
├── claudebox-0 (v0.1 - generic)
|
||||
│ └── /workspace (empty, for testing)
|
||||
│
|
||||
├── claudebox-pantheon-0 (v0.2)
|
||||
│ ├── /workspace (pantheon repo)
|
||||
│ ├── /root/.claude (Claude auth)
|
||||
│ └── /root/.ssh (deploy key)
|
||||
│
|
||||
└── claudebox-aeries-0 (v0.2)
|
||||
├── /workspace (aeries repo)
|
||||
├── /root/.claude (Claude auth)
|
||||
└── /root/.ssh (deploy key)
|
||||
```
|
||||
|
||||
## What's Next (v0.3)
|
||||
|
||||
1. Git config (user.name, user.email) for commits
|
||||
2. Test git push from inside pod
|
||||
3. Proper SSH key mounting for main container (currently init-only)
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Set kubeconfig
|
||||
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml
|
||||
|
||||
# Deploy
|
||||
kubectl apply -k deployments/k8s/base
|
||||
|
||||
# Check status
|
||||
kubectl get pods -n rdev
|
||||
kubectl get pvc -n rdev
|
||||
|
||||
# View init container logs
|
||||
kubectl logs -n rdev claudebox-pantheon-0 -c git-clone
|
||||
|
||||
# Interactive Claude session
|
||||
kubectl exec -it -n rdev claudebox-pantheon-0 -- claude
|
||||
|
||||
# Run Claude with prompt
|
||||
kubectl exec -it -n rdev claudebox-pantheon-0 -- claude "what files are in this project?"
|
||||
|
||||
# Git status
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- git -C /workspace status
|
||||
|
||||
# Shell access
|
||||
kubectl exec -it -n rdev claudebox-pantheon-0 -- bash
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Init container fails with "Permission denied (publickey)"
|
||||
- Verify deploy key is added to GitHub
|
||||
- Check secret has correct base64-encoded private key
|
||||
- Ensure known_hosts includes github.com
|
||||
|
||||
### Workspace is empty after pod starts
|
||||
- Check init container logs: `kubectl logs -n rdev <pod> -c git-clone`
|
||||
- Verify SSH key permissions (should be 600)
|
||||
|
||||
### Claude auth not persisting
|
||||
- Check PVC is bound: `kubectl get pvc -n rdev`
|
||||
- Verify claude-config volume is mounted at /root/.claude
|
||||
180
history/v0.3.0.md
Normal file
180
history/v0.3.0.md
Normal file
@ -0,0 +1,180 @@
|
||||
# rdev v0.3.0 - Git Integration
|
||||
|
||||
**Date**: 2026-01-24
|
||||
**Status**: Ready for deployment
|
||||
|
||||
## Summary
|
||||
|
||||
Full git integration - claudebox pods can now commit and push changes back to GitHub. The container image includes git config for the rdev-bot identity, and SSH keys are mounted for authenticated operations.
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Updated Dockerfile
|
||||
|
||||
Added to the claudebox image:
|
||||
- `openssh-client` package for SSH operations
|
||||
- Git global config for rdev-bot identity
|
||||
- Pre-created `/root/.ssh` directory with correct permissions
|
||||
- `push.autoSetupRemote` for easier branch pushing
|
||||
|
||||
```dockerfile
|
||||
# Configure git for rdev-bot identity
|
||||
RUN git config --global user.name "rdev-bot" \
|
||||
&& git config --global user.email "rdev@orchard9.ai" \
|
||||
&& git config --global init.defaultBranch main \
|
||||
&& git config --global push.autoSetupRemote true
|
||||
|
||||
# Create SSH directory with correct permissions
|
||||
RUN mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
```
|
||||
|
||||
### Image Version Bump
|
||||
|
||||
All StatefulSets updated to use `v0.3.0`:
|
||||
- `claudebox.yaml`
|
||||
- `claudebox-pantheon.yaml`
|
||||
- `claudebox-aeries.yaml`
|
||||
|
||||
## Changes from v0.2
|
||||
|
||||
| Component | v0.2 | v0.3 |
|
||||
|-----------|------|------|
|
||||
| Image | v0.1.0 | v0.3.0 |
|
||||
| Git config | None | rdev-bot identity |
|
||||
| SSH client | Not installed | Installed |
|
||||
| Push capability | Clone only | Full read/write |
|
||||
|
||||
## Git Identity
|
||||
|
||||
All commits from rdev claudeboxes will be attributed to:
|
||||
- **Name**: rdev-bot
|
||||
- **Email**: rdev@orchard9.ai
|
||||
|
||||
Example commit:
|
||||
```
|
||||
commit abc123...
|
||||
Author: rdev-bot <rdev@orchard9.ai>
|
||||
Date: Fri Jan 24 2026
|
||||
|
||||
Fix authentication bug in handler
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
### 1. Build and Push New Image
|
||||
|
||||
```bash
|
||||
cd /path/to/rdev
|
||||
|
||||
# Build and push v0.3.0
|
||||
./scripts/build-push.sh v0.3.0
|
||||
```
|
||||
|
||||
### 2. Complete v0.2 Setup (if not done)
|
||||
|
||||
Ensure deploy keys are configured:
|
||||
```bash
|
||||
# Generate keys
|
||||
./scripts/generate-deploy-key.sh pantheon
|
||||
./scripts/generate-deploy-key.sh aeries
|
||||
|
||||
# Add public keys to GitHub (with write access!)
|
||||
# Update secrets.yaml with base64-encoded private keys
|
||||
```
|
||||
|
||||
### 3. Deploy
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml
|
||||
kubectl apply -k deployments/k8s/base
|
||||
|
||||
# Restart pods to pick up new image
|
||||
kubectl rollout restart statefulset -n rdev claudebox
|
||||
kubectl rollout restart statefulset -n rdev claudebox-pantheon
|
||||
kubectl rollout restart statefulset -n rdev claudebox-aeries
|
||||
```
|
||||
|
||||
### 4. Verify Git Push
|
||||
|
||||
```bash
|
||||
# Test push capability
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- bash -c "
|
||||
cd /workspace
|
||||
git checkout -b rdev-test-push
|
||||
echo '# Test from rdev' >> README.md
|
||||
git add README.md
|
||||
git commit -m 'test: verify rdev push capability'
|
||||
git push origin rdev-test-push
|
||||
"
|
||||
|
||||
# Clean up test branch
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- bash -c "
|
||||
cd /workspace
|
||||
git checkout main
|
||||
git branch -D rdev-test-push
|
||||
"
|
||||
# Also delete the remote branch via GitHub UI or gh cli
|
||||
```
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
```bash
|
||||
# 1. Check git config in container
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- git config --global --list
|
||||
# Should show:
|
||||
# user.name=rdev-bot
|
||||
# user.email=rdev@orchard9.ai
|
||||
# init.defaultbranch=main
|
||||
# push.autosetupremote=true
|
||||
|
||||
# 2. Verify SSH access to GitHub
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- ssh -T git@github.com
|
||||
# Should show: Hi orchard9/pantheon! You've successfully authenticated...
|
||||
|
||||
# 3. Test fetch
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- bash -c "cd /workspace && git fetch origin"
|
||||
|
||||
# 4. Test commit (local)
|
||||
kubectl exec -n rdev claudebox-pantheon-0 -- bash -c "
|
||||
cd /workspace
|
||||
echo 'test' >> /tmp/test.txt
|
||||
git add /tmp/test.txt 2>/dev/null || echo 'File outside repo - expected'
|
||||
"
|
||||
|
||||
# 5. Test push (creates branch, then clean up)
|
||||
# See verification section above
|
||||
```
|
||||
|
||||
## What's Next (v0.4)
|
||||
|
||||
Go API server for controlling claudebox pods:
|
||||
- REST endpoints for claude, shell, git commands
|
||||
- SSE streaming for output
|
||||
- Kubernetes RBAC for pod exec
|
||||
- Project registry
|
||||
|
||||
## Files Modified
|
||||
|
||||
```
|
||||
Dockerfile # Added git config, openssh-client
|
||||
deployments/k8s/base/claudebox.yaml # Image v0.1.0 → v0.3.0
|
||||
deployments/k8s/base/claudebox-pantheon.yaml # Image v0.1.0 → v0.3.0
|
||||
deployments/k8s/base/claudebox-aeries.yaml # Image v0.1.0 → v0.3.0
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Push fails with "Permission denied"
|
||||
- Ensure deploy key has "Allow write access" checked in GitHub
|
||||
- Verify SSH key is correctly mounted: `ls -la /root/.ssh/`
|
||||
- Test SSH: `ssh -vT git@github.com`
|
||||
|
||||
### Commits show wrong author
|
||||
- Check git config: `git config --global --list`
|
||||
- Image might be old: verify `v0.3.0` is running
|
||||
|
||||
### "Host key verification failed"
|
||||
- Ensure known_hosts is mounted correctly
|
||||
- Check secret contains github.com host keys
|
||||
259
history/v0.4.0.md
Normal file
259
history/v0.4.0.md
Normal file
@ -0,0 +1,259 @@
|
||||
# rdev v0.4.0 - API Server
|
||||
|
||||
**Date**: 2026-01-24
|
||||
**Status**: Ready for deployment
|
||||
|
||||
## Summary
|
||||
|
||||
Go REST API server for controlling claudebox pods. External clients (Discord bots, CLI tools, etc.) can now interact with Claude Code via HTTP endpoints with SSE streaming for real-time output.
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Go API Server (`rdev-api`)
|
||||
|
||||
A chi-based HTTP server with:
|
||||
- Project discovery and status
|
||||
- Command execution (claude, shell, git)
|
||||
- SSE streaming for real-time output
|
||||
- OpenAPI documentation via Scalar
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/health` | Health check |
|
||||
| GET | `/ready` | Readiness check |
|
||||
| GET | `/docs` | Scalar API documentation |
|
||||
| GET | `/openapi.json` | OpenAPI 3.0 specification |
|
||||
| GET | `/projects` | List available projects |
|
||||
| GET | `/projects/{id}` | Get project details |
|
||||
| POST | `/projects/{id}/claude` | Run Claude command |
|
||||
| POST | `/projects/{id}/shell` | Run shell command |
|
||||
| POST | `/projects/{id}/git` | Run git command |
|
||||
| GET | `/projects/{id}/events` | SSE stream for output |
|
||||
|
||||
### Packages Created
|
||||
|
||||
```
|
||||
cmd/rdev-api/
|
||||
└── main.go # Entry point, OpenAPI spec
|
||||
|
||||
pkg/api/
|
||||
├── app.go # HTTP server chassis
|
||||
├── response.go # JSON response helpers
|
||||
└── openapi.go # OpenAPI spec builder
|
||||
|
||||
internal/
|
||||
├── handlers/
|
||||
│ └── projects.go # HTTP handlers + SSE streaming
|
||||
├── executor/
|
||||
│ └── executor.go # kubectl exec wrapper
|
||||
└── projects/
|
||||
└── registry.go # Project discovery
|
||||
```
|
||||
|
||||
### Kubernetes Resources
|
||||
|
||||
- **Deployment**: `rdev-api` - Single replica
|
||||
- **Service**: `rdev-api` - ClusterIP on port 8080
|
||||
- **ServiceAccount**: `rdev-api`
|
||||
- **Role/RoleBinding**: Permissions for pod exec
|
||||
|
||||
### RBAC Permissions
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/exec"]
|
||||
verbs: ["create"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log"]
|
||||
verbs: ["get"]
|
||||
```
|
||||
|
||||
## Files Created
|
||||
|
||||
```
|
||||
Dockerfile.api # API server image
|
||||
cmd/rdev-api/main.go # Entry point
|
||||
pkg/api/app.go # HTTP chassis
|
||||
pkg/api/response.go # Response helpers
|
||||
pkg/api/openapi.go # OpenAPI builder
|
||||
internal/handlers/projects.go # Handlers + SSE
|
||||
internal/executor/executor.go # kubectl exec
|
||||
internal/projects/registry.go # Project registry
|
||||
deployments/k8s/base/rdev-api.yaml # Deployment + Service
|
||||
deployments/k8s/base/rbac.yaml # RBAC
|
||||
go.mod, go.sum # Go modules
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
github.com/go-chi/chi/v5 # HTTP router
|
||||
github.com/bdpiprava/scalar-go # API documentation
|
||||
```
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
### 1. Build Images
|
||||
|
||||
```bash
|
||||
cd /path/to/rdev
|
||||
|
||||
# Build both claudebox and api images
|
||||
./scripts/build-push.sh v0.4.0
|
||||
|
||||
# Or just the api
|
||||
./scripts/build-push.sh v0.4.0 api
|
||||
```
|
||||
|
||||
### 2. Complete Prerequisites
|
||||
|
||||
Ensure v0.2 and v0.3 are deployed:
|
||||
- Deploy keys configured for projects
|
||||
- Secrets updated with base64 private keys
|
||||
- claudebox image v0.3.0 with git config
|
||||
|
||||
### 3. Deploy
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml
|
||||
kubectl apply -k deployments/k8s/base
|
||||
```
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
# Check API is running
|
||||
kubectl get pods -n rdev -l app=rdev-api
|
||||
|
||||
# Port forward for testing
|
||||
kubectl port-forward -n rdev svc/rdev-api 8080:8080
|
||||
|
||||
# Test health endpoint
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# Test projects list
|
||||
curl http://localhost:8080/projects
|
||||
|
||||
# Test Claude command
|
||||
curl -X POST http://localhost:8080/projects/pantheon/claude \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "what files are in this project?"}'
|
||||
|
||||
# View API docs
|
||||
open http://localhost:8080/docs
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Run Claude Command
|
||||
|
||||
```bash
|
||||
# Start command
|
||||
curl -X POST http://rdev-api.rdev.svc:8080/projects/pantheon/claude \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "fix the bug in auth handler"}'
|
||||
|
||||
# Response
|
||||
{
|
||||
"data": {
|
||||
"id": "cmd-pantheon-001",
|
||||
"project": "pantheon",
|
||||
"type": "claude",
|
||||
"status": "running",
|
||||
"stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-001"
|
||||
},
|
||||
"meta": {"request_id": "...", "timestamp": "..."}
|
||||
}
|
||||
```
|
||||
|
||||
### Stream Output (SSE)
|
||||
|
||||
```javascript
|
||||
const events = new EventSource(
|
||||
'http://rdev-api.rdev.svc:8080/projects/pantheon/events?stream_id=cmd-pantheon-001'
|
||||
);
|
||||
|
||||
events.addEventListener('output', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log(`[${data.stream}] ${data.line}`);
|
||||
});
|
||||
|
||||
events.addEventListener('complete', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log(`Done: exit=${data.exit_code}, ${data.duration_ms}ms`);
|
||||
events.close();
|
||||
});
|
||||
```
|
||||
|
||||
### Run Shell Command
|
||||
|
||||
```bash
|
||||
curl -X POST http://rdev-api.rdev.svc:8080/projects/pantheon/shell \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "go test ./..."}'
|
||||
```
|
||||
|
||||
### Run Git Command
|
||||
|
||||
```bash
|
||||
curl -X POST http://rdev-api.rdev.svc:8080/projects/pantheon/git \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"args": ["status"]}'
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ rdev namespace │
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ rdev-api │ │
|
||||
│ │ (Go server) │ │
|
||||
│ │ │ │
|
||||
│ │ /projects │ kubectl exec │
|
||||
│ │ /claude │─────────────────┐ │
|
||||
│ │ /shell │ │ │
|
||||
│ │ /git │ ▼ │
|
||||
│ │ /events │ ┌──────────────────────────────────────┐ │
|
||||
│ └──────────────┘ │ claudebox pods │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │claudebox- │ │claudebox- │ │ │
|
||||
│ │ │pantheon-0 │ │aeries-0 │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ Claude Code │ │ Claude Code │ │ │
|
||||
│ │ │ /workspace │ │ /workspace │ │ │
|
||||
│ │ └────────────────┘ └──────────────┘ │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## What's Next (v0.5)
|
||||
|
||||
Enhanced SSE streaming:
|
||||
- Output buffering and chunking
|
||||
- Connection management (heartbeats, reconnection)
|
||||
- Event filtering by stream_id
|
||||
- Better error handling
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API returns 404 for project
|
||||
- Check project registry includes the project
|
||||
- Verify claudebox pod exists: `kubectl get pods -n rdev`
|
||||
|
||||
### Commands fail with "permission denied"
|
||||
- Check RBAC is applied: `kubectl get role rdev-api -n rdev`
|
||||
- Verify ServiceAccount is bound: `kubectl get rolebinding rdev-api -n rdev`
|
||||
|
||||
### SSE not receiving events
|
||||
- Ensure stream_id matches the command ID
|
||||
- Check for Connection: keep-alive in client
|
||||
- Verify no proxy is buffering responses
|
||||
183
internal/executor/executor.go
Normal file
183
internal/executor/executor.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Package executor provides kubectl exec functionality for running commands in pods.
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Executor runs commands in Kubernetes pods via kubectl exec.
|
||||
type Executor struct {
|
||||
namespace string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates a new Executor for the given namespace.
|
||||
func New(namespace string) *Executor {
|
||||
return &Executor{
|
||||
namespace: namespace,
|
||||
}
|
||||
}
|
||||
|
||||
// CommandType represents the type of command being executed.
|
||||
type CommandType string
|
||||
|
||||
const (
|
||||
CommandTypeClaude CommandType = "claude"
|
||||
CommandTypeShell CommandType = "shell"
|
||||
CommandTypeGit CommandType = "git"
|
||||
)
|
||||
|
||||
// Command represents a command to execute in a pod.
|
||||
type Command struct {
|
||||
ID string
|
||||
PodName string
|
||||
Type CommandType
|
||||
Args []string
|
||||
StartedAt time.Time
|
||||
}
|
||||
|
||||
// Result represents the result of command execution.
|
||||
type Result struct {
|
||||
ExitCode int
|
||||
DurationMs int64
|
||||
Error error
|
||||
}
|
||||
|
||||
// OutputHandler is called for each line of output from the command.
|
||||
type OutputHandler func(stream string, line string)
|
||||
|
||||
// Exec executes a command in the specified pod.
|
||||
// It streams output to the provided handler and returns when complete.
|
||||
func (e *Executor) Exec(ctx context.Context, cmd *Command, handler OutputHandler) Result {
|
||||
e.mu.RLock()
|
||||
namespace := e.namespace
|
||||
e.mu.RUnlock()
|
||||
|
||||
startTime := time.Now()
|
||||
var args []string
|
||||
|
||||
switch cmd.Type {
|
||||
case CommandTypeClaude:
|
||||
// claude "prompt"
|
||||
args = []string{
|
||||
"exec", "-n", namespace, cmd.PodName, "--",
|
||||
"claude", cmd.Args[0], // prompt is first arg
|
||||
}
|
||||
case CommandTypeShell:
|
||||
// bash -c "command"
|
||||
args = []string{
|
||||
"exec", "-n", namespace, cmd.PodName, "--",
|
||||
"bash", "-c", cmd.Args[0], // command is first arg
|
||||
}
|
||||
case CommandTypeGit:
|
||||
// git <args...>
|
||||
args = append([]string{
|
||||
"exec", "-n", namespace, cmd.PodName, "--",
|
||||
"git", "-C", "/workspace",
|
||||
}, cmd.Args...)
|
||||
default:
|
||||
return Result{
|
||||
ExitCode: 1,
|
||||
Error: fmt.Errorf("unknown command type: %s", cmd.Type),
|
||||
}
|
||||
}
|
||||
|
||||
// Create the kubectl command
|
||||
kubectl := exec.CommandContext(ctx, "kubectl", args...)
|
||||
|
||||
// Get stdout and stderr pipes
|
||||
stdout, err := kubectl.StdoutPipe()
|
||||
if err != nil {
|
||||
return Result{ExitCode: 1, Error: fmt.Errorf("stdout pipe: %w", err)}
|
||||
}
|
||||
stderr, err := kubectl.StderrPipe()
|
||||
if err != nil {
|
||||
return Result{ExitCode: 1, Error: fmt.Errorf("stderr pipe: %w", err)}
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := kubectl.Start(); err != nil {
|
||||
return Result{ExitCode: 1, Error: fmt.Errorf("start: %w", err)}
|
||||
}
|
||||
|
||||
// Stream output concurrently
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
streamOutput(stdout, "stdout", handler)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
streamOutput(stderr, "stderr", handler)
|
||||
}()
|
||||
|
||||
// Wait for output to be consumed
|
||||
wg.Wait()
|
||||
|
||||
// Wait for command to complete
|
||||
err = kubectl.Wait()
|
||||
duration := time.Since(startTime)
|
||||
|
||||
result := Result{
|
||||
DurationMs: duration.Milliseconds(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
result.ExitCode = exitError.ExitCode()
|
||||
} else {
|
||||
result.ExitCode = 1
|
||||
result.Error = err
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// streamOutput reads from a reader and sends each line to the handler.
|
||||
func streamOutput(r io.Reader, stream string, handler OutputHandler) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
// Increase buffer size for long lines
|
||||
buf := make([]byte, 0, 64*1024)
|
||||
scanner.Buffer(buf, 1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
handler(stream, scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
// CheckConnection verifies kubectl can connect to the cluster.
|
||||
func (e *Executor) CheckConnection(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "kubectl", "cluster-info", "--request-timeout=5s")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// PodExists checks if a pod exists and is running.
|
||||
func (e *Executor) PodExists(ctx context.Context, podName string) (bool, error) {
|
||||
e.mu.RLock()
|
||||
namespace := e.namespace
|
||||
e.mu.RUnlock()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "kubectl",
|
||||
"get", "pod", podName,
|
||||
"-n", namespace,
|
||||
"-o", "jsonpath={.status.phase}",
|
||||
)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Pod doesn't exist or error
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return string(output) == "Running", nil
|
||||
}
|
||||
@ -2,19 +2,35 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/orchard9/rdev/internal/executor"
|
||||
"github.com/orchard9/rdev/internal/projects"
|
||||
"github.com/orchard9/rdev/pkg/api"
|
||||
)
|
||||
|
||||
// ProjectsHandler handles project-related endpoints.
|
||||
type ProjectsHandler struct{}
|
||||
type ProjectsHandler struct {
|
||||
registry *projects.Registry
|
||||
executor *executor.Executor
|
||||
streams *streamManager
|
||||
cmdID atomic.Uint64
|
||||
}
|
||||
|
||||
// NewProjectsHandler creates a new projects handler.
|
||||
func NewProjectsHandler() *ProjectsHandler {
|
||||
return &ProjectsHandler{}
|
||||
return &ProjectsHandler{
|
||||
registry: projects.NewRegistry("rdev"),
|
||||
executor: executor.New("rdev"),
|
||||
streams: newStreamManager(),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount registers the projects routes.
|
||||
@ -32,24 +48,12 @@ func (h *ProjectsHandler) Mount(r api.Router) {
|
||||
// List returns all available projects.
|
||||
// GET /projects
|
||||
func (h *ProjectsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Implement project discovery from K8s
|
||||
projects := []map[string]any{
|
||||
{
|
||||
"id": "pantheon",
|
||||
"name": "Pantheon",
|
||||
"description": "Go API backend",
|
||||
"pod": "claudebox-pantheon-0",
|
||||
"status": "running",
|
||||
},
|
||||
{
|
||||
"id": "aeries",
|
||||
"name": "Aeries",
|
||||
"description": "Note community platform",
|
||||
"pod": "claudebox-aeries-0",
|
||||
"status": "running",
|
||||
},
|
||||
}
|
||||
// Refresh status from K8s
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
h.registry.RefreshStatus(ctx)
|
||||
|
||||
projects := h.registry.List()
|
||||
api.WriteSuccess(w, r, projects)
|
||||
}
|
||||
|
||||
@ -58,87 +62,230 @@ func (h *ProjectsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *ProjectsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// TODO: Look up project from registry
|
||||
project := map[string]any{
|
||||
"id": id,
|
||||
"name": id,
|
||||
"description": "Project description",
|
||||
"pod": "claudebox-" + id + "-0",
|
||||
"status": "running",
|
||||
"workspace": "/workspace",
|
||||
"config": map[string]any{
|
||||
"claude_auth": true,
|
||||
"git_enabled": true,
|
||||
"last_command": nil,
|
||||
},
|
||||
project, ok := h.registry.Get(id)
|
||||
if !ok {
|
||||
api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh this project's status
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
h.registry.RefreshStatus(ctx)
|
||||
|
||||
api.WriteSuccess(w, r, project)
|
||||
}
|
||||
|
||||
// ClaudeRequest is the request body for POST /projects/{id}/claude.
|
||||
type ClaudeRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
StreamID string `json:"stream_id,omitempty"`
|
||||
}
|
||||
|
||||
// RunClaude executes a Claude command in the project's claudebox.
|
||||
// POST /projects/{id}/claude
|
||||
func (h *ProjectsHandler) RunClaude(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// TODO: Parse request body for prompt
|
||||
// TODO: Execute kubectl exec -n rdev claudebox-{id}-0 -- claude "{prompt}"
|
||||
project, ok := h.registry.Get(id)
|
||||
if !ok {
|
||||
api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
var req ClaudeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
api.WriteBadRequest(w, r, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Prompt == "" {
|
||||
api.WriteBadRequest(w, r, "prompt is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate command ID
|
||||
cmdNum := h.cmdID.Add(1)
|
||||
cmdID := fmt.Sprintf("cmd-%s-%03d", id, cmdNum)
|
||||
if req.StreamID != "" {
|
||||
cmdID = req.StreamID
|
||||
}
|
||||
|
||||
// Create the command
|
||||
cmd := &executor.Command{
|
||||
ID: cmdID,
|
||||
PodName: project.PodName,
|
||||
Type: executor.CommandTypeClaude,
|
||||
Args: []string{req.Prompt},
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Execute in background
|
||||
go h.executeCommand(cmd)
|
||||
|
||||
result := map[string]any{
|
||||
"id": "cmd-" + id + "-001",
|
||||
"id": cmdID,
|
||||
"project": id,
|
||||
"type": "claude",
|
||||
"status": "queued",
|
||||
"stream_url": "/projects/" + id + "/events?stream_id=cmd-" + id + "-001",
|
||||
"status": "running",
|
||||
"stream_url": fmt.Sprintf("/projects/%s/events?stream_id=%s", id, cmdID),
|
||||
}
|
||||
|
||||
api.WriteCreated(w, r, result)
|
||||
}
|
||||
|
||||
// ShellRequest is the request body for POST /projects/{id}/shell.
|
||||
type ShellRequest struct {
|
||||
Command string `json:"command"`
|
||||
StreamID string `json:"stream_id,omitempty"`
|
||||
}
|
||||
|
||||
// RunShell executes a shell command in the project's claudebox.
|
||||
// POST /projects/{id}/shell
|
||||
func (h *ProjectsHandler) RunShell(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// TODO: Parse request body for command
|
||||
// TODO: Execute kubectl exec -n rdev claudebox-{id}-0 -- bash -c "{command}"
|
||||
project, ok := h.registry.Get(id)
|
||||
if !ok {
|
||||
api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
var req ShellRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
api.WriteBadRequest(w, r, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Command == "" {
|
||||
api.WriteBadRequest(w, r, "command is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate command ID
|
||||
cmdNum := h.cmdID.Add(1)
|
||||
cmdID := fmt.Sprintf("cmd-%s-%03d", id, cmdNum)
|
||||
if req.StreamID != "" {
|
||||
cmdID = req.StreamID
|
||||
}
|
||||
|
||||
// Create the command
|
||||
cmd := &executor.Command{
|
||||
ID: cmdID,
|
||||
PodName: project.PodName,
|
||||
Type: executor.CommandTypeShell,
|
||||
Args: []string{req.Command},
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Execute in background
|
||||
go h.executeCommand(cmd)
|
||||
|
||||
result := map[string]any{
|
||||
"id": "cmd-" + id + "-002",
|
||||
"id": cmdID,
|
||||
"project": id,
|
||||
"type": "shell",
|
||||
"status": "queued",
|
||||
"stream_url": "/projects/" + id + "/events?stream_id=cmd-" + id + "-002",
|
||||
"status": "running",
|
||||
"stream_url": fmt.Sprintf("/projects/%s/events?stream_id=%s", id, cmdID),
|
||||
}
|
||||
|
||||
api.WriteCreated(w, r, result)
|
||||
}
|
||||
|
||||
// GitRequest is the request body for POST /projects/{id}/git.
|
||||
type GitRequest struct {
|
||||
Args []string `json:"args"`
|
||||
StreamID string `json:"stream_id,omitempty"`
|
||||
}
|
||||
|
||||
// RunGit executes a git command in the project's claudebox.
|
||||
// POST /projects/{id}/git
|
||||
func (h *ProjectsHandler) RunGit(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// TODO: Parse request body for git command
|
||||
// TODO: Execute kubectl exec -n rdev claudebox-{id}-0 -- git {args}
|
||||
project, ok := h.registry.Get(id)
|
||||
if !ok {
|
||||
api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
var req GitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
api.WriteBadRequest(w, r, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Args) == 0 {
|
||||
api.WriteBadRequest(w, r, "args is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate command ID
|
||||
cmdNum := h.cmdID.Add(1)
|
||||
cmdID := fmt.Sprintf("cmd-%s-%03d", id, cmdNum)
|
||||
if req.StreamID != "" {
|
||||
cmdID = req.StreamID
|
||||
}
|
||||
|
||||
// Create the command
|
||||
cmd := &executor.Command{
|
||||
ID: cmdID,
|
||||
PodName: project.PodName,
|
||||
Type: executor.CommandTypeGit,
|
||||
Args: req.Args,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Execute in background
|
||||
go h.executeCommand(cmd)
|
||||
|
||||
result := map[string]any{
|
||||
"id": "cmd-" + id + "-003",
|
||||
"id": cmdID,
|
||||
"project": id,
|
||||
"type": "git",
|
||||
"status": "queued",
|
||||
"stream_url": "/projects/" + id + "/events?stream_id=cmd-" + id + "-003",
|
||||
"status": "running",
|
||||
"stream_url": fmt.Sprintf("/projects/%s/events?stream_id=%s", id, cmdID),
|
||||
}
|
||||
|
||||
api.WriteCreated(w, r, result)
|
||||
}
|
||||
|
||||
// executeCommand runs a command and streams output to subscribers.
|
||||
func (h *ProjectsHandler) executeCommand(cmd *executor.Command) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
result := h.executor.Exec(ctx, cmd, func(stream, line string) {
|
||||
h.streams.Send(cmd.ID, "output", map[string]any{
|
||||
"line": line,
|
||||
"stream": stream,
|
||||
})
|
||||
})
|
||||
|
||||
// Send completion event
|
||||
h.streams.Send(cmd.ID, "complete", map[string]any{
|
||||
"exit_code": result.ExitCode,
|
||||
"duration_ms": result.DurationMs,
|
||||
})
|
||||
|
||||
// Clean up stream after a delay
|
||||
go func() {
|
||||
time.Sleep(30 * time.Second)
|
||||
h.streams.Close(cmd.ID)
|
||||
}()
|
||||
}
|
||||
|
||||
// Events streams command output via Server-Sent Events.
|
||||
// GET /projects/{id}/events
|
||||
func (h *ProjectsHandler) Events(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
streamID := r.URL.Query().Get("stream_id")
|
||||
|
||||
if !h.registry.Exists(id) {
|
||||
api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
// Set SSE headers
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
@ -151,8 +298,9 @@ func (h *ProjectsHandler) Events(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Stream actual command output
|
||||
// For now, send mock events
|
||||
// Subscribe to events
|
||||
events := h.streams.Subscribe(streamID)
|
||||
defer h.streams.Unsubscribe(streamID, events)
|
||||
|
||||
// Send initial connected event
|
||||
writeSSE(w, flusher, "connected", map[string]any{
|
||||
@ -160,33 +308,98 @@ func (h *ProjectsHandler) Events(w http.ResponseWriter, r *http.Request) {
|
||||
"stream_id": streamID,
|
||||
})
|
||||
|
||||
// Send mock output
|
||||
writeSSE(w, flusher, "output", map[string]any{
|
||||
"line": "Starting command execution...",
|
||||
"stream": "stdout",
|
||||
})
|
||||
// Stream events until client disconnects or stream closes
|
||||
ctx := r.Context()
|
||||
heartbeat := time.NewTicker(30 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
|
||||
writeSSE(w, flusher, "output", map[string]any{
|
||||
"line": "Command completed successfully.",
|
||||
"stream": "stdout",
|
||||
})
|
||||
|
||||
// Send complete event
|
||||
writeSSE(w, flusher, "complete", map[string]any{
|
||||
"exit_code": 0,
|
||||
"duration_ms": 1234,
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeSSE(w, flusher, event.Type, event.Data)
|
||||
if event.Type == "complete" {
|
||||
return
|
||||
}
|
||||
case <-heartbeat.C:
|
||||
writeSSE(w, flusher, "heartbeat", map[string]any{
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeSSE writes a Server-Sent Event.
|
||||
func writeSSE(w http.ResponseWriter, flusher http.Flusher, event string, data map[string]any) {
|
||||
dataBytes, _ := jsonMarshal(data)
|
||||
w.Write([]byte("event: " + event + "\n"))
|
||||
w.Write([]byte("data: " + string(dataBytes) + "\n\n"))
|
||||
dataBytes, _ := json.Marshal(data)
|
||||
fmt.Fprintf(w, "event: %s\n", event)
|
||||
fmt.Fprintf(w, "data: %s\n\n", dataBytes)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
// jsonMarshal is a simple JSON marshal helper.
|
||||
func jsonMarshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
// streamManager manages SSE event streams.
|
||||
type streamManager struct {
|
||||
mu sync.RWMutex
|
||||
streams map[string][]chan streamEvent
|
||||
}
|
||||
|
||||
type streamEvent struct {
|
||||
Type string
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
func newStreamManager() *streamManager {
|
||||
return &streamManager{
|
||||
streams: make(map[string][]chan streamEvent),
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *streamManager) Subscribe(streamID string) chan streamEvent {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
ch := make(chan streamEvent, 100)
|
||||
sm.streams[streamID] = append(sm.streams[streamID], ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (sm *streamManager) Unsubscribe(streamID string, ch chan streamEvent) {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
channels := sm.streams[streamID]
|
||||
for i, c := range channels {
|
||||
if c == ch {
|
||||
sm.streams[streamID] = append(channels[:i], channels[i+1:]...)
|
||||
close(ch)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *streamManager) Send(streamID, eventType string, data map[string]any) {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
|
||||
for _, ch := range sm.streams[streamID] {
|
||||
select {
|
||||
case ch <- streamEvent{Type: eventType, Data: data}:
|
||||
default:
|
||||
// Channel full, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *streamManager) Close(streamID string) {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
for _, ch := range sm.streams[streamID] {
|
||||
close(ch)
|
||||
}
|
||||
delete(sm.streams, streamID)
|
||||
}
|
||||
|
||||
148
internal/projects/registry.go
Normal file
148
internal/projects/registry.go
Normal file
@ -0,0 +1,148 @@
|
||||
// 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)
|
||||
}
|
||||
@ -1,5 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Build and push claudebox image to GitHub Container Registry
|
||||
# Build and push rdev images to GitHub Container Registry
|
||||
#
|
||||
# Usage:
|
||||
# ./build-push.sh # Build both images with 'latest' tag
|
||||
# ./build-push.sh v0.4.0 # Build both images with version tag
|
||||
# ./build-push.sh v0.4.0 claudebox # Build only claudebox image
|
||||
# ./build-push.sh v0.4.0 api # Build only api image
|
||||
|
||||
set -e
|
||||
|
||||
@ -8,31 +14,77 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Image configuration
|
||||
REGISTRY="ghcr.io/orchard9"
|
||||
IMAGE_NAME="rdev-claudebox"
|
||||
VERSION="${1:-latest}"
|
||||
|
||||
IMAGE_TAG="$REGISTRY/$IMAGE_NAME:$VERSION"
|
||||
|
||||
echo "Building claudebox image..."
|
||||
echo "Image: $IMAGE_TAG"
|
||||
echo ""
|
||||
TARGET="${2:-all}"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Build the image for linux/amd64 (k3s nodes are amd64)
|
||||
docker build --platform linux/amd64 -t "$IMAGE_TAG" -t "$REGISTRY/$IMAGE_NAME:latest" .
|
||||
build_claudebox() {
|
||||
local IMAGE_TAG="$REGISTRY/rdev-claudebox:$VERSION"
|
||||
echo "Building claudebox image..."
|
||||
echo "Image: $IMAGE_TAG"
|
||||
echo ""
|
||||
|
||||
echo ""
|
||||
echo "Pushing to GitHub Container Registry..."
|
||||
# Build the image for linux/amd64 (k3s nodes are amd64)
|
||||
docker build --platform linux/amd64 \
|
||||
-t "$IMAGE_TAG" \
|
||||
-t "$REGISTRY/rdev-claudebox:latest" \
|
||||
-f Dockerfile \
|
||||
.
|
||||
|
||||
# Push both tags
|
||||
docker push "$IMAGE_TAG"
|
||||
docker push "$REGISTRY/$IMAGE_NAME:latest"
|
||||
echo ""
|
||||
echo "Pushing claudebox to GitHub Container Registry..."
|
||||
docker push "$IMAGE_TAG"
|
||||
docker push "$REGISTRY/rdev-claudebox:latest"
|
||||
|
||||
echo "Pushed: $IMAGE_TAG"
|
||||
}
|
||||
|
||||
build_api() {
|
||||
local IMAGE_TAG="$REGISTRY/rdev-api:$VERSION"
|
||||
echo "Building rdev-api image..."
|
||||
echo "Image: $IMAGE_TAG"
|
||||
echo ""
|
||||
|
||||
# Build the image for linux/amd64
|
||||
docker build --platform linux/amd64 \
|
||||
-t "$IMAGE_TAG" \
|
||||
-t "$REGISTRY/rdev-api:latest" \
|
||||
-f Dockerfile.api \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo "Pushing rdev-api to GitHub Container Registry..."
|
||||
docker push "$IMAGE_TAG"
|
||||
docker push "$REGISTRY/rdev-api:latest"
|
||||
|
||||
echo "Pushed: $IMAGE_TAG"
|
||||
}
|
||||
|
||||
case "$TARGET" in
|
||||
claudebox)
|
||||
build_claudebox
|
||||
;;
|
||||
api)
|
||||
build_api
|
||||
;;
|
||||
all)
|
||||
build_claudebox
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
build_api
|
||||
;;
|
||||
*)
|
||||
echo "Unknown target: $TARGET"
|
||||
echo "Usage: $0 [version] [claudebox|api|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Done!"
|
||||
echo ""
|
||||
echo "Image pushed: $IMAGE_TAG"
|
||||
echo ""
|
||||
echo "To deploy, run:"
|
||||
echo " ./scripts/deploy.sh"
|
||||
echo " export KUBECONFIG=~/.kube/orchard9-k3sf.yaml"
|
||||
echo " kubectl apply -k deployments/k8s/base"
|
||||
|
||||
81
scripts/generate-deploy-key.sh
Executable file
81
scripts/generate-deploy-key.sh
Executable file
@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
# Generate SSH deploy key for a GitHub repository
|
||||
#
|
||||
# Usage: ./generate-deploy-key.sh <project-name>
|
||||
# Example: ./generate-deploy-key.sh pantheon
|
||||
#
|
||||
# This generates:
|
||||
# - <project>-deploy-key (private key)
|
||||
# - <project>-deploy-key.pub (public key - add to GitHub)
|
||||
# - <project>-deploy-key.b64 (base64 encoded for K8s secret)
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <project-name>"
|
||||
echo "Example: $0 pantheon"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT="$1"
|
||||
KEY_FILE="${PROJECT}-deploy-key"
|
||||
|
||||
echo "Generating deploy key for project: $PROJECT"
|
||||
echo ""
|
||||
|
||||
# Check if key already exists
|
||||
if [ -f "$KEY_FILE" ]; then
|
||||
echo "WARNING: Key file $KEY_FILE already exists!"
|
||||
read -p "Overwrite? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate ED25519 key (no passphrase for automated use)
|
||||
ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "rdev-${PROJECT}@orchard9.ai"
|
||||
|
||||
# Create base64 encoded version for K8s secret
|
||||
cat "$KEY_FILE" | base64 > "${KEY_FILE}.b64"
|
||||
|
||||
echo ""
|
||||
echo "=== Generated Files ==="
|
||||
echo ""
|
||||
echo "Private key: $KEY_FILE"
|
||||
echo "Public key: ${KEY_FILE}.pub"
|
||||
echo "Base64: ${KEY_FILE}.b64"
|
||||
echo ""
|
||||
echo "=== Next Steps ==="
|
||||
echo ""
|
||||
echo "1. Add the PUBLIC key to GitHub:"
|
||||
echo " - Go to: https://github.com/orchard9/${PROJECT}/settings/keys"
|
||||
echo " - Click 'Add deploy key'"
|
||||
echo " - Title: rdev-${PROJECT}"
|
||||
echo " - Key: (paste contents of ${KEY_FILE}.pub)"
|
||||
echo " - Check 'Allow write access' if you need push capability"
|
||||
echo ""
|
||||
echo " Public key to copy:"
|
||||
echo " ---"
|
||||
cat "${KEY_FILE}.pub"
|
||||
echo " ---"
|
||||
echo ""
|
||||
echo "2. Update the Kubernetes secret:"
|
||||
echo " - Edit deployments/k8s/base/secrets.yaml"
|
||||
echo " - Replace REPLACE_WITH_BASE64_ENCODED_PRIVATE_KEY for ${PROJECT}"
|
||||
echo " - With contents of: ${KEY_FILE}.b64"
|
||||
echo ""
|
||||
echo " Base64 encoded private key:"
|
||||
echo " ---"
|
||||
cat "${KEY_FILE}.b64"
|
||||
echo " ---"
|
||||
echo ""
|
||||
echo "3. Apply the secret:"
|
||||
echo " export KUBECONFIG=~/.kube/orchard9-k3sf.yaml"
|
||||
echo " kubectl apply -f deployments/k8s/base/secrets.yaml"
|
||||
echo ""
|
||||
echo "4. IMPORTANT: Keep the private key files secure!"
|
||||
echo " - Do NOT commit them to git"
|
||||
echo " - Store them securely or delete after updating K8s secret"
|
||||
echo ""
|
||||
Loading…
Reference in New Issue
Block a user