1use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15use std::time::Duration;
16
17use futures_util::StreamExt;
18use k8s_openapi::api::core::v1::{ConfigMap, Namespace, Pod, Secret, Service};
19use k8s_openapi::api::discovery::v1::EndpointSlice;
20use k8s_openapi::apimachinery::pkg::apis::meta::v1::Time;
21use kube::api::{Api, Patch, PatchParams};
22use kube::runtime::reflector::{self, store::Writer, Store};
23use kube::runtime::{watcher, WatchStreamExt};
24use kube::{Client, Resource, ResourceExt};
25use serde::de::DeserializeOwned;
26use tokio::sync::Notify;
27
28use super::resources::{self, BackendTlsPolicy, Gateway, GatewayClass, GrpcRoute, HttpRoute, ListenerSet, ReferenceGrant, TcpRoute, TlsRoute, UdpRoute};
29use super::translate::{translate, GatewayConfig, Settings, Snapshot};
30use super::types::RouteParentStatus;
31
32pub const DEBOUNCE: Duration = Duration::from_millis(100);
34pub const RESYNC: Duration = Duration::from_secs(30);
36
37pub struct Options {
39 pub settings: Settings,
40 pub leader: Arc<AtomicBool>,
43 pub on_config: Arc<dyn Fn(GatewayConfig) + Send + Sync>,
45}
46
47#[derive(Clone)]
49pub struct Stores {
50 pub classes: Store<GatewayClass>,
51 pub gateways: Store<Gateway>,
52 pub http_routes: Store<HttpRoute>,
53 pub grpc_routes: Store<GrpcRoute>,
54 pub tls_routes: Store<TlsRoute>,
55 pub tcp_routes: Store<TcpRoute>,
56 pub udp_routes: Store<UdpRoute>,
57 pub listener_sets: Store<ListenerSet>,
58 pub backend_tls_policies: Store<BackendTlsPolicy>,
59 pub reference_grants: Store<ReferenceGrant>,
60 pub services: Store<Service>,
61 pub namespaces: Store<Namespace>,
62 pub secrets: Store<Secret>,
63 pub config_maps: Store<ConfigMap>,
64 pub endpoint_slices: Store<EndpointSlice>,
65 pub pods: Store<Pod>,
66 pub deployments: Store<k8s_openapi::api::apps::v1::Deployment>,
67}
68
69impl Stores {
70 pub fn snapshot(&self) -> Snapshot {
71 fn all<K: Clone + kube::Resource>(store: &Store<K>) -> Vec<K>
72 where
73 K::DynamicType: std::hash::Hash + Eq + Clone,
74 {
75 store.state().iter().map(|o| (**o).clone()).collect()
76 }
77 Snapshot {
78 classes: all(&self.classes),
79 gateways: all(&self.gateways),
80 http_routes: all(&self.http_routes),
81 grpc_routes: all(&self.grpc_routes),
82 tls_routes: all(&self.tls_routes),
83 tcp_routes: all(&self.tcp_routes),
84 udp_routes: all(&self.udp_routes),
85 listener_sets: all(&self.listener_sets),
86 backend_tls_policies: all(&self.backend_tls_policies),
87 reference_grants: all(&self.reference_grants),
88 services: all(&self.services),
89 namespaces: all(&self.namespaces),
90 secrets: all(&self.secrets),
91 config_maps: all(&self.config_maps),
92 endpoint_slices: all(&self.endpoint_slices),
93 pods: all(&self.pods),
94 deployments: all(&self.deployments),
95 }
96 }
97}
98
99fn watch<K>(api: Api<K>, dyntype: K::DynamicType, changed: Arc<Notify>) -> Store<K>
101where
102 K: Resource + Clone + DeserializeOwned + std::fmt::Debug + Send + Sync + 'static,
103 K::DynamicType: std::hash::Hash + Eq + Clone + Send + Sync,
104{
105 watch_with(api, dyntype, watcher::Config::default(), changed)
106}
107
108fn watch_with<K>(api: Api<K>, dyntype: K::DynamicType, config: watcher::Config, changed: Arc<Notify>) -> Store<K>
109where
110 K: Resource + Clone + DeserializeOwned + std::fmt::Debug + Send + Sync + 'static,
111 K::DynamicType: std::hash::Hash + Eq + Clone + Send + Sync,
112{
113 let writer = Writer::new(dyntype);
114 let reader = writer.as_reader();
115 let stream = reflector::reflector(writer, watcher(api, config)).default_backoff().touched_objects();
116 tokio::spawn(async move {
117 let mut stream = std::pin::pin!(stream);
118 while let Some(event) = stream.next().await {
119 if let Err(e) = event {
120 eprintln!("[gateway-api] watch error: {e}");
121 }
122 changed.notify_one();
123 }
124 });
125 reader
126}
127
128pub fn start_watches(client: &Client, services: &super::services::ServiceSettings) -> (Stores, Arc<Notify>) {
131 let pod_labels = services.pod_watch_labels().iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(",");
132 let managed = format!("app.kubernetes.io/managed-by={}", super::translate::MANAGED_BY);
133 let changed = Arc::new(Notify::new());
134 let stores = Stores {
135 classes: watch(Api::all_with(client.clone(), &resources::gateway_classes()), resources::gateway_classes(), Arc::clone(&changed)),
136 gateways: watch(Api::all_with(client.clone(), &resources::gateways()), resources::gateways(), Arc::clone(&changed)),
137 http_routes: watch(Api::all_with(client.clone(), &resources::http_routes()), resources::http_routes(), Arc::clone(&changed)),
138 grpc_routes: watch(Api::all_with(client.clone(), &resources::grpc_routes()), resources::grpc_routes(), Arc::clone(&changed)),
139 tls_routes: watch(Api::all_with(client.clone(), &resources::tls_routes()), resources::tls_routes(), Arc::clone(&changed)),
140 tcp_routes: watch(Api::all_with(client.clone(), &resources::tcp_routes()), resources::tcp_routes(), Arc::clone(&changed)),
141 udp_routes: watch(Api::all_with(client.clone(), &resources::udp_routes()), resources::udp_routes(), Arc::clone(&changed)),
142 listener_sets: watch(Api::all_with(client.clone(), &resources::listener_sets()), resources::listener_sets(), Arc::clone(&changed)),
143 backend_tls_policies: watch(Api::all_with(client.clone(), &resources::backend_tls_policies()), resources::backend_tls_policies(), Arc::clone(&changed)),
144 reference_grants: watch(Api::all_with(client.clone(), &resources::reference_grants()), resources::reference_grants(), Arc::clone(&changed)),
145 services: watch(Api::all(client.clone()), (), Arc::clone(&changed)),
146 namespaces: watch(Api::all(client.clone()), (), Arc::clone(&changed)),
147 secrets: watch(Api::all(client.clone()), (), Arc::clone(&changed)),
148 config_maps: watch(Api::all(client.clone()), (), Arc::clone(&changed)),
149 endpoint_slices: watch(Api::all(client.clone()), (), Arc::clone(&changed)),
150 pods: watch_with(Api::namespaced(client.clone(), &services.pod_namespace), (), watcher::Config::default().labels(&pod_labels), Arc::clone(&changed)),
151 deployments: watch_with(Api::namespaced(client.clone(), &services.pod_namespace), (), watcher::Config::default().labels(&managed), Arc::clone(&changed)),
152 };
153 (stores, changed)
154}
155
156pub async fn run(client: Client, options: Options, became_leader: Arc<Notify>) {
159 let (stores, changed) = start_watches(&client, &options.settings.services);
160 loop {
161 tokio::select! {
162 _ = changed.notified() => {}
163 _ = became_leader.notified() => {}
164 _ = tokio::time::sleep(RESYNC) => {}
165 }
166 tokio::time::sleep(DEBOUNCE).await;
167 if let Err(e) = reconcile(&client, &stores, &options).await {
168 eprintln!("[gateway-api] reconcile: {e}");
169 let changed = Arc::clone(&changed);
171 tokio::spawn(async move {
172 tokio::time::sleep(Duration::from_secs(1)).await;
173 changed.notify_one();
174 });
175 }
176 }
177}
178
179pub async fn reconcile(client: &Client, stores: &Stores, options: &Options) -> Result<(), String> {
181 let snapshot = stores.snapshot();
182 let now = Time(k8s_openapi::jiff::Timestamp::now());
183 let (config, desired) = translate(&snapshot, &options.settings, &now);
184 (options.on_config)(config);
185 if !options.leader.load(Ordering::SeqCst) {
186 return Ok(());
187 }
188 let mut failures = Vec::new();
189
190 for ((namespace, name), service) in &desired.services {
193 let current = snapshot.services.iter().find(|s| s.namespace().as_deref() == Some(namespace.as_str()) && s.name_any() == *name);
194 if current.is_some_and(|c| super::services::same(c, service)) {
195 continue;
196 }
197 let api: Api<Service> = Api::namespaced(client.clone(), namespace);
198 let mut applied = service.clone();
199 applied.metadata.managed_fields = None;
200 let body = serde_json::json!({"apiVersion": "v1", "kind": "Service", "metadata": applied.metadata, "spec": applied.spec});
201 if let Err(e) = api.patch(name, &PatchParams::apply(super::FIELD_MANAGER).force(), &Patch::Apply(&body)).await {
202 failures.push(format!("Service {namespace}/{name}: {e}"));
203 }
204 }
205
206 if options.settings.provision.is_some() {
208 for deployment in desired.deployments.values() {
209 let (namespace, name) = (deployment.namespace().unwrap_or_default(), deployment.name_any());
210 let current = snapshot.deployments.iter().find(|d| d.namespace().as_deref() == Some(namespace.as_str()) && d.name_any() == name);
211 if current.is_some_and(|c| super::provision::same(c, deployment)) {
212 continue;
213 }
214 let api: Api<k8s_openapi::api::apps::v1::Deployment> = Api::namespaced(client.clone(), &namespace);
215 let body = serde_json::json!({"apiVersion": "apps/v1", "kind": "Deployment", "metadata": deployment.metadata, "spec": deployment.spec});
216 if let Err(e) = api.patch(&name, &PatchParams::apply(super::FIELD_MANAGER).force(), &Patch::Apply(&body)).await {
217 failures.push(format!("Deployment {namespace}/{name}: {e}"));
218 }
219 }
220 for stale in super::provision::stale(&snapshot.deployments, &desired.deployments) {
221 let api: Api<k8s_openapi::api::apps::v1::Deployment> = Api::namespaced(client.clone(), &stale.namespace().unwrap_or_default());
222 if let Err(e) = api.delete(&stale.name_any(), &Default::default()).await {
223 failures.push(format!("deleting Deployment {}: {e}", stale.name_any()));
224 }
225 }
226 }
227
228 for ((namespace, name), slice) in &desired.endpoint_slices {
229 let current = snapshot.endpoint_slices.iter().find(|s| s.namespace().as_deref() == Some(namespace.as_str()) && s.name_any() == *name);
230 if current.is_some_and(|c| super::services::same_slice(c, slice)) {
231 continue;
232 }
233 let api: Api<EndpointSlice> = Api::namespaced(client.clone(), namespace);
234 let body = serde_json::json!({
235 "apiVersion": "discovery.k8s.io/v1", "kind": "EndpointSlice",
236 "metadata": slice.metadata, "addressType": slice.address_type, "endpoints": slice.endpoints, "ports": slice.ports,
237 });
238 if let Err(e) = api.patch(name, &PatchParams::apply(super::FIELD_MANAGER).force(), &Patch::Apply(&body)).await {
239 failures.push(format!("EndpointSlice {namespace}/{name}: {e}"));
240 }
241 }
242
243 for class in &snapshot.classes {
244 let Some(status) = desired.classes.get(&class.name_any()) else { continue };
245 if class.status.as_ref() == Some(status) {
246 continue;
247 }
248 let api: Api<GatewayClass> = Api::all_with(client.clone(), &resources::gateway_classes());
249 if let Err(e) = patch_status(&api, &class.name_any(), status, None).await {
250 failures.push(e);
251 }
252 }
253 for gateway in &snapshot.gateways {
254 let namespace = gateway.namespace().unwrap_or_default();
255 let Some(status) = desired.gateways.get(&(namespace.clone(), gateway.name_any())) else { continue };
256 if gateway.status.as_ref() == Some(status) {
257 continue;
258 }
259 let api: Api<Gateway> = Api::namespaced_with(client.clone(), &namespace, &resources::gateways());
260 if let Err(e) = patch_status(&api, &gateway.name_any(), status, None).await {
261 failures.push(e);
262 }
263 }
264 for set in &snapshot.listener_sets {
265 let namespace = set.namespace().unwrap_or_default();
266 let Some(status) = desired.listener_sets.get(&(namespace.clone(), set.name_any())) else { continue };
267 if set.status.as_ref() == Some(status) {
268 continue;
269 }
270 let api: Api<ListenerSet> = Api::namespaced_with(client.clone(), &namespace, &resources::listener_sets());
271 if let Err(e) = patch_status(&api, &set.name_any(), status, None).await {
272 failures.push(e);
273 }
274 }
275 let controller_name = options.settings.controller_name.as_str();
276 for policy in &snapshot.backend_tls_policies {
277 let namespace = policy.namespace().unwrap_or_default();
278 let Some(ours) = desired.backend_tls_policies.get(&(namespace.clone(), policy.name_any())) else { continue };
279 let current = policy.status.as_ref().map(|s| s.ancestors.clone()).unwrap_or_default();
280 let mut ancestors: Vec<super::types::PolicyAncestorStatus> = current.iter().filter(|a| a.controller_name != controller_name).cloned().collect();
281 ancestors.extend(ours.iter().cloned());
282 if ancestors == current {
283 continue;
284 }
285 let api: Api<BackendTlsPolicy> = Api::namespaced_with(client.clone(), &namespace, &resources::backend_tls_policies());
286 let status = super::types::PolicyStatus { ancestors };
287 if let Err(e) = patch_status(&api, &policy.name_any(), &status, policy.resource_version()).await {
288 failures.push(e);
289 }
290 }
291 failures.extend(write_route_status(client, &snapshot.http_routes, &desired.http_routes, &resources::http_routes(), controller_name).await);
292 failures.extend(write_route_status(client, &snapshot.grpc_routes, &desired.grpc_routes, &resources::grpc_routes(), controller_name).await);
293 failures.extend(write_route_status(client, &snapshot.tls_routes, &desired.tls_routes, &resources::tls_routes(), controller_name).await);
294 failures.extend(write_route_status(client, &snapshot.tcp_routes, &desired.tcp_routes, &resources::tcp_routes(), controller_name).await);
295 failures.extend(write_route_status(client, &snapshot.udp_routes, &desired.udp_routes, &resources::udp_routes(), controller_name).await);
296 if failures.is_empty() {
297 Ok(())
298 } else {
299 Err(failures.join("; "))
300 }
301}
302
303async fn write_route_status<S>(
306 client: &Client,
307 routes: &[kube::core::Object<S, super::types::RouteStatus>],
308 desired: &std::collections::BTreeMap<(String, String), Vec<RouteParentStatus>>,
309 resource: &kube::core::ApiResource,
310 controller_name: &str,
311) -> Vec<String>
312where
313 S: Clone + DeserializeOwned + std::fmt::Debug,
314{
315 let mut failures = Vec::new();
316 for route in routes {
317 let namespace = route.namespace().unwrap_or_default();
318 let Some(ours) = desired.get(&(namespace.clone(), route.name_any())) else { continue };
319 let current = route.status.as_ref().map(|s| s.parents.clone()).unwrap_or_default();
320 let parents = merge_parents(¤t, ours, controller_name);
321 if parents == current {
322 continue;
323 }
324 let api: Api<kube::core::Object<S, super::types::RouteStatus>> = Api::namespaced_with(client.clone(), &namespace, resource);
325 let status = super::types::RouteStatus { parents };
326 if let Err(e) = patch_status(&api, &route.name_any(), &status, route.resource_version()).await {
327 failures.push(e);
328 }
329 }
330 failures
331}
332
333pub fn merge_parents(current: &[RouteParentStatus], ours: &[RouteParentStatus], controller_name: &str) -> Vec<RouteParentStatus> {
335 let mut parents: Vec<RouteParentStatus> = current.iter().filter(|p| p.controller_name != controller_name).cloned().collect();
336 parents.extend(ours.iter().cloned());
337 parents
338}
339
340async fn patch_status<K, S>(api: &Api<K>, name: &str, status: &S, resource_version: Option<String>) -> Result<(), String>
341where
342 K: Resource + Clone + DeserializeOwned + std::fmt::Debug,
343 S: serde::Serialize,
344{
345 let mut patch = serde_json::json!({ "status": status });
346 if let Some(version) = resource_version {
347 patch["metadata"] = serde_json::json!({ "resourceVersion": version });
348 }
349 api.patch_status(name, &PatchParams::apply(super::FIELD_MANAGER), &Patch::Merge(&patch))
350 .await
351 .map(|_| ())
352 .map_err(|e| format!("status of {name}: {e}"))
353}
354
355#[cfg(test)]
356mod tests;