microsandbox-rb 0.11.0 → 0.13.0
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 +126 -0
- data/Cargo.lock +1006 -557
- data/README.md +48 -10
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/backend.rs +29 -9
- data/ext/microsandbox/src/error.rs +89 -1
- data/ext/microsandbox/src/image.rs +18 -8
- data/ext/microsandbox/src/lib.rs +9 -9
- data/ext/microsandbox/src/sandbox.rs +236 -32
- data/ext/microsandbox/src/snapshot.rs +10 -3
- data/ext/microsandbox/src/volume.rs +14 -3
- data/lib/microsandbox/backend_info.rb +46 -0
- data/lib/microsandbox/errors.rb +16 -0
- data/lib/microsandbox/root_disk.rb +24 -0
- data/lib/microsandbox/sandbox.rb +247 -17
- data/lib/microsandbox/snapshot.rb +12 -8
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +16 -0
- data/lib/microsandbox.rb +16 -3
- data/sig/microsandbox.rbs +48 -5
- metadata +2 -1
|
@@ -18,11 +18,11 @@ use microsandbox::logs::{
|
|
|
18
18
|
LogCursor, LogEntry, LogOptions, LogSource, LogStreamOptions, LogStreamStart,
|
|
19
19
|
};
|
|
20
20
|
use microsandbox::sandbox::{
|
|
21
|
-
AttachOptionsBuilder, DiskImageFormat, EnvVar, FsEntry, FsEntryKind, FsMetadata,
|
|
21
|
+
AttachOptionsBuilder, DiskImageFormat, EnvVar, FlatClone, FsEntry, FsEntryKind, FsMetadata,
|
|
22
22
|
HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle, RlimitResource,
|
|
23
|
-
RootDiskBuilder, SandboxBuilder,
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
RootDiskBuilder, SandboxBuilder, SandboxHandle, SandboxMetrics, SandboxModificationBuilder,
|
|
24
|
+
SandboxModificationPatch, SandboxStatus, SandboxStopResult, SecretBuilder,
|
|
25
|
+
SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
|
|
26
26
|
};
|
|
27
27
|
use microsandbox::LogLevel;
|
|
28
28
|
use microsandbox::MicrosandboxResult;
|
|
@@ -128,13 +128,41 @@ impl Sandbox {
|
|
|
128
128
|
for (k, v) in conv::opt_string_map(opts, "scripts")? {
|
|
129
129
|
b = b.script(k, v);
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// entrypoint/cmd: presence-keyed, NOT non-emptiness-keyed — an
|
|
132
|
+
// explicitly *empty* array is meaningful for both (it clears the
|
|
133
|
+
// image's ENTRYPOINT / CMD, blocking the image-config merge), exactly
|
|
134
|
+
// like the Python binding. Keying on non-emptiness silently resurrects
|
|
135
|
+
// the image ENTRYPOINT and runs the wrong command under
|
|
136
|
+
// `exec_default`/`attach_default`.
|
|
137
|
+
if let Some(entrypoint) = conv::opt::<Vec<String>>(opts, "entrypoint")? {
|
|
133
138
|
b = b.entrypoint(entrypoint);
|
|
134
139
|
}
|
|
140
|
+
if let Some(cmd) = conv::opt::<Vec<String>>(opts, "cmd")? {
|
|
141
|
+
b = b.cmd(cmd);
|
|
142
|
+
}
|
|
135
143
|
for (host, guest) in conv::opt_port_map(opts, "ports")? {
|
|
136
144
|
b = b.port(host, guest);
|
|
137
145
|
}
|
|
146
|
+
// vsock: host Unix sockets exposed on guest-to-host vsock ports
|
|
147
|
+
// (v0.6.9). Each route is normalized by the Ruby layer to a
|
|
148
|
+
// string-keyed Hash {host_socket:, port:, socket_type: "stream"|"dgram"}.
|
|
149
|
+
for route in conv::opt_hash_vec(opts, "vsock")? {
|
|
150
|
+
let Some(host_socket) = conv::opt_string(route, "host_socket")? else {
|
|
151
|
+
return Err(error::base_error("vsock route requires host_socket:"));
|
|
152
|
+
};
|
|
153
|
+
let Some(port) = conv::opt_u32(route, "port")? else {
|
|
154
|
+
return Err(error::base_error("vsock route requires port:"));
|
|
155
|
+
};
|
|
156
|
+
match conv::opt_string(route, "socket_type")?.as_deref() {
|
|
157
|
+
None | Some("stream") => b = b.vsock(host_socket, port),
|
|
158
|
+
Some("dgram") => b = b.vsock_dgram(host_socket, port),
|
|
159
|
+
Some(other) => {
|
|
160
|
+
return Err(error::base_error(format!(
|
|
161
|
+
"unknown vsock socket_type {other:?} (expected stream/dgram)"
|
|
162
|
+
)))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
138
166
|
// volumes: each mount is normalized by the Ruby layer to a string-keyed
|
|
139
167
|
// Hash — guest (req), kind ("bind"/"named"/"tmpfs"/"disk"), source
|
|
140
168
|
// (bind/named/disk), size_mib (tmpfs/disk), format + fstype (disk),
|
|
@@ -413,6 +441,28 @@ impl Sandbox {
|
|
|
413
441
|
n
|
|
414
442
|
});
|
|
415
443
|
}
|
|
444
|
+
// rate_limiter: per-sandbox egress/ingress token-bucket limits
|
|
445
|
+
// (v0.6.9). The Ruby layer normalizes the option to a string-keyed
|
|
446
|
+
// Hash {egress:, ingress:} of {bandwidth:, ops:} buckets
|
|
447
|
+
// {size:, refill_time_ms:, one_time_burst:}; mirrors the Python SDK's
|
|
448
|
+
// `NetworkRateLimiter`. Applied via the network builder, accumulating
|
|
449
|
+
// on top of any configuration above.
|
|
450
|
+
if let Some(spec) = conv::opt::<RHash>(opts, "rate_limiter")?
|
|
451
|
+
.map(parse_rate_limiter)
|
|
452
|
+
.transpose()?
|
|
453
|
+
{
|
|
454
|
+
b = b.network(move |n| {
|
|
455
|
+
n.rate_limiter(move |mut r| {
|
|
456
|
+
if let Some(egress) = spec.egress {
|
|
457
|
+
r = r.egress(move |rl| egress.apply(rl));
|
|
458
|
+
}
|
|
459
|
+
if let Some(ingress) = spec.ingress {
|
|
460
|
+
r = r.ingress(move |rl| ingress.apply(rl));
|
|
461
|
+
}
|
|
462
|
+
r
|
|
463
|
+
})
|
|
464
|
+
});
|
|
465
|
+
}
|
|
416
466
|
// init: hand guest PID 1 to an init system. The Ruby layer normalizes
|
|
417
467
|
// `init:` to a Hash { cmd:, args?:, env?: }. `init_with` with empty
|
|
418
468
|
// args/env builds the same HandoffInit as the plain `init(cmd)`, so route
|
|
@@ -477,29 +527,44 @@ impl Sandbox {
|
|
|
477
527
|
Ok(SbHandle::from_inner(handle))
|
|
478
528
|
}
|
|
479
529
|
|
|
480
|
-
///
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
let arr = ruby().ary_new();
|
|
484
|
-
for h in handles {
|
|
485
|
-
arr.push(SbHandle::from_inner(h))?;
|
|
486
|
-
}
|
|
487
|
-
Ok(arr)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/// Sandboxes filtered by required `key=value` labels (AND-matched), as
|
|
491
|
-
/// controllable handles. `opts` carries a string→string `labels` map.
|
|
492
|
-
fn list_with(opts: RHash) -> Result<RArray, Error> {
|
|
493
|
-
let mut filter = SandboxFilter::new();
|
|
494
|
-
for (k, v) in conv::opt_string_map(opts, "labels")? {
|
|
495
|
-
filter = filter.label(k, v);
|
|
496
|
-
}
|
|
497
|
-
let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
|
|
530
|
+
/// Convert one `SandboxPage` into a `{ "sandboxes" => [SbHandle], "next_cursor" => String|nil }`
|
|
531
|
+
/// Ruby Hash (v0.6.8 paginated listing contract).
|
|
532
|
+
fn page_to_hash(page: microsandbox::sandbox::SandboxPage) -> Result<RHash, Error> {
|
|
498
533
|
let arr = ruby().ary_new();
|
|
499
|
-
for h in
|
|
534
|
+
for h in page.sandboxes {
|
|
500
535
|
arr.push(SbHandle::from_inner(h))?;
|
|
501
536
|
}
|
|
502
|
-
|
|
537
|
+
let hash = ruby().hash_new();
|
|
538
|
+
hash.aset("sandboxes", arr)?;
|
|
539
|
+
hash.aset("next_cursor", page.next_cursor)?;
|
|
540
|
+
Ok(hash)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/// First page of sandboxes (default page size), as a page Hash of
|
|
544
|
+
/// controllable handles.
|
|
545
|
+
fn list() -> Result<RHash, Error> {
|
|
546
|
+
let page = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
|
|
547
|
+
Self::page_to_hash(page)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/// One configured page of sandboxes as a page Hash. `opts` carries an
|
|
551
|
+
/// optional string→string `labels` map (AND-matched), an optional `limit`
|
|
552
|
+
/// (1..=100) and an optional opaque `cursor` from a previous page.
|
|
553
|
+
fn list_with(opts: RHash) -> Result<RHash, Error> {
|
|
554
|
+
let labels = conv::opt_string_map(opts, "labels")?;
|
|
555
|
+
let limit = conv::opt_u32(opts, "limit")?;
|
|
556
|
+
let cursor = conv::opt_string(opts, "cursor")?;
|
|
557
|
+
let page = block_on(microsandbox::Sandbox::list_with(move |mut b| {
|
|
558
|
+
if let Some(limit) = limit {
|
|
559
|
+
b = b.limit(limit);
|
|
560
|
+
}
|
|
561
|
+
if let Some(cursor) = cursor {
|
|
562
|
+
b = b.cursor(cursor);
|
|
563
|
+
}
|
|
564
|
+
b.labels(labels)
|
|
565
|
+
}))
|
|
566
|
+
.map_err(error::to_ruby)?;
|
|
567
|
+
Self::page_to_hash(page)
|
|
503
568
|
}
|
|
504
569
|
|
|
505
570
|
/// Remove a (stopped) sandbox by name.
|
|
@@ -545,6 +610,29 @@ impl Sandbox {
|
|
|
545
610
|
Ok(ExecHandle::from_core(handle))
|
|
546
611
|
}
|
|
547
612
|
|
|
613
|
+
/// Run the image's resolved OCI ENTRYPOINT and CMD (the default workload,
|
|
614
|
+
/// v0.6.9) and wait for completion. `create` is strictly boot-only, so this
|
|
615
|
+
/// is how the image's own command is executed. `opts` matches `exec` minus
|
|
616
|
+
/// the command: cwd, user, env, timeout, tty, stdin, rlimits. Raises
|
|
617
|
+
/// NoDefaultCommandError when the image resolves no executable command.
|
|
618
|
+
fn exec_default(&self, opts: RHash) -> Result<RHash, Error> {
|
|
619
|
+
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
620
|
+
let output = block_on(self.inner.exec_default_with(move |b| parsed.apply(b)))
|
|
621
|
+
.map_err(error::to_ruby)?;
|
|
622
|
+
exec_output_to_hash(output)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/// Streaming default-workload execution. Returns an ExecHandle.
|
|
626
|
+
fn exec_default_stream(&self, opts: RHash) -> Result<ExecHandle, Error> {
|
|
627
|
+
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
628
|
+
let handle = block_on(
|
|
629
|
+
self.inner
|
|
630
|
+
.exec_default_stream_with(move |b| parsed.apply(b)),
|
|
631
|
+
)
|
|
632
|
+
.map_err(error::to_ruby)?;
|
|
633
|
+
Ok(ExecHandle::from_core(handle))
|
|
634
|
+
}
|
|
635
|
+
|
|
548
636
|
/// Streaming shell execution.
|
|
549
637
|
fn shell_stream(&self, script: String, opts: RHash) -> Result<ExecHandle, Error> {
|
|
550
638
|
let parsed = ExecOpts::parse(Vec::new(), opts)?;
|
|
@@ -838,6 +926,14 @@ impl Sandbox {
|
|
|
838
926
|
fn attach_shell(&self) -> Result<i32, Error> {
|
|
839
927
|
block_on(self.inner.attach_shell()).map_err(error::to_ruby)
|
|
840
928
|
}
|
|
929
|
+
|
|
930
|
+
/// Attach an interactive terminal to the image's resolved OCI ENTRYPOINT
|
|
931
|
+
/// and CMD (the default workload, v0.6.9); returns its exit code. `opts`:
|
|
932
|
+
/// cwd, user, env, detach_keys, rlimits.
|
|
933
|
+
fn attach_default(&self, opts: RHash) -> Result<i32, Error> {
|
|
934
|
+
let parsed = AttachOpts::parse(Vec::new(), opts)?;
|
|
935
|
+
block_on(self.inner.attach_default_with(move |b| parsed.apply(b))).map_err(error::to_ruby)
|
|
936
|
+
}
|
|
841
937
|
}
|
|
842
938
|
|
|
843
939
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -1253,6 +1349,83 @@ fn parse_dns(d: RHash) -> Result<DnsSpec, Error> {
|
|
|
1253
1349
|
})
|
|
1254
1350
|
}
|
|
1255
1351
|
|
|
1352
|
+
/// One token bucket of the `rate_limiter` create option (v0.6.9):
|
|
1353
|
+
/// `(size, refill_time_ms, one_time_burst)`. Size is bytes for bandwidth
|
|
1354
|
+
/// buckets, frames for ops buckets.
|
|
1355
|
+
type TokenBucketSpec = (u64, u64, u64);
|
|
1356
|
+
|
|
1357
|
+
/// One direction of the `rate_limiter` create option.
|
|
1358
|
+
struct RateLimiterSpec {
|
|
1359
|
+
bandwidth: Option<TokenBucketSpec>,
|
|
1360
|
+
ops: Option<TokenBucketSpec>,
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/// Parsed `rate_limiter` create option (v0.6.9, mirrors the Python SDK's
|
|
1364
|
+
/// `NetworkRateLimiter`): per-direction bandwidth/ops token buckets. Parsed up
|
|
1365
|
+
/// front because the network builder closure cannot return an error.
|
|
1366
|
+
struct NetworkRateLimiterSpec {
|
|
1367
|
+
egress: Option<RateLimiterSpec>,
|
|
1368
|
+
ingress: Option<RateLimiterSpec>,
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
fn parse_token_bucket(b: RHash, dir: &str, dim: &str) -> Result<TokenBucketSpec, Error> {
|
|
1372
|
+
let Some(size) = conv::opt::<u64>(b, "size")? else {
|
|
1373
|
+
return Err(error::base_error(format!(
|
|
1374
|
+
"rate_limiter {dir} {dim} bucket requires size:"
|
|
1375
|
+
)));
|
|
1376
|
+
};
|
|
1377
|
+
let Some(refill) = conv::opt::<u64>(b, "refill_time_ms")? else {
|
|
1378
|
+
return Err(error::base_error(format!(
|
|
1379
|
+
"rate_limiter {dir} {dim} bucket requires refill_time_ms:"
|
|
1380
|
+
)));
|
|
1381
|
+
};
|
|
1382
|
+
let burst = conv::opt::<u64>(b, "one_time_burst")?.unwrap_or(0);
|
|
1383
|
+
Ok((size, refill, burst))
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
fn parse_rate_limiter_direction(h: RHash, dir: &str) -> Result<RateLimiterSpec, Error> {
|
|
1387
|
+
Ok(RateLimiterSpec {
|
|
1388
|
+
bandwidth: conv::opt::<RHash>(h, "bandwidth")?
|
|
1389
|
+
.map(|b| parse_token_bucket(b, dir, "bandwidth"))
|
|
1390
|
+
.transpose()?,
|
|
1391
|
+
ops: conv::opt::<RHash>(h, "ops")?
|
|
1392
|
+
.map(|b| parse_token_bucket(b, dir, "ops"))
|
|
1393
|
+
.transpose()?,
|
|
1394
|
+
})
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
fn parse_rate_limiter(h: RHash) -> Result<NetworkRateLimiterSpec, Error> {
|
|
1398
|
+
Ok(NetworkRateLimiterSpec {
|
|
1399
|
+
egress: conv::opt::<RHash>(h, "egress")?
|
|
1400
|
+
.map(|d| parse_rate_limiter_direction(d, "egress"))
|
|
1401
|
+
.transpose()?,
|
|
1402
|
+
ingress: conv::opt::<RHash>(h, "ingress")?
|
|
1403
|
+
.map(|d| parse_rate_limiter_direction(d, "ingress"))
|
|
1404
|
+
.transpose()?,
|
|
1405
|
+
})
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
impl RateLimiterSpec {
|
|
1409
|
+
fn apply(
|
|
1410
|
+
self,
|
|
1411
|
+
mut rl: microsandbox_network::builder::RateLimiterBuilder,
|
|
1412
|
+
) -> microsandbox_network::builder::RateLimiterBuilder {
|
|
1413
|
+
if let Some((size, refill_ms, burst)) = self.bandwidth {
|
|
1414
|
+
rl = rl.bandwidth(size, Duration::from_millis(refill_ms));
|
|
1415
|
+
if burst > 0 {
|
|
1416
|
+
rl = rl.bandwidth_burst(burst);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if let Some((count, refill_ms, burst)) = self.ops {
|
|
1420
|
+
rl = rl.ops(count, Duration::from_millis(refill_ms));
|
|
1421
|
+
if burst > 0 {
|
|
1422
|
+
rl = rl.ops_burst(burst);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
rl
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1256
1429
|
struct TlsSpec {
|
|
1257
1430
|
bypass: Vec<String>,
|
|
1258
1431
|
verify_upstream: Option<bool>,
|
|
@@ -1394,12 +1567,14 @@ struct RootDiskSpec {
|
|
|
1394
1567
|
size_mib: Option<u32>,
|
|
1395
1568
|
format: Option<DiskImageFormat>,
|
|
1396
1569
|
fstype: Option<String>,
|
|
1570
|
+
clone: Option<FlatClone>,
|
|
1397
1571
|
}
|
|
1398
1572
|
|
|
1399
1573
|
enum RootDiskKindSpec {
|
|
1400
1574
|
Managed,
|
|
1401
1575
|
Tmpfs,
|
|
1402
1576
|
Disk(String),
|
|
1577
|
+
Flat,
|
|
1403
1578
|
}
|
|
1404
1579
|
|
|
1405
1580
|
impl RootDiskSpec {
|
|
@@ -1408,6 +1583,7 @@ impl RootDiskSpec {
|
|
|
1408
1583
|
RootDiskKindSpec::Managed => {}
|
|
1409
1584
|
RootDiskKindSpec::Tmpfs => d = d.tmpfs(),
|
|
1410
1585
|
RootDiskKindSpec::Disk(path) => d = d.disk_image(path),
|
|
1586
|
+
RootDiskKindSpec::Flat => d = d.flat(),
|
|
1411
1587
|
}
|
|
1412
1588
|
if let Some(mib) = self.size_mib {
|
|
1413
1589
|
d = d.size(mib);
|
|
@@ -1418,6 +1594,9 @@ impl RootDiskSpec {
|
|
|
1418
1594
|
if let Some(ft) = self.fstype {
|
|
1419
1595
|
d = d.fstype(ft);
|
|
1420
1596
|
}
|
|
1597
|
+
if let Some(c) = self.clone {
|
|
1598
|
+
d = d.clone_strategy(c);
|
|
1599
|
+
}
|
|
1421
1600
|
d
|
|
1422
1601
|
}
|
|
1423
1602
|
}
|
|
@@ -1429,12 +1608,13 @@ fn parse_root_disk(v: Value) -> Result<RootDiskSpec, Error> {
|
|
|
1429
1608
|
size_mib: Some(mib),
|
|
1430
1609
|
format: None,
|
|
1431
1610
|
fstype: None,
|
|
1611
|
+
clone: None,
|
|
1432
1612
|
});
|
|
1433
1613
|
}
|
|
1434
1614
|
let Ok(h) = RHash::try_convert(v) else {
|
|
1435
1615
|
return Err(error::base_error(
|
|
1436
1616
|
"root_disk: expects an Integer (managed size in MiB) or a Hash \
|
|
1437
|
-
(use Microsandbox::RootDisk.managed/tmpfs/disk)",
|
|
1617
|
+
(use Microsandbox::RootDisk.managed/tmpfs/disk/flat)",
|
|
1438
1618
|
));
|
|
1439
1619
|
};
|
|
1440
1620
|
let kind = match conv::opt_string(h, "kind")?.as_deref() {
|
|
@@ -1446,20 +1626,35 @@ fn parse_root_disk(v: Value) -> Result<RootDiskSpec, Error> {
|
|
|
1446
1626
|
};
|
|
1447
1627
|
RootDiskKindSpec::Disk(path)
|
|
1448
1628
|
}
|
|
1629
|
+
Some("flat") => RootDiskKindSpec::Flat,
|
|
1449
1630
|
Some(other) => {
|
|
1450
1631
|
return Err(error::base_error(format!(
|
|
1451
|
-
"unknown root_disk kind {other:?} (expected managed/tmpfs/disk)"
|
|
1632
|
+
"unknown root_disk kind {other:?} (expected managed/tmpfs/disk/flat)"
|
|
1452
1633
|
)))
|
|
1453
1634
|
}
|
|
1454
1635
|
};
|
|
1455
1636
|
let format = conv::opt_string(h, "format")?
|
|
1456
1637
|
.map(|f| disk_format_from_str(&f))
|
|
1457
1638
|
.transpose()?;
|
|
1639
|
+
// clone: flat-only private-disk clone strategy (v0.6.9). Validated here
|
|
1640
|
+
// (unknown values error) but kind cross-validation stays in the core
|
|
1641
|
+
// builder, matching the other fields.
|
|
1642
|
+
let clone = conv::opt_string(h, "clone")?
|
|
1643
|
+
.map(|c| match c.as_str() {
|
|
1644
|
+
"auto" => Ok(FlatClone::Auto),
|
|
1645
|
+
"copy" => Ok(FlatClone::Copy),
|
|
1646
|
+
"reflink" => Ok(FlatClone::Reflink),
|
|
1647
|
+
other => Err(error::base_error(format!(
|
|
1648
|
+
"unknown root_disk clone strategy {other:?} (expected auto/copy/reflink)"
|
|
1649
|
+
))),
|
|
1650
|
+
})
|
|
1651
|
+
.transpose()?;
|
|
1458
1652
|
Ok(RootDiskSpec {
|
|
1459
1653
|
kind,
|
|
1460
1654
|
size_mib: conv::opt_u32(h, "size_mib")?,
|
|
1461
1655
|
format,
|
|
1462
1656
|
fstype: conv::opt_string(h, "fstype")?,
|
|
1657
|
+
clone,
|
|
1463
1658
|
})
|
|
1464
1659
|
}
|
|
1465
1660
|
|
|
@@ -1954,9 +2149,9 @@ fn run_modify(builder: SandboxModificationBuilder, opts: RHash) -> Result<String
|
|
|
1954
2149
|
|
|
1955
2150
|
/// Build the canonical `SandboxModificationPatch` from the modify Hash. Env and
|
|
1956
2151
|
/// label pairs are sorted so repeated calls with the same arguments produce the
|
|
1957
|
-
/// same patch (and plan) ordering, mirroring the Python binding.
|
|
1958
|
-
///
|
|
1959
|
-
///
|
|
2152
|
+
/// same patch (and plan) ordering, mirroring the Python binding. As of v0.6.9
|
|
2153
|
+
/// every patch field is surfaced (`root_disk_size` covers what the deprecated
|
|
2154
|
+
/// CLI-only `oci_upper_size` used to mean).
|
|
1960
2155
|
fn build_modify_patch(opts: RHash) -> Result<SandboxModificationPatch, Error> {
|
|
1961
2156
|
let mut env_pairs = conv::opt_string_map(opts, "env")?;
|
|
1962
2157
|
env_pairs.sort();
|
|
@@ -1968,6 +2163,10 @@ fn build_modify_patch(opts: RHash) -> Result<SandboxModificationPatch, Error> {
|
|
|
1968
2163
|
max_cpus: conv::opt_u8(opts, "max_cpus")?,
|
|
1969
2164
|
memory_mib: conv::opt_u32(opts, "memory")?,
|
|
1970
2165
|
max_memory_mib: conv::opt_u32(opts, "max_memory")?,
|
|
2166
|
+
// v0.6.9 ("root disk resizing in every SDK"): grow the sandbox-owned
|
|
2167
|
+
// layered upper or flat root disk. Restart/next-start semantics and
|
|
2168
|
+
// backing-specific limits are enforced by the core.
|
|
2169
|
+
root_disk_size_mib: conv::opt_u32(opts, "root_disk_size")?,
|
|
1971
2170
|
env: env_pairs
|
|
1972
2171
|
.into_iter()
|
|
1973
2172
|
.map(|(k, v)| EnvVar::new(k, v))
|
|
@@ -1978,7 +2177,6 @@ fn build_modify_patch(opts: RHash) -> Result<SandboxModificationPatch, Error> {
|
|
|
1978
2177
|
workdir: conv::opt_string(opts, "workdir")?,
|
|
1979
2178
|
secrets: parse_modify_secrets(opts)?,
|
|
1980
2179
|
secrets_remove: conv::opt_string_vec(opts, "remove_secrets")?,
|
|
1981
|
-
..Default::default()
|
|
1982
2180
|
})
|
|
1983
2181
|
}
|
|
1984
2182
|
|
|
@@ -2407,6 +2605,11 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2407
2605
|
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
2408
2606
|
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
2409
2607
|
class.define_method("shell_stream", method!(Sandbox::shell_stream, 2))?;
|
|
2608
|
+
class.define_method("exec_default", method!(Sandbox::exec_default, 1))?;
|
|
2609
|
+
class.define_method(
|
|
2610
|
+
"exec_default_stream",
|
|
2611
|
+
method!(Sandbox::exec_default_stream, 1),
|
|
2612
|
+
)?;
|
|
2410
2613
|
class.define_method("stop", method!(Sandbox::stop, 0))?;
|
|
2411
2614
|
class.define_method("stop_and_wait", method!(Sandbox::stop_and_wait, 0))?;
|
|
2412
2615
|
class.define_method("kill", method!(Sandbox::kill, 0))?;
|
|
@@ -2446,6 +2649,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2446
2649
|
)?;
|
|
2447
2650
|
|
|
2448
2651
|
class.define_method("attach", method!(Sandbox::attach, 3))?;
|
|
2652
|
+
class.define_method("attach_default", method!(Sandbox::attach_default, 1))?;
|
|
2449
2653
|
class.define_method("attach_shell", method!(Sandbox::attach_shell, 0))?;
|
|
2450
2654
|
|
|
2451
2655
|
let handle = native.define_class("SandboxHandle", ruby.class_object())?;
|
|
@@ -171,9 +171,10 @@ fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/// Verify a snapshot's recorded upper-layer integrity. Returns
|
|
174
|
-
/// {digest, path, upper_status, upper_algorithm, upper_digest}.
|
|
175
|
-
///
|
|
176
|
-
///
|
|
174
|
+
/// {digest, path, upper_status, upper_algorithm, upper_digest}. As of v0.6.9
|
|
175
|
+
/// payload integrity is opt-in at create time: opted-in snapshots verify to
|
|
176
|
+
/// "verified" (mismatches raise SnapshotIntegrityError), snapshots without
|
|
177
|
+
/// recorded integrity report "not_recorded" with no algorithm/digest keys.
|
|
177
178
|
fn verify(name_or_path: String) -> Result<RHash, Error> {
|
|
178
179
|
let snap = block_on(Snapshot::open(&name_or_path)).map_err(error::to_ruby)?;
|
|
179
180
|
let report = block_on(snap.verify()).map_err(error::to_ruby)?;
|
|
@@ -248,6 +249,12 @@ fn verify_report_to_hash(report: &SnapshotVerifyReport) -> RHash {
|
|
|
248
249
|
let _ = hash.aset("upper_algorithm", algorithm.clone());
|
|
249
250
|
let _ = hash.aset("upper_digest", digest.clone());
|
|
250
251
|
}
|
|
252
|
+
// v0.6.9 (#1346): payload integrity is opt-in at create time
|
|
253
|
+
// (`record_integrity`); a snapshot without it verifies structurally
|
|
254
|
+
// but has no content digest to check.
|
|
255
|
+
UpperVerifyStatus::NotRecorded => {
|
|
256
|
+
let _ = hash.aset("upper_status", "not_recorded");
|
|
257
|
+
}
|
|
251
258
|
}
|
|
252
259
|
hash
|
|
253
260
|
}
|
|
@@ -9,7 +9,6 @@ use magnus::{function, method, prelude::*, Error, RArray, RHash, RModule, RStrin
|
|
|
9
9
|
use microsandbox::volume::VolumeHandle;
|
|
10
10
|
use microsandbox::Backend;
|
|
11
11
|
|
|
12
|
-
use crate::backend::local_backend;
|
|
13
12
|
use crate::conv;
|
|
14
13
|
use crate::error;
|
|
15
14
|
use crate::runtime::{block_on, ruby};
|
|
@@ -19,6 +18,7 @@ fn handle_to_hash(h: &VolumeHandle) -> RHash {
|
|
|
19
18
|
let hash = ruby().hash_new();
|
|
20
19
|
let _ = hash.aset("name", h.name().to_string());
|
|
21
20
|
let _ = hash.aset("kind", h.kind().as_str().to_string());
|
|
21
|
+
let _ = hash.aset("default", h.is_default());
|
|
22
22
|
let _ = hash.aset("quota_mib", h.quota_mib());
|
|
23
23
|
let _ = hash.aset("used_bytes", h.used_bytes());
|
|
24
24
|
let _ = hash.aset("capacity_bytes", h.capacity_bytes());
|
|
@@ -84,6 +84,13 @@ fn get(name: String) -> Result<RHash, Error> {
|
|
|
84
84
|
Ok(handle_to_hash(&handle))
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/// The backend's default volume (v0.6.9). Cloud-backend only — the local
|
|
88
|
+
/// backend rejects it (Unsupported) to avoid accidental host access.
|
|
89
|
+
fn get_default() -> Result<RHash, Error> {
|
|
90
|
+
let handle = block_on(microsandbox::Volume::get_default()).map_err(error::to_ruby)?;
|
|
91
|
+
Ok(handle_to_hash(&handle))
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
fn list() -> Result<RArray, Error> {
|
|
88
95
|
let handles = block_on(microsandbox::Volume::list()).map_err(error::to_ruby)?;
|
|
89
96
|
let arr = ruby().ary_new();
|
|
@@ -108,10 +115,13 @@ pub struct VolumeFs {
|
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
impl VolumeFs {
|
|
111
|
-
/// Resolve the
|
|
118
|
+
/// Resolve the ambient backend once and bind it to `name`. As of v0.6.8
|
|
119
|
+
/// every `VolumeFs` op dispatches through the backend's `VolumeBackend`
|
|
120
|
+
/// trait, which yields precise per-operation `Unsupported` errors on
|
|
121
|
+
/// backends that can't serve it — no local-only downcast needed here.
|
|
112
122
|
fn for_volume(name: String) -> Result<VolumeFs, Error> {
|
|
113
123
|
Ok(VolumeFs {
|
|
114
|
-
backend:
|
|
124
|
+
backend: microsandbox::default_backend(),
|
|
115
125
|
name,
|
|
116
126
|
})
|
|
117
127
|
}
|
|
@@ -194,6 +204,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
194
204
|
let class = native.define_class("Volume", ruby.class_object())?;
|
|
195
205
|
class.define_singleton_method("create", function!(create, 2))?;
|
|
196
206
|
class.define_singleton_method("get", function!(get, 1))?;
|
|
207
|
+
class.define_singleton_method("get_default", function!(get_default, 0))?;
|
|
197
208
|
class.define_singleton_method("list", function!(list, 0))?;
|
|
198
209
|
class.define_singleton_method("remove", function!(remove, 1))?;
|
|
199
210
|
class.define_singleton_method("fs", function!(fs, 1))?;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Microsandbox
|
|
4
|
+
# Secret-safe description of the active default backend, from
|
|
5
|
+
# {Microsandbox.default_backend_info} (runtime v0.6.9). Describes what
|
|
6
|
+
# selected the backend and where it points — never its credential: the API
|
|
7
|
+
# key is deliberately absent.
|
|
8
|
+
class BackendInfo
|
|
9
|
+
# @return [String, nil] effective cloud API endpoint (nil for local)
|
|
10
|
+
attr_reader :api_url
|
|
11
|
+
# @return [String, nil] the selected profile name, when a profile chose
|
|
12
|
+
# the backend
|
|
13
|
+
attr_reader :profile
|
|
14
|
+
|
|
15
|
+
def initialize(data)
|
|
16
|
+
@kind = data["kind"]
|
|
17
|
+
@api_url = data["api_url"]
|
|
18
|
+
@source = data["source"]
|
|
19
|
+
@profile = data["profile"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Symbol] :local or :cloud
|
|
23
|
+
def kind
|
|
24
|
+
@kind.to_sym
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# What selected this backend: `:programmatic` (an SDK setter),
|
|
28
|
+
# `:MSB_BACKEND` / `:MSB_API_KEY` / `:MSB_PROFILE` (environment),
|
|
29
|
+
# `:profile` (an explicit SDK profile constructor), `:active_profile`
|
|
30
|
+
# (the SDK config file), or `:default` (the final local fallback).
|
|
31
|
+
# @return [Symbol]
|
|
32
|
+
def source
|
|
33
|
+
@source.to_sym
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def local? = kind == :local
|
|
37
|
+
|
|
38
|
+
def cloud? = kind == :cloud
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<Microsandbox::BackendInfo kind=#{@kind} source=#{@source.inspect}" \
|
|
42
|
+
"#{" api_url=#{@api_url.inspect}" if @api_url}" \
|
|
43
|
+
"#{" profile=#{@profile.inspect}" if @profile}>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/microsandbox/errors.rb
CHANGED
|
@@ -39,6 +39,10 @@ module Microsandbox
|
|
|
39
39
|
# Execution errors --------------------------------------------------------
|
|
40
40
|
define_error(:ExecTimeoutError, "exec-timeout")
|
|
41
41
|
define_error(:ExecFailedError, "exec-failed")
|
|
42
|
+
# v0.6.9: `exec_default`/`attach_default` on an image whose resolved
|
|
43
|
+
# ENTRYPOINT+CMD provide no executable command. Mirrors the Python SDK's
|
|
44
|
+
# NoDefaultCommandError.
|
|
45
|
+
define_error(:NoDefaultCommandError, "no-default-command")
|
|
42
46
|
|
|
43
47
|
# Filesystem errors -------------------------------------------------------
|
|
44
48
|
define_error(:FilesystemError, "filesystem-error")
|
|
@@ -90,4 +94,16 @@ module Microsandbox
|
|
|
90
94
|
# cloud backend) is distinct from `UnsupportedOperationError` above.
|
|
91
95
|
define_error(:CloudHttpError, "cloud-http")
|
|
92
96
|
define_error(:UnsupportedError, "unsupported")
|
|
97
|
+
|
|
98
|
+
# As of runtime v0.6.8 the core keys Unsupported errors by a structured
|
|
99
|
+
# (operation, reason) pair. The native layer renders both into the message
|
|
100
|
+
# ("sandbox.kill is not supported by this backend: ...") and also attaches
|
|
101
|
+
# them here as structured attributes, mirroring the Python SDK's
|
|
102
|
+
# `UnsupportedError.operation` / `.hint`.
|
|
103
|
+
class UnsupportedError
|
|
104
|
+
# @return [String, nil] the rejected API in Ruby rendering, e.g. "sandbox.kill"
|
|
105
|
+
attr_reader :operation
|
|
106
|
+
# @return [String, nil] why it was rejected / what to use instead
|
|
107
|
+
attr_reader :hint
|
|
108
|
+
end
|
|
93
109
|
end
|
|
@@ -13,12 +13,18 @@ module Microsandbox
|
|
|
13
13
|
# - {disk} — a user-supplied disk image attached writable as the upper. The
|
|
14
14
|
# file determines its own size; the runtime never creates, resizes, or
|
|
15
15
|
# deletes it. Cannot be snapshotted or patched.
|
|
16
|
+
# - {flat} — a single complete ext4 root disk materialized directly from the
|
|
17
|
+
# OCI image (runtime v0.6.9), skipping the layered EROFS+OverlayFS stack at
|
|
18
|
+
# runtime. Content-addressed and cached across sandboxes; resizable.
|
|
19
|
+
# Pre-materialize with `msb pull IMAGE --materialize flat`.
|
|
16
20
|
#
|
|
17
21
|
# @example
|
|
18
22
|
# Sandbox.create("worker", image: "python", root_disk: 8192)
|
|
19
23
|
# Sandbox.create("ci", image: "python", root_disk: Microsandbox::RootDisk.tmpfs(2048))
|
|
20
24
|
# Sandbox.create("warm", image: "python",
|
|
21
25
|
# root_disk: Microsandbox::RootDisk.disk("./scratch.img", fstype: "ext4"))
|
|
26
|
+
# Sandbox.create("fast", image: "python",
|
|
27
|
+
# root_disk: Microsandbox::RootDisk.flat(8192, clone: :reflink))
|
|
22
28
|
#
|
|
23
29
|
# Mirrors the `RootDisk` factory in the official Python/Node/Go SDKs.
|
|
24
30
|
module RootDisk
|
|
@@ -54,5 +60,23 @@ module Microsandbox
|
|
|
54
60
|
h["fstype"] = fstype.to_s if fstype
|
|
55
61
|
h
|
|
56
62
|
end
|
|
63
|
+
|
|
64
|
+
# A complete, microsandbox-owned root disk materialized from the OCI image
|
|
65
|
+
# (runtime v0.6.9).
|
|
66
|
+
# @param size_mib [Integer, nil] resizable ext4 size in MiB
|
|
67
|
+
# @param fstype [String, nil] generated filesystem type (default "ext4")
|
|
68
|
+
# @param clone [Symbol, String, nil] how each sandbox's private disk is
|
|
69
|
+
# created from the cached artifact: `:auto` (CoW clone when the host
|
|
70
|
+
# filesystem supports it, else a sparse copy — the default), `:copy`
|
|
71
|
+
# (always an independent sparse copy), or `:reflink` (require a CoW
|
|
72
|
+
# clone; fail where unsupported)
|
|
73
|
+
# @return [Hash]
|
|
74
|
+
def flat(size_mib = nil, fstype: nil, clone: nil)
|
|
75
|
+
h = {"kind" => "flat"}
|
|
76
|
+
h["size_mib"] = Integer(size_mib) if size_mib
|
|
77
|
+
h["fstype"] = fstype.to_s if fstype
|
|
78
|
+
h["clone"] = clone.to_s if clone
|
|
79
|
+
h
|
|
80
|
+
end
|
|
57
81
|
end
|
|
58
82
|
end
|