k8s_operator_controller/
finalizer.rs1use kube::api::{Patch, PatchParams, Resource};
2use kube::{Api, Client};
3use serde::de::DeserializeOwned;
4use serde::Serialize;
5use serde_json::json;
6use tracing::info;
7
8use k8s_operator_core::OperatorError;
9
10pub const DEFAULT_FINALIZER: &str = "k8s-operator.io/finalizer";
11
12pub async fn add_finalizer<K>(client: &Client, resource: &K, finalizer: &str) -> k8s_operator_core::Result<()>
13where
14 K: Resource + Clone + std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync + 'static,
15 K::DynamicType: Default,
16{
17 let name = resource.meta().name.clone().ok_or_else(|| {
18 OperatorError::Internal("Resource has no name".to_string())
19 })?;
20
21 let api: Api<K> = Api::all(client.clone());
22
23 let patch = json!({
24 "metadata": {
25 "finalizers": [finalizer]
26 }
27 });
28
29 api.patch(&name, &PatchParams::apply("k8s-operator"), &Patch::Merge(&patch))
30 .await
31 .map_err(|e| OperatorError::KubeError(e))?;
32
33 info!("Added finalizer {} to {}", finalizer, name);
34 Ok(())
35}
36
37pub async fn remove_finalizer<K>(client: &Client, resource: &K, finalizer: &str) -> k8s_operator_core::Result<()>
38where
39 K: Resource + Clone + std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync + 'static,
40 K::DynamicType: Default,
41{
42 let name = resource.meta().name.clone().ok_or_else(|| {
43 OperatorError::Internal("Resource has no name".to_string())
44 })?;
45
46 let api: Api<K> = Api::all(client.clone());
47
48 let current_finalizers: Vec<String> = resource
49 .meta()
50 .finalizers
51 .clone()
52 .unwrap_or_default()
53 .into_iter()
54 .filter(|f| f != finalizer)
55 .collect();
56
57 let patch = json!({
58 "metadata": {
59 "finalizers": current_finalizers
60 }
61 });
62
63 api.patch(&name, &PatchParams::apply("k8s-operator"), &Patch::Merge(&patch))
64 .await
65 .map_err(|e| OperatorError::KubeError(e))?;
66
67 info!("Removed finalizer {} from {}", finalizer, name);
68 Ok(())
69}
70
71pub fn has_finalizer<K>(resource: &K, finalizer: &str) -> bool
72where
73 K: Resource,
74{
75 resource
76 .meta()
77 .finalizers
78 .as_ref()
79 .map(|f| f.contains(&finalizer.to_string()))
80 .unwrap_or(false)
81}
82
83pub fn is_being_deleted<K>(resource: &K) -> bool
84where
85 K: Resource,
86{
87 resource.meta().deletion_timestamp.is_some()
88}
89
90pub struct FinalizerGuard<'a, K>
91where
92 K: Resource + Clone + std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync + 'static,
93 K::DynamicType: Default,
94{
95 client: &'a Client,
96 resource: &'a K,
97 finalizer: String,
98}
99
100impl<'a, K> FinalizerGuard<'a, K>
101where
102 K: Resource + Clone + std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync + 'static,
103 K::DynamicType: Default,
104{
105 pub fn new(client: &'a Client, resource: &'a K) -> Self {
106 Self {
107 client,
108 resource,
109 finalizer: DEFAULT_FINALIZER.to_string(),
110 }
111 }
112
113 pub fn with_finalizer(mut self, finalizer: impl Into<String>) -> Self {
114 self.finalizer = finalizer.into();
115 self
116 }
117
118 pub async fn ensure(&self) -> k8s_operator_core::Result<()> {
119 if !has_finalizer(self.resource, &self.finalizer) {
120 add_finalizer(self.client, self.resource, &self.finalizer).await?;
121 }
122 Ok(())
123 }
124
125 pub fn is_being_deleted(&self) -> bool {
126 is_being_deleted(self.resource)
127 }
128
129 pub fn has_finalizer(&self) -> bool {
130 has_finalizer(self.resource, &self.finalizer)
131 }
132
133 pub async fn cleanup_complete(&self) -> k8s_operator_core::Result<()> {
134 if has_finalizer(self.resource, &self.finalizer) {
135 remove_finalizer(self.client, self.resource, &self.finalizer).await?;
136 }
137 Ok(())
138 }
139}