Skip to main content

common_options/
plugin_options.rs

1// Copyright 2023 Greptime 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 std::sync::{Arc, Mutex};
16
17use serde::de::DeserializeOwned;
18use serde_json::Value;
19
20/// A trait for serializing Metasrv config to a JSON string.
21/// So it can be used in the metasrv's crate instead of depending on the plugins' crate.
22pub trait PluginOptionsSerializer: Send + Sync {
23    fn serialize(&self) -> Result<String, serde_json::Error>;
24}
25pub type PluginOptionsSerializerRef = Arc<dyn PluginOptionsSerializer>;
26
27/// A trait for deserializing Metasrv config from a JSON string.
28pub trait PluginOptionsDeserializer<T: DeserializeOwned>: Send + Sync {
29    fn deserialize(&self, payload: &str) -> Result<T, serde_json::Error>;
30}
31
32/// A flag for stating the standalone mode in the plugins.
33///
34/// The standalone build and start process calls `setup_frontend_plugins_pre_build` and `setup_datanode_plugins_pre_build`,
35/// so we add a flag to the plugins to indicate that the plugins are running in the standalone mode.
36#[derive(Clone, Copy, Debug)]
37pub struct StandaloneFlag;
38
39/// Buffer of plugin option tags that were dropped during config loading because
40/// they are not recognized by the current build.
41///
42/// Config loading typically happens *before* the global tracing subscriber is
43/// installed, so a `warn!` emitted at that point is silently lost. Instead we
44/// buffer the dropped tags here and let the server flush them (via
45/// [`take_dropped_plugin_warnings`]) once logging is initialized, so the warning
46/// actually reaches the configured log output.
47static DROPPED_PLUGIN_TAGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
48
49/// Records a plugin option tag that was dropped because it is not recognized by
50/// this build (for example, an enterprise plugin option seen by an open-source
51/// build).
52pub fn record_dropped_plugin(tag: &str) {
53    if let Ok(mut tags) = DROPPED_PLUGIN_TAGS.lock() {
54        tags.push(tag.to_string());
55    }
56}
57
58/// Takes (and clears) the buffered dropped-plugin tags so they can be logged
59/// after the global logging is up.
60pub fn take_dropped_plugin_warnings() -> Vec<String> {
61    DROPPED_PLUGIN_TAGS
62        .lock()
63        .map(|mut tags| std::mem::take(&mut *tags))
64        .unwrap_or_default()
65}
66
67/// Deserializes a list of plugin options leniently.
68///
69/// Each entry is normally a single-key `{"tag": payload}` object, but a
70/// *multi-key* object is also accepted and split: every top-level key is
71/// treated as an independent plugin option. Keys naming a *known* variant are
72/// deserialized and kept; keys naming a variant this build doesn't recognize are
73/// dropped (and buffered via [`record_dropped_plugin`] for deferred logging).
74/// This lets a single config file be shared across builds that compile different
75/// sets of plugins — including a table that mixes a known plugin with unknown
76/// ones — without aborting startup.
77///
78/// A *known* variant whose payload is genuinely malformed still propagates its
79/// error, so a misconfigured plugin is never silently disabled; only genuinely
80/// unknown options are ignored. For privacy only the unrecognized variant *tag*
81/// is recorded, never the raw payload (which may contain secrets).
82///
83/// This is generic over `T` so the open-source `plugins` crate and the
84/// enterprise `ent-plugins` crate share a single implementation without either
85/// having to enumerate its variants (see [`is_unknown_variant_tag`]).
86pub fn filter_known_plugin_options<T: DeserializeOwned>(
87    values: Vec<Value>,
88) -> Result<Vec<T>, serde_json::Error> {
89    let mut out = Vec::with_capacity(values.len());
90    for value in values {
91        collect_from_entry::<T>(&value, &mut out)?;
92    }
93    Ok(out)
94}
95
96/// Extracts the recognized plugin options from one config entry.
97///
98/// A normal entry is a single-key `{"tag": payload}` object. We additionally
99/// accept a *multi-key* object and treat every top-level key as an independent
100/// plugin option: each key naming a *known* variant is deserialized (and a
101/// malformed payload for a known variant still errors, so a misconfigured plugin
102/// is never silently disabled), while keys naming unknown variants are dropped
103/// with a warning. A non-object entry (e.g. a bare number) is deserialized
104/// directly and surfaces any error.
105fn collect_from_entry<T: DeserializeOwned>(
106    value: &Value,
107    out: &mut Vec<T>,
108) -> Result<(), serde_json::Error> {
109    let Some(map) = value.as_object() else {
110        // Not a tagged object (e.g. a bare number); a bare string can still
111        // deserialize a unit variant. Surface any error.
112        out.push(T::deserialize(value)?);
113        return Ok(());
114    };
115    for (tag, payload) in map {
116        if is_unknown_variant_tag::<T>(tag) {
117            record_dropped_plugin(tag);
118            continue;
119        }
120        // Known variant: deserialize just `{"tag": payload}` so a malformed
121        // payload is reported against this variant rather than swallowed.
122        let mut single = serde_json::Map::new();
123        single.insert(tag.clone(), payload.clone());
124        out.push(T::deserialize(&Value::Object(single))?);
125    }
126    Ok(())
127}
128
129/// Decides whether `tag` names a variant unknown to `T`, *without* `T` having
130/// to enumerate its variants.
131///
132/// Two probe payloads (`{"tag": null}` and `{"tag": true}`) are re-deserialized
133/// for the same tag:
134/// - If `T` accepts either payload, the tag is recognized (a known variant).
135/// - If both are rejected, the tag is unknown iff the two errors are identical.
136///   An unknown variant yields a payload-independent "unknown variant" error for
137///   both probes, whereas a known variant rejects `null` and `bool` with
138///   *different* "invalid type" messages.
139fn is_unknown_variant_tag<T: DeserializeOwned>(tag: &str) -> bool {
140    let probe = |payload: Value| {
141        let mut map = serde_json::Map::new();
142        map.insert(tag.to_string(), payload);
143        Value::Object(map)
144    };
145    match (
146        T::deserialize(&probe(Value::Null)),
147        T::deserialize(&probe(Value::Bool(true))),
148    ) {
149        (Ok(_), _) | (_, Ok(_)) => false,
150        (Err(a), Err(b)) => a.to_string() == b.to_string(),
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use std::sync::{Mutex, OnceLock};
157
158    use serde::Deserialize;
159
160    use super::*;
161
162    // The dropped-tag buffer is process-global, so every test in this module
163    // serializes through this lock to keep buffer assertions deterministic.
164    fn lock() -> std::sync::MutexGuard<'static, ()> {
165        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
166        LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
167    }
168
169    #[derive(Debug, PartialEq, Deserialize)]
170    struct UnitPayload;
171
172    /// A stand-in for a real `PluginOptions` enum covering the variant shapes
173    /// that occur in practice (newtype-over-unit, struct, newtype-over-scalar).
174    #[derive(Debug, PartialEq, Deserialize)]
175    enum DummyPlugins {
176        Unit(UnitPayload),
177        Struct { x: u32 },
178        Newtype(String),
179    }
180
181    #[test]
182    fn detects_unknown_vs_known_tags() {
183        let _g = lock();
184        // `is_unknown_variant_tag` never touches the dropped-tag buffer.
185        for tag in ["Bogus", "DoesNotExist", "NotARealVariant"] {
186            assert!(
187                is_unknown_variant_tag::<DummyPlugins>(tag),
188                "{tag} should be unknown"
189            );
190        }
191        for tag in ["Unit", "Struct", "Newtype"] {
192            assert!(
193                !is_unknown_variant_tag::<DummyPlugins>(tag),
194                "{tag} should be known"
195            );
196        }
197    }
198
199    #[test]
200    fn filter_drops_unknown_keeps_known() {
201        let _g = lock();
202        let _ = take_dropped_plugin_warnings();
203        let values: Vec<Value> = serde_json::from_str(
204            r#"[
205                {"Unit": null},
206                {"Bogus": {"a": 1}},
207                {"AnotherUnknown": {}},
208                {"Struct": {"x": 7}},
209                {"Newtype": "hi"}
210            ]"#,
211        )
212        .unwrap();
213        let kept = filter_known_plugin_options::<DummyPlugins>(values).unwrap();
214        assert_eq!(
215            kept,
216            vec![
217                DummyPlugins::Unit(UnitPayload),
218                DummyPlugins::Struct { x: 7 },
219                DummyPlugins::Newtype("hi".to_string()),
220            ]
221        );
222    }
223
224    #[test]
225    fn filter_errors_on_malformed_known_variant() {
226        let _g = lock();
227        // struct/newtype variants cannot come from these payloads, so a *known*
228        // variant's malformed payload must error (not be silently dropped).
229        for bad in [
230            r#"[{"Struct": 5}]"#,
231            r#"[{"Struct": null}]"#,
232            r#"[{"Struct": "oops"}]"#,
233            r#"[{"Newtype": 7}]"#,
234        ] {
235            let values: Vec<Value> = serde_json::from_str(bad).unwrap();
236            assert!(
237                filter_known_plugin_options::<DummyPlugins>(values).is_err(),
238                "expected error for {bad}"
239            );
240        }
241    }
242
243    #[test]
244    fn filter_errors_on_non_tagged_shape() {
245        let _g = lock();
246        let values: Vec<Value> = serde_json::from_str(r#"[123]"#).unwrap();
247        assert!(filter_known_plugin_options::<DummyPlugins>(values).is_err());
248    }
249
250    #[test]
251    fn filter_drops_multi_key_all_unknown_entry() {
252        let _g = lock();
253        let _ = take_dropped_plugin_warnings();
254        // A single entry carrying several *unknown* keys must be dropped, not
255        // error with serde's "expected map with a single key".
256        let values: Vec<Value> =
257            serde_json::from_str(r#"[{"unknown_one": 1, "unknown_two": 2}]"#).unwrap();
258        let kept = filter_known_plugin_options::<DummyPlugins>(values).unwrap();
259        assert!(kept.is_empty());
260        let mut recorded = take_dropped_plugin_warnings();
261        recorded.sort();
262        assert_eq!(
263            recorded,
264            vec!["unknown_one".to_string(), "unknown_two".to_string()]
265        );
266    }
267
268    #[test]
269    fn filter_keeps_known_drops_unknown_in_mixed_entry() {
270        let _g = lock();
271        let _ = take_dropped_plugin_warnings();
272        // A table mixing a *known* plugin with unknown ones keeps the known
273        // plugin and drops (warns) the unknown ones — it must not error.
274        let values: Vec<Value> =
275            serde_json::from_str(r#"[{"Struct": {"x": 1}, "bogus": 2}]"#).unwrap();
276        let kept = filter_known_plugin_options::<DummyPlugins>(values).unwrap();
277        assert_eq!(kept, vec![DummyPlugins::Struct { x: 1 }]);
278        assert_eq!(take_dropped_plugin_warnings(), vec!["bogus".to_string()]);
279    }
280
281    #[test]
282    fn filter_errors_on_malformed_known_in_mixed_entry() {
283        let _g = lock();
284        let _ = take_dropped_plugin_warnings();
285        // A *known* variant with a genuinely malformed payload still errors,
286        // even when an unknown key sits next to it in the same table.
287        let values: Vec<Value> = serde_json::from_str(r#"[{"Struct": 5, "bogus": 2}]"#).unwrap();
288        assert!(filter_known_plugin_options::<DummyPlugins>(values).is_err());
289        // The malformed known variant aborted before the unknown was recorded.
290        assert!(take_dropped_plugin_warnings().is_empty());
291    }
292
293    #[test]
294    fn dropped_tags_are_buffered_with_tag_only() {
295        let _g = lock();
296        let _ = take_dropped_plugin_warnings();
297        // Privacy: only the tag is recorded, never the raw payload.
298        let values: Vec<Value> =
299            serde_json::from_str(r#"[{"SecretPlugin": {"token": "hunter2"}}]"#).unwrap();
300        filter_known_plugin_options::<DummyPlugins>(values).unwrap();
301        assert_eq!(
302            take_dropped_plugin_warnings(),
303            vec!["SecretPlugin".to_string()]
304        );
305    }
306}