-
-
Notifications
You must be signed in to change notification settings - Fork 325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for CRD ConversionReview
types
#999
Merged
Merged
Changes from 13 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
e8564ab
Add conversion webhook typings
MikailBag b6d6d48
Add simple test
MikailBag 6be27e1
Fix formatting
MikailBag edad3da
Improve docs
MikailBag eaa34e4
Add is_success & is_failure methods
MikailBag 1012bdc
Use our Status in admission
MikailBag a3d36ec
Expand test
MikailBag 307f4c8
Rename response methods
MikailBag 1b2cef4
Avoid optionality
MikailBag ff50777
Move some code around
MikailBag 68748d7
Fix copy-paste error
MikailBag adf736b
Improve response signatures
MikailBag 4d1a801
Change status in invalid admission response
MikailBag f03ec56
Merge branch 'master' into conv-types
clux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
//! Contains types useful for implementing custom resource conversion webhooks. | ||
|
||
pub use self::types::{ | ||
ConversionRequest, ConversionResponse, ConversionReview, ConvertConversionReviewError, | ||
}; | ||
|
||
/// Defines low-level typings. | ||
mod types; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"kind": "ConversionReview", | ||
"apiVersion": "apiextensions.k8s.io/v1", | ||
"request": { | ||
"uid": "f263987e-4d58-465a-9195-bf72a1c83623", | ||
"desiredAPIVersion": "nullable.se/v1", | ||
"objects": [ | ||
{ | ||
"apiVersion": "nullable.se/v2", | ||
"kind": "ConfigMapGenerator", | ||
"metadata": { | ||
"annotations": { | ||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"nullable.se/v2\",\"kind\":\"ConfigMapGenerator\",\"metadata\":{\"annotations\":{},\"name\":\"kek\",\"namespace\":\"default\"},\"spec\":{\"content\":\"x\"}}\n" | ||
}, | ||
"creationTimestamp": "2022-09-04T14:21:34Z", | ||
"generation": 1, | ||
"managedFields": [ | ||
{ | ||
"apiVersion": "nullable.se/v2", | ||
"fieldsType": "FieldsV1", | ||
"fieldsV1": { | ||
"f:metadata": { | ||
"f:annotations": { | ||
".": {}, | ||
"f:kubectl.kubernetes.io/last-applied-configuration": {} | ||
} | ||
}, | ||
"f:spec": { | ||
".": {}, | ||
"f:content": {} | ||
} | ||
}, | ||
"manager": "kubectl-client-side-apply", | ||
"operation": "Update", | ||
"time": "2022-09-04T14:21:34Z" | ||
} | ||
], | ||
"name": "kek", | ||
"namespace": "default", | ||
"uid": "af7e84e4-573e-4b6e-bb66-0ea578c740da" | ||
}, | ||
"spec": { | ||
"content": "x" | ||
} | ||
} | ||
] | ||
}, | ||
"response": { | ||
"uid": "", | ||
"convertedObjects": null, | ||
"result": { | ||
"metadata": {} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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, | ||
MikailBag marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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(()), | ||
} | ||
MikailBag marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 { | ||
MikailBag marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In admission we have this as a non-optional field for both Response and Request (with skip still), is there a reason to keep it optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong reason. It simplifies manual creation of
ConversionRequest
a little bit (i.e. it may be surprising for user to see non-optional field which is not set by apiserver).