1use std::collections::BTreeMap;
45use std::fmt::Debug;
46use std::future::Future;
47use std::pin::Pin;
48use std::sync::atomic::{AtomicU64, Ordering};
49use std::sync::{Arc, Mutex, OnceLock};
50use std::time::Duration;
51
52use futures_util::StreamExt;
53use kube::api::{Api, Patch, PatchParams};
54use kube::runtime::events::{Event as KubeEvent, EventType, Recorder, Reporter};
55use kube::runtime::finalizer::{finalizer, Event as FinalizerEvent};
56use kube::runtime::watcher;
57use kube::{Resource, ResourceExt};
58use serde::de::DeserializeOwned;
59use serde::Serialize;
60
61pub use kube::runtime::controller::Action;
62
63use super::manifests::Rule;
64use super::Error;
65
66type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
67
68pub struct Context {
71 pub client: kube::Client,
72 pub operator: String,
73 recorder: Recorder,
74}
75
76impl Context {
77 fn new(client: kube::Client, operator: &str) -> Self {
78 let reporter = Reporter { controller: operator.to_string(), instance: Some(super::client::identity()) };
79 Context { recorder: Recorder::new(client.clone(), reporter), client, operator: operator.to_string() }
80 }
81
82 pub async fn patch_status<K>(&self, obj: &K, status: serde_json::Value) -> Result<K, Error>
84 where
85 K: Resource<DynamicType = ()> + Clone + DeserializeOwned + Debug,
86 K::Scope: Scope,
87 {
88 let api = api_for(&self.client, obj);
89 let patch = serde_json::json!({ "status": status });
90 Ok(api.patch_status(&obj.name_any(), &PatchParams::default(), &Patch::Merge(&patch)).await?)
91 }
92
93 pub async fn apply_owned<O, C>(&self, owner: &O, mut child: C) -> Result<C, Error>
97 where
98 O: Resource<DynamicType = ()>,
99 C: Resource<DynamicType = ()> + Clone + DeserializeOwned + Serialize + Debug,
100 C::Scope: Scope,
101 {
102 let reference = owner.controller_owner_ref(&()).ok_or_else(|| Error::Other("the owner has no uid yet".into()))?;
103 child.meta_mut().owner_references = Some(vec![reference]);
104 if child.meta().namespace.is_none() {
105 child.meta_mut().namespace = owner.meta().namespace.clone();
106 }
107 let api = api_for(&self.client, &child);
108 let name = child.name_any();
109 let params = PatchParams::apply(&self.operator).force();
110 Ok(api.patch(&name, ¶ms, &Patch::Apply(&child)).await?)
111 }
112
113 pub async fn event<K: Resource<DynamicType = ()>>(&self, obj: &K, warning: bool, reason: &str, note: &str) -> Result<(), Error> {
115 let event = KubeEvent {
116 type_: if warning { EventType::Warning } else { EventType::Normal },
117 reason: reason.to_string(),
118 note: Some(note.to_string()),
119 action: reason.to_string(),
120 secondary: None,
121 };
122 Ok(self.recorder.publish(&event, &obj.object_ref(&())).await?)
123 }
124}
125
126use super::{api_for, Scope};
127
128type Reconcile<K> = Arc<dyn Fn(Arc<K>, Arc<Context>) -> BoxFuture<Result<Action, Error>> + Send + Sync>;
129type Cleanup<K> = Arc<dyn Fn(Arc<K>, Arc<Context>) -> BoxFuture<Result<(), Error>> + Send + Sync>;
130type Owns<K> = Box<dyn FnOnce(kube::runtime::Controller<K>, &kube::Client) -> kube::runtime::Controller<K> + Send>;
131
132pub struct Controller<K: Resource<DynamicType = ()> + Clone + Debug + 'static> {
135 api: Api<K>,
136 config: watcher::Config,
137 owns: Vec<Owns<K>>,
138 reconcile: Option<Reconcile<K>>,
139 cleanup: Option<(String, Cleanup<K>)>,
140 error_delay: Duration,
141 rules: Vec<Rule>,
142}
143
144pub fn group_and_plural<K: Resource<DynamicType = ()>>() -> (String, String) {
146 (K::group(&()).to_string(), K::plural(&()).to_string())
147}
148
149impl<K> Controller<K>
150where
151 K: Resource<DynamicType = ()> + Clone + DeserializeOwned + Serialize + Debug + Send + Sync + 'static,
152 K::Scope: Scope,
153{
154 pub fn new(api: Api<K>) -> Self {
156 let (group, plural) = group_and_plural::<K>();
157 let rules = vec![
158 Rule::new(&group, &[&plural], &["get", "list", "watch", "patch", "update"]),
159 Rule::new(&group, &[&format!("{plural}/status")], &["get", "patch", "update"]),
160 Rule::new("events.k8s.io", &["events"], &["create", "patch"]),
161 ];
162 Controller { api, config: watcher::Config::default(), owns: Vec::new(), reconcile: None, cleanup: None, error_delay: Duration::from_secs(5), rules }
163 }
164
165 pub fn watching(mut self, config: watcher::Config) -> Self {
167 self.config = config;
168 self
169 }
170
171 pub fn owns<C>(mut self, api: Api<C>) -> Self
174 where
175 C: Resource<DynamicType = ()> + Clone + DeserializeOwned + Debug + Send + Sync + 'static,
176 {
177 let (group, plural) = group_and_plural::<C>();
178 self.rules.push(Rule::new(&group, &[&plural], &["get", "list", "watch", "create", "update", "patch", "delete"]));
179 self.owns.push(Box::new(move |c, _| c.owns(api, watcher::Config::default())));
180 self
181 }
182
183 pub fn reconcile<F, Fut>(mut self, f: F) -> Self
186 where
187 F: Fn(Arc<K>, Arc<Context>) -> Fut + Send + Sync + 'static,
188 Fut: Future<Output = Result<Action, Error>> + Send + 'static,
189 {
190 self.reconcile = Some(Arc::new(move |obj, ctx| Box::pin(f(obj, ctx))));
191 self
192 }
193
194 pub fn finalizer<F, Fut>(mut self, name: &str, cleanup: F) -> Self
198 where
199 F: Fn(Arc<K>, Arc<Context>) -> Fut + Send + Sync + 'static,
200 Fut: Future<Output = Result<(), Error>> + Send + 'static,
201 {
202 self.cleanup = Some((name.to_string(), Arc::new(move |obj, ctx| Box::pin(cleanup(obj, ctx)))));
203 self
204 }
205
206 pub fn on_error(mut self, first_delay: Duration) -> Self {
209 self.error_delay = first_delay.max(Duration::from_millis(10));
210 self
211 }
212
213 pub fn rule(mut self, rule: Rule) -> Self {
215 self.rules.push(rule);
216 self
217 }
218}
219
220#[derive(Default)]
222struct Counters {
223 reconciles: AtomicU64,
224 errors: AtomicU64,
225}
226
227fn counters() -> &'static Mutex<BTreeMap<String, Arc<Counters>>> {
228 static C: OnceLock<Mutex<BTreeMap<String, Arc<Counters>>>> = OnceLock::new();
229 C.get_or_init(|| Mutex::new(BTreeMap::new()))
230}
231
232fn counters_for(kind: &str) -> Arc<Counters> {
233 rws_server::metrics::register_section(rws_server::metrics::section::CONTROLLERS, metrics_text);
234 Arc::clone(counters().lock().unwrap_or_else(|e| e.into_inner()).entry(kind.to_string()).or_default())
235}
236
237pub fn metrics_text() -> String {
240 let all = counters().lock().unwrap_or_else(|e| e.into_inner());
241 if all.is_empty() {
242 return String::new();
243 }
244 let mut out = String::from("# HELP rws_controller_reconciles_total Reconciles run, by controller.\n# TYPE rws_controller_reconciles_total counter\n");
245 for (kind, c) in all.iter() {
246 out.push_str(&format!("rws_controller_reconciles_total{{controller=\"{kind}\"}} {}\n", c.reconciles.load(Ordering::Relaxed)));
247 }
248 out.push_str("# HELP rws_controller_reconcile_errors_total Reconciles that failed, by controller.\n# TYPE rws_controller_reconcile_errors_total counter\n");
249 for (kind, c) in all.iter() {
250 out.push_str(&format!("rws_controller_reconcile_errors_total{{controller=\"{kind}\"}} {}\n", c.errors.load(Ordering::Relaxed)));
251 }
252 out
253}
254
255pub fn backoff(first: Duration, failures: u32) -> Duration {
258 let factor = 1u64 << failures.saturating_sub(1).min(16);
259 first.saturating_mul(factor as u32).min(Duration::from_secs(300))
260}
261
262pub trait Runnable: Send {
264 fn kind(&self) -> String;
265 fn rules(&self) -> Vec<Rule>;
266 fn start(self: Box<Self>, client: kube::Client, ctx: Arc<Context>) -> BoxFuture<()>;
267}
268
269#[derive(Debug)]
270struct Failure(Error);
271
272impl std::fmt::Display for Failure {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 write!(f, "{}", self.0)
275 }
276}
277
278impl std::error::Error for Failure {}
279
280impl<K> Runnable for Controller<K>
281where
282 K: Resource<DynamicType = ()> + Clone + DeserializeOwned + Serialize + Debug + Send + Sync + 'static,
283 K::Scope: Scope,
284{
285 fn kind(&self) -> String {
286 K::kind(&()).to_string()
287 }
288
289 fn rules(&self) -> Vec<Rule> {
290 self.rules.clone()
291 }
292
293 fn start(self: Box<Self>, client: kube::Client, ctx: Arc<Context>) -> BoxFuture<()> {
294 let this = *self;
295 let kind = K::kind(&()).to_string();
296 let counters = counters_for(&kind);
297 let Some(reconcile) = this.reconcile else {
298 eprintln!("[k8s] controller {kind}: no reconcile function; not started");
299 return Box::pin(async {});
300 };
301 let cleanup = this.cleanup;
302 let error_delay = this.error_delay;
303 let failures: Arc<Mutex<BTreeMap<String, u32>>> = Arc::new(Mutex::new(BTreeMap::new()));
304 let mut controller = kube::runtime::Controller::new(this.api, this.config);
305 for owns in this.owns {
306 controller = owns(controller, &client);
307 }
308 let reconcile_counters = Arc::clone(&counters);
309 let reconcile_failures = Arc::clone(&failures);
310 let reconciler = move |obj: Arc<K>, ctx: Arc<Context>| {
311 let (reconcile, cleanup) = (Arc::clone(&reconcile), cleanup.clone());
312 let (counters, failures) = (Arc::clone(&reconcile_counters), Arc::clone(&reconcile_failures));
313 async move {
314 counters.reconciles.fetch_add(1, Ordering::Relaxed);
315 let key = format!("{}/{}", obj.namespace().unwrap_or_default(), obj.name_any());
316 let result = match cleanup {
317 None => reconcile(obj, ctx).await,
318 Some((name, cleanup)) => {
319 let object_api = api_for(&ctx.client, &*obj);
320 finalizer(&object_api, &name, obj, |event| async {
321 match event {
322 FinalizerEvent::Apply(o) => reconcile(o, Arc::clone(&ctx)).await.map_err(Failure),
323 FinalizerEvent::Cleanup(o) => cleanup(o, Arc::clone(&ctx)).await.map(|_| Action::await_change()).map_err(Failure),
324 }
325 })
326 .await
327 .map_err(|e| Error::Other(e.to_string()))
328 }
329 };
330 if result.is_ok() {
331 failures.lock().unwrap_or_else(|e| e.into_inner()).remove(&key);
332 }
333 result.map_err(Failure)
334 }
335 };
336 let error_counters = Arc::clone(&counters);
337 let error_policy = move |obj: Arc<K>, error: &Failure, _ctx: Arc<Context>| {
338 error_counters.errors.fetch_add(1, Ordering::Relaxed);
339 let key = format!("{}/{}", obj.namespace().unwrap_or_default(), obj.name_any());
340 let n = {
341 let mut f = failures.lock().unwrap_or_else(|e| e.into_inner());
342 let n = f.entry(key.clone()).or_insert(0);
343 *n += 1;
344 *n
345 };
346 let delay = backoff(error_delay, n);
347 eprintln!("[k8s] reconcile {} {key} failed ({error}); retry in {delay:?}", K::kind(&()));
348 Action::requeue(delay)
349 };
350 Box::pin(async move {
351 controller
352 .run(reconciler, error_policy, ctx)
353 .for_each(|result| async move {
354 if let Err(e) = result {
355 eprintln!("[k8s] controller: {e}");
358 }
359 })
360 .await;
361 })
362 }
363}
364
365pub struct Operator {
368 client: kube::Client,
369 name: String,
370 controllers: Vec<Box<dyn Runnable>>,
371 lease: Option<(String, String)>,
372}
373
374impl Operator {
375 pub fn new(client: kube::Client, name: &str) -> Self {
377 Operator { client, name: name.to_string(), controllers: Vec::new(), lease: None }
378 }
379
380 pub fn controller(mut self, controller: impl Runnable + 'static) -> Self {
381 self.controllers.push(Box::new(controller));
382 self
383 }
384
385 pub fn leader_election(mut self, namespace: &str, name: &str) -> Self {
388 self.lease = Some((namespace.to_string(), name.to_string()));
389 self
390 }
391
392 pub fn rules(&self) -> Vec<Rule> {
394 let mut rules: Vec<Rule> = self.controllers.iter().flat_map(|c| c.rules()).collect();
395 if self.lease.is_some() {
396 rules.push(Rule::new("coordination.k8s.io", &["leases"], &["get", "create", "update"]));
397 }
398 super::manifests::merge_rules(rules)
399 }
400
401 pub fn kinds(&self) -> Vec<String> {
403 self.controllers.iter().map(|c| c.kind()).collect()
404 }
405
406 pub async fn run(self) {
410 let ctx = Arc::new(Context::new(self.client.clone(), &self.name));
411 rws_server::health::readiness(&format!("operator {}", self.name), || Ok(()));
412 match self.lease {
413 None => run_all(self.controllers, self.client, ctx).await,
414 Some((namespace, lease)) => {
415 let elector = super::leader::LeaderElector::new(self.client.clone(), &namespace, &lease);
416 let mut handle = elector.handle();
417 tokio::spawn(elector.run());
418 let mut controllers = Some(self.controllers);
419 loop {
420 handle.wait_until_leader().await;
421 let Some(all) = controllers.take() else {
425 eprintln!("[k8s] operator {}: leadership lost; exiting so the pod restarts as a follower", self.name);
426 std::process::exit(1);
427 };
428 println!("[k8s] operator {}: leading; starting {} controller(s)", self.name, all.len());
429 let running = run_all(all, self.client.clone(), Arc::clone(&ctx));
430 tokio::pin!(running);
431 loop {
432 tokio::select! {
433 _ = &mut running => return,
434 leading = handle.changed() => if !leading { break },
435 }
436 }
437 }
438 }
439 }
440 }
441}
442
443async fn run_all(controllers: Vec<Box<dyn Runnable>>, client: kube::Client, ctx: Arc<Context>) {
444 let futures: Vec<BoxFuture<()>> = controllers.into_iter().map(|c| c.start(client.clone(), Arc::clone(&ctx))).collect();
445 futures_util::future::join_all(futures).await;
446}
447
448#[cfg(test)]
449mod tests;