Skip to content

Commit df4921b

Browse files
authored
chore(core): Add a CLI flag to allow for empty configs (#19021)
* chore(core): Add a CLI flag to allow for empty configs This allows for users to set up an empty config that may later be replaced with actual running components. With this option enabled, Vector will not immediately shut down with an empty config, but it will follow the normal shutdown path when components are present. * Add reference docs * Fix tabs in cue docs * Tweak wording * Fix option typo
1 parent 9d006c7 commit df4921b

File tree

8 files changed

+59
-14
lines changed

8 files changed

+59
-14
lines changed

src/app.rs

+21-9
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub struct ApplicationConfig {
5454
}
5555

5656
pub struct Application {
57-
pub require_healthy: Option<bool>,
57+
pub root_opts: RootOpts,
5858
pub config: ApplicationConfig,
5959
pub signals: SignalPair,
6060
}
@@ -73,6 +73,7 @@ impl ApplicationConfig {
7373
&config_paths,
7474
opts.watch_config,
7575
opts.require_healthy,
76+
opts.allow_empty_config,
7677
graceful_shutdown_duration,
7778
signal_handler,
7879
)
@@ -211,7 +212,7 @@ impl Application {
211212
Ok((
212213
runtime,
213214
Self {
214-
require_healthy: opts.root.require_healthy,
215+
root_opts: opts.root,
215216
config,
216217
signals,
217218
},
@@ -227,7 +228,7 @@ impl Application {
227228
handle.spawn(heartbeat::heartbeat());
228229

229230
let Self {
230-
require_healthy,
231+
root_opts,
231232
config,
232233
signals,
233234
} = self;
@@ -237,7 +238,7 @@ impl Application {
237238
api_server: config.setup_api(handle),
238239
topology: config.topology,
239240
config_paths: config.config_paths.clone(),
240-
require_healthy,
241+
require_healthy: root_opts.require_healthy,
241242
#[cfg(feature = "enterprise")]
242243
enterprise_reporter: config.enterprise,
243244
});
@@ -248,6 +249,7 @@ impl Application {
248249
graceful_crash_receiver: config.graceful_crash_receiver,
249250
signals,
250251
topology_controller,
252+
allow_empty_config: root_opts.allow_empty_config,
251253
})
252254
}
253255
}
@@ -258,6 +260,7 @@ pub struct StartedApplication {
258260
pub graceful_crash_receiver: ShutdownErrorReceiver,
259261
pub signals: SignalPair,
260262
pub topology_controller: SharedTopologyController,
263+
pub allow_empty_config: bool,
261264
}
262265

263266
impl StartedApplication {
@@ -272,6 +275,7 @@ impl StartedApplication {
272275
signals,
273276
topology_controller,
274277
internal_topologies,
278+
allow_empty_config,
275279
} = self;
276280

277281
let mut graceful_crash = UnboundedReceiverStream::new(graceful_crash_receiver);
@@ -280,18 +284,20 @@ impl StartedApplication {
280284
let mut signal_rx = signals.receiver;
281285

282286
let signal = loop {
287+
let has_sources = !topology_controller.lock().await.topology.config.is_empty();
283288
tokio::select! {
284289
signal = signal_rx.recv() => if let Some(signal) = handle_signal(
285290
signal,
286291
&topology_controller,
287292
&config_paths,
288293
&mut signal_handler,
294+
allow_empty_config,
289295
).await {
290296
break signal;
291297
},
292298
// Trigger graceful shutdown if a component crashed, or all sources have ended.
293299
error = graceful_crash.next() => break SignalTo::Shutdown(error),
294-
_ = TopologyController::sources_finished(topology_controller.clone()) => {
300+
_ = TopologyController::sources_finished(topology_controller.clone()), if has_sources => {
295301
info!("All sources have finished.");
296302
break SignalTo::Shutdown(None)
297303
} ,
@@ -313,6 +319,7 @@ async fn handle_signal(
313319
topology_controller: &SharedTopologyController,
314320
config_paths: &[ConfigPath],
315321
signal_handler: &mut SignalHandler,
322+
allow_empty_config: bool,
316323
) -> Option<SignalTo> {
317324
match signal {
318325
Ok(SignalTo::ReloadFromConfigBuilder(config_builder)) => {
@@ -335,6 +342,7 @@ async fn handle_signal(
335342
let new_config = config::load_from_paths_with_provider_and_secrets(
336343
&topology_controller.config_paths,
337344
signal_handler,
345+
allow_empty_config,
338346
)
339347
.await
340348
.map_err(handle_config_errors)
@@ -479,6 +487,7 @@ pub async fn load_configs(
479487
config_paths: &[ConfigPath],
480488
watch_config: bool,
481489
require_healthy: Option<bool>,
490+
allow_empty_config: bool,
482491
graceful_shutdown_duration: Option<Duration>,
483492
signal_handler: &mut SignalHandler,
484493
) -> Result<Config, ExitCode> {
@@ -503,10 +512,13 @@ pub async fn load_configs(
503512
#[cfg(not(feature = "enterprise-tests"))]
504513
config::init_log_schema(&config_paths, true).map_err(handle_config_errors)?;
505514

506-
let mut config =
507-
config::load_from_paths_with_provider_and_secrets(&config_paths, signal_handler)
508-
.await
509-
.map_err(handle_config_errors)?;
515+
let mut config = config::load_from_paths_with_provider_and_secrets(
516+
&config_paths,
517+
signal_handler,
518+
allow_empty_config,
519+
)
520+
.await
521+
.map_err(handle_config_errors)?;
510522

511523
config::init_telemetry(config.global.telemetry.clone(), true);
512524

src/cli.rs

+7
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ pub struct RootOpts {
204204
/// default inherits the environment of the Vector process.
205205
#[arg(long, env = "VECTOR_OPENSSL_NO_PROBE", default_value = "false")]
206206
pub openssl_no_probe: bool,
207+
208+
/// Allow the configuration to run without any components. This is useful for loading in an
209+
/// empty stub config that will later be replaced with actual components. Note that this is
210+
/// likely not useful without also watching for config file changes as described in
211+
/// `--watch-config`.
212+
#[arg(long, env = "VECTOR_ALLOW_EMPTY_CONFIG", default_value = "false")]
213+
pub allow_empty_config: bool,
207214
}
208215

209216
impl RootOpts {

src/config/builder.rs

+6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ pub struct ConfigBuilder {
8383
#[serde(default, skip)]
8484
#[doc(hidden)]
8585
pub graceful_shutdown_duration: Option<Duration>,
86+
87+
/// Allow the configuration to be empty, resulting in a topology with no components.
88+
#[serde(default, skip)]
89+
#[doc(hidden)]
90+
pub allow_empty: bool,
8691
}
8792

8893
#[cfg(feature = "enterprise")]
@@ -232,6 +237,7 @@ impl From<Config> for ConfigBuilder {
232237
tests,
233238
secret,
234239
graceful_shutdown_duration,
240+
allow_empty: false,
235241
}
236242
}
237243
}

src/config/compiler.rs

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub fn compile(mut builder: ConfigBuilder) -> Result<(Config, Vec<String>), Vec<
5757
provider: _,
5858
secret,
5959
graceful_shutdown_duration,
60+
allow_empty: _,
6061
} = builder;
6162

6263
let graph = match Graph::new(&sources, &transforms, &sinks, schema) {

src/config/loading/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ pub fn load_from_paths(config_paths: &[ConfigPath]) -> Result<Config, Vec<String
133133
pub async fn load_from_paths_with_provider_and_secrets(
134134
config_paths: &[ConfigPath],
135135
signal_handler: &mut signal::SignalHandler,
136+
allow_empty: bool,
136137
) -> Result<Config, Vec<String>> {
137138
// Load secret backends first
138139
let (mut secrets_backends_loader, secrets_warning) =
@@ -149,6 +150,8 @@ pub async fn load_from_paths_with_provider_and_secrets(
149150
load_builder_from_paths(config_paths)?
150151
};
151152

153+
builder.allow_empty = allow_empty;
154+
152155
validation::check_provider(&builder)?;
153156
signal_handler.clear();
154157

src/config/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ impl Config {
127127
Default::default()
128128
}
129129

130+
pub fn is_empty(&self) -> bool {
131+
self.sources.is_empty()
132+
}
133+
130134
pub fn sources(&self) -> impl Iterator<Item = (&ComponentKey, &SourceOuter)> {
131135
self.sources.iter()
132136
}

src/config/validation.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ pub fn check_names<'a, I: Iterator<Item = &'a ComponentKey>>(names: I) -> Result
4444
pub fn check_shape(config: &ConfigBuilder) -> Result<(), Vec<String>> {
4545
let mut errors = vec![];
4646

47-
if config.sources.is_empty() {
48-
errors.push("No sources defined in the config.".to_owned());
49-
}
47+
if !config.allow_empty {
48+
if config.sources.is_empty() {
49+
errors.push("No sources defined in the config.".to_owned());
50+
}
5051

51-
if config.sinks.is_empty() {
52-
errors.push("No sinks defined in the config.".to_owned());
52+
if config.sinks.is_empty() {
53+
errors.push("No sinks defined in the config.".to_owned());
54+
}
5355
}
5456

5557
// Helper for below

website/cue/reference/cli.cue

+10
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ cli: {
117117
description: env_vars.VECTOR_OPENSSL_NO_PROBE.description
118118
env_var: "VECTOR_OPENSSL_NO_PROBE"
119119
}
120+
"allow-empty-config": {
121+
description: env_vars.VECTOR_ALLOW_EMPTY_CONFIG.description
122+
env_var: "VECTOR_ALLOW_EMPTY_CONFIG"
123+
}
120124
}
121125

122126
_core_config_options: {
@@ -636,6 +640,12 @@ cli: {
636640
"""
637641
type: bool: default: false
638642
}
643+
VECTOR_ALLOW_EMPTY_CONFIG: {
644+
description: """
645+
Allow the configuration to run without any components. This is useful for loading in an empty stub config that will later be replaced with actual components. Note that this is likely not useful without also watching for config file changes as described in `--watch-config`.
646+
"""
647+
type: bool: default: false
648+
}
639649
}
640650

641651
// Helpers

0 commit comments

Comments
 (0)