Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/9766_data_dir_cli_env.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added a `--data-dir` command-line flag (and corresponding `VECTOR_DATA_DIR` environment variable) to override the `data_dir` global configuration option. When set, it takes precedence over any `data_dir` value in the configuration file. This is useful for keeping deployment-specific paths out of the configuration file, for example when validating a configuration in a CI environment where the configured `data_dir` may not exist.

authors: xfocus3
163 changes: 160 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::os::unix::process::ExitStatusExt;
use std::os::windows::process::ExitStatusExt;
use std::{
num::{NonZeroU64, NonZeroUsize},
path::PathBuf,
path::{Path, PathBuf},
process::ExitStatus,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
Expand Down Expand Up @@ -90,6 +90,7 @@ impl ApplicationConfig {
&config_paths,
watcher_conf,
opts.require_healthy,
opts.data_dir.clone(),
opts.allow_empty_config,
graceful_shutdown_duration,
signal_handler,
Expand Down Expand Up @@ -267,7 +268,11 @@ impl Application {
opts.root.dangerously_allow_env_var_interpolation
|| sub_command.dangerously_allow_env_var_interpolation(),
);
return Err(runtime.block_on(sub_command.execute(signals, color)));
return Err(runtime.block_on(sub_command.execute(
signals,
color,
opts.root.data_dir.as_deref(),
)));
}

config::set_env_var_interpolation(opts.root.dangerously_allow_env_var_interpolation);
Expand Down Expand Up @@ -323,6 +328,7 @@ impl Application {
signals,
topology_controller,
allow_empty_config: root_opts.allow_empty_config,
data_dir: root_opts.data_dir,
})
}
}
Expand All @@ -334,6 +340,7 @@ pub struct StartedApplication {
pub signals: SignalPair,
pub topology_controller: SharedTopologyController,
pub allow_empty_config: bool,
pub data_dir: Option<PathBuf>,
}

impl StartedApplication {
Expand All @@ -353,6 +360,7 @@ impl StartedApplication {
topology_controller,
internal_topologies,
allow_empty_config,
data_dir,
} = self;

let mut graceful_crash = UnboundedReceiverStream::new(graceful_crash_receiver);
Expand All @@ -369,6 +377,7 @@ impl StartedApplication {
&config_paths,
&mut signal_handler,
allow_empty_config,
data_dir.as_deref(),
).await {
break signal;
},
Expand Down Expand Up @@ -399,6 +408,7 @@ async fn handle_signal(
config_paths: &[ConfigPath],
signal_handler: &mut SignalHandler,
allow_empty_config: bool,
data_dir: Option<&Path>,
) -> Option<SignalTo> {
match signal {
Ok(SignalTo::ReloadComponents(components_to_reload)) => {
Expand All @@ -417,12 +427,16 @@ async fn handle_signal(
&topology_controller.config_paths,
signal_handler,
allow_empty_config,
data_dir,
)
.await;

reload_config_from_result(topology_controller, new_config).await
}
Ok(SignalTo::ReloadFromConfigBuilder(config_builder)) => {
Ok(SignalTo::ReloadFromConfigBuilder(mut config_builder)) => {
if let Some(data_dir) = data_dir {
config_builder.set_data_dir(data_dir);
}
let topology_controller = topology_controller.lock().await;
reload_config_from_result(topology_controller, config_builder.build()).await
}
Expand All @@ -439,6 +453,7 @@ async fn handle_signal(
&topology_controller.config_paths,
signal_handler,
allow_empty_config,
data_dir,
)
.await;

Expand Down Expand Up @@ -645,10 +660,12 @@ pub fn build_runtime(
/// Returns a configuration exit code if paths, configuration loading, or watcher setup fail.
// https://github.com/vectordotdev/vector/issues/23659
#[allow(clippy::large_futures, reason = "Boxing needs profiling")]
#[allow(clippy::too_many_arguments)]
pub async fn load_configs(
config_paths: &[ConfigPath],
watcher_conf: Option<config::watcher::WatcherConfig>,
require_healthy: Option<bool>,
data_dir: Option<PathBuf>,
allow_empty_config: bool,
graceful_shutdown_duration: Option<Duration>,
signal_handler: &mut SignalHandler,
Expand All @@ -669,6 +686,7 @@ pub async fn load_configs(
&config_paths,
signal_handler,
allow_empty_config,
data_dir.as_deref(),
)
.await
.map_err(handle_config_errors)?;
Expand Down Expand Up @@ -776,3 +794,142 @@ pub fn watcher_config(
WatchConfigMethod::Poll => config::watcher::WatcherConfig::PollWatcher(interval.into()),
}
}

#[cfg(all(test, feature = "sources-demo_logs", feature = "sinks-blackhole"))]
mod data_dir_tests {
use clap::Parser;

use super::*;

fn config_text(data_dir: &Path, sink: &str) -> String {
let data_dir = serde_json::to_string(data_dir).unwrap();
format!(
"data_dir: {data_dir}\nsources:\n input:\n type: demo_logs\n format: shuffle\n lines: [\"log\"]\nsinks:\n {sink}:\n type: blackhole\n inputs: [input]\n"
)
}

fn run_reload_test_in_subprocess() -> bool {
const CHILD: &str = "VECTOR_TEST_DATA_DIR_RELOAD_CHILD";
if std::env::var_os(CHILD).is_some() {
return false;
}
let output = std::process::Command::new(std::env::current_exe().unwrap())
.env(CHILD, "1")
.env_remove("VECTOR_DATA_DIR")
.args([
"--exact",
"app::data_dir_tests::cli_data_dir_survives_topology_reload_signals",
"--nocapture",
])
.output()
.unwrap();
assert!(
output.status.success(),
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
assert!(
String::from_utf8_lossy(&output.stdout)
.contains("topology reload assertions completed")
);
true
}

#[tokio::test]
async fn cli_data_dir_survives_topology_reload_signals() {
if run_reload_test_in_subprocess() {
return;
}

crate::test_util::trace_init();
let directory = tempfile::tempdir().unwrap();
let config_file = directory.path().join("vector.yaml");
let override_dir = directory.path().join("override-state");
std::fs::create_dir(&override_dir).unwrap();
std::fs::write(
&config_file,
config_text(&directory.path().join("configured-state"), "initial"),
)
.unwrap();
let opts = RootOpts::try_parse_from([
"vector",
"--config",
config_file.to_str().unwrap(),
"--data-dir",
override_dir.to_str().unwrap(),
])
.unwrap();
let (mut signals, _receiver) = SignalHandler::new();
let application =
ApplicationConfig::from_opts(&opts, &mut signals, ExtraContext::default())
.await
.unwrap();
let paths = application.config_paths.clone();
let controller = SharedTopologyController::new(TopologyController {
topology: application.topology,
config_paths: paths.clone(),
require_healthy: opts.require_healthy,
#[cfg(feature = "api")]
api_server: None,
extra_context: ExtraContext::default(),
});

for replacement in ["disk", "provider", "components"] {
let text = config_text(&directory.path().join(replacement), replacement);
let signal = if replacement == "provider" {
let builder = config::loading::ConfigBuilderLoader::default()
.load_from_input(text.as_bytes(), config::Format::Yaml)
.unwrap();
SignalTo::ReloadFromConfigBuilder(builder)
} else {
std::fs::write(&config_file, text).unwrap();
if replacement == "disk" {
SignalTo::ReloadFromDisk
} else {
SignalTo::ReloadComponents(std::collections::HashSet::new())
}
};
let outcome = tokio::time::timeout(
Duration::from_secs(10),
handle_signal(
Ok(signal),
&controller,
&paths,
&mut signals,
false,
opts.data_dir.as_deref(),
),
)
.await
.expect("topology reload timed out");
assert!(outcome.is_none());
let current = controller.lock().await;
// A retained data_dir alone is insufficient: the new topology must actually
// have replaced the old one, rather than rolling back a rejected reload.
assert!(
current
.topology
.config
.sink(&config::ComponentKey::from(replacement))
.is_some()
);
assert_eq!(
current.topology.config.global.data_dir.as_ref(),
Some(&override_dir)
);
}
let controller = controller
.try_into_inner()
.expect("no other controller owner")
.into_inner();
assert!(controller.stop().await);
#[allow(
clippy::print_stdout,
reason = "The parent verifies the isolated reload assertions ran."
)]
{
println!("topology reload assertions completed");
}
}
}
Loading
Loading