You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Arctic Wolf currently has no admin interface — operators can't inspect runtime state, change settings without restarting, or coordinate shutdown without a hard signal. This issue tracks the v1 admin CLI: a daemon-side Unix domain socket server, a separate arcticwolfctl client binary, basic observability, and runtime mutation of the export list that landed in #26.
The multi-export work (#26 / PR #27) is the prerequisite — admin operations on exports only make sense once the FSAL supports more than one. With that merged, the admin CLI can build on top of ExportRegistry/NfsBackend cleanly.
Goals
Runtime export mutation — add, remove, and toggle read-only on exports without restarting the daemon. In-memory only in v1.
Metrics — counters and histograms exposed through the admin protocol for operators and (later) scrapers.
Audit trail — every admin write recorded in a JSONL audit log so operators can answer "who changed what, when?"
Architecture summary
New arcticwolfctl binary (separate [[bin]] from arcticwolf daemon), follows the systemctl / kubectl pattern.
Daemon listens on a Unix domain socket (default /run/arcticwolf/admin.sock, mode 0600). No TCP transport in v1 — cross-pod admin in Kubernetes is expected to go via kubectl exec.
Wire protocol: length-prefixed JSON. Each message is one AdminRequest → one AdminResponse. Reasoning: framing is explicit; payloads containing newlines (log messages, multi-line errors) don't require escaping; tokio_util::codec::LengthDelimitedCodec is ready-made. Debug ergonomics covered by an arcticwolfctl raw subcommand.
The daemon's tokio::select! gains an admin task alongside the three RPC servers; a CancellationToken is shared so SIGTERM/SIGINT and the admin shutdown command all flow through the same path.
MultiExportFilesystem is refactored from a build-once HashMap to an arc-swap::ArcSwap<ExportsSnapshot>. Read path stays wait-free (every NFS RPC reads); admin write rebuilds a small snapshot (typically <10 exports) and atomically swaps. This avoids the cross-await lock-guard hazards of RwLock.
Runtime export semantics
All export mutation is in-memory only. The config file is never rewritten by the admin CLI in v1. After daemon restart the in-memory state reverts to the file.
exports update accepts only --read-only=<bool> in v1. Changing name, uid, backend, or path is rejected (use exports remove + exports add to migrate, accepting the file-handle stale cost).
When an export is removed, its uid is moved into a retired_uids: HashSet<u32> and cannot be reused by a subsequent exports add until the daemon restarts. This prevents in-flight client handles whose first 4 bytes still encode that uid from being silently re-routed to a new, unrelated export. The set is cleared on startup since the configuration file is re-read fresh.
After exports remove, clients holding handles for that export will see NFS3ERR_STALE on their next operation — this is the existing behavior of MultiExportFilesystem::entry_for_handle plus classify_handle_error and requires no new code.
Scope
In scope (v1)
Daemon-side
Unix domain socket admin server with length-prefixed JSON framing.
New [admin] config section: enabled (bool, default false), socket_path, socket_mode.
MultiExportFilesystem migration to ArcSwap<ExportsSnapshot> with retired_uids.
tracing_subscriber::reload::Layer so log level can be changed without restart.
CancellationToken-based graceful shutdown that drains in-flight RPCs.
SIGTERM and SIGINT trigger the same graceful path.
Metrics registry (metrics + metrics-exporter-prometheus) instrumented in the NFS dispatcher and FSAL: per-procedure RPC counters and latency histograms, per-export bytes counters, error counts by nfsstat3, active connection gauges by service.
JSONL audit log (default /var/log/arcticwolf/audit.jsonl, configurable). Each admin write captures peer pid/uid via SO_PEERCRED, command, args, result, and duration. Append-only; rotation is left to the system (e.g. logrotate).
[audit] config section (path; enabled flag).
Client-side (arcticwolfctl)
status — uptime, bind address, actual listening ports, daemon version, current log level, number of exports.
version — client and daemon versions (build commit / Rust version via vergen).
log-level get / log-level set <level> — adjust global tracing filter at runtime.
config show — emit the live in-memory Config as JSON. No masking in v1 (no sensitive fields exist; revisit when S3 backend lands).
exports list — list configured exports (name, uid, backend, path, read_only).
exports add --name <path> --uid <u32> --backend local --path <path> [--read-only] [--dry-run] — add a new export. --dry-run validates the candidate (path resolution, uid uniqueness, name uniqueness, no collision with retired_uids) without committing.
exports remove <name|uid> — remove an export; its uid is retired until restart.
exports update <name|uid> --read-only=<bool> [--dry-run] — toggle the read-only flag.
metrics [--format json|prometheus] — render the metrics registry; JSON by default.
raw '<json>' — send a raw admin request; debug aid.
Global flag --socket <path> to override the default socket location.
Out of scope (tracked separately)
HTTP server (/metrics, /healthz, /readyz) — defer; Prometheus scraping is expected to use kubectl exec arcticwolfctl metrics --format prometheus in v1.
Configuration persistence — admin writes are never reflected back to the config file. Operators who want changes to survive restart edit the file directly.
exports update --name/--uid/--backend/--path — requires extra handle-rewrite or migration semantics; revisit only if a concrete need surfaces.
config show masking for sensitive fields — implement when the first backend with secrets (S3, Ceph) is introduced.
Per-module tracing filter (debug enable-tracing --filter <env_filter>) — v1 only supports global level.
Mount-table tracking and clients list — requires non-trivial state; large enough for its own issue.
TCP / mTLS admin transport — Unix socket plus kubectl exec covers v1 deployment.
Role-based admin authorization — single-tier privilege in v1 (whoever can open() the socket is admin).
config reload — explicit per-resource commands cover the v1 mutation surface; "reload from file" introduces ambiguity (file vs in-memory) that we don't yet need.
arcticwolfctl binary builds from cargo build --release --bin arcticwolfctl and is included in the release artifact path; symlink/install instructions are documented in README.md.
Daemon refuses to start with [admin] enabled = true if socket_path's parent directory does not exist or is not writable; error is explicit.
When [admin] enabled = false (the default), no admin socket is created and admin code paths never run; make test still passes.
arcticwolfctl status returns daemon_version, uptime_seconds, bind_address, the actual nfs_port/mount_port, log_level, and export_count.
arcticwolfctl version shows the client version, the daemon's reported version, and includes a build_commit field derived from vergen.
arcticwolfctl log-level set debug changes the live filter; a subsequent NFS request logs at debug level without a daemon restart. log-level set tomato returns an error and the filter is unchanged.
arcticwolfctl config show returns valid JSON that reflects the in-memory config (including any runtime export mutations applied so far in the current process).
arcticwolfctl exports list lists every configured export with name, uid, backend, path, and read_only.
arcticwolfctl exports add --name /new --uid 9 --backend local --path /srv/new makes the export visible to subsequent arcticwolfctl exports list calls and to mount -t nfs server:/new from a Linux client.
arcticwolfctl exports add --uid 9 ... is rejected when uid 9 is currently in use, when 9 is in retired_uids, or when the candidate fails any of the existing Config::validate() invariants (e.g. relative path, name not starting with /).
arcticwolfctl exports add --dry-run ... validates but does not commit; the daemon's state is unchanged and exports list does not include the candidate.
arcticwolfctl exports remove <name> removes the export; a client holding a handle for that export receives NFS3ERR_STALE on its next request.
After exports remove /old --uid 7, a subsequent exports add --uid 7 ... is rejected with a "uid retired this run" error until the daemon restarts.
arcticwolfctl exports update /data --read-only=true flips the export to read-only; subsequent WRITE requests against that export return NFS3ERR_ROFS.
arcticwolfctl exports update /data --name=/renamed is rejected in v1 with a clear "field not mutable in v1" message.
arcticwolfctl metrics returns JSON containing at minimum arcticwolf_nfs_rpc_total{proc=...}, arcticwolf_nfs_errors_total{proc=...,nfsstat=...}, arcticwolf_nfs_bytes_total{export=...,direction=...}, and arcticwolf_rpc_connections_active{service=...}. --format prometheus returns the same data in Prometheus exposition format.
arcticwolfctl shutdown returns success when the daemon process exits cleanly within the timeout; in-flight RPCs are allowed to complete.
SIGTERM and SIGINT to the daemon trigger the same graceful path as arcticwolfctl shutdown.
Every admin command that mutates state (exports add/remove/update, log-level set, shutdown) writes one JSONL record to the audit log containing ts, peer_pid, peer_uid, command, args, result, and duration_ms. Read-only commands (status, version, exports list, config show, metrics) do not write audit records.
If the audit log path cannot be written (disk full, perms), the admin command still completes; the daemon logs a single warning. The admin response includes audit_warning: "..." in that case.
arcticwolfctl --socket /tmp/test.sock status connects to a non-default socket location.
arcticwolfctl raw '{"command": "status"}' succeeds and prints the raw JSON response.
make test passes. make nfstest passes including new admin-vs-NFS interaction tests (notably: exports remove followed by an in-flight client returns NFS3ERR_STALE; exports update --read-only=true is observed by an NFS client as EROFS).
README.md documents how to enable the admin server, the available arcticwolfctl commands, and the Kubernetes kubectl exec workflow.
Implementation phases
Each phase is one squashed commit. make build, make test, make lint must pass on every phase; make nfstest must pass on the phases marked NFS below.
Admin transport scaffolding. New src/admin/ module: protocol.rs (length-prefixed JSON codec with tokio_util::codec::LengthDelimitedCodec), request.rs/response.rs enums, server.rs accepting connections and routing to a not_implemented dispatcher, context.rs carrying shared state. New [admin] config section with enabled = false by default. main.rs spawns the admin task into tokio::select!. Adds tokio-util and nix (for SO_PEERCRED) to Cargo.toml. Tests: codec round-trip; unknown command error shape.
arcticwolfctl binary + status + version. New src/bin/arcticwolfctl.rs with a clap-driven subcommand tree. Implements status and version commands end-to-end. Adds vergen as a build dep for build_commit/rustc info. Tests: spawn daemon + ctl in tokio::test; assert JSON shape.
Log level reload (log-level get/set). Migrates tracing_subscriber::fmt().init() to EnvFilter + reload::Layer. Reload handle stored on AdminContext. Tests: set → get round-trip; invalid level string returns an error.
MultiExportFilesystemarc-swap refactor. Switches exports/name_index storage to ArcSwap<ExportsSnapshot> where the snapshot also carries retired_uids. Adds inherent methods add_export(), remove_export(), update_export() on MultiExportFilesystem. All trait methods (Filesystem and ExportRegistry) clone the inner Arc<LocalFilesystem> out of the snapshot before any .await, never holding a guard across an await. This phase does not add any admin command; behavior to NFS clients is identical to the current state. Tests: existing multi_export.rs tests must pass; new tests exercise add_export / remove_export / update_export directly, including uid retirement. NFS — full nfstest required.
Admin commands: config show + exports list/add/remove/update + --dry-run. Wires the new inherent methods to admin commands. Adds Serialize impls on Config/ExportConfig/BackendConfig for config show. Tests: end-to-end exports add → list → remove → list; dry-run does not commit; retired uid rejection; exports update --read-only observable to a real Linux NFS client as EROFS. NFS — full nfstest required.
Audit log. New src/admin/audit.rs with append-only JSONL writer. SO_PEERCRED extraction via nix::sys::socket. New [audit] config section. Audit write failures emit a warning but do not block admin operations. Tests: every mutating command produces exactly one record; read-only commands produce zero; audit write failure path returns audit_warning in the response.
Metrics. Adds metrics and metrics-exporter-prometheus to Cargo.toml. Installs a PrometheusBuilder recorder at daemon startup. Instruments the NFS dispatcher (counter!/histogram! per-procedure), read/write handlers (per-export bytes), the RPC server (active connections gauge), and the dispatcher's error path (errors by nfsstat3). Implements arcticwolfctl metrics [--format json|prometheus]. Tests: counters/histograms increment as expected over a small NFS workload; both output formats parse.
Graceful shutdown + signal handling. Introduces tokio_util::sync::CancellationToken. Each RpcServer::serve_until(cancel) selects on accept() vs cancel.cancelled(). Active connections drain via a JoinSet before the task returns. tokio::signal::unix listens for SIGTERM and SIGINT. Admin shutdown command and signals both trigger the same path. Tests: admin shutdown, SIGTERM, SIGINT all cause the process to exit cleanly within the timeout; an in-flight large WRITE completes successfully. NFS — full nfstest required.
Background
Arctic Wolf currently has no admin interface — operators can't inspect runtime state, change settings without restarting, or coordinate shutdown without a hard signal. This issue tracks the v1 admin CLI: a daemon-side Unix domain socket server, a separate
arcticwolfctlclient binary, basic observability, and runtime mutation of the export list that landed in #26.The multi-export work (#26 / PR #27) is the prerequisite — admin operations on exports only make sense once the FSAL supports more than one. With that merged, the admin CLI can build on top of
ExportRegistry/NfsBackendcleanly.Goals
Architecture summary
arcticwolfctlbinary (separate[[bin]]fromarcticwolfdaemon), follows thesystemctl/kubectlpattern./run/arcticwolf/admin.sock, mode0600). No TCP transport in v1 — cross-pod admin in Kubernetes is expected to go viakubectl exec.AdminRequest→ oneAdminResponse. Reasoning: framing is explicit; payloads containing newlines (log messages, multi-line errors) don't require escaping;tokio_util::codec::LengthDelimitedCodecis ready-made. Debug ergonomics covered by anarcticwolfctl rawsubcommand.tokio::select!gains an admin task alongside the three RPC servers; aCancellationTokenis shared so SIGTERM/SIGINT and the adminshutdowncommand all flow through the same path.MultiExportFilesystemis refactored from a build-onceHashMapto anarc-swap::ArcSwap<ExportsSnapshot>. Read path stays wait-free (every NFS RPC reads); admin write rebuilds a small snapshot (typically <10 exports) and atomically swaps. This avoids the cross-awaitlock-guard hazards ofRwLock.Runtime export semantics
exports updateaccepts only--read-only=<bool>in v1. Changingname,uid,backend, orpathis rejected (useexports remove+exports addto migrate, accepting the file-handle stale cost).uidis moved into aretired_uids: HashSet<u32>and cannot be reused by a subsequentexports adduntil the daemon restarts. This prevents in-flight client handles whose first 4 bytes still encode thatuidfrom being silently re-routed to a new, unrelated export. The set is cleared on startup since the configuration file is re-read fresh.exports remove, clients holding handles for that export will seeNFS3ERR_STALEon their next operation — this is the existing behavior ofMultiExportFilesystem::entry_for_handleplusclassify_handle_errorand requires no new code.Scope
In scope (v1)
Daemon-side
[admin]config section:enabled(bool, defaultfalse),socket_path,socket_mode.MultiExportFilesystemmigration toArcSwap<ExportsSnapshot>withretired_uids.tracing_subscriber::reload::Layerso log level can be changed without restart.CancellationToken-based graceful shutdown that drains in-flight RPCs.metrics+metrics-exporter-prometheus) instrumented in the NFS dispatcher and FSAL: per-procedure RPC counters and latency histograms, per-export bytes counters, error counts bynfsstat3, active connection gauges by service./var/log/arcticwolf/audit.jsonl, configurable). Each admin write captures peerpid/uidviaSO_PEERCRED, command, args, result, and duration. Append-only; rotation is left to the system (e.g.logrotate).[audit]config section (path; enabled flag).Client-side (
arcticwolfctl)status— uptime, bind address, actual listening ports, daemon version, current log level, number of exports.version— client and daemon versions (build commit / Rust version viavergen).log-level get/log-level set <level>— adjust globaltracingfilter at runtime.config show— emit the live in-memoryConfigas JSON. No masking in v1 (no sensitive fields exist; revisit when S3 backend lands).exports list— list configured exports (name, uid, backend, path, read_only).exports add --name <path> --uid <u32> --backend local --path <path> [--read-only] [--dry-run]— add a new export.--dry-runvalidates the candidate (path resolution, uid uniqueness, name uniqueness, no collision withretired_uids) without committing.exports remove <name|uid>— remove an export; its uid is retired until restart.exports update <name|uid> --read-only=<bool> [--dry-run]— toggle the read-only flag.metrics [--format json|prometheus]— render the metrics registry; JSON by default.shutdown [--timeout=<seconds>]— request graceful shutdown.raw '<json>'— send a raw admin request; debug aid.--socket <path>to override the default socket location.Out of scope (tracked separately)
/metrics,/healthz,/readyz) — defer; Prometheus scraping is expected to usekubectl exec arcticwolfctl metrics --format prometheusin v1.exports update --name/--uid/--backend/--path— requires extra handle-rewrite or migration semantics; revisit only if a concrete need surfaces.config showmasking for sensitive fields — implement when the first backend with secrets (S3, Ceph) is introduced.debug enable-tracing --filter <env_filter>) — v1 only supports global level.clients list— requires non-trivial state; large enough for its own issue.kubectl execcovers v1 deployment.open()the socket is admin).config reload— explicit per-resource commands cover the v1 mutation surface; "reload from file" introduces ambiguity (file vs in-memory) that we don't yet need.open_handles{export=...}metric — requires a persistent handle table; ties into [TASK] Persistent file handle for LocalFilesystem #10 (if applicable) or a future tracking system.Acceptance criteria
arcticwolfctlbinary builds fromcargo build --release --bin arcticwolfctland is included in the release artifact path; symlink/install instructions are documented inREADME.md.[admin] enabled = trueifsocket_path's parent directory does not exist or is not writable; error is explicit.[admin] enabled = false(the default), no admin socket is created and admin code paths never run;make teststill passes.arcticwolfctl statusreturnsdaemon_version,uptime_seconds,bind_address, the actualnfs_port/mount_port,log_level, andexport_count.arcticwolfctl versionshows the client version, the daemon's reported version, and includes abuild_commitfield derived fromvergen.arcticwolfctl log-level set debugchanges the live filter; a subsequent NFS request logs at debug level without a daemon restart.log-level set tomatoreturns an error and the filter is unchanged.arcticwolfctl config showreturns valid JSON that reflects the in-memory config (including any runtime export mutations applied so far in the current process).arcticwolfctl exports listlists every configured export withname,uid,backend,path, andread_only.arcticwolfctl exports add --name /new --uid 9 --backend local --path /srv/newmakes the export visible to subsequentarcticwolfctl exports listcalls and tomount -t nfs server:/newfrom a Linux client.arcticwolfctl exports add --uid 9 ...is rejected when uid 9 is currently in use, when 9 is inretired_uids, or when the candidate fails any of the existingConfig::validate()invariants (e.g. relative path, name not starting with/).arcticwolfctl exports add --dry-run ...validates but does not commit; the daemon's state is unchanged andexports listdoes not include the candidate.arcticwolfctl exports remove <name>removes the export; a client holding a handle for that export receivesNFS3ERR_STALEon its next request.exports remove /old --uid 7, a subsequentexports add --uid 7 ...is rejected with a "uid retired this run" error until the daemon restarts.arcticwolfctl exports update /data --read-only=trueflips the export to read-only; subsequentWRITErequests against that export returnNFS3ERR_ROFS.arcticwolfctl exports update /data --name=/renamedis rejected in v1 with a clear "field not mutable in v1" message.arcticwolfctl metricsreturns JSON containing at minimumarcticwolf_nfs_rpc_total{proc=...},arcticwolf_nfs_errors_total{proc=...,nfsstat=...},arcticwolf_nfs_bytes_total{export=...,direction=...}, andarcticwolf_rpc_connections_active{service=...}.--format prometheusreturns the same data in Prometheus exposition format.arcticwolfctl shutdownreturns success when the daemon process exits cleanly within the timeout; in-flight RPCs are allowed to complete.SIGTERMandSIGINTto the daemon trigger the same graceful path asarcticwolfctl shutdown.exports add/remove/update,log-level set,shutdown) writes one JSONL record to the audit log containingts,peer_pid,peer_uid,command,args,result, andduration_ms. Read-only commands (status,version,exports list,config show,metrics) do not write audit records.audit_warning: "..."in that case.arcticwolfctl --socket /tmp/test.sock statusconnects to a non-default socket location.arcticwolfctl raw '{"command": "status"}'succeeds and prints the raw JSON response.make testpasses.make nfstestpasses including new admin-vs-NFS interaction tests (notably:exports removefollowed by an in-flight client returnsNFS3ERR_STALE;exports update --read-only=trueis observed by an NFS client asEROFS).README.mddocuments how to enable the admin server, the availablearcticwolfctlcommands, and the Kuberneteskubectl execworkflow.Implementation phases
Each phase is one squashed commit.
make build,make test,make lintmust pass on every phase;make nfstestmust pass on the phases marked NFS below.Admin transport scaffolding. New
src/admin/module:protocol.rs(length-prefixed JSON codec withtokio_util::codec::LengthDelimitedCodec),request.rs/response.rsenums,server.rsaccepting connections and routing to anot_implementeddispatcher,context.rscarrying shared state. New[admin]config section withenabled = falseby default.main.rsspawns the admin task intotokio::select!. Addstokio-utilandnix(forSO_PEERCRED) toCargo.toml. Tests: codec round-trip; unknown command error shape.arcticwolfctlbinary +status+version. Newsrc/bin/arcticwolfctl.rswith aclap-driven subcommand tree. Implementsstatusandversioncommands end-to-end. Addsvergenas a build dep forbuild_commit/rustcinfo. Tests: spawn daemon + ctl intokio::test; assert JSON shape.Log level reload (
log-level get/set). Migratestracing_subscriber::fmt().init()toEnvFilter+reload::Layer. Reload handle stored onAdminContext. Tests: set → get round-trip; invalid level string returns an error.MultiExportFilesystemarc-swaprefactor. Switchesexports/name_indexstorage toArcSwap<ExportsSnapshot>where the snapshot also carriesretired_uids. Adds inherent methodsadd_export(),remove_export(),update_export()onMultiExportFilesystem. All trait methods (FilesystemandExportRegistry) clone the innerArc<LocalFilesystem>out of the snapshot before any.await, never holding a guard across an await. This phase does not add any admin command; behavior to NFS clients is identical to the current state. Tests: existingmulti_export.rstests must pass; new tests exerciseadd_export/remove_export/update_exportdirectly, including uid retirement. NFS — full nfstest required.Admin commands:
config show+exports list/add/remove/update+--dry-run. Wires the new inherent methods to admin commands. AddsSerializeimpls onConfig/ExportConfig/BackendConfigforconfig show. Tests: end-to-endexports add → list → remove → list; dry-run does not commit; retired uid rejection;exports update --read-onlyobservable to a real Linux NFS client asEROFS. NFS — full nfstest required.Audit log. New
src/admin/audit.rswith append-only JSONL writer.SO_PEERCREDextraction vianix::sys::socket. New[audit]config section. Audit write failures emit a warning but do not block admin operations. Tests: every mutating command produces exactly one record; read-only commands produce zero; audit write failure path returnsaudit_warningin the response.Metrics. Adds
metricsandmetrics-exporter-prometheustoCargo.toml. Installs aPrometheusBuilderrecorder at daemon startup. Instruments the NFS dispatcher (counter!/histogram!per-procedure),read/writehandlers (per-export bytes), the RPC server (active connections gauge), and the dispatcher's error path (errors bynfsstat3). Implementsarcticwolfctl metrics [--format json|prometheus]. Tests: counters/histograms increment as expected over a small NFS workload; both output formats parse.Graceful shutdown + signal handling. Introduces
tokio_util::sync::CancellationToken. EachRpcServer::serve_until(cancel)selects onaccept()vscancel.cancelled(). Active connections drain via aJoinSetbefore the task returns.tokio::signal::unixlistens forSIGTERMandSIGINT. Adminshutdowncommand and signals both trigger the same path. Tests: admin shutdown, SIGTERM, SIGINT all cause the process to exit cleanly within the timeout; an in-flight largeWRITEcompletes successfully. NFS — full nfstest required.References
MultiExportFilesystemtrait split context:src/fsal/multi_export.rs.classify_handle_erroralready maps stale-uid lookups toNFS3ERR_STALE:src/nfs/error.rs.Config::validate()invariants the new candidate must pass:src/config.rs.