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:
parent
afb7d0ae6f
commit
e939a8fffb
3 changed files with 179 additions and 37 deletions
180
src/cloud.rs
180
src/cloud.rs
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
src/main.rs
23
src/main.rs
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue