Design
These pages cover the packages, the provisioning sequence, and the credential handling, for contributors and for auditors.
- Code Map: How the source is laid out, what each package does, and how a server moves from unconfigured to running.
These pages cover the packages, the provisioning sequence, and the credential handling, for contributors and for auditors.
Choria Provisioner is a single Go binary, about 2,400 lines across four packages. It finds Choria Servers that have no configuration, asks a site-supplied helper what their configuration should be, writes it over Choria RPC, and restarts them.
Generated 2026-08-19 against commit aca2b62 on branch main, with a clean working tree.
A freshly installed Choria Server with provisioning enabled connects to the broker’s provisioning
collective and answers only the choria_provision agent. Provisioner is a client on that same
collective. It finds such a server through broadcast discovery or a lifecycle event and puts it on
the work queue. A worker then runs a fixed sequence of RPC calls against that one server: fetch its
JWT, fetch its key material, fetch its inventory, ask the helper for a configuration, send the
configuration back, and restart it. The helper handles certificate authorities, naming schemes, fact
sources, and policy decisions. It is a program in any language that reads JSON on standard input and
writes JSON on standard output.
A Provisioner that wins the election logs its discovery interval and worker count, followed by one line for each host admitted and one for each RPC issued:
Architecture covers the package layering. Discovery and the work queue follows a server from first sighting to a worker.
The source is four packages and about 2,400 lines.
main.go calls into cmd. Packages: cmd, config, hosts, host.
fisk command line, flag parsing, and process startup. Loads both configuration files, builds the Choria framework, starts the Prometheus listener and the signal handler, then calls hosts.Process. One file, cmd/provisioner.go.rpcWrapper and runHelper check before every outbound action. Files: config.go, pausable.go, stats.go.hosts.go, event.go, provisioner.go, election.go, stats.go.host.go, rpc.go, helper.go, version.go, stats.go.cmd imports hosts, hosts imports host, and all three import config, which imports none of
them. host does not import hosts, so host/host_test.go builds a Host from a config.Config
and a logger with no scheduler involved.
cmd imports hosts, hosts imports host, and nothing imports upward, so host compiles and tests on its own.The helper, the pause flag, and the generated RPC clients each change without touching the other two.
Host to JSON, writes it to a child process on standard input, and unmarshals the reply into host.ConfigResponse (host/helper.go:38). The helper reaches certificate authorities, CMDBs, and naming services, in any language.config.Config declares Pause, Resume, Flip, and Paused (config/pausable.go). Leader election calls Pause and Resume, and both rpcWrapper and runHelper check the flag before sending an RPC request or starting the helper process.host/rpc.go uses two clients generated in go-choria, provclient.ChoriaProvisionClient for the choria_provision agent and rpcutilclient.RpcutilClient for rpcutil. Every call to either goes through rpcWrapper, which holds the retries, timing, and pause check for both.hostsThe hosts package declares its state as package variables rather than struct fields
(hosts/hosts.go:24-33): the hosts map of identity to *host.Host, the buffered work and done
channels, a mutex, and the logger, the Choria framework, and the configuration.
hosts.Process writes these package variables on entry, so exactly one instance can run per process.
That matches the deployment model, where a process runs one Provisioner against one site, and the
scheduler functions take no receiver. Running two Provisioners in one binary requires moving this
state into a struct.
cmd.run loads the Provisioner YAML from --config through config.Load, and the Choria client
configuration from --choria-config, defaulting to choria.UserConfig(). cmd.run then overrides
the Choria client configuration in code (cmd/provisioner.go:61-79): log level and log file come from
the Provisioner configuration, and the collective is forced to provisioning for both Collectives
and MainCollective.
Setting choria_insecure disables TLS, sets protocol.Secure to false, and forces the file
security provider. Setting broker_provisioning_password switches the NATS credentials to user
provisioner with that password. The broker’s dedicated provisioning account accepts that user.
config.Load fills defaults and rejects configurations that cannot work
(config/config.go:64-142):
| Rule | Behavior |
|---|---|
workers unset | Defaults to runtime.NumCPU(). |
lifecycle_component unset | Defaults to provision_mode_server. |
lifecycle_component contains ., >, or * | Rejected, since the value is interpolated into a NATS subject. |
cert_deny_list empty | Defaults to four patterns that block privileged Choria certificate names. |
features.pki and features.ed25519 both set | Rejected. The two enrollment models are exclusive. |
features.ed25519 set | Implies features.jwt. |
server_jwt_validity unset | Defaults to one year. |
interval below one minute | Rejected. |
config/stats.go, hosts/stats.go, and host/stats.go each register their metrics in an init
function: the pause gauge, the fleet-level counters, and per-server timing and errors. Every metric
carries a site label taken from the site configuration key, so a query across installations can
sum or group by site. cmd.setupPrometheus serves them on /metrics when monitor_port is set.
Continue to Discovery and the work queue to see how a server reaches a worker.
Broadcast discovery and lifecycle events both reach the same admission function, add. Everything
after add is identical.
hosts: fleet-level scheduling. Files: hosts.go for the main loop, discovery, and the add
and remove pair; event.go for the lifecycle listener; provisioner.go for the worker pool and the
finisher.
hosts.Process (hosts/hosts.go:36) runs for the life of the process. It stores the framework,
configuration, and logger in package variables, opens a connector, and publishes a startup lifecycle
event for the provisioner component so a fleet operator can see the instance start.
It then starts goroutines in a fixed order:
leader_election is set, conf.Pause() runs first so the instance starts inert, then startElection begins campaigning.done channel.cfg.Workers goroutines, each running provisioner().hosts.Process then sets every Prometheus counter and gauge to zero, so the full metric set appears
on /metrics before the first server arrives. It runs discover(ctx) once, then blocks on a select
over the interval ticker, the election trigger channel, and context cancellation.
add. A worker checks the map, not the queue, before acting.discoverProvisionableNodes (hosts/hosts.go:201) builds a filter for the choria_provision agent
and runs a broadcast discovery against the provisioning collective:
The timeout is a fixed two seconds and the window slides, so the call returns as soon as replies stop
arriving rather than always waiting the full two seconds. Every returned identity becomes a new
host.Host and goes through add.
discover (hosts/hosts.go:186) checks the pause flag first. When paused it logs, returns before
sending the broadcast request, and leaves the cycle counter unchanged, so a standby instance only
campaigns for the election.
listen (hosts/event.go:26) queue-subscribes to
choria.lifecycle.event.startup.<lifecycle_component>, where the component defaults to
provision_mode_server. config.Load rejects a component containing ., >, or *, because the
value is interpolated straight into a NATS subject and those characters would widen the subscription.
Each message goes through handle, which checks the pause flag, parses the event, and returns the
identity only for events of type lifecycle.Startup. listen therefore admits a server that has just
booted into provisioning mode without waiting for the next discovery cycle. Broadcast discovery and
the listener increment separate counters, choria_provisioner_discovered and
choria_provisioner_event_discovered, so their relative contribution is visible in monitoring.
add (hosts/hosts.go:153) takes the mutex, and no other function writes to the work channel.
For an identity already in the map, add compares the age of the new sighting against
conf.IntervalDuration:
In that comparison, host is the newly constructed Host, whose discovered field NewHost sets to
time.Now(). The comparison therefore measures the age of the sighting being processed, not the age
of the entry already in the map.
A duplicate provision costs less than a node that never gets provisioned, so add re-admits on the
older sighting. A duplicate is self-limiting, because the second attempt times out on its first RPC
failure and Provision also refuses nodes that have been waiting too long. A dropped node stays
unconfigured until the next event or discovery cycle.
add then writes to the buffered work channel in a non-blocking select. If the buffer is full,
add logs a failure and deletes the identity from the map, so the map holds only entries that are on
the queue.
Each worker (hosts/provisioner.go:15) blocks on the work channel. It then calls isCurrent, which
looks the identity up in the map, before calling provisionTarget:
An entry can be on the queue but no longer in the map, which happens when removeAllHosts runs after
a lost election. The worker skips the entry and does not send it to done, because removeAllHosts
already deleted the identity.
The worker then calls provisionTarget, which increments choria_provisioner_busy_workers for the
duration and delegates to host.Provision. The return value carries a delay flag alongside the error:
| Outcome | What the worker does |
|---|---|
| Error | Increments choria_provisioner_provision_errors, logs, and sends to done immediately. |
Success, delay false | Sends to done immediately, so the host can be rediscovered on the next cycle. |
Success, delay true | Sends to done after 60 seconds via time.AfterFunc. |
The 60 second delay leaves the node in the map while it restarts, so the next discovery cycle skips it. After an in-place version upgrade the flag is false instead, and the node is re-provisioned on the next cycle.
One goroutine drains done and calls remove, which deletes the identity from the map and updates
both the choria_provisioner_waiting_nodes and choria_provisioner_work_queue_entries gauges. Only
the finisher calls remove, so workers never take the mutex for deletion and one goroutine writes
both gauges.
The provisioning cycle covers the RPC sequence a worker runs against a server.
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.
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 feature flags gate individual steps. The order never changes.
configure runs.Provision checks provisioned and discovery age before it issues any RPC.
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.
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.
getConfig marshals the whole Host to JSON and runs the helper. Its reply, a
host.ConfigResponse, can end the cycle:
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.
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:
| Result | Effect |
|---|---|
No error, skipped true | Versions already match. Falls through and configures the node normally. |
No error, skipped false | An upgrade ran. Returns (false, nil) immediately, ending the cycle before configuration. |
Error, upgrades_optional true | Logs a warning and configures the node on its old version. |
Error, upgrades_optional false | Returns (true, err), so the finisher holds the node out of the map for 60 seconds. |
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 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).
The helper contract covers the JSON exchange. RPC, retries, and upgrades covers what happens inside each call.
Provisioner ships no knowledge of any particular site. It cannot name a node, reach a certificate
authority, pick a broker, or decide which policies a machine should carry, because every one of those
answers differs between deployments and most of them live in systems Provisioner has never heard of.
The helper supplies them. It is a program in any language, named by the helper configuration key,
that reads a JSON description of one server on standard input and writes that server’s configuration
back on standard output.
The helper runs at the midpoint of the provisioning cycle, once per server. By the time Provisioner
calls it, the fetch steps have already collected everything the node can say about itself: its
verified provisioning.jwt, its inventory, and its CSR or ed25519 public key, depending on the
enrollment model. The helper turns that description into a decision. It can return a configuration
map, x509 credentials, a signed-token request, Open Policy Agent and Action Policy documents, and a
version to upgrade to. It can also refuse: defer leaves the node unconfigured for a later cycle, and
shutdown stops it. Provisioner takes whatever comes back, sends it to the node in a single
configure call, and restarts the node.
host/helper.go, 131 lines. getConfig at host/helper.go:38 marshals the input,
runDecodedHelper decodes the reply, and runHelper at host/helper.go:68 starts the child process
and waits on it.
getConfig marshals the Host itself, so the input shape is exactly the struct’s exported fields
(host/host.go:29-51). The JSON-tagged fields are the only ones the helper sees; every other field on
Host is unexported and stays in the process.
provision.CSRReply, non-null only when features.pki is enabled.provision.ED25519Reply, non-null only when features.ed25519 is enabled.rpcutil#inventory result. A JSON string, not a nested object.provisioning.jwt claims, non-null only when features.jwt is enabled.inventory is a string containing JSON, because fetchInventory assigns
h.Metadata = string(j) from the RPC reply. A helper has to decode it a second time to reach the
facts, the agent list, or the version. The identity value at the top level and the one inside the
inventory come from different sources, so a helper that needs the identity should use the top-level
field.
The token, the signing keys, and the helper path itself are not sent. A helper that needs a secret reads it from its own environment or files.
The reply decodes into host.ConfigResponse (host/helper.go:23). Unknown keys are ignored and every
field is optional, so a minimal helper answers with a configuration object alone.
Provision checks defer and shutdown first, and either one ends the cycle. upgrade diverts to
the upgrade path. Provision copies the rest onto the Host and sends it to the node in the single
configure RPC. The provisioning cycle gives the order those
checks run in, and Writing a helper gives worked examples in
Ruby.
runHelper runs these steps in order.
prometheus.NewTimer on choria_provisioner_helper_time is deferred before anything else, so a timed-out helper still records its duration.context.WithTimeout(ctx, 10*time.Second) wraps the whole call. No configuration key sets the value.shellquote.Split handles quoting, so helper can carry arguments, for example /opt/prov/helper.rb --site london. An empty result is rejected.Wait is required: Wait closes the pipes.ProcessState.Success() must be true.runHelper sets a stdin and a stdout pipe but leaves Stderr nil, and os/exec connects a nil
Stderr to the null device. Anything a helper writes to standard error is discarded and never reaches
the Provisioner log. A helper that has a reason to report should return defer with a msg, which is
logged, or write its own log file.
Provision counts the failures. It increments choria_provisioner_helper_errors when getConfig
returns an error (host/host.go:134), and the successful shutdown path increments
choria_provisioner_helper_shutdown_requests from host/rpc.go:169.
Security and enrollment covers what happens to the key material that the helper returns.
Provisioner talks to servers that have no credentials yet. It checks every value it accepts from a node before using it, and signs every credential it returns with keys that the node cannot reach.
host: host.go holds validateJWT, validateCSR, generateServerJWT, and encryptPrivateKey.
rpc.go holds fetchJWT, fetchCSR, and fetchEd25519PubKey. Key material paths come from
config.Config.
features.pki and features.ed25519 select mutually exclusive models, and config.Load rejects a
configuration that sets both. features.jwt is independent, and enabling features.ed25519 turns it
on implicitly.
| Feature | What Provisioner fetches | What it gives back |
|---|---|---|
jwt | The node’s provisioning.jwt. | The node already holds this token, so Provisioner returns none. |
pki | A CSR generated by the node. | A signed certificate and CA from the helper. |
ed25519 | The node’s ed25519 public key, with a signed nonce. | A server JWT signed by Provisioner. |
Runs when features.jwt is set. Setting features.ed25519 sets it too, so this step also runs in
Organization Issuer deployments.
validateJWT (host/host.go:405) refuses to run without jwt_verify_cert. os.Stat decides whether
that value is a key file path or a hex ed25519 public key:
A path that exists on disk is read as a key file. Anything else is decoded as a hex ed25519 public
key, the form of an Organization Issuer public key. validateJWT stores the parsed claims on
h.JWT, and getConfig sends them to the helper as the verified jwt field.
Runs when features.ed25519 is set. features.pki deployments skip it, and config.Load rejects a
configuration that sets both.
fetchEd25519PubKey (host/rpc.go:228) generates a nonce
with choria.NewRequestID(), sends it in the gen25519 request, and verifies the node’s signature
over it:
Without the nonce, a node could present any public key, including one belonging to a different machine, and receive a server JWT bound to that key. The signature proves the node holds the matching seed. The call also records the directory that the node created the seed in, which becomes the SSL directory in the generated configuration.
Runs when features.ed25519 is set, immediately after the helper returns. It also needs
jwt_signing_key, and server_jwt_validity or its one year default.
generateServerJWT (host/host.go:251) builds the claims in increasing order of precedence:
built-in defaults, the configuration that the helper returned, and the helper’s explicit
server_claims.
choria. Overridden by server_claims.ou.mcollective. Becomes choria when the returned configuration sets plugin.security.provider to choria, then a comma-split of the collectives configuration key, then server_claims.collectives.server_jwt_validity, or one year. A server_claims expiry is converted with time.Until.server_claims.permissions is set.server_claims.additional_publish_subjects is set.A computed validity of one hour or less is discarded and replaced with the configured default, which covers both an expiry already in the past and a claim that failed to parse.
When jwt_signing_token is set, generateServerJWT reads the signing token and the seed file from
disk for every server. The code does this deliberately: “a bunch of redundant repeated reading happens
here of the same files but I prefer to do that so just updating the secrets will update the running
instance”. Rotating the issuer credentials takes effect on the next server rather than on the next
restart. Provisioning a thousand servers therefore performs two thousand small file reads.
With a signing token present, the token must carry a TrustChainSignature claim, and
claims.AddChainIssuerData attaches the chain before signing. tokens.SignTokenWithKeyFile signs the
result, and the configure call sends it to the node as server_jwt.
Runs when features.pki is set. features.ed25519 deployments skip it, because no x509 certificate
is issued.
validateCSR (host/host.go:435) parses the PEM that the node sent. The common name must equal the
identity that Provisioner discovered, and no name on the request may match the denylist. validateCSR
checks the common name and every DNS SAN.
The default denylist blocks the certificate names that Choria reserves for privileged clients:
matchAnyRegex (host/host.go:468) treats a pattern wrapped in forward slashes as a bare regular
expression and strips them, then matches with regexp.MatchString. Matching is unanchored, so a
pattern without ^ or $ matches anywhere in the name.
A node in provisioning mode is unauthenticated, so validateCSR is the only check on the name it
receives, and that name can request a certificate with privileged access to the whole fleet. Setting
cert_deny_list replaces the defaults rather than adding to them, so a custom list must repeat these
four patterns.
Runs when the helper returns a key, which no feature flag controls. It also needs features.jwt,
because the server’s ECDH public key arrives on the jwt reply.
Some certificate authorities generate the key pair themselves rather than signing a CSR. For those,
the helper can return a key, and it has to cross the network to a node that holds no credentials.
encryptPrivateKey (host/host.go:354) uses an ephemeral Diffie-Hellman exchange over the two RPC
calls that already happen.
The server’s ECDH public key arrives on the jwt reply as EcdhPublic, so key encryption requires
features.jwt. Without it encryptPrivateKey fails with “private key received from helper but server
did not start Diffie-Hellman exchange”. encryptPrivateKey stores Provisioner’s own public key on
h.provisionPubKey, and the configure call sends it as EcdhPublic, so the node can derive the
same secret and decrypt the PEM block.
x509.EncryptPEMBlock is deprecated in the standard library and the call carries a
//lint:ignore SA1019 there is no alternative comment. The format is fixed by what Choria Server can
decrypt.
RPC, retries, and upgrades covers the wrapper that every one of these calls runs through.
Every call Provisioner makes to a server goes through one wrapper, so rpcWrapper holds the retries,
timing, error counting, and pause check for every call.
host/rpc.go, 404 lines. Every action calls rpcWrapper at host/rpc.go:58.
host/version.go holds the RPM version comparison that the upgrade decision uses.
rpcUtilClient and provisionClient (host/rpc.go:22 and host/rpc.go:40) build the generated
go-choria clients for the rpcutil and choria_provision agents. Both narrow the client the same way
before calling rpcWrapper:
The client uses one worker and one target because a Host is one machine. The action name passed to
rpcWrapper is formatted as agent#action, and that string becomes the rpc label on both
choria_provisioner_rpc_time and choria_provisioner_rpc_errors, so a dashboard can show which call
is slow or failing.
backoff.Default.For retries until the callback returns nil or the context ends, so the try limit is
enforced from inside:
The pause flag is read twice: once before the loop and once on every attempt. A provisioner that loses an election mid-provision stops at the next attempt boundary rather than finishing the sequence against a node that another instance is now provisioning.
The timer is deferred inside the callback, so choria_provisioner_rpc_time records each attempt
separately rather than the total across retries. choria_provisioner_rpc_errors increments once per
wrapper call, after the loop gives up.
Each caller picks its own limit, and the values reflect how expensive a repeat is.
| Call | Tries | Reasoning |
|---|---|---|
rpcutil#inventory | 5 | Read-only and cheap to repeat. |
choria_provision#gen25519 | 5 | Read-only. A repeat generates a fresh nonce. |
choria_provision#configure | 5 | The call that writes the configuration. Repeats are idempotent on the node. |
choria_provision#jwt | 3 | Read-only. |
choria_provision#restart | 3 | A repeat restarts a node that may already be restarting. |
choria_provision#shutdown | 3 | A repeat targets a node that may already be shutting down. |
choria_provision#release_update | 3 | A repeat downloads and rewrites the binary again. |
choria_provision#gencsr | 1 | Each call makes the node generate a new private key. |
Every action checks the response count first. It must be exactly one:
Zero means the node did not answer within the client timeout. More than one means two machines answer
to the same identity, and the wrapper cannot tell which machine the configuration would reach, so it
fails the call. EachOutput then checks ResultDetails().OK() and assigns to a captured err that
the enclosing closure returns.
shutdown is the exception. It logs a non-OK result as a warning without setting err, and
increments choria_provisioner_helper_shutdown_requests only on the OK path. The helper already
rejected the node, so a failed shutdown confirmation does not fail the cycle.
handleHostUpgrade (host/host.go:219) runs before configuration and refuses the node when
upgrades_repository is unset and upgrades_optional is off, when the inventory carried no version,
or when the node did not report upgradable.
The comparison itself uses host/version.go, a vendored copy of
go-rpm-version, MIT licensed, vendored because the
upstream project is unmaintained. It implements RPM’s epoch:version-release parsing and the
rpmvercmp segment comparison, including tilde handling and the rule that numeric segments sort
higher than alphabetic ones.
handleHostUpgrade compares the two versions with Equal and acts on any difference. Version
exposes GreaterThan and LessThan, and neither is called anywhere in the codebase. A helper that
returns an upgrade value older than the node’s current version triggers a release update that moves
the node backwards. Whether a version change is an upgrade or a rollback is therefore the helper’s
decision. A helper that must prevent rollbacks compares the versions itself before setting the field.
NewVersion never returns an error. An epoch that does not parse as an integer silently becomes 0,
which follows the upstream behavior.
When the versions differ, upgrade sends release_update with the configured repository, the token,
and the target version, and Provision returns with no delay so the node is re-provisioned on the
next cycle.
Pausing and leader election covers the pause flag that gates every call on this page.
Running several Provisioners against one broker would have them race to configure the same servers. Instead, every instance starts paused and the election winner resumes.
config/pausable.go holds the flag and its four methods. hosts/election.go calls Pause and
Resume from the election callbacks. config/stats.go exports the flag as a gauge.
config.Config embeds a sync.Mutex and carries an unexported paused bool. Pause, Resume,
Flip, and Paused guard it, and each sets choria_provisioner_paused before returning:
config.Load sets the gauge to 0 at startup, so the metric exists before any election runs.
Every action that leaves the process reads the flag first:
| Reader | Effect when paused |
|---|---|
hosts/hosts.go:187 in discover | Logs and returns without sending the broadcast request. |
hosts/event.go:63 in handle | Logs and drops the lifecycle event. |
host/rpc.go:59 in rpcWrapper | Fails the call before the retry loop starts. |
host/rpc.go:74 inside the retry loop | Cancels the context and fails at the next attempt. |
host/helper.go:72 in runHelper | Refuses to run the helper. |
rpcWrapper reads the flag inside the retry loop as well as before it, so a Provisioner that loses an
election mid-sequence stops at the next attempt boundary. Without the inner check, an instance could
carry on sending configuration to a node that the new leader has already started working on.
Flip is defined alongside the other three and has no caller anywhere in the repository. All four
methods carry comments saying they implement backplane.Pausable, but this codebase does not import
the backplane package. The four methods satisfy the interface for a consumer that this repository
does not contain.
Setting leader_election makes hosts.Process call conf.Pause() before it starts any goroutine, so
the instance is inert from its first line of work. startElection (hosts/election.go:17) then
registers two callbacks with a Choria Streams election named provisioner.
The won callback resumes work and then writes to discoverTrigger, a channel of capacity one that
hosts.Process selects on alongside the interval ticker. A new leader therefore runs a discovery
immediately instead of waiting up to a full interval, so provisioning continues within seconds of a
failover.
The lost callback pauses and calls removeAllHosts, which empties the map and then drains the work
channel in a non-blocking loop until it is empty. A host already passed to a worker stays with that
worker, but the worker’s isCurrent check rejects it before provisioning starts.
Discovery and the work queue covers that check.
Campaigning uses backoff.TwentySec, so a failover can take up to a minute of standby time before a
new leader starts working.
hosts.Process launches startElection in a goroutine, so its returned error is discarded. If
fw.NewElection fails, the instance stays paused and logs nothing further.
elect.Start runs in a nested goroutine and calls log.Fatalf on error, ending the process rather
than leaving the instance paused:
startElection defers wg.Done() and returns once the election is registered, so its WaitGroup
entry clears at setup rather than at shutdown.
With leader_election unset, paused stays false for the life of the process and every gate passes.
A single instance needs no election, and the documentation recommends that deployment unless high
availability is required.
Configuration File covers the operational side.
Reference and map collects the file map, configuration keys, and metrics in one place.
Each table links to the page that explains the area it covers.
The whole repository, from main.go down. ABTaskFile, Rakefile, and packager/ hold the build
tooling.
main.go calls cmd.Run, which builds a fisk application named choria-provisioner. The run
command is the default, so choria-provisioner --config ... and
choria-provisioner run --config ... are equivalent.
| Flag | Command | Required | Default | Effect |
|---|---|---|---|---|
--debug | Global | No | Off | Forces the Choria log level to debug. |
--config | run | Yes | None | Names the Provisioner YAML. Must be an existing file. |
--choria-config | run | No | choria.UserConfig() | Names the Choria client configuration. Must be an existing file. |
--pid | run | No | Unset | Writes the PID, and removes the file on exit. |
--version prints config.Version, which is 0.0.0 in a plain go build. The release build injects
it through -ldflags, mapped in packager/buildspec.yaml to
github.com/choria-io/provisioner/config.Version.
| File | Lines | Holds |
|---|---|---|
main.go | 9 | The call to cmd.Run. |
cmd/provisioner.go | 130 | Flags, both config loads, the Choria framework, /metrics, signal handling, and the PID file. Architecture. |
config/config.go | 142 | The Config struct, Load, the defaults, and the validation. |
config/pausable.go | 48 | Pause, Resume, Flip, and Paused. Pausing and leader election. |
config/stats.go | 18 | The choria_provisioner_paused gauge. |
hosts/hosts.go | 223 | Process, the package state, broadcast discovery, add, remove, and isCurrent. Discovery and the work queue. |
hosts/event.go | 82 | The connector and the lifecycle startup subscription. |
hosts/provisioner.go | 84 | The worker loop and the finisher. |
hosts/election.go | 51 | The leader election setup and its two callbacks. |
hosts/stats.go | 66 | Nine fleet-level counters and gauges. |
host/host.go | 483 | The Host struct, Provision, JWT and CSR validation, server JWT issuance, and key encryption. The provisioning cycle and Security and enrollment. |
host/rpc.go | 404 | rpcWrapper and every Choria RPC action. RPC, retries, and upgrades. |
host/helper.go | 131 | ConfigResponse and the child process. The helper contract. |
host/version.go | 210 | The vendored RPM version comparison. |
host/stats.go | 42 | Five per-server metrics. |
host/host_test.go | 238 | Ginkgo specs for generateServerJWT, validateCSR, and encryptPrivateKey. |
tools.go | 18 | Build-tagged tools imports that pin the Ginkgo CLI, excluded from normal builds. |
config/config.go:25. The parsed YAML plus derived durations, the config file path, and the pause flag. Passed by pointer to every package.host/host.go:29. One server. Five exported fields form the helper's input. The remaining fields hold state that the RPC sequence fills in.host/helper.go:23. The helper's reply. Twelve optional fields.host/version.go:21. An RPM epoch, version, and release triple with a Compare method.Every metric carries a site label taken from the site configuration key. Three init functions
register them, one per package.
| Metric | Type | Extra labels | Registered in |
|---|---|---|---|
choria_provisioner_paused | Gauge | None | config/stats.go |
choria_provisioner_discovered | Counter | None | hosts/stats.go |
choria_provisioner_event_discovered | Counter | None | hosts/stats.go |
choria_provisioner_discover_cycles | Counter | None | hosts/stats.go |
choria_provisioner_discovery_errors | Counter | None | hosts/stats.go |
choria_provisioner_provision_errors | Counter | None | hosts/stats.go |
choria_provisioner_provisioned | Counter | None | hosts/stats.go |
choria_provisioner_busy_workers | Gauge | None | hosts/stats.go |
choria_provisioner_work_queue_entries | Gauge | None | hosts/stats.go |
choria_provisioner_waiting_nodes | Gauge | None | hosts/stats.go |
choria_provisioner_rpc_time | Summary | rpc | host/stats.go |
choria_provisioner_rpc_errors | Counter | rpc | host/stats.go |
choria_provisioner_helper_time | Summary | None | host/stats.go |
choria_provisioner_helper_errors | Counter | None | host/stats.go |
choria_provisioner_helper_shutdown_requests | Counter | None | host/stats.go |
The rpc label is the agent#action string, for example choria_provision#configure or
rpcutil#inventory. Monitoring describes what each metric measures.
Configuration File gives the full operational description. The following table gives the struct field and the reader for each key.
| YAML key | Field | Read by |
|---|---|---|
workers | Workers | hosts.Process, to size the worker pool. |
interval | Interval, IntervalDuration | hosts.Process for the ticker, add for the dedup window, Provision for the staleness check. |
helper | Helper | runHelper, split with shellquote. |
token | Token | NewHost, which copies it to Host.token for every choria_provision action. |
site | Site | Every metric, as the site label. |
lifecycle_component | LifecycleComponent | listen, as the startup event subject. |
logfile, loglevel | Logfile, Loglevel | cmd.run, which copies them onto the Choria configuration. |
choria_insecure | Insecure | cmd.run, which disables TLS and forces the file security provider. |
monitor_port | MonitorPort | cmd.setupPrometheus. |
broker_provisioning_password | BrokerProvisionPassword | cmd.run, which sets the NATS user to provisioner. |
cert_deny_list | CertDenyList | validateCSR, through matchAnyRegex. |
jwt_verify_cert | JWTVerifyCert | validateJWT, as a file path or a hex public key. |
jwt_signing_key | JWTSigningKey | generateServerJWT. |
jwt_signing_token | JWTSigningToken | generateServerJWT, re-read for every server. |
server_jwt_validity | ServerJWTValidity, ServerJWTValidityDuration | generateServerJWT. |
upgrades_repository | UpgradesRepo | upgrade, as the release_update repository. |
upgrades_optional | UpgradesOptional | Provision, in the upgrade failure branch. |
leader_election | LeaderElection | hosts.Process, to start startElection. |
features.jwt | Features.JWT | Provision, to gate fetchJWT and validateJWT. |
features.pki | Features.PKI | Provision, to gate fetchCSR and validateCSR. |
features.ed25519 | Features.ED25519 | Provision, to gate fetchEd25519PubKey and generateServerJWT. |
features.upgrades | Features.VersionUpgrades | Provision, to gate handleHostUpgrade. |
rego_policy is declared on Config as RegoPolicy and read nowhere. OPA policies reach a node
through the helper’s opa_policies reply instead.
| Agent | Action | Purpose |
|---|---|---|
rpcutil | inventory | Facts, agent list, version, and the upgradable flag. |
choria_provision | jwt | The provisioning.jwt and the server’s ECDH public key. |
choria_provision | gen25519 | An ed25519 public key and a signature over a nonce. |
choria_provision | gencsr | A CSR generated by the node. |
choria_provision | release_update | An in-place binary upgrade. |
choria_provision | configure | Configuration, credentials, and policies in one call. |
choria_provision | restart | Restart into the new configuration. |
choria_provision | shutdown | Exit with code 0, which does not trigger a systemd restart. |
collectives and main_collective to this value.choria.lifecycle.event.>. Provisioner subscribes to the startup ones.The Code Map overview returns to the start. Writing a helper gives the operator’s view of the helper contract.