Skip to main content

rustfs_targets/
manifest.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Shared plugin manifest metadata for a target implementation.
28#[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/// Declares how a plugin is packaged relative to the RustFS process boundary.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum TargetPluginPackaging {
42    Builtin,
43    External,
44}
45
46/// Declares what kind of entrypoint a plugin would use when instantiated.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TargetPluginEntrypointKind {
49    Builtin,
50    Sidecar,
51    Wasm,
52}
53
54/// Declares the transport boundary RustFS would use to communicate with a
55/// plugin runtime without committing to any concrete loader implementation.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum TargetPluginRuntimeTransport {
58    InProcess,
59    Grpc,
60    WasmHost,
61}
62
63/// Declarative external runtime contract for future installable plugins.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct TargetPluginExternalRuntimeContract {
66    pub protocol_version: &'static str,
67    pub transport: TargetPluginRuntimeTransport,
68}
69
70/// Declarative distribution metadata for an installable target plugin.
71#[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/// Declarative distribution metadata for an installable target plugin.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct TargetPluginDistributionManifest {
85    pub artifacts: &'static [TargetPluginArtifactManifest],
86}
87
88/// Marketplace-oriented manifest metadata that is explicit about future
89/// installable plugin boundaries without introducing any loading behavior.
90#[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
106/// The plugin API compatibility version this build understands. Installable
107/// external plugins must declare exactly this value in their marketplace
108/// manifest to be installable; anything else is rejected during planning.
109pub 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
225/// Interns a unique `custom:<target_type>` plugin id per non-builtin target type.
226/// A shared fallback id would make distinct custom plugins collide in every
227/// identity-keyed surface (registry keys, canonical instance ids, admin catalog).
228/// The leak is bounded by the number of distinct registered target types.
229fn 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        // Interned: repeated lookups return the same &'static str.
295        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}