feat: integrate dynamic mode into Helm chart v2.0.0-dev

Implements unified Helm chart supporting both deployment modes:
- persistent: Traditional PVC-based deployment (v1.x behavior)
- dynamic: Serverless Knative with auto-scaling and dynamic routing

## Chart Changes
- Chart.yaml: Bump to v2.0.0-dev with deployment mode support
- values.yaml: Add deploymentMode field and dynamic configuration
- All templates: Conditional rendering based on deploymentMode

## Dynamic Mode Templates
- knative-service.yaml: Auto-scaling dev containers with repo routing
- routing-proxy.yaml: GitHub repo extraction service
- dynamic-ingress.yaml: Ingress with Authentik auth support

## Usage Examples
```bash
# Traditional persistent mode (default)
helm install mydev ./chart --set name=mydev --set githubRepo=...

# Dynamic serverless mode
helm install mydev ./chart -f values-dynamic.yaml \
  --set name=mydev --set dynamic.ingress.host=devcontainer.example.com

# Development builds
helm install mydev ./chart --set deploymentMode=dynamic \
  --set image.tag=2.0.0-dev --set dynamic.ingress.host=...
```

All existing persistent deployments remain compatible (deploymentMode defaults to "persistent").

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:
DevContainer User
2026-02-25 13:12:46 +00:00
parent b69cd80cae
commit 5565354127
10 changed files with 451 additions and 3 deletions
+9 -2
View File
@@ -1,6 +1,13 @@
apiVersion: v2
name: devcontainer
description: Dev Container with AI coding agents and MCP sidecars
description: Dev Container with AI coding agents and MCP sidecars - supports persistent and dynamic deployment modes
type: application
version: 1.0.2
version: 2.0.0-dev
appVersion: "latest"
keywords:
- development
- devcontainer
- vscode
- ai
- knative
- serverless
+2
View File
@@ -1,3 +1,4 @@
{{- if eq .Values.deploymentMode "persistent" }}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -288,3 +289,4 @@ spec:
- name: userhome
persistentVolumeClaim:
claimName: {{ include "devcontainer.pvcName" . }}
{{- end }}
+68
View File
@@ -0,0 +1,68 @@
{{- if and (eq .Values.deploymentMode "dynamic") .Values.dynamic.ingress.enabled .Values.dynamic.ingress.host }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "devcontainer.fullname" . }}-dynamic
labels:
{{- include "devcontainer.labels" . | nindent 4 }}
app.kubernetes.io/component: dynamic-ingress
annotations:
{{- if .Values.dynamic.ingress.className }}
kubernetes.io/ingress.class: {{ .Values.dynamic.ingress.className }}
{{- end }}
# SSL configuration
{{- if .Values.dynamic.ingress.tls.enabled }}
cert-manager.io/cluster-issuer: {{ .Values.dynamic.ingress.tls.issuer | quote }}
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
{{- end }}
# Authentik forward auth (if enabled)
{{- if .Values.dynamic.ingress.authentik.enabled }}
nginx.ingress.kubernetes.io/auth-url: {{ .Values.dynamic.ingress.authentik.authUrl | quote }}
nginx.ingress.kubernetes.io/auth-signin: {{ .Values.dynamic.ingress.authentik.signIn | quote }}
nginx.ingress.kubernetes.io/auth-response-headers: "X-Authentik-Username,X-Authentik-Groups,X-Authentik-Email,X-Authentik-Name"
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Host $http_host;
{{- end }}
# WebSocket support for VNC connections
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# Large file upload support (for file manager)
nginx.ingress.kubernetes.io/client-max-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
# Custom server snippet for GitHub repo logging
nginx.ingress.kubernetes.io/server-snippet: |
location ~ ^/github/([^/]+/[^/]+) {
# Log the GitHub repo being accessed
access_log /var/log/nginx/devcontainer-access.log combined;
# Set additional headers for audit/monitoring
proxy_set_header X-GitHub-Repo-Requested https://github.com/$1;
proxy_set_header X-Request-Timestamp $time_iso8601;
proxy_set_header X-Client-IP $remote_addr;
}
spec:
{{- if .Values.dynamic.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.dynamic.ingress.host }}
secretName: {{ .Values.dynamic.ingress.tls.secretName | default (printf "%s-tls" (include "devcontainer.fullname" .)) }}
{{- end }}
rules:
- host: {{ .Values.dynamic.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "devcontainer.fullname" . }}-routing-proxy
port:
number: 80
{{- end }}
+111
View File
@@ -0,0 +1,111 @@
{{- if eq .Values.deploymentMode "dynamic" }}
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: {{ include "devcontainer.fullname" . }}
labels:
{{- include "devcontainer.labels" . | nindent 4 }}
annotations:
# Knative scaling annotations
autoscaling.knative.dev/minScale: {{ .Values.dynamic.knative.minScale | quote }}
autoscaling.knative.dev/maxScale: {{ .Values.dynamic.knative.maxScale | quote }}
autoscaling.knative.dev/target: {{ .Values.dynamic.knative.target | quote }}
autoscaling.knative.dev/scale-to-zero-grace-period: {{ .Values.dynamic.knative.scaleToZeroGracePeriod | quote }}
spec:
template:
metadata:
labels:
{{- include "devcontainer.labels" . | nindent 8 }}
annotations:
# Container configuration
autoscaling.knative.dev/targetPort: "5800"
serving.knative.dev/timeoutSeconds: {{ .Values.dynamic.knative.timeoutSeconds | quote }}
# Scaling configuration
autoscaling.knative.dev/class: "kpa.autoscaling.knative.dev"
autoscaling.knative.dev/metric: "concurrency"
spec:
# Container startup timeout
timeoutSeconds: {{ .Values.dynamic.knative.timeoutSeconds }}
containers:
- name: devcontainer
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 5800
name: vnc-web
env:
# Dynamic mode flags
- name: SERVERLESS_MODE
value: "true"
- name: DYNAMIC_GITHUB_ROUTING
value: "true"
- name: DEPLOYMENT_MODE
value: "dynamic"
# Standard configuration
- name: IDE
value: {{ .Values.ide.type | default "vscode" | quote }}
- name: USER_ID
value: {{ .Values.user.id | quote }}
- name: GROUP_ID
value: {{ .Values.user.groupId | quote }}
- name: DISPLAY_WIDTH
value: {{ .Values.display.width | quote }}
- name: DISPLAY_HEIGHT
value: {{ .Values.display.height | quote }}
- name: SECURE_CONNECTION
value: {{ .Values.display.secureConnection | quote }}
# File manager (always enabled in dynamic mode for easy file transfer)
- name: WEB_FILE_MANAGER
value: "1"
- name: WEB_FILE_MANAGER_ALLOWED_PATHS
value: "/workspace,/tmp" # No persistent /config in dynamic mode
# Happy Coder (ephemeral in dynamic mode)
- name: HAPPY_HOME_DIR
value: "/tmp/.happy"
- name: HAPPY_EXPERIMENTAL
value: {{ .Values.happy.experimental | quote }}
{{- if .Values.happy.serverUrl }}
- name: HAPPY_SERVER_URL
value: {{ .Values.happy.serverUrl | quote }}
{{- end }}
{{- if .Values.happy.webappUrl }}
- name: HAPPY_WEBAPP_URL
value: {{ .Values.happy.webappUrl | quote }}
{{- end }}
# Secret environment variables
envFrom:
- secretRef:
name: {{ include "devcontainer.envSecretName" . }}
optional: true
resources:
{{- toYaml .Values.dynamic.knative.resources | nindent 10 }}
volumeMounts:
- name: tmp-home
mountPath: /config
- name: shm
mountPath: /dev/shm
# Health probes (adjusted for dynamic mode startup time)
readinessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /
port: 5800
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
volumes:
- name: tmp-home
emptyDir: {} # Ephemeral - each instance gets fresh home
- name: shm
emptyDir:
medium: Memory
sizeLimit: {{ .Values.shm.sizeLimit }}
{{- end }}
+2
View File
@@ -1,3 +1,4 @@
{{- if eq .Values.deploymentMode "persistent" }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -15,3 +16,4 @@ spec:
resources:
requests:
storage: {{ .Values.storage.size }}
{{- end }}
+2
View File
@@ -1,3 +1,4 @@
{{- if eq .Values.deploymentMode "persistent" }}
{{- $access := .Values.clusterAccess | default "none" }}
{{- $name := include "devcontainer.fullname" . }}
{{- $ns := .Release.Namespace }}
@@ -95,3 +96,4 @@ roleRef:
{{- end }}
{{- end }}
{{- end }}
+66
View File
@@ -0,0 +1,66 @@
{{- if and (eq .Values.deploymentMode "dynamic") .Values.dynamic.routingProxy.enabled }}
---
# Routing proxy deployment for dynamic GitHub repo extraction
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "devcontainer.fullname" . }}-routing-proxy
labels:
{{- include "devcontainer.labels" . | nindent 4 }}
app.kubernetes.io/component: routing-proxy
spec:
replicas: {{ .Values.dynamic.routingProxy.replicas }}
selector:
matchLabels:
{{- include "devcontainer.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: routing-proxy
template:
metadata:
labels:
{{- include "devcontainer.labels" . | nindent 8 }}
app.kubernetes.io/component: routing-proxy
spec:
containers:
- name: routing-proxy
image: "{{ .Values.dynamic.routingProxy.image.repository }}:{{ .Values.dynamic.routingProxy.image.tag }}"
imagePullPolicy: {{ .Values.dynamic.routingProxy.image.pullPolicy }}
ports:
- containerPort: 8080
name: http
env:
- name: DEVCONTAINER_SERVICE_URL
value: "{{ include "devcontainer.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local"
resources:
{{- toYaml .Values.dynamic.routingProxy.resources | nindent 10 }}
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
---
# Service for routing proxy
apiVersion: v1
kind: Service
metadata:
name: {{ include "devcontainer.fullname" . }}-routing-proxy
labels:
{{- include "devcontainer.labels" . | nindent 4 }}
app.kubernetes.io/component: routing-proxy
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
name: http
selector:
{{- include "devcontainer.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: routing-proxy
{{- end }}
+2
View File
@@ -1,3 +1,4 @@
{{- if eq .Values.deploymentMode "persistent" }}
apiVersion: v1
kind: Service
metadata:
@@ -20,3 +21,4 @@ spec:
{{- end }}
selector:
{{- include "devcontainer.labels" . | nindent 4 }}
{{- end }}
+122
View File
@@ -0,0 +1,122 @@
# Example values for dynamic (serverless) deployment mode
# Copy this file and customize for your environment:
# cp values-dynamic.yaml my-dynamic-values.yaml
# =============================================================================
# BASIC CONFIGURATION
# =============================================================================
name: "mydev" # REQUIRED: Instance name
deploymentMode: dynamic # Use serverless/dynamic mode
# Container images
image:
repository: ghcr.io/cpfarhood/devcontainer
tag: "2.0.0-dev"
pullPolicy: Always
# githubRepo is ignored in dynamic mode - repos are specified via URL routing
# =============================================================================
# ACCESS & INTERFACE
# =============================================================================
ide:
type: vscode # vscode | antigravity | none
# SSH not supported in dynamic mode (ephemeral containers)
ssh:
enabled: false
# File manager automatically enabled in dynamic mode for file transfer
fileManager:
enabled: true
# =============================================================================
# DYNAMIC MODE CONFIGURATION
# =============================================================================
dynamic:
# Knative Service auto-scaling configuration
knative:
minScale: 0 # Scale to zero when not in use
maxScale: 10 # Maximum concurrent instances
target: 1 # Requests per instance (1 = perfect isolation)
scaleToZeroGracePeriod: "5m" # Keep instances warm for 5 minutes
timeoutSeconds: 600 # 10 minutes for repo cloning + IDE startup
# Resources per container instance
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2000m"
# Routing proxy (extracts GitHub repo from URL path)
routingProxy:
enabled: true
replicas: 2 # High availability
image:
repository: ghcr.io/cpfarhood/devcontainer-routing-proxy
tag: latest
pullPolicy: Always
# Ingress configuration
ingress:
enabled: true
className: nginx
host: "devcontainer.example.com" # REQUIRED: Set your domain
# SSL with cert-manager
tls:
enabled: true
# secretName: "" # Auto-generated if empty
issuer: "letsencrypt-prod"
# Authentik forward auth (configure after Authentik setup)
authentik:
enabled: false # Set to true when ready
authUrl: "http://authentik.authentik.svc.cluster.local/outpost.goauthentik.io/auth/nginx"
signIn: "https://auth.example.com/outpost.goauthentik.io/start?rd=$escaped_request_uri"
# =============================================================================
# STANDARD CONFIGURATION (applies to both modes)
# =============================================================================
# Display settings
display:
width: "1920"
height: "1080"
secureConnection: "0"
# User configuration
user:
id: "1000"
groupId: "1000"
# Resource allocation (container shared memory)
shm:
sizeLimit: 2Gi
# Happy Coder (ephemeral in dynamic mode)
happy:
serverUrl: ""
webappUrl: ""
homeDir: "/tmp/.happy" # Ephemeral location in dynamic mode
experimental: "true"
# MCP sidecars are not supported in dynamic mode (Knative limitation)
mcp:
sidecars:
kubernetes:
enabled: false
flux:
enabled: false
homeassistant:
enabled: false
pgtuner:
enabled: false
playwright:
enabled: false
+67 -1
View File
@@ -5,13 +5,18 @@
# Instance name — used to generate resource names (devcontainer-{name}, userhome-{name})
name: ""
# Deployment mode controls the infrastructure pattern
# - persistent: Traditional model with PVC storage, single long-lived deployment
# - dynamic: Serverless model with Knative, auto-scaling from 0, dynamic GitHub routing
deploymentMode: persistent # persistent | dynamic
# Container image configuration
image:
repository: ghcr.io/cpfarhood/devcontainer
tag: latest
pullPolicy: Always
# GitHub repository to clone into /workspace
# GitHub repository to clone into /workspace (ignored in dynamic mode - uses URL routing)
githubRepo: ""
# =============================================================================
@@ -180,6 +185,67 @@ autoDetect:
# Override specific values above to customize
resourceProfile: auto # auto | small | medium | large | xlarge
# =============================================================================
# DYNAMIC MODE CONFIGURATION (deploymentMode: dynamic)
# =============================================================================
# Dynamic mode uses Knative Services and routing proxy for serverless operation
dynamic:
# Knative Service configuration
knative:
# Scaling configuration
minScale: 0 # Scale to zero when not in use
maxScale: 10 # Maximum number of concurrent instances
target: 1 # Requests per instance (isolation = 1 request per pod)
scaleToZeroGracePeriod: "5m" # Keep instances warm for 5 minutes
# Container startup timeout (repo cloning + IDE startup)
timeoutSeconds: 600 # 10 minutes
# Resource configuration (per instance)
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2000m"
# Routing proxy configuration (extracts GitHub repo from URL)
routingProxy:
enabled: true
replicas: 2 # High availability
image:
repository: ghcr.io/cpfarhood/devcontainer-routing-proxy
tag: latest
pullPolicy: Always
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
# Ingress configuration for dynamic mode
ingress:
enabled: true
className: nginx
host: "" # Set this to your domain (e.g., devcontainer.farh.net)
# TLS configuration
tls:
enabled: true
secretName: "" # Auto-generated if empty
issuer: "letsencrypt-prod" # cert-manager ClusterIssuer
# Authentik forward auth configuration
authentik:
enabled: false # Set to true when Authentik is configured
authUrl: "http://authentik.authentik.svc.cluster.local/outpost.goauthentik.io/auth/nginx"
signIn: "https://auth.example.com/outpost.goauthentik.io/start?rd=$escaped_request_uri"
# =============================================================================
# ADVANCED CONFIGURATION
# =============================================================================