3 Commits
Author SHA1 Message Date
Dreaded_X 427ec6f61d feat: Reuse compiled regex
Committed / committed (pull_request) Successful in 26s
CI / cargo shear (pull_request) Successful in 5m12s
CI / prek (pull_request) Failing after 6m43s
2026-10-20 03:11:27 +02:00
Dreaded_X 8149ffbc37 feat: Include boot.ipxe script 2026-10-20 02:59:13 +02:00
Dreaded_X 620f36d0bb feat: Added http server that serve the same files 2026-10-20 02:56:39 +02:00
5 changed files with 123 additions and 114 deletions
-1
View File
@@ -23,7 +23,6 @@ fn main() {
.get() .get()
.to_string(), .to_string(),
]) ])
.arg("DOWNLOAD_PROTO_HTTP=1")
.arg(format!( .arg(format!(
"EMBED={}", "EMBED={}",
manifest_dir manifest_dir
+10
View File
@@ -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),
}
+58
View File
@@ -0,0 +1,58 @@
use crete::cluster::Cluster;
use crete::environment::configure_environment;
use ipxe::IPXE_DIR;
use minijinja::{Environment, context};
use regex::regex;
// TODO: Make this a proper async function
pub fn get_file(
filename: impl ToString,
http_url: impl ToString,
) -> impl Future<Output = Option<(Vec<u8>, &'static str)>> {
let re = regex!(r"^talos/(?<cluster>.+)/(?<node>.+)\.ya?ml$");
let filename: String = filename.to_string();
let re = re.clone();
async move {
const IPXE_PATTERN: &str = "ipxe/";
if filename == format!("{IPXE_PATTERN}boot.ipxe") {
let mut env = Environment::new();
configure_environment(&mut env);
let template = env
.template_from_str(include_str!("./boot.ipxe"))
.expect("Template should be valid");
let clusters = Cluster::get_all().await;
let content = template
.render(context! {
clusters => clusters.values().collect::<Vec<_>>(),
httpUrl => http_url.to_string()
})
.unwrap();
Some((content.into(), "text/plain; charset=utf-8"))
} else 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
}
}
}
+46
View File
@@ -0,0 +1,46 @@
use std::path::PathBuf;
use axum::debug_handler;
use axum::extract::Request;
use axum::http::Uri;
use axum::response::IntoResponse;
use reqwest::{StatusCode, header};
use crate::file::get_file;
#[debug_handler]
pub async fn handler(req: Request) -> impl IntoResponse {
let uri: Uri = req
.headers()
.iter()
.find(|(name, _)| header::HOST.eq(name))
.map(|(_, value)| value)
.expect("Host header should exist")
.to_str()
.expect("Header value should be valid ASCII")
.parse()
.expect("Host should be valid Uri");
let host = uri.host().expect("Uri should have host");
let port = uri.port().map(|port| port.as_u16()).unwrap_or(80);
let Some(path) = req.uri().path().strip_prefix("/") else {
return StatusCode::BAD_REQUEST.into_response();
};
let Some((file, content_type)) = get_file(path, format!("http://{host}:{port}")).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()
}
+9 -113
View File
@@ -1,38 +1,27 @@
mod cli; mod cli;
mod error;
mod file;
mod http;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf;
use std::pin::pin; use std::pin::pin;
use axum::extract::Request; use axum::Router;
use axum::http::Uri;
use axum::response::IntoResponse;
use axum::routing::get; use axum::routing::get;
use axum::{Router, debug_handler};
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use clap_complete::{Shell, generate as generate_complete}; use clap_complete::{Shell, generate as generate_complete};
use crete::cluster::Cluster; use crete::cluster::Cluster;
use crete::environment::{PathEnvironment, configure_environment}; use crete::environment::PathEnvironment;
use crete::talosconfig::{get_current_context, set_context}; 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 crete::{get_configs_path, get_repo_path, set_repo_path, tftp};
use futures::StreamExt; use futures::StreamExt;
use futures::future::join_all; use futures::future::join_all;
use ipxe::IPXE_DIR; use minijinja::context;
use minijinja::{Environment, context};
use regex::Regex;
use reqwest::{StatusCode, header};
use thiserror::Error;
use crate::cli::{Cli, Commands, GlobalOpts}; use crate::cli::{Cli, Commands, GlobalOpts};
use crate::error::Error;
#[derive(Debug, Error)] use crate::file::get_file;
enum Error { use crate::http::handler;
#[error("No clusters where found")]
NoClustersFound,
#[error("Server error: {0}")]
ServerError(#[from] ServerError),
}
async fn generate(opts: &GlobalOpts) -> Result<(), Error> { async fn generate(opts: &GlobalOpts) -> Result<(), Error> {
let clusters = Cluster::get_all().await; let clusters = Cluster::get_all().await;
@@ -101,99 +90,6 @@ async fn generate(opts: &GlobalOpts) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[debug_handler]
async fn handler(req: Request) -> impl IntoResponse {
println!("{req:?}");
let uri: Uri = req
.headers()
.iter()
.find(|(name, _)| header::HOST.eq(name))
.map(|(_, value)| value)
.expect("Host header should exist")
.to_str()
.expect("Header value should be valid ASCII")
.parse()
.expect("Host should be valid Uri");
let host = uri.host().expect("Uri should have host");
let port = uri.port().map(|port| port.as_u16()).unwrap_or(80);
let Some(path) = req.uri().path().strip_prefix("/") else {
return StatusCode::BAD_REQUEST.into_response();
};
let Some((file, content_type)) = get_file(path, format!("http://{host}:{port}")).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()
}
// TODO: Make this a proper async function
fn get_file(
filename: impl ToString,
http_url: impl ToString,
) -> impl Future<Output = Option<(Vec<u8>, &'static str)>> {
// TODO: Make this a static that is initialized once
let re =
Regex::new(r"^talos/(?<cluster>.+)/(?<node>.+)\.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 filename == format!("{IPXE_PATTERN}boot.ipxe") {
let mut env = Environment::new();
configure_environment(&mut env);
let template = env
.template_from_str(include_str!("./boot.ipxe"))
.expect("Template should be valid");
let clusters = Cluster::get_all().await;
let content = template
.render(context! {
clusters => clusters.values().collect::<Vec<_>>(),
httpUrl => http_url.to_string()
})
.unwrap();
Some((content.into(), "text/plain; charset=utf-8"))
} else 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
}
}
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Error> {
let cli = Cli::parse(); let cli = Cli::parse();