Skip to main content

bssh/config/
types.rs

1// Copyright 2025 Lablup Inc. and Jeongkyu Shin
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Configuration type definitions.
16
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20/// Main configuration structure.
21#[derive(Debug, Serialize, Deserialize, Default, Clone)]
22pub struct Config {
23    #[serde(default)]
24    pub defaults: Defaults,
25
26    #[serde(default)]
27    pub clusters: HashMap<String, Cluster>,
28
29    #[serde(default)]
30    pub interactive: InteractiveConfig,
31}
32
33/// Jump host configuration format.
34///
35/// Supports multiple formats:
36/// - Legacy string format: `"[user@]hostname[:port]"`
37/// - SSH config reference: `"@alias"` (references ~/.ssh/config Host alias)
38/// - Structured format with optional ssh_key
39/// - Structured SSH config reference with ssh_config_host field
40///
41/// Uses `#[serde(untagged)]` to allow seamless deserialization of all formats.
42#[derive(Debug, Serialize, Deserialize, Clone)]
43#[serde(untagged)]
44pub enum JumpHostConfig {
45    /// Structured SSH config reference format with ssh_config_host field
46    /// Must be listed first for serde to try matching object format before string
47    SshConfigHostRef {
48        /// SSH config Host alias to reference (from ~/.ssh/config)
49        ssh_config_host: String,
50    },
51    /// Structured format with optional ssh_key field
52    Detailed {
53        host: String,
54        #[serde(default)]
55        user: Option<String>,
56        #[serde(default)]
57        port: Option<u16>,
58        #[serde(default)]
59        ssh_key: Option<String>,
60    },
61    /// Legacy string format: "[user@]hostname[:port]"
62    /// Also supports SSH config reference with "@" prefix: "@alias"
63    Simple(String),
64}
65
66/// Global default settings.
67#[derive(Debug, Serialize, Deserialize, Default, Clone)]
68pub struct Defaults {
69    pub user: Option<String>,
70    pub port: Option<u16>,
71    pub ssh_key: Option<String>,
72    pub parallel: Option<usize>,
73    pub timeout: Option<u64>,
74    /// Jump host specification for all connections.
75    /// Supports both string format and structured format with optional ssh_key.
76    /// Empty string explicitly disables jump host inheritance.
77    pub jump_host: Option<JumpHostConfig>,
78    /// SSH keepalive interval in seconds.
79    /// Sends keepalive packets to prevent idle connection timeouts.
80    /// Default: 30 seconds. Set to 0 to disable.
81    pub server_alive_interval: Option<u64>,
82    /// Maximum keepalive messages without response before disconnect.
83    /// Default: 3
84    pub server_alive_count_max: Option<usize>,
85}
86
87/// Interactive mode configuration.
88#[derive(Debug, Serialize, Deserialize, Default, Clone)]
89pub struct InteractiveConfig {
90    #[serde(default = "default_interactive_mode")]
91    pub default_mode: InteractiveMode,
92
93    #[serde(default = "default_prompt_format")]
94    pub prompt_format: String,
95
96    #[serde(default)]
97    pub history_file: Option<String>,
98
99    #[serde(default)]
100    pub colors: HashMap<String, String>,
101
102    #[serde(default)]
103    pub keybindings: KeyBindings,
104
105    #[serde(default)]
106    pub broadcast_prefix: Option<String>,
107
108    #[serde(default)]
109    pub node_switch_prefix: Option<String>,
110
111    #[serde(default)]
112    pub show_timestamps: bool,
113
114    #[serde(default)]
115    pub work_dir: Option<String>,
116}
117
118/// Interactive mode type.
119#[derive(Debug, Serialize, Deserialize, Clone)]
120#[serde(rename_all = "snake_case")]
121#[derive(Default)]
122pub enum InteractiveMode {
123    #[default]
124    SingleNode,
125    Multiplex,
126}
127
128/// Keyboard bindings configuration.
129#[derive(Debug, Serialize, Deserialize, Default, Clone)]
130pub struct KeyBindings {
131    #[serde(default = "default_switch_node")]
132    pub switch_node: String,
133
134    #[serde(default = "default_broadcast_toggle")]
135    pub broadcast_toggle: String,
136
137    #[serde(default = "default_quit")]
138    pub quit: String,
139
140    #[serde(default)]
141    pub clear_screen: Option<String>,
142}
143
144/// Cluster configuration.
145#[derive(Debug, Serialize, Deserialize, Clone)]
146pub struct Cluster {
147    pub nodes: Vec<NodeConfig>,
148
149    #[serde(flatten)]
150    pub defaults: ClusterDefaults,
151
152    #[serde(default)]
153    pub interactive: Option<InteractiveConfig>,
154}
155
156/// Cluster-specific default settings.
157#[derive(Debug, Serialize, Deserialize, Default, Clone)]
158pub struct ClusterDefaults {
159    pub user: Option<String>,
160    pub port: Option<u16>,
161    pub ssh_key: Option<String>,
162    pub parallel: Option<usize>,
163    pub timeout: Option<u64>,
164    /// Jump host specification for this cluster.
165    /// Supports both string format and structured format with optional ssh_key.
166    /// Empty string explicitly disables jump host inheritance.
167    pub jump_host: Option<JumpHostConfig>,
168    /// SSH keepalive interval in seconds.
169    /// Sends keepalive packets to prevent idle connection timeouts.
170    /// Default: 30 seconds. Set to 0 to disable.
171    pub server_alive_interval: Option<u64>,
172    /// Maximum keepalive messages without response before disconnect.
173    /// Default: 3
174    pub server_alive_count_max: Option<usize>,
175}
176
177/// Node configuration within a cluster.
178#[derive(Debug, Serialize, Deserialize, Clone)]
179#[serde(untagged)]
180pub enum NodeConfig {
181    Simple(String),
182    Detailed {
183        host: String,
184        #[serde(default)]
185        port: Option<u16>,
186        #[serde(default)]
187        user: Option<String>,
188        /// Jump host specification for this node.
189        /// Supports both string format and structured format with optional ssh_key.
190        /// Empty string explicitly disables jump host inheritance.
191        #[serde(default)]
192        jump_host: Option<JumpHostConfig>,
193    },
194}
195
196/// Structure for updating interactive configuration preferences.
197#[derive(Debug, Default)]
198pub struct InteractiveConfigUpdate {
199    pub default_mode: Option<InteractiveMode>,
200    pub prompt_format: Option<String>,
201    pub history_file: Option<String>,
202    pub work_dir: Option<String>,
203    pub show_timestamps: Option<bool>,
204    pub colors: Option<HashMap<String, String>>,
205}
206
207// Default value functions for serde
208pub(super) fn default_interactive_mode() -> InteractiveMode {
209    InteractiveMode::SingleNode
210}
211
212pub(super) fn default_prompt_format() -> String {
213    "[{node}:{user}@{host}:{pwd}]$ ".to_string()
214}
215
216pub(super) fn default_switch_node() -> String {
217    "Ctrl+N".to_string()
218}
219
220pub(super) fn default_broadcast_toggle() -> String {
221    "Ctrl+B".to_string()
222}
223
224pub(super) fn default_quit() -> String {
225    "Ctrl+Q".to_string()
226}
227
228impl JumpHostConfig {
229    /// Convert to a connection string for resolution.
230    ///
231    /// Note: For SSH config references (`@alias` or `ssh_config_host`), this returns
232    /// the alias name with "@" prefix. The actual resolution to hostname/user/port
233    /// must be done by the caller using SSH config parsing.
234    pub fn to_connection_string(&self) -> String {
235        match self {
236            JumpHostConfig::Simple(s) => s.clone(),
237            JumpHostConfig::Detailed {
238                host,
239                user,
240                port,
241                ssh_key: _,
242            } => {
243                let mut result = String::new();
244                if let Some(u) = user {
245                    result.push_str(u);
246                    result.push('@');
247                }
248                result.push_str(host);
249                if let Some(p) = port {
250                    result.push(':');
251                    result.push_str(&p.to_string());
252                }
253                result
254            }
255            JumpHostConfig::SshConfigHostRef { ssh_config_host } => {
256                format!("@{}", ssh_config_host)
257            }
258        }
259    }
260
261    /// Get the SSH key path if specified
262    pub fn ssh_key(&self) -> Option<&str> {
263        match self {
264            JumpHostConfig::Simple(_) => None,
265            JumpHostConfig::Detailed { ssh_key, .. } => ssh_key.as_deref(),
266            JumpHostConfig::SshConfigHostRef { .. } => None,
267        }
268    }
269
270    /// Check if this is an SSH config reference (either `@alias` string or `ssh_config_host` field)
271    pub fn is_ssh_config_ref(&self) -> bool {
272        match self {
273            JumpHostConfig::Simple(s) => s.starts_with('@'),
274            JumpHostConfig::SshConfigHostRef { .. } => true,
275            JumpHostConfig::Detailed { .. } => false,
276        }
277    }
278
279    /// Get the SSH config host alias if this is an SSH config reference.
280    ///
281    /// Returns the alias name (without "@" prefix) for:
282    /// - `JumpHostConfig::Simple("@alias")` -> Some("alias")
283    /// - `JumpHostConfig::SshConfigHostRef { ssh_config_host: "alias" }` -> Some("alias")
284    /// - Other variants -> None
285    pub fn ssh_config_host(&self) -> Option<&str> {
286        match self {
287            JumpHostConfig::Simple(s) if s.starts_with('@') => Some(&s[1..]),
288            JumpHostConfig::SshConfigHostRef { ssh_config_host } => Some(ssh_config_host),
289            _ => None,
290        }
291    }
292}