Skip to main content

kube_core/conversion/
types.rs

1use crate::{Status, TypeMeta};
2use serde::{Deserialize, Deserializer, Serialize};
3use thiserror::Error;
4
5/// The `kind` field in [`TypeMeta`]
6pub const META_KIND: &str = "ConversionReview";
7/// The `api_version` field in [`TypeMeta`] on the v1 version
8pub const META_API_VERSION_V1: &str = "apiextensions.k8s.io/v1";
9
10#[derive(Debug, Error)]
11#[error("request missing in ConversionReview")]
12/// Returned when `ConversionReview` cannot be converted into `ConversionRequest`
13pub struct ConvertConversionReviewError;
14
15/// Struct that describes both request and response
16#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
17pub struct ConversionReview {
18    /// Contains the API version and type of the request
19    #[serde(flatten)]
20    pub types: TypeMeta,
21    /// Contains conversion request
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub request: Option<ConversionRequest>,
24    /// Contains conversion response
25    #[serde(skip_serializing_if = "Option::is_none")]
26    #[serde(default)]
27    pub response: Option<ConversionResponse>,
28}
29
30/// Part of ConversionReview which is set on input (i.e. generated by apiserver)
31#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
32pub struct ConversionRequest {
33    /// [`TypeMeta`] of the [`ConversionReview`] this response was created from
34    ///  
35    /// This field dopied from the corresponding [`ConversionReview`].
36    /// It is not part of the Kubernetes API, it's consumed only by `kube`.
37    #[serde(skip)]
38    pub types: Option<TypeMeta>,
39    /// Random uid uniquely identifying this conversion call
40    pub uid: String,
41    /// The API group and version the objects should be converted to
42    #[serde(rename = "desiredAPIVersion")]
43    pub desired_api_version: String,
44    /// The list of objects to convert
45    ///
46    /// Note that list may contain one or more objects, in one or more versions.
47    // This field uses raw Value instead of Object/DynamicObject to simplify
48    // further downcasting.
49    pub objects: Vec<serde_json::Value>,
50}
51
52impl ConversionRequest {
53    /// Extracts request from the [`ConversionReview`]
54    pub fn from_review(review: ConversionReview) -> Result<Self, ConvertConversionReviewError> {
55        ConversionRequest::try_from(review)
56    }
57}
58
59impl TryFrom<ConversionReview> for ConversionRequest {
60    type Error = ConvertConversionReviewError;
61
62    fn try_from(review: ConversionReview) -> Result<Self, Self::Error> {
63        match review.request {
64            Some(mut req) => {
65                req.types = Some(review.types);
66                Ok(req)
67            }
68            None => Err(ConvertConversionReviewError),
69        }
70    }
71}
72
73/// Part of ConversionReview which is set on output (i.e. generated by conversion webhook)
74#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
75pub struct ConversionResponse {
76    /// [`TypeMeta`] of the [`ConversionReview`] this response was derived from
77    ///  
78    /// This field is copied from the corresponding [`ConversionRequest`].
79    /// It is not part of the Kubernetes API, it's consumed only by `kube`.
80    #[serde(skip)]
81    pub types: Option<TypeMeta>,
82    /// Copy of .request.uid
83    pub uid: String,
84    /// Outcome of the conversion operation
85    ///
86    /// Success: all objects were successfully converted
87    /// Failure: at least one object could not be converted.
88    /// It is recommended that conversion fails as rare as possible.
89    pub result: Status,
90    /// Converted objects
91    ///
92    /// This field should contain objects in the same order as in the request
93    /// Should be empty if conversion failed.
94    #[serde(rename = "convertedObjects")]
95    #[serde(deserialize_with = "parse_converted_objects")]
96    pub converted_objects: Vec<serde_json::Value>,
97}
98
99fn parse_converted_objects<'de, D>(de: D) -> Result<Vec<serde_json::Value>, D::Error>
100where
101    D: Deserializer<'de>,
102{
103    #[derive(Deserialize)]
104    #[serde(untagged)]
105    enum Helper {
106        List(Vec<serde_json::Value>),
107        Null(()),
108    }
109
110    let h: Helper = Helper::deserialize(de)?;
111    let res = match h {
112        Helper::List(l) => l,
113        Helper::Null(()) => Vec::new(),
114    };
115    Ok(res)
116}
117
118impl ConversionResponse {
119    /// Creates a new response, matching provided request
120    ///
121    /// This response must be finalized with one of:
122    /// - [`ConversionResponse::success`] when conversion succeeded
123    /// - [`ConversionResponse::failure`] when conversion failed
124    pub fn for_request(request: ConversionRequest) -> Self {
125        ConversionResponse::from(request)
126    }
127
128    /// Creates successful conversion response
129    ///
130    /// `converted_objects` must specify objects in the exact same order as on input.
131    pub fn success(mut self, converted_objects: Vec<serde_json::Value>) -> Self {
132        self.result = Status::success();
133        self.converted_objects = converted_objects;
134        self
135    }
136
137    /// Creates failed conversion response (discouraged)
138    ///
139    /// `request_uid` must be equal to the `.uid` field in the request.
140    /// `message` and `reason` will be returned to the apiserver.
141    pub fn failure(mut self, status: Status) -> Self {
142        self.result = status;
143        self
144    }
145
146    /// Creates failed conversion response, not matched with any request
147    ///
148    /// You should only call this function when request couldn't be parsed into [`ConversionRequest`].
149    /// Otherwise use `error`.
150    pub fn invalid(status: Status) -> Self {
151        ConversionResponse {
152            types: None,
153            uid: String::new(),
154            result: status,
155            converted_objects: Vec::new(),
156        }
157    }
158
159    /// Converts response into a [`ConversionReview`] value, ready to be sent as a response
160    pub fn into_review(self) -> ConversionReview {
161        self.into()
162    }
163}
164
165impl From<ConversionRequest> for ConversionResponse {
166    fn from(request: ConversionRequest) -> Self {
167        ConversionResponse {
168            types: request.types,
169            uid: request.uid,
170            result: Status {
171                status: None,
172                code: 0,
173                message: String::new(),
174                metadata: None,
175                reason: String::new(),
176                details: None,
177            },
178            converted_objects: Vec::new(),
179        }
180    }
181}
182
183impl From<ConversionResponse> for ConversionReview {
184    fn from(mut response: ConversionResponse) -> Self {
185        ConversionReview {
186            types: response.types.take().unwrap_or_else(|| {
187                // we don't know which uid, apiVersion and kind to use, let's just use something
188                TypeMeta {
189                    api_version: META_API_VERSION_V1.to_string(),
190                    kind: META_KIND.to_string(),
191                }
192            }),
193            request: None,
194            response: Some(response),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::{ConversionRequest, ConversionResponse};
202
203    #[test]
204    fn simple_request_parses() {
205        // this file contains dump of real request generated by kubernetes v1.22
206        let data = include_str!("./test_data/simple.json");
207        // check that we can parse this review, and all chain of conversion works
208        let review = serde_json::from_str(data).unwrap();
209        let req = ConversionRequest::from_review(review).unwrap();
210        let res = ConversionResponse::for_request(req);
211        let _ = res.into_review();
212    }
213}