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::trace;
22
23use k8s::authenticationv1;
24use k8s::authorizationv1;
25use k8s::corev1;
26use k8s::metav1;
27use k8s::rbacv1;
28
29use rkubectl_features::Feature;
30
31pub use cache::Cache;
32pub use cascade::Cascade;
33pub use dryrun::DryRun;
34pub use namespace::Namespace;
35pub use options::KubeConfigOptions;
36pub use options::KubeapiOptions;
37
38mod cache;
39mod cascade;
40mod dryrun;
41mod features;
42mod info;
43mod kubeconfig;
44mod namespace;
45mod options;
46mod raw;
47mod server;
48mod version;
49
50/// Kubeapi is a higher-level Kubernetes API client that provides additional features
51/// such as caching, namespace management, and easier access to common Kubernetes resources.
52#[derive(Debug)]
53pub struct Kubeapi {
54    config: kube::Config,
55    kubeconfig: kube::config::Kubeconfig,
56    cache: Cache,
57    namespace: Namespace,
58    debug: bool,
59    options: KubeapiOptions,
60}
61
62impl Kubeapi {
63    pub async fn new(
64        config: &KubeConfigOptions,
65        options: &KubeapiOptions,
66        debug: bool,
67    ) -> kube::Result<Self> {
68        let config_options = config.kube_config_options();
69        let options = options.clone();
70        let namespace = default();
71        let cache = cache::Cache::default();
72        Self::kubeconfig(config_options, debug)
73            .await
74            .inspect_err(|err| error!(%err, "from_kubeconfig"))
75            .map(|(config, kubeconfig)| Self {
76                config,
77                kubeconfig,
78                cache,
79                namespace,
80                debug,
81                options,
82            })
83            .and_then(Self::try_load_cache)
84            .map_err(|_| kube::Error::LinesCodecMaxLineLengthExceeded)
85    }
86
87    pub fn cluster_url(&self) -> String {
88        self.config.cluster_url.to_string()
89    }
90
91    pub fn debug(&self, item: impl fmt::Debug) {
92        if self.debug {
93            println!("{item:?}")
94        }
95    }
96
97    /// Create a kube::Client from the current configuration.
98    pub fn client(&self) -> kube::Result<kube::Client> {
99        kube::Client::try_from(self.config.clone())
100    }
101
102    /// Returns the path to the cache file based on the current kubeconfig context.
103    fn cache_path(&self) -> Result<PathBuf, kube::config::KubeconfigError> {
104        self.options.discovery_cache_for_config(&self.config)
105    }
106
107    fn try_load_cache(self) -> Result<Self, kube::config::KubeconfigError> {
108        let path = self.cache_path()?;
109        let cache = self.cache.try_load(path);
110        if self.debug {
111            println!("Loading cache took {:?}", cache.took());
112        }
113        Ok(Self { cache, ..self })
114    }
115
116    /// Set the namespace for the Kubeapi instance.
117    /// This method returns a new instance with the updated namespace.
118    pub fn with_namespace(self, namespace: Namespace) -> Self {
119        Self { namespace, ..self }
120    }
121
122    /// Get the current namespace of the Kubeapi instance.
123    pub fn namespace(&self) -> &Namespace {
124        &self.namespace
125    }
126
127    pub fn show_namespace(&self) -> bool {
128        matches!(self.namespace, Namespace::All)
129    }
130
131    pub fn cached_server_api_resources(&self) -> Vec<metav1::APIResourceList> {
132        self.cache.api_resources().unwrap_or_default()
133    }
134
135    pub async fn server_preferred_resources(&self) -> kube::Result<Vec<metav1::APIResourceList>> {
136        let ag = self.server_api_groups().await?;
137        let preferred_versions = ag
138            .groups
139            .into_iter()
140            .map(|mut group| {
141                group
142                    .preferred_version
143                    .unwrap_or_else(|| group.versions.remove(0))
144            })
145            .map(|gv| gv.group_version)
146            .collect::<BTreeSet<_>>();
147        let resources = self
148            .server_api_resources()
149            .await?
150            .into_iter()
151            .filter(|arl| preferred_versions.contains(&arl.group_version))
152            .collect();
153        Ok(resources)
154    }
155
156    pub async fn api_versions(&self) -> kube::Result<()> {
157        self.server_api_groups()
158            .await?
159            .groups
160            .iter()
161            .flat_map(|group| group.versions.iter())
162            .for_each(|version| println!("{}", version.group_version));
163        Ok(())
164    }
165
166    pub fn dynamic_object_api(
167        &self,
168        scope: discovery::Scope,
169        dyntype: &discovery::ApiResource,
170    ) -> kube::Result<api::Api<api::DynamicObject>> {
171        trace!(?scope, ?dyntype, "dynamic_object_api");
172        let client = self.client()?;
173        let dynamic_api = match scope {
174            discovery::Scope::Cluster => api::Api::all_with(client, dyntype),
175            discovery::Scope::Namespaced => match &self.namespace {
176                Namespace::All => api::Api::all_with(client, dyntype),
177                Namespace::Default => api::Api::default_namespaced_with(client, dyntype),
178                Namespace::Namespace(ns) => api::Api::namespaced_with(client, ns, dyntype),
179            },
180        };
181
182        Ok(dynamic_api)
183    }
184
185    // pub fn dynamic_object_api0(
186    //     &self,
187    //     resource: &Resource,
188    // ) -> kube::Result<api::Api<api::DynamicObject>> {
189    //     let client = self.client()?;
190    //     let (scope, ref dyntype) = resource.api_resource();
191
192    //     trace!(?dyntype, "dynamic_object_api");
193
194    //     let dynamic_api = match scope {
195    //         discovery::Scope::Cluster => api::Api::all_with(client, dyntype),
196    //         discovery::Scope::Namespaced => match &self.namespace {
197    //             Namespace::All => api::Api::all_with(client, dyntype),
198    //             Namespace::Default => api::Api::default_namespaced_with(client, dyntype),
199    //             Namespace::Namespace(ns) => api::Api::namespaced_with(client, ns, dyntype),
200    //         },
201    //     };
202
203    //     Ok(dynamic_api)
204    // }
205
206    // pub async fn get(&self, resource: Vec<Resource>, output: OutputFormat) -> kube::Result<()> {
207    //     println!("Getting {resource:?} [{output:?}]");
208    //     Ok(())
209    // }
210
211    pub fn get_params(&self) -> api::GetParams {
212        api::GetParams::default()
213    }
214
215    pub fn list_params(&self) -> api::ListParams {
216        api::ListParams::default()
217    }
218
219    pub fn post_params(&self) -> api::PostParams {
220        api::PostParams::default()
221    }
222
223    pub fn delete_params(&self, cascade: Cascade, dry_run: DryRun) -> api::DeleteParams {
224        let dp = match cascade {
225            Cascade::Background => api::DeleteParams::background(),
226            Cascade::Foreground => api::DeleteParams::foreground(),
227            Cascade::Orphan => api::DeleteParams::orphan(),
228        };
229
230        match dry_run {
231            DryRun::Server => dp.dry_run(),
232            DryRun::None | DryRun::Client => dp,
233        }
234    }
235
236    pub fn post_params_with_manager(&self, manager: &str) -> api::PostParams {
237        api::PostParams {
238            field_manager: Some(manager.to_string()),
239            ..default()
240        }
241    }
242
243    pub fn clusterroles(&self) -> kube::Result<api::Api<rbacv1::ClusterRole>> {
244        self.cluster_api()
245    }
246
247    pub fn namespaces(&self) -> kube::Result<api::Api<corev1::Namespace>> {
248        self.cluster_api()
249    }
250
251    pub fn pods(&self) -> kube::Result<api::Api<corev1::Pod>> {
252        self.namespaced_api()
253    }
254
255    pub fn configmaps(&self) -> kube::Result<api::Api<corev1::ConfigMap>> {
256        self.namespaced_api()
257    }
258
259    pub fn secrets(&self) -> kube::Result<api::Api<corev1::Secret>> {
260        self.namespaced_api()
261    }
262
263    pub fn componentstatuses(&self) -> kube::Result<api::Api<corev1::ComponentStatus>> {
264        self.cluster_api()
265    }
266
267    pub fn nodes(&self) -> kube::Result<api::Api<corev1::Node>> {
268        self.cluster_api()
269    }
270
271    pub fn selfsubjectaccessreviews(
272        &self,
273    ) -> kube::Result<api::Api<authorizationv1::SelfSubjectAccessReview>> {
274        self.cluster_api()
275    }
276
277    pub fn selfsubjectrulesreviews(
278        &self,
279    ) -> kube::Result<api::Api<authorizationv1::SelfSubjectRulesReview>> {
280        self.cluster_api()
281    }
282
283    pub fn selfsubjectreviews(
284        &self,
285    ) -> kube::Result<api::Api<authenticationv1::SelfSubjectReview>> {
286        self.cluster_api()
287    }
288
289    pub fn inspect<K>(&self, k: &K)
290    where
291        K: serde::Serialize,
292    {
293        if self.debug {
294            let k = yaml::to_string(k).unwrap_or_default();
295            println!("{k}");
296        }
297    }
298
299    pub fn inspect_err(&self, err: &kube::Error) {
300        if self.debug {
301            println!("{err:?}");
302        }
303    }
304
305    fn cluster_api<K>(&self) -> kube::Result<api::Api<K>>
306    where
307        K: kube::Resource<Scope = k8s::openapi::ClusterResourceScope>,
308        <K as kube::Resource>::DynamicType: Default,
309    {
310        self.client().map(|client| client.api())
311    }
312
313    fn namespaced_api<K>(&self) -> kube::Result<api::Api<K>>
314    where
315        K: kube::Resource<Scope = k8s::openapi::NamespaceResourceScope>,
316        <K as kube::Resource>::DynamicType: Default,
317    {
318        let client = self.client()?;
319        let api = match &self.namespace {
320            Namespace::All => client.api(),
321            Namespace::Default => client.default_namespaced_api(),
322            Namespace::Namespace(namespace) => client.namespaced_api(namespace),
323        };
324        Ok(api)
325    }
326
327    pub fn full_name<K>(&self, k: &K) -> String
328    where
329        K: kube::Resource + kube::ResourceExt,
330        <K as kube::Resource>::DynamicType: Default,
331    {
332        let kind = K::kind(&default()).to_lowercase();
333        let name = k.name_any();
334        format!("{kind}/{name}")
335    }
336}
337
338impl Kubeapi {
339    pub fn local() -> Self {
340        let config = kube::Config::new("http://localhost:6443".parse().unwrap());
341        Self {
342            config,
343            kubeconfig: default(),
344            cache: default(),
345            namespace: default(),
346            debug: default(),
347            options: default(),
348        }
349    }
350}
351
352fn default<T: Default>() -> T {
353    T::default()
354}