# walknot disclaimer: slop A self-hostable [Tangled](https://tangled.org) knot whose storage is an S3 bucket. walknot is [knot2](https://tangled.org/tangled.org/core/tree/master/knot2) — Tangled's git + atproto knot — with its storage rebuilt on the design of [walgit](https://github.com/tobi/walgit): **every repository is a write-ahead log in an object store, and every pod's disk is a disposable cache.** Run one replica or many (Kubernetes, with Redis); they are interchangeable, and losing any of them — or all their disks — loses nothing. It speaks exactly knot2's protocol (XRPC, smart HTTP and SSH git, `subscribeRepos`, records and COBs), so the Tangled appview, knotmirror, hydrant and spindle work with it unchanged. Without a bucket configured it *is* knot2. ## How it works - **Repositories are WALs.** `repos//manifest.json` is rewritten with compare-and-swap (`If-Match`) on every commit — the only consensus there is. Ref edits are immutable log entries; objects are content-addressed packs; checkpoints fold the log. Lost responses are resolved by re-reading, so every acknowledged write is durable exactly once. → `crates/knot-wal`, `crates/knot-store` - **Local directories are caches.** Opening a repository revalidates it with one conditional GET and folds in what other replicas committed; writes go through the WAL first and are applied locally after. → `crates/knot-git/src/storage.rs` - **One writer per repository at a time**, by a lease that moves to whichever replica needs to write (re-entrant, renewed while used, released when idle). The CAS still guards correctness if a lease is lost. - **Knot-wide state** (members, registry, outbox, sidecars, LFS, sealed keys, SSH host key) lives in the bucket too; a leader lease elects the replica that runs singleton jobs. - **Redis** (only with several replicas) sequences the firehose knot-wide and shares single-use token nonces. It holds nothing durable. `docs/DESIGN.md` has the details; `UPSTREAM.md` how this tree maps to Tangled's. ## Running it ```toml # config.toml — the knot2 settings you already know, plus: [storage] backend = "s3" s3_bucket = "my-knot" # s3_endpoint = "https://minio.example.com" # any S3-compatible store with conditional writes [redis] url = "redis://redis:6379" # required for more than one replica ``` - Credentials come from the usual `AWS_*` environment (or the pod's cloud identity). - `KNOT_MASTER_KEY` must be the same on every replica: it seals the knot's identity, which lives in the bucket. - At boot the knot verifies the store really enforces `If-None-Match`/`If-Match` and refuses to start otherwise. **Kubernetes:** `deploy/kubernetes/base` (Deployment, Services, PodDisruptionBudget); `deploy/kubernetes/overlays/dev` adds MinIO and Redis for a local cluster. **Image:** `docker build -t walknot .` (`--build-arg PROFILE=container` for a faster build). **Migrating an existing knot2:** stop it, set `storage.backend = "s3"`, then `knot-server import --repos --lfs ` publishes every repository (the meta repo included) and LFS object. The old directory is copied, never modified. Keep the same `KNOT_MASTER_KEY` and `secrets.sealed_key_file` / SSH host key files for the first boot: they are sealed into the bucket then. ## Testing ```sh cargo test --workspace # knot2's suite, local storage KNOT_TEST_STORAGE=wal cargo test --workspace # the same suite, every repository in a bucket ci/test-linux.sh # in a Linux container, as CI does ``` With real services (`docker run quay.io/minio/minio server /data`, `redis`): ```sh export WALKNOT_TEST_S3_BUCKET=… WALKNOT_TEST_S3_ENDPOINT=http://127.0.0.1:9000 \ AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=… WALKNOT_TEST_REDIS=redis://127.0.0.1:6379 cargo test -p knot-store -p knot-wal -p knot-redis # contract, protocol, chaos on real S3 cargo test -p knot-server --test replicas_e2e # three real replicas ci/kind-smoke.sh # 3 replicas on a kind cluster ``` What is covered: the store contract (CAS, create-if-absent, conditional reads) on MemoryStore and MinIO; the WAL protocol under every injected fault, seeded chaos, racing writers and a proptest model; leases; materialization checked by `git fsck`; maintenance and compaction racing other writers; multi-replica behaviour in-process (pushes, races, firehose order, tickets, ACL changes, token replay) and with real processes; and knot2's own suite in both storage modes. ## Not yet - Repositories larger than a pod's cache (walgit's remote range reader). - Pruning unreachable *git objects* (repacks keep everything). Retired packs, superseded sidecars and orphaned log entries are garbage-collected. - Firehose replay from a cursor scans every repository's chain (fine for small knots). - Multi-pack index and bitmaps are not built for bucket-backed repositories.