Skip to content

Commit

Permalink
Introduce GetParams support (#1214)
Browse files Browse the repository at this point in the history
* Introduce `GetParams` support

As part of a larger change to support version matching strategies (to
reduce I/O pressure), ListParams were separated into List and Watch
params. To complete the feature set, this change adds support for
`GetParams`, mirroing the upstream client-go layout for the type.

In the context of this change, we added a GetParams struct that does
implicit version matching (semantics are quite easy, any = "0",
NotOlderThan = specific, non-zero version, and unspecified is most
recent). We use the struct in all request methods and in Api core
methods. `ListParams` with version matching semantics is used for
list_metadata, so the same was done here (extended GetParams to
get_metadata and all other asssociated methods) even if it was outside
of the scope of the issue.

As part of the change, we also had to change the examples and tests.
Since the version was (largely) unspecified, we use the default derived
implementation; for tests and examples, this change should be
non-functional (unspecified is the same as unset).

Fixes #1174

Signed-off-by: Matei David <[email protected]>

* Correctly quote resourceVersion in code docs

Signed-off-by: Matei David <[email protected]>

* Add unit tests

Signed-off-by: Matei David <[email protected]>

---------

Signed-off-by: Matei David <[email protected]>
  • Loading branch information
mateiidavid authored May 5, 2023
1 parent 546e618 commit 894b508
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 15 deletions.
63 changes: 59 additions & 4 deletions kube-client/src/api/core_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ where
/// This function assumes that the object is expected to always exist, and returns [`Error`] if it does not.
/// Consider using [`Api::get_opt`] if you need to handle missing objects.
pub async fn get(&self, name: &str) -> Result<K> {
let mut req = self.request.get(name).map_err(Error::BuildRequest)?;
req.extensions_mut().insert("get");
self.client.request::<K>(req).await
self.get_with(name, &GetParams::default()).await
}

/// Get only the metadata for a named resource as [`PartialObjectMeta`]
Expand All @@ -58,7 +56,64 @@ where
/// This function assumes that the object is expected to always exist, and returns [`Error`] if it does not.
/// Consider using [`Api::get_metadata_opt`] if you need to handle missing objects.
pub async fn get_metadata(&self, name: &str) -> Result<PartialObjectMeta<K>> {
let mut req = self.request.get_metadata(name).map_err(Error::BuildRequest)?;
self.get_metadata_with(name, &GetParams::default()).await
}

/// [Get](`Api::get`) a named resource with an explicit resourceVersion
///
/// This function allows the caller to pass in a [`GetParams`](`super::GetParams`) type containing
/// a `resourceVersion` to a [Get](`Api::get`) call.
/// For example
///
/// ```no_run
/// # use kube::{Api, api::GetParams};
/// use k8s_openapi::api::core::v1::Pod;
///
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
/// # let client: kube::Client = todo!();
/// let pods: Api<Pod> = Api::namespaced(client, "apps");
/// let p: Pod = pods.get_with("blog", &GetParams::any()).await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// This function assumes that the object is expected to always exist, and returns [`Error`] if it does not.
/// Consider using [`Api::get_opt`] if you need to handle missing objects.
pub async fn get_with(&self, name: &str, gp: &GetParams) -> Result<K> {
let mut req = self.request.get(name, gp).map_err(Error::BuildRequest)?;
req.extensions_mut().insert("get");
self.client.request::<K>(req).await
}

/// [Get](`Api::get_metadata`) the metadata of an object using an explicit `resourceVersion`
///
/// This function allows the caller to pass in a [`GetParams`](`super::GetParams`) type containing
/// a `resourceVersion` to a [Get](`Api::get_metadata`) call.
/// For example
///
///
/// ```no_run
/// use kube::{Api, api::GetParams, core::PartialObjectMeta};
/// use k8s_openapi::api::core::v1::Pod;
///
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
/// # let client: kube::Client = todo!();
/// let pods: Api<Pod> = Api::namespaced(client, "apps");
/// let p: PartialObjectMeta<Pod> = pods.get_metadata_with("blog", &GetParams::any()).await?;
/// # Ok(())
/// # }
/// ```
/// Note that the type may be converted to `ObjectMeta` through the usual
/// conversion traits.
///
/// # Errors
///
/// This function assumes that the object is expected to always exist, and returns [`Error`] if it does not.
/// Consider using [`Api::get_metadata_opt`] if you need to handle missing objects.
pub async fn get_metadata_with(&self, name: &str, gp: &GetParams) -> Result<PartialObjectMeta<K>> {
let mut req = self.request.get_metadata(name, gp).map_err(Error::BuildRequest)?;
req.extensions_mut().insert("get_metadata");
self.client.request::<PartialObjectMeta<K>>(req).await
}
Expand Down
2 changes: 1 addition & 1 deletion kube-client/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub use kube_core::{
};
use kube_core::{DynamicResourceScope, NamespaceResourceScope};
pub use params::{
DeleteParams, ListParams, Patch, PatchParams, PostParams, Preconditions, PropagationPolicy,
DeleteParams, GetParams, ListParams, Patch, PatchParams, PostParams, Preconditions, PropagationPolicy,
ValidationDirective, VersionMatch, WatchParams,
};

Expand Down
35 changes: 35 additions & 0 deletions kube-core/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,41 @@ impl ListParams {
}
}

/// Common query parameters used in get calls
#[derive(Clone, Debug, Default)]
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 {
Expand Down
67 changes: 57 additions & 10 deletions kube-core/src/request.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! 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";
Expand Down Expand Up @@ -77,10 +79,16 @@ impl Request {
}

/// Get a single instance
pub fn get(&self, name: &str) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}/{}", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
let urlstr = qp.finish();
pub fn get(&self, name: &str, gp: &GetParams) -> Result<http::Request<Vec<u8>>, Error> {
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)
}
Expand Down Expand Up @@ -247,10 +255,16 @@ impl Request {
/// 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) -> Result<http::Request<Vec<u8>>, Error> {
let target = format!("{}/{}", self.url_path, name);
let mut qp = form_urlencoded::Serializer::new(target);
let urlstr = qp.finish();
pub fn get_metadata(&self, name: &str, gp: &GetParams) -> Result<http::Request<Vec<u8>>, Error> {
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);
Expand Down Expand Up @@ -316,7 +330,7 @@ impl Request {
#[cfg(test)]
mod test {
use crate::{
params::{PostParams, VersionMatch, WatchParams},
params::{GetParams, PostParams, VersionMatch, WatchParams},
request::Request,
resource::Resource,
};
Expand Down Expand Up @@ -434,7 +448,10 @@ mod test {
#[test]
fn get_metadata_path() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
let req = Request::new(url).get_metadata("mydeploy").unwrap();
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);
Expand All @@ -443,6 +460,36 @@ mod test {
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 list_path() {
let url = appsv1::Deployment::url_path(&(), Some("ns"));
Expand Down

0 comments on commit 894b508

Please sign in to comment.