From 3e04b0f952ac70fe944d49bff7fd9a4bce238848 Mon Sep 17 00:00:00 2001 From: loi Date: Thu, 9 Apr 2026 09:40:20 -0700 Subject: [PATCH 1/3] FIREFLY-1939: use uppercase TAP_SCHEMA in ADQL query - add pathPrefix to helm chart livenessProbe path when set --- helm/README.md | 3 +-- helm/templates/deployment.yaml | 5 ++++- helm/templates/service.yaml | 2 +- helm/values.yaml | 8 +------ .../js/api/webApiCommands/TapCommands.js | 4 ++-- src/firefly/js/ui/tap/TapUtil.js | 22 +++++++++---------- src/firefly/js/ui/tap/TapViewType.jsx | 4 ++-- 7 files changed, 22 insertions(+), 26 deletions(-) diff --git a/helm/README.md b/helm/README.md index a4f6582b81..eadd852813 100644 --- a/helm/README.md +++ b/helm/README.md @@ -98,7 +98,7 @@ Automatically deployed when `replicaCount > 1`. Consult `Reference Values` for c | adminPassword.secretName | string | `""` | Name of the Kubernetes secret containing the password. | | adminPassword.value | string | `""` | Plain text password. | | cleanupInterval | string | `"1h"` | Interval for cleaning up temporary files. | -| extraEnv | list | `[]` | Additional environment variables. | +| env | list | `[]` | Additional environment variables. | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. | | image.repository | string | `"ghcr.io/caltech-ipac/firefly"` | Image repository. | | image.tag | string | `""` | Image tag. Defaults to appVersion in Chart.yaml when not set. | @@ -124,7 +124,6 @@ Automatically deployed when `replicaCount > 1`. Consult `Reference Values` for c | service.port | int | `80` | Service port. | | service.sessionAffinity | string | `"ClientIP"` | Session affinity type. ClientIP is used as a fallback for non-ingress traffic. | | service.sessionAffinityTimeout | int | `3600` | Session affinity timeout in seconds. | -| service.targetPort | int | `8080` | Container port. | | service.type | string | `"ClusterIP"` | Service type. | | serviceAccount.create | bool | `false` | Create a service account. | | serviceAccount.name | string | `""` | Service account name. Defaults to the release fullname when create is true. | \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 988be4254b..f865dbf9a2 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -27,7 +27,7 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http - containerPort: {{ .Values.service.targetPort }} + containerPort: 8080 protocol: TCP env: {{- with .Values.env }} @@ -54,6 +54,9 @@ spec: value: {{ include "firefly.redisName" . }} {{- end }} livenessProbe: + httpGet: + path: {{ if .Values.ingress.pathPrefix }}/{{ trimPrefix "/" .Values.ingress.pathPrefix }}{{ end }}/firefly/healthz + port: 8080 {{- toYaml .Values.livenessProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml index e5179ce57b..62970e1a55 100644 --- a/helm/templates/service.yaml +++ b/helm/templates/service.yaml @@ -8,7 +8,7 @@ spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} - targetPort: {{ .Values.service.targetPort }} + targetPort: 8080 protocol: TCP name: http sessionAffinity: {{ .Values.service.sessionAffinity }} diff --git a/helm/values.yaml b/helm/values.yaml index 69b359ab76..ee41c970da 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -18,8 +18,6 @@ service: type: ClusterIP # -- Service port. port: 80 - # -- Container port. - targetPort: 8080 # -- Session affinity type. ClientIP is used as a fallback for non-ingress traffic. sessionAffinity: ClientIP # -- Session affinity timeout in seconds. @@ -43,10 +41,6 @@ ingress: # -- Liveness probe configuration. Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ livenessProbe: - # @ignored - httpGet: - path: firefly/healthz - port: 8080 initialDelaySeconds: 120 periodSeconds: 60 timeoutSeconds: 10 @@ -59,7 +53,7 @@ resources: memory: 512Mi # -- Additional environment variables. -extraEnv: [] +env: [] # - name: FOO # value: bar diff --git a/src/firefly/js/api/webApiCommands/TapCommands.js b/src/firefly/js/api/webApiCommands/TapCommands.js index 219e8a348f..ab7d36b53e 100644 --- a/src/firefly/js/api/webApiCommands/TapCommands.js +++ b/src/firefly/js/api/webApiCommands/TapCommands.js @@ -123,8 +123,8 @@ WHERE CONTAINS(POINT('ICRS', ra, dec),CIRCLE('ICRS', 83.63321237, 22.01446012, 0 desc:'Show tap tables for CADC', params:{ service: 'https://ws.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/argus/', - schema:'tap_schema', - table:'tap_schema.tables', + schema:'TAP_SCHEMA', + table:'TAP_SCHEMA.tables', execute: 'true' } }, diff --git a/src/firefly/js/ui/tap/TapUtil.js b/src/firefly/js/ui/tap/TapUtil.js index a4eff43a42..1e61592a23 100644 --- a/src/firefly/js/ui/tap/TapUtil.js +++ b/src/firefly/js/ui/tap/TapUtil.js @@ -345,8 +345,8 @@ function makeTapSchemaRequest(serviceUrl, QUERY, title) { async function loadSchemaDefJoin(serviceUrl) { const QUERY = ` SELECT * - FROM tap_schema.schemas - INNER JOIN tap_schema.tables ON tap_schema.tables.schema_name = tap_schema.schemas.schema_name + FROM TAP_SCHEMA.schemas + INNER JOIN TAP_SCHEMA.tables ON TAP_SCHEMA.tables.schema_name = TAP_SCHEMA.schemas.schema_name `; const tableModel= await doFetchTable(makeTapSchemaRequest(serviceUrl, QUERY, 'loadSchemaDefJoin')); @@ -388,8 +388,8 @@ function modifyModelForOldTap(tableModel) { async function loadSchemaDefNoJoin(serviceUrl) { - const schemasQuery = 'SELECT * FROM tap_schema.schemas'; - const tablesQuery = 'SELECT * FROM tap_schema.tables'; + const schemasQuery = 'SELECT * FROM TAP_SCHEMA.schemas'; + const tablesQuery = 'SELECT * FROM TAP_SCHEMA.tables'; const [schemas,tables]= await Promise.all([ @@ -474,13 +474,13 @@ export const loadSchemaDef = memoize(async (serviceUrl) => { export const loadTapKeys = memoize(async (serviceUrl) => { const QUERY = ` - SELECT tap_schema.keys.key_id, - tap_schema.keys.from_table, - tap_schema.keys.target_table, - tap_schema.keys.description, - tap_schema.key_columns.from_column, - tap_schema.key_columns.target_column - FROM tap_schema.keys INNER JOIN tap_schema.key_columns ON tap_schema.keys.key_id = tap_schema.key_columns.key_id + SELECT TAP_SCHEMA.keys.key_id, + TAP_SCHEMA.keys.from_table, + TAP_SCHEMA.keys.target_table, + TAP_SCHEMA.keys.description, + TAP_SCHEMA.key_columns.from_column, + TAP_SCHEMA.key_columns.target_column + FROM TAP_SCHEMA.keys INNER JOIN TAP_SCHEMA.key_columns ON TAP_SCHEMA.keys.key_id = TAP_SCHEMA.key_columns.key_id `; const url= makeSyncQueryURL(serviceUrl,QUERY.trim()); diff --git a/src/firefly/js/ui/tap/TapViewType.jsx b/src/firefly/js/ui/tap/TapViewType.jsx index 7e90548066..e6a98421bc 100644 --- a/src/firefly/js/ui/tap/TapViewType.jsx +++ b/src/firefly/js/ui/tap/TapViewType.jsx @@ -195,8 +195,8 @@ function BasicUI(props) { } else { const foundSchema= schemaOptions.find( (s) => { - if (schemaOptions.length===2) return s.value==='tap_schema'; - else return s.value!=='tap_schema' && s.value!=='ivoa'; + if (schemaOptions.length===2) return s.value==='TAP_SCHEMA'; + else return s.value!=='TAP_SCHEMA' && s.value!=='ivoa'; }); if (foundSchema) setSchemaName(foundSchema.value); } From b4bb3b03a26c38c2420c1a4ddfa63e7d6a6ba24c Mon Sep 17 00:00:00 2001 From: loi Date: Mon, 13 Apr 2026 13:57:46 -0700 Subject: [PATCH 2/3] Add helm autoscaling and update docs --- docker/entrypoint.py | 1 + helm/README.md | 168 +++++++++++++++++++++++---------- helm/README.md.gotmpl | 156 +++++++++++++++++++++--------- helm/env/dev.yaml | 3 + helm/env/prod.yaml | 8 +- helm/templates/_helpers.tpl | 18 ++++ helm/templates/deployment.yaml | 5 +- helm/templates/hpa.yaml | 27 ++++++ helm/templates/ingress.yaml | 2 +- helm/templates/pvc.yaml | 4 +- helm/templates/redis.yaml | 2 +- helm/values.yaml | 24 ++++- 12 files changed, 316 insertions(+), 102 deletions(-) create mode 100644 helm/templates/hpa.yaml diff --git a/docker/entrypoint.py b/docker/entrypoint.py index 1bbafbd15e..2422c3c0f6 100644 --- a/docker/entrypoint.py +++ b/docker/entrypoint.py @@ -305,6 +305,7 @@ def main(): f"-XX:MaxRAMPercentage={os.getenv('MAX_RAM_PERCENT', '80')}", "-XX:+UnlockExperimentalVMOptions", "-XX:TrimNativeHeapInterval=30000", + "-XX:+UseZGC", f"-DADMIN_USER={admin_user}", f"-DADMIN_PASSWORD={admin_password}", f"-Dhost.name={os.getenv('HOSTNAME', '')}", diff --git a/helm/README.md b/helm/README.md index eadd852813..64267aa5ca 100644 --- a/helm/README.md +++ b/helm/README.md @@ -7,81 +7,147 @@ Helm chart for deploying [Firefly](https://github.com/Caltech-IPAC/firefly), a web application for astronomical data visualization. -## Chart Structure +## Quick Start -``` -helm/ -├── Chart.yaml -├── values.yaml # defaults -└── env/ - ├── dev.yaml # sample dev overrides - └── prod.yaml # sample prod overrides -``` +**Deploy Firefly with one command:** -## Install +```bash +helm upgrade --install firefly oci://ghcr.io/caltech-ipac/helm-charts/firefly \ + -n firefly \ + --create-namespace \ + --set ingress.host=firefly.example.com +``` -**From GHCR:** +Then open `http://firefly.example.com/firefly` in your browser. -Available versions: -https://github.com/Caltech-IPAC/firefly/pkgs/container/helm-charts%2Ffirefly/versions +> For more scenarios see [Examples](#examples). For all available options see [Configuration](#configuration) and [Reference](#reference). -Inspect a specific version: +**Uninstall:** ```bash -helm show chart oci://ghcr.io/caltech-ipac/helm-charts/firefly --version +helm -n firefly uninstall firefly ``` -Install: +## Examples + +### Pinned version with TLS + +Use a specific Firefly version and enable HTTPS via an existing TLS secret: + ```bash helm upgrade --install firefly oci://ghcr.io/caltech-ipac/helm-charts/firefly \ - -n my-namespace \ + -n firefly \ --create-namespace \ - --version \ - -f env/dev.yaml \ - --set ingress.host=my.example.com + --set image.tag=2026.2.1 \ + --set ingress.host=firefly.example.com \ + --set ingress.tlsSecretName=firefly-tls ``` -> Omit `--version` to use the latest published chart. -**From local chart:** -```bash -helm upgrade --install firefly ./helm \ - -n my-namespace \ - --create-namespace \ - -f env/dev.yaml \ - --set ingress.host=my.example.com +> See [`ingress`](#configuration) in Configuration for annotation and className options. + +--- + +### Multi-replica with autoscaling + +Scale horizontally based on CPU. Redis and session affinity are automatically enabled when running more than one replica: + +```yaml +# my-values.yaml +ingress: + host: firefly.example.com + className: nginx + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCpuUtilization: 80 + +persistence: + sharedWorkDir: + pvc: + size: 50Gi + storageClass: efs # create an EFS-backed StorageClass for ReadWriteMany access + accessMode: ReadWriteMany ``` -**Uninstall:** ```bash -helm -n my-namespace uninstall firefly +helm upgrade --install firefly oci://ghcr.io/caltech-ipac/helm-charts/firefly \ + -n firefly \ + --create-namespace \ + -f my-values.yaml ``` +> `sharedWorkDir` with `ReadWriteMany` is required in multi-replica mode. See [`autoscaling`](#reference) and [`redis`](#redis) in Reference for tuning options. + ## Configuration +### Ingress + +Ingress is enabled by default and exposes Firefly at `http(s):///firefly`. The path is always `/firefly`, optionally prefixed by `pathPrefix`: + +| `pathPrefix` | Resulting path | +|---|---| +| _(empty)_ | `/firefly` | +| `myapp` | `/myapp/firefly` | + +**TLS** — set `tlsSecretName` to enable HTTPS. The secret must already exist in the same namespace: +```yaml +ingress: + host: firefly.example.com + tlsSecretName: firefly-tls +``` + +**Ingress class** — omit `className` to use the cluster default, or set it explicitly: +```yaml +ingress: + className: nginx # nginx | traefik | haproxy | +``` + +When `className` is set to `nginx`, `traefik`, or `haproxy` and running in multi-replica mode, cookie-based session affinity annotations are automatically injected. For other controllers, add the annotations manually via `ingress.annotations`. + +**Disable ingress** — set `ingress.enabled: false` to skip creating the Ingress resource entirely (e.g. when using a custom gateway or accessing the service directly). + ### Session Affinity -Session affinity is required when `replicaCount > 1`. If `ingress.className` is set, -the appropriate cookie-based affinity annotations are automatically injected. Supported -values: `nginx`, `traefik`, `haproxy`. For other ingress controllers, set the annotations -manually via `ingress.annotations`. + +Session affinity is required for multi-replica deployments and is automatically enabled when `replicaCount > 1` or `autoscaling.enabled` is `true`. + +When `ingress.className` is set to `nginx`, `traefik`, or `haproxy`, the appropriate cookie-based affinity annotations are automatically injected into the ingress. For other ingress controllers, add the annotations manually via `ingress.annotations`. ### Persistence -All volumes default to `emptyDir`. Each can be overridden with a new PVC or an existing one. -The examples below are samples — actual values for `storageClass`, `size`, and `existingClaim` -depend on your Kubernetes setup: +There are three volumes, each defaulting to `emptyDir`: + +| Volume | Description | Default size | Default accessMode | +|---|---|---|---| +| `workDir` | Working/temp files | `50Gi` | `ReadWriteOnce` | +| `logsDir` | Application logs | _(required)_ | `ReadWriteOnce` | +| `sharedWorkDir` | Shared work across replicas | `50Gi` | `ReadWriteMany` | + +> `sharedWorkDir` must use `ReadWriteMany` and is required in multi-replica mode. + +Each volume can be configured in one of three ways: + +**emptyDir** (default) — data is lost when the pod restarts: +```yaml +persistence: + workDir: {} +``` + +**New PVC** — chart creates and manages the PVC (`storageClass` is optional, omit to use the cluster default): ```yaml persistence: workDir: pvc: - size: 10Gi - storageClass: standard + size: 50Gi + storageClass: gp3 accessMode: ReadWriteOnce +``` + +**Existing PVC** — use a PVC you already created: +```yaml +persistence: logsDir: existingClaim: my-logs-pvc - sharedWorkDir: - pvc: - size: 10Gi - storageClass: efs - accessMode: ReadWriteMany # required for multi-replica ``` ### Redis @@ -97,6 +163,12 @@ Automatically deployed when `replicaCount > 1`. Consult `Reference Values` for c | adminPassword.secretKey | string | `""` | Key within the secret. | | adminPassword.secretName | string | `""` | Name of the Kubernetes secret containing the password. | | adminPassword.value | string | `""` | Plain text password. | +| autoscaling.enabled | bool | `false` | Enable horizontal pod autoscaling. When enabled, Redis is automatically deployed and session affinity annotations are injected into the ingress. | +| autoscaling.maxReplicas | int | `3` | Maximum number of replicas. | +| autoscaling.minReplicas | int | `1` | Minimum number of replicas. | +| autoscaling.scaleDownWindow | int | `300` | Stabilization window in seconds before scaling down. | +| autoscaling.scaleUpWindow | int | `60` | Stabilization window in seconds before scaling up. | +| autoscaling.targetCpuUtilization | int | `80` | Target average CPU utilization (%) across pods to trigger scale up. | | cleanupInterval | string | `"1h"` | Interval for cleaning up temporary files. | | env | list | `[]` | Additional environment variables. | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. | @@ -109,17 +181,17 @@ Automatically deployed when `replicaCount > 1`. Consult `Reference Values` for c | ingress.pathPrefix | string | `""` | Optional path prefix prepended to the Firefly path: [/pathPrefix]/firefly. | | ingress.tlsSecretName | string | `""` | TLS secret name. When set, TLS is enabled for the ingress host. | | livenessProbe | object | `{"initialDelaySeconds":120,"periodSeconds":60,"timeoutSeconds":10}` | Liveness probe configuration. Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ | -| persistence | object | `{"logsDir":{},"sharedWorkDir":{},"workDir":{}}` | Persistence volume configuration. Each volume defaults to emptyDir. To use a new PVC: set pvc.size, pvc.storageClass, pvc.accessMode. To use an existing PVC: set existingClaim. | +| persistence | object | `{"logsDir":{},"sharedWorkDir":{},"workDir":{}}` | Persistence volume configuration. Each volume defaults to emptyDir. To use a new PVC: set pvc.storageClass(Required), pvc.size[=50Gi], pvc.accessMode[=ReadWriteMany]. To use an existing PVC: set existingClaim. | | persistence.logsDir | object | `{}` | Logs directory volume. | -| persistence.sharedWorkDir | object | `{}` | Shared work directory volume. Use ReadWriteMany accessMode for multi-replica. | +| persistence.sharedWorkDir | object | `{}` | Shared work directory volume. Required in multi-replica mode; must use ReadWriteMany accessMode. | | persistence.workDir | object | `{}` | Work directory volume. | | redis.image | string | `"redis:6.2"` | Redis image. | | redis.maxmemory | string | `"250M"` | Redis maxmemory limit. | | redis.persistence | object | `{}` | Redis persistence. Falls back to sharedWorkDir PVC if configured, otherwise emptyDir. | | redis.port | int | `6379` | Redis port. | | redis.resources | object | `{"limits":{"memory":"256Mi"}}` | Redis resource limits. | -| replicaCount | int | `1` | Number of replicas. When > 1, Redis is automatically deployed and session affinity annotations are injected into the ingress. | -| resources | object | `{"limits":{"memory":"512Mi"},"requests":{"memory":"256Mi"}}` | Resource requests and limits. | +| replicaCount | int | `1` | Number of replicas. When > 1, Redis is automatically deployed and session affinity annotations are injected into the ingress. Ignored when autoscaling.enabled is true — the HPA controls replica count in that case. | +| resources | object | `{"limits":{"memory":"4Gi"},"requests":{"memory":"2Gi"}}` | Resource requests and limits. | | securityContext | object | `{}` | Pod security context. Shared by Firefly and Redis. When not set, containers run as the default image user (e.g. tomcat(91)). | | service.port | int | `80` | Service port. | | service.sessionAffinity | string | `"ClientIP"` | Session affinity type. ClientIP is used as a fallback for non-ingress traffic. | diff --git a/helm/README.md.gotmpl b/helm/README.md.gotmpl index 57a3bdee25..9fd0177a15 100644 --- a/helm/README.md.gotmpl +++ b/helm/README.md.gotmpl @@ -7,81 +7,149 @@ Helm chart for deploying [Firefly](https://github.com/Caltech-IPAC/firefly), a web application for astronomical data visualization. -## Chart Structure +## Quick Start -``` -helm/ -├── Chart.yaml -├── values.yaml # defaults -└── env/ - ├── dev.yaml # sample dev overrides - └── prod.yaml # sample prod overrides -``` +**Deploy Firefly with one command:** -## Install +```bash +helm upgrade --install firefly oci://ghcr.io/caltech-ipac/helm-charts/firefly \ + -n firefly \ + --create-namespace \ + --set ingress.host=firefly.example.com +``` -**From GHCR:** +Then open `http://firefly.example.com/firefly` in your browser. -Available versions: -https://github.com/Caltech-IPAC/firefly/pkgs/container/helm-charts%2Ffirefly/versions +> For more scenarios see [Examples](#examples). For all available options see [Configuration](#configuration) and [Reference](#reference). -Inspect a specific version: +**Uninstall:** ```bash -helm show chart oci://ghcr.io/caltech-ipac/helm-charts/firefly --version +helm -n firefly uninstall firefly ``` -Install: +## Examples + +### Pinned version with TLS + +Use a specific Firefly version and enable HTTPS via an existing TLS secret: + ```bash helm upgrade --install firefly oci://ghcr.io/caltech-ipac/helm-charts/firefly \ - -n my-namespace \ + -n firefly \ --create-namespace \ - --version \ - -f env/dev.yaml \ - --set ingress.host=my.example.com + --set image.tag=2026.2.1 \ + --set ingress.host=firefly.example.com \ + --set ingress.tlsSecretName=firefly-tls ``` -> Omit `--version` to use the latest published chart. -**From local chart:** -```bash -helm upgrade --install firefly ./helm \ - -n my-namespace \ - --create-namespace \ - -f env/dev.yaml \ - --set ingress.host=my.example.com +> See [`ingress`](#configuration) in Configuration for annotation and className options. + +--- + +### Multi-replica with autoscaling + +Scale horizontally based on CPU. Redis and session affinity are automatically enabled when running more than one replica: + +```yaml +# my-values.yaml +ingress: + host: firefly.example.com + className: nginx + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCpuUtilization: 80 + +persistence: + sharedWorkDir: + pvc: + size: 50Gi + storageClass: efs # create an EFS-backed StorageClass for ReadWriteMany access + accessMode: ReadWriteMany ``` -**Uninstall:** ```bash -helm -n my-namespace uninstall firefly +helm upgrade --install firefly oci://ghcr.io/caltech-ipac/helm-charts/firefly \ + -n firefly \ + --create-namespace \ + -f my-values.yaml ``` + +> `sharedWorkDir` with `ReadWriteMany` is required in multi-replica mode. See [`autoscaling`](#reference) and [`redis`](#redis) in Reference for tuning options. + + ## Configuration +### Ingress + +Ingress is enabled by default and exposes Firefly at `http(s):///firefly`. The path is always `/firefly`, optionally prefixed by `pathPrefix`: + +| `pathPrefix` | Resulting path | +|---|---| +| _(empty)_ | `/firefly` | +| `myapp` | `/myapp/firefly` | + +**TLS** — set `tlsSecretName` to enable HTTPS. The secret must already exist in the same namespace: +```yaml +ingress: + host: firefly.example.com + tlsSecretName: firefly-tls +``` + +**Ingress class** — omit `className` to use the cluster default, or set it explicitly: +```yaml +ingress: + className: nginx # nginx | traefik | haproxy | +``` + +When `className` is set to `nginx`, `traefik`, or `haproxy` and running in multi-replica mode, cookie-based session affinity annotations are automatically injected. For other controllers, add the annotations manually via `ingress.annotations`. + +**Disable ingress** — set `ingress.enabled: false` to skip creating the Ingress resource entirely (e.g. when using a custom gateway or accessing the service directly). + ### Session Affinity -Session affinity is required when `replicaCount > 1`. If `ingress.className` is set, -the appropriate cookie-based affinity annotations are automatically injected. Supported -values: `nginx`, `traefik`, `haproxy`. For other ingress controllers, set the annotations -manually via `ingress.annotations`. + +Session affinity is required for multi-replica deployments and is automatically enabled when `replicaCount > 1` or `autoscaling.enabled` is `true`. + +When `ingress.className` is set to `nginx`, `traefik`, or `haproxy`, the appropriate cookie-based affinity annotations are automatically injected into the ingress. For other ingress controllers, add the annotations manually via `ingress.annotations`. ### Persistence -All volumes default to `emptyDir`. Each can be overridden with a new PVC or an existing one. -The examples below are samples — actual values for `storageClass`, `size`, and `existingClaim` -depend on your Kubernetes setup: +There are three volumes, each defaulting to `emptyDir`: + +| Volume | Description | Default size | Default accessMode | +|---|---|---|---| +| `workDir` | Working/temp files | `50Gi` | `ReadWriteOnce` | +| `logsDir` | Application logs | _(required)_ | `ReadWriteOnce` | +| `sharedWorkDir` | Shared work across replicas | `50Gi` | `ReadWriteMany` | + +> `sharedWorkDir` must use `ReadWriteMany` and is required in multi-replica mode. + +Each volume can be configured in one of three ways: + +**emptyDir** (default) — data is lost when the pod restarts: +```yaml +persistence: + workDir: {} +``` + +**New PVC** — chart creates and manages the PVC (`storageClass` is optional, omit to use the cluster default): ```yaml persistence: workDir: pvc: - size: 10Gi - storageClass: standard + size: 50Gi + storageClass: gp3 accessMode: ReadWriteOnce +``` + +**Existing PVC** — use a PVC you already created: +```yaml +persistence: logsDir: existingClaim: my-logs-pvc - sharedWorkDir: - pvc: - size: 10Gi - storageClass: efs - accessMode: ReadWriteMany # required for multi-replica ``` ### Redis diff --git a/helm/env/dev.yaml b/helm/env/dev.yaml index 30da82058b..9917a4f9da 100644 --- a/helm/env/dev.yaml +++ b/helm/env/dev.yaml @@ -1,5 +1,8 @@ replicaCount: 1 +image: + pullPolicy: Always + resources: requests: memory: 1Gi diff --git a/helm/env/prod.yaml b/helm/env/prod.yaml index fe8da6cc99..32dd4446f9 100644 --- a/helm/env/prod.yaml +++ b/helm/env/prod.yaml @@ -1,4 +1,5 @@ -replicaCount: 2 +autoscaling: + enabled: true resources: requests: @@ -11,3 +12,8 @@ redis: resources: limits: memory: 512Mi + +persistence: + sharedWorkDir: + pvc: + storageClass: "efs" diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 069c4803e9..9879f78031 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -16,3 +16,21 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{/* +Returns "true" when running in multi-replica mode — either replicaCount > 1 or +autoscaling is enabled. Used to gate Redis deployment, session affinity annotations, +and the Redis host env var. +*/}} +{{- define "firefly.multiReplica" -}} +{{- if or (gt (int .Values.replicaCount) 1) .Values.autoscaling.enabled }}true{{- end }} +{{- end }} + +{{/* +Validates chart values. Call this from deployment.yaml to catch misconfigurations early. +*/}} +{{- define "firefly.validate" -}} +{{- if and (include "firefly.multiReplica" .) (not .Values.persistence.sharedWorkDir.pvc) (not .Values.persistence.sharedWorkDir.existingClaim) }} +{{- fail "persistence.sharedWorkDir must be configured (pvc or existingClaim) in multi-replica mode (replicaCount > 1 or autoscaling.enabled). Use ReadWriteMany accessMode." }} +{{- end }} +{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index f865dbf9a2..a990bb570a 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- include "firefly.validate" . }} apiVersion: apps/v1 kind: Deployment metadata: @@ -5,7 +6,9 @@ metadata: labels: {{- include "firefly.labels" . | nindent 4 }} spec: + {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} + {{- end }} selector: matchLabels: {{- include "firefly.selectorLabels" . | nindent 6 }} @@ -49,7 +52,7 @@ spec: - name: ADMIN_PASSWORD value: {{ .Values.adminPassword.value | quote }} {{- end }} - {{- if gt (int .Values.replicaCount) 1 }} + {{- if include "firefly.multiReplica" . }} - name: PROPS_redis__host value: {{ include "firefly.redisName" . }} {{- end }} diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 0000000000..49b8002fd2 --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,27 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "firefly.fullname" . }} + labels: + {{- include "firefly.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "firefly.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCpuUtilization }} + behavior: + scaleUp: + stabilizationWindowSeconds: {{ .Values.autoscaling.scaleUpWindow }} + scaleDown: + stabilizationWindowSeconds: {{ .Values.autoscaling.scaleDownWindow }} +{{- end }} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml index ea1852e744..3f3ce0c851 100644 --- a/helm/templates/ingress.yaml +++ b/helm/templates/ingress.yaml @@ -1,6 +1,6 @@ {{- if .Values.ingress.enabled }} {{- $affinityAnnotations := dict }} -{{- if gt (int .Values.replicaCount) 1 }} +{{- if include "firefly.multiReplica" . }} {{- if eq .Values.ingress.className "nginx" }} {{- $affinityAnnotations = dict "nginx.ingress.kubernetes.io/affinity" "cookie" diff --git a/helm/templates/pvc.yaml b/helm/templates/pvc.yaml index 095961e31d..8dcf7b730b 100644 --- a/helm/templates/pvc.yaml +++ b/helm/templates/pvc.yaml @@ -10,7 +10,7 @@ spec: - {{ .Values.persistence.workDir.pvc.accessMode | default "ReadWriteOnce" }} resources: requests: - storage: {{ .Values.persistence.workDir.pvc.size }} + storage: {{ .Values.persistence.workDir.pvc.size | default "50Gi" }} {{- if .Values.persistence.workDir.pvc.storageClass }} storageClassName: {{ .Values.persistence.workDir.pvc.storageClass }} {{- end }} @@ -46,7 +46,7 @@ spec: - {{ .Values.persistence.sharedWorkDir.pvc.accessMode | default "ReadWriteMany" }} resources: requests: - storage: {{ .Values.persistence.sharedWorkDir.pvc.size }} + storage: {{ .Values.persistence.sharedWorkDir.pvc.size | default "50Gi" }} {{- if .Values.persistence.sharedWorkDir.pvc.storageClass }} storageClassName: {{ .Values.persistence.sharedWorkDir.pvc.storageClass }} {{- end }} diff --git a/helm/templates/redis.yaml b/helm/templates/redis.yaml index 39623a807d..550bcc481d 100644 --- a/helm/templates/redis.yaml +++ b/helm/templates/redis.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if include "firefly.multiReplica" . }} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/helm/values.yaml b/helm/values.yaml index ee41c970da..db8aa65e7a 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,5 +1,6 @@ # -- Number of replicas. When > 1, Redis is automatically deployed and # session affinity annotations are injected into the ingress. +# Ignored when autoscaling.enabled is true — the HPA controls replica count in that case. replicaCount: 1 # -- Interval for cleaning up temporary files. @@ -48,9 +49,9 @@ livenessProbe: # -- Resource requests and limits. resources: requests: - memory: 256Mi + memory: 2Gi limits: - memory: 512Mi + memory: 4Gi # -- Additional environment variables. env: [] @@ -67,14 +68,14 @@ adminPassword: secretKey: "" # -- Persistence volume configuration. Each volume defaults to emptyDir. -# To use a new PVC: set pvc.size, pvc.storageClass, pvc.accessMode. +# To use a new PVC: set pvc.storageClass(Required), pvc.size[=50Gi], pvc.accessMode[=ReadWriteMany]. # To use an existing PVC: set existingClaim. persistence: # -- Work directory volume. workDir: {} # -- Logs directory volume. logsDir: {} - # -- Shared work directory volume. Use ReadWriteMany accessMode for multi-replica. + # -- Shared work directory volume. Required in multi-replica mode; must use ReadWriteMany accessMode. sharedWorkDir: {} # -- Pod security context. Shared by Firefly and Redis. When not set, containers @@ -95,6 +96,21 @@ redis: # -- Redis persistence. Falls back to sharedWorkDir PVC if configured, otherwise emptyDir. persistence: {} +autoscaling: + # -- Enable horizontal pod autoscaling. When enabled, Redis is automatically deployed + # and session affinity annotations are injected into the ingress. + enabled: false + # -- Minimum number of replicas. + minReplicas: 1 + # -- Maximum number of replicas. + maxReplicas: 3 + # -- Target average CPU utilization (%) across pods to trigger scale up. + targetCpuUtilization: 80 + # -- Stabilization window in seconds before scaling up. + scaleUpWindow: 60 + # -- Stabilization window in seconds before scaling down. + scaleDownWindow: 300 + serviceAccount: # -- Create a service account. create: false From 5c0b45584df81b807dce906cd2b883311ef6fb2d Mon Sep 17 00:00:00 2001 From: loi Date: Mon, 13 Apr 2026 14:47:21 -0700 Subject: [PATCH 3/3] Add ability to scale on memory as well --- helm/README.md | 7 ++++--- helm/README.md.gotmpl | 2 +- helm/templates/hpa.yaml | 12 +++++++++++- helm/values.yaml | 7 +++++-- src/firefly/js/ui/tap/TapViewType.jsx | 4 ++-- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/helm/README.md b/helm/README.md index 64267aa5ca..1e4060b3c6 100644 --- a/helm/README.md +++ b/helm/README.md @@ -60,7 +60,7 @@ autoscaling: enabled: true minReplicas: 1 maxReplicas: 3 - targetCpuUtilization: 80 + targetCpuUsage: 80 persistence: sharedWorkDir: @@ -168,7 +168,8 @@ Automatically deployed when `replicaCount > 1`. Consult `Reference Values` for c | autoscaling.minReplicas | int | `1` | Minimum number of replicas. | | autoscaling.scaleDownWindow | int | `300` | Stabilization window in seconds before scaling down. | | autoscaling.scaleUpWindow | int | `60` | Stabilization window in seconds before scaling up. | -| autoscaling.targetCpuUtilization | int | `80` | Target average CPU utilization (%) across pods to trigger scale up. | +| autoscaling.targetCpuUsage | int | `80` | Target average CPU utilization (%) across pods to trigger scale up. Set to empty to disable. | +| autoscaling.targetMemoryUsage | int | `100` | Target average memory utilization (%) across pods to trigger scale up. Set to empty to disable. | | cleanupInterval | string | `"1h"` | Interval for cleaning up temporary files. | | env | list | `[]` | Additional environment variables. | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. | @@ -191,7 +192,7 @@ Automatically deployed when `replicaCount > 1`. Consult `Reference Values` for c | redis.port | int | `6379` | Redis port. | | redis.resources | object | `{"limits":{"memory":"256Mi"}}` | Redis resource limits. | | replicaCount | int | `1` | Number of replicas. When > 1, Redis is automatically deployed and session affinity annotations are injected into the ingress. Ignored when autoscaling.enabled is true — the HPA controls replica count in that case. | -| resources | object | `{"limits":{"memory":"4Gi"},"requests":{"memory":"2Gi"}}` | Resource requests and limits. | +| resources | object | `{"limits":{"memory":"4Gi"},"requests":{"cpu":1,"memory":"2Gi"}}` | Resource requests and limits. | | securityContext | object | `{}` | Pod security context. Shared by Firefly and Redis. When not set, containers run as the default image user (e.g. tomcat(91)). | | service.port | int | `80` | Service port. | | service.sessionAffinity | string | `"ClientIP"` | Session affinity type. ClientIP is used as a fallback for non-ingress traffic. | diff --git a/helm/README.md.gotmpl b/helm/README.md.gotmpl index 9fd0177a15..19c37c262c 100644 --- a/helm/README.md.gotmpl +++ b/helm/README.md.gotmpl @@ -60,7 +60,7 @@ autoscaling: enabled: true minReplicas: 1 maxReplicas: 3 - targetCpuUtilization: 80 + targetCpuUsage: 80 persistence: sharedWorkDir: diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml index 49b8002fd2..8023f54043 100644 --- a/helm/templates/hpa.yaml +++ b/helm/templates/hpa.yaml @@ -13,12 +13,22 @@ spec: minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: + {{- if .Values.autoscaling.targetCpuUsage }} - type: Resource resource: name: cpu target: type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCpuUtilization }} + averageUtilization: {{ .Values.autoscaling.targetCpuUsage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUsage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUsage }} + {{- end }} behavior: scaleUp: stabilizationWindowSeconds: {{ .Values.autoscaling.scaleUpWindow }} diff --git a/helm/values.yaml b/helm/values.yaml index db8aa65e7a..f12a88dd1f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -50,6 +50,7 @@ livenessProbe: resources: requests: memory: 2Gi + cpu: 1 limits: memory: 4Gi @@ -104,8 +105,10 @@ autoscaling: minReplicas: 1 # -- Maximum number of replicas. maxReplicas: 3 - # -- Target average CPU utilization (%) across pods to trigger scale up. - targetCpuUtilization: 80 + # -- Target average CPU utilization (%) across pods to trigger scale up. Set to empty to disable. + targetCpuUsage: 80 + # -- Target average memory utilization (%) across pods to trigger scale up. Set to empty to disable. + targetMemoryUsage: 100 # -- Stabilization window in seconds before scaling up. scaleUpWindow: 60 # -- Stabilization window in seconds before scaling down. diff --git a/src/firefly/js/ui/tap/TapViewType.jsx b/src/firefly/js/ui/tap/TapViewType.jsx index e6a98421bc..a24542eac6 100644 --- a/src/firefly/js/ui/tap/TapViewType.jsx +++ b/src/firefly/js/ui/tap/TapViewType.jsx @@ -195,8 +195,8 @@ function BasicUI(props) { } else { const foundSchema= schemaOptions.find( (s) => { - if (schemaOptions.length===2) return s.value==='TAP_SCHEMA'; - else return s.value!=='TAP_SCHEMA' && s.value!=='ivoa'; + if (schemaOptions.length===2) return s.value?.upperCase() === 'TAP_SCHEMA'; + else return s.value?.upperCase() !== 'TAP_SCHEMA' && s.value!=='ivoa'; }); if (foundSchema) setSchemaName(foundSchema.value); }