1#![cfg_attr(docsrs, feature(doc_cfg))]
107
108macro_rules! cfg_client {
109 ($($item:item)*) => {
110 $(
111 #[cfg_attr(docsrs, doc(cfg(feature = "client")))]
112 #[cfg(feature = "client")]
113 $item
114 )*
115 }
116}
117macro_rules! cfg_config {
118 ($($item:item)*) => {
119 $(
120 #[cfg_attr(docsrs, doc(cfg(feature = "config")))]
121 #[cfg(feature = "config")]
122 $item
123 )*
124 }
125}
126
127macro_rules! cfg_error {
128 ($($item:item)*) => {
129 $(
130 #[cfg_attr(docsrs, doc(cfg(any(feature = "config", feature = "client"))))]
131 #[cfg(any(feature = "config", feature = "client"))]
132 $item
133 )*
134 }
135}
136
137cfg_client! {
138 pub use kube_client::api;
139 pub use kube_client::discovery;
140 pub use kube_client::client;
141
142 #[doc(inline)]
143 pub use api::Api;
144 #[doc(inline)]
145 pub use client::Client;
146 #[doc(inline)]
147 pub use discovery::Discovery;
148}
149
150cfg_config! {
151 pub use kube_client::config;
152 #[doc(inline)]
153 pub use config::Config;
154}
155
156cfg_error! {
157 pub use kube_client::error;
158 #[doc(inline)] pub use error::Error;
159 pub type Result<T, E = Error> = std::result::Result<T, E>;
161}
162
163#[cfg(feature = "derive")]
165#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
166pub use kube_derive::CustomResource;
167
168#[cfg(feature = "runtime")]
170#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
171#[doc(inline)]
172pub use kube_runtime as runtime;
173
174pub use crate::core::{CustomResourceExt, Resource, ResourceExt};
175#[doc(inline)]
177pub use kube_core as core;
178
179#[cfg(test)]
181#[cfg(all(feature = "derive", feature = "runtime"))]
182mod mock_tests;
183
184#[cfg(test)]
187#[cfg(all(feature = "derive", feature = "client"))]
188mod test {
189 use crate::{
190 api::{DeleteParams, Patch, PatchParams},
191 Api, Client, CustomResourceExt, Resource, ResourceExt,
192 };
193 use kube_derive::CustomResource;
194 use schemars::JsonSchema;
195 use serde::{Deserialize, Serialize};
196
197 #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
198 #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
199 #[kube(status = "FooStatus")]
200 #[kube(scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#)]
201 #[kube(crates(kube_core = "crate::core"))] pub struct FooSpec {
203 name: String,
204 info: Option<String>,
205 replicas: isize,
206 }
207
208 #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
209 pub struct FooStatus {
210 is_bad: bool,
211 replicas: isize,
212 }
213
214 #[tokio::test]
215 #[ignore = "needs kubeconfig"]
216 async fn custom_resource_generates_correct_core_structs() {
217 use crate::core::{ApiResource, DynamicObject, GroupVersionKind};
218 let client = Client::try_default().await.unwrap();
219
220 let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo");
221 let api_resource = ApiResource::from_gvk(&gvk);
222 let a1: Api<DynamicObject> = Api::namespaced_with(client.clone(), "myns", &api_resource);
223 let a2: Api<Foo> = Api::namespaced(client, "myns");
224
225 assert_eq!(a1.resource_url(), a2.resource_url());
227 }
228
229 use k8s_openapi::{
230 api::core::v1::ConfigMap,
231 apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition,
232 };
233 #[tokio::test]
234 #[ignore = "needs cluster (creates + patches foo crd)"]
235 #[cfg(all(feature = "derive", feature = "runtime"))]
236 async fn derived_resource_queriable_and_has_subresources() -> Result<(), Box<dyn std::error::Error>> {
237 use crate::runtime::wait::{await_condition, conditions};
238
239 use serde_json::json;
240 let client = Client::try_default().await?;
241 let ssapply = PatchParams::apply("kube").force();
242 let crds: Api<CustomResourceDefinition> = Api::all(client.clone());
243 crds.patch("foos.clux.dev", &ssapply, &Patch::Apply(Foo::crd()))
245 .await?;
246 let establish = await_condition(crds.clone(), "foos.clux.dev", conditions::is_crd_established());
247 let _ = tokio::time::timeout(std::time::Duration::from_secs(10), establish).await?;
248 let foos: Api<Foo> = Api::default_namespaced(client.clone());
250 {
252 let foo = Foo::new("baz", FooSpec {
253 name: "baz".into(),
254 info: Some("old baz".into()),
255 replicas: 1,
256 });
257 let o = foos.patch("baz", &ssapply, &Patch::Apply(&foo)).await?;
258 assert_eq!(o.spec.name, "baz");
259 let oref = o.object_ref(&());
260 assert_eq!(oref.name.unwrap(), "baz");
261 assert_eq!(oref.uid, o.uid());
262 }
263 {
265 let patch = json!({
266 "apiVersion": "clux.dev/v1",
267 "kind": "Foo",
268 "spec": {
269 "name": "foo",
270 "replicas": 2
271 }
272 });
273 let o = foos.patch("baz", &ssapply, &Patch::Apply(patch)).await?;
274 assert_eq!(o.spec.replicas, 2, "patching spec updated spec.replicas");
275 }
276 {
278 let scale = foos.get_scale("baz").await?;
279 assert_eq!(scale.spec.unwrap().replicas, Some(2));
280 let status = foos.get_status("baz").await?;
281 assert!(status.status.is_none(), "nothing has set status");
282 }
283 {
285 let fs = serde_json::json!({"status": FooStatus { is_bad: false, replicas: 1 }});
286 let o = foos
287 .patch_status("baz", &Default::default(), &Patch::Merge(&fs))
288 .await?;
289 assert!(o.status.is_some(), "status set after patch_status");
290 }
291 {
293 let fs = serde_json::json!({"spec": { "replicas": 3 }});
294 let o = foos
295 .patch_scale("baz", &Default::default(), &Patch::Merge(&fs))
296 .await?;
297 assert_eq!(o.status.unwrap().replicas, 1, "scale replicas got patched");
298 let linked_replicas = o.spec.unwrap().replicas.unwrap();
299 assert_eq!(linked_replicas, 3, "patch_scale updates linked spec.replicas");
300 }
301
302 foos.delete_collection(&DeleteParams::default(), &Default::default())
304 .await?;
305 crds.delete("foos.clux.dev", &DeleteParams::default()).await?;
306 Ok(())
307 }
308
309 #[tokio::test]
310 #[ignore = "needs cluster (lists pods)"]
311 async fn custom_serialized_objects_are_queryable_and_iterable() -> Result<(), Box<dyn std::error::Error>>
312 {
313 use crate::core::{
314 object::{HasSpec, HasStatus, NotUsed, Object},
315 ApiResource,
316 };
317 use k8s_openapi::api::core::v1::Pod;
318 #[derive(Clone, Deserialize, Debug)]
319 struct PodSpecSimple {
320 containers: Vec<ContainerSimple>,
321 }
322 #[derive(Clone, Deserialize, Debug)]
323 struct ContainerSimple {
324 #[allow(dead_code)]
325 image: String,
326 }
327 type PodSimple = Object<PodSpecSimple, NotUsed>;
328
329 let ar = ApiResource::erase::<Pod>(&());
331
332 let client = Client::try_default().await?;
333 let api: Api<PodSimple> = Api::default_namespaced_with(client, &ar);
334 let mut list = api.list(&Default::default()).await?;
335 for pod in &mut list {
337 pod.spec_mut().containers = vec![];
338 *pod.status_mut() = None;
339 pod.annotations_mut()
340 .entry("kube-seen".to_string())
341 .or_insert_with(|| "yes".to_string());
342 pod.labels_mut()
343 .entry("kube.rs".to_string())
344 .or_insert_with(|| "hello".to_string());
345 pod.finalizers_mut().push("kube-finalizer".to_string());
346 pod.managed_fields_mut().clear();
347 }
349 for pod in list {
351 assert!(pod.annotations().get("kube-seen").is_some());
352 assert!(pod.labels().get("kube.rs").is_some());
353 assert!(pod.finalizers().contains(&"kube-finalizer".to_string()));
354 assert!(pod.spec().containers.is_empty());
355 assert!(pod.managed_fields().is_empty());
356 }
357 Ok(())
358 }
359
360 #[tokio::test]
361 #[ignore = "needs cluster (fetches api resources, and lists all)"]
362 #[cfg(feature = "derive")]
363 async fn derived_resources_discoverable() -> Result<(), Box<dyn std::error::Error>> {
364 use crate::{
365 core::{DynamicObject, GroupVersion, GroupVersionKind},
366 discovery::{self, verbs, ApiGroup, Discovery, Scope},
367 runtime::wait::{await_condition, conditions, Condition},
368 };
369
370 #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
371 #[kube(group = "kube.rs", version = "v1", kind = "TestCr", namespaced)]
372 #[kube(crates(kube_core = "crate::core"))] struct TestCrSpec {}
374
375 let client = Client::try_default().await?;
376
377 let crds: Api<CustomResourceDefinition> = Api::all(client.clone());
379 let ssapply = PatchParams::apply("kube").force();
380 crds.patch("testcrs.kube.rs", &ssapply, &Patch::Apply(TestCr::crd()))
381 .await?;
382 let establish = await_condition(crds.clone(), "testcrs.kube.rs", conditions::is_crd_established());
383 let crd = tokio::time::timeout(std::time::Duration::from_secs(10), establish).await??;
384 assert!(conditions::is_crd_established().matches_object(crd.as_ref()));
385 tokio::time::sleep(std::time::Duration::from_secs(2)).await; let gvk = GroupVersionKind::gvk("kube.rs", "v1", "TestCr");
389 let gv = GroupVersion::gv("kube.rs", "v1");
390
391 let apigroup = discovery::oneshot::pinned_group(&client, &gv).await?;
393 let (ar1, caps1) = apigroup.recommended_kind("TestCr").unwrap();
394 let (ar2, caps2) = discovery::pinned_kind(&client, &gvk).await?;
395 assert_eq!(caps1.operations.len(), caps2.operations.len(), "unequal caps");
396 assert_eq!(ar1, ar2, "unequal apiresource");
397 assert_eq!(DynamicObject::api_version(&ar2), "kube.rs/v1", "unequal dynver");
398
399 let discovery = Discovery::new(client.clone())
401 .exclude(&["rbac.authorization.k8s.io", "clux.dev"])
403 .run()
404 .await?;
405
406 assert!(discovery.has_group("kube.rs"), "missing group kube.rs");
408 let (ar, _caps) = discovery.resolve_gvk(&gvk).unwrap();
409 assert_eq!(ar.group, gvk.group, "unexpected discovered group");
410 assert_eq!(ar.version, gvk.version, "unexcepted discovered ver");
411 assert_eq!(ar.kind, gvk.kind, "unexpected discovered kind");
412
413 let mut groups = discovery.groups_alphabetical().into_iter();
415 let firstgroup = groups.next().unwrap();
416 assert_eq!(firstgroup.name(), ApiGroup::CORE_GROUP, "core not first");
417 for group in groups {
418 for (ar, caps) in group.recommended_resources() {
419 if !caps.supports_operation(verbs::LIST) {
420 continue;
421 }
422 let api: Api<DynamicObject> = if caps.scope == Scope::Namespaced {
423 Api::default_namespaced_with(client.clone(), &ar)
424 } else {
425 Api::all_with(client.clone(), &ar)
426 };
427 api.list(&Default::default()).await?;
428 }
429 }
430
431 crds.delete("testcrs.kube.rs", &DeleteParams::default()).await?;
433 Ok(())
434 }
435
436 #[tokio::test]
437 #[ignore = "needs cluster (will create await a pod)"]
438 #[cfg(feature = "runtime")]
439 async fn pod_can_await_conditions() -> Result<(), Box<dyn std::error::Error>> {
440 use crate::{
441 api::{DeleteParams, PostParams},
442 runtime::wait::{await_condition, conditions, delete::delete_and_finalize, Condition},
443 Api, Client,
444 };
445 use k8s_openapi::api::core::v1::Pod;
446 use std::time::Duration;
447 use tokio::time::timeout;
448
449 let client = Client::try_default().await?;
450 let pods: Api<Pod> = Api::default_namespaced(client);
451
452 let data: Pod = serde_json::from_value(serde_json::json!({
454 "apiVersion": "v1",
455 "kind": "Pod",
456 "metadata": {
457 "name": "busybox-kube4",
458 "labels": { "app": "kube-rs-test" },
459 },
460 "spec": {
461 "terminationGracePeriodSeconds": 1,
462 "restartPolicy": "Never",
463 "containers": [{
464 "name": "busybox",
465 "image": "busybox:1.34.1",
466 "command": ["sh", "-c", "sleep 20"],
467 }],
468 }
469 }))?;
470
471 let pp = PostParams::default();
472 assert_eq!(
473 data.name_unchecked(),
474 pods.create(&pp, &data).await?.name_unchecked()
475 );
476
477 let is_running = await_condition(pods.clone(), "busybox-kube4", conditions::is_pod_running());
479 let _ = timeout(Duration::from_secs(15), is_running).await?;
480
481 let pod = pods.get("busybox-kube4").await?;
483 assert_eq!(pod.spec.as_ref().unwrap().containers[0].name, "busybox");
484
485 fn is_each_container_ready() -> impl Condition<Pod> {
488 |obj: Option<&Pod>| {
489 if let Some(o) = obj {
490 if let Some(s) = &o.status {
491 if let Some(conds) = &s.conditions {
492 if let Some(pcond) = conds.iter().find(|c| c.type_ == "ContainersReady") {
493 return pcond.status == "True";
494 }
495 }
496 }
497 }
498 false
499 }
500 }
501 let is_fully_ready = await_condition(
502 pods.clone(),
503 "busybox-kube4",
504 conditions::is_pod_running().and(is_each_container_ready()),
505 );
506 let _ = timeout(Duration::from_secs(10), is_fully_ready).await?;
507
508 let dp = DeleteParams::default();
510 delete_and_finalize(pods.clone(), "busybox-kube4", &dp).await?;
511
512 assert!(pods.get("busybox-kube4").await.is_err());
514
515 Ok(())
516 }
517
518 #[tokio::test]
519 #[ignore = "needs cluster (lists cms)"]
520 async fn api_get_opt_handles_404() -> Result<(), Box<dyn std::error::Error>> {
521 let client = Client::try_default().await?;
522 let api = Api::<ConfigMap>::default_namespaced(client);
523 assert_eq!(
524 api.get_opt("this-cm-does-not-exist-ajklisdhfqkljwhreq").await?,
525 None
526 );
527 Ok(())
528 }
529}