Compare commits

..

82 Commits

Author SHA1 Message Date
DevContainer User ac110f3edf chore: bump chart version to 0.1.22 [skip ci] 2026-02-21 15:09:38 +00:00
DevContainer User c311312a87 fix: correct Home Assistant MCP sidecar command for SSE mode
The Home Assistant MCP was failing with 'fastmcp-sse.json not found' error.
Updated command to use proper fastmcp arguments for SSE transport mode
without requiring a configuration file.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 15:09:26 +00:00
DevContainer User 68110d911f chore: bump chart version to 0.1.21 [skip ci] 2026-02-21 14:33:16 +00:00
DevContainer User 427f7a710c fix: automate git credentials setup during container initialization
- Configure git credentials at the beginning of init-repo.sh
- Set up git user name/email with defaults or from environment variables
- Create .git-credentials file with proper permissions (600)
- Support multiple GitHub credential formats for better compatibility
- Create symlinks to handle different credential file locations
- Add test script to verify credentials configuration
- Update documentation with new environment variables

This fixes issues where containers fail due to missing .git-credentials
by ensuring credentials are properly configured before any git operations.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 14:28:58 +00:00
github-actions[bot] 745a0cdf59 chore: bump chart version to 0.1.20 [skip ci] 2026-02-21 13:54:03 +00:00
Developer 115907cdc8 fix: pin MCP sidecar versions for stability
- Pin kubernetes-mcp to v0.0.57 (Jan 27, 2025) with token exchange and field selector support
- Pin flux-mcp to v0.41.1 (already pinned)
- Pin homeassistant-mcp to v6.7.1 (Feb 20, 2026) - latest stable release
- Update documentation with version details
- Bump chart version to 0.1.19

This ensures reproducible deployments and prevents unexpected breaking changes
from floating tags (latest/stable).

Generated with Claude Code via Happy

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 13:53:49 +00:00
github-actions[bot] 0c4f93c077 chore: bump chart version to 0.1.19 [skip ci] 2026-02-21 13:27:34 +00:00
Developer a83d79bc10 feat: add Home Assistant MCP sidecar and fix K8s/Flux MCP deployment logic
Added features:
- Home Assistant MCP server as optional sidecar (mcpSidecars.homeassistant)
- Requires homeassistant-url and homeassistant-token secrets
- Runs on port 8087 using SSE transport mode
- Disabled by default due to credential requirements

Fixed deployment logic:
- Kubernetes and Flux MCP sidecars now only deploy when:
  1. They are enabled in values (mcpSidecars.<name>.enabled: true)
  2. AND clusterAccess is not "none" (they need RBAC to function)
- Prevents unnecessary container failures when no permissions exist

Documentation updates:
- Complete Helm values reference for all MCP sidecars
- Deployment examples and troubleshooting guides
- Updated memory notes with current architecture

Breaking change:
- K8s/Flux MCP sidecars won't deploy with clusterAccess=none
- This is intentional as they cannot function without RBAC

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 13:27:20 +00:00
Developer 2258df4ae3 docs: comprehensive documentation update for Helm chart and MCP sidecars
- Added MCP sidecar configuration documentation across all docs
- Migrated from kustomize to Helm-based deployment instructions
- Updated Makefile with new helm-* targets, removed outdated k8s-* targets
- Rewrote DEPLOYMENT.md to focus on Helm chart deployment
- Transformed VARIABLES.md into complete Helm values reference
- Added MCP sidecar section to README.md with configuration examples

Key improvements:
- Clear instructions for enabling/disabling MCP servers
- Consistent Helm-based deployment throughout documentation
- Comprehensive values reference with examples
- Better organization for different deployment scenarios

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 12:48:40 +00:00
github-actions[bot] d4b069cbdc chore: bump chart version to 0.1.17 [skip ci] 2026-02-21 12:11:46 +00:00
Chris Farhood db7e422b96 fix(mcp): use v0.41.1 for flux-operator-mcp instead of latest 2026-02-21 07:11:37 -05:00
Chris Farhood d5bbf21578 docs: update CLAUDE.md for Helm chart and MCP sidecars
Reflect current architecture: Helm chart instead of raw k8s/ manifests,
document MCP sidecar containers, fix storage mount path, and update
Kubernetes notes for Flux-based deployment.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 02:01:40 +00:00
github-actions[bot] 1c3398b178 chore: bump chart version to 0.1.15 [skip ci] 2026-02-21 00:33:29 +00:00
Chris Farhood 4e67c48a4c Merge pull request #25 from cpfarhood/feat/mcp-sidecars
feat: add Kubernetes and Flux MCP servers as pod sidecars
2026-02-21 00:33:20 +00:00
Chris Farhood df3413f54e feat: add Kubernetes and Flux MCP servers as pod sidecars
Run MCP servers as sidecar containers so they inherit the pod's
ServiceAccount permissions instead of requiring separate deployments
with their own RBAC. Kubernetes MCP on :8080, Flux MCP on :8081.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-21 00:29:55 +00:00
Chris Farhood 6a35f38a8c add mcp to repo 2026-02-20 23:33:40 +00:00
github-actions[bot] 431b9079ee chore: bump chart version to 0.1.14 [skip ci] 2026-02-20 22:27:56 +00:00
Chris Farhood 00d88b16b5 Merge pull request #24 from cpfarhood/fix/persist-config-to-pvc
fix: mount PVC at /config to persist Chrome and app state
2026-02-20 22:27:47 +00:00
Chris Farhood c10dd718e1 Merge pull request #23 from cpfarhood/docs/final-readme-pass
docs: final README pass
2026-02-20 22:27:05 +00:00
Antigravity Developer b6bf4b6640 fix: mount PVC at /config to persist Chrome and app state across restarts
The jlesage/baseimage-gui sets XDG_CONFIG_HOME=/config/xdg/config at
runtime, so Chrome was writing its profile to /config/xdg/config/google-chrome
which lived on ephemeral storage. This caused Chrome to open as a fresh
install on every pod restart.

Changes:
- Mount the PVC at /config instead of /home (aligns with baseimage-gui convention)
- Move user home directory to /config/userdata (on the PVC)
- Add explicit --user-data-dir for Chrome pointing to PVC path
- Clean up Chrome crash lock files and patch Preferences on startup
  to prevent session/cookie loss after unclean pod shutdown
- Update all scripts (sshd, init-repo, cont-init) to use new paths
- Remove unnecessary cont-init-home.sh

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 22:21:36 +00:00
Antigravity Developer c42b47bb56 fix: persist Chrome profile data and settings across container restarts
- Add explicit --user-data-dir flag to Chrome wrapper to ensure profile data
  is stored in the persistent home directory
- Add cont-init-home.sh script to properly initialize home directory structure
  on container startup with correct permissions
- Ensure Chrome config directory exists before Chrome starts
- Bump chart version to 0.1.13

This fixes the issue where Chrome loses authentication and settings after
pod restarts by explicitly managing where Chrome stores its profile data.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 21:31:32 +00:00
Chris Farhood 288c1a4103 docs: final README pass
- Add Antigravity and SSH to feature list in intro
- Add shm.sizeLimit to Display/resources table
- Update startup flow to show accurate antigravity launch flags
- Fix kubectl describe label selector
- Add troubleshooting note for latest-tag pod restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:52:09 -05:00
github-actions[bot] 2caa8a790f chore: bump chart version to 0.1.12 [skip ci] 2026-02-20 20:46:18 +00:00
Chris Farhood 7a6a515b53 Merge pull request #22 from cpfarhood/fix/antigravity-userdata-persistence
fix: pin Antigravity user data to home PVC to survive pod restarts
2026-02-20 15:46:07 -05:00
Chris Farhood 4f126a938b fix: persist SSH host keys on home PVC to avoid known_hosts warnings
On first boot, generated host keys are saved to ~/.ssh/host_keys/ on
the persistent home PVC. On subsequent boots they are restored, so SSH
clients never see a "host key changed" warning after a pod restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:45:34 -05:00
Chris Farhood 4af38a5d2e fix: pin Antigravity user data to home PVC to survive pod restarts
Without explicit --user-data-dir and --extensions-dir, Antigravity may
default to a path outside /home and lose settings on restart, causing
the setup wizard to reappear. Pinning both to $HOME ensures they land
on the persistent home PVC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:43:21 -05:00
github-actions[bot] 90350a2090 chore: bump chart version to 0.1.10 [skip ci] 2026-02-20 20:25:42 +00:00
Chris Farhood 5b8e6a290b Merge pull request #21 from cpfarhood/fix/antigravity-no-sandbox
fix: add --no-sandbox to antigravity launch command
2026-02-20 15:25:33 -05:00
Chris Farhood e860499757 fix: mount memory-backed emptyDir at /dev/shm for Electron apps
Instead of disabling shared memory usage, mount a proper tmpfs at
/dev/shm so Antigravity (and Chrome) have real shared memory available.
Removes --disable-dev-shm-usage; keeps --no-sandbox (separate issue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:24:15 -05:00
Chris Farhood e90a2fe553 fix: add --no-sandbox to antigravity launch command
Electron apps crash in Docker without --no-sandbox and
--disable-dev-shm-usage, same as Chrome. VSCode handles this
internally; Antigravity does not.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:22:19 -05:00
github-actions[bot] 897f1409b5 chore: bump chart version to 0.1.7 [skip ci] 2026-02-20 19:21:23 +00:00
Chris Farhood 32d4fe4944 Merge pull request #20 from cpfarhood/feat/ide-choice
feat: add IDE choice (VSCode, Google Antigravity, SSH)
2026-02-20 14:21:11 -05:00
Chris Farhood e8c263a045 docs: add SSH_AUTHORIZED_KEYS to secret keys reference table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:02:15 -05:00
Chris Farhood 927c9f1051 refactor: make SSH additive boolean, add ide=none
SSH is now a standalone `ssh: true/false` value that starts sshd on
port 22 *in addition to* whatever IDE is running, rather than replacing
it. The `ide` value loses the `ssh` option and gains `none` (keep
container alive with no GUI IDE, useful when ssh: true is the only
access method).

- chart/values.yaml: replace `ide: ssh` with `ssh: false` boolean
- chart/templates/deployment.yaml: expose port 22 when ssh=true,
  port 5800 when ide!=none; probes use HTTP (VNC) or TCP socket (SSH-only)
- chart/templates/service.yaml: include both ports when both enabled
- scripts/cont-init-sshd.sh: check SSH=true instead of IDE=ssh
- scripts/startapp.sh: add ide=none case (sleep infinity), drop ssh case
- chart/Chart.yaml: bump to 0.1.6
- README.md: update IDE choice and SSH access docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 14:01:05 -05:00
Chris Farhood 298a1ce6ec feat: add IDE choice — VSCode (default), Google Antigravity, SSH
- Add `ide` Helm value with options: vscode, antigravity, ssh
- Dockerfile: install Google Antigravity via apt and openssh-server
- scripts/startapp.sh: branch on IDE env var to launch the right app
- scripts/cont-init-sshd.sh: start sshd as root in SSH mode, set up
  authorized_keys from SSH_AUTHORIZED_KEYS env var
- chart/templates/deployment.yaml: pass IDE env var, conditional ports
  and probes (HTTP for VNC modes, TCP socket for SSH mode)
- chart/templates/service.yaml: expose port 5800 (VNC) or 22 (SSH)
- chart/values.yaml: add ide field with documentation
- README.md: document IDE choice, fix stale happyHomeDir references
- chart/Chart.yaml: bump to 0.1.5

Closes #10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:49:31 -05:00
github-actions[bot] f33c7e1ae8 chore: bump chart version to 0.1.4 [skip ci] 2026-02-20 18:22:44 +00:00
Chris Farhood b0d4b98bb4 fix: restore HAPPY_HOME_DIR to PVC to preserve settings across restarts (#19)
Moving it to /workspace (emptyDir) wiped Happy Coder's auth, config,
and state on every pod restart. The daemon also failed to start on boot
because the settings were gone.

Keep HAPPY_HOME_DIR on the home PVC (/home/user/.happy) for persistence.
The stale lock cleanup in init-repo.sh already handles the daemon.state.json.lock
problem that motivated the workspace move.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 13:22:33 -05:00
github-actions[bot] b5820cfc7f chore: bump chart version to 0.1.3 [skip ci] 2026-02-20 16:31:45 +00:00
Chris Farhood bace308394 Merge pull request #18 from cpfarhood/feat/cluster-access-rbac
feat: add clusterAccess option for Kubernetes RBAC
2026-02-20 11:31:35 -05:00
Chris Farhood 9c964e7069 docs: rewrite README with current architecture and clusterAccess docs
- Replace all stale references (claude user, k8s/ dir, StatefulSet,
  old Happy paths) with current state (app user, Helm chart, Deployment)
- Add full Helm chart reference table including new clusterAccess option
  with usage examples for all five access levels
- Document Happy Coder daemon setup and startup flow accurately
- Update troubleshooting commands to match actual resource names
- Add Claude authentication notes (browser OAuth + API key fallback)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 11:26:45 -05:00
Chris Farhood d7210fb4e5 feat: add clusterAccess option for Kubernetes RBAC in Helm chart
Adds a clusterAccess value with five levels:
  none        — no cluster access (default, no RBAC resources created)
  readonlyns  — Role + RoleBinding: get/list/watch in release namespace
  readwritens — Role + RoleBinding: full access in release namespace
  readonly    — ClusterRole + ClusterRoleBinding: get/list/watch cluster-wide
  readwrite   — ClusterRole + ClusterRoleBinding: full access cluster-wide

A ServiceAccount is created for the pod whenever access != none and
referenced in the Deployment's serviceAccountName.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 11:25:29 -05:00
github-actions[bot] 7a96f5156c chore: bump chart version to 0.1.2 [skip ci] 2026-02-20 15:22:03 +00:00
Chris Farhood 8df46d6b6f Merge pull request #17 from cpfarhood/fix/happy-daemon-stale-lock
fix: remove stale Happy daemon lock file on startup
2026-02-20 10:21:53 -05:00
Chris Farhood 5d8b1369c3 fix: move HAPPY_HOME_DIR to /workspace so lock never persists across restarts
The daemon.state.json.lock lives on the home PVC and survives pod
restarts, causing happy daemon start to fail on every reboot. Moving
HAPPY_HOME_DIR to /workspace (emptyDir) means the entire happy state
directory is ephemeral and always clean on startup.

The rm -f in init-repo.sh is kept as a safety net for the within-run
case but is now a no-op on fresh starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:20:49 -05:00
Chris Farhood 751402be44 fix: remove stale happy daemon lock file before starting
daemon.state.json.lock is left behind when the daemon crashes or is
killed (e.g. pod restart). On next startup happy daemon start sees the
lock and exits with "Failed to start daemon" without further detail.

Remove the lock file unconditionally at startup — if no daemon is
running, the lock is stale by definition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:18:12 -05:00
Chris Farhood 66e0d1f406 Merge pull request #16 from cpfarhood/fix/happy-daemon-no-sudo
fix: remove sudo from happy daemon start
2026-02-20 10:07:24 -05:00
Chris Farhood e89c3040b7 fix: remove sudo from happy daemon start — startapp.sh already runs as app user
sudo -u failed because the app user is not in sudoers. It was unnecessary
anyway: startapp.sh (and init-repo.sh) run as the app user (UID 1000) so
happy daemon start already executes as the correct user with the right HOME.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:05:41 -05:00
Chris Farhood 8d7b39f1b5 Merge pull request #15 from cpfarhood/fix/happy-daemon-user
fix: run Happy daemon as app user so it can find credentials
2026-02-20 10:01:06 -05:00
Chris Farhood 32e87254d2 fix: run Happy daemon as app user, not root
When init-repo.sh starts the happy daemon as root, HOME=/root so
Happy and Claude Code can't find credentials stored in /home/user.
The mobile app works when the user manually runs happy from a VSCode
terminal (as the user user) because the right home dir is in scope.

Use sudo -u with -E (preserve environment) so the daemon runs as the
correct user while still inheriting the pod's env vars (HAPPY_SERVER_URL,
HAPPY_WEBAPP_URL, ANTHROPIC_API_KEY, etc.).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 09:59:59 -05:00
Chris Farhood 66ccee1202 Merge pull request #14 from cpfarhood/fix/chrome-in-docker
fix: make Chrome work inside Docker for Claude Max OAuth login
2026-02-20 09:31:59 -05:00
Chris Farhood 1909c2a3aa fix: make Chrome work inside Docker for Claude OAuth browser login
Chrome requires --no-sandbox and --disable-dev-shm-usage when running
inside a Docker container, otherwise it crashes silently and the OAuth
popup never completes.

- Add a /usr/local/bin/google-chrome wrapper that injects these flags
- Install xdg-utils so xdg-open can resolve browser handlers in VNC
- Set BROWSER env var to the wrapper so Claude Code and xdg-open both
  use it when opening the Claude Max login URL

The OAuth callback (to localhost) works fine inside VNC because both
the browser and the Claude Code local auth server share the same
container network namespace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 09:30:36 -05:00
Chris Farhood d078bb1c44 Merge pull request #12 from cpfarhood/fix/install-claude-code
fix: install Claude Code CLI so Happy Coder can find it
2026-02-20 09:15:36 -05:00
Chris Farhood 56c648187a fix: install Claude Code CLI so Happy Coder can find it
Happy Coder requires the `claude` CLI to be present but it was never
installed in the image. Add @anthropic-ai/claude-code to the npm
global install step alongside happy-coder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 09:13:39 -05:00
Chris Farhood 8870d60ccc Merge pull request #9 from cpfarhood/fix/app-user-shell
fix: set app user shell to /bin/bash so VSCode terminals work
2026-02-20 07:51:35 -05:00
Chris Farhood d54515244c fix: set app user shell to /bin/bash so VSCode terminals work
baseimage-gui creates the app user (UID 1000) at runtime with
shell=/sbin/nologin and home=/dev/null. VSCode tries to spawn the
user's login shell for terminals, which fails with exit code 1.

Adds a cont-init script that runs as root after baseimage-gui's
adduser step and corrects both the shell and home directory.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 07:50:25 -05:00
Chris Farhood 2918cfde25 Merge pull request #7 from cpfarhood/fix/happy-daemon-nonblocking
fix: don't abort startup if happy daemon fails to start
2026-02-20 07:37:10 -05:00
Chris Farhood dd77cf6a48 fix: don't abort startup if happy daemon fails to start
happy daemon start failing should not crash-loop the container.
VSCode should still open regardless.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 07:36:21 -05:00
github-actions[bot] 961a0985b6 chore: bump chart version to 0.1.1 [skip ci] 2026-02-20 12:29:12 +00:00
Chris Farhood d3f5e9f185 Merge pull request #6 from cpfarhood/chore/rename-chart-cpfarhood
chore: rename Helm chart from antigravity to cpfarhood
2026-02-20 07:29:01 -05:00
Chris Farhood 9aab08b8e4 Merge pull request #2 from cpfarhood/dependabot/github_actions/docker/build-push-action-6
build(deps): Bump docker/build-push-action from 5 to 6
2026-02-20 07:28:37 -05:00
Chris Farhood 727487053d Merge pull request #1 from cpfarhood/dependabot/github_actions/actions/checkout-6
build(deps): Bump actions/checkout from 4 to 6
2026-02-20 07:28:17 -05:00
Chris Farhood 47a275d667 chore: auto-bump chart patch version on every merge to main
Workflow now increments the patch version in Chart.yaml, commits it
back with [skip ci], then packages and pushes to GHCR so Flux always
picks up a new version on chart changes.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 07:27:04 -05:00
Chris Farhood 7788352995 chore: rename Helm chart from antigravity to devcontainer
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 07:25:07 -05:00
Chris Farhood 81a7098c21 chore: rename Helm chart from antigravity to cpfarhood
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 07:24:25 -05:00
Chris Farhood 3832fd922b Merge pull request #5 from cpfarhood/chore/renovate
chore(deps): replace Dependabot with Renovate
2026-02-20 07:18:10 -05:00
Chris Farhood 4984e5200c chore(deps): replace Dependabot with Renovate
Adds renovate.json modelled after the kubernetes repo config.
Removes .github/dependabot.yml.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 07:17:18 -05:00
Chris Farhood c8f51beac6 Merge pull request #4 from cpfarhood/feat/helm-oci-publish
Publish Helm chart to GHCR OCI registry
2026-02-20 07:16:35 -05:00
Chris Farhood ee7a4a0be8 Merge pull request #3 from cpfarhood/fix/happy-daemon-start
Fix Happy Coder startup
2026-02-20 07:16:22 -05:00
Chris Farhood 2472dc5b3f Add GitHub Actions workflow to publish Helm chart to GHCR OCI
Packages chart/ and pushes to oci://ghcr.io/cpfarhood/charts on every
push to main that touches chart/** files.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 06:51:40 -05:00
Chris Farhood 23ba5c2e35 Fix Happy Coder startup: use happy daemon start, create home dir
The npm package installs a binary named 'happy', not 'happy-coder'.
Use 'happy daemon start' to run it as a background service.

Also create $HOME on the PVC if it doesn't exist yet, which was
causing git config failures on fresh volumes.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 06:33:40 -05:00
Chris Farhood 256a6871e8 Add Helm chart, replace flux/ variable-substitution templates
Introduces chart/ with deployment, service, and PVC templates driven
by Helm values instead of Kustomize variable substitution. Removes
the flux/ directory which is superseded by the chart.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-20 06:14:40 -05:00
Chris Farhood 720a2a982f try change to deployment 2026-02-19 20:49:58 -05:00
Chris Farhood fdfccb17bd catch up 2026-02-19 20:00:37 -05:00
Chris Farhood dfc0a3c1e3 catchup 2026-02-19 19:55:05 -05:00
Chris Farhood 7003d860b7 Move deployment to cpfarhood namespace
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-19 18:20:48 -05:00
Chris Farhood 8eab159e6f Rename user from claude to user, remove HTTPRoute
- Rename container user from 'claude' to 'user' throughout Dockerfile
- Remove httproute.yaml from kustomization (Authentik outpost will manage routing)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-19 18:08:39 -05:00
Chris Farhood 13f1878ce5 Fix hostname and TLS config for gateway
- Change hostname from antigravity.dev.farh.net to antigravity.farh.net
  to match the gateway's *.farh.net listener
- Set SECURE_CONNECTION=0 since TLS is terminated at the gateway,
  avoiding the redirect loop caused by the app doing its own HTTP->HTTPS redirect

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-19 18:01:52 -05:00
Chris Farhood c01f85ef24 Remove sudo from startup scripts
baseimage-gui already runs startapp.sh as the app user (UID 1000),
so sudo -u is unnecessary and fails since 'app' is not in sudoers.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-19 17:54:35 -05:00
Chris Farhood 3db5961b1c Fix chown errors in startup script
- Remove chown on /home mount root (PVC root not owned by container user)
- Change imagePullPolicy to Always to pick up new images on pod restart

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-19 17:51:56 -05:00
Chris Farhood 3794ec60d7 Fix startup scripts and k8s config for initial deployment
- Fix chown/sudo to use numeric UID/GID instead of hardcoded 'claude'
  username (baseimage-gui creates users dynamically, name not available
  at script runtime)
- Fix image name: ghcr.io/cpfarhood/devcontainer (matches github.repository)
- Fix ConfigMap name: antigravity-config -> antigravity (matches statefulset refs)
- Set github-repo in ConfigMap to headlamp-polaris-plugin
- Set HTTPRoute to external gateway at antigravity.dev.farh.net
- Add CLAUDE.md for Claude Code guidance

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-19 17:39:59 -05:00
dependabot[bot] 17c2d04d70 build(deps): Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 13:34:38 +00:00
dependabot[bot] 636b68d263 build(deps): Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 13:34:35 +00:00
31 changed files with 2259 additions and 1280 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"enabledMcpjsonServers": [
"kubernetes",
"flux",
"playwright"
]
}
-19
View File
@@ -1,19 +0,0 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "github-actions"
# Maintain dependencies for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "docker"
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -53,7 +53,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+57
View File
@@ -0,0 +1,57 @@
name: Publish Helm Chart
on:
push:
branches:
- main
paths:
- 'chart/**'
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Bump patch version
id: bump
run: |
CURRENT=$(grep '^version:' chart/Chart.yaml | awk '{print $2}')
MAJOR=$(echo $CURRENT | cut -d. -f1)
MINOR=$(echo $CURRENT | cut -d. -f2)
PATCH=$(echo $CURRENT | cut -d. -f3)
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
sed -i "s/^version: .*/version: ${NEW_VERSION}/" chart/Chart.yaml
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
- name: Commit version bump
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add chart/Chart.yaml
git commit -m "chore: bump chart version to ${{ steps.bump.outputs.version }} [skip ci]"
git push
- name: Log in to GHCR
run: |
helm registry login ghcr.io \
--username ${{ github.actor }} \
--password ${{ secrets.GITHUB_TOKEN }}
- name: Package chart
run: helm package chart/
- name: Push chart to GHCR
run: |
helm push devcontainer-${{ steps.bump.outputs.version }}.tgz oci://ghcr.io/cpfarhood/charts
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
+21
View File
@@ -0,0 +1,21 @@
{
"mcpServers": {
"kubernetes": {
"type": "sse",
"url": "http://localhost:8080/sse"
},
"flux": {
"type": "sse",
"url": "http://localhost:8081/sse"
},
"homeassistant": {
"type": "sse",
"url": "http://localhost:8087/sse"
},
"playwright": {
"type": "sse",
"url": "http://playwright-mcp.playwright.svc.cluster.local:3000/sse"
}
}
}
+168
View File
@@ -0,0 +1,168 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Antigravity is a Docker-based cloud development environment that provides:
- Web-based GUI IDE (VSCode/Antigravity) via VNC on port 5800
- Happy Coder AI assistant integration
- Automatic GitHub repository cloning on startup
- Kubernetes-native deployment with persistent home storage
The stack is primarily **Bash scripts + YAML** — there is no Node.js package, compiled language, or test framework.
## Common Commands
### Building
```bash
make build # Build Docker image
make build REGISTRY=ghcr.io/myuser IMAGE_TAG=v1.0 # Custom registry/tag
docker build -t ghcr.io/cpfarhood/antigravity:latest . # Direct build
```
### Running Locally
```bash
GITHUB_REPO="https://github.com/user/repo" make run # Run with Docker
make stop # Stop container
make clean # Remove volumes
```
### Kubernetes Deployment
```bash
GITHUB_REPO="https://github.com/user/repo" make helm-deploy # Deploy with Helm
make helm-delete # Tear down Helm release
make helm-port-forward # Forward port 5800 to localhost
make helm-logs # Stream container logs
make helm-shell # Open interactive shell in pod
# Or use Helm directly
helm install mydev ./chart --set name=mydev --set githubRepo=https://github.com/user/repo
```
### Other Useful Targets
```bash
make help # List all Makefile targets with descriptions
make push # Push image to registry (build first)
```
## Architecture
### Startup Flow
```
Container start
→ scripts/startapp.sh
→ scripts/init-repo.sh
→ Configure git user & credentials
→ Clone GITHUB_REPO (if set)
→ Start Happy Coder
→ Launch VSCode as user `user` in /workspace
```
### Key Files
| File | Purpose |
|------|---------|
| `Dockerfile` | Image definition — installs Chrome, Node.js, VSCode, Happy Coder; creates non-root user (UID 1000) |
| `scripts/init-repo.sh` | Configures git credentials, clones GitHub repo, starts Happy Coder background service |
| `scripts/startapp.sh` | Calls init-repo.sh then opens VSCode in the workspace |
| `chart/` | Helm chart for Kubernetes deployment |
| `chart/templates/deployment.yaml` | Deployment spec — main container + MCP sidecar containers |
| `chart/templates/rbac.yaml` | ServiceAccount, Role/ClusterRole based on `clusterAccess` value |
| `chart/templates/pvc.yaml` | PersistentVolumeClaim for user home |
| `chart/templates/service.yaml` | ClusterIP Service (VNC + optional SSH) |
| `chart/values.yaml` | Default Helm values |
| `.mcp.json` | MCP server connection config (Kubernetes, Flux, Playwright) |
| `Makefile` | Build/deploy automation |
### MCP Sidecars
MCP (Model Context Protocol) servers run as sidecar containers in the pod, enabling AI assistants to interact with various services:
| Sidecar | Image | Version | Port | Endpoint | Default |
|---------|-------|---------|------|----------|---------|
| `kubernetes-mcp` | `quay.io/containers/kubernetes_mcp_server` | v0.0.57 | 8080 | `http://localhost:8080/sse` | Enabled |
| `flux-mcp` | `ghcr.io/controlplaneio-fluxcd/flux-operator-mcp` | v0.41.1 | 8081 | `http://localhost:8081/sse` | Enabled |
| `homeassistant-mcp` | `ghcr.io/homeassistant-ai/ha-mcp` | v6.7.1 | 8087 | `http://localhost:8087/sse` | Disabled |
**Note:**
- Kubernetes and Flux sidecars require `clusterAccess` != `none` to be deployed (they need RBAC permissions)
- Kubernetes and Flux sidecars inherit the pod's ServiceAccount RBAC permissions
- Home Assistant sidecar requires `HOMEASSISTANT_URL` and `HOMEASSISTANT_TOKEN` in the env secret
- Playwright MCP remains an external service
#### Enabling/Disabling MCP Servers
To control MCP sidecars, set the `enabled` flag in your values override:
```yaml
# Disable all MCP sidecars
mcpSidecars:
kubernetes:
enabled: false
flux:
enabled: false
homeassistant:
enabled: false
# Or selectively enable/disable
mcpSidecars:
kubernetes:
enabled: true # Keep Kubernetes MCP enabled
flux:
enabled: false # Disable Flux MCP
homeassistant:
enabled: true # Enable Home Assistant MCP (requires secrets)
```
When deploying via Helm:
```bash
# Using --set flag
helm install my-devcontainer ./chart --set mcpSidecars.kubernetes.enabled=false --set mcpSidecars.flux.enabled=false
# Or with a values file
helm install my-devcontainer ./chart -f custom-values.yaml
```
### Storage Model
- `/config` — ReadWriteMany PVC (persists across pod restarts, holds user config/dotfiles)
- `/workspace` — emptyDir by default (ephemeral; can be changed to PVC)
### Environment Variables
**Required:**
- `GITHUB_REPO` — URL of repository to clone into `/workspace`
**Optional:**
- `GITHUB_TOKEN` — PAT for private repo access (automatically configures git credentials)
- `GIT_USER_NAME` — Git user name for commits (default: "DevContainer User")
- `GIT_USER_EMAIL` — Git user email for commits (default: "devcontainer@example.com")
- `GITLAB_HOST` — GitLab hostname if using GitLab with same token
- `VNC_PASSWORD` — VNC web interface password
- `DISPLAY_WIDTH` / `DISPLAY_HEIGHT` — VNC resolution
- `USER_ID` / `GROUP_ID` — Override UID/GID (default 1000)
- `HAPPY_SERVER_URL` / `HAPPY_WEBAPP_URL` — Custom Happy Coder endpoints
- `HAPPY_HOME_DIR` / `HAPPY_EXPERIMENTAL`
### CI/CD
- **`build-and-push.yaml`** — Builds and pushes to GHCR on every push to `main`, version tags (`v*`), and PRs. Tags: `latest` (main), semver, branch name, commit SHA.
- **`release.yaml`** — Creates a GitHub Release with docker pull instructions when a version tag is pushed.
- **`dependabot.yml`** — Weekly updates for GitHub Actions and Docker base image.
Image registry: `ghcr.io/cpfarhood/devcontainer`
## Kubernetes Notes
- Deployed via Helm chart (`chart/`), published as OCI artifact to GHCR, reconciled by Flux
- Storage class is `ceph-filesystem` by default — change via `storage.className` in values
- Resource limits: 14 CPU, 28Gi memory
- Health checks (liveness/readiness probes) on port 5800
- Secrets: optional env Secret (`devcontainer-{name}-secrets-env`) for `GITHUB_TOKEN`, `VNC_PASSWORD`, etc.
- RBAC: controlled by `clusterAccess` value (`none`, `readonlyns`, `readwritens`, `readonly`, `readwrite`)
+377 -364
View File
@@ -1,436 +1,449 @@
# Deployment Guide
This guide provides step-by-step instructions for deploying the Antigravity Dev Container to Kubernetes.
This guide provides step-by-step instructions for deploying the Antigravity Dev Container using Helm.
## Prerequisites
- Kubernetes cluster with Gateway API support
- Kubernetes cluster (1.19+)
- `kubectl` configured to access your cluster
- `helm` CLI installed (3.0+)
- ReadWriteMany storage class available (e.g., `ceph-filesystem`, `nfs-client`, `efs-sc`)
- Sealed Secrets controller installed (for secret encryption)
- GitHub Container Registry access (images are public)
## Required Configuration Variables
## Quick Start
Before deploying, you need to provide the following configuration:
### 1. Storage Configuration
**Variable:** `storageClassName`
**Location:** `k8s/statefulset.yaml` (line ~117)
**Description:** The ReadWriteMany storage class name in your cluster
**Example values:**
- `ceph-filesystem` (Rook-Ceph)
- `nfs-client` (NFS)
- `efs-sc` (AWS EFS)
- `azurefile` (Azure Files)
- `filestore` (GCP Filestore)
**How to find your storage class:**
```bash
kubectl get storageclass
```
Look for a storage class that supports `ReadWriteMany` access mode.
### 2. GitHub Repository (Required)
**Variable:** `github-repo`
**Location:** `k8s/configmap.yaml` (line ~9)
**Description:** The GitHub repository URL to clone on container startup
**Format:** `https://github.com/username/repository`
**Example:** `https://github.com/cpfarhood/my-project`
### 3. GitHub Token (Optional, for private repos)
**Variable:** `github-token`
**Location:** `k8s/secrets-example.yaml` (sealed secret)
**Description:** GitHub Personal Access Token for cloning private repositories
**Format:** `ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
**Required:** Only if cloning a private repository
**How to create a GitHub token:**
1. Go to https://github.com/settings/tokens
2. Click "Generate new token (classic)"
3. Select scopes: `repo` (for private repos)
4. Generate and copy the token
### 4. VNC Password (Optional)
**Variable:** `vnc-password`
**Location:** `k8s/secrets-example.yaml` (sealed secret)
**Description:** Password for accessing the VNC web interface
**Format:** Any string (recommend 12+ characters)
**Required:** Optional, but recommended for security
### 5. Gateway Configuration (Required for external access)
**Variables:**
- `parentRefs.name` - Your Gateway resource name
- `parentRefs.namespace` - Namespace where Gateway is deployed
- `hostnames` - Domain name for accessing the container
**Location:** `k8s/httproute.yaml`
**Example:**
```yaml
parentRefs:
- name: cilium-gateway # Your Gateway name
namespace: kube-system # Your Gateway namespace
hostnames:
- "devcontainer.example.com" # Your domain
```
### 6. Namespace (Optional)
**Variable:** `namespace`
**Location:** `k8s/kustomization.yaml` (line ~5)
**Description:** Kubernetes namespace to deploy into
**Default:** `default`
**Example:** `devcontainer`, `development`, `team-workspaces`
### 7. Container Image (Optional)
**Variable:** `image`
**Location:** `k8s/statefulset.yaml` (line ~32)
**Description:** Docker image to use
**Default:** `ghcr.io/cpfarhood/devcontainer:latest`
**Format:** `registry/repository:tag`
### 8. Resource Limits (Optional)
**Variables:**
- `resources.requests.memory` (default: `2Gi`)
- `resources.requests.cpu` (default: `1000m`)
- `resources.limits.memory` (default: `8Gi`)
- `resources.limits.cpu` (default: `4000m`)
**Location:** `k8s/statefulset.yaml` (lines ~98-103)
### 9. Happy Coder Configuration (Optional)
**Variables:**
- `happy-server-url` - Custom Happy server URL
- `happy-webapp-url` - Custom Happy webapp URL
**Location:** `k8s/configmap.yaml` (lines ~12-13, commented out)
**Default:** Uses Happy's default servers
**When to set:** Only if using a self-hosted Happy instance
## Deployment Steps
### Step 1: Clone the Repository
### 1. Clone the Repository
```bash
git clone https://github.com/cpfarhood/devcontainer.git
cd devcontainer
```
### Step 2: Configure Storage Class
### 2. Create Secret (Optional)
Edit `k8s/statefulset.yaml` and find the `volumeClaimTemplates` section (around line 117):
For private repos or VNC password:
```bash
# Find your storage class
kubectl get storageclass
# Edit the file
vi k8s/statefulset.yaml
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=VNC_PASSWORD='changeme' \
--from-literal=ANTHROPIC_API_KEY='sk-ant-...'
```
Change `storageClassName` to match your cluster:
```yaml
volumeClaimTemplates:
- metadata:
name: userhome
spec:
accessModes: [ "ReadWriteMany" ]
storageClassName: "ceph-filesystem" # ← Change this
resources:
requests:
storage: 10Gi
```
### Step 3: Configure GitHub Repository
Edit `k8s/configmap.yaml`:
### 3. Deploy with Helm
```bash
vi k8s/configmap.yaml
# Basic deployment
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo
# With custom storage class
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set storage.className=nfs-client
# With cluster access for kubectl
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set clusterAccess=readwritens
```
Set your repository URL:
```yaml
data:
github-repo: "https://github.com/yourusername/yourrepo"
```
### Step 4: Configure Gateway (HTTPRoute)
Edit `k8s/httproute.yaml`:
### 4. Access the Container
```bash
# Find your Gateway
kubectl get gateway -A
# Edit the file
vi k8s/httproute.yaml
```
Update with your Gateway details:
```yaml
spec:
parentRefs:
- name: your-gateway-name # ← Change this
namespace: your-gateway-namespace # ← Change this
hostnames:
- "devcontainer.yourdomain.com" # ← Change this
```
### Step 5: Create Secrets
Create the secrets for GitHub token and VNC password:
```bash
# Create the secret
kubectl create secret generic antigravity-secrets \
--from-literal=github-token='ghp_your_token_here' \
--from-literal=vnc-password='your_vnc_password' \
--dry-run=client -o yaml | \
kubeseal --format=yaml > k8s/sealedsecrets.yaml
# Verify the sealed secret was created
cat k8s/sealedsecrets.yaml
```
**If you don't have Sealed Secrets controller:**
Option 1: Install Sealed Secrets
```bash
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
```
Option 2: Use plain secrets (not recommended for production)
```bash
kubectl create secret generic antigravity-secrets \
--from-literal=github-token='ghp_your_token_here' \
--from-literal=vnc-password='your_vnc_password'
```
### Step 6: Review Configuration (Optional)
Review and adjust optional settings:
**Namespace:**
```bash
vi k8s/kustomization.yaml
# Change line 5: namespace: default
```
**Resource limits:**
```bash
vi k8s/statefulset.yaml
# Adjust lines 98-103 for your needs
```
### Step 7: Deploy to Kubernetes
```bash
# Deploy everything
kubectl apply -k k8s/
# Or if you changed the namespace
kubectl apply -k k8s/ -n your-namespace
```
### Step 8: Verify Deployment
```bash
# Check StatefulSet
kubectl get statefulset antigravity
# Check Pod
kubectl get pods -l app=antigravity
# Check PVC
kubectl get pvc -l app=antigravity
# Check HTTPRoute
kubectl get httproute antigravity
# View logs
kubectl logs antigravity-0
```
### Step 9: Access the Container
**Option A: Via HTTPRoute (external access)**
```bash
# Open in browser
open https://devcontainer.yourdomain.com
```
**Option B: Via Port Forward (local access)**
```bash
# Port forward to localhost
kubectl port-forward statefulset/antigravity 5800:5800
# Open in browser
# Port forward
kubectl port-forward deployment/devcontainer-mydev 5800:5800
open http://localhost:5800
```
## Configuration Summary
## Deployment Options
Here's a quick checklist of all variables you need to set:
### Using Values File
### Required Variables
Create a custom `values.yaml`:
| Variable | File | Line | Example Value |
|----------|------|------|---------------|
| `storageClassName` | `k8s/statefulset.yaml` | ~117 | `ceph-filesystem` |
| `github-repo` | `k8s/configmap.yaml` | ~9 | `https://github.com/user/repo` |
| `parentRefs.name` | `k8s/httproute.yaml` | ~8 | `cilium-gateway` |
| `parentRefs.namespace` | `k8s/httproute.yaml` | ~9 | `kube-system` |
| `hostnames` | `k8s/httproute.yaml` | ~10 | `devcontainer.example.com` |
```yaml
name: mydev
githubRepo: https://github.com/youruser/yourrepo
ide: vscode
ssh: false
### Optional Variables
# Storage
storage:
size: 32Gi
className: ceph-filesystem
| Variable | File | Line | Default | When to Change |
|----------|------|------|---------|----------------|
| `namespace` | `k8s/kustomization.yaml` | ~5 | `default` | If deploying to different namespace |
| `github-token` | Sealed secret | N/A | None | For private repos |
| `vnc-password` | Sealed secret | N/A | None | For VNC security |
| `image` | `k8s/statefulset.yaml` | ~32 | `ghcr.io/cpfarhood/devcontainer:latest` | For specific version or custom build |
| `resources.*` | `k8s/statefulset.yaml` | ~98-103 | 2Gi/8Gi RAM, 1/4 CPU | Based on workload needs |
| `happy-server-url` | `k8s/configmap.yaml` | ~12 | Default Happy server | For self-hosted Happy |
| `happy-webapp-url` | `k8s/configmap.yaml` | ~13 | Default Happy webapp | For self-hosted Happy |
# Resources
resources:
requests:
memory: "4Gi"
cpu: "2000m"
limits:
memory: "16Gi"
cpu: "8000m"
# Kubernetes access
clusterAccess: readwritens
# MCP sidecars
mcpSidecars:
kubernetes:
enabled: true
flux:
enabled: false
```
Deploy:
```bash
helm install mydev ./chart -f values.yaml
```
### SSH Access Setup
Enable SSH and add your public key:
```bash
# Create secret with SSH key
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=SSH_AUTHORIZED_KEYS='ssh-ed25519 AAAA...'
# Deploy with SSH enabled
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set ssh=true
# Connect via SSH
kubectl port-forward deployment/devcontainer-mydev 2222:22
ssh -p 2222 user@localhost
```
### MCP Sidecar Configuration
Control MCP servers for AI-assisted operations.
**Important:** Kubernetes and Flux MCP sidecars are only deployed when:
1. They are enabled in values (`mcpSidecars.<name>.enabled: true`)
2. AND `clusterAccess` is not `none` (they need RBAC permissions to function)
```bash
# Disable all MCP sidecars
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.kubernetes.enabled=false \
--set mcpSidecars.flux.enabled=false \
--set mcpSidecars.homeassistant.enabled=false
# Enable only Kubernetes MCP
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.kubernetes.enabled=true \
--set mcpSidecars.flux.enabled=false
# Enable Home Assistant MCP (requires credentials)
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=homeassistant-url='http://homeassistant.local:8123' \
--from-literal=homeassistant-token='your_long_lived_token'
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.homeassistant.enabled=true
```
### Cluster Access Levels
Configure Kubernetes RBAC permissions:
| Value | Scope | Permissions | Use Case |
|-------|-------|-------------|----------|
| `none` | No access | None | Default, isolated development |
| `readonlyns` | Namespace | Read-only | View resources in namespace |
| `readwritens` | Namespace | Full access | Deploy apps in namespace |
| `readonly` | Cluster-wide | Read-only | View all cluster resources |
| `readwrite` | Cluster-wide | Full access | Cluster administration |
```bash
# Example: Full access within namespace
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set clusterAccess=readwritens
```
## Ingress Configuration
### Using Gateway API HTTPRoute
Create an HTTPRoute for external access:
```yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: devcontainer-mydev
spec:
parentRefs:
- name: your-gateway
namespace: your-gateway-namespace
hostnames:
- devcontainer.example.com
rules:
- backendRefs:
- name: devcontainer-mydev
port: 5800
```
### Using Traditional Ingress
Create an Ingress resource:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: devcontainer-mydev
spec:
rules:
- host: devcontainer.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: devcontainer-mydev
port:
number: 5800
```
## Advanced Configurations
### Custom Happy Coder Endpoints
For self-hosted Happy instances:
```bash
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set happyServerUrl=https://your-happy-server.com \
--set happyWebappUrl=https://your-happy-webapp.com
```
### Custom Display Resolution
```bash
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set display.width=2560 \
--set display.height=1440
```
### Different IDE Options
```bash
# Use Google Antigravity
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set ide=antigravity
# SSH-only mode (no GUI)
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set ide=none \
--set ssh=true
```
## Helm Operations
### List Deployments
```bash
helm list
```
### Upgrade Deployment
```bash
# Change values
helm upgrade mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/newrepo
# Upgrade with new chart version
git pull
helm upgrade mydev ./chart
```
### Uninstall
```bash
helm uninstall mydev
# Note: PVC persists by default
kubectl delete pvc userhome-mydev
```
### Rollback
```bash
# View history
helm history mydev
# Rollback to previous version
helm rollback mydev
# Rollback to specific revision
helm rollback mydev 3
```
## Troubleshooting
### Pod not starting
### Pod Not Starting
**Check events:**
```bash
kubectl describe pod antigravity-0
# Check pod status
kubectl get pods -l app.kubernetes.io/instance=mydev
# Describe pod for events
kubectl describe pod -l app.kubernetes.io/instance=mydev
# Check logs
kubectl logs deployment/devcontainer-mydev
```
**Common issues:**
- Storage class doesn't support ReadWriteMany
- PVC not binding (check storage class exists)
- Image pull errors (check image name)
### Repository Not Cloning
### Repository not cloning
**Check logs:**
```bash
kubectl logs antigravity-0 | grep -A 10 "Repository Initialization"
# Check init logs
kubectl logs deployment/devcontainer-mydev | grep "Repository Initialization"
# Verify secret exists
kubectl get secret devcontainer-mydev-secrets-env
# Check environment
kubectl exec deployment/devcontainer-mydev -- env | grep GITHUB
```
**Common issues:**
- Invalid GitHub URL
- Private repo without token
- Token doesn't have correct permissions
### VNC Not Accessible
### HTTPRoute not working
**Check HTTPRoute:**
```bash
kubectl describe httproute antigravity
# Check service
kubectl get svc devcontainer-mydev
kubectl describe svc devcontainer-mydev
# Test with port-forward
kubectl port-forward deployment/devcontainer-mydev 5800:5800
```
**Common issues:**
- Gateway name/namespace incorrect
- Domain not pointing to Gateway
- TLS certificate not issued
### MCP Sidecar Issues
### VNC not accessible
**Check service:**
```bash
kubectl get svc antigravity
kubectl describe svc antigravity
# Check all containers
kubectl get pod -l app.kubernetes.io/instance=mydev -o jsonpath='{.items[0].spec.containers[*].name}'
# Check MCP container logs
kubectl logs deployment/devcontainer-mydev -c kubernetes-mcp
kubectl logs deployment/devcontainer-mydev -c flux-mcp
kubectl logs deployment/devcontainer-mydev -c homeassistant-mcp
# Verify RBAC permissions (for Kubernetes/Flux MCP)
kubectl auth can-i --list --as system:serviceaccount:default:devcontainer-mydev
# Check Home Assistant MCP credentials
kubectl get secret devcontainer-mydev-secrets-env -o jsonpath='{.data.homeassistant-url}' | base64 -d
# Verify the URL is accessible from the pod
kubectl exec deployment/devcontainer-mydev -- curl -s http://homeassistant.local:8123/api/
```
**Port forward test:**
### Storage Issues
```bash
kubectl port-forward antigravity-0 5800:5800
# Try accessing http://localhost:5800
# Check PVC
kubectl get pvc userhome-mydev
kubectl describe pvc userhome-mydev
# Check available storage classes
kubectl get storageclass
# Verify ReadWriteMany support
kubectl get storageclass <class-name> -o yaml | grep -i accessmodes
```
## Quick Deploy Example
## Best Practices
Complete deployment with all values filled in:
### Production Deployment
1. **Use specific image tags** instead of `latest`:
```bash
helm install mydev ./chart --set image.tag=v1.0.0
```
2. **Set resource limits** appropriately:
```yaml
resources:
requests:
memory: "4Gi"
cpu: "2000m"
limits:
memory: "8Gi"
cpu: "4000m"
```
3. **Enable VNC password**:
```bash
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=VNC_PASSWORD='strong-password-here'
```
4. **Use dedicated namespace**:
```bash
kubectl create namespace dev-environments
helm install mydev ./chart -n dev-environments
```
5. **Configure appropriate cluster access**:
- Use `readonlyns` or `readwritens` for namespace-scoped work
- Avoid `readwrite` cluster-wide access unless necessary
### Multi-User Deployment
For teams, create separate deployments per user:
```bash
# 1. Set your values
STORAGE_CLASS="ceph-filesystem"
GITHUB_REPO="https://github.com/myuser/myproject"
GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VNC_PASSWORD="my-secure-password-123"
GATEWAY_NAME="cilium-gateway"
GATEWAY_NAMESPACE="kube-system"
DOMAIN="devcontainer.example.com"
# User 1
helm install alice-dev ./chart \
--set name=alice-dev \
--set githubRepo=https://github.com/alice/project
# 2. Update storage class
sed -i "s/storageClassName: .*/storageClassName: \"$STORAGE_CLASS\"/" k8s/statefulset.yaml
# 3. Update GitHub repo
sed -i "s|github-repo: .*|github-repo: \"$GITHUB_REPO\"|" k8s/configmap.yaml
# 4. Update Gateway
sed -i "s/- name: gateway/- name: $GATEWAY_NAME/" k8s/httproute.yaml
sed -i "s/namespace: gateway-system/namespace: $GATEWAY_NAMESPACE/" k8s/httproute.yaml
sed -i "s/antigravity.example.com/$DOMAIN/" k8s/httproute.yaml
# 5. Create sealed secret
kubectl create secret generic antigravity-secrets \
--from-literal=github-token="$GITHUB_TOKEN" \
--from-literal=vnc-password="$VNC_PASSWORD" \
--dry-run=client -o yaml | \
kubeseal --format=yaml > k8s/sealedsecrets.yaml
# 6. Deploy
kubectl apply -k k8s/
# 7. Watch deployment
kubectl get pods -l app=antigravity -w
# User 2
helm install bob-dev ./chart \
--set name=bob-dev \
--set githubRepo=https://github.com/bob/project
```
## Updates and Maintenance
### Backup and Recovery
### Updating the Image
The image is automatically built and pushed to ghcr.io on every commit to main.
**To use latest:**
```bash
kubectl set image statefulset/antigravity \
antigravity=ghcr.io/cpfarhood/devcontainer:latest
```
**To use specific version:**
```bash
kubectl set image statefulset/antigravity \
antigravity=ghcr.io/cpfarhood/devcontainer:v1.0.0
```
### Changing Repository
Edit the ConfigMap and restart:
```bash
kubectl edit configmap antigravity-config
# Change github-repo value
kubectl rollout restart statefulset/antigravity
```
### Scaling
The home directory persists on PVC. To backup:
```bash
# Scale to multiple instances (each gets own home PVC)
kubectl scale statefulset antigravity --replicas=3
# Create backup pod
kubectl run backup --image=busybox --restart=Never --rm -i --tty \
-- tar czf - -C /home . | gzip > home-backup.tar.gz
```
## Support
For issues or questions:
- GitHub Issues: https://github.com/cpfarhood/devcontainer/issues
- Documentation: https://github.com/cpfarhood/devcontainer
- Documentation: https://github.com/cpfarhood/devcontainer
+63 -17
View File
@@ -8,7 +8,7 @@ ENV APP_NAME="Antigravity Dev Container" \
SECURE_CONNECTION=1 \
USER_ID=1000 \
GROUP_ID=1000 \
CLAUDE_USER=claude
CLAUDE_USER=user
# Install system dependencies
RUN apt-get update && apt-get install -y \
@@ -25,49 +25,95 @@ RUN apt-get update && apt-get install -y \
sudo \
&& rm -rf /var/lib/apt/lists/*
# Install Chrome
# Install Chrome and xdg-utils (needed for xdg-open to work in VNC)
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
apt-get update && \
apt-get install -y google-chrome-stable && \
apt-get install -y google-chrome-stable xdg-utils && \
rm -rf /var/lib/apt/lists/*
# 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.
# 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)
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*
# Install Happy Coder globally
RUN npm install -g happy-coder
# Install Happy Coder and Claude Code globally
RUN npm install -g happy-coder @anthropic-ai/claude-code
# Install Antigravity (Google's Project IDX / Cloud Code alternative)
# Note: Antigravity might be packaged differently - adjust as needed
# For now, we'll use VSCode with Project IDX extensions as a placeholder
# Install VSCode
RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/packages.microsoft.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list && \
apt-get update && \
apt-get install -y code && \
rm -rf /var/lib/apt/lists/*
# Create claude user with specific UID/GID
RUN groupadd -g 1000 claude && \
useradd -u 1000 -g 1000 -m -s /bin/bash claude && \
echo "claude ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Install Google Antigravity IDE
RUN mkdir -p /etc/apt/keyrings && \
curl -fsSL https://us-central1-apt.pkg.dev/doc/repo-signing-key.gpg | \
gpg --dearmor --yes -o /etc/apt/keyrings/antigravity-repo-key.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/antigravity-repo-key.gpg] https://us-central1-apt.pkg.dev/projects/antigravity-auto-updater-dev/ antigravity-debian main" \
> /etc/apt/sources.list.d/antigravity.list && \
apt-get update && \
apt-get install -y antigravity && \
rm -rf /var/lib/apt/lists/*
# Install OpenSSH server (for SSH IDE mode)
RUN apt-get update && \
apt-get install -y openssh-server && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /var/run/sshd && \
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config && \
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config && \
echo "PermitRootLogin no" >> /etc/ssh/sshd_config
# Create user user with specific UID/GID
RUN groupadd -g 1000 user && \
useradd -u 1000 -g 1000 -m -s /bin/bash user && \
echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Create workspace directory
RUN mkdir -p /workspace && \
chown -R claude:claude /workspace
chown -R user:user /workspace
# Copy startup script
# Copy startup scripts
COPY --chmod=755 scripts/startapp.sh /startapp.sh
COPY --chmod=755 scripts/init-repo.sh /usr/local/bin/init-repo
# Fix app user shell after baseimage-gui creates it at runtime
COPY --chmod=755 scripts/cont-init-user.sh /etc/cont-init.d/20-fix-user-shell.sh
COPY --chmod=755 scripts/cont-init-sshd.sh /etc/cont-init.d/25-start-sshd.sh
# Set working directory
WORKDIR /workspace
# Configure container to run as claude user
ENV HOME=/home/claude \
USER=claude
# Configure container to run as user user
ENV HOME=/config/userdata \
USER=user \
BROWSER=/usr/local/bin/google-chrome
# Expose VNC port (baseimage-gui default)
EXPOSE 5800
+42 -23
View File
@@ -44,26 +44,40 @@ clean: stop
@echo "Cleaning up..."
rm -rf ./home ./workspace
# Kubernetes deployment
k8s-deploy:
@echo "Deploying to Kubernetes..."
kubectl apply -k k8s/
# Helm deployment
RELEASE_NAME ?= mydev
NAMESPACE ?= default
k8s-delete:
@echo "Deleting from Kubernetes..."
kubectl delete -k k8s/
helm-deploy:
@echo "Deploying with Helm (release: $(RELEASE_NAME))..."
@if [ -z "$(GITHUB_REPO)" ]; then \
echo "ERROR: GITHUB_REPO environment variable is required"; \
echo "Usage: GITHUB_REPO=https://github.com/user/repo make helm-deploy"; \
exit 1; \
fi
helm upgrade --install $(RELEASE_NAME) ./chart \
--namespace $(NAMESPACE) \
--set name=$(RELEASE_NAME) \
--set githubRepo="$(GITHUB_REPO)" \
--set image.repository=$(REGISTRY)/$(IMAGE_NAME) \
--set image.tag=$(IMAGE_TAG)
k8s-logs:
@echo "Showing logs..."
kubectl logs -f antigravity-0
helm-delete:
@echo "Deleting Helm release $(RELEASE_NAME)..."
helm uninstall $(RELEASE_NAME) --namespace $(NAMESPACE)
@echo "Note: PVC persists. To delete: kubectl delete pvc userhome-$(RELEASE_NAME) -n $(NAMESPACE)"
k8s-shell:
@echo "Opening shell..."
kubectl exec -it antigravity-0 -- bash
helm-logs:
@echo "Showing logs for $(RELEASE_NAME)..."
kubectl logs -f deployment/devcontainer-$(RELEASE_NAME) -n $(NAMESPACE)
k8s-port-forward:
@echo "Port forwarding to localhost:5800..."
kubectl port-forward antigravity-0 5800:5800
helm-shell:
@echo "Opening shell in $(RELEASE_NAME)..."
kubectl exec -it deployment/devcontainer-$(RELEASE_NAME) -n $(NAMESPACE) -- bash
helm-port-forward:
@echo "Port forwarding $(RELEASE_NAME) to localhost:5800..."
kubectl port-forward deployment/devcontainer-$(RELEASE_NAME) 5800:5800 -n $(NAMESPACE)
# Show help
help:
@@ -78,24 +92,29 @@ help:
@echo " stop - Stop running container"
@echo " clean - Clean up containers and volumes"
@echo ""
@echo "Kubernetes Targets:"
@echo " k8s-deploy - Deploy to Kubernetes"
@echo " k8s-delete - Delete from Kubernetes"
@echo " k8s-logs - Show container logs"
@echo " k8s-shell - Open shell in container"
@echo " k8s-port-forward - Port forward to localhost"
@echo "Helm/Kubernetes Targets:"
@echo " helm-deploy - Deploy with Helm chart (requires GITHUB_REPO)"
@echo " helm-delete - Delete Helm release"
@echo " helm-logs - Show container logs"
@echo " helm-shell - Open shell in container"
@echo " helm-port-forward - Port forward to localhost"
@echo ""
@echo "Variables:"
@echo " REGISTRY - Docker registry (default: ghcr.io/cpfarhood)"
@echo " IMAGE_NAME - Image name (default: antigravity)"
@echo " IMAGE_TAG - Image tag (default: latest)"
@echo " RELEASE_NAME - Helm release name (default: mydev)"
@echo " NAMESPACE - Kubernetes namespace (default: default)"
@echo " GITHUB_REPO - GitHub repository URL (required for helm-deploy)"
@echo ""
@echo "Environment Variables for 'make run':"
@echo " GITHUB_REPO - GitHub repository URL"
@echo " GITHUB_TOKEN - GitHub token (optional)"
@echo " VNC_PASSWORD - VNC password (optional)"
@echo ""
@echo "Example:"
@echo "Examples:"
@echo " make build"
@echo " make push REGISTRY=ghcr.io/myuser IMAGE_TAG=v1.0"
@echo " GITHUB_REPO=https://github.com/user/repo make run"
@echo " GITHUB_REPO=https://github.com/user/repo make helm-deploy"
@echo " RELEASE_NAME=alice-dev GITHUB_REPO=https://github.com/alice/project make helm-deploy"
+343 -345
View File
@@ -2,366 +2,364 @@
![Build and Push](https://github.com/cpfarhood/devcontainer/actions/workflows/build-and-push.yaml/badge.svg)
A containerized development environment with GUI access, featuring:
- **Antigravity** (VSCode/Cloud IDE) via web browser
- **Happy Coder** - AI-powered development assistant
- **Automatic GitHub repo cloning**
- **Persistent user home directory**
- **Secure non-root execution**
## Features
### GUI Access
- Web-based VNC interface (port 5800)
- Full desktop environment in your browser
- Secure connections with optional password protection
### Development Tools
- Antigravity IDE (VSCode-based)
- Happy Coder AI assistant
- Git integration
- Node.js and npm
- Python 3
- Chrome browser
### Security
- Runs as non-root user `claude` (UID 1000, GID 1000)
- Secure VNC connections
- Token-based GitHub authentication
- Isolated workspace
### Persistence
- ReadWriteMany PVC for `/home` (user data persists)
- Workspace mounted at `/workspace`
- Repository cloned on first startup
## Documentation
- **[DEPLOYMENT.md](DEPLOYMENT.md)** - Complete deployment guide with step-by-step instructions
- **[VARIABLES.md](VARIABLES.md)** - Reference for all configuration variables
- **[README.md](README.md)** - This file (overview and quick start)
A containerized cloud development environment with web-based GUI access, featuring:
- **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
- **Kubernetes-native** Helm chart deployment
## Quick Start
**👉 For detailed deployment instructions, see [DEPLOYMENT.md](DEPLOYMENT.md)**
### 1. Create a secret
### 1. Get the Image
The secret is picked up automatically via `envFrom`. Keys recognised:
The image is automatically built and published to GitHub Container Registry on every push to main.
| Key | Purpose |
|-----|---------|
| `GITHUB_TOKEN` | PAT for private repo access (`repo` scope) |
| `VNC_PASSWORD` | Password for the VNC web UI |
| `ANTHROPIC_API_KEY` | API key — alternative to browser-based Claude login |
| `SSH_AUTHORIZED_KEYS` | Public key(s) for SSH access (required when `ssh: true`) |
| `homeassistant-url` | Home Assistant URL (required when `mcpSidecars.homeassistant.enabled: true`) |
| `homeassistant-token` | Home Assistant long-lived access token (required when `mcpSidecars.homeassistant.enabled: true`) |
```bash
# Pull the latest image
docker pull ghcr.io/cpfarhood/devcontainer:latest
# Or pull a specific version
docker pull ghcr.io/cpfarhood/devcontainer:v1.0.0
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=VNC_PASSWORD='changeme'
```
**Building locally (optional):**
Or use SealedSecrets:
```bash
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=VNC_PASSWORD='changeme' \
--dry-run=client -o yaml | \
kubeseal --format=yaml | kubectl apply -f -
```
### 2. Deploy with Helm
```bash
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo
```
### 3. Access
```bash
# Local port-forward
kubectl port-forward deployment/devcontainer-mydev 5800:5800
open http://localhost:5800
```
Or configure an ingress / Gateway API HTTPRoute pointing at port 5800.
### 4. Authenticate Claude
On first launch, open a terminal in the VSCode GUI and run:
```bash
claude
```
A Chrome browser window will open inside VNC for the Claude Max OAuth login. Credentials are stored on the home PVC and persist across pod restarts.
---
## Helm Chart Reference
### Core values
| Value | Default | Description |
|-------|---------|-------------|
| `name` | `""` | Instance name — used in all resource names (`devcontainer-{name}`) |
| `githubRepo` | `""` | Repository to clone into `/workspace` on startup |
| `ide` | `vscode` | IDE to launch — `vscode`, `antigravity`, or `none` (see below) |
| `ssh` | `false` | Also start an OpenSSH server on port 22 (additive, any `ide`) |
| `image.repository` | `ghcr.io/cpfarhood/devcontainer` | Container image |
| `image.tag` | `latest` | Image tag |
### IDE choice
`ide` controls what GUI is launched in the VNC session:
| Value | Port | Description |
|-------|------|-------------|
| `vscode` (default) | 5800 (VNC) | VSCode desktop via browser-based VNC |
| `antigravity` | 5800 (VNC) | Google Antigravity (VSCode fork with AI) via VNC |
| `none` | — | No IDE; container stays alive (useful when `ssh: true`) |
### SSH access
`ssh: true` starts OpenSSH on port 22 **in addition to** the IDE. It works with any `ide` value:
```bash
# SSH-only (no VNC)
helm install mydev ./chart --set name=mydev --set ide=none --set ssh=true
# VSCode in VNC + SSH access at the same time
helm install mydev ./chart --set name=mydev --set ssh=true
```
Add your public key to the env secret:
```bash
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=SSH_AUTHORIZED_KEYS='ssh-ed25519 AAAA...'
```
Then connect:
```bash
kubectl port-forward deployment/devcontainer-mydev 2222:22
ssh -p 2222 user@localhost
```
### Happy Coder
| Value | Default | Description |
|-------|---------|-------------|
| `happyServerUrl` | `https://happy.farh.net` | Happy Coder server endpoint |
| `happyWebappUrl` | `https://happy-coder.farh.net` | Happy Coder webapp URL |
| `happyHomeDir` | `/home/user/.happy` | Happy runtime state directory (persists on the home PVC) |
| `happyExperimental` | `true` | Enable experimental Happy features |
### Kubernetes cluster access
The `clusterAccess` value provisions a ServiceAccount, Role/ClusterRole, and binding so the devcontainer pod can interact with the Kubernetes API. The default is `none` — no RBAC resources are created.
| Value | Scope | Verbs |
|-------|-------|-------|
| `none` (default) | — | no access |
| `readonlyns` | release namespace | `get`, `list`, `watch` |
| `readwritens` | release namespace | `*` |
| `readonly` | cluster-wide | `get`, `list`, `watch` |
| `readwrite` | cluster-wide | `*` |
```bash
# Give the pod read-only access to its own namespace
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set clusterAccess=readonlyns
```
With any non-`none` value, a `ServiceAccount` named `devcontainer-{name}` is created and set as the pod's `serviceAccountName`, so `kubectl` and any in-cluster API calls use it automatically.
### MCP Sidecars
The devcontainer includes MCP (Model Context Protocol) servers as sidecar containers that enable AI assistants to interact with various services:
| Sidecar | Default | Purpose |
|---------|---------|---------|
| `mcpSidecars.kubernetes.enabled` | `true` | Kubernetes API access via MCP |
| `mcpSidecars.flux.enabled` | `true` | Flux GitOps operations via MCP |
| `mcpSidecars.homeassistant.enabled` | `false` | Home Assistant smart home control via MCP |
**Notes:**
- Kubernetes and Flux sidecars require `clusterAccess` != `none` to be deployed (automatically disabled when no cluster access)
- Kubernetes and Flux sidecars inherit the pod's ServiceAccount RBAC permissions (controlled by `clusterAccess`)
- Home Assistant sidecar requires additional configuration (see below)
**Disable MCP sidecars:**
```bash
# Disable both sidecars
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.kubernetes.enabled=false \
--set mcpSidecars.flux.enabled=false
# Or selectively disable
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.flux.enabled=false # Disable only Flux MCP
```
**Enable Home Assistant MCP:**
```bash
# Create secret with Home Assistant credentials
kubectl create secret generic devcontainer-mydev-secrets-env \
--from-literal=GITHUB_TOKEN='ghp_...' \
--from-literal=homeassistant-url='http://homeassistant.local:8123' \
--from-literal=homeassistant-token='your_long_lived_access_token'
# Deploy with Home Assistant MCP enabled
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/youruser/yourrepo \
--set mcpSidecars.homeassistant.enabled=true
```
**Custom MCP configuration:**
```yaml
# values.yaml override
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: false # Disabled in this example
homeassistant:
enabled: true
image:
repository: ghcr.io/homeassistant-ai/ha-mcp
tag: v6.7.1 # Override the pinned version if needed
port: 8087
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
```
### Display and resources
| Value | Default | Description |
|-------|---------|-------------|
| `display.width` | `1920` | VNC width (px) |
| `display.height` | `1080` | VNC height (px) |
| `secureConnection` | `0` | Set to `1` if TLS is not terminated upstream |
| `userId` | `1000` | UID for the app user |
| `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` | |
| `resources.limits.cpu` | `4000m` | |
| `envSecretName` | `devcontainer-{name}-secrets-env` | Override the secret name |
---
## Architecture
### Startup flow
```
Container start
→ cont-init.d/20-fix-user-shell.sh — fix shell/home on baseimage-gui app user
→ cont-init.d/25-start-sshd.sh — start sshd if SSH=true
→ /startapp.sh (runs as app user, UID 1000)
→ init-repo.sh
→ clone / pull GITHUB_REPO into /workspace/{repo}
→ 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 --no-sandbox --user-data-dir ~/.config/antigravity ... /workspace/{repo}
IDE=none: sleep infinity
(SSH=true: sshd also running as root on port 22; host keys persisted on PVC)
```
### Storage
| Mount | Source | Persistence |
|-------|--------|-------------|
| `/home` | ReadWriteMany PVC (`userhome-{name}`) | Survives pod restarts — stores Claude credentials, dotfiles, git config |
| `/workspace` | `emptyDir` | Ephemeral — repo is re-cloned on each pod start |
Happy Coder's runtime state (`HAPPY_HOME_DIR`) is kept in `/home/user/.happy` on the persistent home PVC, so auth credentials and settings survive pod restarts. A stale lock file (`daemon.state.json.lock`) is removed automatically on each startup.
---
## Troubleshooting
### Happy Coder daemon not starting
```bash
# Check daemon status
happy daemon status
# Start manually (also clears any stale lock)
happy daemon start
# View daemon logs
ls ~/.happy/logs/
```
### Claude not authenticated
Browser-based OAuth login is the primary method (works inside VNC via the Chrome wrapper). If you prefer API key auth:
```bash
kubectl patch secret devcontainer-mydev-secrets-env \
--type='json' \
-p='[{"op":"add","path":"/data/ANTHROPIC_API_KEY","value":"'$(echo -n "sk-ant-..." | base64)'"}]'
```
Then restart the pod to pick up the new env var.
### VNC not loading
```bash
kubectl port-forward deployment/devcontainer-mydev 5800:5800
kubectl logs deployment/devcontainer-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
```bash
kubectl logs deployment/devcontainer-mydev | grep "Repository Initialization"
kubectl exec deployment/devcontainer-mydev -- env | grep GITHUB
```
---
## Local Docker run
```bash
docker run -d \
-p 5800:5800 \
-e GITHUB_REPO="https://github.com/youruser/yourrepo" \
-e GITHUB_TOKEN="ghp_..." \
-e VNC_PASSWORD="changeme" \
-v $(pwd)/home:/home \
ghcr.io/cpfarhood/devcontainer:latest
```
---
## Building
```bash
docker build -t ghcr.io/cpfarhood/devcontainer:latest .
docker push ghcr.io/cpfarhood/devcontainer:latest
```
### 2. Configure Secrets
The image is also built and pushed automatically by CI on every push to `main` and on version tags (`v*`).
Edit `k8s/secrets-example.yaml` and create a sealed secret:
```bash
kubectl create secret generic antigravity-secrets \
--from-literal=github-token='ghp_your_token' \
--from-literal=vnc-password='your_password' \
--dry-run=client -o yaml | \
kubeseal --format=yaml > k8s/sealedsecrets.yaml
```
### 3. Configure Repository
Edit `k8s/configmap.yaml`:
```yaml
data:
github-repo: "https://github.com/yourusername/yourrepo"
```
### 4. Deploy to Kubernetes
```bash
kubectl apply -k k8s/
```
### 5. Access the Interface
```bash
# Port forward for local access
kubectl port-forward statefulset/antigravity 5800:5800
# Open in browser
open http://localhost:5800
```
Or configure HTTPRoute (Gateway API) for external access via your domain.
## Environment Variables
### Required
- `GITHUB_REPO` - GitHub repository URL to clone
### Optional
- `GITHUB_TOKEN` - GitHub Personal Access Token (for private repos)
- `VNC_PASSWORD` - Password for VNC access
- `USER_ID` - UID for claude user (default: 1000)
- `GROUP_ID` - GID for claude user (default: 1000)
- `DISPLAY_WIDTH` - VNC display width (default: 1920)
- `DISPLAY_HEIGHT` - VNC display height (default: 1080)
### Happy Coder Configuration (Optional)
- `HAPPY_SERVER_URL` - Custom Happy server URL (default: https://api.cluster-fluster.com)
- `HAPPY_WEBAPP_URL` - Custom Happy webapp URL (default: https://app.happy.engineering)
- `HAPPY_HOME_DIR` - Happy data directory (default: /home/claude/.happy)
- `HAPPY_EXPERIMENTAL` - Enable experimental features (default: true in container)
## Architecture
```
┌─────────────────────────────────────┐
│ Web Browser (Port 5800) │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ VNC Web Interface │
│ (jlesage/baseimage-gui) │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ Antigravity IDE │
│ (VSCode + Extensions) │
│ Running as user: claude (1000) │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ Happy Coder (Background Process) │
│ AI Development Assistant │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Workspace: /workspace/{repo} │
│ Home: /home/claude (RWX PVC) │
└─────────────────────────────────────┘
```
## Startup Flow
1. **Container starts** - baseimage-gui initializes
2. **init-repo.sh runs**:
- Checks for `GITHUB_REPO` environment variable
- Clones repository to `/workspace/{repo-name}` if not exists
- Configures git credentials with `GITHUB_TOKEN`
- Starts Happy Coder in background
3. **startapp.sh runs**:
- Opens Antigravity IDE in the cloned repository
- Happy Coder is already running and accessible
## Happy Coder Integration
Happy Coder runs as a background service and is accessible within the IDE:
```bash
# Check Happy Coder status
ps aux | grep happy-coder
# View logs
cat /tmp/happy-coder.log
# Restart Happy Coder
sudo -u claude bash -c "cd /workspace/your-repo && happy-coder &"
```
## Local Development
### Run with Docker Compose
```yaml
version: '3.8'
services:
antigravity:
build: .
ports:
- "5800:5800"
environment:
- GITHUB_REPO=https://github.com/yourusername/yourrepo
- GITHUB_TOKEN=ghp_your_token
- VNC_PASSWORD=yourpassword
- HAPPY_EXPERIMENTAL=true
volumes:
- ./home:/home
- ./workspace:/workspace
```
```bash
docker-compose up
```
### Run with Docker
```bash
docker run -d \
-p 5800:5800 \
-e GITHUB_REPO="https://github.com/yourusername/yourrepo" \
-e GITHUB_TOKEN="ghp_your_token" \
-e VNC_PASSWORD="yourpassword" \
-e HAPPY_EXPERIMENTAL="true" \
-v $(pwd)/home:/home \
-v $(pwd)/workspace:/workspace \
ghcr.io/cpfarhood/antigravity:latest
```
## Kubernetes Deployment
### With Flux
See the animaniacs cluster configuration for GitOps deployment patterns.
### Standalone
```bash
# Apply manifests
kubectl apply -k k8s/
# Check status
kubectl get statefulset antigravity
kubectl get pods -l app=antigravity
# Access logs
kubectl logs antigravity-0
# Access shell
kubectl exec -it antigravity-0 -- bash
```
## Troubleshooting
### Repository not cloning
```bash
# Check logs
kubectl logs antigravity-0 | grep "Repository Initialization"
# Verify GITHUB_REPO is set
kubectl exec antigravity-0 -- env | grep GITHUB
# Check git credentials
kubectl exec antigravity-0 -- cat /home/claude/.git-credentials
```
### Happy Coder not starting
```bash
# Check Happy Coder logs
kubectl exec antigravity-0 -- cat /tmp/happy-coder.log
# Verify API key
kubectl exec antigravity-0 -- env | grep HAPPY_CODER
# Restart Happy Coder
kubectl exec antigravity-0 -- sudo -u claude bash -c "cd /workspace/repo && happy-coder &"
```
### VNC not accessible
```bash
# Check port forwarding
kubectl port-forward antigravity-0 5800:5800
# Verify service
kubectl get svc antigravity
# Check pod status
kubectl describe pod antigravity-0
```
### Permission issues
```bash
# Check ownership
kubectl exec antigravity-0 -- ls -la /home/claude
kubectl exec antigravity-0 -- ls -la /workspace
# Fix ownership
kubectl exec antigravity-0 -- chown -R claude:claude /home/claude
kubectl exec antigravity-0 -- chown -R claude:claude /workspace
```
## Security Considerations
1. **Secrets Management**: Use SealedSecrets or external secret managers
2. **Network Policies**: Restrict ingress/egress as needed
3. **RBAC**: Limit who can access the namespace
4. **VNC Password**: Always set a strong VNC password
5. **GitHub Token**: Use fine-grained tokens with minimal permissions
6. **Container Security**: Runs as non-root user (claude:1000)
## Storage
### Home Directory (`/home`)
- Mounted from ReadWriteMany PVC (`userhome`)
- Persists user settings, credentials, history
- Survives pod restarts
### Workspace (`/workspace`)
- ephemeral emptyDir (can be changed to PVC)
- Contains cloned repository
- Rebuild on pod restart
To persist workspace:
1. Create a PVC for workspace
2. Update `statefulset.yaml` to use PVC instead of emptyDir
## Customization
### Add More Tools
Edit `Dockerfile`:
```dockerfile
RUN apt-get update && apt-get install -y \
your-package-here \
&& rm -rf /var/lib/apt/lists/*
```
### Change Display Resolution
Set environment variables:
```yaml
env:
- name: DISPLAY_WIDTH
value: "2560"
- name: DISPLAY_HEIGHT
value: "1440"
```
### Auto-clone Multiple Repos
Modify `init-repo.sh` to support `GITHUB_REPOS` (comma-separated):
```bash
IFS=',' read -ra REPOS <<< "$GITHUB_REPOS"
for repo in "${REPOS[@]}"; do
# Clone each repo
done
```
## License
MIT
---
## Credits
- Built on [jlesage/baseimage-gui](https://github.com/jlesage/docker-baseimage-gui)
- Uses [Happy Coder](https://happy.engineering)
- Inspired by Google's Project IDX
- Base image: [jlesage/docker-baseimage-gui](https://github.com/jlesage/docker-baseimage-gui)
- AI assistant: [Happy Coder](https://happy.engineering) + [Claude](https://claude.ai)
+414 -275
View File
@@ -1,305 +1,444 @@
# Configuration Variables Reference
# Helm Chart Values Reference
Quick reference for all configurable variables in this project.
Complete reference for all configurable values in the Antigravity Dev Container Helm chart.
## Required Variables
## Core Configuration
These MUST be configured before deployment:
### Storage Class Name
- **Variable:** `storageClassName`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~117
### name
- **Type:** String
- **Description:** ReadWriteMany storage class available in your cluster
- **Example:** `ceph-filesystem`, `nfs-client`, `efs-sc`
- **How to find:** `kubectl get storageclass`
### GitHub Repository URL
- **Variable:** `github-repo`
- **File:** `k8s/configmap.yaml`
- **Line:** ~9
- **Type:** String (URL)
- **Description:** Repository to clone on container startup
- **Format:** `https://github.com/username/repository`
- **Example:** `https://github.com/cpfarhood/my-project`
### Gateway Name
- **Variable:** `parentRefs[0].name`
- **File:** `k8s/httproute.yaml`
- **Line:** ~8
- **Type:** String
- **Description:** Name of your Gateway resource
- **How to find:** `kubectl get gateway -A`
### Gateway Namespace
- **Variable:** `parentRefs[0].namespace`
- **File:** `k8s/httproute.yaml`
- **Line:** ~9
- **Type:** String
- **Description:** Namespace where Gateway is deployed
- **How to find:** `kubectl get gateway -A`
### Domain Hostname
- **Variable:** `hostnames[0]`
- **File:** `k8s/httproute.yaml`
- **Line:** ~11
- **Type:** String (FQDN)
- **Description:** Domain name for accessing the container
- **Example:** `devcontainer.example.com`
## Optional Variables
### GitHub Token
- **Variable:** `github-token`
- **File:** Sealed Secret
- **Type:** String (GitHub PAT)
- **Description:** Personal Access Token for private repos
- **Required:** Only for private repositories
- **Format:** `ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
- **Scopes:** `repo`
### VNC Password
- **Variable:** `vnc-password`
- **File:** Sealed Secret
- **Type:** String
- **Description:** Password for VNC web interface
- **Required:** Recommended for security
- **Format:** Any string (12+ characters recommended)
### Namespace
- **Variable:** `namespace`
- **File:** `k8s/kustomization.yaml`
- **Line:** ~5
- **Type:** String
- **Description:** Kubernetes namespace for deployment
- **Default:** `default`
### Container Image
- **Variable:** `image`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~32
- **Type:** String (image reference)
- **Description:** Docker image to deploy
- **Default:** `ghcr.io/cpfarhood/devcontainer:latest`
- **Format:** `registry/repository:tag`
### Memory Request
- **Variable:** `resources.requests.memory`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~99
- **Type:** String (quantity)
- **Description:** Minimum memory to reserve
- **Default:** `2Gi`
- **Format:** `<number>Gi` or `<number>Mi`
### Memory Limit
- **Variable:** `resources.limits.memory`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~102
- **Type:** String (quantity)
- **Description:** Maximum memory allowed
- **Default:** `8Gi`
- **Format:** `<number>Gi` or `<number>Mi`
### CPU Request
- **Variable:** `resources.requests.cpu`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~100
- **Type:** String (quantity)
- **Description:** Minimum CPU to reserve
- **Default:** `1000m` (1 core)
- **Format:** `<number>m` (millicores) or `<number>` (cores)
### CPU Limit
- **Variable:** `resources.limits.cpu`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~103
- **Type:** String (quantity)
- **Description:** Maximum CPU allowed
- **Default:** `4000m` (4 cores)
- **Format:** `<number>m` (millicores) or `<number>` (cores)
### Storage Size
- **Variable:** `storage` (under volumeClaimTemplates)
- **File:** `k8s/statefulset.yaml`
- **Line:** ~120
- **Type:** String (quantity)
- **Description:** Size of home directory PVC
- **Default:** `10Gi`
- **Format:** `<number>Gi` or `<number>Ti`
### Happy Server URL
- **Variable:** `happy-server-url`
- **File:** `k8s/configmap.yaml`
- **Line:** ~12 (commented)
- **Type:** String (URL)
- **Description:** Custom Happy Coder server
- **Default:** `https://api.cluster-fluster.com`
- **When to set:** Self-hosted Happy instance only
### Happy Webapp URL
- **Variable:** `happy-webapp-url`
- **File:** `k8s/configmap.yaml`
- **Line:** ~13 (commented)
- **Type:** String (URL)
- **Description:** Custom Happy Coder webapp
- **Default:** `https://app.happy.engineering`
- **When to set:** Self-hosted Happy instance only
### Display Width
- **Variable:** `DISPLAY_WIDTH`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~56
- **Type:** String (number)
- **Description:** VNC display width in pixels
- **Default:** `1920`
### Display Height
- **Variable:** `DISPLAY_HEIGHT`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~58
- **Type:** String (number)
- **Description:** VNC display height in pixels
- **Default:** `1080`
### User ID
- **Variable:** `USER_ID`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~51
- **Type:** String (number)
- **Description:** UID for claude user
- **Default:** `1000`
### Group ID
- **Variable:** `GROUP_ID`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~53
- **Type:** String (number)
- **Description:** GID for claude user
- **Default:** `1000`
### StatefulSet Replicas
- **Variable:** `replicas`
- **File:** `k8s/statefulset.yaml`
- **Line:** ~21
- **Type:** Integer
- **Description:** Number of container instances
- **Default:** `1`
- **Note:** Each replica gets own home PVC
## Environment Variables (Runtime)
These are set at runtime, not in configuration files:
### GITHUB_REPO
- **Type:** String (URL)
- **Description:** Repository URL (from ConfigMap)
- **Default:** `""`
- **Required:** Yes
- **Source:** ConfigMap `antigravity.github-repo`
- **Description:** Instance name used to generate resource names (`devcontainer-{name}`, `userhome-{name}`)
- **Example:** `mydev`, `alice-dev`, `team-workspace`
### GITHUB_TOKEN
### githubRepo
- **Type:** String
- **Description:** GitHub PAT (from Secret)
- **Required:** No (only for private repos)
- **Source:** Secret `antigravity.github-token`
- **Default:** `""`
- **Required:** Yes
- **Description:** GitHub repository URL to clone into `/workspace`
- **Example:** `https://github.com/username/repository`
### VNC_PASSWORD
### ide
- **Type:** String
- **Description:** VNC password (from Secret)
- **Required:** No
- **Source:** Secret `antigravity.vnc-password`
- **Default:** `vscode`
- **Options:** `vscode`, `antigravity`, `none`
- **Description:** IDE to launch inside the container
- `vscode` — VSCode via VNC browser UI on port 5800
- `antigravity` — Google Antigravity (VSCode fork) via VNC on port 5800
- `none` — No IDE; useful when `ssh: true` is the sole access method
### HAPPY_SERVER_URL
- **Type:** String (URL)
- **Description:** Happy server URL (from ConfigMap)
- **Required:** No
- **Source:** ConfigMap `antigravity.happy-server-url`
### ssh
- **Type:** Boolean
- **Default:** `false`
- **Description:** Start an OpenSSH server on port 22 in addition to the IDE
- **Note:** Requires `SSH_AUTHORIZED_KEYS` in env secret for key-based login
### HAPPY_WEBAPP_URL
- **Type:** String (URL)
- **Description:** Happy webapp URL (from ConfigMap)
- **Required:** No
- **Source:** ConfigMap `antigravity.happy-webapp-url`
## Image Configuration
### HAPPY_HOME_DIR
- **Type:** String (path)
- **Description:** Happy data directory
- **Required:** No
- **Default:** `/home/claude/.happy`
- **Source:** Hardcoded in StatefulSet
### image.repository
- **Type:** String
- **Default:** `ghcr.io/cpfarhood/devcontainer`
- **Description:** Container image repository
### HAPPY_EXPERIMENTAL
- **Type:** String (boolean)
- **Description:** Enable Happy experimental features
- **Required:** No
### image.tag
- **Type:** String
- **Default:** `latest`
- **Description:** Container image tag
- **Best Practice:** Use specific version tags for production
### image.pullPolicy
- **Type:** String
- **Default:** `Always`
- **Options:** `Always`, `IfNotPresent`, `Never`
- **Description:** Image pull policy
## Happy Coder Configuration
### happyServerUrl
- **Type:** String
- **Default:** `https://happy.farh.net`
- **Description:** Happy Coder server endpoint
- **When to Change:** Self-hosted Happy instance
### happyWebappUrl
- **Type:** String
- **Default:** `https://happy-coder.farh.net`
- **Description:** Happy Coder webapp URL
- **When to Change:** Self-hosted Happy instance
### happyHomeDir
- **Type:** String
- **Default:** `/config/userdata/.happy`
- **Description:** Happy runtime state directory (persists on PVC)
### happyExperimental
- **Type:** String
- **Default:** `"true"`
- **Description:** Enable experimental Happy features
## Display Configuration
### display.width
- **Type:** String
- **Default:** `"1920"`
- **Description:** VNC display width in pixels
### display.height
- **Type:** String
- **Default:** `"1080"`
- **Description:** VNC display height in pixels
### secureConnection
- **Type:** String
- **Default:** `"0"`
- **Options:** `"0"`, `"1"`
- **Description:** Set to `"0"` when TLS is terminated at the gateway layer
## User Configuration
### userId
- **Type:** String
- **Default:** `"1000"`
- **Description:** UID for the app user
### groupId
- **Type:** String
- **Default:** `"1000"`
- **Description:** GID for the app user
## Storage Configuration
### storage.size
- **Type:** String
- **Default:** `32Gi`
- **Description:** Size of the persistent home directory
- **Format:** Kubernetes quantity (e.g., `10Gi`, `100Gi`, `1Ti`)
### storage.className
- **Type:** String
- **Default:** `ceph-filesystem`
- **Description:** StorageClass name (must support ReadWriteMany)
- **Examples:** `ceph-filesystem`, `nfs-client`, `efs-sc`, `azurefile`
### shm.sizeLimit
- **Type:** String
- **Default:** `2Gi`
- **Description:** `/dev/shm` size (memory-backed emptyDir for Electron apps)
## Resource Limits
### resources.requests.memory
- **Type:** String
- **Default:** `2Gi`
- **Description:** Minimum memory to reserve
- **Format:** Kubernetes quantity
### resources.requests.cpu
- **Type:** String
- **Default:** `1000m`
- **Description:** Minimum CPU to reserve
- **Format:** Millicores (`1000m` = 1 CPU core)
### resources.limits.memory
- **Type:** String
- **Default:** `8Gi`
- **Description:** Maximum memory allowed
- **Format:** Kubernetes quantity
### resources.limits.cpu
- **Type:** String
- **Default:** `4000m`
- **Description:** Maximum CPU allowed
- **Format:** Millicores (`4000m` = 4 CPU cores)
## Kubernetes Access
### clusterAccess
- **Type:** String
- **Default:** `none`
- **Options:**
- `none` — No cluster access
- `readonlyns` — Read-only access to release namespace
- `readwritens` — Full access to release namespace
- `readonly` — Read-only access cluster-wide
- `readwrite` — Full access cluster-wide
- **Description:** RBAC permissions for the pod's ServiceAccount
## Secrets
### envSecretName
- **Type:** String
- **Default:** `""` (auto-generates as `devcontainer-{name}-secrets-env`)
- **Description:** Name of existing Secret containing environment variables
- **Keys Recognized:**
- `GITHUB_TOKEN` — PAT for private repo access
- `VNC_PASSWORD` — Password for VNC web UI
- `ANTHROPIC_API_KEY` — API key for Claude
- `SSH_AUTHORIZED_KEYS` — Public keys for SSH access
- `homeassistant-url` — Home Assistant base URL (e.g., http://homeassistant.local:8123)
- `homeassistant-token` — Home Assistant long-lived access token
## MCP Sidecars
### mcpSidecars.kubernetes.enabled
- **Type:** Boolean
- **Default:** `true`
- **Source:** Hardcoded in StatefulSet
- **Description:** Enable Kubernetes MCP server sidecar
## Variable Groups by Use Case
### mcpSidecars.kubernetes.image.repository
- **Type:** String
- **Default:** `quay.io/containers/kubernetes_mcp_server`
- **Description:** Kubernetes MCP server image
### Minimal Deployment
Only these variables are required for basic deployment:
1. `storageClassName`
2. `github-repo`
3. `parentRefs.name`
4. `parentRefs.namespace`
5. `hostnames`
### mcpSidecars.kubernetes.image.tag
- **Type:** String
- **Default:** `latest`
- **Description:** Kubernetes MCP server image tag
### Private Repository Deployment
Add these for private repos:
1. All minimal deployment variables
2. `github-token` (sealed secret)
### mcpSidecars.kubernetes.port
- **Type:** Integer
- **Default:** `8080`
- **Description:** Port for Kubernetes MCP server
### Production Deployment
Recommended for production:
1. All private repository variables
2. `vnc-password` (sealed secret)
3. `resources.requests.*` (adjusted for workload)
4. `resources.limits.*` (adjusted for workload)
5. `namespace` (dedicated namespace)
### mcpSidecars.kubernetes.resources
- **Type:** Object
- **Default:**
```yaml
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
```
- **Description:** Resource limits for Kubernetes MCP sidecar
### Multi-User Deployment
For multiple users:
1. All production deployment variables
2. `replicas` (set to number of users)
3. Larger `storage` size for home PVCs
### mcpSidecars.flux.enabled
- **Type:** Boolean
- **Default:** `true`
- **Description:** Enable Flux MCP server sidecar
## Quick Copy Templates
### mcpSidecars.flux.image.repository
- **Type:** String
- **Default:** `ghcr.io/controlplaneio-fluxcd/flux-operator-mcp`
- **Description:** Flux MCP server image
### mcpSidecars.flux.image.tag
- **Type:** String
- **Default:** `v0.41.1`
- **Description:** Flux MCP server image tag
### mcpSidecars.flux.port
- **Type:** Integer
- **Default:** `8081`
- **Description:** Port for Flux MCP server
### mcpSidecars.flux.resources
- **Type:** Object
- **Default:**
```yaml
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
```
- **Description:** Resource limits for Flux MCP sidecar
### mcpSidecars.homeassistant.enabled
- **Type:** Boolean
- **Default:** `false`
- **Description:** Enable Home Assistant MCP server sidecar
- **Note:** Requires `homeassistant-url` and `homeassistant-token` in env secret
### mcpSidecars.homeassistant.image.repository
- **Type:** String
- **Default:** `ghcr.io/homeassistant-ai/ha-mcp`
- **Description:** Home Assistant MCP server image
### mcpSidecars.homeassistant.image.tag
- **Type:** String
- **Default:** `stable`
- **Description:** Home Assistant MCP server image tag
- **Options:** `stable` (recommended), `latest` (dev builds), `v{version}` (specific version)
### mcpSidecars.homeassistant.port
- **Type:** Integer
- **Default:** `8087`
- **Description:** Port for Home Assistant MCP server (SSE mode)
### mcpSidecars.homeassistant.resources
- **Type:** Object
- **Default:**
```yaml
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
```
- **Description:** Resource limits for Home Assistant MCP sidecar
## Usage Examples
### Minimal Configuration
### Minimal Required Variables
```yaml
# k8s/statefulset.yaml
storageClassName: "CHANGE_ME" # Line ~117
# k8s/configmap.yaml
github-repo: "CHANGE_ME" # Line ~9
# k8s/httproute.yaml
parentRefs:
- name: CHANGE_ME # Line ~8
namespace: CHANGE_ME # Line ~9
hostnames:
- "CHANGE_ME" # Line ~11
name: mydev
githubRepo: https://github.com/user/repo
```
### With Secrets
```bash
kubectl create secret generic antigravity-secrets \
--from-literal=github-token='CHANGE_ME' \
--from-literal=vnc-password='CHANGE_ME' \
--dry-run=client -o yaml | \
kubeseal --format=yaml > k8s/sealedsecrets.yaml
```
### Production Configuration
### With Resource Adjustments
```yaml
# k8s/statefulset.yaml (lines ~98-103)
name: prod-workspace
githubRepo: https://github.com/company/application
ide: vscode
ssh: true
image:
tag: v1.0.0
storage:
size: 100Gi
className: ceph-filesystem
resources:
requests:
memory: "CHANGE_ME" # e.g., 4Gi
cpu: "CHANGE_ME" # e.g., 2000m
memory: "4Gi"
cpu: "2000m"
limits:
memory: "CHANGE_ME" # e.g., 16Gi
cpu: "CHANGE_ME" # e.g., 8000m
memory: "16Gi"
cpu: "8000m"
clusterAccess: readwritens
mcpSidecars:
kubernetes:
enabled: true
flux:
enabled: false
```
### Development Team Configuration
```yaml
name: team-dev
githubRepo: https://github.com/team/project
ide: antigravity
display:
width: "2560"
height: "1440"
storage:
size: 50Gi
className: nfs-client
clusterAccess: readonly
happyServerUrl: https://happy.internal.company.com
happyWebappUrl: https://happy-app.internal.company.com
```
### Smart Home Development Configuration
```yaml
name: smarthome-dev
githubRepo: https://github.com/user/home-automation
ide: vscode
clusterAccess: readwritens
mcpSidecars:
kubernetes:
enabled: true
flux:
enabled: false
homeassistant:
enabled: true
image:
tag: stable
# Requires secrets:
# homeassistant-url: http://homeassistant.local:8123
# homeassistant-token: <long-lived-access-token>
```
## Helm CLI Examples
### Using --set Flags
```bash
# Basic deployment
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/user/repo
# With multiple values
helm install mydev ./chart \
--set name=mydev \
--set githubRepo=https://github.com/user/repo \
--set ide=antigravity \
--set storage.size=50Gi \
--set clusterAccess=readwritens \
--set mcpSidecars.flux.enabled=false
```
### Using Values File
Create `custom-values.yaml`:
```yaml
name: mydev
githubRepo: https://github.com/user/repo
storage:
size: 50Gi
clusterAccess: readwritens
```
Deploy:
```bash
helm install mydev ./chart -f custom-values.yaml
```
### Combining Methods
```bash
helm install mydev ./chart \
-f base-values.yaml \
-f prod-values.yaml \
--set githubRepo=https://github.com/user/repo \
--set image.tag=v2.0.0
```
## Value Precedence
Values are applied in order of precedence (highest to lowest):
1. `--set` flags on command line
2. `-f` values files (later files override earlier)
3. `chart/values.yaml` defaults
## Environment Variables
These environment variables are set in the container based on chart values:
| Environment Variable | Source Value | Description |
|---------------------|--------------|-------------|
| `GITHUB_REPO` | `githubRepo` | Repository to clone |
| `GITHUB_TOKEN` | Secret: `github-token` | PAT for private repos |
| `VNC_PASSWORD` | Secret: `vnc-password` | VNC access password |
| `ANTHROPIC_API_KEY` | Secret: `anthropic-api-key` | Claude API key |
| `SSH_AUTHORIZED_KEYS` | Secret: `ssh-authorized-keys` | SSH public keys |
| `HAPPY_SERVER_URL` | `happyServerUrl` | Happy server endpoint |
| `HAPPY_WEBAPP_URL` | `happyWebappUrl` | Happy webapp URL |
| `HAPPY_HOME_DIR` | `happyHomeDir` | Happy data directory |
| `HAPPY_EXPERIMENTAL` | `happyExperimental` | Experimental features |
| `DISPLAY_WIDTH` | `display.width` | VNC width |
| `DISPLAY_HEIGHT` | `display.height` | VNC height |
| `SECURE_CONNECTION` | `secureConnection` | TLS termination |
| `USER_ID` | `userId` | App user UID |
| `GROUP_ID` | `groupId` | App user GID |
| `IDE` | `ide` | IDE to launch |
| `SSH` | `ssh` | SSH server enabled |
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: devcontainer
description: Antigravity Dev Container with Happy Coder AI assistant
type: application
version: 0.1.22
appVersion: "latest"
+28
View File
@@ -0,0 +1,28 @@
{{/*
Resource name prefix: devcontainer-{name}
*/}}
{{- define "antigravity.fullname" -}}
{{- printf "devcontainer-%s" .Values.name }}
{{- end }}
{{/*
PVC name: userhome-{name}
*/}}
{{- define "antigravity.pvcName" -}}
{{- printf "userhome-%s" .Values.name }}
{{- end }}
{{/*
Secret name for env vars, default to devcontainer-{name}-secrets-env
*/}}
{{- define "antigravity.envSecretName" -}}
{{- .Values.envSecretName | default (printf "devcontainer-%s-secrets-env" .Values.name) }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "antigravity.labels" -}}
app: devcontainer
instance: {{ .Values.name }}
{{- end }}
+193
View File
@@ -0,0 +1,193 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "antigravity.fullname" . }}
labels:
{{- include "antigravity.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
{{- include "antigravity.labels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "antigravity.labels" . | nindent 8 }}
spec:
{{- if ne (.Values.clusterAccess | default "none") "none" }}
serviceAccountName: {{ include "antigravity.fullname" . }}
{{- end }}
securityContext:
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
containers:
- name: devcontainer
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
{{- if ne (.Values.ide | default "vscode") "none" }}
- containerPort: 5800
name: vnc-web
protocol: TCP
{{- end }}
{{- if .Values.ssh }}
- containerPort: 22
name: ssh
protocol: TCP
{{- end }}
env:
- name: IDE
value: {{ .Values.ide | default "vscode" | quote }}
- name: SSH
value: {{ .Values.ssh | toString | quote }}
- name: USER_ID
value: {{ .Values.userId | quote }}
- name: GROUP_ID
value: {{ .Values.groupId | quote }}
- name: DISPLAY_WIDTH
value: {{ .Values.display.width | quote }}
- name: DISPLAY_HEIGHT
value: {{ .Values.display.height | quote }}
- name: SECURE_CONNECTION
value: {{ .Values.secureConnection | quote }}
- name: HAPPY_HOME_DIR
value: {{ .Values.happyHomeDir | quote }}
- name: HAPPY_EXPERIMENTAL
value: {{ .Values.happyExperimental | quote }}
- name: HAPPY_SERVER_URL
value: {{ .Values.happyServerUrl | quote }}
- name: HAPPY_WEBAPP_URL
value: {{ .Values.happyWebappUrl | quote }}
- name: GITHUB_REPO
value: {{ .Values.githubRepo | quote }}
envFrom:
- secretRef:
name: {{ include "antigravity.envSecretName" . }}
optional: true
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: userhome
mountPath: /config
- name: workspace
mountPath: /workspace
- name: shm
mountPath: /dev/shm
{{- if ne (.Values.ide | default "vscode") "none" }}
livenessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 10
periodSeconds: 5
{{- else if .Values.ssh }}
livenessProbe:
tcpSocket:
port: 22
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 22
initialDelaySeconds: 5
periodSeconds: 5
{{- end }}
{{- if and .Values.mcpSidecars.kubernetes.enabled (ne .Values.clusterAccess "none") }}
- 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 and .Values.mcpSidecars.flux.enabled (ne .Values.clusterAccess "none") }}
- 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 }}
{{- if .Values.mcpSidecars.homeassistant.enabled }}
- name: homeassistant-mcp
image: "{{ .Values.mcpSidecars.homeassistant.image.repository }}:{{ .Values.mcpSidecars.homeassistant.image.tag }}"
imagePullPolicy: Always
command: ["fastmcp", "run", "ha_mcp.main:app", "--transport", "sse", "--sse-server-host", "0.0.0.0", "--sse-server-port", "{{ .Values.mcpSidecars.homeassistant.port }}"]
ports:
- name: homeassistant
containerPort: {{ .Values.mcpSidecars.homeassistant.port }}
env:
- name: HOMEASSISTANT_URL
valueFrom:
secretKeyRef:
name: {{ include "antigravity.envSecretName" . }}
key: homeassistant-url
optional: true
- name: HOMEASSISTANT_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "antigravity.envSecretName" . }}
key: homeassistant-token
optional: true
livenessProbe:
tcpSocket:
port: {{ .Values.mcpSidecars.homeassistant.port }}
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
tcpSocket:
port: {{ .Values.mcpSidecars.homeassistant.port }}
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.mcpSidecars.homeassistant.resources | nindent 12 }}
{{- end }}
volumes:
- name: workspace
emptyDir: {}
- name: shm
emptyDir:
medium: Memory
sizeLimit: {{ .Values.shm.sizeLimit }}
- name: userhome
persistentVolumeClaim:
claimName: {{ include "antigravity.pvcName" . }}
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "antigravity.pvcName" . }}
labels:
{{- include "antigravity.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteMany
storageClassName: {{ .Values.storage.className }}
resources:
requests:
storage: {{ .Values.storage.size }}
+97
View File
@@ -0,0 +1,97 @@
{{- $access := .Values.clusterAccess | default "none" }}
{{- $name := include "antigravity.fullname" . }}
{{- $ns := .Release.Namespace }}
{{- $labels := include "antigravity.labels" . }}
{{- if ne $access "none" }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- $labels | nindent 4 }}
{{- if or (eq $access "readonlyns") (eq $access "readwritens") }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- $labels | nindent 4 }}
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs:
{{- if eq $access "readonlyns" }}
- get
- list
- watch
{{- else }}
- "*"
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- $labels | nindent 4 }}
subjects:
- kind: ServiceAccount
name: {{ $name }}
namespace: {{ $ns }}
roleRef:
kind: Role
name: {{ $name }}
apiGroup: rbac.authorization.k8s.io
{{- end }}
{{- if or (eq $access "readonly") (eq $access "readwrite") }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ $name }}
labels:
{{- $labels | nindent 4 }}
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs:
{{- if eq $access "readonly" }}
- get
- list
- watch
{{- else }}
- "*"
{{- end }}
- nonResourceURLs: ["*"]
verbs:
{{- if eq $access "readonly" }}
- get
{{- else }}
- "*"
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ $name }}
labels:
{{- $labels | nindent 4 }}
subjects:
- kind: ServiceAccount
name: {{ $name }}
namespace: {{ $ns }}
roleRef:
kind: ClusterRole
name: {{ $name }}
apiGroup: rbac.authorization.k8s.io
{{- end }}
{{- end }}
+22
View File
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "antigravity.fullname" . }}
labels:
{{- include "antigravity.labels" . | nindent 4 }}
spec:
ports:
{{- if ne (.Values.ide | default "vscode") "none" }}
- port: 5800
name: vnc-web
protocol: TCP
targetPort: vnc-web
{{- end }}
{{- if .Values.ssh }}
- port: 22
name: ssh
protocol: TCP
targetPort: ssh
{{- end }}
selector:
{{- include "antigravity.labels" . | nindent 4 }}
+110
View File
@@ -0,0 +1,110 @@
# Instance name — used to generate resource names (devcontainer-{name}, userhome-{name})
name: ""
image:
repository: ghcr.io/cpfarhood/devcontainer
tag: latest
pullPolicy: Always
# GitHub repository to clone into /workspace
githubRepo: ""
# IDE to launch inside the container.
# Options:
# vscode — VSCode via VNC browser UI on port 5800 (default)
# antigravity — Google Antigravity (VSCode fork) via VNC on port 5800
# none — no IDE; useful when ssh: true is the sole access method
ide: vscode
# Start an OpenSSH server on port 22 in addition to the IDE.
# Set SSH_AUTHORIZED_KEYS in the env secret to allow key-based login.
ssh: false
# Happy Coder endpoints
happyServerUrl: "https://happy.farh.net"
happyWebappUrl: "https://happy-coder.farh.net"
happyHomeDir: "/config/userdata/.happy"
happyExperimental: "true"
# VNC display
display:
width: "1920"
height: "1080"
# Set to "0" when TLS is terminated at the gateway layer
secureConnection: "0"
userId: "1000"
groupId: "1000"
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"
cpu: "1000m"
limits:
memory: "8Gi"
cpu: "4000m"
# Kubernetes cluster access granted to the devcontainer pod via RBAC.
# Options:
# none — no cluster access (default)
# readonlyns — get/list/watch all resources in the release namespace
# readwritens — full access to all resources in the release namespace
# readonly — get/list/watch all resources cluster-wide
# readwrite — full access to all resources cluster-wide
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: v0.0.57 # Pinned version (Jan 27, 2025) with token exchange and field selector support
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: v0.41.1
port: 8081
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
homeassistant:
enabled: false # Disabled by default, requires HOMEASSISTANT_URL and HOMEASSISTANT_TOKEN
image:
repository: ghcr.io/homeassistant-ai/ha-mcp
tag: v6.7.1 # Pinned version (Feb 20, 2026) - latest stable release
port: 8087
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
-13
View File
@@ -1,13 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: antigravity-config
data:
# GitHub repository to clone on startup
# Example: "https://github.com/username/repository"
github-repo: ""
# Happy Coder configuration (optional)
# happy-server-url: "https://api.cluster-fluster.com"
# happy-webapp-url: "https://app.happy.engineering"
-20
View File
@@ -1,20 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: antigravity
spec:
parentRefs:
- name: gateway # Replace with your Gateway name
namespace: gateway-system # Replace with your Gateway namespace
hostnames:
- "antigravity.example.com" # Replace with your domain
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: antigravity
port: 5800
weight: 1
-25
View File
@@ -1,25 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
resources:
- configmap.yaml
- statefulset.yaml
- httproute.yaml
# Uncomment to create secrets from files
# secretGenerator:
# - name: antigravity-secrets
# literals:
# - github-token=ghp_your_token
# - vnc-password=your_password
commonLabels:
app: antigravity
environment: production
commonAnnotations:
managed-by: kustomize
description: "Antigravity Dev Container with Happy Coder"
-15
View File
@@ -1,15 +0,0 @@
---
# Example secrets - DO NOT commit actual secrets!
# Use SealedSecrets or another secret management solution
apiVersion: v1
kind: Secret
metadata:
name: antigravity-secrets
type: Opaque
stringData:
# GitHub Personal Access Token (for private repos)
github-token: "ghp_your_token_here"
# VNC Password (optional, for secure VNC access)
vnc-password: "your_vnc_password"
-127
View File
@@ -1,127 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: antigravity
labels:
app: antigravity
spec:
ports:
- port: 5800
name: vnc-web
protocol: TCP
clusterIP: None
selector:
app: antigravity
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: antigravity
spec:
serviceName: "antigravity"
replicas: 1
selector:
matchLabels:
app: antigravity
template:
metadata:
labels:
app: antigravity
spec:
securityContext:
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
containers:
- name: antigravity
image: ghcr.io/cpfarhood/antigravity:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5800
name: vnc-web
protocol: TCP
volumeMounts:
- name: userhome
mountPath: /home
- name: workspace
mountPath: /workspace
env:
# User/Group IDs for the claude user
- name: USER_ID
value: "1000"
- name: GROUP_ID
value: "1000"
# VNC display settings
- name: DISPLAY_WIDTH
value: "1920"
- name: DISPLAY_HEIGHT
value: "1080"
- name: SECURE_CONNECTION
value: "1"
- name: VNC_PASSWORD
valueFrom:
secretKeyRef:
name: antigravity
key: vnc-password
optional: true
# GitHub configuration
- name: GITHUB_REPO
valueFrom:
configMapKeyRef:
name: antigravity
key: github-repo
optional: true
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: antigravity
key: github-token
optional: true
# Happy Coder configuration (optional)
- name: HAPPY_SERVER_URL
valueFrom:
configMapKeyRef:
name: antigravity
key: happy-server-url
optional: true
- name: HAPPY_WEBAPP_URL
valueFrom:
configMapKeyRef:
name: antigravity
key: happy-webapp-url
optional: true
- name: HAPPY_HOME_DIR
value: "/home/claude/.happy"
- name: HAPPY_EXPERIMENTAL
value: "true"
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "8Gi"
cpu: "4000m"
livenessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: workspace
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: userhome
spec:
accessModes: [ "ReadWriteMany" ]
storageClassName: "ceph-filesystem"
resources:
requests:
storage: 10Gi
+30
View File
@@ -0,0 +1,30 @@
# Antigravity Dev Container - Session Notes
## Key Architecture Facts
- Image: `ghcr.io/cpfarhood/devcontainer:latest` (repo name is `devcontainer`, not `antigravity`)
- Deployed via Helm chart (`chart/`), not kustomize anymore
- Service must NOT be headless (`clusterIP: None`) — Cilium gateway can't route to headless services
- `SECURE_CONNECTION=0` — TLS is terminated at the gateway, not the app
- Container user is `user` (UID 1000) — baseimage-gui runs startapp.sh as `app` user, sudo is not available
## Deployment Method
- **Primary**: Helm chart in `chart/` directory
- **Makefile targets**: `helm-deploy`, `helm-delete`, `helm-logs`, `helm-shell`, `helm-port-forward`
- **Old kustomize** (`k8s/` directory) has been removed — all deployments use Helm now
- Chart published as OCI artifact to GHCR, reconciled by Flux
## MCP Sidecars
- **Kubernetes MCP** (v0.0.57, port 8080): Only deployed when enabled AND `clusterAccess` != `none`
- **Flux MCP** (v0.41.1, port 8081): Only deployed when enabled AND `clusterAccess` != `none`
- **Home Assistant MCP** (v6.7.1, port 8087): Disabled by default, requires secrets:
- `homeassistant-url`: Base URL like `http://homeassistant.local:8123`
- `homeassistant-token`: Long-lived access token
- **Playwright MCP**: External service, not a sidecar
- Configure via `mcpSidecars.<name>.enabled` in values
- **Version Strategy**: All MCP images use pinned versions for stability (no `latest` tags)
## Common Gotchas
- `baseimage-gui` creates user dynamically — don't hardcode usernames in scripts, use numeric UID/GID
- `chown /home` fails (PVC root not owned by container) — only chown subdirectories
- `sudo` not available in startapp.sh — script already runs as correct user
- MCP sidecars need appropriate secrets and RBAC permissions to function
+72
View File
@@ -0,0 +1,72 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":gitSignOff"
],
"semanticCommits": "enabled",
"dependencyDashboard": true,
"suppressNotifications": [
"prEditedNotification"
],
"rebaseWhen": "conflicted",
"commitMessagePrefix": "chore(deps):",
"commitMessageAction": "update",
"commitMessageTopic": "{{depName}}",
"prConcurrentLimit": 5,
"prHourlyLimit": 2,
"schedule": [
"before 6am on monday"
],
"packageRules": [
{
"description": "GitHub Actions",
"matchManagers": [
"github-actions"
],
"groupName": "github-actions",
"additionalBranchPrefix": "github-actions-",
"semanticCommitScope": "github-actions",
"pinDigests": true
},
{
"description": "Docker base image",
"matchManagers": [
"dockerfile"
],
"groupName": "docker",
"additionalBranchPrefix": "docker-",
"semanticCommitScope": "docker"
},
{
"description": "Automerge patch updates",
"matchUpdateTypes": [
"patch"
],
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true
},
{
"description": "Automerge minor updates for stable packages",
"matchUpdateTypes": [
"minor"
],
"matchCurrentVersion": "!/^0/",
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true
},
{
"description": "Separate major updates - require manual review",
"matchUpdateTypes": [
"major"
],
"automerge": false,
"additionalBranchPrefix": "major-"
}
],
"ignorePaths": [
"**/node_modules/**"
]
}
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
# Start OpenSSH server when SSH=true.
# Runs as root during container initialisation (cont-init.d).
[ "${SSH:-false}" = "true" ] || exit 0
echo "=== SSH enabled: starting sshd ==="
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
mkdir -p "$HOME_DIR/.ssh"
chmod 700 "$HOME_DIR/.ssh"
printf '%s\n' "$SSH_AUTHORIZED_KEYS" > "$HOME_DIR/.ssh/authorized_keys"
chmod 600 "$HOME_DIR/.ssh/authorized_keys"
chown -R 1000:1000 "$HOME_DIR/.ssh"
echo "SSH authorized keys configured."
else
echo "WARNING: SSH_AUTHORIZED_KEYS not set — you will not be able to log in."
fi
# Start sshd in background (root required to bind :22 and fork sessions)
/usr/sbin/sshd -D &
echo "sshd started (PID $!)"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# Fix the app user (UID 1000) created by baseimage-gui at runtime.
# baseimage-gui sets shell=/sbin/nologin and home=/dev/null, which
# prevents VSCode from opening terminals.
usermod -s /bin/bash app
usermod -d /config/userdata app
+73 -30
View File
@@ -4,6 +4,62 @@ set -e
echo "=== Repository Initialization ==="
# Set up basic git configuration
echo "Configuring git user settings..."
# Use environment variables if provided, otherwise use defaults
GIT_USER_NAME="${GIT_USER_NAME:-DevContainer User}"
GIT_USER_EMAIL="${GIT_USER_EMAIL:-devcontainer@example.com}"
git config --global user.name "$GIT_USER_NAME"
git config --global user.email "$GIT_USER_EMAIL"
# Set up git credentials early if GITHUB_TOKEN is provided
# This ensures all git operations have proper authentication
if [ -n "$GITHUB_TOKEN" ]; then
echo "Setting up git credentials..."
# Configure git to use credential store globally
git config --global credential.helper store
# Create or update the credentials file
CREDENTIALS_FILE="/config/userdata/.git-credentials"
# Support multiple git hosting providers
# GitHub supports both oauth2 and token as username
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > "$CREDENTIALS_FILE"
echo "https://${GITHUB_TOKEN}:x-oauth-basic@github.com" >> "$CREDENTIALS_FILE"
echo "https://token:${GITHUB_TOKEN}@github.com" >> "$CREDENTIALS_FILE"
# GitLab format (if same token works)
if [ -n "$GITLAB_HOST" ]; then
echo "https://oauth2:${GITHUB_TOKEN}@${GITLAB_HOST}" >> "$CREDENTIALS_FILE"
fi
chmod 600 "$CREDENTIALS_FILE"
# Also create a symlink in the home directory if it doesn't exist
# This handles cases where git might look in different locations
if [ ! -f "$HOME/.git-credentials" ] && [ "$HOME" != "/config/userdata" ]; then
ln -sf "$CREDENTIALS_FILE" "$HOME/.git-credentials"
fi
echo "Git credentials configured"
else
# Even without a token, ensure git has a proper credential helper configured
# This prevents errors when credentials are added later
echo "No GITHUB_TOKEN provided, configuring basic git settings..."
git config --global credential.helper store
# Create an empty credentials file with proper permissions
CREDENTIALS_FILE="/config/userdata/.git-credentials"
touch "$CREDENTIALS_FILE"
chmod 600 "$CREDENTIALS_FILE"
# Create symlink if needed
if [ ! -f "$HOME/.git-credentials" ] && [ "$HOME" != "/config/userdata" ]; then
ln -sf "$CREDENTIALS_FILE" "$HOME/.git-credentials"
fi
fi
# Check if GITHUB_REPO is set
if [ -z "$GITHUB_REPO" ]; then
echo "GITHUB_REPO not set, skipping repository clone"
@@ -21,14 +77,6 @@ else
if [ -d "$WORKSPACE_DIR/.git" ]; then
echo "Repository already exists, pulling latest changes..."
cd "$WORKSPACE_DIR"
# 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/claude/.git-credentials
chmod 600 /home/claude/.git-credentials
fi
git pull || echo "Pull failed, continuing anyway..."
else
echo "Cloning repository..."
@@ -39,38 +87,33 @@ else
# Replace https://github.com/ with https://oauth2:token@github.com/
CLONE_URL=$(echo "$GITHUB_REPO" | sed "s|https://github.com/|https://oauth2:${GITHUB_TOKEN}@github.com/|")
git clone "$CLONE_URL" "$WORKSPACE_DIR"
# Configure credentials for future use
git config --global credential.helper store
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > /home/claude/.git-credentials
chmod 600 /home/claude/.git-credentials
else
git clone "$GITHUB_REPO" "$WORKSPACE_DIR"
fi
fi
fi
# Set ownership
chown -R claude:claude "$WORKSPACE_DIR"
chown -R claude:claude /home/claude
# Set ownership using numeric IDs (username may not exist yet in baseimage-gui)
RUN_UID="${USER_ID:-1000}"
RUN_GID="${GROUP_ID:-1000}"
chown -R "$RUN_UID:$RUN_GID" "$WORKSPACE_DIR"
# Start Happy Coder in background as claude user
# Ensure home directory exists on the PVC (may be absent on a fresh volume)
mkdir -p "$HOME"
chown "$RUN_UID:$RUN_GID" "$HOME"
# Start Happy Coder daemon. startapp.sh already runs as the app user (UID 1000),
# so no sudo needed — Happy/Claude Code will find credentials in the correct home dir.
echo "Starting Happy Coder..."
# Remove stale lock file. HAPPY_HOME_DIR lives on the home PVC so it survives
# pod restarts — without this cleanup the daemon refuses to start after a crash.
rm -f "${HAPPY_HOME_DIR:-$HOME/.happy}/daemon.state.json.lock"
cd "$WORKSPACE_DIR"
happy daemon start || echo "Happy Coder daemon failed to start, continuing anyway..."
# Create Happy Coder log file
HAPPY_LOG="/tmp/happy-coder.log"
touch "$HAPPY_LOG"
chown claude:claude "$HAPPY_LOG"
# Start Happy Coder as claude user
sudo -u claude bash -c "cd '$WORKSPACE_DIR' && happy-coder > '$HAPPY_LOG' 2>&1 &"
# Save PID for monitoring
echo $! > /tmp/happy-coder.pid
echo "Happy Coder started (PID: $(cat /tmp/happy-coder.pid))"
echo "Logs available at: $HAPPY_LOG"
echo "Happy Coder daemon started"
# Export workspace directory for startapp.sh
echo "$WORKSPACE_DIR" > /tmp/workspace-dir
+23 -4
View File
@@ -14,8 +14,27 @@ else
WORKSPACE_DIR="/workspace/default"
fi
echo "Opening Antigravity in: $WORKSPACE_DIR"
IDE="${IDE:-vscode}"
echo "IDE mode: $IDE"
echo "Workspace: $WORKSPACE_DIR"
# Start Antigravity (VSCode) in the workspace directory as claude user
# The baseimage-gui will handle the GUI display
exec sudo -u claude code --new-window --wait "$WORKSPACE_DIR"
case "$IDE" in
antigravity)
echo "Opening Google Antigravity in: $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."
exec sleep infinity
;;
*)
echo "Opening VSCode in: $WORKSPACE_DIR"
exec code --new-window --wait "$WORKSPACE_DIR"
;;
esac
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Test script to verify git credentials configuration
set -e
echo "=== Git Credentials Test ==="
# Check git configuration
echo "1. Git user configuration:"
git config --global user.name || echo " ❌ user.name not set"
git config --global user.email || echo " ❌ user.email not set"
echo ""
echo "2. Git credential helper:"
git config --global credential.helper || echo " ❌ credential.helper not set"
echo ""
echo "3. Credentials file locations:"
CREDENTIALS_FILE="/config/userdata/.git-credentials"
if [ -f "$CREDENTIALS_FILE" ]; then
echo "$CREDENTIALS_FILE exists"
echo " Permissions: $(stat -c %a $CREDENTIALS_FILE)"
echo " Lines in file: $(wc -l < $CREDENTIALS_FILE)"
else
echo "$CREDENTIALS_FILE does not exist"
fi
if [ -f "$HOME/.git-credentials" ]; then
if [ -L "$HOME/.git-credentials" ]; then
echo "$HOME/.git-credentials is a symlink to $(readlink -f $HOME/.git-credentials)"
else
echo "$HOME/.git-credentials exists (not a symlink)"
fi
else
echo "$HOME/.git-credentials does not exist"
fi
echo ""
echo "4. Environment check:"
echo " HOME=$HOME"
echo " GITHUB_TOKEN=${GITHUB_TOKEN:+[SET]}"
echo " GIT_USER_NAME=${GIT_USER_NAME:-[NOT SET]}"
echo " GIT_USER_EMAIL=${GIT_USER_EMAIL:-[NOT SET]}"
echo ""
echo "=== Test Complete ==="