swactor/crates/mvp-system/src/docker_cluster_provisioning.rs
Zachery Aaron Shores-Chmielewski 8363be74cd feat: docker provisioning
Treat Docker as a first-class provisioning provider for the local e2e cluster and add the weight-shard fetch/validate/bind lifecycle behind it.

- docker_cluster_provisioning: model Docker as a provider adapter behind a `DockerCli` boundary (`run_container`/`inspect_ssh_endpoint`/`remove_force`) exposing only per-node ownership primitives
- local_e2e_cluster: wire Docker provisioning into the e2e driver (default `swactor-mvp-local-e2e-cluster` image) across the bootstrap/teardown flow
- weight_shards: add `ModelArtifactRef` (parses `hf://repo@rev/path`), `ShardAssignment`, `ShardManifest`, and `ValidatedShard` with digest-based validation
- shard_fetch: add `ShardLocator` (digest/split/stage -> uri + cache key), `ShardCache`/`ShardFetcher` traits, a `ShardFetchCoordinator`, and typed `FetchError`s
- shard_weight_lifecycle: add the `ShardWeightLifecycle` state machine (Idle->Assigned->Located->Fetching->Fetched->Validating->Binding->Ready/Faulted) with a `WorkerShardBinder` trait
- provisioner/telemetry: route provision logs onto the datastream via per-node/stream channels (`submit_bytes`) and add shard_fetch/shard_weight_lifecycle/weight_shards guarantee tests

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-06-28 22:54:02 +04:00

669 lines
22 KiB
Rust

//! Docker-backed provider and single-node bootstrap wiring.
//!
//! Docker is treated as a concrete provider adapter here: it creates and destroys
//! real Docker container leases through a `DockerCli` boundary. Higher-level
//! callers may start many nodes, but this module intentionally exposes only
//! per-node ownership primitives so deployment code does not grow a cluster
//! provisioning layer.
use std::collections::BTreeMap;
use std::time::SystemTime;
use crate::node_provisioning::{
BootstrapDatastreamSink, BootstrapLogRecord, BootstrapLogSource, BootstrapLogStream,
BootstrapObservation, BootstrapSessionEvent, BootstrapSessionSpec, BootstrapStage,
CreateLeaseRequest, CreateLeaseResult, DesiredNodeShape, DestroyHandle, LeaseFacts,
LogicalNodeId, NodeManager, NodeManagerCommand, NodeManagerMsg, NodeRecord, ProviderError,
ProviderKind, ProviderLeaseId, ProviderPlugin, RunId, SshEndpoint, SwactorId, SwarmJoinSpec,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DockerCliError {
pub reason: String,
}
impl DockerCliError {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DockerRunRequest {
pub container_name: String,
pub image: String,
pub run_id: RunId,
pub logical_node_id: LogicalNodeId,
pub ssh_user: String,
pub labels: BTreeMap<String, String>,
pub env: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DockerRunResult {
pub container_id: String,
}
pub trait DockerCli {
fn run_container(
&mut self,
request: DockerRunRequest,
) -> Result<DockerRunResult, DockerCliError>;
fn inspect_ssh_endpoint(
&mut self,
container_id: &str,
) -> Result<Option<SshEndpoint>, DockerCliError>;
fn remove_force(&mut self, container_id: &str) -> Result<(), DockerCliError>;
}
#[derive(Clone, Debug)]
pub struct DockerProvider<C> {
cli: C,
}
impl<C> DockerProvider<C> {
pub fn new(cli: C) -> Self {
Self { cli }
}
pub fn cli(&self) -> &C {
&self.cli
}
pub fn cli_mut(&mut self) -> &mut C {
&mut self.cli
}
pub fn into_cli(self) -> C {
self.cli
}
}
impl<C: DockerCli> DockerProvider<C> {
fn build_run_request(request: &CreateLeaseRequest) -> DockerRunRequest {
let spec = &request.spec;
let mut labels = spec.shape.provider_labels.clone();
labels.insert("mvp.provider".into(), "docker".into());
labels.insert("mvp.run_id".into(), spec.run_id.0.to_string());
labels.insert("mvp.logical_node_id".into(), spec.logical_node_id.0.clone());
let mut env = BTreeMap::new();
env.insert("MVP_RUN_ID".into(), spec.run_id.0.to_string());
env.insert("MVP_LOGICAL_NODE_ID".into(), spec.logical_node_id.0.clone());
env.insert(
"MVP_ORCH_SWACTOR_ADDR".into(),
spec.swarm_join.orch_swactor_addr.clone(),
);
env.insert(
"MVP_JOIN_TOKEN_REF".into(),
spec.swarm_join.join_token_ref.clone(),
);
DockerRunRequest {
container_name: format!("mvp-{}-{}", spec.run_id.0, spec.logical_node_id.0),
image: spec.shape.image.clone(),
run_id: spec.run_id.clone(),
logical_node_id: spec.logical_node_id.clone(),
ssh_user: spec.boot.ssh_user.clone(),
labels,
env,
}
}
fn lease_from_container(
shape: &DesiredNodeShape,
logical_node_id: &LogicalNodeId,
container_id: String,
endpoint: &Option<SshEndpoint>,
) -> LeaseFacts {
let lease_id = ProviderLeaseId(format!("docker:{container_id}"));
let mut provider_metadata = BTreeMap::new();
provider_metadata.insert("container_id".into(), container_id.clone());
provider_metadata.insert("image".into(), shape.image.clone());
provider_metadata.insert("logical_node_id".into(), logical_node_id.0.clone());
if let Some(endpoint) = endpoint {
provider_metadata.insert("ssh_host".into(), endpoint.host.clone());
provider_metadata.insert("ssh_port".into(), endpoint.port.to_string());
}
LeaseFacts {
provider: ProviderKind::Docker,
lease_id: lease_id.clone(),
provider_contract_id: container_id.clone(),
offer_id: None,
destroy_handle: DestroyHandle {
provider: ProviderKind::Docker,
lease_id,
provider_contract_id: container_id,
},
provider_metadata,
}
}
}
impl<C: DockerCli> ProviderPlugin for DockerProvider<C> {
fn create_lease(
&mut self,
request: CreateLeaseRequest,
) -> Result<CreateLeaseResult, ProviderError> {
if request.spec.provider != ProviderKind::Docker {
return Err(ProviderError::new(
"docker provider received non-docker node spec",
));
}
let run_request = Self::build_run_request(&request);
let run_result = self
.cli
.run_container(run_request)
.map_err(|error| ProviderError::new(error.reason))?;
let endpoint = self
.cli
.inspect_ssh_endpoint(&run_result.container_id)
.map_err(|error| ProviderError::new(error.reason))?;
let lease = Self::lease_from_container(
&request.spec.shape,
&request.spec.logical_node_id,
run_result.container_id,
&endpoint,
);
Ok(CreateLeaseResult { lease, endpoint })
}
fn lookup_endpoint(
&mut self,
lease: &LeaseFacts,
) -> Result<Option<SshEndpoint>, ProviderError> {
self.cli
.inspect_ssh_endpoint(&lease.provider_contract_id)
.map_err(|error| ProviderError::new(error.reason))
}
fn destroy_lease(&mut self, handle: &DestroyHandle) -> Result<(), ProviderError> {
if handle.provider != ProviderKind::Docker {
return Err(ProviderError::new(
"docker provider received non-docker destroy handle",
));
}
self.cli
.remove_force(&handle.provider_contract_id)
.map_err(|error| ProviderError::new(error.reason))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BootstrapSshError {
pub reason: String,
}
impl BootstrapSshError {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
pub trait BootstrapSshClient {
fn connect(&mut self, endpoint: &SshEndpoint) -> Result<(), BootstrapSshError>;
fn probe_stdout(&mut self) -> Result<(), BootstrapSshError>;
fn read_bootstrap_logs(
&mut self,
stdout_sources: &[String],
stderr_sources: &[String],
) -> Result<Vec<(BootstrapLogStream, String)>, BootstrapSshError>;
fn run_verify_commands(&mut self, commands: &[String]) -> Result<(), BootstrapSshError>;
fn start_swactor(
&mut self,
command: &str,
join: &SwarmJoinSpec,
) -> Result<(), BootstrapSshError>;
fn close(&mut self);
}
#[derive(Clone, Debug)]
pub struct SshBootstrapSession<C> {
spec: BootstrapSessionSpec,
client: C,
stage: BootstrapStage,
next_seq: u64,
last_stdout_seq: Option<u64>,
last_stderr_seq: Option<u64>,
closed: bool,
}
impl<C> SshBootstrapSession<C> {
pub fn new(spec: BootstrapSessionSpec, client: C) -> Self {
Self {
spec,
client,
stage: BootstrapStage::Created,
next_seq: 1,
last_stdout_seq: None,
last_stderr_seq: None,
closed: false,
}
}
pub fn client(&self) -> &C {
&self.client
}
pub fn client_mut(&mut self) -> &mut C {
&mut self.client
}
pub fn stage(&self) -> BootstrapStage {
self.stage
}
pub fn is_closed(&self) -> bool {
self.closed
}
}
impl<C: BootstrapSshClient> SshBootstrapSession<C> {
pub fn start(&mut self, sink: &mut dyn BootstrapDatastreamSink) -> Vec<BootstrapSessionEvent> {
let mut events = Vec::new();
self.stage = BootstrapStage::SshConnecting;
if let Err(error) = self.client.connect(&self.spec.ssh) {
self.stage = BootstrapStage::SshTimeout;
return vec![BootstrapSessionEvent::Failed(format!(
"ssh connect: {}",
error.reason
))];
}
if let Err(error) = self.client.probe_stdout() {
self.stage = BootstrapStage::SshTimeout;
return vec![BootstrapSessionEvent::Failed(format!(
"ssh probe: {}",
error.reason
))];
}
self.stage = BootstrapStage::SshReady;
events.push(BootstrapSessionEvent::Observed(
BootstrapObservation::stage(BootstrapStage::SshReady),
));
self.stage = BootstrapStage::StdoutStreaming;
let records = match self.client.read_bootstrap_logs(
&self.spec.boot.stdout_sources,
&self.spec.boot.stderr_sources,
) {
Ok(records) => records,
Err(error) => {
self.stage = BootstrapStage::StreamError;
events.push(BootstrapSessionEvent::Failed(format!(
"bootstrap log stream: {}",
error.reason
)));
return events;
}
};
for (stream, line) in records {
let seq = self.next_seq;
self.next_seq += 1;
match stream {
BootstrapLogStream::Stdout => self.last_stdout_seq = Some(seq),
BootstrapLogStream::Stderr => self.last_stderr_seq = Some(seq),
}
sink.record(BootstrapLogRecord {
run_id: self.spec.run_id.clone(),
logical_node_id: self.spec.logical_node_id.clone(),
lease_id: self.spec.lease_id.clone(),
source: BootstrapLogSource::SshBootstrap,
stream,
seq,
timestamp: SystemTime::now(),
line,
});
}
events.push(BootstrapSessionEvent::Observed(BootstrapObservation {
stage: BootstrapStage::StdoutStreaming,
last_stdout_seq: self.last_stdout_seq,
last_stderr_seq: self.last_stderr_seq,
marker: None,
}));
self.stage = BootstrapStage::BootChecking;
events.push(BootstrapSessionEvent::Observed(
BootstrapObservation::stage(BootstrapStage::BootChecking),
));
if let Err(error) = self
.client
.run_verify_commands(&self.spec.boot.verify_commands)
{
self.stage = BootstrapStage::BootCheckFailed;
events.push(BootstrapSessionEvent::Failed(format!(
"boot check failed: {}",
error.reason
)));
return events;
}
self.stage = BootstrapStage::SwactorStarting;
events.push(BootstrapSessionEvent::Observed(
BootstrapObservation::stage(BootstrapStage::SwactorStarting),
));
if let Err(error) = self
.client
.start_swactor(&self.spec.boot.start_swactor_command, &self.spec.swarm_join)
{
self.stage = BootstrapStage::StartFailed;
events.push(BootstrapSessionEvent::Failed(format!(
"swactor start failed: {}",
error.reason
)));
return events;
}
self.stage = BootstrapStage::WaitingForSwactorJoin;
events.push(BootstrapSessionEvent::Observed(
BootstrapObservation::stage(BootstrapStage::WaitingForSwactorJoin),
));
events
}
pub fn convergence_observed(
&mut self,
_swactor_id: SwactorId,
sink: &mut dyn BootstrapDatastreamSink,
) -> Vec<BootstrapSessionEvent> {
self.stage = BootstrapStage::Converged;
sink.flush();
self.client.close();
self.closed = true;
self.stage = BootstrapStage::Closed;
vec![
BootstrapSessionEvent::Observed(BootstrapObservation::stage(BootstrapStage::Converged)),
BootstrapSessionEvent::Closed,
]
}
pub fn cancel(&mut self) -> Vec<BootstrapSessionEvent> {
self.stage = BootstrapStage::Cancelled;
self.client.close();
self.closed = true;
vec![BootstrapSessionEvent::Closed]
}
}
pub trait SshBootstrapClientFactory {
type Client: BootstrapSshClient;
fn client_for(&mut self, spec: &BootstrapSessionSpec) -> Self::Client;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DockerNodeProvisionError {
Provider(String),
Node(String),
EndpointUnavailable(ProviderLeaseId),
MissingBootstrap(LogicalNodeId),
Bootstrap(String),
UnexpectedCommand(String),
}
pub struct DockerNodeProvisioner<D, F, S>
where
D: DockerCli,
F: SshBootstrapClientFactory,
S: BootstrapDatastreamSink,
{
provider: DockerProvider<D>,
client_factory: F,
datastream: S,
manager: NodeManager,
bootstrap: Option<SshBootstrapSession<F::Client>>,
teardown_complete: bool,
}
impl<D, F, S> DockerNodeProvisioner<D, F, S>
where
D: DockerCli,
F: SshBootstrapClientFactory,
S: BootstrapDatastreamSink,
{
pub fn new(provider: DockerProvider<D>, client_factory: F, datastream: S) -> Self {
Self {
provider,
client_factory,
datastream,
manager: NodeManager::new(),
bootstrap: None,
teardown_complete: true,
}
}
pub fn provider(&self) -> &DockerProvider<D> {
&self.provider
}
pub fn provider_mut(&mut self) -> &mut DockerProvider<D> {
&mut self.provider
}
pub fn datastream(&self) -> &S {
&self.datastream
}
pub fn manager(&self) -> &NodeManager {
&self.manager
}
pub fn bootstrap(&self) -> Option<&SshBootstrapSession<F::Client>> {
self.bootstrap.as_ref()
}
pub fn record(&self) -> Option<&NodeRecord> {
self.manager.record()
}
pub fn is_ready(&self) -> bool {
self.manager.is_ready()
}
pub fn start(
&mut self,
spec: crate::node_provisioning::LogicalNodeSpec,
) -> Result<(), DockerNodeProvisionError> {
let commands = self
.manager
.handle(NodeManagerMsg::Start(spec))
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
self.teardown_complete = false;
if let Err(error) = self.process_commands(commands) {
let _ = self.stop();
return Err(error);
}
Ok(())
}
pub fn observe_swactor_join(
&mut self,
swactor_id: SwactorId,
) -> Result<(), DockerNodeProvisionError> {
let logical_node_id = self
.manager
.record()
.ok_or_else(|| DockerNodeProvisionError::Node("node manager not started".into()))?
.logical_node_id
.clone();
let commands = self
.manager
.handle(NodeManagerMsg::SwactorJoined {
logical_node_id,
swactor_id,
})
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
self.process_commands(commands)
}
pub fn stop(&mut self) -> Result<(), DockerNodeProvisionError> {
if self.manager.record().is_none() {
self.teardown_complete = true;
return Ok(());
}
let commands = self
.manager
.handle(NodeManagerMsg::Destroy)
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
self.process_stop_commands(commands)?;
self.teardown_complete = true;
Ok(())
}
fn process_commands(
&mut self,
commands: Vec<NodeManagerCommand>,
) -> Result<(), DockerNodeProvisionError> {
let mut pending = commands;
while let Some(command) = pending.pop() {
match command {
NodeManagerCommand::CreateLease(request) => {
let result = self
.provider
.create_lease(request)
.map_err(|error| DockerNodeProvisionError::Provider(error.reason))?;
let more = self
.manager
.handle(NodeManagerMsg::LeaseCreated(result))
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
pending.extend(more);
}
NodeManagerCommand::LookupEndpoint(lease) => {
let lease_id = lease.lease_id.clone();
let endpoint = match self
.provider
.lookup_endpoint(&lease)
.map_err(|error| DockerNodeProvisionError::Provider(error.reason))?
{
Some(endpoint) => endpoint,
None => {
let _ = self.manager.handle(NodeManagerMsg::EndpointFailed(
"docker ssh endpoint unavailable".into(),
));
return Err(DockerNodeProvisionError::EndpointUnavailable(lease_id));
}
};
let more = self
.manager
.handle(NodeManagerMsg::EndpointKnown(endpoint))
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
pending.extend(more);
}
NodeManagerCommand::StartBootstrap(spec) => {
let logical_node_id = spec.logical_node_id.clone();
let client = self.client_factory.client_for(&spec);
let mut session = SshBootstrapSession::new(spec, client);
let events = session.start(&mut self.datastream);
let bootstrap_error = events.iter().find_map(|event| match event {
BootstrapSessionEvent::Failed(reason) => Some(reason.clone()),
BootstrapSessionEvent::Observed(_) | BootstrapSessionEvent::Closed => None,
});
Self::feed_bootstrap_events(&mut self.manager, events)?;
self.bootstrap = Some(session);
if let Some(reason) = bootstrap_error {
return Err(DockerNodeProvisionError::Bootstrap(format!(
"{}: {reason}",
logical_node_id.0
)));
}
}
NodeManagerCommand::BootstrapConvergenceObserved { swactor_id, .. } => {
let logical_node_id = self
.manager
.record()
.ok_or_else(|| {
DockerNodeProvisionError::Node("node manager not started".into())
})?
.logical_node_id
.clone();
let bootstrap = self.bootstrap.as_mut().ok_or_else(|| {
DockerNodeProvisionError::MissingBootstrap(logical_node_id.clone())
})?;
let events = bootstrap.convergence_observed(swactor_id, &mut self.datastream);
Self::feed_bootstrap_events(&mut self.manager, events)?;
}
NodeManagerCommand::CancelBootstrap { .. }
| NodeManagerCommand::DestroyLease(_) => {
return Err(DockerNodeProvisionError::UnexpectedCommand(format!(
"lifecycle command {command:?} outside stop"
)));
}
}
}
Ok(())
}
fn process_stop_commands(
&mut self,
commands: Vec<NodeManagerCommand>,
) -> Result<(), DockerNodeProvisionError> {
for command in commands {
match command {
NodeManagerCommand::CancelBootstrap { .. } => {
if let Some(bootstrap) = self.bootstrap.as_mut() {
let _ = bootstrap.cancel();
}
}
NodeManagerCommand::DestroyLease(handle) => {
self.provider
.destroy_lease(&handle)
.map_err(|error| DockerNodeProvisionError::Provider(error.reason))?;
self.manager
.handle(NodeManagerMsg::LeaseDestroyed)
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
}
other => {
return Err(DockerNodeProvisionError::UnexpectedCommand(format!(
"unexpected stop command {other:?}"
)));
}
}
}
Ok(())
}
fn feed_bootstrap_events(
manager: &mut NodeManager,
events: Vec<BootstrapSessionEvent>,
) -> Result<(), DockerNodeProvisionError> {
for event in events {
match event {
BootstrapSessionEvent::Observed(observation) => {
manager
.handle(NodeManagerMsg::BootstrapObserved(observation))
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
}
BootstrapSessionEvent::Failed(reason) => {
manager
.handle(NodeManagerMsg::BootstrapFailed(reason))
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
}
BootstrapSessionEvent::Closed => {
manager
.handle(NodeManagerMsg::BootstrapClosed)
.map_err(|error| DockerNodeProvisionError::Node(error.reason))?;
}
}
}
Ok(())
}
}
impl<D, F, S> Drop for DockerNodeProvisioner<D, F, S>
where
D: DockerCli,
F: SshBootstrapClientFactory,
S: BootstrapDatastreamSink,
{
fn drop(&mut self) {
if !self.teardown_complete {
let _ = self.stop();
}
}
}