@@ -39,7 +39,7 @@ use std::sync::{Arc, RwLock};
3939use std:: time:: Instant ;
4040
4141use k8s_openapi:: api:: core:: v1:: Secret ;
42- use kube:: { config:: Kubeconfig , Api , Client , ResourceExt } ;
42+ use kube:: { client :: ConfigExt , config:: Kubeconfig , Api , Client , ResourceExt } ;
4343use tracing:: { debug, info, warn} ;
4444
4545use crate :: constants:: { CHILD_CLIENT_CACHE_CAP , HTTP_NOT_FOUND , K8S_API_TIMEOUT_SECS } ;
@@ -453,12 +453,53 @@ impl ChildClientCache {
453453 // would stall reconciliation indefinitely.
454454 config. read_timeout = Some ( std:: time:: Duration :: from_secs ( K8S_API_TIMEOUT_SECS ) ) ;
455455 config. write_timeout = Some ( std:: time:: Duration :: from_secs ( K8S_API_TIMEOUT_SECS ) ) ;
456-
457- let child = Client :: try_from ( config) . map_err ( |e| ReconcilerError :: KubeconfigInvalid {
456+ config. connect_timeout = Some ( std:: time:: Duration :: from_secs ( K8S_API_TIMEOUT_SECS ) ) ;
457+
458+ // Build the child client BY HAND with rustls TLS-1.3 session resumption DISABLED.
459+ //
460+ // kube-rs's default `Client::try_from` resumes the TLS session (PSK) on reconnect.
461+ // A k0smotron-hosted control plane requires fresh client-certificate (mTLS) auth on
462+ // every connection and will not complete a resumed handshake that skips it — so the
463+ // connection stalls until the wire timeout ("client error (Connect) -> deadline has
464+ // elapsed") and the Node is never tainted/drained. Disabling resumption forces a full
465+ // mTLS handshake every time, which is exactly what client-go and curl do (both connect
466+ // in ~10ms). This otherwise mirrors kube's own connector stack (timeouts, base-URI and
467+ // auth layers).
468+ let make_err = |reason : String | ReconcilerError :: KubeconfigInvalid {
458469 namespace : namespace. to_string ( ) ,
459470 name : secret_name. to_string ( ) ,
460- reason : format ! ( "could not build kube::Client: {e}" ) ,
461- } ) ?;
471+ reason,
472+ } ;
473+ let mut tls = config
474+ . rustls_client_config ( )
475+ . map_err ( |e| make_err ( format ! ( "could not build rustls config: {e}" ) ) ) ?;
476+ tls. resumption = rustls:: client:: Resumption :: disabled ( ) ;
477+ let mut http = hyper_util:: client:: legacy:: connect:: HttpConnector :: new ( ) ;
478+ http. enforce_http ( false ) ;
479+ let https = hyper_rustls:: HttpsConnectorBuilder :: new ( )
480+ . with_tls_config ( tls)
481+ . https_or_http ( )
482+ . enable_http1 ( )
483+ . enable_http2 ( )
484+ . wrap_connector ( http) ;
485+ let mut connector = hyper_timeout:: TimeoutConnector :: new ( https) ;
486+ connector. set_connect_timeout ( config. connect_timeout ) ;
487+ connector. set_read_timeout ( config. read_timeout ) ;
488+ connector. set_write_timeout ( config. write_timeout ) ;
489+ let hyper_client: hyper_util:: client:: legacy:: Client < _ , kube:: client:: Body > =
490+ hyper_util:: client:: legacy:: Client :: builder ( hyper_util:: rt:: TokioExecutor :: new ( ) )
491+ . build ( connector) ;
492+ let auth = config
493+ . auth_layer ( )
494+ . map_err ( |e| make_err ( format ! ( "could not build auth layer: {e}" ) ) ) ?;
495+ let service = tower:: ServiceBuilder :: new ( )
496+ . layer ( config. base_uri_layer ( ) )
497+ . option_layer ( auth)
498+ // Box the hyper client's error so the auth / base-URI layers see a uniform
499+ // `BoxError` (mirrors kube's own `make_generic_builder`).
500+ . map_err ( tower:: BoxError :: from)
501+ . service ( hyper_client) ;
502+ let child = Client :: new ( service, config. default_namespace . clone ( ) ) ;
462503
463504 // Insert (with LRU eviction if at cap) and return. Track whether
464505 // we're replacing an existing entry for the same key (RV change
0 commit comments