diff --git a/.github/linters/ct.yaml b/.github/linters/ct.yaml index 0420d84..3b925ef 100644 --- a/.github/linters/ct.yaml +++ b/.github/linters/ct.yaml @@ -6,3 +6,4 @@ chart-dirs: helm-extra-args: --timeout 1000s chart-repos: - opensearch=https://opensearch-project.github.io/helm-charts + - ollama=https://otwld.github.io/ollama-helm diff --git a/.github/workflows/kubernetes-charts-build.yaml b/.github/workflows/kubernetes-charts-build.yaml index 7fe36ca..5519c12 100644 --- a/.github/workflows/kubernetes-charts-build.yaml +++ b/.github/workflows/kubernetes-charts-build.yaml @@ -30,7 +30,8 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4.3.1 - - uses: actions/setup-python@v6.0.0 + - name: Set up Python + uses: actions/setup-python@v6.0.0 with: python-version: "3.x" check-latest: true @@ -131,6 +132,7 @@ jobs: helm package ./medcat-service-helm --version ${{ steps.version.outputs.chart_version }} helm package ./medcat-trainer-helm --version ${{ steps.version.outputs.chart_version }} --dependency-update helm package ./cogstack-ce-helm --version ${{ steps.version.outputs.chart_version }} --dependency-update + helm package ./cogstack-cohorter-helm --version ${{ steps.version.outputs.chart_version }} --dependency-update helm package ./cogstack-observability-helm --version ${{ steps.version.outputs.chart_version }} --dependency-update - name: Helm OCI login to Docker Hub @@ -141,6 +143,7 @@ jobs: helm push ./medcat-service-helm-${{ steps.version.outputs.chart_version }}.tgz oci://registry-1.docker.io/cogstacksystems helm push ./medcat-trainer-helm-${{ steps.version.outputs.chart_version }}.tgz oci://registry-1.docker.io/cogstacksystems helm push ./cogstack-ce-helm-${{ steps.version.outputs.chart_version }}.tgz oci://registry-1.docker.io/cogstacksystems + helm push ./cogstack-cohorter-helm-${{ steps.version.outputs.chart_version }}.tgz oci://registry-1.docker.io/cogstacksystems helm push ./cogstack-observability-helm-${{ steps.version.outputs.chart_version }}.tgz oci://registry-1.docker.io/cogstacksystems - name: Release diff --git a/helm-charts/cogstack-cohorter-helm/.helmignore b/helm-charts/cogstack-cohorter-helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/.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/helm-charts/cogstack-cohorter-helm/Chart.lock b/helm-charts/cogstack-cohorter-helm/Chart.lock new file mode 100644 index 0000000..1f36690 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: medcat-service-helm + repository: oci://registry-1.docker.io/cogstacksystems + version: 0.0.1 +- name: ollama + repository: https://otwld.github.io/ollama-helm/ + version: 1.54.0 +digest: sha256:c19deffe7da9495af6f74b6a6bb26157bf7faf6abac6be8fc94638deff3bc6d3 +generated: "2026-04-16T14:51:41.6563526+01:00" diff --git a/helm-charts/cogstack-cohorter-helm/Chart.yaml b/helm-charts/cogstack-cohorter-helm/Chart.yaml new file mode 100644 index 0000000..d11cf5d --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v2 +name: cogstack-cohorter-helm +description: CogStack Cohorter — cohort identification powered by Ollama and MedCAT +type: application +version: 0.0.1 +appVersion: "latest" + +maintainers: + - name: jocelyneholdbrook + email: jocelyne@cogstack.org + +icon: "https://avatars.githubusercontent.com/u/28688163" + +dependencies: + # MedCAT annotation service + - name: medcat-service-helm + version: "0.0.1" + repository: "oci://registry-1.docker.io/cogstacksystems" + alias: medcat + condition: medcat.enabled + + # Ollama LLM serving — https://github.com/otwld/ollama-helm + - name: ollama + version: ">=0.1.0" + repository: "https://otwld.github.io/ollama-helm/" + alias: ollama + condition: ollama.enabled diff --git a/helm-charts/cogstack-cohorter-helm/README.md b/helm-charts/cogstack-cohorter-helm/README.md new file mode 100644 index 0000000..dc20e0e --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/README.md @@ -0,0 +1,225 @@ +# CogStack Cohorter Helm Chart + +CogStack Cohorter — cohort identification powered by Ollama and MedCAT + +## Architecture + +| Component | Image | Description | +|-----------|-------|-------------| +| **WebApp** | `cogstacksystems/cogstack-cohorter-webapp` | React + Node.js frontend and API | +| **NL2DSL** | `cogstacksystems/cogstack-cohorter-nl2dsl` | Natural language → cohort DSL compiler | +| **MedCAT** | `cogstacksystems/medcat-service` | Clinical NER and concept normalisation (subchart) | +| **Ollama** | `ollama/ollama` | LLM serving backend (subchart) | + +MedCAT and Ollama are deployed as **subcharts**: +- MedCAT: [`cogstacksystems/medcat-service-helm`](https://hub.docker.com/r/cogstacksystems/medcat-service-helm) (OCI) +- Ollama: [`otwld/ollama`](https://github.com/otwld/ollama-helm) + +## Prerequisites + +- Kubernetes 1.21+ +- Helm 3.10+ +- Sufficient node resources for the Ollama model (the default `gpt-oss:20b` requires ~14 GB of memory/VRAM) + +## Installation + +From Docker Hub OCI (published chart): + +```bash +helm install cogstack-cohorter oci://registry-1.docker.io/cogstacksystems/cogstack-cohorter-helm +``` + +## Configuration + +All configurable values are in [`values.yaml`](./values.yaml). Key sections: + +### Ollama + +```yaml +ollama: + enabled: true + ollama: + models: + pull: + - gpt-oss:20b # pulled automatically on first startup + persistentVolume: + enabled: true + size: 10Gi +``` + +Models are pulled automatically by the otwld subchart's built-in init container. Change `ollama.ollama.models.pull` to use a different model — make sure `nl2dsl.env.OLLAMA_MODEL` matches. + +### MedCAT + +```yaml +medcat: + enabled: true + env: + APP_MEDCAT_MODEL_PACK: "/cat/models/examples/example-medcat-v2-model-pack.zip" +``` + +To use a custom model pack, provide a download URL: + +```yaml +medcat: + model: + downloadUrl: "https://your-host/medcat_model_pack.zip" + name: "medcat_model_pack.zip" +``` + +### WebApp data volume + +The WebApp requires a SNOMED data directory mounted at `/usr/src/app/server/data`. A PVC is provisioned automatically: + +```yaml +webapp: + persistence: + enabled: true + size: 5Gi +``` + +Populate the PVC with either: +- `snomed_terms_data.tar.gz` — auto-extracted by the entrypoint on first startup, or +- Pre-extracted files: `snomed_terms.json`, `cui_pt2ch.json`, and patient data files + +To generate synthetic patient data on first startup (demo mode): + +```yaml +webapp: + env: + RANDOM_DATA: "true" +``` + +### Ingress + +```yaml +ingress: + enabled: true + className: nginx + hosts: + - host: cohorter.example.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: cohorter-tls + hosts: + - cohorter.example.com +``` + +### Autoscaling (webapp only) + +```yaml +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 +``` + +## Uninstallation + +```bash +helm uninstall cogstack-cohorter +``` + +## Support + +For issues and questions, please visit the [CogStack GitHub repository](https://github.com/CogStack/cogstack-platform). + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://otwld.github.io/ollama-helm/ | ollama(ollama) | >=0.1.0 | +| oci://registry-1.docker.io/cogstacksystems | medcat(medcat-service-helm) | 0.0.1 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| autoscaling.enabled | bool | `false` | | +| autoscaling.maxReplicas | int | `3` | | +| autoscaling.minReplicas | int | `1` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| fullnameOverride | string | `""` | | +| global.imagePullSecrets | list | `[]` | | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `""` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"cogstack-cohort.local"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | +| ingress.tls | list | `[]` | | +| medcat.enabled | bool | `true` | | +| medcat.env.APP_ENABLE_METRICS | string | `"true"` | | +| medcat.env.APP_MEDCAT_MODEL_PACK | string | `"/cat/models/examples/example-medcat-v2-model-pack.zip"` | | +| medcat.image.tag | string | `"latest"` | | +| medcat.resources | object | `{}` | | +| medcat.service.port | int | `5000` | | +| nameOverride | string | `""` | | +| nl2dsl.affinity | object | `{}` | | +| nl2dsl.enabled | bool | `true` | | +| nl2dsl.env.ALLOW_ORIGINS | string | `"*"` | | +| nl2dsl.env.OLLAMA_MODEL | string | `"gpt-oss:20b"` | | +| nl2dsl.image.pullPolicy | string | `"IfNotPresent"` | | +| nl2dsl.image.repository | string | `"cogstacksystems/cogstack-cohorter-nl2dsl"` | | +| nl2dsl.image.tag | string | `"latest"` | | +| nl2dsl.livenessProbe.httpGet.path | string | `"/"` | | +| nl2dsl.livenessProbe.httpGet.port | string | `"http"` | | +| nl2dsl.livenessProbe.initialDelaySeconds | int | `30` | | +| nl2dsl.livenessProbe.periodSeconds | int | `10` | | +| nl2dsl.nodeSelector | object | `{}` | | +| nl2dsl.readinessProbe.httpGet.path | string | `"/"` | | +| nl2dsl.readinessProbe.httpGet.port | string | `"http"` | | +| nl2dsl.readinessProbe.initialDelaySeconds | int | `10` | | +| nl2dsl.readinessProbe.periodSeconds | int | `5` | | +| nl2dsl.replicaCount | int | `1` | | +| nl2dsl.resources | object | `{}` | | +| nl2dsl.service.port | int | `3002` | | +| nl2dsl.service.type | string | `"ClusterIP"` | | +| nl2dsl.tolerations | list | `[]` | | +| ollama.enabled | bool | `true` | | +| ollama.ollama.models.pull[0] | string | `"gpt-oss:20b"` | | +| ollama.persistentVolume.enabled | bool | `true` | | +| ollama.persistentVolume.size | string | `"10Gi"` | | +| ollama.persistentVolume.storageClass | string | `""` | | +| ollama.resources | object | `{}` | | +| ollama.service.port | int | `11434` | | +| ollama.service.type | string | `"ClusterIP"` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| securityContext | object | `{}` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.automount | bool | `true` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | +| webapp.affinity | object | `{}` | | +| webapp.enabled | bool | `true` | | +| webapp.env.RANDOM_DATA | string | `"false"` | | +| webapp.image.pullPolicy | string | `"IfNotPresent"` | | +| webapp.image.repository | string | `"cogstacksystems/cogstack-cohorter-webapp"` | | +| webapp.image.tag | string | `"latest"` | | +| webapp.livenessProbe.httpGet.path | string | `"/"` | | +| webapp.livenessProbe.httpGet.port | string | `"http"` | | +| webapp.livenessProbe.initialDelaySeconds | int | `60` | | +| webapp.livenessProbe.periodSeconds | int | `15` | | +| webapp.nodeSelector | object | `{}` | | +| webapp.persistence.accessMode | string | `"ReadWriteOnce"` | | +| webapp.persistence.enabled | bool | `true` | | +| webapp.persistence.existingClaim | string | `""` | | +| webapp.persistence.size | string | `"5Gi"` | | +| webapp.persistence.storageClass | string | `""` | | +| webapp.readinessProbe.httpGet.path | string | `"/"` | | +| webapp.readinessProbe.httpGet.port | string | `"http"` | | +| webapp.readinessProbe.initialDelaySeconds | int | `30` | | +| webapp.readinessProbe.periodSeconds | int | `10` | | +| webapp.replicaCount | int | `1` | | +| webapp.resources | object | `{}` | | +| webapp.service.port | int | `3000` | | +| webapp.service.type | string | `"ClusterIP"` | | +| webapp.tolerations | list | `[]` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/helm-charts/cogstack-cohorter-helm/README.md.gotmpl b/helm-charts/cogstack-cohorter-helm/README.md.gotmpl new file mode 100644 index 0000000..beea0c7 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/README.md.gotmpl @@ -0,0 +1,137 @@ +# CogStack Cohorter Helm Chart + +{{ template "chart.description" . }} + +{{ template "chart.homepageLine" . }} + +## Architecture + +| Component | Image | Description | +|-----------|-------|-------------| +| **WebApp** | `cogstacksystems/cogstack-cohorter-webapp` | React + Node.js frontend and API | +| **NL2DSL** | `cogstacksystems/cogstack-cohorter-nl2dsl` | Natural language → cohort DSL compiler | +| **MedCAT** | `cogstacksystems/medcat-service` | Clinical NER and concept normalisation (subchart) | +| **Ollama** | `ollama/ollama` | LLM serving backend (subchart) | + +MedCAT and Ollama are deployed as **subcharts**: +- MedCAT: [`cogstacksystems/medcat-service-helm`](https://hub.docker.com/r/cogstacksystems/medcat-service-helm) (OCI) +- Ollama: [`otwld/ollama`](https://github.com/otwld/ollama-helm) + +## Prerequisites + +- Kubernetes 1.21+ +- Helm 3.10+ +- A storage class that supports `ReadWriteOnce` PVCs +- Sufficient node resources for the Ollama model (the default `gpt-oss:20b` requires ~14 GB of memory/VRAM) + +## Installation + +From Docker Hub OCI (published chart): + +```bash +helm install cogstack-cohorter oci://registry-1.docker.io/cogstacksystems/cogstack-cohorter-helm +``` + +## Configuration + +All configurable values are in [`values.yaml`](./values.yaml). Key sections: + +### Ollama + +```yaml +ollama: + enabled: true + ollama: + models: + pull: + - gpt-oss:20b # pulled automatically on first startup + persistentVolume: + enabled: true + size: 10Gi +``` + +Models are pulled automatically by the otwld subchart's built-in init container. Change `ollama.ollama.models.pull` to use a different model — make sure `nl2dsl.env.OLLAMA_MODEL` matches. + +### MedCAT + +```yaml +medcat: + enabled: true + env: + APP_MEDCAT_MODEL_PACK: "/cat/models/examples/example-medcat-v2-model-pack.zip" +``` + +To use a custom model pack, provide a download URL: + +```yaml +medcat: + model: + downloadUrl: "https://your-host/medcat_model_pack.zip" + name: "medcat_model_pack.zip" +``` + +### WebApp data volume + +The WebApp requires a SNOMED data directory mounted at `/usr/src/app/server/data`. A PVC is provisioned automatically: + +```yaml +webapp: + persistence: + enabled: true + size: 5Gi +``` + +Populate the PVC with either: +- `snomed_terms_data.tar.gz` — auto-extracted by the entrypoint on first startup, or +- Pre-extracted files: `snomed_terms.json`, `cui_pt2ch.json`, and patient data files + +To generate synthetic patient data on first startup (demo mode): + +```yaml +webapp: + env: + RANDOM_DATA: "true" +``` + +### Ingress + +```yaml +ingress: + enabled: true + className: nginx + hosts: + - host: cohorter.example.com + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: cohorter-tls + hosts: + - cohorter.example.com +``` + +### Autoscaling (webapp only) + +```yaml +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 +``` + +## Uninstallation + +```bash +helm uninstall cogstack-cohorter +``` + +## Support + +For issues and questions, please visit the [CogStack GitHub repository](https://github.com/CogStack/cogstack-platform). + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/helm-charts/cogstack-cohorter-helm/ci/ci-values.yaml b/helm-charts/cogstack-cohorter-helm/ci/ci-values.yaml new file mode 100644 index 0000000..786fd3f --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/ci/ci-values.yaml @@ -0,0 +1,36 @@ +# CI smoke-test overrides. +# An init container seeds the empty data volume with minimal stub SNOMED data +# so the webapp entrypoint can proceed and RANDOM_DATA=true can generate +# synthetic patient records without requiring a real data mount. +webapp: + env: + RANDOM_DATA: "true" + persistence: + enabled: false + initContainers: + - name: init-snomed-stub + image: busybox + command: + - sh + - -c + - | + mkdir -p /data + # Minimal snomed_terms.json — a few entries covering each clinical + # category that gen_random_data.js filters on. + cat > /data/snomed_terms.json << 'EOF' + [ + {"cui":"73211009","str":"Diabetes mellitus (disorder)"}, + {"cui":"44054006","str":"Diabetes mellitus type 2 (disorder)"}, + {"cui":"38341003","str":"Hypertensive disorder (disorder)"}, + {"cui":"195967001","str":"Asthma (disorder)"}, + {"cui":"271807003","str":"Eruption of skin (finding)"}, + {"cui":"386661006","str":"Fever (finding)"}, + {"cui":"80146002","str":"Appendectomy (procedure)"}, + {"cui":"387517004","str":"Paracetamol (substance)"} + ] + EOF + # cui_pt2ch.json — empty hierarchy is valid; server handles missing keys + echo '{}' > /data/cui_pt2ch.json + volumeMounts: + - name: data + mountPath: /data diff --git a/helm-charts/cogstack-cohorter-helm/templates/NOTES.txt b/helm-charts/cogstack-cohorter-helm/templates/NOTES.txt new file mode 100644 index 0000000..5b03841 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/NOTES.txt @@ -0,0 +1,46 @@ +CogStack Cohort has been deployed. + +Components: +{{- if .Values.webapp.enabled }} + - WebApp: {{ include "cogstack-cohorter-helm.fullname" . }}-webapp (port {{ .Values.webapp.service.port }}) +{{- end }} +{{- if .Values.nl2dsl.enabled }} + - NL2DSL: {{ include "cogstack-cohorter-helm.fullname" . }}-nl2dsl (port {{ .Values.nl2dsl.service.port }}) +{{- end }} +{{- if .Values.medcat.enabled }} + - MedCAT: {{ include "cogstack-cohorter-helm.medcatServiceName" . }} (port {{ .Values.medcat.service.port }}) [subchart: medcat-service-helm] +{{- end }} +{{- if .Values.ollama.enabled }} + - Ollama: {{ include "cogstack-cohorter-helm.ollamaServiceName" . }} (port {{ .Values.ollama.service.port }}) [subchart: otwld/ollama] + Models pulled on startup: + {{- range .Values.ollama.ollama.models.pull }} + - {{ . }} + {{- end }} +{{- end }} + +WebApp data volume: +{{- if .Values.webapp.persistence.enabled }} + A PersistentVolumeClaim is provisioned for the SNOMED data directory. + Populate it with snomed_terms_data.tar.gz (auto-extracted on startup) + or pre-extracted files (snomed_terms.json, cui_pt2ch.json, etc.). +{{- else }} + WARNING: persistence is disabled — data will be lost on pod restart. + Enable webapp.persistence for production use. +{{- end }} + +Access the WebApp: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else if contains "NodePort" .Values.webapp.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "cogstack-cohorter-helm.fullname" . }}-webapp) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.webapp.service.type }} + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "cogstack-cohorter-helm.fullname" . }}-webapp --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.webapp.service.port }} +{{- else }} + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "cogstack-cohorter-helm.fullname" . }}-webapp {{ .Values.webapp.service.port }}:{{ .Values.webapp.service.port }} + Then open: http://localhost:{{ .Values.webapp.service.port }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/_helpers.tpl b/helm-charts/cogstack-cohorter-helm/templates/_helpers.tpl new file mode 100644 index 0000000..2967786 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/_helpers.tpl @@ -0,0 +1,79 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cogstack-cohorter-helm.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cogstack-cohorter-helm.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 "cogstack-cohorter-helm.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cogstack-cohorter-helm.labels" -}} +helm.sh/chart: {{ include "cogstack-cohorter-helm.chart" . }} +{{ include "cogstack-cohorter-helm.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cogstack-cohorter-helm.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cogstack-cohorter-helm.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cogstack-cohorter-helm.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cogstack-cohorter-helm.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + + +{{/* +Fully-qualified service name for the ollama subchart. +The otwld/ollama chart names its service -ollama. +*/}} +{{- define "cogstack-cohorter-helm.ollamaServiceName" -}} +{{- printf "%s-ollama" .Release.Name }} +{{- end }} + +{{/* +Fully-qualified service name for the medcat subchart. +The medcat-service-helm chart names its service -medcat. +*/}} +{{- define "cogstack-cohorter-helm.medcatServiceName" -}} +{{- printf "%s-medcat" .Release.Name }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/hpa.yaml b/helm-charts/cogstack-cohorter-helm/templates/hpa.yaml new file mode 100644 index 0000000..2d51d37 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "cogstack-cohorter-helm.fullname" . }} + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "cogstack-cohorter-helm.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/httproute.yaml b/helm-charts/cogstack-cohorter-helm/templates/httproute.yaml new file mode 100644 index 0000000..9299a03 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "cogstack-cohorter-helm.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/ingress.yaml b/helm-charts/cogstack-cohorter-helm/templates/ingress.yaml new file mode 100644 index 0000000..3fb07ef --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "cogstack-cohorter-helm.fullname" . }} + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- 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 }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "cogstack-cohorter-helm.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} + {{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/nl2dsl-deployment.yaml b/helm-charts/cogstack-cohorter-helm/templates/nl2dsl-deployment.yaml new file mode 100644 index 0000000..421e281 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/nl2dsl-deployment.yaml @@ -0,0 +1,79 @@ +{{- if .Values.nl2dsl.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "cogstack-cohorter-helm.fullname" . }}-nl2dsl + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.nl2dsl.replicaCount }} + selector: + matchLabels: + {{- include "cogstack-cohorter-helm.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "cogstack-cohorter-helm.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: nl2dsl + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.nl2dsl.image.repository }}:{{ .Values.nl2dsl.image.tag }}" + imagePullPolicy: {{ .Values.nl2dsl.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.nl2dsl.service.port }} + protocol: TCP + env: + - name: OLLAMA_URL + value: "http://{{ include "cogstack-cohorter-helm.ollamaServiceName" . }}:{{ .Values.ollama.service.port }}/api/generate" + - name: MEDCAT_URL + value: "http://{{ include "cogstack-cohorter-helm.medcatServiceName" . }}:{{ .Values.medcat.service.port }}" + {{- range $key, $value := .Values.nl2dsl.env }} + - name: {{ $key | quote }} + value: {{ $value | quote }} + {{- end }} + {{- with .Values.nl2dsl.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.nl2dsl.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.nl2dsl.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.nl2dsl.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nl2dsl.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nl2dsl.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/nl2dsl-service.yaml b/helm-charts/cogstack-cohorter-helm/templates/nl2dsl-service.yaml new file mode 100644 index 0000000..376c5bb --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/nl2dsl-service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.nl2dsl.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cogstack-cohorter-helm.fullname" . }}-nl2dsl + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} +spec: + type: {{ .Values.nl2dsl.service.type }} + ports: + - port: {{ .Values.nl2dsl.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "cogstack-cohorter-helm.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/serviceaccount.yaml b/helm-charts/cogstack-cohorter-helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..5bf39c1 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "cogstack-cohorter-helm.serviceAccountName" . }} + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/tests/test-connection.yaml b/helm-charts/cogstack-cohorter-helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..afcbb0c --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "cogstack-cohorter-helm.fullname" . }}-test-connection" + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "cogstack-cohorter-helm.fullname" . }}-webapp:{{ .Values.webapp.service.port }}'] + restartPolicy: Never \ No newline at end of file diff --git a/helm-charts/cogstack-cohorter-helm/templates/webapp-deployment.yaml b/helm-charts/cogstack-cohorter-helm/templates/webapp-deployment.yaml new file mode 100644 index 0000000..7c72f46 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/webapp-deployment.yaml @@ -0,0 +1,95 @@ +{{- if .Values.webapp.enabled }} +{{- $fullName := include "cogstack-cohorter-helm.fullname" . }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $fullName }}-webapp + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.webapp.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "cogstack-cohorter-helm.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "cogstack-cohorter-helm.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.webapp.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: webapp + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.webapp.image.repository }}:{{ .Values.webapp.image.tag }}" + imagePullPolicy: {{ .Values.webapp.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.webapp.service.port }} + protocol: TCP + env: + - name: NL2DSL_SERVER + value: "http://{{ $fullName }}-nl2dsl:{{ .Values.nl2dsl.service.port }}/api/compile" + {{- range $key, $value := .Values.webapp.env }} + - name: {{ $key | quote }} + value: {{ $value | quote }} + {{- end }} + volumeMounts: + - name: data + mountPath: /usr/src/app/server/data + {{- with .Values.webapp.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.webapp.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.webapp.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + {{- if .Values.webapp.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.webapp.persistence.existingClaim | default (printf "%s-webapp-data" $fullName) }} + {{- else }} + emptyDir: {} + {{- end }} + {{- with .Values.webapp.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.webapp.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.webapp.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/webapp-pvc.yaml b/helm-charts/cogstack-cohorter-helm/templates/webapp-pvc.yaml new file mode 100644 index 0000000..2dde988 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/webapp-pvc.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.webapp.enabled .Values.webapp.persistence.enabled (not .Values.webapp.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "cogstack-cohorter-helm.fullname" . }}-webapp-data + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.webapp.persistence.accessMode }} + {{- if .Values.webapp.persistence.storageClass }} + storageClassName: {{ .Values.webapp.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.webapp.persistence.size }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/templates/webapp-service.yaml b/helm-charts/cogstack-cohorter-helm/templates/webapp-service.yaml new file mode 100644 index 0000000..403a915 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/templates/webapp-service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.webapp.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cogstack-cohorter-helm.fullname" . }}-webapp + labels: + {{- include "cogstack-cohorter-helm.labels" . | nindent 4 }} +spec: + type: {{ .Values.webapp.service.type }} + ports: + - port: {{ .Values.webapp.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "cogstack-cohorter-helm.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/helm-charts/cogstack-cohorter-helm/values.yaml b/helm-charts/cogstack-cohorter-helm/values.yaml new file mode 100644 index 0000000..dabbc86 --- /dev/null +++ b/helm-charts/cogstack-cohorter-helm/values.yaml @@ -0,0 +1,274 @@ +# Default values for cogstack-cohorter-helm. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# --------------------------------------------------------------------------- +# Global +# --------------------------------------------------------------------------- +global: + # Image pull secrets shared across all components (including subcharts). + imagePullSecrets: [] + +# --------------------------------------------------------------------------- +# Service account +# More information: https://kubernetes.io/docs/concepts/security/service-accounts/ +# --------------------------------------------------------------------------- +serviceAccount: + # Specifies whether a service account should be created. + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account. + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template. + name: "" + +# --------------------------------------------------------------------------- +# Chart name overrides +# --------------------------------------------------------------------------- +nameOverride: "cogstack-cohorter" +fullnameOverride: "" + +# --------------------------------------------------------------------------- +# Pod-level settings (shared by webapp and nl2dsl pods) +# --------------------------------------------------------------------------- +podAnnotations: {} +podLabels: {} +podSecurityContext: {} + # fsGroup: 2000 +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# --------------------------------------------------------------------------- +# Autoscaling (webapp only) +# More information: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +# --------------------------------------------------------------------------- +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# --------------------------------------------------------------------------- +# Ingress +# More information: https://kubernetes.io/docs/concepts/services-networking/ingress/ +# --------------------------------------------------------------------------- +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: cogstack-cohort.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: cogstack-cohort-tls + # hosts: + # - cogstack-cohort.local + +# --------------------------------------------------------------------------- +# Gateway API HTTPRoute (alternative to Ingress) +# Requires Gateway API resources and a suitable controller installed in the cluster. +# See: https://gateway-api.sigs.k8s.io/guides/ +# --------------------------------------------------------------------------- +httpRoute: + enabled: false + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + hostnames: + - chart-example.local + rules: + - matches: + - path: + type: PathPrefix + value: / + +# --------------------------------------------------------------------------- +# WebApp — React + Node.js frontend and REST API +# --------------------------------------------------------------------------- +webapp: + # -- Enable the WebApp deployment and service. + enabled: true + + replicaCount: 1 + + image: + repository: cogstacksystems/cogstack-cohorter-webapp + tag: latest + pullPolicy: IfNotPresent + + # Environment variables injected into the webapp container. + # NL2DSL_SERVER is set automatically from nl2dsl.service.port. + env: + # Set to "true" to generate synthetic patient data on first startup (demo mode). + RANDOM_DATA: "false" + + service: + type: ClusterIP + port: 3000 + + # Persistent volume for SNOMED data directory (/usr/src/app/server/data). + # Populate the PVC with snomed_terms_data.tar.gz (auto-extracted on startup) + # or the pre-extracted files (snomed_terms.json, cui_pt2ch.json, patient data). + persistence: + enabled: true + # Use an existing PVC instead of creating a new one. + existingClaim: "" + storageClass: "" + accessMode: ReadWriteOnce + size: 5Gi + + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 60 + periodSeconds: 15 + + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + + resources: {} + # limits: + # cpu: 500m + # memory: 512Mi + # requests: + # cpu: 250m + # memory: 256Mi + + # Optional init containers run before the webapp container starts. + # Useful for seeding the data volume (see ci/ci-values.yaml for an example). + initContainers: [] + + nodeSelector: {} + tolerations: [] + affinity: {} + +# --------------------------------------------------------------------------- +# NL2DSL — natural language → cohort DSL compiler (backed by Ollama + MedCAT) +# --------------------------------------------------------------------------- +nl2dsl: + # -- Enable the NL2DSL deployment and service. + enabled: true + + replicaCount: 1 + + image: + repository: cogstacksystems/cogstack-cohorter-nl2dsl + tag: latest + pullPolicy: IfNotPresent + + # Environment variables injected into the nl2dsl container. + # OLLAMA_URL and MEDCAT_URL are set automatically from subchart service names. + env: + # Ollama model to use for NL → DSL compilation. + OLLAMA_MODEL: "gpt-oss:20b" + # CORS origins allowed to call the NL2DSL API. + ALLOW_ORIGINS: "*" + + service: + type: ClusterIP + port: 3002 + + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + + resources: {} + # limits: + # cpu: 500m + # memory: 512Mi + # requests: + # cpu: 250m + # memory: 256Mi + + nodeSelector: {} + tolerations: [] + affinity: {} + +# --------------------------------------------------------------------------- +# MedCAT subchart — clinical NER and concept normalisation +# Source: oci://registry-1.docker.io/cogstacksystems/medcat-service-helm +# Full list of medcat-service-helm values: +# https://hub.docker.com/r/cogstacksystems/medcat-service-helm +# --------------------------------------------------------------------------- +medcat: + # -- Enable the MedCAT subchart. + enabled: true + + image: + tag: latest + + env: + # Path to the MedCAT model pack inside the container. + APP_MEDCAT_MODEL_PACK: "/cat/models/examples/example-medcat-v2-model-pack.zip" + APP_ENABLE_METRICS: "true" + + # To download a custom model pack at startup: + # model: + # downloadUrl: "https://your-host/medcat_model_pack.zip" + # name: "medcat_model_pack.zip" + + service: + port: 5000 + + resources: {} + +# --------------------------------------------------------------------------- +# Ollama subchart — LLM serving backend +# Source: https://github.com/otwld/ollama-helm +# Full list of otwld/ollama values: +# https://github.com/otwld/ollama-helm/blob/main/charts/ollama/values.yaml +# --------------------------------------------------------------------------- +ollama: + # -- Enable the Ollama subchart. + enabled: true + + ollama: + # Models pulled automatically by the built-in init container on first startup. + # Ensure nl2dsl.env.OLLAMA_MODEL matches the model listed here. + models: + pull: + - gpt-oss:20b + + persistentVolume: + enabled: true + storageClass: "" + size: 10Gi + + service: + type: ClusterIP + port: 11434 + + resources: {} + # GPU example: + # limits: + # nvidia.com/gpu: 1