1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 17:14:08 +02:00

container management 101

This commit is contained in:
Robin Appelman 2021-03-12 19:09:44 +01:00
commit e939a8fffb
3 changed files with 179 additions and 37 deletions

View file

@ -1,13 +1,21 @@
use crate::config::HazeConfig; use crate::config::HazeConfig;
use bollard::container::{Config, CreateContainerOptions}; use bollard::container::{Config, CreateContainerOptions, RemoveContainerOptions};
use bollard::models::HostConfig; use bollard::models::{ContainerState, HostConfig};
use bollard::network::CreateNetworkOptions; use bollard::network::CreateNetworkOptions;
use bollard::Docker; use bollard::Docker;
use camino::Utf8Path; use camino::{Utf8Path, Utf8PathBuf};
use color_eyre::{eyre::WrapErr, Report, Result}; use color_eyre::{eyre::WrapErr, Report, Result};
use maplit::hashmap;
use min_id::generate_id; use min_id::generate_id;
use tokio::fs::{create_dir_all, write}; use std::collections::HashMap;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
use tokio::fs::{create_dir_all, remove_dir_all, write};
use tokio::time::sleep;
#[derive(Debug)]
#[allow(dead_code)]
pub enum Database { pub enum Database {
Sqlite, Sqlite,
Mysql, Mysql,
@ -34,6 +42,20 @@ impl Default for Database {
} }
} }
impl FromStr for Database {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sqlite" => Ok(Database::Sqlite),
"mysql" => Ok(Database::Mysql),
"mariadb" => Ok(Database::MariaDB),
"postgresql" => Ok(Database::Postgres),
_ => Err(()),
}
}
}
impl Database { impl Database {
pub fn image(&self) -> &'static str { pub fn image(&self) -> &'static str {
match self { match self {
@ -129,12 +151,20 @@ impl Database {
network_mode: Some(network.to_string()), network_mode: Some(network.to_string()),
..Default::default() ..Default::default()
}), }),
labels: Some(hashmap! {
"haze-type" => "db",
"haze-cloud-id" => cloud_id
}),
..Default::default() ..Default::default()
}; };
Ok(Some(docker.create_container(options, config).await?.id)) let id = docker.create_container(options, config).await?.id;
docker.start_container::<String>(&id, None).await?;
Ok(Some(id))
} }
} }
#[derive(Debug)]
#[allow(dead_code)]
pub enum PhpVersion { pub enum PhpVersion {
Latest, Latest,
// Php80, // Php80,
@ -156,6 +186,7 @@ impl PhpVersion {
docker: &mut Docker, docker: &mut Docker,
id: &str, id: &str,
env: Vec<String>, env: Vec<String>,
db: &Database,
network: &str, network: &str,
links: Vec<String>, links: Vec<String>,
volumes: Vec<String>, volumes: Vec<String>,
@ -172,9 +203,17 @@ impl PhpVersion {
binds: Some(volumes), binds: Some(volumes),
..Default::default() ..Default::default()
}), }),
labels: Some(hashmap! {
"haze-type".to_string() => "cloud".to_string(),
"haze-db".to_string() => db.name().to_string(),
"haze-cloud-id".to_string() => id.to_string(),
}),
..Default::default() ..Default::default()
}; };
Ok(docker.create_container(options, config).await?.id)
let id = docker.create_container(options, config).await?.id;
docker.start_container::<String>(&id, None).await?;
Ok(id)
} }
} }
@ -184,33 +223,37 @@ impl Default for PhpVersion {
} }
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct CloudOptions { pub struct CloudOptions {
db: Database, db: Database,
php: PhpVersion, php: PhpVersion,
} }
#[derive(Debug)]
pub struct Cloud { pub struct Cloud {
id: String, pub id: String,
network: String, network: String,
containers: Vec<String>, containers: Vec<String>,
db: Database,
pub ip: IpAddr,
workdir: Utf8PathBuf,
} }
impl Cloud { impl Cloud {
pub async fn create( pub async fn create(
docker: &mut Docker, docker: &mut Docker,
options: CloudOptions, options: CloudOptions,
config: HazeConfig, config: &HazeConfig,
) -> Result<Self> { ) -> Result<Self> {
let id = generate_id(); let id = format!("haze-{}", generate_id());
setup_workdir(&config.work_dir, &id) let workdir = setup_workdir(&config.work_dir, &id)
.await .await
.wrap_err("Failed to setup work directories")?; .wrap_err("Failed to setup work directories")?;
let network = docker let network = docker
.create_network(CreateNetworkOptions { .create_network(CreateNetworkOptions {
name: format!("cloud-{}", id), name: id.as_str(),
..Default::default() ..Default::default()
}) })
.await? .await?
@ -235,7 +278,7 @@ impl Cloud {
), ),
format!( format!(
"{}/{}/skeleton/welcome.txt:/var/www/html/core/skeleton/welcome.txt:ro", "{}/{}/skeleton/welcome.txt:/var/www/html/core/skeleton/welcome.txt:ro",
config.work_dir, id config.sources_root, id
), ),
format!( format!(
"{}/{}/integration/vendor:/var/www/html/build/integration/vendor", "{}/{}/integration/vendor:/var/www/html/build/integration/vendor",
@ -263,33 +306,93 @@ impl Cloud {
), ),
]; ];
if let Some(db) = options if let Some(db_name) = options
.db .db
.spawn(docker, &id, &network) .spawn(docker, &id, &network)
.await .await
.wrap_err("Failed to start database")? .wrap_err("Failed to start database")?
{ {
containers.push(db); containers.push(db_name);
links.push(format!("{}-db:{}", id, options.db.name())); links.push(format!("{}-db:{}", id, options.db.name()));
env.push(format!("SQL={}", options.db.name())); env.push(format!("SQL={}", options.db.name()));
} }
let container = options let container = options
.php .php
.spawn(docker, &id, env, &network, links, volumes) .spawn(docker, &id, env, &options.db, &network, links, volumes)
.await .await
.wrap_err("Failed to start php container")?; .wrap_err("Failed to start php container")?;
let mut tries = 0;
let ip = loop {
let info = docker.inspect_container(&container, None).await?;
if matches!(
info.state,
Some(ContainerState {
running: Some(true),
..
})
) {
break info
.network_settings
.unwrap()
.networks
.unwrap()
.values()
.next()
.unwrap()
.ip_address
.as_ref()
.unwrap()
.parse()
.unwrap();
} else if tries > 100 {
return Err(Report::msg("starting container timed out"));
} else {
tries += 1;
sleep(Duration::from_millis(100)).await;
}
};
containers.push(container); containers.push(container);
Ok(Cloud { Ok(Cloud {
id, id,
network, network,
containers, containers,
db: options.db,
ip,
workdir,
}) })
} }
#[allow(dead_code)]
pub async fn destroy(self, docker: &mut Docker) -> Result<()> {
for container in self.containers {
docker
.remove_container(
&container,
Some(RemoveContainerOptions {
force: true,
..Default::default()
}),
)
.await
.wrap_err("Failed to remove container")?;
}
docker
.remove_network(&self.network)
.await
.wrap_err("Failed to remove network")?;
remove_dir_all(self.workdir)
.await
.wrap_err("Failed to remove work directory")?;
Ok(())
}
} }
async fn setup_workdir(base: &Utf8Path, id: &str) -> Result<()> { async fn setup_workdir(base: &Utf8Path, id: &str) -> Result<Utf8PathBuf> {
let workdir = base.join(id); let workdir = base.join(id);
create_dir_all(workdir.join("data")).await?; create_dir_all(workdir.join("data")).await?;
create_dir_all(workdir.join("config")).await?; create_dir_all(workdir.join("config")).await?;
@ -305,5 +408,46 @@ async fn setup_workdir(base: &Utf8Path, id: &str) -> Result<()> {
create_dir_all(base.join("composer/cache")).await?; create_dir_all(base.join("composer/cache")).await?;
Ok(()) Ok(workdir)
}
pub async fn parse(docker: &mut Docker, config: &HazeConfig) -> Result<Vec<Cloud>> {
let containers = docker.list_containers::<String>(None).await?;
let mut containers_by_id: HashMap<String, (Option<_>, Vec<_>)> = HashMap::new();
for container in containers {
let labels = container.labels.clone().unwrap_or_default();
if let Some(cloud_id) = labels.get("haze-cloud-id") {
let mut entry = containers_by_id.entry(cloud_id.to_string()).or_default();
if labels.get("haze-type").map(String::as_str) == Some("cloud") {
entry.0 = Some(container);
} else {
entry.1.push(container)
}
}
}
Ok(containers_by_id
.into_iter()
.filter_map(|(id, (cloud, services))| {
let cloud = cloud?;
let network = id.clone();
let networks = cloud.network_settings?.networks?;
let network_info = networks.get(&network)?;
let workdir = config.work_dir.join(&id);
let db = cloud.labels?.get("haze-db")?.parse().ok()?;
let mut service_ids: Vec<String> = services
.iter()
.filter_map(|service| service.names.as_ref()?.first().map(String::clone))
.collect();
service_ids.push(id.clone());
Some(Cloud {
id,
network,
db,
containers: service_ids,
ip: network_info.ip_address.as_ref()?.parse().ok()?,
workdir,
})
})
.collect())
} }

View file

@ -1,13 +0,0 @@
use color_eyre::Result;
pub struct Docker {
handle: bollard::Docker,
}
impl Docker {
pub fn new() -> Result<Self> {
Ok(Docker {
handle: bollard::Docker::connect_with_local_defaults()?,
})
}
}

View file

@ -1,15 +1,26 @@
use crate::cloud::{parse, Cloud, CloudOptions};
use crate::config::HazeConfig; use crate::config::HazeConfig;
use camino::Utf8Path; use bollard::Docker;
use color_eyre::{eyre::WrapErr, Report, Result}; use color_eyre::{eyre::WrapErr, Result};
use std::fs::create_dir_all;
mod cloud; mod cloud;
mod config; mod config;
mod docker;
fn main() { #[tokio::main]
async fn main() -> Result<()> {
let mut docker =
Docker::connect_with_local_defaults().wrap_err("Failed to connect to docker")?;
let config = HazeConfig { let config = HazeConfig {
sources_root: "/srv/http/owncloud".into(), sources_root: "/srv/http/owncloud".into(),
work_dir: "/tmp/oc-docket".into(), work_dir: "/tmp/haze".into(),
}; };
let options = CloudOptions::default();
// let cloud = Cloud::create(&mut docker, options, &config).await?;
// println!("{} running on http://{}", cloud.id, cloud.ip);
let clouds = parse(&mut docker, &config).await?;
dbg!(clouds);
Ok(())
} }