diff --git a/testing-framework/deployers/compose/src/env.rs b/testing-framework/deployers/compose/src/env.rs index 81af8cd..d3fe31f 100644 --- a/testing-framework/deployers/compose/src/env.rs +++ b/testing-framework/deployers/compose/src/env.rs @@ -35,32 +35,46 @@ use crate::{ /// Handle returned by a compose config server (cfgsync or equivalent). pub trait ConfigServerHandle: Send + Sync { + /// Stops the config server and releases any local resources it owns. fn shutdown(&mut self); + /// Marks the config server as preserved so runner cleanup should not remove + /// it. fn mark_preserved(&mut self); + /// Returns the backing container name when the handle is container-backed. fn container_name(&self) -> Option<&str> { None } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Selects how compose nodes receive config updates. pub enum ComposeConfigServerMode { + /// Do not start any config server. Disabled, + /// Start a Docker-backed config server sidecar. Docker, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Readiness probe used for compose nodes. pub enum ComposeReadinessProbe { + /// Probe a concrete HTTP path on each node. Http { path: &'static str }, + /// Probe raw TCP reachability on the testing port. Tcp, } #[derive(Clone, Copy)] +/// Naming strategy for static compose node config files. pub enum ComposeNodeConfigFileName { + /// Use `node-{index}.`. FixedExtension(&'static str), + /// Build the file name directly from the node index. Custom(fn(usize) -> String), } impl ComposeNodeConfigFileName { + /// Resolves the config file name for one node index. #[must_use] pub fn resolve(&self, index: usize) -> String { match self { @@ -73,6 +87,7 @@ impl ComposeNodeConfigFileName { /// Advanced compose deployer integration. #[async_trait] pub trait ComposeDeployEnv: Application + Sized { + /// Prepares compose workspace files before the stack is started. fn prepare_compose_configs( _path: &Path, _topology: &::Deployment, @@ -82,10 +97,13 @@ pub trait ComposeDeployEnv: Application + Sized { Ok(()) } + /// Returns the static config file name used for one node. fn static_node_config_file_name(index: usize) -> String { format!("node-{index}.yaml") } + /// Returns the runtime spec for one loopback node when using the standard + /// one-container-per-node shape. fn loopback_node_runtime_spec( topology: &::Deployment, index: usize, @@ -96,6 +114,8 @@ pub trait ComposeDeployEnv: Application + Sized { Ok(None) } + /// Returns the binary+config node spec when the app follows the standard + /// binary compose path. fn binary_config_node_spec( _topology: &::Deployment, _index: usize, @@ -103,6 +123,7 @@ pub trait ComposeDeployEnv: Application + Sized { Ok(None) } + /// Builds the full compose descriptor for the deployment. fn compose_descriptor( topology: &::Deployment, _cfgsync_port: u16, @@ -116,6 +137,7 @@ pub trait ComposeDeployEnv: Application + Sized { Ok(ComposeDescriptor::new(nodes)) } + /// Returns the container ports exposed by each compose node. fn node_container_ports( topology: &::Deployment, ) -> Result, DynError> { @@ -129,12 +151,14 @@ pub trait ComposeDeployEnv: Application + Sized { .collect()) } + /// Returns the hostnames advertised to cfgsync-rendered node configs. fn cfgsync_hostnames(topology: &::Deployment) -> Vec { (0..topology.node_count()) .map(crate::infrastructure::ports::node_identifier) .collect() } + /// Adds extra artifacts to the cfgsync materialization output. fn enrich_cfgsync_artifacts( _topology: &::Deployment, _artifacts: &mut MaterializedArtifacts, @@ -142,10 +166,12 @@ pub trait ComposeDeployEnv: Application + Sized { Ok(()) } + /// Selects how compose nodes receive config updates. fn cfgsync_server_mode() -> ComposeConfigServerMode { ComposeConfigServerMode::Disabled } + /// Builds the config-server container spec when cfgsync is enabled. fn cfgsync_container_spec( _cfgsync_path: &Path, _port: u16, @@ -154,10 +180,12 @@ pub trait ComposeDeployEnv: Application + Sized { Err(std::io::Error::other("cfgsync_container_spec is not implemented for this app").into()) } + /// Returns how long the runner should wait for the config server to start. fn cfgsync_start_timeout() -> Duration { Duration::from_secs(180) } + /// Builds one node client from mapped compose ports. fn node_client_from_ports( ports: &NodeHostPorts, host: &str, @@ -165,6 +193,7 @@ pub trait ComposeDeployEnv: Application + Sized { ::build_node_client(&discovered_node_access(host, ports)) } + /// Builds node clients for the full compose deployment. fn build_node_clients( _topology: &::Deployment, host_ports: &HostPortMapping, @@ -178,16 +207,19 @@ pub trait ComposeDeployEnv: Application + Sized { Ok(NodeClients::new(clients)) } + /// Returns the readiness probe used for compose nodes. fn readiness_probe() -> ComposeReadinessProbe { ComposeReadinessProbe::Http { path: ::node_readiness_path(), } } + /// Returns the host that should be used to access forwarded compose ports. fn compose_runner_host() -> String { compose_runner_host() } + /// Waits for remote readiness using the app's compose-specific probe model. async fn wait_remote_readiness( _topology: &::Deployment, mapping: &HostPortMapping, @@ -204,6 +236,7 @@ pub trait ComposeDeployEnv: Application + Sized { } } + /// Waits for local host ports using the app's compose-specific probe model. async fn wait_for_nodes( ports: &[u16], host: &str, @@ -234,8 +267,10 @@ pub trait ComposeDeployEnv: Application + Sized { pub trait ComposeBinaryApp: Application + Sized + StaticArtifactRenderer::Deployment> { + /// Returns the binary+config runtime spec shared by all compose nodes. fn compose_node_spec() -> BinaryConfigNodeSpec; + /// Prepares any extra workspace files needed before config rendering. fn prepare_compose_workspace( _path: &Path, _topology: &::Deployment, @@ -244,17 +279,21 @@ pub trait ComposeBinaryApp: Ok(()) } + /// Returns extra non-node services that should be added to the compose + /// stack. fn compose_extra_services( _topology: &::Deployment, ) -> Result, DynError> { Ok(Vec::new()) } + /// Returns the static node config file name used for one node. fn static_node_config_file_name(index: usize) -> String { make_extension_node_config_file_name(&Self::compose_node_spec().config_file_extension) .resolve(index) } + /// Builds one node client from mapped compose ports. fn node_client_from_ports( ports: &NodeHostPorts, host: &str, @@ -262,12 +301,14 @@ pub trait ComposeBinaryApp: ::build_node_client(&discovered_node_access(host, ports)) } + /// Returns the readiness probe used for compose nodes. fn readiness_probe() -> ComposeReadinessProbe { ComposeReadinessProbe::Http { path: ::node_readiness_path(), } } + /// Returns the host that should be used to access forwarded compose ports. fn compose_runner_host() -> String { compose_runner_host() } @@ -439,6 +480,7 @@ where Ok(()) } +/// Materializes cfgsync registration-server config files for a compose stack. pub fn write_registration_server_compose_configs( path: &Path, topology: &::Deployment, @@ -529,6 +571,7 @@ async fn wait_for_tcp_readiness( } } +/// Converts mapped compose ports into generic node access. pub fn discovered_node_access(host: &str, ports: &NodeHostPorts) -> NodeAccess { NodeAccess::new(host, ports.api).with_testing_port(ports.testing) } diff --git a/testing-framework/deployers/k8s/src/env.rs b/testing-framework/deployers/k8s/src/env.rs index 3928635..ae33efa 100644 --- a/testing-framework/deployers/k8s/src/env.rs +++ b/testing-framework/deployers/k8s/src/env.rs @@ -26,32 +26,47 @@ use crate::{ lifecycle::cleanup::RunnerCleanup, }; +/// Assets that can be installed as one Helm release. pub trait HelmReleaseAssets { + /// Returns the Helm bundle used to install the release. fn release_bundle(&self) -> HelmReleaseBundle; } #[derive(Debug)] +/// Rendered chart directory backed by a temporary workspace. pub struct RenderedHelmChartAssets { chart_path: PathBuf, _tempdir: TempDir, } #[derive(Clone, Debug, Default)] +/// In-memory YAML manifest that can be rendered into a single Helm template. pub struct HelmManifest { documents: Vec, } #[derive(Clone, Debug)] +/// Standard binary+config k8s node installation contract. pub struct BinaryConfigK8sSpec { + /// Helm chart name used for the generated chart. pub chart_name: String, + /// Prefix used for generated node deployment and service names. pub node_name_prefix: String, + /// Binary path inside the container image. pub binary_path: String, + /// Absolute config path inside the container. pub config_container_path: String, + /// Main HTTP port exposed by the container. pub container_http_port: u16, + /// Auxiliary service port exposed by the Service object. pub service_testing_port: u16, + /// Primary environment variable used to override the image. pub image_env_var: String, + /// Secondary fallback environment variable used to override the image. pub fallback_image_env_var: String, + /// Default local image tag used when no override is present. pub default_image: String, + /// Image pull policy written into the generated Deployment. pub image_pull_policy: String, } @@ -62,11 +77,13 @@ impl HelmReleaseAssets for RenderedHelmChartAssets { } impl HelmManifest { + /// Creates an empty manifest. #[must_use] pub fn new() -> Self { Self::default() } + /// Serializes one structured value as YAML and appends it as a document. pub fn push_yaml(&mut self, value: &T) -> Result<(), DynError> where T: Serialize, @@ -76,6 +93,7 @@ impl HelmManifest { Ok(()) } + /// Appends a raw YAML document after trimming surrounding whitespace. pub fn push_raw_yaml(&mut self, yaml: &str) { let yaml = yaml.trim(); if !yaml.is_empty() { @@ -83,16 +101,20 @@ impl HelmManifest { } } + /// Appends all documents from another manifest. pub fn extend(&mut self, other: Self) { self.documents.extend(other.documents); } + /// Renders all documents separated by `---`. #[must_use] pub fn render(&self) -> String { self.documents.join("\n---\n") } } +/// Builds the standard forwarded port layout for a generated binary-config +/// k8s stack. pub fn standard_port_specs(node_count: usize, api: u16, auxiliary: u16) -> PortSpecs { PortSpecs { nodes: (0..node_count) @@ -102,6 +124,7 @@ pub fn standard_port_specs(node_count: usize, api: u16, auxiliary: u16) -> PortS } impl BinaryConfigK8sSpec { + /// Builds the conventional binary-config spec for a generated chart. #[must_use] pub fn conventional( chart_name: &str, @@ -137,6 +160,8 @@ impl BinaryConfigK8sSpec { } } +/// Renders a temporary single-template Helm chart for the standard +/// binary-config node layout. pub fn render_binary_config_node_chart_assets( deployment: &E::Deployment, spec: &BinaryConfigK8sSpec, @@ -153,6 +178,8 @@ where ) } +/// Renders the standard ConfigMap, Deployment, and Service objects for each +/// node in a binary-config k8s stack. pub fn render_binary_config_node_manifest( deployment: &E::Deployment, spec: &BinaryConfigK8sSpec, @@ -222,6 +249,7 @@ fn k8s_image(spec: &BinaryConfigK8sSpec) -> String { .unwrap_or_else(|_| spec.default_image.clone()) } +/// Renders a minimal chart directory with a single YAML template file. pub fn render_single_template_chart_assets( chart_name: &str, template_name: &str, @@ -239,6 +267,7 @@ pub fn render_single_template_chart_assets( }) } +/// Renders a chart from an in-memory manifest. pub fn render_manifest_chart_assets( chart_name: &str, template_name: &str, @@ -247,6 +276,7 @@ pub fn render_manifest_chart_assets( render_single_template_chart_assets(chart_name, template_name, &manifest.render()) } +/// Converts forwarded k8s ports into generic node access. pub fn discovered_node_access(host: &str, api_port: u16, auxiliary_port: u16) -> NodeAccess { NodeAccess::new(host, api_port).with_testing_port(auxiliary_port) } @@ -259,6 +289,7 @@ fn normalize_yaml_document(yaml: &str) -> String { yaml.trim_start_matches("---\n").trim().to_owned() } +/// Installs a Helm release and returns the corresponding cleanup handle. pub async fn install_helm_release_with_cleanup( client: &Client, assets: &A, @@ -281,7 +312,9 @@ pub async fn install_helm_release_with_cleanup( } #[async_trait] +/// Prepared k8s stack ready to be installed into a namespace. pub trait PreparedK8sStack: Send + Sync { + /// Installs the prepared stack and returns the cleanup handle. async fn install( &self, client: &Client, @@ -311,15 +344,19 @@ where /// Advanced k8s deployer integration. #[async_trait] pub trait K8sDeployEnv: Application + Sized { + /// Prepared stack type produced by this environment. type Assets: PreparedK8sStack + Send + Sync + 'static; + /// Returns the ports that must be exposed and forwarded for the deployment. fn collect_port_specs(topology: &Self::Deployment) -> PortSpecs; + /// Prepares installable assets for the deployment. fn prepare_assets( topology: &Self::Deployment, metrics_otlp_ingest_url: Option<&Url>, ) -> Result; + /// Installs one prepared stack into the target namespace and release name. async fn install_stack( client: &Client, assets: &Self::Assets, @@ -333,10 +370,12 @@ pub trait K8sDeployEnv: Application + Sized { assets.install(client, namespace, release, nodes).await } + /// Returns the generated namespace and release names for one test run. fn cluster_identifiers() -> (String, String) { default_cluster_identifiers() } + /// Builds one node client from forwarded k8s ports. fn node_client_from_ports( host: &str, api_port: u16, @@ -349,6 +388,7 @@ pub trait K8sDeployEnv: Application + Sized { )) } + /// Builds node clients for the full deployment. fn build_node_clients( host: &str, node_api_ports: &[u16], @@ -363,10 +403,12 @@ pub trait K8sDeployEnv: Application + Sized { .collect() } + /// Returns the readiness endpoint path used for remote HTTP probes. fn node_readiness_path() -> &'static str { ::node_readiness_path() } + /// Waits for remote node readiness after port-forwarding is established. async fn wait_remote_readiness( _deployment: &Self::Deployment, urls: &[Url], @@ -384,22 +426,27 @@ pub trait K8sDeployEnv: Application + Sized { Ok(()) } + /// Returns the Kubernetes role label used for node workloads. fn node_role() -> &'static str { "node" } + /// Returns the Deployment name for one node. fn node_deployment_name(release: &str, index: usize) -> String { default_node_name(release, index) } + /// Returns the Service name for one node. fn node_service_name(release: &str, index: usize) -> String { default_node_name(release, index) } + /// Returns the label selector used by attach flows to locate node services. fn attach_node_service_selector(release: &str) -> String { default_attach_node_service_selector(release) } + /// Waits for direct HTTP readiness against forwarded node ports. async fn wait_for_node_http( ports: &[u16], role: &'static str, @@ -421,20 +468,26 @@ pub trait K8sDeployEnv: Application + Sized { Ok(()) } + /// Returns the externally reachable base URL for a node client, if the app + /// needs it for attach or manual flows. fn node_base_url(_client: &Self::NodeClient) -> Option { None } + /// Returns the cfgsync service host and port when manual-cluster override + /// flows are enabled. fn cfgsync_service(_release: &str) -> Option<(String, u16)> { None } + /// Returns cfgsync hostnames for all nodes in the deployment. fn cfgsync_hostnames(release: &str, node_count: usize) -> Vec { (0..node_count) .map(|index| Self::node_service_name(release, index)) .collect() } + /// Builds app-specific cfgsync override artifacts for one node. fn build_cfgsync_override_artifacts( _deployment: &Self::Deployment, _node_index: usize, @@ -450,8 +503,10 @@ pub trait K8sBinaryApp: Application + StaticNodeConfigProvider + Sized where Self::Deployment: DeploymentDescriptor, { + /// Returns the standard binary+config install specification. fn k8s_binary_spec() -> BinaryConfigK8sSpec; + /// Adds extra YAML resources to the generated manifest. fn extend_k8s_manifest( _deployment: &Self::Deployment, _manifest: &mut HelmManifest, @@ -459,6 +514,7 @@ where Ok(()) } + /// Builds node clients for the full deployment. fn build_node_clients( host: &str, node_api_ports: &[u16], @@ -477,10 +533,12 @@ where .collect() } + /// Returns the Kubernetes role label used for node workloads. fn node_role() -> &'static str { "node" } + /// Returns the externally reachable base URL for a node client, if needed. fn node_base_url(_client: &Self::NodeClient) -> Option { None } diff --git a/testing-framework/deployers/k8s/src/infrastructure/cluster.rs b/testing-framework/deployers/k8s/src/infrastructure/cluster.rs index 6e00640..dedd6c5 100644 --- a/testing-framework/deployers/k8s/src/infrastructure/cluster.rs +++ b/testing-framework/deployers/k8s/src/infrastructure/cluster.rs @@ -18,7 +18,9 @@ use crate::{ }; #[derive(Default)] +/// Port specification for a k8s deployment before port-forwarding starts. pub struct PortSpecs { + /// Per-node API and auxiliary ports that must be exposed. pub nodes: Vec, } @@ -35,12 +37,15 @@ pub struct ClusterEnvironment { } #[derive(Debug, thiserror::Error)] +/// Failures while managing the cluster environment wrapper. pub enum ClusterEnvironmentError { #[error("cleanup guard is missing (it may have already been consumed)")] + /// The environment no longer owns a cleanup guard. MissingCleanupGuard, } impl ClusterEnvironment { + /// Creates a cluster environment from forwarded ports and cleanup state. pub fn new( client: Client, namespace: String, @@ -64,6 +69,8 @@ impl ClusterEnvironment { } } + /// Marks the environment as failed, dumps diagnostics, and triggers + /// cleanup. pub async fn fail(&mut self, reason: &str) { tracing::error!( reason = reason, @@ -78,6 +85,8 @@ impl ClusterEnvironment { } } + /// Consumes the environment and returns the cleanup handle plus remaining + /// port-forwards. pub fn into_cleanup( self, ) -> Result<(RunnerCleanup, Vec), ClusterEnvironmentError> { @@ -88,38 +97,44 @@ impl ClusterEnvironment { } #[allow(dead_code)] + /// Returns the namespace backing this cluster. pub fn namespace(&self) -> &str { &self.namespace } #[allow(dead_code)] + /// Returns the Helm release name backing this cluster. pub fn release(&self) -> &str { &self.release } + /// Returns the Kubernetes client used by this environment. pub fn client(&self) -> &Client { &self.client } + /// Returns forwarded API and auxiliary node ports. pub fn node_ports(&self) -> (&[u16], &[u16]) { (&self.node_api_ports, &self.node_auxiliary_ports) } } -#[derive(Debug, thiserror::Error)] /// Failures while building node clients against forwarded ports. +#[derive(Debug, thiserror::Error)] pub enum NodeClientError { #[error("failed to build node clients: {source}")] + /// Building one or more node clients failed. Build { #[source] source: DynError, }, } -#[derive(Debug, thiserror::Error)] /// Readiness check failures for the remote cluster endpoints. +#[derive(Debug, thiserror::Error)] pub enum RemoteReadinessError { #[error("failed to build readiness URL for {role} port {port}: {source}")] + /// Building one readiness URL failed. Endpoint { role: &'static str, port: u16, @@ -127,18 +142,21 @@ pub enum RemoteReadinessError { source: ParseError, }, #[error("remote readiness probe failed: {source}")] + /// The remote readiness probe failed after the URLs were built. Remote { #[source] source: DynError, }, } +/// Collects the required port-forward specification for one deployment. pub fn collect_port_specs(descriptors: &E::Deployment) -> PortSpecs { let specs = collect_k8s_port_specs::(descriptors); debug!(nodes = specs.nodes.len(), "collected k8s port specs"); specs } +/// Builds node clients against the forwarded cluster ports. pub fn build_node_clients( cluster: &ClusterEnvironment, ) -> Result, NodeClientError> { @@ -154,6 +172,8 @@ pub fn build_node_clients( Ok(NodeClients::new(nodes)) } +/// Waits until the remote k8s cluster satisfies the requested readiness +/// requirement. pub async fn ensure_cluster_readiness( descriptors: &E::Deployment, cluster: &ClusterEnvironment, @@ -176,6 +196,7 @@ pub async fn ensure_cluster_readiness( Ok(()) } +/// Waits for cluster port-forwards and cleans up on failure. pub async fn wait_for_ports_or_cleanup( client: &Client, namespace: &str, @@ -205,6 +226,7 @@ pub async fn wait_for_ports_or_cleanup( } } +/// Stops all active port-forwards and clears the handle list. pub fn kill_port_forwards(handles: &mut Vec) { for handle in handles.iter_mut() { handle.shutdown(); diff --git a/testing-framework/deployers/local/src/env/helpers.rs b/testing-framework/deployers/local/src/env/helpers.rs index 0602c7f..e2d54fe 100644 --- a/testing-framework/deployers/local/src/env/helpers.rs +++ b/testing-framework/deployers/local/src/env/helpers.rs @@ -14,64 +14,81 @@ use crate::{ process::{LaunchSpec, NodeEndpointPort, NodeEndpoints, ProcessSpawnError}, }; +/// Result of building a local node config together with the node's reserved +/// network port. pub struct BuiltNodeConfig { + /// Materialized node config value. pub config: Config, + /// Reserved network port used for peer traffic. pub network_port: u16, } +/// Named initial config entry generated for one node. pub struct NodeConfigEntry { + /// Stable generated node name. pub name: String, + /// Config value associated with `name`. pub config: NodeConfigValue, } +/// Reserved local ports assigned to one node. pub struct LocalNodePorts { network_port: u16, named_ports: HashMap<&'static str, u16>, } impl LocalNodePorts { + /// Returns the reserved network port. #[must_use] pub fn network_port(&self) -> u16 { self.network_port } + /// Returns a reserved named port, if present. #[must_use] pub fn get(&self, name: &str) -> Option { self.named_ports.get(name).copied() } + /// Returns a reserved named port or an error if it is missing. pub fn require(&self, name: &str) -> Result { self.get(name) .ok_or_else(|| format!("missing reserved local port '{name}'").into()) } + /// Iterates over all reserved named ports. pub fn iter(&self) -> impl Iterator + '_ { self.named_ports.iter().map(|(name, port)| (*name, *port)) } } #[derive(Clone, Debug)] +/// Peer node view used while constructing local configs. pub struct LocalPeerNode { index: usize, network_port: u16, } impl LocalPeerNode { + /// Returns the peer's zero-based node index. #[must_use] pub fn index(&self) -> usize { self.index } + /// Returns the peer's reserved network port. #[must_use] pub fn network_port(&self) -> u16 { self.network_port } + /// Returns the peer's loopback HTTP authority as `127.0.0.1:`. #[must_use] pub fn http_address(&self) -> String { format!("127.0.0.1:{}", self.network_port) } + /// Returns the peer authority used in local configs. #[must_use] pub fn authority(&self) -> String { self.http_address() @@ -79,16 +96,24 @@ impl LocalPeerNode { } #[derive(Clone, Default)] +/// Standard local process description for one node binary plus one config file. pub struct LocalProcessSpec { + /// Environment variable that points to the node binary. pub binary_env_var: String, + /// Fallback binary name resolved from `PATH` or `target/`. pub binary_name: String, + /// Config file name written into the temp launch directory. pub config_file_name: String, + /// CLI flag used to point the process at `config_file_name`. pub config_arg: String, + /// Extra CLI arguments passed after the config flag. pub extra_args: Vec, + /// Extra environment variables for the child process. pub env: Vec, } impl LocalProcessSpec { + /// Creates a standard binary+config local process spec. #[must_use] pub fn new(binary_env_var: &str, binary_name: &str) -> Self { Self { @@ -101,6 +126,7 @@ impl LocalProcessSpec { } } + /// Overrides the config file name and CLI flag used to pass it. #[must_use] pub fn with_config_file(mut self, file_name: &str, arg: &str) -> Self { self.config_file_name = file_name.to_owned(); @@ -108,17 +134,20 @@ impl LocalProcessSpec { self } + /// Appends one extra environment variable. #[must_use] pub fn with_env(mut self, key: &str, value: &str) -> Self { self.env.push(crate::process::LaunchEnvVar::new(key, value)); self } + /// Convenience helper for setting `RUST_LOG`. #[must_use] pub fn with_rust_log(self, value: &str) -> Self { self.with_env("RUST_LOG", value) } + /// Appends extra CLI arguments after the config flag pair. #[must_use] pub fn with_args(mut self, args: impl IntoIterator) -> Self { self.extra_args.extend(args); @@ -126,6 +155,7 @@ impl LocalProcessSpec { } } +/// Preallocates `count` local TCP ports for later use. pub fn preallocate_ports(count: usize, label: &str) -> Result, ProcessSpawnError> { (0..count) .map(|_| crate::process::allocate_available_port()) @@ -135,6 +165,7 @@ pub fn preallocate_ports(count: usize, label: &str) -> Result, ProcessS }) } +/// Builds a stable `name_prefix-{index}` config list. pub fn build_indexed_node_configs( count: usize, name_prefix: &str, @@ -150,6 +181,7 @@ pub fn build_indexed_node_configs( .collect() } +/// Reserves network and named ports for `count` local nodes. pub fn reserve_local_node_ports( count: usize, names: &[&'static str], @@ -172,10 +204,13 @@ pub fn reserve_local_node_ports( .collect()) } +/// Builds the default single-HTTP-endpoint node access shape. pub fn single_http_node_endpoints(port: u16) -> NodeEndpoints { NodeEndpoints::from_api_port(port) } +/// Builds a cluster node config for the local loopback environment from the +/// shared cluster-config application model. pub fn build_local_cluster_node_config( index: usize, ports: &LocalNodePorts, @@ -197,6 +232,8 @@ where E::build_cluster_node_config(&node, &peer_views).map_err(Into::into) } +/// Converts discovered local node endpoints into the generic `NodeAccess` +/// shape used by `Application::build_node_client`. pub fn discovered_node_access(endpoints: &NodeEndpoints) -> NodeAccess { let mut access = NodeAccess::new("127.0.0.1", endpoints.api.port()); @@ -215,6 +252,8 @@ pub fn discovered_node_access(endpoints: &NodeEndpoints) -> NodeAccess { access } +/// Builds peer values from a full indexed port list while skipping +/// `self_index`. pub fn build_indexed_http_peers( node_count: usize, self_index: usize, @@ -235,6 +274,8 @@ pub(crate) fn compact_peer_ports(peer_ports: &[u16], self_index: usize) -> Vec Vec { peer_ports .iter() @@ -248,6 +289,7 @@ pub fn build_local_peer_nodes(peer_ports: &[u16], self_index: usize) -> Vec( topology: &E::Deployment, node_name_prefix: &str, @@ -292,6 +334,7 @@ where .collect() } +/// Serializes a config as YAML and builds a launch spec for `spec`. pub fn yaml_config_launch_spec( config: &T, spec: &LocalProcessSpec, @@ -300,6 +343,7 @@ pub fn yaml_config_launch_spec( rendered_config_launch_spec(config_yaml.into_bytes(), spec) } +/// Uses an already rendered text config to build a launch spec for `spec`. pub fn text_config_launch_spec( rendered_config: impl Into>, spec: &LocalProcessSpec, @@ -307,6 +351,7 @@ pub fn text_config_launch_spec( rendered_config_launch_spec(rendered_config.into(), spec) } +/// Uses the standard binary+config launch shape for a YAML-rendered config. pub fn default_yaml_launch_spec( config: &T, binary_env_var: &str, @@ -319,10 +364,12 @@ pub fn default_yaml_launch_spec( ) } +/// Serializes a node config as YAML bytes. pub fn yaml_node_config(config: &T) -> Result, DynError> { Ok(serde_yaml::to_string(config)?.into_bytes()) } +/// Converts an already rendered text config into launch-file bytes. pub fn text_node_config(rendered_config: impl Into>) -> Vec { rendered_config.into() } diff --git a/testing-framework/deployers/local/src/env/mod.rs b/testing-framework/deployers/local/src/env/mod.rs index 9fbce4b..17ee9ea 100644 --- a/testing-framework/deployers/local/src/env/mod.rs +++ b/testing-framework/deployers/local/src/env/mod.rs @@ -29,13 +29,21 @@ pub use helpers::{ /// Context passed while building a local node config. pub struct LocalBuildContext<'a, E: Application> { + /// Full deployment topology for the current scenario. pub topology: &'a E::Deployment, + /// Zero-based node index being built. pub index: usize, + /// Reserved local ports assigned to this node. pub ports: &'a LocalNodePorts, + /// Peer nodes visible to this node after excluding `index`. pub peers: &'a [LocalPeerNode], + /// Peer network ports for the current node view. pub peer_ports: &'a [u16], + /// Peer ports keyed by application-defined port name. pub peer_ports_by_name: &'a HashMap, + /// Start-time options for the node being built. pub options: &'a StartNodeOptions, + /// Optional template config to derive from during manual restart flows. pub template_config: Option<&'a E::NodeConfig>, } @@ -52,10 +60,14 @@ pub trait LocalDeployerEnv: Application + Sized where ::NodeConfig: Clone + Send + Sync + 'static, { + /// Returns named ports that should be reserved in addition to the main + /// network port for each node. fn local_port_names() -> &'static [&'static str] { Self::initial_local_port_names() } + /// Builds a node config and reserves any local ports needed for a node + /// started after initial cluster creation. fn build_node_config( topology: &Self::Deployment, index: usize, @@ -73,6 +85,8 @@ where ) } + /// Builds a node config from an optional template during restart or + /// manual-cluster flows. fn build_node_config_from_template( topology: &Self::Deployment, index: usize, @@ -103,6 +117,7 @@ where }) } + /// Builds the initial local configs for every node in the deployment. fn build_initial_node_configs( topology: &Self::Deployment, ) -> Result::NodeConfig>>, ProcessSpawnError> { @@ -129,14 +144,17 @@ where ) } + /// Prefix used for generated node names such as `node-0`. fn initial_node_name_prefix() -> &'static str { "node" } + /// Additional named ports to reserve for each initial node. fn initial_local_port_names() -> &'static [&'static str] { &[] } + /// Builds one initial node config from already reserved ports. fn build_initial_node_config( topology: &Self::Deployment, index: usize, @@ -157,6 +175,7 @@ where ) } + /// Builds a local node config from peer port information. fn build_local_node_config( topology: &Self::Deployment, index: usize, @@ -178,6 +197,7 @@ where ) } + /// Builds a local node config from full peer node descriptions. fn build_local_node_config_with_peers( _topology: &Self::Deployment, _index: usize, @@ -193,6 +213,8 @@ where .into()) } + /// Returns the initial persist directory for a node, if one should be + /// mounted before startup. fn initial_persist_dir( _topology: &Self::Deployment, _node_name: &str, @@ -201,6 +223,8 @@ where None } + /// Returns the initial snapshot directory for a node, if one should be + /// mounted before startup. fn initial_snapshot_dir( _topology: &Self::Deployment, _node_name: &str, @@ -209,16 +233,20 @@ where None } + /// Returns the default local process description for this app. fn local_process_spec() -> Option { None } + /// Serializes a local node config into the file bytes written next to the + /// spawned process. fn render_local_config( _config: &::NodeConfig, ) -> Result, DynError> { Err(std::io::Error::other("render_local_config is not implemented for this app").into()) } + /// Builds the full launch spec for a local node process. fn build_launch_spec( config: &::NodeConfig, _dir: &Path, @@ -231,10 +259,13 @@ where helpers::rendered_config_launch_spec(rendered, &spec) } + /// Returns the main HTTP API port from a node config when the app follows + /// the standard single-HTTP-endpoint pattern. fn http_api_port(_config: &::NodeConfig) -> Option { None } + /// Resolves the full local endpoint set exposed by a node. fn node_endpoints( config: &::NodeConfig, ) -> Result { @@ -248,14 +279,18 @@ where Err(std::io::Error::other("node_endpoints is not implemented for this app").into()) } + /// Resolves the port peers should use for cluster traffic. fn node_peer_port(node: &Node) -> u16 { node.endpoints().api.port() } + /// Builds a node client directly from the API endpoint when the default + /// `NodeAccess`-based path is not suitable. fn node_client_from_api_endpoint(_api: SocketAddr) -> Option { None } + /// Builds a node client from discovered local endpoints. fn node_client(endpoints: &NodeEndpoints) -> Result { if let Ok(client) = ::build_node_client(&discovered_node_access(endpoints)) @@ -270,10 +305,13 @@ where Err(std::io::Error::other("node_client is not implemented for this app").into()) } + /// Returns the readiness endpoint path used for local HTTP probes. fn readiness_endpoint_path() -> &'static str { ::node_readiness_path() } + /// Waits for any additional cluster-specific stabilization after the HTTP + /// readiness probe succeeds. async fn wait_readiness_stable(_nodes: &[Node]) -> Result<(), DynError> { Ok(()) } @@ -290,12 +328,15 @@ pub trait LocalBinaryApp: Application + Sized where ::NodeConfig: Clone + Send + Sync + 'static, { + /// Prefix used for generated initial node names such as `node-0`. fn initial_node_name_prefix() -> &'static str; + /// Additional named ports to reserve for each local node. fn initial_local_port_names() -> &'static [&'static str] { &[] } + /// Builds a local node config from full peer node descriptions. fn build_local_node_config_with_peers( topology: &Self::Deployment, index: usize, @@ -306,17 +347,24 @@ where template_config: Option<&::NodeConfig>, ) -> Result<::NodeConfig, DynError>; + /// Returns the standard process description for launching one local node. fn local_process_spec() -> LocalProcessSpec; + /// Serializes a local node config into the file bytes written next to the + /// spawned process. fn render_local_config(config: &::NodeConfig) -> Result, DynError>; + /// Returns the main HTTP API port used for discovery and readiness. fn http_api_port(config: &::NodeConfig) -> u16; + /// Returns the readiness endpoint path used for local HTTP probes. fn readiness_endpoint_path() -> &'static str { ::node_readiness_path() } + /// Waits for any additional cluster-specific stabilization after the HTTP + /// readiness probe succeeds. async fn wait_readiness_stable(_nodes: &[Node]) -> Result<(), DynError> { Ok(()) } @@ -433,6 +481,8 @@ pub(crate) fn readiness_endpoint_path() -> &'static str { E::readiness_endpoint_path() } +/// Waits for local HTTP readiness across the provided nodes and then applies +/// any app-specific stabilization hook. pub async fn wait_local_http_readiness( nodes: &[Node], requirement: HttpReadinessRequirement, @@ -449,6 +499,7 @@ pub async fn wait_local_http_readiness( .map_err(|source| ReadinessError::ClusterStable { source }) } +/// Spawns a local process node from an already prepared config value. pub async fn spawn_node_from_config( label: String, config: ::NodeConfig,