Allow multiple networks and network groups

This commit is contained in:
Maurice
2025-08-13 13:57:08 +02:00
parent 9146046b91
commit 19d1bfe9a2
5 changed files with 214 additions and 41 deletions

36
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"

View File

@@ -24,7 +24,6 @@ capabilities = ["NET_BIND_SERVICE"] # Optional property, add Linux capabilities
[service]
name = "<CONTAINER NAME>" # Container name, required
image = "<IMAGE>" # Podman image name
networks = ["<NETWORK>"] # 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 = ["<SERVICE NAME>"] # 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.
@@ -36,6 +35,19 @@ command = "<COMMAND>" # Container command to run, optional.
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]]
host = 80 # Port on your computer

View File

@@ -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<String> = 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::<Vec<String>>().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,7 +98,10 @@ 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");
}
@@ -109,18 +130,29 @@ pub fn generate_openrc(config: &ServiceConfig) -> String {
arguments.push(command.clone());
}
script.push_str(&wrap(&format!("podman run {}", arguments.iter()
script.push_str(&wrap(&format!(
"podman run {}",
arguments
.iter()
.enumerate()
.map(|(i, arg)| if i > 0 { format!("\t{}", arg) } else { arg.to_string() })
.map(|(i, arg)| if i > 0 {
format!("\t{}", arg)
} else {
arg.to_string()
})
.collect::<Vec<_>>()
.join(" \\\n"))));
.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<P: AsRef<Path>>(path: &P, services: &mut Vec<ServiceConfig>) {
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<ServiceConfig>) {
// Map network groups
let mut network_groups: HashMap<String, Vec<String>> = 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);
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<ServiceConfig> = 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");
fs::write(&args.out, output).expect("Failed to write OpenRC script to output file");
}
}

View File

@@ -24,6 +24,9 @@ pub struct ServiceConfig {
#[serde(default)]
pub mounts: Vec<MountMapping>,
#[serde(default)]
pub networks: Vec<NetworkMapping>,
pub user: Option<String>,
#[serde(default)]
@@ -36,9 +39,6 @@ pub struct Service {
pub hostname: Option<String>,
pub image: String,
#[serde(default)]
pub networks: Vec<String>,
pub restart: Option<String>,
pub detach: Option<bool>,
pub healthcheck: Option<HealthCheck>,
@@ -90,3 +90,9 @@ pub struct HealthCheck {
pub retries: Option<u32>,
pub on_failure: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct NetworkMapping {
pub name: Option<String>,
pub group: Option<String>,
}