Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

.nornir/gunnar-quickstart.md

gunnar — fast setup

New here? Start here below: clone the source, build the server with one cargo line, run it on your own computer and push to it. Nothing else is needed for that. The five numbered lists after it are the appliance route (a bootable medium, a twin, the console, the metered medium) — the advanced path.

Addresses are examples (RFC 5737): 192.0.2.10 is the main, 198.51.100.10 the twin, 203.0.113.10 the browse replica. Substitute yours.

Every command below was run on one laptop before it was written down: §1 and §3 against three appliances installed from the medium §4 builds and booted in KVM (cargo xtask install-test pair), §2 against the console in podman, pointed at one of them.


Start here — clone, build, run

The first way, and the simplest: Gunnar is a Rust program. You build it from its source and run it on any Linux computer — no installer, no medium, no virtual machine.

What you need: a Linux computer (x86-64), about 10 GB of free disk and 20–40 minutes for the first build.

 1  sudo apt install git build-essential cmake pkg-config protobuf-compiler

 2  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    . "$HOME/.cargo/env"

 3  mkdir ~/nordisk && cd ~/nordisk

 4  export GIT_LFS_SKIP_SMUDGE=1
    git clone ssh://git.gunnar.rs:2222/nordisk/gunnar
    git clone ssh://git.gunnar.rs:2222/nordisk/gunnar-ui
    git clone ssh://git.gunnar.rs:2222/nordisk/monetize

 5  git clone ssh://git.gunnar.rs:2222/nordisk/edda
    git clone ssh://git.gunnar.rs:2222/nordisk/Skidbladnir
    git clone ssh://git.gunnar.rs:2222/nordisk/znippy
    git clone ssh://git.gunnar.rs:2222/nordisk/znippy-zoomies
    git clone ssh://git.gunnar.rs:2222/mikronova/draupnir

 6  cd gunnar
    cargo build --release -p gunnar-server -p gunnar-cli

 7  ./target/release/gunnar-server serve

Leave 7 running. Open a second terminal:

 8  cd ~/nordisk/gunnar && mkdir -p ~/gunnar

 9  ssh-keygen -t ed25519 -N "" -f ~/gunnar/admin

10  ./target/release/gunnar-cli control status --at 127.0.0.1:50051

11  ./target/release/gunnar-cli control bootstrap alice --key @$HOME/gunnar/admin.pub \
        -i ~/gunnar/admin --at 127.0.0.1:50051

12  ./target/release/gunnar-cli control repo create alice/hello \
        -i ~/gunnar/admin --at 127.0.0.1:50051

13  export GIT_SSH_COMMAND="ssh -i ~/gunnar/admin -o IdentitiesOnly=yes"

14  cd ~ && git clone ssh://git@127.0.0.1:2222/alice/hello.git

15  cd hello && git commit --allow-empty -m first && git push origin HEAD:refs/heads/main

What the lines mean, one clause each:

When this works, the numbered lists below turn the same server into an appliance of its own: a bootable medium (§4), a twin that takes over (§3), and the web console (§2).


1. One gunnar server

 1  cargo xtask iso-installer --variant gunnar-installer-open \
        --opt dhcp=on --opt wireguard=off

 2  cargo xtask install-test pair --members main --keep

 3  ssh-keygen -t ed25519 -N "" -f ~/gunnar/admin

 4  gunnar-cli control status --at 192.0.2.10:50051

 5  gunnar-cli control bootstrap alice --key @~/gunnar/admin.pub \
        -i ~/gunnar/admin --at 192.0.2.10:50051

 6  gunnar-cli control user add bob --key @~/gunnar/bob.pub \
        -i ~/gunnar/admin --at 192.0.2.10:50051

 7  gunnar-cli control repo create alice/hello \
        -i ~/gunnar/admin --at 192.0.2.10:50051

 8  export GIT_SSH_COMMAND="ssh -i ~/gunnar/admin -o IdentitiesOnly=yes"

 9  git clone ssh://git@192.0.2.10:2222/alice/hello.git

10  cd hello && git commit --allow-empty -m first && git push origin HEAD:refs/heads/main

What the lines mean, one clause each:


2. The console beside it

 1  cd ../gunnar-ui
 2  cargo build --release -p gunnar-ui-server --target x86_64-unknown-linux-musl
 3  (cd crates/gunnar-ui-web && trunk build --release)

 4  ssh-keygen -t ed25519 -N "" -f ~/gunnar/ui-id
 5  ssh-keygen -t ed25519 -N "" -f ~/gunnar/ui-guest

 6  GUNNAR_UI_CONTROL=192.0.2.10:50051 GUNNAR_UI_IDENTITY=~/gunnar/ui-id \
    GUNNAR_UI_STATE=~/gunnar/ui-state.redb \
      target/x86_64-unknown-linux-musl/release/gunnar-ui-server \
      enroll --admin-key ~/gunnar/admin --name gunnar-ui

 7  gunnar-cli control user add gunnar-ui-guest --key @~/gunnar/ui-guest.pub \
        --kind machine -i ~/gunnar/admin --at 192.0.2.10:50051

 8  mkdir -p ~/gunnar/console && cd ~/gunnar/console
 9  cp ~/git/gunnar-ui/target/x86_64-unknown-linux-musl/release/gunnar-ui-server .
10  cp -r ~/git/gunnar-ui/crates/gunnar-ui-web/dist .
11  printf 'FROM scratch\nCOPY gunnar-ui-server /gunnar-ui-server\nCOPY dist /dist\nENTRYPOINT ["/gunnar-ui-server"]\n' > Containerfile
12  podman build -t gunnar-console:local .

13  export TOKEN=$(openssl rand -hex 16)
14  podman run -d --name gunnar-console --network=host \
        -v ~/gunnar:/keys:Z \
        -e GUNNAR_UI_CONTROL=192.0.2.10:50051 \
        -e GUNNAR_UI_IDENTITY=/keys/ui-id \
        -e GUNNAR_UI_GUEST_IDENTITY=/keys/ui-guest \
        -e GUNNAR_UI_WEBAUTHN_RP_ID=localhost \
        -e GUNNAR_UI_WEBAUTHN_ORIGIN=http://localhost:3210 \
        -e GUNNAR_UI_ADMIN_TOKEN=$TOKEN \
        -e GUNNAR_UI_BIND=127.0.0.1:3210 \
        -e GUNNAR_UI_DIR=/dist \
        -e GUNNAR_UI_STATE=/keys/ui-state.redb \
        -e GUNNAR_UI_GIT_URL=ssh://192.0.2.10:2222 \
        gunnar-console:local

15  curl -s http://127.0.0.1:3210/auth/status

16  curl -s -H "Authorization: Bearer $TOKEN" \
        "http://127.0.0.1:3210/auth/passkey/register/start?user=alice"

17  open http://localhost:3210/login   # finish the passkey in the browser

18  curl -s -X POST -H "Authorization: Bearer $TOKEN" \
        -H 'content-type: application/json' \
        -d '{"role":"operator","user":"bob"}' \
        http://127.0.0.1:3210/auth/passkey/invites

The six that matter, and what each is:

variable what it takes
GUNNAR_UI_CONTROL the server's control plane, host:port
GUNNAR_UI_IDENTITY path to an ed25519 private key; enrolled by 5 as a Machine principal with a server-wide admin grant
GUNNAR_UI_GUEST_IDENTITY a second key, enrolled with no grant, for visitors with no session. Unset means nothing is served anonymously. Never the key above
GUNNAR_UI_WEBAUTHN_RP_ID a domain. An IP address is refused at start-up; in development the domain is localhost
GUNNAR_UI_WEBAUTHN_ORIGIN the exact browser origin, scheme and port included. It is also what invite links are built from
GUNNAR_UI_ADMIN_TOKEN break-glass bearer for passkey number one. It stops working the moment an admin passkey exists

3. A twin, and the two flips

§2 is unchanged and applies to the pair: the console is one process pointed at one control plane, and the only thing a failover changes about it is GUNNAR_UI_CONTROL.

3a. Install the twin and start replicating

 1  cargo xtask install-test pair --members main,twin,spare --keep

 2  gunnar-cli control bootstrap alice --key @~/gunnar/admin.pub \
        -i ~/gunnar/admin --at 198.51.100.10:50051

 3  gunnar-cli control replication standby -i ~/gunnar/admin --at 198.51.100.10:50051

 4  gunnar-cli control replication add twin ssh://git@198.51.100.10:2222 \
        --control 198.51.100.10:50051 -i ~/gunnar/admin --at 192.0.2.10:50051

 5  gunnar-cli control user add primary --key '<the line step 4 printed>' \
        --kind machine -i ~/gunnar/admin --at 198.51.100.10:50051

 6  gunnar-cli control grant primary admin --namespace alice \
        -i ~/gunnar/admin --at 198.51.100.10:50051

 7  gunnar-cli control replication resync twin -i ~/gunnar/admin --at 192.0.2.10:50051

 8  gunnar-cli control replication list -i ~/gunnar/admin --at 192.0.2.10:50051
twin  pending 0  failures 0  last success NEVER
  NO COPY  alice/hello  [twin]

and after it, once the queue drains,

replication    1 target(s) · synced 1 · failed 0 · bus gaps 0
twin  pending 0  failures 0  last success <ms> ms  idle
  copied   alice/hello  [twin]  1 refs there

last success NEVER with pending 0 means the twin has never taken a byte and nothing is asking it to — usually 7 not run. failures climbing means 5/6 not run, or run with write instead of admin.

3b. The twin becomes the writable main

 1  gunnar-cli control replication promote -i ~/gunnar/admin --at 198.51.100.10:50051

 2  gunnar-cli control replication demote -i ~/gunnar/admin --at 192.0.2.10:50051

 3  gunnar-cli control replication standby -i ~/gunnar/admin --at 192.0.2.10:50051

 4  git remote set-url origin ssh://git@198.51.100.10:2222/alice/hello.git

 5  git push origin HEAD:refs/heads/main
demoted — 1 standby target(s) cleared; this box ships no second copy

  It STILL TAKES CLIENT WRITES. If this is a failover and another box is
  the primary now, freeze this one too or it will accept a push nobody
  replicates:

      gunnar-cli control replication standby

3 is that freeze:

standby — this box is now read-only to clients; it accepts only its primary's replication push
role           standby (read-only to clients)

A client still pointed at it is then refused by name — fatal: remote error: this box is a standby; push to the primary — instead of leaving a commit nobody replicates. demote alone leaves the box writable, and says so.

    gunnar-cli control replication abdicate --promote 198.51.100.10:50051 \
        -i ~/gunnar/admin --at 192.0.2.10:50051

3c. The main was nuked

 1  cargo xtask iso-installer --variant gunnar-installer-open \
        --opt dhcp=on --opt wireguard=off

 2  gunnar-cli control bootstrap alice --key @~/gunnar/admin.pub \
        -i ~/gunnar/admin --at 192.0.2.10:50051

 3  gunnar-cli control replication standby -i ~/gunnar/admin --at 192.0.2.10:50051

 4  gunnar-cli control replication add rebuilt ssh://git@192.0.2.10:2222 \
        --control 192.0.2.10:50051 -i ~/gunnar/admin --at 198.51.100.10:50051

 5  gunnar-cli control user add survivor --key '<the line step 4 printed>' \
        --kind machine -i ~/gunnar/admin --at 192.0.2.10:50051

 6  gunnar-cli control grant survivor admin --namespace alice \
        -i ~/gunnar/admin --at 192.0.2.10:50051

 7  gunnar-cli control replication resync rebuilt -i ~/gunnar/admin --at 198.51.100.10:50051

 8  gunnar-cli control replication list -i ~/gunnar/admin --at 198.51.100.10:50051

3d. A browse replica, in one line

 1  gunnar-cli control replication add public ssh://git@203.0.113.10:2222 \
        --scope public-only --control 203.0.113.10:50051 \
        -i ~/gunnar/admin --at 192.0.2.10:50051

 2  gunnar-cli control user add primary --key '<the line step 1 printed>' \
        --kind machine -i ~/gunnar/admin --at 203.0.113.10:50051

 3  gunnar-cli control grant primary admin --namespace alice \
        -i ~/gunnar/admin --at 203.0.113.10:50051

 4  gunnar-cli control replication browse -i ~/gunnar/admin --at 203.0.113.10:50051
this box is a browse replica: it holds public repositories read-only and cannot be
promoted or changed here — Access.AddPrincipal is refused; run it against the primary

so 2 and 3 are unrunnable on a box that has already been flipped. Authorise the key first, flip second. replication standby is the way back.


4. Building gunnar

The advanced route. The plain cargo build in Start here gives you the server as a program. The lines below build the appliance medium instead: an installer image that turns a whole machine into a Gunnar server, composed by the Build Thing in a pinned container.

 1  dwarves buildthing gunnar --variant gunnar-installer

 2  dwarves buildthing gunnar --variant gunnar-installer-open --opt dhcp=on

 3  dwarves buildthing gunnar --variant gunnar-installer-metered

 4  cd ../gunnar-ui && cargo build --release -p gunnar-ui-server \
        && (cd crates/gunnar-ui-web && trunk build --release)

and the one equivalent from inside the gunnar checkout, which finds the dwarves sibling and the kernel for you:

    cargo xtask iso-installer --variant gunnar-installer-open --opt dhcp=on

5. The metered medium, and the money half

Skip this section entirely unless you built gunnar-installer-metered (§4, line 3). The open medium has no money code in it at all: no plans, no quotas, no buying, storage unlimited. Everything below describes the OTHER medium.

gunnar never takes money. It does three things and stops:

The money half is a separate product in a separate repository, monetize, running as its own binary and talking to this one over that signed seam. It is not part of this checkout and is not built by §4. An appliance from the metered medium with no monetize server in front of it enforces the entitlements it was last given and issues no new ones.

 1  gunnar control appliance-identity

 2  gunnar control plan show <namespace>

 3  gunnar control plan list --expiring

Growing the data set while it serves

This part is gunnar's own, and it works the same whoever asks for it.

 4  gunnar control capacity

 5  gunnar control grow status