kube_runtime/controller/mod.rs
1//! Runs a user-supplied reconciler function on objects when they (or related objects) are updated
2
3use self::runner::Runner;
4#[allow(deprecated)] use crate::watcher::metadata_watcher;
5use crate::{
6 reflector::{
7 self, ObjectRef, reflector,
8 store::{Store, Writer},
9 },
10 scheduler::{ScheduleRequest, debounced_scheduler},
11 utils::{
12 Backoff, CancelableJoinHandle, KubeRuntimeStreamExt, StreamBackoff, WatchStreamExt, trystream_try_via,
13 },
14 watcher::{self, DefaultBackoff, watcher},
15};
16use educe::Educe;
17use futures::{
18 FutureExt, Stream, StreamExt, TryFuture, TryFutureExt, TryStream, TryStreamExt, channel,
19 future::{self, BoxFuture},
20 stream,
21};
22use kube_client::api::{Api, DynamicObject, Resource};
23use pin_project::pin_project;
24use serde::de::DeserializeOwned;
25use std::{
26 fmt::{Debug, Display},
27 hash::Hash,
28 sync::Arc,
29 task::{Poll, ready},
30 time::Duration,
31};
32use stream::BoxStream;
33use thiserror::Error;
34use tokio::{runtime::Handle, time::Instant};
35use tracing::{Instrument, info_span};
36
37mod future_hash_map;
38mod runner;
39mod trigger;
40
41use trigger::trigger_others;
42#[cfg(feature = "unstable-runtime-subscribe")]
43use trigger::trigger_owners_shared;
44pub use trigger::{trigger_owners, trigger_self, trigger_with};
45
46/// The reasons the internal runner can fail
47pub type RunnerError = runner::Error<reflector::store::WriterDropped>;
48
49/// Errors returned by the applier and visible in a controller stream if inspecting it
50///
51/// WARNING: These errors do not terminate `Controller::run`, and are not passed to the `reconcile` fn
52/// as they exist primarily for diagnostics.
53///
54/// To inspect these errors, you can run a `for_each` on the run stream:
55///
56/// ```compile_fail
57/// Controller::new(api, watcher_config)
58/// .run(reconcile, error_policy, context)
59/// .for_each(|res| async move {
60/// match res {
61/// Ok(o) => info!("reconciled {:?}", o),
62/// /// Reconciler errors visible here:
63/// Err(e) => warn!("reconcile failed: {}", e),
64/// }
65/// })
66/// .await;
67/// ```
68#[derive(Debug, Error)]
69pub enum Error<ReconcilerErr: 'static, QueueErr: 'static> {
70 /// A scheduled reconcile for an object refers to an object that no longer exists
71 ///
72 /// This is usually not a problem and often expected with certain relations.
73 /// See <https://github.com/kube-rs/kube/issues/1167#issuecomment-1636773541>
74 /// for a more detailed explanation of how/why this happens.
75 #[error("tried to reconcile object {0} that was not found in local store")]
76 ObjectNotFound(Box<ObjectRef<DynamicObject>>),
77
78 /// User's reconcile fn failed for the object
79 #[error("reconciler for object {1} failed: {0}")]
80 ReconcilerFailed(#[source] ReconcilerErr, Box<ObjectRef<DynamicObject>>),
81
82 /// The queue stream contained an error
83 #[error("event queue error: {0}")]
84 QueueError(#[source] QueueErr),
85
86 /// The internal runner returned an error
87 #[error("runner error: {0}")]
88 RunnerError(#[source] RunnerError),
89}
90
91/// Results of the reconciliation attempt
92#[derive(Debug, Clone, Eq, PartialEq)]
93pub struct Action {
94 /// Whether (and when) to next trigger the reconciliation if no external watch triggers hit
95 ///
96 /// For example, use this to query external systems for updates, expire time-limited resources, or
97 /// (in your `error_policy`) retry after errors.
98 requeue_after: Option<Duration>,
99}
100
101impl Action {
102 /// Action to the reconciliation at this time even if no external watch triggers hit
103 ///
104 /// This is the best-practice action that ensures eventual consistency of your controller
105 /// even in the case of missed changes (which can happen).
106 ///
107 /// Watch events are not normally missed, so running this once per hour (`Default`) as a fallback is reasonable.
108 #[must_use]
109 pub const fn requeue(duration: Duration) -> Self {
110 Self {
111 requeue_after: Some(duration),
112 }
113 }
114
115 /// Do nothing until a change is detected
116 ///
117 /// This stops the controller periodically reconciling this object until a relevant watch event
118 /// was **detected**.
119 ///
120 /// **Warning**: If you have watch desyncs, it is possible to miss changes entirely.
121 /// It is therefore not recommended to disable requeuing this way, unless you have
122 /// frequent changes to the underlying object, or some other hook to retain eventual consistency.
123 #[must_use]
124 pub const fn await_change() -> Self {
125 Self { requeue_after: None }
126 }
127}
128
129/// A request to reconcile an object, annotated with why that request was made.
130///
131/// NOTE: The reason is ignored for comparison purposes. This means that, for example,
132/// an object can only occupy one scheduler slot, even if it has been scheduled for multiple reasons.
133/// In this case, only *the first* reason is stored.
134#[derive(Educe)]
135#[educe(
136 Debug(bound("K::DynamicType: Debug")),
137 Clone(bound("K::DynamicType: Clone")),
138 PartialEq(bound("K::DynamicType: PartialEq")),
139 Hash(bound("K::DynamicType: Hash"))
140)]
141pub struct ReconcileRequest<K: Resource> {
142 /// A reference to the object to be reconciled
143 pub obj_ref: ObjectRef<K>,
144 /// The reason for why reconciliation was requested
145 #[educe(PartialEq(ignore), Hash(ignore))]
146 pub reason: ReconcileReason,
147}
148
149impl<K: Resource> Eq for ReconcileRequest<K> where K::DynamicType: Eq {}
150
151impl<K: Resource> From<ObjectRef<K>> for ReconcileRequest<K> {
152 fn from(obj_ref: ObjectRef<K>) -> Self {
153 ReconcileRequest {
154 obj_ref,
155 reason: ReconcileReason::Unknown,
156 }
157 }
158}
159
160/// The reason a reconcile was requested
161///
162/// Note that this reason is deliberately hidden from the reconciler.
163/// See <https://kube.rs/controllers/reconciler/#reasons-for-reconciliation>.
164#[derive(Debug, Clone)]
165pub enum ReconcileReason {
166 /// A custom reconcile triggered via `reconcile_on`
167 Unknown,
168
169 /// The main object was updated.
170 ObjectUpdated,
171
172 /// A related object was updated through a mapper
173 ///
174 /// The related object traversed its relation up to the object kind you are reconciling.
175 /// Your object may not have changed, but you may need to update child objects.
176 RelatedObjectUpdated {
177 /// An object ref to the related object
178 obj_ref: Box<ObjectRef<DynamicObject>>,
179 },
180
181 /// The users `reconcile` scheduled a reconciliation via an `Action`
182 ReconcilerRequestedRetry,
183
184 /// The users `error_policy` scheduled a reconciliation via an `Action`
185 ErrorPolicyRequestedRetry,
186
187 /// A bulk reconcile was requested via `reconcile_all_on`
188 BulkReconcile,
189
190 /// A custom reconcile reason for custom integrations.
191 ///
192 /// Can be used when injecting elements into the queue stream directly.
193 Custom {
194 /// A user provided reason through a custom integration
195 reason: String,
196 },
197}
198
199impl Display for ReconcileReason {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 ReconcileReason::Unknown => f.write_str("unknown"),
203 ReconcileReason::ObjectUpdated => f.write_str("object updated"),
204 ReconcileReason::RelatedObjectUpdated { obj_ref: object } => {
205 f.write_fmt(format_args!("related object updated: {object}"))
206 }
207 ReconcileReason::BulkReconcile => f.write_str("bulk reconcile requested"),
208 ReconcileReason::ReconcilerRequestedRetry => f.write_str("reconciler requested retry"),
209 ReconcileReason::ErrorPolicyRequestedRetry => f.write_str("error policy requested retry"),
210 ReconcileReason::Custom { reason } => f.write_str(reason),
211 }
212 }
213}
214
215const APPLIER_REQUEUE_BUF_SIZE: usize = 100;
216
217/// Apply a reconciler to an input stream, with a given retry policy
218///
219/// Takes a `store` parameter for the core objects, which should usually be updated by a [`reflector()`].
220///
221/// The `queue` indicates which objects should be reconciled. For the core objects this will usually be
222/// the [`reflector()`] (piped through [`trigger_self`]). If your core objects own any subobjects then you
223/// can also make them trigger reconciliations by [merging](`futures::stream::select`) the [`reflector()`]
224/// with a [`watcher()`] or [`reflector()`] for the subobject.
225///
226/// This is the "hard-mode" version of [`Controller`], which allows you some more customization
227/// (such as triggering from arbitrary [`Stream`]s), at the cost of being a bit more verbose.
228#[allow(clippy::needless_pass_by_value)]
229#[allow(clippy::type_complexity)]
230pub fn applier<K, QueueStream, ReconcilerFut, Ctx>(
231 mut reconciler: impl FnMut(Arc<K>, Arc<Ctx>) -> ReconcilerFut,
232 error_policy: impl Fn(Arc<K>, &ReconcilerFut::Error, Arc<Ctx>) -> Action,
233 context: Arc<Ctx>,
234 store: Store<K>,
235 queue: QueueStream,
236 config: Config,
237) -> impl Stream<Item = Result<(ObjectRef<K>, Action), Error<ReconcilerFut::Error, QueueStream::Error>>>
238where
239 K: Clone + Resource + 'static,
240 K::DynamicType: Debug + Eq + Hash + Clone + Unpin,
241 ReconcilerFut: TryFuture<Ok = Action> + Unpin,
242 ReconcilerFut::Error: std::error::Error + 'static,
243 QueueStream: TryStream,
244 QueueStream::Ok: Into<ReconcileRequest<K>>,
245 QueueStream::Error: std::error::Error + 'static,
246{
247 let (scheduler_shutdown_tx, scheduler_shutdown_rx) = channel::oneshot::channel();
248 let (scheduler_tx, scheduler_rx) =
249 channel::mpsc::channel::<ScheduleRequest<ReconcileRequest<K>>>(APPLIER_REQUEUE_BUF_SIZE);
250 let error_policy = Arc::new(error_policy);
251 let delay_store = store.clone();
252 // Create a stream of ObjectRefs that need to be reconciled
253 trystream_try_via(
254 // input: stream combining scheduled tasks and user specified inputs event
255 Box::pin(stream::select(
256 // 1. inputs from users queue stream
257 queue
258 .map_err(Error::QueueError)
259 .map_ok(|request| ScheduleRequest {
260 message: request.into(),
261 run_at: Instant::now(),
262 })
263 .on_complete(async move {
264 // On error: scheduler has already been shut down and there is nothing for us to do
265 let _ = scheduler_shutdown_tx.send(());
266 tracing::debug!("applier queue terminated, starting graceful shutdown")
267 }),
268 // 2. requests sent to scheduler_tx
269 scheduler_rx
270 .map(Ok)
271 .take_until(scheduler_shutdown_rx)
272 .on_complete(async { tracing::debug!("applier scheduler consumer terminated") }),
273 )),
274 // all the Oks from the select gets passed through the scheduler stream, and are then executed
275 move |s| {
276 Runner::new(
277 debounced_scheduler(s, config.debounce),
278 config.concurrency,
279 move |request| {
280 let request = request.clone();
281 match store.get(&request.obj_ref) {
282 Some(obj) => {
283 let scheduler_tx = scheduler_tx.clone();
284 let error_policy_ctx = context.clone();
285 let error_policy = error_policy.clone();
286 let reconciler_span = info_span!(
287 "reconciling object",
288 "object.ref" = %request.obj_ref,
289 object.reason = %request.reason
290 );
291 TryFutureExt::into_future(
292 reconciler_span.in_scope(|| reconciler(Arc::clone(&obj), context.clone())),
293 )
294 .then(move |res| {
295 let error_policy = error_policy;
296 RescheduleReconciliation::new(
297 res,
298 |err| error_policy(obj, err, error_policy_ctx),
299 request.obj_ref.clone(),
300 scheduler_tx,
301 )
302 // Reconciler errors are OK from the applier's PoV, we need to apply the error policy
303 // to them separately
304 .map(|res| Ok((request.obj_ref, res)))
305 })
306 .instrument(reconciler_span)
307 .left_future()
308 }
309 None => {
310 std::future::ready(Err(Error::ObjectNotFound(Box::new(request.obj_ref.erase()))))
311 .right_future()
312 }
313 }
314 },
315 )
316 .delay_tasks_until(async move {
317 tracing::debug!("applier runner held until store is ready");
318 let res = delay_store.wait_until_ready().await;
319 tracing::debug!("store is ready, starting runner");
320 res
321 })
322 .map(|runner_res| runner_res.unwrap_or_else(|err| Err(Error::RunnerError(err))))
323 .on_complete(async { tracing::debug!("applier runner terminated") })
324 },
325 )
326 .on_complete(async { tracing::debug!("applier runner-merge terminated") })
327 // finally, for each completed reconcile call:
328 .and_then(move |(obj_ref, reconciler_result)| async move {
329 match reconciler_result {
330 Ok(action) => Ok((obj_ref, action)),
331 Err(err) => Err(Error::ReconcilerFailed(err, Box::new(obj_ref.erase()))),
332 }
333 })
334 .on_complete(async { tracing::debug!("applier terminated") })
335}
336
337/// Internal helper [`Future`] that reschedules reconciliation of objects (if required), in the scheduled context of the reconciler
338///
339/// This could be an `async fn`, but isn't because we want it to be [`Unpin`]
340#[pin_project]
341#[must_use]
342struct RescheduleReconciliation<K: Resource, ReconcilerErr> {
343 reschedule_tx: channel::mpsc::Sender<ScheduleRequest<ReconcileRequest<K>>>,
344
345 reschedule_request: Option<ScheduleRequest<ReconcileRequest<K>>>,
346 result: Option<Result<Action, ReconcilerErr>>,
347}
348
349impl<K, ReconcilerErr> RescheduleReconciliation<K, ReconcilerErr>
350where
351 K: Resource,
352{
353 fn new(
354 result: Result<Action, ReconcilerErr>,
355 error_policy: impl FnOnce(&ReconcilerErr) -> Action,
356 obj_ref: ObjectRef<K>,
357 reschedule_tx: channel::mpsc::Sender<ScheduleRequest<ReconcileRequest<K>>>,
358 ) -> Self {
359 let reconciler_finished_at = Instant::now();
360
361 let (action, reschedule_reason) = result.as_ref().map_or_else(
362 |err| (error_policy(err), ReconcileReason::ErrorPolicyRequestedRetry),
363 |action| (action.clone(), ReconcileReason::ReconcilerRequestedRetry),
364 );
365
366 Self {
367 reschedule_tx,
368 reschedule_request: action.requeue_after.map(|requeue_after| ScheduleRequest {
369 message: ReconcileRequest {
370 obj_ref,
371 reason: reschedule_reason,
372 },
373 run_at: reconciler_finished_at
374 .checked_add(requeue_after)
375 .unwrap_or_else(crate::scheduler::max_schedule_time),
376 }),
377 result: Some(result),
378 }
379 }
380}
381
382impl<K, ReconcilerErr> Future for RescheduleReconciliation<K, ReconcilerErr>
383where
384 K: Resource,
385{
386 type Output = Result<Action, ReconcilerErr>;
387
388 fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
389 let this = self.get_mut();
390
391 if this.reschedule_request.is_some() {
392 let rescheduler_ready = ready!(this.reschedule_tx.poll_ready(cx));
393 let reschedule_request = this
394 .reschedule_request
395 .take()
396 .expect("PostReconciler::reschedule_request was taken during processing");
397 // Failure to schedule item = in graceful shutdown mode, ignore
398 if let Ok(()) = rescheduler_ready {
399 let _ = this.reschedule_tx.start_send(reschedule_request);
400 }
401 }
402
403 Poll::Ready(
404 this.result
405 .take()
406 .expect("PostReconciler::result was already taken"),
407 )
408 }
409}
410
411/// Accumulates all options that can be used on a [`Controller`] invocation.
412#[derive(Clone, Debug, Default)]
413pub struct Config {
414 debounce: Duration,
415 concurrency: u16,
416}
417
418impl Config {
419 /// The debounce duration used to deduplicate reconciliation requests.
420 ///
421 /// When set to a non-zero duration, debouncing is enabled in the [`scheduler`](crate::scheduler())
422 /// resulting in __trailing edge debouncing__ of reconciler requests.
423 /// This option can help to reduce the amount of unnecessary reconciler calls
424 /// when using multiple controller relations, or during rapid phase transitions.
425 ///
426 /// ## Warning
427 /// This option delays (and keeps delaying) reconcile requests for objects while
428 /// the object is updated. It can **permanently hide** updates from your reconciler
429 /// if set too high on objects that are updated frequently (like nodes).
430 #[must_use]
431 pub fn debounce(mut self, debounce: Duration) -> Self {
432 self.debounce = debounce;
433 self
434 }
435
436 /// The number of concurrent reconciliations of that are allowed to run at an given moment.
437 ///
438 /// This can be adjusted to the controller's needs to increase
439 /// performance and/or make performance predictable. By default, its 0 meaning
440 /// the controller runs with unbounded concurrency.
441 ///
442 /// Note that despite concurrency, a controller never schedules concurrent reconciles
443 /// on the same object.
444 #[must_use]
445 pub fn concurrency(mut self, concurrency: u16) -> Self {
446 self.concurrency = concurrency;
447 self
448 }
449}
450
451/// Controller for a Resource `K`
452///
453/// A controller is an infinite stream of objects to be reconciled.
454///
455/// Once `run` and continuously awaited, it continuously calls out to user provided
456/// `reconcile` and `error_policy` callbacks whenever relevant changes are detected
457/// or if errors are seen from `reconcile`.
458///
459/// Reconciles are generally requested for all changes on your root objects.
460/// Changes to managed child resources will also trigger the reconciler for the
461/// managing object by traversing owner references (for `Controller::owns`),
462/// or traverse a custom mapping (for `Controller::watches`).
463///
464/// This mapping mechanism ultimately hides the reason for the reconciliation request,
465/// and forces you to write an idempotent reconciler.
466///
467/// General setup:
468/// ```no_run
469/// use kube::{Api, Client, CustomResource};
470/// use kube::runtime::{controller::{Controller, Action}, watcher};
471/// # use serde::{Deserialize, Serialize};
472/// # use tokio::time::Duration;
473/// use futures::StreamExt;
474/// use k8s_openapi::api::core::v1::ConfigMap;
475/// use schemars::JsonSchema;
476/// # use std::sync::Arc;
477/// use thiserror::Error;
478///
479/// #[derive(Debug, Error)]
480/// enum Error {}
481///
482/// /// A custom resource
483/// #[derive(CustomResource, Debug, Clone, Deserialize, Serialize, JsonSchema)]
484/// #[kube(group = "nullable.se", version = "v1", kind = "ConfigMapGenerator", namespaced)]
485/// struct ConfigMapGeneratorSpec {
486/// content: String,
487/// }
488///
489/// /// The reconciler that will be called when either object change
490/// async fn reconcile(g: Arc<ConfigMapGenerator>, _ctx: Arc<()>) -> Result<Action, Error> {
491/// // .. use api here to reconcile a child ConfigMap with ownerreferences
492/// // see configmapgen_controller example for full info
493/// Ok(Action::requeue(Duration::from_secs(300)))
494/// }
495/// /// an error handler that will be called when the reconciler fails with access to both the
496/// /// object that caused the failure and the actual error
497/// fn error_policy(obj: Arc<ConfigMapGenerator>, _error: &Error, _ctx: Arc<()>) -> Action {
498/// Action::requeue(Duration::from_secs(60))
499/// }
500///
501/// /// something to drive the controller
502///
503/// async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
504/// # let client: Client = todo!();
505/// let context = Arc::new(()); // bad empty context - put client in here
506/// let cmgs = Api::<ConfigMapGenerator>::all(client.clone());
507/// let cms = Api::<ConfigMap>::all(client.clone());
508/// Controller::new(cmgs, watcher::Config::default())
509/// .owns(cms, watcher::Config::default())
510/// .run(reconcile, error_policy, context)
511/// .for_each(|res| async move {
512/// match res {
513/// Ok(o) => println!("reconciled {:?}", o),
514/// Err(e) => println!("reconcile failed: {:?}", e),
515/// }
516/// })
517/// .await; // controller does nothing unless polled
518/// # Ok(())
519/// # }
520/// ```
521pub struct Controller<K>
522where
523 K: Clone + Resource + Debug + 'static,
524 K::DynamicType: Eq + Hash,
525{
526 // NB: Need to Unpin for stream::select_all
527 trigger_selector: stream::SelectAll<BoxStream<'static, Result<ReconcileRequest<K>, watcher::Error>>>,
528 trigger_backoff: Box<dyn Backoff + Send>,
529 /// [`run`](crate::Controller::run) starts a graceful shutdown when any of these [`Future`]s complete,
530 /// refusing to start any new reconciliations but letting any existing ones finish.
531 graceful_shutdown_selector: Vec<BoxFuture<'static, ()>>,
532 /// [`run`](crate::Controller::run) terminates immediately when any of these [`Future`]s complete,
533 /// requesting that all running reconciliations be aborted.
534 /// However, note that they *will* keep running until their next yield point (`.await`),
535 /// blocking [`tokio::runtime::Runtime`] destruction (unless you follow up by calling [`std::process::exit`] after `run`).
536 forceful_shutdown_selector: Vec<BoxFuture<'static, ()>>,
537 dyntype: K::DynamicType,
538 reader: Store<K>,
539 config: Config,
540}
541
542impl<K> Controller<K>
543where
544 K: Clone + Resource + DeserializeOwned + Debug + Send + Sync + 'static,
545 K::DynamicType: Eq + Hash + Clone,
546{
547 /// Create a Controller for a resource `K`
548 ///
549 /// Takes an [`Api`] object that determines how the `Controller` listens for changes to the `K`.
550 ///
551 /// The [`watcher::Config`] controls to the possible subset of objects of `K` that you want to manage
552 /// and receive reconcile events for.
553 /// For the full set of objects `K` in the given `Api` scope, you can use [`watcher::Config::default`].
554 #[must_use]
555 pub fn new(main_api: Api<K>, wc: watcher::Config) -> Self
556 where
557 K::DynamicType: Default,
558 {
559 Self::new_with(main_api, wc, Default::default())
560 }
561
562 /// Create a Controller for a resource `K`
563 ///
564 /// Takes an [`Api`] object that determines how the `Controller` listens for changes to the `K`.
565 ///
566 /// The [`watcher::Config`] lets you define a possible subset of objects of `K` that you want the [`Api`]
567 /// to watch - in the Api's configured scope - and receive reconcile events for.
568 /// For the full set of objects `K` in the given `Api` scope, you can use [`Config::default`].
569 ///
570 /// This variant constructor is for [`dynamic`] types found through discovery. Prefer [`Controller::new`] for static types.
571 ///
572 /// [`watcher::Config`]: crate::watcher::Config
573 /// [`Api`]: kube_client::Api
574 /// [`dynamic`]: kube_client::core::dynamic
575 /// [`Config::default`]: crate::watcher::Config::default
576 pub fn new_with(main_api: Api<K>, wc: watcher::Config, dyntype: K::DynamicType) -> Self {
577 let writer = Writer::<K>::new(dyntype.clone());
578 let reader = writer.as_reader();
579 let mut trigger_selector = stream::SelectAll::new();
580 let self_watcher = trigger_self(
581 reflector(writer, watcher(main_api, wc)).applied_objects(),
582 dyntype.clone(),
583 )
584 .boxed();
585 trigger_selector.push(self_watcher);
586 Self {
587 trigger_selector,
588 trigger_backoff: Box::<DefaultBackoff>::default(),
589 graceful_shutdown_selector: vec![
590 // Fallback future, ensuring that we never terminate if no additional futures are added to the selector
591 future::pending().boxed(),
592 ],
593 forceful_shutdown_selector: vec![
594 // Fallback future, ensuring that we never terminate if no additional futures are added to the selector
595 future::pending().boxed(),
596 ],
597 dyntype,
598 reader,
599 config: Default::default(),
600 }
601 }
602
603 /// Create a Controller for a resource `K` from a stream of `K` objects
604 ///
605 /// Same as [`Controller::new`], but instead of an `Api`, a stream of resources is used.
606 /// This allows for customized and pre-filtered watch streams to be used as a trigger,
607 /// as well as sharing input streams between multiple controllers.
608 ///
609 /// # Example:
610 ///
611 /// ```no_run
612 /// # use futures::StreamExt;
613 /// # use k8s_openapi::api::apps::v1::Deployment;
614 /// # use kube::runtime::controller::{Action, Controller};
615 /// # use kube::runtime::{predicates, watcher, reflector, WatchStreamExt};
616 /// # use kube::{Api, Client, Error, ResourceExt};
617 /// # use std::sync::Arc;
618 /// # async fn reconcile(_: Arc<Deployment>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
619 /// # fn error_policy(_: Arc<Deployment>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
620 /// # async fn doc(client: kube::Client) {
621 /// let api: Api<Deployment> = Api::default_namespaced(client);
622 /// let (reader, writer) = reflector::store();
623 /// let deploys = watcher(api, watcher::Config::default())
624 /// .default_backoff()
625 /// .reflect(writer)
626 /// .applied_objects()
627 /// .predicate_filter(predicates::generation, Default::default());
628 ///
629 /// Controller::for_stream(deploys, reader)
630 /// .run(reconcile, error_policy, Arc::new(()))
631 /// .for_each(|_| std::future::ready(()))
632 /// .await;
633 /// # }
634 /// ```
635 ///
636 /// Prefer [`Controller::new`] if you do not need to share the stream, or do not need pre-filtering.
637 #[cfg(feature = "unstable-runtime-stream-control")]
638 pub fn for_stream(
639 trigger: impl Stream<Item = Result<K, watcher::Error>> + Send + 'static,
640 reader: Store<K>,
641 ) -> Self
642 where
643 K::DynamicType: Default,
644 {
645 Self::for_stream_with(trigger, reader, Default::default())
646 }
647
648 /// Create a Controller for a resource `K` from a stream of `K` objects
649 ///
650 /// Same as [`Controller::new`], but instead of an `Api`, a stream of resources is used.
651 /// This allows for customized and pre-filtered watch streams to be used as a trigger,
652 /// as well as sharing input streams between multiple controllers.
653 ///
654 /// Prefer [`Controller::new`] if you do not need to share the stream, or do not need pre-filtering.
655 ///
656 /// This variant constructor is for [`dynamic`] types found through discovery. Prefer [`Controller::for_stream`] for static types.
657 ///
658 /// [`dynamic`]: kube_client::core::dynamic
659 #[cfg(feature = "unstable-runtime-stream-control")]
660 pub fn for_stream_with(
661 trigger: impl Stream<Item = Result<K, watcher::Error>> + Send + 'static,
662 reader: Store<K>,
663 dyntype: K::DynamicType,
664 ) -> Self {
665 let mut trigger_selector = stream::SelectAll::new();
666 let self_watcher = trigger_self(trigger, dyntype.clone()).boxed();
667 trigger_selector.push(self_watcher);
668 Self {
669 trigger_selector,
670 trigger_backoff: Box::<DefaultBackoff>::default(),
671 graceful_shutdown_selector: vec![
672 // Fallback future, ensuring that we never terminate if no additional futures are added to the selector
673 future::pending().boxed(),
674 ],
675 forceful_shutdown_selector: vec![
676 // Fallback future, ensuring that we never terminate if no additional futures are added to the selector
677 future::pending().boxed(),
678 ],
679 dyntype,
680 reader,
681 config: Default::default(),
682 }
683 }
684
685 /// This is the same as [`Controller::for_stream`]. Instead of taking an
686 /// `Api` (e.g. [`Controller::new`]), a stream of resources is used. Shared
687 /// streams can be created out-of-band by subscribing on a store `Writer`.
688 /// Through this interface, multiple controllers can use the same root
689 /// (shared) input stream of resources to keep memory overheads smaller.
690 ///
691 /// Prefer [`Controller::new`] or [`Controller::for_stream`] if you do not
692 /// need to share the stream.
693 ///
694 /// ## Warning:
695 ///
696 /// You **must** ensure the root stream (i.e. stream created through a `reflector()`)
697 /// is driven to readiness independently of this controller to ensure the
698 /// watcher never deadlocks.
699 ///
700 /// # Example:
701 ///
702 /// ```no_run
703 /// # use futures::StreamExt;
704 /// # use k8s_openapi::api::apps::v1::Deployment;
705 /// # use kube::runtime::controller::{Action, Controller};
706 /// # use kube::runtime::{predicates, watcher, reflector, WatchStreamExt};
707 /// # use kube::{Api, Client, Error, ResourceExt};
708 /// # use std::sync::Arc;
709 /// # async fn reconcile(_: Arc<Deployment>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
710 /// # fn error_policy(_: Arc<Deployment>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
711 /// # async fn doc(client: kube::Client) {
712 /// let api: Api<Deployment> = Api::default_namespaced(client);
713 /// let (reader, writer) = reflector::store_shared(128);
714 /// let subscriber = writer
715 /// .subscribe()
716 /// .expect("subscribers can only be created from shared stores");
717 /// let deploys = watcher(api, watcher::Config::default())
718 /// .default_backoff()
719 /// .reflect(writer)
720 /// .applied_objects()
721 /// .for_each(|ev| async move {
722 /// match ev {
723 /// Ok(obj) => tracing::info!("got obj {obj:?}"),
724 /// Err(error) => tracing::error!(%error, "received error")
725 /// }
726 /// });
727 ///
728 /// let controller = Controller::for_shared_stream(subscriber, reader)
729 /// .run(reconcile, error_policy, Arc::new(()))
730 /// .for_each(|ev| async move {
731 /// tracing::info!("reconciled {ev:?}")
732 /// });
733 ///
734 /// // Drive streams using a select statement
735 /// tokio::select! {
736 /// _ = deploys => {},
737 /// _ = controller => {},
738 /// }
739 /// # }
740 #[cfg(feature = "unstable-runtime-subscribe")]
741 pub fn for_shared_stream(trigger: impl Stream<Item = Arc<K>> + Send + 'static, reader: Store<K>) -> Self
742 where
743 K::DynamicType: Default,
744 {
745 Self::for_shared_stream_with(trigger, reader, Default::default())
746 }
747
748 /// This is the same as [`Controller::for_stream`]. Instead of taking an
749 /// `Api` (e.g. [`Controller::new`]), a stream of resources is used. Shared
750 /// streams can be created out-of-band by subscribing on a store `Writer`.
751 /// Through this interface, multiple controllers can use the same root
752 /// (shared) input stream of resources to keep memory overheads smaller.
753 ///
754 /// Prefer [`Controller::new`] or [`Controller::for_stream`] if you do not
755 /// need to share the stream.
756 ///
757 /// This variant constructor is used for [`dynamic`] types found through
758 /// discovery. Prefer [`Controller::for_shared_stream`] for static types (i.e.
759 /// known at compile time).
760 ///
761 /// [`dynamic`]: kube_client::core::dynamic
762 #[cfg(feature = "unstable-runtime-subscribe")]
763 pub fn for_shared_stream_with(
764 trigger: impl Stream<Item = Arc<K>> + Send + 'static,
765 reader: Store<K>,
766 dyntype: K::DynamicType,
767 ) -> Self {
768 let mut trigger_selector = stream::SelectAll::new();
769 let self_watcher = trigger_self(trigger.map(Ok), dyntype.clone()).boxed();
770 trigger_selector.push(self_watcher);
771 Self {
772 trigger_selector,
773 trigger_backoff: Box::<DefaultBackoff>::default(),
774 graceful_shutdown_selector: vec![
775 // Fallback future, ensuring that we never terminate if no additional futures are added to the selector
776 future::pending().boxed(),
777 ],
778 forceful_shutdown_selector: vec![
779 // Fallback future, ensuring that we never terminate if no additional futures are added to the selector
780 future::pending().boxed(),
781 ],
782 dyntype,
783 reader,
784 config: Default::default(),
785 }
786 }
787
788 /// Specify the configuration for the controller's behavior.
789 #[must_use]
790 pub fn with_config(mut self, config: Config) -> Self {
791 self.config = config;
792 self
793 }
794
795 /// Specify the backoff policy for "trigger" watches
796 ///
797 /// This includes the core watch, as well as auxiliary watches introduced by [`Self::owns`] and [`Self::watches`].
798 ///
799 /// The [`default_backoff`](crate::watcher::default_backoff) follows client-go conventions,
800 /// but can be overridden by calling this method.
801 #[must_use]
802 pub fn trigger_backoff(mut self, backoff: impl Backoff + 'static) -> Self {
803 self.trigger_backoff = Box::new(backoff);
804 self
805 }
806
807 /// Retrieve a copy of the reader before starting the controller
808 pub fn store(&self) -> Store<K> {
809 self.reader.clone()
810 }
811
812 /// Specify `Child` objects which `K` owns and should be watched
813 ///
814 /// Takes an [`Api`] object that determines how the `Controller` listens for changes to the `Child`.
815 /// All owned `Child` objects **must** contain an [`OwnerReference`] pointing back to a `K`.
816 ///
817 /// The [`watcher::Config`] controls the subset of `Child` objects that you want the [`Api`]
818 /// to watch - in the Api's configured scope - and receive reconcile events for.
819 /// To watch the full set of `Child` objects in the given `Api` scope, you can use [`watcher::Config::default`].
820 ///
821 /// [`OwnerReference`]: k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference
822 #[must_use]
823 pub fn owns<Child: Clone + Resource<DynamicType = ()> + DeserializeOwned + Debug + Send + 'static>(
824 self,
825 api: Api<Child>,
826 wc: watcher::Config,
827 ) -> Self {
828 self.owns_with(api, (), wc)
829 }
830
831 /// Specify `Child` objects which `K` owns and should be watched
832 ///
833 /// Same as [`Controller::owns`], but accepts a `DynamicType` so it can be used with dynamic resources.
834 #[must_use]
835 pub fn owns_with<Child: Clone + Resource + DeserializeOwned + Debug + Send + 'static>(
836 mut self,
837 api: Api<Child>,
838 dyntype: Child::DynamicType,
839 wc: watcher::Config,
840 ) -> Self
841 where
842 Child::DynamicType: Debug + Eq + Hash + Clone,
843 {
844 // TODO: call owns_stream_with when it's stable
845 #[allow(deprecated)]
846 let child_watcher = trigger_owners(
847 metadata_watcher(api, wc).touched_objects(),
848 self.dyntype.clone(),
849 dyntype,
850 );
851 self.trigger_selector.push(child_watcher.boxed());
852 self
853 }
854
855 /// Trigger the reconciliation process for a stream of `Child` objects of the owner `K`
856 ///
857 /// Same as [`Controller::owns`], but instead of an `Api`, a stream of resources is used.
858 /// This allows for customized and pre-filtered watch streams to be used as a trigger,
859 /// as well as sharing input streams between multiple controllers.
860 ///
861 /// Watcher streams passed in here should be filtered first through `touched_objects`.
862 ///
863 /// # Example:
864 ///
865 /// ```no_run
866 /// # use futures::StreamExt;
867 /// # use k8s_openapi::api::core::v1::ConfigMap;
868 /// # use k8s_openapi::api::apps::v1::StatefulSet;
869 /// # use kube::runtime::controller::Action;
870 /// # use kube::runtime::{predicates, metadata_watcher, watcher, Controller, WatchStreamExt};
871 /// # use kube::{Api, Client, Error, ResourceExt};
872 /// # use std::sync::Arc;
873 /// # type CustomResource = ConfigMap;
874 /// # async fn reconcile(_: Arc<CustomResource>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
875 /// # fn error_policy(_: Arc<CustomResource>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
876 /// # async fn doc(client: kube::Client) {
877 /// let sts_stream = metadata_watcher(Api::<StatefulSet>::all(client.clone()), watcher::Config::default())
878 /// .touched_objects()
879 /// // Retain observed deletions so the owner can recreate missing children.
880 /// .predicate_filter(predicates::resource_version, Default::default());
881 ///
882 /// Controller::new(Api::<CustomResource>::all(client), watcher::Config::default())
883 /// .owns_stream(sts_stream)
884 /// .run(reconcile, error_policy, Arc::new(()))
885 /// .for_each(|_| std::future::ready(()))
886 /// .await;
887 /// # }
888 /// ```
889 ///
890 /// A generation predicate can suppress deletion triggers because deletion need not
891 /// change generation. Resource-version filtering also passes status-only updates.
892 /// See [predicate deletion handling](crate::predicates) for details.
893 #[cfg(feature = "unstable-runtime-stream-control")]
894 #[must_use]
895 pub fn owns_stream<Child: Resource<DynamicType = ()> + Send + 'static>(
896 self,
897 trigger: impl Stream<Item = Result<Child, watcher::Error>> + Send + 'static,
898 ) -> Self {
899 self.owns_stream_with(trigger, ())
900 }
901
902 /// Trigger the reconciliation process for a stream of `Child` objects of the owner `K`
903 ///
904 /// Same as [`Controller::owns`], but instead of an `Api`, a stream of resources is used.
905 /// This allows for customized and pre-filtered watch streams to be used as a trigger,
906 /// as well as sharing input streams between multiple controllers.
907 ///
908 /// Same as [`Controller::owns_stream`], but accepts a `DynamicType` so it can be used with dynamic resources.
909 #[cfg(feature = "unstable-runtime-stream-control")]
910 #[must_use]
911 pub fn owns_stream_with<Child: Resource + Send + 'static>(
912 mut self,
913 trigger: impl Stream<Item = Result<Child, watcher::Error>> + Send + 'static,
914 dyntype: Child::DynamicType,
915 ) -> Self
916 where
917 Child::DynamicType: Debug + Eq + Hash + Clone,
918 {
919 let child_watcher = trigger_owners(trigger, self.dyntype.clone(), dyntype);
920 self.trigger_selector.push(child_watcher.boxed());
921 self
922 }
923
924 /// This is the same as [`Controller::for_stream`]. Instead of taking an
925 /// `Api` (e.g. [`Controller::new`]), a stream of resources is used. Shared
926 /// streams can be created out-of-band by subscribing on a store `Writer`.
927 /// Through this interface, multiple controllers can use the same root
928 /// (shared) input stream of resources to keep memory overheads smaller.
929 ///
930 /// Prefer [`Controller::new`] or [`Controller::for_stream`] if you do not
931 /// need to share the stream.
932 ///
933 /// ## Warning:
934 ///
935 /// You **must** ensure the root stream (i.e. stream created through a `reflector()`)
936 /// is driven to readiness independently of this controller to ensure the
937 /// watcher never deadlocks.
938 ///
939 ///
940 /// Trigger the reconciliation process for a shared stream of `Child`
941 /// objects of the owner `K`
942 ///
943 /// Conceptually the same as [`Controller::owns`], but a stream is used
944 /// instead of an `Api`. This interface behaves similarly to its non-shared
945 /// counterpart [`Controller::owns_stream`].
946 ///
947 /// # Example:
948 ///
949 /// ```no_run
950 /// # use futures::StreamExt;
951 /// # use k8s_openapi::api::{apps::v1::Deployment, core::v1::Pod};
952 /// # use kube::runtime::controller::{Action, Controller};
953 /// # use kube::runtime::{predicates, watcher, reflector, WatchStreamExt};
954 /// # use kube::{Api, Client, Error, ResourceExt};
955 /// # use std::sync::Arc;
956 /// # async fn reconcile(_: Arc<Deployment>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
957 /// # fn error_policy(_: Arc<Deployment>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
958 /// # async fn doc(client: kube::Client) {
959 /// let deploys: Api<Deployment> = Api::default_namespaced(client.clone());
960 /// let pod_api: Api<Pod> = Api::default_namespaced(client);
961 ///
962 /// let (reader, writer) = reflector::store_shared(128);
963 /// let subscriber = writer
964 /// .subscribe()
965 /// .expect("subscribers can only be created from shared stores");
966 /// let pods = watcher(pod_api, watcher::Config::default())
967 /// .default_backoff()
968 /// .reflect(writer)
969 /// .applied_objects()
970 /// .for_each(|ev| async move {
971 /// match ev {
972 /// Ok(obj) => tracing::info!("got obj {obj:?}"),
973 /// Err(error) => tracing::error!(%error, "received error")
974 /// }
975 /// });
976 ///
977 /// let controller = Controller::new(deploys, Default::default())
978 /// .owns_shared_stream(subscriber)
979 /// .run(reconcile, error_policy, Arc::new(()))
980 /// .for_each(|ev| async move {
981 /// tracing::info!("reconciled {ev:?}")
982 /// });
983 ///
984 /// // Drive streams using a select statement
985 /// tokio::select! {
986 /// _ = pods => {},
987 /// _ = controller => {},
988 /// }
989 /// # }
990 #[cfg(feature = "unstable-runtime-subscribe")]
991 #[must_use]
992 pub fn owns_shared_stream<Child: Resource<DynamicType = ()> + Send + 'static>(
993 self,
994 trigger: impl Stream<Item = Arc<Child>> + Send + 'static,
995 ) -> Self {
996 self.owns_shared_stream_with(trigger, ())
997 }
998
999 /// Trigger the reconciliation process for a shared stream of `Child` objects of the owner `K`
1000 ///
1001 /// Same as [`Controller::owns`], but instead of an `Api`, a shared stream of resources is used.
1002 /// The source stream can be shared between multiple controllers, optimising
1003 /// resource usage.
1004 ///
1005 /// Same as [`Controller::owns_shared_stream`], but accepts a `DynamicType` so it can be used with dynamic resources.
1006 #[cfg(feature = "unstable-runtime-subscribe")]
1007 #[must_use]
1008 pub fn owns_shared_stream_with<Child: Resource<DynamicType = ()> + Send + 'static>(
1009 mut self,
1010 trigger: impl Stream<Item = Arc<Child>> + Send + 'static,
1011 dyntype: Child::DynamicType,
1012 ) -> Self
1013 where
1014 Child::DynamicType: Debug + Eq + Hash + Clone,
1015 {
1016 let child_watcher = trigger_owners_shared(trigger.map(Ok), self.dyntype.clone(), dyntype);
1017 self.trigger_selector.push(child_watcher.boxed());
1018 self
1019 }
1020
1021 /// Specify `Watched` object which `K` has a custom relation to and should be watched
1022 ///
1023 /// To define the `Watched` relation with `K`, you **must** define a custom relation mapper, which,
1024 /// when given a `Watched` object, returns an option or iterator of relevant `ObjectRef<K>` to reconcile.
1025 ///
1026 /// If the relation `K` has to `Watched` is that `K` owns `Watched`, consider using [`Controller::owns`].
1027 ///
1028 /// Takes an [`Api`] object that determines how the `Controller` listens for changes to the `Watched`.
1029 ///
1030 /// The [`watcher::Config`] controls the subset of `Watched` objects that you want the [`Api`]
1031 /// to watch - in the Api's configured scope - and run through the custom mapper.
1032 /// To watch the full set of `Watched` objects in given the `Api` scope, you can use [`watcher::Config::default`].
1033 ///
1034 /// # Example
1035 ///
1036 /// Tracking cross cluster references using the [Operator-SDK] annotations.
1037 ///
1038 /// ```
1039 /// # use kube::runtime::{Controller, controller::Action, reflector::ObjectRef, watcher};
1040 /// # use kube::{Api, ResourceExt};
1041 /// # use k8s_openapi::api::core::v1::{ConfigMap, Namespace};
1042 /// # use futures::StreamExt;
1043 /// # use std::sync::Arc;
1044 /// # type WatchedResource = Namespace;
1045 /// # struct Context;
1046 /// # async fn reconcile(_: Arc<ConfigMap>, _: Arc<Context>) -> Result<Action, kube::Error> {
1047 /// # Ok(Action::await_change())
1048 /// # };
1049 /// # fn error_policy(_: Arc<ConfigMap>, _: &kube::Error, _: Arc<Context>) -> Action {
1050 /// # Action::await_change()
1051 /// # }
1052 /// # async fn doc(client: kube::Client) -> Result<(), Box<dyn std::error::Error>> {
1053 /// # let memcached = Api::<ConfigMap>::all(client.clone());
1054 /// # let context = Arc::new(Context);
1055 /// Controller::new(memcached, watcher::Config::default())
1056 /// .watches(
1057 /// Api::<WatchedResource>::all(client.clone()),
1058 /// watcher::Config::default(),
1059 /// |ar| {
1060 /// let prt = ar
1061 /// .annotations()
1062 /// .get("operator-sdk/primary-resource-type")
1063 /// .map(String::as_str);
1064 ///
1065 /// if prt != Some("Memcached.cache.example.com") {
1066 /// return None;
1067 /// }
1068 ///
1069 /// let (namespace, name) = ar
1070 /// .annotations()
1071 /// .get("operator-sdk/primary-resource")?
1072 /// .split_once('/')?;
1073 ///
1074 /// Some(ObjectRef::new(name).within(namespace))
1075 /// }
1076 /// )
1077 /// .run(reconcile, error_policy, context)
1078 /// .for_each(|_| futures::future::ready(()))
1079 /// .await;
1080 /// # Ok(())
1081 /// # }
1082 /// ```
1083 ///
1084 /// [Operator-SDK]: https://sdk.operatorframework.io/docs/building-operators/ansible/reference/retroactively-owned-resources/
1085 #[must_use]
1086 pub fn watches<Other, I>(
1087 self,
1088 api: Api<Other>,
1089 wc: watcher::Config,
1090 mapper: impl Fn(Other) -> I + Sync + Send + 'static,
1091 ) -> Self
1092 where
1093 Other: Clone + Resource + DeserializeOwned + Debug + Send + 'static,
1094 Other::DynamicType: Default + Debug + Clone + Eq + Hash,
1095 I: 'static + IntoIterator<Item = ObjectRef<K>>,
1096 I::IntoIter: Send,
1097 {
1098 self.watches_with(api, Default::default(), wc, mapper)
1099 }
1100
1101 /// Specify `Watched` object which `K` has a custom relation to and should be watched
1102 ///
1103 /// Same as [`Controller::watches`], but accepts a `DynamicType` so it can be used with dynamic resources.
1104 #[must_use]
1105 pub fn watches_with<Other, I>(
1106 mut self,
1107 api: Api<Other>,
1108 dyntype: Other::DynamicType,
1109 wc: watcher::Config,
1110 mapper: impl Fn(Other) -> I + Sync + Send + 'static,
1111 ) -> Self
1112 where
1113 Other: Clone + Resource + DeserializeOwned + Debug + Send + 'static,
1114 I: 'static + IntoIterator<Item = ObjectRef<K>>,
1115 I::IntoIter: Send,
1116 Other::DynamicType: Debug + Clone + Eq + Hash,
1117 {
1118 let other_watcher = trigger_others(watcher(api, wc).touched_objects(), mapper, dyntype);
1119 self.trigger_selector.push(other_watcher.boxed());
1120 self
1121 }
1122
1123 /// Trigger the reconciliation process for a stream of `Other` objects related to a `K`
1124 ///
1125 /// Same as [`Controller::watches`], but instead of an `Api`, a stream of resources is used.
1126 /// This allows for customized and pre-filtered watch streams to be used as a trigger,
1127 /// as well as sharing input streams between multiple controllers.
1128 ///
1129 /// Watcher streams passed in here should be filtered first through `touched_objects`.
1130 ///
1131 /// # Example:
1132 ///
1133 /// ```no_run
1134 /// # use futures::StreamExt;
1135 /// # use k8s_openapi::api::core::v1::ConfigMap;
1136 /// # use k8s_openapi::api::apps::v1::DaemonSet;
1137 /// # use kube::runtime::controller::Action;
1138 /// # use kube::runtime::{predicates, reflector::ObjectRef, watcher, Controller, WatchStreamExt};
1139 /// # use kube::{Api, Client, Error, ResourceExt};
1140 /// # use std::sync::Arc;
1141 /// # type CustomResource = ConfigMap;
1142 /// # async fn reconcile(_: Arc<CustomResource>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
1143 /// # fn error_policy(_: Arc<CustomResource>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
1144 /// fn mapper(_: DaemonSet) -> Option<ObjectRef<CustomResource>> { todo!() }
1145 /// # async fn doc(client: kube::Client) {
1146 /// let api: Api<DaemonSet> = Api::all(client.clone());
1147 /// let cr: Api<CustomResource> = Api::all(client.clone());
1148 /// let daemons = watcher(api, watcher::Config::default())
1149 /// .touched_objects()
1150 /// // Retain observed deletions as triggers for the mapped objects.
1151 /// .predicate_filter(predicates::resource_version, Default::default());
1152 ///
1153 /// Controller::new(cr, watcher::Config::default())
1154 /// .watches_stream(daemons, mapper)
1155 /// .run(reconcile, error_policy, Arc::new(()))
1156 /// .for_each(|_| std::future::ready(()))
1157 /// .await;
1158 /// # }
1159 /// ```
1160 ///
1161 /// A generation predicate can suppress deletion triggers because deletion need not
1162 /// change generation. Resource-version filtering also passes status-only updates.
1163 /// See [predicate deletion handling](crate::predicates) for details.
1164 #[cfg(feature = "unstable-runtime-stream-control")]
1165 #[must_use]
1166 pub fn watches_stream<Other, I>(
1167 self,
1168 trigger: impl Stream<Item = Result<Other, watcher::Error>> + Send + 'static,
1169 mapper: impl Fn(Other) -> I + Sync + Send + 'static,
1170 ) -> Self
1171 where
1172 Other: Clone + Resource + DeserializeOwned + Debug + Send + 'static,
1173 Other::DynamicType: Default + Debug + Clone,
1174 I: 'static + IntoIterator<Item = ObjectRef<K>>,
1175 I::IntoIter: Send,
1176 {
1177 self.watches_stream_with(trigger, mapper, Default::default())
1178 }
1179
1180 /// Trigger the reconciliation process for a stream of `Other` objects related to a `K`
1181 ///
1182 /// Same as [`Controller::watches`], but instead of an `Api`, a stream of resources is used.
1183 /// This allows for customized and pre-filtered watch streams to be used as a trigger,
1184 /// as well as sharing input streams between multiple controllers.
1185 ///
1186 /// Same as [`Controller::watches_stream`], but accepts a `DynamicType` so it can be used with dynamic resources.
1187 #[cfg(feature = "unstable-runtime-stream-control")]
1188 #[must_use]
1189 pub fn watches_stream_with<Other, I>(
1190 mut self,
1191 trigger: impl Stream<Item = Result<Other, watcher::Error>> + Send + 'static,
1192 mapper: impl Fn(Other) -> I + Sync + Send + 'static,
1193 dyntype: Other::DynamicType,
1194 ) -> Self
1195 where
1196 Other: Clone + Resource + DeserializeOwned + Debug + Send + 'static,
1197 Other::DynamicType: Debug + Clone,
1198 I: 'static + IntoIterator<Item = ObjectRef<K>>,
1199 I::IntoIter: Send,
1200 {
1201 let other_watcher = trigger_others(trigger, mapper, dyntype);
1202 self.trigger_selector.push(other_watcher.boxed());
1203 self
1204 }
1205
1206 /// Trigger the reconciliation process for a shared stream of `Other`
1207 /// objects related to a `K`
1208 ///
1209 /// Same as [`Controller::watches`], but instead of an `Api`, a shared
1210 /// stream of resources is used. This allows for sharing input streams
1211 /// between multiple controllers.
1212 ///
1213 /// Watcher streams passed in here should be filtered first through `touched_objects`.
1214 ///
1215 /// # Example:
1216 ///
1217 /// ```no_run
1218 /// # use futures::StreamExt;
1219 /// # use k8s_openapi::api::core::v1::ConfigMap;
1220 /// # use k8s_openapi::api::apps::v1::DaemonSet;
1221 /// # use kube::runtime::controller::Action;
1222 /// # use kube::runtime::{predicates, reflector::ObjectRef, watcher, Controller, WatchStreamExt};
1223 /// # use kube::{Api, Client, Error, ResourceExt};
1224 /// # use std::sync::Arc;
1225 /// # type CustomResource = ConfigMap;
1226 /// # async fn reconcile(_: Arc<CustomResource>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
1227 /// # fn error_policy(_: Arc<CustomResource>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
1228 /// fn mapper(_: Arc<DaemonSet>) -> Option<ObjectRef<CustomResource>> { todo!() }
1229 /// # async fn doc(client: kube::Client) {
1230 /// let api: Api<DaemonSet> = Api::all(client.clone());
1231 /// let cr: Api<CustomResource> = Api::all(client.clone());
1232 /// let (reader, writer) = kube_runtime::reflector::store_shared(128);
1233 /// let subscriber = writer
1234 /// .subscribe()
1235 /// .expect("subscribers can only be created from shared stores");
1236 /// let daemons = watcher(api, watcher::Config::default())
1237 /// .reflect(writer)
1238 /// .touched_objects()
1239 /// .for_each(|ev| async move {
1240 /// match ev {
1241 /// Ok(obj) => {},
1242 /// Err(error) => tracing::error!(%error, "received err")
1243 /// }
1244 /// });
1245 ///
1246 /// let controller = Controller::new(cr, watcher::Config::default())
1247 /// .watches_shared_stream(subscriber, mapper)
1248 /// .run(reconcile, error_policy, Arc::new(()))
1249 /// .for_each(|_| std::future::ready(()));
1250 ///
1251 /// // Drive streams using a select statement
1252 /// tokio::select! {
1253 /// _ = daemons => {},
1254 /// _ = controller => {},
1255 /// }
1256 /// # }
1257 /// ```
1258 #[cfg(feature = "unstable-runtime-subscribe")]
1259 #[must_use]
1260 pub fn watches_shared_stream<Other, I>(
1261 self,
1262 trigger: impl Stream<Item = Arc<Other>> + Send + 'static,
1263 mapper: impl Fn(Arc<Other>) -> I + Sync + Send + 'static,
1264 ) -> Self
1265 where
1266 Other: Clone + Resource + DeserializeOwned + Debug + Send + 'static,
1267 Other::DynamicType: Default + Debug + Clone,
1268 I: 'static + IntoIterator<Item = ObjectRef<K>>,
1269 I::IntoIter: Send,
1270 {
1271 self.watches_shared_stream_with(trigger, mapper, Default::default())
1272 }
1273
1274 /// Trigger the reconciliation process for a shared stream of `Other` objects related to a `K`
1275 ///
1276 /// Same as [`Controller::watches`], but instead of an `Api`, a shared
1277 /// stream of resources is used. This allows for sharing of streams between
1278 /// multiple controllers.
1279 ///
1280 /// Same as [`Controller::watches_shared_stream`], but accepts a `DynamicType` so it can be used with dynamic resources.
1281 #[cfg(feature = "unstable-runtime-subscribe")]
1282 #[must_use]
1283 pub fn watches_shared_stream_with<Other, I>(
1284 mut self,
1285 trigger: impl Stream<Item = Arc<Other>> + Send + 'static,
1286 mapper: impl Fn(Arc<Other>) -> I + Sync + Send + 'static,
1287 dyntype: Other::DynamicType,
1288 ) -> Self
1289 where
1290 Other: Clone + Resource + DeserializeOwned + Debug + Send + 'static,
1291 Other::DynamicType: Debug + Clone,
1292 I: 'static + IntoIterator<Item = ObjectRef<K>>,
1293 I::IntoIter: Send,
1294 {
1295 let other_watcher = trigger_others::<_, Other, _, _>(trigger.map(Ok), mapper, dyntype);
1296 self.trigger_selector.push(other_watcher.boxed());
1297 self
1298 }
1299
1300 /// Trigger a reconciliation for all managed objects whenever `trigger` emits a value
1301 ///
1302 /// For example, this can be used to reconcile all objects whenever the controller's configuration changes.
1303 ///
1304 /// To reconcile all objects when a new line is entered:
1305 ///
1306 /// ```
1307 /// # async {
1308 /// use futures::stream::StreamExt;
1309 /// use k8s_openapi::api::core::v1::ConfigMap;
1310 /// use kube::{
1311 /// Client,
1312 /// api::{Api, ResourceExt},
1313 /// runtime::{
1314 /// controller::{Controller, Action},
1315 /// watcher,
1316 /// },
1317 /// };
1318 /// use std::{convert::Infallible, io::BufRead, sync::Arc};
1319 /// let (mut reload_tx, reload_rx) = futures::channel::mpsc::channel(0);
1320 /// // Using a regular background thread since tokio::io::stdin() doesn't allow aborting reads,
1321 /// // and its worker prevents the Tokio runtime from shutting down.
1322 /// std::thread::spawn(move || {
1323 /// for _ in std::io::BufReader::new(std::io::stdin()).lines() {
1324 /// let _ = reload_tx.try_send(());
1325 /// }
1326 /// });
1327 /// Controller::new(
1328 /// Api::<ConfigMap>::all(Client::try_default().await.unwrap()),
1329 /// watcher::Config::default(),
1330 /// )
1331 /// .reconcile_all_on(reload_rx.map(|_| ()))
1332 /// .run(
1333 /// |o, _| async move {
1334 /// println!("Reconciling {}", o.name_any());
1335 /// Ok(Action::await_change())
1336 /// },
1337 /// |_object: Arc<ConfigMap>, err: &Infallible, _| Err(err).unwrap(),
1338 /// Arc::new(()),
1339 /// );
1340 /// # };
1341 /// ```
1342 ///
1343 /// This can be called multiple times, in which case they are additive; reconciles are scheduled whenever *any* [`Stream`] emits a new item.
1344 ///
1345 /// If a [`Stream`] is terminated (by emitting [`None`]) then the [`Controller`] keeps running, but the [`Stream`] stops being polled.
1346 #[must_use]
1347 pub fn reconcile_all_on(mut self, trigger: impl Stream<Item = ()> + Send + 'static) -> Self {
1348 let store = self.store();
1349 let dyntype = self.dyntype.clone();
1350 self.trigger_selector.push(
1351 trigger
1352 .flat_map(move |()| {
1353 let dyntype = dyntype.clone();
1354 stream::iter(store.state().into_iter().map(move |obj| {
1355 Ok(ReconcileRequest {
1356 obj_ref: ObjectRef::from_obj_with(&*obj, dyntype.clone()),
1357 reason: ReconcileReason::BulkReconcile,
1358 })
1359 }))
1360 })
1361 .boxed(),
1362 );
1363 self
1364 }
1365
1366 /// Trigger the reconciliation process for a managed object `ObjectRef<K>` whenever `trigger` emits a value
1367 ///
1368 /// This can be used to inject reconciliations for specific objects from an external resource.
1369 ///
1370 /// # Example:
1371 ///
1372 /// ```no_run
1373 /// # async {
1374 /// # use futures::{StreamExt, Stream, stream, TryStreamExt};
1375 /// # use k8s_openapi::api::core::v1::{ConfigMap};
1376 /// # use kube::api::Api;
1377 /// # use kube::runtime::controller::Action;
1378 /// # use kube::runtime::reflector::{ObjectRef, Store};
1379 /// # use kube::runtime::{reflector, watcher, Controller, WatchStreamExt};
1380 /// # use kube::runtime::watcher::Config;
1381 /// # use kube::{Client, Error, ResourceExt};
1382 /// # use std::future;
1383 /// # use std::sync::Arc;
1384 /// #
1385 /// # let client: Client = todo!();
1386 /// # async fn reconcile(_: Arc<ConfigMap>, _: Arc<()>) -> Result<Action, Error> { Ok(Action::await_change()) }
1387 /// # fn error_policy(_: Arc<ConfigMap>, _: &kube::Error, _: Arc<()>) -> Action { Action::await_change() }
1388 /// # fn watch_external_objects() -> impl Stream<Item = ExternalObject> { stream::iter(vec![]) }
1389 /// # let ns = "controller-ns".to_string();
1390 /// struct ExternalObject {
1391 /// name: String,
1392 /// }
1393 /// let external_stream = watch_external_objects().map(|ext| {
1394 /// ObjectRef::new(&format!("{}-cm", ext.name)).within(&ns)
1395 /// });
1396 ///
1397 /// Controller::new(Api::<ConfigMap>::namespaced(client, &ns), Config::default())
1398 /// .reconcile_on(external_stream)
1399 /// .run(reconcile, error_policy, Arc::new(()))
1400 /// .for_each(|_| future::ready(()))
1401 /// .await;
1402 /// # };
1403 /// ```
1404 #[cfg(feature = "unstable-runtime-reconcile-on")]
1405 #[must_use]
1406 pub fn reconcile_on(mut self, trigger: impl Stream<Item = ObjectRef<K>> + Send + 'static) -> Self {
1407 self.trigger_selector.push(
1408 trigger
1409 .map(move |obj| {
1410 Ok(ReconcileRequest {
1411 obj_ref: obj,
1412 reason: ReconcileReason::Unknown,
1413 })
1414 })
1415 .boxed(),
1416 );
1417 self
1418 }
1419
1420 /// Start a graceful shutdown when `trigger` resolves. Once a graceful shutdown has been initiated:
1421 ///
1422 /// - No new reconciliations are started from the scheduler
1423 /// - The underlying Kubernetes watch is terminated
1424 /// - All running reconciliations are allowed to finish
1425 /// - [`Controller::run`]'s [`Stream`] terminates once all running reconciliations are done.
1426 ///
1427 /// For example, to stop the reconciler whenever the user presses Ctrl+C:
1428 ///
1429 /// ```rust
1430 /// # async {
1431 /// use futures::future::FutureExt;
1432 /// use k8s_openapi::api::core::v1::ConfigMap;
1433 /// use kube::{Api, Client, ResourceExt};
1434 /// use kube_runtime::{
1435 /// controller::{Controller, Action},
1436 /// watcher,
1437 /// };
1438 /// use std::{convert::Infallible, sync::Arc};
1439 /// Controller::new(
1440 /// Api::<ConfigMap>::all(Client::try_default().await.unwrap()),
1441 /// watcher::Config::default(),
1442 /// )
1443 /// .graceful_shutdown_on(tokio::signal::ctrl_c().map(|_| ()))
1444 /// .run(
1445 /// |o, _| async move {
1446 /// println!("Reconciling {}", o.name_any());
1447 /// Ok(Action::await_change())
1448 /// },
1449 /// |_, err: &Infallible, _| Err(err).unwrap(),
1450 /// Arc::new(()),
1451 /// );
1452 /// # };
1453 /// ```
1454 ///
1455 /// This can be called multiple times, in which case they are additive; the [`Controller`] starts to terminate
1456 /// as soon as *any* [`Future`] resolves.
1457 #[must_use]
1458 pub fn graceful_shutdown_on(mut self, trigger: impl Future<Output = ()> + Send + Sync + 'static) -> Self {
1459 self.graceful_shutdown_selector.push(trigger.boxed());
1460 self
1461 }
1462
1463 /// Initiate graceful shutdown on Ctrl+C or SIGTERM (on Unix), waiting for all reconcilers to finish.
1464 ///
1465 /// Once a graceful shutdown has been initiated, Ctrl+C (or SIGTERM) can be sent again
1466 /// to request a forceful shutdown (requesting that all reconcilers abort on the next yield point).
1467 ///
1468 /// NOTE: On Unix this leaves the default handlers for SIGINT and SIGTERM disabled after the [`Controller`] has
1469 /// terminated. If you run this in a process containing more tasks than just the [`Controller`], ensure that
1470 /// all other tasks either terminate when the [`Controller`] does, that they have their own signal handlers,
1471 /// or use [`Controller::graceful_shutdown_on`] to manage your own shutdown strategy.
1472 ///
1473 /// NOTE: If developing a Windows service then you need to listen to its lifecycle events instead, and hook that into
1474 /// [`Controller::graceful_shutdown_on`].
1475 ///
1476 /// NOTE: [`Controller::run`] terminates as soon as a forceful shutdown is requested, but leaves the reconcilers running
1477 /// in the background while they terminate. This will block [`tokio::runtime::Runtime`] termination until they actually terminate,
1478 /// unless you run [`std::process::exit`] afterwards.
1479 #[must_use]
1480 pub fn shutdown_on_signal(mut self) -> Self {
1481 async fn shutdown_signal() {
1482 futures::future::select(
1483 tokio::signal::ctrl_c().map(|_| ()).boxed(),
1484 #[cfg(unix)]
1485 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
1486 .unwrap()
1487 .recv()
1488 .map(|_| ())
1489 .boxed(),
1490 // Assume that ctrl_c is enough on non-Unix platforms (such as Windows)
1491 #[cfg(not(unix))]
1492 futures::future::pending::<()>(),
1493 )
1494 .await;
1495 }
1496
1497 let (graceful_tx, graceful_rx) = channel::oneshot::channel();
1498 self.graceful_shutdown_selector
1499 .push(graceful_rx.map(|_| ()).boxed());
1500 self.forceful_shutdown_selector.push(
1501 async {
1502 tracing::info!("press ctrl+c to shut down gracefully");
1503 shutdown_signal().await;
1504 if let Ok(()) = graceful_tx.send(()) {
1505 tracing::info!("graceful shutdown requested, press ctrl+c again to force shutdown");
1506 } else {
1507 tracing::info!(
1508 "graceful shutdown already requested, press ctrl+c again to force shutdown"
1509 );
1510 }
1511 shutdown_signal().await;
1512 tracing::info!("forced shutdown requested");
1513 }
1514 .boxed(),
1515 );
1516 self
1517 }
1518
1519 /// Consume all the parameters of the Controller and start the applier stream
1520 ///
1521 /// This creates a stream from all builder calls and starts an applier with
1522 /// a specified `reconciler` and `error_policy` callbacks. Each of these will be called
1523 /// with a configurable `context`.
1524 pub fn run<ReconcilerFut, Ctx>(
1525 self,
1526 mut reconciler: impl FnMut(Arc<K>, Arc<Ctx>) -> ReconcilerFut,
1527 error_policy: impl Fn(Arc<K>, &ReconcilerFut::Error, Arc<Ctx>) -> Action,
1528 context: Arc<Ctx>,
1529 ) -> impl Stream<Item = Result<(ObjectRef<K>, Action), Error<ReconcilerFut::Error, watcher::Error>>>
1530 where
1531 K::DynamicType: Debug + Unpin,
1532 ReconcilerFut: TryFuture<Ok = Action> + Send + 'static,
1533 ReconcilerFut::Error: std::error::Error + Send + 'static,
1534 {
1535 applier(
1536 move |obj, ctx| {
1537 CancelableJoinHandle::spawn(
1538 TryFutureExt::into_future(reconciler(obj, ctx)).in_current_span(),
1539 &Handle::current(),
1540 )
1541 },
1542 error_policy,
1543 context,
1544 self.reader,
1545 StreamBackoff::new(self.trigger_selector, self.trigger_backoff)
1546 .take_until(future::select_all(self.graceful_shutdown_selector)),
1547 self.config,
1548 )
1549 .take_until(futures::future::select_all(self.forceful_shutdown_selector))
1550 }
1551}
1552
1553#[cfg(test)]
1554mod tests {
1555 use std::{convert::Infallible, pin::pin, sync::Arc, time::Duration};
1556
1557 use super::{APPLIER_REQUEUE_BUF_SIZE, Action};
1558 use crate::{
1559 Config, Controller, applier,
1560 reflector::{self, ObjectRef},
1561 watcher::{self, Event, watcher},
1562 };
1563 use futures::{Stream, StreamExt, TryStreamExt};
1564 use k8s_openapi::api::core::v1::ConfigMap;
1565 use kube_client::{
1566 Api, Resource,
1567 core::{ObjectMeta, PartialObjectMeta},
1568 };
1569 use serde::de::DeserializeOwned;
1570 use tokio::time::timeout;
1571
1572 fn assert_send<T: Send>(x: T) -> T {
1573 x
1574 }
1575
1576 // Used to typecheck that a type T is a generic type that implements Stream
1577 // and returns a WatchEvent generic over a resource `K`
1578 fn assert_stream<T, K>(x: T) -> T
1579 where
1580 T: Stream<Item = watcher::Result<Event<K>>> + Send,
1581 K: Resource + Clone + DeserializeOwned + std::fmt::Debug + Send + 'static,
1582 {
1583 x
1584 }
1585
1586 fn mock_type<T>() -> T {
1587 unimplemented!(
1588 "mock_type is not supposed to be called, only used for filling holes in type assertions"
1589 )
1590 }
1591
1592 // not #[test] because we don't want to actually run it, we just want to assert that it typechecks
1593 #[allow(dead_code, unused_must_use)]
1594 fn test_controller_should_be_send() {
1595 assert_send(
1596 Controller::new(mock_type::<Api<ConfigMap>>(), Default::default()).run(
1597 |_, _| async { Ok(mock_type::<Action>()) },
1598 |_: Arc<ConfigMap>, _: &std::io::Error, _| mock_type::<Action>(),
1599 Arc::new(()),
1600 ),
1601 );
1602 }
1603
1604 // not #[test] because we don't want to actually run it, we just want to
1605 // assert that it typechecks
1606 //
1607 // will check return types for `watcher` and `watcher with PartialObjectMeta` do not drift
1608 // given an arbitrary K that implements `Resource` (e.g ConfigMap)
1609 #[allow(dead_code, unused_must_use)]
1610 fn test_watcher_stream_type_drift() {
1611 assert_stream(watcher(mock_type::<Api<ConfigMap>>(), Default::default()));
1612 assert_stream(watcher(
1613 mock_type::<Api<PartialObjectMeta<ConfigMap>>>(),
1614 Default::default(),
1615 ));
1616 }
1617
1618 #[tokio::test]
1619 async fn applier_must_not_deadlock_if_reschedule_buffer_fills() {
1620 // This tests that `applier` handles reschedule queue backpressure correctly, by trying to flood it with no-op reconciles
1621 // This is intended to avoid regressing on https://github.com/kube-rs/kube/issues/926
1622
1623 // Assume that we can keep APPLIER_REQUEUE_BUF_SIZE flooded if we have 100x the number of objects "in rotation"
1624 // On my (@nightkr)'s 3900X I can reliably trigger this with 10x, but let's have some safety margin to avoid false negatives
1625 let items = APPLIER_REQUEUE_BUF_SIZE * 50;
1626 // Assume that everything's OK if we can reconcile every object 3 times on average
1627 let reconciles = items * 3;
1628
1629 let (queue_tx, queue_rx) = futures::channel::mpsc::unbounded::<ObjectRef<ConfigMap>>();
1630 let (store_rx, mut store_tx) = reflector::store();
1631 let mut applier = pin!(applier(
1632 |_obj, _| {
1633 Box::pin(async move {
1634 // Try to flood the rescheduling buffer buffer by just putting it back in the queue immediately
1635 //println!("reconciling {:?}", obj.metadata.name);
1636 Ok(Action::requeue(Duration::ZERO))
1637 })
1638 },
1639 |_: Arc<ConfigMap>, _: &Infallible, _| todo!(),
1640 Arc::new(()),
1641 store_rx,
1642 queue_rx.map(Result::<_, Infallible>::Ok),
1643 Config::default(),
1644 ));
1645 store_tx.apply_watcher_event(&watcher::Event::InitDone);
1646 for i in 0..items {
1647 let obj = ConfigMap {
1648 metadata: ObjectMeta {
1649 name: Some(format!("cm-{i}")),
1650 namespace: Some("default".to_string()),
1651 ..Default::default()
1652 },
1653 ..Default::default()
1654 };
1655 store_tx.apply_watcher_event(&watcher::Event::Apply(obj.clone()));
1656 queue_tx.unbounded_send(ObjectRef::from_obj(&obj)).unwrap();
1657 }
1658
1659 timeout(
1660 Duration::from_secs(10),
1661 applier
1662 .as_mut()
1663 .take(reconciles)
1664 .try_for_each(|_| async { Ok(()) }),
1665 )
1666 .await
1667 .expect("test timeout expired, applier likely deadlocked")
1668 .unwrap();
1669
1670 // Do an orderly shutdown to ensure that no individual reconcilers are stuck
1671 drop(queue_tx);
1672 timeout(
1673 Duration::from_secs(10),
1674 applier.try_for_each(|_| async { Ok(()) }),
1675 )
1676 .await
1677 .expect("applier cleanup timeout expired, individual reconciler likely deadlocked?")
1678 .unwrap();
1679 }
1680}