mirror of
https://codeberg.org/icewind/haze.git
synced 2026-06-03 17:14:08 +02:00
add proxy option
This commit is contained in:
parent
0843863a40
commit
6239b0cab7
9 changed files with 742 additions and 29 deletions
|
|
@ -60,6 +60,7 @@ pub enum HazeArgs {
|
|||
Unpin {
|
||||
filter: Option<String>,
|
||||
},
|
||||
Proxy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
|
|
@ -210,6 +211,7 @@ impl HazeArgs {
|
|||
}
|
||||
HazeCommand::Pin => Ok(HazeArgs::Pin { filter }),
|
||||
HazeCommand::Unpin => Ok(HazeArgs::Unpin { filter }),
|
||||
HazeCommand::Proxy => Ok(HazeArgs::Proxy),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -231,6 +233,7 @@ pub enum HazeCommand {
|
|||
Shell,
|
||||
Pin,
|
||||
Unpin,
|
||||
Proxy,
|
||||
}
|
||||
|
||||
impl FromStr for HazeCommand {
|
||||
|
|
@ -254,6 +257,7 @@ impl FromStr for HazeCommand {
|
|||
"shell" => Ok(HazeCommand::Shell),
|
||||
"pin" => Ok(HazeCommand::Pin),
|
||||
"unpin" => Ok(HazeCommand::Unpin),
|
||||
"proxy" => Ok(HazeCommand::Proxy),
|
||||
_ => Err(Report::msg(format!("Unknown command: {}", s))),
|
||||
}
|
||||
}
|
||||
|
|
@ -277,6 +281,7 @@ impl HazeCommand {
|
|||
HazeCommand::Shell => false,
|
||||
HazeCommand::Pin => true,
|
||||
HazeCommand::Unpin => true,
|
||||
HazeCommand::Proxy => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
src/cloud.rs
32
src/cloud.rs
|
|
@ -158,6 +158,7 @@ pub struct Cloud {
|
|||
pub workdir: Utf8PathBuf,
|
||||
pub services: Vec<Service>,
|
||||
pub pinned: bool,
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
impl Cloud {
|
||||
|
|
@ -373,6 +374,14 @@ impl Cloud {
|
|||
}
|
||||
});
|
||||
|
||||
let clean_id = id.strip_prefix("haze-").unwrap_or(&id);
|
||||
|
||||
let address = match (&config.proxy.address, config.proxy.https) {
|
||||
(public, true) if !public.is_empty() => format!("https://{clean_id}.{public}"),
|
||||
(public, false) if !public.is_empty() => format!("http://{clean_id}.{public}"),
|
||||
_ => format!("http://{ip}"),
|
||||
};
|
||||
|
||||
Ok(Cloud {
|
||||
id,
|
||||
network,
|
||||
|
|
@ -383,6 +392,7 @@ impl Cloud {
|
|||
workdir,
|
||||
services: options.services,
|
||||
pinned: false,
|
||||
address,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -451,7 +461,7 @@ impl Cloud {
|
|||
}
|
||||
|
||||
pub async fn list(
|
||||
docker: &mut Docker,
|
||||
docker: &Docker,
|
||||
filter: Option<String>,
|
||||
config: &HazeConfig,
|
||||
) -> Result<Vec<Cloud>> {
|
||||
|
|
@ -514,6 +524,21 @@ impl Cloud {
|
|||
% 2)
|
||||
== 1;
|
||||
|
||||
let ip = network_info.ip_address.as_ref()?.parse().ok();
|
||||
|
||||
let clean_id = id.strip_prefix("haze-").unwrap_or(&id);
|
||||
|
||||
let address = match (&config.proxy.address, config.proxy.https, ip) {
|
||||
(public, true, Some(_)) if !public.is_empty() => {
|
||||
format!("https://{clean_id}.{public}")
|
||||
}
|
||||
(public, false, Some(_)) if !public.is_empty() => {
|
||||
format!("http://{clean_id}.{public}")
|
||||
}
|
||||
(_, _, Some(ip)) => format!("http://{ip}"),
|
||||
_ => "Not running".into(),
|
||||
};
|
||||
|
||||
service_ids.push(id.clone());
|
||||
Some((
|
||||
cloud.created.unwrap_or_default(),
|
||||
|
|
@ -523,10 +548,11 @@ impl Cloud {
|
|||
db,
|
||||
php,
|
||||
containers: service_ids,
|
||||
ip: network_info.ip_address.as_ref()?.parse().ok(),
|
||||
ip,
|
||||
workdir,
|
||||
services: found_services,
|
||||
pinned,
|
||||
address,
|
||||
},
|
||||
))
|
||||
})
|
||||
|
|
@ -541,7 +567,7 @@ impl Cloud {
|
|||
}
|
||||
|
||||
pub async fn get_by_filter(
|
||||
docker: &mut Docker,
|
||||
docker: &Docker,
|
||||
filter: Option<String>,
|
||||
config: &HazeConfig,
|
||||
) -> Result<Cloud> {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub struct HazeConfig {
|
|||
pub auto_setup: HazeAutoSetupConfig,
|
||||
pub volume: Vec<HazeVolumeConfig>,
|
||||
pub blackfire: Option<HazeBlackfireConfig>,
|
||||
pub proxy: ProxyConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -27,6 +28,8 @@ pub struct RawHazeConfig {
|
|||
pub volume: Vec<HazeVolumeConfig>,
|
||||
#[serde(default)]
|
||||
pub blackfire: Option<HazeBlackfireConfig>,
|
||||
#[serde(default)]
|
||||
pub proxy: ProxyConfig,
|
||||
}
|
||||
|
||||
impl From<RawHazeConfig> for HazeConfig {
|
||||
|
|
@ -46,6 +49,7 @@ impl From<RawHazeConfig> for HazeConfig {
|
|||
auto_setup: raw.auto_setup,
|
||||
volume: raw.volume,
|
||||
blackfire: raw.blackfire,
|
||||
proxy: raw.proxy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -147,6 +151,15 @@ fn load_secret(name: &str, path: Option<String>, raw: Option<String>) -> Result<
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Debug)]
|
||||
pub struct ProxyConfig {
|
||||
pub listen: String,
|
||||
#[serde(default)]
|
||||
pub address: String,
|
||||
#[serde(default)]
|
||||
pub https: bool,
|
||||
}
|
||||
|
||||
impl HazeConfig {
|
||||
pub fn load() -> Result<Self> {
|
||||
let dirs = ProjectDirs::from("nl", "icewind", "haze").unwrap();
|
||||
|
|
|
|||
39
src/main.rs
39
src/main.rs
|
|
@ -4,6 +4,7 @@ use crate::config::HazeConfig;
|
|||
use crate::exec::container_logs;
|
||||
use crate::network::clear_networks;
|
||||
use crate::php::PhpVersion;
|
||||
use crate::proxy::proxy;
|
||||
use crate::service::Service;
|
||||
use crate::service::ServiceTrait;
|
||||
use bollard::Docker;
|
||||
|
|
@ -19,6 +20,7 @@ mod image;
|
|||
mod mapping;
|
||||
mod network;
|
||||
mod php;
|
||||
mod proxy;
|
||||
mod service;
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -49,23 +51,14 @@ async fn main() -> Result<()> {
|
|||
services.push(cloud.db.name());
|
||||
let services = services.join(", ");
|
||||
let pin = if cloud.pinned { "*" } else { "" };
|
||||
match cloud.ip {
|
||||
Some(ip) => println!(
|
||||
"Cloud {}{}, {}, {}, running on http://{}",
|
||||
cloud.id,
|
||||
pin,
|
||||
cloud.php.name(),
|
||||
services,
|
||||
ip
|
||||
),
|
||||
None => println!(
|
||||
"Cloud {}{}, {}, {}, not running",
|
||||
cloud.id,
|
||||
pin,
|
||||
cloud.php.name(),
|
||||
services
|
||||
),
|
||||
}
|
||||
println!(
|
||||
"Cloud {}{}, {}, {}, running on {}",
|
||||
cloud.id,
|
||||
pin,
|
||||
cloud.php.name(),
|
||||
services,
|
||||
cloud.address
|
||||
);
|
||||
}
|
||||
}
|
||||
HazeArgs::Start { options } => {
|
||||
|
|
@ -294,6 +287,9 @@ async fn main() -> Result<()> {
|
|||
let cloud = Cloud::get_by_filter(&mut docker, filter, &config).await?;
|
||||
cloud.unpin(&mut docker).await?;
|
||||
}
|
||||
HazeArgs::Proxy => {
|
||||
proxy(docker, config).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
|
@ -301,7 +297,8 @@ async fn main() -> Result<()> {
|
|||
|
||||
async fn setup(docker: &mut Docker, options: CloudOptions, config: &HazeConfig) -> Result<Cloud> {
|
||||
let cloud = Cloud::create(docker, options, &config).await?;
|
||||
println!("http://{}", cloud.ip.unwrap());
|
||||
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?;
|
||||
|
|
@ -328,7 +325,7 @@ async fn setup(docker: &mut Docker, options: CloudOptions, config: &HazeConfig)
|
|||
"config:system:set",
|
||||
"overwrite.cli.url",
|
||||
"--value",
|
||||
&format!("http://{}", cloud.ip.unwrap()),
|
||||
&cloud.address,
|
||||
],
|
||||
None,
|
||||
)
|
||||
|
|
@ -336,12 +333,12 @@ async fn setup(docker: &mut Docker, options: CloudOptions, config: &HazeConfig)
|
|||
cloud
|
||||
.occ(
|
||||
docker,
|
||||
vec!["config:system:set", "overwritehost", "--value", &ip_str],
|
||||
vec!["config:system:set", "overwritehost", "--value", host],
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let domains = [ip_str.as_str(), "cloud", &cloud.id];
|
||||
let domains = [ip_str.as_str(), "cloud", &cloud.id, host];
|
||||
for (i, domain) in domains.iter().enumerate() {
|
||||
cloud
|
||||
.occ(
|
||||
|
|
|
|||
|
|
@ -155,14 +155,14 @@ impl PhpVersion {
|
|||
ip.ok_or(Report::msg("Container not running"))?
|
||||
))
|
||||
.into_diagnostic()?;
|
||||
timeout(Duration::from_secs(5), async {
|
||||
timeout(Duration::from_secs(15), async {
|
||||
while !client.get(url.clone()).send().await.is_ok() {
|
||||
sleep(Duration::from_millis(100)).await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Timeout after 5 seconds")
|
||||
.wrap_err("Timeout after 15 seconds")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
151
src/proxy.rs
Normal file
151
src/proxy.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
use crate::Result;
|
||||
use crate::{Cloud, HazeConfig};
|
||||
use bollard::Docker;
|
||||
use futures_util::future::Either;
|
||||
use futures_util::FutureExt;
|
||||
use miette::{miette, Context, IntoDiagnostic};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{create_dir_all, remove_file, set_permissions};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::net::UnixListener;
|
||||
use tokio::signal::ctrl_c;
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use warp::host::Authority;
|
||||
use warp::http::{HeaderMap, HeaderValue, Method};
|
||||
use warp::hyper::body::Bytes;
|
||||
use warp::path::FullPath;
|
||||
use warp::Filter;
|
||||
use warp_reverse_proxy::{
|
||||
extract_request_data_filter, proxy_to_and_forward_response, QueryParameters,
|
||||
};
|
||||
|
||||
struct ActiveInstances {
|
||||
known: Mutex<HashMap<String, IpAddr>>,
|
||||
docker: Docker,
|
||||
config: HazeConfig,
|
||||
}
|
||||
|
||||
impl ActiveInstances {
|
||||
pub fn new(docker: Docker, config: HazeConfig) -> Self {
|
||||
ActiveInstances {
|
||||
known: Mutex::default(),
|
||||
docker,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, name: &str) -> Option<IpAddr> {
|
||||
if let Some(ip) = self.known.lock().unwrap().get(name).cloned() {
|
||||
return Some(ip);
|
||||
}
|
||||
|
||||
let cloud = Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
if let Some(ip) = cloud.ip {
|
||||
println!("{name} => {ip}");
|
||||
|
||||
self.known.lock().unwrap().insert(name.into(), ip);
|
||||
return Some(ip);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn proxy(docker: Docker, config: HazeConfig) -> Result<()> {
|
||||
if config.proxy.listen.is_empty() {
|
||||
return Err(miette!("Proxy not configured"));
|
||||
}
|
||||
let listen = config.proxy.listen.clone();
|
||||
|
||||
let instances = ActiveInstances::new(docker, config);
|
||||
serve(instances, listen).await
|
||||
}
|
||||
|
||||
async fn serve(instances: ActiveInstances, listen: String) -> Result<()> {
|
||||
let instances = Arc::new(instances);
|
||||
let instances = warp::any().map(move || instances.clone());
|
||||
|
||||
let proxy = warp::any()
|
||||
.and(warp::filters::host::optional())
|
||||
.and(instances)
|
||||
.and_then(
|
||||
move |host: Option<Authority>, instances: Arc<ActiveInstances>| async move {
|
||||
let host = match host {
|
||||
Some(host) => host,
|
||||
None => return Err(warp::reject::not_found()),
|
||||
};
|
||||
let requested_instance = host.as_str().split('.').next().unwrap();
|
||||
if let Some(ip) = instances.get(requested_instance).await {
|
||||
Ok((format!("http://{}", ip), host.to_string()))
|
||||
} else {
|
||||
eprintln!("Error {} has no known ip", requested_instance);
|
||||
Err(warp::reject::not_found())
|
||||
}
|
||||
},
|
||||
)
|
||||
.untuple_one()
|
||||
.and(extract_request_data_filter())
|
||||
.and_then(
|
||||
move |proxy_address: String,
|
||||
host: String,
|
||||
uri: FullPath,
|
||||
params: QueryParameters,
|
||||
method: Method,
|
||||
mut headers: HeaderMap,
|
||||
body: Bytes| {
|
||||
headers.insert("host", HeaderValue::from_str(&host).unwrap());
|
||||
proxy_to_and_forward_response(
|
||||
proxy_address,
|
||||
String::new(),
|
||||
uri,
|
||||
params,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let cancel = async {
|
||||
ctrl_c().await.ok();
|
||||
};
|
||||
|
||||
let warp_server = warp::serve(proxy);
|
||||
|
||||
let server = if !listen.starts_with('/') {
|
||||
let listen = SocketAddr::from_str(&listen)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to parse proxy listen address")?;
|
||||
Either::Left(warp_server.bind_with_graceful_shutdown(listen, cancel).1)
|
||||
} else {
|
||||
let listen: PathBuf = listen.into();
|
||||
if let Some(parent) = listen.parent() {
|
||||
if !parent.exists() {
|
||||
create_dir_all(&parent).into_diagnostic()?;
|
||||
set_permissions(&parent, PermissionsExt::from_mode(0o755)).into_diagnostic()?;
|
||||
}
|
||||
}
|
||||
remove_file(&listen).ok();
|
||||
|
||||
let listener = UnixListener::bind(&listen).into_diagnostic()?;
|
||||
set_permissions(&listen, PermissionsExt::from_mode(0o666)).into_diagnostic()?;
|
||||
let stream = UnixListenerStream::new(listener);
|
||||
Either::Right(
|
||||
warp_server
|
||||
.serve_incoming_with_graceful_shutdown(stream, cancel)
|
||||
.map(move |_| {
|
||||
remove_file(&listen).ok();
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
server.await;
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue