@opencode-cloud/core 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +9 -2
- package/README.md +7 -0
- package/package.json +1 -1
- package/src/config/mod.rs +8 -3
- package/src/config/paths.rs +14 -0
- package/src/config/schema.rs +470 -0
- package/src/config/validation.rs +271 -0
- package/src/docker/Dockerfile +278 -153
- package/src/docker/client.rs +132 -3
- package/src/docker/container.rs +90 -33
- package/src/docker/exec.rs +278 -0
- package/src/docker/health.rs +165 -0
- package/src/docker/image.rs +2 -4
- package/src/docker/mod.rs +47 -4
- package/src/docker/progress.rs +4 -4
- package/src/docker/update.rs +156 -0
- package/src/docker/users.rs +357 -0
- package/src/docker/version.rs +95 -0
- package/src/host/error.rs +61 -0
- package/src/host/mod.rs +29 -0
- package/src/host/provision.rs +394 -0
- package/src/host/schema.rs +308 -0
- package/src/host/ssh_config.rs +282 -0
- package/src/host/storage.rs +118 -0
- package/src/host/tunnel.rs +268 -0
- package/src/lib.rs +10 -1
- package/src/platform/launchd.rs +1 -1
- package/src/platform/systemd.rs +6 -6
- package/src/singleton/mod.rs +1 -1
- package/src/version.rs +1 -6
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
//! Container user management operations
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides functions to manage Linux system users inside
|
|
4
|
+
//! the running Docker container. opencode authenticates against PAM,
|
|
5
|
+
//! so opencode-cloud must manage system users in the container.
|
|
6
|
+
//!
|
|
7
|
+
//! Security note: Passwords are never passed as command arguments.
|
|
8
|
+
//! Instead, we use `chpasswd` which reads from stdin.
|
|
9
|
+
|
|
10
|
+
use super::exec::{exec_command, exec_command_exit_code, exec_command_with_stdin};
|
|
11
|
+
use super::{DockerClient, DockerError};
|
|
12
|
+
|
|
13
|
+
/// Information about a container user
|
|
14
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
15
|
+
pub struct UserInfo {
|
|
16
|
+
/// Username
|
|
17
|
+
pub username: String,
|
|
18
|
+
/// User ID (uid)
|
|
19
|
+
pub uid: u32,
|
|
20
|
+
/// Home directory path
|
|
21
|
+
pub home: String,
|
|
22
|
+
/// Login shell
|
|
23
|
+
pub shell: String,
|
|
24
|
+
/// Whether the account is locked
|
|
25
|
+
pub locked: bool,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Create a new user in the container
|
|
29
|
+
///
|
|
30
|
+
/// Creates a user with a home directory and /bin/bash shell.
|
|
31
|
+
/// Returns an error if the user already exists.
|
|
32
|
+
///
|
|
33
|
+
/// # Arguments
|
|
34
|
+
/// * `client` - Docker client
|
|
35
|
+
/// * `container` - Container name or ID
|
|
36
|
+
/// * `username` - Username to create
|
|
37
|
+
///
|
|
38
|
+
/// # Example
|
|
39
|
+
/// ```ignore
|
|
40
|
+
/// create_user(&client, "opencode-cloud", "admin").await?;
|
|
41
|
+
/// ```
|
|
42
|
+
pub async fn create_user(
|
|
43
|
+
client: &DockerClient,
|
|
44
|
+
container: &str,
|
|
45
|
+
username: &str,
|
|
46
|
+
) -> Result<(), DockerError> {
|
|
47
|
+
let cmd = vec!["useradd", "-m", "-s", "/bin/bash", username];
|
|
48
|
+
|
|
49
|
+
let exit_code = exec_command_exit_code(client, container, cmd).await?;
|
|
50
|
+
|
|
51
|
+
if exit_code != 0 {
|
|
52
|
+
// Check if user already exists
|
|
53
|
+
if user_exists(client, container, username).await? {
|
|
54
|
+
return Err(DockerError::Container(format!(
|
|
55
|
+
"User '{username}' already exists"
|
|
56
|
+
)));
|
|
57
|
+
}
|
|
58
|
+
return Err(DockerError::Container(format!(
|
|
59
|
+
"Failed to create user '{username}': useradd returned exit code {exit_code}"
|
|
60
|
+
)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Ok(())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Set or change a user's password
|
|
67
|
+
///
|
|
68
|
+
/// Uses chpasswd with stdin for secure password setting.
|
|
69
|
+
/// The password never appears in command arguments or process list.
|
|
70
|
+
///
|
|
71
|
+
/// # Arguments
|
|
72
|
+
/// * `client` - Docker client
|
|
73
|
+
/// * `container` - Container name or ID
|
|
74
|
+
/// * `username` - Username to set password for
|
|
75
|
+
/// * `password` - New password (will be written to stdin)
|
|
76
|
+
///
|
|
77
|
+
/// # Security
|
|
78
|
+
/// The password is written directly to chpasswd's stdin, never appearing
|
|
79
|
+
/// in command arguments, environment variables, or process listings.
|
|
80
|
+
///
|
|
81
|
+
/// # Example
|
|
82
|
+
/// ```ignore
|
|
83
|
+
/// set_user_password(&client, "opencode-cloud", "admin", "secret123").await?;
|
|
84
|
+
/// ```
|
|
85
|
+
pub async fn set_user_password(
|
|
86
|
+
client: &DockerClient,
|
|
87
|
+
container: &str,
|
|
88
|
+
username: &str,
|
|
89
|
+
password: &str,
|
|
90
|
+
) -> Result<(), DockerError> {
|
|
91
|
+
let cmd = vec!["chpasswd"];
|
|
92
|
+
let stdin_data = format!("{username}:{password}\n");
|
|
93
|
+
|
|
94
|
+
exec_command_with_stdin(client, container, cmd, &stdin_data).await?;
|
|
95
|
+
|
|
96
|
+
Ok(())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Check if a user exists in the container
|
|
100
|
+
///
|
|
101
|
+
/// # Arguments
|
|
102
|
+
/// * `client` - Docker client
|
|
103
|
+
/// * `container` - Container name or ID
|
|
104
|
+
/// * `username` - Username to check
|
|
105
|
+
///
|
|
106
|
+
/// # Returns
|
|
107
|
+
/// `true` if the user exists, `false` otherwise
|
|
108
|
+
pub async fn user_exists(
|
|
109
|
+
client: &DockerClient,
|
|
110
|
+
container: &str,
|
|
111
|
+
username: &str,
|
|
112
|
+
) -> Result<bool, DockerError> {
|
|
113
|
+
let cmd = vec!["id", "-u", username];
|
|
114
|
+
let exit_code = exec_command_exit_code(client, container, cmd).await?;
|
|
115
|
+
|
|
116
|
+
Ok(exit_code == 0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Lock a user account (disable password authentication)
|
|
120
|
+
///
|
|
121
|
+
/// Uses `passwd -l` to lock the account. The user will not be able
|
|
122
|
+
/// to log in using password authentication.
|
|
123
|
+
///
|
|
124
|
+
/// # Arguments
|
|
125
|
+
/// * `client` - Docker client
|
|
126
|
+
/// * `container` - Container name or ID
|
|
127
|
+
/// * `username` - Username to lock
|
|
128
|
+
pub async fn lock_user(
|
|
129
|
+
client: &DockerClient,
|
|
130
|
+
container: &str,
|
|
131
|
+
username: &str,
|
|
132
|
+
) -> Result<(), DockerError> {
|
|
133
|
+
let cmd = vec!["passwd", "-l", username];
|
|
134
|
+
let exit_code = exec_command_exit_code(client, container, cmd).await?;
|
|
135
|
+
|
|
136
|
+
if exit_code != 0 {
|
|
137
|
+
return Err(DockerError::Container(format!(
|
|
138
|
+
"Failed to lock user '{username}': passwd returned exit code {exit_code}"
|
|
139
|
+
)));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Ok(())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Unlock a user account (re-enable password authentication)
|
|
146
|
+
///
|
|
147
|
+
/// Uses `passwd -u` to unlock the account.
|
|
148
|
+
///
|
|
149
|
+
/// # Arguments
|
|
150
|
+
/// * `client` - Docker client
|
|
151
|
+
/// * `container` - Container name or ID
|
|
152
|
+
/// * `username` - Username to unlock
|
|
153
|
+
pub async fn unlock_user(
|
|
154
|
+
client: &DockerClient,
|
|
155
|
+
container: &str,
|
|
156
|
+
username: &str,
|
|
157
|
+
) -> Result<(), DockerError> {
|
|
158
|
+
let cmd = vec!["passwd", "-u", username];
|
|
159
|
+
let exit_code = exec_command_exit_code(client, container, cmd).await?;
|
|
160
|
+
|
|
161
|
+
if exit_code != 0 {
|
|
162
|
+
return Err(DockerError::Container(format!(
|
|
163
|
+
"Failed to unlock user '{username}': passwd returned exit code {exit_code}"
|
|
164
|
+
)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Ok(())
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Delete a user from the container
|
|
171
|
+
///
|
|
172
|
+
/// Uses `userdel -r` to remove the user and their home directory.
|
|
173
|
+
///
|
|
174
|
+
/// # Arguments
|
|
175
|
+
/// * `client` - Docker client
|
|
176
|
+
/// * `container` - Container name or ID
|
|
177
|
+
/// * `username` - Username to delete
|
|
178
|
+
pub async fn delete_user(
|
|
179
|
+
client: &DockerClient,
|
|
180
|
+
container: &str,
|
|
181
|
+
username: &str,
|
|
182
|
+
) -> Result<(), DockerError> {
|
|
183
|
+
let cmd = vec!["userdel", "-r", username];
|
|
184
|
+
let exit_code = exec_command_exit_code(client, container, cmd).await?;
|
|
185
|
+
|
|
186
|
+
if exit_code != 0 {
|
|
187
|
+
// Check if user doesn't exist
|
|
188
|
+
if !user_exists(client, container, username).await? {
|
|
189
|
+
return Err(DockerError::Container(format!(
|
|
190
|
+
"User '{username}' does not exist"
|
|
191
|
+
)));
|
|
192
|
+
}
|
|
193
|
+
return Err(DockerError::Container(format!(
|
|
194
|
+
"Failed to delete user '{username}': userdel returned exit code {exit_code}"
|
|
195
|
+
)));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Ok(())
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// List users in the container with home directories
|
|
202
|
+
///
|
|
203
|
+
/// Returns users that have home directories under /home/.
|
|
204
|
+
/// Excludes system users.
|
|
205
|
+
///
|
|
206
|
+
/// # Arguments
|
|
207
|
+
/// * `client` - Docker client
|
|
208
|
+
/// * `container` - Container name or ID
|
|
209
|
+
pub async fn list_users(
|
|
210
|
+
client: &DockerClient,
|
|
211
|
+
container: &str,
|
|
212
|
+
) -> Result<Vec<UserInfo>, DockerError> {
|
|
213
|
+
// Get all users with home directories in /home
|
|
214
|
+
let cmd = vec!["sh", "-c", "getent passwd | grep '/home/'"];
|
|
215
|
+
let output = exec_command(client, container, cmd).await?;
|
|
216
|
+
|
|
217
|
+
let mut users = Vec::new();
|
|
218
|
+
|
|
219
|
+
for line in output.lines() {
|
|
220
|
+
if let Some(info) = parse_passwd_line(line) {
|
|
221
|
+
// Check if user is locked
|
|
222
|
+
let locked = is_user_locked(client, container, &info.username).await?;
|
|
223
|
+
|
|
224
|
+
users.push(UserInfo {
|
|
225
|
+
username: info.username,
|
|
226
|
+
uid: info.uid,
|
|
227
|
+
home: info.home,
|
|
228
|
+
shell: info.shell,
|
|
229
|
+
locked,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Ok(users)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Check if a user account is locked
|
|
238
|
+
///
|
|
239
|
+
/// Uses `passwd -S` to get account status.
|
|
240
|
+
/// Returns true if the status starts with "L" (locked).
|
|
241
|
+
async fn is_user_locked(
|
|
242
|
+
client: &DockerClient,
|
|
243
|
+
container: &str,
|
|
244
|
+
username: &str,
|
|
245
|
+
) -> Result<bool, DockerError> {
|
|
246
|
+
let cmd = vec!["passwd", "-S", username];
|
|
247
|
+
let output = exec_command(client, container, cmd).await?;
|
|
248
|
+
|
|
249
|
+
// passwd -S output format: "username L/P/NP ... "
|
|
250
|
+
// L = locked, P = password set, NP = no password
|
|
251
|
+
let parts: Vec<&str> = output.split_whitespace().collect();
|
|
252
|
+
if parts.len() >= 2 {
|
|
253
|
+
return Ok(parts[1] == "L");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Ok(false)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Parsed user info from /etc/passwd line (intermediate struct)
|
|
260
|
+
struct ParsedUser {
|
|
261
|
+
username: String,
|
|
262
|
+
uid: u32,
|
|
263
|
+
home: String,
|
|
264
|
+
shell: String,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Parse a line from /etc/passwd
|
|
268
|
+
///
|
|
269
|
+
/// Format: username:x:uid:gid:gecos:home:shell
|
|
270
|
+
fn parse_passwd_line(line: &str) -> Option<ParsedUser> {
|
|
271
|
+
let fields: Vec<&str> = line.split(':').collect();
|
|
272
|
+
if fields.len() < 7 {
|
|
273
|
+
return None;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let uid = fields[2].parse::<u32>().ok()?;
|
|
277
|
+
|
|
278
|
+
Some(ParsedUser {
|
|
279
|
+
username: fields[0].to_string(),
|
|
280
|
+
uid,
|
|
281
|
+
home: fields[5].to_string(),
|
|
282
|
+
shell: fields[6].to_string(),
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[cfg(test)]
|
|
287
|
+
mod tests {
|
|
288
|
+
use super::*;
|
|
289
|
+
|
|
290
|
+
#[test]
|
|
291
|
+
fn test_parse_passwd_line_valid() {
|
|
292
|
+
let line = "admin:x:1001:1001:Admin User:/home/admin:/bin/bash";
|
|
293
|
+
let parsed = parse_passwd_line(line).unwrap();
|
|
294
|
+
assert_eq!(parsed.username, "admin");
|
|
295
|
+
assert_eq!(parsed.uid, 1001);
|
|
296
|
+
assert_eq!(parsed.home, "/home/admin");
|
|
297
|
+
assert_eq!(parsed.shell, "/bin/bash");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn test_parse_passwd_line_minimal() {
|
|
302
|
+
let line = "user:x:1000:1000::/home/user:/bin/sh";
|
|
303
|
+
let parsed = parse_passwd_line(line).unwrap();
|
|
304
|
+
assert_eq!(parsed.username, "user");
|
|
305
|
+
assert_eq!(parsed.uid, 1000);
|
|
306
|
+
assert_eq!(parsed.home, "/home/user");
|
|
307
|
+
assert_eq!(parsed.shell, "/bin/sh");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[test]
|
|
311
|
+
fn test_parse_passwd_line_invalid() {
|
|
312
|
+
assert!(parse_passwd_line("invalid").is_none());
|
|
313
|
+
assert!(parse_passwd_line("too:few:fields").is_none());
|
|
314
|
+
assert!(parse_passwd_line("user:x:not_a_number:1000::/home/user:/bin/bash").is_none());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn test_user_info_struct() {
|
|
319
|
+
let info = UserInfo {
|
|
320
|
+
username: "admin".to_string(),
|
|
321
|
+
uid: 1001,
|
|
322
|
+
home: "/home/admin".to_string(),
|
|
323
|
+
shell: "/bin/bash".to_string(),
|
|
324
|
+
locked: false,
|
|
325
|
+
};
|
|
326
|
+
assert_eq!(info.username, "admin");
|
|
327
|
+
assert!(!info.locked);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn test_user_info_equality() {
|
|
332
|
+
let info1 = UserInfo {
|
|
333
|
+
username: "admin".to_string(),
|
|
334
|
+
uid: 1001,
|
|
335
|
+
home: "/home/admin".to_string(),
|
|
336
|
+
shell: "/bin/bash".to_string(),
|
|
337
|
+
locked: false,
|
|
338
|
+
};
|
|
339
|
+
let info2 = info1.clone();
|
|
340
|
+
assert_eq!(info1, info2);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn test_user_info_debug() {
|
|
345
|
+
let info = UserInfo {
|
|
346
|
+
username: "test".to_string(),
|
|
347
|
+
uid: 1000,
|
|
348
|
+
home: "/home/test".to_string(),
|
|
349
|
+
shell: "/bin/bash".to_string(),
|
|
350
|
+
locked: true,
|
|
351
|
+
};
|
|
352
|
+
let debug = format!("{info:?}");
|
|
353
|
+
assert!(debug.contains("test"));
|
|
354
|
+
assert!(debug.contains("1000"));
|
|
355
|
+
assert!(debug.contains("locked: true"));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//! Docker image version detection
|
|
2
|
+
//!
|
|
3
|
+
//! Reads version information from Docker image labels.
|
|
4
|
+
|
|
5
|
+
use super::{DockerClient, DockerError};
|
|
6
|
+
|
|
7
|
+
/// Version label key in Docker image
|
|
8
|
+
pub const VERSION_LABEL: &str = "org.opencode-cloud.version";
|
|
9
|
+
|
|
10
|
+
/// Get version from image label
|
|
11
|
+
///
|
|
12
|
+
/// Returns None if image doesn't exist or has no version label.
|
|
13
|
+
/// Version label is set during automated builds; local builds have "dev".
|
|
14
|
+
pub async fn get_image_version(
|
|
15
|
+
client: &DockerClient,
|
|
16
|
+
image_name: &str,
|
|
17
|
+
) -> Result<Option<String>, DockerError> {
|
|
18
|
+
let inspect = match client.inner().inspect_image(image_name).await {
|
|
19
|
+
Ok(info) => info,
|
|
20
|
+
Err(bollard::errors::Error::DockerResponseServerError {
|
|
21
|
+
status_code: 404, ..
|
|
22
|
+
}) => {
|
|
23
|
+
return Ok(None);
|
|
24
|
+
}
|
|
25
|
+
Err(e) => {
|
|
26
|
+
return Err(DockerError::Connection(format!(
|
|
27
|
+
"Failed to inspect image: {e}"
|
|
28
|
+
)));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Extract version from labels
|
|
33
|
+
let version = inspect
|
|
34
|
+
.config
|
|
35
|
+
.and_then(|c| c.labels)
|
|
36
|
+
.and_then(|labels| labels.get(VERSION_LABEL).cloned());
|
|
37
|
+
|
|
38
|
+
Ok(version)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// CLI version from Cargo.toml
|
|
42
|
+
pub fn get_cli_version() -> &'static str {
|
|
43
|
+
env!("CARGO_PKG_VERSION")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Compare versions and determine if they match
|
|
47
|
+
///
|
|
48
|
+
/// Returns true if versions are compatible (same or dev build).
|
|
49
|
+
/// Returns false if versions differ and user should be prompted.
|
|
50
|
+
pub fn versions_compatible(cli_version: &str, image_version: Option<&str>) -> bool {
|
|
51
|
+
match image_version {
|
|
52
|
+
None => true, // No version label = local build, assume compatible
|
|
53
|
+
Some("dev") => true, // Dev build, assume compatible
|
|
54
|
+
Some(img_ver) => cli_version == img_ver,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[cfg(test)]
|
|
59
|
+
mod tests {
|
|
60
|
+
use super::*;
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn test_versions_compatible_none() {
|
|
64
|
+
assert!(versions_compatible("1.0.8", None));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn test_versions_compatible_dev() {
|
|
69
|
+
assert!(versions_compatible("1.0.8", Some("dev")));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn test_versions_compatible_same() {
|
|
74
|
+
assert!(versions_compatible("1.0.8", Some("1.0.8")));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn test_versions_compatible_different() {
|
|
79
|
+
assert!(!versions_compatible("1.0.8", Some("1.0.7")));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn test_get_cli_version_format() {
|
|
84
|
+
let version = get_cli_version();
|
|
85
|
+
// Should be semver format
|
|
86
|
+
assert!(version.contains('.'));
|
|
87
|
+
let parts: Vec<&str> = version.split('.').collect();
|
|
88
|
+
assert_eq!(parts.len(), 3);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn test_version_label_constant() {
|
|
93
|
+
assert_eq!(VERSION_LABEL, "org.opencode-cloud.version");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//! Host-specific error types
|
|
2
|
+
//!
|
|
3
|
+
//! Errors that can occur during remote host operations.
|
|
4
|
+
|
|
5
|
+
use thiserror::Error;
|
|
6
|
+
|
|
7
|
+
/// Errors that can occur during host operations
|
|
8
|
+
#[derive(Error, Debug)]
|
|
9
|
+
pub enum HostError {
|
|
10
|
+
/// Failed to spawn SSH process
|
|
11
|
+
#[error("Failed to spawn SSH: {0}")]
|
|
12
|
+
SshSpawn(String),
|
|
13
|
+
|
|
14
|
+
/// SSH connection failed
|
|
15
|
+
#[error("SSH connection failed: {0}")]
|
|
16
|
+
ConnectionFailed(String),
|
|
17
|
+
|
|
18
|
+
/// SSH authentication failed (key not in agent, passphrase needed)
|
|
19
|
+
#[error("SSH authentication failed. Ensure your key is loaded: ssh-add {}", .key_hint.as_deref().unwrap_or("~/.ssh/id_rsa"))]
|
|
20
|
+
AuthFailed { key_hint: Option<String> },
|
|
21
|
+
|
|
22
|
+
/// Host not found in hosts.json
|
|
23
|
+
#[error("Host not found: {0}")]
|
|
24
|
+
NotFound(String),
|
|
25
|
+
|
|
26
|
+
/// Host already exists
|
|
27
|
+
#[error("Host already exists: {0}")]
|
|
28
|
+
AlreadyExists(String),
|
|
29
|
+
|
|
30
|
+
/// Failed to allocate local port for tunnel
|
|
31
|
+
#[error("Failed to allocate local port: {0}")]
|
|
32
|
+
PortAllocation(String),
|
|
33
|
+
|
|
34
|
+
/// Failed to load hosts file
|
|
35
|
+
#[error("Failed to load hosts file: {0}")]
|
|
36
|
+
LoadFailed(String),
|
|
37
|
+
|
|
38
|
+
/// Failed to save hosts file
|
|
39
|
+
#[error("Failed to save hosts file: {0}")]
|
|
40
|
+
SaveFailed(String),
|
|
41
|
+
|
|
42
|
+
/// Invalid host configuration
|
|
43
|
+
#[error("Invalid host configuration: {0}")]
|
|
44
|
+
InvalidConfig(String),
|
|
45
|
+
|
|
46
|
+
/// Tunnel connection timed out
|
|
47
|
+
#[error("SSH tunnel connection timed out after {0} attempts")]
|
|
48
|
+
TunnelTimeout(u32),
|
|
49
|
+
|
|
50
|
+
/// Remote Docker not available
|
|
51
|
+
#[error("Docker not available on remote host: {0}")]
|
|
52
|
+
RemoteDockerUnavailable(String),
|
|
53
|
+
|
|
54
|
+
/// Failed to read SSH config
|
|
55
|
+
#[error("Failed to read SSH config: {0}")]
|
|
56
|
+
SshConfigRead(String),
|
|
57
|
+
|
|
58
|
+
/// Failed to write SSH config
|
|
59
|
+
#[error("Failed to write SSH config: {0}")]
|
|
60
|
+
SshConfigWrite(String),
|
|
61
|
+
}
|
package/src/host/mod.rs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//! Host management module
|
|
2
|
+
//!
|
|
3
|
+
//! Provides functionality for managing remote Docker hosts:
|
|
4
|
+
//! - Host configuration schema and storage
|
|
5
|
+
//! - SSH tunnel management for remote Docker access
|
|
6
|
+
//! - Connection testing and validation
|
|
7
|
+
//! - SSH config file parsing and writing
|
|
8
|
+
//! - Remote Docker provisioning
|
|
9
|
+
|
|
10
|
+
mod error;
|
|
11
|
+
mod provision;
|
|
12
|
+
mod schema;
|
|
13
|
+
mod ssh_config;
|
|
14
|
+
mod storage;
|
|
15
|
+
mod tunnel;
|
|
16
|
+
|
|
17
|
+
// Public exports
|
|
18
|
+
pub use error::HostError;
|
|
19
|
+
pub use provision::{
|
|
20
|
+
DistroFamily, DistroInfo, detect_distro, get_docker_install_commands, install_docker,
|
|
21
|
+
verify_docker_installed,
|
|
22
|
+
};
|
|
23
|
+
pub use schema::{HostConfig, HostsFile};
|
|
24
|
+
pub use ssh_config::{
|
|
25
|
+
SshConfigMatch, get_ssh_config_path, host_exists_in_ssh_config, query_ssh_config,
|
|
26
|
+
write_ssh_config_entry,
|
|
27
|
+
};
|
|
28
|
+
pub use storage::{load_hosts, save_hosts};
|
|
29
|
+
pub use tunnel::{SshTunnel, test_connection};
|