Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| def2c5b3f3 | |||
| df3413f54e | |||
| 6a35f38a8c | |||
| 431b9079ee | |||
| 00d88b16b5 | |||
| c10dd718e1 | |||
| b6bf4b6640 | |||
| c42b47bb56 | |||
| 288c1a4103 | |||
| 2caa8a790f | |||
| 7a6a515b53 | |||
| 4f126a938b | |||
| 4af38a5d2e | |||
| 90350a2090 | |||
| 5b8e6a290b | |||
| e860499757 | |||
| e90a2fe553 | |||
| 897f1409b5 | |||
| 32d4fe4944 |
@@ -1,29 +1,17 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "github-mcp-server",
|
||||
"args": ["stdio"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${CLAUDE_GITHUB_TOKEN}"
|
||||
}
|
||||
"kubernetes": {
|
||||
"type": "sse",
|
||||
"url": "http://localhost:8080/sse"
|
||||
},
|
||||
"kubernetes (local)": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"kubernetes-mcp-server@latest"
|
||||
]
|
||||
},
|
||||
"flux (local)":{
|
||||
"command":"flux-operator-mcp",
|
||||
"args":["serve"],
|
||||
"env":{
|
||||
"KUBECONFIG":"/Users/cpfarhood/.kube/config"
|
||||
}
|
||||
"flux": {
|
||||
"type": "sse",
|
||||
"url": "http://localhost:8081/sse"
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@latest"]
|
||||
}
|
||||
"type": "sse",
|
||||
"url": "http://playwright-mcp.playwright.svc.cluster.local:3000/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-2
@@ -35,7 +35,25 @@ RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearm
|
||||
# Chrome wrapper: adds flags required for running inside a Docker container.
|
||||
# xdg-open (used by Claude Code on Linux) respects $BROWSER, so pointing it
|
||||
# here ensures the OAuth popup works without manual --no-sandbox invocations.
|
||||
RUN printf '#!/bin/bash\nexec /usr/bin/google-chrome-stable \\\n --no-sandbox \\\n --disable-dev-shm-usage \\\n --disable-gpu \\\n "$@"\n' > /usr/local/bin/google-chrome && \
|
||||
# Cleans up crash lock files and suppresses the crash-restore bubble so that
|
||||
# sessions/cookies survive unclean pod shutdowns (SIGKILL).
|
||||
RUN printf '#!/bin/bash\n\
|
||||
CHROME_DIR="/config/userdata/.config/google-chrome"\n\
|
||||
mkdir -p "$CHROME_DIR"\n\
|
||||
# Remove stale lock files left by unclean container shutdown\n\
|
||||
rm -f "$CHROME_DIR/SingletonLock" "$CHROME_DIR/SingletonSocket" "$CHROME_DIR/SingletonCookie"\n\
|
||||
# Mark the previous session as clean so Chrome does not clear cookies\n\
|
||||
PREFS="$CHROME_DIR/Default/Preferences"\n\
|
||||
if [ -f "$PREFS" ]; then\n\
|
||||
sed -i '\''s/"exit_type":"Crashed"/"exit_type":"Normal"/g; s/"exited_cleanly":false/"exited_cleanly":true/g'\'' "$PREFS"\n\
|
||||
fi\n\
|
||||
exec /usr/bin/google-chrome-stable \\\n\
|
||||
--no-sandbox \\\n\
|
||||
--disable-dev-shm-usage \\\n\
|
||||
--disable-gpu \\\n\
|
||||
--disable-session-crashed-bubble \\\n\
|
||||
--user-data-dir="$CHROME_DIR" \\\n\
|
||||
"$@"\n' > /usr/local/bin/google-chrome && \
|
||||
chmod +x /usr/local/bin/google-chrome
|
||||
|
||||
# Install Node.js (LTS version for Happy Coder)
|
||||
@@ -93,7 +111,7 @@ COPY --chmod=755 scripts/cont-init-sshd.sh /etc/cont-init.d/25-start-sshd.sh
|
||||
WORKDIR /workspace
|
||||
|
||||
# Configure container to run as user user
|
||||
ENV HOME=/home/user \
|
||||
ENV HOME=/config/userdata \
|
||||
USER=user \
|
||||
BROWSER=/usr/local/bin/google-chrome
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||

|
||||
|
||||
A containerized cloud development environment with web-based GUI access, featuring:
|
||||
- **VSCode** via browser-based VNC (port 5800)
|
||||
- **VSCode or Google Antigravity** via browser-based VNC (port 5800)
|
||||
- **SSH access** option (OpenSSH on port 22, additive with any IDE)
|
||||
- **Happy Coder** AI assistant backed by Claude
|
||||
- **Automatic GitHub repo cloning** on startup
|
||||
- **Persistent home directory** via ReadWriteMany PVC
|
||||
@@ -160,6 +161,7 @@ With any non-`none` value, a `ServiceAccount` named `devcontainer-{name}` is cre
|
||||
| `groupId` | `1000` | GID for the app user |
|
||||
| `storage.size` | `32Gi` | Home PVC size |
|
||||
| `storage.className` | `ceph-filesystem` | StorageClass (must be ReadWriteMany) |
|
||||
| `shm.sizeLimit` | `2Gi` | `/dev/shm` size (memory-backed; used by Electron apps) |
|
||||
| `resources.requests.memory` | `2Gi` | |
|
||||
| `resources.requests.cpu` | `1000m` | |
|
||||
| `resources.limits.memory` | `8Gi` | |
|
||||
@@ -182,9 +184,9 @@ Container start
|
||||
→ rm daemon.state.json.lock — clear stale Happy lock
|
||||
→ happy daemon start — starts Happy Coder background daemon
|
||||
→ IDE=vscode: code --new-window --wait /workspace/{repo}
|
||||
IDE=antigravity: antigravity --new-window --wait /workspace/{repo}
|
||||
IDE=antigravity: antigravity --no-sandbox --user-data-dir ~/.config/antigravity ... /workspace/{repo}
|
||||
IDE=none: sleep infinity
|
||||
(SSH=true: sshd also running as root on port 22)
|
||||
(SSH=true: sshd also running as root on port 22; host keys persisted on PVC)
|
||||
```
|
||||
|
||||
### Storage
|
||||
@@ -230,7 +232,15 @@ Then restart the pod to pick up the new env var.
|
||||
```bash
|
||||
kubectl port-forward deployment/devcontainer-mydev 5800:5800
|
||||
kubectl logs deployment/devcontainer-mydev
|
||||
kubectl describe pod -l instance=mydev
|
||||
kubectl describe pod -l app.kubernetes.io/instance=mydev
|
||||
```
|
||||
|
||||
### Pod not picking up new image after upgrade
|
||||
|
||||
The chart uses `image.tag: latest`. Kubernetes won't restart the pod on a Helm upgrade unless the Deployment spec changes. Force a restart manually:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/devcontainer-mydev
|
||||
```
|
||||
|
||||
### Repository not cloning
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@ apiVersion: v2
|
||||
name: devcontainer
|
||||
description: Antigravity Dev Container with Happy Coder AI assistant
|
||||
type: application
|
||||
version: 0.1.6
|
||||
version: 0.1.15
|
||||
appVersion: "latest"
|
||||
|
||||
@@ -68,9 +68,11 @@ spec:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: userhome
|
||||
mountPath: /home
|
||||
mountPath: /config
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: shm
|
||||
mountPath: /dev/shm
|
||||
{{- if ne (.Values.ide | default "vscode") "none" }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -96,9 +98,62 @@ spec:
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
{{- end }}
|
||||
{{- if .Values.mcpSidecars.kubernetes.enabled }}
|
||||
- name: kubernetes-mcp
|
||||
image: "{{ .Values.mcpSidecars.kubernetes.image.repository }}:{{ .Values.mcpSidecars.kubernetes.image.tag }}"
|
||||
args:
|
||||
- --port
|
||||
- {{ .Values.mcpSidecars.kubernetes.port | quote }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.mcpSidecars.kubernetes.port }}
|
||||
name: k8s-mcp
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: {{ .Values.mcpSidecars.kubernetes.port }}
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: {{ .Values.mcpSidecars.kubernetes.port }}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
{{- toYaml .Values.mcpSidecars.kubernetes.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.mcpSidecars.flux.enabled }}
|
||||
- name: flux-mcp
|
||||
image: "{{ .Values.mcpSidecars.flux.image.repository }}:{{ .Values.mcpSidecars.flux.image.tag }}"
|
||||
args:
|
||||
- serve
|
||||
- --transport=sse
|
||||
- --port={{ .Values.mcpSidecars.flux.port }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.mcpSidecars.flux.port }}
|
||||
name: flux-mcp
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: {{ .Values.mcpSidecars.flux.port }}
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: {{ .Values.mcpSidecars.flux.port }}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
{{- toYaml .Values.mcpSidecars.flux.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
- name: shm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: {{ .Values.shm.sizeLimit }}
|
||||
- name: userhome
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "antigravity.pvcName" . }}
|
||||
|
||||
+35
-1
@@ -23,7 +23,7 @@ ssh: false
|
||||
# Happy Coder endpoints
|
||||
happyServerUrl: "https://happy.farh.net"
|
||||
happyWebappUrl: "https://happy-coder.farh.net"
|
||||
happyHomeDir: "/home/user/.happy"
|
||||
happyHomeDir: "/config/userdata/.happy"
|
||||
happyExperimental: "true"
|
||||
|
||||
# VNC display
|
||||
@@ -41,6 +41,11 @@ storage:
|
||||
size: 32Gi
|
||||
className: ceph-filesystem
|
||||
|
||||
# Shared memory size — mounted at /dev/shm as a memory-backed emptyDir.
|
||||
# Electron apps (Antigravity, Chrome) use /dev/shm for GPU/IPC buffers.
|
||||
shm:
|
||||
sizeLimit: 2Gi
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
@@ -61,3 +66,32 @@ clusterAccess: none
|
||||
# Name of existing Secret containing env vars (GITHUB_TOKEN, VNC_PASSWORD, etc.)
|
||||
# Defaults to: devcontainer-{name}-secrets-env
|
||||
envSecretName: ""
|
||||
|
||||
# MCP server sidecars — run alongside the devcontainer to inherit pod RBAC.
|
||||
mcpSidecars:
|
||||
kubernetes:
|
||||
enabled: true
|
||||
image:
|
||||
repository: quay.io/containers/kubernetes_mcp_server
|
||||
tag: latest
|
||||
port: 8080
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
flux:
|
||||
enabled: true
|
||||
image:
|
||||
repository: ghcr.io/controlplaneio-fluxcd/flux-operator-mcp
|
||||
tag: latest
|
||||
port: 8081
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
|
||||
@@ -5,12 +5,30 @@
|
||||
|
||||
echo "=== SSH enabled: starting sshd ==="
|
||||
|
||||
# Generate host keys if missing (first boot or ephemeral /etc/ssh)
|
||||
ssh-keygen -A 2>/dev/null || true
|
||||
HOME_DIR="/config/userdata"
|
||||
HOST_KEY_STORE="$HOME_DIR/.ssh/host_keys"
|
||||
|
||||
# Persist host keys on the home PVC so clients don't see a "host key
|
||||
# changed" warning after pod restarts.
|
||||
if [ -d "$HOST_KEY_STORE" ] && [ -n "$(ls "$HOST_KEY_STORE"/ssh_host_* 2>/dev/null)" ]; then
|
||||
# Restore previously generated host keys
|
||||
echo "Restoring SSH host keys from PVC..."
|
||||
cp "$HOST_KEY_STORE"/ssh_host_* /etc/ssh/
|
||||
chmod 600 /etc/ssh/ssh_host_*_key
|
||||
chmod 644 /etc/ssh/ssh_host_*_key.pub
|
||||
else
|
||||
# First boot: generate and save host keys to PVC
|
||||
echo "Generating SSH host keys (first boot)..."
|
||||
ssh-keygen -A 2>/dev/null || true
|
||||
mkdir -p "$HOST_KEY_STORE"
|
||||
cp /etc/ssh/ssh_host_* "$HOST_KEY_STORE/"
|
||||
chmod 700 "$HOST_KEY_STORE"
|
||||
chown -R 1000:1000 "$HOST_KEY_STORE"
|
||||
echo "SSH host keys saved to PVC."
|
||||
fi
|
||||
|
||||
# Populate authorized_keys from env var (injected via Kubernetes secret)
|
||||
if [ -n "$SSH_AUTHORIZED_KEYS" ]; then
|
||||
HOME_DIR="/home/user"
|
||||
mkdir -p "$HOME_DIR/.ssh"
|
||||
chmod 700 "$HOME_DIR/.ssh"
|
||||
printf '%s\n' "$SSH_AUTHORIZED_KEYS" > "$HOME_DIR/.ssh/authorized_keys"
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# baseimage-gui sets shell=/sbin/nologin and home=/dev/null, which
|
||||
# prevents VSCode from opening terminals.
|
||||
usermod -s /bin/bash app
|
||||
usermod -d /home/user app
|
||||
usermod -d /config/userdata app
|
||||
|
||||
@@ -25,8 +25,8 @@ else
|
||||
# Configure git to use token if provided
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
git config credential.helper store
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/.git-credentials
|
||||
chmod 600 /home/.git-credentials
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /config/userdata/.git-credentials
|
||||
chmod 600 /config/userdata/.git-credentials
|
||||
fi
|
||||
|
||||
git pull || echo "Pull failed, continuing anyway..."
|
||||
@@ -42,8 +42,8 @@ else
|
||||
|
||||
# Configure credentials for future use
|
||||
git config --global credential.helper store
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/.git-credentials
|
||||
chmod 600 /home/.git-credentials
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /config/userdata/.git-credentials
|
||||
chmod 600 /config/userdata/.git-credentials
|
||||
else
|
||||
git clone "$GITHUB_REPO" "$WORKSPACE_DIR"
|
||||
fi
|
||||
|
||||
+7
-1
@@ -21,7 +21,13 @@ echo "Workspace: $WORKSPACE_DIR"
|
||||
case "$IDE" in
|
||||
antigravity)
|
||||
echo "Opening Google Antigravity in: $WORKSPACE_DIR"
|
||||
exec antigravity --new-window --wait "$WORKSPACE_DIR"
|
||||
# --no-sandbox is required for Electron apps in Docker (no kernel sandbox available).
|
||||
# Explicit --user-data-dir and --extensions-dir pin config to the home PVC so
|
||||
# settings and the setup wizard state survive pod restarts.
|
||||
exec antigravity --no-sandbox \
|
||||
--user-data-dir "$HOME/.config/antigravity" \
|
||||
--extensions-dir "$HOME/.antigravity/extensions" \
|
||||
--new-window --wait "$WORKSPACE_DIR"
|
||||
;;
|
||||
none)
|
||||
echo "IDE=none: no IDE launched, keeping container alive."
|
||||
|
||||
Reference in New Issue
Block a user