use std::sync::Arc; use axum::{ body::Body, extract::State, http::{header, HeaderMap, HeaderValue, StatusCode}, response::IntoResponse, }; use tokio::fs; use crate::{auth::require_bootstrap_auth, state::AppState}; /// GET /api/v1/bootstrap/flake-bundle pub async fn flake_bundle( State(state): State>, headers: HeaderMap, ) -> Result { require_bootstrap_auth(&state, &headers)?; let Some(path) = state.config.bootstrap_flake_bundle_path.as_ref() else { return Err(( StatusCode::SERVICE_UNAVAILABLE, "bootstrap flake bundle not configured".to_string(), )); }; let bytes = fs::read(path).await.map_err(|error| { let status = if error.kind() == std::io::ErrorKind::NotFound { StatusCode::NOT_FOUND } else { StatusCode::INTERNAL_SERVER_ERROR }; ( status, format!( "failed to read bootstrap flake bundle {}: {}", path.display(), error ), ) })?; let headers = [ ( header::CONTENT_TYPE, HeaderValue::from_static("application/gzip"), ), ( header::CONTENT_DISPOSITION, HeaderValue::from_static("attachment; filename=\"plasmacloud-flake-bundle.tar.gz\""), ), ]; Ok((headers, Body::from(bytes))) } #[cfg(test)] mod tests { use super::*; use crate::{build_router, config::Config}; use axum::{body::to_bytes, http::Request}; use std::{ fs, time::{SystemTime, UNIX_EPOCH}, }; use tower::ServiceExt; fn temp_path(name: &str) -> std::path::PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); std::env::temp_dir().join(format!("{}-{}-{}", name, std::process::id(), nanos)) } #[tokio::test] async fn flake_bundle_route_serves_configured_bundle() { let bundle_path = temp_path("deployer-flake-bundle"); fs::write(&bundle_path, b"bundle-bytes").unwrap(); let mut config = Config::default(); config.bootstrap_token = Some("test-token".to_string()); config.bootstrap_flake_bundle_path = Some(bundle_path.clone()); let state = Arc::new(AppState::with_config(config)); let app = build_router(state); let response = app .oneshot( Request::builder() .uri("/api/v1/bootstrap/flake-bundle") .header("x-deployer-token", "test-token") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!( response .headers() .get(header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()), Some("application/gzip") ); let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); assert_eq!(body.as_ref(), b"bundle-bytes"); let _ = fs::remove_file(bundle_path); } #[tokio::test] async fn flake_bundle_route_requires_configured_bundle() { let mut config = Config::default(); config.bootstrap_token = Some("test-token".to_string()); let state = Arc::new(AppState::with_config(config)); let app = build_router(state); let response = app .oneshot( Request::builder() .uri("/api/v1/bootstrap/flake-bundle") .header("x-deployer-token", "test-token") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } }