Skip to content

ateapi: add the ExternalVolume resource - #3744

Open
hajiler wants to merge 1 commit into
agent-substrate:mainfrom
hajiler:shared-volumes/pr1-external-volume
Open

hajiler wants to merge 1 commit into
agent-substrate:mainfrom
hajiler:shared-volumes/pr1-external-volume

Conversation

@hajiler

@hajiler hajiler commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Part 1 of Shared Volumes #1637 (design: Substrate Shared External Volumes).

This PR introduces a new atespaced ExternalVolume object, whose functionality will be added in a follow up PR. An ExternalVolume is an atespace-scoped record of a CSI volume that already exists in the storage system: volumeType (CSI driver), volumeId and volumeContext. Its lifetime is independent of any actor. Later changes let templates reference one, record actor references on it, and add its delete trigger.

  • Resource, Postgres table (000002_external_volumes.sql, ON DELETE RESTRICT to atespaces), store methods and contract tests.
  • Create/Get/List/DeleteExternalVolume. Create needs the ExternalVolumes preview gate; Get, List and Delete work with the gate off, so turning it off cannot strand a record. Delete removes the record only; the storage stays.
  • OpenFGA external_volume type. Registering a volume is reserved for global owners, as the design's authorization section asks: providers do not scope volume IDs to atespaces, so an atespace editor could otherwise name storage another atespace uses. Viewers get and list, editors delete.
  • kubectl ate {create,get,delete} externalvolume, docs.

This will be followed up with a series of other PRs:

  1. Integrate the object into the actor's lifecycle (maintain existing RWO volume per actor functionality)
  2. Add support for sequential sharing of an RWO volume between multiple actors
  3. Add support for concurrent sharing
  • Tests pass: go test for apivalidation, authz, store (Postgres), controlapi and the functional suite; hack/verify-all.sh; apitool validate (no new exemptions); hack/verify/proto-breaking.sh; hack/verify/postgresql-migrations.sh main. No e2e: the resource has no behavior on a cluster yet.
  • Docs updated

@hajiler hajiler left a comment •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Claude Review: Reviewed with a focus on the object shape and how it holds up against the later changes in the series (template reference, actor references on the volume, delete trigger). The handler, store, authz and CLI code follow the Tag precedent closely and read fine.

Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread cmd/ateapi/internal/authz/authorizer.go Resolved
@hajiler
hajiler force-pushed the shared-volumes/pr1-external-volume branch 2 times, most recently from c1365a3 to 5d9d73e Compare November 10, 2026 20:37
@hajiler
hajiler marked this pull request as ready for review November 10, 2026 20:41
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread cmd/ateapi/internal/authz/authorizer.go Resolved
Comment thread internal/resources/resourceref.go Resolved
Comment thread cmd/ateapi/internal/authz/authorizer.go Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note on syncing the Shared External Volumes design doc:
This PR is up to date on volume_type, volumes[].external.template (in docs/guides/csi-volumes.md), --volume-context / --preview, and DeleteExternalVolume leaving underlying storage intact, and you noted on ateapi.proto that the ExternalVolumeStatus / storage_volume_id alignment will be handled in the follow-up PR. However, the Shared External Volumes design doc is still outdated on these — could you update the design doc when you get a chance so other docs referencing it stay in sync? Specifically:

  1. volumes[].external nesting (from PR #2653): The design doc (Tab 1 "Changes to ActorTemplate" & Tab 2 CUJs S1, S3, C3, C4) still uses top-level volumes[].externalVolumeTemplate and volumes[].externalVolumeRef instead of volumes[].external.template and volumes[].external.ref (which you flagged on our FusionFS doc).
  2. ExternalVolume fields (from PR #3744): In Tab 1 ("New API Type: ExternalVolume"), update Driver string → volume_type (to match this PR), and add volume_context (map<string, string>).
  3. CLI flags & delete behavior (from PR #3744): Add --volume-context and --preview to the kubectl ate create externalvolume examples, and update Tab 2 S1 (which currently says "the user deletes the ExternalVolume, which deletes the disk", whereas DeleteExternalVolume only removes the Substrate DB record and leaves storage in place).
  4. ExternalVolume.status & ActorStatus volume status (for PR 2): Update the ExternalVolume sketch to nest output-only fields (assigned_actors with ObjectRef actor + string actor_uid, and state) under ExternalVolume.status per API Style Guide §8, note the rename of the existing per-actor ExternalVolumeStatus message (storage_volume_id → volume_id) to avoid colliding with ExternalVolume's status type, and reconcile .status.externalVolumes.volumes[] (Tab 1) vs. .status.actorVolumes[] (Tab 2 S3/D6).

hajiler added a commit to hajiler/substrate that referenced this pull request Nov 11, 2026
Cross-resource references are named after the resource they point to
(docs/api-style-guide.md 2.3), as agent-substrate#3744 does for its references to the
same resource. Rename ExternalVolumeSource.ref to external_volume before
it is served.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hajiler
hajiler force-pushed the shared-volumes/pr1-external-volume branch from 5d9d73e to c94af04 Compare November 11, 2026 20:20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all the review comments.

hajiler added a commit to hajiler/substrate that referenced this pull request Nov 11, 2026
Cross-resource references are named after the resource they point to
(docs/api-style-guide.md 2.3), as agent-substrate#3744 does for its references to the
same resource. Rename ExternalVolumeSource.ref to external_volume before
it is served.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hajiler added a commit to hajiler/substrate that referenced this pull request Nov 11, 2026
Cross-resource references are named after the resource they point to
(docs/api-style-guide.md 2.3), as agent-substrate#3744 does for its references to the
same resource. Rename ExternalVolumeSource.ref to external_volume before
it is served.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread api/ateapi/v1/ateapi.proto Resolved
Comment thread cmd/ateapi/internal/authz/model.fga Resolved
Comment thread cmd/ateapi/internal/authz/model.fga Resolved
An ExternalVolume is an atespace-scoped record of a CSI volume that already
exists in the storage system: the driver, the volume ID and the volume
context the node plugin needs to mount it. Its lifetime is independent of
any actor, and an atespace cannot be deleted while it holds one.

This adds the resource, its Postgres table and store methods, the OpenFGA
type and RPC permissions, the Create, Get, List and Delete RPCs, and the
kubectl-ate commands. Create requires the ExternalVolumes preview gate and
a global owner. Get, List and Delete work with the gate off, so turning it
off cannot strand a record. Delete removes the record and leaves the
storage in place.

Nothing references an ExternalVolume yet. Actor templates still provision
a volume per actor from external.template.

Signed-off-by: Elijah Rodriguez-Beltran <elijahrb@google.com>
@hajiler
hajiler force-pushed the shared-volumes/pr1-external-volume branch from c94af04 to 731ccef Compare November 12, 2026 20:12
uid text NOT NULL,
version bigint NOT NULL,
proto bytea NOT NULL,
PRIMARY KEY (atespace, name),

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.

🤖 Multiple ExternalVolume copies for the same (volume_type, volume_id):

With ExternalVolume being atespace-scoped and external_volumes only enforcing PRIMARY KEY (atespace, name), the same underlying CSI (volume_type, volume_id) can be registered multiple times (e.g. across multiple atespaces to share a volume, or under multiple names in the same atespace). Because the design treats each ExternalVolume row as the single source of truth for that storage volume, having multiple copies for the same (volume_type, volume_id) creates split-brain issues once lifecycle/deletion policies and control-plane locking land:

  1. Premature storage deletion (ReclaimPolicy=Delete / DeleteTrigger): DeleteTrigger=LAST_ACTOR and manual DeleteExternalVolume only inspect actor_refs on that single (atespace, name) row. Deleting one copy (or its last bound actor in team-a) can trigger CSI.DeleteVolume(volume_id) while another copy (team-b/vol) still has active or suspended actors using the same underlying volume.
  2. Broken control-plane serialization & detach safety: Per-row locking (SELECT ... FOR UPDATE on external_volumes) and actor_refs are scoped to (atespace, name):
    • Two actors in different atespaces can simultaneously pass RWO (SINGLE_NODE_WRITER) checks against separate rows for the same volume_id.
    • When suspending an actor on node N, checking actor_refs on team-a/vol will not see active actors on node N bound via team-b/vol, causing ateapi to call CSI.ControllerUnpublishVolume while team-b's actor is still running on that node.
  3. Conflicting per-volume configuration: Separate copies can declare divergent volume_context, access_mode (RWO vs RWX), delete_trigger, and reclaim_policy for the same underlying CSI volume.

Proposed solution:

  1. Enforce 1:1 (volume_type, volume_id) uniqueness in external_volumes (in this PR): Lift volume_type and volume_id into columns on external_volumes with a UNIQUE (volume_type, volume_id) constraint so each underlying CSI volume is represented by at most one authoritative ExternalVolume record (CreateExternalVolume already maps unique violations to store.ErrAlreadyExists).
  2. Support cross-atespace references via ObjectRef instead of duplicating records (in the design doc & follow-up PRs): Use ObjectRef (atespace + name) rather than a bare name string for ExternalVolumeRef, Actor.external_volume_bindings, and ActorRef (similar to Tag). Even if M1 initially restricts references to the actor's own atespace, using ObjectRef keeps the API forward-compatible so cross-atespace sharing can be granted via OpenFGA permissions on the single ExternalVolume object rather than creating duplicate copies.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure we want to support cross-atespace references. If ExternalVolume is an atespaces resource, we would suddenly have an object in some atespace influencing another (i.e blocking actors from resuming, or from terminating an atespace). It's something we can discuss as a follow up but will pass on this PR.

AS for volume ID uniqueness that something we can enforce now.
AI: Enforce 1:1 (volume_type, volume_id) uniqueness in external_volumes in PostGres store.

This branch has not been deployed

No deployments
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.

4 participants