MVP Helm chart (#2140)
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
actionlint / Lint GitHub Actions workflows (push) Failing after 1s
ci / Detect CI change scopes (push) Successful in 0s
nix-check / build (push) Failing after 1s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 1s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped

* introduce MVP Helm chart

* address review feedback and add config checksums

* implement zero-trust sidecar proxy and fix health probes

* address PR feedback on publicBaseUrl and sidecar

* pass ingress headers and consolidate proxy port

* fallback allowed origins for localhost when ingress is disabled to prevent CORS errors during port-forwarding

* enforce proxy security and auto-derive base URL

* hardcode service targetPort to proxy

* require explicit base url for multi-host ingress

* prevent silent CORS failures for LoadBalancer and NodePort and README updates

* Added  to the deployment pod annotations. This ensures that changes made to  during a helm upgrade will properly trigger a rolling update of the pods instead of serving stale proxy configurations

* dynamically derive scheme per host in allowed origins

* dynamically derive scheme for single-host publicBaseUrl

* add default apiToken placeholder and wrap ingress path guardrail

* add recent changes to readme

* explicitly disallow non-root ingress path prefixes

* update README to reflect path and origin guardrails

---------

Co-authored-by: shaarron <sharon.kroch@gmail.com>
This commit is contained in:
Sharon K 2026-05-24 17:37:25 +03:00 committed by GitHub
parent b22c7713db
commit 6244b67295
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 751 additions and 0 deletions

View file

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View file

@ -0,0 +1,6 @@
apiVersion: v2
name: open-design
description: A Helm chart for deploying Open Design on Kubernetes
type: application
version: 0.1.0
appVersion: "0.7.0"

View file

@ -0,0 +1,165 @@
<!--- app-name: Open Design -->
## Introduction
This chart bootstraps an [Open Design](https://github.com/nexu-io/open-design) deployment on a [Kubernetes](https://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
## Prerequisites
- Kubernetes 1.23+
- Helm 3.8.0+
- PV provisioner support in the underlying infrastructure (if persistence is enabled)
## Installing the Chart
To install the chart with the release name `my-release`:
```console
helm install my-release ./charts/open-design
```
These commands deploy an Open Design application on the Kubernetes cluster in the default configuration.
> **Tip**: List all releases using `helm list`
### Architecture and Configuration Notes
#### SQLite State & Concurrency Limitations
The current Open Design runtime stores state in local files and SQLite under `/app/.od`. Because SQLite does not support concurrent writes from multiple network replicas, **this chart is strictly limited to 1 replica**.
Horizontal Pod Autoscaling (HPA) is disabled by default. Do not enable HPA or scale the deployment beyond `replicas: 1` unless you have modified the application to externalize the state to a standalone database.
#### Server-Sent Events (SSE) and Ingress
Open Design relies on Server-Sent Events (SSE) for real-time streaming. If you enable the Ingress resource, it is critical to disable reverse-proxy buffering. If you are using the NGINX Ingress Controller, this chart automatically applies the required annotations by default:
```yaml
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
```
**Path Constraints**: Non-root ingress path prefixes (sub-paths) are explicitly **unsupported** by the proxy routing stack. Ingress paths must be configured as `/`.
#### Authentication Proxy
An authentication proxy (NGINX) is introduced to front the application. This proxy runs as a mandatory sidecar container alongside the main application. The Kubernetes Service routes traffic to the proxy, which handles authentication for the API and health checks, proxying valid requests to the application.
#### Security Context
This chart adheres to strict security defaults:
- Runs as non-root user `1001`.
- Drops all kernel capabilities (`ALL`).
- Enforces a `readOnlyRootFilesystem`.
- Prevents privilege escalation.
## Parameters
### Global & Image Parameters
| Name | Description | Value |
| ------------------ | ----------------------------------------- | ---------------------------- |
| `commonLabels` | Custom labels injected into all resources | `{app.kubernetes.io/environment: production}` |
| `image.repository` | Open Design image repository | `vanjayak/open-design` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `image.tag` | Image tag (overrides AppVersion) | `latest` |
### Application Configuration
| Name | Description | Value |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `config.nodeEnv` | Node.js environment (`production` or `development`) | `production` |
| `config.allowedOrigins`| CORS allowed origins. Mandatory if service.type is LoadBalancer or NodePort to prevent 403 render failures. | `""` |
| `config.publicBaseUrl` | Public base URL used by the application (derived dynamically if empty) | `""` |
| `config.nodeOptions` | V8 engine memory optimizations | `--max-old-space-size=192` |
| `config.webPort` | Web server listening port | `7456` |
| `config.bindHost` | Host to bind the web server to | `"127.0.0.1"` |
| `config.apiToken` | API authentication token (must be changed from default) | `"secure-default-token-change-me"` |
### Auth Proxy Parameters
| Name | Description | Value |
| -------------------------------------- | ------------------------------------------------- | ------------------------------------------------ |
| `authProxy.image` | NGINX proxy image | `nginxinc/nginx-unprivileged:1.25-alpine-slim` |
| `authProxy.port` | Proxy server port inside the container | `8080` |
| `authProxy.securityContext` | Security context for the proxy container | `{...}` |
### Health Check Parameters
| Name | Description | Value |
| ------------------------------------------- | -------------------------------------------------- | ------ |
| `livenessProbe.enabled` | Enable liveness probe | `true` |
| `livenessProbe.initialDelaySeconds` | Initial delay seconds for liveness probe | `20` |
| `livenessProbe.periodSeconds` | Period seconds for liveness probe | `30` |
| `livenessProbe.timeoutSeconds` | Timeout seconds for liveness probe | `5` |
| `livenessProbe.failureThreshold` | Failure threshold for liveness probe | `3` |
| `readinessProbe.enabled` | Enable readiness probe | `true` |
| `readinessProbe.initialDelaySeconds` | Initial delay seconds for readiness probe | `5` |
| `readinessProbe.periodSeconds` | Period seconds for readiness probe | `10` |
| `readinessProbe.timeoutSeconds` | Timeout seconds for readiness probe | `5` |
| `readinessProbe.failureThreshold` | Failure threshold for readiness probe | `3` |
### Network & Ingress Parameters
| Name | Description | Value |
| --------------------------------------- | ------------------------------------------------------------ | -------------------------- |
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `80` |
| `ingress.enabled` | Enable ingress record generation | `false` |
| `ingress.className` | Ingress class name | `nginx` |
| `ingress.annotations` | Additional custom annotations for Ingress (e.g., SSE fixes) | `{...}` |
| `ingress.hosts[0].host` | Hostname for the ingress record | `open-design.local` |
| `ingress.tls` | TLS configuration for ingress records | `[]` |
### Persistence Parameters
| Name | Description | Value |
| -------------------------- | ------------------------------------------------------------ | --------------- |
| `persistence.enabled` | Enable PVC for SQLite and file state | `true` |
| `persistence.storageClass` | Storage class (leave empty to use cluster default) | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` | PVC Storage Request | `10Gi` |
### Resources & Autoscaling Parameters
| Name | Description | Value |
| ----------------------------------- | --------------------------------------------------------------- | --------- |
| `replicaCount` | Number of application replicas | `1` |
| `resources.limits.cpu` | CPU limits for the container | `1000m` |
| `resources.limits.memory` | Memory limits for the container | `1024Mi` |
| `resources.requests.cpu` | CPU requests for the container | `200m` |
| `resources.requests.memory` | Memory requests for the container | `256Mi` |
| `hpa.enabled` | Enable Horizontal Pod Autoscaler (WARNING: Breaks SQLite) | `false` |
### Security & Scheduling Parameters
| Name | Description | Value |
| ----------------------------------------------------------------- | ----------------------------------------------- | ------------------ |
| `podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy | `Always` |
| `podSecurityContext.sysctls` | Set kernel settings using the sysctl interface | `[]` |
| `podSecurityContext.supplementalGroups` | Set filesystem extra groups | `[]` |
| `podSecurityContext.fsGroup` | Group ID for the persistent volume | `1001` |
| `containerSecurityContext.seLinuxOptions` | Set SELinux options in container | `{}` |
| `containerSecurityContext.runAsUser` | Run the application as this UID | `1001` |
| `containerSecurityContext.runAsGroup` | Run the application as this GID | `1001` |
| `containerSecurityContext.runAsNonRoot` | Set container's Security Context runAsNonRoot | `true` |
| `containerSecurityContext.privileged` | Set container's Security Context privileged | `false` |
| `containerSecurityContext.readOnlyRootFilesystem` | Enforce read-only root FS | `true` |
| `containerSecurityContext.allowPrivilegeEscalation` | Set container's Security Context allowPrivilegeEscalation | `false` |
| `containerSecurityContext.capabilities.drop` | List of capabilities to be dropped | `["ALL"]` |
| `containerSecurityContext.seccompProfile.type` | Set container's Security Context seccomp profile| `"RuntimeDefault"` |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Tolerations for pod assignment | `[]` |
| `affinity` | Affinity rules for pod assignment | `{}` |
| `initContainers` | Additional init containers to add to the pod | `[]` |
| `sidecars` | Additional sidecar containers to add to the pod | `[]` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,
```console
helm install my-release --set config.nodeEnv=development ./charts/open-design
```
The above command sets the Open Design node environment to `development`.
Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example,
```console
helm install my-release -f values.yaml ./charts/open-design
```
> **Tip**: You can use the default [values.yaml](values.yaml)

View file

@ -0,0 +1,52 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "open-design.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "open-design.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "open-design.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels injected into all resources.
*/}}
{{- define "open-design.labels" -}}
helm.sh/chart: {{ include "open-design.chart" . }}
{{ include "open-design.selectorLabels" . }}
{{- if .Values.image.tag }}
app.kubernetes.io/version: {{ .Values.image.tag | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.commonLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Selector labels used by Services and HPAs.
*/}}
{{- define "open-design.selectorLabels" -}}
app.kubernetes.io/name: {{ include "open-design.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View file

@ -0,0 +1,59 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "open-design.fullname" . }}
labels:
{{- include "open-design.labels" . | nindent 4 }}
data:
OD_BIND_HOST: {{ .Values.config.bindHost | quote }}
OD_PORT: {{ .Values.config.webPort | quote }}
OD_WEB_PORT: {{ .Values.config.webPort | quote }}
PROXY_PORT: {{ .Values.authProxy.port | quote }}
NODE_ENV: {{ .Values.config.nodeEnv | quote }}
NODE_OPTIONS: {{ .Values.config.nodeOptions | quote }}
{{- $publicBaseUrl := .Values.config.publicBaseUrl }}
{{- if and (not $publicBaseUrl) .Values.ingress.enabled .Values.ingress.hosts }}
{{- if eq (len .Values.ingress.hosts) 1 }}
{{- $singleHost := (index .Values.ingress.hosts 0).host }}
{{- $isSecure := false }}
{{- range $.Values.ingress.tls }}
{{- if has $singleHost .hosts }}
{{- $isSecure = true }}
{{- end }}
{{- end }}
{{- $scheme := ternary "https://" "http://" $isSecure }}
{{- $publicBaseUrl = printf "%s%s" $scheme $singleHost }}
{{- else if gt (len .Values.ingress.hosts) 1 }}
{{- fail "CRITICAL ERROR: Multiple ingress hosts configured. You must explicitly set config.publicBaseUrl to define the canonical origin for OAuth callbacks." }}
{{- end }}
{{- end }}
{{- if $publicBaseUrl }}
OD_PUBLIC_BASE_URL: {{ $publicBaseUrl | quote }}
{{- end }}
{{- $origins := .Values.config.allowedOrigins }}
{{- if and .Values.ingress.enabled (not $origins) }}
{{- $hostList := list }}
{{- range .Values.ingress.hosts }}
{{- $currentHost := .host }}
{{- $isSecure := false }}
{{- range $.Values.ingress.tls }}
{{- if has $currentHost .hosts }}
{{- $isSecure = true }}
{{- end }}
{{- end }}
{{- $scheme := ternary "https://" "http://" $isSecure }}
{{- $hostList = append $hostList (printf "%s%s" $scheme $currentHost) }}
{{- end }}
{{- $origins = join "," $hostList }}
{{- else if not $origins }}
{{- if or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") }}
{{- fail "CRITICAL ERROR: service.type is set to LoadBalancer or NodePort but config.allowedOrigins is empty. You must explicitly configure config.allowedOrigins when exposing the service directly without an Ingress." }}
{{- else }}
{{- /* Synthesize default local origins for port-forwarding when Ingress is disabled */ -}}
{{- $localProxy := printf "http://localhost:%v" .Values.authProxy.port }}
{{- $localSvc := printf "http://localhost:%v" .Values.service.port }}
{{- $origins = printf "%s,%s" $localProxy $localSvc }}
{{- end }}
{{- end }}
OD_ALLOWED_ORIGINS: {{ $origins | quote }}

View file

@ -0,0 +1,162 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "open-design.fullname" . }}
labels:
{{- include "open-design.labels" . | nindent 4 }}
spec:
{{- if not .Values.hpa.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: Recreate
selector:
matchLabels:
{{- include "open-design.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/proxy-config: {{ include (print $.Template.BasePath "/proxy-configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
labels:
{{- include "open-design.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
initContainers:
{{- if .Values.initContainers }}
{{- toYaml .Values.initContainers | nindent 8 }}
{{- end }}
- name: auth-proxy-init
image: {{ .Values.authProxy.image }}
securityContext:
{{- toYaml .Values.authProxy.securityContext | nindent 12 }}
command: ["/bin/sh", "-c"]
args:
- envsubst '$PROXY_PORT $OD_BIND_HOST $OD_WEB_PORT $PROXY_API_TOKEN' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf
envFrom:
- configMapRef:
name: {{ include "open-design.fullname" . }}
env:
- name: PROXY_API_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "open-design.fullname" . }}
key: OD_API_TOKEN
volumeMounts:
- name: proxy-template
mountPath: /etc/nginx/templates
- name: proxy-config
mountPath: /etc/nginx/conf.d
- name: proxy-tmp
mountPath: /tmp
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.config.webPort }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "open-design.fullname" . }}
env:
- name: OD_API_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "open-design.fullname" . }}
key: OD_API_TOKEN
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
exec:
command: ["wget", "-qO-", "http://{{ .Values.config.bindHost }}:{{ .Values.config.webPort }}/api/health"]
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
exec:
command: ["wget", "-qO-", "http://{{ .Values.config.bindHost }}:{{ .Values.config.webPort }}/api/health"]
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: storage
mountPath: /app/.od
- name: tmp
mountPath: /tmp
- name: auth-proxy
image: {{ .Values.authProxy.image }}
securityContext:
{{- toYaml .Values.authProxy.securityContext | nindent 12 }}
ports:
- name: http-proxy
containerPort: {{ .Values.authProxy.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /nginx-health
port: http-proxy
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
readinessProbe:
httpGet:
path: /api/health
port: http-proxy
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
volumeMounts:
- name: proxy-config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: proxy-tmp
mountPath: /tmp
{{- if .Values.sidecars }}
{{- toYaml .Values.sidecars | nindent 8 }}
{{- end }}
volumes:
- name: storage
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "open-design.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
- name: tmp
emptyDir: {}
- name: proxy-template
configMap:
name: {{ include "open-design.fullname" . }}-proxy
- name: proxy-config
emptyDir: {}
- name: proxy-tmp
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -0,0 +1,30 @@
# WARNING: SQLite does not support concurrent writes from multiple replicas.
# Only enable HPA if open-design has been configured with an external database.
{{- if .Values.hpa.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "open-design.fullname" . }}
labels:
{{- include "open-design.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "open-design.fullname" . }}
minReplicas: {{ .Values.hpa.minReplicas }}
maxReplicas: {{ .Values.hpa.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.hpa.targetMemoryUtilizationPercentage }}
{{- end }}

View file

@ -0,0 +1,48 @@
{{- if .Values.ingress.enabled -}}
{{- range .Values.ingress.hosts }}
{{- range .paths }}
{{- if ne .path "/" }}
{{- fail "CRITICAL ERROR: Non-root ingress path prefixes (sub-paths) are not supported by the proxy routing stack. Ingress paths must be fixed to '/'." }}
{{- end }}
{{- end }}
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "open-design.fullname" . }}
labels:
{{- include "open-design.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "open-design.fullname" $ }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,43 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "open-design.fullname" . }}-proxy
labels:
{{- include "open-design.labels" . | nindent 4 }}
data:
default.conf.template: |
server {
listen ${PROXY_PORT};
location /nginx-health {
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location / {
proxy_pass http://${OD_BIND_HOST}:${OD_WEB_PORT};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
}
location /api/ {
proxy_pass http://${OD_BIND_HOST}:${OD_WEB_PORT};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header Authorization "Bearer ${PROXY_API_TOKEN}";
# Critical for Server-Sent Events (SSE)
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_set_header Connection '';
http2_push_preload on;
}
}

View file

@ -0,0 +1,17 @@
{{- if .Values.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "open-design.fullname" . }}
labels:
{{- include "open-design.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "open-design.fullname" . }}
type: Opaque
data:
{{- if .Values.config.apiToken }}
OD_API_TOKEN: {{ .Values.config.apiToken | b64enc | quote }}
{{- else }}
{{- fail "CRITICAL ERROR: config.apiToken cannot be empty." }}
{{- end }}

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "open-design.fullname" . }}
labels:
{{- include "open-design.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http-proxy
protocol: TCP
name: http
selector:
{{- include "open-design.selectorLabels" . | nindent 4 }}

View file

@ -0,0 +1,120 @@
# Safety constraint for SQLite data consistency
replicaCount: 1
commonLabels:
app.kubernetes.io/environment: "production"
image:
repository: vanjayak/open-design
pullPolicy: IfNotPresent
tag: "latest"
config:
# The runtime environment for the Node.js process.
# Must be either "production" or "development".
# For Staging clusters, leave this as "production".
nodeEnv: "production"
allowedOrigins: ""
publicBaseUrl: ""
nodeOptions: "--max-old-space-size=192"
webPort: 7456
bindHost: "127.0.0.1"
apiToken: "secure-default-token-change-me"
service:
type: ClusterIP
port: 80
persistence:
enabled: true
storageClass: ""
accessMode: ReadWriteOnce
size: 10Gi
ingress:
enabled: false
className: "nginx"
annotations:
# Critical: Disables proxy buffering so Server-Sent Events (SSE) stream in real-time
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
hosts:
- host: open-design.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
## example for tls configuration:
# - secretName: open-design-tls
# hosts:
# - open-design.local
resources:
limits:
cpu: 1000m
memory: 1024Mi
requests:
cpu: 200m
memory: 256Mi
containerSecurityContext:
seLinuxOptions: {}
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
privileged: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: "RuntimeDefault"
podSecurityContext:
fsGroupChangePolicy: Always
sysctls: []
supplementalGroups: []
fsGroup: 1001
authProxy:
image: "nginxinc/nginx-unprivileged:1.25-alpine-slim"
port: 8080
securityContext:
runAsUser: 101
runAsGroup: 101
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
livenessProbe:
enabled: true
initialDelaySeconds: 20
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
enabled: true
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# WARNING: Do not enable HPA if using the default SQLite storage.
# Concurrent writes from multiple pods will corrupt the database file.
hpa:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
affinity: {}
tolerations: []
initContainers: []
sidecars: []