Skip to main content

rws_gateway_api/gateway_api/
provision.rs

1//! The provisioned mode (`RWS_CONFIG_GATEWAY_API_MODE=provisioned`): every
2//! Gateway gets its own Deployment of rws data-plane pods, in the
3//! controller's namespace and under its ServiceAccount, so no Gateway
4//! shares pods with another. Those pods run with
5//! `RWS_CONFIG_GATEWAY_API_GATEWAY=<namespace>/<name>`: they watch like the
6//! controller but serve only that Gateway and never write. The Gateway's
7//! Service lists them in its EndpointSlice ([`super::services`]). A
8//! Deployment can't be owned by an object in another namespace, so the
9//! controller deletes those whose Gateway is gone ([`stale`]).
10
11use std::collections::BTreeMap;
12
13use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec};
14use k8s_openapi::api::core::v1::{Container, EnvVar, EnvVarSource, HTTPGetAction, ObjectFieldSelector, PodSpec, PodTemplateSpec, Probe};
15use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta};
16use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
17use kube::ResourceExt;
18
19use super::resources::Gateway;
20use super::translate::{GATEWAY_NAME_LABEL, MANAGED_BY};
21
22/// `app.kubernetes.io/name` of provisioned pods.
23pub const APP_NAME: &str = "rws-gateway";
24/// The Gateway's namespace on its Deployment and pods.
25pub const GATEWAY_NAMESPACE_LABEL: &str = "rws8.tech/gateway-namespace";
26
27#[derive(Clone, Debug, PartialEq)]
28pub struct ProvisionSettings {
29    /// The rws image provisioned pods run (the chart's).
30    pub image: String,
31    /// The controller's ServiceAccount, which they share.
32    pub service_account: String,
33    /// rws's HTTP port in the pods, for the probes.
34    pub health_port: i32,
35    /// Replicas per Gateway.
36    pub replicas: i32,
37}
38
39impl Default for ProvisionSettings {
40    fn default() -> Self {
41        ProvisionSettings { image: String::new(), service_account: "default".to_string(), health_port: 7878, replicas: 1 }
42    }
43}
44
45/// The labels of a Gateway's pods (and its Deployment's selector).
46pub fn pod_labels(namespace: &str, name: &str) -> BTreeMap<String, String> {
47    BTreeMap::from([
48        ("app.kubernetes.io/name".to_string(), APP_NAME.to_string()),
49        (GATEWAY_NAME_LABEL.to_string(), name.to_string()),
50        (GATEWAY_NAMESPACE_LABEL.to_string(), namespace.to_string()),
51    ])
52}
53
54/// `rws-gw-<namespace>-<name>`, shortened with a hash past 63 characters.
55pub fn deployment_name(namespace: &str, name: &str) -> String {
56    let full = format!("rws-gw-{namespace}-{name}");
57    if full.len() <= 63 {
58        return full;
59    }
60    use std::hash::{Hash, Hasher};
61    let mut hasher = std::collections::hash_map::DefaultHasher::new();
62    full.hash(&mut hasher);
63    format!("{}-{:08x}", &full[..54], hasher.finish() as u32)
64}
65
66/// The Deployment each Gateway should have, keyed by (its namespace, name).
67pub fn desired(gateways: &[&Gateway], pod_namespace: &str, controller_name: &str, port_range: (u16, u16), settings: &ProvisionSettings) -> BTreeMap<(String, String), Deployment> {
68    let mut out = BTreeMap::new();
69    for gateway in gateways {
70        let (namespace, name) = (gateway.namespace().unwrap_or_default(), gateway.name_any());
71        let labels = pod_labels(&namespace, &name);
72        let infrastructure = gateway.spec.infrastructure.clone().unwrap_or_default();
73        let mut pod_labels_all = infrastructure.labels.clone();
74        pod_labels_all.extend(labels.clone());
75        let mut deployment_labels = labels.clone();
76        deployment_labels.insert("app.kubernetes.io/managed-by".to_string(), MANAGED_BY.to_string());
77        let env = |key: &str, value: String| EnvVar { name: key.to_string(), value: Some(value), value_from: None };
78        let field = |key: &str, path: &str| EnvVar {
79            name: key.to_string(),
80            value: None,
81            value_from: Some(EnvVarSource { field_ref: Some(ObjectFieldSelector { field_path: path.to_string(), api_version: None }), ..Default::default() }),
82        };
83        let probe = |path: &str| Probe {
84            http_get: Some(HTTPGetAction { path: Some(path.to_string()), port: IntOrString::Int(settings.health_port), ..Default::default() }),
85            period_seconds: Some(5),
86            ..Default::default()
87        };
88        let container = Container {
89            name: "rws".to_string(),
90            image: Some(settings.image.clone()),
91            env: Some(vec![
92                env(super::RWS_CONFIG_GATEWAY_API_ENABLED, "true".to_string()),
93                env(super::RWS_CONFIG_GATEWAY_API_GATEWAY, format!("{namespace}/{name}")),
94                env(super::RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME, controller_name.to_string()),
95                env(super::RWS_CONFIG_GATEWAY_API_PORT_RANGE, format!("{}-{}", port_range.0, port_range.1)),
96                field("POD_NAME", "metadata.name"),
97                field("POD_NAMESPACE", "metadata.namespace"),
98            ]),
99            readiness_probe: Some(probe("/readyz")),
100            liveness_probe: Some(probe("/healthz")),
101            ..Default::default()
102        };
103        let deployment = Deployment {
104            metadata: ObjectMeta {
105                name: Some(deployment_name(&namespace, &name)),
106                namespace: Some(pod_namespace.to_string()),
107                labels: Some(deployment_labels),
108                annotations: (!infrastructure.annotations.is_empty()).then(|| infrastructure.annotations.clone()),
109                ..Default::default()
110            },
111            spec: Some(DeploymentSpec {
112                replicas: Some(settings.replicas),
113                selector: LabelSelector { match_labels: Some(labels), match_expressions: None },
114                template: PodTemplateSpec {
115                    metadata: Some(ObjectMeta {
116                        labels: Some(pod_labels_all),
117                        annotations: (!infrastructure.annotations.is_empty()).then(|| infrastructure.annotations.clone()),
118                        ..Default::default()
119                    }),
120                    spec: Some(PodSpec { service_account_name: Some(settings.service_account.clone()), containers: vec![container], ..Default::default() }),
121                },
122                ..Default::default()
123            }),
124            status: None,
125        };
126        out.insert((namespace, name), deployment);
127    }
128    out
129}
130
131/// Whether `current` already runs what `desired` asks for (the fields rws sets).
132pub fn same(current: &Deployment, desired: &Deployment) -> bool {
133    let spec = |d: &Deployment| {
134        let s = d.spec.as_ref();
135        let template = s.map(|s| &s.template);
136        let container = template.and_then(|t| t.spec.as_ref()).and_then(|p| p.containers.first());
137        (
138            s.and_then(|s| s.replicas),
139            template.and_then(|t| t.metadata.as_ref()).and_then(|m| m.labels.clone()),
140            template.and_then(|t| t.metadata.as_ref()).and_then(|m| m.annotations.clone()),
141            container.and_then(|c| c.image.clone()),
142            container.and_then(|c| c.env.clone()),
143        )
144    };
145    spec(current) == spec(desired)
146}
147
148/// Provisioned Deployments in `deployments` whose Gateway no longer wants one.
149pub fn stale<'a>(deployments: &'a [Deployment], wanted: &BTreeMap<(String, String), Deployment>) -> Vec<&'a Deployment> {
150    deployments
151        .iter()
152        .filter(|d| d.labels().get("app.kubernetes.io/managed-by").map(String::as_str) == Some(MANAGED_BY))
153        .filter(|d| !wanted.values().any(|w| w.metadata.name == d.metadata.name && w.metadata.namespace == d.metadata.namespace))
154        .collect()
155}
156
157#[cfg(test)]
158mod tests;