vector/sinks/webhdfs/
config.rs1use opendal::{Operator, layers::LoggingLayer, services::Webhdfs};
2use tower::ServiceBuilder;
3use vector_lib::{
4 codecs::{JsonSerializerConfig, NewlineDelimitedEncoderConfig, encoding::Framer},
5 config::{AcknowledgementsConfig, DataType, Input},
6 configurable::configurable_component,
7 sink::VectorSink,
8};
9
10use crate::{
11 codecs::{Encoder, EncodingConfigWithFraming, SinkType},
12 config::{GenerateConfig, SinkConfig, SinkContext},
13 sinks::{
14 Healthcheck,
15 opendal_common::*,
16 util::{
17 BatchConfig, BulkSizeBasedDefaultBatchSettings, Compression,
18 partitioner::KeyPartitioner,
19 },
20 },
21 template::{ConfinementConfig, Template},
22};
23
24#[configurable_component(sink("webhdfs", "WebHDFS."))]
26#[derive(Clone, Debug)]
27#[serde(deny_unknown_fields)]
28pub struct WebHdfsConfig {
29 #[serde(default)]
35 pub root: String,
36
37 #[serde(default)]
45 #[configurable(metadata(docs::templateable))]
46 pub prefix: String,
47
48 #[serde(default)]
56 #[configurable(metadata(docs::examples = "http://127.0.0.1:9870"))]
57 pub endpoint: String,
58
59 #[serde(flatten)]
60 pub encoding: EncodingConfigWithFraming,
61
62 #[configurable(derived)]
63 #[serde(default = "Compression::gzip_default")]
64 pub compression: Compression,
65
66 #[configurable(derived)]
67 #[serde(default)]
68 pub batch: BatchConfig<BulkSizeBasedDefaultBatchSettings>,
69
70 #[configurable(derived)]
71 #[serde(
72 default,
73 deserialize_with = "crate::serde::bool_or_struct",
74 skip_serializing_if = "crate::serde::is_default"
75 )]
76 pub acknowledgements: AcknowledgementsConfig,
77
78 #[serde(flatten)]
79 pub confinement: ConfinementConfig,
80}
81
82impl GenerateConfig for WebHdfsConfig {
83 fn generate_config() -> toml::Value {
84 toml::Value::try_from(Self {
85 root: "/".to_string(),
86 prefix: "%F/".to_string(),
87 endpoint: "http://127.0.0.1:9870".to_string(),
88
89 encoding: (
90 Some(NewlineDelimitedEncoderConfig::new()),
91 JsonSerializerConfig::default(),
92 )
93 .into(),
94 compression: Compression::gzip_default(),
95 batch: BatchConfig::default(),
96
97 acknowledgements: Default::default(),
98 confinement: ConfinementConfig::default(),
99 })
100 .unwrap()
101 }
102}
103
104#[async_trait::async_trait]
105#[typetag::serde(name = "webhdfs")]
106impl SinkConfig for WebHdfsConfig {
107 async fn build(&self, _cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
108 let op = self.build_operator()?;
109
110 let check_op = op.clone();
111 let healthcheck = Box::pin(async move { Ok(check_op.check().await?) });
112
113 let sink = self.build_processor(op)?;
114 self.confinement.set_confinement_gauge("sink", Self::NAME);
115 Ok((sink, healthcheck))
116 }
117
118 fn input(&self) -> Input {
119 Input::new(self.encoding.config().1.input_type() & DataType::Log)
120 }
121
122 fn acknowledgements(&self) -> &AcknowledgementsConfig {
123 &self.acknowledgements
124 }
125}
126
127impl WebHdfsConfig {
128 pub fn build_operator(&self) -> crate::Result<Operator> {
129 let mut builder = Webhdfs::default();
131 builder = builder.root(&self.root);
133 builder = builder.endpoint(&self.endpoint);
134
135 let op = Operator::new(builder)?
136 .layer(LoggingLayer::default())
137 .finish();
138 Ok(op)
139 }
140
141 pub fn build_processor(&self, op: Operator) -> crate::Result<VectorSink> {
142 let batcher_settings = self.batch.into_batcher_settings()?;
144
145 let transformer = self.encoding.transformer();
146 let (framer, serializer) = self.encoding.build(SinkType::MessageBased)?;
147 let encoder = Encoder::<Framer>::new(framer, serializer);
148
149 let request_builder = OpenDalRequestBuilder {
150 encoder: (transformer, encoder),
151 compression: self.compression,
152 };
153
154 let svc = ServiceBuilder::new().service(OpenDalService::new(op));
156
157 let sink = OpenDalSink::new(
158 svc,
159 request_builder,
160 self.key_partitioner()?,
161 batcher_settings,
162 );
163
164 Ok(VectorSink::from_event_streamsink(sink))
165 }
166
167 pub fn key_partitioner(&self) -> crate::Result<KeyPartitioner> {
168 let prefix: Template = self.prefix.clone().try_into()?;
169 let prefix = prefix.confine(&self.confinement, Self::NAME, "prefix")?;
170 Ok(KeyPartitioner::new(prefix, None))
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn generate_config() {
180 crate::test_util::test_generate_config::<WebHdfsConfig>();
181 }
182
183 fn base_config() -> WebHdfsConfig {
184 WebHdfsConfig {
185 root: "/tmp/test/".into(),
186 prefix: String::new(),
187 endpoint: "http://127.0.0.1:9870".into(),
188 encoding: (
189 None::<vector_lib::codecs::encoding::FramingConfig>,
190 vector_lib::codecs::TextSerializerConfig::default(),
191 )
192 .into(),
193 compression: crate::sinks::util::Compression::None,
194 batch: Default::default(),
195 acknowledgements: Default::default(),
196 confinement: ConfinementConfig::default(),
197 }
198 }
199
200 #[test]
201 fn confinement_rejects_unconfined_prefix() {
202 let config = WebHdfsConfig {
203 prefix: "{{ tenant }}".into(),
204 ..base_config()
205 };
206 match config.key_partitioner() {
207 Err(err) => assert!(
208 err.to_string().contains("no literal string prefix"),
209 "unexpected error: {err}"
210 ),
211 Ok(_) => panic!("expected confinement error"),
212 }
213 }
214
215 #[test]
216 fn confinement_opt_out_allows_unconfined_prefix() {
217 let config = WebHdfsConfig {
218 prefix: "{{ tenant }}".into(),
219 confinement: ConfinementConfig {
220 dangerously_allow_unconfined_template_resolution: true,
221 },
222 ..base_config()
223 };
224 assert!(config.key_partitioner().is_ok());
225 }
226
227 #[test]
228 fn confinement_blocks_dotdot_escape_at_render() {
229 use crate::event::Event;
230 use vector_lib::event::LogEvent;
231 use vector_lib::partition::Partitioner;
232 use vrl::event_path;
233
234 let config = WebHdfsConfig {
235 prefix: "safe/{{ tenant }}/".into(),
236 ..base_config()
237 };
238 let partitioner = config.key_partitioner().unwrap();
239 let mut event = Event::Log(LogEvent::from("x"));
240 event
241 .as_mut_log()
242 .insert(event_path!("tenant"), "../../escape");
243 assert!(partitioner.partition(&event).is_none());
244 }
245}