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},
|
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,
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user