mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-05-17 07:09:28 +00:00
Define attached runner readiness contract
This commit is contained in:
parent
fd547aa119
commit
d4c5b9fe99
@ -33,6 +33,11 @@ async fn compose_attach_mode_queries_node_api_opt_in() -> Result<()> {
|
|||||||
Err(error) => return Err(Error::new(error)),
|
Err(error) => return Err(Error::new(error)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
attached_runner
|
||||||
|
.wait_network_ready()
|
||||||
|
.await
|
||||||
|
.map_err(|err| anyhow!("compose attached runner readiness failed: {err}"))?;
|
||||||
|
|
||||||
if attached_runner.context().node_clients().is_empty() {
|
if attached_runner.context().node_clients().is_empty() {
|
||||||
return Err(anyhow!("compose attach resolved no node clients"));
|
return Err(anyhow!("compose attach resolved no node clients"));
|
||||||
}
|
}
|
||||||
@ -89,12 +94,7 @@ async fn compose_attach_mode_restart_node_opt_in() -> Result<()> {
|
|||||||
.node_control()
|
.node_control()
|
||||||
.ok_or_else(|| anyhow!("attached compose node control is unavailable"))?;
|
.ok_or_else(|| anyhow!("attached compose node control is unavailable"))?;
|
||||||
|
|
||||||
let services: Vec<String> = attached_runner
|
let services = discover_attached_services(&project_name).await?;
|
||||||
.context()
|
|
||||||
.borrowed_nodes()
|
|
||||||
.into_iter()
|
|
||||||
.map(|node| node.identity)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if services.is_empty() {
|
if services.is_empty() {
|
||||||
return Err(anyhow!("attached compose runner discovered no services"));
|
return Err(anyhow!("attached compose runner discovered no services"));
|
||||||
@ -166,6 +166,36 @@ async fn service_started_at(project: &str, service: &str) -> Result<String> {
|
|||||||
Ok(started_at)
|
Ok(started_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn discover_attached_services(project: &str) -> Result<Vec<String>> {
|
||||||
|
let output = run_docker(&[
|
||||||
|
"ps",
|
||||||
|
"--filter",
|
||||||
|
&format!("label=com.docker.compose.project={project}"),
|
||||||
|
"--filter",
|
||||||
|
"label=testing-framework.node=true",
|
||||||
|
"--format",
|
||||||
|
"{{.Label \"com.docker.compose.service\"}}",
|
||||||
|
])
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut services: Vec<String> = output
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect();
|
||||||
|
services.sort();
|
||||||
|
services.dedup();
|
||||||
|
|
||||||
|
if services.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"attached compose runner discovered no labeled services"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(services)
|
||||||
|
}
|
||||||
|
|
||||||
async fn wait_until_service_restarted(
|
async fn wait_until_service_restarted(
|
||||||
project: &str,
|
project: &str,
|
||||||
service: &str,
|
service: &str,
|
||||||
|
|||||||
@ -254,6 +254,7 @@ fn build_compose_node_descriptor(
|
|||||||
base_volumes(),
|
base_volumes(),
|
||||||
default_extra_hosts(),
|
default_extra_hosts(),
|
||||||
ports,
|
ports,
|
||||||
|
api_port,
|
||||||
environment,
|
environment,
|
||||||
platform,
|
platform,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,3 +33,11 @@ pub trait NodeControlHandle<E: Application>: Send + Sync {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deployer-agnostic wait surface for cluster readiness checks.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ClusterWaitHandle<E: Application>: Send + Sync {
|
||||||
|
async fn wait_network_ready(&self) -> Result<(), DynError> {
|
||||||
|
Err("wait_network_ready not supported by this deployer".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ pub use capabilities::{
|
|||||||
StartNodeOptions, StartedNode,
|
StartNodeOptions, StartedNode,
|
||||||
};
|
};
|
||||||
pub use common_builder_ext::CoreBuilderExt;
|
pub use common_builder_ext::CoreBuilderExt;
|
||||||
pub use control::NodeControlHandle;
|
pub use control::{ClusterWaitHandle, NodeControlHandle};
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use definition::{
|
pub use definition::{
|
||||||
Builder as CoreBuilder, // internal adapter-facing core builder
|
Builder as CoreBuilder, // internal adapter-facing core builder
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use super::{metrics::Metrics, node_clients::ClusterClient};
|
use super::{metrics::Metrics, node_clients::ClusterClient};
|
||||||
use crate::scenario::{Application, BorrowedNode, ManagedNode, NodeClients, NodeControlHandle};
|
use crate::scenario::{
|
||||||
|
Application, BorrowedNode, ClusterWaitHandle, DynError, ManagedNode, NodeClients,
|
||||||
|
NodeControlHandle,
|
||||||
|
};
|
||||||
|
|
||||||
/// Shared runtime context available to workloads and expectations.
|
/// Shared runtime context available to workloads and expectations.
|
||||||
pub struct RunContext<E: Application> {
|
pub struct RunContext<E: Application> {
|
||||||
@ -12,6 +15,7 @@ pub struct RunContext<E: Application> {
|
|||||||
telemetry: Metrics,
|
telemetry: Metrics,
|
||||||
feed: <E::FeedRuntime as super::FeedRuntime>::Feed,
|
feed: <E::FeedRuntime as super::FeedRuntime>::Feed,
|
||||||
node_control: Option<Arc<dyn NodeControlHandle<E>>>,
|
node_control: Option<Arc<dyn NodeControlHandle<E>>>,
|
||||||
|
cluster_wait: Option<Arc<dyn ClusterWaitHandle<E>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Application> RunContext<E> {
|
impl<E: Application> RunContext<E> {
|
||||||
@ -36,9 +40,16 @@ impl<E: Application> RunContext<E> {
|
|||||||
telemetry,
|
telemetry,
|
||||||
feed,
|
feed,
|
||||||
node_control,
|
node_control,
|
||||||
|
cluster_wait: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_cluster_wait(mut self, cluster_wait: Arc<dyn ClusterWaitHandle<E>>) -> Self {
|
||||||
|
self.cluster_wait = Some(cluster_wait);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn descriptors(&self) -> &E::Deployment {
|
pub fn descriptors(&self) -> &E::Deployment {
|
||||||
&self.descriptors
|
&self.descriptors
|
||||||
@ -104,11 +115,29 @@ impl<E: Application> RunContext<E> {
|
|||||||
self.node_control.clone()
|
self.node_control.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn cluster_wait(&self) -> Option<Arc<dyn ClusterWaitHandle<E>>> {
|
||||||
|
self.cluster_wait.clone()
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn controls_nodes(&self) -> bool {
|
pub const fn controls_nodes(&self) -> bool {
|
||||||
self.node_control.is_some()
|
self.node_control.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn can_wait_network_ready(&self) -> bool {
|
||||||
|
self.cluster_wait.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_network_ready(&self) -> Result<(), DynError> {
|
||||||
|
let Some(cluster_wait) = self.cluster_wait() else {
|
||||||
|
return Err("wait_network_ready is not available for this runner".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
cluster_wait.wait_network_ready().await
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn cluster_client(&self) -> ClusterClient<'_, E> {
|
pub const fn cluster_client(&self) -> ClusterClient<'_, E> {
|
||||||
self.node_clients.cluster_client()
|
self.node_clients.cluster_client()
|
||||||
@ -156,6 +185,10 @@ impl<E: Application> RunHandle<E> {
|
|||||||
pub fn context(&self) -> &RunContext<E> {
|
pub fn context(&self) -> &RunContext<E> {
|
||||||
&self.run_context
|
&self.run_context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn wait_network_ready(&self) -> Result<(), DynError> {
|
||||||
|
self.run_context.wait_network_ready().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derived metrics about the current run timing.
|
/// Derived metrics about the current run timing.
|
||||||
|
|||||||
@ -43,6 +43,10 @@ impl<E: Application> Runner<E> {
|
|||||||
Arc::clone(&self.context)
|
Arc::clone(&self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn wait_network_ready(&self) -> Result<(), DynError> {
|
||||||
|
self.context.wait_network_ready().await
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn cleanup(&mut self) {
|
pub(crate) fn cleanup(&mut self) {
|
||||||
if let Some(guard) = self.cleanup_guard.take() {
|
if let Some(guard) = self.cleanup_guard.take() {
|
||||||
guard.cleanup();
|
guard.cleanup();
|
||||||
|
|||||||
@ -20,6 +20,7 @@ services:
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
labels:
|
labels:
|
||||||
testing-framework.node: "true"
|
testing-framework.node: "true"
|
||||||
|
testing-framework.api-container-port: "{{ node.api_container_port }}"
|
||||||
environment:
|
environment:
|
||||||
{% for env in node.environment %}
|
{% for env in node.environment %}
|
||||||
{{ env.key }}: "{{ env.value }}"
|
{{ env.key }}: "{{ env.value }}"
|
||||||
|
|||||||
@ -2,13 +2,15 @@ use std::marker::PhantomData;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use testing_framework_core::scenario::{
|
use testing_framework_core::scenario::{
|
||||||
AttachProvider, AttachProviderError, AttachSource, AttachedNode, DynError, ExternalNodeSource,
|
AttachProvider, AttachProviderError, AttachSource, AttachedNode, ClusterWaitHandle, DynError,
|
||||||
|
ExternalNodeSource, HttpReadinessRequirement, wait_http_readiness,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
docker::attached::{
|
docker::attached::{
|
||||||
discover_attachable_services, discover_service_container_id, inspect_mapped_tcp_ports,
|
discover_attachable_services, discover_service_container_id,
|
||||||
|
inspect_api_container_port_label, inspect_mapped_tcp_ports,
|
||||||
},
|
},
|
||||||
env::ComposeDeployEnv,
|
env::ComposeDeployEnv,
|
||||||
};
|
};
|
||||||
@ -18,6 +20,12 @@ pub(super) struct ComposeAttachProvider<E: ComposeDeployEnv> {
|
|||||||
_env: PhantomData<E>,
|
_env: PhantomData<E>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) struct ComposeAttachedClusterWait<E: ComposeDeployEnv> {
|
||||||
|
host: String,
|
||||||
|
source: AttachSource,
|
||||||
|
_env: PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
enum ComposeAttachDiscoveryError {
|
enum ComposeAttachDiscoveryError {
|
||||||
#[error("compose attach source requires an explicit project name")]
|
#[error("compose attach source requires an explicit project name")]
|
||||||
@ -33,6 +41,16 @@ impl<E: ComposeDeployEnv> ComposeAttachProvider<E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<E: ComposeDeployEnv> ComposeAttachedClusterWait<E> {
|
||||||
|
pub(super) fn new(host: String, source: AttachSource) -> Self {
|
||||||
|
Self {
|
||||||
|
host,
|
||||||
|
source,
|
||||||
|
_env: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<E: ComposeDeployEnv> AttachProvider<E> for ComposeAttachProvider<E> {
|
impl<E: ComposeDeployEnv> AttachProvider<E> for ComposeAttachProvider<E> {
|
||||||
async fn discover(
|
async fn discover(
|
||||||
@ -87,7 +105,10 @@ fn to_discovery_error(source: DynError) -> AttachProviderError {
|
|||||||
AttachProviderError::Discovery { source }
|
AttachProviderError::Discovery { source }
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn resolve_services(project: &str, requested: &[String]) -> Result<Vec<String>, DynError> {
|
pub(super) async fn resolve_services(
|
||||||
|
project: &str,
|
||||||
|
requested: &[String],
|
||||||
|
) -> Result<Vec<String>, DynError> {
|
||||||
if !requested.is_empty() {
|
if !requested.is_empty() {
|
||||||
return Ok(requested.to_owned());
|
return Ok(requested.to_owned());
|
||||||
}
|
}
|
||||||
@ -95,34 +116,61 @@ async fn resolve_services(project: &str, requested: &[String]) -> Result<Vec<Str
|
|||||||
discover_attachable_services(project).await
|
discover_attachable_services(project).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn discover_api_port(container_id: &str) -> Result<u16, DynError> {
|
pub(super) async fn discover_api_port(container_id: &str) -> Result<u16, DynError> {
|
||||||
let mapped_ports = inspect_mapped_tcp_ports(container_id).await?;
|
let mapped_ports = inspect_mapped_tcp_ports(container_id).await?;
|
||||||
match mapped_ports.as_slice() {
|
let api_container_port = inspect_api_container_port_label(container_id).await?;
|
||||||
[] => Err(format!(
|
let Some(api_port) = mapped_ports
|
||||||
"no mapped tcp ports discovered for attached compose service container '{container_id}'"
|
.iter()
|
||||||
)
|
.find(|port| port.container_port == api_container_port)
|
||||||
.into()),
|
.map(|port| port.host_port)
|
||||||
[port] => Ok(port.host_port),
|
else {
|
||||||
_ => {
|
let mapped_ports = mapped_ports
|
||||||
let mapped_ports = mapped_ports
|
.iter()
|
||||||
.iter()
|
.map(|port| format!("{}->{}", port.container_port, port.host_port))
|
||||||
.map(|port| format!("{}->{}", port.container_port, port.host_port))
|
.collect::<Vec<_>>()
|
||||||
.collect::<Vec<_>>()
|
.join(", ");
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
Err(format!(
|
return Err(format!(
|
||||||
"attached compose service container '{container_id}' has multiple mapped tcp ports ({mapped_ports}); provide a single exposed API port"
|
"attached compose service container '{container_id}' does not expose labeled API container port {api_container_port}; mapped tcp ports: {mapped_ports}"
|
||||||
)
|
)
|
||||||
.into())
|
.into());
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
Ok(api_port)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_service_endpoint(host: &str, port: u16) -> Result<Url, DynError> {
|
pub(super) fn build_service_endpoint(host: &str, port: u16) -> Result<Url, DynError> {
|
||||||
let endpoint = Url::parse(&format!("http://{host}:{port}/"))?;
|
let endpoint = Url::parse(&format!("http://{host}:{port}/"))?;
|
||||||
Ok(endpoint)
|
Ok(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E: ComposeDeployEnv> ClusterWaitHandle<E> for ComposeAttachedClusterWait<E> {
|
||||||
|
async fn wait_network_ready(&self) -> Result<(), DynError> {
|
||||||
|
let AttachSource::Compose { project, services } = &self.source else {
|
||||||
|
return Err("compose cluster wait requires a compose attach source".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = project
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(ComposeAttachDiscoveryError::MissingProjectName)?;
|
||||||
|
let services = resolve_services(project, services).await?;
|
||||||
|
|
||||||
|
let mut endpoints = Vec::with_capacity(services.len());
|
||||||
|
for service in &services {
|
||||||
|
let container_id = discover_service_container_id(project, service).await?;
|
||||||
|
let api_port = discover_api_port(&container_id).await?;
|
||||||
|
let mut endpoint = build_service_endpoint(&self.host, api_port)?;
|
||||||
|
endpoint.set_path(E::readiness_path());
|
||||||
|
endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_http_readiness(&endpoints, HttpReadinessRequirement::AllNodesReady).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::build_service_endpoint;
|
use super::build_service_endpoint;
|
||||||
|
|||||||
@ -3,11 +3,12 @@ use std::{env, sync::Arc, time::Duration};
|
|||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use testing_framework_core::{
|
use testing_framework_core::{
|
||||||
scenario::{
|
scenario::{
|
||||||
ApplicationExternalProvider, AttachSource, CleanupGuard, DeploymentPolicy, FeedHandle,
|
ApplicationExternalProvider, AttachSource, CleanupGuard, ClusterWaitHandle,
|
||||||
FeedRuntime, HttpReadinessRequirement, Metrics, NodeClients, NodeControlHandle,
|
DeploymentPolicy, FeedHandle, FeedRuntime, HttpReadinessRequirement, Metrics, NodeClients,
|
||||||
ObservabilityCapabilityProvider, ObservabilityInputs, RequiresNodeControl, RunContext,
|
NodeControlHandle, ObservabilityCapabilityProvider, ObservabilityInputs,
|
||||||
Runner, Scenario, ScenarioSources, SourceOrchestrationPlan, SourceProviders,
|
RequiresNodeControl, RunContext, Runner, Scenario, ScenarioSources,
|
||||||
StaticManagedProvider, build_source_orchestration_plan, orchestrate_sources_with_providers,
|
SourceOrchestrationPlan, SourceProviders, StaticManagedProvider,
|
||||||
|
build_source_orchestration_plan, orchestrate_sources_with_providers,
|
||||||
},
|
},
|
||||||
topology::DeploymentDescriptor,
|
topology::DeploymentDescriptor,
|
||||||
};
|
};
|
||||||
@ -15,7 +16,7 @@ use tracing::info;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ComposeDeployer, ComposeDeploymentMetadata,
|
ComposeDeployer, ComposeDeploymentMetadata,
|
||||||
attach_provider::ComposeAttachProvider,
|
attach_provider::{ComposeAttachProvider, ComposeAttachedClusterWait},
|
||||||
clients::ClientBuilder,
|
clients::ClientBuilder,
|
||||||
make_cleanup_guard,
|
make_cleanup_guard,
|
||||||
ports::PortManager,
|
ports::PortManager,
|
||||||
@ -117,6 +118,7 @@ impl<E: ComposeDeployEnv> DeploymentOrchestrator<E> {
|
|||||||
deployed,
|
deployed,
|
||||||
observability,
|
observability,
|
||||||
readiness_enabled,
|
readiness_enabled,
|
||||||
|
project_name.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -154,6 +156,7 @@ impl<E: ComposeDeployEnv> DeploymentOrchestrator<E> {
|
|||||||
self.ensure_non_empty_node_clients(&node_clients)?;
|
self.ensure_non_empty_node_clients(&node_clients)?;
|
||||||
|
|
||||||
let node_control = self.attached_node_control::<Caps>(scenario)?;
|
let node_control = self.attached_node_control::<Caps>(scenario)?;
|
||||||
|
let cluster_wait = self.attached_cluster_wait(scenario)?;
|
||||||
let (feed, feed_task) = spawn_block_feed_with_retry::<E>(&node_clients).await?;
|
let (feed, feed_task) = spawn_block_feed_with_retry::<E>(&node_clients).await?;
|
||||||
let context = RunContext::new(
|
let context = RunContext::new(
|
||||||
scenario.deployment().clone(),
|
scenario.deployment().clone(),
|
||||||
@ -163,7 +166,8 @@ impl<E: ComposeDeployEnv> DeploymentOrchestrator<E> {
|
|||||||
observability.telemetry_handle()?,
|
observability.telemetry_handle()?,
|
||||||
feed,
|
feed,
|
||||||
node_control,
|
node_control,
|
||||||
);
|
)
|
||||||
|
.with_cluster_wait(cluster_wait);
|
||||||
|
|
||||||
let cleanup_guard: Box<dyn CleanupGuard> = Box::new(feed_task);
|
let cleanup_guard: Box<dyn CleanupGuard> = Box::new(feed_task);
|
||||||
Ok(Runner::new(context, Some(cleanup_guard)))
|
Ok(Runner::new(context, Some(cleanup_guard)))
|
||||||
@ -237,6 +241,25 @@ impl<E: ComposeDeployEnv> DeploymentOrchestrator<E> {
|
|||||||
}) as Arc<dyn NodeControlHandle<E>>))
|
}) as Arc<dyn NodeControlHandle<E>>))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn attached_cluster_wait<Caps>(
|
||||||
|
&self,
|
||||||
|
scenario: &Scenario<E, Caps>,
|
||||||
|
) -> Result<Arc<dyn ClusterWaitHandle<E>>, ComposeRunnerError>
|
||||||
|
where
|
||||||
|
Caps: Send + Sync,
|
||||||
|
{
|
||||||
|
let ScenarioSources::Attached { attach, .. } = scenario.sources() else {
|
||||||
|
return Err(ComposeRunnerError::InternalInvariant {
|
||||||
|
message: "compose attached cluster wait requested outside attached source mode",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Arc::new(ComposeAttachedClusterWait::<E>::new(
|
||||||
|
compose_runner_host(),
|
||||||
|
attach.clone(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
async fn build_runner<Caps>(
|
async fn build_runner<Caps>(
|
||||||
&self,
|
&self,
|
||||||
scenario: &Scenario<E, Caps>,
|
scenario: &Scenario<E, Caps>,
|
||||||
@ -244,6 +267,7 @@ impl<E: ComposeDeployEnv> DeploymentOrchestrator<E> {
|
|||||||
deployed: DeployedNodes<E>,
|
deployed: DeployedNodes<E>,
|
||||||
observability: ObservabilityInputs,
|
observability: ObservabilityInputs,
|
||||||
readiness_enabled: bool,
|
readiness_enabled: bool,
|
||||||
|
project_name: String,
|
||||||
) -> Result<Runner<E>, ComposeRunnerError>
|
) -> Result<Runner<E>, ComposeRunnerError>
|
||||||
where
|
where
|
||||||
Caps: RequiresNodeControl + ObservabilityCapabilityProvider + Send + Sync,
|
Caps: RequiresNodeControl + ObservabilityCapabilityProvider + Send + Sync,
|
||||||
@ -263,6 +287,10 @@ impl<E: ComposeDeployEnv> DeploymentOrchestrator<E> {
|
|||||||
telemetry,
|
telemetry,
|
||||||
environment: &mut prepared.environment,
|
environment: &mut prepared.environment,
|
||||||
node_control,
|
node_control,
|
||||||
|
cluster_wait: Arc::new(ComposeAttachedClusterWait::<E>::new(
|
||||||
|
compose_runner_host(),
|
||||||
|
AttachSource::compose(Vec::new()).with_project(project_name),
|
||||||
|
)),
|
||||||
};
|
};
|
||||||
let runtime = build_compose_runtime::<E>(input).await?;
|
let runtime = build_compose_runtime::<E>(input).await?;
|
||||||
let cleanup_guard =
|
let cleanup_guard =
|
||||||
@ -376,6 +404,7 @@ struct RuntimeBuildInput<'a, E: ComposeDeployEnv> {
|
|||||||
telemetry: Metrics,
|
telemetry: Metrics,
|
||||||
environment: &'a mut StackEnvironment,
|
environment: &'a mut StackEnvironment,
|
||||||
node_control: Option<Arc<dyn NodeControlHandle<E>>>,
|
node_control: Option<Arc<dyn NodeControlHandle<E>>>,
|
||||||
|
cluster_wait: Arc<dyn ClusterWaitHandle<E>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_compose_runtime<E: ComposeDeployEnv>(
|
async fn build_compose_runtime<E: ComposeDeployEnv>(
|
||||||
@ -400,6 +429,7 @@ async fn build_compose_runtime<E: ComposeDeployEnv>(
|
|||||||
input.telemetry,
|
input.telemetry,
|
||||||
feed,
|
feed,
|
||||||
input.node_control,
|
input.node_control,
|
||||||
|
input.cluster_wait,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(ComposeRuntime { context, feed_task })
|
Ok(ComposeRuntime { context, feed_task })
|
||||||
@ -443,6 +473,7 @@ fn build_run_context<E: ComposeDeployEnv>(
|
|||||||
telemetry: Metrics,
|
telemetry: Metrics,
|
||||||
feed: <E::FeedRuntime as FeedRuntime>::Feed,
|
feed: <E::FeedRuntime as FeedRuntime>::Feed,
|
||||||
node_control: Option<Arc<dyn NodeControlHandle<E>>>,
|
node_control: Option<Arc<dyn NodeControlHandle<E>>>,
|
||||||
|
cluster_wait: Arc<dyn ClusterWaitHandle<E>>,
|
||||||
) -> RunContext<E> {
|
) -> RunContext<E> {
|
||||||
RunContext::new(
|
RunContext::new(
|
||||||
descriptors,
|
descriptors,
|
||||||
@ -453,6 +484,7 @@ fn build_run_context<E: ComposeDeployEnv>(
|
|||||||
feed,
|
feed,
|
||||||
node_control,
|
node_control,
|
||||||
)
|
)
|
||||||
|
.with_cluster_wait(cluster_wait)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_observability_inputs<E, Caps>(
|
fn resolve_observability_inputs<E, Caps>(
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pub struct NodeDescriptor {
|
|||||||
volumes: Vec<String>,
|
volumes: Vec<String>,
|
||||||
extra_hosts: Vec<String>,
|
extra_hosts: Vec<String>,
|
||||||
ports: Vec<String>,
|
ports: Vec<String>,
|
||||||
|
api_container_port: u16,
|
||||||
environment: Vec<EnvEntry>,
|
environment: Vec<EnvEntry>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
platform: Option<String>,
|
platform: Option<String>,
|
||||||
@ -49,6 +50,7 @@ impl NodeDescriptor {
|
|||||||
volumes: Vec<String>,
|
volumes: Vec<String>,
|
||||||
extra_hosts: Vec<String>,
|
extra_hosts: Vec<String>,
|
||||||
ports: Vec<String>,
|
ports: Vec<String>,
|
||||||
|
api_container_port: u16,
|
||||||
environment: Vec<EnvEntry>,
|
environment: Vec<EnvEntry>,
|
||||||
platform: Option<String>,
|
platform: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@ -59,6 +61,7 @@ impl NodeDescriptor {
|
|||||||
volumes,
|
volumes,
|
||||||
extra_hosts,
|
extra_hosts,
|
||||||
ports,
|
ports,
|
||||||
|
api_container_port,
|
||||||
environment,
|
environment,
|
||||||
platform,
|
platform,
|
||||||
}
|
}
|
||||||
@ -77,4 +80,9 @@ impl NodeDescriptor {
|
|||||||
pub fn environment(&self) -> &[EnvEntry] {
|
pub fn environment(&self) -> &[EnvEntry] {
|
||||||
&self.environment
|
&self.environment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn api_container_port(&self) -> u16 {
|
||||||
|
self.api_container_port
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use tokio::process::Command;
|
|||||||
|
|
||||||
pub const ATTACHABLE_NODE_LABEL_KEY: &str = "testing-framework.node";
|
pub const ATTACHABLE_NODE_LABEL_KEY: &str = "testing-framework.node";
|
||||||
pub const ATTACHABLE_NODE_LABEL_VALUE: &str = "true";
|
pub const ATTACHABLE_NODE_LABEL_VALUE: &str = "true";
|
||||||
|
pub const API_CONTAINER_PORT_LABEL_KEY: &str = "testing-framework.api-container-port";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct MappedTcpPort {
|
pub struct MappedTcpPort {
|
||||||
@ -75,6 +76,18 @@ pub async fn inspect_mapped_tcp_ports(container_id: &str) -> Result<Vec<MappedTc
|
|||||||
parse_mapped_tcp_ports(&stdout)
|
parse_mapped_tcp_ports(&stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn inspect_api_container_port_label(container_id: &str) -> Result<u16, DynError> {
|
||||||
|
let stdout = run_docker_capture([
|
||||||
|
"inspect",
|
||||||
|
"--format",
|
||||||
|
"{{index .Config.Labels \"testing-framework.api-container-port\"}}",
|
||||||
|
container_id,
|
||||||
|
])
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
parse_api_container_port_label(&stdout)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_mapped_tcp_ports(raw: &str) -> Result<Vec<MappedTcpPort>, DynError> {
|
pub fn parse_mapped_tcp_ports(raw: &str) -> Result<Vec<MappedTcpPort>, DynError> {
|
||||||
let ports_value: Value = serde_json::from_str(raw.trim())?;
|
let ports_value: Value = serde_json::from_str(raw.trim())?;
|
||||||
let ports_object = ports_value
|
let ports_object = ports_value
|
||||||
@ -194,3 +207,21 @@ fn parse_host_port_binding(binding: &Value) -> Option<u16> {
|
|||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_api_container_port_label(raw: &str) -> Result<u16, DynError> {
|
||||||
|
let value = raw.trim();
|
||||||
|
|
||||||
|
if value.is_empty() || value == "<no value>" {
|
||||||
|
return Err(format!(
|
||||||
|
"attached compose container is missing required label '{API_CONTAINER_PORT_LABEL_KEY}'"
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
value.parse::<u16>().map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"attached compose container label '{API_CONTAINER_PORT_LABEL_KEY}' has invalid value '{value}': {err}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user