microsandbox-rb 0.5.9 → 0.5.11
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +91 -0
- data/Cargo.lock +90 -45
- data/DESIGN.md +7 -3
- data/README.md +91 -26
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/extconf.rb +6 -2
- data/ext/microsandbox/src/backend.rs +170 -0
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/image.rs +7 -7
- data/ext/microsandbox/src/lib.rs +27 -4
- data/ext/microsandbox/src/sandbox.rs +208 -61
- data/ext/microsandbox/src/volume.rs +6 -1
- data/lib/microsandbox/errors.rb +6 -0
- data/lib/microsandbox/exec_handle.rb +14 -11
- data/lib/microsandbox/fs.rb +7 -7
- data/lib/microsandbox/image.rb +1 -1
- data/lib/microsandbox/network.rb +19 -19
- data/lib/microsandbox/patch.rb +8 -8
- data/lib/microsandbox/sandbox.rb +230 -84
- data/lib/microsandbox/snapshot.rb +2 -2
- data/lib/microsandbox/ssh.rb +2 -2
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +3 -3
- data/lib/microsandbox.rb +61 -0
- data/sig/microsandbox.rbs +31 -13
- metadata +2 -1
|
@@ -89,9 +89,11 @@ impl Sandbox {
|
|
|
89
89
|
for (host, guest) in conv::opt_port_map(opts, "ports")? {
|
|
90
90
|
b = b.port(host, guest);
|
|
91
91
|
}
|
|
92
|
-
// volumes: normalized by the Ruby layer to [guest, kind, source] triples
|
|
92
|
+
// volumes: normalized by the Ruby layer to [guest, kind, source] triples,
|
|
93
|
+
// or [guest, kind, source, options] quads where options is a comma-joined
|
|
94
|
+
// list (ro/readonly, rw, noexec, nosuid, nodev).
|
|
93
95
|
for spec in conv::opt::<Vec<Vec<String>>>(opts, "volumes")?.unwrap_or_default() {
|
|
94
|
-
if spec.len()
|
|
96
|
+
if spec.len() < 3 || spec.len() > 4 {
|
|
95
97
|
return Err(error::base_error("invalid volume mount spec"));
|
|
96
98
|
}
|
|
97
99
|
let (guest, kind, source) = (spec[0].clone(), spec[1].clone(), spec[2].clone());
|
|
@@ -103,12 +105,43 @@ impl Sandbox {
|
|
|
103
105
|
)))
|
|
104
106
|
}
|
|
105
107
|
}
|
|
108
|
+
// Validate options up front (the volume closure cannot return an error).
|
|
109
|
+
let mount_opts: Vec<String> = spec
|
|
110
|
+
.get(3)
|
|
111
|
+
.map(|s| {
|
|
112
|
+
s.split(',')
|
|
113
|
+
.map(|o| o.trim().to_string())
|
|
114
|
+
.filter(|o| !o.is_empty())
|
|
115
|
+
.collect()
|
|
116
|
+
})
|
|
117
|
+
.unwrap_or_default();
|
|
118
|
+
for opt in &mount_opts {
|
|
119
|
+
match opt.as_str() {
|
|
120
|
+
"ro" | "readonly" | "rw" | "noexec" | "nosuid" | "nodev" => {}
|
|
121
|
+
other => {
|
|
122
|
+
return Err(error::base_error(format!(
|
|
123
|
+
"unknown volume mount option {other:?} \
|
|
124
|
+
(expected ro/rw/noexec/nosuid/nodev)"
|
|
125
|
+
)))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
106
129
|
b = b.volume(guest, move |m| {
|
|
107
|
-
if kind == "named" {
|
|
130
|
+
let mut m = if kind == "named" {
|
|
108
131
|
m.named(source)
|
|
109
132
|
} else {
|
|
110
133
|
m.bind(source)
|
|
134
|
+
};
|
|
135
|
+
for opt in &mount_opts {
|
|
136
|
+
m = match opt.as_str() {
|
|
137
|
+
"ro" | "readonly" => m.readonly(),
|
|
138
|
+
"noexec" => m.noexec(),
|
|
139
|
+
"nosuid" => m.nosuid(),
|
|
140
|
+
"nodev" => m.nodev(),
|
|
141
|
+
_ => m, // "rw" — default; already validated above
|
|
142
|
+
};
|
|
111
143
|
}
|
|
144
|
+
m
|
|
112
145
|
});
|
|
113
146
|
}
|
|
114
147
|
// patches: rootfs modifications applied before boot. The Ruby layer
|
|
@@ -209,27 +242,36 @@ impl Sandbox {
|
|
|
209
242
|
Ok(Sandbox::from_inner(inner))
|
|
210
243
|
}
|
|
211
244
|
|
|
212
|
-
///
|
|
213
|
-
|
|
245
|
+
/// A controllable handle for a sandbox by name (running or not). Carries
|
|
246
|
+
/// metadata accessors and the full lifecycle surface (see `SbHandle`).
|
|
247
|
+
fn get(name: String) -> Result<SbHandle, Error> {
|
|
214
248
|
let handle = block_on(microsandbox::Sandbox::get(&name)).map_err(error::to_ruby)?;
|
|
215
|
-
Ok(
|
|
249
|
+
Ok(SbHandle::from_inner(handle))
|
|
216
250
|
}
|
|
217
251
|
|
|
218
|
-
/// All sandboxes as
|
|
252
|
+
/// All sandboxes as controllable handles.
|
|
219
253
|
fn list() -> Result<RArray, Error> {
|
|
220
254
|
let handles = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
|
|
221
|
-
|
|
255
|
+
let arr = ruby().ary_new();
|
|
256
|
+
for h in handles {
|
|
257
|
+
arr.push(SbHandle::from_inner(h))?;
|
|
258
|
+
}
|
|
259
|
+
Ok(arr)
|
|
222
260
|
}
|
|
223
261
|
|
|
224
|
-
/// Sandboxes filtered by required `key=value` labels (AND-matched)
|
|
225
|
-
/// carries a string→string `labels` map.
|
|
262
|
+
/// Sandboxes filtered by required `key=value` labels (AND-matched), as
|
|
263
|
+
/// controllable handles. `opts` carries a string→string `labels` map.
|
|
226
264
|
fn list_with(opts: RHash) -> Result<RArray, Error> {
|
|
227
265
|
let mut filter = SandboxFilter::new();
|
|
228
266
|
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
229
267
|
filter = filter.label(k, v);
|
|
230
268
|
}
|
|
231
269
|
let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
|
|
232
|
-
|
|
270
|
+
let arr = ruby().ary_new();
|
|
271
|
+
for h in handles {
|
|
272
|
+
arr.push(SbHandle::from_inner(h))?;
|
|
273
|
+
}
|
|
274
|
+
Ok(arr)
|
|
233
275
|
}
|
|
234
276
|
|
|
235
277
|
/// Remove a (stopped) sandbox by name.
|
|
@@ -286,44 +328,46 @@ impl Sandbox {
|
|
|
286
328
|
Ok(ExecHandle::from_core(handle))
|
|
287
329
|
}
|
|
288
330
|
|
|
289
|
-
/// Graceful stop
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
331
|
+
/// Graceful stop. Mirrors the official SDKs: the live handle routes through
|
|
332
|
+
/// a freshly fetched `SandboxHandle::stop` (SIGTERM→SIGKILL escalation with
|
|
333
|
+
/// a 10s default). Fine-grained control — a custom timeout or fire-and-
|
|
334
|
+
/// return `request_*` — lives on `SandboxHandle`, obtained via `Sandbox.get`.
|
|
335
|
+
fn stop(&self) -> Result<(), Error> {
|
|
336
|
+
let name = self.inner.name().to_string();
|
|
337
|
+
block_on(async move {
|
|
338
|
+
let handle = microsandbox::sandbox::Sandbox::get(&name).await?;
|
|
339
|
+
handle.stop().await
|
|
340
|
+
})
|
|
295
341
|
.map_err(error::to_ruby)
|
|
296
342
|
}
|
|
297
343
|
|
|
298
|
-
///
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
.map_err(error::to_ruby)
|
|
344
|
+
/// Graceful stop, then wait for the process to exit. Returns an exit-status
|
|
345
|
+
/// Hash (`exit_code`, `success`). Local backend only.
|
|
346
|
+
fn stop_and_wait(&self) -> Result<RHash, Error> {
|
|
347
|
+
let status = block_on(self.inner.stop_and_wait()).map_err(error::to_ruby)?;
|
|
348
|
+
Ok(exit_status_to_hash(status))
|
|
305
349
|
}
|
|
306
350
|
|
|
307
|
-
///
|
|
308
|
-
fn
|
|
309
|
-
block_on(self.inner.
|
|
351
|
+
/// Force kill (SIGKILL).
|
|
352
|
+
fn kill(&self) -> Result<(), Error> {
|
|
353
|
+
block_on(self.inner.kill()).map_err(error::to_ruby)
|
|
310
354
|
}
|
|
311
355
|
|
|
312
|
-
///
|
|
313
|
-
fn
|
|
314
|
-
block_on(self.inner.
|
|
356
|
+
/// Trigger a graceful drain (SIGUSR1 on local).
|
|
357
|
+
fn drain(&self) -> Result<(), Error> {
|
|
358
|
+
block_on(self.inner.drain()).map_err(error::to_ruby)
|
|
315
359
|
}
|
|
316
360
|
|
|
317
|
-
///
|
|
318
|
-
fn
|
|
319
|
-
block_on(self.inner.
|
|
361
|
+
/// Wait for the process to exit. Returns an exit-status Hash. Local only.
|
|
362
|
+
fn wait(&self) -> Result<RHash, Error> {
|
|
363
|
+
let status = block_on(self.inner.wait()).map_err(error::to_ruby)?;
|
|
364
|
+
Ok(exit_status_to_hash(status))
|
|
320
365
|
}
|
|
321
366
|
|
|
322
|
-
///
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
Ok(stop_result_to_hash(&result))
|
|
367
|
+
/// Live status fetched from the backend (a round-trip per call).
|
|
368
|
+
fn status(&self) -> Result<String, Error> {
|
|
369
|
+
let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
|
|
370
|
+
Ok(sandbox_status_str(status).to_string())
|
|
327
371
|
}
|
|
328
372
|
|
|
329
373
|
/// Whether this handle owns the sandbox process lifecycle (a synchronous,
|
|
@@ -1064,6 +1108,7 @@ struct ExecOpts {
|
|
|
1064
1108
|
timeout: Option<Duration>,
|
|
1065
1109
|
tty: bool,
|
|
1066
1110
|
stdin: Option<Vec<u8>>,
|
|
1111
|
+
stdin_pipe: bool,
|
|
1067
1112
|
rlimits: Vec<(RlimitResource, u64, u64)>,
|
|
1068
1113
|
}
|
|
1069
1114
|
|
|
@@ -1078,6 +1123,7 @@ impl ExecOpts {
|
|
|
1078
1123
|
timeout: conv::opt_f64(opts, "timeout")?.map(Duration::from_secs_f64),
|
|
1079
1124
|
tty: conv::opt_bool(opts, "tty")?,
|
|
1080
1125
|
stdin,
|
|
1126
|
+
stdin_pipe: conv::opt_bool(opts, "stdin_pipe")?,
|
|
1081
1127
|
rlimits: parse_rlimits(opts)?,
|
|
1082
1128
|
})
|
|
1083
1129
|
}
|
|
@@ -1104,7 +1150,13 @@ impl ExecOpts {
|
|
|
1104
1150
|
if self.tty {
|
|
1105
1151
|
b = b.tty(true);
|
|
1106
1152
|
}
|
|
1107
|
-
|
|
1153
|
+
// Pipe mode opens a writable stdin sink (lifted out by ExecHandle via
|
|
1154
|
+
// `take_stdin`); bytes mode feeds a fixed buffer and closes. The core's
|
|
1155
|
+
// `StdinMode` is a single enum, so the two are mutually exclusive — pipe
|
|
1156
|
+
// wins if a caller somehow sets both.
|
|
1157
|
+
if self.stdin_pipe {
|
|
1158
|
+
b = b.stdin_pipe();
|
|
1159
|
+
} else if let Some(stdin) = self.stdin {
|
|
1108
1160
|
b = b.stdin_bytes(stdin);
|
|
1109
1161
|
}
|
|
1110
1162
|
for (resource, soft, hard) in self.rlimits {
|
|
@@ -1195,7 +1247,13 @@ pub(crate) fn metrics_to_hash(m: &SandboxMetrics) -> RHash {
|
|
|
1195
1247
|
}
|
|
1196
1248
|
|
|
1197
1249
|
fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
1250
|
+
// Lowercased `Debug` names, matching the official SDKs' `format!("{:?}")`.
|
|
1251
|
+
// `Created`/`Starting` are new in v0.5.8 (cloud-only today). The match is
|
|
1252
|
+
// intentionally exhaustive — no wildcard — so a future upstream variant
|
|
1253
|
+
// surfaces as a compile error rather than a silent fallback.
|
|
1198
1254
|
match status {
|
|
1255
|
+
SandboxStatus::Created => "created",
|
|
1256
|
+
SandboxStatus::Starting => "starting",
|
|
1199
1257
|
SandboxStatus::Running => "running",
|
|
1200
1258
|
SandboxStatus::Draining => "draining",
|
|
1201
1259
|
SandboxStatus::Paused => "paused",
|
|
@@ -1204,6 +1262,15 @@ fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
|
1204
1262
|
}
|
|
1205
1263
|
}
|
|
1206
1264
|
|
|
1265
|
+
/// A `std::process::ExitStatus` as a Ruby Hash: `exit_code` (Integer or nil) and
|
|
1266
|
+
/// `success` (Boolean). Returned by the live `Sandbox#wait` / `#stop_and_wait`.
|
|
1267
|
+
fn exit_status_to_hash(status: std::process::ExitStatus) -> RHash {
|
|
1268
|
+
let hash = ruby().hash_new();
|
|
1269
|
+
let _ = hash.aset("exit_code", status.code());
|
|
1270
|
+
let _ = hash.aset("success", status.success());
|
|
1271
|
+
hash
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1207
1274
|
fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
1208
1275
|
let hash = ruby().hash_new();
|
|
1209
1276
|
let _ = hash.aset("name", result.name.clone());
|
|
@@ -1215,19 +1282,85 @@ fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
|
1215
1282
|
hash
|
|
1216
1283
|
}
|
|
1217
1284
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1285
|
+
//--------------------------------------------------------------------------------------------------
|
|
1286
|
+
// SandboxHandle — the controllable lightweight handle
|
|
1287
|
+
//--------------------------------------------------------------------------------------------------
|
|
1288
|
+
|
|
1289
|
+
/// Wraps a core `microsandbox::sandbox::SandboxHandle` (returned by
|
|
1290
|
+
/// `Sandbox.get`/`list`/`list_with`). Carries metadata accessors plus the rich
|
|
1291
|
+
/// lifecycle surface that moved off the live `Sandbox` in v0.5.8 — mirroring the
|
|
1292
|
+
/// official Python (`PySandboxHandle`) and Node (`SandboxHandle`) SDKs. Status is
|
|
1293
|
+
/// a synchronous snapshot read off the handle (no round-trip).
|
|
1294
|
+
#[magnus::wrap(class = "Microsandbox::Native::SandboxHandle", free_immediately, size)]
|
|
1295
|
+
pub struct SbHandle {
|
|
1296
|
+
inner: SandboxHandle,
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
impl SbHandle {
|
|
1300
|
+
fn from_inner(inner: SandboxHandle) -> Self {
|
|
1301
|
+
Self { inner }
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
fn name(&self) -> String {
|
|
1305
|
+
self.inner.name().to_string()
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/// Status snapshot captured when the handle was fetched (synchronous).
|
|
1309
|
+
fn status(&self) -> String {
|
|
1310
|
+
sandbox_status_str(self.inner.status_snapshot()).to_string()
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
fn created_at_ms(&self) -> Option<i64> {
|
|
1314
|
+
self.inner.created_at().map(|dt| dt.timestamp_millis())
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
fn updated_at_ms(&self) -> Option<i64> {
|
|
1318
|
+
self.inner.updated_at().map(|dt| dt.timestamp_millis())
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/// Graceful stop (SIGTERM→SIGKILL escalation, 10s default) and wait.
|
|
1322
|
+
fn stop(&self) -> Result<(), Error> {
|
|
1323
|
+
block_on(self.inner.stop()).map_err(error::to_ruby)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/// Graceful stop with a custom escalation timeout (seconds).
|
|
1327
|
+
fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1328
|
+
block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs)))
|
|
1329
|
+
.map_err(error::to_ruby)
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/// Force kill (SIGKILL) and wait.
|
|
1333
|
+
fn kill(&self) -> Result<(), Error> {
|
|
1334
|
+
block_on(self.inner.kill()).map_err(error::to_ruby)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/// Force kill, waiting up to `secs` for the process to disappear.
|
|
1338
|
+
fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
|
|
1339
|
+
block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs)))
|
|
1340
|
+
.map_err(error::to_ruby)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/// Send the graceful-shutdown request and return without waiting.
|
|
1344
|
+
fn request_stop(&self) -> Result<(), Error> {
|
|
1345
|
+
block_on(self.inner.request_stop()).map_err(error::to_ruby)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/// Send the force-kill request and return without waiting.
|
|
1349
|
+
fn request_kill(&self) -> Result<(), Error> {
|
|
1350
|
+
block_on(self.inner.request_kill()).map_err(error::to_ruby)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/// Send the drain request (SIGUSR1) and return without waiting.
|
|
1354
|
+
fn request_drain(&self) -> Result<(), Error> {
|
|
1355
|
+
block_on(self.inner.request_drain()).map_err(error::to_ruby)
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/// Block until the sandbox reaches a terminal state; returns a stop-result
|
|
1359
|
+
/// Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
1360
|
+
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
1361
|
+
let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
|
|
1362
|
+
Ok(stop_result_to_hash(&result))
|
|
1363
|
+
}
|
|
1231
1364
|
}
|
|
1232
1365
|
|
|
1233
1366
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -1328,15 +1461,12 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
1328
1461
|
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
1329
1462
|
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
1330
1463
|
class.define_method("shell_stream", method!(Sandbox::shell_stream, 2))?;
|
|
1331
|
-
class.define_method("stop", method!(Sandbox::stop,
|
|
1332
|
-
class.define_method("
|
|
1333
|
-
class.define_method("
|
|
1334
|
-
class.define_method("
|
|
1335
|
-
class.define_method("
|
|
1336
|
-
class.define_method(
|
|
1337
|
-
"wait_until_stopped",
|
|
1338
|
-
method!(Sandbox::wait_until_stopped, 0),
|
|
1339
|
-
)?;
|
|
1464
|
+
class.define_method("stop", method!(Sandbox::stop, 0))?;
|
|
1465
|
+
class.define_method("stop_and_wait", method!(Sandbox::stop_and_wait, 0))?;
|
|
1466
|
+
class.define_method("kill", method!(Sandbox::kill, 0))?;
|
|
1467
|
+
class.define_method("drain", method!(Sandbox::drain, 0))?;
|
|
1468
|
+
class.define_method("wait", method!(Sandbox::wait, 0))?;
|
|
1469
|
+
class.define_method("status", method!(Sandbox::status, 0))?;
|
|
1340
1470
|
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
1341
1471
|
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
1342
1472
|
class.define_method("metrics", method!(Sandbox::metrics, 0))?;
|
|
@@ -1367,5 +1497,22 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
1367
1497
|
class.define_method("attach", method!(Sandbox::attach, 3))?;
|
|
1368
1498
|
class.define_method("attach_shell", method!(Sandbox::attach_shell, 0))?;
|
|
1369
1499
|
|
|
1500
|
+
let handle = native.define_class("SandboxHandle", ruby.class_object())?;
|
|
1501
|
+
handle.define_method("name", method!(SbHandle::name, 0))?;
|
|
1502
|
+
handle.define_method("status", method!(SbHandle::status, 0))?;
|
|
1503
|
+
handle.define_method("created_at_ms", method!(SbHandle::created_at_ms, 0))?;
|
|
1504
|
+
handle.define_method("updated_at_ms", method!(SbHandle::updated_at_ms, 0))?;
|
|
1505
|
+
handle.define_method("stop", method!(SbHandle::stop, 0))?;
|
|
1506
|
+
handle.define_method("stop_with_timeout", method!(SbHandle::stop_with_timeout, 1))?;
|
|
1507
|
+
handle.define_method("kill", method!(SbHandle::kill, 0))?;
|
|
1508
|
+
handle.define_method("kill_with_timeout", method!(SbHandle::kill_with_timeout, 1))?;
|
|
1509
|
+
handle.define_method("request_stop", method!(SbHandle::request_stop, 0))?;
|
|
1510
|
+
handle.define_method("request_kill", method!(SbHandle::request_kill, 0))?;
|
|
1511
|
+
handle.define_method("request_drain", method!(SbHandle::request_drain, 0))?;
|
|
1512
|
+
handle.define_method(
|
|
1513
|
+
"wait_until_stopped",
|
|
1514
|
+
method!(SbHandle::wait_until_stopped, 0),
|
|
1515
|
+
)?;
|
|
1516
|
+
|
|
1370
1517
|
Ok(())
|
|
1371
1518
|
}
|
|
@@ -65,7 +65,12 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
65
65
|
let vol = block_on(builder.create()).map_err(error::to_ruby)?;
|
|
66
66
|
let hash = ruby().hash_new();
|
|
67
67
|
hash.aset("name", vol.name().to_string())?;
|
|
68
|
-
|
|
68
|
+
// `path()` is fallible as of v0.5.8 (returns `Unsupported` for cloud
|
|
69
|
+
// volumes); the volume just created here is local, so this resolves.
|
|
70
|
+
hash.aset(
|
|
71
|
+
"path",
|
|
72
|
+
vol.path().map_err(error::to_ruby)?.display().to_string(),
|
|
73
|
+
)?;
|
|
69
74
|
Ok(hash)
|
|
70
75
|
}
|
|
71
76
|
|
data/lib/microsandbox/errors.rb
CHANGED
|
@@ -65,4 +65,10 @@ module Microsandbox
|
|
|
65
65
|
|
|
66
66
|
# Runtime compatibility ---------------------------------------------------
|
|
67
67
|
define_error(:UnsupportedOperationError, "unsupported-operation")
|
|
68
|
+
|
|
69
|
+
# Cloud / backend routing errors (v0.5.8) ---------------------------------
|
|
70
|
+
# `UnsupportedError` (a backend feature gap, e.g. an op not yet wired on the
|
|
71
|
+
# cloud backend) is distinct from `UnsupportedOperationError` above.
|
|
72
|
+
define_error(:CloudHttpError, "cloud-http")
|
|
73
|
+
define_error(:UnsupportedError, "unsupported")
|
|
68
74
|
end
|
|
@@ -22,25 +22,28 @@ module Microsandbox
|
|
|
22
22
|
|
|
23
23
|
# @return [String, nil] {#data} decoded as UTF-8 (lenient)
|
|
24
24
|
def text
|
|
25
|
-
@data
|
|
25
|
+
@data&.dup&.force_encoding(Encoding::UTF_8)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def started?
|
|
29
|
-
def stdout?
|
|
30
|
-
def stderr?
|
|
31
|
-
def exited?
|
|
32
|
-
def failed?
|
|
33
|
-
def stdin_error?
|
|
28
|
+
def started? = @type == :started
|
|
29
|
+
def stdout? = @type == :stdout
|
|
30
|
+
def stderr? = @type == :stderr
|
|
31
|
+
def exited? = @type == :exited
|
|
32
|
+
def failed? = @type == :failed
|
|
33
|
+
def stdin_error? = @type == :stdin_error
|
|
34
34
|
|
|
35
35
|
def inspect
|
|
36
|
-
"#<Microsandbox::ExecEvent type=#{@type}#{
|
|
37
|
-
"#{
|
|
36
|
+
"#<Microsandbox::ExecEvent type=#{@type}#{" pid=#{@pid}" if @pid}" \
|
|
37
|
+
"#{" code=#{@code}" if @code}#{" data=#{@data.bytesize}B" if @data}>"
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# The terminal status of a streamed execution
|
|
41
|
+
# The terminal status of a streamed execution ({ExecHandle#wait}) or of a
|
|
42
|
+
# sandbox process ({Sandbox#wait}, {Sandbox#stop_and_wait}).
|
|
42
43
|
class ExitStatus
|
|
43
|
-
# @return [Integer]
|
|
44
|
+
# @return [Integer, nil] the exit code, or nil when the process was
|
|
45
|
+
# terminated by a signal and so carries no code (e.g. a SIGKILL'd sandbox
|
|
46
|
+
# from {Sandbox#kill} then {Sandbox#wait}) — check {#success?} in that case.
|
|
44
47
|
attr_reader :exit_code
|
|
45
48
|
|
|
46
49
|
def initialize(data)
|
data/lib/microsandbox/fs.rb
CHANGED
|
@@ -30,9 +30,9 @@ module Microsandbox
|
|
|
30
30
|
@modified_ms && Time.at(@modified_ms / 1000.0)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def file?
|
|
34
|
-
def directory?
|
|
35
|
-
def symlink?
|
|
33
|
+
def file? = @type == :file
|
|
34
|
+
def directory? = @type == :directory
|
|
35
|
+
def symlink? = @type == :symlink
|
|
36
36
|
|
|
37
37
|
def inspect
|
|
38
38
|
"#<Microsandbox::FsEntry path=#{@path.inspect} type=#{@type} size=#{@size}>"
|
|
@@ -57,10 +57,10 @@ module Microsandbox
|
|
|
57
57
|
@created_ms = data["created_ms"]
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def readonly?
|
|
61
|
-
def file?
|
|
62
|
-
def directory?
|
|
63
|
-
def symlink?
|
|
60
|
+
def readonly? = @readonly
|
|
61
|
+
def file? = @type == :file
|
|
62
|
+
def directory? = @type == :directory
|
|
63
|
+
def symlink? = @type == :symlink
|
|
64
64
|
|
|
65
65
|
# @return [Time, nil] last-modified time, if known
|
|
66
66
|
def modified
|
data/lib/microsandbox/image.rb
CHANGED
|
@@ -56,7 +56,7 @@ module Microsandbox
|
|
|
56
56
|
# The result of {Image.prune}.
|
|
57
57
|
class ImagePruneReport
|
|
58
58
|
attr_reader :image_refs_removed, :manifests_removed, :layers_removed,
|
|
59
|
-
|
|
59
|
+
:fsmeta_removed, :vmdk_removed, :bytes_reclaimed
|
|
60
60
|
|
|
61
61
|
def initialize(data)
|
|
62
62
|
@image_refs_removed = data["image_refs_removed"]
|
data/lib/microsandbox/network.rb
CHANGED
|
@@ -16,23 +16,23 @@ module Microsandbox
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
18
|
# Match any destination.
|
|
19
|
-
def any = {
|
|
19
|
+
def any = {"destination_kind" => "any"}
|
|
20
20
|
|
|
21
21
|
# A single IP address (stored as a /32 or /128).
|
|
22
|
-
def ip(value) = {
|
|
22
|
+
def ip(value) = {"destination_kind" => "ip", "destination" => value.to_s}
|
|
23
23
|
|
|
24
24
|
# An IP network in CIDR notation (e.g. "10.0.0.0/8").
|
|
25
|
-
def cidr(value) = {
|
|
25
|
+
def cidr(value) = {"destination_kind" => "cidr", "destination" => value.to_s}
|
|
26
26
|
|
|
27
27
|
# An exact domain name (matched against the resolved-hostname cache / SNI).
|
|
28
|
-
def domain(value) = {
|
|
28
|
+
def domain(value) = {"destination_kind" => "domain", "destination" => value.to_s}
|
|
29
29
|
|
|
30
30
|
# A domain suffix — matches the apex and any subdomain (e.g. ".internal").
|
|
31
|
-
def domain_suffix(value) = {
|
|
31
|
+
def domain_suffix(value) = {"destination_kind" => "domain_suffix", "destination" => value.to_s}
|
|
32
32
|
|
|
33
33
|
# A predefined group: :public, :loopback, :private, :link_local, :metadata,
|
|
34
34
|
# :multicast, or :host.
|
|
35
|
-
def group(value) = {
|
|
35
|
+
def group(value) = {"destination_kind" => "group", "destination" => value.to_s.tr("_", "-")}
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Factory for a single network-policy **rule**. A rule pairs an action
|
|
@@ -64,7 +64,7 @@ module Microsandbox
|
|
|
64
64
|
|
|
65
65
|
# @api private
|
|
66
66
|
def build(action, destination, direction, protocol, protocols, port, ports)
|
|
67
|
-
rule = {
|
|
67
|
+
rule = {"action" => action, "direction" => direction.to_s}
|
|
68
68
|
rule.merge!(normalize_destination(destination))
|
|
69
69
|
protos = (Array(protocols) + Array(protocol)).compact.map(&:to_s)
|
|
70
70
|
rule["protocols"] = protos unless protos.empty?
|
|
@@ -78,7 +78,7 @@ module Microsandbox
|
|
|
78
78
|
case dest
|
|
79
79
|
when nil then {}
|
|
80
80
|
when Hash then dest.each_with_object({}) { |(k, v), a| a[k.to_s] = v }
|
|
81
|
-
when String, Symbol then {
|
|
81
|
+
when String, Symbol then {"destination" => dest.to_s}
|
|
82
82
|
else raise ArgumentError, "invalid rule destination: #{dest.inspect}"
|
|
83
83
|
end
|
|
84
84
|
end
|
|
@@ -149,7 +149,7 @@ module Microsandbox
|
|
|
149
149
|
# @param deny_domain_suffixes [Array<String>] domain suffixes to deny
|
|
150
150
|
# @return [NetworkPolicy]
|
|
151
151
|
def custom(default_egress: :deny, default_ingress: :allow, rules: [],
|
|
152
|
-
|
|
152
|
+
deny_domains: [], deny_domain_suffixes: [])
|
|
153
153
|
h = {}
|
|
154
154
|
h["default_egress"] = action_str(default_egress) unless default_egress.nil?
|
|
155
155
|
h["default_ingress"] = action_str(default_ingress) unless default_ingress.nil?
|
|
@@ -163,12 +163,12 @@ module Microsandbox
|
|
|
163
163
|
def coerce(network)
|
|
164
164
|
case network
|
|
165
165
|
when NetworkPolicy then network.to_h
|
|
166
|
-
when String, Symbol then {
|
|
166
|
+
when String, Symbol then {"preset" => canonical_preset(network)}
|
|
167
167
|
when Hash then from_hash(network)
|
|
168
168
|
else
|
|
169
169
|
raise ArgumentError,
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
"network: expects a preset name, a Microsandbox::NetworkPolicy, or a Hash " \
|
|
171
|
+
"(got #{network.class})"
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
@@ -186,11 +186,11 @@ module Microsandbox
|
|
|
186
186
|
if sym.key?(:preset)
|
|
187
187
|
if sym.key?(:rules) || sym.key?(:default_egress) || sym.key?(:default_ingress)
|
|
188
188
|
raise ArgumentError,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
"network preset: cannot be combined with rules:/default_egress:/" \
|
|
190
|
+
"default_ingress: (the preset already defines its rules and defaults); " \
|
|
191
|
+
"only deny_domains:/deny_domain_suffixes: may be layered on a preset"
|
|
192
192
|
end
|
|
193
|
-
h = {
|
|
193
|
+
h = {"preset" => canonical_preset(sym[:preset])}
|
|
194
194
|
add_deny_lists(h, sym[:deny_domains], sym[:deny_domain_suffixes])
|
|
195
195
|
h
|
|
196
196
|
elsif sym.key?(:rules) || sym.key?(:default_egress) || sym.key?(:default_ingress)
|
|
@@ -213,7 +213,7 @@ module Microsandbox
|
|
|
213
213
|
ds = Array(sym[:deny_domain_suffixes]).map(&:to_s)
|
|
214
214
|
return {} if dd.empty? && ds.empty?
|
|
215
215
|
|
|
216
|
-
h = {
|
|
216
|
+
h = {"default_egress" => "allow", "default_ingress" => "allow"}
|
|
217
217
|
add_deny_lists(h, dd, ds)
|
|
218
218
|
h
|
|
219
219
|
end
|
|
@@ -233,8 +233,8 @@ module Microsandbox
|
|
|
233
233
|
key = name.to_s.downcase
|
|
234
234
|
PRESET_ALIASES[key] ||
|
|
235
235
|
raise(ArgumentError,
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
"unknown network preset #{name.inspect} " \
|
|
237
|
+
"(expected one of public_only/none/allow_all/non_local)")
|
|
238
238
|
end
|
|
239
239
|
|
|
240
240
|
def action_str(action)
|