From 59c29b5e19376376535f5081df1169d995de190d Mon Sep 17 00:00:00 2001 From: Guilherme Ananias Date: Fri, 13 Jun 2025 18:50:15 -0300 Subject: [PATCH] feat: add poc implementation for container run command --- .gitignore | 1 + Cargo.lock | 138 +++++++++++++++++++++++++++++++++++ Cargo.toml | 9 +++ examples/docker-compose.yaml | 9 +++ src/container.rs | 97 ++++++++++++++++++++++++ src/deserializer.rs | 71 ++++++++++++++++++ src/main.rs | 72 ++++++++++++++++++ 7 files changed, 397 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 examples/docker-compose.yaml create mode 100644 src/container.rs create mode 100644 src/deserializer.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1b01932 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,138 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "container-compose" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "serde_yaml", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d545d9c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "container-compose" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_yaml = "0.9.34" diff --git a/examples/docker-compose.yaml b/examples/docker-compose.yaml new file mode 100644 index 0000000..3e4f522 --- /dev/null +++ b/examples/docker-compose.yaml @@ -0,0 +1,9 @@ +version: '3.8' + +services: + postgres: + image: postgres:15 + ports: + - 5432:5432 + environment: + - POSTGRES_PASSWORD=postgres diff --git a/src/container.rs b/src/container.rs new file mode 100644 index 0000000..e0caf52 --- /dev/null +++ b/src/container.rs @@ -0,0 +1,97 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[allow(dead_code, unused_variables)] + +#[derive(Debug, Deserialize, Clone)] +pub struct Container { + pub networks: Vec, + pub status: String, + pub configuration: Configuration, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Network { + pub address: String, + pub gateway: String, + pub network: String, + pub hostname: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Configuration { + pub resources: Resources, + pub labels: HashMap, + pub hostname: String, + pub sysctls: HashMap, + pub networks: Vec, + pub initProcess: InitProcess, + pub id: String, + pub rosetta: bool, + pub runtimeHandler: String, + pub platform: Platform, + pub mounts: Vec, + pub image: Image, + pub dns: Dns, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Resources { + pub cpus: u32, + pub memoryInBytes: u64, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct InitProcess { + pub environment: Vec, + pub arguments: Vec, + pub executable: String, + pub workingDirectory: String, + pub terminal: bool, + pub user: User, + pub supplementalGroups: Vec, + pub rlimits: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct User { + pub id: Id, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Id { + pub uid: u32, + pub gid: u32, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Platform { + pub os: String, + pub architecture: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Mount { + // Fill in fields as needed +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Image { + pub reference: String, + pub descriptor: Descriptor, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Descriptor { + pub size: u64, + pub digest: String, + // pub annotations: HashMap, + // pub media_type: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Dns { + pub nameservers: Vec, + // pub search_domains: Vec, + pub options: Vec, +} \ No newline at end of file diff --git a/src/deserializer.rs b/src/deserializer.rs new file mode 100644 index 0000000..1c4b985 --- /dev/null +++ b/src/deserializer.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use serde::{Deserialize}; +use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; +use std::fmt; + +#[derive(Debug, Deserialize)] +pub struct Service { + pub name: Option, + pub image: String, + pub ports: Vec, + #[serde(deserialize_with = "deserialize_environment")] + pub environment: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct Compose { + pub version: String, + pub services: HashMap, +} + +pub fn deserialize_yaml(yaml: &str) -> Result { + match serde_yaml::from_str(yaml) { + Ok(compose) => Ok(compose), + Err(e) => Err(e), + } +} + +fn deserialize_environment<'a, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'a>, +{ + struct EnvVisitor; + + impl<'a> Visitor<'a> for EnvVisitor { + type Value = HashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map or a list of key=value strings") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'a>, + { + let mut map = HashMap::new(); + while let Some((key, value)) = access.next_entry()? { + map.insert(key, value); + } + Ok(map) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'a>, + { + let mut map = HashMap::new(); + while let Some(entry) = seq.next_element::()? { + let parts: Vec<&str> = entry.splitn(2, '=').collect(); + if parts.len() == 2 { + map.insert(parts[0].to_string(), parts[1].to_string()); + } else { + return Err(de::Error::custom(format!("Invalid environment variable: {}", entry))); + } + } + Ok(map) + } + } + + deserializer.deserialize_any(EnvVisitor) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9557995 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,72 @@ +use std::{fs, process::Command}; + +mod deserializer; +mod container; + +fn main() { + let yaml = fs::read_to_string("examples/docker-compose.yaml").unwrap(); + let compose = deserializer::deserialize_yaml(&yaml).unwrap(); + + println!("{:?}", compose); + + for (name, service) in compose.services.iter() { + let command = format!("docker run -d --name {} {}", name, service.image); + println!("{}", command); + + let container_name = service.name.clone().unwrap_or(name.clone()); + + // call a command in terminal + let mut output = Command::new("container"); + output + .arg("run") + .arg("--name") + .arg(container_name.clone()); + + for (key, value) in service.environment.iter() { + let env_var = format!("{}={}", key, value); + println!("-e env {}", env_var); + output.arg("-e"); + output.arg(env_var); + } + + + let output = output + .arg("-d") + .arg(service.image.clone()); + + let output = dbg!(output); + let output = output.output().expect("Failed to execute process"); + + println!("{}", String::from_utf8(output.stdout).unwrap()); + println!("{}", String::from_utf8(output.stderr).unwrap()); + + if service.ports.len() > 0 { + println!("Found ports in service, container does not support mapping port yet. Running socat fallback.)"); + let command = Command::new("container") + .arg("inspect") + .arg(container_name.clone()) + .output() + .expect("Failed to execute process"); + + let value = String::from_utf8(command.stdout).unwrap(); + let container = serde_json::from_str::>(&value).unwrap()[0].clone(); + + for port in service.ports.iter() { + let port = port.split(":").collect::>(); + let host_port = port[0].parse::().unwrap(); + let container_port = port[1].parse::().unwrap(); + + let container_ip = format!("{}:{}", container.configuration.networks[0], container_port); + + let output = Command::new("socat") + .arg(format!("TCP-LISTEN:{},fork", host_port)) + .arg(format!("TCP:{}", container_ip)) + .output() + .expect("Failed to execute process"); + + println!("success: {}", String::from_utf8(output.stdout).unwrap()); + println!("error: {}", String::from_utf8(output.stderr).unwrap()); + } + } + } +}