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
data/README.md
CHANGED
|
@@ -20,7 +20,13 @@ them. Our deepest thanks to the maintainers and community. ๐
|
|
|
20
20
|
[Rust](https://github.com/superradcompany/microsandbox/tree/main/sdk) ยท
|
|
21
21
|
[Python](https://github.com/superradcompany/microsandbox/tree/main/sdk/python) ยท
|
|
22
22
|
[TypeScript / Node](https://github.com/superradcompany/microsandbox/tree/main/sdk/node-ts) ยท
|
|
23
|
-
[Go](https://github.com/superradcompany/microsandbox/tree/main/sdk/go)
|
|
23
|
+
[Go](https://github.com/superradcompany/microsandbox/tree/main/sdk/go) ยท
|
|
24
|
+
[Ruby](https://github.com/superradcompany/microsandbox/tree/main/sdk/ruby)
|
|
25
|
+
(since upstream `v0.6.9` there is an **official** `microsandbox` gem โ a
|
|
26
|
+
compact veneer over the same Rust SDK. This gem predates it and covers a
|
|
27
|
+
larger surface (snapshots, SSH, streaming, volumes fs, network policy DSL,
|
|
28
|
+
RBS types); both define the `Microsandbox` module, so use one or the other,
|
|
29
|
+
not both, in a single process.)
|
|
24
30
|
- **Agents** โ [Agent Skills](https://github.com/superradcompany/skills) ยท [MCP server](https://github.com/superradcompany/microsandbox-mcp)
|
|
25
31
|
- **Community** โ [Discord](https://discord.gg/T95Y3XnEAK)
|
|
26
32
|
|
|
@@ -126,10 +132,13 @@ ensure
|
|
|
126
132
|
# sb.kill # force (SIGKILL); sb.drain for a graceful drain
|
|
127
133
|
end
|
|
128
134
|
|
|
129
|
-
# Inspect / manage existing sandboxes. `get
|
|
135
|
+
# Inspect / manage existing sandboxes. `get` returns a controllable
|
|
130
136
|
# SandboxHandle (the live `stop`/`kill`/`drain`/`wait` live on the object from
|
|
131
|
-
# `create`/`start`; fine-grained control lives on the handle).
|
|
132
|
-
|
|
137
|
+
# `create`/`start`; fine-grained control lives on the handle). `list` returns a
|
|
138
|
+
# cursor-paginated, Enumerable SandboxPage of handles (runtime v0.6.8).
|
|
139
|
+
Microsandbox::Sandbox.list # => Microsandbox::SandboxPage (first page)
|
|
140
|
+
Microsandbox::Sandbox.list.map(&:name) # enumerate the page's handles
|
|
141
|
+
# next page: Sandbox.list_with(cursor: page.next_cursor, limit: 50)
|
|
133
142
|
h = Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxHandle
|
|
134
143
|
h.status # :running, :stopped, :created, ...
|
|
135
144
|
h.stop_with_timeout(5) # custom escalation timeout
|
|
@@ -190,6 +199,22 @@ end
|
|
|
190
199
|
A non-zero exit is **not** an error โ inspect `exit_code`/`success?`. Spawn-time
|
|
191
200
|
failures (e.g. command not found) and timeouts raise typed errors (see below).
|
|
192
201
|
|
|
202
|
+
**Default workload** (runtime `v0.6.9`): `create` is strictly boot-only โ it
|
|
203
|
+
never runs the image's `ENTRYPOINT`/`CMD`. Execute the image's own command
|
|
204
|
+
explicitly:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
Microsandbox::Sandbox.create("worker", image: "example/worker:latest",
|
|
208
|
+
cmd: ["worker.py", "--once"]) do |sb| # cmd: overrides the durable image CMD
|
|
209
|
+
out = sb.exec_default(timeout: 300) # buffered; exec-style options
|
|
210
|
+
handle = sb.exec_default_stream # or streaming (returns an ExecHandle)
|
|
211
|
+
sb.attach_default # or interactive (host TTY)
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
An image whose entrypoint and CMD resolve to no executable command raises
|
|
216
|
+
`Microsandbox::NoDefaultCommandError`.
|
|
217
|
+
|
|
193
218
|
### Guest filesystem
|
|
194
219
|
|
|
195
220
|
```ruby
|
|
@@ -253,6 +278,10 @@ Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpin
|
|
|
253
278
|
# Live resize โ applies to the running VM under the default :no_restart policy:
|
|
254
279
|
sb.modify(cpus: 2, memory: 1024)
|
|
255
280
|
|
|
281
|
+
# Grow the root disk (managed upper or flat, MiB โ runtime v0.6.9). Applied
|
|
282
|
+
# while stopped; growth-only:
|
|
283
|
+
sb.modify(root_disk_size: 8192, policy: :next_start)
|
|
284
|
+
|
|
256
285
|
# env/labels/workdir changes on a *running* sandbox require a restart, so the
|
|
257
286
|
# default :no_restart policy rejects the whole apply (it raises rather than
|
|
258
287
|
# partially applying). Persist them for the next start โ or restart now โ
|
|
@@ -427,9 +456,15 @@ Microsandbox.with_backend(:local) { Microsandbox::Sandbox.create("box", image: "
|
|
|
427
456
|
```
|
|
428
457
|
|
|
429
458
|
Resolution order when no backend is set programmatically: `MSB_BACKEND`
|
|
430
|
-
(`local`/`cloud`) โ `
|
|
431
|
-
|
|
432
|
-
|
|
459
|
+
(`local`/`cloud`) โ `MSB_PROFILE` โ the `active_profile` in
|
|
460
|
+
`~/.microsandbox/config.json` (path overridable via `MSB_CONFIG_PATH`) โ
|
|
461
|
+
local. **Cloud intent must be explicit** (since runtime `v0.6.9`): a bare
|
|
462
|
+
`MSB_API_KEY` is treated as credential material, not backend intent, and no
|
|
463
|
+
longer selects the cloud on its own โ pair it with `MSB_BACKEND=cloud` (which
|
|
464
|
+
reads `MSB_API_URL`/`MSB_API_KEY`), or select a cloud profile. Invalid cloud
|
|
465
|
+
configuration (e.g. `MSB_BACKEND=cloud` without a usable API key or cloud
|
|
466
|
+
profile) fails closed with `Microsandbox::InvalidConfigError` instead of
|
|
467
|
+
silently running locally. The cloud backend currently supports a subset of
|
|
433
468
|
operations (create/start/stop/remove/get/list, one-shot exec, follow log
|
|
434
469
|
streaming); unsupported operations raise `Microsandbox::UnsupportedError`.
|
|
435
470
|
|
|
@@ -442,8 +477,8 @@ change diverged the two numbers โ the gem version is **not** a reliable indica
|
|
|
442
477
|
of the embedded runtime version. To learn which runtime a build wraps, ask it:
|
|
443
478
|
|
|
444
479
|
```ruby
|
|
445
|
-
Microsandbox::VERSION # => "0.
|
|
446
|
-
Microsandbox.runtime_version # => "v0.6.
|
|
480
|
+
Microsandbox::VERSION # => "0.13.0" (the gem's own version)
|
|
481
|
+
Microsandbox.runtime_version # => "v0.6.9" (the embedded upstream runtime tag)
|
|
447
482
|
```
|
|
448
483
|
|
|
449
484
|
| Gem version | Upstream runtime | Notes |
|
|
@@ -465,6 +500,8 @@ Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag
|
|
|
465
500
|
| `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
501
|
| `0.10.0` | `v0.6.6` | `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
|
|
467
502
|
| `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 |
|
|
503
|
+
| `0.12.0` | `v0.6.8` | adopts upstream `v0.6.8` (**breaking**): `Sandbox.list`/`.list_with` return a cursor-paginated `SandboxPage` (`limit:`/`cursor:` keywords), `UnsupportedError` re-keyed by structured operations with `#operation`/`#hint`; runtime adds a shared log registry for followed streams and cloud exec/ssh reconnects |
|
|
504
|
+
| `0.13.0` | `v0.6.9` | adopts upstream `v0.6.9` (**breaking**): a bare `MSB_API_KEY` no longer selects the cloud backend (explicit `MSB_BACKEND=cloud` or a cloud profile required; invalid cloud config fails closed with `InvalidConfigError`); snapshot payload integrity becomes opt-in (`record_integrity:`, `verify` can report `:not_recorded`). Parity: default-workload execution (`exec_default`/`exec_default_stream`/`attach_default`, `cmd:`), flat root disks (`RootDisk.flat`), `modify(root_disk_size:)`, `rate_limiter:`, `vsock:`, `default_backend_info`, `Volume.get_default` |
|
|
468
505
|
|
|
469
506
|
**Going forward** โ the gem version moves on its own semver track and no longer
|
|
470
507
|
mirrors the upstream tag:
|
|
@@ -544,7 +581,8 @@ lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
|
|
|
544
581
|
`status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
|
|
545
582
|
`stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
|
|
546
583
|
`wait_until_stopped`/`config`/`config_json`/`snapshot` from
|
|
547
|
-
`Sandbox.get`, and
|
|
584
|
+
`Sandbox.get`, and the cursor-paginated `list`/`list_with` with label
|
|
585
|
+
filters),
|
|
548
586
|
backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
|
|
549
587
|
`exec`/`shell` (collected and streaming), interactive `attach`/
|
|
550
588
|
`attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
|
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.9).
|
|
10
|
+
version = "0.13.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.9", default-features = true, features = ["ssh"] }
|
|
39
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.9" }
|
|
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"] }
|
|
@@ -20,7 +20,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
20
20
|
use std::sync::{Arc, Mutex, OnceLock};
|
|
21
21
|
|
|
22
22
|
use magnus::{function, prelude::*, Error, RModule, Ruby};
|
|
23
|
-
use microsandbox::{Backend, MicrosandboxError};
|
|
23
|
+
use microsandbox::{Backend, MicrosandboxError, Operation};
|
|
24
24
|
|
|
25
25
|
use crate::error;
|
|
26
26
|
use crate::runtime::block_on;
|
|
@@ -31,17 +31,14 @@ use crate::runtime::block_on;
|
|
|
31
31
|
/// borrowing `&LocalBackend` from it (`as_local()` borrows the `Arc`). Pure
|
|
32
32
|
/// Rust โ safe to call inside `block_on` (no Ruby C API), and it returns a raw
|
|
33
33
|
/// `MicrosandboxError` so the Ruby-exception mapping happens *after* `block_on`
|
|
34
|
-
/// re-acquires the GVL. Cloud backends yield `Unsupported
|
|
35
|
-
/// `resolve_local`.
|
|
36
|
-
pub fn local_backend() -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
34
|
+
/// re-acquires the GVL. Cloud backends yield `Unsupported` for the given
|
|
35
|
+
/// operation (v0.6.8 `Operation`-keyed shape), mirroring pyo3's `resolve_local`.
|
|
36
|
+
pub fn local_backend(op: Operation) -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
37
37
|
let backend = microsandbox::default_backend();
|
|
38
38
|
if backend.as_local().is_some() {
|
|
39
39
|
Ok(backend)
|
|
40
40
|
} else {
|
|
41
|
-
Err(MicrosandboxError::
|
|
42
|
-
feature: "this operation requires a local backend".into(),
|
|
43
|
-
available_when: "with the local backend (the default)".into(),
|
|
44
|
-
})
|
|
41
|
+
Err(MicrosandboxError::local_only(op))
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
@@ -53,10 +50,11 @@ pub fn local_backend() -> Result<Arc<dyn Backend>, MicrosandboxError> {
|
|
|
53
50
|
/// it and returns the same `Arc` kept alive for the borrow โ so it lives here
|
|
54
51
|
/// once rather than at every call site.
|
|
55
52
|
pub fn with_local_backend<T>(
|
|
53
|
+
operation: Operation,
|
|
56
54
|
op: impl AsyncFnOnce(µsandbox::LocalBackend) -> Result<T, MicrosandboxError>,
|
|
57
55
|
) -> Result<T, Error> {
|
|
58
56
|
block_on(async move {
|
|
59
|
-
let backend = local_backend()?;
|
|
57
|
+
let backend = local_backend(operation)?;
|
|
60
58
|
let local = backend
|
|
61
59
|
.as_local()
|
|
62
60
|
.expect("local_backend() guarantees a local backend");
|
|
@@ -161,10 +159,32 @@ fn default_backend_kind() -> String {
|
|
|
161
159
|
.to_string()
|
|
162
160
|
}
|
|
163
161
|
|
|
162
|
+
/// Secret-safe description of the active default backend (v0.6.9):
|
|
163
|
+
/// {kind, api_url, source, profile}. `source` names what selected the backend
|
|
164
|
+
/// (e.g. "MSB_BACKEND", "programmatic", "default"); the API key is never
|
|
165
|
+
/// included. Like `default_backend_kind`, the first call freezes ambient
|
|
166
|
+
/// env/profile resolution for the process.
|
|
167
|
+
fn default_backend_info() -> magnus::RHash {
|
|
168
|
+
let info = microsandbox::default_backend_info();
|
|
169
|
+
let hash = crate::runtime::ruby().hash_new();
|
|
170
|
+
let _ = hash.aset(
|
|
171
|
+
"kind",
|
|
172
|
+
match info.kind {
|
|
173
|
+
microsandbox::BackendKind::Local => "local",
|
|
174
|
+
microsandbox::BackendKind::Cloud => "cloud",
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
let _ = hash.aset("api_url", info.api_url);
|
|
178
|
+
let _ = hash.aset("source", info.source.as_str());
|
|
179
|
+
let _ = hash.aset("profile", info.profile);
|
|
180
|
+
hash
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
pub fn define(_ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
165
184
|
native.define_singleton_method("set_default_backend", function!(set_default_backend, 4))?;
|
|
166
185
|
native.define_singleton_method("push_default_backend", function!(push_default_backend, 4))?;
|
|
167
186
|
native.define_singleton_method("pop_default_backend", function!(pop_default_backend, 1))?;
|
|
168
187
|
native.define_singleton_method("default_backend_kind", function!(default_backend_kind, 0))?;
|
|
188
|
+
native.define_singleton_method("default_backend_info", function!(default_backend_info, 0))?;
|
|
169
189
|
Ok(())
|
|
170
190
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//! is always the core error's `to_string()`.
|
|
7
7
|
|
|
8
8
|
use magnus::{value::ReprValue, Error, ExceptionClass, Module, RClass, RModule, Ruby};
|
|
9
|
-
use microsandbox::{AgentClientError, MicrosandboxError};
|
|
9
|
+
use microsandbox::{AgentClientError, MicrosandboxError, Operation, UnsupportedReason};
|
|
10
10
|
|
|
11
11
|
/// The Ruby class (relative to the `Microsandbox` module) for a core error.
|
|
12
12
|
/// `"Error"` is the base class; anything else is a named subclass.
|
|
@@ -60,6 +60,10 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
60
60
|
// unconditionally enables the core's `net` feature (default-features),
|
|
61
61
|
// so this variant is always present.
|
|
62
62
|
NetworkBuilder(_) => "NetworkPolicyError",
|
|
63
|
+
// v0.6.9: `exec_default`/`attach_default` on an image whose resolved
|
|
64
|
+
// ENTRYPOINT+CMD provide no executable command. Mirrors the Python
|
|
65
|
+
// SDK's `NoDefaultCommandError`.
|
|
66
|
+
NoDefaultCommand => "NoDefaultCommandError",
|
|
63
67
|
_ => "Error",
|
|
64
68
|
}
|
|
65
69
|
}
|
|
@@ -83,12 +87,96 @@ pub fn to_ruby(err: MicrosandboxError) -> Error {
|
|
|
83
87
|
Err(_) => return Error::new(magnus::exception::runtime_error(), message),
|
|
84
88
|
};
|
|
85
89
|
|
|
90
|
+
// `Unsupported` gets a Ruby-idiom message (`sandbox.kill` instead of
|
|
91
|
+
// `Sandbox::kill`) plus structured `operation` / `hint` attributes on the
|
|
92
|
+
// exception instance, mirroring the Python SDK's enrichment (v0.6.8).
|
|
93
|
+
if let MicrosandboxError::Unsupported { op, reason } = &err {
|
|
94
|
+
return unsupported_error(&ruby, &ruby_api_name(*op), &ruby_hint(reason));
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
match exception_class(&ruby, class_name(&err)) {
|
|
87
98
|
Some(class) => Error::new(class, message),
|
|
88
99
|
None => Error::new(ruby.exception_runtime_error(), message),
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
/// `UnsupportedError` for shim-only entry points that require the local
|
|
104
|
+
/// backend but have no SDK [`Operation`] (Ruby-only diagnostic hooks such as
|
|
105
|
+
/// `Microsandbox.runtime_path`). `name` is the Ruby-facing API name. Mirrors
|
|
106
|
+
/// the Python SDK's name-based `local_only` helper.
|
|
107
|
+
#[allow(deprecated)]
|
|
108
|
+
pub fn local_only(name: &str) -> Error {
|
|
109
|
+
match Ruby::get() {
|
|
110
|
+
Ok(ruby) => unsupported_error(&ruby, name, "use a local backend"),
|
|
111
|
+
Err(_) => Error::new(
|
|
112
|
+
magnus::exception::runtime_error(),
|
|
113
|
+
format!("{name} is not supported by this backend: use a local backend"),
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Build a `Microsandbox::UnsupportedError` carrying the rendered message and
|
|
119
|
+
/// the structured `@operation` / `@hint` attributes.
|
|
120
|
+
fn unsupported_error(ruby: &Ruby, operation: &str, hint: &str) -> Error {
|
|
121
|
+
let message = format!("{operation} is not supported by this backend: {hint}");
|
|
122
|
+
let Some(class) = exception_class(ruby, "UnsupportedError") else {
|
|
123
|
+
return Error::new(ruby.exception_runtime_error(), message);
|
|
124
|
+
};
|
|
125
|
+
match class
|
|
126
|
+
.as_value()
|
|
127
|
+
.funcall::<_, _, magnus::Exception>("new", (message.as_str(),))
|
|
128
|
+
{
|
|
129
|
+
Ok(exc) => {
|
|
130
|
+
// Best-effort extras; the message already carries both.
|
|
131
|
+
let _ = exc
|
|
132
|
+
.funcall::<_, _, magnus::Value>("instance_variable_set", ("@operation", operation));
|
|
133
|
+
let _ = exc.funcall::<_, _, magnus::Value>("instance_variable_set", ("@hint", hint));
|
|
134
|
+
exc.into()
|
|
135
|
+
}
|
|
136
|
+
Err(_) => Error::new(class, message),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Render an [`Operation`] as the Ruby API it corresponds to: `Sandbox::kill`
|
|
141
|
+
/// becomes `sandbox.kill` and `SandboxFsOps::stat_handle` becomes
|
|
142
|
+
/// `sandbox_fs_ops.stat_handle`; a parenthetical keeps its Ruby keyword shape
|
|
143
|
+
/// (`log_stream(follow=false)` becomes `log_stream(follow: false)`). Plain
|
|
144
|
+
/// phrases without a `Type::method` shape (`config`, `snapshot operations`)
|
|
145
|
+
/// pass through as-is. Mirrors the Python SDK's `py_api_name`.
|
|
146
|
+
fn ruby_api_name(op: Operation) -> String {
|
|
147
|
+
let path = op.api_path();
|
|
148
|
+
let Some((ty, method)) = path.split_once("::") else {
|
|
149
|
+
return path.to_string();
|
|
150
|
+
};
|
|
151
|
+
format!("{}.{}", camel_to_snake(ty), method.replace('=', ": "))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Render an [`UnsupportedReason`] with `use instead` targets pointing at the
|
|
155
|
+
/// Ruby API name rather than the Rust path.
|
|
156
|
+
fn ruby_hint(reason: &UnsupportedReason) -> String {
|
|
157
|
+
match reason {
|
|
158
|
+
UnsupportedReason::UseInstead(op) => format!("use {}", ruby_api_name(*op)),
|
|
159
|
+
other => other.hint(),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Lower a `CamelCase` type name to `snake_case` (`SandboxFsOps` becomes
|
|
164
|
+
/// `sandbox_fs_ops`).
|
|
165
|
+
fn camel_to_snake(name: &str) -> String {
|
|
166
|
+
let mut out = String::with_capacity(name.len() + 4);
|
|
167
|
+
for (i, ch) in name.char_indices() {
|
|
168
|
+
if ch.is_ascii_uppercase() {
|
|
169
|
+
if i > 0 {
|
|
170
|
+
out.push('_');
|
|
171
|
+
}
|
|
172
|
+
out.push(ch.to_ascii_lowercase());
|
|
173
|
+
} else {
|
|
174
|
+
out.push(ch);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
out
|
|
178
|
+
}
|
|
179
|
+
|
|
92
180
|
/// A plain `Microsandbox::Error` (base) with a custom message โ used for
|
|
93
181
|
/// binding-level validation errors that have no core counterpart.
|
|
94
182
|
#[allow(deprecated)]
|
|
@@ -7,7 +7,7 @@
|
|
|
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
|
+
use microsandbox::{ImageArchiveFormat, Operation};
|
|
11
11
|
|
|
12
12
|
use crate::backend::with_local_backend;
|
|
13
13
|
use crate::conv;
|
|
@@ -84,12 +84,16 @@ fn report_to_hash(report: ImagePruneReport) -> RHash {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
fn get(reference: String) -> Result<RHash, Error> {
|
|
87
|
-
let handle = with_local_backend(async |local|
|
|
87
|
+
let handle = with_local_backend(Operation::ImageGet, async |local| {
|
|
88
|
+
Image::get_local(local, &reference).await
|
|
89
|
+
})?;
|
|
88
90
|
Ok(handle_to_hash(&handle))
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
fn list() -> Result<RArray, Error> {
|
|
92
|
-
let handles = with_local_backend(async |local|
|
|
94
|
+
let handles = with_local_backend(Operation::ImageList, async |local| {
|
|
95
|
+
Image::list_local(local).await
|
|
96
|
+
})?;
|
|
93
97
|
let arr = ruby().ary_new();
|
|
94
98
|
for h in handles.iter() {
|
|
95
99
|
arr.push(handle_to_hash(h))?;
|
|
@@ -98,16 +102,22 @@ fn list() -> Result<RArray, Error> {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
fn inspect(reference: String) -> Result<RHash, Error> {
|
|
101
|
-
let detail = with_local_backend(async |local|
|
|
105
|
+
let detail = with_local_backend(Operation::ImageInspect, async |local| {
|
|
106
|
+
Image::inspect_local(local, &reference).await
|
|
107
|
+
})?;
|
|
102
108
|
Ok(detail_to_hash(detail))
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
fn remove(reference: String, force: bool) -> Result<(), Error> {
|
|
106
|
-
with_local_backend(async |local|
|
|
112
|
+
with_local_backend(Operation::ImageRemove, async |local| {
|
|
113
|
+
Image::remove_local(local, &reference, force).await
|
|
114
|
+
})
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
fn prune() -> Result<RHash, Error> {
|
|
110
|
-
let report = with_local_backend(async |local|
|
|
118
|
+
let report = with_local_backend(Operation::ImagePrune, async |local| {
|
|
119
|
+
Image::prune_local(local).await
|
|
120
|
+
})?;
|
|
111
121
|
Ok(report_to_hash(report))
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -117,7 +127,7 @@ fn prune() -> Result<RHash, Error> {
|
|
|
117
127
|
/// `imageLoad` added in v0.6.7. (The `"-"` stdin form is spooled to a temp
|
|
118
128
|
/// file by the Ruby layer โ the core reads seekable files only.)
|
|
119
129
|
fn load(input_path: String, tags: Vec<String>) -> Result<RArray, Error> {
|
|
120
|
-
let handles = with_local_backend(async |local| {
|
|
130
|
+
let handles = with_local_backend(Operation::ImageLoad, async |local| {
|
|
121
131
|
Image::load_local(local, std::path::Path::new(&input_path), tags).await
|
|
122
132
|
})?;
|
|
123
133
|
let arr = ruby().ary_new();
|
|
@@ -140,7 +150,7 @@ fn save(references: Vec<String>, output_path: String, format: String) -> Result<
|
|
|
140
150
|
)))
|
|
141
151
|
}
|
|
142
152
|
};
|
|
143
|
-
with_local_backend(async |local| {
|
|
153
|
+
with_local_backend(Operation::ImageSave, async |local| {
|
|
144
154
|
Image::save_local(local, &references, std::path::Path::new(&output_path), fmt).await
|
|
145
155
|
})
|
|
146
156
|
}
|
data/ext/microsandbox/src/lib.rs
CHANGED
|
@@ -33,9 +33,10 @@ fn version() -> String {
|
|
|
33
33
|
/// Ruby Hash. Mirrors the official `all_sandbox_metrics` / `allSandboxMetrics`
|
|
34
34
|
/// helpers (Python/Node/Go).
|
|
35
35
|
fn all_sandbox_metrics() -> Result<RHash, Error> {
|
|
36
|
-
let map =
|
|
37
|
-
microsandbox::
|
|
38
|
-
|
|
36
|
+
let map =
|
|
37
|
+
backend::with_local_backend(microsandbox::Operation::AllSandboxMetrics, async |local| {
|
|
38
|
+
microsandbox::sandbox::all_sandbox_metrics_local(local).await
|
|
39
|
+
})?;
|
|
39
40
|
let hash = runtime::ruby().hash_new();
|
|
40
41
|
for (name, metrics) in &map {
|
|
41
42
|
hash.aset(name.as_str(), sandbox::metrics_to_hash(metrics))?;
|
|
@@ -107,12 +108,11 @@ fn set_runtime_libkrunfw_path(path: String) {
|
|
|
107
108
|
/// backend's config.
|
|
108
109
|
fn resolved_msb_path() -> Result<String, Error> {
|
|
109
110
|
let backend = microsandbox::default_backend();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})?;
|
|
111
|
+
// Shim-only entry point with no SDK `Operation` โ report the Ruby-facing
|
|
112
|
+
// name (`Microsandbox.runtime_path`), like Python's name-based local_only.
|
|
113
|
+
let local = backend
|
|
114
|
+
.as_local()
|
|
115
|
+
.ok_or_else(|| error::local_only("runtime_path"))?;
|
|
116
116
|
let path = local.config().resolve_msb_path().map_err(error::to_ruby)?;
|
|
117
117
|
Ok(path.to_string_lossy().into_owned())
|
|
118
118
|
}
|