diff --git a/Cargo.lock b/Cargo.lock index feb6127..0cac29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,70 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "base64" version = "0.23.1" @@ -293,6 +357,7 @@ name = "crete" version = "0.1.3" dependencies = [ "async-walkdir", + "axum", "byteorder", "clap", "clap_complete", @@ -862,6 +927,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.11.1" @@ -875,6 +946,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1213,6 +1285,12 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.3" @@ -1234,6 +1312,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5449c8c750f1a07ea702bbd212bd999fceece9b3d1508b17023b3e174583124b" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minijinja" version = "2.24.0" @@ -1792,6 +1876,29 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -2044,6 +2151,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2082,6 +2190,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] diff --git a/Cargo.toml b/Cargo.toml index 0a39bd7..5ebf523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ serde_yaml = "0.9.34" thiserror = "2.0.21" tokio = { version = "1.52.1", features = ["full"] } ipxe = { path = "./ipxe" } +axum = { version = "0.8.9", features = ["macros"] } socket2 = "0.6.5" socket-pktinfo = "0.4.1" diff --git a/src/cli.rs b/src/cli.rs index 902a5b6..cfcf512 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -52,6 +52,8 @@ pub enum Commands { ShellCompletions, /// Serve config files over tftp Serve { + #[arg(short, long, default_value = "3000")] + port: u16, #[arg(long, default_value = "0.0.0.0")] ip: IpAddr, }, diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..05be665 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,10 @@ +use crete::tftp::ServerError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("No clusters where found")] + NoClustersFound, + #[error("Server error: {0}")] + ServerError(#[from] ServerError), +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..0bf8dab --- /dev/null +++ b/src/file.rs @@ -0,0 +1,37 @@ +use crete::cluster::Cluster; +use ipxe::IPXE_DIR; +use regex::Regex; + +// TODO: Make this a proper async function +pub fn get_file(filename: impl ToString) -> impl Future, &'static str)>> { + // TODO: Make this a static that is initialized once + let re = + Regex::new(r"^talos/(?.+)/(?.+)\.ya?ml$").expect("regex should be valid"); + + let filename: String = filename.to_string(); + let re = re.clone(); + + async move { + const IPXE_PATTERN: &str = "ipxe/"; + if let Some(filename) = filename.strip_prefix(IPXE_PATTERN) { + Some(( + IPXE_DIR.get_file(filename)?.contents().to_owned(), + "application/octet-stream", + )) + } else if let Some(caps) = re.captures(&filename) { + let cluster = &caps["cluster"]; + let node = &caps["node"]; + + let clusters = Cluster::get_all().await; + let cluster = clusters.get(cluster)?; + + let node = cluster.node(node)?; + + let config = node.config(cluster).await; + + Some((config.into(), "application/yaml; charset=utf-8")) + } else { + None + } + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..08eefa5 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use axum::debug_handler; +use axum::extract::Request; +use axum::response::IntoResponse; +use reqwest::{StatusCode, header}; + +use crate::file::get_file; + +#[debug_handler] +pub async fn handler(req: Request) -> impl IntoResponse { + let Some(path) = req.uri().path().strip_prefix("/") else { + return StatusCode::BAD_REQUEST.into_response(); + }; + + let Some((file, content_type)) = get_file(path).await else { + return StatusCode::NOT_FOUND.into_response(); + }; + + let path = PathBuf::from(path); + let filename = path.file_name().unwrap(); + let filename = filename.to_str().unwrap(); + + let content_type = [(header::CONTENT_TYPE, content_type)]; + let disposition = [( + header::CONTENT_DISPOSITION, + format!("inline; filename=\"{filename}\""), + )]; + + (content_type, disposition, file).into_response() +} diff --git a/src/main.rs b/src/main.rs index 7bfdcbe..ec232de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,27 @@ mod cli; +mod error; +mod file; +mod http; +use std::net::SocketAddr; use std::pin::pin; +use axum::Router; +use axum::routing::get; use clap::{CommandFactory, Parser}; use clap_complete::{Shell, generate as generate_complete}; use crete::cluster::Cluster; use crete::environment::PathEnvironment; use crete::talosconfig::{get_current_context, set_context}; -use crete::tftp::ServerError; use crete::{get_configs_path, get_repo_path, set_repo_path, tftp}; use futures::StreamExt; use futures::future::join_all; -use ipxe::IPXE_DIR; use minijinja::context; -use regex::Regex; -use thiserror::Error; use crate::cli::{Cli, Commands, GlobalOpts}; - -#[derive(Debug, Error)] -enum Error { - #[error("No clusters where found")] - NoClustersFound, - #[error("Server error: {0}")] - ServerError(#[from] ServerError), -} +use crate::error::Error; +use crate::file::get_file; +use crate::http::handler; async fn generate(opts: &GlobalOpts) -> Result<(), Error> { let clusters = Cluster::get_all().await; @@ -107,36 +104,21 @@ async fn main() -> Result<(), Error> { "crete", &mut std::io::stdout(), ), - Commands::Serve { ip } => { - let re = Regex::new(r"talos/(?.+)/(?.+)\.ya?ml") - .expect("regex should be valid"); + Commands::Serve { port, ip } => { + tokio::spawn(async move { + let app = Router::new().fallback_service(get(handler)); + let addr: SocketAddr = (ip, port).into(); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + + println!("Http server started on {addr}"); + axum::serve(listener, app).await.unwrap(); + }); tftp::serve(ip, move |filename, _| { let filename = filename.to_owned(); - let re = re.clone(); - - async move { - const IPXE_PATTERN: &str = "ipxe/"; - if let Some(filename) = filename.strip_prefix(IPXE_PATTERN) { - Some(IPXE_DIR.get_file(filename)?.contents().to_owned()) - } else if let Some(caps) = re.captures(&filename) { - let cluster = &caps["cluster"]; - let node = &caps["node"]; - - let clusters = Cluster::get_all().await; - let cluster = clusters.get(cluster)?; - - let node = cluster.node(node)?; - - let config = node.config(cluster).await; - - Some(config.into()) - } else { - None - } - } + async move { get_file(filename).await.map(|(file, _)| file) } }) - .await? + .await?; } };