From 84cd33bc674186d2ad9ffb5c2be964e5637e31ce Mon Sep 17 00:00:00 2001 From: andrussal Date: Sun, 29 Mar 2026 15:22:35 +0200 Subject: [PATCH] feat(k8s): support config overrides in manual cluster --- testing-framework/core/src/scenario/config.rs | 66 +++++++++++-- testing-framework/deployers/k8s/src/manual.rs | 96 ++++++++++++++----- 2 files changed, 130 insertions(+), 32 deletions(-) diff --git a/testing-framework/core/src/scenario/config.rs b/testing-framework/core/src/scenario/config.rs index 5d03ae3..be20e05 100644 --- a/testing-framework/core/src/scenario/config.rs +++ b/testing-framework/core/src/scenario/config.rs @@ -154,22 +154,32 @@ where hostnames: &[String], options: &StartNodeOptions, ) -> Result, Self::Error> { - match &options.peers { - PeerSelection::DefaultLayout => Ok(None), + let mut config = match &options.peers { + PeerSelection::DefaultLayout => { + if options.config_override.is_none() && options.config_patch.is_none() { + return Ok(None); + } + build_static_cluster_node_config::(deployment, node_index, Some(hostnames))? + } PeerSelection::None => { - let config = - build_cluster_node_config_for_indices::(node_index, hostnames, &[])?; - let yaml = T::serialize_cluster_node_config(&config)?; - Ok(Some(single_config_artifact(yaml))) + build_cluster_node_config_for_indices::(node_index, hostnames, &[])? } PeerSelection::Named(names) => { let indices = resolve_named_peer_indices::(deployment, node_index, names)?; - let config = - build_cluster_node_config_for_indices::(node_index, hostnames, &indices)?; - let yaml = T::serialize_cluster_node_config(&config)?; - Ok(Some(single_config_artifact(yaml))) + build_cluster_node_config_for_indices::(node_index, hostnames, &indices)? } + }; + + if let Some(override_config) = options.config_override.clone() { + config = override_config; } + + if let Some(config_patch) = &options.config_patch { + config = config_patch(config).map_err(|source| io::Error::other(source.to_string()))? + } + + let yaml = T::serialize_cluster_node_config(&config)?; + Ok(Some(single_config_artifact(yaml))) } } @@ -370,4 +380,40 @@ mod tests { assert_eq!(artifacts.files[0].content, "node=node-1.svc:9000;peers="); } + + #[test] + fn cluster_app_builds_default_layout_patch_override_artifacts() { + let deployment = crate::topology::ClusterTopology::new(2); + let hostnames = vec!["node-0.svc".to_owned(), "node-1.svc".to_owned()]; + let options = StartNodeOptions::::default().create_patch(|mut config| { + config.push_str(";patched=true"); + Ok(config) + }); + + let artifacts = + DummyClusterApp::build_node_artifacts_for_options(&deployment, 1, &hostnames, &options) + .expect("override artifacts") + .expect("expected override"); + + assert_eq!( + artifacts.files[0].content, + "node=node-1.svc:9000;peers=node-0.svc:9000;patched=true" + ); + } + + #[test] + fn cluster_app_prefers_config_override_for_override_artifacts() { + let deployment = crate::topology::ClusterTopology::new(2); + let hostnames = vec!["node-0.svc".to_owned(), "node-1.svc".to_owned()]; + let options = StartNodeOptions::::default() + .with_peers(PeerSelection::Named(vec!["node-0".to_owned()])) + .with_config_override("override-config".to_owned()); + + let artifacts = + DummyClusterApp::build_node_artifacts_for_options(&deployment, 1, &hostnames, &options) + .expect("override artifacts") + .expect("expected override"); + + assert_eq!(artifacts.files[0].content, "override-config"); + } } diff --git a/testing-framework/deployers/k8s/src/manual.rs b/testing-framework/deployers/k8s/src/manual.rs index 9c4f643..f868c7d 100644 --- a/testing-framework/deployers/k8s/src/manual.rs +++ b/testing-framework/deployers/k8s/src/manual.rs @@ -405,7 +405,7 @@ where options: &StartNodeOptions, ) -> Result<(), ManualClusterError> { let Some((service, port)) = E::cfgsync_service(&self.release) else { - return ensure_default_peer_selection(options); + return ensure_default_cfgsync_options(options); }; let hostnames = E::cfgsync_hostnames(&self.release, self.node_count); @@ -417,7 +417,7 @@ where })?; let Some(artifacts) = artifacts else { - return ensure_default_peer_selection(options); + return ensure_default_cfgsync_options(options); }; let forward = port_forward_service(&self.namespace, &service, port)?; @@ -614,11 +614,6 @@ async fn wait_for_replicas( fn validate_start_options( options: &StartNodeOptions, ) -> Result<(), ManualClusterError> { - if options.config_override.is_some() || options.config_patch.is_some() { - return Err(ManualClusterError::UnsupportedStartOptions { - message: "config overrides/patches are not supported".to_owned(), - }); - } if options.persist_dir.is_some() || options.snapshot_dir.is_some() { return Err(ManualClusterError::UnsupportedStartOptions { message: "persist/snapshot directories are not supported".to_owned(), @@ -627,18 +622,19 @@ fn validate_start_options( Ok(()) } -fn ensure_default_peer_selection( +fn ensure_default_cfgsync_options( options: &StartNodeOptions, ) -> Result<(), ManualClusterError> { - if matches!( + let default_peers = matches!( options.peers, testing_framework_core::scenario::PeerSelection::DefaultLayout - ) { + ); + if default_peers && options.config_override.is_none() && options.config_patch.is_none() { return Ok(()); } Err(ManualClusterError::UnsupportedStartOptions { - message: "custom peer selection is not supported".to_owned(), + message: "cfgsync override support is not configured for these start options".to_owned(), }) } @@ -758,16 +754,28 @@ mod tests { _hostnames: &[String], options: &StartNodeOptions, ) -> Result, Self::Error> { - let peers = match &options.peers { - PeerSelection::DefaultLayout => return Ok(None), - PeerSelection::None => "none".to_owned(), - PeerSelection::Named(names) => names.join(","), + let mut config = match &options.peers { + PeerSelection::DefaultLayout => { + if options.config_override.is_none() && options.config_patch.is_none() { + return Ok(None); + } + format!("node={node_index};peers=default") + } + PeerSelection::None => format!("node={node_index};peers=none"), + PeerSelection::Named(names) => { + format!("node={node_index};peers={}", names.join(",")) + } }; + if let Some(override_config) = options.config_override.clone() { + config = override_config; + } + if let Some(config_patch) = &options.config_patch { + config = config_patch(config).map_err(|source| { + std::io::Error::other(format!("failed to patch dummy config: {source}")) + })?; + } Ok(Some(cfgsync_artifacts::ArtifactSet::new(vec![ - cfgsync_artifacts::ArtifactFile::new( - "/config.yaml".to_string(), - format!("node={node_index};peers={peers}"), - ), + cfgsync_artifacts::ArtifactFile::new("/config.yaml".to_string(), config), ]))) } } @@ -780,21 +788,46 @@ mod tests { } #[test] - fn validate_start_options_rejects_non_peer_overrides() { + fn validate_start_options_accepts_config_overrides() { + let override_config = + StartNodeOptions::::default().with_config_override("override".to_owned()); + let patched = StartNodeOptions::::default().create_patch(|mut config| { + config.push_str(";patched"); + Ok(config) + }); + + assert!(validate_start_options(&override_config).is_ok()); + assert!(validate_start_options(&patched).is_ok()); + } + + #[test] + fn validate_start_options_rejects_persist_and_snapshot_dirs() { let persist = StartNodeOptions::::default() .with_persist_dir(std::path::PathBuf::from("/tmp/demo")); + let snapshot = StartNodeOptions::::default() + .with_snapshot_dir(std::path::PathBuf::from("/tmp/snapshot")); assert!(matches!( validate_start_options(&persist), Err(ManualClusterError::UnsupportedStartOptions { .. }) )); + assert!(matches!( + validate_start_options(&snapshot), + Err(ManualClusterError::UnsupportedStartOptions { .. }) + )); } #[test] - fn ensure_default_peer_selection_rejects_named_peers() { + fn ensure_default_cfgsync_options_rejects_non_default_overrides() { let peers = StartNodeOptions::::default() .with_peers(PeerSelection::Named(vec!["node-0".to_owned()])); + let override_config = + StartNodeOptions::::default().with_config_override("override".to_owned()); assert!(matches!( - ensure_default_peer_selection(&peers), + ensure_default_cfgsync_options(&peers), + Err(ManualClusterError::UnsupportedStartOptions { .. }) + )); + assert!(matches!( + ensure_default_cfgsync_options(&override_config), Err(ManualClusterError::UnsupportedStartOptions { .. }) )); } @@ -817,4 +850,23 @@ mod tests { assert_eq!(artifacts.files.len(), 1); assert_eq!(artifacts.files[0].content, "node=1;peers=node-0"); } + + #[test] + fn dummy_env_builds_cfgsync_override_artifacts_for_config_override() { + let topology = testing_framework_core::topology::ClusterTopology::new(2); + let options = + StartNodeOptions::::default().with_config_override("override".to_owned()); + + let artifacts = DummyEnv::build_cfgsync_override_artifacts( + &topology, + 1, + &["node-0".to_owned(), "node-1".to_owned()], + &options, + ) + .expect("build override") + .expect("expected override"); + + assert_eq!(artifacts.files.len(), 1); + assert_eq!(artifacts.files[0].content, "override"); + } }