1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 17:14:08 +02:00
This commit is contained in:
Robin Appelman 2021-03-14 17:58:01 +01:00
commit 1d20b7937c
5 changed files with 154 additions and 29 deletions

View file

@ -1,7 +1,7 @@
use crate::config::HazeConfig; use crate::config::HazeConfig;
use crate::database::Database; use crate::database::Database;
use crate::exec::{exec, exec_tty};
use crate::php::PhpVersion; use crate::php::PhpVersion;
use crate::tty::exec_tty;
use bollard::container::{ListContainersOptions, LogsOptions, RemoveContainerOptions}; use bollard::container::{ListContainersOptions, LogsOptions, RemoveContainerOptions};
use bollard::models::ContainerState; use bollard::models::ContainerState;
use bollard::network::CreateNetworkOptions; use bollard::network::CreateNetworkOptions;
@ -10,17 +10,17 @@ use camino::{Utf8Path, Utf8PathBuf};
use color_eyre::{eyre::WrapErr, Report, Result}; use color_eyre::{eyre::WrapErr, Report, Result};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use petname::petname; use petname::petname;
use reqwest::{Client, Url};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::fs; use std::fs;
use std::io::stdout;
use std::iter::Peekable; use std::iter::Peekable;
use std::net::IpAddr; use std::net::IpAddr;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use tokio::fs::{create_dir_all, remove_dir_all, write}; use tokio::fs::{create_dir_all, remove_dir_all, write};
use tokio::time::{sleep, timeout}; use tokio::time::sleep;
#[derive(Clone, Default, Debug, Eq, PartialEq)] #[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct CloudOptions { pub struct CloudOptions {
@ -288,8 +288,17 @@ impl Cloud {
Ok(logs) Ok(logs)
} }
pub async fn exec<S: Into<String>>(&self, docker: &mut Docker, cmd: Vec<S>) -> Result<()> { pub async fn exec<S: Into<String>>(
&self,
docker: &mut Docker,
cmd: Vec<S>,
tty: bool,
) -> Result<i64> {
if tty {
exec_tty(docker, &self.id, "haze", cmd, vec![]).await exec_tty(docker, &self.id, "haze", cmd, vec![]).await
} else {
exec(docker, &self.id, "haze", cmd, vec![], Some(stdout())).await
}
} }
pub async fn list( pub async fn list(
@ -372,19 +381,16 @@ impl Cloud {
.ok_or(Report::msg("No clouds running matching filter")) .ok_or(Report::msg("No clouds running matching filter"))
} }
pub async fn wait_for_start(&self) -> Result<()> { pub async fn wait_for_start(&self, docker: &mut Docker) -> Result<()> {
let client = Client::new(); self.php
let url = Url::parse(&format!( .wait_for_start(self.ip)
"http://{}/status.php",
self.ip.ok_or(Report::msg("Container not running"))?
))?;
timeout(Duration::from_secs(5), async {
while !client.get(url.clone()).send().await.is_ok() {
sleep(Duration::from_millis(100)).await
}
})
.await .await
.wrap_err("Timeout after 5 seconds") .wrap_err("Failed to wait for php container")?;
self.db
.wait_for_start(docker, &self.id)
.await
.wrap_err("Failed to wait for database container")?;
Ok(())
} }
pub async fn enable_app<S: Into<String>>(&self, docker: &mut Docker, app: S) -> Result<()> { pub async fn enable_app<S: Into<String>>(&self, docker: &mut Docker, app: S) -> Result<()> {
@ -396,8 +402,10 @@ impl Cloud {
app.into(), app.into(),
"--force".to_string(), "--force".to_string(),
], ],
false,
) )
.await .await?;
Ok(())
} }
} }

View file

@ -1,11 +1,14 @@
use crate::exec::{exec, exec_tty};
use crate::image::pull_image; use crate::image::pull_image;
use crate::tty::exec_tty;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig}; use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig}; use bollard::models::{EndpointSettings, HostConfig};
use bollard::Docker; use bollard::Docker;
use color_eyre::{Report, Result}; use color_eyre::{eyre::WrapErr, Report, Result};
use maplit::hashmap; use maplit::hashmap;
use std::io::Stdout;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration;
use tokio::time::{sleep, timeout};
pub enum DatabaseFamily { pub enum DatabaseFamily {
Sqlite, Sqlite,
@ -198,7 +201,7 @@ impl Database {
Ok(Some(id)) Ok(Some(id))
} }
pub async fn exec(&self, docker: &mut Docker, cloud_id: &str) -> Result<()> { pub async fn exec(&self, docker: &mut Docker, cloud_id: &str) -> Result<i64> {
match self.family() { match self.family() {
DatabaseFamily::Sqlite => { DatabaseFamily::Sqlite => {
exec_tty( exec_tty(
@ -232,4 +235,48 @@ impl Database {
} }
} }
} }
pub async fn wait_for_start(&self, docker: &mut Docker, cloud_id: &str) -> Result<()> {
timeout(Duration::from_secs(15), async {
while !self.is_healthy(docker, cloud_id).await? {
sleep(Duration::from_millis(100)).await
}
Ok(())
})
.await
.wrap_err("Timeout after 15 seconds")?
}
async fn is_healthy(&self, docker: &mut Docker, cloud_id: &str) -> Result<bool> {
match self.family() {
DatabaseFamily::Sqlite => Ok(true),
DatabaseFamily::Mysql => Ok(true),
DatabaseFamily::MariaDB => Ok(true),
DatabaseFamily::Postgres => {
let is_ready_status = exec(
docker,
format!("{}-db", cloud_id),
"root",
vec!["pg_isready", "-U", "haze", "-q"],
vec![],
Option::<Stdout>::None,
)
.await?;
if is_ready_status == 0 {
let connect_status = exec(
docker,
format!("{}-db", cloud_id),
"root",
vec!["psql", "-U", "haze", "-qtA", "-c", ""],
vec![],
Option::<Stdout>::None,
)
.await?;
Ok(connect_status == 0)
} else {
Ok(false)
}
}
}
}
} }

View file

@ -1,6 +1,7 @@
use bollard::exec::{CreateExecOptions, StartExecResults}; use bollard::exec::{CreateExecOptions, StartExecResults};
use bollard::Docker; use bollard::Docker;
use color_eyre::{eyre::WrapErr, Result}; use color_eyre::{eyre::WrapErr, Result};
use futures_util::StreamExt;
use std::io::{stdout, Read, Write}; use std::io::{stdout, Read, Write};
use std::time::Duration; use std::time::Duration;
use termion::async_stdin; use termion::async_stdin;
@ -15,7 +16,7 @@ pub async fn exec_tty<S1: AsRef<str>, S2: Into<String>>(
user: &str, user: &str,
cmd: Vec<S2>, cmd: Vec<S2>,
env: Vec<&str>, env: Vec<&str>,
) -> Result<()> { ) -> Result<i64> {
let cmd = cmd.into_iter().map(S2::into).collect(); let cmd = cmd.into_iter().map(S2::into).collect();
let env = env.into_iter().map(String::from).collect(); let env = env.into_iter().map(String::from).collect();
let config = CreateExecOptions { let config = CreateExecOptions {
@ -68,5 +69,53 @@ pub async fn exec_tty<S1: AsRef<str>, S2: Into<String>>(
} else { } else {
unreachable!(); unreachable!();
} }
Ok(())
Ok(docker
.inspect_exec(&message.id)
.await?
.exit_code
.unwrap_or_default())
}
pub async fn exec<S1: AsRef<str>, S2: Into<String>>(
docker: &mut Docker,
container: S1,
user: &str,
cmd: Vec<S2>,
env: Vec<&str>,
mut std_out: Option<impl Write>,
) -> Result<i64> {
let cmd = cmd.into_iter().map(S2::into).collect();
let env = env.into_iter().map(String::from).collect();
let config = CreateExecOptions {
cmd: Some(cmd),
user: Some(user.to_string()),
attach_stdout: Some(true),
attach_stderr: Some(true),
env: Some(env),
..Default::default()
};
let message = docker
.create_exec(container.as_ref(), config)
.await
.wrap_err("Failed to setup exec")?;
if let StartExecResults::Attached { mut output, .. } = docker
.start_exec(&message.id, None, false)
.await
.wrap_err("Failed to start exec")?
{
while let Some(Ok(line)) = output.next().await {
if let Some(std_out) = &mut std_out {
write!(std_out, "{}", line)?;
}
}
} else {
unreachable!();
}
Ok(docker
.inspect_exec(&message.id)
.await?
.exit_code
.unwrap_or_default())
} }

View file

@ -8,9 +8,9 @@ mod args;
mod cloud; mod cloud;
mod config; mod config;
mod database; mod database;
mod exec;
mod image; mod image;
mod php; mod php;
mod tty;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@ -74,6 +74,7 @@ async fn main() -> Result<()> {
} else { } else {
command command
}, },
true,
) )
.await?; .await?;
} }
@ -83,7 +84,7 @@ async fn main() -> Result<()> {
} => { } => {
let cloud = Cloud::get_by_filter(&mut docker, filter, &config).await?; let cloud = Cloud::get_by_filter(&mut docker, filter, &config).await?;
command.insert(0, "occ".to_string()); command.insert(0, "occ".to_string());
cloud.exec(&mut docker, command).await?; cloud.exec(&mut docker, command, true).await?;
} }
HazeArgs::Db { filter } => { HazeArgs::Db { filter } => {
let cloud = Cloud::get_by_filter(&mut docker, filter, &config).await?; let cloud = Cloud::get_by_filter(&mut docker, filter, &config).await?;
@ -98,10 +99,10 @@ async fn main() -> Result<()> {
} }
HazeArgs::Test { options, path } => { HazeArgs::Test { options, path } => {
let cloud = Cloud::create(&mut docker, options, &config).await?; let cloud = Cloud::create(&mut docker, options, &config).await?;
cloud.wait_for_start().await?; cloud.wait_for_start(&mut docker).await?;
println!("Installing"); println!("Installing");
cloud cloud
.exec(&mut docker, vec!["install", "admin", "admin"]) .exec(&mut docker, vec!["install", "admin", "admin"], false)
.await?; .await?;
if let Some(app) = path if let Some(app) = path
.as_ref() .as_ref()
@ -118,6 +119,7 @@ async fn main() -> Result<()> {
.exec( .exec(
&mut docker, &mut docker,
vec!["tests".to_string(), path.unwrap_or_default()], vec!["tests".to_string(), path.unwrap_or_default()],
false,
) )
.await?; .await?;
cloud.destroy(&mut docker).await?; cloud.destroy(&mut docker).await?;

View file

@ -3,9 +3,13 @@ use crate::image::pull_image;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig}; use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig}; use bollard::models::{EndpointSettings, HostConfig};
use bollard::Docker; use bollard::Docker;
use color_eyre::Result; use color_eyre::{eyre::WrapErr, Report, Result};
use maplit::hashmap; use maplit::hashmap;
use reqwest::{Client, Url};
use std::net::IpAddr;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration;
use tokio::time::{sleep, timeout};
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
@ -87,6 +91,21 @@ impl PhpVersion {
docker.start_container::<String>(&id, None).await?; docker.start_container::<String>(&id, None).await?;
Ok(id) Ok(id)
} }
pub async fn wait_for_start(&self, ip: Option<IpAddr>) -> Result<()> {
let client = Client::new();
let url = Url::parse(&format!(
"http://{}/status.php",
ip.ok_or(Report::msg("Container not running"))?
))?;
timeout(Duration::from_secs(5), async {
while !client.get(url.clone()).send().await.is_ok() {
sleep(Duration::from_millis(100)).await
}
})
.await
.wrap_err("Timeout after 5 seconds")
}
} }
impl Default for PhpVersion { impl Default for PhpVersion {