Skip to content

Commit a0fd1c3

Browse files
use sdk container_args for provision commands
1 parent ab02d93 commit a0fd1c3

File tree

2 files changed

+200
-17
lines changed

2 files changed

+200
-17
lines changed

src/commands/provision.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,15 @@ impl ProvisionCommand {
4747
// Load config to access provision profiles
4848
let config = crate::utils::config::Config::load(&self.config.config_path)?;
4949

50-
// Merge provision profile container args with CLI container args
51-
let merged_container_args = config.merge_provision_container_args(
52-
self.config.provision_profile.as_deref(),
53-
self.config.container_args.as_ref(),
54-
);
55-
5650
// Get state file path from provision profile if available
5751
let state_file = self
5852
.config
5953
.provision_profile
6054
.as_ref()
6155
.map(|profile| config.get_provision_state_file(profile));
6256

57+
// Pass raw CLI container_args - RuntimeProvisionCommand will handle merging
58+
// with SDK and provision profile args to avoid double-merging
6359
let mut runtime_provision_cmd = RuntimeProvisionCommand::new(
6460
crate::commands::runtime::provision::RuntimeProvisionConfig {
6561
runtime_name: self.config.runtime.clone(),
@@ -70,7 +66,7 @@ impl ProvisionCommand {
7066
provision_profile: self.config.provision_profile.clone(),
7167
env_vars: self.config.env_vars.clone(),
7268
out: self.config.out.clone(),
73-
container_args: merged_container_args,
69+
container_args: self.config.container_args.clone(),
7470
dnf_args: self.config.dnf_args.clone(),
7571
state_file,
7672
no_stamps: self.config.no_stamps,

src/utils/config.rs

Lines changed: 197 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,26 +1842,122 @@ impl Config {
18421842
}
18431843
}
18441844

1845-
/// Merge provision profile container args with CLI args, expanding environment variables
1846-
/// Returns a new Vec containing provision profile args first, then CLI args
1845+
/// Merge SDK container args, provision profile container args, and CLI args
1846+
/// Returns a new Vec containing: SDK args first, then provision profile args, then CLI args
1847+
/// This ensures SDK defaults are used as a base, with provision profiles and CLI overriding
1848+
/// Duplicate args are removed (later args take precedence for flags with values)
18471849
pub fn merge_provision_container_args(
18481850
&self,
18491851
provision_profile: Option<&str>,
18501852
cli_args: Option<&Vec<String>>,
18511853
) -> Option<Vec<String>> {
1854+
let sdk_args = self.get_sdk_container_args();
18521855
let profile_args = provision_profile
18531856
.and_then(|profile| self.get_provision_profile_container_args(profile));
18541857

1855-
match (profile_args, cli_args) {
1856-
(Some(profile), Some(cli)) => {
1857-
let mut merged = Self::process_container_args(Some(profile)).unwrap_or_default();
1858-
merged.extend(Self::process_container_args(Some(cli)).unwrap_or_default());
1859-
Some(merged)
1858+
// Collect all args in order: SDK first, then provision profile, then CLI
1859+
let mut all_args: Vec<String> = Vec::new();
1860+
1861+
if let Some(sdk) = sdk_args {
1862+
all_args.extend(Self::process_container_args(Some(sdk)).unwrap_or_default());
1863+
}
1864+
1865+
if let Some(profile) = profile_args {
1866+
all_args.extend(Self::process_container_args(Some(profile)).unwrap_or_default());
1867+
}
1868+
1869+
if let Some(cli) = cli_args {
1870+
all_args.extend(Self::process_container_args(Some(cli)).unwrap_or_default());
1871+
}
1872+
1873+
if all_args.is_empty() {
1874+
return None;
1875+
}
1876+
1877+
// Deduplicate args, keeping the last occurrence for flags with values
1878+
// This allows provision profile and CLI to override SDK defaults
1879+
let deduped = Self::deduplicate_container_args(all_args);
1880+
1881+
if deduped.is_empty() {
1882+
None
1883+
} else {
1884+
Some(deduped)
1885+
}
1886+
}
1887+
1888+
/// Deduplicate container args, keeping the last occurrence for each unique arg or flag
1889+
/// Handles both standalone flags (--privileged) and flag-value pairs (-v /dev:/dev, --network=host)
1890+
fn deduplicate_container_args(args: Vec<String>) -> Vec<String> {
1891+
use std::collections::HashSet;
1892+
1893+
// First pass: identify which args are flags that take a separate value argument
1894+
// (e.g., -v, -e, --volume, --env, etc.)
1895+
let flags_with_separate_values: HashSet<&str> = [
1896+
"-v", "--volume",
1897+
"-e", "--env",
1898+
"-p", "--publish",
1899+
"-w", "--workdir",
1900+
"-u", "--user",
1901+
"-l", "--label",
1902+
"--mount",
1903+
"--device",
1904+
"--add-host",
1905+
"--dns",
1906+
"--cap-add",
1907+
"--cap-drop",
1908+
"--security-opt",
1909+
"--ulimit",
1910+
]
1911+
.iter()
1912+
.cloned()
1913+
.collect();
1914+
1915+
// Parse args into (key, full_representation) pairs for deduplication
1916+
// key is used for deduplication, full_representation is what we keep
1917+
let mut parsed_args: Vec<(String, Vec<String>)> = Vec::new();
1918+
let mut i = 0;
1919+
1920+
while i < args.len() {
1921+
let arg = &args[i];
1922+
1923+
if flags_with_separate_values.contains(arg.as_str()) && i + 1 < args.len() {
1924+
// Flag with separate value: combine flag and value as key
1925+
let value = &args[i + 1];
1926+
let key = format!("{} {}", arg, value);
1927+
parsed_args.push((key, vec![arg.clone(), value.clone()]));
1928+
i += 2;
1929+
} else if arg.starts_with('-') && arg.contains('=') {
1930+
// Flag with inline value (e.g., --network=host)
1931+
// Use just the flag name as key for network/other single-value flags
1932+
let flag_name = arg.split('=').next().unwrap_or(arg);
1933+
let key = flag_name.to_string();
1934+
parsed_args.push((key, vec![arg.clone()]));
1935+
i += 1;
1936+
} else if arg.starts_with('-') {
1937+
// Standalone flag (e.g., --privileged, --rm)
1938+
parsed_args.push((arg.clone(), vec![arg.clone()]));
1939+
i += 1;
1940+
} else {
1941+
// Non-flag argument (shouldn't happen normally, but handle it)
1942+
parsed_args.push((arg.clone(), vec![arg.clone()]));
1943+
i += 1;
18601944
}
1861-
(Some(profile), None) => Self::process_container_args(Some(profile)),
1862-
(None, Some(cli)) => Self::process_container_args(Some(cli)),
1863-
(None, None) => None,
18641945
}
1946+
1947+
// Deduplicate by key, keeping the last occurrence
1948+
let mut seen_keys: HashSet<String> = HashSet::new();
1949+
let mut result: Vec<Vec<String>> = Vec::new();
1950+
1951+
// Iterate in reverse to keep last occurrence, then reverse the result
1952+
for (key, values) in parsed_args.into_iter().rev() {
1953+
if !seen_keys.contains(&key) {
1954+
seen_keys.insert(key);
1955+
result.push(values);
1956+
}
1957+
}
1958+
1959+
result.reverse();
1960+
result.into_iter().flatten().collect()
18651961
}
18661962

18671963
/// Get compile section dependencies
@@ -3239,6 +3335,97 @@ image = "docker.io/avocadolinux/sdk:apollo-edge"
32393335
assert!(merged.is_none());
32403336
}
32413337

3338+
#[test]
3339+
fn test_merge_provision_container_args_with_sdk_defaults() {
3340+
// Test that SDK container_args are included as base defaults
3341+
let config_content = r#"
3342+
[sdk]
3343+
image = "docker.io/avocadolinux/sdk:apollo-edge"
3344+
container_args = ["--privileged", "--network=host"]
3345+
3346+
[provision.usb]
3347+
container_args = ["-v", "/dev:/dev"]
3348+
"#;
3349+
3350+
let config = Config::load_from_str(config_content).unwrap();
3351+
3352+
// Test merging SDK + provision profile + CLI args
3353+
let cli_args = vec!["--rm".to_string()];
3354+
let merged = config.merge_provision_container_args(Some("usb"), Some(&cli_args));
3355+
3356+
assert!(merged.is_some());
3357+
let merged_args = merged.unwrap();
3358+
// Should have SDK args first, then provision profile args, then CLI args
3359+
assert_eq!(merged_args.len(), 5);
3360+
assert_eq!(merged_args[0], "--privileged");
3361+
assert_eq!(merged_args[1], "--network=host");
3362+
assert_eq!(merged_args[2], "-v");
3363+
assert_eq!(merged_args[3], "/dev:/dev");
3364+
assert_eq!(merged_args[4], "--rm");
3365+
}
3366+
3367+
#[test]
3368+
fn test_merge_provision_container_args_sdk_defaults_only() {
3369+
// Test that SDK container_args are used when no provision profile or CLI args
3370+
let config_content = r#"
3371+
[sdk]
3372+
image = "docker.io/avocadolinux/sdk:apollo-edge"
3373+
container_args = ["--privileged", "-v", "/dev:/dev"]
3374+
"#;
3375+
3376+
let config = Config::load_from_str(config_content).unwrap();
3377+
3378+
let merged = config.merge_provision_container_args(None, None);
3379+
3380+
assert!(merged.is_some());
3381+
let merged_args = merged.unwrap();
3382+
assert_eq!(merged_args.len(), 3);
3383+
assert_eq!(merged_args[0], "--privileged");
3384+
assert_eq!(merged_args[1], "-v");
3385+
assert_eq!(merged_args[2], "/dev:/dev");
3386+
}
3387+
3388+
#[test]
3389+
fn test_merge_provision_container_args_deduplication() {
3390+
// Test that duplicate args are removed (keeping the last occurrence)
3391+
let config_content = r#"
3392+
[sdk]
3393+
image = "docker.io/avocadolinux/sdk:apollo-edge"
3394+
container_args = ["--privileged", "--network=host", "-v", "/dev:/dev"]
3395+
3396+
[provision.tegraflash]
3397+
container_args = ["--privileged", "--network=host", "-v", "/dev:/dev", "-v", "/sys:/sys"]
3398+
"#;
3399+
3400+
let config = Config::load_from_str(config_content).unwrap();
3401+
3402+
// Test that duplicates are removed
3403+
let merged = config.merge_provision_container_args(Some("tegraflash"), None);
3404+
3405+
assert!(merged.is_some());
3406+
let merged_args = merged.unwrap();
3407+
// Should only have unique args: --privileged, --network=host, -v /dev:/dev, -v /sys:/sys
3408+
// Note: --network=host keeps last occurrence (same value), -v /dev:/dev and -v /sys:/sys are different
3409+
assert_eq!(merged_args.len(), 6); // --privileged, --network=host, -v, /dev:/dev, -v, /sys:/sys
3410+
assert!(merged_args.contains(&"--privileged".to_string()));
3411+
assert!(merged_args.contains(&"--network=host".to_string()));
3412+
// Count occurrences of --privileged and --network=host - should be 1 each
3413+
assert_eq!(
3414+
merged_args
3415+
.iter()
3416+
.filter(|a| *a == "--privileged")
3417+
.count(),
3418+
1
3419+
);
3420+
assert_eq!(
3421+
merged_args
3422+
.iter()
3423+
.filter(|a| *a == "--network=host")
3424+
.count(),
3425+
1
3426+
);
3427+
}
3428+
32423429
#[test]
32433430
fn test_provision_state_file_default() {
32443431
// Test that state_file defaults to .avocado/provision-{profile}.state when not configured

0 commit comments

Comments
 (0)