// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::domain::TargetDomain; use std::collections::HashMap; use std::sync::{Mutex, OnceLock}; use rustfs_config::{ AMQP_PASSWORD, AMQP_TLS_CLIENT_CERT, AMQP_TLS_CLIENT_KEY, KAFKA_SASL_PASSWORD, KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY, MQTT_PASSWORD, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MYSQL_DSN_STRING, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY, NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, NATS_TOKEN, POSTGRES_DSN_STRING, POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY, PULSAR_AUTH_TOKEN, PULSAR_PASSWORD, REDIS_PASSWORD, REDIS_TLS_CLIENT_CERT, REDIS_TLS_CLIENT_KEY, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, }; /// Shared plugin manifest metadata for a target implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TargetPluginManifest { pub plugin_id: &'static str, pub display_name: &'static str, pub provider: &'static str, pub version: &'static str, pub target_type: &'static str, pub supported_domains: &'static [TargetDomain], pub secret_fields: &'static [&'static str], } /// Declares how a plugin is packaged relative to the RustFS process boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TargetPluginPackaging { Builtin, External, } /// Declares what kind of entrypoint a plugin would use when instantiated. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TargetPluginEntrypointKind { Builtin, Sidecar, Wasm, } /// Declares the transport boundary RustFS would use to communicate with a /// plugin runtime without committing to any concrete loader implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TargetPluginRuntimeTransport { InProcess, Grpc, WasmHost, } /// Declarative external runtime contract for future installable plugins. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TargetPluginExternalRuntimeContract { pub protocol_version: &'static str, pub transport: TargetPluginRuntimeTransport, } /// Declarative distribution metadata for an installable target plugin. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TargetPluginArtifactManifest { pub artifact_id: &'static str, pub target_triple: &'static str, pub download_uri: &'static str, pub digest_sha256: &'static str, pub signature_uri: &'static str, pub provenance_uri: &'static str, pub size_bytes: u64, } /// Declarative distribution metadata for an installable target plugin. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TargetPluginDistributionManifest { pub artifacts: &'static [TargetPluginArtifactManifest], } /// Marketplace-oriented manifest metadata that is explicit about future /// installable plugin boundaries without introducing any loading behavior. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TargetPluginMarketplaceManifest { pub plugin_id: &'static str, pub display_name: &'static str, pub provider: &'static str, pub version: &'static str, pub target_type: &'static str, pub supported_domains: &'static [TargetDomain], pub secret_fields: &'static [&'static str], pub packaging: TargetPluginPackaging, pub entrypoint_kind: TargetPluginEntrypointKind, pub api_compatibility_version: &'static str, pub runtime_contract: TargetPluginExternalRuntimeContract, pub distribution: Option, } /// The plugin API compatibility version this build understands. Installable /// external plugins must declare exactly this value in their marketplace /// manifest to be installable; anything else is rejected during planning. pub const SUPPORTED_PLUGIN_API_COMPATIBILITY_VERSION: &str = "rustfs.target-plugin.v1"; const BUILTIN_PLUGIN_API_COMPATIBILITY_VERSION: &str = SUPPORTED_PLUGIN_API_COMPATIBILITY_VERSION; const BUILTIN_PLUGIN_RUNTIME_PROTOCOL_VERSION: &str = "rustfs.target-runtime.v1"; const SUPPORTED_BUILTIN_DOMAINS: &[TargetDomain] = &[TargetDomain::Audit, TargetDomain::Notify]; const NO_SECRET_FIELDS: &[&str] = &[]; const WEBHOOK_SECRET_FIELDS: &[&str] = &[WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY]; const MQTT_SECRET_FIELDS: &[&str] = &[MQTT_PASSWORD, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY]; const KAFKA_SECRET_FIELDS: &[&str] = &[KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY, KAFKA_SASL_PASSWORD]; const AMQP_SECRET_FIELDS: &[&str] = &[AMQP_PASSWORD, AMQP_TLS_CLIENT_CERT, AMQP_TLS_CLIENT_KEY]; const NATS_SECRET_FIELDS: &[&str] = &[ NATS_PASSWORD, NATS_TOKEN, NATS_CREDENTIALS_FILE, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, ]; const PULSAR_SECRET_FIELDS: &[&str] = &[PULSAR_AUTH_TOKEN, PULSAR_PASSWORD]; const MYSQL_SECRET_FIELDS: &[&str] = &[MYSQL_DSN_STRING, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY]; const REDIS_SECRET_FIELDS: &[&str] = &[REDIS_PASSWORD, REDIS_TLS_CLIENT_CERT, REDIS_TLS_CLIENT_KEY]; const POSTGRES_SECRET_FIELDS: &[&str] = &[POSTGRES_DSN_STRING, POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY]; #[inline] pub fn builtin_target_manifest(target_type: &'static str) -> TargetPluginManifest { let (display_name, secret_fields) = match target_type { "webhook" => ("Webhook", WEBHOOK_SECRET_FIELDS), "mqtt" => ("MQTT", MQTT_SECRET_FIELDS), "kafka" => ("Kafka", KAFKA_SECRET_FIELDS), "amqp" => ("AMQP", AMQP_SECRET_FIELDS), "nats" => ("NATS", NATS_SECRET_FIELDS), "pulsar" => ("Pulsar", PULSAR_SECRET_FIELDS), "mysql" => ("MySQL", MYSQL_SECRET_FIELDS), "redis" => ("Redis", REDIS_SECRET_FIELDS), "postgres" => ("Postgres", POSTGRES_SECRET_FIELDS), _ => ("Custom Target", NO_SECRET_FIELDS), }; TargetPluginManifest { plugin_id: builtin_plugin_id(target_type), display_name, provider: "rustfs", version: env!("CARGO_PKG_VERSION"), target_type, supported_domains: SUPPORTED_BUILTIN_DOMAINS, secret_fields, } } #[inline] pub fn builtin_target_marketplace_manifest(target_type: &'static str) -> TargetPluginMarketplaceManifest { TargetPluginMarketplaceManifest::from(builtin_target_manifest(target_type)) } impl From for TargetPluginMarketplaceManifest { fn from(value: TargetPluginManifest) -> Self { Self { plugin_id: value.plugin_id, display_name: value.display_name, provider: value.provider, version: value.version, target_type: value.target_type, supported_domains: value.supported_domains, secret_fields: value.secret_fields, packaging: TargetPluginPackaging::Builtin, entrypoint_kind: TargetPluginEntrypointKind::Builtin, api_compatibility_version: BUILTIN_PLUGIN_API_COMPATIBILITY_VERSION, runtime_contract: TargetPluginExternalRuntimeContract { protocol_version: BUILTIN_PLUGIN_RUNTIME_PROTOCOL_VERSION, transport: TargetPluginRuntimeTransport::InProcess, }, distribution: None, } } } #[inline] pub fn installable_target_marketplace_manifest( base: TargetPluginManifest, entrypoint_kind: TargetPluginEntrypointKind, runtime_contract: TargetPluginExternalRuntimeContract, distribution: TargetPluginDistributionManifest, ) -> TargetPluginMarketplaceManifest { TargetPluginMarketplaceManifest { plugin_id: base.plugin_id, display_name: base.display_name, provider: base.provider, version: base.version, target_type: base.target_type, supported_domains: base.supported_domains, secret_fields: base.secret_fields, packaging: TargetPluginPackaging::External, entrypoint_kind, api_compatibility_version: BUILTIN_PLUGIN_API_COMPATIBILITY_VERSION, runtime_contract, distribution: Some(distribution), } } #[inline] fn builtin_plugin_id(target_type: &'static str) -> &'static str { match target_type { "webhook" => "builtin:webhook", "mqtt" => "builtin:mqtt", "kafka" => "builtin:kafka", "amqp" => "builtin:amqp", "nats" => "builtin:nats", "pulsar" => "builtin:pulsar", "mysql" => "builtin:mysql", "redis" => "builtin:redis", "postgres" => "builtin:postgres", _ => custom_plugin_id(target_type), } } /// Interns a unique `custom:` plugin id per non-builtin target type. /// A shared fallback id would make distinct custom plugins collide in every /// identity-keyed surface (registry keys, canonical instance ids, admin catalog). /// The leak is bounded by the number of distinct registered target types. fn custom_plugin_id(target_type: &'static str) -> &'static str { static CUSTOM_PLUGIN_IDS: OnceLock>> = OnceLock::new(); let ids = CUSTOM_PLUGIN_IDS.get_or_init(|| Mutex::new(HashMap::new())); let mut ids = ids.lock().unwrap_or_else(std::sync::PoisonError::into_inner); ids.entry(target_type) .or_insert_with(|| Box::leak(format!("custom:{target_type}").into_boxed_str())) } #[cfg(test)] mod tests { use super::{ TargetPluginArtifactManifest, TargetPluginDistributionManifest, TargetPluginEntrypointKind, TargetPluginExternalRuntimeContract, TargetPluginMarketplaceManifest, TargetPluginPackaging, TargetPluginRuntimeTransport, builtin_target_manifest, builtin_target_marketplace_manifest, installable_target_marketplace_manifest, }; use crate::domain::TargetDomain; use rustfs_config::{KAFKA_SASL_PASSWORD, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY}; #[test] fn builtin_webhook_manifest_marks_secret_fields() { let manifest = builtin_target_manifest("webhook"); assert_eq!(manifest.plugin_id, "builtin:webhook"); assert_eq!(manifest.display_name, "Webhook"); assert!(manifest.secret_fields.contains(&WEBHOOK_AUTH_TOKEN)); assert!(manifest.secret_fields.contains(&WEBHOOK_CLIENT_CERT)); assert!(manifest.secret_fields.contains(&WEBHOOK_CLIENT_KEY)); } #[test] fn builtin_manifest_derives_marketplace_boundary_metadata() { let manifest = builtin_target_marketplace_manifest("webhook"); assert_eq!(manifest.plugin_id, "builtin:webhook"); assert_eq!(manifest.display_name, "Webhook"); assert_eq!(manifest.target_type, "webhook"); assert_eq!(manifest.packaging, TargetPluginPackaging::Builtin); assert_eq!(manifest.entrypoint_kind, TargetPluginEntrypointKind::Builtin); assert_eq!(manifest.api_compatibility_version, "rustfs.target-plugin.v1"); assert_eq!( manifest.runtime_contract, TargetPluginExternalRuntimeContract { protocol_version: "rustfs.target-runtime.v1", transport: TargetPluginRuntimeTransport::InProcess, } ); assert_eq!(manifest.distribution, None); } #[test] fn marketplace_manifest_preserves_supported_domains() { let manifest = builtin_target_marketplace_manifest("kafka"); assert_eq!(manifest.supported_domains, &[TargetDomain::Audit, TargetDomain::Notify]); } #[test] fn custom_target_types_get_unique_stable_plugin_ids() { let first = builtin_target_manifest("custom-a"); let second = builtin_target_manifest("custom-b"); assert_eq!(first.plugin_id, "custom:custom-a"); assert_eq!(second.plugin_id, "custom:custom-b"); assert_ne!(first.plugin_id, second.plugin_id); // Interned: repeated lookups return the same &'static str. assert!(std::ptr::eq(first.plugin_id, builtin_target_manifest("custom-a").plugin_id)); } #[test] fn builtin_kafka_manifest_marks_sasl_password_secret() { let manifest = builtin_target_manifest("kafka"); assert!(manifest.secret_fields.contains(&KAFKA_SASL_PASSWORD)); } #[test] fn marketplace_manifest_from_builtin_manifest_is_stable() { let base = builtin_target_manifest("redis"); let derived = TargetPluginMarketplaceManifest::from(base); assert_eq!(derived.plugin_id, "builtin:redis"); assert_eq!(derived.target_type, "redis"); assert_eq!(derived.packaging, TargetPluginPackaging::Builtin); assert_eq!(derived.entrypoint_kind, TargetPluginEntrypointKind::Builtin); assert_eq!(derived.runtime_contract.transport, TargetPluginRuntimeTransport::InProcess); assert_eq!(derived.distribution, None); } #[test] fn installable_manifest_expresses_external_boundary_declaratively() { let base = builtin_target_manifest("webhook"); let manifest = installable_target_marketplace_manifest( base, TargetPluginEntrypointKind::Sidecar, TargetPluginExternalRuntimeContract { protocol_version: "rustfs.target-runtime.v1", transport: TargetPluginRuntimeTransport::Grpc, }, TargetPluginDistributionManifest { artifacts: &[TargetPluginArtifactManifest { artifact_id: "sidecar-linux-amd64", target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-plugin.tar.zst", digest_sha256: "0123456789abcdef", signature_uri: "https://plugins.example.test/webhook-plugin.tar.zst.sig", provenance_uri: "https://plugins.example.test/webhook-plugin.tar.zst.intoto.jsonl", size_bytes: 4096, }], }, ); assert_eq!(manifest.packaging, TargetPluginPackaging::External); assert_eq!(manifest.entrypoint_kind, TargetPluginEntrypointKind::Sidecar); assert_eq!(manifest.runtime_contract.transport, TargetPluginRuntimeTransport::Grpc); assert_eq!( manifest.distribution, Some(TargetPluginDistributionManifest { artifacts: &[TargetPluginArtifactManifest { artifact_id: "sidecar-linux-amd64", target_triple: "x86_64-unknown-linux-gnu", download_uri: "https://plugins.example.test/webhook-plugin.tar.zst", digest_sha256: "0123456789abcdef", signature_uri: "https://plugins.example.test/webhook-plugin.tar.zst.sig", provenance_uri: "https://plugins.example.test/webhook-plugin.tar.zst.intoto.jsonl", size_bytes: 4096, }], }) ); } }