@@ -22,7 +22,10 @@ use error::{RobotLBError, RobotLBResult};
2222use futures:: StreamExt ;
2323use hcloud:: apis:: configuration:: Configuration as HCloudConfig ;
2424use 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} ;
2831use 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