The helper contract
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.
Where it lives
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.
One exec per server
What the helper receives
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.
- identity
- The Choria Server identity, typically the fully qualified domain name.
- csr
- A
provision.CSRReply, non-null only whenfeatures.pkiis enabled. - ed25519_pubkey
- A
provision.ED25519Reply, non-null only whenfeatures.ed25519is enabled. - inventory
- The
rpcutil#inventoryresult. A JSON string, not a nested object. - jwt
- The parsed and already-verified
provisioning.jwtclaims, non-null only whenfeatures.jwtis enabled.
Caveat
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.
What the helper returns
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.
The child process
runHelper runs these steps in order.
- Timing starts first A
prometheus.NewTimeronchoria_provisioner_helper_timeis deferred before anything else, so a timed-out helper still records its duration. - The pause flag is checked A paused instance refuses to start the helper, and the error includes the helper path.
- A 10 second timeout is applied
context.WithTimeout(ctx, 10*time.Second)wraps the whole call. No configuration key sets the value. - The command string is split with shellquote
shellquote.Splithandles quoting, sohelpercan carry arguments, for example/opt/prov/helper.rb --site london. An empty result is rejected. - Input is written from a goroutine The write to stdin runs concurrently with the read from stdout and closes the pipe on completion. Doing both in one goroutine would deadlock on any helper whose output exceeds the pipe buffer.
- Output is read to completion, then the process is waited on Reading before
Waitis required:Waitcloses the pipes. - Zero bytes is an error A helper that exits cleanly without writing anything fails with "zero bytes received" rather than producing an empty configuration.
- The exit code is checked
ProcessState.Success()must be true.
Caveat
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.
Next
Security and enrollment covers what happens to the key material that the helper returns.