use owo_colors::OwoColorize; use crate::config::ProxyConfig; use crate::database::Database; use crate::image::{image_version, pull_image, ImageVersion}; use crate::network::ensure_network_exists; use crate::service::Service; use crate::service::ServiceTrait; use bollard::config::{ContainerCreateBody, NetworkConnectRequest, NetworkingConfig}; use bollard::models::{EndpointSettings, HostConfig}; use bollard::query_parameters::CreateContainerOptions; use bollard::Docker; use itertools::Itertools; use maplit::hashmap; use miette::{IntoDiagnostic, Report, Result, WrapErr}; use reqwest::{Client, Url}; use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use std::time::Duration; use tokio::time::{sleep, timeout}; #[derive(Clone, Debug, Eq, PartialEq, Default)] #[allow(dead_code)] pub enum PhpVersion { #[default] Php85, Php84, Php83, Php82, Php81, Php80, } pub const PHP_MEMORY_LIMIT: i64 = 2 * 1024 * 1024 * 1024; impl FromStr for PhpVersion { type Err = (); fn from_str(s: &str) -> Result { match s { "8" => Ok(PhpVersion::Php80), "8.0" => Ok(PhpVersion::Php80), "8.1" => Ok(PhpVersion::Php81), "8.2" => Ok(PhpVersion::Php82), "8.3" => Ok(PhpVersion::Php83), "8.4" => Ok(PhpVersion::Php84), "8.5" => Ok(PhpVersion::Php85), _ => Err(()), } } } impl PhpVersion { pub fn supported_versions() -> &'static [&'static str] { &["8.1", "8.2", "8.3", "8.4"] } pub fn image(&self) -> &'static str { match self { PhpVersion::Php80 => "icewind1991/haze:8.0", PhpVersion::Php81 => "icewind1991/haze:8.1", PhpVersion::Php82 => "icewind1991/haze:8.2", PhpVersion::Php83 => "icewind1991/haze:8.3", PhpVersion::Php84 => "icewind1991/haze:8.4", PhpVersion::Php85 => "icewind1991/haze:8.5", } } pub fn name(&self) -> &'static str { match self { PhpVersion::Php80 => "8.0", PhpVersion::Php81 => "8.1", PhpVersion::Php82 => "8.2", PhpVersion::Php83 => "8.3", PhpVersion::Php84 => "8.4", PhpVersion::Php85 => "8.4", } } pub fn from_number(major: u8, minor: u8) -> Option { match (major, minor) { (8, 0) => Some(PhpVersion::Php80), (8, 1) => Some(PhpVersion::Php81), (8, 2) => Some(PhpVersion::Php82), (8, 3) => Some(PhpVersion::Php83), (8, 4) => Some(PhpVersion::Php84), (8, 5) => Some(PhpVersion::Php85), _ => None, } } pub fn max_minor(major: u8) -> u8 { match major { 7 => 4, 8 => 5, _ => 0, } } pub fn all() -> impl Iterator { [ PhpVersion::Php80, PhpVersion::Php81, PhpVersion::Php82, PhpVersion::Php83, PhpVersion::Php84, PhpVersion::Php85, ] .into_iter() } #[allow(clippy::too_many_arguments)] pub async fn spawn( &self, docker: &Docker, id: &str, mut env: Vec, db: &Database, network: &str, volumes: Vec, host: &str, services: &[Service], proxy_config: &ProxyConfig, version: Option<&str>, ) -> Result { ensure_network_exists(docker, "haze").await?; pull_image(docker, self.image()).await?; let image_version = image_version(&docker, self.image()).await; let haze_version = ImageVersion::from_str(env!("CARGO_PKG_VERSION")); if let (Some(image_version), Ok(haze_version)) = (image_version, haze_version) { if image_version < haze_version { eprintln!("{}: image version is out of date, run {} to update.", "Warning".red(), "haze update".blue()); eprintln!(" Haze version: {}", haze_version.bright_yellow()); eprintln!(" Image version: {}", image_version.bright_yellow()); } } let options = Some(CreateContainerOptions { name: Some(id.to_string()), ..CreateContainerOptions::default() }); let clean_id = id.strip_prefix("haze-").unwrap_or(id); let proxy_base = &proxy_config.address; let mut hosts = if proxy_base.is_empty() { vec![] } else { let mut hosts = services .iter() .map(|service| service.name()) .map(|name| format!("{clean_id}-{name}.{proxy_base}:{host}")) .collect::>(); hosts.push(format!("{proxy_base}:{host}")); hosts.push(format!("{clean_id}.{proxy_base}:{host}")); hosts }; hosts.push(format!("hazehost:{host}")); env.push(format!( "NEXTCLOUD_BASE_URL={}", proxy_config.addr(id, IpAddr::V4(Ipv4Addr::LOCALHOST)) )); let mut labels = hashmap! { "haze-type".to_string() => "cloud".to_string(), "haze-db".to_string() => db.name().to_string(), "haze-php".to_string() => self.name().to_string(), "haze-cloud-id".to_string() => id.to_string(), "haze-services".to_string() => services.iter().map(|s| s.name()).join(","), }; if let Some(version) = version { labels.insert("haze-version".to_string(), version.to_string()); } let config = ContainerCreateBody { image: Some(self.image().to_string()), env: Some(env), host_config: Some(HostConfig { network_mode: Some(network.to_string()), binds: Some(volumes), extra_hosts: Some(hosts), memory: Some(PHP_MEMORY_LIMIT), nano_cpus: Some(2_000_000_000), ..Default::default() }), networking_config: Some(NetworkingConfig { endpoints_config: Some(hashmap! { network.to_string() => EndpointSettings { aliases: Some(vec!["cloud".to_string()]), ..Default::default() } }), }), labels: Some(labels), ..Default::default() }; let id = docker .create_container(options, config) .await .into_diagnostic()? .id; if let Err(e) = docker.start_container(&id, None).await.into_diagnostic() { docker.remove_container(&id, None).await.ok(); return Err(e); } if let Err(e) = docker .connect_network( "haze", NetworkConnectRequest { container: id.to_string(), endpoint_config: Some(EndpointSettings { aliases: Some(vec![id.to_string()]), ..Default::default() }), }, ) .await .into_diagnostic() { docker.remove_container(&id, None).await.ok(); return Err(e); } Ok(id) } pub async fn wait_for_start(&self, ip: Option) -> Result<()> { let client = Client::new(); let url = Url::parse(&format!( "http://{}/status.php", ip.ok_or_else(|| Report::msg("Container not running"))? )) .into_diagnostic()?; timeout(Duration::from_secs(15), async { while client.get(url.clone()).send().await.is_err() { sleep(Duration::from_millis(100)).await } }) .await .into_diagnostic() .wrap_err("Timeout after 15 seconds") } }