Skip to content

Commit

Permalink
Merge pull request #305 from clux/dyn-resource-getters
Browse files Browse the repository at this point in the history
tweaks for dynamic resource getters for #301
  • Loading branch information
clux authored Aug 10, 2020
2 parents 130bc75 + e11223b commit 3c230e0
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 90 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
0.40.0 / 2020-07-XX
===================
* `DynamicResource::from_api_resource` added to allow apiserver returned resources - #305 via #301
* `Client::list_api_groups` added
* `Client::list_ap_group_resources` added
* `Client::list_core_api_versions` added
* `Client::list_core_api_resources` added
* `kube::DynamicResource` exposed at top level

0.39.0 / 2020-07-05
===================
* Bug: `ObjectRef` tweak in `kube-runtime` to allow controllers triggering across cluster and namespace scopes - #293 via #294
Expand Down
4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ path = "crd_reflector.rs"
name = "deployment_reflector"
path = "deployment_reflector.rs"

[[example]]
name = "dynamic_api"
path = "dynamic_api.rs"

[[example]]
name = "event_watcher"
path = "event_watcher.rs"
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ cargo run --example crd_api
cargo run --example job_api
cargo run --example log_stream
cargo run --example pod_api
cargo run --example dynamic_api
NAMESPACE=dev cargo run --example log_stream -- kafka-manager-7d4f4bd8dc-f6c44
```

Expand Down
50 changes: 50 additions & 0 deletions examples/dynamic_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#[macro_use] extern crate log;
use kube::{api::DynamicResource, Client};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
std::env::set_var("RUST_LOG", "info,kube=debug");
env_logger::init();
let client = Client::try_default().await?;

let v = client.apiserver_version().await?;
info!("api version: {:?}", v);

// The following loops turn the /api or /apis listers into kube::api::Resource
// objects which can be used to make dynamic api calls.
// This is slightly awkward because of corev1 types
// and data split over the list types and the inner get calls.

// loop over all api groups (except core v1)
let apigroups = client.list_api_groups().await?;
for g in apigroups.groups {
info!("group: {}", g.name);
debug!("group: {:?}", g);
let ver = g
.preferred_version
.as_ref()
.or_else(|| g.versions.first())
.expect("preferred or versions exists");
info!("polling: {} at {:?}", g.name, ver);
let apis = client.list_api_group_resources(&ver.group_version).await?;
dbg!(&apis);
for ar in apis.resources {
let r = DynamicResource::from_api_resource(&ar, &apis.group_version).into_resource();
dbg!(r);
}
}
// core/v1 has a legacy endpoint
let coreapis = client.list_core_api_versions().await?;
for corever in coreapis.versions {
dbg!(&corever);
let apis = client.list_core_api_resources(&corever).await?;
debug!("Got {:?}", apis);
for cr in apis.resources {
dbg!(&cr);
let r = DynamicResource::from_api_resource(&cr, &apis.group_version).into_resource();
dbg!(r);
}
}

Ok(())
}
119 changes: 56 additions & 63 deletions kube/src/api/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ use std::iter;

/// A dynamic builder for Resource
///
/// Can be used to interact with a dynamic api resources.
/// Can be constructed either from [`DynamicResource::from_api_resource`], or directly.
///
/// ### Direct usage
/// ```
/// use kube::api::Resource;
/// struct FooSpec {};
/// struct FooStatus {};
/// struct Foo {
/// spec: FooSpec,
/// status: FooStatus
/// };
/// let foos = Resource::dynamic("Foo") // <.spec.kind>
/// .group("clux.dev") // <.spec.group>
/// .version("v1")
/// .into_resource();
/// ```
///
/// It is recommended to use [`kube::CustomResource`] (from kube's `derive` feature)
/// for CRD cases where you own a struct rather than this.
///
/// **Note:** You will need to implement `k8s_openapi` traits yourself to use the typed `Api`
/// with a `Resource` built from a `DynamicResource` (and this is not always feasible).
#[derive(Default)]
pub struct DynamicResource {
pub(crate) kind: String,
Expand All @@ -33,6 +37,42 @@ pub struct DynamicResource {
}

impl DynamicResource {
/// Creates `DynamicResource` from an [`APIResource`](https://docs.rs/k8s-openapi/0.9.0/k8s_openapi/apimachinery/pkg/apis/meta/v1/struct.APIResource.html)
///
/// `APIResource` objects can be extracted from [`Client::list_api_group_resources`].
/// If it does not specify version and/or group, they will be taken
/// from `group_version`.
///
/// ### Example usage:
/// ```
/// use kube::api::DynamicResource;
/// # async fn scope(client: kube::Client) -> Result<(), Box<dyn std::error::Error>> {
/// let apps = client.list_api_group_resources("apps/v1").await?;
/// for ar in &apps.resources {
/// let dr = DynamicResource::from_api_resource(ar, &apps.group_version);
/// let r = dr.within("kube-system").into_resource();
/// dbg!(r);
/// }
/// # Ok(())
/// # }
/// ```
pub fn from_api_resource(ar: &APIResource, group_version: &str) -> Self {
let gvsplit = group_version.splitn(2, '/').collect::<Vec<_>>();
let (default_group, default_version) = match *gvsplit.as_slice() {
[g, v] => (g, v), // standard case
[v] => ("", v), // core v1 case
_ => unreachable!(),
};
let version = ar.version.clone().unwrap_or_else(|| default_version.into());
let group = ar.group.clone().unwrap_or_else(|| default_group.into());
DynamicResource {
kind: ar.kind.to_string(),
version: Some(version),
group: Some(group),
namespace: None,
}
}

/// Create a DynamicResource specifying the kind
///
/// The kind must not be plural and it must be in PascalCase
Expand Down Expand Up @@ -100,69 +140,12 @@ impl DynamicResource {
phantom: iter::empty(),
})
}

/// Creates `DynamicResource`, based on `metav1::APIResource`.
/// If it does not specify version and/or group, they will be taken
/// from `api_resource_list_group_version`.
/// Example usage:
/// ```
/// use kube::api::DynamicResource;
/// # async fn scope(client: kube::Client) -> Result<(), Box<dyn std::error::Error>> {
/// let apps_apis = client.list_api_group_resources("apps", "v1").await?;
/// for api_res in &apps_apis.resources {
/// let kube_resource = DynamicResource::from_metav1_api_resource(api_res, &apps_apis.group_version);
/// // do not forget to select a namespace
/// let kube_resource = kube_resource.within("kube-system").into_resource();
/// dbg!(kube_resource);
/// }
/// # Ok(())
/// # }
/// ```
pub fn from_metav1_api_resource(
k8s_resource: &APIResource,
api_resource_list_group_version: &str,
) -> Self {
let (default_group, default_version) = if api_resource_list_group_version.contains('/') {
let mut iter = api_resource_list_group_version.splitn(2, '/');
let g = iter.next().unwrap();
let v = iter.next().unwrap();
(g, v)
} else {
("", api_resource_list_group_version)
};
let version = k8s_resource
.version
.clone()
.unwrap_or_else(|| default_version.to_string());
let group = k8s_resource
.group
.clone()
.unwrap_or_else(|| default_group.to_string());
DynamicResource {
kind: k8s_resource.kind.clone(),
version: Some(version),
group: Some(group),
namespace: None,
}
}
}

impl TryFrom<DynamicResource> for Resource {
type Error = crate::Error;

fn try_from(rb: DynamicResource) -> Result<Self> {
if to_plural(&rb.kind) == rb.kind {
return Err(Error::DynamicResource(format!(
"DynamicResource kind '{}' must not be pluralized",
rb.kind
)));
}
if !is_pascal_case(&rb.kind) {
return Err(Error::DynamicResource(format!(
"DynamicResource kind '{}' must be PascalCase",
rb.kind
)));
}
if rb.version.is_none() {
return Err(Error::DynamicResource(format!(
"DynamicResource '{}' must have a version",
Expand All @@ -177,6 +160,16 @@ impl TryFrom<DynamicResource> for Resource {
}
let version = rb.version.unwrap();
let group = rb.group.unwrap();

// pedantic conventions we enforce internally in kube-derive
// but are broken by a few native / common custom resources such as istio, or
// kinds matching: CRI*, *Options, *Metrics, CSI*, ENI*, API*
if to_plural(&rb.kind) == rb.kind {
debug!("DynamicResource kind '{}' should be singular", rb.kind);
}
if !is_pascal_case(&rb.kind) {
debug!("DynamicResource kind '{}' should be PascalCase", rb.kind);
}
Ok(Self {
api_version: if group == "" {
version.clone()
Expand Down
55 changes: 29 additions & 26 deletions kube/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use bytes::Bytes;
use either::{Either, Left, Right};
use futures::{self, Stream, TryStream, TryStreamExt};
use http::{self, Request, StatusCode};
use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta_v1;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::{self, Value};

Expand Down Expand Up @@ -271,45 +272,47 @@ impl Client {

/// Returns apiserver version.
pub async fn apiserver_version(&self) -> Result<k8s_openapi::apimachinery::pkg::version::Info> {
self.request(Request::builder().uri("/version").body(Vec::new())?)
self.request(Request::builder().uri("/version").body(vec![])?)
.await
}

/// Lists api groups that apiserver serves.
pub async fn list_api_groups(&self) -> Result<k8s_openapi::apimachinery::pkg::apis::meta::v1::APIGroupList> {
self.request(Request::builder().uri("/apis").body(Vec::new())?)
.await
pub async fn list_api_groups(&self) -> Result<k8s_meta_v1::APIGroupList> {
self.request(Request::builder().uri("/apis").body(vec![])?).await
}

/// Lists resources served in given API group.
/// There resources can be then converted to `kube::api::DynamicResource`
/// using the `from_metav1_api_resource` method.
pub async fn list_api_group_resources(
&self,
group: &str,
version: &str,
) -> Result<k8s_openapi::apimachinery::pkg::apis::meta::v1::APIResourceList> {
let url = format!("/apis/{}/{}", group, version);
self.request(Request::builder().uri(url).body(Vec::new())?).await
///
/// ### Example usage:
/// ```rust
/// # async fn scope(client: kube::Client) -> Result<(), Box<dyn std::error::Error>> {
/// let apigroups = client.list_api_groups().await?;
/// for g in apigroups.groups {
/// let ver = g
/// .preferred_version
/// .as_ref()
/// .or_else(|| g.versions.first())
/// .expect("preferred or versions exists");
/// let apis = client.list_api_group_resources(&ver.group_version).await?;
/// dbg!(apis);
/// }
/// # Ok(())
/// # }
/// ```
pub async fn list_api_group_resources(&self, apiversion: &str) -> Result<k8s_meta_v1::APIResourceList> {
let url = format!("/apis/{}", apiversion);
self.request(Request::builder().uri(url).body(vec![])?).await
}

/// Lists versions of `core` a.k.a. `""` API group.
pub async fn list_core_api_versions(
&self,
) -> Result<k8s_openapi::apimachinery::pkg::apis::meta::v1::APIVersions> {
self.request(Request::builder().uri("/api").body(Vec::new())?)
.await
/// Lists versions of `core` a.k.a. `""` legacy API group.
pub async fn list_core_api_versions(&self) -> Result<k8s_meta_v1::APIVersions> {
self.request(Request::builder().uri("/api").body(vec![])?).await
}

/// Lists resources served in particular `core` group version.
/// There resources can be then converted to `kube::api::DynamicResource`
/// using the `from_metav1_api_resource` method.
pub async fn list_core_api_resources(
&self,
version: &str,
) -> Result<k8s_openapi::apimachinery::pkg::apis::meta::v1::APIResourceList> {
pub async fn list_core_api_resources(&self, version: &str) -> Result<k8s_meta_v1::APIResourceList> {
let url = format!("/api/{}", version);
self.request(Request::builder().uri(url).body(Vec::new())?).await
self.request(Request::builder().uri(url).body(vec![])?).await
}
}

Expand Down
1 change: 1 addition & 0 deletions kube/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use tokio::sync::Mutex;
use std::{sync::Arc, time::Duration};

#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Authentication {
None,
Basic(String),
Expand Down
2 changes: 1 addition & 1 deletion kube/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ mod oauth2;

#[cfg(feature = "derive")] pub use kube_derive::CustomResource;

pub use api::{Api, Resource};
pub use api::{Api, DynamicResource, Resource};
#[doc(inline)] pub use client::Client;
#[doc(inline)] pub use config::Config;
#[doc(inline)] pub use error::Error;
Expand Down
1 change: 1 addition & 0 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ main() {
# Usage:
#
# cargo release minor --exclude tests --exclude examples --skip-tag --skip-push --no-dev-version
# TODO: --consolidate-commits
# Then amend commits / squash.
# Finally run this script to bump readme's and then tag.
#
Expand Down

0 comments on commit 3c230e0

Please sign in to comment.