Internals
Read from the source at commit
56169b71. Every claim here points at a file and line.
Code map
| Path | Responsibility |
|---|---|
apis/metal3.io/v1alpha1/ | CRD types (BareMetalHost and companions), a separate Go module |
internal/controller/metal3.io/ | Reconcile logic and the finite state machine |
internal/webhooks/metal3.io/ | Validating and defaulting webhooks |
pkg/provisioner/ | Provisioner interface, ironic/fixture/demo backends, plugin loader |
pkg/hardwareutils/ | BMC protocol handling, a separate Go module |
config/, ironic-deployment/ | Kustomize manifests, generated by make manifests |
Core data structures
BareMetalHost (apis/metal3.io/v1alpha1/baremetalhost_types.go:865) pairs a Spec and a Status. Its kubebuilder markers define the short names bmh and bmhost, the status subresource, and print columns for Status, State, Consumer, BMC, Online, Error, and Age (:855-:863).
ProvisioningState (baremetalhost_types.go:294) is a string and the key into the state machine. Its constants include StateNone (the empty string), registering, inspecting, preparing, available/ready, provisioning, provisioned, deprovisioning, powering off before delete, deleting, externally provisioned, and unmanaged (:297 onward).
ProvisionStatus (baremetalhost_types.go:822) records the last provisioning result: the State, the Ironic node UUID in ID (:829), and the applied Image, RootDeviceHints, BootMode, RAID, Firmware, and CustomDeploy.
OperationalStatus (baremetalhost_types.go:225) is orthogonal to the provisioning lifecycle: OK, discovered, error, delayed, detached. The delayed value means the host is waiting on capacity.
The Provisioner interface (pkg/provisioner/provisioner.go:143) has nearly 30 methods (Register, InspectHardware, Prepare, Provision, Deprovision, Delete, PowerOn, PowerOff, HasCapacity, and more). Each returns a Result (:257) holding Dirty, RequeueAfter, and ErrorMessage, giving the whole API an asynchronous, poll-again shape.
A path worth tracing
The most non-obvious design choice is that the provisioner backend is a Go .so plugin loaded at runtime. The main binary resolves a plugin path and opens it before Kubernetes I/O starts.
In main.go, the defaults are set as constants: defaultProvisionerName = "ironic", defaultProvisionerPluginDir = "/plugins", and the suffix -provisioner.so (main.go:78-:81). The fixture backend is compiled in, so selecting it skips plugin loading entirely. The path is built and opened at:
pluginDir := cmp.Or(os.Getenv("PROVISIONER_PLUGIN_DIR"), defaultProvisionerPluginDir)
pluginPath := filepath.Join(pluginDir, provisionerName+provisionerPluginSuffix)
p, err := provisioner.Open(pluginPath, provisionerName)provisioner.Open (pkg/provisioner/plugin.go:99) calls plugin.Open, then looks up two required symbols and checks their signatures: PluginName (func() string) and NewProvisionerFactory (func(PluginConfig) (Factory, error)). If PluginName() does not match the expected name, the load is rejected:
name := nameFunc()
if expectedName != "" && name != expectedName {
return nil, fmt.Errorf("plugin %s: PluginName() returned %q, expected %q", path, name, expectedName)
}HostConfigure is optional; only a wrong signature is an error (plugin.go:135). The effect is that the ironic backend is loosely coupled from the core, and a third party can build a provisioner plugin (via the docker-build-sdk make target) and drop it in. The cost is the Go plugin constraints: it must be built with the same Go and dependency versions, and is effectively Linux-only.
Things that surprised me
Deletion makes forward progress even when deprovisioning keeps failing. If deprovision fails and delete has been requested, the state machine gives up after the retry count is exceeded and moves on to power off before delete, logging that the host may still be operational (host_state_machine.go:578-:585). And if the host is not registered in Ironic and BMO lacks the credentials to deprovision it, it skips power off and goes straight to deleting (:587-:595). The operational stance is that a host may be left behind, but a delete request must not get stuck.
The capacity gate sits in the state-transition path, not the action path. ensureCapacity (host_state_machine.go:87) is consulted from updateHostStateFrom only when the next state is inspecting, provisioning, or deprovisioning, so the check is deliberately limited to avoid putting excessive pressure on the provisioner (host_state_machine.go:107-:114).