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

Pause gateno exec while pausedHost structidentity, csr, jwt,ed25519, inventoryhelper processany languageargv via shellquoteConfigResponse12 fields, alloptionalstdinstdoutcontext timeout: 10 seconds, not configurableFailureempty output or exit != 0helper stderr is discardedby os/exec, not logged
A fresh process per server, so no state carries over between calls.

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 when features.pki is enabled.
ed25519_pubkey
A provision.ED25519Reply, non-null only when features.ed25519 is enabled.
inventory
The rpcutil#inventory result. A JSON string, not a nested object.
jwt
The parsed and already-verified provisioning.jwt claims, non-null only when features.jwt is 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.

type ConfigResponse struct {
	Defer          bool                 `json:"defer"`
	Shutdown       bool                 `json:"shutdown"`
	Msg            string               `json:"msg"`
	Key            string               `json:"key"`
	Certificate    string               `json:"certificate"`
	CA             string               `json:"ca"`
	SSLDir         string               `json:"ssldir"`
	ServerClaims   *tokens.ServerClaims `json:"server_claims"`
	Configuration  map[string]string    `json:"configuration"`
	ActionPolicies map[string]string    `json:"action_policies"`
	OPAPolicies    map[string]string    `json:"opa_policies"`
	UpgradeVersion string               `json:"upgrade"`
}

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.

  1. Timing starts first A prometheus.NewTimer on choria_provisioner_helper_time is deferred before anything else, so a timed-out helper still records its duration.
  2. The pause flag is checked A paused instance refuses to start the helper, and the error includes the helper path.
  3. A 10 second timeout is applied context.WithTimeout(ctx, 10*time.Second) wraps the whole call. No configuration key sets the value.
  4. The command string is split with shellquote shellquote.Split handles quoting, so helper can carry arguments, for example /opt/prov/helper.rb --site london. An empty result is rejected.
  5. 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.
  6. Output is read to completion, then the process is waited on Reading before Wait is required: Wait closes the pipes.
  7. Zero bytes is an error A helper that exits cleanly without writing anything fails with "zero bytes received" rather than producing an empty configuration.
  8. 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.