Skip to main content

kube_runtime/controller/
trigger.rs

1//! Trigger mappers that turn streams of objects into streams of [`ReconcileRequest`]s
2//!
3//! Every relation (self, owns, watches) is a mapping from an object in a watched stream
4//! to zero or more reconcile requests for the controlled kind, built on top of [`trigger_with`].
5//!
6//! [`trigger_self`] and [`trigger_others`] accept streams of either owned objects (`K`) or
7//! shared objects (`Arc<K>`), since both implement [`Borrow<K>`](std::borrow::Borrow).
8//! [`trigger_owners`] takes owned objects so it can move out their metadata instead of cloning.
9
10use super::{ReconcileReason, ReconcileRequest};
11use crate::reflector::ObjectRef;
12use futures::{Stream, TryStream, TryStreamExt, stream};
13use kube_client::Resource;
14use std::borrow::Borrow;
15
16/// Helper for building custom trigger filters, see the implementations of [`trigger_self`] and [`trigger_owners`] for some examples.
17pub fn trigger_with<T, K, I, S>(
18    stream: S,
19    mapper: impl Fn(T) -> I,
20) -> impl Stream<Item = Result<ReconcileRequest<K>, S::Error>>
21where
22    S: TryStream<Ok = T>,
23    I: IntoIterator,
24    I::Item: Into<ReconcileRequest<K>>,
25    K: Resource,
26{
27    stream
28        .map_ok(move |obj| stream::iter(mapper(obj).into_iter().map(Into::into).map(Ok)))
29        .try_flatten()
30}
31
32/// Enqueues the object itself for reconciliation
33///
34/// The stream can yield `K` or `Arc<K>`.
35pub fn trigger_self<K, S>(
36    stream: S,
37    dyntype: K::DynamicType,
38) -> impl Stream<Item = Result<ReconcileRequest<K>, S::Error>>
39where
40    S: TryStream,
41    S::Ok: Borrow<K>,
42    K: Resource,
43    K::DynamicType: Clone,
44{
45    trigger_with(stream, move |obj: S::Ok| {
46        Some(ReconcileRequest {
47            obj_ref: ObjectRef::from_obj_with(obj.borrow(), dyntype.clone()),
48            reason: ReconcileReason::ObjectUpdated,
49        })
50    })
51}
52
53/// Enqueues any mapper returned `K` types for reconciliation
54///
55/// The stream can yield `O` or `Arc<O>`, and the mapper receives the item as yielded.
56pub(crate) fn trigger_others<S, O, K, I>(
57    stream: S,
58    mapper: impl Fn(S::Ok) -> I + Sync + Send + 'static,
59    dyntype: O::DynamicType,
60) -> impl Stream<Item = Result<ReconcileRequest<K>, S::Error>>
61where
62    // Input stream has items as some Resource (via Controller::watches)
63    S: TryStream,
64    S::Ok: Borrow<O>,
65    O: Resource,
66    O::DynamicType: Clone,
67    // Output stream is requests for the root type K
68    K: Resource,
69    K::DynamicType: Clone,
70    // but the mapper can produce many of them
71    I: 'static + IntoIterator<Item = ObjectRef<K>>,
72    I::IntoIter: Send,
73{
74    trigger_with(stream, move |obj: S::Ok| {
75        let watch_ref = ObjectRef::from_obj_with(obj.borrow(), dyntype.clone()).erase();
76        mapper(obj)
77            .into_iter()
78            .map(move |mapped_obj_ref| ReconcileRequest {
79                obj_ref: mapped_obj_ref,
80                reason: ReconcileReason::RelatedObjectUpdated {
81                    obj_ref: Box::new(watch_ref.clone()),
82                },
83            })
84    })
85}
86
87/// Enqueues any owners of type `KOwner` for reconciliation
88pub fn trigger_owners<KOwner, S>(
89    stream: S,
90    owner_type: KOwner::DynamicType,
91    child_type: <S::Ok as Resource>::DynamicType,
92) -> impl Stream<Item = Result<ReconcileRequest<KOwner>, S::Error>>
93where
94    S: TryStream,
95    S::Ok: Resource,
96    <S::Ok as Resource>::DynamicType: Clone,
97    KOwner: Resource,
98    KOwner::DynamicType: Clone,
99{
100    let mapper = move |mut obj: S::Ok| {
101        // `obj` is owned here, so take the metadata instead of deep-cloning the whole
102        // ObjectMeta (labels/annotations/managedFields) just to read two fields.
103        let meta = std::mem::take(obj.meta_mut());
104        let ns = meta.namespace;
105        let owner_type = owner_type.clone();
106        meta.owner_references
107            .into_iter()
108            .flatten()
109            .filter_map(move |owner| ObjectRef::from_owner_ref(ns.as_deref(), &owner, owner_type.clone()))
110    };
111    trigger_others::<S, S::Ok, KOwner, _>(stream, mapper, child_type)
112}
113
114/// Enqueues any owners of type `KOwner` for reconciliation based on a stream of shared `Arc<K>` objects
115#[cfg(feature = "unstable-runtime-subscribe")]
116pub(crate) fn trigger_owners_shared<KOwner, K, S>(
117    stream: S,
118    owner_type: KOwner::DynamicType,
119    child_type: K::DynamicType,
120) -> impl Stream<Item = Result<ReconcileRequest<KOwner>, S::Error>>
121where
122    S: TryStream<Ok = std::sync::Arc<K>>,
123    K: Resource,
124    K::DynamicType: Clone,
125    KOwner: Resource,
126    KOwner::DynamicType: Clone,
127{
128    let mapper = move |obj: S::Ok| {
129        // `obj` is shared, so clone only the two fields we need rather than the whole ObjectMeta
130        let meta = obj.meta();
131        let ns = meta.namespace.clone();
132        let owner_type = owner_type.clone();
133        meta.owner_references
134            .clone()
135            .into_iter()
136            .flatten()
137            .filter_map(move |owner| ObjectRef::from_owner_ref(ns.as_deref(), &owner, owner_type.clone()))
138    };
139    trigger_others::<S, K, KOwner, _>(stream, mapper, child_type)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use futures::{StreamExt, TryStreamExt};
146    use k8s_openapi::{
147        api::{
148            apps::v1::{Deployment, ReplicaSet},
149            core::v1::{ConfigMap, Pod},
150        },
151        apimachinery::pkg::apis::meta::v1::OwnerReference,
152    };
153    use kube_client::core::ObjectMeta;
154    use std::{convert::Infallible, sync::Arc};
155
156    fn owner_ref<K: Resource<DynamicType = ()>>(name: &str) -> OwnerReference {
157        OwnerReference {
158            api_version: K::api_version(&()).into(),
159            kind: K::kind(&()).into(),
160            name: name.into(),
161            uid: format!("{name}-uid"),
162            ..Default::default()
163        }
164    }
165
166    fn pod(name: &str, owners: Vec<OwnerReference>) -> Pod {
167        Pod {
168            metadata: ObjectMeta {
169                name: Some(name.into()),
170                namespace: Some("ns".into()),
171                owner_references: Some(owners),
172                ..Default::default()
173            },
174            ..Default::default()
175        }
176    }
177
178    fn ok<T>(items: Vec<T>) -> impl Stream<Item = Result<T, Infallible>> {
179        stream::iter(items).map(Ok)
180    }
181
182    async fn collect<K: Resource>(
183        s: impl Stream<Item = Result<ReconcileRequest<K>, Infallible>>,
184    ) -> Vec<ReconcileRequest<K>> {
185        s.try_collect().await.unwrap()
186    }
187
188    fn assert_related_to(req: &ReconcileRequest<impl Resource>, expected: &ObjectRef<Pod>) {
189        match &req.reason {
190            ReconcileReason::RelatedObjectUpdated { obj_ref } => {
191                assert_eq!(**obj_ref, expected.clone().erase())
192            }
193            reason => panic!("unexpected reason: {reason}"),
194        }
195    }
196
197    #[tokio::test]
198    async fn trigger_self_maps_owned_and_shared_objects() {
199        let p = pod("a", vec![]);
200        let expected = ObjectRef::from_obj(&p);
201
202        for reqs in [
203            collect(trigger_self::<Pod, _>(ok(vec![p.clone()]), ())).await,
204            collect(trigger_self::<Pod, _>(ok(vec![Arc::new(p.clone())]), ())).await,
205        ] {
206            assert_eq!(reqs.len(), 1);
207            assert_eq!(reqs[0].obj_ref, expected);
208            assert!(matches!(reqs[0].reason, ReconcileReason::ObjectUpdated));
209        }
210    }
211
212    /// A pod owned by a `ReplicaSet` and a `Deployment`, plus an orphan pod
213    fn owned_pods() -> [Pod; 2] {
214        [
215            pod("a", vec![
216                owner_ref::<ReplicaSet>("rs"),
217                owner_ref::<Deployment>("ignored"),
218            ]),
219            pod("b", vec![]),
220        ]
221    }
222
223    /// Only the `ReplicaSet` owner of pod `a` should be enqueued
224    fn assert_owner_requests(reqs: &[ReconcileRequest<ReplicaSet>]) {
225        assert_eq!(reqs.len(), 1);
226        let req = &reqs[0];
227        assert_eq!(req.obj_ref, ObjectRef::new("rs").within("ns"));
228        assert_eq!(req.obj_ref.extra.uid.as_deref(), Some("rs-uid"));
229        assert_related_to(req, &ObjectRef::new("a").within("ns"));
230    }
231
232    #[tokio::test]
233    async fn trigger_owners_only_enqueues_matching_owner_kind() {
234        let reqs = collect(trigger_owners::<ReplicaSet, _>(ok(owned_pods().to_vec()), (), ())).await;
235        assert_owner_requests(&reqs);
236    }
237
238    #[cfg(feature = "unstable-runtime-subscribe")]
239    #[tokio::test]
240    async fn trigger_owners_shared_only_enqueues_matching_owner_kind() {
241        let pods = owned_pods().map(Arc::new).to_vec();
242        let reqs = collect(trigger_owners_shared::<ReplicaSet, _, _>(ok(pods), (), ())).await;
243        assert_owner_requests(&reqs);
244    }
245
246    #[tokio::test]
247    async fn trigger_others_enqueues_every_mapped_object() {
248        let p = pod("a", vec![]);
249        let pod_ref = ObjectRef::from_obj(&p);
250        let mapper = |pod: Arc<Pod>| {
251            let ns = pod.metadata.namespace.clone().unwrap();
252            ["cm1", "cm2"].map(|name| ObjectRef::<ConfigMap>::new(name).within(&ns))
253        };
254
255        let reqs = collect(trigger_others::<_, Pod, ConfigMap, _>(
256            ok(vec![Arc::new(p)]),
257            mapper,
258            (),
259        ))
260        .await;
261
262        let names: Vec<_> = reqs.iter().map(|r| r.obj_ref.name.as_str()).collect();
263        assert_eq!(names, ["cm1", "cm2"]);
264        for req in &reqs {
265            assert_eq!(req.obj_ref.namespace.as_deref(), Some("ns"));
266            assert_related_to(req, &pod_ref);
267        }
268    }
269
270    #[tokio::test]
271    async fn trigger_with_passes_errors_through() {
272        let input = stream::iter(vec![Err("boom"), Ok(pod("a", vec![]))]);
273        let out: Vec<_> = trigger_self::<Pod, _>(input, ()).collect().await;
274
275        assert!(matches!(out[0], Err("boom")));
276        assert_eq!(out[1].as_ref().unwrap().obj_ref.name, "a");
277    }
278}