config: introduce per-export configuration#27
Conversation
|
Important Installation incomplete: to start using Gemini Code Assist, please ask the organization owner(s) to visit the Gemini Code Assist Admin Console and sign the Terms of Services. |
dce7ed4 to
8f2139e
Compare
- Add #[serde(deny_unknown_fields)] to Config so legacy [fsal] sections fail to parse instead of being silently ignored, matching issue polrstorage#26 acceptance criteria. - Update deploy/k8s/deployment.yaml ConfigMap to the new [[exports]] schema; the old [fsal] schema would have failed deserialization once the previous bullet landed. - Document the new config schema in CLAUDE.md so future contributors have a definitive reference. Fixes review must-fix items on PR polrstorage#27. Phase 1 of polrstorage#26. Signed-off-by: amarok-bot <freeze.amarok@gmail.com>
a6aa97e to
be29508
Compare
|
Important Installation incomplete: to start using Gemini Code Assist, please ask the organization owner(s) to visit the Gemini Code Assist Admin Console and sign the Terms of Services. |
There was a problem hiding this comment.
Pull request overview
This PR refactors Arctic Wolf from a single-root NFSv3 server to a multi-export server with per-export configuration, routing, MOUNT support, per-export stats, and read-only enforcement—enabling Kubernetes-style multiple PersistentVolumes backed by one NFS endpoint.
Changes:
- Replace single
[fsal]config with[[exports]](validated for non-empty, uniqueuid/name, leading/,uid != 0, and unknown top-level fields rejected). - Introduce export-uid-prefixed file handles and a
MultiExportFilesystemrouter implementingNfsBackend = Filesystem + ExportRegistry. - Implement MOUNT dirpath resolution + EXPORT listing, per-export
fsstat/pathconfviafs_stats, and enforceread_onlyacross write-class NFS procedures.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/rpc/server.rs | Switch server to hold Arc<dyn NfsBackend> and upcast for MOUNT vs NFS dispatch. |
| src/protocol/v3/mount.rs | Add helpers for MNT status-only replies and hand-rolled EXPORT list serialization + tests. |
| src/nfs/write.rs | Use NfsBackend and gate writes via check_writable; update unit tests for new FSAL API. |
| src/nfs/symlink.rs | Use NfsBackend and deny symlink creation on read-only exports. |
| src/nfs/setattr.rs | Use NfsBackend and deny setattr mutations on read-only exports; update tests. |
| src/nfs/rmdir.rs | Use NfsBackend and deny rmdir on read-only exports; update tests for new LocalFilesystem ctor/root handle accessor. |
| src/nfs/rename.rs | Use NfsBackend and deny rename when either side is read-only; update tests. |
| src/nfs/remove.rs | Use NfsBackend and deny remove on read-only exports; update tests. |
| src/nfs/readlink.rs | Use NfsBackend plumbing update. |
| src/nfs/readdirplus.rs | Use NfsBackend plumbing update; update tests for new LocalFilesystem ctor/root handle accessor. |
| src/nfs/readdir.rs | Use NfsBackend plumbing update. |
| src/nfs/read.rs | Use NfsBackend plumbing update; update unit tests for new FSAL API. |
| src/nfs/pathconf.rs | Make PATHCONF per-export by querying fs_stats(handle) instead of hardcoded values. |
| src/nfs/mod.rs | Add access_check module. |
| src/nfs/mknod.rs | Use NfsBackend and deny mknod on read-only exports. |
| src/nfs/mkdir.rs | Use NfsBackend and deny mkdir on read-only exports; update tests. |
| src/nfs/lookup.rs | Use NfsBackend plumbing update; update unit tests for new FSAL API. |
| src/nfs/link.rs | Use NfsBackend and deny link when either handle is read-only. |
| src/nfs/getattr.rs | Use NfsBackend plumbing update; update unit tests for new FSAL API. |
| src/nfs/fsstat.rs | Make FSSTAT per-export via fs_stats(handle) and adjust error mapping; update tests. |
| src/nfs/fsinfo.rs | Use NfsBackend plumbing update; update tests. |
| src/nfs/dispatcher.rs | Dispatcher now takes &dyn NfsBackend and routes all procedures accordingly (COMMIT explicitly ungated). |
| src/nfs/create.rs | Use NfsBackend and deny create on read-only exports; update tests. |
| src/nfs/commit.rs | Use NfsBackend plumbing update. |
| src/nfs/access.rs | Use NfsBackend plumbing update; update tests. |
| src/nfs/access_check.rs | New helper + tests for enforcing read-only exports in write-class procedures. |
| src/mount/mod.rs | Dispatch MOUNT using &dyn ExportRegistry and implement EXPORT procedure routing. |
| src/mount/mnt.rs | Make MNT resolve client dirpath to export root handle; return MNT3ERR_NOENT on miss + tests. |
| src/mount/export.rs | Implement MOUNT EXPORT (proc 5) returning configured exports + tests. |
| src/main.rs | Wire Config from library crate, build MultiExportFilesystem from config.exports, and print per-export banner. |
| src/lib.rs | Export config module and re-export FSAL types including MultiExportFilesystem/NfsBackend. |
| src/fsal/multi_export.rs | New router implementing Filesystem + ExportRegistry, dispatching by export-uid handle prefix. |
| src/fsal/mod.rs | Split FSAL surface into Filesystem + ExportRegistry, add NfsBackend, add per-export FsStats, and adjust test-only BackendConfig. |
| src/fsal/local/mod.rs | Local FSAL now bound to export_uid, provides root_file_handle(), implements fs_stats() via statvfs/pathconf, and adds test-only ExportRegistry impl. |
| src/fsal/handle.rs | Change handle format to [uid prefix][random tail], add parsing helper, and switch to getrandom-backed handle tails. |
| src/config.rs | Replace [fsal] with validated [[exports]] schema; add deny_unknown_fields to top-level config and extensive validation tests. |
| nfstest/scripts/runner.py | Update nfstest runner to mount a configured export (/data) instead of /. |
| deploy/k8s/deployment.yaml | Update CI deployment config to define /data (rw) and /backup (ro) exports and mount corresponding volumes. |
| CLAUDE.md | Document the new [[exports]] schema, validation, and [fsal] removal. |
| Cargo.toml | Add getrandom dependency for randomized handle tails. |
| arcticwolf.example.toml | Update example config to use [[exports]] with /data and /backup and read_only. |
| ARCHITECTURE.md | Update architecture docs for multi-export, new traits, routing behavior, and completion status text. |
| .github/workflows/nfstest-factory.yml | Update integration workflow to verify EXPORT listing, mount both exports, isolation, ROFS enforcement, per-export df stats, and NOENT on unknown export. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
be29508 to
aecfc47
Compare
aecfc47 to
c3b868a
Compare
c3b868a to
88d8be7
Compare
Phase 1 of polrstorage#26. Replaces the single `[fsal]` section with an `[[exports]]` array. Each export specifies `name`, `uid` (u32, 1..=u32::MAX, unique), `backend` (tagged enum, `local` only in v1), backend-specific fields (path for `local`), and an optional `read_only` flag. Config::validate() fails fast on empty exports, uid==0, duplicate uid, duplicate name, or names not starting with "/". Duplicate uid errors report both the original and conflicting export names so users can locate both occurrences quickly. The Config struct uses #[serde(deny_unknown_fields)] so legacy [fsal] sections fail to parse instead of being silently ignored. BackendConfig also denies unknown fields so a typo inside a backend block (e.g. `pat` for `path`) is rejected at parse time rather than silently dropped. ExportConfig itself cannot use deny_unknown_fields because the `backend` field is `#[serde(flatten)]` and serde rejects that combination — at the outer struct level the flattened type's own fields get reported as unknown. The trade-off: typos at the export level (e.g. `readOnly` for `read_only`) still parse silently and must be caught by review or external schema validation. BackendConfig::name() returns a static string for the active variant so banner/log code does not need to hardcode backend names. main.rs temporarily consumes config.exports[0] to keep the existing single-FSAL plumbing compiling — later phases (polrstorage#26) replace this with the per-export registry. It also prints a warning if more than one export is configured, since Phase 1 only serves the first. Updates: - arcticwolf.example.toml to the new schema - deploy/k8s/deployment.yaml ConfigMap to the new schema - CLAUDE.md with a Configuration Schema section documenting the required fields and validation rules Tests cover: parsing multiple exports, the local-backend tagged enum, unknown backend discriminators, unknown fields inside a backend block, empty TOML using defaults, and every validation failure mode (empty list, uid==0, duplicate uid, duplicate name, name not starting with "/"). Signed-off-by: amarok-bot <freeze.amarok@gmail.com>
88d8be7 to
bdab933
Compare
bdab933 to
97f8c32
Compare
Phase 2 of polrstorage#26. File handles now reserve the first 4 bytes for the export uid (big-endian); the remaining 28 bytes are the existing random opaque id. HandleManager and LocalFilesystem take an export_uid at construction so the wrapper added in Phase 3 can route operations by parsing the prefix. FileHandleExt::export_uid() returns Option<u32> and rejects slices shorter than the uid prefix, so the Phase 3 router can feed untrusted wire bytes through it without panicking. HandleManager uses the getrandom crate instead of opening /dev/urandom on every call, keeping handle creation off the syscall hot path. fsal::BackendConfig changes from a struct to an enum mirroring config::BackendConfig so create_filesystem(export_uid) can dispatch per backend; v1 still only ships the Local variant. This is a wire-format change to file handles. Acceptable since the project is pre-release. Signed-off-by: amarok-bot <freeze.amarok@gmail.com>
Phase 3 of polrstorage#26. Adds: - ExportRegistry trait — root_handle_for(name), list_exports(), is_read_only(handle), export_for_handle(handle). - NfsBackend super-trait (Filesystem + ExportRegistry) with a blanket impl, so RpcServer can keep a single Arc<dyn NfsBackend> while later phases hand each dispatcher the trait view it needs. - MultiExportFilesystem in src/fsal/multi_export.rs implementing both traits. It owns HashMap<u32, ExportEntry> keyed by export uid (with a name index for MOUNT MNT) and routes Filesystem operations to the inner LocalFilesystem identified by the handle's uid prefix. - Inherent LocalFilesystem::root_file_handle() so the wrapper can fetch each export's root without shadowing the async trait method. The trait method is kept until Phase 4, where MOUNT MNT switches to ExportRegistry::root_handle_for(). Error strings raised by the wrapper align with the substring-based matchers in src/nfs/* so the NFS status codes returned over the wire are correct: - Cross-export rename/link → NFS3ERR_XDEV via "cross-device ..." - Short or unknown-uid handle → NFS3ERR_STALE via "Invalid handle: ..." LocalFilesystem::resolve_handle's pre-existing "Invalid file handle" string is also aligned so existing single-export paths return STALE instead of falling through to IO. main.rs constructs MultiExportFilesystem::build_from_config(), prints every configured export at startup, and emits a warning when more than one export is configured to flag that MOUNT MNT will still route every mount request to the lowest-uid export until Phase 4. No MOUNT/NFS dispatcher signature changes yet (Phase 4/5 work). Signed-off-by: amarok-bot <freeze.amarok@gmail.com>
Phase 4 of polrstorage#26. MOUNT dispatcher now operates against &dyn ExportRegistry instead of &dyn Filesystem; rpc::server upcasts &dyn NfsBackend to that view at dispatch time. The MNT handler resolves the client-supplied dirpath via ExportRegistry::root_handle_for(name) and returns MNT3ERR_NOENT for unknown paths (constant bound to the xdrgen- generated mountstat3 enum). MOUNT EXPORT (procedure 5) is implemented in src/mount/export.rs. The exports/exportnode/groupnode wire format is hand-rolled in src/protocol/v3/mount.rs::serialize_exports because xdrgen 0.4 handles self-referential linked lists awkwardly; three byte-layout unit tests (empty, single entry, two entries) lock the wire format against regressions. showmount -e now lists every configured export. The Filesystem trait no longer exposes root_handle() — MOUNT MNT was its only caller. LocalFilesystem retains its inherent root_file_handle() for the wrapper to call internally; all NFS handler tests switch over. deploy/k8s/deployment.yaml now ships two exports (/data on a PVC, /backup on emptyDir) so the nfstest workflow exercises multi-export routing end-to-end. .github/workflows/nfstest-factory.yml verifies that showmount -e lists both exports, both mount independently, files written under /data are not visible under /backup, mounting an unknown export path fails, and nfstest_posix runs against /data. ARCHITECTURE.md is updated to drop root_handle() from the FSAL trait example and point readers at ExportRegistry::root_handle_for. No NFS dispatcher signature changes yet (Phase 5 work). Signed-off-by: amarok-bot <freeze.amarok@gmail.com>
97f8c32 to
264e0ac
Compare
264e0ac to
c8e3c9b
Compare
Phase 5 of polrstorage#26. The NFS dispatcher widens to &dyn NfsBackend so every per-operation handler can borrow either the Filesystem or ExportRegistry view it needs without a parallel dispatch table. Filesystem::fs_stats(handle) is a new trait method returning per- export statvfs(2) + pathconf(2) data. LocalFilesystem implements it via raw libc calls inside tokio::task::spawn_blocking so the runtime is not stalled; MultiExportFilesystem routes by the handle's uid prefix. fsstat and pathconf reply with per-export data; fsinfo keeps server-side advertised constants because its fields (rtmax/wtmax/dtpref/maxfilesize) are not per-mount facts. A new check_writable(registry, handle) helper short-circuits all ten write-class handlers (write/create/setattr/mkdir/mknod/remove/ rmdir/rename/symlink/link) with NFS3ERR_ROFS when the handle's export has read_only=true. RENAME and LINK gate both involved handles since both write to the source export. COMMIT is intentionally not gated, matching Linux nfs-utils behaviour. impl ExportRegistry for LocalFilesystem is gated behind #[cfg(test)] so unit tests can use a bare LocalFilesystem as &dyn NfsBackend while production code can never reach the degenerate "always rw, empty exports" answers; main.rs only constructs &dyn NfsBackend via MultiExportFilesystem. deploy/k8s/deployment.yaml flips /backup to read_only=true so the nfstest workflow exercises the EROFS path against a real Linux client. .github/workflows/nfstest-factory.yml adds an explicit write/mkdir failure assertion against /backup, confirms reads still succeed, and verifies per-export df stats. ARCHITECTURE.md is refreshed to match the post-polrstorage#26 trait layout (Filesystem keeps per-handle ops + fs_stats; ExportRegistry holds per-export queries; NfsBackend super-trait combines them) and the post-Phase-4 dispatcher signatures. Stale 13/22 implementation status tables and the "hardcoded /tmp/nfs_exports" TODO are removed. main.rs now consumes config via use arcticwolf::config instead of declaring its own mod config. Signed-off-by: amarok-bot <freeze.amarok@gmail.com>
c8e3c9b to
f5bf832
Compare
Closes #26.
Overview
Arctic Wolf moves from a single-root NFSv3 server to multi-export. The repository now serves multiple independently configured exports, each with its own root, uid, FSAL backend, and (optional) read-only flag — the standard model for backing Kubernetes PersistentVolumes against a shared NFS server.
The change is delivered in five squashed commits, one per phase, all DCO-signed:
e25f4c3bb5590cae04416MultiExportFilesystemwrapper (also pinsrust-version = "1.86")a90cb4588d8be7All four CI jobs (Build / Test / Lint / NFS Integration Test) are green on the final head. The integration test runs against a real Linux NFS client mounting both
/data(rw, PVC) and/backup(ro, emptyDir).What changes (user-visible)
Config
[fsal]is removed. New[[exports]]array. Each entry must specifyname(dirpath the client mounts),uid(u32 in1..=u32::MAX, unique, embedded in file handles),backend(tagged enum —localonly in v1), the backend-specific fields (forlocal:path), and an optionalread_only(default false).uid == 0, duplicateuid, duplicatename, and names that do not start with/.#[serde(deny_unknown_fields)]onConfigandBackendConfig: any leftover[fsal]section in an old config now fails to parse with an explicit error, and typos inside a backend block (e.g.patforpath) are rejected at parse time.ExportConfigcannot apply#[serde(deny_unknown_fields)]directly because itsbackendfield is#[serde(flatten)]and serde rejects that combination, but export-level typos (e.g.readOnlyinstead ofread_only) are still rejected at config-load time: unknown keys at the export level cascade through the#[serde(flatten)]intoBackendConfig, which carries#[serde(deny_unknown_fields)], so e.g.readOnly = truefails withunknown field 'readOnly', expected 'path'. Non-flattened sections ([server],[logging]) are independently caught byserde_ignoredinConfig::load().Wire protocol (NFSv3 / MOUNT)
MultiExportFilesystemrouter parses the prefix to dispatch to the correct innerLocalFilesystem.MNT3ERR_NOENT.showmount -e <server>lists every configured export.WRITE,CREATE,SETATTR,MKDIR,MKNOD,REMOVE,RMDIR,RENAME,SYMLINK,LINK) reject withNFS3ERR_ROFSwhen the handle belongs to aread_onlyexport.COMMITis intentionally not gated (matches Linux nfs-utils).fsstatandpathconfreport per-exportstatvfs(2)+pathconf(2)data.fsinfokeeps server-side advertised constants (rtmax/wtmax/dtpref/maxfilesize are not per-mount facts). Stale or short handles surface asNFS3ERR_STALErather than falling through toNFS3ERR_IO.File-handle wire format is a breaking change, acceptable since the project is pre-release. Clients caching handles across server restarts will see them invalidated; CHANGELOG-equivalent rationale lives in commit
bb5590c.Architecture
The FSAL surface splits into two traits, joined by a supertrait so
RpcServerkeeps a singleArc<dyn NfsBackend>:Filesystem— per-handle operations (lookup, read, write, readdir, setattr, create/remove/mkdir/rmdir/rename/symlink/readlink/link/commit/mknod, plus the newfs_stats(handle))ExportRegistry— per-export queries:root_handle_for(name),list_exports(),is_read_only(handle),export_for_handle(handle)NfsBackend: Filesystem + ExportRegistry(blanket impl)MultiExportFilesysteminsrc/fsal/multi_export.rsimplements both traits and routes eachFilesystemcall to the innerLocalFilesystemidentified by the handle's uid prefix. Production code only ever constructsArc<dyn NfsBackend>viaMultiExportFilesystem::build_from_config; a degenerateimpl ExportRegistry for LocalFilesystemis gated behind#[cfg(test)]so unit tests can use a bareLocalFilesystemas&dyn NfsBackendwithout leaking the always-rw answer into a binary.Review evolution
The PR went through three rounds of automated Copilot review and two independent Opus 4.7 (1M context) reviews before merge-readiness:
frsizefallback infs_statswhen the platform returns 0, and absolute-path normalization in the local backend so config-relative paths don't silently break handle resolution.#[serde(deny_unknown_fields)]applied toConfigandBackendConfig(with the documentedExportConfigflatten limitation), andpathconf/fsstaterror mapping tightened so stale or short handles surface asNFS3ERR_STALEinstead ofNFS3ERR_IO.rust-version = "1.86"declared inCargo.tomlso the MSRV is explicit and CI can pin against it (folded into the Phase 3 commit,ae04416).Acceptance criteria (issue #26)
config.tomlaccepts[[exports]]with name/uid/backend/path/read_onlyuid==0/ duplicate uid / duplicate name / name not starting with/[fsal]section produces a parse errormount server:/dataandmount server:/backupsucceed independently; cross-export isolation verifiedmount server:/nonexistentfails with ENOENT (MNT3ERR_NOENT)showmount -e <server>lists every configured exportread_only = trueexport return EROFS (NFS3ERR_ROFS)dfreports per-export filesystem statisticsuidin big-endianFilesystemno longer exposesroot_handle();ExportRegistry::root_handle_for(name)is the new entry pointmake testandmake nfstestpass; multi-export covered by Rust unit tests + a real-client CI integration suite (delivered via.github/workflows/nfstest-factory.ymlrather than the originally-proposed standalonetests/test_mount_multi_export.py/tests/test_export_isolation.pyfiles — same behavioral coverage)arcticwolf.example.tomlandCLAUDE.mddocument the new schemaTest coverage
[fsal]rejection and unknown backend discriminator); handle uid encode/decode round-trip and short-slice rejection;MultiExportFilesystembuild, name and uid lookups,is_read_onlypaths, handle routing, cross-export rename/link rejection wired toNFS3ERR_XDEV;check_writablerw/ro/unknown-uid behaviour;LocalFilesystemfs_statsinvariants; MOUNT MNT hit/miss/empty; MOUNT EXPORT byte-layout (empty / one entry / two entries).deploy/k8s/deployment.yamlships/data(rw) and/backup(ro)..github/workflows/nfstest-factory.ymlverifies showmount lists both exports, both mount, cross-export isolation, MNT of an unknown path fails, writes to/backupreturn EROFS while reads still work,dfreports per-export stats, andnfstest_posixruns against/data.Out of scope / follow-ups
These are intentionally not in this PR; they'll be tracked as separate issues:
allowed_clientsstyle host control.subtree_check,secure, and other NFS export options — kernel-style fine-grained behaviour knobs.BackendConfigis already a tagged enum, but S3 / Ceph backends are not implemented; their arrival will widenBackendConfig::create_filesystemback to a trait object (currently narrowed toBox<LocalFilesystem>so NFS unit tests can call the inherentroot_file_handle()).src/nfs/rename.rsandsrc/nfs/link.rsuseerror.to_string().contains("cross-device")style matchers; a future refactor should switch to a typed error enum.tests/test_mount_multi_export.py/tests/test_export_isolation.pywere folded into the nfstest CI workflow; splitting them back out would help developers reproduce CI behavior locally without spinning up the full k8s harness.References
src/config.rs:38–43(pre-[FEATURE] Support multiple NFS exports #26 line numbers)src/mount/mnt.rs:36–50(pre-[FEATURE] Support multiple NFS exports #26)src/mount/mod.rs:95–98(pre-[FEATURE] Support multiple NFS exports #26)src/fsal/local/mod.rs:18–58(pre-[FEATURE] Support multiple NFS exports #26)src/fsal/mod.rs:96–312(pre-[FEATURE] Support multiple NFS exports #26)src/main.rs:107–123(pre-[FEATURE] Support multiple NFS exports #26)src/fsal/handle.rs:18–113(pre-[FEATURE] Support multiple NFS exports #26)Arc<dyn NfsBackend>):src/rpc/server.rs:20–78(pre-[FEATURE] Support multiple NFS exports #26)Marking this PR ready for review.