From 5565354127caf5dcf1dc5aada04a585dd9e68ff9 Mon Sep 17 00:00:00 2001 From: DevContainer User Date: Wed, 25 Feb 2026 13:12:46 +0000 Subject: [PATCH] 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 Co-Authored-By: Happy --- chart/Chart.yaml | 11 ++- chart/templates/deployment.yaml | 2 + chart/templates/dynamic-ingress.yaml | 68 +++++++++++++++ chart/templates/knative-service.yaml | 111 ++++++++++++++++++++++++ chart/templates/pvc.yaml | 2 + chart/templates/rbac.yaml | 2 + chart/templates/routing-proxy.yaml | 66 +++++++++++++++ chart/templates/service.yaml | 2 + chart/values-dynamic.yaml | 122 +++++++++++++++++++++++++++ chart/values.yaml | 68 ++++++++++++++- 10 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 chart/templates/dynamic-ingress.yaml create mode 100644 chart/templates/knative-service.yaml create mode 100644 chart/templates/routing-proxy.yaml create mode 100644 chart/values-dynamic.yaml diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 3add4a5..2433abd 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -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 diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index c2a3f69..72e148d 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -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 }} diff --git a/chart/templates/dynamic-ingress.yaml b/chart/templates/dynamic-ingress.yaml new file mode 100644 index 0000000..b995d9f --- /dev/null +++ b/chart/templates/dynamic-ingress.yaml @@ -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 }} \ No newline at end of file diff --git a/chart/templates/knative-service.yaml b/chart/templates/knative-service.yaml new file mode 100644 index 0000000..79982c6 --- /dev/null +++ b/chart/templates/knative-service.yaml @@ -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 }} \ No newline at end of file diff --git a/chart/templates/pvc.yaml b/chart/templates/pvc.yaml index b79659a..49aea39 100644 --- a/chart/templates/pvc.yaml +++ b/chart/templates/pvc.yaml @@ -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 }} diff --git a/chart/templates/rbac.yaml b/chart/templates/rbac.yaml index 2e792bf..baf7e44 100644 --- a/chart/templates/rbac.yaml +++ b/chart/templates/rbac.yaml @@ -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 }} diff --git a/chart/templates/routing-proxy.yaml b/chart/templates/routing-proxy.yaml new file mode 100644 index 0000000..42bee23 --- /dev/null +++ b/chart/templates/routing-proxy.yaml @@ -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 }} \ No newline at end of file diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml index 42b3d2d..04ee2a7 100644 --- a/chart/templates/service.yaml +++ b/chart/templates/service.yaml @@ -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 }} diff --git a/chart/values-dynamic.yaml b/chart/values-dynamic.yaml new file mode 100644 index 0000000..9898e1d --- /dev/null +++ b/chart/values-dynamic.yaml @@ -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 \ No newline at end of file diff --git a/chart/values.yaml b/chart/values.yaml index 9412a86..2231c15 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -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 # =============================================================================