Skip to main content

sk_core/k8s/
owners.rs

1use std::collections::HashMap;
2
3use async_recursion::async_recursion;
4use kube::Resource;
5use kube::api::ListParams;
6use kube::discovery::{
7    ApiCapabilities,
8    Scope,
9};
10use tracing::*;
11
12use super::*;
13use crate::k8s::{
14    DynamicApiSet,
15    format_gvk_name,
16};
17use crate::prelude::*;
18
19// TODO I really want a way to mock out the OwnersCache, because
20// any tests that depend on it implicitly now have to depend on tokio
21// and also the fake_apiserver, which is cumbersome to deal with; unfortunately,
22// the &(impl Resource) argument to the traits makes mockall really unhappy;
23// I also tried re-architecting it so that we just passed in the object's
24// list of OwnerRefs, but then it turns out that the #[async_recursion] bit
25// _also_ makes mockall really unhappy.  I think if we're going to mock
26// this we'll have to implement the mock ourselves.
27pub struct OwnersCache {
28    apiset: DynamicApiSet,
29    owners: HashMap<(GVK, String), Vec<metav1::OwnerReference>>,
30}
31
32impl OwnersCache {
33    pub fn new(apiset: DynamicApiSet) -> OwnersCache {
34        OwnersCache { apiset, owners: HashMap::new() }
35    }
36
37    pub fn new_from_parts(
38        apiset: DynamicApiSet,
39        owners: HashMap<(GVK, String), Vec<metav1::OwnerReference>>,
40    ) -> OwnersCache {
41        OwnersCache { apiset, owners }
42    }
43
44    // Recursively look up all of the owning objects for a given Kubernetes object
45    #[async_recursion]
46    pub async fn compute_owners_for(&mut self, gvk: &GVK, obj: &(impl Resource + Sync)) -> Vec<metav1::OwnerReference> {
47        let ns_name = obj.namespaced_name();
48        let gvk_name = format_gvk_name(gvk, &ns_name);
49        debug!("computing owner references for {gvk_name}");
50
51        let key = (gvk.clone(), ns_name.clone());
52        if let Some(owners) = self.owners.get(&key) {
53            debug!("found owners {owners:?} for {gvk_name} in cache");
54            return owners.clone();
55        }
56
57        // Requires that the object's owner references haven't been sanitized away
58        let mut owners = Vec::new();
59
60        for rf in obj.owner_references() {
61            let owner_gvk = match GVK::from_owner_ref(rf) {
62                Ok(gvk) => gvk,
63                Err(err) => {
64                    error!("malformed owner reference {rf:?}: {err}");
65                    continue;
66                },
67            };
68            let (api, cap) = match self.apiset.unnamespaced_api_by_gvk(&owner_gvk).await {
69                Ok((a, c)) => (a, c),
70                Err(err) => {
71                    // Just a warning because it may be some CRD we intentionally haven't installed
72                    warn!("could not query {owner_gvk}: {err}; skipping ownerref");
73                    continue;
74                },
75            };
76            let sel = build_owner_selector(&rf.name, obj, cap);
77            let items = match api.list(&sel).await {
78                Ok(objlist) => objlist.items,
79                Err(err) => {
80                    error!("Could not list {owner_gvk}: {err}; skipping ownerref");
81                    continue;
82                },
83            };
84
85            if items.len() != 1 {
86                error!("could not find single owner for {gvk_name}, found {items:?}; skipping ownerref");
87                continue;
88            }
89
90            owners.push(rf.clone());
91            owners.extend(self.compute_owners_for(&owner_gvk, &items[0]).await);
92        }
93
94        debug!("computed owners {owners:?} for {gvk_name}");
95        self.owners.insert(key, owners.clone());
96        owners
97    }
98
99    pub async fn lookup_by_name_or_obj(
100        &mut self,
101        gvk: &GVK,
102        ns_name: &str,
103        maybe_obj: Option<&(impl Resource + Sync)>,
104    ) -> Vec<metav1::OwnerReference> {
105        match (self.owners.get(&(gvk.clone(), ns_name.into())), maybe_obj) {
106            (Some(o), _) => o.clone(),
107            (None, Some(obj)) => self.compute_owners_for(gvk, obj).await,
108            _ => {
109                error!("could not determine owner chain for {ns_name}");
110                vec![]
111            },
112        }
113    }
114}
115
116fn build_owner_selector(owner_name: &str, obj: &(impl Resource + Sync), owner_cap: ApiCapabilities) -> ListParams {
117    let sel = match owner_cap.scope {
118        Scope::Cluster => Some(format!("metadata.name={owner_name}")),
119        Scope::Namespaced => {
120            // if it's namespaced, the namespace field should be populated, so the unwrap is
121            // safe/should never trigger
122            Some(format!("metadata.namespace={},metadata.name={}", obj.namespace().unwrap(), owner_name))
123        },
124    };
125    ListParams { field_selector: sel, ..Default::default() }
126}