1use super::{
2 ControllerId, KanidmClients, idm_reconcile_interval,
3 kanidm::{ClientLockKey, KanidmKey, KanidmResource, KanidmUser},
4};
5
6use crate::kanidm::crd::Kanidm;
7use crate::metrics::ControllerMetrics;
8use kaniop_k8s_util::error::{Error, Result};
9
10use kaniop_k8s_util::types::short_type_name;
11
12use std::collections::HashMap;
13use std::sync::Arc;
14
15use backon::{BackoffBuilder, ExponentialBackoff, ExponentialBuilder};
16use k8s_openapi::{NamespaceResourceScope, api::core::v1::Namespace};
17use kanidm_client::KanidmClient;
18use kube::runtime::events::{Event, EventType, Recorder};
19use kube::{Api, client::Client};
20use kube::{Resource, ResourceExt};
21use kube::{
22 api::{Patch, PatchParams},
23 runtime::reflector::{Lookup, ObjectRef, Store},
24};
25use serde::{Deserialize, Serialize};
26use tokio::sync::{Mutex, RwLock};
27use tokio::time::Duration;
28use tracing::{debug, error, info, trace};
29
30#[derive(Clone)]
32pub struct Context<K: Resource> {
33 pub controller_id: ControllerId,
35 pub client: Client,
37 pub metrics: Arc<ControllerMetrics>,
39 error_backoff_cache: Arc<RwLock<HashMap<ObjectRef<K>, RwLock<ExponentialBackoff>>>>,
41 pub recorder: Recorder,
43 pub namespace_store: Store<Namespace>,
45 pub kanidm_store: Store<Kanidm>,
47 idm_clients: Arc<RwLock<KanidmClients>>,
49 system_clients: Arc<RwLock<KanidmClients>>,
52 client_creation_locks: Arc<RwLock<HashMap<ClientLockKey, Arc<Mutex<()>>>>>,
54}
55
56impl<K> Context<K>
57where
58 K: Resource + ResourceExt + Lookup + Clone + 'static,
59 <K as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
60{
61 #[allow(clippy::too_many_arguments)]
62 pub fn new(
63 controller_id: ControllerId,
64 client: Client,
65 metrics: Arc<ControllerMetrics>,
66 recorder: Recorder,
67 idm_clients: Arc<RwLock<KanidmClients>>,
68 system_clients: Arc<RwLock<KanidmClients>>,
69 namespace_store: Store<Namespace>,
70 kanidm_store: Store<Kanidm>,
71 ) -> Self {
72 Self {
73 controller_id,
74 client,
75 metrics,
76 recorder,
77 namespace_store,
78 kanidm_store,
79 idm_clients,
80 system_clients,
81 error_backoff_cache: Arc::default(),
82 client_creation_locks: Arc::default(),
83 }
84 }
85
86 pub async fn release_kanidm_clients(&self, kanidm: &Kanidm) {
87 let key = KanidmKey {
88 namespace: kube::ResourceExt::namespace(kanidm).unwrap(),
90 name: kanidm.name_any(),
91 };
92
93 self.idm_clients.write().await.remove(&key);
94 self.system_clients.write().await.remove(&key);
95 {
96 let mut locks = self.client_creation_locks.write().await;
97 locks.remove(&ClientLockKey {
98 namespace: key.namespace.clone(),
99 name: key.name.clone(),
100 user: KanidmUser::IdmAdmin,
101 });
102 locks.remove(&ClientLockKey {
103 namespace: key.namespace.clone(),
104 name: key.name.clone(),
105 user: KanidmUser::Admin,
106 });
107 }
108 }
109}
110
111impl<K> Context<K>
112where
113 K: Resource<DynamicType = ()> + ResourceExt + KanidmResource + Lookup + Clone + 'static,
114 <K as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
115{
116 async fn get_valid_cached_client(
118 cache: &Arc<RwLock<KanidmClients>>,
119 key: &KanidmKey,
120 namespace: &str,
121 name: &str,
122 ) -> Option<Arc<KanidmClient>> {
123 let client = cache.read().await.get(key).cloned()?;
124
125 trace!(
126 msg = "check existing Kanidm client session",
127 namespace, name
128 );
129 if client.auth_valid().await.is_ok() {
130 trace!(msg = "reuse Kanidm client session", namespace, name);
131 Some(client)
132 } else {
133 None
134 }
135 }
136
137 async fn get_kanidm_client(&self, obj: &K, user: KanidmUser) -> Result<Arc<KanidmClient>> {
140 let namespace = obj.kanidm_namespace();
141 let name = obj.kanidm_name();
142 debug!(msg = "get Kanidm client", namespace, name);
143
144 let cache = match user {
145 KanidmUser::Admin => self.system_clients.clone(),
146 KanidmUser::IdmAdmin => self.idm_clients.clone(),
147 };
148 let key = KanidmKey {
149 namespace: namespace.clone(),
150 name: name.clone(),
151 };
152 if let Some(client) = Self::get_valid_cached_client(&cache, &key, &namespace, &name).await {
153 return Ok(client);
154 }
155
156 let creation_lock = self
158 .client_creation_locks
159 .write()
160 .await
161 .entry(ClientLockKey {
162 namespace: namespace.clone(),
163 name: name.clone(),
164 user: user.clone(),
165 })
166 .or_insert_with(Arc::default)
167 .clone();
168 let _guard = creation_lock.lock().await;
169
170 if let Some(client) = Self::get_valid_cached_client(&cache, &key, &namespace, &name).await {
172 return Ok(client);
173 }
174
175 match KanidmClients::create_client(&namespace, &name, user, self.client.clone()).await {
176 Ok(client) => {
177 cache.write().await.insert(key.clone(), client.clone());
178 Ok(client)
179 }
180 Err(e) => {
181 self.recorder
182 .publish(
183 &Event {
184 type_: EventType::Warning,
185 reason: "KanidmClientError".to_string(),
186 note: Some(e.to_string()),
187 action: "KanidmClientCreating".into(),
188 secondary: None,
189 },
190 &obj.object_ref(&()),
191 )
192 .await
193 .map_err(|e| {
194 error!(msg = "failed to create Kanidm client", %e);
195 Error::KubeError("failed to publish event".to_string(), Box::new(e))
196 })?;
197 Err(e)
198 }
199 }
200 }
201
202 pub fn get_kanidm(&self, obj: &K) -> Option<Arc<Kanidm>> {
206 let namespace = obj.kanidm_namespace();
207 let name = obj.kanidm_name();
208 self.kanidm_store.find(|k| {
209 kube::ResourceExt::namespace(k).as_ref() == Some(&namespace) && k.name_any() == name
210 })
211 }
212}
213
214#[allow(async_fn_in_trait)]
215pub trait BackoffContext<K: Resource> {
216 fn metrics(&self) -> &Arc<ControllerMetrics>;
217 async fn get_backoff(&self, obj_ref: ObjectRef<K>) -> Duration;
218 async fn reset_backoff(&self, obj_ref: ObjectRef<K>);
219}
220
221impl<K> BackoffContext<K> for Context<K>
222where
223 K: Resource<DynamicType = ()> + ResourceExt + Lookup + Clone + 'static,
224 <K as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
225{
226 fn metrics(&self) -> &Arc<ControllerMetrics> {
227 &self.metrics
228 }
229
230 async fn get_backoff(&self, obj_ref: ObjectRef<K>) -> Duration {
232 {
233 let read_guard = self.error_backoff_cache.read().await;
234 if let Some(backoff) = read_guard.get(&obj_ref) {
235 if let Some(duration) = backoff.write().await.next() {
236 return duration;
237 }
238 }
239 }
240
241 let mut backoff = ExponentialBuilder::default()
243 .with_max_delay(idm_reconcile_interval())
244 .without_max_times()
245 .build();
246 let duration = backoff.next().unwrap_or_else(|| {
248 trace!("backoff returned None, using default duration");
249 Duration::from_secs(1)
250 });
251 self.error_backoff_cache
252 .write()
253 .await
254 .insert(obj_ref.clone(), RwLock::new(backoff));
255 trace!(
256 msg = format!("recreate backoff policy"),
257 namespace = obj_ref.namespace.as_deref().unwrap(),
258 name = obj_ref.name,
259 );
260 duration
261 }
262
263 async fn reset_backoff(&self, obj_ref: ObjectRef<K>) {
265 let read_guard = self.error_backoff_cache.read().await;
266 if read_guard.get(&obj_ref).is_some() {
267 drop(read_guard);
268 trace!(
269 msg = "reset backoff policy",
270 namespace = obj_ref.namespace.as_deref().unwrap(),
271 name = obj_ref.name
272 );
273 self.error_backoff_cache.write().await.remove(&obj_ref);
274 }
275 }
276}
277
278#[allow(async_fn_in_trait)]
279pub trait IdmClientContext<K: Resource> {
280 async fn get_idm_client(&self, obj: &K) -> Result<Arc<KanidmClient>>;
281}
282
283impl<K> IdmClientContext<K> for Context<K>
284where
285 K: Resource<DynamicType = ()> + ResourceExt + KanidmResource + Lookup + Clone + 'static,
286 <K as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
287{
288 async fn get_idm_client(&self, obj: &K) -> Result<Arc<KanidmClient>> {
289 self.get_kanidm_client(obj, KanidmUser::IdmAdmin).await
290 }
291}
292
293#[allow(async_fn_in_trait)]
294pub trait SystemClientContext<K: Resource> {
295 async fn get_system_client(&self, obj: &K) -> Result<Arc<KanidmClient>>;
296}
297
298impl<K> SystemClientContext<K> for Context<K>
299where
300 K: Resource<DynamicType = ()> + ResourceExt + KanidmResource + Lookup + Clone + 'static,
301 <K as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
302{
303 async fn get_system_client(&self, obj: &K) -> Result<Arc<KanidmClient>> {
304 self.get_kanidm_client(obj, KanidmUser::Admin).await
305 }
306}
307
308#[allow(async_fn_in_trait)]
309pub trait KubeOperations<T, K>
310where
311 T: Resource + ResourceExt + Lookup + Clone + 'static,
312 <T as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
313 K: Resource<Scope = NamespaceResourceScope>
314 + Serialize
315 + Clone
316 + std::fmt::Debug
317 + for<'de> Deserialize<'de>,
318 <K as kube::Resource>::DynamicType: Default,
319 <K as Resource>::Scope: std::marker::Sized,
320{
321 async fn kube_delete(&self, client: Client, metrics: &ControllerMetrics, obj: &K)
322 -> Result<()>;
323 async fn kube_patch(
324 &self,
325 client: Client,
326 metrics: &ControllerMetrics,
327 obj: K,
328 operator_name: &str,
329 ) -> Result<K>;
330}
331
332impl<T, K> KubeOperations<T, K> for T
333where
334 T: Resource + ResourceExt + Lookup + Clone + 'static,
335 <T as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
336 K: Resource<Scope = NamespaceResourceScope>
337 + Serialize
338 + Clone
339 + std::fmt::Debug
340 + for<'de> Deserialize<'de>,
341 <K as kube::Resource>::DynamicType: Default,
342 <K as Resource>::Scope: std::marker::Sized,
343{
344 async fn kube_delete(
345 &self,
346 client: Client,
347 _metrics: &ControllerMetrics,
348 obj: &K,
349 ) -> Result<()> {
350 let name = obj.name_any();
351 let namespace = kube::ResourceExt::namespace(self).unwrap();
353 trace!(
354 msg = format!("deleting {}", short_type_name::<K>().unwrap_or("Unknown")),
355 resource.name = &name,
356 resource.namespace = &namespace
357 );
358 let api = Api::<K>::namespaced(client, &namespace);
359 api.delete(&name, &Default::default()).await.map_err(|e| {
360 Error::KubeError(
361 format!(
362 "failed to delete {} {namespace}/{name}",
363 short_type_name::<K>().unwrap_or("Unknown")
364 ),
365 Box::new(e),
366 )
367 })?;
368 Ok(())
369 }
370
371 async fn kube_patch(
372 &self,
373 client: Client,
374 metrics: &ControllerMetrics,
375 obj: K,
376 operator_name: &str,
377 ) -> Result<K> {
378 let name = obj.name_any();
379 let namespace = kube::ResourceExt::namespace(self).unwrap();
381 trace!(
382 msg = format!("patching {}", short_type_name::<K>().unwrap_or("Unknown")),
383 resource.name = &name,
384 resource.namespace = &namespace
385 );
386 let resource_api = Api::<K>::namespaced(client.clone(), &namespace);
387
388 let result = resource_api
389 .patch(
390 &name,
391 &PatchParams::apply(operator_name).force(),
392 &Patch::Apply(&obj),
393 )
394 .await;
395 match result {
396 Ok(resource) => Ok(resource),
397 Err(e) => match e {
398 kube::Error::Api(ae) if ae.code == 422 => {
399 info!(
400 msg = format!(
401 "recreating {} because the update operation was not possible",
402 short_type_name::<K>().unwrap_or("Unknown")
403 ),
404 reason = ae.reason
405 );
406 trace!(msg = "operation was not possible because of 422", ?ae);
407 self.kube_delete(client.clone(), metrics, &obj).await?;
408 metrics.reconcile_deploy_delete_create_inc();
409 resource_api
410 .patch(
411 &name,
412 &PatchParams::apply(operator_name).force(),
413 &Patch::Apply(&obj),
414 )
415 .await
416 .map_err(|e| {
417 Error::KubeError(
418 format!(
419 "failed to re-try patch {} {namespace}/{name}",
420 short_type_name::<K>().unwrap_or("Unknown")
421 ),
422 Box::new(e),
423 )
424 })
425 }
426 _ => Err(Error::KubeError(
427 format!(
428 "failed to patch {} {namespace}/{name}",
429 short_type_name::<K>().unwrap_or("Unknown")
430 ),
431 Box::new(e),
432 )),
433 },
434 }
435 }
436}