Skip to main content

sk_core/k8s/
util.rs

1use std::collections::BTreeMap;
2
3use kube::api::Resource;
4use serde_json::{
5    Map,
6    Value,
7};
8
9use super::*;
10use crate::constants::*;
11use crate::errors::*;
12
13const MAX_LABEL_LENGTH: usize = 63;
14
15pub fn add_common_metadata<K>(sim_name: &str, owner: &K, meta: &mut metav1::ObjectMeta)
16where
17    K: Resource<DynamicType = ()>,
18{
19    let labels = &mut meta.labels.get_or_insert_default();
20    labels.insert(SIMULATION_LABEL_KEY.into(), truncate_label(sim_name.into()));
21    labels.insert(
22        APP_KUBERNETES_IO_NAME_KEY.into(),
23        truncate_label(meta.name.clone().unwrap()), // everything should have a name (???)
24    );
25
26    meta.owner_references.get_or_insert_default().push(metav1::OwnerReference {
27        api_version: K::api_version(&()).into(),
28        kind: K::kind(&()).into(),
29        name: owner.name_any(),
30
31        // if the delete propagation policy is set to foreground, this will block
32        // the owner from being deleted until this object is deleted
33        // (note _both_ must be set, otherwise it doesn't work)
34        //
35        // https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion
36        block_owner_deletion: Some(true),
37
38        // Kubernetes "should" always set this and I'm tired of all the
39        // error propogation trying to check for this induces
40        uid: owner.uid().unwrap(),
41        ..Default::default()
42    });
43}
44
45pub fn build_deletable(gvk: &GVK, ns_name: &str) -> DynamicObject {
46    let (ns, name) = split_namespaced_name(ns_name);
47    DynamicObject {
48        metadata: metav1::ObjectMeta {
49            namespace: Some(ns),
50            name: Some(name),
51            ..Default::default()
52        },
53        types: Some(gvk.into_type_meta()),
54        data: Value::Null,
55    }
56}
57
58pub fn build_containment_label_selector(key: &str, labels: Vec<String>) -> metav1::LabelSelector {
59    metav1::LabelSelector {
60        match_expressions: Some(vec![metav1::LabelSelectorRequirement {
61            key: key.into(),
62            operator: "In".into(),
63            values: Some(labels),
64        }]),
65        ..Default::default()
66    }
67}
68
69pub fn build_global_object_meta<K>(name: &str, sim_name: &str, owner: &K) -> metav1::ObjectMeta
70where
71    K: Resource<DynamicType = ()>,
72{
73    build_object_meta_helper(None, name, sim_name, owner)
74}
75
76pub fn build_object_meta<K>(namespace: &str, name: &str, sim_name: &str, owner: &K) -> metav1::ObjectMeta
77where
78    K: Resource<DynamicType = ()>,
79{
80    build_object_meta_helper(Some(namespace.into()), name, sim_name, owner)
81}
82
83pub fn build_pod_self_owner_reference(pod: &corev1::Pod) -> metav1::OwnerReference {
84    metav1::OwnerReference {
85        api_version: "v1".into(),
86        kind: POD_GVK.kind.clone(),
87        name: pod.name_any(),
88        ..Default::default()
89    }
90}
91
92pub fn dyn_obj_spec(obj: &DynamicObject) -> Option<&Map<String, Value>> {
93    obj.data
94        .as_object()
95        .and_then(|data| data.get("spec").and_then(|spec| spec.as_object()))
96}
97
98pub fn dyn_obj_spec_mut(obj: &mut DynamicObject) -> Option<&mut Map<String, Value>> {
99    obj.data
100        .as_object_mut()
101        .and_then(|data| data.get_mut("spec").and_then(|spec| spec.as_object_mut()))
102}
103
104pub fn dyn_obj_type_str(obj: &DynamicObject) -> String {
105    obj.types
106        .as_ref()
107        .map(|tm| format!("{}.{}", tm.api_version, tm.kind))
108        .unwrap_or("<unknown type>".into())
109}
110
111pub fn format_gvk_name(gvk: &GVK, ns_name: &str) -> String {
112    format!("{gvk}:{ns_name}")
113}
114
115pub fn sanitize_obj(gvk: &GVK, obj: &mut DynamicObject) {
116    // Kubernetes does not always fill out the TypeMeta (possibly for deleted resources, but I'm
117    // pretty sure definitely for the results of a List API call -- you know, like when you run
118    // ListAndWatch to start a new informer).  I _believe_ the type information for the list call only
119    // gets populated on the outer wrapper of the list and not for individual objects in the list.
120    //
121    // There are a couple related GitHub issues here:
122    //   - https://github.com/kubernetes-sigs/controller-runtime/issues/1517
123    //   - https://github.com/kubernetes-sigs/controller-runtime/issues/1735
124    //
125    // ANYWAYS as a result of this extremely annoying behaviour, we fill in the type meta here,
126    // which we know as a part of setting up the informer.
127    obj.types = Some(gvk.into_type_meta());
128
129    // N.B. We do not sanitize owner references here, since we need them
130    // to compute owner chains in the TraceStore
131    obj.metadata.creation_timestamp = None;
132    obj.metadata.deletion_timestamp = None;
133    obj.metadata.deletion_grace_period_seconds = None;
134    obj.metadata.generation = None;
135    obj.metadata.managed_fields = None;
136    obj.metadata.resource_version = None;
137    obj.metadata.uid = None;
138
139    obj.annotations_mut().remove(LAST_APPLIED_CONFIG_LABEL_KEY);
140    obj.annotations_mut().remove(DEPL_REVISION_LABEL_KEY);
141
142    // If we are tracking pod objects (whether bare or not!), we want to ignore the node it was
143    // assigned to "in production" since this will certainly not exist in the simulation.
144    if gvk == &*POD_GVK {
145        dyn_obj_spec_mut(obj).map(|spec| spec.remove("nodeName"));
146    }
147}
148
149pub fn split_namespaced_name(name: &str) -> (String, String) {
150    match name.split_once('/') {
151        Some((namespace, name)) => (namespace.into(), name.into()),
152        None => ("".into(), name.into()),
153    }
154}
155
156pub fn truncate_label(mut value: String) -> String {
157    if value.len() > MAX_LABEL_LENGTH {
158        value.truncate(MAX_LABEL_LENGTH - 4);
159        value.push_str("XXXX");
160    }
161    value
162}
163
164impl<T: Resource> KubeResourceExt for T {
165    fn namespaced_name(&self) -> String {
166        match self.namespace() {
167            Some(ns) => format!("{}/{}", ns, self.name_any()),
168            None => self.name_any().clone(),
169        }
170    }
171
172    fn matches(&self, sel: &metav1::LabelSelector) -> anyhow::Result<bool> {
173        if let Some(exprs) = &sel.match_expressions {
174            for expr in exprs {
175                if !label_expr_match(self.labels(), expr)? {
176                    return Ok(false);
177                }
178            }
179        }
180
181        if let Some(labels) = &sel.match_labels {
182            for (k, v) in labels {
183                if self.labels().get(k) != Some(v) {
184                    return Ok(false);
185                }
186            }
187        }
188        Ok(true)
189    }
190}
191
192fn build_object_meta_helper<K>(namespace: Option<String>, name: &str, sim_name: &str, owner: &K) -> metav1::ObjectMeta
193where
194    K: Resource<DynamicType = ()>,
195{
196    let mut meta = metav1::ObjectMeta {
197        namespace,
198        name: Some(name.into()),
199        ..Default::default()
200    };
201
202    add_common_metadata(sim_name, owner, &mut meta);
203    meta
204}
205
206// The meanings of these operators is explained here:
207// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#set-based-requirement
208pub(super) const OPERATOR_IN: &str = "In";
209pub(super) const OPERATOR_NOT_IN: &str = "NotIn";
210pub(super) const OPERATOR_EXISTS: &str = "Exists";
211pub(super) const OPERATOR_DOES_NOT_EXIST: &str = "DoesNotExist";
212
213fn label_expr_match(
214    obj_labels: &BTreeMap<String, String>,
215    expr: &metav1::LabelSelectorRequirement,
216) -> anyhow::Result<bool> {
217    // LabelSelectorRequirement is considered invalid if the Operator is "In" or NotIn"
218    // and there are no values; conversely for "Exists" and "DoesNotExist".
219    match expr.operator.as_str() {
220        OPERATOR_IN => match obj_labels.get(&expr.key) {
221            Some(v) => match &expr.values {
222                Some(values) if !values.is_empty() => Ok(values.contains(v)),
223                _ => bail!(KubernetesError::malformed_label_selector(expr)),
224            },
225            None => Ok(false),
226        },
227        OPERATOR_NOT_IN => match obj_labels.get(&expr.key) {
228            Some(v) => match &expr.values {
229                Some(values) if !values.is_empty() => Ok(!values.contains(v)),
230                _ => bail!(KubernetesError::malformed_label_selector(expr)),
231            },
232            None => Ok(true),
233        },
234        OPERATOR_EXISTS => match &expr.values {
235            Some(values) if !values.is_empty() => bail!(KubernetesError::malformed_label_selector(expr)),
236            _ => Ok(obj_labels.contains_key(&expr.key)),
237        },
238        OPERATOR_DOES_NOT_EXIST => match &expr.values {
239            Some(values) if !values.is_empty() => {
240                bail!(KubernetesError::malformed_label_selector(expr));
241            },
242            _ => Ok(!obj_labels.contains_key(&expr.key)),
243        },
244        _ => bail!("malformed label selector expression: {:?}", expr),
245    }
246}