1use super::{ReconcileReason, ReconcileRequest};
11use crate::reflector::ObjectRef;
12use futures::{Stream, TryStream, TryStreamExt, stream};
13use kube_client::Resource;
14use std::borrow::Borrow;
15
16pub 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
32pub 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
53pub(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 S: TryStream,
64 S::Ok: Borrow<O>,
65 O: Resource,
66 O::DynamicType: Clone,
67 K: Resource,
69 K::DynamicType: Clone,
70 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
87pub 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 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#[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 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 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 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}