Skip to content

agent

Contributor documentation

This section explains how the Manager, Nodes, and cluster work internally and how to contribute to the opensource cluster itself. If you are a developer who just wants to run your own code on the cluster, see the Users documentation instead.

Node.js (TypeScript) check-in agent that reports host/service status to the manager and applies nginx and dnsmasq configuration. Installed on agent and manager containers. For deployment instructions, see Deploying Agents.

Architecture

A systemd timer launches the oneshot agent every 30 seconds. Each run checks in with the manager (POST /api/v1/agents), applies any config changes, and exits.

TypeScript sources live in agent/src (compiled to dist/), the locally rendered EJS templates in agent/templates, and the systemd service/timer units in agent/contrib/systemd. The agent/Makefile builds the opensource-agent package (see Release Pipeline). Service reloads and state queries go through the systemd D-Bus API; config validation uses nginx -t and dnsmasq --test.

Check-in Protocol

sequenceDiagram
    participant Timer as systemd timer
    participant Agent
    participant Manager

    Timer->>Agent: start (every 30s)
    Agent->>Manager: POST /api/v1/agents<br/>{siteId, hostname, ipv4Address, services}<br/>If-None-Match: {etag}
    alt Config changed
        Manager-->>Agent: 200 OK + config snapshot + ETag
        Agent->>Agent: Render templates, validate<br/>(nginx -t, dnsmasq --test)
        Agent->>Agent: Apply + reload (rollback on failure)
        Agent->>Manager: POST again (reports apply results)
        Manager-->>Agent: 304 Not Modified
    else No changes
        Manager-->>Agent: 304 Not Modified
    end
    Agent->>Timer: exit

The check-in body carries system info, per-service status, and — when the agent provisioned volume directories this pass — per-volume results:

{
  "siteId": 1,
  "hostname": "agent",
  "currentTime": 1771234560,
  "ipv4Address": "172.17.0.2",
  "services": {
    "nginx":   { "state": "active", "lastApply": "success" },
    "dnsmasq": { "state": "active", "lastApply": "success" }
  },
  "volumes": {
    "42": { "applied": true },
    "43": { "applied": false, "message": "mkdir: permission denied" }
  }
}

The manager records every check-in in the Agents table (shown on the web client's /agents page) and responds with the site's config snapshot as JSON. A strong ETag covers the snapshot; the agent stores it in /var/lib/opensource-agent/state.json and sends it back via If-None-Match, so unchanged configs cost a single 304 round trip.

Volumes

The config snapshot includes, at the site level, the volume directories the agent must ensure exist (site.volumes[] = { id, hostPath, mode, uid, gid }). There is one agent per site, and the shared volumes root is bind-mounted into the agent container, so this single agent provisions volumes for every node in the site. Each pass the agent first checks, via /proc/self/mountinfo, that the path lies on a mount other than its own root filesystem (otherwise the volumes root isn't bind-mounted and the volume is reported failed rather than created inside the agent). It then mkdir -ps the directory, chowns it to the supplied uid/gid (the consuming containers' id-mapped root, 100000) best-effort, and chmods it per mode. The chown is a tolerated no-op (EINVAL/EPERM) in an unprivileged agent guest — the norm, since pct create defaults to --unprivileged 1, where the id-map already yields the right owner and 100000 is outside the guest's mapped range — and the actual fix in a privileged agent guest (deliberately created with --unprivileged 0), where the agent is host root and mkdir would otherwise create a root-owned directory. Per-volume results are reported back keyed by volume id via the volumes field above; the manager writes them into Volume.status (ready / failed). The container create job blocks on Volume.status = 'ready' before attaching the Proxmox mpN bind mount. The agent only ever creates directories — it never removes them, so volume data is retained across container delete + recreate. See Volumes and Deploying Agents → Volume storage.

Environment Variables

Read from the process environment (systemd loads /etc/environment). Set via container runtime (Docker ENV, Proxmox LXC config) — the base image's environment.sh service propagates them on boot.

Variable Required Description
SITE_ID Yes Numeric site ID from the manager
MANAGER_URL Yes Base URL of the manager (e.g., http://192.168.1.10:3000)
API_KEY No Admin API key for remote agents. Not needed on the manager (localhost is trusted).
STATE_DIRECTORY No State directory, set by systemd via StateDirectory= (default /var/lib/opensource-agent)

The agent Dockerfile defaults to SITE_ID=1 and MANAGER_URL=http://localhost:3000 so the manager container works without configuration.

Managed Services

Service Files Test Reload
nginx /etc/nginx/nginx.conf nginx -t systemctl reload-or-restart nginx
dnsmasq /etc/dnsmasq.conf, /var/lib/dnsmasq/{dhcp-hosts,hosts,dhcp-opts,servers} dnsmasq --test restart when /etc/dnsmasq.conf changed, otherwise SIGHUP

Apply flow per service: render templates, skip if nothing changed, stage new files, run the test command, roll back on failure, then reload. Failures are reported as lastApply: "failure" at the next check-in.

To add a managed service, add a ManagedService entry in agent/src/apply.ts plus its template(s) under agent/templates/, and extend the config snapshot in create-a-container/utils/agent-config.js.

Running Manually

systemctl status opensource-agent.timer     # timer state
journalctl -u opensource-agent.service      # past runs
node /opt/opensource-server/agent/dist/index.js   # single run by hand