@opencode-cloud/core 1.0.7 → 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 +39 -32
- package/README.md +17 -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,271 @@
|
|
|
1
|
+
//! Configuration validation with actionable error messages
|
|
2
|
+
//!
|
|
3
|
+
//! Validates the configuration and provides exact commands to fix issues.
|
|
4
|
+
|
|
5
|
+
use super::schema::{Config, validate_bind_address};
|
|
6
|
+
use console::style;
|
|
7
|
+
|
|
8
|
+
/// A configuration validation error with an actionable fix command
|
|
9
|
+
#[derive(Debug, Clone)]
|
|
10
|
+
pub struct ValidationError {
|
|
11
|
+
/// The config field that has an error
|
|
12
|
+
pub field: String,
|
|
13
|
+
/// Description of what's wrong
|
|
14
|
+
pub message: String,
|
|
15
|
+
/// Exact occ command to fix the issue
|
|
16
|
+
pub fix_command: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// A configuration validation warning (non-fatal)
|
|
20
|
+
#[derive(Debug, Clone)]
|
|
21
|
+
pub struct ValidationWarning {
|
|
22
|
+
/// The config field with a potential issue
|
|
23
|
+
pub field: String,
|
|
24
|
+
/// Description of the warning
|
|
25
|
+
pub message: String,
|
|
26
|
+
/// Suggested occ command to address the warning
|
|
27
|
+
pub fix_command: String,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Validate configuration and return warnings or first error
|
|
31
|
+
///
|
|
32
|
+
/// Returns Ok(warnings) if validation passes (possibly with non-fatal warnings).
|
|
33
|
+
/// Returns Err(error) on the first fatal validation error encountered.
|
|
34
|
+
///
|
|
35
|
+
/// Validation is performed in order, stopping at the first error.
|
|
36
|
+
pub fn validate_config(config: &Config) -> Result<Vec<ValidationWarning>, ValidationError> {
|
|
37
|
+
let mut warnings = Vec::new();
|
|
38
|
+
|
|
39
|
+
// Port validation
|
|
40
|
+
if config.opencode_web_port < 1024 {
|
|
41
|
+
return Err(ValidationError {
|
|
42
|
+
field: "opencode_web_port".to_string(),
|
|
43
|
+
message: "Port must be >= 1024 (non-privileged)".to_string(),
|
|
44
|
+
fix_command: "occ config set opencode_web_port 3000".to_string(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Note: No need to check > 65535 - u16 type enforces this limit
|
|
48
|
+
|
|
49
|
+
// Bind address validation
|
|
50
|
+
if let Err(msg) = validate_bind_address(&config.bind_address) {
|
|
51
|
+
return Err(ValidationError {
|
|
52
|
+
field: "bind_address".to_string(),
|
|
53
|
+
message: msg,
|
|
54
|
+
fix_command: "occ config set bind_address 127.0.0.1".to_string(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Boot mode validation
|
|
59
|
+
if config.boot_mode != "user" && config.boot_mode != "system" {
|
|
60
|
+
return Err(ValidationError {
|
|
61
|
+
field: "boot_mode".to_string(),
|
|
62
|
+
message: "boot_mode must be 'user' or 'system'".to_string(),
|
|
63
|
+
fix_command: "occ config set boot_mode user".to_string(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Rate limit validation
|
|
68
|
+
if config.rate_limit_attempts == 0 {
|
|
69
|
+
return Err(ValidationError {
|
|
70
|
+
field: "rate_limit_attempts".to_string(),
|
|
71
|
+
message: "rate_limit_attempts must be > 0".to_string(),
|
|
72
|
+
fix_command: "occ config set rate_limit_attempts 5".to_string(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if config.rate_limit_window_seconds == 0 {
|
|
77
|
+
return Err(ValidationError {
|
|
78
|
+
field: "rate_limit_window_seconds".to_string(),
|
|
79
|
+
message: "rate_limit_window_seconds must be > 0".to_string(),
|
|
80
|
+
fix_command: "occ config set rate_limit_window_seconds 60".to_string(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Warnings (non-fatal)
|
|
85
|
+
|
|
86
|
+
// Network exposure without auth
|
|
87
|
+
if config.is_network_exposed()
|
|
88
|
+
&& config.users.is_empty()
|
|
89
|
+
&& !config.allow_unauthenticated_network
|
|
90
|
+
{
|
|
91
|
+
warnings.push(ValidationWarning {
|
|
92
|
+
field: "bind_address".to_string(),
|
|
93
|
+
message: "Network exposed without authentication".to_string(),
|
|
94
|
+
fix_command: "occ user add".to_string(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Legacy auth fields present
|
|
99
|
+
if let Some(ref username) = config.auth_username {
|
|
100
|
+
if !username.is_empty() {
|
|
101
|
+
warnings.push(ValidationWarning {
|
|
102
|
+
field: "auth_username".to_string(),
|
|
103
|
+
message: "Legacy auth fields present; consider using 'occ user add' instead"
|
|
104
|
+
.to_string(),
|
|
105
|
+
fix_command: "occ config set auth_username ''".to_string(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if let Some(ref password) = config.auth_password {
|
|
111
|
+
if !password.is_empty() {
|
|
112
|
+
warnings.push(ValidationWarning {
|
|
113
|
+
field: "auth_password".to_string(),
|
|
114
|
+
message: "Legacy auth fields present; consider using 'occ user add' instead"
|
|
115
|
+
.to_string(),
|
|
116
|
+
fix_command: "occ config set auth_password ''".to_string(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ok(warnings)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Display a validation error with styled formatting
|
|
125
|
+
pub fn display_validation_error(error: &ValidationError) {
|
|
126
|
+
eprintln!();
|
|
127
|
+
eprintln!("{}", style("Error: Configuration error").red().bold());
|
|
128
|
+
eprintln!();
|
|
129
|
+
eprintln!(" {} {}", style("Field:").dim(), error.field);
|
|
130
|
+
eprintln!(" {} {}", style("Problem:").dim(), error.message);
|
|
131
|
+
eprintln!();
|
|
132
|
+
eprintln!("{}:", style("To fix, run").dim());
|
|
133
|
+
eprintln!(" {}", style(&error.fix_command).cyan());
|
|
134
|
+
eprintln!();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Display a validation warning with styled formatting
|
|
138
|
+
pub fn display_validation_warning(warning: &ValidationWarning) {
|
|
139
|
+
eprintln!();
|
|
140
|
+
eprintln!(
|
|
141
|
+
"{}",
|
|
142
|
+
style("Warning: Configuration warning").yellow().bold()
|
|
143
|
+
);
|
|
144
|
+
eprintln!();
|
|
145
|
+
eprintln!(" {} {}", style("Field:").dim(), warning.field);
|
|
146
|
+
eprintln!(" {} {}", style("Issue:").dim(), warning.message);
|
|
147
|
+
eprintln!();
|
|
148
|
+
eprintln!("{}:", style("To address, run").dim());
|
|
149
|
+
eprintln!(" {}", style(&warning.fix_command).cyan());
|
|
150
|
+
eprintln!();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[cfg(test)]
|
|
154
|
+
mod tests {
|
|
155
|
+
use super::*;
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn test_valid_config_passes() {
|
|
159
|
+
let config = Config::default();
|
|
160
|
+
let result = validate_config(&config);
|
|
161
|
+
assert!(result.is_ok());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[test]
|
|
165
|
+
fn test_port_too_low() {
|
|
166
|
+
let config = Config {
|
|
167
|
+
opencode_web_port: 80,
|
|
168
|
+
..Config::default()
|
|
169
|
+
};
|
|
170
|
+
let result = validate_config(&config);
|
|
171
|
+
assert!(result.is_err());
|
|
172
|
+
let err = result.unwrap_err();
|
|
173
|
+
assert_eq!(err.field, "opencode_web_port");
|
|
174
|
+
assert!(err.message.contains("1024"));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Note: No test for port > 65535 - u16 type enforces this limit at compile time
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn test_invalid_bind_address() {
|
|
181
|
+
let config = Config {
|
|
182
|
+
bind_address: "not-an-ip".to_string(),
|
|
183
|
+
..Config::default()
|
|
184
|
+
};
|
|
185
|
+
let result = validate_config(&config);
|
|
186
|
+
assert!(result.is_err());
|
|
187
|
+
let err = result.unwrap_err();
|
|
188
|
+
assert_eq!(err.field, "bind_address");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn test_invalid_boot_mode() {
|
|
193
|
+
let config = Config {
|
|
194
|
+
boot_mode: "invalid".to_string(),
|
|
195
|
+
..Config::default()
|
|
196
|
+
};
|
|
197
|
+
let result = validate_config(&config);
|
|
198
|
+
assert!(result.is_err());
|
|
199
|
+
let err = result.unwrap_err();
|
|
200
|
+
assert_eq!(err.field, "boot_mode");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn test_rate_limit_attempts_zero() {
|
|
205
|
+
let config = Config {
|
|
206
|
+
rate_limit_attempts: 0,
|
|
207
|
+
..Config::default()
|
|
208
|
+
};
|
|
209
|
+
let result = validate_config(&config);
|
|
210
|
+
assert!(result.is_err());
|
|
211
|
+
let err = result.unwrap_err();
|
|
212
|
+
assert_eq!(err.field, "rate_limit_attempts");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn test_rate_limit_window_zero() {
|
|
217
|
+
let config = Config {
|
|
218
|
+
rate_limit_window_seconds: 0,
|
|
219
|
+
..Config::default()
|
|
220
|
+
};
|
|
221
|
+
let result = validate_config(&config);
|
|
222
|
+
assert!(result.is_err());
|
|
223
|
+
let err = result.unwrap_err();
|
|
224
|
+
assert_eq!(err.field, "rate_limit_window_seconds");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn test_network_exposed_without_auth_warning() {
|
|
229
|
+
let config = Config {
|
|
230
|
+
bind_address: "0.0.0.0".to_string(),
|
|
231
|
+
users: Vec::new(),
|
|
232
|
+
allow_unauthenticated_network: false,
|
|
233
|
+
..Config::default()
|
|
234
|
+
};
|
|
235
|
+
let result = validate_config(&config);
|
|
236
|
+
assert!(result.is_ok());
|
|
237
|
+
let warnings = result.unwrap();
|
|
238
|
+
assert!(!warnings.is_empty());
|
|
239
|
+
assert!(
|
|
240
|
+
warnings
|
|
241
|
+
.iter()
|
|
242
|
+
.any(|w| w.message.contains("Network exposed"))
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn test_legacy_auth_username_warning() {
|
|
248
|
+
let config = Config {
|
|
249
|
+
auth_username: Some("admin".to_string()),
|
|
250
|
+
..Config::default()
|
|
251
|
+
};
|
|
252
|
+
let result = validate_config(&config);
|
|
253
|
+
assert!(result.is_ok());
|
|
254
|
+
let warnings = result.unwrap();
|
|
255
|
+
assert!(!warnings.is_empty());
|
|
256
|
+
assert!(warnings.iter().any(|w| w.field == "auth_username"));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn test_legacy_auth_password_warning() {
|
|
261
|
+
let config = Config {
|
|
262
|
+
auth_password: Some("secret".to_string()),
|
|
263
|
+
..Config::default()
|
|
264
|
+
};
|
|
265
|
+
let result = validate_config(&config);
|
|
266
|
+
assert!(result.is_ok());
|
|
267
|
+
let warnings = result.unwrap();
|
|
268
|
+
assert!(!warnings.is_empty());
|
|
269
|
+
assert!(warnings.iter().any(|w| w.field == "auth_password"));
|
|
270
|
+
}
|
|
271
|
+
}
|