From 6244b67295d9b3a6e6ca8454cf5af9d8ce2fcb10 Mon Sep 17 00:00:00 2001 From: Sharon K <199145162+shaarron@users.noreply.github.com> Date: Sun, 24 May 2026 17:37:25 +0300 Subject: [PATCH] MVP Helm chart (#2140) * 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 --- charts/open-design/.helmignore | 23 +++ charts/open-design/Chart.yaml | 6 + charts/open-design/README.md | 165 ++++++++++++++++++ charts/open-design/templates/_helpers.tpl | 52 ++++++ charts/open-design/templates/configmap.yaml | 59 +++++++ charts/open-design/templates/deployment.yaml | 162 +++++++++++++++++ charts/open-design/templates/hpa.yaml | 30 ++++ charts/open-design/templates/ingress.yaml | 48 +++++ .../templates/proxy-configmap.yaml | 43 +++++ charts/open-design/templates/pvc.yaml | 17 ++ charts/open-design/templates/secret.yaml | 11 ++ charts/open-design/templates/service.yaml | 15 ++ charts/open-design/values.yaml | 120 +++++++++++++ 13 files changed, 751 insertions(+) create mode 100644 charts/open-design/.helmignore create mode 100644 charts/open-design/Chart.yaml create mode 100644 charts/open-design/README.md create mode 100644 charts/open-design/templates/_helpers.tpl create mode 100644 charts/open-design/templates/configmap.yaml create mode 100644 charts/open-design/templates/deployment.yaml create mode 100644 charts/open-design/templates/hpa.yaml create mode 100644 charts/open-design/templates/ingress.yaml create mode 100644 charts/open-design/templates/proxy-configmap.yaml create mode 100644 charts/open-design/templates/pvc.yaml create mode 100644 charts/open-design/templates/secret.yaml create mode 100644 charts/open-design/templates/service.yaml create mode 100644 charts/open-design/values.yaml diff --git a/charts/open-design/.helmignore b/charts/open-design/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/open-design/.helmignore @@ -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/ diff --git a/charts/open-design/Chart.yaml b/charts/open-design/Chart.yaml new file mode 100644 index 000000000..745735890 --- /dev/null +++ b/charts/open-design/Chart.yaml @@ -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" diff --git a/charts/open-design/README.md b/charts/open-design/README.md new file mode 100644 index 000000000..84dfd6d5c --- /dev/null +++ b/charts/open-design/README.md @@ -0,0 +1,165 @@ + + +## 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) \ No newline at end of file diff --git a/charts/open-design/templates/_helpers.tpl b/charts/open-design/templates/_helpers.tpl new file mode 100644 index 000000000..d4bc8f794 --- /dev/null +++ b/charts/open-design/templates/_helpers.tpl @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/configmap.yaml b/charts/open-design/templates/configmap.yaml new file mode 100644 index 000000000..21dfc3946 --- /dev/null +++ b/charts/open-design/templates/configmap.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/deployment.yaml b/charts/open-design/templates/deployment.yaml new file mode 100644 index 000000000..b1fdc545e --- /dev/null +++ b/charts/open-design/templates/deployment.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/hpa.yaml b/charts/open-design/templates/hpa.yaml new file mode 100644 index 000000000..a2df9f265 --- /dev/null +++ b/charts/open-design/templates/hpa.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/ingress.yaml b/charts/open-design/templates/ingress.yaml new file mode 100644 index 000000000..b94f2d183 --- /dev/null +++ b/charts/open-design/templates/ingress.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/proxy-configmap.yaml b/charts/open-design/templates/proxy-configmap.yaml new file mode 100644 index 000000000..67a2b5ca9 --- /dev/null +++ b/charts/open-design/templates/proxy-configmap.yaml @@ -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; + } + } \ No newline at end of file diff --git a/charts/open-design/templates/pvc.yaml b/charts/open-design/templates/pvc.yaml new file mode 100644 index 000000000..78373393c --- /dev/null +++ b/charts/open-design/templates/pvc.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/secret.yaml b/charts/open-design/templates/secret.yaml new file mode 100644 index 000000000..e7180eda1 --- /dev/null +++ b/charts/open-design/templates/secret.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/templates/service.yaml b/charts/open-design/templates/service.yaml new file mode 100644 index 000000000..fdd69da90 --- /dev/null +++ b/charts/open-design/templates/service.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/open-design/values.yaml b/charts/open-design/values.yaml new file mode 100644 index 000000000..5996f239b --- /dev/null +++ b/charts/open-design/values.yaml @@ -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: [] \ No newline at end of file