diff --git a/Cargo.lock b/Cargo.lock index 3cf733e..83a0170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -64,9 +64,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -98,6 +98,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -132,6 +138,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "once_cell_polyfill" version = "1.70.1" @@ -143,6 +158,7 @@ name = "podman-openrc" version = "0.1.2" dependencies = [ "clap", + "itertools", "serde", "toml", ] @@ -213,9 +229,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap", "serde", @@ -237,9 +253,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 01f041c..cb14194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://git.plabble.org/Maurice/podman-openrc" readme = "README.md" [dependencies] -clap = { version = "4.5.41", features = ["derive"] } +clap = { version = "4.5.45", features = ["derive"] } +itertools = "0.14.0" serde = { version = "1.0", features = ["derive"] } -toml = "0.9.2" +toml = "0.9.5" diff --git a/README.md b/README.md index 6dfbe6a..d1a2f17 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ capabilities = ["NET_BIND_SERVICE"] # Optional property, add Linux capabilities [service] name = "" # Container name, required image = "" # Podman image name -networks = [""] # Optional, if you want to run the container within a specific network. Set to "host" if you don't want to use the podman networking. depend = [""] # Name of any service in /etc/init.d to depend on restart = "unless-stopped" # Restart, optional. Defaults to "unless-stopped" detach = true # Run container in detach mode, optional, default true. Recommended. @@ -35,6 +34,19 @@ command = "" # Container command to run, optional. [environment] ASPNETCORE_ENVIRONMENT = "Test" # If you have a not TOML-compatible key name, use "" around the key name + +# Optional, if you want to run the container within specific network(s). Set to "host" if you don't want to use the podman networking. +[[networks]] +name = "host" + +# You can also create groups +[[networks]] +name = "netw-service-test" +group = "http-networks" + +# And assign ALL networks assigned to a group to a service +[[networks]] +group = "http-networks" # Optionally, you can assign one or more port mappings [[ports]] diff --git a/src/main.rs b/src/main.rs index c49cfeb..7a634c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,20 @@ -use std::fs; +use std::{collections::HashMap, fs, path::Path}; use clap::Parser; +use itertools::Itertools; -use crate::service::ServiceConfig; +use crate::service::{NetworkMapping, ServiceConfig}; mod service; pub fn generate_openrc(config: &ServiceConfig) -> String { + let networks: Vec = config.networks.clone() + .into_iter() + .filter(|n|n.name.is_some()) + .map(|n|n.name.unwrap()) + .dedup() + .collect(); + let mut script = String::from("#!/sbin/openrc-run\n# !!! AUTO GENERATED - DO NOT EDIT !!!\n\n"); let wrap = |cmd: &str| { if let Some(user) = config.user.as_ref() { @@ -27,11 +35,11 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { // start_pre() script.push_str("start_pre() {\n"); let mut start_pre_commands = Vec::new(); - for network in &config.service.networks { + for network in networks.iter() { start_pre_commands.push(format!("podman network create {} --ignore;", network)); } start_pre_commands.push(format!("podman rm {} --ignore;", config.service.name)); - script.push_str(&wrap(&start_pre_commands.join("\n"))); + script.push_str(&start_pre_commands.iter().map(|c|wrap(c)).collect::>().join("\n")); script.push_str("\n}\n\n"); // } @@ -39,8 +47,15 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { script.push_str("start() {\n"); let mut arguments = vec![ - format!("--restart {}", config.service.restart.as_deref().unwrap_or("unless-stopped")), - format!("--name {}", config.service.name) + format!( + "--restart {}", + config + .service + .restart + .as_deref() + .unwrap_or("unless-stopped") + ), + format!("--name {}", config.service.name), ]; if let Some(hostname) = &config.service.hostname { @@ -51,7 +66,7 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { arguments.push("--detach".to_string()); } - for network in &config.service.networks { + for network in networks.iter() { arguments.push(format!("--network {}", network)); } @@ -72,7 +87,10 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { } for secret in &config.environment_secrets { - arguments.push(format!("--env {}=$(podman secret inspect --showsecret --format {{{{.SecretData}}}} {})", secret.name, secret.secret)); + arguments.push(format!( + "--env {}=$(podman secret inspect --showsecret --format {{{{.SecretData}}}} {})", + secret.name, secret.secret + )); } for volume in &config.volumes { @@ -80,13 +98,16 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { } for mount in &config.mounts { - let mut mount_str = format!("--mount type={},source={},target={}", mount.typ, mount.source, mount.target); + let mut mount_str = format!( + "--mount type={},source={},target={}", + mount.typ, mount.source, mount.target + ); if mount.read_only.unwrap_or(false) { mount_str.push_str(",readonly"); } arguments.push(mount_str); } - + if let Some(healthcheck) = &config.service.healthcheck { arguments.push(format!("--health-cmd '{}'", healthcheck.cmd)); if let Some(interval) = &healthcheck.interval { @@ -109,18 +130,29 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { arguments.push(command.clone()); } - script.push_str(&wrap(&format!("podman run {}", arguments.iter() - .enumerate() - .map(|(i, arg)| if i > 0 { format!("\t{}", arg) } else { arg.to_string() }) - .collect::>() - .join(" \\\n")))); + script.push_str(&wrap(&format!( + "podman run {}", + arguments + .iter() + .enumerate() + .map(|(i, arg)| if i > 0 { + format!("\t{}", arg) + } else { + arg.to_string() + }) + .collect::>() + .join(" \\\n") + ))); script.push_str("\n}\n\n"); // } // stop() script.push_str("stop() {\n"); - script.push_str(&wrap(&format!("podman stop {} --ignore", config.service.name))); + script.push_str(&wrap(&format!( + "podman stop {} --ignore", + config.service.name + ))); script.push_str("\n}\n\n"); // } @@ -130,21 +162,127 @@ pub fn generate_openrc(config: &ServiceConfig) -> String { /// Program to generate OpenRC scripts from Podman service definitions in TOML format. #[derive(Debug, Parser)] struct Args { - /// Definition file in TOML format + /// Definition file in TOML format, or directory with definition files definition: String, - /// Output file for the OpenRC script + /// Output file for the OpenRC script, or output directory for the service files out: String, } +fn read_dir>(path: &P, services: &mut Vec) { + for entry in fs::read_dir(path).expect(&format!( + "Failed to read directory: {}", + path.as_ref().display() + )) { + let path = entry.unwrap().path(); + + if path.is_dir() { + read_dir(&path, services); + } else if path.is_file() + && path.extension().is_some_and(|ext| ext == "toml") + && path + .file_name() + .unwrap() + .to_str() + .unwrap() + .ends_with("service.toml") + { + println!("Processing: {}", path.display()); + let input = fs::read_to_string(&path).expect(&format!( + "Failed to read definition file: {}", + path.display() + )); + + let config: ServiceConfig = toml::from_str(&input).expect(&format!( + "Failed to parse definition file: {}", + path.display() + )); + + services.push(config); + } else { + println!("Skipped: {}", path.display()); + } + } +} + +fn process_network_groups(services: &mut Vec) { + // Map network groups + let mut network_groups: HashMap> = HashMap::new(); + for service in services.iter() { + for network in &service.networks { + if let NetworkMapping { + name: Some(name), + group: Some(group), + } = network + { + if network_groups.contains_key(group) { + network_groups.get_mut(group).unwrap().push(name.clone()); + } else { + network_groups.insert(group.clone(), vec![name.clone()]); + } + } + } + } + + // Expand network groups in services + for service in services { + let mut networks_to_add = Vec::new(); + service.networks.retain(|network| { + if let NetworkMapping { + name: None, + group: Some(group), + } = network + { + if let Some(names) = network_groups.get(group) { + for name in names { + networks_to_add.push(NetworkMapping { + name: Some(name.clone()), + group: Some(group.clone()), + }); + } + } + false // Remove this network mapping + } else { + true // Keep this network mapping + } + }); + service.networks.extend(networks_to_add); + } +} + fn main() { let args = Args::parse(); - let input = fs::read_to_string(&args.definition) - .expect("Failed to read definition file"); - let config: ServiceConfig = toml::from_str(&input) - .expect("Failed to parse definition file"); + let input_path = Path::new(&args.definition); + let output_path = Path::new(&args.out); - let output = generate_openrc(&config); - fs::write(&args.out, output) - .expect("Failed to write OpenRC script to output file"); + if input_path.is_dir() { + if !output_path.exists() { + fs::create_dir_all(&output_path).expect("Failed to create output directory"); + } + + if !output_path.is_dir() { + panic!( + "Input path is a directory, but output path is not a directory: {}", + output_path.display() + ); + } + + let mut services: Vec = Vec::new(); + read_dir(&input_path, &mut services); + process_network_groups(&mut services); + + // Write + for config in services { + let output = generate_openrc(&config); + fs::write(output_path.join(format!("{}.service.sh", config.service.name)), output) + .expect("Failed to write OpenRC script to output file"); + } + } else { + let input = fs::read_to_string(&args.definition).expect("Failed to read definition file"); + let config: ServiceConfig = + toml::from_str(&input).expect("Failed to parse definition file"); + + let output = generate_openrc(&config); + fs::write(&args.out, output).expect("Failed to write OpenRC script to output file"); + } } diff --git a/src/service.rs b/src/service.rs index 7240eef..c064de0 100644 --- a/src/service.rs +++ b/src/service.rs @@ -24,6 +24,9 @@ pub struct ServiceConfig { #[serde(default)] pub mounts: Vec, + #[serde(default)] + pub networks: Vec, + pub user: Option, #[serde(default)] @@ -36,9 +39,6 @@ pub struct Service { pub hostname: Option, pub image: String, - #[serde(default)] - pub networks: Vec, - pub restart: Option, pub detach: Option, pub healthcheck: Option, @@ -89,4 +89,10 @@ pub struct HealthCheck { pub start_period: Option, pub retries: Option, pub on_failure: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct NetworkMapping { + pub name: Option, + pub group: Option, } \ No newline at end of file