feat(console): expose component capabilities

- project structured capability package, name, version, and interface fields
- derive capabilities during registration and artifact restoration
- keep Component imports as the source of truth without changing .wasmpkg
- verify empty and Clock capability responses through the management API
This commit is contained in:
Maofeng
2026-07-29 19:13:41 +08:00
parent 5b32886efe
commit 72a264e0d1
2 changed files with 83 additions and 3 deletions
+44 -3
View File
@@ -39,7 +39,8 @@ use wasmeld_package::{
wit_package::{WitPackageError, WitPackageMetadata}, wit_package::{WitPackageError, WitPackageMetadata},
}; };
use wasmeld_runtime::{ use wasmeld_runtime::{
ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey, ServiceManifest, CapabilityDescriptor, ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey,
ServiceManifest,
}; };
use persistence::{Persistence, StoredDeployment, StoredEvent, StoredService}; use persistence::{Persistence, StoredDeployment, StoredEvent, StoredService};
@@ -113,6 +114,7 @@ struct ConsoleState {
#[derive(Clone)] #[derive(Clone)]
struct ServiceRecord { struct ServiceRecord {
manifest: ServiceManifest, manifest: ServiceManifest,
capabilities: Vec<CapabilityView>,
status: ServiceStatus, status: ServiceStatus,
updated_at_ms: u64, updated_at_ms: u64,
calls: u64, calls: u64,
@@ -144,6 +146,26 @@ pub struct DeploymentView {
pub updated_at_ms: u64, pub updated_at_ms: u64,
} }
/// Exact versioned host interface imported by a registered Component.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CapabilityView {
pub interface: String,
pub package: String,
pub name: String,
pub version: String,
}
impl From<CapabilityDescriptor> for CapabilityView {
fn from(capability: CapabilityDescriptor) -> Self {
Self {
interface: capability.interface().to_owned(),
package: capability.package().to_owned(),
name: capability.name().to_owned(),
version: capability.version().to_owned(),
}
}
}
/// API projection of a registered service and its current metrics. /// API projection of a registered service and its current metrics.
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct ServiceView { pub struct ServiceView {
@@ -151,6 +173,7 @@ pub struct ServiceView {
pub revision: String, pub revision: String,
pub artifact: String, pub artifact: String,
pub world: String, pub world: String,
pub capabilities: Vec<CapabilityView>,
pub status: ServiceStatus, pub status: ServiceStatus,
pub updated_at_ms: u64, pub updated_at_ms: u64,
pub limits: ResourceLimits, pub limits: ResourceLimits,
@@ -166,6 +189,7 @@ impl ServiceRecord {
revision: self.manifest.revision.clone(), revision: self.manifest.revision.clone(),
artifact: self.manifest.component.clone(), artifact: self.manifest.component.clone(),
world: self.manifest.world.clone(), world: self.manifest.world.clone(),
capabilities: self.capabilities.clone(),
status: self.status, status: self.status,
updated_at_ms: self.updated_at_ms, updated_at_ms: self.updated_at_ms,
limits: self.manifest.limits.clone(), limits: self.manifest.limits.clone(),
@@ -743,8 +767,9 @@ impl Console {
let _ = fs::remove_file(&manifest_path); let _ = fs::remove_file(&manifest_path);
return Err(error.into()); return Err(error.into());
} }
let capabilities = runtime_capabilities(runtime, &key)?;
let view = self.insert_service(manifest, ServiceStatus::Stopped)?; let view = self.insert_service(manifest, capabilities, ServiceStatus::Stopped)?;
self.push_event( self.push_event(
EventKind::Registered, EventKind::Registered,
Some(&key), Some(&key),
@@ -910,10 +935,12 @@ impl Console {
))); )));
} }
runtime.register_from_file(manifest.clone())?; runtime.register_from_file(manifest.clone())?;
let capabilities = runtime_capabilities(runtime, &key)?;
self.state()?.services.insert( self.state()?.services.insert(
key, key,
ServiceRecord { ServiceRecord {
manifest, manifest,
capabilities,
status: ServiceStatus::Stopped, status: ServiceStatus::Stopped,
updated_at_ms: stored.updated_at_ms, updated_at_ms: stored.updated_at_ms,
calls: stored.calls, calls: stored.calls,
@@ -956,7 +983,8 @@ impl Console {
continue; continue;
} }
runtime.register_from_file(manifest.clone())?; runtime.register_from_file(manifest.clone())?;
self.insert_service(manifest, ServiceStatus::Stopped)?; let capabilities = runtime_capabilities(runtime, &key)?;
self.insert_service(manifest, capabilities, ServiceStatus::Stopped)?;
} }
Ok(()) Ok(())
@@ -1070,12 +1098,14 @@ impl Console {
fn insert_service( fn insert_service(
&self, &self,
manifest: ServiceManifest, manifest: ServiceManifest,
capabilities: Vec<CapabilityView>,
status: ServiceStatus, status: ServiceStatus,
) -> Result<ServiceView, ConsoleError> { ) -> Result<ServiceView, ConsoleError> {
let key = manifest.key()?; let key = manifest.key()?;
let mut state = self.state()?; let mut state = self.state()?;
let record = ServiceRecord { let record = ServiceRecord {
manifest, manifest,
capabilities,
status, status,
updated_at_ms: unix_time_ms(), updated_at_ms: unix_time_ms(),
calls: 0, calls: 0,
@@ -1140,6 +1170,17 @@ impl Console {
} }
} }
fn runtime_capabilities(
runtime: &Runtime,
key: &ServiceKey,
) -> Result<Vec<CapabilityView>, ConsoleError> {
Ok(runtime
.capabilities(key)?
.into_iter()
.map(Into::into)
.collect())
}
fn deployment_view( fn deployment_view(
state: &ConsoleState, state: &ConsoleState,
service_id: &str, service_id: &str,
+39
View File
@@ -40,6 +40,7 @@ async fn manages_a_resident_component_over_http() {
assert_eq!(status, StatusCode::CREATED, "{registered}"); assert_eq!(status, StatusCode::CREATED, "{registered}");
assert_eq!(registered["id"], "echo"); assert_eq!(registered["id"], "echo");
assert_eq!(registered["status"], "stopped"); assert_eq!(registered["status"], "stopped");
assert_eq!(registered["capabilities"], json!([]));
let response = application let response = application
.clone() .clone()
@@ -89,6 +90,41 @@ async fn manages_a_resident_component_over_http() {
assert!(events["events"].as_array().unwrap().len() >= 5); assert!(events["events"].as_array().unwrap().len() >= 5);
} }
#[tokio::test]
async fn reports_exact_component_host_capabilities() {
let artifact_dir = TempDir::new().expect("temporary artifact directory");
let application = test_app(artifact_dir.path()).await;
let component = fs::read(component_artifact("clock_probe_component.wasm"))
.expect("clock probe component should be readable");
let response = application
.clone()
.oneshot(package_request("clock-probe", "0.1.0", &component))
.await
.expect("register request should complete");
assert_eq!(response.status(), StatusCode::CREATED);
let registered = response_json(response).await;
assert_eq!(
registered["capabilities"],
json!([{
"interface": "wasmeld:clock/monotonic-clock@0.1.0",
"package": "wasmeld:clock",
"name": "monotonic-clock",
"version": "0.1.0"
}])
);
let response = application
.oneshot(get_request("/api/v1/services"))
.await
.expect("service list request should complete");
let services = response_json(response).await;
assert_eq!(
services["services"][0]["capabilities"],
registered["capabilities"]
);
}
#[tokio::test] #[tokio::test]
async fn routes_raw_gateway_calls_through_the_active_deployment() { async fn routes_raw_gateway_calls_through_the_active_deployment() {
let artifact_dir = TempDir::new().expect("temporary artifact directory"); let artifact_dir = TempDir::new().expect("temporary artifact directory");
@@ -686,6 +722,8 @@ fn package_request(id: &str, revision: &str, component: &[u8]) -> Request<Body>
let mut package = Cursor::new(Vec::new()); let mut package = Cursor::new(Vec::new());
let world = if id == "counter" { let world = if id == "counter" {
"component:counter/counter-component@0.1.0" "component:counter/counter-component@0.1.0"
} else if id == "clock-probe" {
"component:clock-probe/clock-probe-component@0.1.0"
} else { } else {
"component:echo/echo-component@0.1.0" "component:echo/echo-component@0.1.0"
}; };
@@ -787,6 +825,7 @@ fn component_artifact(name: &str) -> PathBuf {
for manifest in [ for manifest in [
"components/echo/Cargo.toml", "components/echo/Cargo.toml",
"components/counter/Cargo.toml", "components/counter/Cargo.toml",
"components/clock-probe/Cargo.toml",
] { ] {
let status = Command::new("rustup") let status = Command::new("rustup")
.args([ .args([