rws_gateway_api/gateway_api/
provision.rs1use 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
22pub const APP_NAME: &str = "rws-gateway";
24pub const GATEWAY_NAMESPACE_LABEL: &str = "rws8.tech/gateway-namespace";
26
27#[derive(Clone, Debug, PartialEq)]
28pub struct ProvisionSettings {
29 pub image: String,
31 pub service_account: String,
33 pub health_port: i32,
35 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
45pub 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
54pub 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
66pub 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
131pub 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
148pub 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;