The provisioning cycle

Host.Provision runs on a worker goroutine, holds the host’s mutex for the duration of the call, and returns a delay flag and an error.

Where it lives

host: everything that happens to one server. Main file: host/host.go, with Provision at host/host.go:78 and handleHostUpgrade at host/host.go:219.

The sequence

The feature flags gate individual steps. The order never changes.

Fetch JWTfeatures.jwtFetch ed25519features.ed25519Fetch inventoryalwaysFetch CSRfeatures.pkiCall helperalwaysUpgradefeatures.upgradesConfigurealwaysRestartalwaysDefer or shutdownhelper decidedRequeueupgraded, no delayProvisioned60 second hold
Defer, shutdown, and a completed upgrade all end the cycle before configure runs.

Checks before the first RPC

Provision checks provisioned and discovery age before it issues any RPC.

if h.provisioned {
	return true, nil
}

if !h.discovered.IsZero() {
	since := time.Since(h.discovered)
	if since > 2*h.cfg.IntervalDuration {
		return false, fmt.Errorf("skipping node that's been waiting %v", since)
	}
}

The provisioned short-circuit returns a delay, so the finisher holds a node that has already been through the sequence out of the map for a further 60 seconds.

The staleness check is the second half of the duplicate-provision trade-off described in Discovery and the work queue. Because the queue holds up to 50,000 entries, a host can wait long enough that the facts the helper would see no longer match the node. Provision refuses a host that has waited longer than two discovery intervals.

Gathering facts

Each fetch is a single RPC against one target, described in RPC, retries, and upgrades. fetchJWT, fetchInventory, and fetchEd25519PubKey each return early when the value they collect is already populated, so a retried Provision on the same Host does not re-fetch.

fetchInventory always runs. Besides the facts, it records h.version and h.upgradable from the reply, and the upgrade path requires both.

The helper’s answer

getConfig marshals the whole Host to JSON and runs the helper. Its reply, a host.ConfigResponse, can end the cycle:

defer
Returns an error carrying the helper's message. The node stays unconfigured. The finisher removes it from the map, and a later discovery cycle picks it up again.
shutdown
Issues a shutdown RPC to the node and returns a delay. The node exits with code 0, which systemd does not restart. When msg is empty, Provision logs a warning that carries no shutdown reason.

Provision copies the rest of the reply onto the Host: the configuration map, the CA, certificate, key, and SSL directory, the target upgrade version, and the two policy maps. The helper contract gives the full shape.

The upgrade interruption

When features.upgrades is on and the helper returned an upgrade version, handleHostUpgrade runs before configuration. It refuses to proceed if the inventory carried no version or the node reported upgradable as false, then compares versions with the RPM comparison in host/version.go.

The switch that consumes its result (host/host.go:189-202) handles these outcomes:

ResultEffect
No error, skipped trueVersions already match. Falls through and configures the node normally.
No error, skipped falseAn upgrade ran. Returns (false, nil) immediately, ending the cycle before configuration.
Error, upgrades_optional trueLogs a warning and configures the node on its old version.
Error, upgrades_optional falseReturns (true, err), so the finisher holds the node out of the map for 60 seconds.
Upgrade and configure never share a pass

An upgraded node returns a false delay flag so the next discovery cycle picks it up at once (“no delay so we reprov asap”). The node has already replaced its own binary and restarted into provisioning mode on the new version. Returning no delay leaves it eligible for the next discovery cycle, where it runs the full sequence again and this time reaches configure. A node is never both upgraded and configured in a single pass.

Configure and restart

configure sends the whole payload in one RPC: the configuration map as a JSON string, the token, the CA, certificate, key, SSL directory, the Provisioner’s ECDH public key, both policy maps, and the signed server JWT. It refuses to send an empty configuration map. If the CSR reply carried an SSL directory, that value overrides whatever the helper chose.

restart follows with a one second splay, and the node restarts into its new configuration. Provision sets h.provisioned to true after the restart returns, then returns (true, nil).

Next

The helper contract covers the JSON exchange. RPC, retries, and upgrades covers what happens inside each call.