mirror of
https://codeberg.org/icewind/haze.git
synced 2026-06-03 09:04:12 +02:00
656 lines
23 KiB
Rust
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)
|
|
}
|