Skip to content
Merged
836 changes: 696 additions & 140 deletions Cargo.lock

Large diffs are not rendered by default.

70 changes: 67 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,96 @@
name = "arcticwolf"
version = "0.1.0"
edition = "2024"
rust-version = "1.86"
# Required by vergen-gitcl 9.1.0 (transitive: vergen 9.0+ uses 1.88 features).
rust-version = "1.88"

[lib]
name = "arcticwolf"
path = "src/lib.rs"

# Both binaries are declared explicitly: `arcticwolf` is the NFS daemon and
# `arcticwolfctl` is the operator admin client (issue #25).
[[bin]]
name = "arcticwolf"
path = "src/main.rs"

[[bin]]
name = "arcticwolfctl"
path = "src/bin/arcticwolfctl.rs"

[dependencies]
tokio = { version = "1", features = ["full"] }
anyhow = "1"
bytes = "1.5"
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
# `env-filter` provides `EnvFilter`, the daemon's log filter (also driven by
# `RUST_LOG`). The `reload` module (used by the admin `log-level` commands
# to swap the filter at runtime) and `fmt`/`registry` all come from the
# default feature set, so only `env-filter` needs to be requested here.
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
async-trait = "0.1"
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_ignored = "0.1"
serde_json = "1"
toml = "0.8"
clap = { version = "4", features = ["derive"] }
getrandom = "0.3"
tokio-util = { version = "0.7", features = ["codec"] }
# Needed for the `Sink`/`Stream` extension traits used on the admin
# `Framed` connection. `default-features = false` keeps the build to just
# the sink/stream helpers we use here.
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
# `nix` is unused by Phase 1 itself; pulled in now so Phase 6 (audit log)
# can read `SO_PEERCRED` without another Cargo.toml churn. The `socket`
# and `user` features cover both the peer credential lookup and the
# subsequent uid/username resolution.
nix = { version = "0.29", features = ["socket", "user"] }
# `arc-swap` backs the live export snapshot in
# `fsal::multi_export::MultiExportFilesystem`. The NFS read path needs to
# pick a backend for every request without ever blocking the (single) admin
# writer, and the admin mutation path needs to replace the map atomically
# without taking a global lock that would stall in-flight RPCs. `ArcSwap`
# gives both: readers get a lock-free `load()` returning an `Arc` snapshot
# they can clone out and drop before any `.await`, writers do a single
# `store(Arc::new(new_snapshot))`.
arc-swap = "1"

# XDR serialization (runtime)
xdr-codec = "0.4"

# Test-only dependency, activated by the `test-util` feature (see
# [features]). The `AdminContext::for_test` helper needs it to build a
# tempdir-backed export when the library is compiled with `test-util` so
# integration tests can link against it. Never compiled into release builds.
tempfile = { version = "3", optional = true }

[features]
default = []
# Enables test-only helpers (e.g. `AdminContext::for_test`) so integration
# tests in tests/ can reuse the same fixtures the in-crate unit tests use.
# Never enable in release builds.
test-util = ["dep:tempfile"]

[dev-dependencies]
tempfile = "3"
# Re-link this crate with the `test-util` feature enabled so integration
# tests in tests/ can call the in-crate test helpers (AdminContext::for_test)
# without duplicating the fixtures.
arcticwolf = { path = ".", features = ["test-util"] }
# Enable tokio's `test-util` feature (virtual-time `start_paused`/`advance`)
# for tests only — it is NOT part of the `full` feature set used by the
# release binary, so it lives here to keep it out of production builds. The
# drain-timeout test runs on paused time to stay deterministic on slow CI.
tokio = { version = "1", features = ["test-util"] }

[build-dependencies]
# No build dependencies - xdrgen is installed as CLI tool
# xdrgen itself is installed as a CLI tool (see build.rs), not a crate.
# `vergen-gitcl` generates the VERGEN_* compile-time env vars consumed by
# the admin `version` command: VERGEN_GIT_SHA (build commit),
# VERGEN_RUSTC_SEMVER (rustc version) and VERGEN_CARGO_DEBUG (build
# profile). The `gitcl` backend shells out to the `git` CLI; when the
# build runs without a `.git` directory (the container build copies only
# sources) vergen falls back gracefully and the handler reports "unknown".
vergen-gitcl = { version = "9", features = ["cargo", "rustc"] }
28 changes: 28 additions & 0 deletions Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ common:
# Copy source code
COPY src ./src

# Copy Rust integration tests (cargo discovers tests/*.rs; the .py
# files in this directory are nfstest scripts and are ignored by cargo)
COPY tests ./tests

# Pre-fetch dependencies to speed up subsequent builds
RUN cargo fetch

Expand All @@ -43,6 +47,30 @@ lint:
RUN cargo fmt -- --check
RUN cargo clippy -- -D warnings

# earthly +lockfile
# Reconcile Cargo.lock with Cargo.toml — preserves existing pins when
# constraints still permit. If a top-level requirement in Cargo.toml
# is changed (e.g. `bytes = "1.5"` → `"1.6"`), that crate will resolve
# to a new version.
# Re-run after adding a dep or enabling a feature that pulls new transitives.
lockfile:
FROM rust:1.91
WORKDIR /src
# Copy the existing Cargo.lock alongside Cargo.toml so cargo treats
# already-pinned entries as fixed and only resolves newly-required
# transitives. `cargo generate-lockfile` (the previous approach)
# re-resolves from scratch and bumps unrelated semver-compatible
# dependencies. The `Cargo.lock*` wildcard mirrors `+common` and
# handles a fresh checkout where no lockfile exists yet — `cargo
# fetch` then resolves from scratch (similar to `generate-lockfile`
# but via the fetch interface).
COPY Cargo.toml Cargo.lock* ./
COPY build.rs ./
COPY xdr ./xdr
COPY src ./src
RUN cargo fetch
SAVE ARTIFACT Cargo.lock AS LOCAL Cargo.lock

# earthly +image
image:
ARG IMAGE_REPO=freezevicente
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ TESTCASE ?= read,write
# Default target
.DEFAULT_GOAL := help

.PHONY: help build test lint start-test-env nfstest stop-test-env clean
.PHONY: help build test lint lockfile start-test-env nfstest stop-test-env clean

# Show available targets and their descriptions
help:
@echo "Available targets:"
@echo " build - Build release binary"
@echo " test - Run unit tests"
@echo " lint - Run clippy and rustfmt checks"
@echo " lockfile - Regenerate Cargo.lock from Cargo.toml"
@echo " start-test-env - Build and start both server and client VM"
@echo " nfstest - Run NFS tests (TESTCASE=read,write)"
@echo " stop-test-env - Stop both server and VM"
Expand All @@ -46,6 +47,10 @@ test:
lint:
$(EARTHLY) +lint

# Regenerate Cargo.lock from Cargo.toml
lockfile:
$(EARTHLY) +lockfile

# Build Docker image
image:
${EARTHLY} +image --IMAGE_REPO=$(IMAGE_REPO) --IMAGE_TAG=$(IMAGE_TAG)
Expand Down
18 changes: 18 additions & 0 deletions arcticwolf.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,21 @@ path = "/srv/nfs/backup"
[logging]
# Log level: "error", "warn", "info", "debug", "trace" (default: "info")
level = "info"

# Graceful shutdown (optional; omit the section to accept the defaults).
#
# On SIGTERM/SIGINT the daemon stops accepting new connections and waits for
# in-flight requests to drain before exiting 0. A second signal during the
# drain forces an immediate exit (128 + signum).
[shutdown]
# Maximum seconds to wait for in-flight requests to drain before forcing
# exit. This is a CEILING, not a target: a server that drains in 5ms exits
# in 5ms. Hitting the ceiling logs a warning but still exits 0 (a slow
# drain is not a failure). Default: 30.
#
# Note: per-connection handlers do not yet check for shutdown between
# requests, so a connected-but-idle client (one holding a connection open
# without sending traffic) keeps its handler alive and will make the drain
# run all the way to this ceiling. Active clients drain promptly; only
# silent-idle ones hit the ceiling. Tracked as a follow-up to #25.
drain_timeout_seconds = 30
36 changes: 35 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,41 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
use vergen_gitcl::{CargoBuilder, Emitter, GitclBuilder, RustcBuilder};

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Emit the VERGEN_* env vars first so build/version metadata is
// available even if XDR codegen is short-circuited by a cached run.
emit_build_metadata()?;
generate_xdr_types();
Ok(())
}

/// Emit the `VERGEN_*` compile-time env vars consumed by the admin
/// `version` command (`VERGEN_GIT_SHA`, `VERGEN_RUSTC_SEMVER`,
/// `VERGEN_CARGO_DEBUG`).
///
/// The `gitcl` backend shells out to the `git` CLI. When the build runs
/// without a `.git` directory available — as in the container build, which
/// copies only the sources — vergen cannot resolve the commit; it falls
/// back to a placeholder rather than failing, and the handler reports the
/// field as "unknown".
fn emit_build_metadata() -> Result<(), Box<dyn std::error::Error>> {
let cargo = CargoBuilder::all_cargo()?;
let rustc = RustcBuilder::all_rustc()?;
let gitcl = GitclBuilder::all_git()?;
Emitter::default()
.add_instructions(&cargo)?
.add_instructions(&rustc)?
.add_instructions(&gitcl)?
.emit()?;
Ok(())
}

/// Run `xdrgen` over the XDR v3 specs and write the generated Rust types
/// into `OUT_DIR`. Panics on failure — a broken codegen step must fail the
/// build loudly.
fn generate_xdr_types() {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let out_path = Path::new(&out_dir);

Expand Down
Loading
Loading