v0.8.1: add ISO by URL, zero-touch admin bootstrap
Ease-of-use pass inspired by Bootimus (Dnsmasq-PXE is a manual dnsmasq setup guide — nothing to adopt; OpenPXE already replaces that stack). Add ISO by URL: - New http-api `fetch` module: a small FetchJobs registry + a background streaming download (reqwest) that pipes a remote .iso through the same UploadHandle + introspection path as an upload, so a URL-fetched image classifies and gains boot entries identically. Progress is polled by the Storage view and rendered as rows, mirroring uploads. - Routes POST/GET/DELETE /api/isos/fetch. http/https only; .iso-only filename derived from Content-Disposition / URL basename with path traversal stripped; 16 GiB cap; cancel; credential-stripped URL display. Operator-gated, no boot-time outbound — offline boot is untouched. - Storage upload card gains an "Or add by URL" field with progress + cancel. Zero-touch admin bootstrap: - OPENPXE_ADMIN_USERNAME + OPENPXE_ADMIN_PASSWORD (or _PASSWORD_FILE for Docker/K8s secrets) auto-create the admin on first run, so a fresh container is usable with no setup wizard. Seeds the first run only — a lingering env var can't reset a rotated password. Tests: URL parse / filename / Content-Disposition unit tests + a wiremock end-to-end fetch-into-store integration test. clippy/fmt/node clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1c262a6d61
commit
5f98e6e03f
@@ -126,6 +126,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
|
||||
sftp_shares,
|
||||
unattended,
|
||||
uploads: openpxe_http_api::uploads::UploadSessions::default(),
|
||||
fetch_jobs: openpxe_http_api::fetch::FetchJobs::default(),
|
||||
log_bus,
|
||||
started_at: time::OffsetDateTime::now_utc(),
|
||||
public_base_url: "http://127.0.0.1".into(),
|
||||
@@ -191,6 +192,55 @@ async fn api_key_authenticates_gated_endpoints() {
|
||||
assert_eq!(res.status(), StatusCode::OK, "valid key must authenticate");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_iso_by_url_downloads_into_store() {
|
||||
// v0.8.1: add-ISO-by-URL. Serve a real ISO over HTTP, POST its URL, poll
|
||||
// the fetch registry until the background download finishes, then assert
|
||||
// the image landed in the store (classified like an upload).
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
let (state, _dir) = build_state().await;
|
||||
let app = build_router(state);
|
||||
|
||||
let iso = fake_alpine_iso();
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/rescue.iso"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_bytes(iso))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let url = format!("{}/rescue.iso", server.uri());
|
||||
|
||||
let (code, _) = post_json(&app, "/api/isos/fetch", &format!(r#"{{"url":"{url}"}}"#)).await;
|
||||
assert_eq!(code, StatusCode::ACCEPTED, "fetch should start");
|
||||
|
||||
// Bounded poll for completion (the download runs on a spawned task).
|
||||
let mut done = false;
|
||||
for _ in 0..100 {
|
||||
let (_, body) = get(&app, "/api/isos/fetch").await;
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let jobs = v["jobs"].as_array().cloned().unwrap_or_default();
|
||||
assert!(
|
||||
!jobs.iter().any(|j| j["state"] == "failed"),
|
||||
"fetch failed: {jobs:?}"
|
||||
);
|
||||
if jobs.iter().any(|j| j["state"] == "done") {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
assert!(done, "fetch did not complete in time");
|
||||
|
||||
// The fetched ISO is now in the store under the URL basename.
|
||||
let (_, body) = get(&app, "/api/isos").await;
|
||||
assert!(
|
||||
String::from_utf8_lossy(&body).contains("rescue.iso"),
|
||||
"fetched ISO should appear in /api/isos"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn health_and_ready_endpoints() {
|
||||
let (state, _dir) = build_state().await;
|
||||
|
||||
Reference in New Issue
Block a user