Skip to main content

        Docka's first dev update: Clusters, WASM, and some data structures.

So it begins

Docka's first dev update: Clusters, WASM, and some data structures.

This is the first of, hopefully, many updates. In this series of posts, I hope to explain what’s happened over the last month of work at Docka. Since this project is still in its early stages, these posts will probably be long but boring for now, since most of it is general architecture stuff.

No central server, only hosts

Puppet and Salt both use a central server to distribute tasks or state to the nodes. This has some undeniable advantages. It opens up a whole lot of options, like centralized reporting and ensuring all nodes apply the same state, but, as usual, this comes at a cost. One downside is that this acts as a single point of failure. While the server is down, at most, the nodes can apply their old state, and reports can’t be collected during this time.

The answer: a cluster. Of course, this still leaves other points of failure, but the outage of one node does not affect any other nodes. Not just that, it opens up other niceties.

What we gain:

  • a distributed state where nodes can share execution logs, making them available at all times
  • a system that is not dependent on single points

What it costs:

  • clusters under three nodes must be considered “degraded” since three are needed for a consensus

Of facts, guards, and states

The core aspects of a playbook are simple:

Facts

Facts are information about the node Docka is executed on. A simple JSON document of a hashmap provides sufficient flexibility to allow for whatever data makes sense.

{
  // …
  "distro.behaves_like": "arch",
  "distro.id": "cachyos",
  "distro.name": "CachyOS Linux",
  "distro.pretty_name": "CachyOS",
  // …
  "kernel.build": "#1 SMP PREEMPT_DYNAMIC Tue, 23 Jun 2026 11:14:21 +0000",
  "kernel.cmdline": "quiet zswap.enabled=0 nowatchdog splash rw rootflags=subvol=/@ root=UUID=db71c261-5963-43d9-bdd9-1cd2c4c39a23 initrd=\\initramfs-linux.img\n",
  "kernel.hostname": "homunculus",
  "kernel.release": "7.0.13-arch1-1",
  // …
  "users": {
    "moritz": {
      "comment": "Moritz Poldrack",
      "disabled": false,
      "gid": 1000,
      "group": "moritz",
      "home": "/home/moritz",
      "shell": "/usr/bin/zsh",
      "uid": 1000
    },
  },
  "packages.installed": {
    "7zip": "26.01-1.1",
    "a52dec": "0.8.0-3.1",
    "aalib": "1.4rc5-19.1",
    "aardvark-dns": "2.0.0-1.1",
    "aaxtomp3": "1.3-2",
    // …
  },
    "packages.upgradable": {
    "android-tools": {
      "new": "36.0.1-2.1",
      "old": "35.0.2-28.1"
    },
    "archlinux-keyring": {
      "new": "1:20260707.1-1",
      "old": "1:20260612-1"
    },
    // …
  }
}

Better yet, using just these facts and diff, we can see what has changed about our system during the run:

@@ -1543,11 +1554,6 @@
   },
   "packages.installed": {
-    "7zip": "26.01-1.1",
+    "7zip": "26.02-1.1",
     "a52dec": "0.8.0-3.1",
     "aalib": "1.4rc5-19.1",
@@ -10674,6 +10873,15 @@
     },
+    "openbao": {
+      "comment": "OpenBao daemon",
+      "disabled": false,
+      "gid": 935,
+      "group": "openbao",
+      "home": "/var/lib/openbao",
+      "shell": "/usr/bin/nologin",
+      "uid": 935
+    },
     "openvpn": {

States

A state is a certain condition of the node. This could be a user account that exists, a package that is uninstalled, or a kernel variable with a certain content. These states can be combined into the desired state of the node. This is non-exhaustive, meaning that an administrator who installed ripgrep, won’t have it uninstalled, unless there is a state that ensures the package is absent. Since states can be numerous and may be quite bothersome to write, they can be combined into playbooks.

Guard clauses

A guard clause is a simple function that evaluates to a boolean. It controls whether a state is applicable or not. This can be used for marker files, or just to skip extra work.

Plugins at the center.

From the start, one thing was clear: nobody needs another r10k. As such, there is little to copy from Puppet in this regard. Instead of the magic r10k does, Docka’s plugin system is a lot more convenient: Docka plugins are WASM binaries that are distributed straight to the cluster. These plugins can then be loaded for states, facts, and guards.

TODO

As for the open issues:

  • how to distribute the plugins. Maybe cosigned binaries from a central registry?
  • how to solve inter-node connectivity. libp2p?
  • how to authenticate when interacting with the cluster? use the User’s SSH key?

And probably some more, once I’ve gotten deeper into writing it. I will try to write these posts about every four weeks to once a month, but schedules may change.