Skip to content

List the Trino fleet, including worker version, from what the cell actually serves - #1133

Merged
fuziontech merged 2 commits into
mainfrom
trino-announce-nodes
Aug 27, 2026
Merged

List the Trino fleet, including worker version, from what the cell actually serves#1133
fuziontech merged 2 commits into
mainfrom
trino-announce-nodes

Conversation

@fuziontech

@fuziontech fuziontech commented Aug 27, 2026

Copy link
Copy Markdown
Member

The Trino cluster page shows "Nodes are not reported by this cell". That
copy is accurate as of #1132 — but the cell can report its fleet, and can
report worker versions too. The console was asking at the one route this cell
does not bind.

Trino binds one node-listing route, chosen by discovery.type

discovery.type route detail
AIRLIFT_DISCOVERY /v1/node (+ /v1/node/failed) per-node heartbeat health
ANNOUNCE (Trino's default, what these cells run) /v1/announce set of announced node URIs
DNS /v1/announce same

system.runtime.nodes is served by every cell regardless of
discovery.type, and is the only source carrying node_version. New order:

/v1/node  →  system.runtime.nodes  →  /v1/announce

Membership is kept as the last resort, so a cell whose grant hasn't rolled
out yet still lists a fleet instead of showing nothing.

The grant, and what stays denied

This widens the observer, so it is written as narrowly as the OPA operations
allow: AccessCatalog on system, and SelectFromColumns pinned to that
one table. AccessCatalog opens nothing by itself — every read still
passes SelectFromColumns.

stays denied why it matters
system.runtime.queries tenant SQL text, unfiltered by query-owner visibility
system.metadata.* enumerates every tenant catalog/schema/table/column
system.jdbc.* the same enumeration by another route
org_<tenant> unchanged — readable_catalog still excludes the observer group

TestObserverSystemGrantIsPinnedToTheNodesTable pins each denial (including a
runtime.nodes in another catalog, and metadata browsing on system);
TestSystemNodesGrantIsObserverOnly pins that no tenant and not the admin
gets it, and that claiming the observer group without the observer username
still grants nothing. system.runtime.nodes holds node_id, http_uri,
node_version, coordinator, stateno tenant identifier, so the
grant exposes no customer data even read in full.

Resource groups

The observer needed its own lane. The last selector is user (?<org>.*),
which matches anything, so without one the console's query lands in
root.tenants.free.__duckgres_observer — and those leaves are
JmxExport: true, so it would appear as a phantom tenant in the
per-tenant metrics from #1124. Both operational principals now select into
the unexported admin tier ahead of the tenant selectors.

UI

Role / Version / State columns, and the Nodes card leads with version skew
when more than one version is present — a stalled rollout is the case worth
seeing first. HasHealth() stays false for this source: it carries lifecycle
state, not heartbeat ratios, and conflating them would put a health badge on
a number nothing measured.

Verification

  • ./controlplane/provisioner/... and ./controlplane/provisioner/opa/... green
  • Trino suites in ./controlplane/admin/ green (the *Postgres failures are a
    stale local schema, unrelated and pre-existing)
  • golangci-lint — 0 issues attributable to this change
  • tsc -b --noEmit, eslint clean; vitest 166/166 (up from 160)
  • new tests: fallback ordering (/v1/announce not queried when the system
    table answers, and not queried when /v1/node answers), nextUri draining,
    denied-grant falls back rather than failing the read, coordinator-first
    sort, version/state decoding, and the full policy deny matrix

The Trino cluster page reports "Nodes are not reported by this cell" against
prod-us. That copy is accurate — #1132 made it so — but it is not the end of
the story: the cell can name its workers, the console was just asking for
them at the one route this cell does not bind.

Trino binds exactly one node-listing route, chosen by discovery.type:

  AIRLIFT_DISCOVERY  /v1/node        heartbeat health, + /v1/node/failed
  ANNOUNCE (default) /v1/announce    the set of announced node URIs
  DNS                /v1/announce    same

AnnounceNodeInventoryModule and DnsNodeInventoryModule both bind
AnnounceNodeResource, so between the two routes every cell can list its
fleet; only the detail differs. Try /v1/node, fall back to /v1/announce on
the endpoint-unavailable error #1132 introduced.

Both routes are declared @ResourceSecurity(MANAGEMENT_READ), which maps to
checkCanReadSystemInformation, which __duckgres_observer already holds via
the ReadSystemInformation grant. No policy.rego change: the tenant-isolation
boundary is untouched, and the observer gains no catalog access.

The announce inventory carries membership and nothing else, so the source
travels with the data rather than being guessed downstream. Every heartbeat
field on an announced node is a zero meaning "not measured", which is not the
same as measured-and-zero: summarizing it would have the console report a
0.0 failure ratio and an "all healthy" fleet on the strength of numbers the
coordinator never sent. TrinoNodeInventory.HasHealth gates that, the status
payload carries node_source, and the page renders a membership-only table
plus a note on what is missing and why, instead of zero-filled health
columns. The Nodes stat card reads "membership only" rather than "all
healthy".

The remaining option, if per-node health on these cells is ever worth it, is
system.runtime.nodes — node_id, http_uri, node_version, coordinator, state,
so it would also close the version-skew gap. It costs the observer a scoped
catalog grant and a resource-group selector that admits it, both changes to
the isolation boundary, so it is deliberately not bundled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fuziontech
fuziontech requested a review from a team August 27, 2026 23:10
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Test Impact Plan

Deterministic summary of how this PR changes tests, CI runners, and coverage-risk signals.

Summary

Area Added Changed Deleted
Test files 0 4 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +10 / -0
  • Assertions: +58 / -10
  • Skips or known failures added: 0
  • Workflow continue-on-error added: 0
  • Workflow path filters added: 0
  • Test commands removed from justfile: 0
  • E2E/journey retry lines added: 0

Coverage risk: neutral or increased

No coverage-reduction warnings detected.

The announce fallback names the workers but reports nothing about them, and
it cannot report the one thing a rollout needs: which version each node is
running. system.runtime.nodes carries node_id, http_uri, node_version,
coordinator and state, and the system connector is served by every cell
regardless of discovery.type. So the order is now

  /v1/node  ->  system.runtime.nodes  ->  /v1/announce

with the SQL path preferred over bare membership, and membership kept as the
last resort so a cell whose grant has not rolled out yet still lists a fleet.

This widens the observer, so the grant is written as narrowly as the OPA
operations allow: AccessCatalog on `system`, and SelectFromColumns pinned to
that one table. AccessCatalog opens nothing by itself -- every read still has
to pass SelectFromColumns -- so what stays denied is the part that matters:

  system.runtime.queries   tenant SQL text, unfiltered by query-owner
                           visibility (the REST path it already has is both
                           redacted and bounded by FilterViewQueryOwnedBy)
  system.metadata.*        enumerates every tenant catalog/schema/table/column
  system.jdbc.*            the same enumeration by another route
  org_<tenant>             unchanged; readable_catalog still excludes the
                           observer group, and this rule does not touch it

TestObserverSystemGrantIsPinnedToTheNodesTable pins each of those denials,
including a nodes table in another catalog and metadata browsing on `system`;
TestSystemNodesGrantIsObserverOnly pins that no tenant and not the admin gets
it, and that claiming the observer group without the observer username still
grants nothing. system.runtime.nodes itself contains no tenant identifier, so
the grant exposes no customer data even read in full.

The observer also needed its own resource-group lane. The last selector is
user `(?<org>.*)`, which matches anything, so without one the console's query
would have been admitted as a tenant into root.tenants.free.__duckgres_observer
-- and those leaves are JmxExport=true, so it would have shown up as a
phantom tenant in the per-tenant metrics added in #1124. Both operational
principals now select into the unexported admin tier ahead of the tenant
selectors.

The page gains Role / Version / State columns, and the Nodes card leads with
version skew when more than one version is present, since a stalled rollout
is the case worth seeing first. HasHealth stays false for this source: it
carries lifecycle state, not the failure detector's heartbeat ratios, and
conflating them would put a health badge on a number nothing measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fuziontech fuziontech changed the title List the Trino fleet from the inventory the cell actually serves List the Trino fleet, including worker version, from what the cell actually serves Aug 27, 2026
@fuziontech
fuziontech merged commit ded285b into main Aug 27, 2026
31 checks passed
@fuziontech
fuziontech deleted the trino-announce-nodes branch August 27, 2026 23:58
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.

1 participant