Skip to content

Commit 91245a4

Browse files
authored
feat(config): add wildcard_matching config option (#23011)
* add relaxed_wildcart_matching config option * allow empty results for glob inputs if relaxed_wildcard_matching is enabled * optimize condition * fix fmt * fix clippy * remove option * fix fmt * Revert "remove option" This reverts commit 901cf08. * generate docs * add changelog fragment * fix typo * add tests * add config example to changelog * use enum instead of boolean * regenerate docs * update changelog
1 parent 9daa00c commit 91245a4

File tree

9 files changed

+156
-13
lines changed

9 files changed

+156
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Added `wildcard_matching` global config option to set wildcard matching mode for inputs. Relaxed mode allows configurations with wildcards that do not match any inputs to be accepted without causing an error.
2+
3+
Example config:
4+
5+
```yaml
6+
wildcard_matching: relaxed
7+
8+
sources:
9+
stdin:
10+
type: stdin
11+
12+
# note - no transforms
13+
14+
sinks:
15+
stdout:
16+
type: console
17+
encoding:
18+
codec: json
19+
inputs:
20+
- "runtime-added-transform-*"
21+
22+
```
23+
24+
authors: simplepad

lib/vector-core/src/config/global_options.rs

+29
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ pub(crate) enum DataDirError {
3131
},
3232
}
3333

34+
/// Specifies the wildcard matching mode, relaxed allows configurations where wildcard doesn not match any existing inputs
35+
#[configurable_component]
36+
#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
37+
#[serde(rename_all = "lowercase")]
38+
pub enum WildcardMatching {
39+
/// Strict matching (must match at least one existing input)
40+
#[default]
41+
Strict,
42+
43+
/// Relaxed matching (must match 0 or more inputs)
44+
Relaxed,
45+
}
46+
3447
/// Global configuration options.
3548
//
3649
// If this is modified, make sure those changes are reflected in the `ConfigBuilder::append`
@@ -48,6 +61,14 @@ pub struct GlobalOptions {
4861
#[configurable(metadata(docs::common = false))]
4962
pub data_dir: Option<PathBuf>,
5063

64+
/// Set wildcard matching mode for inputs
65+
///
66+
/// Setting this to "relaxed" allows configurations with wildcards that do not match any inputs
67+
/// to be accepted without causing an error.
68+
#[serde(skip_serializing_if = "crate::serde::is_default")]
69+
#[configurable(metadata(docs::common = false, docs::required = false))]
70+
pub wildcard_matching: Option<WildcardMatching>,
71+
5172
/// Default log schema for all events.
5273
///
5374
/// This is used if a component does not have its own specific log schema. All events use a log
@@ -182,6 +203,13 @@ impl GlobalOptions {
182203
pub fn merge(&self, with: Self) -> Result<Self, Vec<String>> {
183204
let mut errors = Vec::new();
184205

206+
if conflicts(
207+
self.wildcard_matching.as_ref(),
208+
with.wildcard_matching.as_ref(),
209+
) {
210+
errors.push("conflicting values for 'wildcard_matching' found".to_owned());
211+
}
212+
185213
if conflicts(self.proxy.http.as_ref(), with.proxy.http.as_ref()) {
186214
errors.push("conflicting values for 'proxy.http' found".to_owned());
187215
}
@@ -250,6 +278,7 @@ impl GlobalOptions {
250278
if errors.is_empty() {
251279
Ok(Self {
252280
data_dir,
281+
wildcard_matching: self.wildcard_matching.or(with.wildcard_matching),
253282
log_schema,
254283
telemetry,
255284
acknowledgements: self.acknowledgements.merge_default(&with.acknowledgements),

lib/vector-core/src/config/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub mod proxy;
1313
mod telemetry;
1414

1515
use crate::event::LogEvent;
16-
pub use global_options::GlobalOptions;
16+
pub use global_options::{GlobalOptions, WildcardMatching};
1717
pub use log_schema::{init_log_schema, log_schema, LogSchema};
1818
use lookup::{lookup_v2::ValuePath, path, PathPrefix};
1919
pub use output_id::OutputId;

lib/vector-lib/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub mod config {
3434
clone_input_definitions, init_log_schema, init_telemetry, log_schema, proxy, telemetry,
3535
AcknowledgementsConfig, DataType, GlobalOptions, Input, LegacyKey, LogNamespace, LogSchema,
3636
OutputId, SourceAcknowledgementsConfig, SourceOutput, Tags, Telemetry, TransformOutput,
37-
MEMORY_BUFFER_DEFAULT_MAX_EVENTS,
37+
WildcardMatching, MEMORY_BUFFER_DEFAULT_MAX_EVENTS,
3838
};
3939
}
4040

src/config/compiler.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ pub fn compile(mut builder: ConfigBuilder) -> Result<(Config, Vec<String>), Vec<
7171
)
7272
.collect::<IndexMap<_, _>>();
7373

74-
let graph = match Graph::new(&sources_and_table_sources, &transforms, &all_sinks, schema) {
74+
let graph = match Graph::new(
75+
&sources_and_table_sources,
76+
&transforms,
77+
&all_sinks,
78+
schema,
79+
global.wildcard_matching.unwrap_or_default(),
80+
) {
7581
Ok(graph) => graph,
7682
Err(graph_errors) => {
7783
errors.extend(graph_errors);

src/config/graph.rs

+73-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{
22
schema, ComponentKey, DataType, OutputId, SinkOuter, SourceOuter, SourceOutput, TransformOuter,
3-
TransformOutput,
3+
TransformOutput, WildcardMatching,
44
};
55
use indexmap::{set::IndexSet, IndexMap};
66
use std::collections::{HashMap, HashSet, VecDeque};
@@ -65,17 +65,20 @@ impl Graph {
6565
transforms: &IndexMap<ComponentKey, TransformOuter<String>>,
6666
sinks: &IndexMap<ComponentKey, SinkOuter<String>>,
6767
schema: schema::Options,
68+
wildcard_matching: WildcardMatching,
6869
) -> Result<Self, Vec<String>> {
69-
Self::new_inner(sources, transforms, sinks, false, schema)
70+
Self::new_inner(sources, transforms, sinks, false, schema, wildcard_matching)
7071
}
7172

7273
pub fn new_unchecked(
7374
sources: &IndexMap<ComponentKey, SourceOuter>,
7475
transforms: &IndexMap<ComponentKey, TransformOuter<String>>,
7576
sinks: &IndexMap<ComponentKey, SinkOuter<String>>,
7677
schema: schema::Options,
78+
wildcard_matching: WildcardMatching,
7779
) -> Self {
78-
Self::new_inner(sources, transforms, sinks, true, schema).expect("errors ignored")
80+
Self::new_inner(sources, transforms, sinks, true, schema, wildcard_matching)
81+
.expect("errors ignored")
7982
}
8083

8184
fn new_inner(
@@ -84,6 +87,7 @@ impl Graph {
8487
sinks: &IndexMap<ComponentKey, SinkOuter<String>>,
8588
ignore_errors: bool,
8689
schema: schema::Options,
90+
wildcard_matching: WildcardMatching,
8791
) -> Result<Self, Vec<String>> {
8892
let mut graph = Graph::default();
8993
let mut errors = Vec::new();
@@ -127,15 +131,15 @@ impl Graph {
127131

128132
for (id, config) in transforms.iter() {
129133
for input in config.inputs.iter() {
130-
if let Err(e) = graph.add_input(input, id, &available_inputs) {
134+
if let Err(e) = graph.add_input(input, id, &available_inputs, wildcard_matching) {
131135
errors.push(e);
132136
}
133137
}
134138
}
135139

136140
for (id, config) in sinks {
137141
for input in config.inputs.iter() {
138-
if let Err(e) = graph.add_input(input, id, &available_inputs) {
142+
if let Err(e) = graph.add_input(input, id, &available_inputs, wildcard_matching) {
139143
errors.push(e);
140144
}
141145
}
@@ -153,6 +157,7 @@ impl Graph {
153157
from: &str,
154158
to: &ComponentKey,
155159
available_inputs: &HashMap<String, OutputId>,
160+
wildcard_matching: WildcardMatching,
156161
) -> Result<(), String> {
157162
if let Some(output_id) = available_inputs.get(from) {
158163
self.edges.push(Edge {
@@ -166,6 +171,18 @@ impl Graph {
166171
Some(Node::Sink { .. }) => "sink",
167172
_ => panic!("only transforms and sinks have inputs"),
168173
};
174+
// allow empty result if relaxed wildcard matching is enabled
175+
match wildcard_matching {
176+
WildcardMatching::Relaxed => {
177+
// using value != glob::Pattern::escape(value) to check if value is a glob
178+
// TODO: replace with proper check when https://github.com/rust-lang/glob/issues/72 is resolved
179+
if from != glob::Pattern::escape(from) {
180+
info!("Input \"{from}\" for {output_type} \"{to}\" didn’t match any components, but this was ignored because `relaxed_wildcard_matching` is enabled.");
181+
return Ok(());
182+
}
183+
}
184+
WildcardMatching::Strict => {}
185+
}
169186
info!(
170187
"Available components:\n{}",
171188
self.nodes
@@ -472,9 +489,14 @@ mod test {
472489
}
473490
}
474491

475-
fn test_add_input(&mut self, node: &str, input: &str) -> Result<(), String> {
492+
fn test_add_input(
493+
&mut self,
494+
node: &str,
495+
input: &str,
496+
wildcard_matching: WildcardMatching,
497+
) -> Result<(), String> {
476498
let available_inputs = self.input_map().unwrap();
477-
self.add_input(input, &node.into(), &available_inputs)
499+
self.add_input(input, &node.into(), &available_inputs, wildcard_matching)
478500
}
479501
}
480502

@@ -655,14 +677,22 @@ mod test {
655677
// make sure we're good with dotted paths
656678
assert_eq!(
657679
Ok(()),
658-
graph.test_add_input("errored_log_sink", "log_to_log.errors")
680+
graph.test_add_input(
681+
"errored_log_sink",
682+
"log_to_log.errors",
683+
WildcardMatching::Strict
684+
)
659685
);
660686

661687
// make sure that we're not cool with an unknown dotted path
662688
let expected = "Input \"log_to_log.not_errors\" for sink \"bad_log_sink\" doesn't match any components.".to_string();
663689
assert_eq!(
664690
Err(expected),
665-
graph.test_add_input("bad_log_sink", "log_to_log.not_errors")
691+
graph.test_add_input(
692+
"bad_log_sink",
693+
"log_to_log.not_errors",
694+
WildcardMatching::Strict
695+
)
666696
);
667697
}
668698

@@ -745,6 +775,40 @@ mod test {
745775
);
746776
}
747777

778+
#[test]
779+
fn wildcard_matching() {
780+
let mut graph = Graph::default();
781+
graph.add_source("log_source", DataType::Log);
782+
783+
// don't add inputs to these yet since they're not validated via these helpers
784+
graph.add_sink("sink", DataType::Log, vec![]);
785+
786+
// make sure we're not good with non existing inputs with relaxed wildcard matching disabled
787+
let wildcard_matching = WildcardMatching::Strict;
788+
let expected =
789+
"Input \"bad_source-*\" for sink \"sink\" doesn't match any components.".to_string();
790+
assert_eq!(
791+
Err(expected),
792+
graph.test_add_input("sink", "bad_source-*", wildcard_matching)
793+
);
794+
795+
// make sure we're good with non existing inputs with relaxed wildcard matching enabled
796+
let wildcard_matching = WildcardMatching::Relaxed;
797+
assert_eq!(
798+
Ok(()),
799+
graph.test_add_input("sink", "bad_source-*", wildcard_matching)
800+
);
801+
802+
// make sure we're not good with non existing inputs that are not wildcards even when relaxed wildcard matching is enabled
803+
let wildcard_matching = WildcardMatching::Relaxed;
804+
let expected =
805+
"Input \"bad_source-1\" for sink \"sink\" doesn't match any components.".to_string();
806+
assert_eq!(
807+
Err(expected),
808+
graph.test_add_input("sink", "bad_source-1", wildcard_matching)
809+
);
810+
}
811+
748812
#[test]
749813
fn paths_to_sink_simple() {
750814
let mut graph = Graph::default();

src/config/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use serde::Serialize;
2121
use vector_config::configurable_component;
2222
pub use vector_lib::config::{
2323
AcknowledgementsConfig, DataType, GlobalOptions, Input, LogNamespace,
24-
SourceAcknowledgementsConfig, SourceOutput, TransformOutput,
24+
SourceAcknowledgementsConfig, SourceOutput, TransformOutput, WildcardMatching,
2525
};
2626
pub use vector_lib::configurable::component::{
2727
GenerateConfig, SinkDescription, TransformDescription,

src/config/unit_test/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ async fn build_unit_test(
385385
&transform_only_config.transforms,
386386
&transform_only_config.sinks,
387387
transform_only_config.schema,
388+
transform_only_config
389+
.global
390+
.wildcard_matching
391+
.unwrap_or_default(),
388392
);
389393
let test = test.resolve_outputs(&transform_only_graph)?;
390394

@@ -401,6 +405,7 @@ async fn build_unit_test(
401405
&config_builder.transforms,
402406
&config_builder.sinks,
403407
config_builder.schema,
408+
config_builder.global.wildcard_matching.unwrap_or_default(),
404409
);
405410

406411
let mut valid_components = get_relevant_test_components(
@@ -432,6 +437,7 @@ async fn build_unit_test(
432437
&config_builder.transforms,
433438
&config_builder.sinks,
434439
config_builder.schema,
440+
config_builder.global.wildcard_matching.unwrap_or_default(),
435441
);
436442
let valid_inputs = graph.input_map()?;
437443
for (_, transform) in config_builder.transforms.iter_mut() {

website/cue/reference/base/configuration.cue

+14
Original file line numberDiff line numberDiff line change
@@ -901,4 +901,18 @@ base: configuration: configuration: {
901901
required: false
902902
type: string: examples: ["local", "America/New_York", "EST5EDT"]
903903
}
904+
wildcard_matching: {
905+
common: false
906+
description: """
907+
Set wildcard matching mode for inputs
908+
909+
Setting this to "relaxed" allows configurations with wildcards that do not match any inputs
910+
to be accepted without causing an error.
911+
"""
912+
required: false
913+
type: string: enum: {
914+
relaxed: "Relaxed matching (must match 0 or more inputs)"
915+
strict: "Strict matching (must match at least one existing input)"
916+
}
917+
}
904918
}

0 commit comments

Comments
 (0)