Skip to content

Commit bcaac70

Browse files
hhromicneuronull
authored andcommitted
feat(exec source): add support for customizing command environment (#18223)
* feat(exec source): add support for customizing command environment This commit adds support to the `exec` source for: * Setting or updating environment variables for the command before running. * Clearing all environment variables for the command before setting/updating custom ones (if any). The combination of both options allows to create a fully controlled environment for the command. Signed-off-by: Hugo Hromic <[email protected]> * Rename `clear_env` option to `clear_environment` * Add `configurable` macros for docs generation * Regenerate docs using vdev build component-docs * Add tests for the new options --------- Signed-off-by: Hugo Hromic <[email protected]>
1 parent d8cd2b1 commit bcaac70

File tree

2 files changed

+126
-0
lines changed
  • src/sources/exec
  • website/cue/reference/components/sources/base

2 files changed

+126
-0
lines changed

src/sources/exec/mod.rs

+102
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
collections::HashMap,
23
io::{Error, ErrorKind},
34
path::PathBuf,
45
process::ExitStatus,
@@ -61,6 +62,17 @@ pub struct ExecConfig {
6162
#[configurable(metadata(docs::examples = "echo", docs::examples = "Hello World!"))]
6263
pub command: Vec<String>,
6364

65+
/// Custom environment variables to set or update when running the command.
66+
/// If a variable name already exists in the environment, its value is replaced.
67+
#[serde(default)]
68+
#[configurable(metadata(docs::additional_props_description = "An environment variable."))]
69+
#[configurable(metadata(docs::examples = "environment_examples()"))]
70+
pub environment: Option<HashMap<String, String>>,
71+
72+
/// Whether or not to clear the environment before setting custom environment variables.
73+
#[serde(default = "default_clear_environment")]
74+
pub clear_environment: bool,
75+
6476
/// The directory in which to run the command.
6577
pub working_directory: Option<PathBuf>,
6678

@@ -141,6 +153,8 @@ impl Default for ExecConfig {
141153
}),
142154
streaming: None,
143155
command: vec!["echo".to_owned(), "Hello World!".to_owned()],
156+
environment: None,
157+
clear_environment: default_clear_environment(),
144158
working_directory: None,
145159
include_stderr: default_include_stderr(),
146160
maximum_buffer_size_bytes: default_maximum_buffer_size(),
@@ -168,10 +182,25 @@ const fn default_respawn_on_exit() -> bool {
168182
true
169183
}
170184

185+
const fn default_clear_environment() -> bool {
186+
false
187+
}
188+
171189
const fn default_include_stderr() -> bool {
172190
true
173191
}
174192

193+
fn environment_examples() -> HashMap<String, String> {
194+
HashMap::<_, _>::from_iter(
195+
[
196+
("LANG".to_owned(), "es_ES.UTF-8".to_owned()),
197+
("TZ".to_owned(), "Etc/UTC".to_owned()),
198+
("PATH".to_owned(), "/bin:/usr/bin:/usr/local/bin".to_owned()),
199+
]
200+
.into_iter(),
201+
)
202+
}
203+
175204
fn get_hostname() -> Option<String> {
176205
crate::get_hostname().ok()
177206
}
@@ -610,6 +639,16 @@ fn build_command(config: &ExecConfig) -> Command {
610639

611640
command.kill_on_drop(true);
612641

642+
// Clear environment variables if needed
643+
if config.clear_environment {
644+
command.env_clear();
645+
}
646+
647+
// Configure environment variables if needed
648+
if let Some(envs) = &config.environment {
649+
command.envs(envs);
650+
}
651+
613652
// Explicitly set the current dir if needed
614653
if let Some(current_dir) = &config.working_directory {
615654
command.current_dir(current_dir);
@@ -726,6 +765,7 @@ mod tests {
726765
use super::*;
727766
use crate::{event::LogEvent, test_util::trace_init};
728767
use bytes::Bytes;
768+
use std::ffi::OsStr;
729769
use std::io::Cursor;
730770
use vector_core::event::EventMetadata;
731771
use vrl::value;
@@ -900,6 +940,8 @@ mod tests {
900940
respawn_interval_secs: default_respawn_interval_secs(),
901941
}),
902942
command: vec!["./runner".to_owned(), "arg1".to_owned(), "arg2".to_owned()],
943+
environment: None,
944+
clear_environment: default_clear_environment(),
903945
working_directory: Some(PathBuf::from("/tmp")),
904946
include_stderr: default_include_stderr(),
905947
maximum_buffer_size_bytes: default_maximum_buffer_size(),
@@ -922,6 +964,64 @@ mod tests {
922964
assert_eq!(expected_command_string, command_string);
923965
}
924966

967+
#[test]
968+
fn test_build_command_custom_environment() {
969+
let config = ExecConfig {
970+
mode: Mode::Streaming,
971+
scheduled: None,
972+
streaming: Some(StreamingConfig {
973+
respawn_on_exit: default_respawn_on_exit(),
974+
respawn_interval_secs: default_respawn_interval_secs(),
975+
}),
976+
command: vec!["./runner".to_owned(), "arg1".to_owned(), "arg2".to_owned()],
977+
environment: Some(HashMap::from([("FOO".to_owned(), "foo".to_owned())])),
978+
clear_environment: default_clear_environment(),
979+
working_directory: Some(PathBuf::from("/tmp")),
980+
include_stderr: default_include_stderr(),
981+
maximum_buffer_size_bytes: default_maximum_buffer_size(),
982+
framing: None,
983+
decoding: default_decoding(),
984+
log_namespace: None,
985+
};
986+
987+
let command = build_command(&config);
988+
let cmd = command.as_std();
989+
990+
let idx = cmd
991+
.get_envs()
992+
.position(|v| v == (OsStr::new("FOO"), Some(OsStr::new("foo"))));
993+
994+
assert_ne!(idx, None);
995+
}
996+
997+
#[test]
998+
fn test_build_command_clear_environment() {
999+
let config = ExecConfig {
1000+
mode: Mode::Streaming,
1001+
scheduled: None,
1002+
streaming: Some(StreamingConfig {
1003+
respawn_on_exit: default_respawn_on_exit(),
1004+
respawn_interval_secs: default_respawn_interval_secs(),
1005+
}),
1006+
command: vec!["./runner".to_owned(), "arg1".to_owned(), "arg2".to_owned()],
1007+
environment: Some(HashMap::from([("FOO".to_owned(), "foo".to_owned())])),
1008+
clear_environment: true,
1009+
working_directory: Some(PathBuf::from("/tmp")),
1010+
include_stderr: default_include_stderr(),
1011+
maximum_buffer_size_bytes: default_maximum_buffer_size(),
1012+
framing: None,
1013+
decoding: default_decoding(),
1014+
log_namespace: None,
1015+
};
1016+
1017+
let command = build_command(&config);
1018+
let cmd = command.as_std();
1019+
1020+
let envs: Vec<_> = cmd.get_envs().collect();
1021+
1022+
assert_eq!(envs.len(), 1);
1023+
}
1024+
9251025
#[tokio::test]
9261026
async fn test_spawn_reader_thread() {
9271027
trace_init();
@@ -1112,6 +1212,8 @@ mod tests {
11121212
respawn_interval_secs: default_respawn_interval_secs(),
11131213
}),
11141214
command: vec!["yes".to_owned()],
1215+
environment: None,
1216+
clear_environment: default_clear_environment(),
11151217
working_directory: None,
11161218
include_stderr: default_include_stderr(),
11171219
maximum_buffer_size_bytes: default_maximum_buffer_size(),

website/cue/reference/components/sources/base/exec.cue

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package metadata
22

33
base: components: sources: exec: configuration: {
4+
clear_environment: {
5+
description: "Whether or not to clear the environment before setting custom environment variables."
6+
required: false
7+
type: bool: default: false
8+
}
49
command: {
510
description: "The command to run, plus any arguments required."
611
required: true
@@ -143,6 +148,25 @@ base: components: sources: exec: configuration: {
143148
}
144149
}
145150
}
151+
environment: {
152+
description: """
153+
Custom environment variables to set or update when running the command.
154+
If a variable name already exists in the environment, its value is replaced.
155+
"""
156+
required: false
157+
type: object: {
158+
examples: [{
159+
LANG: "es_ES.UTF-8"
160+
PATH: "/bin:/usr/bin:/usr/local/bin"
161+
TZ: "Etc/UTC"
162+
}]
163+
options: "*": {
164+
description: "An environment variable."
165+
required: true
166+
type: string: {}
167+
}
168+
}
169+
}
146170
framing: {
147171
description: """
148172
Framing configuration.

0 commit comments

Comments
 (0)