Skip to content

Commit 1601ff2

Browse files
authored
fix(vdev): volume path resolution (#23637)
* fix(vdev): volume path resolution * push env var resolution to the forefront * ran cargo fmt * rename to environment.rs * ran cargo fmt * clippy fixes * refactor * unix only - fix windows clippy
1 parent abcb5a7 commit 1601ff2

File tree

10 files changed

+146
-61
lines changed

10 files changed

+146
-61
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vdev/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ toml.workspace = true
3737
semver.workspace = true
3838
indoc.workspace = true
3939
git2 = { version = "0.20.2" }
40+
cfg-if.workspace = true

vdev/src/commands/compose_tests/show.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use crate::environment::Environment;
2+
use crate::testing::{config::ComposeTestConfig, state};
13
use anyhow::Result;
24

3-
use crate::testing::{config::ComposeTestConfig, config::Environment, state};
4-
55
pub fn exec(integration: Option<&String>, path: &str) -> Result<()> {
66
match integration {
77
None => show_all(path),

vdev/src/environment.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use cfg_if::cfg_if;
2+
use std::collections::BTreeMap;
3+
use std::process::Command;
4+
5+
cfg_if! {
6+
if #[cfg(unix)] {
7+
use std::sync::OnceLock;
8+
use regex::{Captures, Regex};
9+
}
10+
}
11+
12+
pub type Environment = BTreeMap<String, Option<String>>;
13+
14+
pub(crate) fn rename_environment_keys(environment: &Environment) -> Environment {
15+
environment
16+
.iter()
17+
.map(|(var, value)| {
18+
(
19+
format!("CONFIG_{}", var.replace('-', "_").to_uppercase()),
20+
value.clone(),
21+
)
22+
})
23+
.collect()
24+
}
25+
26+
pub(crate) fn extract_present(environment: &Environment) -> BTreeMap<String, String> {
27+
environment
28+
.iter()
29+
.filter_map(|(k, v)| v.as_ref().map(|s| (k.clone(), s.clone())))
30+
.collect()
31+
}
32+
33+
pub(crate) fn append_environment_variables(command: &mut Command, environment: &Environment) {
34+
for (key, value) in environment {
35+
command.arg("--env");
36+
match value {
37+
Some(value) => command.arg(format!("{key}={value}")),
38+
None => command.arg(key),
39+
};
40+
}
41+
}
42+
43+
cfg_if! {
44+
if #[cfg(unix)] {
45+
/// Resolve all environment variable placeholders. If the variable is not found or is `None`, it is left unchanged.
46+
pub fn resolve_placeholders(input: &str, environment: &Environment) -> String {
47+
static BRACED: OnceLock<Regex> = OnceLock::new();
48+
static BARE: OnceLock<Regex> = OnceLock::new();
49+
50+
let braced =
51+
BRACED.get_or_init(|| Regex::new(r"\$\{([A-Za-z0-9_]+)\}").expect("cannot build regex"));
52+
let bare = BARE.get_or_init(|| Regex::new(r"\$([A-Za-z0-9_]+)").expect("cannot build regex"));
53+
54+
// First replace ${VAR}
55+
let step1 = braced.replace_all(input, |captures: &Captures| {
56+
resolve_or_keep(&captures[0], &captures[1], environment)
57+
});
58+
59+
// Then replace $VAR
60+
bare.replace_all(&step1, |captures: &Captures| {
61+
resolve_or_keep(&captures[0], &captures[1], environment)
62+
})
63+
.into_owned()
64+
}
65+
66+
#[cfg(unix)]
67+
fn resolve_or_keep(full: &str, name: &str, env: &Environment) -> String {
68+
env.get(name)
69+
.and_then(Clone::clone)
70+
.or_else(|| std::env::var(name).ok())
71+
.unwrap_or_else(|| full.to_string())
72+
}
73+
}
74+
}

vdev/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod macros;
1111
mod app;
1212
mod commands;
1313
mod config;
14+
mod environment;
1415
mod features;
1516
mod git;
1617
mod platform;
@@ -19,9 +20,8 @@ mod util;
1920

2021
use anyhow::Result;
2122
use clap::Parser;
22-
use std::env;
23-
2423
use commands::Cli;
24+
use std::env;
2525

2626
fn main() -> Result<()> {
2727
let cli = Cli::parse();

vdev/src/testing/build.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::app;
22
use crate::app::CommandExt;
3+
use crate::environment::{Environment, extract_present};
34
use crate::testing::config::RustToolchainConfig;
45
use crate::testing::docker::docker_command;
56
use crate::util::IS_A_TTY;
@@ -17,20 +18,21 @@ pub fn prepare_build_command(
1718
image: &str,
1819
dockerfile: &Path,
1920
features: Option<&[String]>,
21+
config_environment_variables: &Environment,
2022
) -> Command {
2123
// Start with `docker build`
22-
let mut cmd = docker_command(["build"]);
24+
let mut command = docker_command(["build"]);
2325

2426
// Ensure we run from the repo root (so `.` context is correct)
25-
cmd.current_dir(app::path());
27+
command.current_dir(app::path());
2628

2729
// If we're attached to a TTY, show fancy progress
2830
if *IS_A_TTY {
29-
cmd.args(["--progress", "tty"]);
31+
command.args(["--progress", "tty"]);
3032
}
3133

3234
// Add all of the flags in one go
33-
cmd.args([
35+
command.args([
3436
"--pull",
3537
"--tag",
3638
image,
@@ -42,10 +44,12 @@ pub fn prepare_build_command(
4244
&format!("RUST_VERSION={}", RustToolchainConfig::rust_version()),
4345
"--build-arg",
4446
&format!("FEATURES={}", features.unwrap_or(&[]).join(",")),
45-
".",
4647
]);
4748

48-
cmd
49+
command.envs(extract_present(config_environment_variables));
50+
51+
command.args(["."]);
52+
command
4953
}
5054

5155
#[allow(dead_code)]
@@ -59,6 +63,7 @@ pub fn build_integration_image() -> Result<()> {
5963
&image,
6064
&dockerfile,
6165
Some(&[ALL_INTEGRATIONS_FEATURE_FLAG.to_string()]),
66+
&Environment::default(),
6267
);
6368
waiting!("Building {image}");
6469
cmd.check_run()

vdev/src/testing/config.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ use itertools::{self, Itertools};
88
use serde::{Deserialize, Serialize};
99
use serde_yaml::Value;
1010

11+
use crate::environment::Environment;
1112
use crate::{app, util};
1213

1314
const FILE_NAME: &str = "test.yaml";
1415

1516
pub const INTEGRATION_TESTS_DIR: &str = "integration";
1617
pub const E2E_TESTS_DIR: &str = "e2e";
1718

18-
pub type Environment = BTreeMap<String, Option<String>>;
19-
2019
#[derive(Deserialize, Debug)]
2120
pub struct RustToolchainRootConfig {
2221
pub toolchain: RustToolchainConfig,

vdev/src/testing/integration.rs

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ use anyhow::{Context, Result, bail};
44
use tempfile::{Builder, NamedTempFile};
55

66
use super::config::{
7-
ComposeConfig, ComposeTestConfig, E2E_TESTS_DIR, Environment, INTEGRATION_TESTS_DIR,
8-
RustToolchainConfig,
7+
ComposeConfig, ComposeTestConfig, E2E_TESTS_DIR, INTEGRATION_TESTS_DIR, RustToolchainConfig,
98
};
109
use super::runner::{ContainerTestRunner as _, IntegrationTestRunner, TestRunner as _};
1110
use super::state::EnvsDir;
1211
use crate::app::CommandExt as _;
12+
use crate::environment::{Environment, extract_present, rename_environment_keys};
1313
use crate::testing::build::ALL_INTEGRATIONS_FEATURE_FLAG;
1414
use crate::testing::docker::{CONTAINER_TOOL, DOCKER_SOCKET};
1515

@@ -103,7 +103,7 @@ impl ComposeTest {
103103
envs_dir,
104104
runner,
105105
compose,
106-
env_config,
106+
env_config: rename_environment_keys(&env_config),
107107
build_all,
108108
retries,
109109
};
@@ -121,8 +121,8 @@ impl ComposeTest {
121121

122122
let mut env_vars = self.config.env.clone();
123123
// Make sure the test runner has the same config environment vars as the services do.
124-
for (key, value) in config_env(&self.env_config) {
125-
env_vars.insert(key, Some(value));
124+
for (key, value) in self.env_config.clone() {
125+
env_vars.insert(key, value);
126126
}
127127

128128
env_vars.insert("TEST_LOG".to_string(), Some("info".into()));
@@ -181,8 +181,11 @@ impl ComposeTest {
181181
// image for the runner. So we must build that image before starting the
182182
// compose so that it is available.
183183
if self.local_config.kind == ComposeTestKind::E2E {
184-
self.runner
185-
.build(Some(&self.config.features), self.local_config.directory)?;
184+
self.runner.build(
185+
Some(&self.config.features),
186+
self.local_config.directory,
187+
&self.env_config,
188+
)?;
186189
}
187190

188191
self.config.check_required()?;
@@ -275,9 +278,11 @@ impl Compose {
275278
}
276279
}
277280

278-
fn start(&self, config: &Environment) -> Result<()> {
279-
self.prepare()?;
280-
self.run("Starting", &["up", "--detach"], Some(config))
281+
fn start(&self, environment: &Environment) -> Result<()> {
282+
#[cfg(unix)]
283+
unix::prepare_compose_volumes(&self.config, &self.test_dir, environment)?;
284+
285+
self.run("Starting", &["up", "--detach"], Some(environment))
281286
}
282287

283288
fn stop(&self) -> Result<()> {
@@ -289,7 +294,12 @@ impl Compose {
289294
)
290295
}
291296

292-
fn run(&self, action: &str, args: &[&'static str], config: Option<&Environment>) -> Result<()> {
297+
fn run(
298+
&self,
299+
action: &str,
300+
args: &[&'static str],
301+
environment: Option<&Environment>,
302+
) -> Result<()> {
293303
let mut command = Command::new(CONTAINER_TOOL.clone());
294304
command.arg("compose");
295305
// When the integration test environment is already active, the tempfile path does not
@@ -320,30 +330,13 @@ impl Compose {
320330
command.env(key, value);
321331
}
322332
}
323-
if let Some(config) = config {
324-
command.envs(config_env(config));
333+
if let Some(environment) = environment {
334+
command.envs(extract_present(environment));
325335
}
326336

327337
waiting!("{action} service environment");
328338
command.check_run()
329339
}
330-
331-
fn prepare(&self) -> Result<()> {
332-
#[cfg(unix)]
333-
unix::prepare_compose_volumes(&self.config, &self.test_dir)?;
334-
Ok(())
335-
}
336-
}
337-
338-
fn config_env(config: &Environment) -> impl Iterator<Item = (String, String)> + '_ {
339-
config.iter().filter_map(|(var, value)| {
340-
value.as_ref().map(|value| {
341-
(
342-
format!("CONFIG_{}", var.replace('-', "_").to_uppercase()),
343-
value.to_string(),
344-
)
345-
})
346-
})
347340
}
348341

349342
#[cfg(unix)]
@@ -353,6 +346,7 @@ mod unix {
353346
use std::path::{Path, PathBuf};
354347

355348
use super::super::config::ComposeConfig;
349+
use crate::environment::{Environment, resolve_placeholders};
356350
use crate::testing::config::VolumeMount;
357351
use anyhow::{Context, Result};
358352

@@ -362,7 +356,11 @@ mod unix {
362356
const ALL_READ_DIR: u32 = 0o555;
363357

364358
/// Fix up potential issues before starting a compose container
365-
pub fn prepare_compose_volumes(config: &ComposeConfig, test_dir: &Path) -> Result<()> {
359+
pub fn prepare_compose_volumes(
360+
config: &ComposeConfig,
361+
test_dir: &Path,
362+
environment: &Environment,
363+
) -> Result<()> {
366364
for service in config.services.values() {
367365
if let Some(volumes) = &service.volumes {
368366
for volume in volumes {
@@ -374,12 +372,12 @@ mod unix {
374372
}
375373
VolumeMount::Long { source, .. } => source,
376374
};
377-
378-
if !config.volumes.contains_key(source)
375+
let source = resolve_placeholders(source, environment);
376+
if !config.volumes.contains_key(&source)
379377
&& !source.starts_with('/')
380378
&& !source.starts_with('$')
381379
{
382-
let path: PathBuf = [test_dir, Path::new(source)].iter().collect();
380+
let path: PathBuf = [test_dir, Path::new(&source)].iter().collect();
383381
add_read_permission(&path)?;
384382
}
385383
}

0 commit comments

Comments
 (0)