kube_runtime/
finalizer.rs

1//! Finalizer helper for [`Controller`](crate::Controller) reconcilers
2use crate::controller::Action;
3use futures::{TryFuture, TryFutureExt};
4use json_patch::{AddOperation, PatchOperation, RemoveOperation, TestOperation};
5use jsonptr::PointerBuf;
6use kube_client::{
7    api::{Patch, PatchParams},
8    Api, Resource, ResourceExt,
9};
10
11use serde::{de::DeserializeOwned, Serialize};
12use std::{error::Error as StdError, fmt::Debug, str::FromStr, sync::Arc};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum Error<ReconcileErr>
17where
18    ReconcileErr: StdError + 'static,
19{
20    #[error("failed to apply object: {0}")]
21    ApplyFailed(#[source] ReconcileErr),
22    #[error("failed to clean up object: {0}")]
23    CleanupFailed(#[source] ReconcileErr),
24    #[error("failed to add finalizer: {0}")]
25    AddFinalizer(#[source] kube_client::Error),
26    #[error("failed to remove finalizer: {0}")]
27    RemoveFinalizer(#[source] kube_client::Error),
28    #[error("object has no name")]
29    UnnamedObject,
30    #[error("invalid finalizer")]
31    InvalidFinalizer,
32}
33
34struct FinalizerState {
35    finalizer_index: Option<usize>,
36    is_deleting: bool,
37}
38
39impl FinalizerState {
40    fn for_object<K: Resource>(obj: &K, finalizer_name: &str) -> Self {
41        Self {
42            finalizer_index: obj
43                .finalizers()
44                .iter()
45                .enumerate()
46                .find(|(_, fin)| *fin == finalizer_name)
47                .map(|(i, _)| i),
48            is_deleting: obj.meta().deletion_timestamp.is_some(),
49        }
50    }
51}
52
53/// Reconcile an object in a way that requires cleanup before an object can be deleted.
54///
55/// It does this by managing a [`ObjectMeta::finalizers`] entry,
56/// which prevents the object from being deleted before the cleanup is done.
57///
58/// In typical usage, if you use `finalizer` then it should be the only top-level "action"
59/// in your [`applier`](crate::applier)/[`Controller`](crate::Controller)'s `reconcile` function.
60///
61/// # Expected Flow
62///
63/// 1. User creates object
64/// 2. Reconciler sees object
65/// 3. `finalizer` adds `finalizer_name` to [`ObjectMeta::finalizers`]
66/// 4. Reconciler sees updated object
67/// 5. `finalizer` runs [`Event::Apply`]
68/// 6. User updates object
69/// 7. Reconciler sees updated object
70/// 8. `finalizer` runs [`Event::Apply`]
71/// 9. User deletes object
72/// 10. Reconciler sees deleting object
73/// 11. `finalizer` runs [`Event::Cleanup`]
74/// 12. `finalizer` removes `finalizer_name` from [`ObjectMeta::finalizers`]
75/// 13. Kubernetes sees that all [`ObjectMeta::finalizers`] are gone and finally deletes the object
76///
77/// # Guarantees
78///
79/// If [`Event::Apply`] is ever started then [`Event::Cleanup`] must succeed before the Kubernetes object deletion completes.
80///
81/// # Assumptions
82///
83/// `finalizer_name` must be unique among the controllers interacting with the object
84///
85/// [`Event::Apply`] and [`Event::Cleanup`] must both be idempotent, and tolerate being executed several times (even if previously cancelled).
86///
87/// [`Event::Cleanup`] must tolerate [`Event::Apply`] never having ran at all, or never having succeeded. Keep in mind that
88/// even infallible `.await`s are cancellation points.
89///
90/// # Caveats
91///
92/// Object deletes will get stuck while the controller is not running, or if `cleanup` fails for some reason.
93///
94/// `reconcile` should take the object that the [`Event`] contains, rather than trying to reuse `obj`, since it may have been updated.
95///
96/// # Errors
97///
98/// [`Event::Apply`] and [`Event::Cleanup`] are both fallible, their errors are passed through as [`Error::ApplyFailed`]
99/// and [`Error::CleanupFailed`], respectively.
100///
101/// In addition, adding and removing the finalizer itself may fail. In particular, this may be because of
102/// network errors, lacking permissions, or because another `finalizer` was updated in the meantime on the same object.
103///
104/// [`ObjectMeta::finalizers`]: kube_client::api::ObjectMeta#structfield.finalizers
105pub async fn finalizer<K, ReconcileFut>(
106    api: &Api<K>,
107    finalizer_name: &str,
108    obj: Arc<K>,
109    reconcile: impl FnOnce(Event<K>) -> ReconcileFut,
110) -> Result<Action, Error<ReconcileFut::Error>>
111where
112    K: Resource + Clone + DeserializeOwned + Serialize + Debug,
113    ReconcileFut: TryFuture<Ok = Action>,
114    ReconcileFut::Error: StdError + 'static,
115{
116    match FinalizerState::for_object(&*obj, finalizer_name) {
117        FinalizerState {
118            finalizer_index: Some(_),
119            is_deleting: false,
120        } => reconcile(Event::Apply(obj))
121            .into_future()
122            .await
123            .map_err(Error::ApplyFailed),
124        FinalizerState {
125            finalizer_index: Some(finalizer_i),
126            is_deleting: true,
127        } => {
128            // Cleanup reconciliation must succeed before it's safe to remove the finalizer
129            let name = obj.meta().name.clone().ok_or(Error::UnnamedObject)?;
130            let action = reconcile(Event::Cleanup(obj))
131                .into_future()
132                .await
133                // Short-circuit, so that we keep the finalizer if cleanup fails
134                .map_err(Error::CleanupFailed)?;
135            // Cleanup was successful, remove the finalizer so that deletion can continue
136            let finalizer_path = format!("/metadata/finalizers/{finalizer_i}");
137            api.patch::<K>(
138                &name,
139                &PatchParams::default(),
140                &Patch::Json(json_patch::Patch(vec![
141                    // All finalizers run concurrently and we use an integer index
142                    // `Test` ensures that we fail instead of deleting someone else's finalizer
143                    // (in which case a new `Cleanup` event will be sent)
144                    PatchOperation::Test(TestOperation {
145                        path: PointerBuf::from_str(finalizer_path.as_str())
146                            .map_err(|_err| Error::InvalidFinalizer)?,
147                        value: finalizer_name.into(),
148                    }),
149                    PatchOperation::Remove(RemoveOperation {
150                        path: PointerBuf::from_str(finalizer_path.as_str())
151                            .map_err(|_err| Error::InvalidFinalizer)?,
152                    }),
153                ])),
154            )
155            .await
156            .map_err(Error::RemoveFinalizer)?;
157            Ok(action)
158        }
159        FinalizerState {
160            finalizer_index: None,
161            is_deleting: false,
162        } => {
163            // Finalizer must be added before it's safe to run an `Apply` reconciliation
164            let patch = json_patch::Patch(if obj.finalizers().is_empty() {
165                vec![
166                    PatchOperation::Test(TestOperation {
167                        path: PointerBuf::from_str("/metadata/finalizers")
168                            .map_err(|_err| Error::InvalidFinalizer)?,
169                        value: serde_json::Value::Null,
170                    }),
171                    PatchOperation::Add(AddOperation {
172                        path: PointerBuf::from_str("/metadata/finalizers")
173                            .map_err(|_err| Error::InvalidFinalizer)?,
174                        value: vec![finalizer_name].into(),
175                    }),
176                ]
177            } else {
178                vec![
179                    // Kubernetes doesn't automatically deduplicate finalizers (see
180                    // https://github.com/kube-rs/kube/issues/964#issuecomment-1197311254),
181                    // so we need to fail and retry if anyone else has added the finalizer in the meantime
182                    PatchOperation::Test(TestOperation {
183                        path: PointerBuf::from_str("/metadata/finalizers")
184                            .map_err(|_err| Error::InvalidFinalizer)?,
185                        value: obj.finalizers().into(),
186                    }),
187                    PatchOperation::Add(AddOperation {
188                        path: PointerBuf::from_str("/metadata/finalizers/-")
189                            .map_err(|_err| Error::InvalidFinalizer)?,
190                        value: finalizer_name.into(),
191                    }),
192                ]
193            });
194            api.patch::<K>(
195                obj.meta().name.as_deref().ok_or(Error::UnnamedObject)?,
196                &PatchParams::default(),
197                &Patch::Json(patch),
198            )
199            .await
200            .map_err(Error::AddFinalizer)?;
201            // No point applying here, since the patch will cause a new reconciliation
202            Ok(Action::await_change())
203        }
204        FinalizerState {
205            finalizer_index: None,
206            is_deleting: true,
207        } => {
208            // Our work here is done
209            Ok(Action::await_change())
210        }
211    }
212}
213
214/// A representation of an action that should be taken by a reconciler.
215pub enum Event<K> {
216    /// The reconciler should ensure that the actual state matches the state desired in the object.
217    ///
218    /// This must be idempotent, since it may be recalled if, for example (this list is non-exhaustive):
219    ///
220    /// - The controller is restarted
221    /// - The object is updated
222    /// - The reconciliation fails
223    /// - The grinch attacks
224    Apply(Arc<K>),
225    /// The object is being deleted, and the reconciler should remove all resources that it owns.
226    ///
227    /// This must be idempotent, since it may be recalled if, for example (this list is non-exhaustive):
228    ///
229    /// - The controller is restarted while the deletion is in progress
230    /// - The reconciliation fails
231    /// - Another finalizer was removed in the meantime
232    /// - The grinch's heart grows a size or two
233    Cleanup(Arc<K>),
234}