Skip to content

Internals

Read from the source at commit 56169b71. Every claim here points at a file and line.

Code map

PathResponsibility
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:

go
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:

go
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).