kube-client 4.0.0

Kubernetes client
Documentation
//! High-level utilities for runtime API discovery.

use crate::{Client, Result};
pub use kube_core::discovery::{ApiCapabilities, ApiResource, Scope, verbs};
use kube_core::gvk::GroupVersionKind;
use std::collections::HashMap;
mod apigroup;
pub mod oneshot;
pub use apigroup::ApiGroup;
mod parse;

// re-export one-shots
pub use oneshot::{group, pinned_group, pinned_kind};

/// How the Discovery client decides what api groups to scan
enum DiscoveryMode {
    /// Only allow explicitly listed apigroups
    Allow(Vec<String>),
    /// Allow all apigroups except the ones listed
    Block(Vec<String>),
}

impl DiscoveryMode {
    fn is_queryable(&self, group: &String) -> bool {
        match &self {
            Self::Allow(allowed) => allowed.contains(group),
            Self::Block(blocked) => !blocked.contains(group),
        }
    }
}

/// A caching client for running API discovery against the Kubernetes API.
///
/// This simplifies the required querying and type matching, and stores the responses
/// for each discovered api group and exposes helpers to access them.
///
/// The discovery process varies in complexity depending on:
/// - how much you know about the kind(s) and group(s) you are interested in
/// - how many groups you are interested in
///
/// Discovery can be performed on:
/// - all api groups (default)
/// - a subset of api groups (by setting Discovery::filter)
///
/// To make use of discovered apis, extract one or more [`ApiGroup`]s from it,
/// or resolve a precise one using [`Discovery::resolve_gvk`](crate::discovery::Discovery::resolve_gvk).
///
/// If caching of results is __not required__, then a simpler [`oneshot`](crate::discovery::oneshot) discovery system can be used.
///
/// [`ApiGroup`]: crate::discovery::ApiGroup
#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
pub struct Discovery {
    client: Client,
    groups: HashMap<String, ApiGroup>,
    mode: DiscoveryMode,
}

/// Caching discovery interface
///
/// Builds an internal map of its cache
impl Discovery {
    /// Construct a caching api discovery client
    #[must_use]
    pub fn new(client: Client) -> Self {
        let groups = HashMap::new();
        let mode = DiscoveryMode::Block(vec![]);
        Self { client, groups, mode }
    }

    /// Configure the discovery client to only look for the listed apigroups
    #[must_use]
    pub fn filter(mut self, allow: &[&str]) -> Self {
        self.mode = DiscoveryMode::Allow(allow.iter().map(ToString::to_string).collect());
        self
    }

    /// Configure the discovery client to look for all apigroups except the listed ones
    #[must_use]
    pub fn exclude(mut self, deny: &[&str]) -> Self {
        self.mode = DiscoveryMode::Block(deny.iter().map(ToString::to_string).collect());
        self
    }

    /// Runs or re-runs the configured discovery algorithm and updates/populates the cache
    ///
    /// The cache is empty cleared when this is started. By default, every api group found is checked,
    /// causing `N+2` queries to the api server (where `N` is number of api groups).
    ///
    /// **Note**: Consider using [`Discovery::run_aggregated`] instead, which only requires
    /// 2 API calls regardless of the number of groups (requires Kubernetes 1.26+).
    ///
    /// ```no_run
    /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt};
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::try_default().await?;
    ///     let discovery = Discovery::new(client.clone()).run().await?;
    ///     for group in discovery.groups() {
    ///         for (ar, caps) in group.recommended_resources() {
    ///             if !caps.supports_operation(verbs::LIST) {
    ///                 continue;
    ///             }
    ///             let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
    ///             // can now api.list() to emulate kubectl get all --all
    ///             for obj in api.list(&Default::default()).await? {
    ///                 println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any());
    ///             }
    ///         }
    ///     }
    ///     Ok(())
    /// }
    /// ```
    /// See a bigger example in [examples/dynamic.api](https://github.com/kube-rs/kube/blob/main/examples/dynamic_api.rs)
    pub async fn run(mut self) -> Result<Self> {
        self.groups.clear();
        let api_groups = self.client.list_api_groups().await?;
        // query regular groups + crds under /apis
        for g in api_groups.groups {
            let key = g.name.clone();
            if self.mode.is_queryable(&key) {
                let apigroup = ApiGroup::query_apis(&self.client, g).await?;
                self.groups.insert(key, apigroup);
            }
        }
        // query core versions under /api
        let corekey = ApiGroup::CORE_GROUP.to_string();
        if self.mode.is_queryable(&corekey) {
            let coreapis = self.client.list_core_api_versions().await?;
            let apigroup = ApiGroup::query_core(&self.client, coreapis).await?;
            self.groups.insert(corekey, apigroup);
        }
        Ok(self)
    }

    /// Runs discovery using the Aggregated Discovery API
    ///
    /// This method uses the Aggregated Discovery API (available since Kubernetes 1.26, stable in 1.30)
    /// to fetch all API resources in just 2 requests instead of N+2 requests.
    ///
    /// The Aggregated Discovery API provides all resource information in the response to `/api` and `/apis`
    /// when the appropriate Accept header is set, eliminating the need to query each group individually.
    ///
    /// ```no_run
    /// use kube::{Client, api::{Api, DynamicObject}, discovery::{Discovery, verbs, Scope}, ResourceExt};
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::try_default().await?;
    ///     // Uses only 2 API calls instead of N+2
    ///     let discovery = Discovery::new(client.clone()).run_aggregated().await?;
    ///     for group in discovery.groups() {
    ///         for (ar, caps) in group.recommended_resources() {
    ///             if !caps.supports_operation(verbs::LIST) {
    ///                 continue;
    ///             }
    ///             let api: Api<DynamicObject> = Api::all_with(client.clone(), &ar);
    ///             for obj in api.list(&Default::default()).await? {
    ///                 println!("{} {}: {}", ar.api_version, ar.kind, obj.name_any());
    ///             }
    ///         }
    ///     }
    ///     Ok(())
    /// }
    /// ```
    ///
    /// # Requirements
    /// - Kubernetes 1.26+ (beta) or 1.30+ (stable)
    ///
    /// # Note
    /// If the server does not support Aggregated Discovery, this will return an error.
    /// Consider falling back to [`Discovery::run`] in that case.
    pub async fn run_aggregated(mut self) -> Result<Self> {
        self.groups.clear();

        // Query /apis for all non-core groups (single request)
        let apis_discovery = self.client.list_api_groups_aggregated().await?;
        for ag in apis_discovery.items {
            let key = ag
                .metadata
                .as_ref()
                .and_then(|m| m.name.clone())
                .unwrap_or_default();
            if self.mode.is_queryable(&key) {
                let apigroup = ApiGroup::from_v2(ag)?;
                self.groups.insert(key, apigroup);
            }
        }

        // Query /api for core group (single request)
        let corekey = ApiGroup::CORE_GROUP.to_string();
        if self.mode.is_queryable(&corekey) {
            let core_discovery = self.client.list_core_api_versions_aggregated().await?;
            // Core group is the first (and usually only) item
            if let Some(core_ag) = core_discovery.items.into_iter().next() {
                let apigroup = ApiGroup::from_v2(core_ag)?;
                self.groups.insert(corekey, apigroup);
            }
        }

        Ok(self)
    }
}

/// Interface to the Discovery cache
impl Discovery {
    /// Returns iterator over all served groups
    pub fn groups(&self) -> impl Iterator<Item = &ApiGroup> {
        self.groups.values()
    }

    /// Returns a sorted vector of all served groups
    ///
    /// This vector is in kubectl's normal alphabetical group order
    pub fn groups_alphabetical(&self) -> Vec<&ApiGroup> {
        let mut values: Vec<_> = self.groups().collect();
        // collect to maintain kubectl order of groups
        values.sort_by_key(|g| g.name());
        values
    }

    /// Returns the [`ApiGroup`] for a given group if served
    pub fn get(&self, group: &str) -> Option<&ApiGroup> {
        self.groups.get(group)
    }

    /// Check if a group is served by the apiserver
    pub fn has_group(&self, group: &str) -> bool {
        self.groups.contains_key(group)
    }

    /// Finds an [`ApiResource`] and its [`ApiCapabilities`] after discovery by matching a GVK
    ///
    /// This is for quick extraction after having done a complete discovery.
    /// If you are only interested in a single kind, consider [`oneshot::pinned_kind`](crate::discovery::pinned_kind).
    pub fn resolve_gvk(&self, gvk: &GroupVersionKind) -> Option<(ApiResource, ApiCapabilities)> {
        self.get(&gvk.group)?
            .versioned_resources(&gvk.version)
            .into_iter()
            .find(|res| res.0.kind == gvk.kind)
    }
}