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")]
164#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
165pub use kube_derive::CustomResource;
166
167#[cfg(feature = "derive")]
168#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
169pub use kube_derive::Resource;
170
171#[cfg(feature = "derive")]
172#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
173pub use kube_derive::CELSchema;
174
175#[cfg(feature = "runtime")]
176#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
177#[doc(inline)]
178pub use kube_runtime as runtime;
179
180pub use crate::core::{CustomResourceExt, Resource, ResourceExt};
181#[doc(inline)] pub use kube_core as core;
182
183#[cfg(test)]
185#[cfg(all(feature = "derive", feature = "runtime"))]
186mod mock_tests;
187
188pub mod prelude {
189 #[cfg(feature = "client")]
202 #[allow(unreachable_pub)]
203 pub use crate::client::ConfigExt as _;
204
205 #[cfg(feature = "unstable-client")] pub use crate::client::scope::NamespacedRef;
206
207 #[allow(unreachable_pub)] pub use crate::core::PartialObjectMetaExt as _;
208 #[allow(unreachable_pub)] pub use crate::core::SelectorExt as _;
209 pub use crate::{core::crd::CustomResourceExt as _, Resource as _, ResourceExt as _};
210
211 #[cfg(feature = "runtime")] pub use crate::runtime::utils::WatchStreamExt as _;
212}
213
214#[cfg(test)]
217#[cfg(all(feature = "derive", feature = "client"))]
218mod test {
219 use crate::{
220 api::{DeleteParams, Patch, PatchParams},
221 Api, Client, CustomResourceExt, Resource, ResourceExt,
222 };
223 use kube_derive::CustomResource;
224 use schemars::JsonSchema;
225 use serde::{Deserialize, Serialize};
226
227 #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
228 #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
229 #[kube(status = "FooStatus")]
230 #[kube(scale = r#"{"specReplicasPath":".spec.replicas", "statusReplicasPath":".status.replicas"}"#)]
231 #[kube(crates(kube_core = "crate::core"))] pub struct FooSpec {
233 name: String,
234 info: Option<String>,
235 replicas: isize,
236 }
237
238 #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
239 pub struct FooStatus {
240 is_bad: bool,
241 replicas: isize,
242 }
243
244 #[tokio::test]
245 #[ignore = "needs kubeconfig"]
246 async fn custom_resource_generates_correct_core_structs() {
247 use crate::core::{ApiResource, DynamicObject, GroupVersionKind};
248 let client = Client::try_default().await.unwrap();
249
250 let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo");
251 let api_resource = ApiResource::from_gvk(&gvk);
252 let a1: Api<DynamicObject> = Api::namespaced_with(client.clone(), "myns", &api_resource);
253 let a2: Api<Foo> = Api::namespaced(client, "myns");
254
255 assert_eq!(a1.resource_url(), a2.resource_url());
257 }
258
259 use k8s_openapi::{
260 api::core::v1::ConfigMap,
261 apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition,
262 };
263 #[tokio::test]
264 #[ignore = "needs cluster (creates + patches foo crd)"]
265 #[cfg(all(feature = "derive", feature = "runtime"))]
266 async fn derived_resource_queriable_and_has_subresources() -> Result<(), Box<dyn std::error::Error>> {
267 use crate::runtime::wait::{await_condition, conditions};
268
269 use serde_json::json;
270 let client = Client::try_default().await?;
271 let ssapply = PatchParams::apply("kube").force();
272 let crds: Api<CustomResourceDefinition> = Api::all(client.clone());
273 crds.patch("foos.clux.dev", &ssapply, &Patch::Apply(Foo::crd()))
275 .await?;
276 let establish = await_condition(crds.clone(), "foos.clux.dev", conditions::is_crd_established());
277 let _ = tokio::time::timeout(std::time::Duration::from_secs(10), establish).await?;
278 let foos: Api<Foo> = Api::default_namespaced(client.clone());
280 {
282 let foo = Foo::new("baz", FooSpec {
283 name: "baz".into(),
284 info: Some("old baz".into()),
285 replicas: 1,
286 });
287 let o = foos.patch("baz", &ssapply, &Patch::Apply(&foo)).await?;
288 assert_eq!(o.spec.name, "baz");
289 let oref = o.object_ref(&());
290 assert_eq!(oref.name.unwrap(), "baz");
291 assert_eq!(oref.uid, o.uid());
292 }
293 {
295 let patch = json!({
296 "apiVersion": "clux.dev/v1",
297 "kind": "Foo",
298 "spec": {
299 "name": "foo",
300 "replicas": 2
301 }
302 });
303 let o = foos.patch("baz", &ssapply, &Patch::Apply(patch)).await?;
304 assert_eq!(o.spec.replicas, 2, "patching spec updated spec.replicas");
305 }
306 {
308 let scale = foos.get_scale("baz").await?;
309 assert_eq!(scale.spec.unwrap().replicas, Some(2));
310 let status = foos.get_status("baz").await?;
311 assert!(status.status.is_none(), "nothing has set status");
312 }
313 {
315 let fs = serde_json::json!({"status": FooStatus { is_bad: false, replicas: 1 }});
316 let o = foos
317 .patch_status("baz", &Default::default(), &Patch::Merge(&fs))
318 .await?;
319 assert!(o.status.is_some(), "status set after patch_status");
320 }
321 {
323 let fs = serde_json::json!({"spec": { "replicas": 3 }});
324 let o = foos
325 .patch_scale("baz", &Default::default(), &Patch::Merge(&fs))
326 .await?;
327 assert_eq!(o.status.unwrap().replicas, 1, "scale replicas got patched");
328 let linked_replicas = o.spec.unwrap().replicas.unwrap();
329 assert_eq!(linked_replicas, 3, "patch_scale updates linked spec.replicas");
330 }
331
332 foos.delete_collection(&DeleteParams::default(), &Default::default())
334 .await?;
335 crds.delete("foos.clux.dev", &DeleteParams::default()).await?;
336 Ok(())
337 }
338
339 #[tokio::test]
340 #[ignore = "needs cluster (lists pods)"]
341 async fn custom_serialized_objects_are_queryable_and_iterable() -> Result<(), Box<dyn std::error::Error>>
342 {
343 use crate::core::{
344 object::{HasSpec, HasStatus, NotUsed, Object},
345 ApiResource,
346 };
347 use k8s_openapi::api::core::v1::Pod;
348 #[derive(Clone, Deserialize, Debug)]
349 struct PodSpecSimple {
350 containers: Vec<ContainerSimple>,
351 }
352 #[derive(Clone, Deserialize, Debug)]
353 struct ContainerSimple {
354 #[allow(dead_code)]
355 image: String,
356 }
357 type PodSimple = Object<PodSpecSimple, NotUsed>;
358
359 let ar = ApiResource::erase::<Pod>(&());
361
362 let client = Client::try_default().await?;
363 let api: Api<PodSimple> = Api::default_namespaced_with(client, &ar);
364 let mut list = api.list(&Default::default()).await?;
365 for pod in &mut list {
367 pod.spec_mut().containers = vec![];
368 *pod.status_mut() = None;
369 pod.annotations_mut()
370 .entry("kube-seen".to_string())
371 .or_insert_with(|| "yes".to_string());
372 pod.labels_mut()
373 .entry("kube.rs".to_string())
374 .or_insert_with(|| "hello".to_string());
375 pod.finalizers_mut().push("kube-finalizer".to_string());
376 pod.managed_fields_mut().clear();
377 }
379 for pod in list {
381 assert!(pod.annotations().get("kube-seen").is_some());
382 assert!(pod.labels().get("kube.rs").is_some());
383 assert!(pod.finalizers().contains(&"kube-finalizer".to_string()));
384 assert!(pod.spec().containers.is_empty());
385 assert!(pod.managed_fields().is_empty());
386 }
387 Ok(())
388 }
389
390 #[tokio::test]
391 #[ignore = "needs cluster (fetches api resources, and lists all)"]
392 #[cfg(feature = "derive")]
393 async fn derived_resources_discoverable() -> Result<(), Box<dyn std::error::Error>> {
394 use crate::{
395 core::{DynamicObject, GroupVersion, GroupVersionKind},
396 discovery::{self, verbs, ApiGroup, Discovery, Scope},
397 runtime::wait::{await_condition, conditions, Condition},
398 };
399
400 #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
401 #[kube(group = "kube.rs", version = "v1", kind = "TestCr", namespaced)]
402 #[kube(crates(kube_core = "crate::core"))] struct TestCrSpec {}
404
405 let client = Client::try_default().await?;
406
407 let crds: Api<CustomResourceDefinition> = Api::all(client.clone());
409 let ssapply = PatchParams::apply("kube").force();
410 crds.patch("testcrs.kube.rs", &ssapply, &Patch::Apply(TestCr::crd()))
411 .await?;
412 let establish = await_condition(crds.clone(), "testcrs.kube.rs", conditions::is_crd_established());
413 let crd = tokio::time::timeout(std::time::Duration::from_secs(10), establish).await??;
414 assert!(conditions::is_crd_established().matches_object(crd.as_ref()));
415 tokio::time::sleep(std::time::Duration::from_secs(2)).await; let gvk = GroupVersionKind::gvk("kube.rs", "v1", "TestCr");
419 let gv = GroupVersion::gv("kube.rs", "v1");
420
421 let apigroup = discovery::oneshot::pinned_group(&client, &gv).await?;
423 let (ar1, caps1) = apigroup.recommended_kind("TestCr").unwrap();
424 let (ar2, caps2) = discovery::pinned_kind(&client, &gvk).await?;
425 assert_eq!(caps1.operations.len(), caps2.operations.len(), "unequal caps");
426 assert_eq!(ar1, ar2, "unequal apiresource");
427 assert_eq!(DynamicObject::api_version(&ar2), "kube.rs/v1", "unequal dynver");
428
429 let discovery = Discovery::new(client.clone())
431 .exclude(&["rbac.authorization.k8s.io", "clux.dev"])
433 .run()
434 .await?;
435
436 assert!(discovery.has_group("kube.rs"), "missing group kube.rs");
438 let (ar, _caps) = discovery.resolve_gvk(&gvk).unwrap();
439 assert_eq!(ar.group, gvk.group, "unexpected discovered group");
440 assert_eq!(ar.version, gvk.version, "unexcepted discovered ver");
441 assert_eq!(ar.kind, gvk.kind, "unexpected discovered kind");
442
443 let mut groups = discovery.groups_alphabetical().into_iter();
445 let firstgroup = groups.next().unwrap();
446 assert_eq!(firstgroup.name(), ApiGroup::CORE_GROUP, "core not first");
447 for group in groups {
448 for (ar, caps) in group.recommended_resources() {
449 if !caps.supports_operation(verbs::LIST) {
450 continue;
451 }
452 let api: Api<DynamicObject> = if caps.scope == Scope::Namespaced {
453 Api::default_namespaced_with(client.clone(), &ar)
454 } else {
455 Api::all_with(client.clone(), &ar)
456 };
457 api.list(&Default::default()).await?;
458 }
459 }
460
461 crds.delete("testcrs.kube.rs", &DeleteParams::default()).await?;
463 Ok(())
464 }
465
466 #[tokio::test]
467 #[ignore = "needs cluster (will create await a pod)"]
468 #[cfg(feature = "runtime")]
469 async fn pod_can_await_conditions() -> Result<(), Box<dyn std::error::Error>> {
470 use crate::{
471 api::{DeleteParams, PostParams},
472 runtime::wait::{await_condition, conditions, delete::delete_and_finalize, Condition},
473 Api, Client,
474 };
475 use k8s_openapi::api::core::v1::Pod;
476 use std::time::Duration;
477 use tokio::time::timeout;
478
479 let client = Client::try_default().await?;
480 let pods: Api<Pod> = Api::default_namespaced(client);
481
482 let data: Pod = serde_json::from_value(serde_json::json!({
484 "apiVersion": "v1",
485 "kind": "Pod",
486 "metadata": {
487 "name": "busybox-kube4",
488 "labels": { "app": "kube-rs-test" },
489 },
490 "spec": {
491 "terminationGracePeriodSeconds": 1,
492 "restartPolicy": "Never",
493 "containers": [{
494 "name": "busybox",
495 "image": "busybox:1.34.1",
496 "command": ["sh", "-c", "sleep 20"],
497 }],
498 }
499 }))?;
500
501 let pp = PostParams::default();
502 assert_eq!(
503 data.name_unchecked(),
504 pods.create(&pp, &data).await?.name_unchecked()
505 );
506
507 let is_running = await_condition(pods.clone(), "busybox-kube4", conditions::is_pod_running());
509 let _ = timeout(Duration::from_secs(15), is_running).await?;
510
511 let pod = pods.get("busybox-kube4").await?;
513 assert_eq!(pod.spec.as_ref().unwrap().containers[0].name, "busybox");
514
515 fn is_each_container_ready() -> impl Condition<Pod> {
518 |obj: Option<&Pod>| {
519 if let Some(o) = obj {
520 if let Some(s) = &o.status {
521 if let Some(conds) = &s.conditions {
522 if let Some(pcond) = conds.iter().find(|c| c.type_ == "ContainersReady") {
523 return pcond.status == "True";
524 }
525 }
526 }
527 }
528 false
529 }
530 }
531 let is_fully_ready = await_condition(
532 pods.clone(),
533 "busybox-kube4",
534 conditions::is_pod_running().and(is_each_container_ready()),
535 );
536 let _ = timeout(Duration::from_secs(10), is_fully_ready).await?;
537
538 let dp = DeleteParams::default();
540 delete_and_finalize(pods.clone(), "busybox-kube4", &dp).await?;
541
542 assert!(pods.get("busybox-kube4").await.is_err());
544
545 Ok(())
546 }
547
548 #[tokio::test]
549 #[ignore = "needs cluster (lists cms)"]
550 async fn api_get_opt_handles_404() -> Result<(), Box<dyn std::error::Error>> {
551 let client = Client::try_default().await?;
552 let api = Api::<ConfigMap>::default_namespaced(client);
553 assert_eq!(
554 api.get_opt("this-cm-does-not-exist-ajklisdhfqkljwhreq").await?,
555 None
556 );
557 Ok(())
558 }
559}