1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 09:04:12 +02:00
haze/src/main.rs

656 lines
23 KiB
Rust

extern crate core;
use crate::args::{ExecService, GitOperation, HazeArgs};
use crate::cloud::{Cloud, CloudOptions};
use crate::config::HazeConfig;
use crate::database::DatabaseFamily;
use crate::exec::{container_logs, exec, exec_tty};
use crate::git::{checkout_all, pull_all};
use crate::help::help;
use crate::image::update_image;
use crate::network::clear_networks;
use crate::php::PhpVersion;
use crate::proxy::proxy;
use crate::service::ServiceTrait;
use crate::service::{RedisTls, Service};
use crate::sources::Sources;
use bollard::Docker;
use itertools::Itertools;
use miette::{IntoDiagnostic, Report, Result, WrapErr};
use std::env::{var, vars};
use std::fs::{create_dir_all, remove_dir_all, write};
use std::io::stdout;
use std::os::unix::process::CommandExt;
use std::process::{Command, ExitCode};
mod args;
mod cloud;
mod config;
mod database;
mod exec;
mod git;
mod help;
mod image;
mod mapping;
mod network;
mod php;
mod proxy;
mod service;
mod sources;
static FORWARD_ENV: &[&str] = &[
"OCC_LOG",
"OC_PASS",
"XDEBUG_MODE",
"XDEBUG_TRIGGER",
"XDEBUG_CONFIG",
];
fn get_forward_env() -> Vec<String> {
vars()
.filter(|(var, _)| FORWARD_ENV.contains(&var.as_str()))
.map(|(var, value)| format!("{var}={value}"))
.collect()
}
#[tokio::main]
async fn main() -> Result<ExitCode> {
miette::set_panic_hook();
tracing_subscriber::fmt::init();
let docker = Docker::connect_with_local_defaults()
.into_diagnostic()
.wrap_err("Failed to connect to docker")?;
let config = HazeConfig::load().wrap_err("Failed to load config")?;
let args = HazeArgs::parse(&config, std::env::args())?;
match args {
HazeArgs::Clean => {
let list = Cloud::list(&docker, None, &config).await?;
let (retain, remove) = list
.into_iter()
.partition::<Vec<_>, _>(|cloud| cloud.pinned);
for cloud in remove {
if let Err(e) = cloud.destroy(&docker).await {
eprintln!("Error while removing cloud: {:#}", e);
}
}
clear_networks(&docker, &retain).await?;
for cache_dir in config.work_dir.read_dir().into_diagnostic()? {
let cache_dir = cache_dir.into_diagnostic()?;
if let Some(id) = cache_dir
.file_name()
.to_str()
.and_then(|name| name.strip_prefix("haze-"))
{
if !retain.iter().any(|cloud| cloud.id == id) {
let path = cache_dir.path();
remove_dir_all(&path).into_diagnostic().wrap_err_with(|| {
format!("Failed to cleanup stray workdir: {}", path.display())
})?;
}
}
}
}
HazeArgs::List { filter } => {
let list = Cloud::list(&docker, filter, &config).await?;
for cloud in list {
let mut services: Vec<_> = cloud.services().map(Service::name).collect();
services.push(cloud.db().name());
let services = services.join(", ");
let pin = if cloud.pinned { "*" } else { "" };
let version = match cloud.options.version.as_ref() {
Some(version) => format!(", v{version}"),
None => String::new(),
};
println!(
"Cloud {}{}, {}, {}{}, running on {}",
cloud.id,
pin,
cloud.php().name(),
services,
version,
cloud.address,
);
}
}
HazeArgs::Start { options } => {
setup(&docker, options, &config).await?;
}
HazeArgs::Stop { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
cloud.destroy(&docker).await?;
}
HazeArgs::Logs {
filter,
follow,
count,
service,
} => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
let container = if let Some(service) = service {
service
.container_name(&cloud.id)
.ok_or_else(|| Report::msg("service has no logs".to_string()))?
} else {
cloud.id
};
container_logs(&docker, stdout(), &container, count.unwrap_or(20), follow).await?;
}
HazeArgs::Exec {
filter,
service,
command,
root,
} => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
match service {
None => {
let command = if command.is_empty() {
vec!["bash".to_string()]
} else {
command
};
let env = get_forward_env();
let tty = atty::is(atty::Stream::Stdout);
let user = if root { "root" } else { "haze" };
if tty {
exec_tty(&docker, &cloud.id, user, command, env).await?;
} else {
exec(&docker, &cloud.id, user, command, env, Some(stdout())).await?;
}
}
Some(ExecService::Db) => {
cloud
.db()
.exec_sh(
&docker,
&cloud.id,
if command.is_empty() {
vec!["bash".to_string()]
} else {
command
},
atty::is(atty::Stream::Stdout),
)
.await?;
}
}
}
HazeArgs::Occ {
filter,
mut command,
} => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
command.insert(0, "occ".to_string());
cloud
.exec(
&docker,
command,
atty::is(atty::Stream::Stdout),
get_forward_env(),
)
.await?;
}
HazeArgs::Db {
filter,
root,
command,
index,
} => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
if Some("all") == index.as_deref() {
for index in ["1", "2", "3", "4"] {
cloud
.db()
.exec(&docker, &cloud.id, root, &command, Some(index))
.await?;
}
} else {
cloud
.db()
.exec(&docker, &cloud.id, root, &command, index.as_deref())
.await?;
}
}
HazeArgs::Open { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
match cloud.ip {
Some(_) => opener::open(cloud.address).into_diagnostic()?,
None => eprintln!("{} is not running", cloud.id),
}
}
HazeArgs::Test { options, mut args } => {
let cloud = Cloud::create(&docker, options, &config).await?;
println!("Waiting for servers to start");
cloud.wait_for_start(&docker).await?;
if !cloud.preset_config.is_empty() {
println!("Writing preset config");
let encoded_preset_config =
serde_json::to_string(&cloud.preset_config).into_diagnostic()?;
cloud
.write_file(&docker, "config/preset.config.json", encoded_preset_config)
.await?;
cloud.write_file(&docker, "config/preset.config.php", "<?php $CONFIG=json_decode(file_get_contents(__DIR__ . '/preset.config.json'), true);").await?;
}
println!("Installing");
if let Err(e) = cloud
.exec(
&docker,
vec![
"install",
&config.auto_setup.username,
&config.auto_setup.password,
],
false,
Vec::<String>::default(),
)
.await
{
cloud.destroy(&docker).await?;
return Err(e);
}
if let Some(app) = args
.first()
.as_ref()
.and_then(|path| path.strip_prefix("apps/"))
.map(|path| &path[0..path.find('/').unwrap_or(path.len())])
{
if app.starts_with("files_") {
cloud.enable_app(&docker, "files_external").await?;
}
println!("Enabling {}", app);
cloud.enable_app(&docker, app).await?;
}
args.insert(0, "tests".to_string());
let result = cloud.exec(&docker, args, false, get_forward_env()).await?;
cloud.destroy(&docker).await?;
return Ok(result.into());
}
HazeArgs::Integration { options, mut args } => {
let cloud = setup(&docker, options, &config).await?;
args.insert(0, "integration".to_string());
cloud.exec(&docker, args, false, get_forward_env()).await?;
cloud.destroy(&docker).await?;
}
HazeArgs::Fmt { path } => {
let cloud = Cloud::create(&docker, CloudOptions::new(&config), &config).await?;
let mut out_buffer = Vec::<u8>::with_capacity(1024);
println!("Waiting for servers to start");
cloud.wait_for_start(&docker).await?;
println!("Installing composer");
if let Err(e) = cloud
.exec_with_output(
&docker,
vec!["composer", "install"],
Some(&mut out_buffer),
Vec::<String>::default(),
)
.await
.and_then(|c| c.to_result())
{
eprintln!("{}", String::from_utf8_lossy(&out_buffer));
cloud.destroy(&docker).await?;
return Err(e);
}
out_buffer.clear();
println!("Formatting");
if let Err(e) = cloud
.exec(
&docker,
vec!["composer", "run", "cs:fix", path.as_str()],
false,
Vec::<String>::default(),
)
.await
{
cloud.destroy(&docker).await?;
return Err(e);
}
println!("Cleanup");
if let Err(e) = cloud
.exec_with_output(
&docker,
vec!["git", "clean", "-fd", "lib/composer"],
Some(&mut out_buffer),
Vec::<String>::default(),
)
.await
.and_then(|c| c.to_result())
{
eprintln!("{}", String::from_utf8_lossy(&out_buffer));
cloud.destroy(&docker).await?;
return Err(e);
}
if let Err(e) = cloud
.exec_with_output(
&docker,
vec!["git", "checkout", "lib/composer"],
Some(&mut out_buffer),
Vec::<String>::default(),
)
.await
.and_then(|c| c.to_result())
{
eprintln!("{}", String::from_utf8_lossy(&out_buffer));
cloud.destroy(&docker).await?;
return Err(e);
}
cloud.destroy(&docker).await?;
}
HazeArgs::Shell { command, options } => {
let cloud = setup(&docker, options, &config).await?;
cloud
.exec(
&docker,
if command.is_empty() {
vec!["bash".to_string()]
} else {
command
},
true,
get_forward_env(),
)
.await?;
cloud.destroy(&docker).await?;
}
HazeArgs::Pin { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
cloud.pin(&docker).await?;
}
HazeArgs::Unpin { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
cloud.unpin(&docker).await?;
}
HazeArgs::Proxy => {
proxy(docker, config).await?;
}
HazeArgs::Git { operation } => match operation {
GitOperation::Checkout { branch } => {
let sources = Sources::new(&config.sources_root)?;
checkout_all(
&config.sources_root,
&branch.unwrap_or_else(|| sources.get_server_version_branch()),
)?;
}
GitOperation::Pull => {
pull_all(&config.sources_root)?;
}
},
HazeArgs::Env {
filter,
command,
args,
} => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
let ip = cloud
.ip
.ok_or_else(|| Report::msg(format!("{} is not running", cloud.id)))?;
let db_type = match cloud.db().family() {
DatabaseFamily::Sqlite => {
return Err(Report::msg("sqlite is not supported with `haze env`"));
}
DatabaseFamily::Oracle => {
return Err(Report::msg("oracle is not supported with `haze env`"));
}
DatabaseFamily::Mysql | DatabaseFamily::MariaDB => "mysql",
DatabaseFamily::Postgres => "postgresql",
};
let db_ip = cloud
.db()
.ip(&docker, &cloud.id)
.await
.ok_or_else(|| Report::msg(format!("{}-db is not running", cloud.id)))?;
let mut command = Command::new(command);
command
.args(args)
.env("REDIS_URL", format!("redis://{}", ip))
.env("NEXTCLOUD_URL", &cloud.address)
.env(
"DATABASE_URL",
format!("{}://haze:haze@{}/haze", db_type, db_ip),
);
if cloud.services().contains(&Service::RedisTls(RedisTls)) {
create_dir_all(config.work_dir.join("redis_certificates"))
.into_diagnostic()
.wrap_err("Failed to create redis certificate directory")?;
let redis_cert_path = config.work_dir.join("redis_certificates/client.cert");
let redis_key_path = config.work_dir.join("redis_certificates/client.key");
let redis_ca_path = config.work_dir.join("redis_certificates/ca.cert");
if !redis_cert_path.exists() {
write(
&redis_cert_path,
include_bytes!("../redis-certificates/client.crt"),
)
.into_diagnostic()
.wrap_err("Failed to write redis client certificate")?;
}
if !redis_key_path.exists() {
write(
&redis_key_path,
include_bytes!("../redis-certificates/client.key"),
)
.into_diagnostic()
.wrap_err("Failed to write redis client key")?;
}
if !redis_ca_path.exists() {
write(
&redis_ca_path,
include_bytes!("../redis-certificates/ca.crt"),
)
.into_diagnostic()
.wrap_err("Failed to write redis ca certificate")?;
}
command
.env("REDIS_URL", format!("rediss://{}", ip))
.env("REDIS_TLS_DONT_VALIDATE_HOSTNAME", "1")
.env("REDIS_TLS_CERT", redis_cert_path)
.env("REDIS_TLS_KEY", redis_key_path)
.env("REDIS_TLS_CA", redis_ca_path);
}
let err = command.exec();
return Err(err).into_diagnostic();
}
HazeArgs::Update => {
for php in PhpVersion::all() {
update_image(&docker, php.image()).await?;
}
}
HazeArgs::Version => {
const VERSION: &str = env!("CARGO_PKG_VERSION");
println!("haze v{}", VERSION);
}
HazeArgs::Help { command } => {
help(command.as_deref());
}
HazeArgs::Edit { filter, path } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
let path = cloud
.get_local_path(&config, &path)
.ok_or_else(|| Report::msg(format!("{path} not found on the instance")))?;
let editor = var("EDITOR").map_err(|_| Report::msg("$EDITOR not set"))?;
let err = Command::new(editor).arg(path).exec(); // this never exists without error
return Err(err)
.into_diagnostic()
.wrap_err("Failed to start $EDITOR");
}
HazeArgs::Reload { filter } => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
exec(
&docker,
&cloud.id,
"root",
vec!["pkill", "php-fpm"],
Vec::<String>::new(),
Some(stdout()),
)
.await?;
exec(
&docker,
&cloud.id,
"root",
vec!["sh", "-c", "php-fpm --fpm-config /etc/php-fpm.conf&"],
Vec::<String>::new(),
Some(stdout()),
)
.await?;
}
};
Ok(ExitCode::SUCCESS)
}
async fn setup(docker: &Docker, options: CloudOptions, config: &HazeConfig) -> Result<Cloud> {
let cloud = Cloud::create(docker, options, config).await?;
println!("{}", cloud.address);
let host = cloud.address.split_once("://").expect("no address?").1;
if config.auto_setup.enabled {
println!("Waiting for servers to start");
cloud.wait_for_start(docker).await?;
if !cloud.preset_config.is_empty() {
println!("Writing preset config");
let encoded_preset_config =
serde_json::to_string(&cloud.preset_config).into_diagnostic()?;
cloud
.write_file(docker, "config/preset.config.json", encoded_preset_config)
.await?;
cloud.write_file(docker, "config/preset.config.php", "<?php $CONFIG=json_decode(file_get_contents(__DIR__ . '/preset.config.json'), true);").await?;
}
println!(
"Installing with username {} and password {}",
config.auto_setup.username, config.auto_setup.password
);
let ip_str = format!("{}", cloud.ip.unwrap());
cloud
.exec(
docker,
vec![
"install",
&config.auto_setup.username,
&config.auto_setup.password,
],
false,
Vec::<String>::default(),
)
.await?;
cloud
.occ(
docker,
vec![
"config:system:set",
"overwrite.cli.url",
"--value",
&cloud.address,
],
None,
Vec::<String>::default(),
)
.await?;
cloud
.occ(
docker,
vec!["config:system:set", "overwritehost", "--value", host],
None,
Vec::<String>::default(),
)
.await?;
if cloud.address.contains("https://") {
cloud
.occ(
docker,
vec!["config:system:set", "overwriteprotocol", "--value", "https"],
None,
Vec::<String>::default(),
)
.await?;
}
let domains = [ip_str.as_str(), "cloud", &cloud.id, host];
for (i, domain) in domains.iter().enumerate() {
cloud
.occ(
docker,
vec![
"config:system:set",
"trusted_domains",
&format!("{}", i),
"--value",
domain,
],
None,
Vec::<String>::default(),
)
.await?;
}
for app in &config.auto_setup.enable_apps {
cloud
.exec(
docker,
vec!["occ", "app:enable", app.as_str(), "--force"],
false,
Vec::<String>::default(),
)
.await?;
}
for app in &config.auto_setup.disable_apps {
cloud
.exec(
docker,
vec!["occ", "app:disable", app.as_str()],
false,
Vec::<String>::default(),
)
.await?;
}
for service in cloud.services() {
for app in service.apps() {
cloud
.exec(
docker,
vec!["occ", "app:enable", *app, "--force"],
false,
Vec::<String>::default(),
)
.await?;
}
}
for service in cloud.services() {
for cmd in service.post_setup(docker, &cloud.id, config).await? {
cloud
.exec(
docker,
shell_words::split(&cmd).into_diagnostic()?,
false,
Vec::<String>::default(),
)
.await?;
}
}
for cmd in &config.auto_setup.post_setup {
cloud
.exec(
docker,
shell_words::split(cmd).into_diagnostic()?,
false,
Vec::<String>::default(),
)
.await?;
}
}
Ok(cloud)
}