init
This commit is contained in:
commit
eb145cdf31
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1227
Cargo.lock
generated
Normal file
1227
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bollard-compose"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bollard = "*"
|
||||
serde = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
tokio = { version = "*", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
anyhow = "*"
|
||||
futures-util = "0.3.30"
|
18
docker-compose.yaml
Normal file
18
docker-compose.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
services:
|
||||
memos:
|
||||
image: ghcr.io/usememos/memos:0.22.4@sha256:b17a43b084327a8e37121fc3cce67a0a43b8a3ad75f9e9fa51c3f5b5ace290b4
|
||||
#container_name: memos
|
||||
restart: unless-stopped
|
||||
auto_remove: false
|
||||
ports:
|
||||
- "8074:5230"
|
||||
volumes:
|
||||
- memos_storage:/var/opt/memos
|
||||
|
||||
volumes:
|
||||
memos_storage:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /home/lukas/Downloads
|
35
src/compose_types.rs
Normal file
35
src/compose_types.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DockerCompose {
|
||||
pub version: Option<String>,
|
||||
pub services: HashMap<String, Service>,
|
||||
pub volumes: Option<HashMap<String, Volume>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
pub image: Option<String>,
|
||||
pub container_name: Option<String>,
|
||||
pub build: Option<Build>,
|
||||
pub ports: Option<Vec<String>>,
|
||||
pub volumes: Option<Vec<String>>,
|
||||
pub environment: Option<HashMap<String, String>>,
|
||||
pub depends_on: Option<Vec<String>>,
|
||||
pub auto_remove: Option<bool>,
|
||||
pub restart: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Build {
|
||||
pub context: Option<String>,
|
||||
pub dockerfile: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Volume {
|
||||
pub driver: Option<String>,
|
||||
pub driver_opts: Option<HashMap<String, String>>,
|
||||
pub labels: Option<HashMap<String, String>>,
|
||||
}
|
305
src/main.rs
Normal file
305
src/main.rs
Normal file
@ -0,0 +1,305 @@
|
||||
mod compose_types;
|
||||
|
||||
use crate::compose_types::DockerCompose;
|
||||
use anyhow::anyhow;
|
||||
use bollard::container::{Config, CreateContainerOptions};
|
||||
use bollard::errors::Error;
|
||||
use bollard::image::CreateImageOptions;
|
||||
use bollard::models::{CreateImageInfo, HostConfig, PortBinding, PortMap, RestartPolicy, RestartPolicyNameEnum};
|
||||
use bollard::Docker;
|
||||
use clap::{Arg, Command};
|
||||
use futures_util::StreamExt;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use bollard::volume::CreateVolumeOptions;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let matches = Command::new("docker-compose-rust")
|
||||
.version("1.0")
|
||||
.author("Your Name <your.email@example.com>")
|
||||
.about("A CLI for managing Docker Compose in Rust")
|
||||
.subcommand(
|
||||
Command::new("up")
|
||||
.about("Start the services defined in the Docker Compose file")
|
||||
.arg(
|
||||
Arg::new("file")
|
||||
.short('f')
|
||||
.long("file")
|
||||
.value_name("FILE")
|
||||
.help("Sets a custom Docker Compose file"),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("down").about("Stop and remove the services"))
|
||||
.subcommand(Command::new("ps").about("List containers"))
|
||||
.get_matches();
|
||||
|
||||
let docker = Docker::connect_with_local_defaults()?;
|
||||
|
||||
docker
|
||||
.ping()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Connection to Docker Socket failed: {}", e))?;
|
||||
|
||||
// Handle the "up" subcommand
|
||||
if let Some(matches) = matches.subcommand_matches("up") {
|
||||
//let file = matches.try_get_one("file")??;
|
||||
let compose = read_docker_compose("docker-compose.yaml")?;
|
||||
up(compose, &docker).await?;
|
||||
|
||||
// Here you would implement starting the services.
|
||||
}
|
||||
|
||||
// Handle the "down" subcommand
|
||||
if matches.subcommand_matches("down").is_some() {
|
||||
println!("Stopping services...");
|
||||
// Here you would implement stopping the services.
|
||||
}
|
||||
|
||||
// Handle the "ps" subcommand
|
||||
if matches.subcommand_matches("ps").is_some() {
|
||||
println!("Listing containers...");
|
||||
// Here you would implement listing running containers.
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parent_dir_name() -> anyhow::Result<String> {
|
||||
let current_dir = env::current_dir().expect("Failed to get current directory");
|
||||
|
||||
// Get the parent directory of the current directory
|
||||
let parent_dir = current_dir.file_name().ok_or(anyhow!("Current directory has no parent"))?;;
|
||||
let parent_dir_name = parent_dir.to_string_lossy().to_string();
|
||||
Ok(parent_dir_name)
|
||||
}
|
||||
|
||||
fn read_docker_compose(file_path: &str) -> anyhow::Result<DockerCompose> {
|
||||
let mut file = File::open(file_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let compose: DockerCompose = serde_yaml::from_str(&contents)?;
|
||||
Ok(compose)
|
||||
}
|
||||
|
||||
async fn up(compose: DockerCompose, docker: &Docker) -> anyhow::Result<()> {
|
||||
pull_images(&compose, docker).await;
|
||||
create_volumes(&compose, docker).await?;
|
||||
let ids = create_containers(&compose, docker).await?;
|
||||
start_containers(&compose, docker, ids).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_env_vec(env: &Option<HashMap<String, String>>) -> Option<Vec<String>> {
|
||||
match env {
|
||||
Some(env) => {
|
||||
let list = env
|
||||
.into_iter()
|
||||
.map(|(key, value)| format!("{}={}", key, value))
|
||||
.collect();
|
||||
Some(list)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_port_map(ports: &Option<Vec<String>>) -> Option<PortMap> {
|
||||
match ports {
|
||||
None => None,
|
||||
Some(ports) => Some(
|
||||
ports
|
||||
.iter()
|
||||
.map(|port| {
|
||||
let parts: Vec<&str> = port.split(':').collect();
|
||||
(
|
||||
parts[1].to_string(),
|
||||
Some(vec![PortBinding {
|
||||
host_ip: None,
|
||||
host_port: Some(parts[0].to_string()),
|
||||
}]),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_restart_policy(restart: &Option<String>) -> Option<RestartPolicy> {
|
||||
match restart {
|
||||
None => None,
|
||||
Some(restart) => match restart.as_str() {
|
||||
"no" => Some(RestartPolicy {
|
||||
name: Some(RestartPolicyNameEnum::NO),
|
||||
..Default::default()
|
||||
}),
|
||||
"always" => Some(RestartPolicy {
|
||||
name: Some(RestartPolicyNameEnum::ALWAYS),
|
||||
..Default::default()
|
||||
}),
|
||||
"unless-stopped" => Some(RestartPolicy {
|
||||
name: Some(RestartPolicyNameEnum::UNLESS_STOPPED),
|
||||
..Default::default()
|
||||
}),
|
||||
"on-failure" => Some(RestartPolicy {
|
||||
name: Some(RestartPolicyNameEnum::NO),
|
||||
..Default::default()
|
||||
}),
|
||||
v => {
|
||||
// handle special case when retry count is specified
|
||||
if v.starts_with("on-failure:") {
|
||||
let parts: Vec<&str> = v.split(':').collect();
|
||||
Some(RestartPolicy {
|
||||
name: Some(RestartPolicyNameEnum::ON_FAILURE),
|
||||
maximum_retry_count: Some(parts[1].parse().unwrap()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_volume_mount(compose: &DockerCompose, volume: String) -> anyhow::Result<String> {
|
||||
let parts: Vec<&str> = volume.split(':').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(anyhow!("Invalid volume mount: {}", volume));
|
||||
}
|
||||
|
||||
if(parts[0].starts_with('/')) {
|
||||
return Ok(volume);
|
||||
}
|
||||
|
||||
let parent_dir = parent_dir_name()?;
|
||||
|
||||
let field_missing = format!("volume field for {} missing", volume);
|
||||
|
||||
// volumes map has to contain the volume defined here
|
||||
if compose.volumes.as_ref().ok_or(anyhow!(field_missing.clone()))?.contains_key(parts[0]) {
|
||||
Ok(format!("{}_{}:{}", parent_dir, parts[0], parts[1]))
|
||||
} else {
|
||||
Err(anyhow!(field_missing))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_volume_mounts(compose: &DockerCompose, volumes: &Option<Vec<String>>) -> anyhow::Result<Option<Vec<String>>> {
|
||||
match volumes {
|
||||
None => Ok(None),
|
||||
Some(volumes) => {
|
||||
let mounts: Vec<anyhow::Result<String>> = volumes
|
||||
.iter()
|
||||
.map(|volume| parse_volume_mount(compose, volume.clone()))
|
||||
.collect();
|
||||
let mounts = mounts.into_iter().collect::<anyhow::Result<Vec<String>>>()?;
|
||||
if mounts.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(mounts))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_volumes(
|
||||
compose: &DockerCompose,
|
||||
docker: &Docker,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(volumes) = &compose.volumes {
|
||||
let parent_dir = parent_dir_name()?;
|
||||
for (name, volume) in volumes {
|
||||
let create_info = docker
|
||||
.create_volume::<String>(
|
||||
CreateVolumeOptions {
|
||||
name: format!("{}_{}", parent_dir, name),
|
||||
driver: volume.driver.clone().unwrap_or_else(Default::default),
|
||||
driver_opts: volume.driver_opts.clone().unwrap_or_else(Default::default),
|
||||
labels: volume.labels.clone().unwrap_or_else(Default::default),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_containers(
|
||||
compose: &DockerCompose,
|
||||
docker: &Docker,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let mut container_ids = Vec::new();
|
||||
let parent_dir = parent_dir_name()?;
|
||||
for (name, service) in &compose.services {
|
||||
let env = create_env_vec(&service.environment);
|
||||
let conf: Config<String> = Config {
|
||||
image: service.image.clone(),
|
||||
attach_stdout: Some(true),
|
||||
attach_stderr: Some(true),
|
||||
open_stdin: Some(false),
|
||||
env,
|
||||
host_config: Some(HostConfig {
|
||||
auto_remove: service.auto_remove,
|
||||
restart_policy: parse_restart_policy(&service.restart),
|
||||
port_bindings: create_port_map(&service.ports),
|
||||
binds: parse_volume_mounts(&compose, &service.volumes)?,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let container_name = service.container_name.clone().unwrap_or_else(|| format!("{}_{}_1", parent_dir, name));
|
||||
let create_info = docker
|
||||
.create_container::<String, String>(
|
||||
Some(CreateContainerOptions {
|
||||
name: container_name,
|
||||
platform: None,
|
||||
}),
|
||||
conf,
|
||||
)
|
||||
.await?;
|
||||
container_ids.push(create_info.id);
|
||||
}
|
||||
Ok(container_ids)
|
||||
}
|
||||
|
||||
async fn start_containers(
|
||||
compose: &DockerCompose,
|
||||
docker: &Docker,
|
||||
ids: Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
for id in ids {
|
||||
docker.start_container::<String>(&id, None).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pull_images(compose: &DockerCompose, docker: &Docker) {
|
||||
for (name, service) in &compose.services {
|
||||
println!("Trying to pull {}", service.image.clone().unwrap());
|
||||
let mut stream = docker.create_image(
|
||||
Some(CreateImageOptions {
|
||||
from_image: service.image.clone().unwrap(),
|
||||
..Default::default()
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
while let Some(pull_result) = stream.next().await {
|
||||
match pull_result {
|
||||
Err(e) => println!("{:?}", e),
|
||||
Ok(CreateImageInfo {
|
||||
status: Some(status),
|
||||
..
|
||||
}) => {
|
||||
println!("{}", status)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn down(compose: &DockerCompose) {
|
||||
// Here you would implement starting the services.
|
||||
}
|
Loading…
Reference in New Issue
Block a user