use std::collections::HashMap;
use std::time::Duration;
use flare_grpc_proto::capability::capability_service_client::CapabilityServiceClient;
use flare_grpc_proto::capability::{
DeregisterPluginEndpointRequest, RegisterPluginEndpointRequest,
};
use tonic::transport::Channel;
#[derive(Debug, thiserror::Error)]
pub enum HostError {
#[error("连接 capability 失败:{0}")]
Connect(String),
#[error("capability 拒绝了注册:{0}")]
Rejected(String),
#[error("gRPC 调用失败:{0}")]
Rpc(String),
#[error("声明不完整:{0}")]
Invalid(&'static str),
}
#[derive(Debug, Clone)]
pub struct PluginDeclaration {
pub tenant_id: String,
pub plugin_id: String,
pub capability_id: String,
pub grpc_authority: String,
pub plugin_version: String,
pub api_version: String,
pub manifest_sha256: String,
pub declared_operations: Vec<String>,
pub labels: HashMap<String, String>,
pub seat_model: SeatModel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeatModel {
Tenant,
PerUser,
}
impl SeatModel {
fn as_wire(self) -> &'static str {
match self {
Self::Tenant => "tenant",
Self::PerUser => "per_user",
}
}
}
impl PluginDeclaration {
pub fn validate(&self) -> Result<(), HostError> {
if self.plugin_id.trim().is_empty() {
return Err(HostError::Invalid("plugin_id 不能为空"));
}
if self.capability_id.trim().is_empty() {
return Err(HostError::Invalid("capability_id 不能为空"));
}
if self.grpc_authority.trim().is_empty() {
return Err(HostError::Invalid("grpc_authority 不能为空"));
}
if self.declared_operations.is_empty() {
return Err(HostError::Invalid(
"declared_operations 为空会让插件退化为 unverified,声明边界无法强制",
));
}
if !self.declared_operations.contains(&self.capability_id) {
return Err(HostError::Invalid(
"capability_id 必须出现在 declared_operations 里",
));
}
Ok(())
}
}
pub struct PluginHost {
client: CapabilityServiceClient<Channel>,
}
impl PluginHost {
pub async fn connect(endpoint: impl Into<String>) -> Result<Self, HostError> {
let endpoint = endpoint.into();
let channel = Channel::from_shared(endpoint.clone())
.map_err(|e| HostError::Connect(format!("{endpoint}: {e}")))?
.connect_timeout(Duration::from_secs(5))
.connect()
.await
.map_err(|e| HostError::Connect(format!("{endpoint}: {e}")))?;
Ok(Self {
client: CapabilityServiceClient::new(channel),
})
}
pub async fn announce(&mut self, declaration: &PluginDeclaration) -> Result<(), HostError> {
declaration.validate()?;
let response = self
.client
.register_plugin_endpoint(RegisterPluginEndpointRequest {
tenant_id: declaration.tenant_id.clone(),
plugin_id: declaration.plugin_id.clone(),
capability_id: declaration.capability_id.clone(),
grpc_authority: declaration.grpc_authority.clone(),
labels: declaration.labels.clone(),
request_id: String::new(),
plugin_version: declaration.plugin_version.clone(),
api_version: declaration.api_version.clone(),
manifest_sha256: declaration.manifest_sha256.clone(),
declared_operations: declaration.declared_operations.clone(),
seat_model: declaration.seat_model.as_wire().to_string(),
})
.await
.map_err(|e| HostError::Rpc(e.to_string()))?
.into_inner();
if !response.accepted {
return Err(HostError::Rejected(response.message));
}
tracing::info!(
plugin_id = %declaration.plugin_id,
capability_id = %declaration.capability_id,
operations = declaration.declared_operations.len(),
"plugin announced to capability"
);
Ok(())
}
pub async fn withdraw(&mut self, tenant_id: &str, plugin_id: &str) -> Result<(), HostError> {
self.client
.deregister_plugin_endpoint(DeregisterPluginEndpointRequest {
tenant_id: tenant_id.to_string(),
plugin_id: plugin_id.to_string(),
request_id: String::new(),
})
.await
.map_err(|e| HostError::Rpc(e.to_string()))?;
tracing::info!(plugin_id, "plugin withdrawn from capability");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn declaration() -> PluginDeclaration {
PluginDeclaration {
tenant_id: "0".into(),
plugin_id: "p1".into(),
capability_id: "vendorx.do".into(),
grpc_authority: "127.0.0.1:1".into(),
plugin_version: "1.0.0".into(),
api_version: "1".into(),
manifest_sha256: "abc".into(),
declared_operations: vec!["vendorx.do".into()],
labels: HashMap::new(),
seat_model: SeatModel::Tenant,
}
}
#[test]
fn complete_declaration_is_valid() {
declaration().validate().expect("完整声明应当通过");
}
#[test]
fn empty_declared_operations_is_rejected_locally() {
let mut d = declaration();
d.declared_operations.clear();
assert!(matches!(d.validate(), Err(HostError::Invalid(_))));
}
#[test]
fn capability_id_must_be_declared() {
let mut d = declaration();
d.declared_operations = vec!["vendorx.other".into()];
assert!(matches!(d.validate(), Err(HostError::Invalid(_))));
}
}