1use std::collections::BTreeMap;
2use std::error::Error as _;
3use std::sync::{Arc, Mutex};
4use std::time::{Duration, Instant};
5
6use futures::future::FutureExt;
7use futures::stream::StreamExt;
8use kube::api::Api;
9use kube::core::{ClusterResourceScope, NamespaceResourceScope};
10use kube::{Client, Resource, ResourceExt};
11use kube_runtime::controller::Action;
12use kube_runtime::finalizer::{Event as FinalizerEvent, finalizer};
13use kube_runtime::watcher;
14use rand::{RngExt, rng};
15use tracing::field::Empty;
16use tracing::{Instrument, Span, error, info, info_span, trace, warn};
17
18use crate::events::{Event, EventRecorder, EventType};
19use crate::observe::{Outcome, Pass, Phase, ReconcileObserver, TraceMetadata};
20
21#[derive(Debug, thiserror::Error)]
24pub enum Error<E: std::error::Error + 'static> {
25 #[error("{0}")]
28 ControllerError(#[source] E),
29 #[error("{0}")]
35 FinalizerError(#[source] kube_runtime::finalizer::Error<E>),
36}
37
38impl<E: std::error::Error + 'static> Error<E> {
39 pub fn display_chain(&self) -> String {
43 let mut out = self.to_string();
44 let mut source = self.source();
45 while let Some(err) = source {
46 let message = err.to_string();
47 if !out.ends_with(&message) {
50 out.push_str(": ");
51 out.push_str(&message);
52 }
53 source = err.source();
54 }
55 out
56 }
57
58 pub fn is_conflict(&self) -> bool {
66 if let Error::FinalizerError(
67 kube_runtime::finalizer::Error::AddFinalizer(e)
68 | kube_runtime::finalizer::Error::RemoveFinalizer(e),
69 ) = self
70 && is_failed_patch_test(e)
71 {
72 return true;
73 }
74 let mut source: Option<&(dyn std::error::Error + 'static)> = Some(self);
75 while let Some(err) = source {
76 if let Some(kube::Error::Api(status)) = err.downcast_ref::<kube::Error>()
77 && status.code == 409
78 && status.reason == "Conflict"
79 {
80 return true;
81 }
82 source = err.source();
83 }
84 false
85 }
86}
87
88fn is_failed_patch_test(e: &kube::Error) -> bool {
91 matches!(
95 e,
96 kube::Error::Api(status)
97 if status.code == 422
98 && status
99 .details
100 .as_ref()
101 .is_none_or(|d| d.kind.is_empty() && d.causes.is_empty())
102 )
103}
104
105struct Instrumentation {
107 name: Arc<str>,
108 observer: Option<Arc<dyn ReconcileObserver>>,
109 events: Option<Arc<EventRecorder>>,
110}
111
112pub struct Controller<Ctx: Context>
115where
116 Ctx: Send + Sync + 'static,
117 Ctx::Error: Send + Sync + 'static,
118 Ctx::Resource: Send + Sync + 'static,
119 Ctx::Resource: Clone + std::fmt::Debug + serde::Serialize,
120 for<'de> Ctx::Resource: serde::Deserialize<'de>,
121 <Ctx::Resource as Resource>::DynamicType:
122 Eq + Clone + std::hash::Hash + std::default::Default + std::fmt::Debug + std::marker::Unpin,
123{
124 client: kube::Client,
125 make_api: Box<dyn Fn(&Ctx::Resource) -> Api<Ctx::Resource> + Sync + Send + 'static>,
126 controller: kube_runtime::controller::Controller<Ctx::Resource>,
127 context: Ctx,
128 name: Option<Arc<str>>,
129 observer: Option<Arc<dyn ReconcileObserver>>,
130 events: Option<Arc<EventRecorder>>,
131}
132
133impl<Ctx: Context> Controller<Ctx>
134where
135 Ctx: Send + Sync + 'static,
136 Ctx::Error: Send + Sync + 'static,
137 Ctx::Resource: Clone + std::fmt::Debug + serde::Serialize,
138 for<'de> Ctx::Resource: serde::Deserialize<'de>,
139 <Ctx::Resource as Resource>::DynamicType:
140 Eq + Clone + std::hash::Hash + std::default::Default + std::fmt::Debug + std::marker::Unpin,
141{
142 pub fn namespaced(client: Client, context: Ctx, namespace: &str, wc: watcher::Config) -> Self
150 where
151 Ctx::Resource: Resource<Scope = NamespaceResourceScope>,
152 {
153 let make_api = {
154 let client = client.clone();
155 Box::new(move |resource: &Ctx::Resource| {
156 Api::<Ctx::Resource>::namespaced(client.clone(), &resource.namespace().unwrap())
157 })
158 };
159 let controller = kube_runtime::controller::Controller::new(
160 Api::<Ctx::Resource>::namespaced(client.clone(), namespace),
161 wc,
162 );
163 Self::new(client, make_api, controller, context)
164 }
165
166 pub fn namespaced_all(client: Client, context: Ctx, wc: watcher::Config) -> Self
174 where
175 Ctx::Resource: Resource<Scope = NamespaceResourceScope>,
176 {
177 let make_api = {
178 let client = client.clone();
179 Box::new(move |resource: &Ctx::Resource| {
180 Api::<Ctx::Resource>::namespaced(client.clone(), &resource.namespace().unwrap())
181 })
182 };
183 let controller = kube_runtime::controller::Controller::new(
184 Api::<Ctx::Resource>::all(client.clone()),
185 wc,
186 );
187 Self::new(client, make_api, controller, context)
188 }
189
190 pub fn cluster(client: Client, context: Ctx, wc: watcher::Config) -> Self
197 where
198 Ctx::Resource: Resource<Scope = ClusterResourceScope>,
199 {
200 let make_api = {
201 let client = client.clone();
202 Box::new(move |_: &Ctx::Resource| Api::<Ctx::Resource>::all(client.clone()))
203 };
204 let controller = kube_runtime::controller::Controller::new(
205 Api::<Ctx::Resource>::all(client.clone()),
206 wc,
207 );
208 Self::new(client, make_api, controller, context)
209 }
210
211 fn new(
212 client: Client,
213 make_api: Box<dyn Fn(&Ctx::Resource) -> Api<Ctx::Resource> + Sync + Send + 'static>,
214 controller: kube_runtime::controller::Controller<Ctx::Resource>,
215 context: Ctx,
216 ) -> Self {
217 Self {
218 client,
219 make_api,
220 controller,
221 context,
222 name: None,
223 observer: None,
224 events: None,
225 }
226 }
227
228 pub fn with_name(mut self, name: impl Into<String>) -> Self {
237 self.name = Some(name.into().into());
238 self
239 }
240
241 pub fn with_observer(mut self, observer: Arc<dyn ReconcileObserver>) -> Self {
245 self.observer = Some(observer);
246 self
247 }
248
249 pub fn with_event_recorder(mut self, events: Arc<EventRecorder>) -> Self {
260 self.events = Some(events);
261 self
262 }
263
264 pub async fn run(self) {
274 let Self {
275 client,
276 make_api,
277 controller,
278 context,
279 name,
280 observer,
281 events,
282 } = self;
283 let instrumentation = Arc::new(Instrumentation {
284 name: name.unwrap_or_else(|| match Ctx::FINALIZER_NAME {
285 Some(finalizer_name) => finalizer_name.into(),
286 None => Ctx::Resource::kind(&Default::default()).into(),
287 }),
288 observer,
289 events,
290 });
291 let instrumentation = &instrumentation;
292 let backoffs = Arc::new(Mutex::new(BTreeMap::new()));
293 let backoffs = &backoffs;
294 controller
295 .run(
296 |resource, context| {
297 let uid = resource.uid().unwrap();
298 let backoffs = Arc::clone(backoffs);
299 reconcile(
300 context,
301 client.clone(),
302 make_api(&resource),
303 resource,
304 Arc::clone(instrumentation),
305 )
306 .inspect(move |result| {
307 if result.is_ok() {
308 backoffs.lock().unwrap().remove(&uid);
309 }
310 })
311 },
312 |resource, err, context| {
313 let consecutive_errors = {
314 let uid = resource.uid().unwrap();
315 let mut backoffs = backoffs.lock().unwrap();
316 let consecutive_errors: u32 =
317 backoffs.get(&uid).copied().unwrap_or_default();
318 backoffs.insert(uid, consecutive_errors.saturating_add(1));
319 consecutive_errors
320 };
321 context.error_action(resource, err, consecutive_errors)
322 },
323 Arc::new(context),
324 )
325 .for_each(|res| async {
326 if let Err(e) = res
329 && !matches!(e, kube_runtime::controller::Error::ReconcilerFailed(..))
330 {
331 warn!(
334 error = %e,
335 source = e.source(),
336 "internal kube controller error",
337 );
338 }
339 })
340 .await
341 }
342
343 pub fn with_controller<F>(mut self, f: F) -> Self
348 where
349 F: FnOnce(
350 kube_runtime::Controller<Ctx::Resource>,
351 ) -> kube_runtime::Controller<Ctx::Resource>,
352 {
353 self.controller = f(self.controller);
354 self
355 }
356}
357
358#[cfg_attr(not(docsrs), async_trait::async_trait)]
361pub trait Context {
362 type Resource: Resource + Send + Sync + 'static;
365 type Error: std::error::Error;
368
369 const FINALIZER_NAME: Option<&'static str> = None;
376
377 async fn apply(
394 &self,
395 client: Client,
396 resource: &Self::Resource,
397 metadata: &mut TraceMetadata,
398 ) -> Result<Option<Action>, Self::Error>;
399
400 async fn cleanup(
413 &self,
414 client: Client,
415 resource: &Self::Resource,
416 metadata: &mut TraceMetadata,
417 ) -> Result<Option<Action>, Self::Error> {
418 let _client = client;
420 let _resource = resource;
421 let _metadata = metadata;
422
423 Ok(Some(Action::await_change()))
424 }
425
426 fn success_action(&self, resource: &Self::Resource) -> Action {
432 let _resource = resource;
434
435 Action::requeue(Duration::from_secs(rng().random_range(2400..3600)))
436 }
437
438 fn error_action(
446 self: Arc<Self>,
447 resource: Arc<Self::Resource>,
448 err: &Error<Self::Error>,
449 consecutive_errors: u32,
450 ) -> Action {
451 let _resource = resource;
453 let _err = err;
454
455 let seconds = 2u64.pow(consecutive_errors.min(7) + 1);
456 Action::requeue(Duration::from_millis(
457 rng().random_range((seconds * 500)..(seconds * 1000)),
458 ))
459 }
460
461 fn failure_event(
476 &self,
477 resource: &Self::Resource,
478 phase: Phase,
479 err: &Error<Self::Error>,
480 ) -> Option<Event> {
481 let _resource = resource;
483
484 let (reason, action) = match phase {
485 Phase::Cleanup | Phase::Delete => ("CleanupFailed", "Cleanup"),
486 _ => ("ReconcileFailed", "Reconcile"),
487 };
488 Some(Event {
489 type_: EventType::Warning,
490 reason: reason.to_owned(),
491 action: action.to_owned(),
492 note: Some(err.display_chain()),
493 related: None,
494 })
495 }
496}
497
498struct PassGuard {
501 pass: Arc<Pass>,
502 start: Instant,
503 phase: Option<Phase>,
504 fallback_phase: Phase,
505 finished: bool,
506}
507
508impl PassGuard {
509 fn phase(&self) -> Phase {
510 self.phase.unwrap_or(self.fallback_phase)
511 }
512
513 fn finish(&mut self, outcome: Outcome) {
514 self.finished = true;
515 self.pass
516 .record(self.phase(), outcome, self.start.elapsed());
517 }
518}
519
520impl Drop for PassGuard {
521 fn drop(&mut self) {
522 if !self.finished {
523 self.pass
524 .record(self.phase(), Outcome::Abandoned, self.start.elapsed());
525 }
526 }
527}
528
529async fn reconcile<Ctx>(
530 ctx: Arc<Ctx>,
531 client: Client,
532 api: Api<Ctx::Resource>,
533 resource: Arc<Ctx::Resource>,
534 instrumentation: Arc<Instrumentation>,
535) -> Result<Action, Error<Ctx::Error>>
536where
537 Ctx: Context + Send + Sync + 'static,
538 Ctx::Error: Send + Sync + 'static,
539 Ctx::Resource: Send + Sync + 'static,
540 Ctx::Resource: Clone + std::fmt::Debug + serde::Serialize,
541 for<'de> Ctx::Resource: serde::Deserialize<'de>,
542 <Ctx::Resource as Resource>::DynamicType:
543 Eq + Clone + std::hash::Hash + std::default::Default + std::fmt::Debug + std::marker::Unpin,
544{
545 let span = info_span!(
546 "reconcile",
547 resource_type = Ctx::Resource::kind(&Default::default()).as_ref(),
548 resource_name = resource.name_unchecked().as_str(),
549 controller = &*instrumentation.name,
550 event_type = Empty,
551 outcome = Empty,
552 success = Empty,
553 duration_seconds = Empty,
554 metadata = Empty,
555 );
556 async {
557 trace!("beginning reconciliation");
558
559 let pass = Arc::new(Pass {
560 controller: Arc::clone(&instrumentation.name),
561 reference: resource.object_ref(&Default::default()),
562 observer: instrumentation.observer.clone(),
563 events: instrumentation.events.clone(),
564 });
565 let mut metadata = TraceMetadata::for_pass(Arc::clone(&pass));
566 let mut guard = PassGuard {
567 pass: Arc::clone(&pass),
568 start: Instant::now(),
569 phase: None,
570 fallback_phase: if resource.meta().deletion_timestamp.is_some() {
571 Phase::Delete
572 } else {
573 Phase::Init
574 },
575 finished: false,
576 };
577 let mut reconciler_outcome = None;
578
579 let res = if let Some(finalizer_name) = Ctx::FINALIZER_NAME {
580 finalizer(&api, finalizer_name, Arc::clone(&resource), |event| async {
581 match event {
582 FinalizerEvent::Apply(resource) => {
583 guard.phase = Some(Phase::Apply);
584 let res = ctx.apply(client, &resource, &mut metadata).await;
585 reconciler_outcome = Some(Outcome::of_result(&res));
586 res.map(|action| action.unwrap_or_else(|| ctx.success_action(&resource)))
587 }
588 FinalizerEvent::Cleanup(resource) => {
589 guard.phase = Some(Phase::Cleanup);
590 let res = ctx.cleanup(client, &resource, &mut metadata).await;
591 reconciler_outcome = Some(Outcome::of_result(&res));
592 res.map(|action| action.unwrap_or_else(Action::await_change))
593 }
594 }
595 })
596 .await
597 .map_err(Error::FinalizerError)
598 } else if resource.meta().deletion_timestamp.is_none() {
599 guard.phase = Some(Phase::Apply);
600 let res = ctx.apply(client, &resource, &mut metadata).await;
601 reconciler_outcome = Some(Outcome::of_result(&res));
602 res.map(|action| action.unwrap_or_else(|| ctx.success_action(&resource)))
603 .map_err(Error::ControllerError)
604 } else {
605 Ok(Action::await_change())
606 };
607
608 let outcome = match &res {
609 Err(_) => Outcome::Failed,
610 Ok(_) => metadata
611 .outcome
612 .or(reconciler_outcome)
613 .unwrap_or(Outcome::Completed),
614 };
615 let phase = guard.phase();
616 let duration = guard.start.elapsed();
617 guard.finish(outcome);
618
619 let span = Span::current();
620 span.record("event_type", phase.as_str());
621 span.record("outcome", outcome.as_str());
622 span.record("duration_seconds", duration.as_secs_f64());
623
624 if !metadata.annotations.is_empty()
625 && let Ok(s) = serde_json::to_string(&metadata.annotations)
626 {
627 span.record("metadata", s);
628 }
629
630 if let Err(e) = &res {
631 span.record("success", false);
632 if e.is_conflict() {
633 info!(error = %e, source = e.source(), "reconcile");
638 } else {
639 error!(error = %e, source = e.source(), "reconcile");
640 }
641 } else {
642 span.record("success", true);
643 info!("reconcile");
644 }
645
646 if let Some(events) = &instrumentation.events {
647 match &res {
648 Err(e) => {
649 if let Some(event) = ctx.failure_event(&resource, phase, e)
650 && let Err(publish_err) =
651 events.publish_to(&pass.reference, &event, true).await
652 {
653 warn!(
654 error = %publish_err,
655 reason = %event.reason,
656 "failed to publish reconciliation failure event",
657 );
658 }
659 }
660 Ok(_) => {
661 if let Some(uid) = &pass.reference.uid {
662 events.forget_failures(uid);
663 }
664 }
665 }
666 }
667
668 res
669 }
670 .instrument(span)
671 .await
672}
673
674#[cfg(test)]
675mod tests {
676 use std::future::pending;
677 use std::sync::Mutex;
678
679 use futures::FutureExt;
680 use k8s_openapi::api::core::v1::ConfigMap;
681 use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, Time};
682 use k8s_openapi::jiff::Timestamp;
683 use kube::core::Status;
684 use kube::core::response::{StatusCause, StatusDetails, StatusSummary};
685
686 use super::*;
687 use crate::events::Reporter;
688 use crate::observe::{ReconcileRecord, StepRecord};
689 use crate::test_util::{MockApiServer, block_on};
690
691 #[derive(Debug, thiserror::Error)]
692 enum TestError {
693 #[error("reconciling failed: {0}")]
694 Wrapped(#[source] std::io::Error),
695 #[error("talking to the API server failed")]
696 Kube(#[source] kube::Error),
697 }
698
699 #[derive(Clone, Copy)]
700 enum Behavior {
701 Done,
702 Requeue,
703 Skip,
704 Fail,
705 FailQuietly,
706 Hang,
707 }
708
709 struct TestContext {
710 behavior: Mutex<Behavior>,
711 }
712
713 impl TestContext {
714 fn new(behavior: Behavior) -> Arc<Self> {
715 Arc::new(Self {
716 behavior: Mutex::new(behavior),
717 })
718 }
719
720 fn set(&self, behavior: Behavior) {
721 *self.behavior.lock().unwrap() = behavior;
722 }
723 }
724
725 #[async_trait::async_trait]
726 impl Context for TestContext {
727 type Resource = ConfigMap;
728 type Error = TestError;
729
730 async fn apply(
731 &self,
732 _client: Client,
733 _resource: &ConfigMap,
734 metadata: &mut TraceMetadata,
735 ) -> Result<Option<Action>, TestError> {
736 let behavior = *self.behavior.lock().unwrap();
737 let step = metadata.step("work");
738 match behavior {
739 Behavior::Done => {
740 step.finish(Outcome::Completed);
741 Ok(None)
742 }
743 Behavior::Requeue => {
744 step.finish(Outcome::Waiting);
745 Ok(Some(Action::requeue(Duration::from_secs(1))))
746 }
747 Behavior::Skip => {
748 step.finish(Outcome::Skipped);
749 metadata.set_outcome(Outcome::Skipped);
750 Ok(None)
751 }
752 Behavior::Fail | Behavior::FailQuietly => {
753 Err(TestError::Wrapped(std::io::Error::other("disk on fire")))
754 }
755 Behavior::Hang => pending().await,
756 }
757 }
758
759 fn failure_event(
760 &self,
761 _resource: &ConfigMap,
762 _phase: Phase,
763 err: &Error<TestError>,
764 ) -> Option<Event> {
765 match *self.behavior.lock().unwrap() {
766 Behavior::FailQuietly => None,
767 _ => Some(Event {
768 type_: EventType::Warning,
769 reason: "ReconcileFailed".to_owned(),
770 action: "Reconcile".to_owned(),
771 note: Some(err.display_chain()),
772 related: None,
773 }),
774 }
775 }
776 }
777
778 #[derive(Debug, Clone, PartialEq)]
779 enum Recorded {
780 Pass(String, Phase, Outcome),
781 Step(String, &'static str, Outcome),
782 }
783
784 #[derive(Default)]
785 struct TestObserver(Mutex<Vec<Recorded>>);
786
787 impl TestObserver {
788 fn take(&self) -> Vec<Recorded> {
789 std::mem::take(&mut self.0.lock().unwrap())
790 }
791 }
792
793 impl ReconcileObserver for TestObserver {
794 fn reconciled(&self, record: &ReconcileRecord<'_>) {
795 assert_eq!(record.kind, "ConfigMap");
796 assert_eq!(record.namespace, Some("ns"));
797 assert_eq!(record.name, "cm");
798 self.0.lock().unwrap().push(Recorded::Pass(
799 record.controller.to_owned(),
800 record.phase,
801 record.outcome,
802 ));
803 }
804
805 fn step_finished(&self, record: &StepRecord<'_>) {
806 self.0.lock().unwrap().push(Recorded::Step(
807 record.controller.to_owned(),
808 record.step,
809 record.outcome,
810 ));
811 }
812 }
813
814 struct Harness {
815 server: MockApiServer,
816 observer: Arc<TestObserver>,
817 instrumentation: Arc<Instrumentation>,
818 }
819
820 impl Harness {
821 fn new() -> Self {
822 let server = MockApiServer::new();
823 let observer = Arc::new(TestObserver::default());
824 let instrumentation = Arc::new(Instrumentation {
825 name: "test".into(),
826 observer: Some(Arc::<TestObserver>::clone(&observer)),
827 events: Some(Arc::new(EventRecorder::new(
828 server.client(),
829 Reporter {
830 controller: "test.example.com".to_owned(),
831 instance: None,
832 },
833 ))),
834 });
835 Self {
836 server,
837 observer,
838 instrumentation,
839 }
840 }
841
842 fn reconcile(
843 &self,
844 ctx: &Arc<TestContext>,
845 resource: ConfigMap,
846 ) -> impl Future<Output = Result<Action, Error<TestError>>> + use<> {
847 let client = self.server.client();
848 reconcile(
849 Arc::clone(ctx),
850 client.clone(),
851 Api::namespaced(client, "ns"),
852 Arc::new(resource),
853 Arc::clone(&self.instrumentation),
854 )
855 }
856
857 fn methods(&self) -> Vec<String> {
858 self.server
859 .requests()
860 .into_iter()
861 .map(|r| r.method)
862 .collect()
863 }
864 }
865
866 fn config_map() -> ConfigMap {
867 ConfigMap {
868 metadata: ObjectMeta {
869 name: Some("cm".to_owned()),
870 namespace: Some("ns".to_owned()),
871 uid: Some("uid-1".to_owned()),
872 ..Default::default()
873 },
874 ..Default::default()
875 }
876 }
877
878 fn pass(phase: Phase, outcome: Outcome) -> Recorded {
879 Recorded::Pass("test".to_owned(), phase, outcome)
880 }
881
882 fn step(outcome: Outcome) -> Recorded {
883 Recorded::Step("test".to_owned(), "work", outcome)
884 }
885
886 #[test]
887 fn classifies_successful_passes() {
888 block_on(async {
889 let harness = Harness::new();
890 let ctx = TestContext::new(Behavior::Done);
891 harness.reconcile(&ctx, config_map()).await.unwrap();
892 assert_eq!(
893 harness.observer.take(),
894 [
895 step(Outcome::Completed),
896 pass(Phase::Apply, Outcome::Completed)
897 ]
898 );
899
900 ctx.set(Behavior::Requeue);
901 harness.reconcile(&ctx, config_map()).await.unwrap();
902 assert_eq!(
903 harness.observer.take(),
904 [step(Outcome::Waiting), pass(Phase::Apply, Outcome::Waiting)]
905 );
906
907 ctx.set(Behavior::Skip);
908 harness.reconcile(&ctx, config_map()).await.unwrap();
909 assert_eq!(
910 harness.observer.take(),
911 [step(Outcome::Skipped), pass(Phase::Apply, Outcome::Skipped)]
912 );
913
914 assert!(harness.server.requests().is_empty());
915 });
916 }
917
918 #[test]
919 fn publishes_and_aggregates_failure_events() {
920 block_on(async {
921 let harness = Harness::new();
922 let ctx = TestContext::new(Behavior::Fail);
923 harness.reconcile(&ctx, config_map()).await.unwrap_err();
924 assert_eq!(
925 harness.observer.take(),
926 [
927 step(Outcome::Abandoned),
928 pass(Phase::Apply, Outcome::Failed)
929 ]
930 );
931 let requests = harness.server.requests();
932 assert_eq!(requests[0].body["reason"], "ReconcileFailed");
933 assert_eq!(requests[0].body["note"], "reconciling failed: disk on fire");
934
935 harness.reconcile(&ctx, config_map()).await.unwrap_err();
936 assert_eq!(harness.methods(), ["POST", "PATCH"]);
937
938 ctx.set(Behavior::Done);
940 harness.reconcile(&ctx, config_map()).await.unwrap();
941 ctx.set(Behavior::Fail);
942 harness.reconcile(&ctx, config_map()).await.unwrap_err();
943 assert_eq!(harness.methods(), ["POST", "PATCH", "POST"]);
944 });
945 }
946
947 #[test]
948 fn failure_event_can_be_suppressed() {
949 block_on(async {
950 let harness = Harness::new();
951 let ctx = TestContext::new(Behavior::FailQuietly);
952 harness.reconcile(&ctx, config_map()).await.unwrap_err();
953 assert_eq!(
954 harness.observer.take(),
955 [
956 step(Outcome::Abandoned),
957 pass(Phase::Apply, Outcome::Failed)
958 ]
959 );
960 assert!(harness.server.requests().is_empty());
961 });
962 }
963
964 #[test]
965 fn deleted_resource_without_finalizer() {
966 block_on(async {
967 let harness = Harness::new();
968 let ctx = TestContext::new(Behavior::Fail);
969 let mut resource = config_map();
970 resource.metadata.deletion_timestamp = Some(Time(Timestamp::now()));
971 harness.reconcile(&ctx, resource).await.unwrap();
972 assert_eq!(
973 harness.observer.take(),
974 [pass(Phase::Delete, Outcome::Completed)]
975 );
976 });
977 }
978
979 #[test]
980 fn cancelled_pass_is_abandoned() {
981 block_on(async {
982 let harness = Harness::new();
983 let ctx = TestContext::new(Behavior::Hang);
984 let mut fut = Box::pin(harness.reconcile(&ctx, config_map()));
985 assert!((&mut fut).now_or_never().is_none());
986 assert!(harness.observer.take().is_empty());
987 drop(fut);
988 assert_eq!(
989 harness.observer.take(),
990 [
991 step(Outcome::Abandoned),
992 pass(Phase::Apply, Outcome::Abandoned)
993 ]
994 );
995 });
996 }
997
998 #[test]
999 fn display_chain_does_not_repeat_messages() {
1000 let err: Error<TestError> =
1001 Error::FinalizerError(kube_runtime::finalizer::Error::ApplyFailed(
1002 TestError::Wrapped(std::io::Error::other("disk on fire")),
1003 ));
1004 assert_eq!(
1005 err.display_chain(),
1006 "failed to apply object: reconciling failed: disk on fire"
1007 );
1008
1009 #[derive(Debug, thiserror::Error)]
1010 #[error("outer")]
1011 struct Outer(#[source] std::io::Error);
1012 let err: Error<Outer> = Error::ControllerError(Outer(std::io::Error::other("inner")));
1013 assert_eq!(err.display_chain(), "outer: inner");
1014
1015 #[derive(Debug, thiserror::Error)]
1016 #[error("reading lock: timed out waiting for lock")]
1017 struct Mentions(#[source] std::io::Error);
1018 let err: Error<Mentions> =
1019 Error::ControllerError(Mentions(std::io::Error::other("timed out")));
1020 assert_eq!(
1021 err.display_chain(),
1022 "reading lock: timed out waiting for lock: timed out"
1023 );
1024 }
1025
1026 fn api_error(code: u16, reason: &str, details: Option<StatusDetails>) -> kube::Error {
1027 kube::Error::Api(Box::new(Status {
1028 status: Some(StatusSummary::Failure),
1029 code,
1030 message: String::new(),
1031 metadata: None,
1032 reason: reason.into(),
1033 details,
1034 }))
1035 }
1036
1037 #[test]
1038 fn conflicts_are_classified() {
1039 use kube_runtime::finalizer::Error as F;
1040
1041 let stale_apply: Error<TestError> = Error::FinalizerError(F::ApplyFailed(TestError::Kube(
1042 api_error(409, "Conflict", None),
1043 )));
1044 assert!(
1045 stale_apply.is_conflict(),
1046 "409 nested in the context's error"
1047 );
1048
1049 let stale_controller: Error<TestError> =
1050 Error::ControllerError(TestError::Kube(api_error(409, "Conflict", None)));
1051 assert!(stale_controller.is_conflict(), "409 without a finalizer");
1052
1053 let failed_test = || {
1054 api_error(
1055 422,
1056 "Invalid",
1057 Some(StatusDetails {
1058 name: String::new(),
1059 group: String::new(),
1060 kind: String::new(),
1061 uid: String::new(),
1062 causes: vec![],
1063 retry_after_seconds: 0,
1064 }),
1065 )
1066 };
1067 let add: Error<TestError> = Error::FinalizerError(F::AddFinalizer(failed_test()));
1068 assert!(
1069 add.is_conflict(),
1070 "failed patch test when adding the finalizer"
1071 );
1072 let remove: Error<TestError> = Error::FinalizerError(F::RemoveFinalizer(failed_test()));
1073 assert!(
1074 remove.is_conflict(),
1075 "failed patch test when removing the finalizer"
1076 );
1077 }
1078
1079 #[test]
1080 fn other_errors_are_not_conflicts() {
1081 use kube_runtime::finalizer::Error as F;
1082
1083 let exists: Error<TestError> =
1084 Error::ControllerError(TestError::Kube(api_error(409, "AlreadyExists", None)));
1085 assert!(
1086 !exists.is_conflict(),
1087 "AlreadyExists does not resolve on retry"
1088 );
1089
1090 let invalid = api_error(
1091 422,
1092 "Invalid",
1093 Some(StatusDetails {
1094 name: "cm".into(),
1095 group: String::new(),
1096 kind: "ConfigMap".into(),
1097 uid: String::new(),
1098 causes: vec![StatusCause {
1099 reason: "FieldValueInvalid".into(),
1100 message: "bad value".into(),
1101 field: "metadata.finalizers[0]".into(),
1102 }],
1103 retry_after_seconds: 0,
1104 }),
1105 );
1106 let add: Error<TestError> = Error::FinalizerError(F::AddFinalizer(invalid));
1107 assert!(
1108 !add.is_conflict(),
1109 "validation failure of the finalizer patch"
1110 );
1111
1112 let invalid_apply: Error<TestError> = Error::FinalizerError(F::ApplyFailed(
1113 TestError::Kube(api_error(422, "Invalid", None)),
1114 ));
1115 assert!(
1116 !invalid_apply.is_conflict(),
1117 "422 from the context's own writes"
1118 );
1119
1120 let io: Error<TestError> =
1121 Error::ControllerError(TestError::Wrapped(std::io::Error::other("disk on fire")));
1122 assert!(!io.is_conflict());
1123 }
1124}