Skip to content

[Feature]: Merge flip-omop-db into the monorepo as the trust/omop-db build source - #837

Merged
atriaybagur merged 28 commits into
developfrom
834-merge-flip-omop-db
Aug 11, 2026
Merged

[Feature]: Merge flip-omop-db into the monorepo as the trust/omop-db build source#837
atriaybagur merged 28 commits into
developfrom
834-merge-flip-omop-db

Conversation

@atriaybagur

@atriaybagur atriaybagur commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes #834
Implements #842 and #843 (folded in by request — each keeps one residual org-admin action, tracked there).

What

Merges the private flip-omop-db repo into the monorepo as trust/omop-db/ — and takes the opportunity to make every published OMOP artifact licence-clean: the image, the pgdata tarballs and the HF dataset now carry no licensed vocabulary content at all.

Snapshot import, not a history merge: one verbatim commit at flip-omop-db main (5cf75d4); the private history (personal identifiers, private-repo references, no DCO) stays archived in the private repo.

The vocab-free architecture (#842 + #843)

The core OMOP vocabulary (an OHDSI Athena export: SNOMED CT Int/US/UK, LOINC 2.77, NHS Read v2, dm+d, … — 59-vocabulary roster table in the README) is licensed material, and it was 99.8% of the published payload: the old image carried a 3.6 GB baked bundle and each 2.2 GB pgdata tarball was ~8 GB of vocab tables around ~15 MB of synthetic cohort. Now:

  • Image (Dockerfile): schema init chain + read-only roles + seed-time helpers only. No data inputs, no credentials → CI-published via the new test-gated docker_build_omop_db.yml (same workflow_run pattern as the other services), and ~4.3 GB → ~450 MB.
  • pgdata tarballs: vocab-free — schema + synthetic cohort + the freely-redistributable DICOM vocab. 2.2 GB → ~11 MB each; published to the public HF dataset as .data_version 20260729 (built via make populate CORE_VOCAB=0 + make export-pgdata).
  • One uniform seeding step (files/load_core_vocab.sh): streams the licensed bundle into a running database over TCP (COPY FROM STDIN — no mounts, no server-side files, no image layers) and applies the FK constraints; core-aware idempotency guards tolerate the DICOM vocab already present in the tarballs. Wired into all three environments: dev make load-omop-vocab, EC2 via a new Ansible play inside seed-trust-data (throwaway container on loopback, kit credentials passed by the AWS Makefile), Kubernetes via the chart's omop-vocab-load post-install/upgrade hook Job (omopDb.vocabLoad values + schema).
  • Vocab acquisition — two licence-proper paths only (README "The core vocabulary bundle"): org members make fetch-vocab-core from s3://flipdev-aicentre/vocab/ (VOCAB_S3_BUCKET parameterised, temp+rename robustness); everyone else self-serves an equivalent export from OHDSI Athena under their own licences (NHS TRUD pointer for UK editions). The image-extraction fallback is removed.
  • The DICOM bundle is a byte-for-byte copy of four files from DICOM2OMOP's files/OMOP CDM Staging/ at upstream commit 1ef3354 (verified by content diff; Apache 2.0; pinned in THIRD_PARTY_NOTICES), fetched anonymously from HF with the upstream pickle converted to CSV — no pickle deserialisation anywhere.

Also in this PR

  • Producer + consumer merged: OMOP CDM 5.4 + MI-CDM DDL/PK/index/constraint SQL, readonly-role SQL, build compose and populate tooling join the existing harness; .data_version path untouched (AWS Makefile hardcodes it).
  • Canonical mock dataset + N-trust split (omop_db_tools.dataset): the synthetic per-trust CSVs (verified: FAK… accession IDs, fake NHS numbers) live as ONE dataset on HF (omop-csv/20260729/, source_trust provenance). legacy mode reproduces the original two-trust membership exactly (frame-equal, keeps accession IDs consistent with the mock Orthanc volumes); modulo partitions person_id % N. Two-trust build stack; N>2 fails fast until a compose service + port exist.
  • Security: no baked build-arg credentials; SQL identifiers validated; sqlalchemy.URL.create escaping; build compose carries the runtime service's hardening (no-new-privileges, cap_drop: ALL + five caps — fresh initdb verified under it); OCI source labels re-link the GHCR package to this repo; data_analyst_reader rotation paths documented (password lives in pgdata, not the image).
  • Multi-agent review round applied on-branch (4 specialist agents): ON_ERROR_STOP on constraints, temp+rename fetches, NUM_TRUSTS-derived fan-out with port fail-fast, bounded first-boot readiness wait, gapped-source_trust and NaN-person_id guards, per-source asymmetry rejection in build_canonical, validate-before-destructive-clean, .env.build.example un-swallowed from trust/.gitignore, radiology_occurrenceimage_occurrence across data-access-api OpenAPI examples and trust docs. Unit tests 26 → 46.
  • CI: new Trust - OMOP DB CI (lint+mypy+unit) and docker_build_omop_db.yml; omop-db in root unit_test; uv-lock hook + UV_PROJECTS.
  • Docs: component-omop-database.rst expanded; README documents both vocab bundles (roster, provenance, acquisition) and the full build/populate/export/seed workflows; CLAUDE/AGENTS pairs throughout.

Verification (all live on this branch)

  • Phase A — vocab-free export build: populate CORE_VOCAB=0 + export-pgdata → 11.4 MB tarballs.
  • Phase B — runtime-seed simulation: seeded a DB from the published tarball, ran load_core_vocab.sh + constraints → 4,187 persons, 6,108,439 concepts (SNOMED 1,084,286 + DICOM 8,811 coexisting), 173 FKs, the user-guide's example JOIN omop.concept query returns 2,479 rows, data_analyst_reader write denied, loader re-run is a clean no-op.
  • Phase C — standard full populate: both trusts exact counts (4,187/4,186 persons, full SNOMED, 173 FKs).
  • Kubernetes: chart lints + renders (job present; absent under omopDb.enabled=false), and the kind E2E does a real cluster install — the StatefulSet + tarball init path are cluster-verified, and the vocab-load hook's machinery was exercised for real (it ran and failed on S3 auth in credential-less CI, as designed; it is disabled in the CI values). Not yet run to completion with real S3 credentials in a cluster — the loader script itself is identical to the host-side-proven one; verify the credentialed Job on the first real k8s deploy. Ansible play: live-verified end-to-end on a stag trust EC2 (targeted terraform create → provisioning → 11 MB tarball restore → instance-role S3 fetch → loader + constraints; on-box verification identical to the local numbers: 4,187 persons / 6.1 M concepts / 173 FKs / roles enforced). Found and fixed en route: per-env vocab bucket wiring, and bundles must be uploaded --sse AES256 (trust role lacks kms:Decrypt on bucket KMS keys — documented in the README).
  • pre-commit clean on the branch diff; omop-db tools 46 tests + data-access-api 151 tests green.
  • HF anonymous fetches verified: new tarballs, canonical CSVs, DICOM bundle.

Review round 3 (@7492916c) — the approving review's four items

All four were flagged non-blocking on the approval; each is fixed, and the same defect is swept where it recurred elsewhere.

  1. Attribution on the imported CDM SQL. The four OMOPCDM_postgresql_5.4_*.sql files carried a sole Copyright 2024 Guy's and St Thomas' header while THIRD_PARTY_NOTICES.md (added by this PR) states they derive from OHDSI CommonDataModel — so the header contradicted our own notices file, which makes this an attribution fix rather than a stale-year one. Rewritten in the dual-attribution form already used for vendored SQL at trust/xnat/postgres/XNAT.sql: upstream OHDSI copyright + SPDX, then FLIP modifications. Also drops the U+2019 smart quotes these four alone carried. Headers only — no SQL changed.
  2. OMOP_DB_TAG=latest. Correct — and this PR is what made it wrong, since the new CI publishing redefines :latest as the production image, so a dev kit ran omop-db from prod while its four siblings ran :stag. The composes now resolve ${OMOP_DB_TAG:-${DOCKER_TAG}}, keeping the per-service override for exactly the case XNAT_TAG exists for (omop-db changes rarely, so its branch tag is the one most likely to be absent). No trust/Makefile export needed — the kit reaches compose via --env-file and DOCKER_TAG is already exported for dev. Swept the same :latest default out of the Helm chart (omopDb.image.tagstag, matching all eight siblings; the tag is now load-bearing because the vocab-load Job runs the image's own loader) and the Ansible play, which now receives the env's DOCKER_TAG from the Makefile.
  3. Helm vocabLoad default. Gated the Job on s3Bucket and defaulted it empty, mirroring initJob's own empty-bucket idiom — no bucket ⇒ no Job ⇒ clean install. A public fallback is not legally available here (SNOMED CT / LOINC / Read v2 / dm+d), which is what makes the initJob pattern the right shape rather than a mirror. This also unhardcodes flipdev-aicentre, which a stag/prod EKS install could never read. Since "no vocabulary" is otherwise silent — cohort queries joining omop.concept return nothing while every probe stays green — the skip is announced in a new NOTES.txt, documented in the chart README (+ a troubleshooting entry), and sync_k8s_kit.py now emits the bucket from the kit's AICENTRE_BUCKET_NAME so the org path stays zero-touch.
  4. Ansible cleanup on failure. Wrapped the play in block/always. This also clears a retry trap the flat list had: the creates: guards key on the unpacked directory, so a re-run after a partial failure skipped both the download and the unzip and reused a possibly-truncated bundle. docker rm -f gets failed_when: false, since in an always: it runs even when the container never started and would otherwise fail the play it exists to clean up after.

Behaviour in the wild is unchanged by item 2: OMOP_DB_TAG is not in HUB_SHARED_KEYS, so sync-trust-kit never strips it — existing kits keep their literal OMOP_DB_TAG=latest line and resolve exactly as before. Only kits newly scaffolded by make new-trust pick up the new default.

Verified on the branch: helm lint; the template gate matrix (no bucket / bucket set / enabled=false / CI values); the NOTES warning fires only when the vocabulary is genuinely absent; compose resolves omop-db:stag beside its four siblings and still honours a pinned OMOP_DB_TAG; ansible-playbook --syntax-check + --list-tasks; prettier (the one hard CI gate on deploy/providers/AWS/*.yml); k8s sync tests 13 passed; omop-db ruff + mypy + 47 unit tests. The Ansible path is not re-run live — the stag trust EC2 used for the original verification was destroyed after that round; the changed play is otherwise identical to the live-verified one.

Post-merge admin actions (kept open on #842/#843)

  1. [Task]: Rebuild omop-db image vocab-free; enable CI publish; purge vocab-bearing package versions #842: delete the old vocab-bearing GHCR package versions once the first CI-published vocab-free image is live (:latest is replaced on the first main publish; historical sha-pinned versions need manual deletion). Note the currently published tags are latest, trust1, trust2, stag — all hand-pushed, all predating the vocab-free rework, so all still carry the licensed bundle.
  2. [Task]: Decide licensing posture of the public HF pgdata tarballs (licensed vocabularies inside) #843: delete the old trust<N>_pgdata_20260129.tar (2.2 GB, vocab-bearing) files from the public HF dataset once deployments are on 20260729.
  3. Archive londonaicentre/flip-omop-db with a pointer README; org-admin confirm the GHCR package re-links to FLIP after the first labelled push.
  4. Seed a vocab-free :stag: gh workflow run docker_build_omop_db.yml --ref develop once after merge. CI has never published this image, and the workflow_run gate does not fire retroactively on the merge that introduces it (workflow_dispatch bypasses the gate). Until then :stag is the old hand-pushed image, which lacks load_core_vocab.sh. :prod does not exist yet and is created on the first main promotion.

Note for reviewers: existing dev environments will download the new 11 MB volumes on their next update-omop-data (version bump) and must then run make load-omop-vocab once per trust — called out in the README.

develop merged in (@8323dba7) + review round 4 (@f64446ea)

Merge. origin/develop (147 commits) merged in; 6 conflicts. Only one had substance:
trust/data-access-api/tests/routers/test_cohort.py#839 deleted the _parse_and_emit helper this
branch's side still called, so the resolution takes develop's validate_query shape carrying this
branch's radiology_occurrenceimage_occurrence rename (that test held the last such reference in
the tree; image_occurrence was already canonical at the merge-base, in the trust-api integration
fixtures). The other five were unions: test_helm_chart.yml (this branch's two vocab-load gates +
develop's FLIP-PT-091 orthanc gate) and both CLAUDE/AGENTS pairs. No test was lost — verified by
enumerating every def test_* across both parents and the merge.

Review round 4. A full-diff review of this PR found 11 defects; each was reproduced locally
before being fixed, and four were downgraded from their initial severity after re-checking.

Deployment

  1. make ansible-init — and so full-deploy — failed outright. The new vocab-load play asserts
    omop_postgres_password, which only seed-trust-data passes. ansible-init runs before
    register-trusts in every chain, has no KIT in scope (hence its trust_num fallback to 1) and
    structurally cannot supply it, so passing the vars there is not an option — the play is now
    excluded via the omop-vocab tag it already carries. Verified: --list-tasks --skip-tags omop-vocab selects zero tasks from that play, --tags data still selects all nine.
  2. Licensed content could be left on the host. A failing task in Ansible's always: aborts the
    remaining ones (verified), so with the container removal ahead of it a docker rm against a dead
    daemon skipped the ~3.6 GB bundle deletion. Deletion now runs first.
  3. creates: guards reused a truncated bundle — both keyed on the unpacked directory, so an
    interrupted unzip made the next run skip download and unzip. Both stages now stage into a temp
    path and rename on success, matching fetch-vocab-core.

CI
4. The uv-lock cooldown gate never checked trust/omop-dbsecret-scanning.yml hardcodes a
second copy of the project list that this PR didn't update, so the supply-chain backstop CLAUDE.md
names was silently absent for the new project.
5. Neither vocab-load render gate could fail. ! grep -q is exempt from set -e and worked only
by being the last line (appending any line disarms it — verified), and the absence assertion also
passed on an empty render. Both are now explicit if + ::error::, with a positive control.
6. test_trust_omop_db.yml lacked environment: flip, so CODECOV_TOKEN resolved empty and the
upload failed silently. (Unverified from the CLI — repo secret scoping isn't readable; harmless if
the token turns out to be repo-level.)

omop-db
7. load_core_vocab.sh swallowed a failed guard. A command substitution inside an if condition
is exempt from set -e, so a failed psql probe yielded "", compared unequal to "t" and fell
through to the COPY — silently duplicating rows in the four tables with no primary key. Now a
plain assignment plus a case that treats anything but t/f as fatal.
8. A degenerate NUM_TRUSTS (0, 00, empty, non-numeric) made populate /apply-constraints /
export-pgdata fan out to zero targets and exit 0 — populate would fetch the bundles and load
nothing. Guarded.
9. constraints.sql guard 18 reused guard 17's sentinel, so PAYER_PLAN_PERIOD's 10 FKs were
never applied on any run. Impact is ~nil (the table is unused), but the DO/IF NOT EXISTS
wrapper is a local addition — upstream OHDSI ships a flat list of plain ALTER TABLE
statements — so the typo is ours.

Docs — 10/11: the README claimed the vocabulary bundle was needed "not for running the trust
stacks", contradicting its own mandatory make load-omop-vocab step; gave those commands an explicit
-C; corrected a workflow comment still calling the vocabulary "baked".

Verified on the branch: omop-db ruff + mypy + 47 tests; bash -n; the NUM_TRUSTS guard across
9 edge cases; ansible-playbook --syntax-check + both tag selections; helm lint + every
helm-template gate, including proof the rewritten gates now fail when the Job is wrongly present
and when the render is empty; prettier on the gated AWS scope; full pre-commit on the diff.

Filed separately (all develop-side, none introduced here): #879 (cohort schema-allowlist
bypassable by dropping the qualifier — the one security finding, demonstrated), #880, #881, #882, #883.

Acceptance Criteria

Imported from issue #834

  • flip-omop-db source (OMOP CDM 5.4 DDL/PK/index/constraint SQL, readonly-role SQL, Dockerfile, populate tooling) lives under trust/omop-db/, snapshot-imported with FLIP conventions applied (Apache headers, ruff/mypy config, uv 72h cooldown, Makefile target contract, CLAUDE.md/AGENTS.md pair, uv-lock pre-commit entry, UV_PROJECTS)
  • Third-party attribution added (OHDSI CommonDataModel, DICOM2OMOP) and internal S3/private-repo references scrubbed
  • Mock CSVs published to the public HF dataset as one canonical set; populate tooling fetches it and deterministically partitions by person_id across N trusts, preserving referential integrity
  • Trust compose files use ${OMOP_DB_TAG} instead of hardcoded :latest; "built from" comments point at the in-repo path
  • Vocabularies remain outside git; manual image build documented (vocab licensing)
  • Docs updated: component-omop-database.rst expanded, stale radiology_occurrence references reconciled to image_occurrence, data-access-api cross-references point in-repo
  • Unit tests cover the split tooling; pre-commit and service lint/mypy/tests green

Verbatim import of the omop-db image build source from the private
londonaicentre/flip-omop-db repository (main @ 5cf75d4) into
trust/omop-db/: OMOP CDM 5.4 DDL/PK/index/constraint SQL, readonly-user
role SQL, image Dockerfile, build compose, populate scripts and uv
project files.

Excluded from the snapshot: mock-data CSVs (moving to the public
Hugging Face dataset), docs/omop.svg, .vscode/, .env.development, the
standalone Makefile (folded into the existing trust/omop-db/Makefile in
a follow-up commit), and the git history (contains personal identifiers
and private-repository references; the archived private repo remains
the historical record).

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
- Package layout: scripts become the omop-db-tools uv project
  (src/omop_db_tools/) with main() entry points, absolute imports, and
  Google-style docstrings; pyproject gains the FLIP ruff select list,
  mypy, the 72h uv exclude-newer cooldown, hatchling packaging and a
  regenerated lockfile; unused asyncpg dependency dropped.
- Security hardening (flagged by review of the snapshot commit): drop
  the POSTGRES_* build args from the Dockerfile (initdb only needs
  runtime env — nothing credential-shaped is baked into image config);
  validate SQL identifiers before interpolation in the insert/delete
  helpers; replace the pickled DICOM relationship frame with a CSV read
  (no pickle deserialisation; the published bundle ships CSVs only).
- New canonical-dataset module (dataset.py): build/fetch the single
  mock dataset (source_trust provenance column) and deterministically
  split it across N trusts — legacy mode reproduces the original
  two-trust membership (consistent with the published mock PACS data),
  modulo mode supports any trust count. 26 unit tests.
- Makefile: fold the private repo's build/populate/push pipeline into
  the existing harness Makefile (fetch-vocab-core extracts the licensed
  vocab bundle from the already-public image instead of org S3;
  fetch-vocab-dicom pulls the freely-redistributable DICOM bundle from
  Hugging Face) and add the standard lint/mypy/local_test contract.
- Build compose: volumes renamed trust1/trust2 -> Trust_1/Trust_2 to
  converge with the runtime harness paths; pgadmin behind an opt-in
  profile; image tag parameterised via OMOP_DB_TAG.
- Apache 2.0 headers on all imported files; OHDSI CommonDataModel and
  DICOM2OMOP entries added to THIRD_PARTY_NOTICES.md; internal S3
  bucket references scrubbed; README/CONTRIBUTING rewritten (stale
  docs removed); CLAUDE.md/AGENTS.md pair added; uv-lock pre-commit
  entry and root UV_PROJECTS registration.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
- Trust composes: omop-db image tag now ${OMOP_DB_TAG:-latest} (the kit
  files already declare OMOP_DB_TAG; it was previously unused) and the
  "built from" comments point at trust/omop-db instead of the private
  repo. No CI build workflow yet: the baked vocabulary is licensed
  material that cannot be fetched in CI, so image publishing stays a
  documented manual step (make build / make push).
- data-access-api: the SQL-defence doc comments now cite the in-repo
  trust/omop-db/files/create_readonly_users.sql path.
- Docs: component-omop-database.rst gains schema (MI-CDM extension),
  access-control and mocked-instance sections; the cohort-query guide's
  stale R-CDM radiology_occurrence references become the MI-CDM
  image_occurrence table actually shipped by the schema.
- Root/trust CLAUDE.md structure tables note the merged build source
  (AGENTS.md mirrors regenerated).

The canonical mock dataset (single dataset, source_trust provenance,
12 CSVs) and the pickle-free DICOM vocab bundle are published to the
public HF dataset aicentreflip/trust-data under omop-csv/20260729/ and
omop-vocab/ — make fetch-dataset / fetch-vocab-dicom pull them
anonymously (verified).

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
… bundles from build context)

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…on next push

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@github-actions github-actions Bot changed the title 834: Merge flip-omop-db into the monorepo as the trust/omop-db build source [Feature]: Merge flip-omop-db into the monorepo as the trust/omop-db build source Jul 29, 2026
@github-actions

Copy link
Copy Markdown

✅ Acceptance criteria have been automatically imported from the linked issue(s) and added to the PR description.

Correctness / silent-failure fixes:
- apply-constraints now runs psql with ON_ERROR_STOP=1 (a failed FK
  block previously left psql exiting 0, shipping pgdata without
  integrity) and fans out per trust like populate.
- fetch-vocab-core / fetch-vocab-dicom extract/download into a temp
  path and rename on success — a failed docker cp is no longer masked
  by the succeeding docker rm, and a partial copy/download can no
  longer satisfy the "already present" check on the next run.
- populate derives its fan-out from NUM_TRUSTS (was hardcoded to two,
  silently dropping slice 3 of a documented three-way example) and
  each populate-trust-<N> fails fast when OMOP_DB_PORT_TRUST_<N> is
  unset — an empty port previously made pg_isready fall back to 5432
  and report readiness of an unrelated server.
- wait-for-postgres.sh: rejects a missing port argument, checks
  pg_isready exists, and bounds the wait (120 attempts).
- up-build depends on build (no silent fallback to the published GHCR
  image); fetch-vocab-core extracts from VOCAB_SOURCE_TAG (default
  latest) instead of the tag being built; push asks for confirmation
  before repointing the default :latest.
- split_for_trust validates source_trust values are exactly 1..N (a
  gapped set previously dropped rows silently) and rejects NaN
  person_id rows (previously lost from every modulo partition).
- build_canonical requires trust dirs to exist and raises when a table
  is present in some sources but missing from others.
- import_tables validates all required CSVs up front (before the
  destructive clean), and refuses headers-only required tables.
- fetch_canonical names the version in 404 errors and rejects HTML
  responses (the /blob/-vs-/resolve/ Hugging Face trap).
- safe_insert reports inserted-vs-skipped counts and its docstring now
  states the real contract (CONCEPT_RELATIONSHIP has no unique
  constraint, so idempotency does not extend to re-runs); date parsing
  uses errors="raise" so a malformed date names the offending value.
- ensure_vocab_dir verifies the four expected CSVs after extraction or
  when accepting a pre-existing directory.
- config: build the DB URL with sqlalchemy URL.create so credentials
  containing @/:/ are escaped.

Repo integration:
- track .env.build.example (trust/.gitignore's .env.* was silently
  swallowing it — every documented entry point referenced an untracked
  file) via a negation rule.
- new "Trust - OMOP DB CI" workflow (lint+mypy+unit tests, path
  filtered) and omop-db added to the root unit_test target — the suite
  previously ran nowhere automated.
- data-access-api OpenAPI examples, tests and trust docs now use
  omop.image_occurrence: the imported DDL ships no radiology_occurrence
  table (MI-CDM superseded R-CDM); data-access-api suite re-run green.
- pgadmin gains no-new-privileges and a working documented invocation
  (--env-file .env.build); Apache header on .dockerignore.

Docs truthed against the code: two-trust build stack stated explicitly
(N>2 needs a compose service + port), pgdata tarball layout specified
(archive root = db_data contents), .data_version consumer chain
corrected (only the AWS Makefile reads the file), stale default-
passwords note replaced with the real rotation path, history-narrating
comments trimmed. 46 unit tests (was 26), all green.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…overridable)

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@github-actions

Copy link
Copy Markdown

✅ Acceptance criteria have been automatically imported from the linked issue(s) and added to the PR description.

…dles

fetch-vocab-core now downloads s3://$(VOCAB_S3_BUCKET)/vocab/ (default
flipdev-aicentre — the same bucket, key layout and technique the private
repo used) with temp+rename at both the download and unzip stages; the
image-extraction path moves to an explicit credential-free fallback,
fetch-vocab-core-from-image.

README documents what the bundles actually are: the core bundle is an
OHDSI Athena export (snapshot v5.0 30-AUG-24, 59 vocabularies, ~3.6 GB;
roster table with versions and licensing character — SNOMED CT
Int/US/UK, LOINC 2.77, NHS Read v2, dm+d, ICD family, RxNorm/NDC), and
the DICOM bundle is a byte-for-byte copy of four files from
DICOM2OMOP's files/OMOP CDM Staging/ at upstream commit 1ef3354
(verified by content diff; the relationship frame converted from pickle
to CSV for the published copy).

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…ole lacks kms:Decrypt)

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@github-actions

Copy link
Copy Markdown

✅ Acceptance criteria have been automatically imported from the linked issue(s) and added to the PR description.

…vocab

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
Comment thread deploy/providers/kubernetes/templates/omop-db-vocab-load-job.yaml

@garciadias garciadias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough migration - all seven acceptance-criteria items from #834 verified satisfied in substance, 37+ new unit tests, and a real reduction in what gets published (image 4.3GB→450MB, pgdata tarballs 2.2GB→11MB). Left one comment on a concrete gap in the new k8s vocab-load Job's local/kind-cluster credential path, which the PR body already flags as not yet run to completion with real credentials - this confirms that caveat with a specific fix.

Two minor items, not blocking on their own:

  • deploy/providers/AWS/Makefile:488 passes -e omop_postgres_password=... as a plaintext ansible-playbook CLI argument - the only secret-shaped -e in this file; every other credential is handled via env/unset rather than argv, which is visible to other local users via ps aux for the process lifetime. Worth passing it via environment: in the task or a cleaned-up temp extra-vars file instead.
  • trust/omop-db/files/OMOPCDM_postgresql_5.4_ddl.sql, _indices.sql, _primary_keys.sql, _constraints.sql all open with a copyright header in a different format from the rest of the tree (missing "(c)", missing "& King's College London", dated 2024 not 2026) - cosmetic, but worth aligning with the template the rest of this PR uses.

…tainer

The fetch-bundle initContainer gated AWS_PROFILE/AWS_DEFAULT_REGION on
omopDb.initJob.hostAwsMount.enabled but carried no matching host-aws
volume/volumeMount, so on a local/kind cluster the profile pointed at no
credentials file and the S3 fetch would fail. Mirror the volume pair the
sibling omop-db-init-job.yaml uses. Dormant on EKS (flag defaults off; IRSA).

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Comment thread trust/omop-db/files/OMOPCDM_postgresql_5.4_ddl.sql Outdated
Comment thread deploy/providers/kubernetes/values.yaml
Comment thread deploy/providers/AWS/site.yml Outdated
garciadias
garciadias previously approved these changes Aug 4, 2026

@garciadias garciadias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough, well-tested vendoring PR. I rebuilt the image from scratch, ran the full init chain (schema → PKs → indices → readonly-role creation), confirmed the read-only role can SELECT but not INSERT (defence-in-depth against the cohort-query docstring claims), ran the new vocab loader twice against a synthetic bundle to confirm its idempotency guards actually work, ran the full unit suite (47 passing, ruff/mypy clean), and rendered the Helm chart both with and without vocab-load enabled. Everything the PR claims held up under independent verification, and all CI checks are green.

One real gap worth a follow-up (doesn't need to block this merge): the dev/trust kit-file examples (trust/.env.example, trust/.env.GSTT.development.example, trust/.env.KCH.development.example) still default OMOP_DB_TAG=latest. Before this PR that was fine (omop-db had no CI publishing, so :latest just meant "whatever was last manually pushed"), but this PR adds the same :stag-on-develop/:prod-on-main CI tagging every other repo-built service uses — and dev environments elsewhere in the project track :stag. Left unchanged, freshly-scaffolded dev/trust stacks will now silently pull the prod-pinned image instead of tracking develop.

Left three minor inline comments (stale copyright header on the imported CDM SQL files, the vocab-load Helm default failing without org S3 credentials, and an Ansible cleanup task that isn't wrapped to run on failure). None of these block. Approving.

Four findings from the approving review, plus the same defect where it
recurred elsewhere.

Attribution: the four OMOPCDM_postgresql_5.4_*.sql files carried a sole
"Copyright 2024 Guy's and St Thomas'" header while THIRD_PARTY_NOTICES.md
(added by this PR) states they derive from OHDSI CommonDataModel — the
header contradicted our own notices file. Replaced with the dual-attribution
form already used for vendored SQL at trust/xnat/postgres/XNAT.sql: upstream
OHDSI copyright + SPDX, then FLIP modifications. Also drops the U+2019 smart
quotes these files alone carried. Headers only; no SQL changed.

Image tag: omop-db was the only trust image defaulting to :latest, which
this PR's new CI publishing redefines as the *production* image — so a dev
kit ran omop-db from prod while its four siblings ran :stag. The composes
now resolve ${OMOP_DB_TAG:-${DOCKER_TAG}}, keeping the per-service override
for the case XNAT_TAG exists for (omop-db changes rarely, so its branch tag
is the one most likely to be missing). No trust/Makefile export is needed —
the kit reaches compose via --env-file and DOCKER_TAG is already exported.
Existing kits keep their literal OMOP_DB_TAG=latest line and are unaffected;
only newly scaffolded kits pick up the new default. Swept the same :latest
default out of the Helm chart (every other service there is :stag, and the
tag is now load-bearing — the vocab-load Job runs the image's own loader)
and the Ansible play, which now takes the env's DOCKER_TAG from the Makefile.

Helm vocab-load: enabled:true against a hardcoded flipdev-aicentre meant a
default install failed its post-install hook — and a failed hook fails the
whole release — for anyone without org S3. The bundle is licensed
(SNOMED CT / LOINC / Read v2 / dm+d) so an initJob-style public fallback is
not legally available. Gated the Job on s3Bucket, mirroring initJob's own
empty-bucket idiom, and defaulted the bucket empty: no bucket, no Job, clean
install. This also unhardcodes the dev bucket, which stag/prod EKS could
never read. Because "no vocabulary" is otherwise silent — cohort queries
joining omop.concept return nothing while every probe stays green — the skip
is announced in a new NOTES.txt and documented in the chart README, and
sync_k8s_kit.py now emits the bucket from the kit's AICENTRE_BUCKET_NAME so
the org path stays zero-touch.

Trust EC2 cleanup: the vocab-load tasks were a flat list, so any failure
between the unzip and the teardown left the unpacked bundle at rest on the
host — against the play's own stated intent — and orphaned the throwaway
container. Wrapped in block/always. This also clears a retry trap: the
creates: guards key on the unpacked directory, so a re-run after a partial
failure previously skipped both the download and the unzip and reused a
possibly-truncated bundle. docker rm -f gets failed_when: false since it
exits non-zero when the container never started.

Verified: helm lint; template gate matrix (no bucket, bucket set, disabled,
CI values); NOTES warning fires only when vocabulary is genuinely absent;
compose resolves omop-db:stag beside its siblings and still honours a pinned
OMOP_DB_TAG; ansible-playbook --syntax-check; prettier (the hard CI gate on
deploy/providers/AWS/*.yml); k8s sync tests 13 passed; omop-db ruff + mypy +
47 unit tests.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@atriaybagur

Copy link
Copy Markdown
Member Author

Thanks for the thorough pass — replies are in-thread on the three inline comments. This is for the fourth item from your review body, which has no thread to reply in: the OMOP_DB_TAG=latest defaults in the kit examples. Fixed in 7492916 along with the rest.

You're right, and it's sharper than "fine before": this PR is what makes it wrong. docker_build_omop_db.yml publishes :prod and :latest from main, so latest now is the production image — a dev kit was running omop-db from prod while its four siblings ran :stag.

Rather than swapping the literal to stag, the composes now resolve ${OMOP_DB_TAG:-${DOCKER_TAG}} and the examples comment the variable out. That keeps a per-service override for the case trust/xnat/Makefile's XNAT_TAG exists for — omop-db changes rarely, so it's precisely the service whose branch tag will be absent when the rest of the stack runs a feature-branch DOCKER_TAG (branch builds are workflow_dispatch-only). A bare ${DOCKER_TAG} would turn those deploys into a manifest unknown on omop-db. No trust/Makefile export was needed: the kit already reaches compose via --env-file (trust/Makefile:90) and DOCKER_TAG is exported for dev at :55-60 — adding a make-level default would in fact clobber a kit's own value, since shell env beats --env-file.

Nothing in the wild changes. OMOP_DB_TAG isn't in HUB_SHARED_KEYS, so sync-trust-kit never strips it — existing kits keep their literal OMOP_DB_TAG=latest line and resolve exactly as they do today. Only kits newly scaffolded by make new-trust pick up the new default.

I swept the same :latest default out of the two other places it had spread to, on your reasoning:

  • Helm omopDb.image.tagstag, matching all eight siblings in values.yaml. This one had become a correctness issue rather than cosmetics: the new omop-vocab-load Job execs /flip/omop/load_core_vocab.sh from the image.
  • Ansible omop_db_tag | default('latest') — nothing ever set it, so it always resolved latest. make seed-trust-data now passes -e omop_db_tag=$(DOCKER_TAG), exactly as it already does for the bucket, with the fallback moved to stag.

One thing worth flagging that fell out of checking this. The published omop-db tags today are latest, trust1, trust2, stag — no :prod, no sha-*. CI has never published this image; every tag was hand-pushed via make push, and they all predate the vocab-free rework, so they still bake the licensed bundle (COPY ./data/vocab_aicentre_core_20240916 /flip/omop/vocab) and none of them contains load_core_vocab.sh. That's the exposure #842 already tracks, but it means a one-time gh workflow run docker_build_omop_db.yml --ref develop is needed right after merge — the workflow_run gate doesn't fire retroactively on the merge that introduces it. I've added that as item 4 under post-merge admin actions in the PR description.

…erage

Multi-agent review of 7492916 found nine defects, seven of them claims that
commit itself introduced. Verified each empirically before fixing.

Two comments asserted things that are false:

- `docker rm -f` does NOT exit non-zero on a missing container — it has exited 0
  since Docker 23 (measured on 29.4.3; plain `docker rm` is the one that exits 1).
  So the blanket `failed_when: false` did nothing for the case it was added for
  and suppressed only genuine failures: a dead daemon or an un-removable
  container would leave a Postgres process holding the OMOP data directory and
  the loopback port while the play reported success, and the trust's real omop-db
  later mounts that same PGDATA. Narrowed to tolerate only "No such container".
- The vocab-load gate does not "mirror" omop-db-init-job. That sibling gates on
  `enabled` alone and handles an empty bucket at runtime by falling back to an
  anonymous Hugging Face download; it always renders. Ours deliberately does the
  opposite, which is the whole point — the licensed bundle has no mirror. The
  claim also contradicted values.yaml and the chart README, which had it right.

Also corrected: values.yaml claimed both jobs "read the same bucket ... one place
to configure it, not two" (they are separate keys, both defaulting empty, and
sync-kit writes only vocabLoad's); the advice to set `vocabLoad.enabled: false`
for an external database is inert, since both Jobs already gate on
`omopDb.enabled`; the README called re-running `helm upgrade` a "cheap no-op"
when `vocab-work` is an emptyDir, so every upgrade re-downloads the multi-GB
bundle; the operator instructions used bare `make sync-kit` / `make
deploy-trust-k8s`, but no root `sync-kit` target exists and the root
`deploy-trust-k8s` forwards to the chart's plain `deploy`, dropping `KIT=` and
deploying with no bucket — the exact silent failure that section warns about;
and trust/omop-db/README.md still said "the trust stacks resolve :latest by
default", the twin of the Makefile claim already fixed in 7492916.

Two coverage gaps, both created by the gating change:

- Defaulting `s3Bucket: ""` turned the vocab-load Job off in *every* CI render
  path, leaving its ~110-line body unrendered. Confirmed by injecting a `.Values`
  typo: the chart still linted and templated clean, i.e. it would merge green and
  then fail the whole Helm release of the first operator to configure a bucket.
  Added two `helm template` steps — one asserting the Job renders with a bucket
  (this one catches the injected typo), one pinning the default-off semantics.
- The new sync_k8s_kit assertions were loose substrings, so a block emitted one
  level too deep (nested under trustApi, which is additionalProperties:true and
  would be silently accepted by Helm) satisfied all three. Anchored on newlines;
  the negative test now keys on `vocabLoad`/`s3Bucket` rather than `omopDb:`, so
  it will not break when an unrelated omopDb key joins the override.

Left alone deliberately: `AICENTRE_BUCKET_NAME=flipdev-aicentre` in the kit
scaffold is wrong for a stag/prod kit, but it is pre-existing and already
consumed by fl-client's kitFromS3 — changing it belongs in its own PR. Softened
the values.yaml and README wording to say the kit must carry your own
environment's bucket rather than implying sync-kit always gets it right.

Verified: ansible --syntax-check; prettier; helm lint; gate matrix (0 default /
3 with bucket / 0 disabled / 0 CI values); both new CI steps pass on the real
chart and the first fails on a deliberately broken one; 13 k8s tests.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…p-db

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>

# Conflicts:
#	.github/workflows/test_helm_chart.yml
#	AGENTS.md
#	CLAUDE.md
#	trust/AGENTS.md
#	trust/CLAUDE.md
#	trust/data-access-api/tests/routers/test_cohort.py
…oader

Eleven defects found by a full-diff review of this PR. Each was reproduced
locally before being accepted, and four were downgraded from their initial
severity after adversarial re-checking.

Deployment:
- `make ansible-init` (and therefore `full-deploy`) failed outright. The new
  vocab-load play asserts `omop_postgres_password`, which only `seed-trust-data`
  passes; ansible-init runs before register-trusts, has no KIT in scope, and so
  cannot supply it. Skip the play there via the `omop-vocab` tag it already
  carries — the vocabulary is still seeded later by seed-trust-data.
- The `always:` cleanup removed the container before deleting the bundle. A
  failing task in `always:` aborts the remaining ones, so a `docker rm` against
  an unreachable daemon left ~3.6GB of licensed vocabulary at rest on the host.
  Deletion now runs first.
- Both `creates:` guards keyed on the unpacked directory, so an interrupted
  unzip made the next run skip both the download and the unzip and reuse a
  truncated bundle. Each stage now stages into a temp path and renames on
  success, matching `fetch-vocab-core` in trust/omop-db/Makefile.

CI:
- secret-scanning.yml's uv-lock cooldown gate never checked trust/omop-db, even
  though UV_PROJECTS and the pre-commit hook both list it — the supply-chain
  backstop was silently absent for the new project.
- Neither vocab-load render gate could fail. `! grep -q` is exempt from `set -e`
  and only failed the step by virtue of being the last line, and the absence
  assertion also passed on an empty render. Both are now explicit `if` blocks
  with ::error:: messages, plus a positive control.
- test_trust_omop_db.yml lacked `environment: flip`, so CODECOV_TOKEN resolved
  empty and the coverage upload failed silently.

omop-db:
- load_core_vocab.sh tested a command substitution inside an `if` condition,
  which `set -e` does not cover. A failed psql probe yielded "", compared
  unequal to "t", and fell through to the COPY — silently duplicating rows in
  the four tables that carry no primary key.
- A degenerate NUM_TRUSTS (0, empty, non-numeric) made populate,
  apply-constraints and export-pgdata fan out to zero targets and exit 0.
- constraints.sql guard 18 reused guard 17's sentinel, so PAYER_PLAN_PERIOD's
  foreign keys were never applied on any run. The DO/IF NOT EXISTS wrapper is a
  local addition, not upstream OHDSI, so the typo is ours to fix.

Docs:
- The README said the vocabulary bundle was needed "not for running the trust
  stacks", contradicting the same file's mandatory `make load-omop-vocab` step
  and leading readers into the silent empty-cohort failure it warns about.
- Gave the load-omop-vocab commands an explicit -C, and corrected a workflow
  comment that still described the vocabulary as "baked".

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@atriaybagur

Copy link
Copy Markdown
Member Author

Ready for another look. CI is green (33 pass, 1 skip) and every review thread is resolved.

Your approval was dismissed by two pushes since, so for the delta:

  • 8323dba7 — merge of develop.
  • f64446ea — eleven defects found by a full-diff self-review, each reproduced locally before being accepted (four were downgraded from their initial severity after re-checking). The two that would have bitten in deployment: make ansible-init (and so full-deploy) failed outright because the new vocab-load play asserts omop_postgres_password, which only seed-trust-data passes — it now skips there via the omop-vocab tag it already carries, with the vocabulary still seeded later by seed-trust-data; and the always: cleanup removed the container before deleting the bundle, so a failing task against an unreachable daemon left ~3.6 GB of licensed vocabulary at rest on the host — deletion now runs first.

Nothing else has changed since you approved.

The omop-vocab-load hook fetched and unzipped the multi-GB licensed bundle
into a fresh emptyDir on every render, then handed it to a loader that skips
every table it already holds. The cheap half was conditional and the expensive
half was not — and because this is a post-upgrade hook whose failure fails the
whole release, that download sat on the critical path of every helm upgrade.

Add a probe-vocab initContainer that asks the database first, via a new
--check mode on load_core_vocab.sh, and skip the fetch when nothing is
missing. A routine upgrade now costs one query per vocabulary table.

The loader still runs when the probe finds everything present: the bundle is
now read only for the tables actually being loaded, so a no-op run needs no
bundle on disk and still re-applies the FK constraints — which is how a
previous run that loaded every table and then died before applying them heals.

Also:
- drop the bundle-present check in fetch-bundle; the emptyDir is recreated
  with the pod (restartPolicy: Never), so it could never match
- declare resources on all three containers. The fetch writes the bundle to
  node-local storage, and without an ephemeral-storage request the kubelet
  ranks this pod first for eviction under disk pressure — and an evicted
  download restarts from zero. Sizing is values-driven (workDirSize,
  fetchResources, loadResources)
- stop discarding both package managers' stderr when installing unzip; a
  trust behind an egress allowlist needs to see why it could not reach a mirror
- share the OMOP connection env between probe and loader via a named template,
  so the two cannot drift onto different databases
- CI gate asserting the probe renders and is ordered before the fetch

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…install

The vocab-load Job installed unzip with microdnf/yum on every fetch, so
loading a vocabulary required egress to the Amazon Linux package mirror on top
of S3 — a trust behind an allowlist has the bucket open and the mirror not.

unzip is the only tool that was missing, and it does not belong in the
download step: the omop-db image already carries the loader and constraints,
so put it there and let the loader unpack. fetch-bundle is now a bare
`aws s3 cp` and S3 is the only host the Job needs to reach.

This adds no new deployment constraint: the Job already requires an image new
enough to carry /flip/omop/load_core_vocab.sh (FLIP#842), and every existing
GHCR tag predates that, so unzip lands in the same first CI publish.

The storage peak moves with the unpack — the fetch now holds only the zip
while the loader briefly holds zip and unpacked contents together — so the
ephemeral-storage requests move with it. CI gate rejects a run-time package
install returning to this Job.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
… vocab Job

Review of the two preceding commits found the new Helm gate did not constrain
what it claimed, the loader had lost a wait it needed, and several comments
stated mechanisms that are not how Kubernetes behaves.

Tests. load_core_vocab.sh had no automated coverage before this branch and the
Kubernetes hook now decides whether to download a multi-GB bundle purely from
its exit codes. tests/test_load_core_vocab.sh drives the real script with psql
stubbed on PATH — no Postgres, no bundle, no credentials — so it is a unit test
by the repo's rule and runs from `make local_test`. Modelled on the existing
harness at deploy/providers/AWS/scripts/tests/test_add_fl_kits.sh. 15 cases,
mutation-tested: breaking --check, merging the two PENDING loops, or letting an
unreadable guard fall through each fail a named case. Every variable the script
reads is set explicitly, because trust/omop-db/Makefile exports the kit env
file's keys into its recipes and an "unset" case would otherwise test nothing.

Gate. It passed five mutants. A Job that probes and fetches but never loads; a
fetch-bundle with its work-dir mount deleted (so its skip test reads a path that
cannot exist and every upgrade silently re-downloads); a resources block
rendering null; the fetch guard INVERTED, which the bare `-f /vocab-work/...`
pattern matched happily as a substring of `! -f /vocab-work/...`; and
`--check || true` followed by an unconditional touch, which writes the marker
whatever the probe says and only surfaces on the next fresh install as a failed
release. The marker assertions now carry their whole `if` line, and the gate
also checks the loader, a mount count of 3, and no null resources.

The image half of the no-package-install contract moves to test_trust_omop_db.yml
and becomes a real check: build the image, assert unzip, the loader and the
constraints are in it. It was asserted as a Dockerfile grep in the Helm workflow,
whose pull_request trigger is path-filtered to deploy/providers/kubernetes/** —
so it could not fire for the Dockerfile change it was guarding. Its new home runs
on every PR and gates the GHCR publish.

Loader wait. Restored. It was dropped on the reasoning that the loader starts
seconds after the probe, which only holds on the cached path: when there is work
to do, a multi-GB download sits between them, and the chart deploys with
`helm upgrade --install` and no --wait, so the release's own rollout may still be
in flight. Failing there discards the Pod and the bundle it just fetched.

Comments. Four claims were wrong, all in ways that invite a bad future edit:
- emptyDir sizeLimit is enforced by the kubelet on its own, so the
  ephemeral-storage limits were not what bounded the work dir; the requests earn
  their place through scheduling and eviction ranking instead.
- The pod's ephemeral-storage ceiling is NOT the sum of all containers' limits.
  Init containers are taken as a max against the regular containers' total —
  the same rule the requests were already documented with. Read as a sum, three
  4Gi limits look like 12Gi of headroom and are really 4Gi.
- A container's own ephemeral-storage accounting covers its writable layer and
  logs, never an emptyDir, so the zip is charged to the pod and not to the fetch.
- The chart README attached the fall-through-to-fetch behaviour to the probe's
  timeout, which is actually a hard failure; only a reachable-but-unqueryable
  database falls through.
Also corrected the cost of a no-op upgrade: two round-trips per table plus a
constraint-catalogue pass, not one query per table.

Smaller: an absent bundle directory reports itself as such instead of blaming an
incomplete bundle, since a caller that probed first skipped the fetch on purpose;
the probe logs why it fell through, so "vocabulary genuinely absent" and "could
not reach the database" are no longer identical Completed pods; the values schema
rejects an emptied workDirSize or resources block rather than silently dropping
the bound.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
Measured from s3://flipdev-aicentre/vocab/: the 20240916 bundle is ~600 MB
zipped against the ~3.6 GB unpacked figure already documented, so a loader that
unpacks beside the zip peaks near 4.2 GB. That is the number sizing the
Kubernetes vocab-load Job's work dir, and it was previously an unverifiable
guess in a comment.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
hostAwsMount mounts a hostPath into the fetch container, and an empty path
renders as `path: ""` — a volume the kubelet refuses, so the pod never
admits and the failure surfaces as an unschedulable hook rather than as the
configuration mistake it is.

Requires the value instead, with a message saying it is a path on the NODE:
on single-node k3s that is the workstation's $HOME/.aws, but on kind the node
is a container and the path only exists if the cluster was created with a
matching extraMount.

The chart-wide cleanup of that value — dropping the hardcoded personal default
and removing the fl-client's redundant copy of the mount — is a separate change
against develop, since it touches templates this PR does not own.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@garciadias garciadias mentioned this pull request Aug 18, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants