Deploying the build system
This guide takes you through the basics of deploying a dispatcher setup on your own system. This setup relies on using federated repositories from the official repositories to build the packages, and to provide you with all the necessary compiler and build tools to do so. It is not a guide on how to bootstrap your own OS.
The guide sets up your own repository called Dev with a provisionary key. These keys are a tradeoff between ease of deployment and security. While not insecure, they do not fully implement TUF and are considerably less secure than proper provisioned root keys. If you are considering being serious about fully implementing TUF, this guide doesn't cover the root ceremony that you need to implement that last part.
The guide takes you through setting up access tokens. These RBAC tokens grant various permissions with the goal being that one should only generate tokens for the purpose they are used for, with the correct permissions. Re-using tokens for other tasks is not recommended: It's easy to make more tokens.
The outcome of this guide is to create a package, and build a container image with that package. The guide finishes with a demonstration that uses the container image to show that the built package is functional.
Here are the steps, and you'll want to walk them in this order:
- Install the stack
- Start it up
- Your access: groups and tokens
- Write a recipe
- Hand it to the designer
- Approve it
- Put it in a container image
- When things go wrong
Install the stack
$ sudo pkg install +build-sys
That single set installs the whole stack. It creates the dispatch service account and all the directories that belong to it, and it enables every unit. Nothing is started yet.
If you want the documentation on your system as well, you can also install dispatch/doc, designer/doc and pkg/man.
Start it up
Reboot, or just start everything that a boot would have started:
$ sudo systemctl start multi-user.target
Every daemon waits for dispatcher-init.service. That one runs once, and it does all of the provisioning for you:
- it adds a
dispatchrange to/etc/subuidand/etc/subgid, - it generates the signing and auth keys,
- it writes out the topology from
/etc/pkg/repos.d/: aDevrepo for your own packages, floored onSandcastle-Common->Sandcastle-Base, federated and fetched on demand, - it pins the Sandcastle trust roots,
- it creates the
devrecipe registry as a git repository, - it downloads the SPDX license texts.
There is nothing else that has to be set up by hand.
To check that everything came up, open the terminal UI:
$ sudo -u dispatch dispatch-tui
The first screen shows the status of every service, and all of them should be running. The exception is dispatch-repo-mirror and its timer, which stay off: those push repositories to a public server, and you don't need that here. You will come back to dispatch-tui later on in this guide.
The web UI is at http://localhost:8081/, and browsing it doesn't need a token.
Opening the web UI to your LAN
dispatch-web only listens on 127.0.0.1:8081 by default. If you want to browse it from another machine, set the keys in the [web] table at the end of /etc/dispatcher/dispatch.toml. They are commented out by default.
$ sudoedit /etc/dispatcher/dispatch.toml
[web]
listen = "0.0.0.0:8081"
allowed_hosts = ["mylaptop.lan"]
$ sudo systemctl restart dispatch-web
This machine's hostname, its first label and its IP addresses are all accepted without being listed. Any other name that you will type into the browser, like a DNS alias, has to be added to allowed_hosts. Other names get a 421 Misdirected Request back.
NOTE: the [web] table is strict. A typo in it stops dispatch-web from starting at all, so check systemctl status dispatch-web after the restart. The comments above [web] in the file explain all of the keys.
Do keep this on your LAN though: the UI shows the designer journal to anyone who can reach it, and it does so without a token.
Your repository's keys are provisional
init gives Dev one single signing key, in /var/lib/dispatcher/keys/Dev.seed. That one online key holds every TUF role, and the root never expires. This is what we call an internal repository, and it is the right thing for a repository that only this machine and your own LAN are ever going to use. A repository that other people install from needs an offline root and rotating clerk keys, which is what the root-ceremony tool is for, and that procedure isn't documented for a new node yet. Until it is:
- Don't publish
Dev, or its root fingerprint, for others to pin. - Back up
/var/lib/dispatcher/keys/. WithoutDev.seedthere is nothing left that can signDevagain. - Expect to have to do a ceremony later on to replace these keys. Compositions will follow along without needing an edit.
Your access: groups and tokens
You file recipes as yourself, through designer-api's socket, and you watch the builds through the dispatcher's socket. So, join both groups, and then log out and back in again:
$ sudo usermod -aG designer-client,dispatch-client "$USER"
Next, mint a submitter token to file recipes with. A submitter can create closures and file into its own, but it approves nothing. The normal way to mint tokens is the TUI:
$ sudo -u dispatch dispatch-tui
- Press
7for the tokens tab. - Press
sto pick thesubmitterrole. - Use up/down to move to
sub, and type in your user name.nameandemailcan stay empty here, since a submitter approves nothing. - Press Enter. The TUI then shows you the token.
Save it where designer-mcp looks for it by default. Paste in the token, press Enter, and then Ctrl-D:
$ mkdir -p ~/.config/dispatch
$ (umask 077; cat > ~/.config/dispatch/designer-token)
Then prove that both the group and the token work:
$ designer-mcp call closure_list
That same token, pasted into the web UI's token bar, lets you act over there as yourself too.
The CLI mints the exact same tokens, which is handy in scripts. You can optionally use it for an operator token, so that you can follow builds with dispatch-cli instead of in the web UI. dispatch-cli has no default token file, so you have to export it:
$ (umask 077; sudo -u dispatch dispatch token-mint --role operator \
--sub "$USER" > ~/.config/dispatch/token)
$ export DISPATCH_TOKEN=$(<~/.config/dispatch/token)
$ dispatch-cli jobs
$ dispatch-cli log <job-id> --follow
dispatch-cli jobs lists the newest job last.
The roles are listed by sudo -u dispatch dispatch token-mint --help: submitter files recipes, curator approves them, operator drives the dispatcher, admin does all of it, and viewer only ever reads. In the TUI you pick those with s, c, o, a and v.
Write a recipe
Write out ~/recipes/hello/recipe.toml:
[recipe]
name = "hello"
version = "2.12.3"
release = 1
description = "GNU hello, prints a friendly greeting"
homepage = "https://www.gnu.org/software/hello/"
license_hint = "GPL-3.0-or-later"
summary = """
The GNU Hello program produces a familiar, friendly greeting. It
allows non-programmers to use a classic computer science tool which
would otherwise be unavailable to them.
"""
[[source]]
name = "main"
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.3.tar.gz"
sha256 = "0d5f60154382fee10b114a1c34e785d8b1f492073ae2d3a6f7b147687b366aa0"
unpack_to = "."
You can get the sha256 with curl -fsSLO <url> && sha256sum <file>. The full recipe format is documented in the designer/doc package.
Now lint it. It stays in your home directory, nothing gets copied anywhere:
$ designer lint ~/recipes/hello
Hand it to the designer
$ designer-mcp call closure_create name=hello-first registry=dev
$ designer-mcp call candidate_submit closure=hello-first name=hello \
recipe_path=$HOME/recipes/hello
$ designer-mcp call closure_resume name=hello-first
designer-mcp call reads the recipe as you, and sends the contents of it over to designer-api. A new closure starts out paused, so that you can file several recipes before any of them starts building. closure_resume is what starts them. The arguments are all key=value strings, and you use key:= for JSON, as in paused:=false.
The designer picks the target up within 30 seconds. It then:
- runs the license gate and the malware gate on the source,
- has the dispatcher build a candidate, which is the full pipeline:
- sandboxed configure/make/install,
- the split into subpackages,
- a second build, compared byte for byte (
reproducibility gate passed (52 manifest(s))), make check(6 passed, 0 failed, 1 skipped),
- files a proposal.
NOTE: your very first package can take a while. The federated repository cache starts out empty, and filling it means downloading the whole build floor, which is a few hundred megabytes. Later builds skip that download. The build's log shows you how far the sync and the download have gotten.
Follow it in the web UI under Designer, or with:
$ designer-mcp call closure_status name=hello-first
"target_counts": { ... "proposed": 1 ... }
Approve it
Automation stops at proposed. A person with the curator role reviews the proposal, and then approves, rejects or withdraws it. Nothing gets approved automatically, and no script or agent should ever approve its own work.
Mint yourself a curator token. Your name and email become the git author of whatever it is that you approve:
$ (umask 077; sudo -u dispatch dispatch token-mint --role curator \ --sub "$USER" --name "Your Name" --email you@example.org \ > ~/.config/dispatch/curator.token)- In the web UI, paste it into the token box in the header, and then open Designer ->
hello-first. - Review the recipe, the gate results and the candidate build, and then decide.
On approval the recipe gets committed to /var/lib/dispatcher/recipes/dev/, and then convergence builds it into Dev all on its own within about 2 minutes. The web UI's Packages page will then list hello.
Put it in a container image
A composition is a hand written composition.toml that says which packages go into an artifact: an OCI container, a disk image, an erofs slice, and others. Put it in a directory of its own under /var/lib/dispatcher/compositions/. The dispatcher notices it, composes it, and then publishes the result under /var/lib/dispatcher/images/<name>/. The pkg-compose manual page documents the format, under COMPOSITION DESCRIPTORS.
Write out ~/hello-oci/composition.toml:
repos = ["Dev"]
[composition]
name = "hello-oci"
description = "OCI container with GNU hello"
type = "oci"
[packages]
needed = [
"hello",
"filesystem", "glibc", "bash", "coreutils", "os-release", "sysusers",
"ca-certs", "ca-trust", "iana-etc", "pkg", "pkg-repo-sandcastle",
]
[oci]
layer_strategy = "merged"
labels = { "org.opencontainers.image.title" = "hello" }
The name field has to match the name of the composition's own directory.
The package list in this example is a good start for most container images. Besides hello itself, it has a shell, the core utilities, certificates and pkg, so the container can do just about any task, including installing more packages while it is running.
There is no URL and no key anywhere in the file: repos = ["Dev"] brings in Dev and every repository that it builds on.
Install it:
$ sudo install -D -o dispatch -g dispatch -m 0640 ~/hello-oci/composition.toml \
/var/lib/dispatcher/compositions/hello-oci/composition.toml
The dispatcher composes a new descriptor within about 5 minutes. The first compose takes a few minutes more while it fetches payloads that no build has needed yet. The result here was 97 MiB, with SPDX and CycloneDX SBOMs, a license report and a signed image.json, all listed at http://localhost:8081/#/images/hello-oci.
The timing after that is:
- A changed descriptor waits up to an hour after the last successful build.
- A failed compose retries after an hour, up to three times.
- Either wait can be skipped by starting the compose by hand.
Copy the image out to a work directory, and then run it as yourself:
$ mkdir -p ~/hello-oci-image
$ sudo -u dispatch tar -C /var/lib/dispatcher/images/hello-oci/latest -c hello-oci \
| tar -x -C ~/hello-oci-image
$ podman run --rm oci:$HOME/hello-oci-image/hello-oci /usr/bin/hello
Hello, world!
When things go wrong
| Question | Where to look |
|---|---|
| Is a daemon up? Why did it die? | systemctl status <unit>, journalctl -u <unit> -o cat -n 50 |
| Is the config valid? | sudo -u dispatch dispatch check-config |
| Where is my recipe? | designer-mcp call closure_status name=<closure>, journalctl -u designer-run |
| A build's log | dispatch-cli log <job-id>, or /var/lib/dispatcher/state/logs/<job-id>.log |
| Why didn't my composition build? | curl -s localhost:8081/api/v1/compositions/<name> for drift and hold_off_remaining_secs, then dispatch-cli jobs and that job's log |
| What will be built next? | curl -s localhost:8081/api/v1/converge/status |