diff --git a/Cargo.lock b/Cargo.lock index 6954294..c26daab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,16 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -81,6 +91,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + [[package]] name = "autocfg" version = "1.5.1" @@ -139,6 +155,36 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-test" +version = "17.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum", + "bytes", + "bytesize", + "cookie", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower", + "url", +] + [[package]] name = "base64" version = "0.22.1" @@ -193,6 +239,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + [[package]] name = "cc" version = "1.2.62" @@ -264,6 +316,16 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -336,6 +398,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1082,6 +1159,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -1195,6 +1278,7 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-test", "figment", "picloud-executor-core", "picloud-manager-core", @@ -1300,6 +1384,7 @@ dependencies = [ name = "picloud-shared" version = "0.1.0" dependencies = [ + "async-trait", "chrono", "serde", "serde_json", @@ -1361,6 +1446,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1370,6 +1461,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1610,6 +1711,15 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "reserve-port" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "rhai" version = "1.24.0" @@ -1674,6 +1784,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.9.4", + "thiserror 2.0.18", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2249,6 +2374,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" diff --git a/crates/manager-core/src/api.rs b/crates/manager-core/src/api.rs index d6cb3b7..1f1ae76 100644 --- a/crates/manager-core/src/api.rs +++ b/crates/manager-core/src/api.rs @@ -11,23 +11,27 @@ use axum::{ routing::get, Json, Router, }; -use picloud_shared::{Script, ScriptId, ScriptValidator, ValidationError}; +use picloud_shared::{ExecutionLog, Script, ScriptId, ScriptValidator, ValidationError}; use serde::Deserialize; -use crate::repo::{NewScript, ScriptPatch, ScriptRepository, ScriptRepositoryError}; +use crate::repo::{ + ExecutionLogRepository, NewScript, ScriptPatch, ScriptRepository, ScriptRepositoryError, +}; /// State shared by control-plane handlers. Separates concerns so the /// manager can validate at upload time without depending on the /// concrete executor-core types. -pub struct AdminState { +pub struct AdminState { pub repo: Arc, + pub logs: Arc, pub validator: Arc, } -impl Clone for AdminState { +impl Clone for AdminState { fn clone(&self) -> Self { Self { repo: self.repo.clone(), + logs: self.logs.clone(), validator: self.validator.clone(), } } @@ -35,15 +39,23 @@ impl Clone for AdminState { /// Build the admin router. The caller (binary) chooses where to mount /// it (typically `Router::new().nest("/api/admin", admin_router(state))`). -pub fn admin_router(state: AdminState) -> Router { +pub fn admin_router(state: AdminState) -> Router +where + R: ScriptRepository + 'static, + L: ExecutionLogRepository + 'static, +{ Router::new() - .route("/scripts", get(list_scripts::).post(create_script::)) + .route( + "/scripts", + get(list_scripts::).post(create_script::), + ) .route( "/scripts/{id}", - get(get_script::) - .put(update_script::) - .delete(delete_script::), + get(get_script::) + .put(update_script::) + .delete(delete_script::), ) + .route("/scripts/{id}/logs", get(list_logs::)) .with_state(state) } @@ -85,14 +97,14 @@ where // Handlers // ---------------------------------------------------------------------------- -async fn list_scripts( - State(state): State>, +async fn list_scripts( + State(state): State>, ) -> Result>, ApiError> { Ok(Json(state.repo.list().await?)) } -async fn get_script( - State(state): State>, +async fn get_script( + State(state): State>, Path(id): Path, ) -> Result, ApiError> { state @@ -103,8 +115,8 @@ async fn get_script( .ok_or(ApiError::NotFound(id)) } -async fn create_script( - State(state): State>, +async fn create_script( + State(state): State>, Json(input): Json, ) -> Result<(StatusCode, Json + +
+ ← Scripts + + {#if scriptLoading} +

Loading…

+ {:else if scriptError} +
{scriptError}
+ {:else if script} + + +
+ +
+

Source

+ + {#if saveError} +
{saveError}
+ {/if} +
+ +
+
+ + +
+

Test invoke

+ + +
+ +
+ {#if testError} +
{testError}
+ {/if} + {#if testResult} +
+
+ HTTP {testResult.status} +
+
{JSON.stringify(testResult.body, null, 2)}
+
+ {/if} +
+
+ + +
+
+

Recent executions

+ +
+ {#if logsError} +
{logsError}
+ {:else if logs && logs.length === 0} +

No executions yet — try the Test invoke panel above.

+ {:else if logs} +
    + {#each logs as log (log.id)} +
  • +
    + + + {log.status} + + {new Date(log.created_at).toLocaleString()} + + {log.response_code ?? '—'} · {log.duration_ms}ms + + +
    +
    + Request body +
    {JSON.stringify(log.request_body, null, 2)}
    +
    +
    + Response body +
    {JSON.stringify(log.response_body, null, 2)}
    +
    + {#if log.script_logs && log.script_logs.length > 0} +
    + Script logs +
      + {#each log.script_logs as entry} +
    • + + {entry.level} + + {entry.message} + {#if entry.data !== null && entry.data !== undefined} +
      {JSON.stringify(entry.data)}
      + {/if} +
    • + {/each} +
    +
    + {/if} +
    +
    +
  • + {/each} +
+ {/if} +
+ {/if} +
+ +