Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tweaks for dynamic resource getters for #301 #305

Merged
merged 7 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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