kaniop_operator/controller/
mod.rs1pub mod context;
2pub mod kanidm;
3
4use self::{context::Context, kanidm::KanidmClients};
5
6use crate::kanidm::crd::Kanidm;
7use crate::metrics;
8use crate::prometheus_exporter;
9
10use kaniop_k8s_util::error::{Error, Result};
11
12use kaniop_k8s_util::types::short_type_name;
13
14use std::fmt::Debug;
15use std::sync::Arc;
16
17use futures::channel::mpsc;
18use futures::future::BoxFuture;
19use futures::{FutureExt, StreamExt};
20use k8s_openapi::api::core::v1::Namespace;
21use kube::Resource;
22use kube::api::{Api, ListParams, PartialObjectMeta, ResourceExt};
23use kube::client::Client;
24use kube::runtime::controller::Action;
25use kube::runtime::events::Recorder;
26use kube::runtime::reflector::store::Writer;
27use kube::runtime::reflector::{self, Lookup, ReflectHandle, Store};
28use kube::runtime::{WatchStreamExt, metadata_watcher, watcher};
29use serde::de::DeserializeOwned;
30use tokio::sync::RwLock;
31use tokio::time::Duration;
32use tracing::{debug, error, info, trace};
33
34use std::sync::OnceLock;
35
36const DEFAULT_IDM_RECONCILE_INTERVAL_SECS: u64 = 60;
37const DEFAULT_CLUSTER_DOMAIN: &str = "cluster.local";
38
39static IDM_RECONCILE_INTERVAL: OnceLock<Duration> = OnceLock::new();
40static CLUSTER_DOMAIN: OnceLock<String> = OnceLock::new();
41
42pub fn set_idm_reconcile_interval(duration: Duration) {
43 let _ = IDM_RECONCILE_INTERVAL.set(duration);
44}
45
46pub fn idm_reconcile_interval() -> Duration {
47 *IDM_RECONCILE_INTERVAL.get_or_init(|| Duration::from_secs(DEFAULT_IDM_RECONCILE_INTERVAL_SECS))
48}
49
50pub fn set_cluster_domain(domain: String) {
51 let _ = CLUSTER_DOMAIN.set(domain);
52}
53
54pub fn cluster_domain() -> &'static str {
55 CLUSTER_DOMAIN.get_or_init(|| DEFAULT_CLUSTER_DOMAIN.to_string())
56}
57pub const SUBSCRIBE_BUFFER_SIZE: usize = 256;
58pub const RELOAD_BUFFER_SIZE: usize = 16;
59pub const NAME_LABEL: &str = "app.kubernetes.io/name";
60pub const INSTANCE_LABEL: &str = "app.kubernetes.io/instance";
61pub const MANAGED_BY_LABEL: &str = "app.kubernetes.io/managed-by";
62
63pub type ControllerId = &'static str;
64
65#[derive(Clone)]
68pub struct State {
69 metrics: Arc<metrics::Metrics>,
71 idm_clients: Arc<RwLock<KanidmClients>>,
73 system_clients: Arc<RwLock<KanidmClients>>,
76 pub namespace_store: Store<Namespace>,
78 pub kanidm_store: Store<Kanidm>,
80 pub client: Option<Client>,
82}
83
84pub struct ResourceReflector<K>
86where
87 K: Resource + Lookup + Clone + 'static,
88 <K as Lookup>::DynamicType: Eq + std::hash::Hash + Clone,
89{
90 pub store: Store<K>,
91 pub writer: Writer<K>,
92 pub subscriber: ReflectHandle<K>,
93}
94
95impl State {
97 pub fn new(
98 metrics: metrics::Metrics,
99 namespace_store: Store<Namespace>,
100 kanidm_store: Store<Kanidm>,
101 client: Option<Client>,
102 ) -> Self {
103 Self {
104 metrics: Arc::new(metrics),
105 idm_clients: Arc::default(),
106 system_clients: Arc::default(),
107 namespace_store,
108 kanidm_store,
109 client,
110 }
111 }
112
113 pub fn metrics(&self) -> Result<String> {
115 prometheus_exporter::format_prometheus_metrics("kaniop").map_err(|e| {
116 Error::FormattingError(format!("failed to export metrics: {}", e), std::fmt::Error)
117 })
118 }
119
120 pub fn to_context<K>(&self, client: Client, controller_id: ControllerId) -> Context<K>
122 where
123 K: Resource + Lookup + Clone + 'static,
124 <K as Lookup>::DynamicType: Default + Eq + std::hash::Hash + Clone,
125 {
126 Context::new(
127 controller_id,
128 client.clone(),
129 self.metrics
130 .controllers
131 .get(controller_id)
132 .expect("all CONTROLLER_IDs have to be registered")
133 .clone(),
134 Recorder::new(client.clone(), controller_id.into()),
135 self.idm_clients.clone(),
136 self.system_clients.clone(),
137 self.namespace_store.clone(),
138 self.kanidm_store.clone(),
139 )
140 }
141}
142
143pub async fn check_api_queryable<K>(client: Client) -> Api<K>
144where
145 K: Resource + Clone + DeserializeOwned + Debug,
146 <K as Resource>::DynamicType: Default,
147{
148 let api = Api::<K>::all(client.clone());
149 if let Err(e) = api.list(&ListParams::default().limit(1)).await {
150 error!(
151 "{} is not queryable; {e:?}. Check controller permissions",
152 short_type_name::<K>().unwrap_or("Unknown resource"),
153 );
154 std::process::exit(1);
155 }
156 api
157}
158
159pub async fn check_api_queryable_optional<K>(client: Client) -> Option<Api<K>>
160where
161 K: Resource + Clone + DeserializeOwned + Debug,
162 <K as Resource>::DynamicType: Default,
163{
164 let api = Api::<K>::all(client.clone());
165 if let Err(e) = api.list(&ListParams::default().limit(1)).await {
166 info!(
167 "{} is not queryable (optional resource); {e:?}. Skipping optional resource support.",
168 short_type_name::<K>().unwrap_or("Unknown resource"),
169 );
170 None
171 } else {
172 Some(api)
173 }
174}
175
176pub fn create_subscriber<K>(buffer_size: usize) -> ResourceReflector<K>
177where
178 K: Resource + Lookup + Clone + 'static,
179 <K as Lookup>::DynamicType: Default + Eq + std::hash::Hash + Clone,
180 <K as Resource>::DynamicType: Default + Eq + std::hash::Hash + Clone,
181{
182 let (store, writer) = reflector::store_shared::<K>(buffer_size);
183 let subscriber = writer
184 .subscribe()
185 .expect("subscribers can only be created from shared stores");
186
187 ResourceReflector {
188 store,
189 writer,
190 subscriber,
191 }
192}
193
194fn create_generic_watcher<K, W, T, S, StreamT>(
195 api: Api<K>,
196 writer: Writer<W>,
197 reload_tx: mpsc::Sender<()>,
198 controller_id: ControllerId,
199 ctx: Arc<Context<T>>,
200 stream_fn: S,
201) -> BoxFuture<'static, ()>
202where
203 K: Resource + Lookup + Clone + DeserializeOwned + Send + Sync + Debug + 'static,
204 W: Resource + ResourceExt + Lookup + Clone + Debug + Send + Sync + 'static,
205 <W as Resource>::DynamicType: Eq + std::hash::Hash + Clone + Send + Sync,
206 <K as Lookup>::DynamicType: Default + Eq + std::hash::Hash + Clone + Send + Sync,
207 <K as Resource>::DynamicType: Default + Eq + std::hash::Hash + Clone,
208 <W as Lookup>::DynamicType: Eq + std::hash::Hash + Clone + Send + Sync,
209 T: Resource<DynamicType = ()> + ResourceExt + Lookup + Clone + 'static,
210 <T as Lookup>::DynamicType: Eq + std::hash::Hash + Clone + Send + Sync,
211 S: Fn(Api<K>, watcher::Config) -> StreamT + 'static,
212 StreamT: futures::Stream<Item = Result<watcher::Event<W>, kube::runtime::watcher::Error>>
213 + Send
214 + 'static,
215{
216 let resource_name = short_type_name::<K>().unwrap_or("Unknown");
217
218 stream_fn(
219 api,
220 watcher::Config::default().labels(&format!("{MANAGED_BY_LABEL}=kaniop-{controller_id}")),
221 )
222 .default_backoff()
223 .reflect_shared(writer)
224 .for_each(move |res| {
225 let mut reload_tx_clone = reload_tx.clone();
226 let ctx = ctx.clone();
227 async move {
228 match res {
229 Ok(event) => {
230 trace!(msg = "watched event", ?event);
231 match event {
232 watcher::Event::Delete(d) => {
233 debug!(
234 msg = format!("delete event for {resource_name} trigger reconcile"),
235 namespace = ResourceExt::namespace(&d).unwrap(),
236 name = d.name_any()
237 );
238
239 let _ignore_errors = reload_tx_clone.try_send(()).map_err(
243 |e| error!(msg = "failed to trigger reconcile on delete", %e),
244 );
245 ctx.metrics
246 .triggered_inc(metrics::Action::Delete, resource_name);
247 }
248 watcher::Event::Apply(d) => {
249 debug!(
250 msg = format!("apply event for {resource_name} trigger reconcile"),
251 namespace = ResourceExt::namespace(&d).unwrap(),
252 name = d.name_any()
253 );
254 ctx.metrics
255 .triggered_inc(metrics::Action::Apply, resource_name);
256 }
257 _ => {}
258 }
259 }
260 Err(e) => {
261 error!(msg = format!("unexpected error when watching {resource_name}"), %e);
262 ctx.metrics.watch_operations_failed_inc();
263 }
264 }
265 }
266 })
267 .boxed()
268}
269
270pub fn create_watcher<K, T>(
271 api: Api<K>,
272 writer: Writer<K>,
273 reload_tx: mpsc::Sender<()>,
274 controller_id: ControllerId,
275 ctx: Arc<Context<T>>,
276) -> BoxFuture<'static, ()>
277where
278 K: Resource + Lookup + Clone + DeserializeOwned + Send + Sync + Debug + 'static,
279 <K as Lookup>::DynamicType: Default + Eq + std::hash::Hash + Clone + Send + Sync,
280 <K as Resource>::DynamicType: Default + Eq + std::hash::Hash + Clone,
281 T: Resource<DynamicType = ()> + ResourceExt + Lookup + Clone + 'static,
282 <T as Lookup>::DynamicType: Eq + std::hash::Hash + Clone + Send + Sync,
283{
284 create_generic_watcher::<K, K, T, _, _>(api, writer, reload_tx, controller_id, ctx, watcher)
285}
286
287pub fn create_metadata_watcher<K, T>(
288 api: Api<K>,
289 writer: Writer<PartialObjectMeta<K>>,
290 reload_tx: mpsc::Sender<()>,
291 controller_id: ControllerId,
292 ctx: Arc<Context<T>>,
293) -> BoxFuture<'static, ()>
294where
295 K: Resource + Lookup + Clone + DeserializeOwned + Send + Sync + Debug + 'static,
296 <K as Lookup>::DynamicType: Default + Eq + std::hash::Hash + Clone + Send + Sync,
297 <K as Resource>::DynamicType: Default + Eq + std::hash::Hash + Clone,
298 T: Resource<DynamicType = ()> + ResourceExt + Lookup + Clone + 'static,
299 <T as Lookup>::DynamicType: Eq + std::hash::Hash + Clone + Send + Sync,
300{
301 create_generic_watcher::<K, PartialObjectMeta<K>, T, _, _>(
302 api,
303 writer,
304 reload_tx,
305 controller_id,
306 ctx,
307 metadata_watcher,
308 )
309}
310
311pub fn error_policy<K>(_obj: Arc<K>, _error: &Error, _ctx: Arc<Context<K>>) -> Action
312where
313 K: Resource + Lookup + Clone + 'static,
314 <K as Lookup>::DynamicType: Default + Eq + std::hash::Hash + Clone,
315{
316 unreachable!("Handle in backoff_reconciler macro")
317}
318
319#[macro_export]
320macro_rules! backoff_reconciler {
321 ($inner_reconciler:ident) => {
322 |obj, ctx| async move {
323 use $crate::controller::context::BackoffContext;
324 match $inner_reconciler(obj.clone(), ctx.clone()).await {
325 Ok(action) => {
326 ctx.reset_backoff(kube::runtime::reflector::ObjectRef::from(obj.as_ref()))
327 .await;
328 Ok(action)
329 }
330 Err(error) => {
331 let namespace = kube::ResourceExt::namespace(obj.as_ref()).unwrap();
333 let name = kube::ResourceExt::name_any(obj.as_ref());
334 tracing::error!(msg = "failed reconciliation", %namespace, %name, %error);
335 ctx.metrics().reconcile_failure_inc();
336 let backoff_duration = ctx
337 .get_backoff(kube::runtime::reflector::ObjectRef::from(obj.as_ref()))
338 .await;
339 tracing::trace!(
340 msg = format!("backoff duration: {backoff_duration:?}"),
341 %namespace,
342 %name,
343 );
344 Ok(kube::runtime::controller::Action::requeue(backoff_duration))
345 }
346 }
347 }
348 };
349}