Skip to content

Commit 113fee7

Browse files
allow making controllers without finalizers
they aren't necessary if the controller is only managing other resources which have owner references set properly, and including them when not necessary can make cleaning things up more annoying (it won't be possible to delete watched resources if the controller is down, for instance)
1 parent 82f96c6 commit 113fee7

File tree

3 files changed

+53
-19
lines changed

3 files changed

+53
-19
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "k8s-controller"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
edition = "2024"
55
rust-version = "1.89.0"
66

@@ -17,6 +17,7 @@ kube = { version = "2.0.1", default-features = false, features = ["client"] }
1717
kube-runtime = "2.0.1"
1818
rand = "0.9.2"
1919
serde = "1"
20+
thiserror = "2.0.17"
2021
tracing = "0.1"
2122

2223
[dev-dependencies]

src/controller.rs

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::collections::BTreeMap;
2-
use std::error::Error;
2+
use std::error::Error as _;
33
use std::sync::{Arc, Mutex};
44
use std::time::Duration;
55

@@ -14,6 +14,14 @@ use kube_runtime::watcher;
1414
use rand::{Rng, rng};
1515
use tracing::{Level, event};
1616

17+
#[derive(Debug, thiserror::Error)]
18+
pub enum Error<E: std::error::Error + 'static> {
19+
#[error("{0}")]
20+
ControllerError(#[source] E),
21+
#[error("{0}")]
22+
FinalizerError(#[source] kube_runtime::finalizer::Error<E>),
23+
}
24+
1725
/// The [`Controller`] watches a set of resources, calling methods on the
1826
/// provided [`Context`] when events occur.
1927
pub struct Controller<Ctx: Context>
@@ -36,7 +44,6 @@ impl<Ctx: Context> Controller<Ctx>
3644
where
3745
Ctx: Send + Sync + 'static,
3846
Ctx::Error: Send + Sync + 'static,
39-
Ctx::Resource: Send + Sync + 'static,
4047
Ctx::Resource: Clone + std::fmt::Debug + serde::Serialize,
4148
for<'de> Ctx::Resource: serde::Deserialize<'de>,
4249
<Ctx::Resource as Resource>::DynamicType:
@@ -213,15 +220,18 @@ where
213220
pub trait Context {
214221
/// The type of Kubernetes [resource](Resource) that will be watched by
215222
/// the [`Controller`] this context is passed to
216-
type Resource: Resource;
223+
type Resource: Resource + Send + Sync + 'static;
217224
/// The error type which will be returned by the [`apply`](Self::apply)
218-
/// and [`cleanup`](Self::apply) methods
225+
/// and [`cleanup`](Self::cleanup) methods
219226
type Error: std::error::Error;
220227

221228
/// The name to use for the finalizer. This must be unique across
222229
/// controllers - if multiple controllers with the same finalizer name
223230
/// run against the same resource, unexpected behavior can occur.
224-
const FINALIZER_NAME: &'static str;
231+
///
232+
/// If this is None (the default), a finalizer will not be used, and
233+
/// cleanup events will not be reported.
234+
const FINALIZER_NAME: Option<&'static str> = None;
225235

226236
/// This method is called when a watched resource is created or updated.
227237
/// The [`Client`] used by the controller is passed in to allow making
@@ -243,11 +253,19 @@ pub trait Context {
243253
/// be performed, otherwise if `None` is returned,
244254
/// [`success_action`](Self::success_action) will be called to find the
245255
/// action to perform.
256+
///
257+
/// Note that this method will only be called if a finalizer is used.
246258
async fn cleanup(
247259
&self,
248260
client: Client,
249261
resource: &Self::Resource,
250-
) -> Result<Option<Action>, Self::Error>;
262+
) -> Result<Option<Action>, Self::Error> {
263+
// use a better name for the parameter name in the docs
264+
let _client = client;
265+
let _resource = resource;
266+
267+
Ok(Some(Action::await_change()))
268+
}
251269

252270
/// This method is called when a call to [`apply`](Self::apply) or
253271
/// [`cleanup`](Self::cleanup) returns `Ok(None)`. It should return the
@@ -271,7 +289,7 @@ pub trait Context {
271289
fn error_action(
272290
self: Arc<Self>,
273291
resource: Arc<Self::Resource>,
274-
err: &kube_runtime::finalizer::Error<Self::Error>,
292+
err: &Error<Self::Error>,
275293
consecutive_errors: u32,
276294
) -> Action {
277295
// use a better name for the parameter name in the docs
@@ -290,7 +308,7 @@ pub trait Context {
290308
client: Client,
291309
api: Api<Self::Resource>,
292310
resource: Arc<Self::Resource>,
293-
) -> Result<Action, kube_runtime::finalizer::Error<Self::Error>>
311+
) -> Result<Action, Error<Self::Error>>
294312
where
295313
Self: Send + Sync + 'static,
296314
Self::Error: Send + Sync + 'static,
@@ -307,11 +325,8 @@ pub trait Context {
307325
let dynamic_type = Default::default();
308326
let kind = Self::Resource::kind(&dynamic_type).into_owned();
309327
let mut ran = false;
310-
let res = finalizer(
311-
&api,
312-
Self::FINALIZER_NAME,
313-
Arc::clone(&resource),
314-
|event| async {
328+
let res = if let Some(finalizer_name) = Self::FINALIZER_NAME {
329+
finalizer(&api, finalizer_name, Arc::clone(&resource), |event| async {
315330
ran = true;
316331
event!(
317332
Level::INFO,
@@ -339,9 +354,27 @@ pub trait Context {
339354
.unwrap_or_else(Action::await_change),
340355
};
341356
Ok(action)
342-
},
343-
)
344-
.await;
357+
})
358+
.await
359+
.map_err(Error::FinalizerError)
360+
} else {
361+
ran = true;
362+
event!(
363+
Level::INFO,
364+
resource_name = %resource.name_unchecked().as_str(),
365+
"Reconciling {} (apply).",
366+
kind,
367+
);
368+
let action = self
369+
.apply(client, &resource)
370+
.await
371+
.map_err(Error::ControllerError)?;
372+
Ok(if let Some(action) = action {
373+
action
374+
} else {
375+
self.success_action(&resource)
376+
})
377+
};
345378
if !ran {
346379
event!(
347380
Level::INFO,

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
//! type Resource = Pod;
9090
//! type Error = kube::Error;
9191
//!
92-
//! const FINALIZER_NAME: &'static str = "example.com/pod-counter";
92+
//! const FINALIZER_NAME: Option<&'static str> = Some("example.com/pod-counter");
9393
//!
9494
//! async fn apply(
9595
//! &self,
@@ -137,7 +137,7 @@
137137
//! # impl k8s_controller::Context for PodCounter {
138138
//! # type Resource = Pod;
139139
//! # type Error = kube::Error;
140-
//! # const FINALIZER_NAME: &'static str = "example.com/pod-counter";
140+
//! # const FINALIZER_NAME: Option<&'static str> = Some("example.com/pod-counter");
141141
//! # async fn apply(
142142
//! # &self,
143143
//! # client: Client,

0 commit comments

Comments
 (0)