New crates
- platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
subject prefixing under provisioning.>, 6 stream definitions on startup
- platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL
Service integrations
- orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
- control-center: solo_auth_middleware (intentional bypass, --mode solo only),
NATS session events, WebSocket bridge via JetStream subscription (no polling)
- vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
SurrealDB storage backend with MVCC retry + exponential backoff
- secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
- extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
cache invalidation via provisioning.workspace.*.deploy.done
Clippy workspace clean
cargo clippy --workspace -- -D warnings: 0 errors
Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
(let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
105 lines
3.2 KiB
Rust
105 lines
3.2 KiB
Rust
use std::sync::Arc;
|
|
|
|
use futures::StreamExt;
|
|
use platform_nats::NatsBridge;
|
|
use serde::Serialize;
|
|
use tracing::{error, info, warn};
|
|
|
|
use crate::cache::ExtensionCache;
|
|
use crate::models::ExtensionType;
|
|
|
|
/// Publishes NATS events for extension lifecycle operations.
|
|
pub struct EventPublisher {
|
|
bridge: Arc<NatsBridge>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct ExtensionEvent<'a> {
|
|
name: &'a str,
|
|
version: &'a str,
|
|
#[serde(rename = "type")]
|
|
extension_type: ExtensionType,
|
|
}
|
|
|
|
impl EventPublisher {
|
|
pub fn new(bridge: Arc<NatsBridge>) -> Self {
|
|
Self { bridge }
|
|
}
|
|
|
|
/// Publish `provisioning.extensions.{type}.installed`.
|
|
///
|
|
/// Fire-and-forget: logs error on failure but never propagates to handler.
|
|
pub async fn publish_installed(
|
|
&self,
|
|
extension_type: ExtensionType,
|
|
name: &str,
|
|
version: &str,
|
|
) {
|
|
let subject = format!("extensions.{}.installed", extension_type);
|
|
let payload = ExtensionEvent {
|
|
name,
|
|
version,
|
|
extension_type,
|
|
};
|
|
match self.bridge.publish_json(&subject, &payload).await {
|
|
Ok(_) => {
|
|
info!(
|
|
subject = %subject,
|
|
extension = %name,
|
|
version = %version,
|
|
"Extension installed event published"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
error!(
|
|
subject = %subject,
|
|
extension = %name,
|
|
"Failed to publish extension event: {}", e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spawn a background task that subscribes to workspace deploy-done events
|
|
/// and invalidates the extension cache on each notification.
|
|
///
|
|
/// Subject: `provisioning.workspace.*.deploy.done` (filter on WORKSPACE stream)
|
|
pub fn spawn_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
|
|
tokio::spawn(async move {
|
|
run_cache_invalidator(bridge, cache).await;
|
|
});
|
|
}
|
|
|
|
async fn run_cache_invalidator(bridge: Arc<NatsBridge>, cache: Arc<ExtensionCache>) {
|
|
const STREAM: &str = "WORKSPACE";
|
|
const CONSUMER: &str = "ext-registry-cache-invalidator";
|
|
|
|
let mut messages = match bridge.subscribe_pull(STREAM, CONSUMER).await {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
warn!("Extension registry cache invalidator: subscribe failed — {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
info!("Extension registry cache invalidator running on stream {STREAM}");
|
|
|
|
while let Some(msg_result) = messages.next().await {
|
|
match msg_result {
|
|
Ok(msg) => {
|
|
// Subject pattern: provisioning.workspace.{ws_id}.deploy.done
|
|
// Filter is applied at JetStream level; any message here triggers invalidation.
|
|
let subject = msg.subject.as_str();
|
|
info!(subject = %subject, "Workspace deploy detected — invalidating extension cache");
|
|
cache.invalidate_all();
|
|
if let Err(e) = msg.ack().await {
|
|
warn!("Cache invalidator: ack failed — {e}");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Cache invalidator: message error — {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|