Skip to main content

slim_controller/
service.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::{HashMap, VecDeque};
5use std::pin::Pin;
6use std::sync::Arc;
7
8use std::time::Duration;
9
10use display_error_chain::ErrorChainExt;
11use slim_config::server::ServerConfig;
12use slim_session::subscription_manager::SubscriptionManager;
13use tokio::sync::mpsc;
14use tokio::task::JoinHandle;
15use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
16use tokio_util::sync::CancellationToken;
17use tonic::{Request, Response, Status};
18use tracing::{debug, error, info};
19
20use crate::api::proto::api::v1::control_message::Payload;
21use crate::api::proto::api::v1::controller_service_server::ControllerServiceServer;
22use crate::api::proto::api::v1::{
23    self, AuthMethod, ConnectionDetails, ConnectionDirection, ConnectionListResponse,
24    ConnectionType, RouteListResponse,
25};
26use crate::api::proto::api::v1::{
27    ConnectionEntry, ControlMessage, RouteEntry,
28    controller_service_client::ControllerServiceClient,
29    controller_service_server::ControllerService as GrpcControllerService,
30};
31use crate::errors::ControllerError;
32use prost_types::Struct;
33use slim_config::client::{
34    ClientConfig, RequiredAuthMethod, ServerConnectionConfig, TransportChannel,
35};
36use slim_config::server::AuthenticationConfig;
37use slim_datapath::api::{
38    MessageType::Link as LinkMessageType, MessageType::Subscribe,
39    MessageType::SubscriptionAck as SubscriptionAckType, MessageType::Unsubscribe,
40    ProtoMessage as DataPlaneMessage,
41};
42use slim_datapath::api::{NameId, ProtoName};
43use slim_datapath::message_processing::MessageProcessor;
44use slim_datapath::messages::utils::SlimHeaderFlags;
45use slim_datapath::tables::{ConnType, SubscriptionTable};
46
47type TxChannel = mpsc::Sender<Result<ControlMessage, Status>>;
48type TxChannels = HashMap<String, TxChannel>;
49
50/// Maximum number of queued subscription notifications
51const MAX_QUEUED_NOTIFICATIONS: usize = 1000;
52
53/// Timeout for waiting on a subscription ack from the datapath.
54const SUBSCRIPTION_ACK_TIMEOUT: Duration = Duration::from_secs(30);
55
56use slim_auth::auth_provider::AuthProvider;
57use slim_auth::traits::TokenProvider;
58
59/// Settings struct for creating a ControlPlane instance
60#[derive(Clone)]
61pub struct ControlPlaneSettings {
62    /// Node ID of this SLIM instance
63    pub id: String,
64    /// Optional group name
65    pub domain_name: Option<String>,
66    /// Server configurations
67    pub servers: Vec<ServerConfig>,
68    /// Client configurations
69    pub clients: Vec<ClientConfig>,
70    /// Client configurations for server nodes
71    pub outbound_clients: Vec<ClientConfig>,
72    /// Message processor instance
73    pub message_processor: MessageProcessor,
74    /// array of connection details used by the control
75    /// plane to store the connection settings (e.g., TLS settings).
76    pub connection_details: Vec<ConnectionDetails>,
77    /// Optional auth provider for generating registration credentials.
78    pub auth_provider: Option<AuthProvider>,
79}
80
81/// Inner structure for the controller service
82/// This structure holds the internal state of the controller service,
83/// including the ID, message processor, connections, and channels.
84/// It is normally wrapped in an Arc to allow shared ownership across multiple threads.
85struct ControllerServiceInternal {
86    /// Node ID of this SLIM instance
87    id: String,
88
89    /// optional group name
90    domain_name: Option<String>,
91
92    /// underlying message processor
93    message_processor: MessageProcessor,
94
95    /// channel to send messages into the datapath
96    tx_slim: mpsc::Sender<Result<DataPlaneMessage, Status>>,
97
98    /// channels to send control messages
99    tx_channels: parking_lot::RwLock<TxChannels>,
100
101    /// cancellation token for graceful shutdown
102    cancellation_tokens: parking_lot::RwLock<HashMap<String, CancellationToken>>,
103
104    /// drain watch channel
105    drain_watch: parking_lot::RwLock<Option<drain::Watch>>,
106
107    /// queue for pending subscription notifications when connections are down
108    pending_notifications: Arc<parking_lot::Mutex<VecDeque<ControlMessage>>>,
109
110    /// Manages pending subscription ack tracking (id generation, registration, resolution).
111    subscription_manager: SubscriptionManager,
112
113    /// connection details used by control plane to store connection settings
114    connection_details: Vec<ConnectionDetails>,
115
116    /// Maps (subscription_name, link_id) → subscription_id for route tracking
117    route_subscription_ids: parking_lot::Mutex<HashMap<(ProtoName, u64), u64>>,
118
119    /// Reverse index: link_id → conn_id for O(1) lookup.
120    /// Populated lazily on first resolve and eagerly on connection create/delete.
121    link_id_to_conn_id: parking_lot::RwLock<HashMap<String, u64>>,
122
123    /// JoinHandles for control-plane stream processing tasks, keyed by endpoint.
124    stream_handles: parking_lot::Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
125
126    /// connection details for CP reconciled outbound data-plane connections
127    outbound_clients: Vec<ClientConfig>,
128
129    /// Optional auth provider for generating registration credentials.
130    auth_provider: Option<AuthProvider>,
131}
132
133#[derive(Clone)]
134struct ControllerService {
135    /// internal service state
136    inner: Arc<ControllerServiceInternal>,
137}
138
139/// The ControlPlane service is the main entry point for the controller service.
140pub struct ControlPlane {
141    /// servers
142    servers: Vec<ServerConfig>,
143
144    /// clients
145    clients: Vec<ClientConfig>,
146
147    /// drain signal channel
148    drain_signal: parking_lot::RwLock<Option<drain::Signal>>,
149
150    /// controller
151    controller: ControllerService,
152
153    /// channel to receive message from the datapath
154    /// to be used in listen_from_data_plane
155    rx_slim_option: Option<mpsc::Receiver<Result<DataPlaneMessage, Status>>>,
156}
157
158/// ControllerServiceInternal implements Drop trait to cancel all running listeners and
159/// clean up resources.
160impl Drop for ControlPlane {
161    fn drop(&mut self) {
162        // cancel all running listeners
163        for (_endpoint, token) in self.controller.inner.cancellation_tokens.write().drain() {
164            token.cancel();
165        }
166    }
167}
168
169pub(crate) fn from_server_config(server_config: &ServerConfig) -> ConnectionDetails {
170    let mut endpoint = server_config.endpoint.clone();
171    let mut external_endpoint = None;
172    let mut spire_socket_path = None;
173    let mut trust_domain = None;
174    let mut client_config_tls_insecure: Option<bool> = None;
175    let mut remaining_fields = std::collections::BTreeMap::new();
176
177    if let Some(m) = &server_config.metadata {
178        for (k, v) in &m.inner {
179            match k.as_str() {
180                "local_endpoint" => {
181                    if let Some(s) = v.as_str()
182                        && !s.is_empty()
183                    {
184                        endpoint = s.to_string();
185                    }
186                }
187                "external_endpoint" => {
188                    if let Some(s) = v.as_str()
189                        && !s.is_empty()
190                    {
191                        external_endpoint = Some(s.to_string());
192                    }
193                }
194                "spire_socket_path" => {
195                    if let Some(s) = v.as_str()
196                        && !s.is_empty()
197                    {
198                        spire_socket_path = Some(s.to_string());
199                    }
200                }
201                "trust_domain" => {
202                    if let Some(s) = v.as_str()
203                        && !s.is_empty()
204                    {
205                        trust_domain = Some(s.to_string());
206                    }
207                }
208                "client_config" => {
209                    // When client_config is present it owns the remote TLS decision;
210                    // the server-level tls.insecure only governs local (intra-cluster)
211                    // connections and must not be used as a fallback.
212                    // Default to insecure=true so that absence of a tls section means
213                    // no TLS rather than inheriting the server-level flag.
214                    if let Some(cc) = v.as_map() {
215                        let insecure = cc
216                            .inner
217                            .get("tls")
218                            .and_then(|v| v.as_map())
219                            .and_then(|tls| tls.inner.get("insecure"))
220                            .and_then(|v| v.as_bool())
221                            .unwrap_or(true);
222                        client_config_tls_insecure = Some(insecure);
223                    }
224                    remaining_fields.insert(k.clone(), prost_types::Value::from(v));
225                }
226                _ => {
227                    remaining_fields.insert(k.clone(), prost_types::Value::from(v));
228                }
229            }
230        }
231    }
232
233    // tls_required is true when:
234    // - client_config is present and client_config.tls.insecure is false (remote TLS config
235    //   wins; absence of tls section defaults to insecure=true, i.e. no TLS)
236    // - or client_config is absent and the server-level TLS is not insecure (fallback)
237    // - or a SPIRE socket path is configured
238    let tls_required = client_config_tls_insecure
239        .map(|insecure| !insecure)
240        .unwrap_or(!server_config.tls_setting.insecure)
241        || spire_socket_path.is_some();
242    // AuthenticationConfig::Spire is gated to non-windows in slim-config.
243    #[cfg(not(target_family = "windows"))]
244    let auth_is_spire = matches!(server_config.auth, AuthenticationConfig::Spire(_));
245    #[cfg(target_family = "windows")]
246    let auth_is_spire = false;
247    let auth_method = if spire_socket_path.is_some() || trust_domain.is_some() || auth_is_spire {
248        AuthMethod::Spire
249    } else {
250        match &server_config.auth {
251            AuthenticationConfig::Basic(_) => AuthMethod::Basic,
252            AuthenticationConfig::Jwt(_) => AuthMethod::Jwt,
253            _ => AuthMethod::None,
254        }
255    } as i32;
256    let metadata = if remaining_fields.is_empty() {
257        None
258    } else {
259        Some(Struct {
260            fields: remaining_fields,
261        })
262    };
263
264    ConnectionDetails {
265        endpoint,
266        external_endpoint,
267        tls_required,
268        auth_method,
269        spire_trust_domain: trust_domain,
270        metadata,
271    }
272}
273
274/// ControlPlane implements the service trait for the controller service.
275impl ControlPlane {
276    /// Create a new ControlPlane service instance
277    /// This function initializes the ControlPlane with the given ID, servers, clients, and message processor.
278    /// It also sets up the internal state, including the connections and channels.
279    /// # Arguments
280    /// * `id` - The ID of the SLIM instance.
281    /// * `servers` - A vector of server configurations.
282    /// * `clients` - A vector of client configurations.
283    /// * `drain_rx` - A drain watch channel for graceful shutdown.
284    /// * `message_processor` - An Arc to the message processor instance.
285    /// * `pubsub_servers` - A slice of server configurations for pub/sub connections.
286    /// # Returns
287    /// A new instance of ControlPlane.
288    pub fn new(config: ControlPlaneSettings) -> Self {
289        // create local connection with the message processor
290        let (_, tx_slim, rx_slim) = config
291            .message_processor
292            .register_local_connection(true)
293            .unwrap();
294
295        let (signal, watch) = drain::channel();
296
297        ControlPlane {
298            servers: config.servers,
299            clients: config.clients,
300            controller: ControllerService {
301                inner: Arc::new(ControllerServiceInternal {
302                    id: config.id,
303                    domain_name: config.domain_name,
304                    message_processor: config.message_processor,
305                    subscription_manager: SubscriptionManager::new(tx_slim.clone()),
306                    tx_slim,
307                    tx_channels: parking_lot::RwLock::new(HashMap::new()),
308                    cancellation_tokens: parking_lot::RwLock::new(HashMap::new()),
309                    drain_watch: parking_lot::RwLock::new(Some(watch)),
310                    pending_notifications: Arc::new(parking_lot::Mutex::new(VecDeque::new())),
311                    connection_details: config.connection_details,
312                    route_subscription_ids: parking_lot::Mutex::new(HashMap::new()),
313                    link_id_to_conn_id: parking_lot::RwLock::new(HashMap::new()),
314                    stream_handles: parking_lot::Mutex::new(HashMap::new()),
315                    outbound_clients: config.outbound_clients,
316                    auth_provider: config.auth_provider,
317                }),
318            },
319            drain_signal: parking_lot::RwLock::new(Some(signal)),
320            rx_slim_option: Some(rx_slim),
321        }
322    }
323
324    /// Take an existing ControlPlane instance and return a new one with the provided clients.
325    pub fn with_clients(mut self, clients: Vec<ClientConfig>) -> Self {
326        self.clients = clients;
327        self
328    }
329
330    /// Take an existing ControlPlane instance and return a new one with the provided servers.
331    pub fn with_servers(mut self, servers: Vec<ServerConfig>) -> Self {
332        self.servers = servers;
333        self
334    }
335
336    /// Run the clients and servers of the ControlPlane service.
337    /// This function starts all the servers and clients defined in the ControlPlane.
338    /// # Returns
339    /// A Result indicating success or failure of the operation.
340    /// # Errors
341    /// If there is an error starting any of the servers or clients, it will return a ControllerError.
342    pub async fn run(&mut self) -> Result<(), ControllerError> {
343        let rx = self
344            .rx_slim_option
345            .take()
346            .ok_or(ControllerError::AlreadyStarted)?;
347
348        // Collect servers to avoid borrowing self both mutably and immutably
349        let servers = self.servers.clone();
350        let clients = self.clients.clone();
351
352        // run all servers
353        for server in servers {
354            self.run_server(server).await?;
355        }
356
357        // run all clients
358        for client in clients {
359            self.run_client(client).await?;
360        }
361
362        self.listen_from_data_plane(rx).await?;
363
364        Ok(())
365    }
366
367    pub async fn deregister(&self) -> Result<(), ControllerError> {
368        let node_id = self.controller.inner.id.clone();
369        let deregister_msg = ControlMessage {
370            message_id: uuid::Uuid::new_v4().to_string(),
371            payload: Some(Payload::DeregisterNodeRequest(v1::DeregisterNodeRequest {
372                node: Some(v1::Node { id: node_id }),
373            })),
374        };
375        let channels: Vec<(String, TxChannel)> = self
376            .controller
377            .inner
378            .tx_channels
379            .read()
380            .iter()
381            .map(|(ep, tx)| (ep.clone(), tx.clone()))
382            .collect();
383        for (endpoint, tx) in channels {
384            if let Err(e) = tx.send(Ok(deregister_msg.clone())).await {
385                error!(%endpoint, error = %e, "failed to send deregister request");
386            }
387        }
388        Ok(())
389    }
390
391    pub async fn shutdown(&self) -> Result<(), ControllerError> {
392        // Get signal drain
393        let signal = self
394            .drain_signal
395            .write()
396            .take()
397            .ok_or(ControllerError::AlreadyStopped)?;
398
399        // Stop everything using the cancellation tokens
400        self.controller
401            .inner
402            .cancellation_tokens
403            .write()
404            .drain()
405            .for_each(|(endpoint, token)| {
406                info!(%endpoint, "stopping");
407                token.cancel();
408            });
409
410        // Drop watch channel
411        self.controller.inner.drain_watch.write().take();
412
413        // Wait for drain to complete
414        signal.drain().await;
415
416        Ok(())
417    }
418
419    async fn listen_from_data_plane(
420        &mut self,
421        mut rx: mpsc::Receiver<Result<DataPlaneMessage, Status>>,
422    ) -> Result<(), ControllerError> {
423        let cancellation_token = CancellationToken::new();
424        let cancellation_token_clone = cancellation_token.clone();
425
426        self.controller
427            .inner
428            .cancellation_tokens
429            .write()
430            .insert("DATA_PLANE".to_string(), cancellation_token_clone);
431
432        let clients = self.clients.clone();
433        let controller = self.controller.clone();
434
435        // Get a drain watch clone
436        let watch = self.controller.drain_watch()?;
437
438        debug!("Starting data plane listener");
439        tokio::spawn(async move {
440            let mut drain_fut = std::pin::pin!(watch.signaled());
441            loop {
442                tokio::select! {
443                    next = rx.recv() => {
444                        match next {
445                            Some(res) => {
446                                match res {
447                                    Ok(msg) => {
448                                        debug!("Received message {:?} from data plane, forwarding to control plane", msg);
449                                        match msg.get_type() {
450                                            Subscribe(_) => {
451                                                controller.handle_subscribe_message(msg.get_dst(), &clients).await;
452                                            }
453                                            Unsubscribe(_) => {
454                                                controller.handle_unsubscribe_message(msg.get_dst(), &clients).await;
455                                            }
456                                            SubscriptionAckType(_) => {
457                                                controller.inner.subscription_manager.resolve_ack(msg.get_subscription_ack());
458                                            }
459                                            LinkMessageType(link) => {
460                                                controller.handle_link_received(link, &clients).await;
461                                            }
462                                            _ => {
463                                                debug!("Ignoring unexpected message type from dataplane: {:?}", msg.get_type());
464                                            }
465                                        }
466                                    }
467                                    Err(e) => {
468                                        error!(error = %e.chain(), "received error from the data plane");
469                                        continue;
470                                    }
471                                }
472                            }
473                            None => {
474                                debug!("Data plane receiver channel closed.");
475                                break;
476                            }
477                        }
478                    }
479                    _ = cancellation_token.cancelled() => {
480                        debug!("shutting down stream on cancellation token");
481                        break;
482                    }
483                    _ = &mut drain_fut => {
484                        debug!("shutting down stream on drain");
485                        break;
486                    }
487                }
488            }
489        });
490        Ok(())
491    }
492
493    /// Stop the ControlPlane service.
494    /// This function stops all running listeners and cancels any ongoing operations.
495    /// It cleans up the internal state and ensures that all resources are released properly.
496    pub fn stop(&mut self) {
497        info!("stopping controller service");
498
499        // cancel all running listeners
500        for (endpoint, token) in self.controller.inner.cancellation_tokens.write().drain() {
501            info!(%endpoint, "stopping");
502            token.cancel();
503        }
504    }
505
506    /// Run a client configuration.
507    /// This function connects to the control plane using the provided client configuration.
508    /// It checks if the client is already running and if not, it starts a new connection.
509    async fn run_client(&mut self, client: ClientConfig) -> Result<(), ControllerError> {
510        if self
511            .controller
512            .inner
513            .cancellation_tokens
514            .read()
515            .contains_key(&client.endpoint)
516        {
517            return Err(ControllerError::ClientAlreadyRunning(client.endpoint));
518        }
519
520        let cancellation_token = CancellationToken::new();
521
522        let tx = self
523            .controller
524            .connect(client.clone(), cancellation_token.clone())
525            .await?;
526
527        // Store the cancellation token in the controller service
528        self.controller
529            .inner
530            .cancellation_tokens
531            .write()
532            .insert(client.endpoint.clone(), cancellation_token);
533
534        // Store the sender in the tx_channels map
535        self.controller
536            .inner
537            .tx_channels
538            .write()
539            .insert(client.endpoint.clone(), tx);
540
541        // return the sender for control messages
542        Ok(())
543    }
544
545    /// Run a server configuration.
546    /// This function starts a server using the provided server configuration.
547    /// It checks if the server is already running and if not, it starts a new server.
548    pub async fn run_server(&mut self, config: ServerConfig) -> Result<(), ControllerError> {
549        // Check if the server is already running
550        if self
551            .controller
552            .inner
553            .cancellation_tokens
554            .read()
555            .contains_key(&config.endpoint)
556        {
557            error!(endpoint = config.endpoint, "server is already running",);
558            return Err(ControllerError::ServerAlreadyRunning(config.endpoint));
559        }
560
561        let token = config
562            .run_grpc_server(
563                &[ControllerServiceServer::new(self.controller.clone())],
564                self.controller.drain_watch()?,
565            )
566            .await?;
567
568        // Store the cancellation token in the controller service
569        self.controller
570            .inner
571            .cancellation_tokens
572            .write()
573            .insert(config.endpoint.clone(), token.clone());
574
575        info!(%config.endpoint, "started controlplane server");
576
577        Ok(())
578    }
579}
580
581impl ControllerService {
582    fn resolve_connection_by_link_id(&self, link_id: &str) -> Result<Option<u64>, String> {
583        let cached = self.inner.link_id_to_conn_id.read().get(link_id).copied();
584        if let Some(conn_id) = cached {
585            if self
586                .inner
587                .message_processor
588                .connection_table()
589                .get(conn_id)
590                .is_some_and(|conn| conn.link_id().as_deref() == Some(link_id))
591            {
592                return Ok(Some(conn_id));
593            }
594            self.inner.link_id_to_conn_id.write().remove(link_id);
595        }
596
597        let mut resolved: Option<u64> = None;
598        self.inner
599            .message_processor
600            .connection_table()
601            .for_each(|id, conn| {
602                if conn.link_id().as_deref() == Some(link_id) && resolved.is_none() {
603                    resolved = Some(id);
604                }
605            });
606
607        if let Some(conn_id) = resolved {
608            self.inner
609                .link_id_to_conn_id
610                .write()
611                .insert(link_id.to_string(), conn_id);
612        }
613
614        Ok(resolved)
615    }
616
617    fn disconnect_connection_by_link_id(&self, link_id: &str) -> Result<(), String> {
618        if link_id.trim().is_empty() {
619            return Err("link_id cannot be empty".to_string());
620        }
621
622        let conn_id = match self.resolve_connection_by_link_id(link_id)? {
623            Some(id) => id,
624            None => {
625                return Err(format!("Connection with link_id {} not found", link_id));
626            }
627        };
628
629        if let Err(e) = self.inner.message_processor.disconnect(conn_id) {
630            // Best-effort delete: local/control-plane connections can lack config_data.
631            info!(
632                link_id = %link_id,
633                conn_id,
634                error = %e,
635                "Disconnect returned an error; continuing delete flow"
636            );
637        }
638
639        self.inner.link_id_to_conn_id.write().remove(link_id);
640        self.inner
641            .route_subscription_ids
642            .lock()
643            .retain(|(_name, cid), _| *cid != conn_id);
644
645        info!(link_id = %link_id, conn_id, "Successfully deleted connection by link_id");
646        Ok(())
647    }
648
649    fn resolve_route_connection(&self, route: &v1::Route) -> Result<Option<u64>, String> {
650        if let Some(link_id) = &route.link_id {
651            let trimmed = link_id.trim();
652            if !trimmed.is_empty() {
653                return self.resolve_connection_by_link_id(trimmed);
654            }
655        }
656
657        Ok(None)
658    }
659
660    async fn diff_connections(
661        &self,
662        desired_connections: &[v1::Connection],
663    ) -> Vec<v1::ConnectionAck> {
664        let mut connections_status = Vec::new();
665
666        let desired_link_ids: std::collections::HashSet<String> = desired_connections
667            .iter()
668            .map(|c| c.link_id.clone())
669            .collect();
670
671        let mut live_outgoing_link_ids: Vec<String> = Vec::new();
672        self.inner
673            .message_processor
674            .connection_table()
675            .for_each(|_id, conn| {
676                // Only manage Remote connections (CP-managed inter-group links).
677                // Peer connections are managed by the peer sync system and must not
678                // be deleted by the reconciler.
679                if conn.is_outgoing()
680                    && conn.connection_type() == slim_datapath::tables::ConnType::Remote
681                    && let Some(lid) = conn.link_id()
682                    && !lid.is_empty()
683                {
684                    live_outgoing_link_ids.push(lid);
685                }
686            });
687
688        for link_id in &live_outgoing_link_ids {
689            if desired_link_ids.contains(link_id) {
690                continue;
691            }
692            info!(link_id = %link_id, "desired state: removing connection");
693            let mut success = true;
694            let mut error_msg = String::new();
695            if let Err(err) = self.disconnect_connection_by_link_id(link_id) {
696                success = false;
697                error_msg = err;
698            }
699            connections_status.push(v1::ConnectionAck {
700                link_id: link_id.clone(),
701                success,
702                error_msg,
703            });
704        }
705
706        for conn in desired_connections {
707            let link_id = &conn.link_id;
708            if link_id.is_empty() {
709                continue;
710            }
711
712            let already_exists = match self.resolve_connection_by_link_id(link_id) {
713                Ok(Some(_)) => true,
714                Ok(None) => false,
715                Err(err) => {
716                    connections_status.push(v1::ConnectionAck {
717                        link_id: link_id.clone(),
718                        success: false,
719                        error_msg: err,
720                    });
721                    continue;
722                }
723            };
724
725            if already_exists {
726                connections_status.push(v1::ConnectionAck {
727                    link_id: link_id.clone(),
728                    success: true,
729                    error_msg: String::new(),
730                });
731                continue;
732            }
733
734            info!(?conn, "desired state: creating connection");
735            let mut success = true;
736            let mut error_msg = String::new();
737
738            match serde_json::from_str::<ServerConnectionConfig>(&conn.config_data) {
739                Err(e) => {
740                    success = false;
741                    error_msg = format!("Failed to parse config: {}", e);
742                }
743                Ok(server_config) => {
744                    let mut client_config = self
745                        .inner
746                        .outbound_clients
747                        .iter()
748                        .find(|c| c.endpoint == server_config.endpoint)
749                        .cloned()
750                        .unwrap_or_default();
751                    match client_config.merge_server_requirements(&server_config) {
752                        Err(err) => {
753                            success = false;
754                            error_msg = format!(
755                                "Failed to merge connection config to client config: {}",
756                                err
757                            );
758                        }
759                        Ok(()) => {
760                            if matches!(
761                                server_config.auth_method,
762                                // Spire config it not mandatory. If not set falls back to default
763                                // socket path.
764                                RequiredAuthMethod::Basic | RequiredAuthMethod::Jwt
765                            ) && !self
766                                .inner
767                                .outbound_clients
768                                .iter()
769                                .any(|c| c.endpoint == server_config.endpoint)
770                            {
771                                success = false;
772                                error_msg = format!(
773                                    "no local credentials configured for {}",
774                                    server_config.endpoint
775                                );
776                            }
777                            if success {
778                                client_config.link_id = link_id.clone();
779                                client_config.connection_type = ConnType::Remote;
780                                match self
781                                    .inner
782                                    .message_processor
783                                    .connect(client_config, None, None)
784                                    .await
785                                {
786                                    Err(e) => {
787                                        success = false;
788                                        error_msg = format!("Connection failed: {}", e);
789                                    }
790                                    Ok(conn_id) => {
791                                        self.inner
792                                            .link_id_to_conn_id
793                                            .write()
794                                            .insert(link_id.clone(), conn_id.1);
795                                        info!(
796                                            link_id = %link_id,
797                                            "Successfully created connection"
798                                        );
799                                    }
800                                }
801                            }
802                        }
803                    }
804                }
805            }
806
807            connections_status.push(v1::ConnectionAck {
808                link_id: link_id.clone(),
809                success,
810                error_msg,
811            });
812        }
813
814        connections_status
815    }
816
817    fn resolve_desired_routes<'a>(
818        &self,
819        desired_routes: &'a [v1::Route],
820    ) -> (HashMap<(ProtoName, u64), &'a v1::Route>, Vec<v1::RouteAck>) {
821        type SubKey = (ProtoName, u64);
822        let mut desired_subs: HashMap<SubKey, &v1::Route> = HashMap::new();
823        let mut failures: Vec<v1::RouteAck> = Vec::new();
824
825        for sub in desired_routes {
826            match self.resolve_route_connection(sub) {
827                Ok(Some(conn_id)) => {
828                    let name = sub.name.clone().unwrap();
829                    desired_subs.insert((name, conn_id), sub);
830                }
831                Ok(None) => {
832                    failures.push(v1::RouteAck {
833                        route: Some(sub.clone()),
834                        success: false,
835                        error_msg: "connection not found".to_string(),
836                    });
837                }
838                Err(err) => {
839                    failures.push(v1::RouteAck {
840                        route: Some(sub.clone()),
841                        success: false,
842                        error_msg: err,
843                    });
844                }
845            }
846        }
847
848        (desired_subs, failures)
849    }
850
851    async fn delete_stale_subscriptions(
852        &self,
853        desired_subs: &HashMap<(ProtoName, u64), &v1::Route>,
854    ) -> Vec<v1::RouteAck> {
855        let active_subs: Vec<((ProtoName, u64), u64)> = self
856            .inner
857            .route_subscription_ids
858            .lock()
859            .iter()
860            .map(|((name, conn_id), sub_id)| ((name.clone(), *conn_id), *sub_id))
861            .collect();
862
863        let stale: Vec<_> = active_subs
864            .into_iter()
865            .filter(|((name, conn_id), _)| !desired_subs.contains_key(&(name.clone(), *conn_id)))
866            .collect();
867
868        let futs = stale.iter().map(|((name, conn_id), sub_id)| {
869            let name = name.clone();
870            let conn_id = *conn_id;
871            let sub_id = *sub_id;
872            async move {
873                let conn_alive = self
874                    .inner
875                    .message_processor
876                    .connection_table()
877                    .get(conn_id)
878                    .is_some();
879
880                let (success, error_msg) = if conn_alive {
881                    let source = name.clone().with_id(0);
882                    let unsub_msg = DataPlaneMessage::builder()
883                        .source(source)
884                        .destination(name.clone())
885                        .identity("")
886                        .flags(SlimHeaderFlags::default().with_recv_from(conn_id))
887                        .build_unsubscribe()
888                        .unwrap();
889
890                    match self
891                        .send_unsubscribe_message_with_ack(unsub_msg, sub_id)
892                        .await
893                    {
894                        Ok(()) => (true, String::new()),
895                        Err(err) => (false, format!("Failed to unsubscribe: {}", err)),
896                    }
897                } else {
898                    (true, String::new())
899                };
900
901                (name, conn_id, success, error_msg)
902            }
903        });
904
905        let results = futures::future::join_all(futs).await;
906
907        let mut routes_status = Vec::with_capacity(results.len());
908        for (name, conn_id, success, error_msg) in results {
909            if success {
910                self.inner
911                    .route_subscription_ids
912                    .lock()
913                    .remove(&(name.clone(), conn_id));
914            }
915
916            routes_status.push(v1::RouteAck {
917                route: Some(v1::Route {
918                    name: Some(name.clone()),
919                    link_id: None,
920                    direction: None,
921                }),
922                success,
923                error_msg,
924            });
925        }
926
927        routes_status
928    }
929
930    async fn create_new_subscriptions(
931        &self,
932        desired_subs: &HashMap<(ProtoName, u64), &v1::Route>,
933    ) -> Vec<v1::RouteAck> {
934        let mut routes_status = Vec::new();
935
936        let to_create: Vec<((ProtoName, u64), v1::Route)> = desired_subs
937            .iter()
938            .filter(|((name, conn_id), _)| {
939                !self
940                    .inner
941                    .route_subscription_ids
942                    .lock()
943                    .contains_key(&(name.clone(), *conn_id))
944            })
945            .map(|((name, conn_id), sub)| ((name.clone(), *conn_id), (*sub).clone()))
946            .collect();
947
948        // Report already-active subscriptions as success.
949        for ((name, conn_id), sub) in desired_subs {
950            let dominated = to_create
951                .iter()
952                .any(|((n, c), _)| n == name && *c == *conn_id);
953            if !dominated {
954                routes_status.push(v1::RouteAck {
955                    route: Some((*sub).clone()),
956                    success: true,
957                    error_msg: String::new(),
958                });
959            }
960        }
961
962        let futs = to_create.iter().map(|((name, conn_id), sub)| {
963            let name = name.clone();
964            let conn_id = *conn_id;
965            let sub = sub.clone();
966            async move {
967                let source = name.clone().with_id(0);
968                let sub_msg = DataPlaneMessage::builder()
969                    .source(source)
970                    .destination(name.clone())
971                    .identity("")
972                    .flags(SlimHeaderFlags::default().with_recv_from(conn_id))
973                    .build_subscribe()
974                    .unwrap();
975
976                let result = self.send_subscribe_message_with_ack(sub_msg).await;
977                (name, conn_id, sub, result)
978            }
979        });
980
981        let results = futures::future::join_all(futs).await;
982
983        for (name, conn_id, sub, result) in results {
984            let (success, error_msg) = match result {
985                Ok(subscription_id) => {
986                    self.inner
987                        .route_subscription_ids
988                        .lock()
989                        .insert((name, conn_id), subscription_id);
990                    info!(?sub, "desired state: created route");
991                    (true, String::new())
992                }
993                Err(err) => (false, format!("Failed to subscribe: {}", err)),
994            };
995
996            routes_status.push(v1::RouteAck {
997                route: Some(sub),
998                success,
999                error_msg,
1000            });
1001        }
1002
1003        routes_status
1004    }
1005
1006    /// Handle new control messages.
1007    async fn handle_new_control_message(
1008        &self,
1009        msg: ControlMessage,
1010        tx: &mpsc::Sender<Result<ControlMessage, Status>>,
1011    ) -> Result<(), ControllerError> {
1012        match msg.payload {
1013            Some(ref payload) => {
1014                match payload {
1015                    Payload::ConfigCommand(config) => {
1016                        let (connections_status, routes_status) = if config.reconcile {
1017                            let connections_status =
1018                                self.diff_connections(&config.connections_to_create).await;
1019                            let (desired_subs, mut routes_status) =
1020                                self.resolve_desired_routes(&config.routes_to_set);
1021                            routes_status
1022                                .extend(self.delete_stale_subscriptions(&desired_subs).await);
1023                            routes_status
1024                                .extend(self.create_new_subscriptions(&desired_subs).await);
1025                            (connections_status, routes_status)
1026                        } else {
1027                            let mut connections_status = Vec::new();
1028                            let mut routes_status = Vec::new();
1029
1030                            // Process connections to delete by link_id.
1031                            for link_id in &config.connections_to_delete {
1032                                info!(link_id = %link_id, "received a connection to delete");
1033                                let mut connection_success = true;
1034                                let mut connection_error_msg = String::new();
1035
1036                                if let Err(err) = self.disconnect_connection_by_link_id(link_id) {
1037                                    connection_success = false;
1038                                    connection_error_msg = err;
1039                                }
1040
1041                                connections_status.push(v1::ConnectionAck {
1042                                    link_id: link_id.clone(),
1043                                    success: connection_success,
1044                                    error_msg: connection_error_msg,
1045                                });
1046                            }
1047
1048                            // Process connections to create
1049                            for conn in &config.connections_to_create {
1050                                info!(?conn, "received a connection to create");
1051                                let mut connection_success = true;
1052                                let mut connection_error_msg = String::new();
1053
1054                                match serde_json::from_str::<ClientConfig>(&conn.config_data) {
1055                                    Err(e) => {
1056                                        connection_success = false;
1057                                        connection_error_msg =
1058                                            format!("Failed to parse config: {}", e);
1059                                    }
1060                                    Ok(client_config) => {
1061                                        let client_endpoint = &client_config.endpoint;
1062                                        let requested_link_id =
1063                                            if client_config.link_id.trim().is_empty() {
1064                                                String::new()
1065                                            } else {
1066                                                client_config.link_id.clone()
1067                                            };
1068                                        let mut existing_conn_for_link_id = false;
1069
1070                                        if !requested_link_id.is_empty() {
1071                                            match self
1072                                                .resolve_connection_by_link_id(&requested_link_id)
1073                                            {
1074                                                Err(err) => {
1075                                                    connection_success = false;
1076                                                    connection_error_msg = err;
1077                                                }
1078                                                Ok(Some(conn_id)) => {
1079                                                    existing_conn_for_link_id = true;
1080                                                    self.inner
1081                                                        .link_id_to_conn_id
1082                                                        .write()
1083                                                        .insert(requested_link_id.clone(), conn_id);
1084                                                    info!(
1085                                                        link_id = %requested_link_id,
1086                                                        conn_id,
1087                                                        "Connection already exists for link_id"
1088                                                    );
1089                                                }
1090                                                Ok(None) => {}
1091                                            }
1092                                        }
1093
1094                                        if connection_success && !existing_conn_for_link_id {
1095                                            match self
1096                                                .inner
1097                                                .message_processor
1098                                                .connect(client_config.clone(), None, None)
1099                                                .await
1100                                            {
1101                                                Err(e) => {
1102                                                    connection_success = false;
1103                                                    connection_error_msg =
1104                                                        format!("Connection failed: {}", e);
1105                                                }
1106                                                Ok(conn_id) => {
1107                                                    if !requested_link_id.is_empty() {
1108                                                        self.inner
1109                                                            .link_id_to_conn_id
1110                                                            .write()
1111                                                            .insert(
1112                                                                requested_link_id.clone(),
1113                                                                conn_id.1,
1114                                                            );
1115                                                    }
1116                                                    info!(
1117                                                        endpoint = %client_endpoint, "Successfully created connection",
1118                                                    );
1119                                                }
1120                                            }
1121                                        }
1122                                    }
1123                                }
1124
1125                                // Add connection status
1126                                connections_status.push(v1::ConnectionAck {
1127                                    link_id: conn.link_id.clone(),
1128                                    success: connection_success,
1129                                    error_msg: connection_error_msg,
1130                                });
1131                            }
1132
1133                            // Process routes to set
1134                            for route in &config.routes_to_set {
1135                                let mut route_success = true;
1136                                let mut route_error_msg = String::new();
1137
1138                                let conn = self.resolve_route_connection(route);
1139
1140                                if let Ok(Some(conn)) = conn {
1141                                    let name = route.name.clone().unwrap();
1142                                    let source = name.clone().with_id(NameId::NULL_COMPONENT);
1143
1144                                    let msg = DataPlaneMessage::builder()
1145                                        .source(source.clone())
1146                                        .destination(name.clone())
1147                                        .identity("")
1148                                        .flags(SlimHeaderFlags::default().with_recv_from(conn))
1149                                        .build_subscribe()
1150                                        .unwrap();
1151
1152                                    match self.send_subscribe_message_with_ack(msg).await {
1153                                        Ok(subscription_id) => {
1154                                            // Store the subscription_id for later unsubscription
1155                                            self.inner
1156                                                .route_subscription_ids
1157                                                .lock()
1158                                                .insert((name.clone(), conn), subscription_id);
1159                                            info!(?route, "Successfully created route");
1160                                        }
1161                                        Err(err) => {
1162                                            route_success = false;
1163                                            route_error_msg =
1164                                                format!("Failed to subscribe: {}", err);
1165                                        }
1166                                    }
1167                                } else {
1168                                    route_success = false;
1169                                    route_error_msg = match conn {
1170                                        Ok(None) => {
1171                                            format!(
1172                                                "Connection with link_id {} not found",
1173                                                route.link_id.as_deref().unwrap_or("<none>")
1174                                            )
1175                                        }
1176                                        Err(err) => err,
1177                                        _ => "unknown connection lookup error".to_string(),
1178                                    };
1179                                }
1180
1181                                // Add route status
1182                                routes_status.push(v1::RouteAck {
1183                                    route: Some(route.clone()),
1184                                    success: route_success,
1185                                    error_msg: route_error_msg,
1186                                });
1187                            }
1188
1189                            // Process routes to delete
1190                            for route in &config.routes_to_delete {
1191                                let mut route_success = true;
1192                                let mut route_error_msg = String::new();
1193
1194                                let conn = self.resolve_route_connection(route);
1195
1196                                if let Ok(Some(conn)) = conn {
1197                                    let name = route.name.clone().unwrap();
1198                                    let source = name.clone().with_id(NameId::NULL_COMPONENT);
1199
1200                                    let msg = DataPlaneMessage::builder()
1201                                        .source(source.clone())
1202                                        .destination(name.clone())
1203                                        .identity("")
1204                                        .flags(SlimHeaderFlags::default().with_recv_from(conn))
1205                                        .build_unsubscribe()
1206                                        .unwrap();
1207
1208                                    let sub_id = self
1209                                        .inner
1210                                        .route_subscription_ids
1211                                        .lock()
1212                                        .remove(&(name.clone(), conn));
1213                                    let unsubscribe_result = match sub_id {
1214                                        Some(subscription_id) => {
1215                                            self.send_unsubscribe_message_with_ack(
1216                                                msg,
1217                                                subscription_id,
1218                                            )
1219                                            .await
1220                                        }
1221                                        None => {
1222                                            // No tracking ID in the map (e.g. after a CP restart
1223                                            // or for orphan cleanup from the reconciler).  Generate
1224                                            // a fresh ID — the datapath locates the subscription
1225                                            // by Name+connection, not by ID; the ID is only used
1226                                            // for ack correlation.
1227                                            let (ack_id, ack_rx) =
1228                                                self.inner.subscription_manager.register_ack();
1229                                            let mut fresh_msg = msg;
1230                                            fresh_msg.set_subscription_id(ack_id);
1231                                            if let Err(e) =
1232                                                self.send_control_message(fresh_msg).await
1233                                            {
1234                                                self.inner.subscription_manager.cancel_ack(ack_id);
1235                                                Err(format!("datapath send error: {}", e.chain()))
1236                                            } else {
1237                                                match tokio::time::timeout(
1238                                                    SUBSCRIPTION_ACK_TIMEOUT,
1239                                                    ack_rx,
1240                                                )
1241                                                .await
1242                                                {
1243                                                    Ok(Ok(Ok(()))) => Ok(()),
1244                                                    Ok(Ok(Err(err))) => Err(err.to_string()),
1245                                                    Ok(Err(_)) => {
1246                                                        Err("subscription ack channel closed"
1247                                                            .to_string())
1248                                                    }
1249                                                    Err(_) => {
1250                                                        self.inner
1251                                                            .subscription_manager
1252                                                            .cancel_ack(ack_id);
1253                                                        Err("subscription ack timed out"
1254                                                            .to_string())
1255                                                    }
1256                                                }
1257                                            }
1258                                        }
1259                                    };
1260                                    if let Err(err) = unsubscribe_result {
1261                                        route_success = false;
1262                                        route_error_msg = format!("Failed to unsubscribe: {}", err);
1263                                    } else {
1264                                        info!(?route, "Successfully deleted route");
1265                                    }
1266                                } else {
1267                                    route_success = false;
1268                                    route_error_msg = match conn {
1269                                        Ok(None) => {
1270                                            format!(
1271                                                "Connection with link_id {} not found",
1272                                                route.link_id.as_deref().unwrap_or("<none>")
1273                                            )
1274                                        }
1275                                        Err(err) => err,
1276                                        _ => "unknown connection lookup error".to_string(),
1277                                    };
1278                                }
1279
1280                                // Add route status (for deletion)
1281                                routes_status.push(v1::RouteAck {
1282                                    route: Some(route.clone()),
1283                                    success: route_success,
1284                                    error_msg: route_error_msg,
1285                                });
1286                            }
1287
1288                            (connections_status, routes_status)
1289                        };
1290
1291                        let config_ack = v1::ConfigurationCommandAck {
1292                            original_message_id: msg.message_id.clone(),
1293                            connections_status,
1294                            routes_status,
1295                        };
1296
1297                        let reply = ControlMessage {
1298                            message_id: uuid::Uuid::new_v4().to_string(),
1299                            payload: Some(Payload::ConfigCommandAck(config_ack)),
1300                        };
1301
1302                        if let Err(e) = tx.send(Ok(reply)).await {
1303                            error!(error = %e.chain(), "failed to send ConfigurationCommandAck");
1304                        }
1305
1306                        info!(
1307                            connections = %config.connections_to_create.len(),
1308                            connections_to_delete = %config.connections_to_delete.len(),
1309                            routes_to_set = %config.routes_to_set.len(),
1310                            routes_to_del = %config.routes_to_delete.len(),
1311                            "Processed ConfigurationCommand"
1312                        );
1313                    }
1314                    Payload::RouteListRequest(_) => {
1315                        const CHUNK_SIZE: usize = 100;
1316
1317                        let conn_table = self.inner.message_processor.connection_table();
1318                        let mut entries = Vec::new();
1319
1320                        self.inner.message_processor.subscription_table().for_each(
1321                            |name, id, local, remote, peer, edge| {
1322                                let mut entry = RouteEntry {
1323                                    name: Some(name.clone().with_id(id)),
1324                                    ..Default::default()
1325                                };
1326
1327                                for &cid in local {
1328                                    entry.connections.push(ConnectionEntry {
1329                                        id: cid,
1330                                        connection_type: ConnectionType::Local as i32,
1331                                        config_data: "{}".to_string(),
1332                                        link_id: None,
1333                                        direction: ConnectionDirection::Outgoing as i32,
1334                                        peer_node_id: None,
1335                                    });
1336                                }
1337
1338                                let conn_slices = [
1339                                    (remote, ConnectionType::Remote),
1340                                    (peer, ConnectionType::Peer),
1341                                    (edge, ConnectionType::Edge),
1342                                ];
1343                                for (conns, ct) in conn_slices {
1344                                    for &cid in conns {
1345                                        if let Some(conn) = conn_table.get(cid) {
1346                                            entry.connections.push(ConnectionEntry {
1347                                                id: cid,
1348                                                connection_type: ct as i32,
1349                                                config_data: conn
1350                                                    .config_data()
1351                                                    .and_then(|d| serde_json::to_string(d).ok())
1352                                                    .unwrap_or_else(|| "{}".to_string()),
1353                                                link_id: conn.link_id(),
1354                                                direction: if conn.is_outgoing() {
1355                                                    ConnectionDirection::Outgoing as i32
1356                                                } else {
1357                                                    ConnectionDirection::Incoming as i32
1358                                                },
1359                                                peer_node_id: conn
1360                                                    .peer_node_id()
1361                                                    .map(str::to_string),
1362                                            });
1363                                        } else {
1364                                            error!(%cid, ?ct, "no connection entry for id");
1365                                        }
1366                                    }
1367                                }
1368                                entries.push(entry);
1369                            },
1370                        );
1371
1372                        let chunks: Vec<_> = entries.chunks(CHUNK_SIZE).collect();
1373                        if chunks.is_empty() {
1374                            let resp = ControlMessage {
1375                                message_id: uuid::Uuid::new_v4().to_string(),
1376                                payload: Some(Payload::RouteListResponse(RouteListResponse {
1377                                    original_message_id: msg.message_id.clone(),
1378                                    entries: vec![],
1379                                    done: true,
1380                                })),
1381                            };
1382                            if let Err(e) = tx.send(Ok(resp)).await {
1383                                error!(error = %e.chain(), "failed to send route list response");
1384                            }
1385                        } else {
1386                            let n = chunks.len();
1387                            for (i, chunk) in chunks.into_iter().enumerate() {
1388                                let resp = ControlMessage {
1389                                    message_id: uuid::Uuid::new_v4().to_string(),
1390                                    payload: Some(Payload::RouteListResponse(RouteListResponse {
1391                                        original_message_id: msg.message_id.clone(),
1392                                        entries: chunk.to_vec(),
1393                                        done: i + 1 == n,
1394                                    })),
1395                                };
1396                                if let Err(e) = tx.send(Ok(resp)).await {
1397                                    error!(error = %e.chain(), "failed to send route batch");
1398                                    break;
1399                                }
1400                            }
1401                        }
1402                    }
1403                    Payload::ConnectionListRequest(_) => {
1404                        let mut all_entries = Vec::new();
1405                        self.inner
1406                            .message_processor
1407                            .connection_table()
1408                            .for_each(|id, conn| {
1409                                let ct = match conn.connection_type() {
1410                                    slim_datapath::tables::ConnType::Local => ConnectionType::Local,
1411                                    slim_datapath::tables::ConnType::Remote => {
1412                                        ConnectionType::Remote
1413                                    }
1414                                    slim_datapath::tables::ConnType::Peer => ConnectionType::Peer,
1415                                    slim_datapath::tables::ConnType::Edge => ConnectionType::Edge,
1416                                };
1417                                all_entries.push(ConnectionEntry {
1418                                    id,
1419                                    connection_type: ct as i32,
1420                                    config_data: conn
1421                                        .config_data()
1422                                        .and_then(|d| serde_json::to_string(d).ok())
1423                                        .unwrap_or_else(|| "{}".to_string()),
1424                                    link_id: conn.link_id(),
1425                                    direction: if conn.is_outgoing() {
1426                                        ConnectionDirection::Outgoing as i32
1427                                    } else {
1428                                        ConnectionDirection::Incoming as i32
1429                                    },
1430                                    peer_node_id: conn.peer_node_id().map(str::to_string),
1431                                });
1432                            });
1433
1434                        const CHUNK_SIZE: usize = 100;
1435                        let chunks: Vec<_> = all_entries.chunks(CHUNK_SIZE).collect();
1436                        if chunks.is_empty() {
1437                            let resp = ControlMessage {
1438                                message_id: uuid::Uuid::new_v4().to_string(),
1439                                payload: Some(Payload::ConnectionListResponse(
1440                                    ConnectionListResponse {
1441                                        original_message_id: msg.message_id.clone(),
1442                                        entries: vec![],
1443                                        done: true,
1444                                    },
1445                                )),
1446                            };
1447                            if let Err(e) = tx.send(Ok(resp)).await {
1448                                error!(error = %e.chain(), "failed to send connection list response");
1449                            }
1450                        } else {
1451                            let n = chunks.len();
1452                            for (i, chunk) in chunks.into_iter().enumerate() {
1453                                let resp = ControlMessage {
1454                                    message_id: uuid::Uuid::new_v4().to_string(),
1455                                    payload: Some(Payload::ConnectionListResponse(
1456                                        ConnectionListResponse {
1457                                            original_message_id: msg.message_id.clone(),
1458                                            entries: chunk.to_vec(),
1459                                            done: i + 1 == n,
1460                                        },
1461                                    )),
1462                                };
1463                                if let Err(e) = tx.send(Ok(resp)).await {
1464                                    error!(error = %e.chain(), "failed to send connection list batch");
1465                                    break;
1466                                }
1467                            }
1468                        }
1469                    }
1470                    Payload::RegisterNodeRequest(_) => {
1471                        error!("received a register node request");
1472                    }
1473                    Payload::DeregisterNodeRequest(_) => {
1474                        error!("received a deregister node request");
1475                    }
1476                    _ => {
1477                        // received unsupported message type, do nothing
1478                        debug!("received unsupported message type from control - ignoring");
1479                    }
1480                }
1481            }
1482            None => {
1483                error!(
1484                    message_id = %msg.message_id,
1485                    "received control message with no payload",
1486                );
1487            }
1488        }
1489
1490        Ok(())
1491    }
1492
1493    async fn handle_subscribe_message(&self, dst: ProtoName, clients: &[ClientConfig]) {
1494        let mut sub_vec = vec![];
1495
1496        let cmd = v1::Route {
1497            name: Some(dst),
1498            link_id: None,
1499            direction: None,
1500        };
1501
1502        debug!(
1503            "handle_subscribe_message: sending route_to_set to control plane: {:?}",
1504            cmd
1505        );
1506        sub_vec.push(cmd);
1507
1508        let ctrl = ControlMessage {
1509            message_id: uuid::Uuid::new_v4().to_string(),
1510            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1511                connections_to_create: vec![],
1512                connections_to_delete: vec![],
1513                routes_to_set: sub_vec,
1514                routes_to_delete: vec![],
1515                reconcile: false,
1516                connections_received: vec![],
1517            })),
1518        };
1519
1520        return self.send_or_queue_notification(ctrl, clients).await;
1521    }
1522
1523    /// Called when the data plane completes a link negotiation for a remote
1524    /// incoming connection. Notifies the control-plane so it can claim the link.
1525    async fn handle_link_received(
1526        &self,
1527        link: &slim_datapath::api::ProtoLink,
1528        clients: &[ClientConfig],
1529    ) {
1530        use slim_datapath::api::ProtoLinkType;
1531
1532        let link_id = match &link.link_type {
1533            Some(ProtoLinkType::LinkNegotiation(payload)) => &payload.link_id,
1534            _ => {
1535                debug!("handle_link_received: ignoring link message without negotiation payload");
1536                return;
1537            }
1538        };
1539
1540        if link_id.is_empty() {
1541            debug!("handle_link_received: ignoring link message with empty link_id");
1542            return;
1543        }
1544
1545        debug!(
1546            "handle_link_received: notifying control-plane of new link_id: {}",
1547            link_id
1548        );
1549
1550        let ctrl = ControlMessage {
1551            message_id: uuid::Uuid::new_v4().to_string(),
1552            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1553                connections_to_create: vec![],
1554                connections_to_delete: vec![],
1555                routes_to_set: vec![],
1556                routes_to_delete: vec![],
1557                reconcile: false,
1558                connections_received: vec![v1::ConnectionEntry {
1559                    id: 0,
1560                    connection_type: ConnectionType::Remote as i32,
1561                    config_data: String::new(),
1562                    link_id: Some(link_id.clone()),
1563                    direction: ConnectionDirection::Incoming as i32,
1564                    peer_node_id: None,
1565                }],
1566            })),
1567        };
1568
1569        self.send_or_queue_notification(ctrl, clients).await;
1570    }
1571
1572    async fn handle_unsubscribe_message(&self, dst: ProtoName, clients: &[ClientConfig]) {
1573        // Remove the subscription from our tracking map so the reconciler
1574        // knows it needs to re-install it if the route is still desired.
1575        // Without this, a reused conn_id would cause create_new_subscriptions
1576        // to skip re-creation because it thinks the subscription is still active.
1577        self.inner
1578            .route_subscription_ids
1579            .lock()
1580            .retain(|(name, _), _| *name != dst);
1581
1582        let mut unsub_vec = vec![];
1583
1584        let cmd = v1::Route {
1585            name: Some(dst),
1586            link_id: None,
1587            direction: None,
1588        };
1589
1590        debug!(
1591            "handle_unsubscribe_message: sending route_to_delete to control plane: {:?}",
1592            cmd
1593        );
1594        unsub_vec.push(cmd);
1595
1596        let ctrl = ControlMessage {
1597            message_id: uuid::Uuid::new_v4().to_string(),
1598            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1599                connections_to_create: vec![],
1600                connections_to_delete: vec![],
1601                routes_to_set: vec![],
1602                routes_to_delete: unsub_vec,
1603                reconcile: false,
1604                connections_received: vec![],
1605            })),
1606        };
1607
1608        return self.send_or_queue_notification(ctrl, clients).await;
1609    }
1610
1611    /// Send a subscribe message and await the ack. Returns the subscription_id.
1612    async fn send_subscribe_message_with_ack(
1613        &self,
1614        mut msg: DataPlaneMessage,
1615    ) -> Result<u64, String> {
1616        let (ack_id, ack_rx) = self.inner.subscription_manager.register_ack();
1617        msg.set_subscription_id(ack_id);
1618
1619        if let Err(e) = self.send_control_message(msg).await {
1620            self.inner.subscription_manager.cancel_ack(ack_id);
1621            return Err(format!("datapath send error: {}", e.chain()));
1622        }
1623
1624        match tokio::time::timeout(SUBSCRIPTION_ACK_TIMEOUT, ack_rx).await {
1625            Ok(Ok(Ok(()))) => Ok(ack_id),
1626            Ok(Ok(Err(err))) => Err(err.to_string()),
1627            Ok(Err(_)) => Err("subscription ack channel closed".to_string()),
1628            Err(_) => {
1629                self.inner.subscription_manager.cancel_ack(ack_id);
1630                Err("subscription ack timed out".to_string())
1631            }
1632        }
1633    }
1634
1635    /// Send an unsubscribe message with a given subscription_id and await the ack.
1636    async fn send_unsubscribe_message_with_ack(
1637        &self,
1638        mut msg: DataPlaneMessage,
1639        subscription_id: u64,
1640    ) -> Result<(), String> {
1641        let ack_rx = self
1642            .inner
1643            .subscription_manager
1644            .register_ack_with_id(subscription_id);
1645        msg.set_subscription_id(subscription_id);
1646
1647        if let Err(e) = self.send_control_message(msg).await {
1648            self.inner.subscription_manager.cancel_ack(subscription_id);
1649            return Err(format!("datapath send error: {}", e.chain()));
1650        }
1651
1652        match tokio::time::timeout(SUBSCRIPTION_ACK_TIMEOUT, ack_rx).await {
1653            Ok(Ok(Ok(()))) => Ok(()),
1654            Ok(Ok(Err(err))) => Err(err.to_string()),
1655            Ok(Err(_)) => Err("subscription ack channel closed".to_string()),
1656            Err(_) => {
1657                self.inner.subscription_manager.cancel_ack(subscription_id);
1658                Err("subscription ack timed out".to_string())
1659            }
1660        }
1661    }
1662
1663    /// Send a control message to SLIM.
1664    async fn send_control_message(&self, msg: DataPlaneMessage) -> Result<(), ControllerError> {
1665        self.inner.tx_slim.send(Ok(msg)).await.map_err(|e| {
1666            error!(error = %e.chain(), "error sending message into datapath");
1667            ControllerError::Datapath(slim_datapath::errors::DataPathError::ConnectionError)
1668        })
1669    }
1670
1671    /// Send notification to control plane or queue it if no connection is available.
1672    ///
1673    /// Uses `try_send` to avoid blocking the caller when the bounded channel is
1674    /// full (e.g. due to HTTP/2 back-pressure).  Blocking here would stall the
1675    /// data-plane listener task, preventing it from resolving subscription acks
1676    /// that the control-message handler is waiting on — a deadlock.
1677    async fn send_or_queue_notification(&self, ctrl_msg: ControlMessage, clients: &[ClientConfig]) {
1678        let mut sent = false;
1679
1680        for c in clients {
1681            let tx = match self.inner.tx_channels.read().get(&c.endpoint) {
1682                Some(tx) => tx.clone(),
1683                None => continue,
1684            };
1685
1686            match tx.try_send(Ok(ctrl_msg.clone())) {
1687                Ok(()) => {
1688                    sent = true;
1689                }
1690                Err(mpsc::error::TrySendError::Full(_)) => {
1691                    debug!(
1692                        endpoint = %c.endpoint,
1693                        "channel full, queuing notification instead of blocking"
1694                    );
1695                }
1696                Err(mpsc::error::TrySendError::Closed(_)) => {
1697                    debug!(
1698                        endpoint = %c.endpoint,
1699                        "channel closed, queuing notification"
1700                    );
1701                }
1702            }
1703        }
1704
1705        if !sent {
1706            let mut queue = self.inner.pending_notifications.lock();
1707            if queue.len() >= MAX_QUEUED_NOTIFICATIONS {
1708                queue.pop_front();
1709                debug!("queue full, removed oldest notification");
1710            }
1711            queue.push_back(ctrl_msg);
1712        }
1713    }
1714
1715    /// Get a drain watch clone to pass to a task
1716    fn drain_watch(&self) -> Result<drain::Watch, ControllerError> {
1717        self.inner
1718            .drain_watch
1719            .read()
1720            .clone()
1721            .ok_or(ControllerError::AlreadyStopped)
1722    }
1723
1724    /// Send all queued subscription notifications when connection is restored.
1725    async fn send_queued_notifications(
1726        &self,
1727        tx: &mpsc::Sender<Result<ControlMessage, Status>>,
1728        endpoint: &str,
1729    ) {
1730        let notifications = {
1731            let mut queue = self.inner.pending_notifications.lock();
1732            if queue.is_empty() {
1733                return;
1734            }
1735            queue.drain(..).collect::<Vec<_>>()
1736        };
1737
1738        if notifications.is_empty() {
1739            return;
1740        }
1741
1742        debug!(
1743            "sending {} queued subscription notifications to {}",
1744            notifications.len(),
1745            endpoint
1746        );
1747
1748        let mut failed_notifications = Vec::new();
1749        for notification in notifications {
1750            if let Err(e) = tx.send(Ok(notification)).await {
1751                error!(
1752                    error = %e.chain(),
1753                    %endpoint,
1754                    "failed to send queued notification to control plane",
1755                );
1756
1757                // we can unwrap here because we know we sent a Ok(ControlMessage)
1758                failed_notifications.push(e.0.unwrap());
1759            }
1760        }
1761
1762        // Re-queue any failed notifications
1763        if !failed_notifications.is_empty() {
1764            self.inner
1765                .pending_notifications
1766                .lock()
1767                .extend(failed_notifications);
1768        }
1769    }
1770
1771    /// Replay all locally-registered agent subscriptions to the control plane.
1772    /// Called after (re)connecting so the CP can recreate wildcard route templates.
1773    async fn replay_local_subscriptions(&self, clients: &[ClientConfig]) {
1774        let mut routes: Vec<v1::Route> = Vec::new();
1775        self.inner.message_processor.subscription_table().for_each(
1776            |name, id, local, _remote, _peer, edge| {
1777                if local.is_empty() && edge.is_empty() {
1778                    return;
1779                }
1780                routes.push(v1::Route {
1781                    name: Some(name.clone().with_id(id)),
1782                    link_id: None,
1783                    direction: None,
1784                });
1785            },
1786        );
1787
1788        if routes.is_empty() {
1789            return;
1790        }
1791
1792        info!(
1793            count = routes.len(),
1794            "replaying local subscriptions to control plane"
1795        );
1796
1797        let ctrl = ControlMessage {
1798            message_id: uuid::Uuid::new_v4().to_string(),
1799            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1800                connections_to_create: vec![],
1801                connections_to_delete: vec![],
1802                routes_to_set: routes,
1803                routes_to_delete: vec![],
1804                reconcile: false,
1805                connections_received: vec![],
1806            })),
1807        };
1808
1809        self.send_or_queue_notification(ctrl, clients).await;
1810    }
1811
1812    /// Process the control message stream.
1813    fn process_control_message_stream(
1814        &self,
1815        config: Option<ClientConfig>,
1816        mut stream: impl Stream<Item = Result<ControlMessage, Status>> + Unpin + Send + 'static,
1817        tx: mpsc::Sender<Result<ControlMessage, Status>>,
1818        cancellation_token: CancellationToken,
1819    ) -> Result<JoinHandle<()>, ControllerError> {
1820        let this = self.clone();
1821        let watch = self.drain_watch()?;
1822
1823        let handle = tokio::spawn(async move {
1824            // Send a register message to the control plane
1825            let endpoint = config
1826                .as_ref()
1827                .map(|c| c.endpoint.clone())
1828                .unwrap_or_else(|| "unknown".to_string());
1829            info!(%endpoint, "connected to control plane");
1830
1831            let mut retry_connect = false;
1832
1833            let mut active_connections = Vec::new();
1834            this.inner
1835                .message_processor
1836                .connection_table()
1837                .for_each(|id, conn| {
1838                    active_connections.push(v1::ConnectionEntry {
1839                        id,
1840                        connection_type: v1::ConnectionType::Remote as i32,
1841                        config_data: match conn.config_data() {
1842                            Some(data) => {
1843                                serde_json::to_string(data).unwrap_or_else(|_| "{}".to_string())
1844                            }
1845                            None => "{}".to_string(),
1846                        },
1847                        link_id: conn.link_id(),
1848                        direction: if conn.is_outgoing() {
1849                            v1::ConnectionDirection::Outgoing as i32
1850                        } else {
1851                            v1::ConnectionDirection::Incoming as i32
1852                        },
1853                        peer_node_id: conn.peer_node_id().map(str::to_string),
1854                    });
1855                });
1856
1857            let active_routes = {
1858                let conn_id_to_link_id: HashMap<u64, String> = this
1859                    .inner
1860                    .link_id_to_conn_id
1861                    .read()
1862                    .iter()
1863                    .map(|(lid, cid)| (*cid, lid.clone()))
1864                    .collect();
1865                this.inner
1866                    .route_subscription_ids
1867                    .lock()
1868                    .iter()
1869                    .map(|((name, conn_id), _sub_id)| v1::Route {
1870                        name: Some(name.clone()),
1871                        link_id: conn_id_to_link_id.get(conn_id).cloned(),
1872                        direction: None,
1873                    })
1874                    .collect::<Vec<_>>()
1875            };
1876
1877            let max_attempts = 10;
1878            let mut credentials = None;
1879            let mut i = 0;
1880            while i < max_attempts && credentials.is_none() {
1881                credentials = match &this.inner.auth_provider {
1882                    Some(provider) => match provider.get_token() {
1883                        Ok(token) => Some(token),
1884                        Err(e) => {
1885                            info!(error = %e, attempt = i + 1, max_attempts, "failed to get auth credentials, will retry");
1886                            tokio::time::sleep(Duration::from_secs(2)).await;
1887                            None
1888                        }
1889                    },
1890                    None => Some(String::new()),
1891                };
1892                i += 1;
1893            }
1894
1895            if credentials.is_none() {
1896                error!(
1897                    attempts = max_attempts,
1898                    "failed to obtain auth credentials, aborting registration"
1899                );
1900                return;
1901            }
1902
1903            let register_request = ControlMessage {
1904                message_id: uuid::Uuid::new_v4().to_string(),
1905                payload: Some(Payload::RegisterNodeRequest(v1::RegisterNodeRequest {
1906                    node_id: this.inner.id.clone(),
1907                    domain_name: this.inner.domain_name.clone(),
1908                    connection_details: this.inner.connection_details.clone(),
1909                    connections: active_connections,
1910                    routes: active_routes,
1911                    credentials: credentials.unwrap(),
1912                })),
1913            };
1914
1915            // send register request if client
1916            if config.is_some()
1917                && let Err(e) = tx.send(Ok(register_request)).await
1918            {
1919                error!(error = %e.chain(), "failed to send register request");
1920                return;
1921            }
1922
1923            // Send queued notifications after the register request so that
1924            // RegisterNodeRequest is always the first message on the stream.
1925            this.send_queued_notifications(&tx, &endpoint).await;
1926
1927            // Wait for RegisterNodeResponse from control plane before processing commands.
1928            if config.is_some() {
1929                let registration_result = tokio::time::timeout(
1930                    Duration::from_secs(10),
1931                    async {
1932                        loop {
1933                            tokio::select! {
1934                                next = stream.next() => {
1935                                    match next {
1936                                        Some(Ok(msg)) => {
1937                                            if let Some(Payload::RegisterNodeResponse(resp)) = msg.payload {
1938                                                if resp.success {
1939                                                    info!(
1940                                                        connections = resp.connections.len(),
1941                                                        routes = resp.routes.len(),
1942                                                        "registration acknowledged by control plane"
1943                                                    );
1944                                                    return Ok(resp);
1945                                                } else {
1946                                                    return Err("registration rejected by control plane".to_string());
1947                                                }
1948                                            }
1949                                        }
1950                                        Some(Err(_)) => return Err("stream error waiting for registration response".to_string()),
1951                                        None => return Err("stream closed before registration response".to_string()),
1952                                    }
1953                                }
1954                                _ = cancellation_token.cancelled() => {
1955                                    return Err("cancelled while waiting for registration response".to_string());
1956                                }
1957                            }
1958                        }
1959                    }
1960                ).await;
1961
1962                match registration_result {
1963                    Ok(Ok(resp)) => {
1964                        // Apply initial desired state from the control plane.
1965                        if !resp.connections.is_empty() || !resp.routes.is_empty() {
1966                            let init_cmd = ControlMessage {
1967                                message_id: uuid::Uuid::new_v4().to_string(),
1968                                payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
1969                                    connections_to_create: resp.connections,
1970                                    routes_to_set: resp.routes,
1971                                    routes_to_delete: vec![],
1972                                    connections_to_delete: vec![],
1973                                    reconcile: true,
1974                                    connections_received: vec![],
1975                                })),
1976                            };
1977                            if let Err(e) = this.handle_new_control_message(init_cmd, &tx).await {
1978                                error!(error = %e.chain(), "failed to apply initial state from registration");
1979                            }
1980                        }
1981                    }
1982                    Ok(Err(e)) => {
1983                        error!(%e, "registration handshake failed");
1984                        return;
1985                    }
1986                    Err(_) => {
1987                        error!("registration handshake timed out");
1988                        return;
1989                    }
1990                }
1991            }
1992
1993            // Replay local agent subscriptions so the CP recreates route templates.
1994            if let Some(ref cfg) = config {
1995                this.replay_local_subscriptions(std::slice::from_ref(cfg))
1996                    .await;
1997            }
1998
1999            let mut drain_fut = std::pin::pin!(watch.clone().signaled());
2000
2001            loop {
2002                tokio::select! {
2003                    next = stream.next() => {
2004                        match next {
2005                            Some(Ok(msg)) => {
2006                                if let Err(e) = this.handle_new_control_message(msg, &tx).await {
2007                                    error!(error = %e.chain(), "error processing incoming control message");
2008                                }
2009                            }
2010                            Some(Err(e)) => {
2011                                if let Some(io_err) = Self::match_for_io_error(&e) {
2012                                    if io_err.kind() == std::io::ErrorKind::BrokenPipe {
2013                                        info!("connection closed by peer");
2014                                    } else {
2015                                        // Handle other IO errors (ConnectionAborted, etc.)
2016                                        error!(
2017                                            error = %e.chain(),
2018                                            io_error_kind = ?io_err.kind(),
2019                                            "IO error receiving control messages"
2020                                        );
2021                                    }
2022                                } else {
2023                                    // Handle non-IO errors (e.g., gRPC Canceled, Unavailable, etc.)
2024                                    error!(error = %e.chain(), "error receiving control messages");
2025                                }
2026
2027                                retry_connect = true;
2028                                break;
2029                            }
2030                            None => {
2031                                debug!("end of stream");
2032                                retry_connect = true;
2033                                break;
2034                            }
2035                        }
2036                    }
2037                    _ = cancellation_token.cancelled() => {
2038                        debug!("shutting down stream on cancellation token");
2039                        break;
2040                    }
2041                    _ = &mut drain_fut => {
2042                        debug!("shutting down stream on drain");
2043                        break;
2044                    }
2045                }
2046            }
2047
2048            info!(%endpoint, "control plane stream closed");
2049
2050            if retry_connect && let Some(config) = config {
2051                info!(%config.endpoint, "retrying connection to control plane");
2052                this.connect(config.clone(), cancellation_token)
2053                    .await
2054                    .map_or_else(
2055                        |e| {
2056                            error!(error = %e.chain(), "failed to reconnect to control plane");
2057                        },
2058                        |tx| {
2059                            info!(%config.endpoint, "reconnected to control plane");
2060
2061                            this.inner
2062                                .tx_channels
2063                                .write()
2064                                .insert(config.endpoint.clone(), tx);
2065                        },
2066                    )
2067            }
2068        });
2069
2070        Ok(handle)
2071    }
2072
2073    /// Connect to the control plane using the provided client configuration.
2074    /// This function attempts to establish a connection to the control plane and returns a sender for control messages.
2075    /// It retries the connection a specified number of times if it fails.
2076    async fn connect(
2077        &self,
2078        config: ClientConfig,
2079        cancellation_token: CancellationToken,
2080    ) -> Result<mpsc::Sender<Result<ControlMessage, Status>>, ControllerError> {
2081        info!(%config.endpoint, "connecting to control plane");
2082
2083        // Disconnect all outgoing remote connections — the CP will create
2084        // fresh links after re-registration and nodes will reconnect.
2085        // This prevents stale connections from causing duplicate links.
2086        let mut remote_conn_ids: Vec<u64> = Vec::new();
2087        self.inner
2088            .message_processor
2089            .connection_table()
2090            .for_each(|id, conn| {
2091                if conn.is_outgoing() && matches!(conn.connection_type(), ConnType::Remote) {
2092                    remote_conn_ids.push(id);
2093                }
2094            });
2095        for conn_id in &remote_conn_ids {
2096            if let Err(e) = self.inner.message_processor.disconnect(*conn_id) {
2097                debug!(conn_id, error = %e.chain(), "failed to disconnect remote connection");
2098            }
2099        }
2100        if !remote_conn_ids.is_empty() {
2101            info!(
2102                count = remote_conn_ids.len(),
2103                "disconnected remote connections for clean restart"
2104            );
2105        }
2106
2107        // Reset connection state before establishing a new session. The CP
2108        // will send fresh ConfigCommands after re-registration to rebuild
2109        // these maps.
2110        self.inner.route_subscription_ids.lock().clear();
2111        self.inner.link_id_to_conn_id.write().clear();
2112
2113        // Obtain drain watch to handle graceful shutdown during connection
2114        let watch = self.drain_watch()?;
2115
2116        let connect_fut = async {
2117            let channel = match config.to_channel().await? {
2118                TransportChannel::Grpc(c) => c,
2119                TransportChannel::Websocket(_) => {
2120                    return Err(ControllerError::ConfigError(
2121                        slim_config::errors::ConfigError::GrpcChannelUnsupportedTransport,
2122                    ));
2123                }
2124            };
2125
2126            let mut client = ControllerServiceClient::new(channel.clone());
2127            let (tx, rx) = mpsc::channel::<Result<ControlMessage, Status>>(128);
2128            let out_stream = ReceiverStream::new(rx).filter_map(|res| match res {
2129                Ok(msg) => Some(msg),
2130                Err(e) => {
2131                    error!(error = %e, "dropping outbound control message due to error");
2132                    None
2133                }
2134            });
2135            let stream = client
2136                .open_control_channel(Request::new(out_stream))
2137                .await?;
2138            Ok((tx, stream))
2139        };
2140
2141        let (tx, stream) = tokio::select! {
2142            result = connect_fut => { result? }
2143            _ = cancellation_token.cancelled() => {
2144                debug!("connection cancelled during setup");
2145                return Err(ControllerError::Canceled);
2146            }
2147            _ = watch.signaled() => {
2148                debug!("drain signal received during connection setup");
2149                return Err(ControllerError::Canceled);
2150            }
2151        };
2152
2153        // start processing the incoming stream
2154        let endpoint_key = config.endpoint.clone();
2155        let handle = self.process_control_message_stream(
2156            Some(config),
2157            stream.into_inner(),
2158            tx.clone(),
2159            cancellation_token.clone(),
2160        )?;
2161        self.inner
2162            .stream_handles
2163            .lock()
2164            .insert(endpoint_key, handle);
2165
2166        // return the sender
2167        Ok(tx)
2168    }
2169
2170    fn match_for_io_error(err_status: &Status) -> Option<&std::io::Error> {
2171        let mut err: &(dyn std::error::Error + 'static) = err_status;
2172
2173        loop {
2174            if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
2175                return Some(io_err);
2176            }
2177
2178            // h2::Error do not expose std::io::Error with `source()`
2179            // https://github.com/hyperium/h2/pull/462
2180            if let Some(h2_err) = err.downcast_ref::<h2::Error>()
2181                && let Some(io_err) = h2_err.get_io()
2182            {
2183                return Some(io_err);
2184            }
2185
2186            err = err.source()?;
2187        }
2188    }
2189}
2190
2191#[tonic::async_trait]
2192impl GrpcControllerService for ControllerService {
2193    type OpenControlChannelStream =
2194        Pin<Box<dyn Stream<Item = Result<ControlMessage, Status>> + Send + 'static>>;
2195
2196    async fn open_control_channel(
2197        &self,
2198        request: Request<tonic::Streaming<ControlMessage>>,
2199    ) -> Result<Response<Self::OpenControlChannelStream>, Status> {
2200        // Get the remote endpoint from the request metadata
2201        let remote_endpoint = request
2202            .remote_addr()
2203            .map(|addr| addr.to_string())
2204            .unwrap_or_else(|| "unknown".to_string());
2205
2206        let stream = request.into_inner();
2207        let (tx, rx) = mpsc::channel::<Result<ControlMessage, Status>>(128);
2208
2209        let cancellation_token = CancellationToken::new();
2210
2211        // Server-side connections don't initiate operations requiring acks, so no timer channel needed
2212        let handle = self
2213            .process_control_message_stream(None, stream, tx.clone(), cancellation_token.clone())
2214            .map_err(|e| {
2215                error!(error = %e.chain(), "error processing control message stream");
2216                Status::unavailable("failed to process control message stream")
2217            })?;
2218        self.inner
2219            .stream_handles
2220            .lock()
2221            .insert(remote_endpoint.clone(), handle);
2222
2223        self.inner
2224            .tx_channels
2225            .write()
2226            .insert(remote_endpoint.clone(), tx);
2227
2228        if let Some(old_token) = self
2229            .inner
2230            .cancellation_tokens
2231            .write()
2232            .insert(remote_endpoint.clone(), cancellation_token)
2233        {
2234            old_token.cancel();
2235        }
2236
2237        let out_stream = ReceiverStream::new(rx);
2238        Ok(Response::new(
2239            Box::pin(out_stream) as Self::OpenControlChannelStream
2240        ))
2241    }
2242}
2243
2244#[cfg(test)]
2245mod tests {
2246    use super::*;
2247    use tracing_test::traced_test;
2248
2249    async fn setup_control_planes(
2250        server_endpoint: &str,
2251        server_name: &str,
2252        client_name: &str,
2253    ) -> (ControlPlane, ControlPlane, ClientConfig) {
2254        let server_config = ServerConfig::with_endpoint(server_endpoint)
2255            .with_tls_settings(slim_config::tls::server::TlsServerConfig::insecure());
2256        let client_config = ClientConfig::with_endpoint(&format!("http://{}", server_endpoint))
2257            .with_tls_setting(slim_config::tls::client::TlsClientConfig::insecure());
2258
2259        let message_processor_server = MessageProcessor::new();
2260        let message_processor_client = MessageProcessor::new();
2261
2262        let control_plane_server = ControlPlane::new(ControlPlaneSettings {
2263            id: server_name.to_string(),
2264            domain_name: None,
2265            servers: vec![server_config.clone()],
2266            clients: vec![],
2267            outbound_clients: vec![],
2268            message_processor: message_processor_server,
2269            connection_details: vec![from_server_config(&server_config)],
2270            auth_provider: None,
2271        });
2272
2273        let control_plane_client = ControlPlane::new(ControlPlaneSettings {
2274            id: client_name.to_string(),
2275            domain_name: None,
2276            servers: vec![],
2277            clients: vec![client_config.clone()],
2278            outbound_clients: vec![],
2279            message_processor: message_processor_client,
2280            connection_details: vec![],
2281            auth_provider: None,
2282        });
2283
2284        (control_plane_server, control_plane_client, client_config)
2285    }
2286
2287    #[tokio::test]
2288    #[traced_test]
2289    async fn test_end_to_end() {
2290        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2291            setup_control_planes(
2292                "127.0.0.1:50051",
2293                "test-server-instance",
2294                "test-client-instance",
2295            )
2296            .await;
2297
2298        control_plane_server.run().await.unwrap();
2299        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2300        control_plane_client.run().await.unwrap();
2301        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2302
2303        assert!(logs_contain("received a register node request"));
2304    }
2305
2306    #[tokio::test]
2307    #[traced_test]
2308    async fn test_subscription_notification_queue_drain() {
2309        // Reuse common setup with a different port to avoid clash with other tests.
2310        let (mut control_plane_server, mut control_plane_client, client_config) =
2311            setup_control_planes(
2312                "127.0.0.1:50061",
2313                "queue-drain-server",
2314                "queue-drain-client",
2315            )
2316            .await;
2317
2318        let controller = control_plane_client.controller.clone();
2319        assert_eq!(controller.inner.pending_notifications.lock().len(), 0);
2320
2321        const N: usize = 5;
2322        for i in 0..N {
2323            let ctrl_msg = ControlMessage {
2324                message_id: uuid::Uuid::new_v4().to_string(),
2325                payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2326                    connections_to_create: vec![],
2327                    connections_to_delete: vec![],
2328                    routes_to_set: vec![v1::Route {
2329                        name: Some(
2330                            ProtoName::from_strings(["queued", "sub", &format!("name-{i}")])
2331                                .with_id(i as u128),
2332                        ),
2333                        link_id: None,
2334                        direction: None,
2335                    }],
2336                    routes_to_delete: vec![],
2337                    reconcile: false,
2338                    connections_received: vec![],
2339                })),
2340            };
2341            controller
2342                .send_or_queue_notification(ctrl_msg, std::slice::from_ref(&client_config))
2343                .await;
2344        }
2345        assert_eq!(controller.inner.pending_notifications.lock().len(), N);
2346
2347        control_plane_server.run().await.expect("server run failed");
2348        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2349        control_plane_client.run().await.expect("client run failed");
2350        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
2351
2352        assert_eq!(controller.inner.pending_notifications.lock().len(), 0);
2353        assert!(
2354            logs_contain(&format!("sending {} queued subscription notifications", N)),
2355            "Expected log about sending queued subscription notifications"
2356        );
2357
2358        drop(controller);
2359        drop(control_plane_server);
2360        drop(control_plane_client);
2361    }
2362
2363    #[tokio::test]
2364    #[traced_test]
2365    async fn test_delete_connection_by_link_id_success_ack() {
2366        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2367            setup_control_planes(
2368                "127.0.0.1:50081",
2369                "delete-linkid-server",
2370                "delete-linkid-client",
2371            )
2372            .await;
2373
2374        control_plane_server.run().await.unwrap();
2375        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2376        control_plane_client.run().await.unwrap();
2377        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2378
2379        let controller = control_plane_client.controller.clone();
2380        let link_id = "test-delete-link-id".to_string();
2381
2382        // Insert a pre-negotiated remote connection with a known link_id for testing.
2383        let (tx, _rx) = tokio::sync::mpsc::channel(16);
2384        let conn = slim_datapath::connection::Connection::new(
2385            slim_datapath::tables::ConnType::Remote,
2386            slim_datapath::connection::Channel::Server(tx),
2387        )
2388        .with_negotiation(&link_id, "1.0.0");
2389        controller
2390            .inner
2391            .message_processor
2392            .forwarder()
2393            .on_connection_established(conn, None)
2394            .unwrap();
2395
2396        let ctrl_msg = ControlMessage {
2397            message_id: uuid::Uuid::new_v4().to_string(),
2398            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2399                connections_to_create: vec![],
2400                connections_to_delete: vec![link_id.clone()],
2401                routes_to_set: vec![],
2402                routes_to_delete: vec![],
2403                reconcile: false,
2404                connections_received: vec![],
2405            })),
2406        };
2407        let (tx, mut rx) = mpsc::channel(1);
2408        controller
2409            .handle_new_control_message(ctrl_msg, &tx)
2410            .await
2411            .expect("config command must be handled");
2412
2413        let ack_msg = rx
2414            .recv()
2415            .await
2416            .expect("expected ack message")
2417            .expect("ack should be ok");
2418        let ack = match ack_msg.payload {
2419            Some(Payload::ConfigCommandAck(ack)) => ack,
2420            _ => panic!("expected ConfigCommandAck payload"),
2421        };
2422        assert_eq!(ack.connections_status.len(), 1);
2423        assert_eq!(ack.connections_status[0].link_id, link_id);
2424        assert!(ack.connections_status[0].success);
2425    }
2426
2427    #[tokio::test]
2428    #[traced_test]
2429    async fn test_delete_connection_by_link_id_unknown_fails_ack() {
2430        let (control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2431            "127.0.0.1:50082",
2432            "delete-linkid-server-unknown",
2433            "delete-linkid-client-unknown",
2434        )
2435        .await;
2436
2437        let controller = control_plane_client.controller.clone();
2438        let ctrl_msg = ControlMessage {
2439            message_id: uuid::Uuid::new_v4().to_string(),
2440            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2441                connections_to_create: vec![],
2442                connections_to_delete: vec!["unknown-link-id".to_string()],
2443                routes_to_set: vec![],
2444                routes_to_delete: vec![],
2445                reconcile: false,
2446                connections_received: vec![],
2447            })),
2448        };
2449        let (tx, mut rx) = mpsc::channel(1);
2450        controller
2451            .handle_new_control_message(ctrl_msg, &tx)
2452            .await
2453            .expect("config command must be handled");
2454
2455        let ack_msg = rx
2456            .recv()
2457            .await
2458            .expect("expected ack message")
2459            .expect("ack should be ok");
2460        let ack = match ack_msg.payload {
2461            Some(Payload::ConfigCommandAck(ack)) => ack,
2462            _ => panic!("expected ConfigCommandAck payload"),
2463        };
2464        assert_eq!(ack.connections_status.len(), 1);
2465        assert_eq!(ack.connections_status[0].link_id, "unknown-link-id");
2466        assert!(!ack.connections_status[0].success);
2467        assert!(ack.connections_status[0].error_msg.contains("not found"));
2468
2469        drop(control_plane_server);
2470    }
2471
2472    #[tokio::test]
2473    #[traced_test]
2474    async fn test_create_connection_with_existing_link_id_reuses_connection_ack() {
2475        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2476            setup_control_planes(
2477                "127.0.0.1:50083",
2478                "create-linkid-server",
2479                "create-linkid-client",
2480            )
2481            .await;
2482
2483        control_plane_server.run().await.unwrap();
2484        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2485        control_plane_client.run().await.unwrap();
2486        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2487
2488        let controller = control_plane_client.controller.clone();
2489        let link_id = "test-create-link-id".to_string();
2490
2491        // Insert a pre-negotiated remote connection with a known link_id for testing.
2492        let (tx, _rx) = tokio::sync::mpsc::channel(16);
2493        let conn = slim_datapath::connection::Connection::new(
2494            slim_datapath::tables::ConnType::Remote,
2495            slim_datapath::connection::Channel::Server(tx),
2496        )
2497        .with_negotiation(&link_id, "1.0.0");
2498        controller
2499            .inner
2500            .message_processor
2501            .forwarder()
2502            .on_connection_established(conn, None)
2503            .unwrap();
2504
2505        let endpoint = "http://127.0.0.1:59999";
2506        let connection_config = serde_json::json!({
2507            "endpoint": endpoint,
2508            "link_id": link_id
2509        })
2510        .to_string();
2511
2512        let ctrl_msg = ControlMessage {
2513            message_id: uuid::Uuid::new_v4().to_string(),
2514            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2515                connections_to_create: vec![v1::Connection {
2516                    link_id: "reuse-existing-link".to_string(),
2517                    config_data: connection_config,
2518                }],
2519                connections_to_delete: vec![],
2520                routes_to_set: vec![],
2521                routes_to_delete: vec![],
2522                reconcile: false,
2523                connections_received: vec![],
2524            })),
2525        };
2526
2527        let (tx, mut rx) = mpsc::channel(1);
2528        controller
2529            .handle_new_control_message(ctrl_msg, &tx)
2530            .await
2531            .expect("config command must be handled");
2532
2533        let ack_msg = rx
2534            .recv()
2535            .await
2536            .expect("expected ack message")
2537            .expect("ack should be ok");
2538        let ack = match ack_msg.payload {
2539            Some(Payload::ConfigCommandAck(ack)) => ack,
2540            _ => panic!("expected ConfigCommandAck payload"),
2541        };
2542        assert_eq!(ack.connections_status.len(), 1);
2543        assert_eq!(ack.connections_status[0].link_id, "reuse-existing-link");
2544        assert!(ack.connections_status[0].success);
2545
2546        assert!(
2547            controller
2548                .inner
2549                .link_id_to_conn_id
2550                .read()
2551                .contains_key(&link_id),
2552            "expected link_id to be mapped to reused connection id"
2553        );
2554    }
2555
2556    #[tokio::test]
2557    #[traced_test]
2558    async fn test_subscription_set_unknown_link_id_fails_ack() {
2559        let (control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2560            "127.0.0.1:50084",
2561            "sub-linkid-server-unknown",
2562            "sub-linkid-client-unknown",
2563        )
2564        .await;
2565
2566        let controller = control_plane_client.controller.clone();
2567        let ctrl_msg = ControlMessage {
2568            message_id: uuid::Uuid::new_v4().to_string(),
2569            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2570                connections_to_create: vec![],
2571                connections_to_delete: vec![],
2572                routes_to_set: vec![v1::Route {
2573                    name: Some(ProtoName::from_strings(["org", "ns", "agent"]).with_id(1u128)),
2574                    link_id: Some("missing-link-id".to_string()),
2575                    direction: None,
2576                }],
2577                routes_to_delete: vec![],
2578                reconcile: false,
2579                connections_received: vec![],
2580            })),
2581        };
2582        let (tx, mut rx) = mpsc::channel(1);
2583        controller
2584            .handle_new_control_message(ctrl_msg, &tx)
2585            .await
2586            .expect("config command must be handled");
2587
2588        let ack_msg = rx
2589            .recv()
2590            .await
2591            .expect("expected ack message")
2592            .expect("ack should be ok");
2593        let ack = match ack_msg.payload {
2594            Some(Payload::ConfigCommandAck(ack)) => ack,
2595            _ => panic!("expected ConfigCommandAck payload"),
2596        };
2597
2598        assert_eq!(ack.routes_status.len(), 1);
2599        assert!(!ack.routes_status[0].success);
2600        assert!(
2601            ack.routes_status[0]
2602                .error_msg
2603                .contains("Connection with link_id missing-link-id not found")
2604        );
2605
2606        drop(control_plane_server);
2607    }
2608
2609    #[tokio::test]
2610    #[traced_test]
2611    async fn test_create_connection_invalid_config_fails_ack() {
2612        let (_control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2613            "127.0.0.1:50085",
2614            "create-invalid-config-server",
2615            "create-invalid-config-client",
2616        )
2617        .await;
2618
2619        let controller = control_plane_client.controller.clone();
2620        let ctrl_msg = ControlMessage {
2621            message_id: uuid::Uuid::new_v4().to_string(),
2622            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2623                connections_to_create: vec![v1::Connection {
2624                    link_id: "invalid-config-conn".to_string(),
2625                    config_data: "{invalid-json".to_string(),
2626                }],
2627                connections_to_delete: vec![],
2628                routes_to_set: vec![],
2629                routes_to_delete: vec![],
2630                reconcile: false,
2631                connections_received: vec![],
2632            })),
2633        };
2634        let (tx, mut rx) = mpsc::channel(1);
2635        controller
2636            .handle_new_control_message(ctrl_msg, &tx)
2637            .await
2638            .expect("config command must be handled");
2639
2640        let ack_msg = rx
2641            .recv()
2642            .await
2643            .expect("expected ack message")
2644            .expect("ack should be ok");
2645        let ack = match ack_msg.payload {
2646            Some(Payload::ConfigCommandAck(ack)) => ack,
2647            _ => panic!("expected ConfigCommandAck payload"),
2648        };
2649        assert_eq!(ack.connections_status.len(), 1);
2650        assert!(!ack.connections_status[0].success);
2651        assert!(
2652            ack.connections_status[0]
2653                .error_msg
2654                .contains("Failed to parse config")
2655        );
2656    }
2657
2658    #[tokio::test]
2659    #[traced_test]
2660    async fn test_subscription_delete_unknown_link_id_fails_ack() {
2661        let (control_plane_server, control_plane_client, _client_cfg) = setup_control_planes(
2662            "127.0.0.1:50086",
2663            "sub-del-linkid-server-unknown",
2664            "sub-del-linkid-client-unknown",
2665        )
2666        .await;
2667
2668        let controller = control_plane_client.controller.clone();
2669        let ctrl_msg = ControlMessage {
2670            message_id: uuid::Uuid::new_v4().to_string(),
2671            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2672                connections_to_create: vec![],
2673                connections_to_delete: vec![],
2674                routes_to_set: vec![],
2675                routes_to_delete: vec![v1::Route {
2676                    name: Some(ProtoName::from_strings(["org", "ns", "agent"]).with_id(1u128)),
2677                    link_id: Some("missing-link-id-delete".to_string()),
2678                    direction: None,
2679                }],
2680                reconcile: false,
2681                connections_received: vec![],
2682            })),
2683        };
2684        let (tx, mut rx) = mpsc::channel(1);
2685        controller
2686            .handle_new_control_message(ctrl_msg, &tx)
2687            .await
2688            .expect("config command must be handled");
2689
2690        let ack_msg = rx
2691            .recv()
2692            .await
2693            .expect("expected ack message")
2694            .expect("ack should be ok");
2695        let ack = match ack_msg.payload {
2696            Some(Payload::ConfigCommandAck(ack)) => ack,
2697            _ => panic!("expected ConfigCommandAck payload"),
2698        };
2699
2700        assert_eq!(ack.routes_status.len(), 1);
2701        assert!(!ack.routes_status[0].success);
2702        assert!(
2703            ack.routes_status[0]
2704                .error_msg
2705                .contains("Connection with link_id missing-link-id-delete not found")
2706        );
2707
2708        drop(control_plane_server);
2709    }
2710
2711    #[tokio::test]
2712    #[traced_test]
2713    async fn test_shutdown_drains_resources() {
2714        // Use a unique port to avoid conflicts with other tests.
2715        let (mut control_plane_server, mut control_plane_client, _client_cfg) =
2716            setup_control_planes(
2717                "127.0.0.1:50071",
2718                "shutdown-server-instance",
2719                "shutdown-client-instance",
2720            )
2721            .await;
2722
2723        // Run both ends to populate cancellation tokens.
2724        control_plane_server
2725            .run()
2726            .await
2727            .expect("server should start");
2728        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2729        control_plane_client
2730            .run()
2731            .await
2732            .expect("client should start");
2733        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2734
2735        // Ensure we have at least one cancellation token (server or client side tasks).
2736        let server_tokens_before = control_plane_server
2737            .controller
2738            .inner
2739            .cancellation_tokens
2740            .read()
2741            .len();
2742        assert!(
2743            server_tokens_before > 0,
2744            "expected server to have active cancellation tokens before shutdown"
2745        );
2746
2747        let client_tokens_before = control_plane_client
2748            .controller
2749            .inner
2750            .cancellation_tokens
2751            .read()
2752            .len();
2753        assert!(
2754            client_tokens_before > 0,
2755            "expected client to have active cancellation tokens before shutdown"
2756        );
2757
2758        // Perform shutdown on both.
2759        control_plane_client
2760            .shutdown()
2761            .await
2762            .expect("client shutdown ok");
2763        control_plane_server
2764            .shutdown()
2765            .await
2766            .expect("server shutdown ok");
2767
2768        // After shutdown, all cancellation tokens should be drained.
2769        let server_tokens_after = control_plane_server
2770            .controller
2771            .inner
2772            .cancellation_tokens
2773            .read()
2774            .len();
2775        assert_eq!(
2776            server_tokens_after, 0,
2777            "expected server cancellation tokens to be drained after shutdown"
2778        );
2779
2780        let client_tokens_after = control_plane_client
2781            .controller
2782            .inner
2783            .cancellation_tokens
2784            .read()
2785            .len();
2786        assert_eq!(
2787            client_tokens_after, 0,
2788            "expected client cancellation tokens to be drained after shutdown"
2789        );
2790
2791        // Second shutdown should error because drain_signal has been taken.
2792        assert!(
2793            control_plane_server.shutdown().await.is_err(),
2794            "second shutdown on server should return an error"
2795        );
2796        assert!(
2797            control_plane_client.shutdown().await.is_err(),
2798            "second shutdown on client should return an error"
2799        );
2800    }
2801
2802    #[tokio::test]
2803    #[traced_test]
2804    async fn test_shutdown_without_run() {
2805        // Build a control plane but do NOT call run()
2806        let (control_plane_server, mut _control_plane_client, _client_cfg) = setup_control_planes(
2807            "127.0.0.1:50072",
2808            "shutdown-no-run-server",
2809            "shutdown-no-run-client",
2810        )
2811        .await;
2812
2813        // No tasks should be registered yet
2814        assert_eq!(
2815            control_plane_server
2816                .controller
2817                .inner
2818                .cancellation_tokens
2819                .read()
2820                .len(),
2821            0,
2822            "expected zero cancellation tokens before shutdown when not run"
2823        );
2824
2825        // Shutdown should still succeed gracefully.
2826        control_plane_server
2827            .shutdown()
2828            .await
2829            .expect("shutdown without prior run should succeed");
2830
2831        // Tokens remain zero.
2832        assert_eq!(
2833            control_plane_server
2834                .controller
2835                .inner
2836                .cancellation_tokens
2837                .read()
2838                .len(),
2839            0,
2840            "expected zero cancellation tokens after shutdown when not run"
2841        );
2842
2843        // Second shutdown should fail.
2844        assert!(
2845            control_plane_server.shutdown().await.is_err(),
2846            "second shutdown should error due to missing drain signal"
2847        );
2848    }
2849
2850    fn make_reconcile_msg(link_id: &str, server_config: &ServerConnectionConfig) -> ControlMessage {
2851        ControlMessage {
2852            message_id: uuid::Uuid::new_v4().to_string(),
2853            payload: Some(Payload::ConfigCommand(v1::ConfigurationCommand {
2854                connections_to_create: vec![v1::Connection {
2855                    link_id: link_id.to_string(),
2856                    config_data: serde_json::to_string(server_config).unwrap(),
2857                }],
2858                connections_to_delete: vec![],
2859                routes_to_set: vec![],
2860                routes_to_delete: vec![],
2861                reconcile: true,
2862                connections_received: vec![],
2863            })),
2864        }
2865    }
2866
2867    fn make_controller(outbound_clients: Vec<ClientConfig>) -> ControllerService {
2868        ControlPlane::new(ControlPlaneSettings {
2869            id: "test-node".to_string(),
2870            domain_name: None,
2871            servers: vec![],
2872            clients: vec![],
2873            outbound_clients,
2874            message_processor: MessageProcessor::new(),
2875            connection_details: vec![],
2876            auth_provider: None,
2877        })
2878        .controller
2879        .clone()
2880    }
2881
2882    #[tokio::test]
2883    async fn test_reconcile_basic_auth_missing_credentials_fails_ack() {
2884        let controller = make_controller(vec![]);
2885        let server_config = ServerConnectionConfig {
2886            endpoint: "http://target:8080".to_string(),
2887            tls_required: false,
2888            auth_method: RequiredAuthMethod::Basic,
2889            ..Default::default()
2890        };
2891        let (tx, mut rx) = mpsc::channel(1);
2892        controller
2893            .handle_new_control_message(make_reconcile_msg("link-1", &server_config), &tx)
2894            .await
2895            .unwrap();
2896        let ack = match rx.recv().await.unwrap().unwrap().payload {
2897            Some(Payload::ConfigCommandAck(a)) => a,
2898            _ => panic!("expected ConfigCommandAck"),
2899        };
2900        assert!(!ack.connections_status[0].success);
2901        assert!(ack.connections_status[0].error_msg.contains("target:8080"));
2902    }
2903
2904    #[tokio::test]
2905    async fn test_reconcile_jwt_auth_missing_credentials_fails_ack() {
2906        let controller = make_controller(vec![]);
2907        let server_config = ServerConnectionConfig {
2908            endpoint: "http://target:9090".to_string(),
2909            tls_required: false,
2910            auth_method: RequiredAuthMethod::Jwt,
2911            ..Default::default()
2912        };
2913        let (tx, mut rx) = mpsc::channel(1);
2914        controller
2915            .handle_new_control_message(make_reconcile_msg("link-2", &server_config), &tx)
2916            .await
2917            .unwrap();
2918        let ack = match rx.recv().await.unwrap().unwrap().payload {
2919            Some(Payload::ConfigCommandAck(a)) => a,
2920            _ => panic!("expected ConfigCommandAck"),
2921        };
2922        assert!(!ack.connections_status[0].success);
2923        assert!(ack.connections_status[0].error_msg.contains("target:9090"));
2924    }
2925}