Skip to main content
Version: v5.6.1

Infrastructure nodes

packs:infrastructure

An infrastructure node is one machine-shaped row in the estate tree: a host, hypervisor, network device or link registered under a project, environment or asset, living at <parent path>/infrastructure/<code>. Where the estate nodes themselves (opschain docs getting-started/1-1-projects-environments-assets-and-hosts) are the things changes target and properties converge over, an infrastructure node is an inventory record with a lifecycle: identity (code, name), indexed address columns, a closed type, an estate-membership state, discovered facts and its own properties document. It exists so a host operated by name has one home — the connection profile, the facts and the lifecycle state — instead of user@host strings scattered through workflows. The kind is engineered for tens of thousands of nodes per instance: hot columns are first-class and indexed, lists are keyset-paginated with server-side filters, and registration arrives in bulk.

The node, attribute by attribute​

AttributeMeaning
node_path / parent_pathThe node's own path (…/infrastructure/<code>) and its parent — a project, environment or asset.
codeLowercase letters, numbers and underscores, at most 50 characters; unique among the parent's infrastructure.
name / descriptionDisplay name (max 50) and description (max 1000).
infrastructure_typeOne of the closed registry: host, hypervisor, network_device, link.
address / fqdnNetwork address and fully qualified domain name. Indexed but unconstrained: a same-environment duplicate address warns, never refuses (duplicate_address on the row, a warning on reads).
lifecycle_stateregistered or deregistered (the manual membership states), or one of the reserved integration-asserted states provisioned / destroyed.
archivedShelved but still a member — set and cleared by update.
last_facts_atWhen discovered facts last landed; reads render freshness as last-observed ("facts last collected …"), never a live judgement.
properties_id / settings_idThe node's own properties and settings document ids. The long tail of configuration lives in the properties document, served on the get.

The properties document is schema-checked on every write:

  • No credential material, anywhere. Keys like password, private_key, token or credential refuse by name — connection custody belongs to transport providers, and facts writes apply the same refusal.
  • A flat per-type reference vocabulary. A hypervisor's host_ref, a device's mgmt_host_ref and a link's endpoint_a_ref/endpoint_b_ref name sibling infrastructure node paths (the host-targeting keys must name a host); any other *_ref key refuses by name. rpc.provider_ref — the transport-provider binding, a provider code string — is recognised on every type.

The duplicate-address warning scope is the environment subtree for environment- and asset-level nodes, the project for project-level ones.

Lifecycle​

Assertion follows authority. The manual lane — API, CLI and the pack's hand-over actions alike — asserts estate membership only:

  • Register creates the node registered under its parent. Registering a code a deregistered node holds revives the same row (same id, history intact) rather than creating a new one.
  • Update changes attributes in place; omitted fields preserve stored values, and a replacement properties document is validated like registration. Facts land the discovered-facts object under the properties document's facts key and stamp last_facts_at.
  • Deregister closes membership — the soft-close posture: hidden from list defaults and estate reads, full history and events retained, revivable by re-registering the code.
  • Bulk upsert registers or updates a batch under one parent in one set-based write — the lane sync jobs use, since inventories arrive in batches. New codes are created, existing codes updated in place; a per-item validation failure lands in that item's result without failing the batch, and a batch over 10,000 items refuses by name.

The provisioned and destroyed states stay in the schema as reserved slots for the integration that performs those operations; nothing in the manual lane writes them. What the infrastructure pack's hand-over automates is exactly this membership lane: an action that makes a machine exist calls infrastructure:host_register (the ordinary register verb, so the machine becomes estate content), and infrastructure:host_deregister closes membership when it leaves — never an operational state.

How a change targets a host​

A change still targets an estate node; the host enters through the host_ref input on a transport-session step (the infrastructure pack's dialing actions). At dispatch the engine resolves host_ref to the registered host node and re-points the whole transport seam at it: the provider is selected at the host's path (its own rpc.provider_ref, else the deepest-wins scope binding there, else the single enabled provider), and the caller's transport grant is enforced against the host node's path — the grant covers the host, not the declaring scope. Every miss refuses by name: an unresolved expression, an unknown node, a non-host type, and a deregistered or destroyed host each fail the step loudly. The dial profile defaults from the host's rpc.target, rpc.hops and rpc.tofu_store properties.

Expressions enumerate the registry: estate(<path>).infrastructure lists a node's infrastructure, and the two-argument estate('<path>', '<filter>') form is a filtered enumerate over the hot-column fields (code, name, path, type, address, fqdn, lifecycle_state — never the property bag), the fan-out idiom for "every host under prod". See opschain docs expressions.

Why hosts carry no catalog​

An infrastructure node never carries an action catalog of its own. There is no opschain actions list against a host, no per-host action files, and no way to create a change on an infrastructure node. That is a deliberate shape, not a gap, and it rests on four facts about how the platform governs execution.

One governed door into execution. Everything OpsChain runs is a change (or a workflow spawning changes), created against an estate node whose actions come from its pinned template version — and run creation is where every control attaches: approval policies hold matching runs before they start, declared inputs are typed and validated before anything exists, entitlement gates refuse by name, plans preview the step tree, and the whole run lands in the events feed. A second catalog hanging off every host would be a second door into execution, and every one of those controls would have to be built, configured and audited twice. Hosts have no door of their own, so work can only reach a host through the one lane that is already governed.

What runs is kept separate from where it reaches. A template version pins an exact commit of a repository, so what an action does is a reviewable git fact — diffable in review, resolved to a stored commit at write time, lockable for release. An infrastructure node answers the other question: where work lands — the address and connection profile (rpc.target, rpc.hops), the transport-provider selection at the host's path, and the caller's transport grant enforced against that same path. Folding a catalog into the host would collapse that separation and put executable content on what is, by design, an inventory record.

A host has no pinned source to derive a catalog from. An asset's catalog is derived from its template version's resolved commit — remove the pin and there is nothing reviewable left to derive. An infrastructure node carries identity, address columns, a lifecycle and a validated properties document, but no template identity and no repository behind it. A host catalog would therefore have no versioned, reviewable source: nothing to diff, nothing to lock, nothing an approver could read before it ran. The properties document refuses credential material by name for the same reason — an inventory record holds facts about the machine, never the means to act on it.

The host still governs — as an authorisation point. Host-lifecycle operations (patch a fleet, reboot a box, move files, gather facts) are expressed as host-targeted pack actions: the change still runs from an asset's catalog, and the host_ref input names the host. At dispatch the transport provider is resolved at the host's path and the caller's transport grant is enforced against the host node's own path — so the host contributes authority, identity and audit (path RBAC on reads, the transport session events, the registry's own event family) to every operation that touches it. It contributes everything except executable content.

The practical consequence: "run something on this host" is always "run a host-targeted action from an asset's catalog, with host_ref naming the host". The worked exercise in opschain docs getting-started/6-4-hosts-without-a-runner builds exactly that shape end to end — a template, an asset pinned to it, and a fleet of registered hosts the actions fan out over.

Settings and configuration​

No config.yaml key or settings document tunes this component. The bulk-batch cap (10,000 items) and the list page caps (default 100, maximum 1000 rows) are built in. The estate read model lives in PostgreSQL: on the embedded SQLite store every infrastructure RPC answers the typed ERROR_REASON_STORE_POSTGRES_REQUIRED error.

Governance and audit​

Authorisation is ordinary path RBAC, 404-on-deny throughout. Lists apply per-row read pushdown (an unreadable node never appears); gets need read on the node's path. Registering (and bulk upsert) needs update on the parent's …/infrastructure rule path and on each new node's own path; update, facts and deregister need update on the node's path. List and get responses carry the caller's own standing (authorisations) so read-only surfaces can drop their edit affordances honestly.

Every write is audited as an ordinary event (opschain docs events):

  • api:infrastructure:registered — a node registered (a revive records the same event).
  • api:nodes:update — a node updated (the same family the estate node writes record).
  • api:infrastructure:bulk_upsert — a batch ran, with item, created, updated and failed counts.
  • api:infrastructure:facts — discovered facts landed.
  • api:infrastructure:deregistered — membership closed.
  • api:infrastructure:provisioned / api:infrastructure:destroyed — reserved for the integration that asserts those states; nothing currently emits them.

The registry itself is base platform — registering, reading, updating and deregistering nodes carries no entitlement gate, so inventory stays fully available on licence lapse. What is gated is operating hosts: the infrastructure:* actions require packs:infrastructure at run creation, with the polite-lapse posture described on the infrastructure pack page.

The CLI family (opschain docs cli/infrastructure):

  • opschain docs cli/infrastructure-list — filters and pagination.
  • opschain docs cli/infrastructure-show — one node with its properties.
  • opschain docs cli/infrastructure-register — register one node.
  • opschain docs cli/infrastructure-update — attributes or properties.
  • opschain docs cli/infrastructure-bulk-upsert — batch from a YAML file.
  • opschain docs cli/infrastructure-deregister — close membership.
  • opschain docs cli/infrastructure-facts — record discovered facts.

The API service is InfrastructureService (v2 — opschain docs api-v2): ListInfrastructureNodes, GetInfrastructureNode, RegisterInfrastructureNode, BulkUpsertInfrastructureNodes, UpdateInfrastructureNode, DeregisterInfrastructureNode and RecordInfrastructureFacts.