Skip to content

Commit e33cb1e

Browse files
authored
feat: fall back to EndpointSlice-based node discovery for selectorless services (#25)
1 parent 00b41d1 commit e33cb1e

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

helm/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ serviceAccount:
3636
- apiGroups: [""]
3737
resources: [nodes, pods]
3838
verbs: [get, list, watch]
39+
- apiGroups: [discovery.k8s.io]
40+
resources: [endpointslices]
41+
verbs: [get, list, watch]
3942

4043
podAnnotations: {}
4144
podLabels: {}

src/main.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ use error::{RobotLBError, RobotLBResult};
2222
use futures::StreamExt;
2323
use hcloud::apis::configuration::Configuration as HCloudConfig;
2424
use k8s_openapi::{
25-
api::core::v1::{Node, Pod, Service},
25+
api::{
26+
core::v1::{Node, Pod, Service},
27+
discovery::v1::EndpointSlice,
28+
},
2629
serde_json::json,
2730
};
2831
use kube::{
@@ -181,8 +184,16 @@ async fn get_nodes_dynamically(
181184
.unwrap_or_else(|| context.client.default_namespace()),
182185
);
183186

184-
let Some(pod_selector) = svc.spec.as_ref().and_then(|spec| spec.selector.clone()) else {
185-
return Err(RobotLBError::ServiceWithoutSelector);
187+
let Some(pod_selector) = svc
188+
.spec
189+
.as_ref()
190+
.and_then(|spec| spec.selector.clone())
191+
.filter(|s| !s.is_empty())
192+
else {
193+
tracing::info!(
194+
"Service has no selector, falling back to EndpointSlice-based node discovery"
195+
);
196+
return get_nodes_from_endpointslices(svc, context).await;
186197
};
187198

188199
let label_selector = pod_selector
@@ -215,6 +226,62 @@ async fn get_nodes_dynamically(
215226
Ok(nodes)
216227
}
217228

229+
/// Get nodes from `EndpointSlice` resources associated with a Service.
230+
/// This method is used as a fallback when the Service has no selector,
231+
/// such as when `EndpointSlice` resources are managed by an external controller
232+
/// (e.g. kubevirt cloud-controller-manager).
233+
/// It discovers target nodes by reading the `nodeName` field from each endpoint.
234+
async fn get_nodes_from_endpointslices(
235+
svc: &Arc<Service>,
236+
context: &Arc<CurrentContext>,
237+
) -> RobotLBResult<Vec<Node>> {
238+
let namespace = svc
239+
.namespace()
240+
.unwrap_or_else(|| context.client.default_namespace().to_string());
241+
let eps_api = kube::Api::<EndpointSlice>::namespaced(context.client.clone(), &namespace);
242+
let eps_list = eps_api
243+
.list(&ListParams {
244+
label_selector: Some(format!(
245+
"kubernetes.io/service-name={}",
246+
svc.name_any()
247+
)),
248+
..Default::default()
249+
})
250+
.await?;
251+
252+
let target_nodes = eps_list
253+
.into_iter()
254+
.flat_map(|eps| eps.endpoints)
255+
.filter(|ep| {
256+
ep.conditions
257+
.as_ref()
258+
.and_then(|c| c.ready)
259+
.unwrap_or(true)
260+
})
261+
.filter_map(|ep| ep.node_name)
262+
.collect::<HashSet<_>>();
263+
264+
if target_nodes.is_empty() {
265+
tracing::warn!("No ready endpoints found in EndpointSlices for service");
266+
return Ok(vec![]);
267+
}
268+
269+
tracing::info!(
270+
"Discovered {} target node(s) from EndpointSlices",
271+
target_nodes.len()
272+
);
273+
274+
let nodes_api = kube::Api::<Node>::all(context.client.clone());
275+
let nodes = nodes_api
276+
.list(&ListParams::default())
277+
.await?
278+
.into_iter()
279+
.filter(|node| target_nodes.contains(&node.name_any()))
280+
.collect::<Vec<_>>();
281+
282+
Ok(nodes)
283+
}
284+
218285
/// Get nodes based on the node selector.
219286
/// This method will find the nodes based on the node selector
220287
/// from the service annotations.

0 commit comments

Comments
 (0)