Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions crates/trigger-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub struct CliArgs {
/// The path to the certificate key to use for https, if this is not set, normal http will be used. The key should be in PKCS#8 format
#[clap(long, env = "SPIN_TLS_KEY", requires = "tls-cert")]
pub tls_key: Option<PathBuf>,

#[clap(long = "find-free-port")]
pub find_free_port: bool,
}

impl CliArgs {
Expand All @@ -73,6 +76,7 @@ pub struct HttpTrigger {
/// If the port is set to 0, the actual address will be determined by the OS.
listen_addr: SocketAddr,
tls_config: Option<TlsConfig>,
find_free_port: bool,
}

impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
Expand All @@ -82,7 +86,14 @@ impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
type InstanceState = ();

fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result<Self> {
Self::new(app, cli_args.address, cli_args.into_tls_config())
let find_free_port = cli_args.find_free_port;

Self::new(
app,
cli_args.address,
cli_args.into_tls_config(),
find_free_port,
)
}

async fn run(self, trigger_app: TriggerApp<F>) -> anyhow::Result<()> {
Expand All @@ -104,12 +115,14 @@ impl HttpTrigger {
app: &spin_app::App,
listen_addr: SocketAddr,
tls_config: Option<TlsConfig>,
find_free_port: bool,
) -> anyhow::Result<Self> {
Self::validate_app(app)?;

Ok(Self {
listen_addr,
tls_config,
find_free_port,
})
}

Expand All @@ -121,8 +134,14 @@ impl HttpTrigger {
let Self {
listen_addr,
tls_config,
find_free_port,
} = self;
let server = Arc::new(HttpServer::new(listen_addr, tls_config, trigger_app)?);
let server = Arc::new(HttpServer::new(
listen_addr,
tls_config,
find_free_port,
trigger_app,
)?);
Ok(server)
}

Expand Down
63 changes: 56 additions & 7 deletions crates/trigger-http/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
use std::{collections::HashMap, future::Future, io::IsTerminal, net::SocketAddr, sync::Arc};
use std::{
collections::HashMap,
future::Future,
io::{ErrorKind, IsTerminal},
net::SocketAddr,
sync::Arc,
};

use anyhow::{bail, Context};
use http::{
Expand Down Expand Up @@ -41,12 +47,16 @@ use crate::{
Body, NotFoundRouteKind, TlsConfig, TriggerApp, TriggerInstanceBuilder,
};

pub const MAX_RETRIES: u16 = 10;

/// An HTTP server which runs Spin apps.
pub struct HttpServer<F: RuntimeFactors> {
/// The address the server is listening on.
listen_addr: SocketAddr,
/// The TLS configuration for the server.
tls_config: Option<TlsConfig>,
/// Whether to find a free port if the specified port is already in use.
find_free_port: bool,
/// Request router.
router: Router,
/// The app being triggered.
Expand All @@ -62,6 +72,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
pub fn new(
listen_addr: SocketAddr,
tls_config: Option<TlsConfig>,
find_free_port: bool,
trigger_app: TriggerApp<F>,
) -> anyhow::Result<Self> {
// This needs to be a vec before building the router to handle duplicate routes
Expand Down Expand Up @@ -129,6 +140,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
Ok(Self {
listen_addr,
tls_config,
find_free_port,
router,
trigger_app,
component_trigger_configs,
Expand All @@ -138,12 +150,18 @@ impl<F: RuntimeFactors> HttpServer<F> {

/// Serve incoming requests over the provided [`TcpListener`].
pub async fn serve(self: Arc<Self>) -> anyhow::Result<()> {
let listener = TcpListener::bind(self.listen_addr).await.with_context(|| {
format!(
"Unable to listen on {listen_addr}",
listen_addr = self.listen_addr
)
})?;
let listener: TcpListener = if self.find_free_port {
self.search_for_free_port().await?
} else {
TcpListener::bind(self.listen_addr).await.map_err(|err| {
if err.kind() == ErrorKind::AddrInUse {
anyhow::anyhow!("{} is already in use. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr)
} else {
anyhow::anyhow!("Unable to listen on {}: {err:?}", self.listen_addr)
}
})?
};

if let Some(tls_config) = self.tls_config.clone() {
self.serve_https(listener, tls_config).await?;
} else {
Expand All @@ -152,6 +170,37 @@ impl<F: RuntimeFactors> HttpServer<F> {
Ok(())
}

async fn search_for_free_port(&self) -> anyhow::Result<TcpListener> {
let mut found_listener = None;
let mut addr = self.listen_addr;

for _ in 1..=MAX_RETRIES {
if addr.port() == u16::MAX {
anyhow::bail!(
"Couldn't find a free port as we've reached the maximum port number. Consider retrying with a lower base port."
);
}

match TcpListener::bind(addr).await {
Ok(listener) => {
found_listener = Some(listener);
break;
}
Err(err) if err.kind() == ErrorKind::AddrInUse => {
addr.set_port(addr.port() + 1);
continue;
}
Err(err) => anyhow::bail!("Unable to listen on {addr}: {err:?}",),
}
}

found_listener.ok_or_else(|| anyhow::anyhow!(
"Couldn't find a free port in the range {}-{}. Consider retrying with a different base port.",
self.listen_addr.port(),
self.listen_addr.port() + MAX_RETRIES
))
}

async fn serve_http(self: Arc<Self>, listener: TcpListener) -> anyhow::Result<()> {
self.print_startup_msgs("http", &listener)?;
loop {
Expand Down
2 changes: 1 addition & 1 deletion tests/testing-framework/src/runtimes/in_process_spin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async fn initialize_trigger(
.await?;

let app = spin_app::App::new("my-app", locked_app);
let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None)?;
let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None, false)?;
let mut builder = TriggerAppBuilder::<_, FactorsBuilder>::new(trigger);
let trigger_app = builder
.build(
Expand Down
Loading