Last verified: 2026-02-13
Overview
The upgrade rollout system performs binary and configuration upgrades on running nodes without reprovisioning. It replaces node binaries and config files in-place while preserving chain data, keys, and node identity. When to use upgrades vs. reprovisioning:
Design principles:
- Chain-agnostic: Same 8 action primitives serve all chains. No chain-specific code in the upgrade engine.
- Runtime-agnostic:
RuntimeAdapterinterface abstracts systemd/Docker. Adding a runtime requires only a new adapter. - Idempotent: Three-layer idempotency (completed store, checkpoints, system state checks) ensures safe re-entry after crashes.
- Compensating: Per-action
Compensate()in reverse order restores previous state on failure.
Architecture
Manifest Loader
Package:internal/upgrade/manifest/
Rollout creation supports two modes:
- Auto mode: Provide
upgrade_id+chain_profile_id+node_type. The control plane loads the manifest from{ConfigDir()}/upgrades/{chain_profile_id}/{upgrade_id}.yamland auto-populatesbinary_name,target_version,target_url,target_checksum,migration_type,recovery_plan,config_changes, andmanifest_content_hash. - Manual mode: Omit
upgrade_id. Operator provides all fields explicitly (existing behavior).
manifest.Reader interface (Get, List, ContentHash) is implemented by FileReader, which resolves the config directory at call time via chains.Provider.ConfigDir(). No new infrastructure dependency.
manifest_content_hash is computed from the raw YAML bytes (sha256:<hex>), not manually provided.
Rollout group component_order entries also support upgrade_id for per-component auto-population.
Agent Three-Layer Architecture
Actions express WHAT happens. Adapters express HOW for each runtime. The executor orchestrates sequencing and compensation. No layer has knowledge of the others’ concerns.Workflow Hierarchy
Control plane to agent flow: Control plane sendsCOMMAND_TYPE_UPGRADE (value 9) with an UpgradePayload containing artifact references (URL, checksum, version). No execution instructions travel over the wire. The agent constructs the action sequence locally and injects the correct RuntimeAdapter based on runtime.type from config.
Agent Upgrade Handler
UpgradeAction Interface
File:internal/opsagent/upgrade/action.go
RuntimeAdapter Interface
File:internal/opsagent/upgrade/adapter.go
Action Primitives
8 actions ininternal/opsagent/upgrade/actions/. The same set serves all runtimes:
Executor
File:internal/opsagent/upgrade/executor.go
The executor iterates actions sequentially. For each action:
- Check
IsAlreadyDone()via adapter (skip if true) - Execute with per-action timeout budget
- On failure: call
Compensate()on all completed actions in reverse order - Checkpoint completed actions for crash recovery
ScheduleToClose: 10 min. Heartbeat timeout: 30 sec.
Phase Reporting
The executor derives upgrade phase from the current action position:
Phase transitions are reported via command response metadata and written to
rollout_nodes.upgrade_phase.
Three-Layer Idempotency
Runtime Adapters
Systemd Adapter
Docker Adapter
See also: Extending for how to add a new runtime adapter.
Data Model
Tables
rollout_groups — multi-component upgrade coordination:
Partial unique index: one active group per chain (
WHERE status IN ('pending', 'running', 'paused')).
rollouts — per-component rollout:
Partial unique index: one active rollout per chain.
rollout_nodes — per-node upgrade tracking:
Unique constraint:
(rollout_id, node_id).
Status Enums
RolloutStatus / RolloutGroupStatus:RolloutGroupStatus adds partial (some components succeeded, some failed with partial_ok policy).
NodeUpgradeStatus:
UpgradePhase (display state on rollout_nodes):
NodeConfigState
Uses the existingnode_config_state table. binary_version is written by UpdateNodeBinaryVersion activity after successful upgrade. Initial version written by the provisioning workflow after CONFIGURE.
See also: Domain Model for canonical node and subscription models.
Temporal Workflows
RolloutGroupWorkflow
File:internal/workflows/rollout_group.go
RolloutWorkflow per component with a health gate between components.
Signals: pause, resume, cancel, rollback, skip
Failure policies:
Standalone single-component rollouts bypass this workflow entirely.
RolloutWorkflow
File:internal/workflows/rollout.go
Processing flow:
- Activity:
ResolveRolloutTargets— filter eligible nodes, assign batch indexes - Per batch: launch child
UpgradeNodeWorkflowper node, wait for batch completion - Activity:
UpdateRolloutProgress— update counters - Evaluate failure threshold (unless
force_continue=true) - Canary: after batch 0, extended observation (2x
health_wait), validate canary health continue-as-newwith remaining batches (event history resets per batch)- Final batch: Activity
FinalizeRollout(set terminal status, forced DB-Temporal sync)
UpgradeNodeWorkflow
File:internal/workflows/upgrade_node.go
Activity Profiles
See also: Workflows for existing Temporal patterns and compensation conventions.
Rollout Strategies
Rolling
- Upgrade
batch_sizenodes at a time (sequential batches, parallel within batch) - After each batch: check failure threshold
- If
failed / attempted > threshold-> auto-pause - Operator decides: resume, retry, cancel, rollback
Canary
- Batch 0:
canary_sizerandom nodes - Extended observation: 2x
health_wait_durationafter canary batch - If canary healthy -> proceed with remaining nodes in rolling batches
- If canary fails -> auto-pause
- Breaking migrations (
migration_type: breaking): canary batch is blocking — rollout cannot proceed until operator sendsapprove_canarysignal
All-at-Once
- All nodes in single batch (no sequential batching)
- For hard forks with block height deadlines
force_continue=true: complete all nodes regardless of failuresSkipValidation=true: skip health wait for deadline pressure
Compensation and Rollback
Per-Action Compensation
On failure at any action, the executor callsCompensate() on all completed actions in reverse order. The ordering guarantees correct restoration:
- Configs restored before daemon-reload
- Daemon-reload before start
- Service started last
Compensate() fails -> FAILED_ROLLBACK terminal state + critical incident. Manual intervention required.
Backup Directory
Each upgrade creates a backup at{data_dir}/.upgrade-backup/{upgrade_id}/:
FAILED_ROLLBACK + incident.
Rollback Guarantee Tiers
Tier is declared via
state_compatibility.migration_type in the upgrade manifest.
Post-rollback crash loop (Tier 3):
- Binary + config restored (Tier 1 always holds)
- Node starts -> old binary encounters incompatible state -> crash loop
- Health validation detects crash loop
- Critical incident created with
recovery_planfrom manifest - Node set to
degraded - Operator-initiated reprovisioning is the recovery path
Full Rollout Rollback
Operator-triggered via API:- Pause rollout (stop new batches)
- For each succeeded node: launch
UpgradeNodeWorkflowwith swapped versions (target = source) - Wait for all rollback workflows
- Mark rollout
rolled_back
UpgradeNodeWorkflow. No separate rollback workflow.
Group Rollback (rollback_all Policy)
When a component fails in a group with rollback_all:
- Reverse-order rollback of ALL completed components
- Each component rolled back via
RolloutWorkflowwith swapped versions - Group marked
rolled_back
Health Validation
WaitForHealthValidation
Polls forhealth_wait_duration with relaxed criteria. ALL must pass:
- At least one heartbeat received (agent alive)
application_health != criticalsync_status != stalled- Node state not
downorfailed - No crashloop (fewer than N restarts within
health_wait_duration)
sync_status == synced (syncing expected after restart) or application_health == ok (degraded tolerated during stabilization).
Maintenance State Exemption
During upgrade (node inmaintenance):
- Heartbeat evaluator skips maintenance nodes (existing behavior)
- Policy evaluator skips maintenance nodes
- No false alarms during upgrade window
MaintenanceCleanup Loop
Periodic check (configurable, default 30 min) for nodes stuck inmaintenance state. Catches nodes where the upgrade workflow failed without cleaning up state.
See also: Health and Incidents for the health evaluation pipeline.
Validation Rules
Creation-Time Rules
State Compatibility Validation
Declared in upgrade manifest (auto-populated fromstate_compatibility in auto mode, or provided directly in manual mode):
Config Rendering Pipeline
For upgrades with config changes, the control plane renders config content:- Load base template from
hoodcloud-chain-configs/ - Build
TemplateVarsfrom chain profile + node config (same variables as CONFIGURE) - Apply
variable_overridesfrom upgrade manifest - Render via Go
text/template-> full content - Include in
UpgradePayload.config_files
API Surface
Rollout Endpoints
Rollout Group Endpoints
Auth:
DualAuthMiddleware (JWT + API key). Required scope: admin (backend-granted only — users cannot self-assign this scope). Both JWT and API key users must have admin scope to access rollout endpoints. The CreatedBy field on rollouts records the authenticated user ID for audit trail.
Integration Points
Incident Categories
See also: Health and Incidents — Incident Service for existing incident categories.
Notification Events
Rollout-level (not per-node):rollout_started, rollout_completed, rollout_paused, rollout_failed, rollback_initiated, rollback_completed
Reuses existing dispatcher infrastructure.
NATS Event Subjects
- Rollout-level:
events.rollout.{rolloutID}.started,.progress,.completed - Per-node:
events.node.{nodeID}.upgrade_started,.upgrade_completed,.upgrade_phase_changed
Prometheus Metrics
Operational Guide
Creating a Rollout
Auto mode (recommended): Create an upgrade manifest YAML in{ConfigDir}/upgrades/{chain_profile_id}/, then:
POST /api/v1/rolloutswithupgrade_id,chain_profile_id,node_type,source_version, strategy, and batch size- Control plane loads manifest, auto-populates binary details, state compatibility, and config changes
POST /api/v1/rollouts/{id}/startto begin execution
upgrade_id):
POST /api/v1/rolloutswithchain_profile_id,node_type,binary_name,source_version,target_version,target_binary_url,target_binary_checksum, strategy, and batch sizePOST /api/v1/rollouts/{id}/startto begin execution
POST /api/v1/rollout-groupswithdesired_versions,component_order(entries may includeupgrade_idfor auto-population), andfailure_policyPOST /api/v1/rollout-groups/{id}/start
Monitoring Progress
GET /api/v1/rollouts/{id}— overall progress (counters)GET /api/v1/rollouts/{id}/nodes— per-node status withupgrade_phaseGET /api/v1/rollout-groups/{id}/status— per-component progress- Temporal UI: workflow status, heartbeat details (
action_index,action_name,progress_pct)
Pausing and Resuming
POST /api/v1/rollouts/{id}/pause— pauses after current batch completesPOST /api/v1/rollouts/{id}/resume— continues from paused state- Auto-pause triggers: failure threshold exceeded, canary failure
Handling Failures
- Individual node failures: agent runs per-action compensation automatically
- Retry a failed node:
POST /api/v1/rollouts/{id}/nodes/{nodeId}/retry FAILED_ROLLBACKnodes: manual intervention required (critical incident created)
Full Rollback
POST /api/v1/rollouts/{id}/rollback- System pauses the rollout, then launches reverse upgrades for all succeeded nodes
- Uses the same
UpgradeNodeWorkflowwith swapped versions
Canary Approval for Breaking Migrations
Whenmigration_type: breaking:
- Canary batch executes and completes
- Rollout blocks — waiting for operator approval
- Operator validates canary nodes manually
- Operator sends
approve_canarysignal to resume - Remaining batches proceed
Chain-Agnostic Verification
Adding a new systemd chain (zero code changes):- Create chain config YAML with
runtime: { type: systemd }and binary definition - Provision nodes
- Create rollout with new version -> agent injects
SystemdAdapter - Executor runs same 8 actions. No code changes.
- Create chain config with
runtime: { type: docker }and image digest - Provision nodes
- Create rollout -> agent injects
DockerAdapter - Executor runs same 8 actions. No code changes.
- New imperative runtime (e.g., LXC, Podman): implement
RuntimeAdapter. Zero changes to actions, executor, workflows, or API.
Related Documents
- Workflows — Temporal workflow patterns, compensation conventions
- Health and Incidents — Health evaluation pipeline, incident management
- Domain Model — Node state machine, subscription lifecycle
- Extending — Adding runtime adapters, notification channels