1use std::{future, pin::pin};
3
4use futures::TryStreamExt;
5use kube_client::{Api, Resource};
6use serde::de::DeserializeOwned;
7use std::fmt::Debug;
8use thiserror::Error;
9
10use crate::watcher::{self, watch_object};
11
12#[derive(Debug, Error)]
14pub enum Error {
15 #[error("failed to probe for whether the condition is fulfilled yet: {0}")]
17 ProbeFailed(#[source] watcher::Error),
18}
19
20#[allow(clippy::missing_panics_doc)] pub async fn await_condition<K>(api: Api<K>, name: &str, cond: impl Condition<K>) -> Result<Option<K>, Error>
55where
56 K: Clone + Debug + Send + DeserializeOwned + Resource + 'static,
57{
58 let mut stream = pin!(watch_object(api, name).try_skip_while(|obj| {
60 let matches = cond.matches_object(obj.as_ref());
61 future::ready(Ok(!matches))
62 }));
63
64 let obj = stream
66 .try_next()
67 .await
68 .map_err(Error::ProbeFailed)?
69 .expect("stream must not terminate");
70 Ok(obj)
71}
72
73pub trait Condition<K> {
96 fn matches_object(&self, obj: Option<&K>) -> bool;
98
99 fn not(self) -> conditions::Not<Self>
110 where
111 Self: Sized,
112 {
113 conditions::Not(self)
114 }
115
116 fn and<Other: Condition<K>>(self, other: Other) -> conditions::And<Self, Other>
130 where
131 Self: Sized,
132 {
133 conditions::And(self, other)
134 }
135
136 fn or<Other: Condition<K>>(self, other: Other) -> conditions::Or<Self, Other>
150 where
151 Self: Sized,
152 {
153 conditions::Or(self, other)
154 }
155}
156
157impl<K, F: Fn(Option<&K>) -> bool> Condition<K> for F {
158 fn matches_object(&self, obj: Option<&K>) -> bool {
159 (self)(obj)
160 }
161}
162
163pub mod conditions {
165 pub use super::Condition;
166 use k8s_openapi::{
167 api::{
168 apps::v1::Deployment,
169 batch::v1::Job,
170 core::v1::{Pod, Service},
171 networking::v1::Ingress,
172 },
173 apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition,
174 };
175 use kube_client::Resource;
176
177 #[must_use]
183 pub fn is_deleted<K: Resource>(uid: &str) -> impl Condition<K> + '_ {
184 move |obj: Option<&K>| {
185 obj.is_none_or(
187 |obj| obj.meta().uid.as_deref() != Some(uid),
189 )
190 }
191 }
192
193 #[must_use]
197 pub fn is_created<K: Resource>() -> impl Condition<K> {
198 |obj: Option<&K>| obj.is_some()
199 }
200
201 #[must_use]
206 pub fn is_crd_established() -> impl Condition<CustomResourceDefinition> {
207 |obj: Option<&CustomResourceDefinition>| {
208 if let Some(o) = obj
209 && let Some(s) = &o.status
210 && let Some(conds) = &s.conditions
211 && let Some(pcond) = conds.iter().find(|c| c.type_ == "Established")
212 {
213 return pcond.status == "True";
214 }
215 false
216 }
217 }
218
219 #[must_use]
221 pub fn is_pod_running() -> impl Condition<Pod> {
222 |obj: Option<&Pod>| {
223 if let Some(pod) = &obj
224 && let Some(status) = &pod.status
225 && let Some(phase) = &status.phase
226 {
227 return phase == "Running";
228 }
229 false
230 }
231 }
232
233 #[must_use]
235 pub fn is_job_completed() -> impl Condition<Job> {
236 |obj: Option<&Job>| {
237 if let Some(job) = &obj
238 && let Some(s) = &job.status
239 && let Some(conds) = &s.conditions
240 && let Some(pcond) = conds.iter().find(|c| c.type_ == "Complete")
241 {
242 return pcond.status == "True";
243 }
244 false
245 }
246 }
247
248 #[must_use]
253 pub fn is_deployment_completed() -> impl Condition<Deployment> {
254 |obj: Option<&Deployment>| {
255 if let Some(depl) = &obj
256 && let Some(s) = &depl.status
257 && let Some(conds) = &s.conditions
258 && let Some(dcond) = conds.iter().find(|c| {
259 k8s_openapi::k8s_if_ge_1_37! {
260 c.type_.as_deref() == Some("Progressing")
261 && c.reason.as_deref() == Some("NewReplicaSetAvailable")
262 }
263 k8s_openapi::k8s_if_le_1_36! {
264 c.type_ == "Progressing" && c.reason == Some("NewReplicaSetAvailable".to_string())
265 }
266 })
267 {
268 k8s_openapi::k8s_if_ge_1_37! {
269 return dcond.status.as_deref() == Some("True");
270 }
271 k8s_openapi::k8s_if_le_1_36! {
272 return dcond.status == "True";
273 }
274 }
275 false
276 }
277 }
278
279 #[must_use]
281 pub fn is_service_loadbalancer_provisioned() -> impl Condition<Service> {
282 |obj: Option<&Service>| {
283 if let Some(svc) = &obj
284 && let Some(spec) = &svc.spec
285 {
286 if spec.type_ != Some("LoadBalancer".to_string()) {
288 return true;
289 }
290 if let Some(s) = &svc.status
292 && let Some(lbs) = &s.load_balancer
293 && let Some(ings) = &lbs.ingress
294 {
295 return ings.iter().all(|ip| ip.ip.is_some() || ip.hostname.is_some());
296 }
297 }
298 false
299 }
300 }
301
302 #[must_use]
304 pub fn is_ingress_provisioned() -> impl Condition<Ingress> {
305 |obj: Option<&Ingress>| {
306 if let Some(ing) = &obj
307 && let Some(s) = &ing.status
308 && let Some(lbs) = &s.load_balancer
309 && let Some(ings) = &lbs.ingress
310 {
311 return ings.iter().all(|ip| ip.ip.is_some() || ip.hostname.is_some());
312 }
313 false
314 }
315 }
316
317 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
319 pub struct Not<A>(pub(super) A);
320 impl<A: Condition<K>, K> Condition<K> for Not<A> {
321 fn matches_object(&self, obj: Option<&K>) -> bool {
322 !self.0.matches_object(obj)
323 }
324 }
325
326 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
328 pub struct And<A, B>(pub(super) A, pub(super) B);
329 impl<A, B, K> Condition<K> for And<A, B>
330 where
331 A: Condition<K>,
332 B: Condition<K>,
333 {
334 fn matches_object(&self, obj: Option<&K>) -> bool {
335 self.0.matches_object(obj) && self.1.matches_object(obj)
336 }
337 }
338
339 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
341 pub struct Or<A, B>(pub(super) A, pub(super) B);
342 impl<A, B, K> Condition<K> for Or<A, B>
343 where
344 A: Condition<K>,
345 B: Condition<K>,
346 {
347 fn matches_object(&self, obj: Option<&K>) -> bool {
348 self.0.matches_object(obj) || self.1.matches_object(obj)
349 }
350 }
351
352 mod tests {
353 #[test]
354 fn crd_established_ok() {
356 use super::{Condition, is_crd_established};
357
358 let crd = r#"
359 apiVersion: apiextensions.k8s.io/v1
360 kind: CustomResourceDefinition
361 metadata:
362 name: testthings.kube.rs
363 spec:
364 group: kube.rs
365 names:
366 categories: []
367 kind: TestThing
368 plural: testthings
369 shortNames: []
370 singular: testthing
371 scope: Namespaced
372 versions:
373 - additionalPrinterColumns: []
374 name: v1
375 schema:
376 openAPIV3Schema:
377 type: object
378 x-kubernetes-preserve-unknown-fields: true
379 served: true
380 storage: true
381 status:
382 acceptedNames:
383 kind: TestThing
384 listKind: TestThingList
385 plural: testthings
386 singular: testthing
387 conditions:
388 - lastTransitionTime: "2025-03-06T03:10:03Z"
389 message: no conflicts found
390 reason: NoConflicts
391 status: "True"
392 type: NamesAccepted
393 - lastTransitionTime: "2025-03-06T03:10:03Z"
394 message: the initial names have been accepted
395 reason: InitialNamesAccepted
396 status: "True"
397 type: Established
398 storedVersions:
399 - v1
400 "#;
401
402 let c = serde_saphyr::from_str(crd).unwrap();
403 assert!(is_crd_established().matches_object(Some(&c)))
404 }
405
406 #[test]
407 fn crd_established_fail() {
409 use super::{Condition, is_crd_established};
410
411 let crd = r#"
412 apiVersion: apiextensions.k8s.io/v1
413 kind: CustomResourceDefinition
414 metadata:
415 name: testthings.kube.rs
416 spec:
417 group: kube.rs
418 names:
419 categories: []
420 kind: TestThing
421 plural: testthings
422 shortNames: []
423 singular: testthing
424 scope: Namespaced
425 versions:
426 - additionalPrinterColumns: []
427 name: v1
428 schema:
429 openAPIV3Schema:
430 type: object
431 x-kubernetes-preserve-unknown-fields: true
432 served: true
433 storage: true
434 status:
435 acceptedNames:
436 kind: TestThing
437 listKind: TestThingList
438 plural: testthings
439 singular: testthing
440 conditions:
441 - lastTransitionTime: "2025-03-06T03:10:03Z"
442 message: no conflicts found
443 reason: NoConflicts
444 status: "True"
445 type: NamesAccepted
446 - lastTransitionTime: "2025-03-06T03:10:03Z"
447 message: the initial names have been accepted
448 reason: InitialNamesAccepted
449 status: "False"
450 type: Established
451 storedVersions:
452 - v1
453 "#;
454
455 let c = serde_saphyr::from_str(crd).unwrap();
456 assert!(!is_crd_established().matches_object(Some(&c)))
457 }
458
459 #[test]
460 fn crd_established_missing() {
462 use super::{Condition, is_crd_established};
463
464 assert!(!is_crd_established().matches_object(None))
465 }
466
467 #[test]
468 fn pod_running_ok() {
470 use super::{Condition, is_pod_running};
471
472 let pod = r#"
473 apiVersion: v1
474 kind: Pod
475 metadata:
476 namespace: default
477 name: testpod
478 spec:
479 containers:
480 - name: testcontainer
481 image: alpine
482 command: [ sleep ]
483 args: [ "100000" ]
484 status:
485 conditions:
486 - lastProbeTime: null
487 lastTransitionTime: "2025-03-06T03:53:07Z"
488 status: "True"
489 type: PodReadyToStartContainers
490 - lastProbeTime: null
491 lastTransitionTime: "2025-03-06T03:52:58Z"
492 status: "True"
493 type: Initialized
494 - lastProbeTime: null
495 lastTransitionTime: "2025-03-06T03:53:24Z"
496 status: "True"
497 type: Ready
498 - lastProbeTime: null
499 lastTransitionTime: "2025-03-06T03:53:24Z"
500 status: "True"
501 type: ContainersReady
502 - lastProbeTime: null
503 lastTransitionTime: "2025-03-06T03:52:58Z"
504 status: "True"
505 type: PodScheduled
506 containerStatuses:
507 - containerID: containerd://598323380ae59d60c1ab98f9091c94659137a976d52136a8083775d47fea5875
508 image: docker.io/library/alpine:latest
509 imageID: docker.io/library/alpine@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
510 lastState: {}
511 name: testcontainer
512 ready: true
513 restartCount: 0
514 started: true
515 state:
516 running:
517 startedAt: "2025-03-06T03:59:20Z"
518 phase: Running
519 qosClass: Burstable
520 "#;
521
522 let p = serde_saphyr::from_str(pod).unwrap();
523 assert!(is_pod_running().matches_object(Some(&p)))
524 }
525
526 #[test]
527 fn pod_running_unschedulable() {
529 use super::{Condition, is_pod_running};
530
531 let pod = r#"
532 apiVersion: v1
533 kind: Pod
534 metadata:
535 namespace: default
536 name: testpod
537 spec:
538 containers:
539 - name: testcontainer
540 image: alpine
541 command: [ sleep ]
542 args: [ "100000" ]
543 status:
544 conditions:
545 - lastProbeTime: null
546 lastTransitionTime: "2025-03-06T03:52:25Z"
547 message: '0/1 nodes are available: 1 node(s) were unschedulable. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling.'
548 reason: Unschedulable
549 status: "False"
550 type: PodScheduled
551 phase: Pending
552 qosClass: Burstable
553 "#;
554
555 let p = serde_saphyr::from_str(pod).unwrap();
556 assert!(!is_pod_running().matches_object(Some(&p)))
557 }
558
559 #[test]
560 fn pod_running_missing() {
562 use super::{Condition, is_pod_running};
563
564 assert!(!is_pod_running().matches_object(None))
565 }
566
567 #[test]
568 fn job_completed_ok() {
570 use super::{Condition, is_job_completed};
571
572 let job = r#"
573 apiVersion: batch/v1
574 kind: Job
575 metadata:
576 name: pi
577 namespace: default
578 spec:
579 template:
580 spec:
581 containers:
582 - name: pi
583 command:
584 - perl
585 - -Mbignum=bpi
586 - -wle
587 - print bpi(2000)
588 image: perl:5.34.0
589 imagePullPolicy: IfNotPresent
590 status:
591 completionTime: "2025-03-06T05:27:56Z"
592 conditions:
593 - lastProbeTime: "2025-03-06T05:27:56Z"
594 lastTransitionTime: "2025-03-06T05:27:56Z"
595 message: Reached expected number of succeeded pods
596 reason: CompletionsReached
597 status: "True"
598 type: SuccessCriteriaMet
599 - lastProbeTime: "2025-03-06T05:27:56Z"
600 lastTransitionTime: "2025-03-06T05:27:56Z"
601 message: Reached expected number of succeeded pods
602 reason: CompletionsReached
603 status: "True"
604 type: Complete
605 ready: 0
606 startTime: "2025-03-06T05:27:27Z"
607 succeeded: 1
608 terminating: 0
609 uncountedTerminatedPods: {}
610 "#;
611
612 let j = serde_saphyr::from_str(job).unwrap();
613 assert!(is_job_completed().matches_object(Some(&j)))
614 }
615
616 #[test]
617 fn job_completed_running() {
619 use super::{Condition, is_job_completed};
620
621 let job = r#"
622 apiVersion: batch/v1
623 kind: Job
624 metadata:
625 name: pi
626 namespace: default
627 spec:
628 backoffLimit: 4
629 completionMode: NonIndexed
630 completions: 1
631 manualSelector: false
632 parallelism: 1
633 template:
634 spec:
635 containers:
636 - name: pi
637 command:
638 - perl
639 - -Mbignum=bpi
640 - -wle
641 - print bpi(2000)
642 image: perl:5.34.0
643 imagePullPolicy: IfNotPresent
644 status:
645 active: 1
646 ready: 0
647 startTime: "2025-03-06T05:27:27Z"
648 terminating: 0
649 uncountedTerminatedPods: {}
650 "#;
651
652 let j = serde_saphyr::from_str(job).unwrap();
653 assert!(!is_job_completed().matches_object(Some(&j)))
654 }
655
656 #[test]
657 fn job_completed_missing() {
659 use super::{Condition, is_job_completed};
660
661 assert!(!is_job_completed().matches_object(None))
662 }
663
664 #[test]
665 fn deployment_completed_ok() {
667 use super::{Condition, is_deployment_completed};
668
669 let depl = r#"
670 apiVersion: apps/v1
671 kind: Deployment
672 metadata:
673 name: testapp
674 namespace: default
675 spec:
676 progressDeadlineSeconds: 600
677 replicas: 3
678 revisionHistoryLimit: 10
679 selector:
680 matchLabels:
681 app: test
682 strategy:
683 rollingUpdate:
684 maxSurge: 25%
685 maxUnavailable: 25%
686 type: RollingUpdate
687 template:
688 metadata:
689 creationTimestamp: null
690 labels:
691 app: test
692 spec:
693 containers:
694 - image: postgres
695 imagePullPolicy: Always
696 name: postgres
697 ports:
698 - containerPort: 5432
699 protocol: TCP
700 env:
701 - name: POSTGRES_PASSWORD
702 value: foobar
703 status:
704 availableReplicas: 3
705 conditions:
706 - lastTransitionTime: "2025-03-06T06:06:57Z"
707 lastUpdateTime: "2025-03-06T06:06:57Z"
708 message: Deployment has minimum availability.
709 reason: MinimumReplicasAvailable
710 status: "True"
711 type: Available
712 - lastTransitionTime: "2025-03-06T06:03:20Z"
713 lastUpdateTime: "2025-03-06T06:06:57Z"
714 message: ReplicaSet "testapp-7fcd4b58c9" has successfully progressed.
715 reason: NewReplicaSetAvailable
716 status: "True"
717 type: Progressing
718 observedGeneration: 2
719 readyReplicas: 3
720 replicas: 3
721 updatedReplicas: 3
722 "#;
723
724 let d = serde_saphyr::from_str(depl).unwrap();
725 assert!(is_deployment_completed().matches_object(Some(&d)))
726 }
727
728 #[test]
729 fn deployment_completed_pending() {
731 use super::{Condition, is_deployment_completed};
732
733 let depl = r#"
734 apiVersion: apps/v1
735 kind: Deployment
736 metadata:
737 name: testapp
738 namespace: default
739 spec:
740 progressDeadlineSeconds: 600
741 replicas: 3
742 revisionHistoryLimit: 10
743 selector:
744 matchLabels:
745 app: test
746 strategy:
747 rollingUpdate:
748 maxSurge: 25%
749 maxUnavailable: 25%
750 type: RollingUpdate
751 template:
752 metadata:
753 creationTimestamp: null
754 labels:
755 app: test
756 spec:
757 containers:
758 - image: postgres
759 imagePullPolicy: Always
760 name: postgres
761 ports:
762 - containerPort: 5432
763 protocol: TCP
764 env:
765 - name: POSTGRES_PASSWORD
766 value: foobar
767 status:
768 conditions:
769 - lastTransitionTime: "2025-03-06T06:03:20Z"
770 lastUpdateTime: "2025-03-06T06:03:20Z"
771 message: Deployment does not have minimum availability.
772 reason: MinimumReplicasUnavailable
773 status: "False"
774 type: Available
775 - lastTransitionTime: "2025-03-06T06:03:20Z"
776 lastUpdateTime: "2025-03-06T06:03:20Z"
777 message: ReplicaSet "testapp-77789cd7d4" is progressing.
778 reason: ReplicaSetUpdated
779 status: "True"
780 type: Progressing
781 observedGeneration: 1
782 replicas: 3
783 unavailableReplicas: 3
784 updatedReplicas: 3
785 "#;
786
787 let d = serde_saphyr::from_str(depl).unwrap();
788 assert!(!is_deployment_completed().matches_object(Some(&d)))
789 }
790
791 #[test]
792 fn deployment_completed_missing() {
794 use super::{Condition, is_deployment_completed};
795
796 assert!(!is_deployment_completed().matches_object(None))
797 }
798
799 #[test]
800 fn service_lb_provisioned_ok_ip() {
802 use super::{Condition, is_service_loadbalancer_provisioned};
803
804 let service = r"
805 apiVersion: v1
806 kind: Service
807 metadata:
808 name: test
809 spec:
810 selector:
811 app.kubernetes.io/name: test
812 type: LoadBalancer
813 ports:
814 - protocol: TCP
815 port: 80
816 targetPort: 9376
817 clusterIP: 10.0.171.239
818 status:
819 loadBalancer:
820 ingress:
821 - ip: 192.0.2.127
822 ";
823
824 let s = serde_saphyr::from_str(service).unwrap();
825 assert!(is_service_loadbalancer_provisioned().matches_object(Some(&s)))
826 }
827
828 #[test]
829 fn service_lb_provisioned_ok_hostname() {
831 use super::{Condition, is_service_loadbalancer_provisioned};
832
833 let service = r"
834 apiVersion: v1
835 kind: Service
836 metadata:
837 name: test
838 spec:
839 selector:
840 app.kubernetes.io/name: test
841 type: LoadBalancer
842 ports:
843 - protocol: TCP
844 port: 80
845 targetPort: 9376
846 clusterIP: 10.0.171.239
847 status:
848 loadBalancer:
849 ingress:
850 - hostname: example.exposed.service
851 ";
852
853 let s = serde_saphyr::from_str(service).unwrap();
854 assert!(is_service_loadbalancer_provisioned().matches_object(Some(&s)))
855 }
856
857 #[test]
858 fn service_lb_provisioned_pending() {
860 use super::{Condition, is_service_loadbalancer_provisioned};
861
862 let service = r"
863 apiVersion: v1
864 kind: Service
865 metadata:
866 name: test
867 spec:
868 selector:
869 app.kubernetes.io/name: test
870 type: LoadBalancer
871 ports:
872 - protocol: TCP
873 port: 80
874 targetPort: 9376
875 clusterIP: 10.0.171.239
876 status:
877 loadBalancer: {}
878 ";
879
880 let s = serde_saphyr::from_str(service).unwrap();
881 assert!(!is_service_loadbalancer_provisioned().matches_object(Some(&s)))
882 }
883
884 #[test]
885 fn service_lb_provisioned_not_loadbalancer() {
887 use super::{Condition, is_service_loadbalancer_provisioned};
888
889 let service = r"
890 apiVersion: v1
891 kind: Service
892 metadata:
893 name: test
894 spec:
895 selector:
896 app.kubernetes.io/name: test
897 type: ClusterIP
898 ports:
899 - protocol: TCP
900 port: 80
901 targetPort: 9376
902 status:
903 loadBalancer: {}
904 ";
905
906 let s = serde_saphyr::from_str(service).unwrap();
907 assert!(is_service_loadbalancer_provisioned().matches_object(Some(&s)))
908 }
909
910 #[test]
911 fn service_lb_provisioned_missing() {
913 use super::{Condition, is_service_loadbalancer_provisioned};
914
915 assert!(!is_service_loadbalancer_provisioned().matches_object(None))
916 }
917
918 #[test]
919 fn ingress_provisioned_ok_ip() {
921 use super::{Condition, is_ingress_provisioned};
922
923 let ingress = r#"
924 apiVersion: networking.k8s.io/v1
925 kind: Ingress
926 metadata:
927 name: test
928 namespace: default
929 resourceVersion: "1401"
930 uid: d653ee4d-0adb-40d9-b03c-7f84f35d4a67
931 spec:
932 ingressClassName: nginx
933 rules:
934 - host: httpbin.local
935 http:
936 paths:
937 - path: /
938 backend:
939 service:
940 name: httpbin
941 port:
942 number: 80
943 status:
944 loadBalancer:
945 ingress:
946 - ip: 10.89.7.3
947 "#;
948
949 let i = serde_saphyr::from_str(ingress).unwrap();
950 assert!(is_ingress_provisioned().matches_object(Some(&i)))
951 }
952
953 #[test]
954 fn ingress_provisioned_ok_hostname() {
956 use super::{Condition, is_ingress_provisioned};
957
958 let ingress = r#"
959 apiVersion: networking.k8s.io/v1
960 kind: Ingress
961 metadata:
962 name: test
963 namespace: default
964 resourceVersion: "1401"
965 uid: d653ee4d-0adb-40d9-b03c-7f84f35d4a67
966 spec:
967 ingressClassName: nginx
968 rules:
969 - host: httpbin.local
970 http:
971 paths:
972 - path: /
973 backend:
974 service:
975 name: httpbin
976 port:
977 number: 80
978 status:
979 loadBalancer:
980 ingress:
981 - hostname: example.exposed.service
982 "#;
983
984 let i = serde_saphyr::from_str(ingress).unwrap();
985 assert!(is_ingress_provisioned().matches_object(Some(&i)))
986 }
987
988 #[test]
989 fn ingress_provisioned_pending() {
991 use super::{Condition, is_ingress_provisioned};
992
993 let ingress = r#"
994 apiVersion: networking.k8s.io/v1
995 kind: Ingress
996 metadata:
997 name: test
998 namespace: default
999 resourceVersion: "1401"
1000 uid: d653ee4d-0adb-40d9-b03c-7f84f35d4a67
1001 spec:
1002 ingressClassName: nginx
1003 rules:
1004 - host: httpbin.local
1005 http:
1006 paths:
1007 - path: /
1008 backend:
1009 service:
1010 name: httpbin
1011 port:
1012 number: 80
1013 status:
1014 loadBalancer: {}
1015 "#;
1016
1017 let i = serde_saphyr::from_str(ingress).unwrap();
1018 assert!(!is_ingress_provisioned().matches_object(Some(&i)))
1019 }
1020
1021 #[test]
1022 fn ingress_provisioned_missing() {
1024 use super::{Condition, is_ingress_provisioned};
1025
1026 assert!(!is_ingress_provisioned().matches_object(None))
1027 }
1028 }
1029}
1030
1031pub mod delete {
1033 use super::{await_condition, conditions};
1034 use kube_client::{Api, Resource, api::DeleteParams};
1035 use serde::de::DeserializeOwned;
1036 use std::fmt::Debug;
1037 use thiserror::Error;
1038
1039 #[derive(Debug, Error)]
1041 pub enum Error {
1042 #[error("deleted object has no UID to wait for")]
1044 NoUid,
1045
1046 #[error("failed to delete object: {0}")]
1048 Delete(#[source] kube_client::Error),
1049
1050 #[error("failed to wait for object to be deleted: {0}")]
1052 Await(#[source] super::Error),
1053 }
1054
1055 pub async fn delete_and_finalize<K: Clone + Debug + Send + DeserializeOwned + Resource + 'static>(
1061 api: Api<K>,
1062 name: &str,
1063 delete_params: &DeleteParams,
1064 ) -> Result<(), Error> {
1065 let deleted_obj_uid = api
1066 .delete(name, delete_params)
1067 .await
1068 .map_err(Error::Delete)?
1069 .either(
1070 |mut obj| obj.meta_mut().uid.take(),
1071 |status| status.details.map(|details| details.uid),
1072 )
1073 .ok_or(Error::NoUid)?;
1074 await_condition(api, name, conditions::is_deleted(&deleted_obj_uid))
1075 .await
1076 .map_err(Error::Await)?;
1077 Ok(())
1078 }
1079}