133 lines
3.9 KiB
Rust
133 lines
3.9 KiB
Rust
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<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
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);
|
|
}
|
|
}
|