mod connection;
mod draining;
mod options;
mod supervisor;
mod targets;
use std::fmt;
use std::future::Future;
use std::path::PathBuf;
use std::time::Duration;
use crate::endpoint::Endpoint;
use options::Config;
use supervisor::Engine;
const DEFAULT_SHUTDOWN_GRACE: Duration = Duration::from_secs(120);
pub struct Tunnel {
endpoint: Endpoint,
config: Config,
shutdown_grace: Duration,
}
impl Tunnel {
pub fn new(endpoint: Endpoint) -> Self {
Self {
endpoint,
config: Config::default(),
shutdown_grace: DEFAULT_SHUTDOWN_GRACE,
}
}
pub fn region(mut self, region: impl Into<String>) -> Self {
self.config.region = Some(region.into());
self
}
pub fn tunnel_servers_srv(mut self, name: impl Into<String>) -> Self {
self.config.tunnel_servers_srv = Some(name.into());
self
}
pub fn tunnel_servers<I, S>(mut self, servers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config.tunnel_servers = Some(servers.into_iter().map(Into::into).collect());
self
}
pub fn environment_id(mut self, environment_id: impl Into<String>) -> Self {
self.config.environment_id = Some(environment_id.into());
self
}
pub fn auth_token(mut self, token: impl Into<String>) -> Self {
self.config.auth_token = Some(token.into());
self
}
pub fn auth_token_file(mut self, path: impl Into<PathBuf>) -> Self {
self.config.auth_token_file = Some(path.into());
self
}
pub fn signing_public_key(mut self, key: impl Into<String>) -> Self {
self.config.signing_public_key = Some(key.into());
self
}
pub fn tunnel_name(mut self, name: impl Into<String>) -> Self {
self.config.tunnel_name = Some(name.into());
self
}
pub fn tunnel_worker_id(mut self, id: impl Into<String>) -> Self {
self.config.tunnel_worker_id = Some(id.into());
self
}
pub fn shutdown_grace(mut self, grace: Duration) -> Self {
self.shutdown_grace = grace;
self
}
pub async fn run(self) -> Result<(), Error> {
let options = self.config.resolve().map_err(Error::configuration)?;
let mut signals = Signals::new().map_err(Error::signal)?;
let engine = Engine::start(self.endpoint, options).map_err(Error::startup)?;
let ready = tokio::select! {
ready = engine.wait_ready() => Some(ready),
_ = signals.recv() => None,
};
match ready {
Some(Ok(info)) => {
tracing::info!(
tunnel_name = info.tunnel_name(),
proxy_url = info.proxy_url(),
tunnel_url = info.tunnel_url(),
"Restate Cloud tunnel connected. Register this deployment with Restate: {}",
info.deployment_url()
);
}
Some(Err(error)) => {
engine.reap().await;
return Err(error);
}
None => return shutdown_after_signal(engine, self.shutdown_grace, &mut signals).await,
}
tokio::select! {
terminal = engine.wait_terminal() => {
engine.reap().await;
terminal
}
_ = signals.recv() => shutdown_after_signal(engine, self.shutdown_grace, &mut signals).await,
}
}
pub async fn connect(self) -> Result<TunnelConnection, Error> {
let options = self.config.resolve().map_err(Error::configuration)?;
let engine = Engine::start(self.endpoint, options).map_err(Error::startup)?;
let info = match engine.wait_ready().await {
Ok(info) => info,
Err(error) => {
engine.reap().await;
return Err(error);
}
};
Ok(TunnelConnection {
engine: Some(engine),
info,
shutdown_grace: self.shutdown_grace,
})
}
}
async fn shutdown_after_signal(
engine: Engine,
grace: Duration,
signals: &mut Signals,
) -> Result<(), Error> {
engine.begin_shutdown();
let deadline = tokio::time::Instant::now().checked_add(grace);
let grace_elapsed = async move {
match deadline {
Some(deadline) => tokio::time::sleep_until(deadline).await,
None => std::future::pending::<()>().await,
}
};
tokio::select! {
terminal = engine.wait_terminal() => {
engine.reap().await;
terminal
}
_ = grace_elapsed => {
engine.force_close();
let result = engine.wait_terminal().await;
engine.reap().await;
result
}
_ = signals.recv() => {
engine.force_close();
let result = engine.wait_terminal().await;
engine.reap().await;
result
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TunnelInfo {
tunnel_name: String,
proxy_url: String,
tunnel_url: String,
deployment_url: String,
}
impl TunnelInfo {
pub(crate) fn from_handshake(
tunnel_name: String,
proxy_url: String,
tunnel_url: String,
) -> Result<Self, &'static str> {
let mut proxy = url::Url::parse(&proxy_url).map_err(|_| "invalid proxy-url")?;
if proxy.host_str().is_none() {
return Err("proxy-url has no host");
}
let has_explicit_port = proxy_url
.parse::<http::Uri>()
.ok()
.and_then(|uri| uri.authority().and_then(|authority| authority.port_u16()))
.is_some();
let deployment_base = if has_explicit_port {
proxy_url.trim_end_matches('/').to_owned()
} else {
proxy
.set_port(Some(9080))
.map_err(|_| "proxy-url cannot accept a port")?;
proxy.as_str().trim_end_matches('/').to_owned()
};
let deployment_url = format!("{deployment_base}/http/in-process/9080/");
Ok(Self {
tunnel_name,
proxy_url,
tunnel_url,
deployment_url,
})
}
pub fn tunnel_name(&self) -> &str {
&self.tunnel_name
}
pub fn proxy_url(&self) -> &str {
&self.proxy_url
}
pub fn tunnel_url(&self) -> &str {
&self.tunnel_url
}
pub fn deployment_url(&self) -> &str {
&self.deployment_url
}
}
pub struct TunnelConnection {
engine: Option<Engine>,
info: TunnelInfo,
shutdown_grace: Duration,
}
impl TunnelConnection {
pub fn info(&self) -> &TunnelInfo {
&self.info
}
pub fn shutdown(self) -> impl Future<Output = Result<(), Error>> + Send + 'static {
let grace = self.shutdown_grace;
self.shutdown_with_grace(grace)
}
pub fn shutdown_with_grace(
mut self,
grace: Duration,
) -> impl Future<Output = Result<(), Error>> + Send + 'static {
let engine = self.engine.take().expect("tunnel engine is present");
engine.begin_shutdown();
let deadline = tokio::time::Instant::now().checked_add(grace);
async move {
let result = match deadline {
Some(deadline) => tokio::time::timeout_at(deadline, engine.wait_terminal()).await,
None => Ok(engine.wait_terminal().await),
};
let result = match result {
Ok(result) => result,
Err(_) => {
engine.force_close();
engine.wait_terminal().await
}
};
engine.reap().await;
result
}
}
pub fn close(mut self) -> impl Future<Output = Result<(), Error>> + Send + 'static {
let engine = self.engine.take().expect("tunnel engine is present");
engine.force_close();
async move {
let result = engine.wait_terminal().await;
engine.reap().await;
result
}
}
}
impl Drop for TunnelConnection {
fn drop(&mut self) {
if let Some(engine) = self.engine.take() {
engine.abort();
}
}
}
#[derive(Debug)]
pub struct Error(ErrorInner);
#[derive(Debug, thiserror::Error)]
enum ErrorInner {
#[error("{0}")]
Configuration(String),
#[error("tunnel: failed to start: {0}")]
Startup(String),
#[error("tunnel: fatal connection error: {0}")]
Fatal(String),
#[error("tunnel: closed before the first successful handshake")]
ClosedBeforeReady,
#[error("tunnel: signal setup failed: {0}")]
Signal(#[source] std::io::Error),
}
impl Error {
fn configuration(error: impl fmt::Display) -> Self {
Self(ErrorInner::Configuration(error.to_string()))
}
fn startup(error: impl fmt::Display) -> Self {
Self(ErrorInner::Startup(error.to_string()))
}
pub(crate) fn fatal(reason: impl Into<String>) -> Self {
Self(ErrorInner::Fatal(reason.into()))
}
pub(crate) fn closed_before_ready() -> Self {
Self(ErrorInner::ClosedBeforeReady)
}
fn signal(error: std::io::Error) -> Self {
Self(ErrorInner::Signal(error))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
pub type TunnelError = Error;
struct Signals {
interrupt: tokio::signal::unix::Signal,
terminate: tokio::signal::unix::Signal,
}
impl Signals {
fn new() -> std::io::Result<Self> {
use tokio::signal::unix::{SignalKind, signal};
Ok(Self {
interrupt: signal(SignalKind::interrupt())?,
terminate: signal(SignalKind::terminate())?,
})
}
async fn recv(&mut self) {
tokio::select! {
_ = self.interrupt.recv() => {},
_ = self.terminate.recv() => {},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deployment_url_defaults_proxy_port() {
let info = TunnelInfo::from_handshake(
"greeter-v1".into(),
"https://proxy.example/env/tunnel".into(),
"https://tunnel.example".into(),
)
.unwrap();
assert_eq!(
info.deployment_url(),
"https://proxy.example:9080/env/tunnel/http/in-process/9080/"
);
assert_eq!(info.proxy_url(), "https://proxy.example/env/tunnel");
let explicit_default = TunnelInfo::from_handshake(
"greeter-v1".into(),
"https://proxy.example:443/env/tunnel".into(),
"https://tunnel.example".into(),
)
.unwrap();
assert_eq!(
explicit_default.deployment_url(),
"https://proxy.example:443/env/tunnel/http/in-process/9080/"
);
}
}