chore: checkpoint before Python removal

This commit is contained in:
2026-03-26 22:33:59 +00:00
parent 683cec9307
commit e568ddf82a
29972 changed files with 11269302 additions and 2 deletions

378
vendor/kube-core/src/admission.rs vendored Normal file
View File

@@ -0,0 +1,378 @@
//! Contains types for implementing admission controllers.
//!
//! For more information on admission controllers, see:
//! <https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/>
//! <https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/>
//! <https://github.com/kubernetes/api/blob/master/admission/v1/types.go>
use crate::{
dynamic::DynamicObject,
gvk::{GroupVersionKind, GroupVersionResource},
metadata::TypeMeta,
resource::Resource,
Status,
};
use std::collections::HashMap;
use k8s_openapi::{api::authentication::v1::UserInfo, apimachinery::pkg::runtime::RawExtension};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("failed to serialize patch")]
/// Failed to serialize patch.
pub struct SerializePatchError(#[source] serde_json::Error);
#[derive(Debug, Error)]
#[error("failed to convert AdmissionReview into AdmissionRequest")]
/// Failed to convert `AdmissionReview` into `AdmissionRequest`.
pub struct ConvertAdmissionReviewError;
/// The `kind` field in [`TypeMeta`].
pub const META_KIND: &str = "AdmissionReview";
/// The `api_version` field in [`TypeMeta`] on the v1 version.
pub const META_API_VERSION_V1: &str = "admission.k8s.io/v1";
/// The `api_version` field in [`TypeMeta`] on the v1beta1 version.
pub const META_API_VERSION_V1BETA1: &str = "admission.k8s.io/v1beta1";
/// The top level struct used for Serializing and Deserializing AdmissionReview
/// requests and responses.
///
/// This is both the input type received by admission controllers, and the
/// output type admission controllers should return.
///
/// An admission controller should start by inspecting the [`AdmissionRequest`].
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AdmissionReview<T: Resource> {
/// Contains the API version and type of the request.
#[serde(flatten)]
pub types: TypeMeta,
/// Describes the attributes for the admission request.
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<AdmissionRequest<T>>,
/// Describes the attributes for the admission response.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub response: Option<AdmissionResponse>,
}
impl<T: Resource> TryInto<AdmissionRequest<T>> for AdmissionReview<T> {
type Error = ConvertAdmissionReviewError;
fn try_into(self) -> Result<AdmissionRequest<T>, Self::Error> {
match self.request {
Some(mut req) => {
req.types = self.types;
Ok(req)
}
None => Err(ConvertAdmissionReviewError),
}
}
}
/// An incoming [`AdmissionReview`] request.
///
/// In an admission controller scenario, this is extracted from an [`AdmissionReview`] via [`TryInto`]
///
/// ```no_run
/// use kube::core::{admission::{AdmissionRequest, AdmissionReview}, DynamicObject};
///
/// // The incoming AdmissionReview received by the controller.
/// let body: AdmissionReview<DynamicObject> = todo!();
/// let req: AdmissionRequest<_> = body.try_into().unwrap();
/// ```
///
/// Based on the contents of the request, an admission controller should construct an
/// [`AdmissionResponse`] using:
///
/// - [`AdmissionResponse::deny`] for illegal/rejected requests
/// - [`AdmissionResponse::invalid`] for malformed requests
/// - [`AdmissionResponse::from`] for the happy path
///
/// then wrap the chosen response in an [`AdmissionReview`] via [`AdmissionResponse::into_review`].
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AdmissionRequest<T: Resource> {
/// Copied from the containing [`AdmissionReview`] and used to specify a
/// response type and version when constructing an [`AdmissionResponse`].
#[serde(skip)]
pub types: TypeMeta,
/// An identifier for the individual request/response. It allows us to
/// distinguish instances of requests which are otherwise identical (parallel
/// requests, requests when earlier requests did not modify, etc). The UID is
/// meant to track the round trip (request/response) between the KAS and the
/// webhook, not the user request. It is suitable for correlating log entries
/// between the webhook and apiserver, for either auditing or debugging.
pub uid: String,
/// The fully-qualified type of object being submitted (for example, v1.Pod
/// or autoscaling.v1.Scale).
pub kind: GroupVersionKind,
/// The fully-qualified resource being requested (for example, v1.pods).
pub resource: GroupVersionResource,
/// The subresource being requested, if any (for example, "status" or
/// "scale").
#[serde(default)]
pub sub_resource: Option<String>,
/// The fully-qualified type of the original API request (for example, v1.Pod
/// or autoscaling.v1.Scale). If this is specified and differs from the value
/// in "kind", an equivalent match and conversion was performed.
///
/// For example, if deployments can be modified via apps/v1 and apps/v1beta1,
/// and a webhook registered a rule of `apiGroups:["apps"],
/// apiVersions:["v1"], resources:["deployments"]` and
/// `matchPolicy:Equivalent`, an API request to apps/v1beta1 deployments
/// would be converted and sent to the webhook with `kind: {group:"apps",
/// version:"v1", kind:"Deployment"}` (matching the rule the webhook
/// registered for), and `requestKind: {group:"apps", version:"v1beta1",
/// kind:"Deployment"}` (indicating the kind of the original API request).
/// See documentation for the "matchPolicy" field in the webhook
/// configuration type for more details.
#[serde(default)]
pub request_kind: Option<GroupVersionKind>,
/// The fully-qualified resource of the original API request (for example,
/// v1.pods). If this is specified and differs from the value in "resource",
/// an equivalent match and conversion was performed.
///
/// For example, if deployments can be modified via apps/v1 and apps/v1beta1,
/// and a webhook registered a rule of `apiGroups:["apps"],
/// apiVersions:["v1"], resources: ["deployments"]` and `matchPolicy:
/// Equivalent`, an API request to apps/v1beta1 deployments would be
/// converted and sent to the webhook with `resource: {group:"apps",
/// version:"v1", resource:"deployments"}` (matching the resource the webhook
/// registered for), and `requestResource: {group:"apps", version:"v1beta1",
/// resource:"deployments"}` (indicating the resource of the original API
/// request).
///
/// See documentation for the "matchPolicy" field in the webhook
/// configuration type.
#[serde(default)]
pub request_resource: Option<GroupVersionResource>,
/// The name of the subresource of the original API request, if any (for
/// example, "status" or "scale"). If this is specified and differs from the
/// value in "subResource", an equivalent match and conversion was performed.
/// See documentation for the "matchPolicy" field in the webhook
/// configuration type.
#[serde(default)]
pub request_sub_resource: Option<String>,
/// The name of the object as presented in the request. On a CREATE
/// operation, the client may omit name and rely on the server to generate
/// the name. If that is the case, this field will contain an empty string.
#[serde(default)]
pub name: String,
/// The namespace associated with the request (if any).
#[serde(default)]
pub namespace: Option<String>,
/// The operation being performed. This may be different than the operation
/// requested. e.g. a patch can result in either a CREATE or UPDATE
/// Operation.
pub operation: Operation,
/// Information about the requesting user.
pub user_info: UserInfo,
/// The object from the incoming request. It's `None` for [`DELETE`](Operation::Delete) operations.
pub object: Option<T>,
/// The existing object. Only populated for DELETE and UPDATE requests.
pub old_object: Option<T>,
/// Specifies that modifications will definitely not be persisted for this
/// request.
#[serde(default)]
pub dry_run: bool,
/// The operation option structure of the operation being performed. e.g.
/// `meta.k8s.io/v1.DeleteOptions` or `meta.k8s.io/v1.CreateOptions`. This
/// may be different than the options the caller provided. e.g. for a patch
/// request the performed [`Operation`] might be a [`CREATE`](Operation::Create), in
/// which case the Options will a `meta.k8s.io/v1.CreateOptions` even though
/// the caller provided `meta.k8s.io/v1.PatchOptions`.
#[serde(default)]
pub options: Option<RawExtension>,
}
/// The operation specified in an [`AdmissionRequest`].
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Operation {
/// An operation that creates a resource.
Create,
/// An operation that updates a resource.
Update,
/// An operation that deletes a resource.
Delete,
/// An operation that connects to a resource.
Connect,
}
/// An outgoing [`AdmissionReview`] response. Constructed from the corresponding
/// [`AdmissionRequest`].
/// ```no_run
/// use kube::core::{
/// admission::{AdmissionRequest, AdmissionResponse, AdmissionReview},
/// DynamicObject,
/// };
///
/// // The incoming AdmissionReview received by the controller.
/// let body: AdmissionReview<DynamicObject> = todo!();
/// let req: AdmissionRequest<_> = body.try_into().unwrap();
///
/// // A normal response with no side effects.
/// let _: AdmissionReview<_> = AdmissionResponse::from(&req).into_review();
///
/// // A response rejecting the admission webhook with a provided reason.
/// let _: AdmissionReview<_> = AdmissionResponse::from(&req)
/// .deny("Some rejection reason.")
/// .into_review();
///
/// use json_patch::{AddOperation, Patch, PatchOperation, jsonptr::PointerBuf};
///
/// // A response adding a label to the resource.
/// let _: AdmissionReview<_> = AdmissionResponse::from(&req)
/// .with_patch(Patch(vec![PatchOperation::Add(AddOperation {
/// path: PointerBuf::from_tokens(["metadata","labels","my-label"]),
/// value: serde_json::Value::String("my-value".to_owned()),
/// })]))
/// .unwrap()
/// .into_review();
///
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AdmissionResponse {
/// Copied from the corresponding consructing [`AdmissionRequest`].
#[serde(skip)]
pub types: TypeMeta,
/// Identifier for the individual request/response. This must be copied over
/// from the corresponding AdmissionRequest.
pub uid: String,
/// Indicates whether or not the admission request was permitted.
pub allowed: bool,
/// Extra details into why an admission request was denied. This field IS NOT
/// consulted in any way if "Allowed" is "true".
#[serde(rename = "status")]
pub result: Status,
/// The patch body. Currently we only support "JSONPatch" which implements
/// RFC 6902.
#[serde(skip_serializing_if = "Option::is_none")]
pub patch: Option<Vec<u8>>,
/// The type of Patch. Currently we only allow "JSONPatch".
#[serde(skip_serializing_if = "Option::is_none")]
patch_type: Option<PatchType>,
/// An unstructured key value map set by remote admission controller (e.g.
/// error=image-blacklisted). MutatingAdmissionWebhook and
/// ValidatingAdmissionWebhook admission controller will prefix the keys with
/// admission webhook name (e.g.
/// imagepolicy.example.com/error=image-blacklisted). AuditAnnotations will
/// be provided by the admission webhook to add additional context to the
/// audit log for this request.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub audit_annotations: HashMap<String, String>,
/// A list of warning messages to return to the requesting API client.
/// Warning messages describe a problem the client making the API request
/// should correct or be aware of. Limit warnings to 120 characters if
/// possible. Warnings over 256 characters and large numbers of warnings may
/// be truncated.
#[serde(skip_serializing_if = "Option::is_none")]
pub warnings: Option<Vec<String>>,
}
impl<T: Resource> From<&AdmissionRequest<T>> for AdmissionResponse {
fn from(req: &AdmissionRequest<T>) -> Self {
Self {
types: req.types.clone(),
uid: req.uid.clone(),
allowed: true,
result: Default::default(),
patch: None,
patch_type: None,
audit_annotations: Default::default(),
warnings: None,
}
}
}
impl AdmissionResponse {
/// Constructs an invalid [`AdmissionResponse`]. It doesn't copy the uid from
/// the corresponding [`AdmissionRequest`], so should only be used when the
/// original request cannot be read.
pub fn invalid<T: ToString>(reason: T) -> Self {
Self {
// Since we don't have a request to use for construction, just
// default to "admission.k8s.io/v1beta1", since it is the most
// supported and we won't be using any of the new fields.
types: TypeMeta {
kind: META_KIND.to_owned(),
api_version: META_API_VERSION_V1BETA1.to_owned(),
},
uid: Default::default(),
allowed: false,
result: Status::failure(&reason.to_string(), "InvalidRequest"),
patch: None,
patch_type: None,
audit_annotations: Default::default(),
warnings: None,
}
}
/// Deny the request with a reason. The reason will be sent to the original caller.
#[must_use]
pub fn deny<T: ToString>(mut self, reason: T) -> Self {
self.allowed = false;
self.result.message = reason.to_string();
self
}
/// Add JSON patches to the response, modifying the object from the request.
pub fn with_patch(mut self, patch: json_patch::Patch) -> Result<Self, SerializePatchError> {
self.patch = Some(serde_json::to_vec(&patch).map_err(SerializePatchError)?);
self.patch_type = Some(PatchType::JsonPatch);
Ok(self)
}
/// Converts an [`AdmissionResponse`] into a generic [`AdmissionReview`] that
/// can be used as a webhook response.
pub fn into_review(self) -> AdmissionReview<DynamicObject> {
AdmissionReview {
types: self.types.clone(),
request: None,
response: Some(self),
}
}
}
/// The type of patch returned in an [`AdmissionResponse`].
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum PatchType {
/// Specifies the patch body implements JSON Patch under RFC 6902.
#[serde(rename = "JSONPatch")]
JsonPatch,
}
#[cfg(test)]
mod test {
const WEBHOOK_BODY: &str = r#"{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"0c9a8d74-9cb7-44dd-b98e-09fd62def2f4","kind":{"group":"","version":"v1","kind":"Pod"},"resource":{"group":"","version":"v1","resource":"pods"},"requestKind":{"group":"","version":"v1","kind":"Pod"},"requestResource":{"group":"","version":"v1","resource":"pods"},"name":"echo-pod","namespace":"colin-coder","operation":"CREATE","userInfo":{"username":"colin@coder.com","groups":["system:authenticated"],"extra":{"iam.gke.io/user-assertion":["REDACTED"],"user-assertion.cloud.google.com":["REDACTED"]}},"object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"echo-pod","namespace":"colin-coder","creationTimestamp":null,"labels":{"app":"echo-server"},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"echo-server\"},\"name\":\"echo-pod\",\"namespace\":\"colin-coder\"},\"spec\":{\"containers\":[{\"image\":\"jmalloc/echo-server\",\"name\":\"echo-server\",\"ports\":[{\"containerPort\":8080,\"name\":\"http-port\"}]}]}}\n"},"managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"v1","time":"2021-03-29T23:02:16Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}},"f:labels":{".":{},"f:app":{}}},"f:spec":{"f:containers":{"k:{\"name\":\"echo-server\"}":{".":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:name":{},"f:protocol":{}}},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}}]},"spec":{"volumes":[{"name":"default-token-rxbqq","secret":{"secretName":"default-token-rxbqq"}}],"containers":[{"name":"echo-server","image":"jmalloc/echo-server","ports":[{"name":"http-port","containerPort":8080,"protocol":"TCP"}],"resources":{},"volumeMounts":[{"name":"default-token-rxbqq","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}],"priority":0,"enableServiceLinks":true},"status":{}},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1"}}}"#;
use crate::{
admission::{AdmissionResponse, AdmissionReview, ConvertAdmissionReviewError},
DynamicObject,
};
#[test]
fn v1_webhook_unmarshals() {
serde_json::from_str::<AdmissionReview<DynamicObject>>(WEBHOOK_BODY).unwrap();
}
#[test]
fn version_passes_through() -> Result<(), ConvertAdmissionReviewError> {
let rev = serde_json::from_str::<AdmissionReview<DynamicObject>>(WEBHOOK_BODY).unwrap();
let rev_typ = rev.types.clone();
let res = AdmissionResponse::from(&rev.try_into()?).into_review();
// Ensure TypeMeta was correctly deserialized.
assert_ne!(&rev_typ.api_version, "");
// The TypeMeta should be correctly passed through from the incoming
// request.
assert_eq!(&rev_typ, &res.types);
Ok(())
}
}

282
vendor/kube-core/src/cel.rs vendored Normal file
View File

@@ -0,0 +1,282 @@
//! CEL validation for CRDs
use std::str::FromStr;
#[cfg(feature = "schema")] use schemars::schema::Schema;
use serde::{Deserialize, Serialize};
/// Rule is a CEL validation rule for the CRD field
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
/// rule represents the expression which will be evaluated by CEL.
/// The `self` variable in the CEL expression is bound to the scoped value.
pub rule: String,
/// message represents CEL validation message for the provided type
/// If unset, the message is "failed rule: {Rule}".
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
/// fieldPath represents the field path returned when the validation fails.
/// It must be a relative JSON path, scoped to the location of the field in the schema
#[serde(skip_serializing_if = "Option::is_none")]
pub field_path: Option<String>,
/// reason is a machine-readable value providing more detail about why a field failed the validation.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<Reason>,
}
impl Rule {
/// Initialize the rule
///
/// ```rust
/// use kube_core::Rule;
/// let r = Rule::new("self == oldSelf");
///
/// assert_eq!(r.rule, "self == oldSelf".to_string())
/// ```
pub fn new(rule: impl Into<String>) -> Self {
Self {
rule: rule.into(),
..Default::default()
}
}
/// Set the rule message.
///
/// use kube_core::Rule;
/// ```rust
/// use kube_core::{Rule, Message};
///
/// let r = Rule::new("self == oldSelf").message("is immutable");
/// assert_eq!(r.rule, "self == oldSelf".to_string());
/// assert_eq!(r.message, Some(Message::Message("is immutable".to_string())));
/// ```
pub fn message(mut self, message: impl Into<Message>) -> Self {
self.message = Some(message.into());
self
}
/// Set the failure reason.
///
/// use kube_core::Rule;
/// ```rust
/// use kube_core::{Rule, Reason};
///
/// let r = Rule::new("self == oldSelf").reason(Reason::default());
/// assert_eq!(r.rule, "self == oldSelf".to_string());
/// assert_eq!(r.reason, Some(Reason::FieldValueInvalid));
/// ```
pub fn reason(mut self, reason: impl Into<Reason>) -> Self {
self.reason = Some(reason.into());
self
}
/// Set the failure field_path.
///
/// use kube_core::Rule;
/// ```rust
/// use kube_core::Rule;
///
/// let r = Rule::new("self == oldSelf").field_path("obj.field");
/// assert_eq!(r.rule, "self == oldSelf".to_string());
/// assert_eq!(r.field_path, Some("obj.field".to_string()));
/// ```
pub fn field_path(mut self, field_path: impl Into<String>) -> Self {
self.field_path = Some(field_path.into());
self
}
}
impl From<&str> for Rule {
fn from(value: &str) -> Self {
Self {
rule: value.into(),
..Default::default()
}
}
}
impl From<(&str, &str)> for Rule {
fn from((rule, msg): (&str, &str)) -> Self {
Self {
rule: rule.into(),
message: Some(msg.into()),
..Default::default()
}
}
}
/// Message represents CEL validation message for the provided type
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Message {
/// Message represents the message displayed when validation fails. The message is required if the Rule contains
/// line breaks. The message must not contain line breaks.
/// Example:
/// "must be a URL with the host matching spec.host"
Message(String),
/// Expression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails.
/// Since messageExpression is used as a failure message, it must evaluate to a string. If messageExpression results in a runtime error, the runtime error is logged, and the validation failure message is produced
/// as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string
/// that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset, and
/// the fact that messageExpression produced an empty string/string with only spaces/string with line breaks will be logged.
/// messageExpression has access to all the same variables as the rule; the only difference is the return type.
/// Example:
/// "x must be less than max ("+string(self.max)+")"
#[serde(rename = "messageExpression")]
Expression(String),
}
impl From<&str> for Message {
fn from(value: &str) -> Self {
Message::Message(value.to_string())
}
}
/// Reason is a machine-readable value providing more detail about why a field failed the validation.
///
/// More in [docs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-reason)
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
pub enum Reason {
/// FieldValueInvalid is used to report malformed values (e.g. failed regex
/// match, too long, out of bounds).
#[default]
FieldValueInvalid,
/// FieldValueForbidden is used to report valid (as per formatting rules)
/// values which would be accepted under some conditions, but which are not
/// permitted by the current conditions (such as security policy).
FieldValueForbidden,
/// FieldValueRequired is used to report required values that are not
/// provided (e.g. empty strings, null values, or empty arrays).
FieldValueRequired,
/// FieldValueDuplicate is used to report collisions of values that must be
/// unique (e.g. unique IDs).
FieldValueDuplicate,
}
impl FromStr for Reason {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
/// Validate takes schema and applies a set of validation rules to it. The rules are stored
/// on the top level under the "x-kubernetes-validations".
///
/// ```rust
/// use schemars::schema::Schema;
/// use kube::core::{Rule, Reason, Message, validate};
///
/// let mut schema = Schema::Object(Default::default());
/// let rules = &[Rule{
/// rule: "self.spec.host == self.url.host".into(),
/// message: Some("must be a URL with the host matching spec.host".into()),
/// field_path: Some("spec.host".into()),
/// ..Default::default()
/// }];
/// validate(&mut schema, rules)?;
/// assert_eq!(
/// serde_json::to_string(&schema).unwrap(),
/// r#"{"x-kubernetes-validations":[{"fieldPath":"spec.host","message":"must be a URL with the host matching spec.host","rule":"self.spec.host == self.url.host"}]}"#,
/// );
/// # Ok::<(), serde_json::Error>(())
///```
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn validate(s: &mut Schema, rules: &[Rule]) -> Result<(), serde_json::Error> {
match s {
Schema::Bool(_) => (),
Schema::Object(schema_object) => {
schema_object
.extensions
.insert("x-kubernetes-validations".into(), serde_json::to_value(rules)?);
}
};
Ok(())
}
/// Validate property mutates property under property_index of the schema
/// with the provided set of validation rules.
///
/// ```rust
/// use schemars::JsonSchema;
/// use kube::core::{Rule, validate_property};
///
/// #[derive(JsonSchema)]
/// struct MyStruct {
/// field: Option<String>,
/// }
///
/// let gen = &mut schemars::gen::SchemaSettings::openapi3().into_generator();
/// let mut schema = MyStruct::json_schema(gen);
/// let rules = &[Rule::new("self != oldSelf")];
/// validate_property(&mut schema, 0, rules)?;
/// assert_eq!(
/// serde_json::to_string(&schema).unwrap(),
/// r#"{"type":"object","properties":{"field":{"type":"string","nullable":true,"x-kubernetes-validations":[{"rule":"self != oldSelf"}]}}}"#
/// );
/// # Ok::<(), serde_json::Error>(())
///```
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn validate_property(
s: &mut Schema,
property_index: usize,
rules: &[Rule],
) -> Result<(), serde_json::Error> {
match s {
Schema::Bool(_) => (),
Schema::Object(schema_object) => {
let obj = schema_object.object();
for (n, (_, schema)) in obj.properties.iter_mut().enumerate() {
if n == property_index {
return validate(schema, rules);
}
}
}
};
Ok(())
}
/// Merge schema properties in order to pass overrides or extension properties from the other schema.
///
/// ```rust
/// use schemars::JsonSchema;
/// use kube::core::{Rule, merge_properties};
///
/// #[derive(JsonSchema)]
/// struct MyStruct {
/// a: Option<bool>,
/// }
///
/// #[derive(JsonSchema)]
/// struct MySecondStruct {
/// a: bool,
/// b: Option<bool>,
/// }
/// let gen = &mut schemars::gen::SchemaSettings::openapi3().into_generator();
/// let mut first = MyStruct::json_schema(gen);
/// let mut second = MySecondStruct::json_schema(gen);
/// merge_properties(&mut first, &mut second);
///
/// assert_eq!(
/// serde_json::to_string(&first).unwrap(),
/// r#"{"type":"object","properties":{"a":{"type":"boolean"},"b":{"type":"boolean","nullable":true}}}"#
/// );
/// # Ok::<(), serde_json::Error>(())
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub fn merge_properties(s: &mut Schema, merge: &mut Schema) {
match s {
schemars::schema::Schema::Bool(_) => (),
schemars::schema::Schema::Object(schema_object) => {
let obj = schema_object.object();
for (k, v) in &merge.clone().into_object().object().properties {
obj.properties.insert(k.clone(), v.clone());
}
}
}
}

View File

@@ -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;

View File

@@ -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": {}
}
}
}

212
vendor/kube-core/src/conversion/types.rs vendored Normal file
View File

@@ -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,
/// 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();
}
}

238
vendor/kube-core/src/crd.rs vendored Normal file
View File

@@ -0,0 +1,238 @@
//! Traits and tyes for CustomResources
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions as apiexts;
/// Types for v1 CustomResourceDefinitions
pub mod v1 {
use super::apiexts::v1::CustomResourceDefinition as Crd;
/// Extension trait that is implemented by kube-derive
pub trait CustomResourceExt {
/// Helper to generate the CRD including the JsonSchema
///
/// This is using the stable v1::CustomResourceDefinitions (present in kubernetes >= 1.16)
fn crd() -> Crd;
/// Helper to return the name of this `CustomResourceDefinition` in kubernetes.
///
/// This is not the name of an _instance_ of this custom resource but the `CustomResourceDefinition` object itself.
fn crd_name() -> &'static str;
/// Helper to generate the api information type for use with the dynamic `Api`
fn api_resource() -> crate::discovery::ApiResource;
/// Shortnames of this resource type.
///
/// For example: [`Pod`] has the shortname alias `po`.
///
/// NOTE: This function returns *declared* short names (at compile-time, using the `#[kube(shortname = "foo")]`), not the
/// shortnames registered with the Kubernetes API (which is what tools such as `kubectl` look at).
///
/// [`Pod`]: `k8s_openapi::api::core::v1::Pod`
fn shortnames() -> &'static [&'static str];
}
/// Possible errors when merging CRDs
#[derive(Debug, thiserror::Error)]
pub enum MergeError {
/// No crds given
#[error("empty list of CRDs cannot be merged")]
MissingCrds,
/// Stored api not present
#[error("stored api version {0} not found")]
MissingStoredApi(String),
/// Root api not present
#[error("root api version {0} not found")]
MissingRootVersion(String),
/// No versions given in one crd to merge
#[error("given CRD must have versions")]
MissingVersions,
/// Too many versions given to individual crds
#[error("mergeable CRDs cannot have multiple versions")]
MultiVersionCrd,
/// Mismatching spec properties on crds
#[error("mismatching {0} property from given CRDs")]
PropertyMismatch(String),
}
/// Merge a collection of crds into a single multiversion crd
///
/// Given multiple [`CustomResource`] derived types granting [`CRD`]s via [`CustomResourceExt::crd`],
/// we can merge them into a single [`CRD`] with multiple [`CRDVersion`] objects, marking only
/// the specified apiversion as `storage: true`.
///
/// This merge algorithm assumes that every [`CRD`]:
///
/// - exposes exactly one [`CRDVersion`]
/// - uses identical values for `spec.group`, `spec.scope`, and `spec.names.kind`
///
/// This is always true for [`CustomResource`] derives.
///
/// ## Usage
///
/// ```no_run
/// # use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
/// use kube::core::crd::merge_crds;
/// # let mycrd_v1: CustomResourceDefinition = todo!(); // v1::MyCrd::crd();
/// # let mycrd_v2: CustomResourceDefinition = todo!(); // v2::MyCrd::crd();
/// let crds = vec![mycrd_v1, mycrd_v2];
/// let multi_version_crd = merge_crds(crds, "v1").unwrap();
/// ```
///
/// Note the merge is done by marking the:
///
/// - crd containing the `stored_apiversion` as the place the other crds merge their [`CRDVersion`] items
/// - stored version is marked with `storage: true`, while all others get `storage: false`
///
/// [`CustomResourceExt::crd`]: crate::CustomResourceExt::crd
/// [`CRD`]: https://docs.rs/k8s-openapi/latest/k8s_openapi/apiextensions_apiserver/pkg/apis/apiextensions/v1/struct.CustomResourceDefinition.html
/// [`CRDVersion`]: https://docs.rs/k8s-openapi/latest/k8s_openapi/apiextensions_apiserver/pkg/apis/apiextensions/v1/struct.CustomResourceDefinitionVersion.html
/// [`CustomResource`]: https://docs.rs/kube/latest/kube/derive.CustomResource.html
pub fn merge_crds(mut crds: Vec<Crd>, stored_apiversion: &str) -> Result<Crd, MergeError> {
if crds.is_empty() {
return Err(MergeError::MissingCrds);
}
for crd in crds.iter() {
if crd.spec.versions.is_empty() {
return Err(MergeError::MissingVersions);
}
if crd.spec.versions.len() != 1 {
return Err(MergeError::MultiVersionCrd);
}
}
let ver = stored_apiversion;
let found = crds.iter().position(|c| c.spec.versions[0].name == ver);
// Extract the root/first object to start with (the one we will merge into)
let mut root = match found {
None => return Err(MergeError::MissingRootVersion(ver.into())),
Some(idx) => crds.remove(idx),
};
root.spec.versions[0].storage = true; // main version - set true in case modified
// Values that needs to be identical across crds:
let group = &root.spec.group;
let kind = &root.spec.names.kind;
let scope = &root.spec.scope;
// sanity; don't merge crds with mismatching groups, versions, or other core properties
for crd in crds.iter() {
if &crd.spec.group != group {
return Err(MergeError::PropertyMismatch("group".to_string()));
}
if &crd.spec.names.kind != kind {
return Err(MergeError::PropertyMismatch("kind".to_string()));
}
if &crd.spec.scope != scope {
return Err(MergeError::PropertyMismatch("scope".to_string()));
}
}
// combine all version objects into the root object
let versions = &mut root.spec.versions;
while let Some(mut crd) = crds.pop() {
while let Some(mut v) = crd.spec.versions.pop() {
v.storage = false; // secondary versions
versions.push(v);
}
}
Ok(root)
}
mod tests {
#[test]
fn crd_merge() {
use super::{merge_crds, Crd};
let crd1 = r#"
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: multiversions.kube.rs
spec:
group: kube.rs
names:
categories: []
kind: MultiVersion
plural: multiversions
shortNames: []
singular: multiversion
scope: Namespaced
versions:
- additionalPrinterColumns: []
name: v1
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
served: true
storage: true"#;
let crd2 = r#"
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: multiversions.kube.rs
spec:
group: kube.rs
names:
categories: []
kind: MultiVersion
plural: multiversions
shortNames: []
singular: multiversion
scope: Namespaced
versions:
- additionalPrinterColumns: []
name: v2
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
served: true
storage: true"#;
let expected = r#"
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: multiversions.kube.rs
spec:
group: kube.rs
names:
categories: []
kind: MultiVersion
plural: multiversions
shortNames: []
singular: multiversion
scope: Namespaced
versions:
- additionalPrinterColumns: []
name: v2
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
served: true
storage: true
- additionalPrinterColumns: []
name: v1
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
served: true
storage: false"#;
let c1: Crd = serde_yaml::from_str(crd1).unwrap();
let c2: Crd = serde_yaml::from_str(crd2).unwrap();
let ce: Crd = serde_yaml::from_str(expected).unwrap();
let combined = merge_crds(vec![c1, c2], "v2").unwrap();
let combo_json = serde_json::to_value(&combined).unwrap();
let exp_json = serde_json::to_value(&ce).unwrap();
assert_json_diff::assert_json_eq!(combo_json, exp_json);
}
}
}
// re-export current latest (v1)
pub use v1::{merge_crds, CustomResourceExt, MergeError};

206
vendor/kube-core/src/discovery.rs vendored Normal file
View File

@@ -0,0 +1,206 @@
//! Type information structs for API discovery
use crate::{gvk::GroupVersionKind, resource::Resource};
use serde::{Deserialize, Serialize};
/// Information about a Kubernetes API resource
///
/// Enough information to use it like a `Resource` by passing it to the dynamic `Api`
/// constructors like `Api::all_with` and `Api::namespaced_with`.
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ApiResource {
/// Resource group, empty for core group.
pub group: String,
/// group version
pub version: String,
/// apiVersion of the resource (v1 for core group,
/// groupName/groupVersions for other).
pub api_version: String,
/// Singular PascalCase name of the resource
pub kind: String,
/// Plural name of the resource
pub plural: String,
}
impl ApiResource {
/// Creates an ApiResource by type-erasing a Resource
pub fn erase<K: Resource>(dt: &K::DynamicType) -> Self {
ApiResource {
group: K::group(dt).to_string(),
version: K::version(dt).to_string(),
api_version: K::api_version(dt).to_string(),
kind: K::kind(dt).to_string(),
plural: K::plural(dt).to_string(),
}
}
/// Creates an ApiResource from group, version, kind and plural name.
pub fn from_gvk_with_plural(gvk: &GroupVersionKind, plural: &str) -> Self {
ApiResource {
api_version: gvk.api_version(),
group: gvk.group.clone(),
version: gvk.version.clone(),
kind: gvk.kind.clone(),
plural: plural.to_string(),
}
}
/// Creates an ApiResource from group, version and kind.
///
/// # Warning
/// This function will **guess** the resource plural name.
/// Usually, this is ok, but for CRDs with complex pluralisations it can fail.
/// If you are getting your values from `kube_derive` use the generated method for giving you an [`ApiResource`].
/// Otherwise consider using [`ApiResource::from_gvk_with_plural`](crate::discovery::ApiResource::from_gvk_with_plural)
/// to explicitly set the plural, or run api discovery on it via `kube::discovery`.
pub fn from_gvk(gvk: &GroupVersionKind) -> Self {
ApiResource::from_gvk_with_plural(gvk, &to_plural(&gvk.kind.to_ascii_lowercase()))
}
}
/// Resource scope
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum Scope {
/// Objects are global
Cluster,
/// Each object lives in namespace.
Namespaced,
}
/// Rbac verbs for ApiCapabilities
pub mod verbs {
/// Create a resource
pub const CREATE: &str = "create";
/// Get single resource
pub const GET: &str = "get";
/// List objects
pub const LIST: &str = "list";
/// Watch for objects changes
pub const WATCH: &str = "watch";
/// Delete single object
pub const DELETE: &str = "delete";
/// Delete multiple objects at once
pub const DELETE_COLLECTION: &str = "deletecollection";
/// Update an object
pub const UPDATE: &str = "update";
/// Patch an object
pub const PATCH: &str = "patch";
}
/// Contains the capabilities of an API resource
#[derive(Debug, Clone)]
pub struct ApiCapabilities {
/// Scope of the resource
pub scope: Scope,
/// Available subresources.
///
/// Please note that returned ApiResources are not standalone resources.
/// Their name will be of form `subresource_name`, not `resource_name/subresource_name`.
/// To work with subresources, use `Request` methods for now.
pub subresources: Vec<(ApiResource, ApiCapabilities)>,
/// Supported operations on this resource
pub operations: Vec<String>,
}
impl ApiCapabilities {
/// Checks that given verb is supported on this resource.
pub fn supports_operation(&self, operation: &str) -> bool {
self.operations.iter().any(|op| op == operation)
}
}
// Simple pluralizer. Handles the special cases.
fn to_plural(word: &str) -> String {
if word == "endpoints" || word == "endpointslices" {
return word.to_owned();
} else if word == "nodemetrics" {
return "nodes".to_owned();
} else if word == "podmetrics" {
return "pods".to_owned();
}
// Words ending in s, x, z, ch, sh will be pluralized with -es (eg. foxes).
if word.ends_with('s')
|| word.ends_with('x')
|| word.ends_with('z')
|| word.ends_with("ch")
|| word.ends_with("sh")
{
return format!("{word}es");
}
// Words ending in y that are preceded by a consonant will be pluralized by
// replacing y with -ies (eg. puppies).
if word.ends_with('y') {
if let Some(c) = word.chars().nth(word.len() - 2) {
if !matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
// Remove 'y' and add `ies`
let mut chars = word.chars();
chars.next_back();
return format!("{}ies", chars.as_str());
}
}
}
// All other words will have "s" added to the end (eg. days).
format!("{word}s")
}
#[test]
fn test_to_plural_native() {
// Extracted from `swagger.json`
#[rustfmt::skip]
let native_kinds = vec![
("APIService", "apiservices"),
("Binding", "bindings"),
("CertificateSigningRequest", "certificatesigningrequests"),
("ClusterRole", "clusterroles"), ("ClusterRoleBinding", "clusterrolebindings"),
("ComponentStatus", "componentstatuses"),
("ConfigMap", "configmaps"),
("ControllerRevision", "controllerrevisions"),
("CronJob", "cronjobs"),
("CSIDriver", "csidrivers"), ("CSINode", "csinodes"), ("CSIStorageCapacity", "csistoragecapacities"),
("CustomResourceDefinition", "customresourcedefinitions"),
("DaemonSet", "daemonsets"),
("Deployment", "deployments"),
("Endpoints", "endpoints"), ("EndpointSlice", "endpointslices"),
("Event", "events"),
("FlowSchema", "flowschemas"),
("HorizontalPodAutoscaler", "horizontalpodautoscalers"),
("Ingress", "ingresses"), ("IngressClass", "ingressclasses"),
("Job", "jobs"),
("Lease", "leases"),
("LimitRange", "limitranges"),
("LocalSubjectAccessReview", "localsubjectaccessreviews"),
("MutatingWebhookConfiguration", "mutatingwebhookconfigurations"),
("Namespace", "namespaces"),
("NetworkPolicy", "networkpolicies"),
("Node", "nodes"),
("PersistentVolumeClaim", "persistentvolumeclaims"),
("PersistentVolume", "persistentvolumes"),
("PodDisruptionBudget", "poddisruptionbudgets"),
("Pod", "pods"),
("PodSecurityPolicy", "podsecuritypolicies"),
("PodTemplate", "podtemplates"),
("PriorityClass", "priorityclasses"),
("PriorityLevelConfiguration", "prioritylevelconfigurations"),
("ReplicaSet", "replicasets"),
("ReplicationController", "replicationcontrollers"),
("ResourceQuota", "resourcequotas"),
("Role", "roles"), ("RoleBinding", "rolebindings"),
("RuntimeClass", "runtimeclasses"),
("Secret", "secrets"),
("SelfSubjectAccessReview", "selfsubjectaccessreviews"),
("SelfSubjectRulesReview", "selfsubjectrulesreviews"),
("ServiceAccount", "serviceaccounts"),
("Service", "services"),
("StatefulSet", "statefulsets"),
("StorageClass", "storageclasses"), ("StorageVersion", "storageversions"),
("SubjectAccessReview", "subjectaccessreviews"),
("TokenReview", "tokenreviews"),
("ValidatingWebhookConfiguration", "validatingwebhookconfigurations"),
("VolumeAttachment", "volumeattachments"),
];
for (kind, plural) in native_kinds {
assert_eq!(to_plural(&kind.to_ascii_lowercase()), plural);
}
}

457
vendor/kube-core/src/duration.rs vendored Normal file
View File

@@ -0,0 +1,457 @@
//! Kubernetes [`Duration`]s.
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::{cmp::Ordering, fmt, str::FromStr, time};
/// A Kubernetes duration.
///
/// This is equivalent to the [`metav1.Duration`] type in the Go Kubernetes
/// apimachinery package. A [`metav1.Duration`] is serialized in YAML and JSON
/// as a string formatted in the format accepted by the Go standard library's
/// [`time.ParseDuration()`] function. This type is a similar wrapper around
/// Rust's [`std::time::Duration`] that can be serialized and deserialized using
/// the same format as `metav1.Duration`.
///
/// # On Signedness
///
/// Go's [`time.Duration`] type is a signed integer type, while Rust's
/// [`std::time::Duration`] is unsigned. Therefore, this type is also capable of
/// representing both positive and negative durations. This is implemented by
/// storing whether or not the parsed duration was negative as a boolean field
/// in the wrapper type. The [`Duration::is_negative`] method returns this
/// value, and when a [`Duration`] is serialized, the negative sign is included
/// if the duration is negative.
///
/// [`Duration`]s can be compared with [`std::time::Duration`]s. If the
/// [`Duration`] is negative, it will always be considered less than the
/// [`std::time::Duration`]. Similarly, because [`std::time::Duration`]s are
/// unsigned, a negative [`Duration`] will never be equal to a
/// [`std::time::Duration`], even if the wrapped [`std::time::Duration`] (the
/// negative duration's absolute value) is equal.
///
/// When converting a [`Duration`] into a [`std::time::Duration`], be aware that
/// *this information is lost*: if a negative [`Duration`] is converted into a
/// [`std::time::Duration`] and then that [`std::time::Duration`] is converted
/// back into a [`Duration`], the second [`Duration`] will *not* be negative.
///
/// [`metav1.Duration`]: https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration
/// [`time.Duration`]: https://pkg.go.dev/time#Duration
/// [`time.ParseDuration()`]: https://pkg.go.dev/time#ParseDuration
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Duration {
duration: time::Duration,
is_negative: bool,
}
/// Errors returned by the [`FromStr`] implementation for [`Duration`].
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum ParseError {
/// An invalid unit was provided. Units must be one of 'ns', 'us', 'μs',
/// 's', 'ms', 's', 'm', or 'h'.
#[error("invalid unit: {}", EXPECTED_UNITS)]
InvalidUnit,
/// No unit was provided.
#[error("missing a unit: {}", EXPECTED_UNITS)]
NoUnit,
/// The number associated with a given unit was invalid.
#[error("invalid floating-point number: {}", .0)]
NotANumber(#[from] std::num::ParseFloatError),
}
const EXPECTED_UNITS: &str = "expected one of 'ns', 'us', '\u{00b5}s', 'ms', 's', 'm', or 'h'";
impl From<time::Duration> for Duration {
fn from(duration: time::Duration) -> Self {
Self {
duration,
is_negative: false,
}
}
}
impl From<Duration> for time::Duration {
fn from(Duration { duration, .. }: Duration) -> Self {
duration
}
}
impl Duration {
/// Returns `true` if this `Duration` is negative.
#[inline]
#[must_use]
pub fn is_negative(&self) -> bool {
self.is_negative
}
}
impl fmt::Debug for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use std::fmt::Write;
if self.is_negative {
f.write_char('-')?;
}
fmt::Debug::fmt(&self.duration, f)
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use std::fmt::Write;
if self.is_negative {
f.write_char('-')?;
}
fmt::Debug::fmt(&self.duration, f)
}
}
impl FromStr for Duration {
type Err = ParseError;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
// implements the same format as
// https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/time/format.go;l=1589
const MINUTE: time::Duration = time::Duration::from_secs(60);
// Go durations are signed. Rust durations aren't.
let is_negative = s.starts_with('-');
s = s.trim_start_matches('+').trim_start_matches('-');
let mut total = time::Duration::from_secs(0);
while !s.is_empty() && s != "0" {
let unit_start = s.find(|c: char| c.is_alphabetic()).ok_or(ParseError::NoUnit)?;
let (val, rest) = s.split_at(unit_start);
let val = val.parse::<f64>()?;
let unit = if let Some(next_numeric_start) = rest.find(|c: char| !c.is_alphabetic()) {
let (unit, rest) = rest.split_at(next_numeric_start);
s = rest;
unit
} else {
s = "";
rest
};
// https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/time/format.go;l=1573
let base = match unit {
"ns" => time::Duration::from_nanos(1),
// U+00B5 is the "micro sign" while U+03BC is "Greek letter mu"
"us" | "\u{00b5}s" | "\u{03bc}s" => time::Duration::from_micros(1),
"ms" => time::Duration::from_millis(1),
"s" => time::Duration::from_secs(1),
"m" => MINUTE,
"h" => MINUTE * 60,
_ => return Err(ParseError::InvalidUnit),
};
total += base.mul_f64(val);
}
Ok(Duration {
duration: total,
is_negative,
})
}
}
impl Serialize for Duration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl de::Visitor<'_> for Visitor {
type Value = Duration;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a string in Go `time.Duration.String()` format")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let val = value.parse::<Duration>().map_err(de::Error::custom)?;
Ok(val)
}
}
deserializer.deserialize_str(Visitor)
}
}
impl PartialEq<time::Duration> for Duration {
fn eq(&self, other: &time::Duration) -> bool {
// Since `std::time::Duration` is unsigned, a negative `Duration` is
// never equal to a `std::time::Duration`.
if self.is_negative {
return false;
}
self.duration == *other
}
}
impl PartialEq<time::Duration> for &'_ Duration {
fn eq(&self, other: &time::Duration) -> bool {
// Since `std::time::Duration` is unsigned, a negative `Duration` is
// never equal to a `std::time::Duration`.
if self.is_negative {
return false;
}
self.duration == *other
}
}
impl PartialEq<Duration> for time::Duration {
fn eq(&self, other: &Duration) -> bool {
// Since `std::time::Duration` is unsigned, a negative `Duration` is
// never equal to a `std::time::Duration`.
if other.is_negative {
return false;
}
self == &other.duration
}
}
impl PartialEq<Duration> for &'_ time::Duration {
fn eq(&self, other: &Duration) -> bool {
// Since `std::time::Duration` is unsigned, a negative `Duration` is
// never equal to a `std::time::Duration`.
if other.is_negative {
return false;
}
*self == &other.duration
}
}
impl PartialOrd for Duration {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Duration {
fn cmp(&self, other: &Self) -> Ordering {
match (self.is_negative, other.is_negative) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
// if both durations are negative, the "higher" Duration value is
// actually the lower one
(true, true) => self.duration.cmp(&other.duration).reverse(),
(false, false) => self.duration.cmp(&other.duration),
}
}
}
impl PartialOrd<time::Duration> for Duration {
fn partial_cmp(&self, other: &time::Duration) -> Option<Ordering> {
// Since `std::time::Duration` is unsigned, a negative `Duration` is
// always less than the `std::time::Duration`.
if self.is_negative {
return Some(Ordering::Less);
}
self.duration.partial_cmp(other)
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for Duration {
// see
// https://github.com/kubernetes/apimachinery/blob/756e2227bf3a486098f504af1a0ffb736ad16f4c/pkg/apis/meta/v1/duration.go#L61
fn schema_name() -> String {
"Duration".to_owned()
}
fn is_referenceable() -> bool {
false
}
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
// the format should *not* be "duration", because "duration" means
// the duration is formatted in ISO 8601, as described here:
// https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7.3.1
format: None,
..Default::default()
}
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_same_as_go() {
const MINUTE: time::Duration = time::Duration::from_secs(60);
const HOUR: time::Duration = time::Duration::from_secs(60 * 60);
// from Go:
// https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/time/time_test.go;l=891-951
// ```
// var parseDurationTests = []struct {
// in string
// want time::Duration
// }{
let cases: &[(&str, Duration)] = &[
// // simple
// {"0", 0},
("0", time::Duration::from_secs(0).into()),
// {"5s", 5 * Second},
("5s", time::Duration::from_secs(5).into()),
// {"30s", 30 * Second},
("30s", time::Duration::from_secs(30).into()),
// {"1478s", 1478 * Second},
("1478s", time::Duration::from_secs(1478).into()),
// // sign
// {"-5s", -5 * Second},
("-5s", Duration {
duration: time::Duration::from_secs(5),
is_negative: true,
}),
// {"+5s", 5 * Second},
("+5s", time::Duration::from_secs(5).into()),
// {"-0", 0},
("-0", Duration {
duration: time::Duration::from_secs(0),
is_negative: true,
}),
// {"+0", 0},
("+0", time::Duration::from_secs(0).into()),
// // decimal
// {"5.0s", 5 * Second},
("5s", time::Duration::from_secs(5).into()),
// {"5.6s", 5*Second + 600*Millisecond},
(
"5.6s",
(time::Duration::from_secs(5) + time::Duration::from_millis(600)).into(),
),
// {"5.s", 5 * Second},
("5.s", time::Duration::from_secs(5).into()),
// {".5s", 500 * Millisecond},
(".5s", time::Duration::from_millis(500).into()),
// {"1.0s", 1 * Second},
("1.0s", time::Duration::from_secs(1).into()),
// {"1.00s", 1 * Second},
("1.00s", time::Duration::from_secs(1).into()),
// {"1.004s", 1*Second + 4*Millisecond},
(
"1.004s",
(time::Duration::from_secs(1) + time::Duration::from_millis(4)).into(),
),
// {"1.0040s", 1*Second + 4*Millisecond},
(
"1.0040s",
(time::Duration::from_secs(1) + time::Duration::from_millis(4)).into(),
),
// {"100.00100s", 100*Second + 1*Millisecond},
(
"100.00100s",
(time::Duration::from_secs(100) + time::Duration::from_millis(1)).into(),
),
// // different units
// {"10ns", 10 * Nanosecond},
("10ns", time::Duration::from_nanos(10).into()),
// {"11us", 11 * Microsecond},
("11us", time::Duration::from_micros(11).into()),
// {"12µs", 12 * Microsecond}, // U+00B5
("12µs", time::Duration::from_micros(12).into()),
// {"12μs", 12 * Microsecond}, // U+03BC
("12μs", time::Duration::from_micros(12).into()),
// {"13ms", 13 * Millisecond},
("13ms", time::Duration::from_millis(13).into()),
// {"14s", 14 * Second},
("14s", time::Duration::from_secs(14).into()),
// {"15m", 15 * Minute},
("15m", (15 * MINUTE).into()),
// {"16h", 16 * Hour},
("16h", (16 * HOUR).into()),
// // composite durations
// {"3h30m", 3*Hour + 30*Minute},
("3h30m", (3 * HOUR + 30 * MINUTE).into()),
// {"10.5s4m", 4*Minute + 10*Second + 500*Millisecond},
(
"10.5s4m",
(4 * MINUTE + time::Duration::from_secs(10) + time::Duration::from_millis(500)).into(),
),
// {"-2m3.4s", -(2*Minute + 3*Second + 400*Millisecond)},
("-2m3.4s", Duration {
duration: 2 * MINUTE + time::Duration::from_secs(3) + time::Duration::from_millis(400),
is_negative: true,
}),
// {"1h2m3s4ms5us6ns", 1*Hour + 2*Minute + 3*Second + 4*Millisecond + 5*Microsecond + 6*Nanosecond},
(
"1h2m3s4ms5us6ns",
(1 * HOUR
+ 2 * MINUTE
+ time::Duration::from_secs(3)
+ time::Duration::from_millis(4)
+ time::Duration::from_micros(5)
+ time::Duration::from_nanos(6))
.into(),
),
// {"39h9m14.425s", 39*Hour + 9*Minute + 14*Second + 425*Millisecond},
(
"39h9m14.425s",
(39 * HOUR + 9 * MINUTE + time::Duration::from_secs(14) + time::Duration::from_millis(425))
.into(),
),
// // large value
// {"52763797000ns", 52763797000 * Nanosecond},
("52763797000ns", time::Duration::from_nanos(52763797000).into()),
// // more than 9 digits after decimal point, see https://golang.org/issue/6617
// {"0.3333333333333333333h", 20 * Minute},
("0.3333333333333333333h", (20 * MINUTE).into()),
// // 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64
// {"9007199254740993ns", (1<<53 + 1) * Nanosecond},
(
"9007199254740993ns",
time::Duration::from_nanos((1 << 53) + 1).into(),
),
// Rust Durations can handle larger durations than Go's
// representation, so skip these tests for their precision limits
// // largest duration that can be represented by int64 in nanoseconds
// {"9223372036854775807ns", (1<<63 - 1) * Nanosecond},
// ("9223372036854775807ns", time::Duration::from_nanos((1 << 63) - 1).into()),
// {"9223372036854775.807us", (1<<63 - 1) * Nanosecond},
// ("9223372036854775.807us", time::Duration::from_nanos((1 << 63) - 1).into()),
// {"9223372036s854ms775us807ns", (1<<63 - 1) * Nanosecond},
// {"-9223372036854775808ns", -1 << 63 * Nanosecond},
// {"-9223372036854775.808us", -1 << 63 * Nanosecond},
// {"-9223372036s854ms775us808ns", -1 << 63 * Nanosecond},
// // largest negative value
// {"-9223372036854775808ns", -1 << 63 * Nanosecond},
// // largest negative round trip value, see https://golang.org/issue/48629
// {"-2562047h47m16.854775808s", -1 << 63 * Nanosecond},
// // huge string; issue 15011.
// {"0.100000000000000000000h", 6 * Minute},
("0.100000000000000000000h", (6 * MINUTE).into()), // // This value tests the first overflow check in leadingFraction.
// {"0.830103483285477580700h", 49*Minute + 48*Second + 372539827*Nanosecond},
// }
// ```
];
for (input, expected) in cases {
let parsed = dbg!(input).parse::<Duration>().unwrap();
assert_eq!(&dbg!(parsed), expected);
}
}
}

171
vendor/kube-core/src/dynamic.rs vendored Normal file
View File

@@ -0,0 +1,171 @@
//! Contains types for using resource kinds not known at compile-time.
//!
//! For concrete usage see [examples prefixed with dynamic_](https://github.com/kube-rs/kube/tree/main/examples).
pub use crate::discovery::ApiResource;
use crate::{
metadata::TypeMeta,
resource::{DynamicResourceScope, Resource},
};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use std::borrow::Cow;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("failed to parse this DynamicObject into a Resource: {source}")]
/// Failed to parse `DynamicObject` into `Resource`
pub struct ParseDynamicObjectError {
#[from]
source: serde_json::Error,
}
/// A dynamic representation of a kubernetes object
///
/// This will work with any non-list type object.
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
pub struct DynamicObject {
/// The type fields, not always present
#[serde(flatten, default)]
pub types: Option<TypeMeta>,
/// Object metadata
#[serde(default)]
pub metadata: ObjectMeta,
/// All other keys
#[serde(flatten)]
pub data: serde_json::Value,
}
impl DynamicObject {
/// Create a DynamicObject with minimal values set from ApiResource.
#[must_use]
pub fn new(name: &str, resource: &ApiResource) -> Self {
Self {
types: Some(TypeMeta {
api_version: resource.api_version.to_string(),
kind: resource.kind.to_string(),
}),
metadata: ObjectMeta {
name: Some(name.to_string()),
..Default::default()
},
data: Default::default(),
}
}
/// Attach dynamic data to a DynamicObject
#[must_use]
pub fn data(mut self, data: serde_json::Value) -> Self {
self.data = data;
self
}
/// Attach a namespace to a DynamicObject
#[must_use]
pub fn within(mut self, ns: &str) -> Self {
self.metadata.namespace = Some(ns.into());
self
}
/// Attempt to convert this `DynamicObject` to a `Resource`
pub fn try_parse<K: Resource + for<'a> serde::Deserialize<'a>>(
self,
) -> Result<K, ParseDynamicObjectError> {
Ok(serde_json::from_value(serde_json::to_value(self)?)?)
}
}
impl Resource for DynamicObject {
type DynamicType = ApiResource;
type Scope = DynamicResourceScope;
fn group(dt: &ApiResource) -> Cow<'_, str> {
dt.group.as_str().into()
}
fn version(dt: &ApiResource) -> Cow<'_, str> {
dt.version.as_str().into()
}
fn kind(dt: &ApiResource) -> Cow<'_, str> {
dt.kind.as_str().into()
}
fn api_version(dt: &ApiResource) -> Cow<'_, str> {
dt.api_version.as_str().into()
}
fn plural(dt: &ApiResource) -> Cow<'_, str> {
dt.plural.as_str().into()
}
fn meta(&self) -> &ObjectMeta {
&self.metadata
}
fn meta_mut(&mut self) -> &mut ObjectMeta {
&mut self.metadata
}
}
#[cfg(test)]
mod test {
use crate::{
dynamic::{ApiResource, DynamicObject},
gvk::GroupVersionKind,
params::{Patch, PatchParams, PostParams},
request::Request,
resource::Resource,
};
use k8s_openapi::api::core::v1::Pod;
#[test]
fn raw_custom_resource() {
let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo");
let res = ApiResource::from_gvk(&gvk);
let url = DynamicObject::url_path(&res, Some("myns"));
let pp = PostParams::default();
let req = Request::new(&url).create(&pp, vec![]).unwrap();
assert_eq!(req.uri(), "/apis/clux.dev/v1/namespaces/myns/foos?");
let patch_params = PatchParams::default();
let req = Request::new(url)
.patch("baz", &patch_params, &Patch::Merge(()))
.unwrap();
assert_eq!(req.uri(), "/apis/clux.dev/v1/namespaces/myns/foos/baz?");
assert_eq!(req.method(), "PATCH");
}
#[test]
fn raw_resource_in_default_group() {
let gvk = GroupVersionKind::gvk("", "v1", "Service");
let api_resource = ApiResource::from_gvk(&gvk);
let url = DynamicObject::url_path(&api_resource, None);
let pp = PostParams::default();
let req = Request::new(url).create(&pp, vec![]).unwrap();
assert_eq!(req.uri(), "/api/v1/services?");
}
#[test]
fn can_parse_dynamic_object_into_pod() -> Result<(), serde_json::Error> {
let original_pod: Pod = serde_json::from_value(serde_json::json!({
"apiVersion": "v1",
"kind": "Pod",
"metadata": { "name": "example" },
"spec": {
"containers": [{
"name": "example",
"image": "alpine",
// Do nothing
"command": ["tail", "-f", "/dev/null"],
}],
}
}))?;
let dynamic_pod: DynamicObject = serde_json::from_str(&serde_json::to_string(&original_pod)?)?;
let parsed_pod: Pod = dynamic_pod.try_parse().unwrap();
assert_eq!(parsed_pod, original_pod);
Ok(())
}
}

18
vendor/kube-core/src/error.rs vendored Normal file
View File

@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// An error response from the API.
#[derive(Error, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[error("{message}: {reason}")]
pub struct ErrorResponse {
/// The status
pub status: String,
/// A message about the error
#[serde(default)]
pub message: String,
/// The reason for the error
#[serde(default)]
pub reason: String,
/// The error code
pub code: u16,
}

145
vendor/kube-core/src/error_boundary.rs vendored Normal file
View File

@@ -0,0 +1,145 @@
//! Types for isolating deserialization failures. See [`DeserializeGuard`].
use std::borrow::Cow;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use serde::Deserialize;
use serde_value::DeserializerError;
use thiserror::Error;
use crate::{PartialObjectMeta, Resource};
/// A wrapper type for K that lets deserializing the parent object succeed, even if the K is invalid.
///
/// For example, this can be used to still access valid objects from an `Api::list` call or `watcher`.
// We can't implement Deserialize on Result<K, InvalidObject> directly, both because of the orphan rule and because
// it would conflict with serde's blanket impl on Result<K, E>, even if E isn't Deserialize.
#[derive(Debug, Clone)]
pub struct DeserializeGuard<K>(pub Result<K, InvalidObject>);
/// An object that failed to be deserialized by the [`DeserializeGuard`].
#[derive(Debug, Clone, Error)]
#[error("{error}")]
pub struct InvalidObject {
// Should ideally be D::Error, but we don't know what type it has outside of Deserialize::deserialize()
// It *could* be Box<std::error::Error>, but we don't know that it is Send+Sync
/// The error message from deserializing the object.
pub error: String,
/// The metadata of the invalid object.
pub metadata: ObjectMeta,
}
impl<'de, K> Deserialize<'de> for DeserializeGuard<K>
where
K: Deserialize<'de>,
// Not actually used, but we assume that K is a Kubernetes-style resource with a `metadata` section
K: Resource,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// Deserialize::deserialize consumes the deserializer, and we want to retry parsing as an ObjectMetaContainer
// if the initial parse fails, so that we can still implement Resource for the error case
let buffer = serde_value::Value::deserialize(deserializer)?;
// FIXME: can we avoid cloning the whole object? metadata should be enough, and even then we could prune managedFields
K::deserialize(buffer.clone())
.map(Ok)
.or_else(|err| {
let PartialObjectMeta { metadata, .. } =
PartialObjectMeta::<K>::deserialize(buffer).map_err(DeserializerError::into_error)?;
Ok(Err(InvalidObject {
error: err.to_string(),
metadata,
}))
})
.map(DeserializeGuard)
}
}
impl<K: Resource> Resource for DeserializeGuard<K> {
type DynamicType = K::DynamicType;
type Scope = K::Scope;
fn kind(dt: &Self::DynamicType) -> Cow<str> {
K::kind(dt)
}
fn group(dt: &Self::DynamicType) -> Cow<str> {
K::group(dt)
}
fn version(dt: &Self::DynamicType) -> Cow<str> {
K::version(dt)
}
fn plural(dt: &Self::DynamicType) -> Cow<str> {
K::plural(dt)
}
fn meta(&self) -> &ObjectMeta {
self.0.as_ref().map_or_else(|err| &err.metadata, K::meta)
}
fn meta_mut(&mut self) -> &mut ObjectMeta {
self.0.as_mut().map_or_else(|err| &mut err.metadata, K::meta_mut)
}
}
#[cfg(test)]
mod tests {
use k8s_openapi::api::core::v1::{ConfigMap, Pod};
use serde_json::json;
use crate::{DeserializeGuard, Resource};
#[test]
fn should_parse_meta_of_invalid_objects() {
let pod_error = serde_json::from_value::<DeserializeGuard<Pod>>(json!({
"metadata": {
"name": "the-name",
"namespace": "the-namespace",
},
"spec": {
"containers": "not-a-list",
},
}))
.unwrap();
assert_eq!(pod_error.meta().name.as_deref(), Some("the-name"));
assert_eq!(pod_error.meta().namespace.as_deref(), Some("the-namespace"));
pod_error.0.unwrap_err();
}
#[test]
fn should_allow_valid_objects() {
let configmap = serde_json::from_value::<DeserializeGuard<ConfigMap>>(json!({
"metadata": {
"name": "the-name",
"namespace": "the-namespace",
},
"data": {
"foo": "bar",
},
}))
.unwrap();
assert_eq!(configmap.meta().name.as_deref(), Some("the-name"));
assert_eq!(configmap.meta().namespace.as_deref(), Some("the-namespace"));
assert_eq!(
configmap.0.unwrap().data,
Some([("foo".to_string(), "bar".to_string())].into())
)
}
#[test]
fn should_catch_invalid_objects() {
serde_json::from_value::<DeserializeGuard<Pod>>(json!({
"spec": {
"containers": "not-a-list"
}
}))
.unwrap()
.0
.unwrap_err();
}
}

196
vendor/kube-core/src/gvk.rs vendored Normal file
View File

@@ -0,0 +1,196 @@
//! Type information structs for dynamic resources.
use std::str::FromStr;
use crate::TypeMeta;
use k8s_openapi::{api::core::v1::ObjectReference, apimachinery::pkg::apis::meta::v1::OwnerReference};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("failed to parse group version: {0}")]
/// Failed to parse group version
pub struct ParseGroupVersionError(pub String);
/// Core information about an API Resource.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct GroupVersionKind {
/// API group
pub group: String,
/// Version
pub version: String,
/// Kind
pub kind: String,
}
impl GroupVersionKind {
/// Construct from explicit group, version, and kind
pub fn gvk(group_: &str, version_: &str, kind_: &str) -> Self {
let version = version_.to_string();
let group = group_.to_string();
let kind = kind_.to_string();
Self { group, version, kind }
}
}
impl TryFrom<&TypeMeta> for GroupVersionKind {
type Error = ParseGroupVersionError;
fn try_from(tm: &TypeMeta) -> Result<Self, Self::Error> {
Ok(GroupVersion::from_str(&tm.api_version)?.with_kind(&tm.kind))
}
}
impl TryFrom<TypeMeta> for GroupVersionKind {
type Error = ParseGroupVersionError;
fn try_from(tm: TypeMeta) -> Result<Self, Self::Error> {
Ok(GroupVersion::from_str(&tm.api_version)?.with_kind(&tm.kind))
}
}
impl From<OwnerReference> for GroupVersionKind {
fn from(value: OwnerReference) -> Self {
let (group, version) = match value.api_version.split_once("/") {
Some((group, version)) => (group, version),
None => ("", value.api_version.as_str()),
};
Self {
group: group.into(),
version: version.into(),
kind: value.kind,
}
}
}
impl From<ObjectReference> for GroupVersionKind {
fn from(value: ObjectReference) -> Self {
let api_version = value.api_version.unwrap_or_default();
let (group, version) = match api_version.split_once("/") {
Some((group, version)) => (group, version),
None => ("", api_version.as_str()),
};
Self {
group: group.into(),
version: version.into(),
kind: value.kind.unwrap_or_default(),
}
}
}
/// Core information about a family of API Resources
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct GroupVersion {
/// API group
pub group: String,
/// Version
pub version: String,
}
impl GroupVersion {
/// Construct from explicit group and version
pub fn gv(group_: &str, version_: &str) -> Self {
let version = version_.to_string();
let group = group_.to_string();
Self { group, version }
}
/// Upgrade a GroupVersion to a GroupVersionKind
pub fn with_kind(self, kind: &str) -> GroupVersionKind {
GroupVersionKind {
group: self.group,
version: self.version,
kind: kind.into(),
}
}
}
impl FromStr for GroupVersion {
type Err = ParseGroupVersionError;
fn from_str(gv: &str) -> Result<Self, Self::Err> {
let gvsplit = gv.splitn(2, '/').collect::<Vec<_>>();
let (group, version) = match *gvsplit.as_slice() {
[g, v] => (g.to_string(), v.to_string()), // standard case
[v] => ("".to_string(), v.to_string()), // core v1 case
_ => return Err(ParseGroupVersionError(gv.into())),
};
Ok(Self { group, version })
}
}
impl GroupVersion {
/// Generate the apiVersion string used in a kind's yaml
pub fn api_version(&self) -> String {
if self.group.is_empty() {
self.version.clone()
} else {
format!("{}/{}", self.group, self.version)
}
}
}
impl GroupVersionKind {
/// Generate the apiVersion string used in a kind's yaml
pub fn api_version(&self) -> String {
if self.group.is_empty() {
self.version.clone()
} else {
format!("{}/{}", self.group, self.version)
}
}
}
/// Represents a type-erased object resource.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct GroupVersionResource {
/// API group
pub group: String,
/// Version
pub version: String,
/// Resource
pub resource: String,
/// Concatenation of group and version
#[serde(default)]
api_version: String,
}
impl GroupVersionResource {
/// Set the api group, version, and the plural resource name.
pub fn gvr(group_: &str, version_: &str, resource_: &str) -> Self {
let version = version_.to_string();
let group = group_.to_string();
let resource = resource_.to_string();
let api_version = if group.is_empty() {
version.to_string()
} else {
format!("{group}/{version}")
};
Self {
group,
version,
resource,
api_version,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn gvk_yaml() {
use crate::{GroupVersionKind, TypeMeta};
let input = r#"---
apiVersion: kube.rs/v1
kind: Example
metadata:
name: doc1
"#;
let tm: TypeMeta = serde_yaml::from_str(input).unwrap();
let gvk = GroupVersionKind::try_from(&tm).unwrap(); // takes ref
let gvk2: GroupVersionKind = tm.try_into().unwrap(); // takes value
assert_eq!(gvk.kind, "Example");
assert_eq!(gvk.group, "kube.rs");
assert_eq!(gvk.version, "v1");
assert_eq!(gvk.kind, gvk2.kind);
}
}

244
vendor/kube-core/src/kubelet_debug.rs vendored Normal file
View File

@@ -0,0 +1,244 @@
//! Node proxy methods
use crate::{
request::Error,
subresource::{AttachParams, LogParams},
Request,
};
use std::fmt::Debug;
/// Struct that hold all required parameters to call specific pod methods from node
#[derive(Default)]
pub struct KubeletDebugParams<'a> {
/// Name of the pod
pub name: &'a str,
/// Namespace of the pod
pub namespace: &'a str,
/// Pod uid used to ensure that the pod name matches the pod uid
pub pod_uid: Option<&'a str>,
}
impl KubeletDebugParams<'_> {
fn with_uid(&self) -> String {
if let Some(uid) = &self.pod_uid {
format!("{}/{}/{}", self.namespace, self.name, uid)
} else {
self.without_uid()
}
}
fn without_uid(&self) -> String {
format!("{}/{}", self.namespace, self.name)
}
}
impl Request {
/// Attach to pod directly from the node
pub fn kubelet_node_attach(
kubelet_debug_params: &KubeletDebugParams<'_>,
container: &str,
ap: &AttachParams,
) -> Result<http::Request<Vec<u8>>, Error> {
ap.validate()?;
let target = format!("/attach/{}/{container}?", kubelet_debug_params.with_uid());
let mut qp = form_urlencoded::Serializer::new(target);
ap.append_to_url_serializer_local(&mut qp);
let req = http::Request::get(qp.finish());
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Execute a command in a pod directly from the node
pub fn kubelet_node_exec<I, T>(
kubelet_debug_params: &KubeletDebugParams<'_>,
container: &str,
command: I,
ap: &AttachParams,
) -> Result<http::Request<Vec<u8>>, Error>
where
I: IntoIterator<Item = T> + Debug,
T: Into<String>,
{
ap.validate()?;
let target = format!("/exec/{}/{container}?", kubelet_debug_params.with_uid());
let mut qp = form_urlencoded::Serializer::new(target);
ap.append_to_url_serializer_local(&mut qp);
for c in command.into_iter() {
qp.append_pair("command", &c.into());
}
let req = http::Request::get(qp.finish());
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Forward ports of a pod directly from the node
pub fn kubelet_node_portforward(
kubelet_debug_params: &KubeletDebugParams<'_>,
ports: &[u16],
) -> Result<http::Request<Vec<u8>>, Error> {
if ports.is_empty() {
return Err(Error::Validation("ports cannot be empty".into()));
}
if ports.len() > 128 {
return Err(Error::Validation(
"the number of ports cannot be more than 128".into(),
));
}
if ports.len() > 1 {
let mut seen = std::collections::HashSet::with_capacity(ports.len());
for port in ports.iter() {
if seen.contains(port) {
return Err(Error::Validation(format!(
"ports must be unique, found multiple {port}"
)));
}
seen.insert(port);
}
}
let base_url = format!("/portForward/{}?", kubelet_debug_params.with_uid());
let mut qp = form_urlencoded::Serializer::new(base_url);
qp.append_pair(
"port",
&ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(","),
);
let req = http::Request::get(qp.finish());
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Stream logs directly from node
pub fn kubelet_node_logs(
kubelet_debug_params: &KubeletDebugParams<'_>,
container: &str,
lp: &LogParams,
) -> Result<http::Request<Vec<u8>>, Error> {
// Node logs is the only one that doesn't accept an uid for pod
let target = format!(
"/containerLogs/{}/{container}?",
kubelet_debug_params.without_uid()
);
let mut qp = form_urlencoded::Serializer::new(target);
if lp.follow {
qp.append_pair("follow", "true");
}
if let Some(lb) = &lp.limit_bytes {
qp.append_pair("limitBytes", &lb.to_string());
}
if lp.pretty {
qp.append_pair("pretty", "true");
}
if lp.previous {
qp.append_pair("previous", "true");
}
if let Some(ss) = &lp.since_seconds {
qp.append_pair("sinceSeconds", &ss.to_string());
} else if let Some(st) = &lp.since_time {
let ser_since = st.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
qp.append_pair("sinceTime", &ser_since);
}
if let Some(tl) = &lp.tail_lines {
qp.append_pair("tailLines", &tl.to_string());
}
if lp.timestamps {
qp.append_pair("timestamps", "true");
}
let urlstr = qp.finish();
let req = http::Request::get(urlstr);
req.body(vec![]).map_err(Error::BuildRequest)
}
}
#[cfg(test)]
mod test {
use crate::{
kubelet_debug::KubeletDebugParams,
subresource::{AttachParams, LogParams},
Request,
};
#[test]
fn node_attach_test() {
let req = Request::kubelet_node_attach(
&KubeletDebugParams {
name: "some-name",
namespace: "some-namespace",
pod_uid: Some("some-uid"),
},
"some-container",
&AttachParams::default().stdin(true).stderr(true).stdout(true),
)
.unwrap();
assert_eq!(
req.uri(),
"/attach/some-namespace/some-name/some-uid/some-container?&input=1&output=1&error=1"
);
}
#[test]
fn node_exec_test() {
let req = Request::kubelet_node_exec(
&KubeletDebugParams {
name: "some-name",
namespace: "some-namespace",
pod_uid: None,
},
"some-container",
"ls -l".split_whitespace(),
&AttachParams::interactive_tty(),
)
.unwrap();
assert_eq!(
req.uri(),
"/exec/some-namespace/some-name/some-container?&input=1&output=1&tty=1&command=ls&command=-l"
);
}
#[test]
fn node_logs_test() {
let lp = LogParams {
tail_lines: Some(10),
follow: true,
timestamps: true,
..Default::default()
};
let req = Request::kubelet_node_logs(
&KubeletDebugParams {
name: "some-name",
namespace: "some-namespace",
pod_uid: None,
},
"some-container",
&lp,
)
.unwrap();
assert_eq!(
req.uri(),
"/containerLogs/some-namespace/some-name/some-container?&follow=true&tailLines=10&timestamps=true"
);
}
#[test]
fn node_portforward_test() {
let req = Request::kubelet_node_portforward(
&KubeletDebugParams {
name: "some-name",
namespace: "some-namespace",
pod_uid: None,
},
&[1204],
)
.unwrap();
assert_eq!(req.uri(), "/portForward/some-namespace/some-name?&port=1204");
}
}

630
vendor/kube-core/src/labels.rs vendored Normal file
View File

@@ -0,0 +1,630 @@
//! Type safe label selector logic
use core::fmt;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, LabelSelectorRequirement};
use serde::{Deserialize, Serialize};
use std::{
cmp::PartialEq,
collections::{BTreeMap, BTreeSet},
fmt::Display,
iter::FromIterator,
option::IntoIter,
};
use thiserror::Error;
mod private {
pub trait Sealed {}
impl Sealed for super::Expression {}
impl Sealed for super::Selector {}
}
#[derive(Debug, Error)]
#[error("failed to parse value as expression: {0}")]
/// Indicates failure of conversion to Expression
pub struct ParseExpressionError(pub String);
// local type aliases
type Expressions = Vec<Expression>;
/// Selector extension trait for querying selector-like objects
pub trait SelectorExt: private::Sealed {
/// Collection type to compare with self
type Search;
/// Perform a match check on the arbitrary components like labels
///
/// ```
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
/// use kube::core::{SelectorExt, Selector};
/// # use std::collections::BTreeMap;
///
/// let selector: Selector = LabelSelector::default().try_into()?;
/// let search = BTreeMap::from([("app".to_string(), "myapp".to_string())]);
/// selector.matches(&search);
/// # Ok::<(), kube_core::ParseExpressionError>(())
/// ```
fn matches(&self, on: &Self::Search) -> bool;
}
/// A selector expression with existing operations
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum Expression {
/// Key exists and in set:
///
/// ```
/// # use kube_core::Expression;
/// let exp = Expression::In("foo".into(), ["bar".into(), "baz".into()].into());
/// assert_eq!(exp.to_string(), "foo in (bar,baz)");
/// let exp = Expression::In("foo".into(), ["bar".into(), "baz".into()].into_iter().collect());
/// assert_eq!(exp.to_string(), "foo in (bar,baz)");
/// ```
In(String, BTreeSet<String>),
/// Key does not exists or not in set:
///
/// ```
/// # use kube_core::Expression;
/// let exp = Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into());
/// assert_eq!(exp.to_string(), "foo notin (bar,baz)");
/// let exp = Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into_iter().collect());
/// assert_eq!(exp.to_string(), "foo notin (bar,baz)");
/// ```
NotIn(String, BTreeSet<String>),
/// Key exists and is equal:
///
/// ```
/// # use kube_core::Expression;
/// let exp = Expression::Equal("foo".into(), "bar".into());
/// assert_eq!(exp.to_string(), "foo=bar")
/// ```
Equal(String, String),
/// Key does not exists or is not equal:
///
/// ```
/// # use kube_core::Expression;
/// let exp = Expression::NotEqual("foo".into(), "bar".into());
/// assert_eq!(exp.to_string(), "foo!=bar")
/// ```
NotEqual(String, String),
/// Key exists:
///
/// ```
/// # use kube_core::Expression;
/// let exp = Expression::Exists("foo".into());
/// assert_eq!(exp.to_string(), "foo")
/// ```
Exists(String),
/// Key does not exist:
///
/// ```
/// # use kube_core::Expression;
/// let exp = Expression::DoesNotExist("foo".into());
/// assert_eq!(exp.to_string(), "!foo")
/// ```
DoesNotExist(String),
}
/// Perform selection on a list of expressions
///
/// Can be injected into [`WatchParams`](crate::params::WatchParams::labels_from) or [`ListParams`](crate::params::ListParams::labels_from).
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Serialize)]
pub struct Selector(Expressions);
impl Selector {
/// Create a selector from a vector of expressions
fn from_expressions(exprs: Expressions) -> Self {
Self(exprs)
}
/// Create a selector from a map of key=value label matches
fn from_map(map: BTreeMap<String, String>) -> Self {
Self(map.into_iter().map(|(k, v)| Expression::Equal(k, v)).collect())
}
/// Indicates whether this label selector matches everything
pub fn selects_all(&self) -> bool {
self.0.is_empty()
}
/// Extend the list of expressions for the selector
///
/// ```
/// use kube::core::{Selector, Expression};
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
///
/// let mut selector = Selector::default();
///
/// // Extend from expressions:
/// selector.extend(Expression::Equal("environment".into(), "production".into()));
/// selector.extend([Expression::Exists("bar".into()), Expression::Exists("foo".into())].into_iter());
///
/// // Extend from native selectors:
/// let label_selector: Selector = LabelSelector::default().try_into()?;
/// selector.extend(label_selector);
/// # Ok::<(), kube_core::ParseExpressionError>(())
/// ```
pub fn extend(&mut self, exprs: impl IntoIterator<Item = Expression>) -> &mut Self {
self.0.extend(exprs);
self
}
}
impl SelectorExt for Selector {
type Search = BTreeMap<String, String>;
/// Perform a match check on the resource labels
fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
for expr in self.0.iter() {
if !expr.matches(labels) {
return false;
}
}
true
}
}
impl SelectorExt for Expression {
type Search = BTreeMap<String, String>;
fn matches(&self, labels: &BTreeMap<String, String>) -> bool {
match self {
Expression::In(key, values) => match labels.get(key) {
Some(v) => values.contains(v),
None => false,
},
Expression::NotIn(key, values) => match labels.get(key) {
Some(v) => !values.contains(v),
None => true,
},
Expression::Exists(key) => labels.contains_key(key),
Expression::DoesNotExist(key) => !labels.contains_key(key),
Expression::Equal(key, value) => labels.get(key) == Some(value),
Expression::NotEqual(key, value) => labels.get(key) != Some(value),
}
}
}
impl Display for Expression {
/// Perform conversion to string
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expression::In(key, values) => {
write!(
f,
"{key} in ({})",
values.iter().cloned().collect::<Vec<_>>().join(",")
)
}
Expression::NotIn(key, values) => {
write!(
f,
"{key} notin ({})",
values.iter().cloned().collect::<Vec<_>>().join(",")
)
}
Expression::Equal(key, value) => write!(f, "{key}={value}"),
Expression::NotEqual(key, value) => write!(f, "{key}!={value}"),
Expression::Exists(key) => write!(f, "{key}"),
Expression::DoesNotExist(key) => write!(f, "!{key}"),
}
}
}
impl Display for Selector {
/// Convert a selector to a string for the API
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let selectors: Vec<String> = self.0.iter().map(|e| e.to_string()).collect();
write!(f, "{}", selectors.join(","))
}
}
// convenience conversions for Selector and Expression
impl IntoIterator for Expression {
type IntoIter = IntoIter<Self::Item>;
type Item = Self;
fn into_iter(self) -> Self::IntoIter {
Some(self).into_iter()
}
}
impl IntoIterator for Selector {
type IntoIter = std::vec::IntoIter<Self::Item>;
type Item = Expression;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<(String, String)> for Selector {
fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
Self::from_map(iter.into_iter().collect())
}
}
impl FromIterator<(&'static str, &'static str)> for Selector {
/// ```
/// use kube_core::{Selector, Expression};
///
/// let sel: Selector = Some(("foo", "bar")).into_iter().collect();
/// let equal: Selector = Expression::Equal("foo".into(), "bar".into()).into();
/// assert_eq!(sel, equal)
/// ```
fn from_iter<T: IntoIterator<Item = (&'static str, &'static str)>>(iter: T) -> Self {
Self::from_map(
iter.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
}
}
impl FromIterator<Expression> for Selector {
fn from_iter<T: IntoIterator<Item = Expression>>(iter: T) -> Self {
Self::from_expressions(iter.into_iter().collect())
}
}
impl From<Expression> for Selector {
fn from(value: Expression) -> Self {
Self(vec![value])
}
}
impl TryFrom<LabelSelector> for Selector {
type Error = ParseExpressionError;
fn try_from(value: LabelSelector) -> Result<Self, Self::Error> {
let expressions = match value.match_expressions {
Some(requirements) => requirements.into_iter().map(TryInto::try_into).collect(),
None => Ok(vec![]),
}?;
let mut equality: Selector = value
.match_labels
.map(|labels| labels.into_iter().collect())
.unwrap_or_default();
equality.extend(expressions);
Ok(equality)
}
}
impl TryFrom<LabelSelectorRequirement> for Expression {
type Error = ParseExpressionError;
fn try_from(requirement: LabelSelectorRequirement) -> Result<Self, Self::Error> {
let key = requirement.key;
let values = requirement.values.map(|values| values.into_iter().collect());
match requirement.operator.as_str() {
"In" => match values {
Some(values) => Ok(Expression::In(key, values)),
None => Err(ParseExpressionError(
"Expected values for In operator, got none".into(),
)),
},
"NotIn" => match values {
Some(values) => Ok(Expression::NotIn(key, values)),
None => Err(ParseExpressionError(
"Expected values for In operator, got none".into(),
)),
},
"Exists" => Ok(Expression::Exists(key)),
"DoesNotExist" => Ok(Expression::DoesNotExist(key)),
_ => Err(ParseExpressionError("Invalid expression operator".into())),
}
}
}
impl From<Selector> for LabelSelector {
fn from(value: Selector) -> Self {
let mut equality = vec![];
let mut expressions = vec![];
for expr in value.0 {
match expr {
Expression::In(key, values) => expressions.push(LabelSelectorRequirement {
key,
operator: "In".into(),
values: Some(values.into_iter().collect()),
}),
Expression::NotIn(key, values) => expressions.push(LabelSelectorRequirement {
key,
operator: "NotIn".into(),
values: Some(values.into_iter().collect()),
}),
Expression::Equal(key, value) => equality.push((key, value)),
Expression::NotEqual(key, value) => expressions.push(LabelSelectorRequirement {
key,
operator: "NotIn".into(),
values: Some(vec![value]),
}),
Expression::Exists(key) => expressions.push(LabelSelectorRequirement {
key,
operator: "Exists".into(),
values: None,
}),
Expression::DoesNotExist(key) => expressions.push(LabelSelectorRequirement {
key,
operator: "DoesNotExist".into(),
values: None,
}),
}
}
LabelSelector {
match_labels: (!equality.is_empty()).then_some(equality.into_iter().collect()),
match_expressions: (!expressions.is_empty()).then_some(expressions),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::iter::FromIterator;
#[test]
fn test_raw_matches() {
for (selector, label_selector, labels, matches, msg) in &[
(
Selector::default(),
LabelSelector::default(),
Default::default(),
true,
"empty match",
),
(
Selector::from_iter(Some(("foo", "bar"))),
LabelSelector {
match_labels: Some([("foo".into(), "bar".into())].into()),
match_expressions: Default::default(),
},
[("foo".to_string(), "bar".to_string())].into(),
true,
"exact label match",
),
(
Selector::from_iter(Some(("foo", "bar"))),
LabelSelector {
match_labels: Some([("foo".to_string(), "bar".to_string())].into()),
match_expressions: None,
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
true,
"sufficient label match",
),
(
Selector::from_iter(Some(Expression::In(
"foo".into(),
Some("bar".to_string()).into_iter().collect(),
))),
LabelSelector {
match_labels: None,
match_expressions: Some(vec![LabelSelectorRequirement {
key: "foo".into(),
operator: "In".to_string(),
values: Some(vec!["bar".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
true,
"In expression match",
),
(
Selector::from_iter(Some(Expression::Equal(
"foo".into(),
Some("bar".to_string()).into_iter().collect(),
))),
LabelSelector {
match_labels: Some([("foo".into(), "bar".into())].into()),
match_expressions: None,
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
true,
"Equal expression match",
),
(
Selector::from_iter(Some(Expression::NotEqual(
"foo".into(),
Some("bar".to_string()).into_iter().collect(),
))),
LabelSelector {
match_labels: None,
match_expressions: Some(vec![LabelSelectorRequirement {
key: "foo".into(),
operator: "NotIn".into(),
values: Some(vec!["bar".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
false,
"NotEqual expression match",
),
(
Selector::from_iter(Some(Expression::In(
"foo".into(),
Some("bar".to_string()).into_iter().collect(),
))),
LabelSelector {
match_labels: None,
match_expressions: Some(vec![LabelSelectorRequirement {
key: "foo".into(),
operator: "In".into(),
values: Some(vec!["bar".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
true,
"In expression match",
),
(
Selector::from_iter(Some(Expression::NotIn(
"foo".into(),
Some("quux".to_string()).into_iter().collect(),
))),
LabelSelector {
match_labels: None,
match_expressions: Some(vec![LabelSelectorRequirement {
key: "foo".into(),
operator: "NotIn".into(),
values: Some(vec!["quux".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
true,
"NotIn expression match",
),
(
Selector::from_iter(Some(Expression::NotIn(
"foo".into(),
Some("bar".to_string()).into_iter().collect(),
))),
LabelSelector {
match_labels: None,
match_expressions: Some(vec![LabelSelectorRequirement {
key: "foo".into(),
operator: "NotIn".into(),
values: Some(vec!["bar".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
false,
"NotIn expression non-match",
),
(
Selector(vec![
Expression::Equal("foo".to_string(), "bar".to_string()),
Expression::In("bah".into(), Some("bar".to_string()).into_iter().collect()),
]),
LabelSelector {
match_labels: Some([("foo".into(), "bar".into())].into()),
match_expressions: Some(vec![LabelSelectorRequirement {
key: "bah".into(),
operator: "In".into(),
values: Some(vec!["bar".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "baz".to_string()),
]
.into(),
false,
"matches labels but not expressions",
),
(
Selector(vec![
Expression::Equal("foo".to_string(), "bar".to_string()),
Expression::In("bah".into(), Some("bar".to_string()).into_iter().collect()),
]),
LabelSelector {
match_labels: Some([("foo".into(), "bar".into())].into()),
match_expressions: Some(vec![LabelSelectorRequirement {
key: "bah".into(),
operator: "In".into(),
values: Some(vec!["bar".into()]),
}]),
},
[
("foo".to_string(), "bar".to_string()),
("bah".to_string(), "bar".to_string()),
]
.into(),
true,
"matches both labels and expressions",
),
] {
assert_eq!(selector.matches(labels), *matches, "{}", msg);
let converted: LabelSelector = selector.clone().into();
assert_eq!(&converted, label_selector);
let converted_selector: Selector = converted.try_into().unwrap();
assert_eq!(
converted_selector.matches(labels),
*matches,
"After conversion: {}",
msg
);
}
}
#[test]
fn test_label_selector_matches() {
let selector: Selector = LabelSelector {
match_expressions: Some(vec![
LabelSelectorRequirement {
key: "foo".into(),
operator: "In".into(),
values: Some(vec!["bar".into()]),
},
LabelSelectorRequirement {
key: "foo".into(),
operator: "NotIn".into(),
values: Some(vec!["baz".into()]),
},
LabelSelectorRequirement {
key: "foo".into(),
operator: "Exists".into(),
values: None,
},
LabelSelectorRequirement {
key: "baz".into(),
operator: "DoesNotExist".into(),
values: None,
},
]),
match_labels: Some([("foo".into(), "bar".into())].into()),
}
.try_into()
.unwrap();
assert!(selector.matches(&[("foo".into(), "bar".into())].into()));
assert!(!selector.matches(&Default::default()));
}
#[test]
fn test_to_string() {
let selector = Selector(vec![
Expression::In("foo".into(), ["bar".into(), "baz".into()].into()),
Expression::NotIn("foo".into(), ["bar".into(), "baz".into()].into()),
Expression::Equal("foo".into(), "bar".into()),
Expression::NotEqual("foo".into(), "bar".into()),
Expression::Exists("foo".into()),
Expression::DoesNotExist("foo".into()),
])
.to_string();
assert_eq!(
selector,
"foo in (bar,baz),foo notin (bar,baz),foo=bar,foo!=bar,foo,!foo"
)
}
}

81
vendor/kube-core/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,81 @@
//! Types and traits necessary for interacting with the Kubernetes API
//!
//! This crate provides the minimal apimachinery necessary to make requests to the kubernetes API.
//!
//! It does not export export a client, but it also has almost no dependencies.
//!
//! Everything in this crate is re-exported from [`kube`](https://crates.io/crates/kube)
//! (even with zero features) under [`kube::core`]((https://docs.rs/kube/*/kube/core/index.html)).
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg_attr(docsrs, doc(cfg(feature = "admission")))]
#[cfg(feature = "admission")]
pub mod admission;
pub mod conversion;
pub mod discovery;
pub mod duration;
pub use duration::Duration;
pub mod dynamic;
pub use dynamic::{ApiResource, DynamicObject};
pub mod crd;
pub use crd::CustomResourceExt;
pub mod cel;
pub use cel::{Message, Reason, Rule};
#[cfg(feature = "schema")]
pub use cel::{merge_properties, validate, validate_property};
pub mod gvk;
pub use gvk::{GroupVersion, GroupVersionKind, GroupVersionResource};
pub mod metadata;
pub use metadata::{ListMeta, ObjectMeta, PartialObjectMeta, PartialObjectMetaExt, TypeMeta};
pub mod labels;
#[cfg(feature = "kubelet-debug")] pub mod kubelet_debug;
pub mod object;
pub use object::{NotUsed, Object, ObjectList};
pub mod params;
pub mod request;
pub use request::Request;
mod resource;
pub use resource::{
api_version_from_group_version, ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope,
Resource, ResourceExt, ResourceScope, SubResourceScope,
};
pub mod response;
pub use response::Status;
pub use labels::{Expression, ParseExpressionError, Selector, SelectorExt};
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
#[cfg(feature = "schema")]
pub mod schema;
pub mod subresource;
pub mod util;
pub mod watch;
pub use watch::WatchEvent;
mod error;
pub use error::ErrorResponse;
mod version;
pub use version::Version;
pub mod error_boundary;
pub use error_boundary::DeserializeGuard;

210
vendor/kube-core/src/metadata.rs vendored Normal file
View File

@@ -0,0 +1,210 @@
//! Metadata structs used in traits, lists, and dynamic objects.
use std::{borrow::Cow, marker::PhantomData};
pub use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ListMeta, ObjectMeta};
use serde::{Deserialize, Serialize};
use crate::{DynamicObject, Resource};
/// Type information that is flattened into every kubernetes object
#[derive(Deserialize, Serialize, Clone, Default, Debug, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TypeMeta {
/// The version of the API
pub api_version: String,
/// The name of the API
pub kind: String,
}
impl TypeMeta {
/// Construct a new `TypeMeta` for the object list from the given resource.
///
/// ```
/// # use k8s_openapi::api::core::v1::Pod;
/// # use kube_core::TypeMeta;
///
/// let type_meta = TypeMeta::list::<Pod>();
/// assert_eq!(type_meta.kind, "PodList");
/// assert_eq!(type_meta.api_version, "v1");
/// ```
pub fn list<K: Resource<DynamicType = ()>>() -> Self {
TypeMeta {
api_version: K::api_version(&()).into(),
kind: K::kind(&()).to_string() + "List",
}
}
/// Construct a new `TypeMeta` for the object from the given resource.
///
/// ```
/// # use k8s_openapi::api::core::v1::Pod;
/// # use kube_core::TypeMeta;
///
/// let type_meta = TypeMeta::resource::<Pod>();
/// assert_eq!(type_meta.kind, "Pod");
/// assert_eq!(type_meta.api_version, "v1");
/// ```
pub fn resource<K: Resource<DynamicType = ()>>() -> Self {
TypeMeta {
api_version: K::api_version(&()).into(),
kind: K::kind(&()).into(),
}
}
}
/// A generic representation of any object with `ObjectMeta`.
///
/// It allows clients to get access to a particular `ObjectMeta`
/// schema without knowing the details of the version.
///
/// See the [`PartialObjectMetaExt`] trait for how to construct one safely.
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PartialObjectMeta<K = DynamicObject> {
/// The type fields, not always present
#[serde(flatten, default)]
pub types: Option<TypeMeta>,
/// Standard object's metadata
#[serde(default)]
pub metadata: ObjectMeta,
/// Type information for static dispatch
#[serde(skip, default)]
pub _phantom: PhantomData<K>,
}
mod private {
pub trait Sealed {}
impl Sealed for super::ObjectMeta {}
}
/// Helper trait for converting `ObjectMeta` into useful `PartialObjectMeta` variants
pub trait PartialObjectMetaExt: private::Sealed {
/// Convert `ObjectMeta` into a Patch-serializable `PartialObjectMeta`
///
/// This object can be passed to `Patch::Apply` and used with `Api::patch_metadata`,
/// for an `Api<K>` using the underlying types `TypeMeta`:
///
/// ```
/// # use k8s_openapi::api::core::v1::Pod;
/// # use kube::core::{ObjectMeta, PartialObjectMetaExt, ResourceExt};
/// let partial = ObjectMeta {
/// labels: Some([("key".to_string(), "value".to_string())].into()),
/// ..Default::default()
/// }.into_request_partial::<Pod>();
///
/// // request partials are generally closer to patches than fully valid resources:
/// assert_eq!(partial.name_any(), "");
///
/// // typemeta is re-used from K:
/// assert_eq!(partial.types.unwrap().kind, "Pod");
/// ```
fn into_request_partial<K: Resource<DynamicType = ()>>(self) -> PartialObjectMeta<K>;
/// Convert `ObjectMeta` into a response object for a specific `Resource`
///
/// This object emulates a response object and **cannot** be used in request bodies
/// because it contains erased `TypeMeta` (and the apiserver is doing the erasing).
///
/// This method is **mostly useful for unit testing** behaviour.
///
/// ```
/// # use k8s_openapi::api::apps::v1::Deployment;
/// # use kube::core::{ObjectMeta, PartialObjectMetaExt, ResourceExt};
/// let partial = ObjectMeta {
/// name: Some("my-deploy".to_string()),
/// namespace: Some("default".to_string()),
/// ..Default::default()
/// }.into_response_partial::<Deployment>();
///
/// assert_eq!(partial.name_any(), "my-deploy");
/// assert_eq!(partial.types.unwrap().kind, "PartialObjectMetadata"); // NB: Pod erased
/// ```
fn into_response_partial<K>(self) -> PartialObjectMeta<K>;
}
impl PartialObjectMetaExt for ObjectMeta {
fn into_request_partial<K: Resource<DynamicType = ()>>(self) -> PartialObjectMeta<K> {
PartialObjectMeta {
types: Some(TypeMeta {
api_version: K::api_version(&()).into(),
kind: K::kind(&()).into(),
}),
metadata: self,
_phantom: PhantomData,
}
}
fn into_response_partial<K>(self) -> PartialObjectMeta<K> {
PartialObjectMeta {
types: Some(TypeMeta {
api_version: "meta.k8s.io/v1".to_string(),
kind: "PartialObjectMetadata".to_string(),
}),
metadata: self,
_phantom: PhantomData,
}
}
}
impl<K: Resource> Resource for PartialObjectMeta<K> {
type DynamicType = K::DynamicType;
type Scope = K::Scope;
fn kind(dt: &Self::DynamicType) -> Cow<'_, str> {
K::kind(dt)
}
fn group(dt: &Self::DynamicType) -> Cow<'_, str> {
K::group(dt)
}
fn version(dt: &Self::DynamicType) -> Cow<'_, str> {
K::version(dt)
}
fn plural(dt: &Self::DynamicType) -> Cow<'_, str> {
K::plural(dt)
}
fn meta(&self) -> &ObjectMeta {
&self.metadata
}
fn meta_mut(&mut self) -> &mut ObjectMeta {
&mut self.metadata
}
}
#[cfg(test)]
mod test {
use super::{ObjectMeta, PartialObjectMeta, PartialObjectMetaExt};
use crate::Resource;
use k8s_openapi::api::core::v1::Pod;
#[test]
fn can_convert_and_derive_partial_metadata() {
// can use generic type for static dispatch;
assert_eq!(PartialObjectMeta::<Pod>::kind(&()), "Pod");
assert_eq!(PartialObjectMeta::<Pod>::api_version(&()), "v1");
// can convert from objectmeta to partials for different use cases:
let meta = ObjectMeta {
name: Some("mypod".into()),
..Default::default()
};
let request_pom = meta.clone().into_request_partial::<Pod>();
let response_pom = meta.into_response_partial::<Pod>();
// they both basically just inline the metadata;
assert_eq!(request_pom.metadata.name, Some("mypod".to_string()));
assert_eq!(response_pom.metadata.name, Some("mypod".to_string()));
// the request_pom will use the TypeMeta from K to support POST/PUT requests
assert_eq!(request_pom.types.as_ref().unwrap().api_version, "v1");
assert_eq!(request_pom.types.as_ref().unwrap().kind, "Pod");
// but the response_pom will use the type-erased kinds from the apiserver
assert_eq!(response_pom.types.as_ref().unwrap().api_version, "meta.k8s.io/v1");
assert_eq!(response_pom.types.as_ref().unwrap().kind, "PartialObjectMetadata");
}
}

428
vendor/kube-core/src/object.rs vendored Normal file
View File

@@ -0,0 +1,428 @@
//! Generic object and objectlist wrappers.
use crate::{
discovery::ApiResource,
metadata::{ListMeta, ObjectMeta, TypeMeta},
resource::{DynamicResourceScope, Resource},
};
use serde::{Deserialize, Deserializer, Serialize};
use std::borrow::Cow;
/// A generic Kubernetes object list
///
/// This is used instead of a full struct for `DeploymentList`, `PodList`, etc.
/// Kubernetes' API [always seem to expose list structs in this manner](https://docs.rs/k8s-openapi/0.10.0/k8s_openapi/apimachinery/pkg/apis/meta/v1/struct.ObjectMeta.html?search=List).
///
/// Note that this is only used internally within reflectors and informers,
/// and is generally produced from list/watch/delete collection queries on an [`Resource`](super::Resource).
///
/// This is almost equivalent to [`k8s_openapi::List<T>`](k8s_openapi::List), but iterable.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ObjectList<T>
where
T: Clone,
{
/// The type fields, always present
#[serde(flatten, deserialize_with = "deserialize_v1_list_as_default")]
pub types: TypeMeta,
/// ListMeta - only really used for its `resourceVersion`
///
/// See [ListMeta](k8s_openapi::apimachinery::pkg::apis::meta::v1::ListMeta)
#[serde(default)]
pub metadata: ListMeta,
/// The items we are actually interested in. In practice; `T := Resource<T,U>`.
#[serde(
deserialize_with = "deserialize_null_as_default",
bound(deserialize = "Vec<T>: Deserialize<'de>")
)]
pub items: Vec<T>,
}
fn deserialize_v1_list_as_default<'de, D>(deserializer: D) -> Result<TypeMeta, D::Error>
where
D: Deserializer<'de>,
{
let meta = Option::<TypeMeta>::deserialize(deserializer)?;
Ok(meta.unwrap_or(TypeMeta {
api_version: "v1".to_owned(),
kind: "List".to_owned(),
}))
}
fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
impl<T: Clone> ObjectList<T> {
/// `iter` returns an Iterator over the elements of this ObjectList
///
/// # Example
///
/// ```
/// use kube::api::{ListMeta, ObjectList, TypeMeta};
/// use k8s_openapi::api::core::v1::Pod;
///
/// let types: TypeMeta = TypeMeta::list::<Pod>();
/// let metadata: ListMeta = Default::default();
/// let items = vec![1, 2, 3];
/// # let objectlist = ObjectList { types, metadata, items };
///
/// let first = objectlist.iter().next();
/// println!("First element: {:?}", first); // prints "First element: Some(1)"
/// ```
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.items.iter()
}
/// `iter_mut` returns an Iterator of mutable references to the elements of this ObjectList
///
/// # Example
///
/// ```
/// use kube::api::{ListMeta, ObjectList, TypeMeta};
/// use k8s_openapi::api::core::v1::Pod;
///
/// let types: TypeMeta = TypeMeta::list::<Pod>();
/// let metadata: ListMeta = Default::default();
/// let items = vec![1, 2, 3];
/// # let mut objectlist = ObjectList { types, metadata, items };
///
/// let mut first = objectlist.iter_mut().next();
///
/// // Reassign the value in first
/// if let Some(elem) = first {
/// *elem = 2;
/// println!("First element: {:?}", elem); // prints "First element: 2"
/// }
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.items.iter_mut()
}
}
impl<T: Clone> IntoIterator for ObjectList<T> {
type IntoIter = ::std::vec::IntoIter<Self::Item>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
}
}
impl<'a, T: Clone> IntoIterator for &'a ObjectList<T> {
type IntoIter = ::std::slice::Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.items.iter()
}
}
impl<'a, T: Clone> IntoIterator for &'a mut ObjectList<T> {
type IntoIter = ::std::slice::IterMut<'a, T>;
type Item = &'a mut T;
fn into_iter(self) -> Self::IntoIter {
self.items.iter_mut()
}
}
/// A trait to access the `spec` of a Kubernetes resource.
///
/// Some built-in Kubernetes resources and all custom resources do have a `spec` field.
/// This trait can be used to access this field.
///
/// This trait is automatically implemented by the kube-derive macro and is _not_ currently
/// implemented for the Kubernetes API objects from `k8s_openapi`.
///
/// Note: Not all Kubernetes resources have a spec (e.g. `ConfigMap`, `Secret`, ...).
pub trait HasSpec {
/// The type of the `spec` of this resource
type Spec;
/// Returns a reference to the `spec` of the object
fn spec(&self) -> &Self::Spec;
/// Returns a mutable reference to the `spec` of the object
fn spec_mut(&mut self) -> &mut Self::Spec;
}
/// A trait to access the `status` of a Kubernetes resource.
///
/// Some built-in Kubernetes resources and custom resources do have a `status` field.
/// This trait can be used to access this field.
///
/// This trait is automatically implemented by the kube-derive macro and is _not_ currently
/// implemented for the Kubernetes API objects from `k8s_openapi`.
///
/// Note: Not all Kubernetes resources have a status (e.g. `ConfigMap`, `Secret`, ...).
pub trait HasStatus {
/// The type of the `status` object
type Status;
/// Returns an optional reference to the `status` of the object
fn status(&self) -> Option<&Self::Status>;
/// Returns an optional mutable reference to the `status` of the object
fn status_mut(&mut self) -> &mut Option<Self::Status>;
}
// -------------------------------------------------------
/// A standard Kubernetes object with `.spec` and `.status`.
///
/// This is a convenience struct provided for serialization/deserialization.
/// It is slightly stricter than ['DynamicObject`] in that it enforces the spec/status convention,
/// and as such will not in general work with all api-discovered resources.
///
/// This can be used to tie existing resources to smaller, local struct variants to optimize for memory use.
/// E.g. if you are only interested in a few fields, but you store tons of them in memory with reflectors.
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Object<P, U>
where
P: Clone,
U: Clone,
{
/// The type fields, not always present
#[serde(flatten, default)]
pub types: Option<TypeMeta>,
/// Resource metadata
///
/// Contains information common to most resources about the Resource,
/// including the object name, annotations, labels and more.
pub metadata: ObjectMeta,
/// The Spec struct of a resource. I.e. `PodSpec`, `DeploymentSpec`, etc.
///
/// This defines the desired state of the Resource as specified by the user.
pub spec: P,
/// The Status of a resource. I.e. `PodStatus`, `DeploymentStatus`, etc.
///
/// This publishes the state of the Resource as observed by the controller.
/// Use `U = NotUsed` when a status does not exist.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<U>,
}
impl<P, U> Object<P, U>
where
P: Clone,
U: Clone,
{
/// A constructor that takes Resource values from an `ApiResource`
pub fn new(name: &str, ar: &ApiResource, spec: P) -> Self {
Self {
types: Some(TypeMeta {
api_version: ar.api_version.clone(),
kind: ar.kind.clone(),
}),
metadata: ObjectMeta {
name: Some(name.to_string()),
..Default::default()
},
spec,
status: None,
}
}
/// Attach a namespace to an Object
#[must_use]
pub fn within(mut self, ns: &str) -> Self {
self.metadata.namespace = Some(ns.into());
self
}
}
impl<P, U> Resource for Object<P, U>
where
P: Clone,
U: Clone,
{
type DynamicType = ApiResource;
type Scope = DynamicResourceScope;
fn group(dt: &ApiResource) -> Cow<'_, str> {
dt.group.as_str().into()
}
fn version(dt: &ApiResource) -> Cow<'_, str> {
dt.version.as_str().into()
}
fn kind(dt: &ApiResource) -> Cow<'_, str> {
dt.kind.as_str().into()
}
fn plural(dt: &ApiResource) -> Cow<'_, str> {
dt.plural.as_str().into()
}
fn api_version(dt: &ApiResource) -> Cow<'_, str> {
dt.api_version.as_str().into()
}
fn meta(&self) -> &ObjectMeta {
&self.metadata
}
fn meta_mut(&mut self) -> &mut ObjectMeta {
&mut self.metadata
}
}
impl<P, U> HasSpec for Object<P, U>
where
P: Clone,
U: Clone,
{
type Spec = P;
fn spec(&self) -> &Self::Spec {
&self.spec
}
fn spec_mut(&mut self) -> &mut Self::Spec {
&mut self.spec
}
}
impl<P, U> HasStatus for Object<P, U>
where
P: Clone,
U: Clone,
{
type Status = U;
fn status(&self) -> Option<&Self::Status> {
self.status.as_ref()
}
fn status_mut(&mut self) -> &mut Option<Self::Status> {
&mut self.status
}
}
/// Empty struct for when data should be discarded
///
/// Not using [`()`](https://doc.rust-lang.org/stable/std/primitive.unit.html), because serde's
/// [`Deserialize`](serde::Deserialize) `impl` is too strict.
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
pub struct NotUsed {}
#[cfg(test)]
mod test {
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ListMeta, ObjectMeta};
use super::{ApiResource, HasSpec, HasStatus, NotUsed, Object, ObjectList, Resource, TypeMeta};
use crate::resource::ResourceExt;
#[test]
fn simplified_k8s_object() {
use k8s_openapi::api::core::v1::Pod;
// Replacing heavy type k8s_openapi::api::core::v1::PodSpec with:
#[derive(Clone)]
struct PodSpecSimple {
#[allow(dead_code)]
containers: Vec<ContainerSimple>,
}
#[derive(Clone, Debug, PartialEq)]
struct ContainerSimple {
#[allow(dead_code)]
image: String,
}
type PodSimple = Object<PodSpecSimple, NotUsed>;
// by grabbing the ApiResource info from the Resource trait
let ar = ApiResource::erase::<Pod>(&());
assert_eq!(ar.group, "");
assert_eq!(ar.kind, "Pod");
let data = PodSpecSimple {
containers: vec![ContainerSimple { image: "blog".into() }],
};
let mypod = PodSimple::new("blog", &ar, data).within("dev");
let meta = mypod.meta();
assert_eq!(&mypod.metadata, meta);
assert_eq!(meta.namespace.as_ref().unwrap(), "dev");
assert_eq!(meta.name.as_ref().unwrap(), "blog");
assert_eq!(mypod.types.as_ref().unwrap().kind, "Pod");
assert_eq!(mypod.types.as_ref().unwrap().api_version, "v1");
assert_eq!(mypod.namespace().unwrap(), "dev");
assert_eq!(mypod.name_unchecked(), "blog");
assert!(mypod.status().is_none());
assert_eq!(mypod.spec().containers[0], ContainerSimple {
image: "blog".into()
});
assert_eq!(PodSimple::api_version(&ar), "v1");
assert_eq!(PodSimple::version(&ar), "v1");
assert_eq!(PodSimple::plural(&ar), "pods");
assert_eq!(PodSimple::kind(&ar), "Pod");
assert_eq!(PodSimple::group(&ar), "");
}
#[test]
fn k8s_object_list() {
use k8s_openapi::api::core::v1::Pod;
// by grabbing the ApiResource info from the Resource trait
let ar = ApiResource::erase::<Pod>(&());
assert_eq!(ar.group, "");
assert_eq!(ar.kind, "Pod");
let podlist: ObjectList<Pod> = ObjectList {
types: TypeMeta {
api_version: ar.api_version,
kind: ar.kind + "List",
},
metadata: ListMeta { ..Default::default() },
items: vec![Pod {
metadata: ObjectMeta {
name: Some("test".into()),
namespace: Some("dev".into()),
..ObjectMeta::default()
},
spec: None,
status: None,
}],
};
assert_eq!(&podlist.types.kind, "PodList");
assert_eq!(&podlist.types.api_version, "v1");
let mypod = &podlist.items[0];
let meta = mypod.meta();
assert_eq!(&mypod.metadata, meta);
assert_eq!(meta.namespace.as_ref().unwrap(), "dev");
assert_eq!(meta.name.as_ref().unwrap(), "test");
assert_eq!(mypod.namespace().unwrap(), "dev");
assert_eq!(mypod.name_unchecked(), "test");
assert!(mypod.status.is_none());
assert!(mypod.spec.is_none());
}
#[test]
fn k8s_object_list_default_types() {
use k8s_openapi::api::core::v1::Pod;
let raw_value = serde_json::json!({
"metadata": {
"resourceVersion": ""
},
"items": []
});
let pod_list: ObjectList<Pod> = serde_json::from_value(raw_value).unwrap();
assert_eq!(
TypeMeta {
api_version: "v1".to_owned(),
kind: "List".to_owned(),
},
pod_list.types,
);
}
}

971
vendor/kube-core/src/params.rs vendored Normal file
View File

@@ -0,0 +1,971 @@
//! A port of request parameter *Optionals from apimachinery/types.go
use crate::{request::Error, Selector};
use serde::Serialize;
/// Controls how the resource version parameter is applied for list calls
///
/// Not specifying a `VersionMatch` strategy will give you different semantics
/// depending on what `resource_version`, `limit`, `continue_token` you include with the list request.
///
/// See <https://kubernetes.io/docs/reference/using-api/api-concepts/#semantics-for-get-and-list> for details.
#[derive(Clone, Debug, PartialEq)]
pub enum VersionMatch {
/// Returns data at least as new as the provided resource version.
///
/// The newest available data is preferred, but any data not older than the provided resource version may be served.
/// This guarantees that the collection's resource version is not older than the requested resource version,
/// but does not make any guarantee about the resource version of any of the items in that collection.
///
/// ### Any Version
/// A degenerate, but common sub-case of `NotOlderThan` is when used together with `resource_version` "0".
///
/// It is possible for a "0" resource version request to return data at a much older resource version
/// than the client has previously observed, particularly in HA configurations, due to partitions or stale caches.
/// Clients that cannot tolerate this should not use this semantic.
NotOlderThan,
/// Return data at the exact resource version provided.
///
/// If the provided resource version is unavailable, the server responds with HTTP 410 "Gone".
/// For list requests to servers that honor the resource version Match parameter, this guarantees that the collection's
/// resource version is the same as the resource version you requested in the query string.
/// That guarantee does not apply to the resource version of any items within that collection.
///
/// Note that `Exact` cannot be used with resource version "0". For the most up-to-date list; use `Unset`.
Exact,
}
/// Common query parameters used in list/delete calls on collections
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ListParams {
/// A selector to restrict the list of returned objects by their labels.
///
/// Defaults to everything if `None`.
pub label_selector: Option<String>,
/// A selector to restrict the list of returned objects by their fields.
///
/// Defaults to everything if `None`.
pub field_selector: Option<String>,
/// Timeout for the list/watch call.
///
/// This limits the duration of the call, regardless of any activity or inactivity.
pub timeout: Option<u32>,
/// Limit the number of results.
///
/// If there are more results, the server will respond with a continue token which can be used to fetch another page
/// of results. See the [Kubernetes API docs](https://kubernetes.io/docs/reference/using-api/api-concepts/#retrieving-large-results-sets-in-chunks)
/// for pagination details.
pub limit: Option<u32>,
/// Fetch a second page of results.
///
/// After listing results with a limit, a continue token can be used to fetch another page of results.
pub continue_token: Option<String>,
/// Determines how resourceVersion is matched applied to list calls.
pub version_match: Option<VersionMatch>,
/// An explicit resourceVersion using the given `VersionMatch` strategy
///
/// See <https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions> for details.
pub resource_version: Option<String>,
}
impl ListParams {
pub(crate) fn validate(&self) -> Result<(), Error> {
if let Some(rv) = &self.resource_version {
if self.version_match == Some(VersionMatch::Exact) && rv == "0" {
return Err(Error::Validation(
"A non-zero resource_version is required when using an Exact match".into(),
));
}
} else if self.version_match.is_some() {
return Err(Error::Validation(
"A resource_version is required when using an explicit match".into(),
));
}
Ok(())
}
// Partially populate query parameters (needs resourceVersion out of band)
pub(crate) fn populate_qp(&self, qp: &mut form_urlencoded::Serializer<String>) {
if let Some(fields) = &self.field_selector {
qp.append_pair("fieldSelector", fields);
}
if let Some(labels) = &self.label_selector {
qp.append_pair("labelSelector", labels);
}
if let Some(limit) = &self.limit {
qp.append_pair("limit", &limit.to_string());
}
if let Some(continue_token) = &self.continue_token {
qp.append_pair("continue", continue_token);
} else {
// When there's a continue token, we don't want to set resourceVersion
if let Some(rv) = &self.resource_version {
if rv != "0" || self.limit.is_none() {
qp.append_pair("resourceVersion", rv.as_str());
match &self.version_match {
None => {}
Some(VersionMatch::NotOlderThan) => {
qp.append_pair("resourceVersionMatch", "NotOlderThan");
}
Some(VersionMatch::Exact) => {
qp.append_pair("resourceVersionMatch", "Exact");
}
}
}
}
}
}
}
/// Builder interface to ListParams
///
/// Usage:
/// ```
/// use kube::api::ListParams;
/// let lp = ListParams::default()
/// .match_any()
/// .timeout(60)
/// .labels("kubernetes.io/lifecycle=spot");
/// ```
impl ListParams {
/// Configure the timeout for list/watch calls
///
/// This limits the duration of the call, regardless of any activity or inactivity.
/// Defaults to 290s
#[must_use]
pub fn timeout(mut self, timeout_secs: u32) -> Self {
self.timeout = Some(timeout_secs);
self
}
/// Configure the selector to restrict the list of returned objects by their fields.
///
/// Defaults to everything.
/// Supports `=`, `==`, `!=`, and can be comma separated: `key1=value1,key2=value2`.
/// The server only supports a limited number of field queries per type.
#[must_use]
pub fn fields(mut self, field_selector: &str) -> Self {
self.field_selector = Some(field_selector.to_string());
self
}
/// Configure the selector to restrict the list of returned objects by their labels.
///
/// Defaults to everything.
/// Supports `=`, `==`, `!=`, and can be comma separated: `key1=value1,key2=value2`.
#[must_use]
pub fn labels(mut self, label_selector: &str) -> Self {
self.label_selector = Some(label_selector.to_string());
self
}
/// Configure typed label selectors
///
/// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists.
///
/// ```
/// use kube::core::{Expression, Selector, ParseExpressionError};
/// # use kube::core::params::ListParams;
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
///
/// // From expressions
/// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
/// let lp = ListParams::default().labels_from(&selector);
/// let lp = ListParams::default().labels_from(&Expression::Exists("foo".into()).into());
///
/// // Native LabelSelector
/// let selector: Selector = LabelSelector::default().try_into()?;
/// let lp = ListParams::default().labels_from(&selector);
/// # Ok::<(), ParseExpressionError>(())
///```
#[must_use]
pub fn labels_from(mut self, selector: &Selector) -> Self {
self.label_selector = Some(selector.to_string());
self
}
/// Sets a result limit.
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
/// Sets a continue token.
#[must_use]
pub fn continue_token(mut self, token: &str) -> Self {
self.continue_token = Some(token.to_string());
self
}
/// Sets the resource version
#[must_use]
pub fn at(mut self, resource_version: &str) -> Self {
self.resource_version = Some(resource_version.into());
self
}
/// Sets an arbitary resource version match strategy
///
/// A non-default strategy such as `VersionMatch::Exact` or `VersionMatch::NotOlderThan`
/// requires an explicit `resource_version` set to pass request validation.
#[must_use]
pub fn matching(mut self, version_match: VersionMatch) -> Self {
self.version_match = Some(version_match);
self
}
/// Use the semantic "any" resource version strategy
///
/// This is a less taxing variant of the default list, returning data at any resource version.
/// It will prefer the newest avialable resource version, but strong consistency is not required;
/// data at any resource version may be served.
/// It is possible for the request to return data at a much older resource version than the client
/// has previously observed, particularly in high availability configurations, due to partitions or stale caches.
/// Clients that cannot tolerate this should not use this semantic.
#[must_use]
pub fn match_any(self) -> Self {
self.matching(VersionMatch::NotOlderThan).at("0")
}
}
/// Common query parameters used in get calls
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GetParams {
/// An explicit resourceVersion with implicit version matching strategies
///
/// Default (unset) gives the most recent version. "0" gives a less
/// consistent, but more performant "Any" version. Specifing a version is
/// like providing a `VersionMatch::NotOlderThan`.
/// See <https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions> for details.
pub resource_version: Option<String>,
}
/// Helper interface to GetParams
///
/// Usage:
/// ```
/// use kube::api::GetParams;
/// let gp = GetParams::at("6664");
/// ```
impl GetParams {
/// Sets the resource version, implicitly applying a 'NotOlderThan' match
#[must_use]
pub fn at(resource_version: &str) -> Self {
Self {
resource_version: Some(resource_version.into()),
}
}
/// Sets the resource version to "0"
#[must_use]
pub fn any() -> Self {
Self::at("0")
}
}
/// The validation directive to use for `fieldValidation` when using server-side apply.
#[derive(Clone, Debug)]
pub enum ValidationDirective {
/// Strict mode will fail any invalid manifests.
///
/// This will fail the request with a BadRequest error if any unknown fields would be dropped from the
/// object, or if any duplicate fields are present. The error returned from the server will contain
/// all unknown and duplicate fields encountered.
Strict,
/// Warn mode will return a warning for invalid manifests.
///
/// This will send a warning via the standard warning response header for each unknown field that
/// is dropped from the object, and for each duplicate field that is encountered. The request will
/// still succeed if there are no other errors, and will only persist the last of any duplicate fields.
Warn,
/// Ignore mode will silently ignore any problems.
///
/// This will ignore any unknown fields that are silently dropped from the object, and will ignore
/// all but the last duplicate field that the decoder encounters.
Ignore,
}
impl ValidationDirective {
/// Returns the string format of the directive
pub fn as_str(&self) -> &str {
match self {
Self::Strict => "Strict",
Self::Warn => "Warn",
Self::Ignore => "Ignore",
}
}
}
/// Common query parameters used in watch calls on collections
#[derive(Clone, Debug, PartialEq)]
pub struct WatchParams {
/// A selector to restrict returned objects by their labels.
///
/// Defaults to everything if `None`.
pub label_selector: Option<String>,
/// A selector to restrict returned objects by their fields.
///
/// Defaults to everything if `None`.
pub field_selector: Option<String>,
/// Timeout for the watch call.
///
/// This limits the duration of the call, regardless of any activity or inactivity.
/// If unset for a watch call, we will use 290s.
/// We limit this to 295s due to [inherent watch limitations](https://github.com/kubernetes/kubernetes/issues/6513).
pub timeout: Option<u32>,
/// Enables watch events with type "BOOKMARK".
///
/// Servers that do not implement bookmarks ignore this flag and
/// bookmarks are sent at the server's discretion. Clients should not
/// assume bookmarks are returned at any specific interval, nor may they
/// assume the server will send any BOOKMARK event during a session.
/// If this is not a watch, this field is ignored.
/// If the feature gate WatchBookmarks is not enabled in apiserver,
/// this field is ignored.
pub bookmarks: bool,
/// Kubernetes 1.27 Streaming Lists
/// `sendInitialEvents=true` may be set together with `watch=true`.
/// In that case, the watch stream will begin with synthetic events to
/// produce the current state of objects in the collection. Once all such
/// events have been sent, a synthetic "Bookmark" event will be sent.
/// The bookmark will report the ResourceVersion (RV) corresponding to the
/// set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation.
/// Afterwards, the watch stream will proceed as usual, sending watch events
/// corresponding to changes (subsequent to the RV) to objects watched.
///
/// When `sendInitialEvents` option is set, we require `resourceVersionMatch`
/// option to also be set. The semantic of the watch request is as following:
/// - `resourceVersionMatch` = NotOlderThan
/// is interpreted as "data at least as new as the provided `resourceVersion`"
/// and the bookmark event is send when the state is synced
/// to a `resourceVersion` at least as fresh as the one provided by the ListOptions.
/// If `resourceVersion` is unset, this is interpreted as "consistent read" and the
/// bookmark event is send when the state is synced at least to the moment
/// when request started being processed.
/// - `resourceVersionMatch` set to any other value or unset
/// Invalid error is returned.
pub send_initial_events: bool,
}
impl WatchParams {
pub(crate) fn validate(&self) -> Result<(), Error> {
if let Some(to) = &self.timeout {
// https://github.com/kubernetes/kubernetes/issues/6513
if *to >= 295 {
return Err(Error::Validation("WatchParams::timeout must be < 295s".into()));
}
}
if self.send_initial_events && !self.bookmarks {
return Err(Error::Validation(
"WatchParams::bookmarks must be set when using send_initial_events".into(),
));
}
Ok(())
}
// Partially populate query parameters (needs resourceVersion out of band)
pub(crate) fn populate_qp(&self, qp: &mut form_urlencoded::Serializer<String>) {
qp.append_pair("watch", "true");
// https://github.com/kubernetes/kubernetes/issues/6513
qp.append_pair("timeoutSeconds", &self.timeout.unwrap_or(290).to_string());
if let Some(fields) = &self.field_selector {
qp.append_pair("fieldSelector", fields);
}
if let Some(labels) = &self.label_selector {
qp.append_pair("labelSelector", labels);
}
if self.bookmarks {
qp.append_pair("allowWatchBookmarks", "true");
}
if self.send_initial_events {
qp.append_pair("sendInitialEvents", "true");
qp.append_pair("resourceVersionMatch", "NotOlderThan");
}
}
}
impl Default for WatchParams {
/// Default `WatchParams` without any constricting selectors
fn default() -> Self {
Self {
// bookmarks stable since 1.17, and backwards compatible
bookmarks: true,
label_selector: None,
field_selector: None,
timeout: None,
send_initial_events: false,
}
}
}
/// Builder interface to WatchParams
///
/// Usage:
/// ```
/// use kube::api::WatchParams;
/// let lp = WatchParams::default()
/// .timeout(60)
/// .labels("kubernetes.io/lifecycle=spot");
/// ```
impl WatchParams {
/// Configure the timeout for watch calls
///
/// This limits the duration of the call, regardless of any activity or inactivity.
/// Defaults to 290s
#[must_use]
pub fn timeout(mut self, timeout_secs: u32) -> Self {
self.timeout = Some(timeout_secs);
self
}
/// Configure the selector to restrict the list of returned objects by their fields.
///
/// Defaults to everything.
/// Supports `=`, `==`, `!=`, and can be comma separated: `key1=value1,key2=value2`.
/// The server only supports a limited number of field queries per type.
#[must_use]
pub fn fields(mut self, field_selector: &str) -> Self {
self.field_selector = Some(field_selector.to_string());
self
}
/// Configure the selector to restrict the list of returned objects by their labels.
///
/// Defaults to everything.
/// Supports `=`, `==`, `!=`, and can be comma separated: `key1=value1,key2=value2`.
#[must_use]
pub fn labels(mut self, label_selector: &str) -> Self {
self.label_selector = Some(label_selector.to_string());
self
}
/// Configure typed label selectors
///
/// Configure typed selectors from [`Selector`](crate::Selector) and [`Expression`](crate::Expression) lists.
///
/// ```
/// use kube::core::{Expression, Selector, ParseExpressionError};
/// # use kube::core::params::WatchParams;
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
///
/// // From expressions
/// let selector: Selector = Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
/// let wp = WatchParams::default().labels_from(&selector);
/// let wp = WatchParams::default().labels_from(&Expression::Exists("foo".into()).into());
///
/// // Native LabelSelector
/// let selector: Selector = LabelSelector::default().try_into()?;
/// let wp = WatchParams::default().labels_from(&selector);
/// # Ok::<(), ParseExpressionError>(())
///```
#[must_use]
pub fn labels_from(mut self, selector: &Selector) -> Self {
self.label_selector = Some(selector.to_string());
self
}
/// Disables watch bookmarks to simplify watch handling
///
/// This is not recommended to use with production watchers as it can cause desyncs.
/// See [#219](https://github.com/kube-rs/kube/issues/219) for details.
#[must_use]
pub fn disable_bookmarks(mut self) -> Self {
self.bookmarks = false;
self
}
/// Kubernetes 1.27 Streaming Lists
/// `sendInitialEvents=true` may be set together with `watch=true`.
/// In that case, the watch stream will begin with synthetic events to
/// produce the current state of objects in the collection. Once all such
/// events have been sent, a synthetic "Bookmark" event will be sent.
/// The bookmark will report the ResourceVersion (RV) corresponding to the
/// set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation.
/// Afterwards, the watch stream will proceed as usual, sending watch events
/// corresponding to changes (subsequent to the RV) to objects watched.
///
/// When `sendInitialEvents` option is set, we require `resourceVersionMatch`
/// option to also be set. The semantic of the watch request is as following:
/// - `resourceVersionMatch` = NotOlderThan
/// is interpreted as "data at least as new as the provided `resourceVersion`"
/// and the bookmark event is send when the state is synced
/// to a `resourceVersion` at least as fresh as the one provided by the ListOptions.
/// If `resourceVersion` is unset, this is interpreted as "consistent read" and the
/// bookmark event is send when the state is synced at least to the moment
/// when request started being processed.
/// - `resourceVersionMatch` set to any other value or unset
/// Invalid error is returned.
///
/// Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward
/// compatibility reasons) and to false otherwise.
#[must_use]
pub fn initial_events(mut self) -> Self {
self.send_initial_events = true;
self
}
/// Constructor for doing Kubernetes 1.27 Streaming List watches
///
/// Enables [`VersionMatch::NotOlderThan`] semantics and [`WatchParams::send_initial_events`].
pub fn streaming_lists() -> Self {
Self {
send_initial_events: true,
bookmarks: true, // required
..WatchParams::default()
}
}
}
/// Common query parameters for put/post calls
#[derive(Default, Clone, Debug, PartialEq)]
pub struct PostParams {
/// Whether to run this as a dry run
pub dry_run: bool,
/// fieldManager is a name of the actor that is making changes
pub field_manager: Option<String>,
}
impl PostParams {
pub(crate) fn populate_qp(&self, qp: &mut form_urlencoded::Serializer<String>) {
if self.dry_run {
qp.append_pair("dryRun", "All");
}
if let Some(ref fm) = self.field_manager {
qp.append_pair("fieldManager", fm);
}
}
pub(crate) fn validate(&self) -> Result<(), Error> {
if let Some(field_manager) = &self.field_manager {
// Implement the easy part of validation, in future this may be extended to provide validation as in go code
// For now it's fine, because k8s API server will return an error
if field_manager.len() > 128 {
return Err(Error::Validation(
"Failed to validate PostParams::field_manager!".into(),
));
}
}
Ok(())
}
}
/// Describes changes that should be applied to a resource
///
/// Takes arbitrary serializable data for all strategies except `Json`.
///
/// We recommend using ([server-side](https://kubernetes.io/blog/2020/04/01/kubernetes-1.18-feature-server-side-apply-beta-2)) `Apply` patches on new kubernetes releases.
///
/// See [kubernetes patch docs](https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment) for the older patch types.
///
/// Note that patches have different effects on different fields depending on their merge strategies.
/// These strategies are configurable when deriving your [`CustomResource`](https://docs.rs/kube-derive/*/kube_derive/derive.CustomResource.html#customizing-schemas).
///
/// # Creating a patch via serde_json
/// ```
/// use kube::api::Patch;
/// let patch = serde_json::json!({
/// "apiVersion": "v1",
/// "kind": "Pod",
/// "metadata": {
/// "name": "blog"
/// },
/// "spec": {
/// "activeDeadlineSeconds": 5
/// }
/// });
/// let patch = Patch::Apply(&patch);
/// ```
/// # Creating a patch from a type
/// ```
/// use kube::api::Patch;
/// use k8s_openapi::api::rbac::v1::Role;
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
/// let r = Role {
/// metadata: ObjectMeta { name: Some("user".into()), ..ObjectMeta::default() },
/// rules: Some(vec![])
/// };
/// let patch = Patch::Apply(&r);
/// ```
#[non_exhaustive]
#[derive(Debug, PartialEq, Clone)]
pub enum Patch<T: Serialize> {
/// [Server side apply](https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply)
///
/// Requires kubernetes >= 1.16
Apply(T),
/// [JSON patch](https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment)
///
/// Using this variant will require you to explicitly provide a type for `T` at the moment.
///
/// # Example
///
/// ```
/// use kube::api::Patch;
/// let json_patch = json_patch::Patch(vec![]);
/// let patch = Patch::Json::<()>(json_patch);
/// ```
#[cfg(feature = "jsonpatch")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonpatch")))]
Json(json_patch::Patch),
/// [JSON Merge patch](https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment)
Merge(T),
/// [Strategic JSON Merge patch](https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-strategic-merge-patch-to-update-a-deployment)
Strategic(T),
}
impl<T: Serialize> Patch<T> {
pub(crate) fn is_apply(&self) -> bool {
matches!(self, Patch::Apply(_))
}
pub(crate) fn content_type(&self) -> &'static str {
match &self {
Self::Apply(_) => "application/apply-patch+yaml",
#[cfg(feature = "jsonpatch")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonpatch")))]
Self::Json(_) => "application/json-patch+json",
Self::Merge(_) => "application/merge-patch+json",
Self::Strategic(_) => "application/strategic-merge-patch+json",
}
}
}
impl<T: Serialize> Patch<T> {
pub(crate) fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
match self {
Self::Apply(p) => serde_json::to_vec(p),
#[cfg(feature = "jsonpatch")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonpatch")))]
Self::Json(p) => serde_json::to_vec(p),
Self::Strategic(p) => serde_json::to_vec(p),
Self::Merge(p) => serde_json::to_vec(p),
}
}
}
/// Common query parameters for patch calls
#[derive(Default, Clone, Debug)]
pub struct PatchParams {
/// Whether to run this as a dry run
pub dry_run: bool,
/// force Apply requests. Applicable only to [`Patch::Apply`].
pub force: bool,
/// fieldManager is a name of the actor that is making changes. Required for [`Patch::Apply`]
/// optional for everything else.
pub field_manager: Option<String>,
/// The server-side validation directive to use. Applicable only to [`Patch::Apply`].
pub field_validation: Option<ValidationDirective>,
}
impl PatchParams {
pub(crate) fn validate<P: Serialize>(&self, patch: &Patch<P>) -> Result<(), Error> {
if let Some(field_manager) = &self.field_manager {
// Implement the easy part of validation, in future this may be extended to provide validation as in go code
// For now it's fine, because k8s API server will return an error
if field_manager.len() > 128 {
return Err(Error::Validation(
"Failed to validate PatchParams::field_manager!".into(),
));
}
}
if self.force && !patch.is_apply() {
return Err(Error::Validation(
"PatchParams::force only works with Patch::Apply".into(),
));
}
Ok(())
}
pub(crate) fn populate_qp(&self, qp: &mut form_urlencoded::Serializer<String>) {
if self.dry_run {
qp.append_pair("dryRun", "All");
}
if self.force {
qp.append_pair("force", "true");
}
if let Some(ref fm) = self.field_manager {
qp.append_pair("fieldManager", fm);
}
if let Some(sv) = &self.field_validation {
qp.append_pair("fieldValidation", sv.as_str());
}
}
/// Construct `PatchParams` for server-side apply
#[must_use]
pub fn apply(manager: &str) -> Self {
Self {
field_manager: Some(manager.into()),
..Self::default()
}
}
/// Force the result through on conflicts
///
/// NB: Force is a concept restricted to the server-side [`Patch::Apply`].
#[must_use]
pub fn force(mut self) -> Self {
self.force = true;
self
}
/// Perform a dryRun only
#[must_use]
pub fn dry_run(mut self) -> Self {
self.dry_run = true;
self
}
/// Set the validation directive for `fieldValidation` during server-side apply.
pub fn validation(mut self, vd: ValidationDirective) -> Self {
self.field_validation = Some(vd);
self
}
/// Set the validation directive to `Ignore`
#[must_use]
pub fn validation_ignore(self) -> Self {
self.validation(ValidationDirective::Ignore)
}
/// Set the validation directive to `Warn`
#[must_use]
pub fn validation_warn(self) -> Self {
self.validation(ValidationDirective::Warn)
}
/// Set the validation directive to `Strict`
#[must_use]
pub fn validation_strict(self) -> Self {
self.validation(ValidationDirective::Strict)
}
}
/// Common query parameters for delete calls
#[derive(Default, Clone, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DeleteParams {
/// When present, indicates that modifications should not be persisted.
#[serde(
serialize_with = "dry_run_all_ser",
skip_serializing_if = "std::ops::Not::not"
)]
pub dry_run: bool,
/// The duration in seconds before the object should be deleted.
///
/// Value must be non-negative integer. The value zero indicates delete immediately.
/// If this value is `None`, the default grace period for the specified type will be used.
/// Defaults to a per object value if not specified. Zero means delete immediately.
#[serde(skip_serializing_if = "Option::is_none")]
pub grace_period_seconds: Option<u32>,
/// Whether or how garbage collection is performed.
///
/// The default policy is decided by the existing finalizer set in
/// `metadata.finalizers`, and the resource-specific default policy.
#[serde(skip_serializing_if = "Option::is_none")]
pub propagation_policy: Option<PropagationPolicy>,
/// Condtions that must be fulfilled before a deletion is carried out
///
/// If not possible, a `409 Conflict` status will be returned.
#[serde(skip_serializing_if = "Option::is_none")]
pub preconditions: Option<Preconditions>,
}
impl DeleteParams {
/// Construct `DeleteParams` with `PropagationPolicy::Background`.
///
/// This allows the garbage collector to delete the dependents in the background.
pub fn background() -> Self {
Self {
propagation_policy: Some(PropagationPolicy::Background),
..Self::default()
}
}
/// Construct `DeleteParams` with `PropagationPolicy::Foreground`.
///
/// This is a cascading policy that deletes all dependents in the foreground.
pub fn foreground() -> Self {
Self {
propagation_policy: Some(PropagationPolicy::Foreground),
..Self::default()
}
}
/// Construct `DeleteParams` with `PropagationPolicy::Orphan`.
///
///
/// This orpans the dependents.
pub fn orphan() -> Self {
Self {
propagation_policy: Some(PropagationPolicy::Orphan),
..Self::default()
}
}
/// Perform a dryRun only
#[must_use]
pub fn dry_run(mut self) -> Self {
self.dry_run = true;
self
}
/// Set the duration in seconds before the object should be deleted.
#[must_use]
pub fn grace_period(mut self, secs: u32) -> Self {
self.grace_period_seconds = Some(secs);
self
}
/// Set the condtions that must be fulfilled before a deletion is carried out.
#[must_use]
pub fn preconditions(mut self, preconditions: Preconditions) -> Self {
self.preconditions = Some(preconditions);
self
}
pub(crate) fn is_default(&self) -> bool {
!self.dry_run
&& self.grace_period_seconds.is_none()
&& self.propagation_policy.is_none()
&& self.preconditions.is_none()
}
}
// dryRun serialization differ when used as body parameters and query strings:
// query strings are either true/false
// body params allow only: missing field, or ["All"]
// The latter is a very awkward API causing users to do to
// dp.dry_run = vec!["All".into()];
// just to turn on dry_run..
// so we hide this detail for now.
fn dry_run_all_ser<S>(t: &bool, s: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeTuple;
match t {
true => {
let mut map = s.serialize_tuple(1)?;
map.serialize_element("All")?;
map.end()
}
false => s.serialize_none(),
}
}
#[cfg(test)]
mod test {
use crate::{params::WatchParams, Expression, Selector};
use super::{DeleteParams, ListParams, PatchParams};
#[test]
fn delete_param_serialize() {
let mut dp = DeleteParams::default();
let emptyser = serde_json::to_string(&dp).unwrap();
//println!("emptyser is: {}", emptyser);
assert_eq!(emptyser, "{}");
dp.dry_run = true;
let ser = serde_json::to_string(&dp).unwrap();
//println!("ser is: {}", ser);
assert_eq!(ser, "{\"dryRun\":[\"All\"]}");
}
#[test]
fn delete_param_constructors() {
let dp_background = DeleteParams::background();
let ser = serde_json::to_value(dp_background).unwrap();
assert_eq!(ser, serde_json::json!({"propagationPolicy": "Background"}));
let dp_foreground = DeleteParams::foreground();
let ser = serde_json::to_value(dp_foreground).unwrap();
assert_eq!(ser, serde_json::json!({"propagationPolicy": "Foreground"}));
let dp_orphan = DeleteParams::orphan();
let ser = serde_json::to_value(dp_orphan).unwrap();
assert_eq!(ser, serde_json::json!({"propagationPolicy": "Orphan"}));
}
#[test]
fn patch_param_serializes_field_validation() {
let pp = PatchParams::default().validation_ignore();
let mut qp = form_urlencoded::Serializer::new(String::from("some/resource?"));
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
assert_eq!(String::from("some/resource?&fieldValidation=Ignore"), urlstr);
let pp = PatchParams::default().validation_warn();
let mut qp = form_urlencoded::Serializer::new(String::from("some/resource?"));
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
assert_eq!(String::from("some/resource?&fieldValidation=Warn"), urlstr);
let pp = PatchParams::default().validation_strict();
let mut qp = form_urlencoded::Serializer::new(String::from("some/resource?"));
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
assert_eq!(String::from("some/resource?&fieldValidation=Strict"), urlstr);
}
#[test]
fn list_params_serialize() {
let selector: Selector =
Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
let lp = ListParams::default().labels_from(&selector);
let labels = lp.label_selector.unwrap();
assert_eq!(labels, "env in (development,sandbox)");
}
#[test]
fn watch_params_serialize() {
let selector: Selector =
Expression::In("env".into(), ["development".into(), "sandbox".into()].into()).into();
let wp = WatchParams::default().labels_from(&selector);
let labels = wp.label_selector.unwrap();
assert_eq!(labels, "env in (development,sandbox)");
}
}
/// Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.
#[derive(Default, Clone, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Preconditions {
/// Specifies the target ResourceVersion
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_version: Option<String>,
/// Specifies the target UID
#[serde(skip_serializing_if = "Option::is_none")]
pub uid: Option<String>,
}
/// Propagation policy when deleting single objects
#[derive(Clone, Debug, Serialize, PartialEq)]
pub enum PropagationPolicy {
/// Orphan dependents
Orphan,
/// Allow the garbage collector to delete the dependents in the background
Background,
/// A cascading policy that deletes all dependents in the foreground
Foreground,
}

831
vendor/kube-core/src/request.rs vendored Normal file
View File

@@ -0,0 +1,831 @@
//! Request builder type for arbitrary api types
use thiserror::Error;
use crate::params::GetParams;
use super::params::{DeleteParams, ListParams, Patch, PatchParams, PostParams, WatchParams};
pub(crate) const JSON_MIME: &str = "application/json";
/// Extended Accept Header
///
/// Requests a meta.k8s.io/v1 PartialObjectMetadata resource (efficiently
/// retrieves object metadata)
///
/// API Servers running Kubernetes v1.14 and below will retrieve the object and then
/// convert the metadata.
pub(crate) const JSON_METADATA_MIME: &str = "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1";
pub(crate) const JSON_METADATA_LIST_MIME: &str =
"application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1";
/// Possible errors when building a request.
#[derive(Debug, Error)]
pub enum Error {
/// Failed to build a request.
#[error("failed to build request: {0}")]
BuildRequest(#[source] http::Error),
/// Failed to serialize body.
#[error("failed to serialize body: {0}")]
SerializeBody(#[source] serde_json::Error),
/// Failed to validate request.
#[error("failed to validate request: {0}")]
Validation(String),
}
/// A Kubernetes request builder
///
/// Takes a base_path and supplies constructors for common operations
/// The extra operations all return `http::Request` objects.
#[derive(Debug, Clone)]
pub struct Request {
/// The path component of a url
pub url_path: String,
}
impl Request {
/// New request with a resource's url path
pub fn new<S: Into<String>>(url_path: S) -> Self {
Self {
url_path: url_path.into(),
}
}
}
// -------------------------------------------------------
/// Convenience methods found from API conventions
impl Request {
/// List a collection of a resource
pub fn list(&self, lp: &ListParams) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}?", self.url_path);
let mut qp = form_urlencoded::Serializer::new(target);
lp.validate()?;
lp.populate_qp(&mut qp);
let urlstr = qp.finish();
let req = http::Request::get(urlstr);
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Watch a resource at a given version
pub fn watch(&self, wp: &WatchParams, ver: &str) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}?", self.url_path);
let mut qp = form_urlencoded::Serializer::new(target);
wp.validate()?;
wp.populate_qp(&mut qp);
qp.append_pair("resourceVersion", ver);
let urlstr = qp.finish();
let req = http::Request::get(urlstr);
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Get a single instance
pub fn get(&self, name: &str, gp: &GetParams) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let urlstr = if let Some(rv) = &gp.resource_version {
let target = format!("{}/{}?", self.url_path, name);
form_urlencoded::Serializer::new(target)
.append_pair("resourceVersion", rv)
.finish()
} else {
let target = format!("{}/{}", self.url_path, name);
form_urlencoded::Serializer::new(target).finish()
};
let req = http::Request::get(urlstr);
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Create an instance of a resource
pub fn create(&self, pp: &PostParams, data: Vec<u8>) -> Result<http::Request<Vec<u8>>, Error> {
pp.validate()?;
let target = format!("{}?", self.url_path);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
let req = http::Request::post(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(data).map_err(Error::BuildRequest)
}
/// Delete an instance of a resource
pub fn delete(&self, name: &str, dp: &DeleteParams) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let target = format!("{}/{}?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
let urlstr = qp.finish();
let body = serde_json::to_vec(&dp).map_err(Error::SerializeBody)?;
let req = http::Request::delete(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(body).map_err(Error::BuildRequest)
}
/// Delete a collection of a resource
pub fn delete_collection(
&self,
dp: &DeleteParams,
lp: &ListParams,
) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}?", self.url_path);
let mut qp = form_urlencoded::Serializer::new(target);
if let Some(fields) = &lp.field_selector {
qp.append_pair("fieldSelector", fields);
}
if let Some(labels) = &lp.label_selector {
qp.append_pair("labelSelector", labels);
}
let urlstr = qp.finish();
let data = if dp.is_default() {
vec![] // default serialize needs to be empty body
} else {
serde_json::to_vec(&dp).map_err(Error::SerializeBody)?
};
let req = http::Request::delete(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(data).map_err(Error::BuildRequest)
}
/// Patch an instance of a resource
///
/// Requires a serialized merge-patch+json at the moment.
pub fn patch<P: serde::Serialize>(
&self,
name: &str,
pp: &PatchParams,
patch: &Patch<P>,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
pp.validate(patch)?;
let target = format!("{}/{}?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
http::Request::patch(urlstr)
.header(http::header::ACCEPT, JSON_MIME)
.header(http::header::CONTENT_TYPE, patch.content_type())
.body(patch.serialize().map_err(Error::SerializeBody)?)
.map_err(Error::BuildRequest)
}
/// Replace an instance of a resource
///
/// Requires `metadata.resourceVersion` set in data
pub fn replace(
&self,
name: &str,
pp: &PostParams,
data: Vec<u8>,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let target = format!("{}/{}?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
let req = http::Request::put(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(data).map_err(Error::BuildRequest)
}
}
/// Subresources
impl Request {
/// Get an instance of the subresource
pub fn get_subresource(
&self,
subresource_name: &str,
name: &str,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let target = format!("{}/{}/{}", self.url_path, name, subresource_name);
let mut qp = form_urlencoded::Serializer::new(target);
let urlstr = qp.finish();
let req = http::Request::get(urlstr);
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Create an instance of the subresource
pub fn create_subresource(
&self,
subresource_name: &str,
name: &str,
pp: &PostParams,
data: Vec<u8>,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let target = format!("{}/{}/{}?", self.url_path, name, subresource_name);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
let req = http::Request::post(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(data).map_err(Error::BuildRequest)
}
/// Patch an instance of the subresource
pub fn patch_subresource<P: serde::Serialize>(
&self,
subresource_name: &str,
name: &str,
pp: &PatchParams,
patch: &Patch<P>,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
pp.validate(patch)?;
let target = format!("{}/{}/{}?", self.url_path, name, subresource_name);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
http::Request::patch(urlstr)
.header(http::header::ACCEPT, JSON_MIME)
.header(http::header::CONTENT_TYPE, patch.content_type())
.body(patch.serialize().map_err(Error::SerializeBody)?)
.map_err(Error::BuildRequest)
}
/// Replace an instance of the subresource
pub fn replace_subresource(
&self,
subresource_name: &str,
name: &str,
pp: &PostParams,
data: Vec<u8>,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let target = format!("{}/{}/{}?", self.url_path, name, subresource_name);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
let req = http::Request::put(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(data).map_err(Error::BuildRequest)
}
}
/// Metadata-only request implementations
///
/// Requests set an extended Accept header compromised of JSON media type and
/// additional parameters that retrieve only necessary metadata from an object.
impl Request {
/// Get a single metadata instance for a named resource
pub fn get_metadata(&self, name: &str, gp: &GetParams) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
let urlstr = if let Some(rv) = &gp.resource_version {
let target = format!("{}/{}?", self.url_path, name);
form_urlencoded::Serializer::new(target)
.append_pair("resourceVersion", rv)
.finish()
} else {
let target = format!("{}/{}", self.url_path, name);
form_urlencoded::Serializer::new(target).finish()
};
let req = http::Request::get(urlstr)
.header(http::header::ACCEPT, JSON_METADATA_MIME)
.header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(vec![]).map_err(Error::BuildRequest)
}
/// List a collection of metadata of a resource
pub fn list_metadata(&self, lp: &ListParams) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}?", self.url_path);
let mut qp = form_urlencoded::Serializer::new(target);
lp.validate()?;
lp.populate_qp(&mut qp);
let urlstr = qp.finish();
let req = http::Request::get(urlstr)
.header(http::header::ACCEPT, JSON_METADATA_LIST_MIME)
.header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(vec![]).map_err(Error::BuildRequest)
}
/// Watch metadata of a resource at a given version
pub fn watch_metadata(&self, wp: &WatchParams, ver: &str) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}?", self.url_path);
let mut qp = form_urlencoded::Serializer::new(target);
wp.validate()?;
wp.populate_qp(&mut qp);
qp.append_pair("resourceVersion", ver);
let urlstr = qp.finish();
http::Request::get(urlstr)
.header(http::header::ACCEPT, JSON_METADATA_MIME)
.header(http::header::CONTENT_TYPE, JSON_MIME)
.body(vec![])
.map_err(Error::BuildRequest)
}
/// Patch an instance of a resource and receive its metadata only
///
/// Requires a serialized merge-patch+json at the moment
pub fn patch_metadata<P: serde::Serialize>(
&self,
name: &str,
pp: &PatchParams,
patch: &Patch<P>,
) -> Result<http::Request<Vec<u8>>, Error> {
validate_name(name)?;
pp.validate(patch)?;
let target = format!("{}/{}?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
http::Request::patch(urlstr)
.header(http::header::ACCEPT, JSON_METADATA_MIME)
.header(http::header::CONTENT_TYPE, patch.content_type())
.body(patch.serialize().map_err(Error::SerializeBody)?)
.map_err(Error::BuildRequest)
}
}
/// Names must not be empty as otherwise API server would interpret a `get` as `list`, or a `delete` as `delete_collection`
fn validate_name(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::Validation("A non-empty name is required".into()));
}
Ok(())
}
/// Extensive tests for Request of k8s_openapi::Resource structs
///
/// Cheap sanity check to ensure type maps work as expected
#[cfg(test)]
mod test {
use crate::{
params::{GetParams, PostParams, VersionMatch, WatchParams},
request::{Error, Request},
resource::Resource,
};
use http::header;
use k8s::{
admissionregistration::v1 as adregv1, apps::v1 as appsv1, authorization::v1 as authv1,
autoscaling::v1 as autoscalingv1, batch::v1 as batchv1, core::v1 as corev1,
networking::v1 as networkingv1, rbac::v1 as rbacv1, storage::v1 as storagev1,
};
use k8s_openapi::api as k8s;
// NB: stable requires >= 1.17
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1 as apiextsv1;
// TODO: fixturize these tests
#[test]
fn api_url_secret() {
let url = corev1::Secret::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(req.uri(), "/api/v1/namespaces/ns/secrets?");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
}
#[test]
fn api_url_rs() {
let url = appsv1::ReplicaSet::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/replicasets?");
}
#[test]
fn api_url_role() {
let url = rbacv1::Role::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(
req.uri(),
"/apis/rbac.authorization.k8s.io/v1/namespaces/ns/roles?"
);
}
#[test]
fn api_url_cj() {
let url = batchv1::CronJob::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(req.uri(), "/apis/batch/v1/namespaces/ns/cronjobs?");
}
#[test]
fn api_url_hpa() {
let url = autoscalingv1::HorizontalPodAutoscaler::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(
req.uri(),
"/apis/autoscaling/v1/namespaces/ns/horizontalpodautoscalers?"
);
}
#[test]
fn api_url_np() {
let url = networkingv1::NetworkPolicy::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(
req.uri(),
"/apis/networking.k8s.io/v1/namespaces/ns/networkpolicies?"
);
}
#[test]
fn api_url_ingress() {
let url = networkingv1::Ingress::url_path(&(), Some("ns"));
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(req.uri(), "/apis/networking.k8s.io/v1/namespaces/ns/ingresses?");
}
#[test]
fn api_url_vattach() {
let url = storagev1::VolumeAttachment::url_path(&(), None);
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(req.uri(), "/apis/storage.k8s.io/v1/volumeattachments?");
}
#[test]
fn api_url_admission() {
let url = adregv1::ValidatingWebhookConfiguration::url_path(&(), None);
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(
req.uri(),
"/apis/admissionregistration.k8s.io/v1/validatingwebhookconfigurations?"
);
}
#[test]
fn api_auth_selfreview() {
//assert_eq!(r.group, "authorization.k8s.io");
//assert_eq!(r.kind, "SelfSubjectRulesReview");
let url = authv1::SelfSubjectRulesReview::url_path(&(), None);
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(
req.uri(),
"/apis/authorization.k8s.io/v1/selfsubjectrulesreviews?"
);
}
#[test]
fn api_apiextsv1_crd() {
let url = apiextsv1::CustomResourceDefinition::url_path(&(), None);
let req = Request::new(url).create(&PostParams::default(), vec![]).unwrap();
assert_eq!(
req.uri(),
"/apis/apiextensions.k8s.io/v1/customresourcedefinitions?"
);
}
/// -----------------------------------------------------------------
/// Tests that the misc mappings are also sensible
use crate::params::{DeleteParams, ListParams, Patch, PatchParams};
#[test]
fn get_metadata_path() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let req = Request::new(url)
.get_metadata("mydeploy", &GetParams::default())
.unwrap();
println!("{}", req.uri());
assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/deployments/mydeploy");
assert_eq!(req.method(), "GET");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
assert_eq!(
req.headers().get(header::ACCEPT).unwrap(),
super::JSON_METADATA_MIME
);
}
#[test]
fn get_path_with_rv() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let req = Request::new(url).get("mydeploy", &GetParams::any()).unwrap();
assert_eq!(
req.uri(),
"/apis/apps/v1/namespaces/ns/deployments/mydeploy?&resourceVersion=0"
);
}
#[test]
fn get_meta_path_with_rv() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let req = Request::new(url)
.get_metadata("mydeploy", &GetParams::at("665"))
.unwrap();
assert_eq!(
req.uri(),
"/apis/apps/v1/namespaces/ns/deployments/mydeploy?&resourceVersion=665"
);
assert_eq!(req.method(), "GET");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
assert_eq!(
req.headers().get(header::ACCEPT).unwrap(),
super::JSON_METADATA_MIME
);
}
#[test]
fn get_empty_name() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let req = Request::new(url).get("", &GetParams::any());
assert!(matches!(req, Err(Error::Validation(_))));
}
#[test]
fn list_path() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let lp = ListParams::default();
let req = Request::new(url).list(&lp).unwrap();
assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/deployments");
}
#[test]
fn list_metadata_path() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let lp = ListParams::default().matching(VersionMatch::NotOlderThan).at("5");
let req = Request::new(url).list_metadata(&lp).unwrap();
assert_eq!(
req.uri(),
"/apis/apps/v1/namespaces/ns/deployments?&resourceVersion=5&resourceVersionMatch=NotOlderThan"
);
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
assert_eq!(
req.headers().get(header::ACCEPT).unwrap(),
super::JSON_METADATA_LIST_MIME
);
}
#[test]
fn watch_path() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let wp = WatchParams::default();
let req = Request::new(url).watch(&wp, "0").unwrap();
assert_eq!(
req.uri(),
"/api/v1/namespaces/ns/pods?&watch=true&timeoutSeconds=290&allowWatchBookmarks=true&resourceVersion=0"
);
}
#[test]
fn watch_streaming_list() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let wp = WatchParams::default().initial_events();
let req = Request::new(url).watch(&wp, "0").unwrap();
assert_eq!(
req.uri(),
"/api/v1/namespaces/ns/pods?&watch=true&timeoutSeconds=290&allowWatchBookmarks=true&sendInitialEvents=true&resourceVersionMatch=NotOlderThan&resourceVersion=0"
);
}
#[test]
fn watch_metadata_path() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let wp = WatchParams::default();
let req = Request::new(url).watch_metadata(&wp, "0").unwrap();
assert_eq!(
req.uri(),
"/api/v1/namespaces/ns/pods?&watch=true&timeoutSeconds=290&allowWatchBookmarks=true&resourceVersion=0"
);
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
assert_eq!(
req.headers().get(header::ACCEPT).unwrap(),
super::JSON_METADATA_MIME
);
}
#[test]
fn replace_path() {
let url = appsv1::DaemonSet::url_path(&(), None);
let pp = PostParams {
dry_run: true,
..Default::default()
};
let req = Request::new(url).replace("myds", &pp, vec![]).unwrap();
assert_eq!(req.uri(), "/apis/apps/v1/daemonsets/myds?&dryRun=All");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
}
#[test]
fn delete_path() {
let url = appsv1::ReplicaSet::url_path(&(), Some("ns"));
let dp = DeleteParams::default();
let req = Request::new(url).delete("myrs", &dp).unwrap();
assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/replicasets/myrs");
assert_eq!(req.method(), "DELETE");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
}
#[test]
fn delete_collection_path() {
let url = appsv1::ReplicaSet::url_path(&(), Some("ns"));
let lp = ListParams::default().labels("app=myapp");
let dp = DeleteParams::default();
let req = Request::new(url).delete_collection(&dp, &lp).unwrap();
assert_eq!(
req.uri(),
"/apis/apps/v1/namespaces/ns/replicasets?&labelSelector=app%3Dmyapp"
);
assert_eq!(req.method(), "DELETE");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
}
#[test]
fn namespace_path() {
let url = corev1::Namespace::url_path(&(), None);
let gp = ListParams::default();
let req = Request::new(url).list(&gp).unwrap();
assert_eq!(req.uri(), "/api/v1/namespaces")
}
// subresources with weird version accuracy
#[test]
fn patch_status_path() {
let url = corev1::Node::url_path(&(), None);
let pp = PatchParams::default();
let req = Request::new(url)
.patch_subresource("status", "mynode", &pp, &Patch::Merge(()))
.unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/status?");
assert_eq!(
req.headers().get("Content-Type").unwrap().to_str().unwrap(),
Patch::Merge(()).content_type()
);
assert_eq!(req.method(), "PATCH");
}
#[test]
fn patch_pod_metadata() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let pp = PatchParams::default();
let req = Request::new(url)
.patch_metadata("mypod", &pp, &Patch::Merge(()))
.unwrap();
assert_eq!(req.uri(), "/api/v1/namespaces/ns/pods/mypod?");
assert_eq!(
req.headers().get(header::CONTENT_TYPE).unwrap(),
Patch::Merge(()).content_type()
);
assert_eq!(
req.headers().get(header::ACCEPT).unwrap(),
super::JSON_METADATA_MIME
);
assert_eq!(req.method(), "PATCH");
}
#[test]
fn replace_status_path() {
let url = corev1::Node::url_path(&(), None);
let pp = PostParams::default();
let req = Request::new(url)
.replace_subresource("status", "mynode", &pp, vec![])
.unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/status?");
assert_eq!(req.method(), "PUT");
assert_eq!(req.headers().get(header::CONTENT_TYPE).unwrap(), super::JSON_MIME);
}
#[test]
fn create_ingress() {
// NB: Ingress exists in extensions AND networking
let url = networkingv1::Ingress::url_path(&(), Some("ns"));
let pp = PostParams::default();
let req = Request::new(&url).create(&pp, vec![]).unwrap();
assert_eq!(req.uri(), "/apis/networking.k8s.io/v1/namespaces/ns/ingresses?");
let patch_params = PatchParams::default();
let req = Request::new(url)
.patch("baz", &patch_params, &Patch::Merge(()))
.unwrap();
assert_eq!(
req.uri(),
"/apis/networking.k8s.io/v1/namespaces/ns/ingresses/baz?"
);
assert_eq!(req.method(), "PATCH");
}
#[test]
fn replace_status() {
let url = apiextsv1::CustomResourceDefinition::url_path(&(), None);
let pp = PostParams::default();
let req = Request::new(url)
.replace_subresource("status", "mycrd.domain.io", &pp, vec![])
.unwrap();
assert_eq!(
req.uri(),
"/apis/apiextensions.k8s.io/v1/customresourcedefinitions/mycrd.domain.io/status?"
);
}
#[test]
fn get_scale_path() {
let url = corev1::Node::url_path(&(), None);
let req = Request::new(url).get_subresource("scale", "mynode").unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale");
assert_eq!(req.method(), "GET");
}
#[test]
fn patch_scale_path() {
let url = corev1::Node::url_path(&(), None);
let pp = PatchParams::default();
let req = Request::new(url)
.patch_subresource("scale", "mynode", &pp, &Patch::Merge(()))
.unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale?");
assert_eq!(req.method(), "PATCH");
}
#[test]
fn replace_scale_path() {
let url = corev1::Node::url_path(&(), None);
let pp = PostParams::default();
let req = Request::new(url)
.replace_subresource("scale", "mynode", &pp, vec![])
.unwrap();
assert_eq!(req.uri(), "/api/v1/nodes/mynode/scale?");
assert_eq!(req.method(), "PUT");
}
#[test]
fn create_subresource_path() {
let url = corev1::ServiceAccount::url_path(&(), Some("ns"));
let pp = PostParams::default();
let data = vec![];
let req = Request::new(url)
.create_subresource("token", "sa", &pp, data)
.unwrap();
assert_eq!(req.uri(), "/api/v1/namespaces/ns/serviceaccounts/sa/token");
}
// TODO: reinstate if we get scoping in trait
//#[test]
//#[should_panic]
//fn all_resources_not_namespaceable() {
// let _r = Request::<corev1::Node>::new(&(), Some("ns"));
//}
#[test]
fn list_pods_from_cache() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default().match_any();
let req = Request::new(url).list(&gp).unwrap();
assert_eq!(
req.uri().query().unwrap(),
"&resourceVersion=0&resourceVersionMatch=NotOlderThan"
);
}
#[test]
fn list_most_recent_pods() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default();
let req = Request::new(url).list(&gp).unwrap();
assert_eq!(
req.uri().query().unwrap(),
"" // No options are required
);
}
#[test]
fn list_invalid_resource_version_combination() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default().at("0").matching(VersionMatch::Exact);
let err = Request::new(url).list(&gp).unwrap_err();
assert!(format!("{err}").contains("non-zero resource_version is required when using an Exact match"));
}
#[test]
fn list_paged_any_semantic() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default().limit(50).match_any();
let req = Request::new(url).list(&gp).unwrap();
assert_eq!(req.uri().query().unwrap(), "&limit=50");
}
#[test]
fn list_paged_with_continue_any_semantic() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default().limit(50).continue_token("1234").match_any();
let req = Request::new(url).list(&gp).unwrap();
assert_eq!(req.uri().query().unwrap(), "&limit=50&continue=1234");
}
#[test]
fn list_paged_with_continue_starting_at() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default()
.limit(50)
.continue_token("1234")
.at("9999")
.matching(VersionMatch::Exact);
let req = Request::new(url).list(&gp).unwrap();
assert_eq!(req.uri().query().unwrap(), "&limit=50&continue=1234");
}
#[test]
fn list_exact_match() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let gp = ListParams::default().at("500").matching(VersionMatch::Exact);
let req = Request::new(url).list(&gp).unwrap();
let query = req.uri().query().unwrap();
assert_eq!(query, "&resourceVersion=500&resourceVersionMatch=Exact");
}
#[test]
fn watch_params() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let wp = WatchParams::default()
.disable_bookmarks()
.fields("metadata.name=pod=1")
.labels("app=web");
let req = Request::new(url).watch(&wp, "0").unwrap();
assert_eq!(
req.uri().query().unwrap(),
"&watch=true&timeoutSeconds=290&fieldSelector=metadata.name%3Dpod%3D1&labelSelector=app%3Dweb&resourceVersion=0"
);
}
#[test]
fn watch_timeout_error() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let wp = WatchParams::default().timeout(100000);
let err = Request::new(url).watch(&wp, "").unwrap_err();
assert!(format!("{err}").contains("timeout must be < 295s"));
}
}

335
vendor/kube-core/src/resource.rs vendored Normal file
View File

@@ -0,0 +1,335 @@
pub use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use k8s_openapi::{
api::core::v1::ObjectReference,
apimachinery::pkg::apis::meta::v1::{ManagedFieldsEntry, OwnerReference, Time},
};
use std::{borrow::Cow, collections::BTreeMap};
pub use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope, ResourceScope, SubResourceScope};
/// Indicates that a [`Resource`] is of an indeterminate dynamic scope.
pub struct DynamicResourceScope {}
impl ResourceScope for DynamicResourceScope {}
/// An accessor trait for a kubernetes Resource.
///
/// This is for a subset of Kubernetes type that do not end in `List`.
/// These types, using [`ObjectMeta`], SHOULD all have required properties:
/// - `.metadata`
/// - `.metadata.name`
///
/// And these optional properties:
/// - `.metadata.namespace`
/// - `.metadata.resource_version`
///
/// This avoids a bunch of the unnecessary unwrap mechanics for apps.
pub trait Resource {
/// Type information for types that do not know their resource information at compile time.
///
/// Types that know their metadata at compile time should select `DynamicType = ()`.
/// Types that require some information at runtime should select `DynamicType`
/// as type of this information.
///
/// See [`DynamicObject`](crate::dynamic::DynamicObject) for a valid implementation of non-k8s-openapi resources.
type DynamicType: Send + Sync + 'static;
/// Type information for the api scope of the resource when known at compile time
///
/// Types from k8s_openapi come with an explicit k8s_openapi::ResourceScope
/// Dynamic types should select `Scope = DynamicResourceScope`
type Scope;
/// Returns kind of this object
fn kind(dt: &Self::DynamicType) -> Cow<'_, str>;
/// Returns group of this object
fn group(dt: &Self::DynamicType) -> Cow<'_, str>;
/// Returns version of this object
fn version(dt: &Self::DynamicType) -> Cow<'_, str>;
/// Returns apiVersion of this object
fn api_version(dt: &Self::DynamicType) -> Cow<'_, str> {
api_version_from_group_version(Self::group(dt), Self::version(dt))
}
/// Returns the plural name of the kind
///
/// This is known as the resource in apimachinery, we rename it for disambiguation.
fn plural(dt: &Self::DynamicType) -> Cow<'_, str>;
/// Creates a url path for http requests for this resource
fn url_path(dt: &Self::DynamicType, namespace: Option<&str>) -> String {
let n = if let Some(ns) = namespace {
format!("namespaces/{ns}/")
} else {
"".into()
};
let group = Self::group(dt);
let api_version = Self::api_version(dt);
let plural = Self::plural(dt);
format!(
"/{group}/{api_version}/{namespaces}{plural}",
group = if group.is_empty() { "api" } else { "apis" },
api_version = api_version,
namespaces = n,
plural = plural
)
}
/// Metadata that all persisted resources must have
fn meta(&self) -> &ObjectMeta;
/// Metadata that all persisted resources must have
fn meta_mut(&mut self) -> &mut ObjectMeta;
/// Generates an object reference for the resource
fn object_ref(&self, dt: &Self::DynamicType) -> ObjectReference {
let meta = self.meta();
ObjectReference {
name: meta.name.clone(),
namespace: meta.namespace.clone(),
uid: meta.uid.clone(),
api_version: Some(Self::api_version(dt).to_string()),
kind: Some(Self::kind(dt).to_string()),
..Default::default()
}
}
/// Generates a controller owner reference pointing to this resource
///
/// Note: this returns an `Option`, but for objects populated from the apiserver,
/// this Option can be safely unwrapped.
///
/// ```
/// use k8s_openapi::api::core::v1::ConfigMap;
/// use k8s_openapi::api::core::v1::Pod;
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
/// use kube_core::Resource;
///
/// let p = Pod::default();
/// let controller_ref = p.controller_owner_ref(&());
/// let cm = ConfigMap {
/// metadata: ObjectMeta {
/// name: Some("pod-configmap".to_string()),
/// owner_references: Some(controller_ref.into_iter().collect()),
/// ..ObjectMeta::default()
/// },
/// ..Default::default()
/// };
/// ```
fn controller_owner_ref(&self, dt: &Self::DynamicType) -> Option<OwnerReference> {
Some(OwnerReference {
controller: Some(true),
..self.owner_ref(dt)?
})
}
/// Generates an owner reference pointing to this resource
///
/// Note: this returns an `Option`, but for objects populated from the apiserver,
/// this Option can be safely unwrapped.
///
/// ```
/// use k8s_openapi::api::core::v1::ConfigMap;
/// use k8s_openapi::api::core::v1::Pod;
/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
/// use kube_core::Resource;
///
/// let p = Pod::default();
/// let owner_ref = p.owner_ref(&());
/// let cm = ConfigMap {
/// metadata: ObjectMeta {
/// name: Some("pod-configmap".to_string()),
/// owner_references: Some(owner_ref.into_iter().collect()),
/// ..ObjectMeta::default()
/// },
/// ..Default::default()
/// };
/// ```
fn owner_ref(&self, dt: &Self::DynamicType) -> Option<OwnerReference> {
let meta = self.meta();
Some(OwnerReference {
api_version: Self::api_version(dt).to_string(),
kind: Self::kind(dt).to_string(),
name: meta.name.clone()?,
uid: meta.uid.clone()?,
..OwnerReference::default()
})
}
}
/// Helper function that creates the `apiVersion` field from the group and version strings.
pub fn api_version_from_group_version<'a>(group: Cow<'a, str>, version: Cow<'a, str>) -> Cow<'a, str> {
if group.is_empty() {
return version;
}
let mut output = group;
output.to_mut().push('/');
output.to_mut().push_str(&version);
output
}
/// Implement accessor trait for any ObjectMeta-using Kubernetes Resource
impl<K, S> Resource for K
where
K: k8s_openapi::Metadata<Ty = ObjectMeta>,
K: k8s_openapi::Resource<Scope = S>,
{
type DynamicType = ();
type Scope = S;
fn kind(_: &()) -> Cow<'_, str> {
K::KIND.into()
}
fn group(_: &()) -> Cow<'_, str> {
K::GROUP.into()
}
fn version(_: &()) -> Cow<'_, str> {
K::VERSION.into()
}
fn api_version(_: &()) -> Cow<'_, str> {
K::API_VERSION.into()
}
fn plural(_: &()) -> Cow<'_, str> {
K::URL_PATH_SEGMENT.into()
}
fn meta(&self) -> &ObjectMeta {
self.metadata()
}
fn meta_mut(&mut self) -> &mut ObjectMeta {
self.metadata_mut()
}
}
/// Helper methods for resources.
pub trait ResourceExt: Resource {
/// Returns the name of the resource, panicking if it is unset
///
/// Only use this function if you know that name is set; for example when
/// the resource was received from the apiserver (post-admission),
/// or if you constructed the resource with the name.
///
/// At admission, `.metadata.generateName` can be set instead of name
/// and in those cases this function can panic.
///
/// Prefer using `.meta().name` or [`name_any`](ResourceExt::name_any)
/// for the more general cases.
fn name_unchecked(&self) -> String;
/// Returns the most useful name identifier available
///
/// This is tries `name`, then `generateName`, and falls back on an empty string when neither is set.
/// Generally you always have one of the two unless you are creating the object locally.
///
/// This is intended to provide something quick and simple for standard logging purposes.
/// For more precise use cases, prefer doing your own defaulting.
/// For true uniqueness, prefer [`uid`](ResourceExt::uid).
fn name_any(&self) -> String;
/// The namespace the resource is in
fn namespace(&self) -> Option<String>;
/// The resource version
fn resource_version(&self) -> Option<String>;
/// Unique ID (if you delete resource and then create a new
/// resource with the same name, it will have different ID)
fn uid(&self) -> Option<String>;
/// Returns the creation timestamp
///
/// This is guaranteed to exist on resources received by the apiserver.
fn creation_timestamp(&self) -> Option<Time>;
/// Returns resource labels
fn labels(&self) -> &BTreeMap<String, String>;
/// Provides mutable access to the labels
fn labels_mut(&mut self) -> &mut BTreeMap<String, String>;
/// Returns resource annotations
fn annotations(&self) -> &BTreeMap<String, String>;
/// Provider mutable access to the annotations
fn annotations_mut(&mut self) -> &mut BTreeMap<String, String>;
/// Returns resource owner references
fn owner_references(&self) -> &[OwnerReference];
/// Provides mutable access to the owner references
fn owner_references_mut(&mut self) -> &mut Vec<OwnerReference>;
/// Returns resource finalizers
fn finalizers(&self) -> &[String];
/// Provides mutable access to the finalizers
fn finalizers_mut(&mut self) -> &mut Vec<String>;
/// Returns managed fields
fn managed_fields(&self) -> &[ManagedFieldsEntry];
/// Provides mutable access to managed fields
fn managed_fields_mut(&mut self) -> &mut Vec<ManagedFieldsEntry>;
}
static EMPTY_MAP: BTreeMap<String, String> = BTreeMap::new();
impl<K: Resource> ResourceExt for K {
fn name_unchecked(&self) -> String {
self.meta().name.clone().expect(".metadata.name missing")
}
fn name_any(&self) -> String {
self.meta()
.name
.clone()
.or_else(|| self.meta().generate_name.clone())
.unwrap_or_default()
}
fn namespace(&self) -> Option<String> {
self.meta().namespace.clone()
}
fn resource_version(&self) -> Option<String> {
self.meta().resource_version.clone()
}
fn uid(&self) -> Option<String> {
self.meta().uid.clone()
}
fn creation_timestamp(&self) -> Option<Time> {
self.meta().creation_timestamp.clone()
}
fn labels(&self) -> &BTreeMap<String, String> {
self.meta().labels.as_ref().unwrap_or(&EMPTY_MAP)
}
fn labels_mut(&mut self) -> &mut BTreeMap<String, String> {
self.meta_mut().labels.get_or_insert_with(BTreeMap::new)
}
fn annotations(&self) -> &BTreeMap<String, String> {
self.meta().annotations.as_ref().unwrap_or(&EMPTY_MAP)
}
fn annotations_mut(&mut self) -> &mut BTreeMap<String, String> {
self.meta_mut().annotations.get_or_insert_with(BTreeMap::new)
}
fn owner_references(&self) -> &[OwnerReference] {
self.meta().owner_references.as_deref().unwrap_or_default()
}
fn owner_references_mut(&mut self) -> &mut Vec<OwnerReference> {
self.meta_mut().owner_references.get_or_insert_with(Vec::new)
}
fn finalizers(&self) -> &[String] {
self.meta().finalizers.as_deref().unwrap_or_default()
}
fn finalizers_mut(&mut self) -> &mut Vec<String> {
self.meta_mut().finalizers.get_or_insert_with(Vec::new)
}
fn managed_fields(&self) -> &[ManagedFieldsEntry] {
self.meta().managed_fields.as_deref().unwrap_or_default()
}
fn managed_fields_mut(&mut self) -> &mut Vec<ManagedFieldsEntry> {
self.meta_mut().managed_fields.get_or_insert_with(Vec::new)
}
}

177
vendor/kube-core/src/response.rs vendored Normal file
View File

@@ -0,0 +1,177 @@
//! Generic api response types
use serde::{Deserialize, Serialize};
/// A Kubernetes status object
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
pub struct Status {
/// Status of the operation
///
/// One of: `Success` or `Failure` - [more info](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<StatusSummary>,
/// Suggested HTTP return code (0 if unset)
#[serde(default, skip_serializing_if = "is_u16_zero")]
pub code: u16,
/// A human-readable description of the status of this operation
#[serde(default, skip_serializing_if = "String::is_empty")]
pub message: String,
/// A machine-readable description of why this operation is in the “Failure” status.
///
/// If this value is empty there is no information available.
/// A Reason clarifies an HTTP status code but does not override it.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub reason: String,
/// Extended data associated with the reason.
///
/// Each reason may define its own extended details.
/// This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<StatusDetails>,
}
impl Status {
/// Returns a successful `Status`
pub fn success() -> Self {
Status {
status: Some(StatusSummary::Success),
code: 0,
message: String::new(),
reason: String::new(),
details: None,
}
}
/// Returns an unsuccessful `Status`
pub fn failure(message: &str, reason: &str) -> Self {
Status {
status: Some(StatusSummary::Failure),
code: 0,
message: message.to_string(),
reason: reason.to_string(),
details: None,
}
}
/// Sets an explicit HTTP status code
pub fn with_code(mut self, code: u16) -> Self {
self.code = code;
self
}
/// Adds details to the `Status`
pub fn with_details(mut self, details: StatusDetails) -> Self {
self.details = Some(details);
self
}
/// Checks if this `Status` represents success
///
/// Note that it is possible for `Status` to be in indeterminate state
/// when both `is_success` and `is_failure` return false.
pub fn is_success(&self) -> bool {
self.status == Some(StatusSummary::Success)
}
/// Checks if this `Status` represents failure
///
/// Note that it is possible for `Status` to be in indeterminate state
/// when both `is_success` and `is_failure` return false.
pub fn is_failure(&self) -> bool {
self.status == Some(StatusSummary::Failure)
}
}
/// Overall status of the operation - whether it succeeded or not
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum StatusSummary {
/// Operation succeeded
Success,
/// Operation failed
Failure,
}
/// Status details object on the [`Status`] object
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct StatusDetails {
/// The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described)
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
/// The group attribute of the resource associated with the status StatusReason
#[serde(default, skip_serializing_if = "String::is_empty")]
pub group: String,
/// The kind attribute of the resource associated with the status StatusReason
///
/// On some operations may differ from the requested resource Kind - [more info](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds)
#[serde(default, skip_serializing_if = "String::is_empty")]
pub kind: String,
/// UID of the resource (when there is a single resource which can be described)
///
/// [More info](http://kubernetes.io/docs/user-guide/identifiers#uids)
#[serde(default, skip_serializing_if = "String::is_empty")]
pub uid: String,
/// The Causes vector includes more details associated with the failure
///
/// Not all StatusReasons may provide detailed causes.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub causes: Vec<StatusCause>,
/// If specified, the time in seconds before the operation should be retried.
///
/// Some errors may indicate the client must take an alternate action -
/// for those errors this field may indicate how long to wait before taking the alternate action.
#[serde(default, skip_serializing_if = "is_u32_zero")]
pub retry_after_seconds: u32,
}
/// Status cause object on the [`StatusDetails`] object
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct StatusCause {
/// A machine-readable description of the cause of the error. If this value is empty there is no information available.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub reason: String,
/// A human-readable description of the cause of the error. This field may be presented as-is to a reader.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub message: String,
/// The field of the resource that has caused this error, as named by its JSON serialization
///
/// May include dot and postfix notation for nested attributes. Arrays are zero-indexed.
/// Fields may appear more than once in an array of causes due to fields having multiple errors.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub field: String,
}
fn is_u16_zero(&v: &u16) -> bool {
v == 0
}
fn is_u32_zero(&v: &u32) -> bool {
v == 0
}
#[cfg(test)]
mod test {
use super::Status;
// ensure our status schema is sensible
#[test]
fn delete_deserialize_test() {
let statusresp = r#"{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","details":{"name":"some-app","group":"clux.dev","kind":"foos","uid":"1234-some-uid"}}"#;
let s: Status = serde_json::from_str::<Status>(statusresp).unwrap();
assert_eq!(s.details.unwrap().name, "some-app");
let statusnoname = r#"{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","details":{"group":"clux.dev","kind":"foos","uid":"1234-some-uid"}}"#;
let s2: Status = serde_json::from_str::<Status>(statusnoname).unwrap();
assert_eq!(s2.details.unwrap().name, ""); // optional probably better..
}
}

196
vendor/kube-core/src/schema.rs vendored Normal file
View File

@@ -0,0 +1,196 @@
//! Utilities for managing [`CustomResourceDefinition`] schemas
//!
//! [`CustomResourceDefinition`]: `k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition`
// Used in docs
#[allow(unused_imports)] use schemars::gen::SchemaSettings;
use schemars::{
schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec},
visit::Visitor,
MapEntry,
};
/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
///
/// The following two transformations are applied
/// * Rewrite enums from `oneOf` to `object`s with multiple variants ([schemars#84](https://github.com/GREsau/schemars/issues/84))
/// * Rewrite untagged enums from `anyOf` to `object`s with multiple variants ([kube#1028](https://github.com/kube-rs/kube/pull/1028))
/// * Rewrite `additionalProperties` from `#[serde(flatten)]` to `x-kubernetes-preserve-unknown-fields` ([kube#844](https://github.com/kube-rs/kube/issues/844))
///
/// This is used automatically by `kube::derive`'s `#[derive(CustomResource)]`,
/// but it can also be used manually with [`SchemaSettings::with_visitor`].
///
/// # Panics
///
/// The [`Visitor`] functions may panic if the transform could not be applied. For example,
/// there must not be any overlapping properties between `oneOf` branches.
#[derive(Debug, Clone)]
pub struct StructuralSchemaRewriter;
impl Visitor for StructuralSchemaRewriter {
fn visit_schema_object(&mut self, schema: &mut schemars::schema::SchemaObject) {
schemars::visit::visit_schema_object(self, schema);
if let Some(subschemas) = &mut schema.subschemas {
if let Some(one_of) = subschemas.one_of.as_mut() {
// Tagged enums are serialized using `one_of`
hoist_subschema_properties(one_of, &mut schema.object, &mut schema.instance_type);
// "Plain" enums are serialized using `one_of` if they have doc tags
hoist_subschema_enum_values(one_of, &mut schema.enum_values, &mut schema.instance_type);
if one_of.is_empty() {
subschemas.one_of = None;
}
}
if let Some(any_of) = &mut subschemas.any_of {
// Untagged enums are serialized using `any_of`
hoist_subschema_properties(any_of, &mut schema.object, &mut schema.instance_type);
}
}
// check for maps without with properties (i.e. flattened maps)
// and allow these to persist dynamically
if let Some(object) = &mut schema.object {
if !object.properties.is_empty()
&& object.additional_properties.as_deref() == Some(&Schema::Bool(true))
{
object.additional_properties = None;
schema
.extensions
.insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
}
}
// As of version 1.30 Kubernetes does not support setting `uniqueItems` to `true`,
// so we need to remove this fields.
// Users can still set `x-kubernetes-list-type=set` in case they want the apiserver
// to do validation, but we can't make an assumption about the Set contents here.
// See https://kubernetes.io/docs/reference/using-api/server-side-apply/ for details.
if let Some(array) = &mut schema.array {
array.unique_items = None;
}
}
}
/// Bring all plain enum values up to the root schema,
/// since Kubernetes doesn't allow subschemas to define enum options.
///
/// (Enum here means a list of hard-coded values, not a tagged union.)
fn hoist_subschema_enum_values(
subschemas: &mut Vec<Schema>,
common_enum_values: &mut Option<Vec<serde_json::Value>>,
instance_type: &mut Option<SingleOrVec<InstanceType>>,
) {
subschemas.retain(|variant| {
if let Schema::Object(SchemaObject {
instance_type: variant_type,
enum_values: Some(variant_enum_values),
..
}) = variant
{
if let Some(variant_type) = variant_type {
match instance_type {
None => *instance_type = Some(variant_type.clone()),
Some(tpe) => {
if tpe != variant_type {
panic!("Enum variant set {variant_enum_values:?} has type {variant_type:?} but was already defined as {instance_type:?}. The instance type must be equal for all subschema variants.")
}
}
}
}
common_enum_values
.get_or_insert_with(Vec::new)
.extend(variant_enum_values.iter().cloned());
false
} else {
true
}
})
}
/// Bring all property definitions from subschemas up to the root schema,
/// since Kubernetes doesn't allow subschemas to define properties.
fn hoist_subschema_properties(
subschemas: &mut Vec<Schema>,
common_obj: &mut Option<Box<ObjectValidation>>,
instance_type: &mut Option<SingleOrVec<InstanceType>>,
) {
for variant in subschemas {
if let Schema::Object(SchemaObject {
instance_type: variant_type,
object: Some(variant_obj),
metadata: variant_metadata,
..
}) = variant
{
let common_obj = common_obj.get_or_insert_with(Box::<ObjectValidation>::default);
if let Some(variant_metadata) = variant_metadata {
// Move enum variant description from oneOf clause to its corresponding property
if let Some(description) = std::mem::take(&mut variant_metadata.description) {
if let Some(Schema::Object(variant_object)) =
only_item(variant_obj.properties.values_mut())
{
let metadata = variant_object
.metadata
.get_or_insert_with(Box::<Metadata>::default);
metadata.description = Some(description);
}
}
}
// Move all properties
let variant_properties = std::mem::take(&mut variant_obj.properties);
for (property_name, property) in variant_properties {
match common_obj.properties.entry(property_name) {
MapEntry::Vacant(entry) => {
entry.insert(property);
}
MapEntry::Occupied(entry) => {
if &property != entry.get() {
panic!("Property {:?} has the schema {:?} but was already defined as {:?} in another subschema. The schemas for a property used in multiple subschemas must be identical",
entry.key(),
&property,
entry.get());
}
}
}
}
// Kubernetes doesn't allow variants to set additionalProperties
variant_obj.additional_properties = None;
merge_metadata(instance_type, variant_type.take());
}
}
}
fn only_item<I: Iterator>(mut i: I) -> Option<I::Item> {
let item = i.next()?;
if i.next().is_some() {
return None;
}
Some(item)
}
fn merge_metadata(
instance_type: &mut Option<SingleOrVec<InstanceType>>,
variant_type: Option<SingleOrVec<InstanceType>>,
) {
match (instance_type, variant_type) {
(_, None) => {}
(common_type @ None, variant_type) => {
*common_type = variant_type;
}
(Some(common_type), Some(variant_type)) => {
if *common_type != variant_type {
panic!(
"variant defined type {variant_type:?}, conflicting with existing type {common_type:?}"
);
}
}
}
}

455
vendor/kube-core/src/subresource.rs vendored Normal file
View File

@@ -0,0 +1,455 @@
//! Request builder types and parameters for subresources
use std::fmt::Debug;
use crate::{
params::{DeleteParams, PostParams},
request::{Error, Request, JSON_MIME},
};
pub use k8s_openapi::api::autoscaling::v1::{Scale, ScaleSpec, ScaleStatus};
// ----------------------------------------------------------------------------
// Log subresource
// ----------------------------------------------------------------------------
/// Params for logging
#[derive(Default, Clone, Debug)]
pub struct LogParams {
/// The container for which to stream logs. Defaults to only container if there is one container in the pod.
pub container: Option<String>,
/// Follow the log stream of the pod. Defaults to `false`.
pub follow: bool,
/// If set, the number of bytes to read from the server before terminating the log output.
/// This may not display a complete final line of logging, and may return slightly more or slightly less than the specified limit.
pub limit_bytes: Option<i64>,
/// If `true`, then the output is pretty printed.
pub pretty: bool,
/// Return previous terminated container logs. Defaults to `false`.
pub previous: bool,
/// A relative time in seconds before the current time from which to show logs.
/// If this value precedes the time a pod was started, only logs since the pod start will be returned.
/// If this value is in the future, no logs will be returned. Only one of sinceSeconds or sinceTime may be specified.
pub since_seconds: Option<i64>,
/// An RFC3339 timestamp from which to show logs. If this value
/// precedes the time a pod was started, only logs since the pod start will be returned.
/// If this value is in the future, no logs will be returned.
/// Only one of sinceSeconds or sinceTime may be specified.
pub since_time: Option<chrono::DateTime<chrono::Utc>>,
/// If set, the number of lines from the end of the logs to show.
/// If not specified, logs are shown from the creation of the container or sinceSeconds or sinceTime
pub tail_lines: Option<i64>,
/// If `true`, add an RFC3339 or RFC3339Nano timestamp at the beginning of every line of log output. Defaults to `false`.
pub timestamps: bool,
}
impl Request {
/// Get a pod logs
pub fn logs(&self, name: &str, lp: &LogParams) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}/{}/log?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
if let Some(container) = &lp.container {
qp.append_pair("container", container);
}
if lp.follow {
qp.append_pair("follow", "true");
}
if let Some(lb) = &lp.limit_bytes {
qp.append_pair("limitBytes", &lb.to_string());
}
if lp.pretty {
qp.append_pair("pretty", "true");
}
if lp.previous {
qp.append_pair("previous", "true");
}
if let Some(ss) = &lp.since_seconds {
qp.append_pair("sinceSeconds", &ss.to_string());
} else if let Some(st) = &lp.since_time {
let ser_since = st.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
qp.append_pair("sinceTime", &ser_since);
}
if let Some(tl) = &lp.tail_lines {
qp.append_pair("tailLines", &tl.to_string());
}
if lp.timestamps {
qp.append_pair("timestamps", "true");
}
let urlstr = qp.finish();
let req = http::Request::get(urlstr);
req.body(vec![]).map_err(Error::BuildRequest)
}
}
// ----------------------------------------------------------------------------
// Eviction subresource
// ----------------------------------------------------------------------------
/// Params for evictable objects
#[derive(Default, Clone)]
pub struct EvictParams {
/// How the eviction should occur
pub delete_options: Option<DeleteParams>,
/// How the http post should occur
pub post_options: PostParams,
}
impl Request {
/// Create an eviction
pub fn evict(&self, name: &str, ep: &EvictParams) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}/{}/eviction?", self.url_path, name);
// This is technically identical to Request::create, but different url
let pp = &ep.post_options;
pp.validate()?;
let mut qp = form_urlencoded::Serializer::new(target);
pp.populate_qp(&mut qp);
let urlstr = qp.finish();
// eviction body parameters are awkward, need metadata with name
let data = serde_json::to_vec(&serde_json::json!({
"delete_options": ep.delete_options,
"metadata": { "name": name }
}))
.map_err(Error::SerializeBody)?;
let req = http::Request::post(urlstr).header(http::header::CONTENT_TYPE, JSON_MIME);
req.body(data).map_err(Error::BuildRequest)
}
}
// ----------------------------------------------------------------------------
// Attach subresource
// ----------------------------------------------------------------------------
/// Parameters for attaching to a container in a Pod.
///
/// - One of `stdin`, `stdout`, or `stderr` must be `true`.
/// - `stderr` and `tty` cannot both be `true` because multiplexing is not supported with TTY.
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
#[derive(Debug)]
pub struct AttachParams {
/// The name of the container to attach.
/// Defaults to the only container if there is only one container in the pod.
pub container: Option<String>,
/// Attach to the container's standard input. Defaults to `false`.
///
/// Call [`AttachedProcess::stdin`](https://docs.rs/kube/*/kube/api/struct.AttachedProcess.html#method.stdin) to obtain a writer.
pub stdin: bool,
/// Attach to the container's standard output. Defaults to `true`.
///
/// Call [`AttachedProcess::stdout`](https://docs.rs/kube/*/kube/api/struct.AttachedProcess.html#method.stdout) to obtain a reader.
pub stdout: bool,
/// Attach to the container's standard error. Defaults to `true`.
///
/// Call [`AttachedProcess::stderr`](https://docs.rs/kube/*/kube/api/struct.AttachedProcess.html#method.stderr) to obtain a reader.
pub stderr: bool,
/// Allocate TTY. Defaults to `false`.
///
/// NOTE: Terminal resizing is not implemented yet.
pub tty: bool,
/// The maximum amount of bytes that can be written to the internal `stdin`
/// pipe before the write returns `Poll::Pending`.
/// Defaults to 1024.
///
/// This is not sent to the server.
pub max_stdin_buf_size: Option<usize>,
/// The maximum amount of bytes that can be written to the internal `stdout`
/// pipe before the write returns `Poll::Pending`.
/// Defaults to 1024.
///
/// This is not sent to the server.
pub max_stdout_buf_size: Option<usize>,
/// The maximum amount of bytes that can be written to the internal `stderr`
/// pipe before the write returns `Poll::Pending`.
/// Defaults to 1024.
///
/// This is not sent to the server.
pub max_stderr_buf_size: Option<usize>,
}
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
impl Default for AttachParams {
// Default matching the server's defaults.
fn default() -> Self {
Self {
container: None,
stdin: false,
stdout: true,
stderr: true,
tty: false,
max_stdin_buf_size: None,
max_stdout_buf_size: None,
max_stderr_buf_size: None,
}
}
}
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
impl AttachParams {
/// Default parameters for an tty exec with stdin and stdout
#[must_use]
pub fn interactive_tty() -> Self {
Self {
stdin: true,
stdout: true,
stderr: false,
tty: true,
..Default::default()
}
}
/// Specify the container to execute in.
#[must_use]
pub fn container<T: Into<String>>(mut self, container: T) -> Self {
self.container = Some(container.into());
self
}
/// Set `stdin` field.
#[must_use]
pub fn stdin(mut self, enable: bool) -> Self {
self.stdin = enable;
self
}
/// Set `stdout` field.
#[must_use]
pub fn stdout(mut self, enable: bool) -> Self {
self.stdout = enable;
self
}
/// Set `stderr` field.
#[must_use]
pub fn stderr(mut self, enable: bool) -> Self {
self.stderr = enable;
self
}
/// Set `tty` field.
#[must_use]
pub fn tty(mut self, enable: bool) -> Self {
self.tty = enable;
self
}
/// Set `max_stdin_buf_size` field.
#[must_use]
pub fn max_stdin_buf_size(mut self, size: usize) -> Self {
self.max_stdin_buf_size = Some(size);
self
}
/// Set `max_stdout_buf_size` field.
#[must_use]
pub fn max_stdout_buf_size(mut self, size: usize) -> Self {
self.max_stdout_buf_size = Some(size);
self
}
/// Set `max_stderr_buf_size` field.
#[must_use]
pub fn max_stderr_buf_size(mut self, size: usize) -> Self {
self.max_stderr_buf_size = Some(size);
self
}
pub(crate) fn validate(&self) -> Result<(), Error> {
if !self.stdin && !self.stdout && !self.stderr {
return Err(Error::Validation(
"AttachParams: one of stdin, stdout, or stderr must be true".into(),
));
}
if self.stderr && self.tty {
// Multiplexing is not supported with TTY
return Err(Error::Validation(
"AttachParams: tty and stderr cannot both be true".into(),
));
}
Ok(())
}
fn append_to_url_serializer(&self, qp: &mut form_urlencoded::Serializer<String>) {
if self.stdin {
qp.append_pair("stdin", "true");
}
if self.stdout {
qp.append_pair("stdout", "true");
}
if self.stderr {
qp.append_pair("stderr", "true");
}
if self.tty {
qp.append_pair("tty", "true");
}
if let Some(container) = &self.container {
qp.append_pair("container", container);
}
}
#[cfg(feature = "kubelet-debug")]
// https://github.com/kubernetes/kubernetes/blob/466d9378dbb0a185df9680657f5cd96d5e5aab57/pkg/apis/core/types.go#L6005-L6013
pub(crate) fn append_to_url_serializer_local(&self, qp: &mut form_urlencoded::Serializer<String>) {
if self.stdin {
qp.append_pair("input", "1");
}
if self.stdout {
qp.append_pair("output", "1");
}
if self.stderr {
qp.append_pair("error", "1");
}
if self.tty {
qp.append_pair("tty", "1");
}
}
}
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
impl Request {
/// Attach to a pod
pub fn attach(&self, name: &str, ap: &AttachParams) -> Result<http::Request<Vec<u8>>, Error> {
ap.validate()?;
let target = format!("{}/{}/attach?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
ap.append_to_url_serializer(&mut qp);
let req = http::Request::get(qp.finish());
req.body(vec![]).map_err(Error::BuildRequest)
}
}
// ----------------------------------------------------------------------------
// Exec subresource
// ----------------------------------------------------------------------------
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
impl Request {
/// Execute command in a pod
pub fn exec<I, T>(
&self,
name: &str,
command: I,
ap: &AttachParams,
) -> Result<http::Request<Vec<u8>>, Error>
where
I: IntoIterator<Item = T>,
T: Into<String>,
{
ap.validate()?;
let target = format!("{}/{}/exec?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
ap.append_to_url_serializer(&mut qp);
for c in command.into_iter() {
qp.append_pair("command", &c.into());
}
let req = http::Request::get(qp.finish());
req.body(vec![]).map_err(Error::BuildRequest)
}
}
// ----------------------------------------------------------------------------
// Portforward subresource
// ----------------------------------------------------------------------------
#[cfg(feature = "ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
impl Request {
/// Request to forward ports of a pod
pub fn portforward(&self, name: &str, ports: &[u16]) -> Result<http::Request<Vec<u8>>, Error> {
if ports.is_empty() {
return Err(Error::Validation("ports cannot be empty".into()));
}
if ports.len() > 128 {
return Err(Error::Validation(
"the number of ports cannot be more than 128".into(),
));
}
if ports.len() > 1 {
let mut seen = std::collections::HashSet::with_capacity(ports.len());
for port in ports.iter() {
if seen.contains(port) {
return Err(Error::Validation(format!(
"ports must be unique, found multiple {port}"
)));
}
seen.insert(port);
}
}
let base_url = format!("{}/{}/portforward?", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(base_url);
qp.append_pair(
"ports",
&ports.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(","),
);
let req = http::Request::get(qp.finish());
req.body(vec![]).map_err(Error::BuildRequest)
}
}
// ----------------------------------------------------------------------------
// tests
// ----------------------------------------------------------------------------
/// Cheap sanity check to ensure type maps work as expected
#[cfg(test)]
mod test {
use crate::{request::Request, resource::Resource};
use chrono::{DateTime, TimeZone, Utc};
use k8s::core::v1 as corev1;
use k8s_openapi::api as k8s;
use crate::subresource::LogParams;
#[test]
fn logs_all_params() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let lp = LogParams {
container: Some("nginx".into()),
follow: true,
limit_bytes: Some(10 * 1024 * 1024),
pretty: true,
previous: true,
since_seconds: Some(3600),
since_time: None,
tail_lines: Some(4096),
timestamps: true,
};
let req = Request::new(url).logs("mypod", &lp).unwrap();
assert_eq!(req.uri(), "/api/v1/namespaces/ns/pods/mypod/log?&container=nginx&follow=true&limitBytes=10485760&pretty=true&previous=true&sinceSeconds=3600&tailLines=4096&timestamps=true");
}
#[test]
fn logs_since_time() {
let url = corev1::Pod::url_path(&(), Some("ns"));
let date: DateTime<Utc> = Utc.with_ymd_and_hms(2023, 10, 19, 13, 14, 26).unwrap();
let lp = LogParams {
since_seconds: None,
since_time: Some(date),
..Default::default()
};
let req = Request::new(url).logs("mypod", &lp).unwrap();
assert_eq!(
req.uri(),
"/api/v1/namespaces/ns/pods/mypod/log?&sinceTime=2023-10-19T13%3A14%3A26Z" // cross-referenced with kubectl
);
}
}

93
vendor/kube-core/src/util.rs vendored Normal file
View File

@@ -0,0 +1,93 @@
//! Utils and helpers
use crate::{
params::{Patch, PatchParams},
request, Request,
};
use chrono::Utc;
use k8s_openapi::api::apps::v1::{DaemonSet, Deployment, ReplicaSet, StatefulSet};
/// Restartable Resource marker trait
pub trait Restart {}
impl Restart for Deployment {}
impl Restart for DaemonSet {}
impl Restart for StatefulSet {}
impl Restart for ReplicaSet {}
impl Request {
/// Restart a resource
pub fn restart(&self, name: &str) -> Result<http::Request<Vec<u8>>, request::Error> {
let patch = serde_json::json!({
"spec": {
"template": {
"metadata": {
"annotations": {
"kube.kubernetes.io/restartedAt": Utc::now().to_rfc3339()
}
}
}
}
});
let pparams = PatchParams::default();
self.patch(name, &pparams, &Patch::Merge(patch))
}
}
impl Request {
/// Cordon a resource
pub fn cordon(&self, name: &str) -> Result<http::Request<Vec<u8>>, request::Error> {
self.set_unschedulable(name, true)
}
/// Uncordon a resource
pub fn uncordon(&self, name: &str) -> Result<http::Request<Vec<u8>>, request::Error> {
self.set_unschedulable(name, false)
}
fn set_unschedulable(
&self,
node_name: &str,
value: bool,
) -> Result<http::Request<Vec<u8>>, request::Error> {
self.patch(
node_name,
&PatchParams::default(),
&Patch::Strategic(serde_json::json!({ "spec": { "unschedulable": value } })),
)
}
}
#[cfg(test)]
mod test {
use crate::{params::Patch, request::Request, resource::Resource};
#[test]
fn restart_patch_is_correct() {
use k8s_openapi::api::apps::v1 as appsv1;
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let req = Request::new(url).restart("mydeploy").unwrap();
assert_eq!(req.uri(), "/apis/apps/v1/namespaces/ns/deployments/mydeploy?");
assert_eq!(req.method(), "PATCH");
assert_eq!(
req.headers().get("Content-Type").unwrap().to_str().unwrap(),
Patch::Merge(()).content_type()
);
}
#[test]
fn cordon_patch_is_correct() {
use k8s_openapi::api::core::v1::Node;
let url = Node::url_path(&(), Some("ns"));
let req = Request::new(url).cordon("mynode").unwrap();
assert_eq!(req.uri(), "/api/v1/namespaces/ns/nodes/mynode?");
assert_eq!(req.method(), "PATCH");
assert_eq!(
req.headers().get("Content-Type").unwrap().to_str().unwrap(),
Patch::Strategic(()).content_type()
);
}
}

394
vendor/kube-core/src/version.rs vendored Normal file
View File

@@ -0,0 +1,394 @@
use std::{cmp::Reverse, convert::Infallible, str::FromStr};
/// Version parser for Kubernetes version patterns
///
/// This type implements two orderings for sorting by:
///
/// - [`Version::priority`] for [Kubernetes/kubectl version priority](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-priority)
/// - [`Version::generation`] for sorting strictly by version generation in a semver style
///
/// To get the api versions sorted by `kubectl` priority:
///
/// ```
/// use kube_core::Version;
/// use std::cmp::Reverse; // for DESCENDING sort
/// let mut versions = vec![
/// "v10beta3",
/// "v2",
/// "foo10",
/// "v1",
/// "v3beta1",
/// "v11alpha2",
/// "v11beta2",
/// "v12alpha1",
/// "foo1",
/// "v10",
/// ];
/// versions.sort_by_cached_key(|v| Reverse(Version::parse(v).priority()));
/// assert_eq!(versions, vec![
/// "v10",
/// "v2",
/// "v1",
/// "v11beta2",
/// "v10beta3",
/// "v3beta1",
/// "v12alpha1",
/// "v11alpha2",
/// "foo1",
/// "foo10",
/// ]);
/// ```
///
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum Version {
/// A major/GA release
///
/// Always considered higher priority than a beta release.
Stable(u32),
/// A beta release for a specific major version
///
/// Always considered higher priority than an alpha release.
Beta(u32, Option<u32>),
/// An alpha release for a specific major version
///
/// Always considered higher priority than a nonconformant version
Alpha(u32, Option<u32>),
/// An non-conformant api string
///
/// CRDs and APIServices can use arbitrary strings as versions.
Nonconformant(String),
}
impl Version {
fn try_parse(v: &str) -> Option<Version> {
let v = v.strip_prefix('v')?;
let major = v.split_terminator(|ch: char| !ch.is_ascii_digit()).next()?;
let v = &v[major.len()..];
let major: u32 = major.parse().ok()?;
if v.is_empty() {
return Some(Version::Stable(major));
}
if let Some(suf) = v.strip_prefix("alpha") {
return if suf.is_empty() {
Some(Version::Alpha(major, None))
} else {
Some(Version::Alpha(major, Some(suf.parse().ok()?)))
};
}
if let Some(suf) = v.strip_prefix("beta") {
return if suf.is_empty() {
Some(Version::Beta(major, None))
} else {
Some(Version::Beta(major, Some(suf.parse().ok()?)))
};
}
None
}
/// An infallble parse of a Kubernetes version string
///
/// ```
/// use kube_core::Version;
/// assert_eq!(Version::parse("v10beta12"), Version::Beta(10, Some(12)));
/// ```
pub fn parse(v: &str) -> Version {
match Self::try_parse(v) {
Some(ver) => ver,
None => Version::Nonconformant(v.to_string()),
}
}
}
/// An infallible FromStr implementation for more generic users
impl FromStr for Version {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Version::parse(s))
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum Stability {
Nonconformant,
Alpha,
Beta,
Stable,
}
/// See [`Version::priority`]
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Priority {
stability: Stability,
major: u32,
minor: Option<u32>,
nonconformant: Option<Reverse<String>>,
}
/// See [`Version::generation`]
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Generation {
major: u32,
stability: Stability,
minor: Option<u32>,
nonconformant: Option<Reverse<String>>,
}
impl Version {
/// An [`Ord`] for `Version` that orders by [Kubernetes version priority](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-priority)
///
/// This order will favour stable versions over newer pre-releases and is used by `kubectl`.
///
/// For example:
///
/// ```
/// # use kube_core::Version;
/// assert!(Version::Stable(2).priority() > Version::Stable(1).priority());
/// assert!(Version::Stable(1).priority() > Version::Beta(1, None).priority());
/// assert!(Version::Stable(1).priority() > Version::Beta(2, None).priority());
/// assert!(Version::Stable(2).priority() > Version::Alpha(1, Some(2)).priority());
/// assert!(Version::Stable(1).priority() > Version::Alpha(2, Some(2)).priority());
/// assert!(Version::Beta(1, None).priority() > Version::Nonconformant("ver3".into()).priority());
/// ```
///
/// Note that the type of release matters more than the version numbers:
/// `Stable(x)` > `Beta(y)` > `Alpha(z)` > `Nonconformant(w)` for all `x`,`y`,`z`,`w`
///
/// `Nonconformant` versions are ordered alphabetically.
pub fn priority(&self) -> impl Ord {
match self {
&Self::Stable(major) => Priority {
stability: Stability::Stable,
major,
minor: None,
nonconformant: None,
},
&Self::Beta(major, minor) => Priority {
stability: Stability::Beta,
major,
minor,
nonconformant: None,
},
&Self::Alpha(major, minor) => Priority {
stability: Stability::Alpha,
major,
minor,
nonconformant: None,
},
Self::Nonconformant(nonconformant) => Priority {
stability: Stability::Nonconformant,
major: 0,
minor: None,
nonconformant: Some(Reverse(nonconformant.clone())),
},
}
}
/// An [`Ord`] for `Version` that orders by version generation
///
/// This order will favour higher version numbers even if it's a pre-release.
///
/// For example:
///
/// ```
/// # use kube_core::Version;
/// assert!(Version::Stable(2).generation() > Version::Stable(1).generation());
/// assert!(Version::Stable(1).generation() > Version::Beta(1, None).generation());
/// assert!(Version::Beta(2, None).generation() > Version::Stable(1).generation());
/// assert!(Version::Stable(2).generation() > Version::Alpha(1, Some(2)).generation());
/// assert!(Version::Alpha(2, Some(2)).generation() > Version::Stable(1).generation());
/// assert!(Version::Beta(1, None).generation() > Version::Nonconformant("ver3".into()).generation());
/// ```
pub fn generation(&self) -> impl Ord {
match self {
&Self::Stable(major) => Generation {
stability: Stability::Stable,
major,
minor: None,
nonconformant: None,
},
&Self::Beta(major, minor) => Generation {
stability: Stability::Beta,
major,
minor,
nonconformant: None,
},
&Self::Alpha(major, minor) => Generation {
stability: Stability::Alpha,
major,
minor,
nonconformant: None,
},
Self::Nonconformant(nonconformant) => Generation {
stability: Stability::Nonconformant,
major: 0,
minor: None,
nonconformant: Some(Reverse(nonconformant.clone())),
},
}
}
}
#[cfg(test)]
mod tests {
use super::Version;
use std::{cmp::Reverse, str::FromStr};
#[test]
fn test_stable() {
assert_eq!(Version::parse("v1"), Version::Stable(1));
assert_eq!(Version::parse("v3"), Version::Stable(3));
assert_eq!(Version::parse("v10"), Version::Stable(10));
}
#[test]
fn test_prerelease() {
assert_eq!(Version::parse("v1beta"), Version::Beta(1, None));
assert_eq!(Version::parse("v2alpha1"), Version::Alpha(2, Some(1)));
assert_eq!(Version::parse("v10beta12"), Version::Beta(10, Some(12)));
}
fn check_not_parses(s: &str) {
assert_eq!(Version::parse(s), Version::Nonconformant(s.to_string()))
}
#[test]
fn test_nonconformant() {
check_not_parses("");
check_not_parses("foo");
check_not_parses("v");
check_not_parses("v-1");
check_not_parses("valpha");
check_not_parses("vbeta3");
check_not_parses("vv1");
check_not_parses("v1alpha1hi");
check_not_parses("v1zeta3");
}
#[test]
fn test_version_fromstr() {
assert_eq!(
Version::from_str("infallible").unwrap(),
Version::Nonconformant("infallible".to_string())
);
}
#[test]
fn test_version_priority_ord() {
// sorting makes sense from a "greater than" generation perspective:
assert!(Version::Stable(2).priority() > Version::Stable(1).priority());
assert!(Version::Stable(1).priority() > Version::Beta(1, None).priority());
assert!(Version::Stable(1).priority() > Version::Beta(2, None).priority());
assert!(Version::Stable(2).priority() > Version::Alpha(1, Some(2)).priority());
assert!(Version::Stable(1).priority() > Version::Alpha(2, Some(2)).priority());
assert!(Version::Beta(1, None).priority() > Version::Nonconformant("ver3".into()).priority());
assert!(Version::Stable(2).priority() > Version::Stable(1).priority());
assert!(Version::Stable(1).priority() > Version::Beta(2, None).priority());
assert!(Version::Stable(1).priority() > Version::Beta(2, Some(2)).priority());
assert!(Version::Stable(1).priority() > Version::Alpha(2, None).priority());
assert!(Version::Stable(1).priority() > Version::Alpha(2, Some(3)).priority());
assert!(Version::Stable(1).priority() > Version::Nonconformant("foo".to_string()).priority());
assert!(Version::Beta(1, Some(1)).priority() > Version::Beta(1, None).priority());
assert!(Version::Beta(1, Some(2)).priority() > Version::Beta(1, Some(1)).priority());
assert!(Version::Beta(1, None).priority() > Version::Alpha(1, None).priority());
assert!(Version::Beta(1, None).priority() > Version::Alpha(1, Some(3)).priority());
assert!(Version::Beta(1, None).priority() > Version::Nonconformant("foo".to_string()).priority());
assert!(Version::Beta(1, Some(2)).priority() > Version::Nonconformant("foo".to_string()).priority());
assert!(Version::Alpha(1, Some(1)).priority() > Version::Alpha(1, None).priority());
assert!(Version::Alpha(1, Some(2)).priority() > Version::Alpha(1, Some(1)).priority());
assert!(Version::Alpha(1, None).priority() > Version::Nonconformant("foo".to_string()).priority());
assert!(Version::Alpha(1, Some(2)).priority() > Version::Nonconformant("foo".to_string()).priority());
assert!(
Version::Nonconformant("bar".to_string()).priority()
> Version::Nonconformant("foo".to_string()).priority()
);
assert!(
Version::Nonconformant("foo1".to_string()).priority()
> Version::Nonconformant("foo10".to_string()).priority()
);
// sort orders by default are ascending
// sorting with std::cmp::Reverse on priority gives you the highest priority first
let mut vers = vec![
Version::Beta(2, Some(2)),
Version::Stable(1),
Version::Nonconformant("hi".into()),
Version::Alpha(3, Some(2)),
Version::Stable(2),
Version::Beta(2, Some(3)),
];
vers.sort_by_cached_key(|x| Reverse(x.priority()));
assert_eq!(vers, vec![
Version::Stable(2),
Version::Stable(1),
Version::Beta(2, Some(3)),
Version::Beta(2, Some(2)),
Version::Alpha(3, Some(2)),
Version::Nonconformant("hi".into()),
]);
}
#[test]
fn test_version_generation_ord() {
assert!(Version::Stable(2).generation() > Version::Stable(1).generation());
assert!(Version::Stable(1).generation() > Version::Beta(1, None).generation());
assert!(Version::Stable(1).generation() < Version::Beta(2, None).generation());
assert!(Version::Stable(2).generation() > Version::Alpha(1, Some(2)).generation());
assert!(Version::Stable(1).generation() < Version::Alpha(2, Some(2)).generation());
assert!(Version::Beta(1, None).generation() > Version::Nonconformant("ver3".into()).generation());
assert!(Version::Stable(2).generation() > Version::Stable(1).generation());
assert!(Version::Stable(1).generation() < Version::Beta(2, None).generation());
assert!(Version::Stable(1).generation() < Version::Beta(2, Some(2)).generation());
assert!(Version::Stable(1).generation() < Version::Alpha(2, None).generation());
assert!(Version::Stable(1).generation() < Version::Alpha(2, Some(3)).generation());
assert!(Version::Stable(1).generation() > Version::Nonconformant("foo".to_string()).generation());
assert!(Version::Beta(1, Some(1)).generation() > Version::Beta(1, None).generation());
assert!(Version::Beta(1, Some(2)).generation() > Version::Beta(1, Some(1)).generation());
assert!(Version::Beta(1, None).generation() > Version::Alpha(1, None).generation());
assert!(Version::Beta(1, None).generation() > Version::Alpha(1, Some(3)).generation());
assert!(Version::Beta(1, None).generation() > Version::Nonconformant("foo".to_string()).generation());
assert!(
Version::Beta(1, Some(2)).generation() > Version::Nonconformant("foo".to_string()).generation()
);
assert!(Version::Alpha(1, Some(1)).generation() > Version::Alpha(1, None).generation());
assert!(Version::Alpha(1, Some(2)).generation() > Version::Alpha(1, Some(1)).generation());
assert!(
Version::Alpha(1, None).generation() > Version::Nonconformant("foo".to_string()).generation()
);
assert!(
Version::Alpha(1, Some(2)).generation() > Version::Nonconformant("foo".to_string()).generation()
);
assert!(
Version::Nonconformant("bar".to_string()).generation()
> Version::Nonconformant("foo".to_string()).generation()
);
assert!(
Version::Nonconformant("foo1".to_string()).generation()
> Version::Nonconformant("foo10".to_string()).generation()
);
// sort orders by default is ascending
// sorting with std::cmp::Reverse on generation gives you the latest generation versions first
let mut vers = vec![
Version::Beta(2, Some(2)),
Version::Stable(1),
Version::Nonconformant("hi".into()),
Version::Alpha(3, Some(2)),
Version::Stable(2),
Version::Beta(2, Some(3)),
];
vers.sort_by_cached_key(|x| Reverse(x.generation()));
assert_eq!(vers, vec![
Version::Alpha(3, Some(2)),
Version::Stable(2),
Version::Beta(2, Some(3)),
Version::Beta(2, Some(2)),
Version::Stable(1),
Version::Nonconformant("hi".into()),
]);
}
}

67
vendor/kube-core/src/watch.rs vendored Normal file
View File

@@ -0,0 +1,67 @@
//! Types for the watch api
//!
//! See <https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes>
use crate::{error::ErrorResponse, metadata::TypeMeta};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// A raw event returned from a watch query
///
/// Note that a watch query returns many of these as newline separated JSON.
#[derive(Deserialize, Serialize, Clone)]
#[serde(tag = "type", content = "object", rename_all = "UPPERCASE")]
pub enum WatchEvent<K> {
/// Resource was added
Added(K),
/// Resource was modified
Modified(K),
/// Resource was deleted
Deleted(K),
/// Resource bookmark. `Bookmark` is a slimmed down `K` due to [#285](https://github.com/kube-rs/kube/issues/285).
///
/// From [Watch bookmarks](https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks).
///
/// NB: This became Beta first in Kubernetes 1.16.
Bookmark(Bookmark),
/// There was some kind of error
Error(ErrorResponse),
}
impl<K> Debug for WatchEvent<K> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {
WatchEvent::Added(_) => write!(f, "Added event"),
WatchEvent::Modified(_) => write!(f, "Modified event"),
WatchEvent::Deleted(_) => write!(f, "Deleted event"),
WatchEvent::Bookmark(_) => write!(f, "Bookmark event"),
WatchEvent::Error(e) => write!(f, "Error event: {e:?}"),
}
}
}
/// Slimed down K for [`WatchEvent::Bookmark`] due to [#285](https://github.com/kube-rs/kube/issues/285).
///
/// Can only be relied upon to have metadata with resource version.
/// Bookmarks contain apiVersion + kind + basically empty metadata.
#[derive(Serialize, Deserialize, Clone)]
pub struct Bookmark {
/// apiVersion + kind
#[serde(flatten)]
pub types: TypeMeta,
/// Basically empty metadata
pub metadata: BookmarkMeta,
}
/// Slimed down Metadata for WatchEvent::Bookmark
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct BookmarkMeta {
/// The only field we need from a Bookmark event.
pub resource_version: String,
/// Kubernetes 1.27 Streaming Lists
/// The rest of the fields are optional and may be empty.
#[serde(default)]
pub annotations: std::collections::BTreeMap<String, String>,
}