feat: serverless 2.0.0 architecture with Authentik auth proxy
Implements a complete serverless development container platform:
## Architecture
- Authentik forward auth for authentication/authorization
- NGINX routing proxy extracts GitHub repo from URL path
- Knative Service auto-scales dev container instances from 0
- Dynamic GitHub repo routing via /github/{owner}/{repo}
## Components
- routing-proxy: NGINX-based service for repo extraction and forwarding
- deployment.yaml: Complete K8s manifests (proxy, Knative, ingress, secrets)
- authentik-config.yaml: Authentik application and provider configs
- serverless scripts: Dynamic repo initialization and startup handling
- Comprehensive documentation and Makefile for ops
## Key Features
- Scale to zero when not in use (cost-effective)
- Per-request isolation (each repo gets own container)
- Built-in file manager for upload/download
- Support for private repos via GitHub tokens
- User attribution via Authentik headers
- WebSocket support for VNC connections
Example usage: https://devcontainer.farh.net/github/microsoft/vscode
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>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Dynamic GitHub repository initialization for serverless mode
|
||||
# This script extracts the GitHub repo from HTTP headers set by the routing proxy
|
||||
|
||||
set -e
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DYNAMIC-INIT: $*" >&2
|
||||
}
|
||||
|
||||
log "Starting dynamic repository initialization..."
|
||||
|
||||
# In serverless mode, we expect the routing proxy to have set these environment variables
|
||||
# from the HTTP headers. If running standalone, fallback to GITHUB_REPO env var.
|
||||
|
||||
if [[ "$SERVERLESS_MODE" == "true" ]]; then
|
||||
log "Serverless mode detected"
|
||||
|
||||
# The routing proxy should have set these via HTTP headers -> env vars
|
||||
# Check if we have the GitHub repo from the X-GitHub-Repo header
|
||||
if [[ -n "$HTTP_X_GITHUB_REPO" ]]; then
|
||||
GITHUB_REPO="$HTTP_X_GITHUB_REPO"
|
||||
log "Using GitHub repo from header: $GITHUB_REPO"
|
||||
elif [[ -n "$X_GITHUB_REPO" ]]; then
|
||||
GITHUB_REPO="$X_GITHUB_REPO"
|
||||
log "Using GitHub repo from X-GitHub-Repo: $GITHUB_REPO"
|
||||
else
|
||||
# Try to extract from a file written by an init container or sidecar
|
||||
if [[ -f "/tmp/github-repo" ]]; then
|
||||
GITHUB_REPO=$(cat /tmp/github-repo)
|
||||
log "Using GitHub repo from file: $GITHUB_REPO"
|
||||
else
|
||||
log "ERROR: No GitHub repository specified in serverless mode"
|
||||
log "Expected HTTP_X_GITHUB_REPO or X_GITHUB_REPO header from routing proxy"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract user info if available
|
||||
if [[ -n "$HTTP_X_AUTHENTIK_USERNAME" ]]; then
|
||||
export GIT_USER_NAME="${HTTP_X_AUTHENTIK_NAME:-$HTTP_X_AUTHENTIK_USERNAME}"
|
||||
export GIT_USER_EMAIL="${HTTP_X_AUTHENTIK_EMAIL:-${HTTP_X_AUTHENTIK_USERNAME}@devcontainer.local}"
|
||||
log "Using Authentik user: $GIT_USER_NAME <$GIT_USER_EMAIL>"
|
||||
fi
|
||||
else
|
||||
log "Traditional mode - using GITHUB_REPO environment variable"
|
||||
if [[ -z "$GITHUB_REPO" ]]; then
|
||||
log "ERROR: GITHUB_REPO environment variable is required"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate the GitHub repo URL
|
||||
if [[ ! "$GITHUB_REPO" =~ ^https://github\.com/[^/]+/[^/]+/?$ ]]; then
|
||||
log "ERROR: Invalid GitHub repository URL: $GITHUB_REPO"
|
||||
log "Expected format: https://github.com/owner/repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract owner and repo name for workspace directory
|
||||
REPO_OWNER=$(echo "$GITHUB_REPO" | sed 's|https://github.com/\([^/]*\)/.*|\1|')
|
||||
REPO_NAME=$(echo "$GITHUB_REPO" | sed 's|https://github.com/[^/]*/\([^/]*\)/?|\1|')
|
||||
WORKSPACE_DIR="/workspace/${REPO_OWNER}-${REPO_NAME}"
|
||||
|
||||
log "Repository: $GITHUB_REPO"
|
||||
log "Owner: $REPO_OWNER"
|
||||
log "Name: $REPO_NAME"
|
||||
log "Workspace: $WORKSPACE_DIR"
|
||||
|
||||
# Configure git user (use defaults if not set via Authentik)
|
||||
GIT_USER_NAME="${GIT_USER_NAME:-DevContainer User}"
|
||||
GIT_USER_EMAIL="${GIT_USER_EMAIL:-devcontainer@example.com}"
|
||||
|
||||
log "Configuring git user: $GIT_USER_NAME <$GIT_USER_EMAIL>"
|
||||
git config --global user.name "$GIT_USER_NAME"
|
||||
git config --global user.email "$GIT_USER_EMAIL"
|
||||
|
||||
# Configure git credentials if GitHub token is available
|
||||
if [[ -n "$GITHUB_TOKEN" ]]; then
|
||||
log "Configuring GitHub credentials..."
|
||||
git config --global credential.helper store
|
||||
echo "https://oauth2:${GITHUB_TOKEN}@github.com" > ~/.git-credentials
|
||||
chmod 600 ~/.git-credentials
|
||||
else
|
||||
log "No GitHub token provided - using public access only"
|
||||
fi
|
||||
|
||||
# Create workspace directory
|
||||
mkdir -p "$(dirname "$WORKSPACE_DIR")"
|
||||
cd "$(dirname "$WORKSPACE_DIR")"
|
||||
|
||||
# Clone the repository
|
||||
if [[ -d "$WORKSPACE_DIR" ]]; then
|
||||
log "Repository directory exists, pulling latest changes..."
|
||||
cd "$WORKSPACE_DIR"
|
||||
git pull --ff-only || {
|
||||
log "WARNING: Could not fast-forward, repository may have diverged"
|
||||
log "Continuing with existing state..."
|
||||
}
|
||||
else
|
||||
log "Cloning repository..."
|
||||
git clone "$GITHUB_REPO" "$WORKSPACE_DIR" || {
|
||||
log "ERROR: Failed to clone repository $GITHUB_REPO"
|
||||
log "This may be a private repository or the URL may be incorrect"
|
||||
exit 1
|
||||
}
|
||||
cd "$WORKSPACE_DIR"
|
||||
fi
|
||||
|
||||
# Set the workspace directory for the IDE
|
||||
export WORKSPACE_DIR
|
||||
|
||||
log "Repository initialization complete!"
|
||||
log "Workspace directory: $WORKSPACE_DIR"
|
||||
|
||||
# Change to the workspace directory so the IDE opens in the right place
|
||||
cd "$WORKSPACE_DIR"
|
||||
|
||||
# Export variables for the parent script
|
||||
export GITHUB_REPO
|
||||
export WORKSPACE_DIR
|
||||
export REPO_OWNER
|
||||
export REPO_NAME
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Serverless-aware startup script for devcontainer
|
||||
# This replaces the standard /startapp.sh when in serverless mode
|
||||
|
||||
set -e
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SERVERLESS-START: $*" >&2
|
||||
}
|
||||
|
||||
log "Starting serverless devcontainer..."
|
||||
log "Mode: ${SERVERLESS_MODE:-traditional}"
|
||||
log "IDE: ${IDE:-vscode}"
|
||||
|
||||
# Wait for HTTP headers to be available (in case of init container pattern)
|
||||
# In Knative, the headers should be available immediately as env vars
|
||||
sleep 2
|
||||
|
||||
# Check if we're in serverless mode with dynamic routing
|
||||
if [[ "$SERVERLESS_MODE" == "true" && "$DYNAMIC_GITHUB_ROUTING" == "true" ]]; then
|
||||
log "Dynamic GitHub routing enabled"
|
||||
|
||||
# In Knative, HTTP headers become environment variables with HTTP_ prefix
|
||||
# But we also check for the unprefixed versions set by proxies
|
||||
AVAILABLE_VARS=$(env | grep -E "(GITHUB|AUTHENTIK|X_)" | sort)
|
||||
if [[ -n "$AVAILABLE_VARS" ]]; then
|
||||
log "Available routing variables:"
|
||||
echo "$AVAILABLE_VARS" | while read -r var; do
|
||||
log " $var"
|
||||
done
|
||||
else
|
||||
log "No routing variables found, checking for alternatives..."
|
||||
# Check if there's a file with the repo info
|
||||
if [[ -f "/tmp/github-repo" ]]; then
|
||||
export GITHUB_REPO=$(cat /tmp/github-repo)
|
||||
log "Found repo file: $GITHUB_REPO"
|
||||
else
|
||||
log "ERROR: No GitHub repository information available"
|
||||
log "Expected routing headers or /tmp/github-repo file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use the dynamic initialization script
|
||||
source /usr/local/bin/dynamic-init-repo
|
||||
else
|
||||
log "Using standard initialization..."
|
||||
# Use the standard initialization
|
||||
source /usr/local/bin/init-repo
|
||||
fi
|
||||
|
||||
# At this point, WORKSPACE_DIR should be set by the init script
|
||||
WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}"
|
||||
log "Working directory: $WORKSPACE_DIR"
|
||||
|
||||
# Ensure we're in the workspace directory
|
||||
cd "$WORKSPACE_DIR"
|
||||
|
||||
# Launch the appropriate IDE based on the IDE environment variable
|
||||
case "${IDE:-vscode}" in
|
||||
"vscode")
|
||||
log "Starting VSCode..."
|
||||
exec code --new-window --wait "$WORKSPACE_DIR"
|
||||
;;
|
||||
"antigravity")
|
||||
log "Starting Antigravity..."
|
||||
exec antigravity \
|
||||
--no-sandbox \
|
||||
--user-data-dir ~/.config/antigravity \
|
||||
--disable-dev-shm-usage \
|
||||
--disable-gpu \
|
||||
--disable-features=VizDisplayCompositor \
|
||||
--new-window \
|
||||
"$WORKSPACE_DIR"
|
||||
;;
|
||||
"none")
|
||||
log "No IDE requested, keeping container alive..."
|
||||
exec sleep infinity
|
||||
;;
|
||||
*)
|
||||
log "ERROR: Unknown IDE type: $IDE"
|
||||
log "Valid options: vscode, antigravity, none"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user