feat(local): support external URL clients in local deployer orchestration

This commit is contained in:
andrussal 2026-02-20 10:30:36 +01:00
parent b3ecc5acf7
commit 674702bd62
4 changed files with 113 additions and 10 deletions

View File

@ -12,8 +12,8 @@ use testing_framework_core::{
scenario::{
Application, CleanupGuard, Deployer, DeploymentPolicy, DynError, FeedHandle, FeedRuntime,
HttpReadinessRequirement, Metrics, NodeClients, NodeControlCapability, NodeControlHandle,
RetryPolicy, RunContext, Runner, Scenario, ScenarioError, build_source_orchestration_plan,
orchestrate_sources, spawn_feed,
RetryPolicy, RunContext, Runner, Scenario, ScenarioError, SourceOrchestrationPlan,
build_source_orchestration_plan, spawn_feed,
},
topology::DeploymentDescriptor,
};
@ -26,6 +26,7 @@ use tracing::{debug, info, warn};
use crate::{
env::{LocalDeployerEnv, Node, wait_local_http_readiness},
external::build_external_client,
keep_tempdir_from_env,
manual::ManualCluster,
node_control::{NodeManager, NodeManagerSeed},
@ -202,9 +203,7 @@ impl<E: LocalDeployerEnv> ProcessDeployer<E> {
let nodes = Self::spawn_nodes_for_scenario(scenario, self.membership_check).await?;
let node_clients = NodeClients::<E>::new(nodes.iter().map(|node| node.client()).collect());
// Source orchestration currently runs here after managed clients are prepared.
let node_clients = orchestrate_sources(&source_plan, node_clients)
.await
let node_clients = merge_source_clients_for_local::<E>(&source_plan, node_clients)
.map_err(|source| ProcessDeployerError::SourceOrchestration { source })?;
let runtime = run_context_for(
@ -241,10 +240,9 @@ impl<E: LocalDeployerEnv> ProcessDeployer<E> {
let nodes = Self::spawn_nodes_for_scenario(scenario, self.membership_check).await?;
let node_control = self.node_control_from(scenario, nodes);
// Source orchestration currently runs here after managed clients are prepared.
let node_clients = orchestrate_sources(&source_plan, node_control.node_clients())
.await
.map_err(|source| ProcessDeployerError::SourceOrchestration { source })?;
let node_clients =
merge_source_clients_for_local::<E>(&source_plan, node_control.node_clients())
.map_err(|source| ProcessDeployerError::SourceOrchestration { source })?;
let runtime = run_context_for(
scenario.deployment().clone(),
node_clients,
@ -314,6 +312,18 @@ impl<E: LocalDeployerEnv> ProcessDeployer<E> {
}
}
fn merge_source_clients_for_local<E: LocalDeployerEnv>(
source_plan: &SourceOrchestrationPlan,
node_clients: NodeClients<E>,
) -> Result<NodeClients<E>, DynError> {
for source in source_plan.external_sources() {
let client =
E::external_node_client(source).or_else(|_| build_external_client::<E>(source))?;
node_clients.add_node(client);
}
Ok(node_clients)
}
fn build_retry_execution_config(
deployment_policy: DeploymentPolicy,
membership_check: bool,

View File

@ -0,0 +1,90 @@
use std::net::ToSocketAddrs;
use testing_framework_core::scenario::{DynError, ExternalNodeSource};
use crate::{LocalDeployerEnv, NodeEndpoints};
#[derive(Debug, thiserror::Error)]
pub enum ExternalClientBuildError {
#[error("external source '{label}' endpoint is empty")]
EmptyEndpoint { label: String },
#[error("external source '{label}' endpoint '{endpoint}' has unsupported scheme")]
UnsupportedScheme { label: String, endpoint: String },
#[error("external source '{label}' endpoint '{endpoint}' is missing host")]
MissingHost { label: String, endpoint: String },
#[error("external source '{label}' endpoint '{endpoint}' is missing port")]
MissingPort { label: String, endpoint: String },
#[error("external source '{label}' endpoint '{endpoint}' failed to resolve: {source}")]
Resolve {
label: String,
endpoint: String,
#[source]
source: std::io::Error,
},
#[error("external source '{label}' endpoint '{endpoint}' resolved to no socket addresses")]
NoResolvedAddress { label: String, endpoint: String },
}
pub fn build_external_client<E: LocalDeployerEnv>(
source: &ExternalNodeSource,
) -> Result<E::NodeClient, DynError> {
let api = resolve_api_socket(source)?;
let mut endpoints = NodeEndpoints::default();
endpoints.api = api;
Ok(E::node_client(&endpoints))
}
fn resolve_api_socket(source: &ExternalNodeSource) -> Result<std::net::SocketAddr, DynError> {
let source_label = source.label.clone();
let endpoint = source.endpoint.trim();
if endpoint.is_empty() {
return Err(ExternalClientBuildError::EmptyEndpoint {
label: source_label,
}
.into());
}
let without_scheme = endpoint
.strip_prefix("http://")
.or_else(|| endpoint.strip_prefix("https://"))
.ok_or_else(|| ExternalClientBuildError::UnsupportedScheme {
label: source_label.clone(),
endpoint: endpoint.to_owned(),
})?;
let authority = without_scheme.trim_end_matches('/');
let (host, port) =
split_host_port(authority).ok_or_else(|| ExternalClientBuildError::MissingPort {
label: source_label.clone(),
endpoint: endpoint.to_owned(),
})?;
if host.is_empty() {
return Err(ExternalClientBuildError::MissingHost {
label: source_label.clone(),
endpoint: endpoint.to_owned(),
}
.into());
}
let resolved = (host, port)
.to_socket_addrs()
.map_err(|source| ExternalClientBuildError::Resolve {
label: source_label.clone(),
endpoint: endpoint.to_owned(),
source,
})?
.next()
.ok_or_else(|| ExternalClientBuildError::NoResolvedAddress {
label: source_label,
endpoint: endpoint.to_owned(),
})?;
Ok(resolved)
}
fn split_host_port(authority: &str) -> Option<(&str, u16)> {
let (host, port) = authority.rsplit_once(':')?;
let port = port.parse::<u16>().ok()?;
Some((host.trim_matches(['[', ']']), port))
}

View File

@ -1,6 +1,7 @@
pub mod binary;
mod deployer;
pub mod env;
mod external;
mod manual;
mod node_control;
pub mod process;

View File

@ -9,6 +9,7 @@ use thiserror::Error;
use crate::{
env::LocalDeployerEnv,
external::build_external_client,
keep_tempdir_from_env,
node_control::{NodeManager, NodeManagerError, NodeManagerSeed},
};
@ -93,7 +94,8 @@ impl<E: LocalDeployerEnv> ManualCluster<E> {
) -> Result<(), DynError> {
let node_clients = self.nodes.node_clients();
for source in external_sources {
let client = E::external_node_client(&source)?;
let client = E::external_node_client(&source)
.or_else(|_| build_external_client::<E>(&source))?;
node_clients.add_node(client);
}