1use std::collections::BTreeMap;
2use std::error::Error as _;
3use std::fmt::Display;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7use futures::future::FutureExt;
8use futures::stream::StreamExt;
9use kube::api::Api;
10use kube::core::{ClusterResourceScope, NamespaceResourceScope};
11use kube::{Client, Resource, ResourceExt};
12use kube_runtime::controller::Action;
13use kube_runtime::finalizer::{Event, finalizer};
14use kube_runtime::watcher;
15use rand::{Rng, rng};
16use tracing::field::Empty;
17use tracing::{Instrument, Span, error, info, info_span, trace, warn};
18
19#[derive(Debug, thiserror::Error)]
20pub enum Error<E: std::error::Error + 'static> {
21 #[error("{0}")]
22 ControllerError(#[source] E),
23 #[error("{0}")]
24 FinalizerError(#[source] kube_runtime::finalizer::Error<E>),
25}
26
27#[derive(Debug, Default)]
28pub struct TraceMetadata(BTreeMap<String, String>);
29
30impl TraceMetadata {
31 pub fn annotate<K: Display, V: Display>(&mut self, key: K, val: V) {
32 self.0.insert(key.to_string(), val.to_string());
33 }
34}
35
36pub struct Controller<Ctx: Context>
39where
40 Ctx: Send + Sync + 'static,
41 Ctx::Error: Send + Sync + 'static,
42 Ctx::Resource: Send + Sync + 'static,
43 Ctx::Resource: Clone + std::fmt::Debug + serde::Serialize,
44 for<'de> Ctx::Resource: serde::Deserialize<'de>,
45 <Ctx::Resource as Resource>::DynamicType:
46 Eq + Clone + std::hash::Hash + std::default::Default + std::fmt::Debug + std::marker::Unpin,
47{
48 client: kube::Client,
49 make_api: Box<dyn Fn(&Ctx::Resource) -> Api<Ctx::Resource> + Sync + Send + 'static>,
50 controller: kube_runtime::controller::Controller<Ctx::Resource>,
51 context: Ctx,
52}
53
54impl<Ctx: Context> Controller<Ctx>
55where
56 Ctx: Send + Sync + 'static,
57 Ctx::Error: Send + Sync + 'static,
58 Ctx::Resource: Clone + std::fmt::Debug + serde::Serialize,
59 for<'de> Ctx::Resource: serde::Deserialize<'de>,
60 <Ctx::Resource as Resource>::DynamicType:
61 Eq + Clone + std::hash::Hash + std::default::Default + std::fmt::Debug + std::marker::Unpin,
62{
63 pub fn namespaced(client: Client, context: Ctx, namespace: &str, wc: watcher::Config) -> Self
71 where
72 Ctx::Resource: Resource<Scope = NamespaceResourceScope>,
73 {
74 let make_api = {
75 let client = client.clone();
76 Box::new(move |resource: &Ctx::Resource| {
77 Api::<Ctx::Resource>::namespaced(client.clone(), &resource.namespace().unwrap())
78 })
79 };
80 let controller = kube_runtime::controller::Controller::new(
81 Api::<Ctx::Resource>::namespaced(client.clone(), namespace),
82 wc,
83 );
84 Self {
85 client,
86 make_api,
87 controller,
88 context,
89 }
90 }
91
92 pub fn namespaced_all(client: Client, context: Ctx, wc: watcher::Config) -> Self
100 where
101 Ctx::Resource: Resource<Scope = NamespaceResourceScope>,
102 {
103 let make_api = {
104 let client = client.clone();
105 Box::new(move |resource: &Ctx::Resource| {
106 Api::<Ctx::Resource>::namespaced(client.clone(), &resource.namespace().unwrap())
107 })
108 };
109 let controller = kube_runtime::controller::Controller::new(
110 Api::<Ctx::Resource>::all(client.clone()),
111 wc,
112 );
113 Self {
114 client,
115 make_api,
116 controller,
117 context,
118 }
119 }
120
121 pub fn cluster(client: Client, context: Ctx, wc: watcher::Config) -> Self
128 where
129 Ctx::Resource: Resource<Scope = ClusterResourceScope>,
130 {
131 let make_api = {
132 let client = client.clone();
133 Box::new(move |_: &Ctx::Resource| Api::<Ctx::Resource>::all(client.clone()))
134 };
135 let controller = kube_runtime::controller::Controller::new(
136 Api::<Ctx::Resource>::all(client.clone()),
137 wc,
138 );
139 Self {
140 client,
141 make_api,
142 controller,
143 context,
144 }
145 }
146
147 pub async fn run(self) {
157 let Self {
158 client,
159 make_api,
160 controller,
161 context,
162 } = self;
163 let backoffs = Arc::new(Mutex::new(BTreeMap::new()));
164 let backoffs = &backoffs;
165 controller
166 .run(
167 |resource, context| {
168 let uid = resource.uid().unwrap();
169 let backoffs = Arc::clone(backoffs);
170 context
171 ._reconcile(client.clone(), make_api(&resource), resource)
172 .inspect(move |result| {
173 if result.is_ok() {
174 backoffs.lock().unwrap().remove(&uid);
175 }
176 })
177 },
178 |resource, err, context| {
179 let consecutive_errors = {
180 let uid = resource.uid().unwrap();
181 let mut backoffs = backoffs.lock().unwrap();
182 let consecutive_errors: u32 =
183 backoffs.get(&uid).copied().unwrap_or_default();
184 backoffs.insert(uid, consecutive_errors.saturating_add(1));
185 consecutive_errors
186 };
187 context.error_action(resource, err, consecutive_errors)
188 },
189 Arc::new(context),
190 )
191 .for_each(|res| async {
192 if let Err(e) = res
195 && !matches!(e, kube_runtime::controller::Error::ReconcilerFailed(..))
196 {
197 warn!(
200 error = %e,
201 source = e.source(),
202 "internal kube controller error",
203 );
204 }
205 })
206 .await
207 }
208
209 pub fn with_controller<F>(mut self, f: F) -> Self
214 where
215 F: FnOnce(
216 kube_runtime::Controller<Ctx::Resource>,
217 ) -> kube_runtime::Controller<Ctx::Resource>,
218 {
219 self.controller = f(self.controller);
220 self
221 }
222}
223
224#[cfg_attr(not(docsrs), async_trait::async_trait)]
227pub trait Context {
228 type Resource: Resource + Send + Sync + 'static;
231 type Error: std::error::Error;
234
235 const FINALIZER_NAME: Option<&'static str> = None;
242
243 async fn apply(
251 &self,
252 client: Client,
253 resource: &Self::Resource,
254 metadata: &mut TraceMetadata,
255 ) -> Result<Option<Action>, Self::Error>;
256
257 async fn cleanup(
267 &self,
268 client: Client,
269 resource: &Self::Resource,
270 metadata: &mut TraceMetadata,
271 ) -> Result<Option<Action>, Self::Error> {
272 let _client = client;
274 let _resource = resource;
275 let _metadata = metadata;
276
277 Ok(Some(Action::await_change()))
278 }
279
280 fn success_action(&self, resource: &Self::Resource) -> Action {
286 let _resource = resource;
288
289 Action::requeue(Duration::from_secs(rng().random_range(2400..3600)))
290 }
291
292 fn error_action(
300 self: Arc<Self>,
301 resource: Arc<Self::Resource>,
302 err: &Error<Self::Error>,
303 consecutive_errors: u32,
304 ) -> Action {
305 let _resource = resource;
307 let _err = err;
308
309 let seconds = 2u64.pow(consecutive_errors.min(7) + 1);
310 Action::requeue(Duration::from_millis(
311 rng().random_range((seconds * 500)..(seconds * 1000)),
312 ))
313 }
314
315 #[doc(hidden)]
316 async fn _reconcile(
317 self: Arc<Self>,
318 client: Client,
319 api: Api<Self::Resource>,
320 resource: Arc<Self::Resource>,
321 ) -> Result<Action, Error<Self::Error>>
322 where
323 Self: Send + Sync + 'static,
324 Self::Error: Send + Sync + 'static,
325 Self::Resource: Send + Sync + 'static,
326 Self::Resource: Clone + std::fmt::Debug + serde::Serialize,
327 for<'de> Self::Resource: serde::Deserialize<'de>,
328 <Self::Resource as Resource>::DynamicType: Eq
329 + Clone
330 + std::hash::Hash
331 + std::default::Default
332 + std::fmt::Debug
333 + std::marker::Unpin,
334 {
335 let span = info_span!(
336 "reconcile",
337 resource_type = Self::Resource::kind(&Default::default()).as_ref(),
338 resource_name = resource.name_unchecked().as_str(),
339 controller = Self::FINALIZER_NAME,
340 event_type = Empty,
341 success = Empty,
342 duration_seconds = Empty,
343 metadata = Empty,
344 );
345 async {
346 trace!("beginning reconciliation");
347
348 let mut metadata = TraceMetadata::default();
349 let mut ran = false;
350 let start = Instant::now();
351
352 let res = if let Some(finalizer_name) = Self::FINALIZER_NAME {
353 finalizer(&api, finalizer_name, Arc::clone(&resource), |event| async {
354 ran = true;
355 Span::current().record(
356 "event_type",
357 match event {
358 Event::Apply(_) => "apply",
359 Event::Cleanup(_) => "cleanup",
360 },
361 );
362 match event {
363 Event::Apply(resource) => self
364 .apply(client, &resource, &mut metadata)
365 .await
366 .map(|action| action.unwrap_or_else(|| self.success_action(&resource))),
367 Event::Cleanup(resource) => self
368 .cleanup(client, &resource, &mut metadata)
369 .await
370 .map(|action| action.unwrap_or_else(Action::await_change)),
371 }
372 })
373 .await
374 .map_err(Error::FinalizerError)
375 } else if resource.meta().deletion_timestamp.is_none() {
376 ran = true;
377 Span::current().record("event_type", "apply");
378 self.apply(client, &resource, &mut metadata)
379 .await
380 .map(|action| action.unwrap_or_else(|| self.success_action(&resource)))
381 .map_err(Error::ControllerError)
382 } else {
383 Ok(Action::await_change())
384 };
385
386 Span::current().record("duration_seconds", start.elapsed().as_secs_f64());
389
390 if !ran {
391 Span::current().record(
392 "event_type",
393 if resource.meta().deletion_timestamp.is_some() {
394 "delete"
395 } else {
396 "init"
397 },
398 );
399 }
400
401 if !metadata.0.is_empty()
402 && let Ok(s) = serde_json::to_string(&metadata.0)
403 {
404 Span::current().record("metadata", s);
405 }
406
407 if let Err(e) = &res {
408 Span::current().record("success", false);
409 error!(error = %e, source = e.source(), "reconcile");
410 } else {
411 Span::current().record("success", true);
412 info!("reconcile");
413 }
414
415 res
416 }
417 .instrument(span)
418 .await
419 }
420}