From 3b6e1a6ad7cdb87c4b0c34f95567ec865e5220a3 Mon Sep 17 00:00:00 2001 From: SOFe Date: Sun, 11 Feb 2024 12:48:02 +0800 Subject: [PATCH] feat: add RawJson implementor for ToObjectRef Motivation explained in #1389 Depends on #1393 to ease reflector bounds --- kube-runtime/Cargo.toml | 2 +- kube-runtime/src/lib.rs | 2 + kube-runtime/src/raw_json.rs | 293 +++++++++++++++++++++++ kube-runtime/src/reflector/object_ref.rs | 2 +- 4 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 kube-runtime/src/raw_json.rs diff --git a/kube-runtime/Cargo.toml b/kube-runtime/Cargo.toml index d06ae6ece..948aaa375 100644 --- a/kube-runtime/Cargo.toml +++ b/kube-runtime/Cargo.toml @@ -39,7 +39,7 @@ tokio = { version = "1.14.0", features = ["time"] } tokio-util = { version = "0.7.0", features = ["time"] } tracing = "0.1.36" json-patch = "1.0.0" -serde_json = "1.0.68" +serde_json = { version = "1.0.68", features = ["raw_value"] } thiserror = "1.0.29" backoff = "0.4.0" async-trait = "0.1.64" diff --git a/kube-runtime/src/lib.rs b/kube-runtime/src/lib.rs index f59d6d607..d0e060e05 100644 --- a/kube-runtime/src/lib.rs +++ b/kube-runtime/src/lib.rs @@ -23,6 +23,7 @@ pub mod controller; pub mod events; pub mod finalizer; +pub mod raw_json; pub mod reflector; pub mod scheduler; pub mod utils; @@ -31,6 +32,7 @@ pub mod watcher; pub use controller::{applier, Config, Controller}; pub use finalizer::finalizer; +pub use raw_json::RawJson; pub use reflector::reflector; pub use scheduler::scheduler; pub use utils::WatchStreamExt; diff --git a/kube-runtime/src/raw_json.rs b/kube-runtime/src/raw_json.rs new file mode 100644 index 000000000..1f5953fb6 --- /dev/null +++ b/kube-runtime/src/raw_json.rs @@ -0,0 +1,293 @@ +//! Utilities for dynamic objects represented in raw JSON. + +use std::{borrow::Cow, fmt, num::NonZeroU32, ops::Index}; + +use kube_client::discovery::ApiResource; +use serde_json::value::RawValue; + +use crate::reflector::ToObjectRef; + +/// The offsets of a string value within a JSON buffer. +#[derive(Clone, Copy)] +pub struct StrOffset { + /// The starting offset. + /// + /// This value is always positive since a string must start with a `"` first. + /// + /// `buffer[start]` may be out-of-bounds if `start == end`. + start: NonZeroU32, + /// The exclusive ending offset. + /// + /// This value is always positive since a string must start with a `"` first. + end: NonZeroU32, +} + +impl StrOffset { + /// Converts a substring slice into string offsets within the buffer. + /// + /// This function is purely arithmetic and O(1). + #[allow(clippy::missing_panics_doc)] // cannot panic + #[must_use] + pub fn new(buffer: &str, substr: &str) -> Option { + if substr.is_empty() { + return Some(Self { + start: NonZeroU32::new(1).expect("1 != 0"), + end: NonZeroU32::new(1).expect("1 != 0"), + }); + } + + let start = substr.as_ptr() as isize - buffer.as_ptr() as isize; + let end = usize::try_from(start).ok()?.checked_add(substr.len())?; + + let start = NonZeroU32::new(u32::try_from(start).ok()?)?; + let end = NonZeroU32::new(u32::try_from(end).ok()?)?; + Some(Self { start, end }) + } +} + +impl Index for str { + type Output = str; + + fn index(&self, index: StrOffset) -> &str { + &self[index.start.get() as usize..index.end.get() as usize] + } +} + +/// A dynamic object represented in raw JSON that can be used in [`reflector`](crate::reflector). +/// +/// Offsets of certain required fields are cached for efficient interaction with caches. +pub struct RawJson { + ref_fields: ObjectRefFields, + /// Extra data to cache from the object. + pub extra: X, + /// The raw JSON data. + pub buffer: Box, +} + +struct ObjectRefFields { + namespace: Option, + name: StrOffset, + resource_version: StrOffset, + uid: StrOffset, +} + +/// Extra data to store within a [`RawJson`] object. +pub trait Extra: Sized + 'static { + /// Extra data to parse from the raw root. + type Root<'a>: Sized; + /// Extra data to parse from the raw meta. + type Meta<'a>: Sized; + + /// The type of error returned when [`new`] fails. + type Error: fmt::Display; + + /// Constructs this type from the two parsed parts. + /// + /// # Errors + /// Returns `Err` if the `root` or `meta` contains invalid data. + fn new(buffer: &str, root: Self::Root<'_>, meta: Self::Meta<'_>) -> Result; +} + +#[allow(clippy::missing_fields_in_debug)] // All fields are actually included +impl fmt::Debug for RawJson { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawJson") + .field("namespace", &self.namespace()) + .field("name", &self.name()) + .field("resource_version", &self.resource_version()) + .field("uid", &self.uid()) + .field("extra", &self.extra) + .field("buffer", &&*self.buffer) + .finish() + } +} + +impl RawJson { + /// Namespace of this object. + pub fn namespace(&self) -> Option<&str> { + self.ref_fields.namespace.map(|offset| &self.buffer.get()[offset]) + } + + /// Name of this object. + pub fn name(&self) -> &str { + &self.buffer.get()[self.ref_fields.name] + } + + /// Resource version of this object. + pub fn resource_version(&self) -> &str { + &self.buffer.get()[self.ref_fields.resource_version] + } + + /// UID of this object. + pub fn uid(&self) -> &str { + &self.buffer.get()[self.ref_fields.uid] + } +} + +impl serde::Serialize for RawJson { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.buffer.serialize(serializer) + } +} + +impl<'de, X: Extra> serde::Deserialize<'de> for RawJson +where + for<'de2> X::Meta<'de2>: serde::Deserialize<'de2>, + for<'de2> X::Root<'de2>: serde::Deserialize<'de2>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct Obj<'a, MetaX, RootX> { + #[serde(borrow)] + metadata: Metadata<'a, MetaX>, + + #[serde(flatten)] + root_extra: RootX, + } + #[derive(serde::Deserialize)] + struct Metadata<'a, MetaX> { + namespace: Option<&'a str>, + name: &'a str, + #[serde(rename = "resourceVersion")] + resource_version: &'a str, + uid: &'a str, + + #[serde(flatten)] + meta_extra: MetaX, + } + + fn convert<'de, D: serde::Deserializer<'de>>( + buffer: &RawValue, + substr: &str, + ) -> Result { + StrOffset::new(buffer.get(), substr) + .ok_or_else(|| ::custom("field not a substring of buffer")) + } + + let buffer: Box = <_>::deserialize(deserializer)?; + + let (ref_fields, extra) = { + let Obj::<'_, X::Meta<'_>, X::Root<'_>> { metadata, root_extra } = + Obj::deserialize(&*buffer).map_err(::custom)?; + + let namespace = match metadata.namespace { + None => None, + Some(substr) => Some(convert::(&buffer, substr)?), + }; + let name = convert::(&buffer, metadata.name)?; + let resource_version = convert::(&buffer, metadata.resource_version)?; + let uid = convert::(&buffer, metadata.uid)?; + + ( + ObjectRefFields { + namespace, + name, + resource_version, + uid, + }, + X::new(buffer.get(), root_extra, metadata.meta_extra) + .map_err(::custom)?, + ) + }; + + Ok(Self { + ref_fields, + extra, + buffer, + }) + } +} + +impl ToObjectRef for RawJson { + type DynamicType = ApiResource; + + fn kind(dyntype: &Self::DynamicType) -> Cow<'_, str> { + dyntype.kind.as_str().into() + } + + fn version(dyntype: &Self::DynamicType) -> Cow<'_, str> { + dyntype.version.as_str().into() + } + + fn group(dyntype: &Self::DynamicType) -> Cow<'_, str> { + dyntype.group.as_str().into() + } + + fn plural(dyntype: &Self::DynamicType) -> Cow<'_, str> { + dyntype.plural.as_str().into() + } + + fn name(&self) -> Option> { + Some(Cow::Borrowed(RawJson::name(self))) + } + + fn namespace(&self) -> Option> { + RawJson::namespace(self).map(Cow::Borrowed) + } + + fn resource_version(&self) -> Option> { + Some(Cow::Borrowed(RawJson::resource_version(self))) + } + + fn uid(&self) -> Option> { + Some(Cow::Borrowed(RawJson::uid(self))) + } +} + +#[cfg(test)] +mod tests { + use crate::raw_json::{Extra, RawJson, StrOffset}; + + struct AppLabelExtra(StrOffset); + #[derive(serde::Deserialize)] + struct AppLabelMeta<'a> { + #[serde(borrow)] + labels: AppLabels<'a>, + } + #[derive(serde::Deserialize)] + struct AppLabels<'a> { + app: &'a str, + } + + impl Extra for AppLabelExtra { + type Error = &'static str; + type Meta<'a> = AppLabelMeta<'a>; + type Root<'a> = (); + + fn new(buffer: &str, (): Self::Root<'_>, meta: Self::Meta<'_>) -> Result { + Ok(Self( + StrOffset::new(buffer, meta.labels.app).ok_or("field not a substring of buffer")?, + )) + } + } + + #[test] + fn test_deser_configmap() { + let object = r#"{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "namespace": "default", + "name": "foo", + "resourceVersion": "1", + "uid": "00000000-0000-0000-0000-000000000000", + "labels": {"app": "bar"} + }, + "items": {} + }"#; + + let raw: RawJson = serde_json::from_str(object).unwrap(); + assert_eq!(raw.namespace(), Some("default")); + assert_eq!(raw.name(), "foo"); + assert_eq!(raw.resource_version(), "1"); + assert_eq!(raw.uid(), "00000000-0000-0000-0000-000000000000"); + assert_eq!(&raw.buffer.get()[raw.extra.0], "bar"); + assert_eq!(raw.buffer.get(), object); + } +} diff --git a/kube-runtime/src/reflector/object_ref.rs b/kube-runtime/src/reflector/object_ref.rs index 9132081b9..ef519887e 100644 --- a/kube-runtime/src/reflector/object_ref.rs +++ b/kube-runtime/src/reflector/object_ref.rs @@ -2,7 +2,7 @@ use derivative::Derivative; use k8s_openapi::{api::core::v1::ObjectReference, apimachinery::pkg::apis::meta::v1::OwnerReference}; use kube_client::{ api::{DynamicObject, Resource}, - core::{api_version_from_group_version, ObjectMeta}, + core::api_version_from_group_version, }; use std::{ borrow::Cow,