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}