Skip to main content

rkubectl_kubeapi/
lib.rs

1//! This crate provides a higher-level API for interacting with Kubernetes clusters.
2//! It builds on top of the `kube` crate and adds features like caching, namespace
3//! management, and easier access to common Kubernetes resources.
4
5use std::collections::BTreeSet;
6use std::fmt;
7use std::fs;
8use std::io;
9use std::path::Path;
10use std::path::PathBuf;
11use std::time;
12
13use k8s_openapi_ext as k8s;
14use kube::api;
15use kube::discovery;
16use kube_client_ext::KubeClientExt;
17use serde_json as json;
18use serde_yaml as yaml;
19use tracing::debug;
20use tracing::error;
21use tracing::info;
22use tracing::trace;
23
24use k8s::authenticationv1;
25use k8s::authorizationv1;
26use k8s::corev1;
27use k8s::metav1;
28use k8s::rbacv1;
29
30use rkubectl_features::Feature;
31
32pub use cache::Cache;
33pub use cascade::Cascade;
34pub use dryrun::DryRun;
35pub use namespace::Namespace;
36pub use options::KubeConfigOptions;
37pub use options::KubeapiOptions;
38
39mod apis;
40mod cache;
41mod cascade;
42mod dryrun;
43mod features;
44mod info;
45mod kubeconfig;
46mod namespace;
47mod options;
48mod params;
49mod raw;
50mod server;
51mod version;
52
53/// Kubeapi is a higher-level Kubernetes API client that provides additional features
54/// such as caching, namespace management, and easier access to common Kubernetes resources.
55#[derive(Clone, Debug)]
56pub struct Kubeapi {
57    config: kube::Config,
58    kubeconfig: kube::config::Kubeconfig,
59    cache: Cache,
60    namespace: Namespace,
61    debug: bool,
62    options: KubeapiOptions,
63}
64
65impl Kubeapi {
66    pub async fn new(
67        config: &KubeConfigOptions,
68        options: &KubeapiOptions,
69        debug: bool,
70    ) -> kube::Result<Self> {
71        let config_options = config.kube_config_options();
72        let options = options.clone();
73        let namespace = default();
74        let cache = cache::Cache::default();
75        Self::kubeconfig(config_options, debug)
76            .await
77            .inspect_err(|err| error!(%err, "from_kubeconfig"))
78            .map(|(config, kubeconfig)| Self {
79                config,
80                kubeconfig,
81                cache,
82                namespace,
83                debug,
84                options,
85            })
86            .and_then(Self::try_load_cache)
87            .map_err(|_| kube::Error::LinesCodecMaxLineLengthExceeded)
88    }
89
90    pub fn cluster_url(&self) -> String {
91        self.config.cluster_url.to_string()
92    }
93
94    pub fn debug(&self, item: impl fmt::Debug) {
95        if self.debug {
96            info!("{item:?}")
97        }
98    }
99
100    /// Create a kube::Client from the current configuration.
101    pub fn client(&self) -> kube::Result<kube::Client> {
102        kube::Client::try_from(self.config.clone())
103    }
104
105    /// Returns the path to the cache file based on the current kubeconfig context.
106    fn cache_path(&self) -> Result<PathBuf, kube::config::KubeconfigError> {
107        self.options.discovery_cache_for_config(&self.config)
108    }
109
110    fn try_load_cache(self) -> Result<Self, kube::config::KubeconfigError> {
111        let path = self.cache_path()?;
112        let cache = self.cache.try_load(path);
113        if self.debug {
114            info!("Loading cache took {:?}", cache.took());
115        }
116        Ok(Self { cache, ..self })
117    }
118
119    /// Set the namespace for the Kubeapi instance.
120    /// This method returns a new instance with the updated namespace.
121    pub fn with_namespace(self, namespace: Namespace) -> Self {
122        Self { namespace, ..self }
123    }
124
125    /// Get the current namespace of the Kubeapi instance.
126    pub fn namespace(&self) -> &Namespace {
127        &self.namespace
128    }
129
130    pub fn show_namespace(&self) -> bool {
131        matches!(self.namespace, Namespace::All)
132    }
133
134    pub fn cached_server_api_resources(&self) -> Vec<metav1::APIResourceList> {
135        self.cache.api_resources().unwrap_or_default()
136    }
137
138    pub async fn server_preferred_resources(&self) -> kube::Result<Vec<metav1::APIResourceList>> {
139        let ag = self.server_api_groups().await?;
140        let preferred_versions = ag
141            .groups
142            .into_iter()
143            .map(|mut group| {
144                group
145                    .preferred_version
146                    .unwrap_or_else(|| group.versions.remove(0))
147            })
148            .map(|gv| gv.group_version)
149            .collect::<BTreeSet<_>>();
150        let resources = self
151            .server_api_resources()
152            .await?
153            .into_iter()
154            .filter(|arl| preferred_versions.contains(&arl.group_version))
155            .collect();
156        Ok(resources)
157    }
158
159    pub async fn api_versions(
160        &self,
161    ) -> kube::Result<impl Iterator<Item = metav1::GroupVersionForDiscovery>> {
162        let items = self
163            .server_api_groups()
164            .await?
165            .groups
166            .into_iter()
167            .flat_map(|group| group.versions.into_iter());
168
169        Ok(items)
170    }
171
172    pub fn dynamic_object_api(
173        &self,
174        scope: discovery::Scope,
175        dyntype: &discovery::ApiResource,
176    ) -> kube::Result<api::Api<api::DynamicObject>> {
177        trace!(?scope, ?dyntype, "dynamic_object_api");
178        let client = self.client()?;
179        let dynamic_api = match scope {
180            discovery::Scope::Cluster => api::Api::all_with(client, dyntype),
181            discovery::Scope::Namespaced => match &self.namespace {
182                Namespace::All => api::Api::all_with(client, dyntype),
183                Namespace::Default => api::Api::default_namespaced_with(client, dyntype),
184                Namespace::Namespace(ns) => api::Api::namespaced_with(client, ns, dyntype),
185            },
186        };
187
188        Ok(dynamic_api)
189    }
190
191    pub fn inspect<K>(&self, k: &K)
192    where
193        K: serde::Serialize,
194    {
195        if self.debug {
196            let k = yaml::to_string(k).unwrap_or_default();
197            info!("{k}");
198        }
199    }
200
201    pub fn inspect_err(&self, err: &kube::Error) {
202        if self.debug {
203            info!("{err:?}");
204        }
205    }
206
207    pub fn full_name<K>(&self, k: &K) -> String
208    where
209        K: kube::Resource + kube::ResourceExt,
210        <K as kube::Resource>::DynamicType: Default,
211    {
212        let kind = K::kind(&default()).to_lowercase();
213        let name = k.name_any();
214        format!("{kind}/{name}")
215    }
216}
217
218impl Kubeapi {
219    /// Create a `Kubeapi` instance configured to connect to a local Kubernetes cluster.
220    /// This is useful for development and testing purposes.
221    /// It assumes the cluster is accessible at `http://localhost:6443`.
222    /// Note: This instance does not load any kubeconfig file and uses default settings.
223    pub fn local() -> Self {
224        let config = kube::Config::new("http://localhost:6443".parse().unwrap());
225        Self {
226            config,
227            kubeconfig: default(),
228            cache: default(),
229            namespace: default(),
230            debug: default(),
231            options: default(),
232        }
233    }
234}
235
236fn default<T: Default>() -> T {
237    T::default()
238}