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:
@@ -39,7 +39,8 @@ use wasmeld_package::{
|
||||
wit_package::{WitPackageError, WitPackageMetadata},
|
||||
};
|
||||
use wasmeld_runtime::{
|
||||
ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey, ServiceManifest,
|
||||
CapabilityDescriptor, ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceKey,
|
||||
ServiceManifest,
|
||||
};
|
||||
|
||||
use persistence::{Persistence, StoredDeployment, StoredEvent, StoredService};
|
||||
@@ -113,6 +114,7 @@ struct ConsoleState {
|
||||
#[derive(Clone)]
|
||||
struct ServiceRecord {
|
||||
manifest: ServiceManifest,
|
||||
capabilities: Vec<CapabilityView>,
|
||||
status: ServiceStatus,
|
||||
updated_at_ms: u64,
|
||||
calls: u64,
|
||||
@@ -144,6 +146,26 @@ pub struct DeploymentView {
|
||||
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.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ServiceView {
|
||||
@@ -151,6 +173,7 @@ pub struct ServiceView {
|
||||
pub revision: String,
|
||||
pub artifact: String,
|
||||
pub world: String,
|
||||
pub capabilities: Vec<CapabilityView>,
|
||||
pub status: ServiceStatus,
|
||||
pub updated_at_ms: u64,
|
||||
pub limits: ResourceLimits,
|
||||
@@ -166,6 +189,7 @@ impl ServiceRecord {
|
||||
revision: self.manifest.revision.clone(),
|
||||
artifact: self.manifest.component.clone(),
|
||||
world: self.manifest.world.clone(),
|
||||
capabilities: self.capabilities.clone(),
|
||||
status: self.status,
|
||||
updated_at_ms: self.updated_at_ms,
|
||||
limits: self.manifest.limits.clone(),
|
||||
@@ -743,8 +767,9 @@ impl Console {
|
||||
let _ = fs::remove_file(&manifest_path);
|
||||
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(
|
||||
EventKind::Registered,
|
||||
Some(&key),
|
||||
@@ -910,10 +935,12 @@ impl Console {
|
||||
)));
|
||||
}
|
||||
runtime.register_from_file(manifest.clone())?;
|
||||
let capabilities = runtime_capabilities(runtime, &key)?;
|
||||
self.state()?.services.insert(
|
||||
key,
|
||||
ServiceRecord {
|
||||
manifest,
|
||||
capabilities,
|
||||
status: ServiceStatus::Stopped,
|
||||
updated_at_ms: stored.updated_at_ms,
|
||||
calls: stored.calls,
|
||||
@@ -956,7 +983,8 @@ impl Console {
|
||||
continue;
|
||||
}
|
||||
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(())
|
||||
@@ -1070,12 +1098,14 @@ impl Console {
|
||||
fn insert_service(
|
||||
&self,
|
||||
manifest: ServiceManifest,
|
||||
capabilities: Vec<CapabilityView>,
|
||||
status: ServiceStatus,
|
||||
) -> Result<ServiceView, ConsoleError> {
|
||||
let key = manifest.key()?;
|
||||
let mut state = self.state()?;
|
||||
let record = ServiceRecord {
|
||||
manifest,
|
||||
capabilities,
|
||||
status,
|
||||
updated_at_ms: unix_time_ms(),
|
||||
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(
|
||||
state: &ConsoleState,
|
||||
service_id: &str,
|
||||
|
||||
@@ -40,6 +40,7 @@ async fn manages_a_resident_component_over_http() {
|
||||
assert_eq!(status, StatusCode::CREATED, "{registered}");
|
||||
assert_eq!(registered["id"], "echo");
|
||||
assert_eq!(registered["status"], "stopped");
|
||||
assert_eq!(registered["capabilities"], json!([]));
|
||||
|
||||
let response = application
|
||||
.clone()
|
||||
@@ -89,6 +90,41 @@ async fn manages_a_resident_component_over_http() {
|
||||
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]
|
||||
async fn routes_raw_gateway_calls_through_the_active_deployment() {
|
||||
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 world = if id == "counter" {
|
||||
"component:counter/counter-component@0.1.0"
|
||||
} else if id == "clock-probe" {
|
||||
"component:clock-probe/clock-probe-component@0.1.0"
|
||||
} else {
|
||||
"component:echo/echo-component@0.1.0"
|
||||
};
|
||||
@@ -787,6 +825,7 @@ fn component_artifact(name: &str) -> PathBuf {
|
||||
for manifest in [
|
||||
"components/echo/Cargo.toml",
|
||||
"components/counter/Cargo.toml",
|
||||
"components/clock-probe/Cargo.toml",
|
||||
] {
|
||||
let status = Command::new("rustup")
|
||||
.args([
|
||||
|
||||
Reference in New Issue
Block a user