1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
use crate::{Status, TypeMeta};
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;

/// The `kind` field in [`TypeMeta`]
pub const META_KIND: &str = "ConversionReview";
/// The `api_version` field in [`TypeMeta`] on the v1 version
pub const META_API_VERSION_V1: &str = "apiextensions.k8s.io/v1";

#[derive(Debug, Error)]
#[error("request missing in ConversionReview")]
/// Returned when `ConversionReview` cannot be converted into `ConversionRequest`
pub struct ConvertConversionReviewError;

/// Struct that describes both request and response
#[derive(Serialize, Deserialize)]
pub struct ConversionReview {
    /// Contains the API version and type of the request
    #[serde(flatten)]
    pub types: TypeMeta,
    /// Contains conversion request
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request: Option<ConversionRequest>,
    /// Contains conversion response
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub response: Option<ConversionResponse>,
}

/// Part of ConversionReview which is set on input (i.e. generated by apiserver)
#[derive(Serialize, Deserialize)]
pub struct ConversionRequest {
    /// [`TypeMeta`] of the [`ConversionReview`] this response was created from
    ///  
    /// This field dopied from the corresponding [`ConversionReview`].
    /// It is not part of the Kubernetes API, it's consumed only by `kube`.
    #[serde(skip)]
    pub types: Option<TypeMeta>,
    /// Random uid uniquely identifying this conversion call
    pub uid: String,
    /// The API group and version the objects should be converted to
    #[serde(rename = "desiredAPIVersion")]
    pub desired_api_version: String,
    /// The list of objects to convert
    ///
    /// Note that list may contain one or more objects, in one or more versions.
    // This field uses raw Value instead of Object/DynamicObject to simplify
    // further downcasting.
    pub objects: Vec<serde_json::Value>,
}

impl ConversionRequest {
    /// Extracts request from the [`ConversionReview`]
    pub fn from_review(review: ConversionReview) -> Result<Self, ConvertConversionReviewError> {
        ConversionRequest::try_from(review)
    }
}

impl TryFrom<ConversionReview> for ConversionRequest {
    type Error = ConvertConversionReviewError;

    fn try_from(review: ConversionReview) -> Result<Self, Self::Error> {
        match review.request {
            Some(mut req) => {
                req.types = Some(review.types);
                Ok(req)
            }
            None => Err(ConvertConversionReviewError),
        }
    }
}

/// Part of ConversionReview which is set on output (i.e. generated by conversion webhook)
#[derive(Serialize, Deserialize)]
pub struct ConversionResponse {
    /// [`TypeMeta`] of the [`ConversionReview`] this response was derived from
    ///  
    /// This field is copied from the corresponding [`ConversionRequest`].
    /// It is not part of the Kubernetes API, it's consumed only by `kube`.
    #[serde(skip)]
    pub types: Option<TypeMeta>,
    /// Copy of .request.uid
    pub uid: String,
    /// Outcome of the conversion operation
    ///
    /// Success: all objects were successfully converted
    /// Failure: at least one object could not be converted.
    /// It is recommended that conversion fails as rare as possible.
    pub result: Status,
    /// Converted objects
    ///
    /// This field should contain objects in the same order as in the request
    /// Should be empty if conversion failed.
    #[serde(rename = "convertedObjects")]
    #[serde(deserialize_with = "parse_converted_objects")]
    pub converted_objects: Vec<serde_json::Value>,
}

fn parse_converted_objects<'de, D>(de: D) -> Result<Vec<serde_json::Value>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum Helper {
        List(Vec<serde_json::Value>),
        Null(()),
    }

    let h: Helper = Helper::deserialize(de)?;
    let res = match h {
        Helper::List(l) => l,
        Helper::Null(()) => Vec::new(),
    };
    Ok(res)
}

impl ConversionResponse {
    /// Creates a new response, matching provided request
    ///
    /// This response must be finalized with one of:
    /// - [`ConversionResponse::success`] when conversion succeeded
    /// - [`ConversionResponse::failure`] when conversion failed
    pub fn for_request(request: ConversionRequest) -> Self {
        ConversionResponse::from(request)
    }

    /// Creates successful conversion response
    ///
    /// `converted_objects` must specify objects in the exact same order as on input.
    pub fn success(mut self, converted_objects: Vec<serde_json::Value>) -> Self {
        self.result = Status::success();
        self.converted_objects = converted_objects;
        self
    }

    /// Creates failed conversion response (discouraged)
    ///
    /// `request_uid` must be equal to the `.uid` field in the request.
    /// `message` and `reason` will be returned to the apiserver.
    pub fn failure(mut self, status: Status) -> Self {
        self.result = status;
        self
    }

    /// Creates failed conversion response, not matched with any request
    ///
    /// You should only call this function when request couldn't be parsed into [`ConversionRequest`].
    /// Otherwise use `error`.
    pub fn invalid(status: Status) -> Self {
        ConversionResponse {
            types: None,
            uid: String::new(),
            result: status,
            converted_objects: Vec::new(),
        }
    }

    /// Converts response into a [`ConversionReview`] value, ready to be sent as a response
    pub fn into_review(self) -> ConversionReview {
        self.into()
    }
}

impl From<ConversionRequest> for ConversionResponse {
    fn from(request: ConversionRequest) -> Self {
        ConversionResponse {
            types: request.types,
            uid: request.uid,
            result: Status {
                status: None,
                code: 0,
                message: String::new(),
                reason: String::new(),
                details: None,
            },
            converted_objects: Vec::new(),
        }
    }
}

impl From<ConversionResponse> for ConversionReview {
    fn from(mut response: ConversionResponse) -> Self {
        ConversionReview {
            types: response.types.take().unwrap_or_else(|| {
                // we don't know which uid, apiVersion and kind to use, let's just use something
                TypeMeta {
                    api_version: META_API_VERSION_V1.to_string(),
                    kind: META_KIND.to_string(),
                }
            }),
            request: None,
            response: Some(response),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{ConversionRequest, ConversionResponse};

    #[test]
    fn simple_request_parses() {
        // this file contains dump of real request generated by kubernetes v1.22
        let data = include_str!("./test_data/simple.json");
        // check that we can parse this review, and all chain of conversion worls
        let review = serde_json::from_str(data).unwrap();
        let req = ConversionRequest::from_review(review).unwrap();
        let res = ConversionResponse::for_request(req);
        let _ = res.into_review();
    }
}