microsandbox-rb 0.10.0 → 0.12.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 +139 -0
- data/Cargo.lock +82 -95
- data/README.md +21 -13
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/backend.rs +7 -9
- data/ext/microsandbox/src/error.rs +89 -1
- data/ext/microsandbox/src/image.rs +53 -5
- data/ext/microsandbox/src/lib.rs +9 -9
- data/ext/microsandbox/src/sandbox.rs +198 -55
- data/ext/microsandbox/src/snapshot.rs +79 -35
- data/ext/microsandbox/src/volume.rs +5 -3
- data/lib/microsandbox/errors.rb +16 -0
- data/lib/microsandbox/image.rb +38 -0
- data/lib/microsandbox/network.rb +139 -31
- data/lib/microsandbox/root_disk.rb +58 -0
- data/lib/microsandbox/sandbox.rb +174 -30
- data/lib/microsandbox/snapshot.rb +87 -37
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox.rb +1 -0
- data/sig/microsandbox.rbs +51 -14
- metadata +2 -1
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.12.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.8", default-features = true, features = ["ssh"] }
|
|
39
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.8" }
|
|
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");
|
|
@@ -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.
|
|
@@ -51,6 +51,10 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
51
51
|
SnapshotSandboxRunning(_) => "SnapshotSandboxRunningError",
|
|
52
52
|
SnapshotImageMissing(_) => "SnapshotImageMissingError",
|
|
53
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",
|
|
54
58
|
// Give the already-defined-but-orphaned `NetworkPolicyError` a mapping:
|
|
55
59
|
// a builder parse/validation error from `network(|n| ...)`. The gem
|
|
56
60
|
// unconditionally enables the core's `net` feature (default-features),
|
|
@@ -79,12 +83,96 @@ pub fn to_ruby(err: MicrosandboxError) -> Error {
|
|
|
79
83
|
Err(_) => return Error::new(magnus::exception::runtime_error(), message),
|
|
80
84
|
};
|
|
81
85
|
|
|
86
|
+
// `Unsupported` gets a Ruby-idiom message (`sandbox.kill` instead of
|
|
87
|
+
// `Sandbox::kill`) plus structured `operation` / `hint` attributes on the
|
|
88
|
+
// exception instance, mirroring the Python SDK's enrichment (v0.6.8).
|
|
89
|
+
if let MicrosandboxError::Unsupported { op, reason } = &err {
|
|
90
|
+
return unsupported_error(&ruby, &ruby_api_name(*op), &ruby_hint(reason));
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
match exception_class(&ruby, class_name(&err)) {
|
|
83
94
|
Some(class) => Error::new(class, message),
|
|
84
95
|
None => Error::new(ruby.exception_runtime_error(), message),
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
/// `UnsupportedError` for shim-only entry points that require the local
|
|
100
|
+
/// backend but have no SDK [`Operation`] (Ruby-only diagnostic hooks such as
|
|
101
|
+
/// `Microsandbox.runtime_path`). `name` is the Ruby-facing API name. Mirrors
|
|
102
|
+
/// the Python SDK's name-based `local_only` helper.
|
|
103
|
+
#[allow(deprecated)]
|
|
104
|
+
pub fn local_only(name: &str) -> Error {
|
|
105
|
+
match Ruby::get() {
|
|
106
|
+
Ok(ruby) => unsupported_error(&ruby, name, "use a local backend"),
|
|
107
|
+
Err(_) => Error::new(
|
|
108
|
+
magnus::exception::runtime_error(),
|
|
109
|
+
format!("{name} is not supported by this backend: use a local backend"),
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Build a `Microsandbox::UnsupportedError` carrying the rendered message and
|
|
115
|
+
/// the structured `@operation` / `@hint` attributes.
|
|
116
|
+
fn unsupported_error(ruby: &Ruby, operation: &str, hint: &str) -> Error {
|
|
117
|
+
let message = format!("{operation} is not supported by this backend: {hint}");
|
|
118
|
+
let Some(class) = exception_class(ruby, "UnsupportedError") else {
|
|
119
|
+
return Error::new(ruby.exception_runtime_error(), message);
|
|
120
|
+
};
|
|
121
|
+
match class
|
|
122
|
+
.as_value()
|
|
123
|
+
.funcall::<_, _, magnus::Exception>("new", (message.as_str(),))
|
|
124
|
+
{
|
|
125
|
+
Ok(exc) => {
|
|
126
|
+
// Best-effort extras; the message already carries both.
|
|
127
|
+
let _ = exc
|
|
128
|
+
.funcall::<_, _, magnus::Value>("instance_variable_set", ("@operation", operation));
|
|
129
|
+
let _ = exc.funcall::<_, _, magnus::Value>("instance_variable_set", ("@hint", hint));
|
|
130
|
+
exc.into()
|
|
131
|
+
}
|
|
132
|
+
Err(_) => Error::new(class, message),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Render an [`Operation`] as the Ruby API it corresponds to: `Sandbox::kill`
|
|
137
|
+
/// becomes `sandbox.kill` and `SandboxFsOps::stat_handle` becomes
|
|
138
|
+
/// `sandbox_fs_ops.stat_handle`; a parenthetical keeps its Ruby keyword shape
|
|
139
|
+
/// (`log_stream(follow=false)` becomes `log_stream(follow: false)`). Plain
|
|
140
|
+
/// phrases without a `Type::method` shape (`config`, `snapshot operations`)
|
|
141
|
+
/// pass through as-is. Mirrors the Python SDK's `py_api_name`.
|
|
142
|
+
fn ruby_api_name(op: Operation) -> String {
|
|
143
|
+
let path = op.api_path();
|
|
144
|
+
let Some((ty, method)) = path.split_once("::") else {
|
|
145
|
+
return path.to_string();
|
|
146
|
+
};
|
|
147
|
+
format!("{}.{}", camel_to_snake(ty), method.replace('=', ": "))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Render an [`UnsupportedReason`] with `use instead` targets pointing at the
|
|
151
|
+
/// Ruby API name rather than the Rust path.
|
|
152
|
+
fn ruby_hint(reason: &UnsupportedReason) -> String {
|
|
153
|
+
match reason {
|
|
154
|
+
UnsupportedReason::UseInstead(op) => format!("use {}", ruby_api_name(*op)),
|
|
155
|
+
other => other.hint(),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Lower a `CamelCase` type name to `snake_case` (`SandboxFsOps` becomes
|
|
160
|
+
/// `sandbox_fs_ops`).
|
|
161
|
+
fn camel_to_snake(name: &str) -> String {
|
|
162
|
+
let mut out = String::with_capacity(name.len() + 4);
|
|
163
|
+
for (i, ch) in name.char_indices() {
|
|
164
|
+
if ch.is_ascii_uppercase() {
|
|
165
|
+
if i > 0 {
|
|
166
|
+
out.push('_');
|
|
167
|
+
}
|
|
168
|
+
out.push(ch.to_ascii_lowercase());
|
|
169
|
+
} else {
|
|
170
|
+
out.push(ch);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
out
|
|
174
|
+
}
|
|
175
|
+
|
|
88
176
|
/// A plain `Microsandbox::Error` (base) with a custom message — used for
|
|
89
177
|
/// binding-level validation errors that have no core counterpart.
|
|
90
178
|
#[allow(deprecated)]
|
|
@@ -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, Operation};
|
|
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 {
|
|
@@ -82,12 +84,16 @@ fn report_to_hash(report: ImagePruneReport) -> RHash {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
fn get(reference: String) -> Result<RHash, Error> {
|
|
85
|
-
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
|
+
})?;
|
|
86
90
|
Ok(handle_to_hash(&handle))
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
fn list() -> Result<RArray, Error> {
|
|
90
|
-
let handles = with_local_backend(async |local|
|
|
94
|
+
let handles = with_local_backend(Operation::ImageList, async |local| {
|
|
95
|
+
Image::list_local(local).await
|
|
96
|
+
})?;
|
|
91
97
|
let arr = ruby().ary_new();
|
|
92
98
|
for h in handles.iter() {
|
|
93
99
|
arr.push(handle_to_hash(h))?;
|
|
@@ -96,19 +102,59 @@ fn list() -> Result<RArray, Error> {
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
fn inspect(reference: String) -> Result<RHash, Error> {
|
|
99
|
-
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
|
+
})?;
|
|
100
108
|
Ok(detail_to_hash(detail))
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
fn remove(reference: String, force: bool) -> Result<(), Error> {
|
|
104
|
-
with_local_backend(async |local|
|
|
112
|
+
with_local_backend(Operation::ImageRemove, async |local| {
|
|
113
|
+
Image::remove_local(local, &reference, force).await
|
|
114
|
+
})
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
fn prune() -> Result<RHash, Error> {
|
|
108
|
-
let report = with_local_backend(async |local|
|
|
118
|
+
let report = with_local_backend(Operation::ImagePrune, async |local| {
|
|
119
|
+
Image::prune_local(local).await
|
|
120
|
+
})?;
|
|
109
121
|
Ok(report_to_hash(report))
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
/// Load images into the cache from a local `docker save` tarball or an OCI
|
|
125
|
+
/// Image Layout archive. `tags` optionally retags what was loaded. Returns an
|
|
126
|
+
/// Array of image-handle Hashes. Mirrors the Python `Image.load` / Node
|
|
127
|
+
/// `imageLoad` added in v0.6.7. (The `"-"` stdin form is spooled to a temp
|
|
128
|
+
/// file by the Ruby layer — the core reads seekable files only.)
|
|
129
|
+
fn load(input_path: String, tags: Vec<String>) -> Result<RArray, Error> {
|
|
130
|
+
let handles = with_local_backend(Operation::ImageLoad, async |local| {
|
|
131
|
+
Image::load_local(local, std::path::Path::new(&input_path), tags).await
|
|
132
|
+
})?;
|
|
133
|
+
let arr = ruby().ary_new();
|
|
134
|
+
for h in handles.iter() {
|
|
135
|
+
arr.push(handle_to_hash(h))?;
|
|
136
|
+
}
|
|
137
|
+
Ok(arr)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Save cached image(s) into a local archive. `format` is "docker" (default
|
|
141
|
+
/// shape callers expect from `docker load`) or "oci" (OCI Image Layout).
|
|
142
|
+
/// Mirrors the Python `Image.save` / Node `imageSave` added in v0.6.7.
|
|
143
|
+
fn save(references: Vec<String>, output_path: String, format: String) -> Result<(), Error> {
|
|
144
|
+
let fmt = match format.as_str() {
|
|
145
|
+
"docker" => ImageArchiveFormat::Docker,
|
|
146
|
+
"oci" => ImageArchiveFormat::Oci,
|
|
147
|
+
other => {
|
|
148
|
+
return Err(error::base_error(format!(
|
|
149
|
+
"invalid archive format {other:?}: expected \"docker\" or \"oci\""
|
|
150
|
+
)))
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
with_local_backend(Operation::ImageSave, async |local| {
|
|
154
|
+
Image::save_local(local, &references, std::path::Path::new(&output_path), fmt).await
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
112
158
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
113
159
|
let class = native.define_class("Image", ruby.class_object())?;
|
|
114
160
|
class.define_singleton_method("get", function!(get, 1))?;
|
|
@@ -116,5 +162,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
116
162
|
class.define_singleton_method("inspect", function!(inspect, 1))?;
|
|
117
163
|
class.define_singleton_method("remove", function!(remove, 2))?;
|
|
118
164
|
class.define_singleton_method("prune", function!(prune, 0))?;
|
|
165
|
+
class.define_singleton_method("load", function!(load, 2))?;
|
|
166
|
+
class.define_singleton_method("save", function!(save, 3))?;
|
|
119
167
|
Ok(())
|
|
120
168
|
}
|
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
|
}
|