Skip to content

feat(volume): real modal.CloudBucketMount -> S3 mount support (calque#91 Workstream A) - #185

Merged
scttfrdmn merged 1 commit into
mainfrom
feat/calque-91a-cloudbucketmount
Aug 15, 2026
Merged

feat(volume): real modal.CloudBucketMount -> S3 mount support (calque#91 Workstream A)#185
scttfrdmn merged 1 commit into
mainfrom
feat/calque-91a-cloudbucketmount

Conversation

@scttfrdmn

Copy link
Copy Markdown
Contributor

Summary

Implements calque#91 Workstream A: replaces modal.CloudBucketMount's
leak-only treatment with a real S3 mount via mountpoint-s3, when it's
used inline as a volumes= value (the real Modal idiom):

@app.function(volumes={"/data": modal.CloudBucketMount("my-bucket", key_prefix="foo/", read_only=True)})
  • tools/pyast/pyast.py: _volumes_map/_cloud_bucket_mount extract
    bucket_name (positional or kwarg), key_prefix, read_only from an
    inline CloudBucketMount(...) call into a
    {"__cloud_bucket_mount__": {...}} wire shape, distinguishable from
    an ordinary Volume mount's plain-string shape. Falls back to the
    pre-existing "recognized but not modeled" leak only when bucket_name
    isn't a string literal. secret= and
    bucket_endpoint_url=/requester_pays=/force_path_style= are each
    leaked separately as unhonored.
  • internal/ir/ir.go: new ir.CloudBucketMount type; Function/Class
    get a CloudBucketMounts map[string]CloudBucketMount field alongside
    the existing Volumes field.
  • internal/parse/parse.go: readConfigKwargs widened to decode both
    the plain-Volume and CloudBucketMount shapes out of one volumes=
    dict, threaded through buildFn/buildClass.
  • internal/plan/cloudbucketmount.go (new): ResolveCloudBucketMounts
    (dedupe-by-mount-path, conflict-leak, mirrors ResolveVolumes) and
    MountCommands (idempotent mount-s3 install check, mkdir -p, then
    mount-s3 <bucket> <mountpoint> [--prefix <prefix>] [--read-only]).
  • internal/exec/bootstrap.go: new BootstrapConfig.CloudBucketMountLines
    field, spliced in after the artifact sync and before either the
    HostMode or docker-mode run invocation.
  • internal/plan/iam.go: RealRunPolicy gains an extraBuckets []string
    param — each distinct bucket gets its own read/write/list grant,
    separate from calque's own --bucket staging area.
  • cmd/calque/realrun.go / cmd/calque/fleetrun.go: new
    cloudBucketMountSpecsForApp wired into both the single-instance and
    fleet (dedicated-fallback-instance) paths.
  • docs/modal-compatibility-matrix.md: modal.CloudBucketMount row
    updated from ⬜ to ✅.

Dict/Queue/NetworkFileSystem/App.include remain leak-only/out of
scope for this PR. modal.NetworkFileSystem is a separate, larger
workstream planned for later — not attempted here.

Test plan

  • go build ./... && go vet ./... && gofmt -l . && go test -count=1 ./...
  • golangci-lint run ./...
  • ruff check .
  • New fixture testdata/scripts/cloud_bucket_mount.py (positive/modeled case)
  • internal/parse/parse_test.go: TestParseCloudBucketMountResolves (new); TestParseRareConstructsAreTaggedNotSilent updated (CloudBucketMount no longer unconditionally leaked)
  • internal/plan/cloudbucketmount_test.go (new): dedupe/conflict-leak + shell-line output
  • internal/exec/bootstrap_demo_test.go: CloudBucketMountLines splice tests (docker mode + host mode + unchanged-default)
  • internal/plan/iam_test.go: extraBuckets coverage

…#91 Workstream A)

Replaces CloudBucketMount's leak-only treatment with a real mountpoint-s3
mount against the script's own S3 bucket: pyast.py extracts bucket_name/
key_prefix/read_only from an inline CloudBucketMount(...) volumes= value,
parse.go decodes them into ir.Function/Class.CloudBucketMounts,
internal/plan/cloudbucketmount.go resolves+renders the mount-s3 shell
lines, bootstrap.go splices them in before @Enter runs, and
RealRunPolicy's new extraBuckets param grants the instance role access
to the script's own bucket (separate from calque's --bucket staging
area). secret=/bucket_endpoint_url=/requester_pays=/force_path_style=
are each leaked distinctly as unhonored. Dict/Queue/NetworkFileSystem/
App.include remain leak-only; NetworkFileSystem is a separate, later
workstream.
@scttfrdmn
scttfrdmn merged commit 1ea7064 into main Aug 15, 2026
7 checks passed
@scttfrdmn
scttfrdmn deleted the feat/calque-91a-cloudbucketmount branch August 15, 2026 06:17
scttfrdmn added a commit that referenced this pull request Aug 15, 2026
…91 Workstream B) (#186)

A real NetworkFileSystem.from_name(name) used as a network_file_systems=
value (a separate decorator kwarg from volumes=, never nested inside it)
now resolves to a real EFS-over-NFS mount, bring-your-own only — calque
never auto-creates an EFS filesystem (create_if_missing=True is a distinct
leak, not a blocker), discovering the pre-provisioned filesystem via a
calque:nfs-name=<name> tag convention.

pyast.py structurally tracks the from_name(...) binding (mirroring Volume's
own zero-leak-on-binding posture) and the network_file_systems= decorator
kwarg; parse.go threads it through ir.Function/Class.NetworkFileSystems;
internal/plan/efs.go resolves the real EFS filesystem/mount-target
coverage/security group; internal/exec/bootstrap.go splices the mount
lines; cmd/calque/realrun.go and fleetrun.go narrow the AZ sweep to AZs
with live mount-target coverage (a hard error, not a leak, if that narrows
to zero) and attach a self-referential NFS/2049-ingress security group via
spawnaws.LaunchConfig.SecurityGroupIDs. IAM (ClientMount/ClientWrite) is
explicitly out of scope for this pass.

This closes out both real-mapping workstreams calque#91 was tracking
(Workstream A, CloudBucketMount -> S3, shipped in #185/v0.5.1).
modal.Dict/Queue/App.include remain deliberately leak-only, out of scope.
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