microsandbox-rb 0.9.2 → 0.11.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 +176 -0
- data/Cargo.lock +126 -141
- data/README.md +63 -9
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/error.rs +10 -0
- data/ext/microsandbox/src/image.rs +38 -0
- data/ext/microsandbox/src/sandbox.rs +357 -36
- data/ext/microsandbox/src/snapshot.rs +79 -35
- data/lib/microsandbox/errors.rb +4 -0
- data/lib/microsandbox/image.rb +38 -0
- data/lib/microsandbox/modification.rb +108 -0
- data/lib/microsandbox/network.rb +139 -31
- data/lib/microsandbox/root_disk.rb +58 -0
- data/lib/microsandbox/sandbox.rb +285 -23
- data/lib/microsandbox/snapshot.rb +87 -37
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox.rb +2 -0
- data/sig/microsandbox.rbs +75 -12
- metadata +3 -1
data/README.md
CHANGED
|
@@ -39,7 +39,7 @@ them. Our deepest thanks to the maintainers and community. 🙏
|
|
|
39
39
|
- **Guest filesystem access** — read, write, list, copy, stat files inside a running sandbox
|
|
40
40
|
- **Metrics & logs** — CPU, memory, disk and network I/O; captured stdout/stderr/system logs
|
|
41
41
|
- **Rootfs patches** — inject files, dirs, and symlinks into the image before boot (`Microsandbox::Patch`)
|
|
42
|
-
- **Fine-grained networking** —
|
|
42
|
+
- **Fine-grained networking** — composable profiles (`:public`/`:private`/`:host`) *and* custom CIDR/domain/group allow-deny rules (`Microsandbox::NetworkPolicy`)
|
|
43
43
|
- **SSH & SFTP** — native in-process SSH client/server and file transfer (`Sandbox#ssh`)
|
|
44
44
|
- **Raw agent client** — byte-level access to the guest `agentd` protocol (`Microsandbox::AgentClient`)
|
|
45
45
|
- **Idiomatic Ruby** — keyword arguments, block-scoped lifecycle, a typed error hierarchy
|
|
@@ -160,7 +160,7 @@ Microsandbox::Sandbox.create(
|
|
|
160
160
|
workdir: "/app",
|
|
161
161
|
labels: { "team" => "research" },
|
|
162
162
|
ports: { 8080 => 80 }, # host => guest (TCP)
|
|
163
|
-
network:
|
|
163
|
+
network: [:public], # composable profiles; :none for airgapped
|
|
164
164
|
replace: true # replace an existing sandbox of the same name
|
|
165
165
|
) do |sb|
|
|
166
166
|
# ...
|
|
@@ -229,6 +229,55 @@ Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine
|
|
|
229
229
|
end
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
+
### Live modification & health
|
|
233
|
+
|
|
234
|
+
Resize, reconfigure, or rotate secrets on a sandbox **without recreating it**,
|
|
235
|
+
and probe the guest agent's liveness. To leave headroom for a live CPU/memory
|
|
236
|
+
resize, reserve a ceiling at create time with `max_cpus:`/`max_memory:`:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpine:latest",
|
|
240
|
+
cpus: 1, max_cpus: 4, memory: 512, max_memory: 2048) do |sb|
|
|
241
|
+
# Health check — does NOT refresh the idle timer:
|
|
242
|
+
ping = sb.ping # => Microsandbox::PingResult
|
|
243
|
+
ping.latency_ms # round-trip latency
|
|
244
|
+
|
|
245
|
+
# Explicitly refresh the idle-activity timer (resets any idle_timeout:):
|
|
246
|
+
sb.touch.activity_seq # => Microsandbox::TouchResult
|
|
247
|
+
|
|
248
|
+
# Preview a change without applying it:
|
|
249
|
+
plan = sb.modify(cpus: 2, memory: 1024, dry_run: true)
|
|
250
|
+
plan.applied? # => false
|
|
251
|
+
plan.changes # => [{ kind: "config", field: "cpus", ... }, ...]
|
|
252
|
+
|
|
253
|
+
# Live resize — applies to the running VM under the default :no_restart policy:
|
|
254
|
+
sb.modify(cpus: 2, memory: 1024)
|
|
255
|
+
|
|
256
|
+
# env/labels/workdir changes on a *running* sandbox require a restart, so the
|
|
257
|
+
# default :no_restart policy rejects the whole apply (it raises rather than
|
|
258
|
+
# partially applying). Persist them for the next start — or restart now —
|
|
259
|
+
# by saying so explicitly:
|
|
260
|
+
sb.modify(env: { "TIER" => "prod" }, remove_env: ["DEBUG"],
|
|
261
|
+
labels: { "role" => "worker" }, policy: :next_start) # or policy: :restart
|
|
262
|
+
|
|
263
|
+
# Rotating/removing an *existing* secret (or updating its allowed hosts) is
|
|
264
|
+
# live; *adding* a new secret is restart-required, like env. Specs are keyed
|
|
265
|
+
# by name; env:/store:/value: are mutually exclusive:
|
|
266
|
+
sb.modify(
|
|
267
|
+
secrets: { "API_KEY" => { env: "HOST_API_KEY", allowed_hosts: ["api.example.com"] } },
|
|
268
|
+
remove_secrets: ["OLD_TOKEN"],
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The apply is **all-or-nothing**: under a given `policy:` every planned change
|
|
274
|
+
must be applicable, or the whole `modify` raises — nothing is partially
|
|
275
|
+
applied. Use `dry_run: true` to inspect each change's `disposition` (`"live"`,
|
|
276
|
+
`"next start"`, `"requires restart"`) before committing.
|
|
277
|
+
|
|
278
|
+
`SandboxHandle` (from `Sandbox.get`/`list`) carries the same `#ping`/`#touch`/
|
|
279
|
+
`#modify`; on a stopped sandbox `#ping`/`#touch` raise `SandboxNotRunningError`.
|
|
280
|
+
|
|
232
281
|
### Streaming output
|
|
233
282
|
|
|
234
283
|
For long-running commands, stream events as they arrive instead of waiting:
|
|
@@ -393,8 +442,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
|
|
|
393
442
|
of the embedded runtime version. To learn which runtime a build wraps, ask it:
|
|
394
443
|
|
|
395
444
|
```ruby
|
|
396
|
-
Microsandbox::VERSION # => "0.
|
|
397
|
-
Microsandbox.runtime_version # => "v0.6.
|
|
445
|
+
Microsandbox::VERSION # => "0.11.0" (the gem's own version)
|
|
446
|
+
Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag)
|
|
398
447
|
```
|
|
399
448
|
|
|
400
449
|
| Gem version | Upstream runtime | Notes |
|
|
@@ -413,6 +462,9 @@ Microsandbox.runtime_version # => "v0.6.3" (the embedded upstream runtime tag
|
|
|
413
462
|
| `0.9.0` | `v0.6.1` | adopts upstream `v0.6.0`+`v0.6.1` (zombie-runtime wait fix, secret substitution through CONNECT proxies, stale-sandbox cleanup); upstream public API is additive — no Ruby surface change |
|
|
414
463
|
| `0.9.1` | `v0.6.2` | adopts upstream `v0.6.2` (faster image loads/pulls via early cache gate + zlib-rs); upstream API unchanged — no Ruby surface change |
|
|
415
464
|
| `0.9.2` | `v0.6.3` | adopts upstream `v0.6.3` (v4-only sandboxes stop advertising AAAA DNS answers — fixes guest gRPC/c-ares preferring unreachable IPv6; scoped upstream TLS verification); glue moves to `*_local` SDK variants — no Ruby surface change |
|
|
465
|
+
| `0.9.3` | `v0.6.6` | adopts upstream `v0.6.4`+`v0.6.6` (`v0.6.5` was yanked upstream): snapshot restore by pinned digest — fixes fatal restore-after-tag-republish bug, fragmented-UDP/PMTU relay fixes, exec kills the whole process group, ephemeral stop-wait tolerance, readdir RSS-leak fix; upstream API growth is additive-only — no Ruby surface change |
|
|
466
|
+
| `0.10.0` | `v0.6.6` | `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
|
|
467
|
+
| `0.11.0` | `v0.6.7` | adopts upstream `v0.6.7` (**breaking**): network profiles replace `public_only`/`non_local`, structured `root_disk:` replaces `oci_upper_size:` (deprecated alias kept), snapshot descriptor contract (`create` re-keyed by name, `save`/`load` rename, `snapshot_to` removed, on-disk auto-migration), `Image.load`/`Image.save`, `follow_root_symlinks:`; runtime carries the GHSA-4vq3-cjpp-v7fg `msb copy` fix |
|
|
416
468
|
|
|
417
469
|
**Going forward** — the gem version moves on its own semver track and no longer
|
|
418
470
|
mirrors the upstream tag:
|
|
@@ -491,7 +543,7 @@ section. The binding covers the official-SDK surface: sandbox
|
|
|
491
543
|
lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
|
|
492
544
|
`status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
|
|
493
545
|
`stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
|
|
494
|
-
`wait_until_stopped`/`config`/`config_json`/`snapshot
|
|
546
|
+
`wait_until_stopped`/`config`/`config_json`/`snapshot` from
|
|
495
547
|
`Sandbox.get`, and label-filtered `list_with`),
|
|
496
548
|
backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
|
|
497
549
|
`exec`/`shell` (collected and streaming), interactive `attach`/
|
|
@@ -499,10 +551,12 @@ backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
|
|
|
499
551
|
`write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
|
|
500
552
|
streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
|
|
501
553
|
named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
|
|
502
|
-
snapshots (create/open/list/list_dir/reindex/verify/
|
|
503
|
-
boot-from-snapshot),
|
|
504
|
-
(`Sandbox.create_with_progress` → `PullSession`),
|
|
505
|
-
(`Microsandbox::Patch`), **
|
|
554
|
+
snapshots (create/open/list/list_dir/reindex/verify/save/load +
|
|
555
|
+
boot-from-snapshot), image archives (`Image.load`/`Image.save`), streaming
|
|
556
|
+
image-pull progress (`Sandbox.create_with_progress` → `PullSession`),
|
|
557
|
+
**rootfs patches** (`Microsandbox::Patch`), the structured **root disk**
|
|
558
|
+
(`root_disk:` managed/tmpfs/disk via `Microsandbox::RootDisk`), **network
|
|
559
|
+
configuration** (composable profiles, custom per-rule
|
|
506
560
|
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
|
|
507
561
|
IPv4/IPv6 pools, `max_connections`, `trust_host_cas`), **secrets** (multi-host /
|
|
508
562
|
wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -6,8 +6,8 @@ name = "microsandbox_rb"
|
|
|
6
6
|
description = "Ruby SDK native extension for microsandbox — secure, fast microVM-based sandboxing."
|
|
7
7
|
# Must equal Microsandbox::VERSION (lib/microsandbox/version.rb) — Native.version
|
|
8
8
|
# returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
|
|
9
|
-
# The core-crate dependency below stays pinned at its own tag (v0.6.
|
|
10
|
-
version = "0.
|
|
9
|
+
# The core-crate dependency below stays pinned at its own tag (v0.6.7).
|
|
10
|
+
version = "0.11.0"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -35,8 +35,8 @@ rb-sys = "0.9"
|
|
|
35
35
|
# `.cargo/config.toml.example`). "ssh" matches the feature set the Python/Node
|
|
36
36
|
# SDKs ship with; default features add "prebuilt" (provisions msb + libkrunfw at
|
|
37
37
|
# build time), "net", and "keyring".
|
|
38
|
-
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
39
|
-
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
38
|
+
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.7", default-features = true, features = ["ssh"] }
|
|
39
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.7" }
|
|
40
40
|
|
|
41
41
|
# Async core bridged to Ruby's synchronous API via a blocking tokio runtime.
|
|
42
42
|
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
|
@@ -17,6 +17,12 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
17
17
|
SandboxNotFound(_) => "SandboxNotFoundError",
|
|
18
18
|
SandboxAlreadyExists(_) => "SandboxAlreadyExistsError",
|
|
19
19
|
SandboxStillRunning(_) => "SandboxStillRunningError",
|
|
20
|
+
// v0.6.6 (#1099): the sandbox exists but isn't running. Raised by the
|
|
21
|
+
// handle's exec/attach/ping/touch not-running guards and the fs
|
|
22
|
+
// agent-endpoint lookup. The `SandboxNotRunningError` class already
|
|
23
|
+
// existed (mirroring the Python SDK); this wires the new core variant to
|
|
24
|
+
// it instead of letting it collapse to the base `Error`.
|
|
25
|
+
SandboxNotRunning(_) => "SandboxNotRunningError",
|
|
20
26
|
ExecTimeout(_) => "ExecTimeoutError",
|
|
21
27
|
ExecFailed(_) => "ExecFailedError",
|
|
22
28
|
SandboxFsOps(_) => "FilesystemError",
|
|
@@ -45,6 +51,10 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
45
51
|
SnapshotSandboxRunning(_) => "SnapshotSandboxRunningError",
|
|
46
52
|
SnapshotImageMissing(_) => "SnapshotImageMissingError",
|
|
47
53
|
SnapshotIntegrity(_) => "SnapshotIntegrityError",
|
|
54
|
+
// v0.6.7 (#1200): the automatic v0.6.6→v0.6.7 snapshot-descriptor
|
|
55
|
+
// migration (run at backend connect / artifact open) failed and needs
|
|
56
|
+
// repair. Mirrors the Python `SnapshotMigrationError`.
|
|
57
|
+
SnapshotMigration { .. } => "SnapshotMigrationError",
|
|
48
58
|
// Give the already-defined-but-orphaned `NetworkPolicyError` a mapping:
|
|
49
59
|
// a builder parse/validation error from `network(|n| ...)`. The gem
|
|
50
60
|
// unconditionally enables the core's `net` feature (default-features),
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
9
9
|
use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
|
|
10
|
+
use microsandbox::ImageArchiveFormat;
|
|
10
11
|
|
|
11
12
|
use crate::backend::with_local_backend;
|
|
12
13
|
use crate::conv;
|
|
14
|
+
use crate::error;
|
|
13
15
|
use crate::runtime::ruby;
|
|
14
16
|
|
|
15
17
|
fn handle_to_hash(h: &ImageHandle) -> RHash {
|
|
@@ -109,6 +111,40 @@ fn prune() -> Result<RHash, Error> {
|
|
|
109
111
|
Ok(report_to_hash(report))
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
/// Load images into the cache from a local `docker save` tarball or an OCI
|
|
115
|
+
/// Image Layout archive. `tags` optionally retags what was loaded. Returns an
|
|
116
|
+
/// Array of image-handle Hashes. Mirrors the Python `Image.load` / Node
|
|
117
|
+
/// `imageLoad` added in v0.6.7. (The `"-"` stdin form is spooled to a temp
|
|
118
|
+
/// file by the Ruby layer — the core reads seekable files only.)
|
|
119
|
+
fn load(input_path: String, tags: Vec<String>) -> Result<RArray, Error> {
|
|
120
|
+
let handles = with_local_backend(async |local| {
|
|
121
|
+
Image::load_local(local, std::path::Path::new(&input_path), tags).await
|
|
122
|
+
})?;
|
|
123
|
+
let arr = ruby().ary_new();
|
|
124
|
+
for h in handles.iter() {
|
|
125
|
+
arr.push(handle_to_hash(h))?;
|
|
126
|
+
}
|
|
127
|
+
Ok(arr)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Save cached image(s) into a local archive. `format` is "docker" (default
|
|
131
|
+
/// shape callers expect from `docker load`) or "oci" (OCI Image Layout).
|
|
132
|
+
/// Mirrors the Python `Image.save` / Node `imageSave` added in v0.6.7.
|
|
133
|
+
fn save(references: Vec<String>, output_path: String, format: String) -> Result<(), Error> {
|
|
134
|
+
let fmt = match format.as_str() {
|
|
135
|
+
"docker" => ImageArchiveFormat::Docker,
|
|
136
|
+
"oci" => ImageArchiveFormat::Oci,
|
|
137
|
+
other => {
|
|
138
|
+
return Err(error::base_error(format!(
|
|
139
|
+
"invalid archive format {other:?}: expected \"docker\" or \"oci\""
|
|
140
|
+
)))
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
with_local_backend(async |local| {
|
|
144
|
+
Image::save_local(local, &references, std::path::Path::new(&output_path), fmt).await
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
112
148
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
113
149
|
let class = native.define_class("Image", ruby.class_object())?;
|
|
114
150
|
class.define_singleton_method("get", function!(get, 1))?;
|
|
@@ -116,5 +152,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
116
152
|
class.define_singleton_method("inspect", function!(inspect, 1))?;
|
|
117
153
|
class.define_singleton_method("remove", function!(remove, 2))?;
|
|
118
154
|
class.define_singleton_method("prune", function!(prune, 0))?;
|
|
155
|
+
class.define_singleton_method("load", function!(load, 2))?;
|
|
156
|
+
class.define_singleton_method("save", function!(save, 3))?;
|
|
119
157
|
Ok(())
|
|
120
158
|
}
|