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:
+9
-2
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user