1use crate::domain::TargetDomain;
16use std::collections::HashMap;
17use std::sync::{Mutex, OnceLock};
18
19use rustfs_config::{
20 AMQP_PASSWORD, AMQP_TLS_CLIENT_CERT, AMQP_TLS_CLIENT_KEY, KAFKA_SASL_PASSWORD, KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY,
21 MQTT_PASSWORD, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MYSQL_DSN_STRING, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY,
22 NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, NATS_TOKEN, POSTGRES_DSN_STRING,
23 POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY, PULSAR_AUTH_TOKEN, PULSAR_PASSWORD, REDIS_PASSWORD, REDIS_TLS_CLIENT_CERT,
24 REDIS_TLS_CLIENT_KEY, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY,
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct TargetPluginManifest {
30 pub plugin_id: &'static str,
31 pub display_name: &'static str,
32 pub provider: &'static str,
33 pub version: &'static str,
34 pub target_type: &'static str,
35 pub supported_domains: &'static [TargetDomain],
36 pub secret_fields: &'static [&'static str],
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum TargetPluginPackaging {
42 Builtin,
43 External,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TargetPluginEntrypointKind {
49 Builtin,
50 Sidecar,
51 Wasm,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum TargetPluginRuntimeTransport {
58 InProcess,
59 Grpc,
60 WasmHost,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct TargetPluginExternalRuntimeContract {
66 pub protocol_version: &'static str,
67 pub transport: TargetPluginRuntimeTransport,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct TargetPluginArtifactManifest {
73 pub artifact_id: &'static str,
74 pub target_triple: &'static str,
75 pub download_uri: &'static str,
76 pub digest_sha256: &'static str,
77 pub signature_uri: &'static str,
78 pub provenance_uri: &'static str,
79 pub size_bytes: u64,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct TargetPluginDistributionManifest {
85 pub artifacts: &'static [TargetPluginArtifactManifest],
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct TargetPluginMarketplaceManifest {
92 pub plugin_id: &'static str,
93 pub display_name: &'static str,
94 pub provider: &'static str,
95 pub version: &'static str,
96 pub target_type: &'static str,
97 pub supported_domains: &'static [TargetDomain],
98 pub secret_fields: &'static [&'static str],
99 pub packaging: TargetPluginPackaging,
100 pub entrypoint_kind: TargetPluginEntrypointKind,
101 pub api_compatibility_version: &'static str,
102 pub runtime_contract: TargetPluginExternalRuntimeContract,
103 pub distribution: Option<TargetPluginDistributionManifest>,
104}
105
106pub const SUPPORTED_PLUGIN_API_COMPATIBILITY_VERSION: &str = "rustfs.target-plugin.v1";
110
111const BUILTIN_PLUGIN_API_COMPATIBILITY_VERSION: &str = SUPPORTED_PLUGIN_API_COMPATIBILITY_VERSION;
112const BUILTIN_PLUGIN_RUNTIME_PROTOCOL_VERSION: &str = "rustfs.target-runtime.v1";
113
114const SUPPORTED_BUILTIN_DOMAINS: &[TargetDomain] = &[TargetDomain::Audit, TargetDomain::Notify];
115const NO_SECRET_FIELDS: &[&str] = &[];
116
117const WEBHOOK_SECRET_FIELDS: &[&str] = &[WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY];
118const MQTT_SECRET_FIELDS: &[&str] = &[MQTT_PASSWORD, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY];
119const KAFKA_SECRET_FIELDS: &[&str] = &[KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY, KAFKA_SASL_PASSWORD];
120const AMQP_SECRET_FIELDS: &[&str] = &[AMQP_PASSWORD, AMQP_TLS_CLIENT_CERT, AMQP_TLS_CLIENT_KEY];
121const NATS_SECRET_FIELDS: &[&str] = &[
122 NATS_PASSWORD,
123 NATS_TOKEN,
124 NATS_CREDENTIALS_FILE,
125 NATS_TLS_CLIENT_CERT,
126 NATS_TLS_CLIENT_KEY,
127];
128const PULSAR_SECRET_FIELDS: &[&str] = &[PULSAR_AUTH_TOKEN, PULSAR_PASSWORD];
129const MYSQL_SECRET_FIELDS: &[&str] = &[MYSQL_DSN_STRING, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY];
130const REDIS_SECRET_FIELDS: &[&str] = &[REDIS_PASSWORD, REDIS_TLS_CLIENT_CERT, REDIS_TLS_CLIENT_KEY];
131const POSTGRES_SECRET_FIELDS: &[&str] = &[POSTGRES_DSN_STRING, POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY];
132
133#[inline]
134pub fn builtin_target_manifest(target_type: &'static str) -> TargetPluginManifest {
135 let (display_name, secret_fields) = match target_type {
136 "webhook" => ("Webhook", WEBHOOK_SECRET_FIELDS),
137 "mqtt" => ("MQTT", MQTT_SECRET_FIELDS),
138 "kafka" => ("Kafka", KAFKA_SECRET_FIELDS),
139 "amqp" => ("AMQP", AMQP_SECRET_FIELDS),
140 "nats" => ("NATS", NATS_SECRET_FIELDS),
141 "pulsar" => ("Pulsar", PULSAR_SECRET_FIELDS),
142 "mysql" => ("MySQL", MYSQL_SECRET_FIELDS),
143 "redis" => ("Redis", REDIS_SECRET_FIELDS),
144 "postgres" => ("Postgres", POSTGRES_SECRET_FIELDS),
145 _ => ("Custom Target", NO_SECRET_FIELDS),
146 };
147
148 TargetPluginManifest {
149 plugin_id: builtin_plugin_id(target_type),
150 display_name,
151 provider: "rustfs",
152 version: env!("CARGO_PKG_VERSION"),
153 target_type,
154 supported_domains: SUPPORTED_BUILTIN_DOMAINS,
155 secret_fields,
156 }
157}
158
159#[inline]
160pub fn builtin_target_marketplace_manifest(target_type: &'static str) -> TargetPluginMarketplaceManifest {
161 TargetPluginMarketplaceManifest::from(builtin_target_manifest(target_type))
162}
163
164impl From<TargetPluginManifest> for TargetPluginMarketplaceManifest {
165 fn from(value: TargetPluginManifest) -> Self {
166 Self {
167 plugin_id: value.plugin_id,
168 display_name: value.display_name,
169 provider: value.provider,
170 version: value.version,
171 target_type: value.target_type,
172 supported_domains: value.supported_domains,
173 secret_fields: value.secret_fields,
174 packaging: TargetPluginPackaging::Builtin,
175 entrypoint_kind: TargetPluginEntrypointKind::Builtin,
176 api_compatibility_version: BUILTIN_PLUGIN_API_COMPATIBILITY_VERSION,
177 runtime_contract: TargetPluginExternalRuntimeContract {
178 protocol_version: BUILTIN_PLUGIN_RUNTIME_PROTOCOL_VERSION,
179 transport: TargetPluginRuntimeTransport::InProcess,
180 },
181 distribution: None,
182 }
183 }
184}
185
186#[inline]
187pub fn installable_target_marketplace_manifest(
188 base: TargetPluginManifest,
189 entrypoint_kind: TargetPluginEntrypointKind,
190 runtime_contract: TargetPluginExternalRuntimeContract,
191 distribution: TargetPluginDistributionManifest,
192) -> TargetPluginMarketplaceManifest {
193 TargetPluginMarketplaceManifest {
194 plugin_id: base.plugin_id,
195 display_name: base.display_name,
196 provider: base.provider,
197 version: base.version,
198 target_type: base.target_type,
199 supported_domains: base.supported_domains,
200 secret_fields: base.secret_fields,
201 packaging: TargetPluginPackaging::External,
202 entrypoint_kind,
203 api_compatibility_version: BUILTIN_PLUGIN_API_COMPATIBILITY_VERSION,
204 runtime_contract,
205 distribution: Some(distribution),
206 }
207}
208
209#[inline]
210fn builtin_plugin_id(target_type: &'static str) -> &'static str {
211 match target_type {
212 "webhook" => "builtin:webhook",
213 "mqtt" => "builtin:mqtt",
214 "kafka" => "builtin:kafka",
215 "amqp" => "builtin:amqp",
216 "nats" => "builtin:nats",
217 "pulsar" => "builtin:pulsar",
218 "mysql" => "builtin:mysql",
219 "redis" => "builtin:redis",
220 "postgres" => "builtin:postgres",
221 _ => custom_plugin_id(target_type),
222 }
223}
224
225fn custom_plugin_id(target_type: &'static str) -> &'static str {
230 static CUSTOM_PLUGIN_IDS: OnceLock<Mutex<HashMap<&'static str, &'static str>>> = OnceLock::new();
231 let ids = CUSTOM_PLUGIN_IDS.get_or_init(|| Mutex::new(HashMap::new()));
232 let mut ids = ids.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
233 ids.entry(target_type)
234 .or_insert_with(|| Box::leak(format!("custom:{target_type}").into_boxed_str()))
235}
236
237#[cfg(test)]
238mod tests {
239 use super::{
240 TargetPluginArtifactManifest, TargetPluginDistributionManifest, TargetPluginEntrypointKind,
241 TargetPluginExternalRuntimeContract, TargetPluginMarketplaceManifest, TargetPluginPackaging,
242 TargetPluginRuntimeTransport, builtin_target_manifest, builtin_target_marketplace_manifest,
243 installable_target_marketplace_manifest,
244 };
245 use crate::domain::TargetDomain;
246 use rustfs_config::{KAFKA_SASL_PASSWORD, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY};
247
248 #[test]
249 fn builtin_webhook_manifest_marks_secret_fields() {
250 let manifest = builtin_target_manifest("webhook");
251
252 assert_eq!(manifest.plugin_id, "builtin:webhook");
253 assert_eq!(manifest.display_name, "Webhook");
254 assert!(manifest.secret_fields.contains(&WEBHOOK_AUTH_TOKEN));
255 assert!(manifest.secret_fields.contains(&WEBHOOK_CLIENT_CERT));
256 assert!(manifest.secret_fields.contains(&WEBHOOK_CLIENT_KEY));
257 }
258
259 #[test]
260 fn builtin_manifest_derives_marketplace_boundary_metadata() {
261 let manifest = builtin_target_marketplace_manifest("webhook");
262
263 assert_eq!(manifest.plugin_id, "builtin:webhook");
264 assert_eq!(manifest.display_name, "Webhook");
265 assert_eq!(manifest.target_type, "webhook");
266 assert_eq!(manifest.packaging, TargetPluginPackaging::Builtin);
267 assert_eq!(manifest.entrypoint_kind, TargetPluginEntrypointKind::Builtin);
268 assert_eq!(manifest.api_compatibility_version, "rustfs.target-plugin.v1");
269 assert_eq!(
270 manifest.runtime_contract,
271 TargetPluginExternalRuntimeContract {
272 protocol_version: "rustfs.target-runtime.v1",
273 transport: TargetPluginRuntimeTransport::InProcess,
274 }
275 );
276 assert_eq!(manifest.distribution, None);
277 }
278
279 #[test]
280 fn marketplace_manifest_preserves_supported_domains() {
281 let manifest = builtin_target_marketplace_manifest("kafka");
282
283 assert_eq!(manifest.supported_domains, &[TargetDomain::Audit, TargetDomain::Notify]);
284 }
285
286 #[test]
287 fn custom_target_types_get_unique_stable_plugin_ids() {
288 let first = builtin_target_manifest("custom-a");
289 let second = builtin_target_manifest("custom-b");
290
291 assert_eq!(first.plugin_id, "custom:custom-a");
292 assert_eq!(second.plugin_id, "custom:custom-b");
293 assert_ne!(first.plugin_id, second.plugin_id);
294 assert!(std::ptr::eq(first.plugin_id, builtin_target_manifest("custom-a").plugin_id));
296 }
297
298 #[test]
299 fn builtin_kafka_manifest_marks_sasl_password_secret() {
300 let manifest = builtin_target_manifest("kafka");
301
302 assert!(manifest.secret_fields.contains(&KAFKA_SASL_PASSWORD));
303 }
304
305 #[test]
306 fn marketplace_manifest_from_builtin_manifest_is_stable() {
307 let base = builtin_target_manifest("redis");
308 let derived = TargetPluginMarketplaceManifest::from(base);
309
310 assert_eq!(derived.plugin_id, "builtin:redis");
311 assert_eq!(derived.target_type, "redis");
312 assert_eq!(derived.packaging, TargetPluginPackaging::Builtin);
313 assert_eq!(derived.entrypoint_kind, TargetPluginEntrypointKind::Builtin);
314 assert_eq!(derived.runtime_contract.transport, TargetPluginRuntimeTransport::InProcess);
315 assert_eq!(derived.distribution, None);
316 }
317
318 #[test]
319 fn installable_manifest_expresses_external_boundary_declaratively() {
320 let base = builtin_target_manifest("webhook");
321 let manifest = installable_target_marketplace_manifest(
322 base,
323 TargetPluginEntrypointKind::Sidecar,
324 TargetPluginExternalRuntimeContract {
325 protocol_version: "rustfs.target-runtime.v1",
326 transport: TargetPluginRuntimeTransport::Grpc,
327 },
328 TargetPluginDistributionManifest {
329 artifacts: &[TargetPluginArtifactManifest {
330 artifact_id: "sidecar-linux-amd64",
331 target_triple: "x86_64-unknown-linux-gnu",
332 download_uri: "https://plugins.example.test/webhook-plugin.tar.zst",
333 digest_sha256: "0123456789abcdef",
334 signature_uri: "https://plugins.example.test/webhook-plugin.tar.zst.sig",
335 provenance_uri: "https://plugins.example.test/webhook-plugin.tar.zst.intoto.jsonl",
336 size_bytes: 4096,
337 }],
338 },
339 );
340
341 assert_eq!(manifest.packaging, TargetPluginPackaging::External);
342 assert_eq!(manifest.entrypoint_kind, TargetPluginEntrypointKind::Sidecar);
343 assert_eq!(manifest.runtime_contract.transport, TargetPluginRuntimeTransport::Grpc);
344 assert_eq!(
345 manifest.distribution,
346 Some(TargetPluginDistributionManifest {
347 artifacts: &[TargetPluginArtifactManifest {
348 artifact_id: "sidecar-linux-amd64",
349 target_triple: "x86_64-unknown-linux-gnu",
350 download_uri: "https://plugins.example.test/webhook-plugin.tar.zst",
351 digest_sha256: "0123456789abcdef",
352 signature_uri: "https://plugins.example.test/webhook-plugin.tar.zst.sig",
353 provenance_uri: "https://plugins.example.test/webhook-plugin.tar.zst.intoto.jsonl",
354 size_bytes: 4096,
355 }],
356 })
357 );
358 }
359}