kube_core/
error_boundary.rs

1//! Types for isolating deserialization failures. See [`DeserializeGuard`].
2
3use std::borrow::Cow;
4
5use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
6use serde::Deserialize;
7use serde_value::DeserializerError;
8use thiserror::Error;
9
10use crate::{PartialObjectMeta, Resource};
11
12/// A wrapper type for K that lets deserializing the parent object succeed, even if the K is invalid.
13///
14/// For example, this can be used to still access valid objects from an `Api::list` call or `watcher`.
15// We can't implement Deserialize on Result<K, InvalidObject> directly, both because of the orphan rule and because
16// it would conflict with serde's blanket impl on Result<K, E>, even if E isn't Deserialize.
17#[derive(Debug, Clone)]
18pub struct DeserializeGuard<K>(pub Result<K, InvalidObject>);
19
20/// An object that failed to be deserialized by the [`DeserializeGuard`].
21#[derive(Debug, Clone, Error)]
22#[error("{error}")]
23pub struct InvalidObject {
24    // Should ideally be D::Error, but we don't know what type it has outside of Deserialize::deserialize()
25    // It *could* be Box<std::error::Error>, but we don't know that it is Send+Sync
26    /// The error message from deserializing the object.
27    pub error: String,
28    /// The metadata of the invalid object.
29    pub metadata: ObjectMeta,
30}
31
32impl<'de, K> Deserialize<'de> for DeserializeGuard<K>
33where
34    K: Deserialize<'de>,
35    // Not actually used, but we assume that K is a Kubernetes-style resource with a `metadata` section
36    K: Resource,
37{
38    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39    where
40        D: serde::Deserializer<'de>,
41    {
42        // Deserialize::deserialize consumes the deserializer, and we want to retry parsing as an ObjectMetaContainer
43        // if the initial parse fails, so that we can still implement Resource for the error case
44        let buffer = serde_value::Value::deserialize(deserializer)?;
45
46        // FIXME: can we avoid cloning the whole object? metadata should be enough, and even then we could prune managedFields
47        K::deserialize(buffer.clone())
48            .map(Ok)
49            .or_else(|err| {
50                let PartialObjectMeta { metadata, .. } =
51                    PartialObjectMeta::<K>::deserialize(buffer).map_err(DeserializerError::into_error)?;
52                Ok(Err(InvalidObject {
53                    error: err.to_string(),
54                    metadata,
55                }))
56            })
57            .map(DeserializeGuard)
58    }
59}
60
61impl<K: Resource> Resource for DeserializeGuard<K> {
62    type DynamicType = K::DynamicType;
63    type Scope = K::Scope;
64
65    fn kind(dt: &Self::DynamicType) -> Cow<str> {
66        K::kind(dt)
67    }
68
69    fn group(dt: &Self::DynamicType) -> Cow<str> {
70        K::group(dt)
71    }
72
73    fn version(dt: &Self::DynamicType) -> Cow<str> {
74        K::version(dt)
75    }
76
77    fn plural(dt: &Self::DynamicType) -> Cow<str> {
78        K::plural(dt)
79    }
80
81    fn meta(&self) -> &ObjectMeta {
82        self.0.as_ref().map_or_else(|err| &err.metadata, K::meta)
83    }
84
85    fn meta_mut(&mut self) -> &mut ObjectMeta {
86        self.0.as_mut().map_or_else(|err| &mut err.metadata, K::meta_mut)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use k8s_openapi::api::core::v1::{ConfigMap, Pod};
93    use serde_json::json;
94
95    use crate::{DeserializeGuard, Resource};
96
97    #[test]
98    fn should_parse_meta_of_invalid_objects() {
99        let pod_error = serde_json::from_value::<DeserializeGuard<Pod>>(json!({
100            "metadata": {
101                "name": "the-name",
102                "namespace": "the-namespace",
103            },
104            "spec": {
105                "containers": "not-a-list",
106            },
107        }))
108        .unwrap();
109        assert_eq!(pod_error.meta().name.as_deref(), Some("the-name"));
110        assert_eq!(pod_error.meta().namespace.as_deref(), Some("the-namespace"));
111        pod_error.0.unwrap_err();
112    }
113
114    #[test]
115    fn should_allow_valid_objects() {
116        let configmap = serde_json::from_value::<DeserializeGuard<ConfigMap>>(json!({
117            "metadata": {
118                "name": "the-name",
119                "namespace": "the-namespace",
120            },
121            "data": {
122                "foo": "bar",
123            },
124        }))
125        .unwrap();
126        assert_eq!(configmap.meta().name.as_deref(), Some("the-name"));
127        assert_eq!(configmap.meta().namespace.as_deref(), Some("the-namespace"));
128        assert_eq!(
129            configmap.0.unwrap().data,
130            Some([("foo".to_string(), "bar".to_string())].into())
131        )
132    }
133
134    #[test]
135    fn should_catch_invalid_objects() {
136        serde_json::from_value::<DeserializeGuard<Pod>>(json!({
137            "spec": {
138                "containers": "not-a-list"
139            }
140        }))
141        .unwrap()
142        .0
143        .unwrap_err();
144    }
145}