Skip to content

[Feature]: SPCS service specs and Makefile for the Snowflake trust (trust/deploy/snowflake/) - #1600

Open
garciadias wants to merge 23 commits into
developfrom
1582-snowflake-spcs-specs
Open

garciadias wants to merge 23 commits into
developfrom
1582-snowflake-spcs-specs

Conversation

@garciadias

Copy link
Copy Markdown
Collaborator

Description

Adds trust/deploy/snowflake/, the Snowpark Container Services node shape for epic #1589: one SPCS service per container, rendered from the trust's kit file. Object names come from naming.py, the same contract as the Terraform root (#1581).

  • Spec templates: trust-api, data-access-api, imaging-api, fl-client-net-N (FLARE and Flower), xnat-web, xnat-db, xnat-nginx, orthanc, dcm2niix job. All endpoints public: false; internal TCP endpoints for DICOM; inter-service URLs derived from SPCS DNS.
  • Secrets per service by snowflakeSecret only (fl-client gets no AES key); fl-client mounts only its net-N image prefix and the FL kit; block volumes only for xnat-db and Orthanc.
  • sync_snowflake_kit.py: kit to SECRET objects via snow sql --stdin (never argv; CREATE IF NOT EXISTS + ALTER SECRET, keeping grants), FL kit and governance doc to their stages.
  • mirror_images.py: GHCR to the account repository, linux/amd64, digests locked.
  • Makefile: render, mirror-images, sync-kit, up (CREATE OR ALTER SERVICE, never OR REPLACE, which would drop block volumes), down, suspend (services and pools), status, selftest; DRY_RUN=true prints SQL.
  • 51 pytest guards; path-gated workflow test_snowflake_specs.yml; README with the assumptions pending spikes [Spike]: Run XNAT on Snowpark Container Services without Docker #1577/[Spike]: dcm2niix as an SPCS job service triggered by imaging-api #1578/[Spike]: SPCS runtime compatibility of the trust images #1579.

Linked Issues

Fixes #1582

Checklist

  • Follows the project's coding conventions and style guide
  • Updates documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Type of Change

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • Breaking change (fix or new feature that would cause existing functionality to change).
  • New tests added to cover the changes.
  • In-line docstrings updated.
  • Documentation updated, tested make -C docs/ docs.

Testing

  • make -C trust/deploy/snowflake test: 51 passed.
  • scripts/tests/test_pr_paths_changed.py: PASS=16 FAIL=0.
  • Repo-wide ruff@0.14.7 check and format --check clean.
  • make render, up DRY_RUN=true, suspend DRY_RUN=true, sync-kit DRY_RUN=true against a throwaway kit produce the expected SQL.

Additional Notes

Not run against a live account. Details depending on the XNAT, dcm2niix and runtime spikes are marked as assumptions in the README. Known gap for #1586: the hub EAI is not yet attached to trust-api / fl-client.

…ake trust

Adds trust/deploy/snowflake/, the Snowflake (Snowpark Container Services) shape of
a FLIP trust node, beside helm/ and the compose files. Infrastructure stays with the
Terraform root trust/deploy/terraform/snowflake (#1581); this tree owns images,
secrets, specifications and the services.

One specification template per service in the naming contract, rendered from the
trust's kit file: trust-api, data-access-api, imaging-api, fl-client-net-<N>,
xnat-web, xnat-db, xnat-nginx, orthanc and the dcm2niix job service. No endpoint is
public; the only inbound ports are the two DICOM TCP endpoints (orthanc 4242,
xnat-web XNAT_PORT) that the DQR return leg needs. Credentials reach containers only
as Snowflake SECRET objects, mapped per the contract: the fl-client maps the
trust-internal key alone and no AES key, so researcher training code cannot pass the
cohort-admin gate, and it mounts only its own net-<N> images prefix and a read-only
FL kit. Block volumes for xnat-db and orthanc, stage volumes for every shared tree.
Inter-service URLs are derived from SPCS DNS rather than copied from compose aliases.

sync_snowflake_kit.py writes the SECRET objects from the kit and pushes the FL kit
and governance document to their stages. Values go to snow sql on stdin, never as
arguments, and never into output; CREATE SECRET IF NOT EXISTS + ALTER SECRET keeps
the grants a CREATE OR REPLACE would drop. mirror_images.py copies GHCR images into
the account repository as linux/amd64 and records digests the renderer pins.

The Makefile adds render, mirror-images, sync-kit, up, down, suspend, status and a
placeholder selftest (real assertions in #1585), with PROD mapped through
deploy/env_mode.mk and DRY_RUN=true printing the SQL instead of running it, which is
what makes the deploy path testable with no account.

51 pytest guards over rendered specs, the deploy SQL and a fake snow binary, run by
the new path-gated workflow test_snowflake_specs.yml.

Open spikes #1577, #1578 and #1579 may change details; the assumptions are tabulated
in trust/deploy/snowflake/README.md.

Refs #1582, #1589

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
@github-actions github-actions Bot changed the title feat(trust): SPCS service specs, renderer and Makefile for the Snowflake trust (#1582) [Feature]: SPCS service specs and Makefile for the Snowflake trust (trust/deploy/snowflake/) Oct 20, 2026
…utput; allowlist detect-secrets false positives (#1582)

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…(CodeQL py/clear-text-logging) (#1582)

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…euristic (#1582)

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
@garciadias
garciadias marked this pull request as ready for review October 20, 2026 19:39
…ired admin role (#1582)

FLIP_<CODE>_ADMIN is retired by the two-tier decision in epic #1589: the
account's security objects belong to the break-glass Terraform root, and
everything this directory does now runs as the least-privilege workload role.

- naming.py drops the admin_role property.
- sync-kit creates the secrets as FLIP_<CODE>_DEPLOY, which owns them, and
  grants READ to FLIP_<CODE>_SVC. An owner grants on its own objects without
  MANAGE GRANTS, which the deploy role deliberately does not hold.
- The rendered manifest carries the role, and the deploy SQL (up, down,
  suspend) opens with USE ROLE FLIP_<CODE>_DEPLOY, so services are created and
  owned by the same role that owns everything they reference.
- README gains a Roles section: the four account privileges the deploy role
  holds, the ownership model, and that FLIP_<CODE>_HUB_EAI is deliberately not
  attached here (tracked by #1586).

New guards: every deploy statement runs as the deploy role and names no admin
role; the secrets are owned by deploy and read by svc; neither the deploy SQL
nor any rendered spec references the hub EAI. The CodeQL-safe structure in
sync_snowflake_kit.py is unchanged: the printed SQL is still built from
placeholders by a function that never receives kit values.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…; never emit STOP ALL (#1582)

The live XNAT spike (#1577) showed snow sql rewriting '&&' to '&' in a service spec: by default it
resolves its own &name / &{name} syntax before sending. sync-kit and every make target pipe SQL
through it, so a secret value holding '&' would have been stored silently altered. Pass
--enable-templating NONE on both paths and test that sync-kit does.

The same spike showed ALTER COMPUTE POOL ... STOP ALL deletes every service in the pool, block
volumes included, past snapshotOnDelete. The tooling never used it; a guard now keeps it that way.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>

# Conflicts:
#	.gitignore
#	AGENTS.md
…ERVICE only for the deploy (#1578)

A service's OAuth token carries its owner's role and SPCS refuses GRANT
OWNERSHIP ON SERVICE, so services created by the deploy role gave every
container, the fl-client among them, a token able to drop the trust
database. make up now runs spcs_sql.py apply-up: grant CREATE SERVICE on
APP to FLIP_<CODE>_SVC, CREATE OR ALTER each service as that role, revoke
in a finally. Specs are rendered before the grant, so a broken one grants
nothing. Tests pin the order, the revoke on failure and on Ctrl-C (proven
to fail with the finally removed), and that make up uses the runner.

The dcm2niix job is now started by the Terraform-owned APP.CONVERT_SCAN;
the README role table and account privilege list are brought up to date.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…FLIP#1479)

kitfile.py reads HUB, and every usage line, error hint and README example
says HUB=<env>. PROD now fails at parse time in deploy/env_mode.mk.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…pushes local builds (#1582)

The node pulled fl-client-nvflare / fl-client-flower, repositories nobody
publishes: the FL workflows push flare-fl-client and flower-supernode, the
names deploy/fl_backend.mk carries. naming.fl_client_image() is now the
one map, used by the renderer and the mirror, and a test pins it to
fl_backend.mk. Found on the live test account.

mirror-images MIRROR_PULL=false pushes the local daemon's image as is, so
an unpublished change (a local build tagged with its GHCR name) can be
tested on Snowflake; it refuses an image that is not built.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
FLARE: the client chmods /app/startup and writes /app/local, but the spec
mounted the kit read-only at /app/fl-kit. It now gets local volumes at
/app/{startup,local,transfer}, seeded from the read-only kit stage before
the image's own entrypoint runs.

Flower: the stage volume is FL_KIT/net-N itself, so the kit sits at the
mount root; the args named /app/fl-kit/net-N/... and pointed at nothing.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
The registry token is short-lived, so a mirror that relied on an earlier
docker login failed at its first push (found on the live test account).
snow spcs image-registry login hands the token to docker directly.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…er existing (#1582)

The live account rejected the first make up with "syntax error ...
unexpected 'OR'". apply-up now reads SHOW SERVICES first: a new service
is CREATEd, an existing one gets ALTER SERVICE ... FROM SPECIFICATION
(an in-place upgrade that keeps its block volumes) plus SET for the
settings CREATE would have given it. Never OR REPLACE.

The same run proved the finally: the failed deploy still revoked
CREATE SERVICE from the services role.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…session (#1582)

The live account rejected data-access-api's spec: "'path' overlaps a
location reserved by Snowflake". SPCS mounts the OAuth token at
/snowflake/session/token by default, which is where every reader looks,
so the setting goes; a guard keeps every rendered spec off that path.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
Snowflake refuses MIN/MAX_INSTANCES on a service with a block volume as
scaling, even to the same value (xnat-db on the live account). Instance
counts are fixed at CREATE.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
… require; no xnat-nginx on SPCS (#1582)

Found on the live account, where all three crash-looped:
- xnat-db's guard needs the superuser password (POSTGRES_PASSWORD, from the
  kit's XNAT_DATASOURCE_ADMIN_PASSWORD) and the xnat role's
  (XNAT_DATASOURCE_PASSWORD) as two different values; the spec passed one.
- Orthanc refuses to start without ORTHANC__REGISTERED_USERS, a JSON map. A
  spec injects a secret whole, so sync-kit composes the map as its own secret.
- xnat-nginx proxies to Compose host names and binds port 80 as non-root,
  which SPCS does not allow; nothing in the trust calls it. Dropped.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
MIRROR_IMAGES=<subset> rewrote images.lock.json with only that subset, so
the next render silently fell back to tags for everything else. The lock
now merges onto the previous one for the same trust, env and registry.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
POSTGRES_USER=xnat made the stock entrypoint create xnat as the superuser
with the admin password, so XNAT's datasource password was refused on the
live account. Same rule the Helm chart already follows.

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
…s-specs

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>

# Conflicts:
#	AGENTS.md
#	trust/deploy/README.md
@codecov

codecov Bot commented Oct 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Compose sets ORTHANC__DICOM_MODALITIES inline; the SPCS spec did not, so
every C-STORE to XNAT was a 404 "No modality with symbolic name: XNAT"
(live account).

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>
Orthanc rejects a modality host of the full 58-character SPCS name as too
long. Every trust service is in the APP schema and the container's DNS
search list holds app.<db>.snowflakecomputing.internal, so the bare name
resolves to the same address (both checked on the live account).

Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com>

This branch was successfully deployed

1 active (outdated) deployment
flip — 11dde466 Deployed Oct 29, 2026 by garciadias via flip package tests #924
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: SPCS service specs and Makefile for the Snowflake trust (trust/deploy/snowflake/)

3 participants