microsandbox-rb 0.9.2 → 0.10.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 +81 -0
- data/Cargo.lock +82 -76
- data/README.md +53 -2
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/sandbox.rs +197 -4
- data/lib/microsandbox/modification.rb +108 -0
- data/lib/microsandbox/sandbox.rb +177 -1
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox.rb +1 -0
- data/sig/microsandbox.rbs +43 -1
- metadata +2 -1
data/README.md
CHANGED
|
@@ -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.10.0" (the gem's own version)
|
|
446
|
+
Microsandbox.runtime_version # => "v0.6.6" (the embedded upstream runtime tag)
|
|
398
447
|
```
|
|
399
448
|
|
|
400
449
|
| Gem version | Upstream runtime | Notes |
|
|
@@ -413,6 +462,8 @@ 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` |
|
|
416
467
|
|
|
417
468
|
**Going forward** — the gem version moves on its own semver track and no longer
|
|
418
469
|
mirrors the upstream tag:
|
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.6).
|
|
10
|
+
version = "0.10.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.6", default-features = true, features = ["ssh"] }
|
|
39
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.6" }
|
|
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",
|
|
@@ -18,10 +18,11 @@ use microsandbox::logs::{
|
|
|
18
18
|
LogCursor, LogEntry, LogOptions, LogSource, LogStreamOptions, LogStreamStart,
|
|
19
19
|
};
|
|
20
20
|
use microsandbox::sandbox::{
|
|
21
|
-
AttachOptionsBuilder, DiskImageFormat, FsEntry, FsEntryKind, FsMetadata,
|
|
22
|
-
Patch, PullPolicy, PullProgress, PullProgressHandle, RlimitResource,
|
|
23
|
-
SandboxFilter, SandboxHandle, SandboxMetrics,
|
|
24
|
-
|
|
21
|
+
AttachOptionsBuilder, DiskImageFormat, EnvVar, FsEntry, FsEntryKind, FsMetadata,
|
|
22
|
+
HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle, RlimitResource,
|
|
23
|
+
SandboxBuilder, SandboxFilter, SandboxHandle, SandboxMetrics, SandboxModificationBuilder,
|
|
24
|
+
SandboxModificationPatch, SandboxStatus, SandboxStopResult, SecretBuilder,
|
|
25
|
+
SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
|
|
25
26
|
};
|
|
26
27
|
use microsandbox::LogLevel;
|
|
27
28
|
use microsandbox::MicrosandboxResult;
|
|
@@ -92,9 +93,19 @@ impl Sandbox {
|
|
|
92
93
|
if let Some(v) = conv::opt_u8(opts, "cpus")? {
|
|
93
94
|
b = b.cpus(v);
|
|
94
95
|
}
|
|
96
|
+
// v0.6.6 (#1099): boot-time maximum vCPU / memory ceilings that a later
|
|
97
|
+
// live `modify` can grow up to. `cpus`/`memory` above already raise these
|
|
98
|
+
// to their own value if lower, so a bare `cpus`/`memory` still works;
|
|
99
|
+
// setting these explicitly reserves headroom for a live resize.
|
|
100
|
+
if let Some(v) = conv::opt_u8(opts, "max_cpus")? {
|
|
101
|
+
b = b.max_cpus(v);
|
|
102
|
+
}
|
|
95
103
|
if let Some(v) = conv::opt_u32(opts, "memory")? {
|
|
96
104
|
b = b.memory(v);
|
|
97
105
|
}
|
|
106
|
+
if let Some(v) = conv::opt_u32(opts, "max_memory")? {
|
|
107
|
+
b = b.max_memory(v);
|
|
108
|
+
}
|
|
98
109
|
if let Some(v) = conv::opt_string(opts, "workdir")? {
|
|
99
110
|
b = b.workdir(v);
|
|
100
111
|
}
|
|
@@ -584,6 +595,34 @@ impl Sandbox {
|
|
|
584
595
|
Ok(metrics_to_hash(&m))
|
|
585
596
|
}
|
|
586
597
|
|
|
598
|
+
//----------------------------------------------------------------------
|
|
599
|
+
// Health & modification (v0.6.6 / #1099)
|
|
600
|
+
//----------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
/// Health-check the guest agent without refreshing the idle timer. Returns
|
|
603
|
+
/// `{ name, latency_secs }` (round-trip latency as f64 seconds; the Ruby
|
|
604
|
+
/// layer exposes both seconds and milliseconds).
|
|
605
|
+
fn ping(&self) -> Result<RHash, Error> {
|
|
606
|
+
let result = block_on(self.inner.ping()).map_err(error::to_ruby)?;
|
|
607
|
+
Ok(ping_result_to_hash(&result))
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/// Explicitly refresh the idle-activity timer. Returns
|
|
611
|
+
/// `{ name, activity_seq }`.
|
|
612
|
+
fn touch(&self) -> Result<RHash, Error> {
|
|
613
|
+
let result = block_on(self.inner.touch()).map_err(error::to_ruby)?;
|
|
614
|
+
Ok(touch_result_to_hash(&result))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// Plan or apply a live sandbox modification. `opts` is the string-keyed
|
|
618
|
+
/// modify Hash normalized by the Ruby layer; returns the resulting
|
|
619
|
+
/// `SandboxModificationPlan` as a JSON string, which the Ruby layer parses
|
|
620
|
+
/// into a `ModificationPlan`. Mirrors the Node native binding (native returns
|
|
621
|
+
/// JSON, the wrapper shapes it).
|
|
622
|
+
fn modify(&self, opts: RHash) -> Result<String, Error> {
|
|
623
|
+
run_modify(self.inner.modify(), opts)
|
|
624
|
+
}
|
|
625
|
+
|
|
587
626
|
/// Read captured logs as an Array of Hashes. `opts`: tail, since_ms,
|
|
588
627
|
/// until_ms, sources (Array of "stdout"/"stderr"/"output"/"system"/"all").
|
|
589
628
|
fn logs(&self, opts: RHash) -> Result<RArray, Error> {
|
|
@@ -1736,6 +1775,133 @@ fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
|
|
|
1736
1775
|
hash
|
|
1737
1776
|
}
|
|
1738
1777
|
|
|
1778
|
+
//--------------------------------------------------------------------------------------------------
|
|
1779
|
+
// Health & live modification (v0.6.6 / #1099)
|
|
1780
|
+
//--------------------------------------------------------------------------------------------------
|
|
1781
|
+
|
|
1782
|
+
/// A `SandboxPingResult` as `{ name, latency_secs }`. Latency is a `Duration`;
|
|
1783
|
+
/// we hand it to Ruby as f64 seconds and let the Ruby layer derive milliseconds
|
|
1784
|
+
/// (the Python/Node SDKs expose only `latency_ms`).
|
|
1785
|
+
fn ping_result_to_hash(result: µsandbox::sandbox::SandboxPingResult) -> RHash {
|
|
1786
|
+
let hash = ruby().hash_new();
|
|
1787
|
+
let _ = hash.aset("name", result.name.clone());
|
|
1788
|
+
let _ = hash.aset("latency_secs", result.latency.as_secs_f64());
|
|
1789
|
+
hash
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/// A `SandboxTouchResult` as `{ name, activity_seq }`.
|
|
1793
|
+
fn touch_result_to_hash(result: µsandbox::sandbox::SandboxTouchResult) -> RHash {
|
|
1794
|
+
let hash = ruby().hash_new();
|
|
1795
|
+
let _ = hash.aset("name", result.name.clone());
|
|
1796
|
+
let _ = hash.aset("activity_seq", result.activity_seq);
|
|
1797
|
+
hash
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/// Drive a modify builder to a plan (dry-run or apply) and return it serialized
|
|
1801
|
+
/// as a JSON string. Shared by `Sandbox::modify` and `SbHandle::modify`. The
|
|
1802
|
+
/// canonical `SandboxModificationPlan` serde shape (snake_case keys, literal-space
|
|
1803
|
+
/// enum strings) is parsed verbatim by the Ruby layer — matching the Python SDK's
|
|
1804
|
+
/// raw-dict contract rather than re-casing it.
|
|
1805
|
+
fn run_modify(builder: SandboxModificationBuilder, opts: RHash) -> Result<String, Error> {
|
|
1806
|
+
let patch = build_modify_patch(opts)?;
|
|
1807
|
+
let policy = conv::opt_string(opts, "policy")?.unwrap_or_else(|| "no_restart".to_string());
|
|
1808
|
+
let dry_run = conv::opt_bool(opts, "dry_run")?;
|
|
1809
|
+
let builder = apply_modify_policy(builder.with_patch(patch), &policy)?;
|
|
1810
|
+
let plan = block_on(async move {
|
|
1811
|
+
if dry_run {
|
|
1812
|
+
builder.dry_run().await
|
|
1813
|
+
} else {
|
|
1814
|
+
builder.apply().await
|
|
1815
|
+
}
|
|
1816
|
+
})
|
|
1817
|
+
.map_err(error::to_ruby)?;
|
|
1818
|
+
serde_json::to_string(&plan)
|
|
1819
|
+
.map_err(|e| error::base_error(format!("failed to serialize modification plan: {e}")))
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/// Build the canonical `SandboxModificationPatch` from the modify Hash. Env and
|
|
1823
|
+
/// label pairs are sorted so repeated calls with the same arguments produce the
|
|
1824
|
+
/// same patch (and plan) ordering, mirroring the Python binding. `oci_upper_size`
|
|
1825
|
+
/// is deliberately not surfaced (CLI-only upstream; the Python/Node SDKs omit it
|
|
1826
|
+
/// too), so it stays unset via `..Default::default()`.
|
|
1827
|
+
fn build_modify_patch(opts: RHash) -> Result<SandboxModificationPatch, Error> {
|
|
1828
|
+
let mut env_pairs = conv::opt_string_map(opts, "env")?;
|
|
1829
|
+
env_pairs.sort();
|
|
1830
|
+
let mut label_pairs = conv::opt_string_map(opts, "labels")?;
|
|
1831
|
+
label_pairs.sort();
|
|
1832
|
+
|
|
1833
|
+
Ok(SandboxModificationPatch {
|
|
1834
|
+
cpus: conv::opt_u8(opts, "cpus")?,
|
|
1835
|
+
max_cpus: conv::opt_u8(opts, "max_cpus")?,
|
|
1836
|
+
memory_mib: conv::opt_u32(opts, "memory")?,
|
|
1837
|
+
max_memory_mib: conv::opt_u32(opts, "max_memory")?,
|
|
1838
|
+
env: env_pairs
|
|
1839
|
+
.into_iter()
|
|
1840
|
+
.map(|(k, v)| EnvVar::new(k, v))
|
|
1841
|
+
.collect(),
|
|
1842
|
+
env_remove: conv::opt_string_vec(opts, "remove_env")?,
|
|
1843
|
+
labels: label_pairs,
|
|
1844
|
+
labels_remove: conv::opt_string_vec(opts, "remove_labels")?,
|
|
1845
|
+
workdir: conv::opt_string(opts, "workdir")?,
|
|
1846
|
+
secrets: parse_modify_secrets(opts)?,
|
|
1847
|
+
secrets_remove: conv::opt_string_vec(opts, "remove_secrets")?,
|
|
1848
|
+
..Default::default()
|
|
1849
|
+
})
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/// Parse the `secrets` modify option (an Array of string-keyed Hashes normalized
|
|
1853
|
+
/// by the Ruby layer) into canonical `SecretModificationPatch`es. Each Hash
|
|
1854
|
+
/// carries `name` plus at most one of `env`/`store` (source) or `value` (raw
|
|
1855
|
+
/// material) — the Ruby layer enforces that mutual exclusion — plus optional
|
|
1856
|
+
/// `placeholder` and `allowed_hosts`. The raw `value` moves straight into the
|
|
1857
|
+
/// `Zeroizing` field; it is never echoed in an error.
|
|
1858
|
+
fn parse_modify_secrets(opts: RHash) -> Result<Vec<SecretModificationPatch>, Error> {
|
|
1859
|
+
let mut out = Vec::new();
|
|
1860
|
+
for h in conv::opt_hash_vec(opts, "secrets")? {
|
|
1861
|
+
let name = conv::opt_string(h, "name")?
|
|
1862
|
+
.ok_or_else(|| error::base_error("secret modification spec requires :name"))?;
|
|
1863
|
+
let env = conv::opt_string(h, "env")?;
|
|
1864
|
+
let store = conv::opt_string(h, "store")?;
|
|
1865
|
+
let value = conv::opt_string(h, "value")?;
|
|
1866
|
+
let source = match (env, store) {
|
|
1867
|
+
(Some(var), None) => Some(SecretSource::Env { var }),
|
|
1868
|
+
(None, Some(reference)) => Some(SecretSource::Store { reference }),
|
|
1869
|
+
(None, None) => None,
|
|
1870
|
+
// The Ruby layer rejects >1 source; guard defensively without
|
|
1871
|
+
// echoing any value.
|
|
1872
|
+
(Some(_), Some(_)) => {
|
|
1873
|
+
return Err(error::base_error(format!(
|
|
1874
|
+
"secret {name:?}: \"env\" and \"store\" are mutually exclusive"
|
|
1875
|
+
)))
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
out.push(SecretModificationPatch {
|
|
1879
|
+
name,
|
|
1880
|
+
source,
|
|
1881
|
+
value: value.unwrap_or_default().into(),
|
|
1882
|
+
placeholder: conv::opt_string(h, "placeholder")?,
|
|
1883
|
+
allowed_hosts: conv::opt_string_vec(h, "allowed_hosts")?,
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
Ok(out)
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/// Apply the parsed `policy` string to a modify builder. `no_restart` (default)
|
|
1890
|
+
/// leaves the builder untouched; `next_start`/`restart` select those policies.
|
|
1891
|
+
fn apply_modify_policy(
|
|
1892
|
+
builder: SandboxModificationBuilder,
|
|
1893
|
+
policy: &str,
|
|
1894
|
+
) -> Result<SandboxModificationBuilder, Error> {
|
|
1895
|
+
match policy {
|
|
1896
|
+
"no_restart" => Ok(builder),
|
|
1897
|
+
"next_start" => Ok(builder.next_start()),
|
|
1898
|
+
"restart" => Ok(builder.restart()),
|
|
1899
|
+
other => Err(error::base_error(format!(
|
|
1900
|
+
"unknown policy {other:?}; expected \"no_restart\", \"next_start\", or \"restart\""
|
|
1901
|
+
))),
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1739
1905
|
//--------------------------------------------------------------------------------------------------
|
|
1740
1906
|
// Pull progress (streaming image-pull during create_with_progress)
|
|
1741
1907
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -1987,6 +2153,27 @@ impl SbHandle {
|
|
|
1987
2153
|
let snap = block_on(self.inner.snapshot_to(path)).map_err(error::to_ruby)?;
|
|
1988
2154
|
Ok(crate::snapshot::snapshot_to_hash(&snap))
|
|
1989
2155
|
}
|
|
2156
|
+
|
|
2157
|
+
/// Health-check the guest agent without refreshing the idle timer. A stopped
|
|
2158
|
+
/// sandbox raises `SandboxNotRunningError` (the handle does not start it
|
|
2159
|
+
/// implicitly). Returns `{ name, latency_secs }`.
|
|
2160
|
+
fn ping(&self) -> Result<RHash, Error> {
|
|
2161
|
+
let result = block_on(self.inner.ping()).map_err(error::to_ruby)?;
|
|
2162
|
+
Ok(ping_result_to_hash(&result))
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/// Explicitly refresh the idle-activity timer. A stopped sandbox raises
|
|
2166
|
+
/// `SandboxNotRunningError`. Returns `{ name, activity_seq }`.
|
|
2167
|
+
fn touch(&self) -> Result<RHash, Error> {
|
|
2168
|
+
let result = block_on(self.inner.touch()).map_err(error::to_ruby)?;
|
|
2169
|
+
Ok(touch_result_to_hash(&result))
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/// Plan or apply a live sandbox modification (see `Sandbox::modify`). Returns
|
|
2173
|
+
/// the `SandboxModificationPlan` as a JSON string.
|
|
2174
|
+
fn modify(&self, opts: RHash) -> Result<String, Error> {
|
|
2175
|
+
run_modify(self.inner.modify(), opts)
|
|
2176
|
+
}
|
|
1990
2177
|
}
|
|
1991
2178
|
|
|
1992
2179
|
//--------------------------------------------------------------------------------------------------
|
|
@@ -2100,6 +2287,9 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2100
2287
|
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
2101
2288
|
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
2102
2289
|
class.define_method("metrics", method!(Sandbox::metrics, 0))?;
|
|
2290
|
+
class.define_method("ping", method!(Sandbox::ping, 0))?;
|
|
2291
|
+
class.define_method("touch", method!(Sandbox::touch, 0))?;
|
|
2292
|
+
class.define_method("modify", method!(Sandbox::modify, 1))?;
|
|
2103
2293
|
class.define_method("metrics_stream", method!(Sandbox::metrics_stream, 1))?;
|
|
2104
2294
|
class.define_method("logs", method!(Sandbox::logs, 1))?;
|
|
2105
2295
|
class.define_method("log_stream", method!(Sandbox::log_stream, 1))?;
|
|
@@ -2148,6 +2338,9 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2148
2338
|
handle.define_method("config_json", method!(SbHandle::config_json, 0))?;
|
|
2149
2339
|
handle.define_method("snapshot", method!(SbHandle::snapshot, 1))?;
|
|
2150
2340
|
handle.define_method("snapshot_to", method!(SbHandle::snapshot_to, 1))?;
|
|
2341
|
+
handle.define_method("ping", method!(SbHandle::ping, 0))?;
|
|
2342
|
+
handle.define_method("touch", method!(SbHandle::touch, 0))?;
|
|
2343
|
+
handle.define_method("modify", method!(SbHandle::modify, 1))?;
|
|
2151
2344
|
|
|
2152
2345
|
let session = native.define_class("PullSession", ruby.class_object())?;
|
|
2153
2346
|
session.define_method("recv", method!(PullSession::recv, 0))?;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Microsandbox
|
|
4
|
+
# The result of {Sandbox#ping} / {SandboxHandle#ping}: a lightweight health
|
|
5
|
+
# check that confirms the guest agent is reachable without refreshing the
|
|
6
|
+
# sandbox idle timer. Mirrors the official SDKs' `SandboxPingResult`.
|
|
7
|
+
#
|
|
8
|
+
# The native layer reports latency as seconds; {#latency} is the canonical
|
|
9
|
+
# value and {#latency_ms} the convenience the Python/Node SDKs expose.
|
|
10
|
+
class PingResult
|
|
11
|
+
# @return [String] the sandbox name that was pinged
|
|
12
|
+
attr_reader :name
|
|
13
|
+
# @return [Float] round-trip latency in seconds
|
|
14
|
+
attr_reader :latency
|
|
15
|
+
|
|
16
|
+
def initialize(data)
|
|
17
|
+
@name = data["name"]
|
|
18
|
+
@latency = data["latency_secs"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Float] round-trip latency in milliseconds
|
|
22
|
+
def latency_ms
|
|
23
|
+
@latency * 1000.0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inspect
|
|
27
|
+
"#<Microsandbox::PingResult name=#{@name.inspect} latency_ms=#{format("%.3f", latency_ms)}>"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The result of {Sandbox#touch} / {SandboxHandle#touch}: an explicit refresh of
|
|
32
|
+
# the sandbox idle-activity timer. Mirrors the official SDKs' `SandboxTouchResult`.
|
|
33
|
+
class TouchResult
|
|
34
|
+
# @return [String] the sandbox name that was touched
|
|
35
|
+
attr_reader :name
|
|
36
|
+
# @return [Integer] the agent activity sequence after this touch was recorded
|
|
37
|
+
attr_reader :activity_seq
|
|
38
|
+
|
|
39
|
+
def initialize(data)
|
|
40
|
+
@name = data["name"]
|
|
41
|
+
@activity_seq = data["activity_seq"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspect
|
|
45
|
+
"#<Microsandbox::TouchResult name=#{@name.inspect} activity_seq=#{@activity_seq}>"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The plan produced by {Sandbox#modify} / {SandboxHandle#modify}: the classified
|
|
50
|
+
# set of changes a modification requested, and (for a non-dry-run apply) whether
|
|
51
|
+
# they were applied plus any live-resize convergence outcomes. Mirrors the
|
|
52
|
+
# official SDKs' `SandboxModificationPlan`.
|
|
53
|
+
#
|
|
54
|
+
# The nested collections ({#changes}, {#conflicts}, {#warnings},
|
|
55
|
+
# {#resize_status}) are frozen Arrays of frozen, symbol-keyed Hashes carrying
|
|
56
|
+
# the canonical snake_case fields verbatim (e.g. a config change is
|
|
57
|
+
# `{ kind: "config", field:, change:, before:, after:, disposition:, reason: }`;
|
|
58
|
+
# a secret change adds `name:`, `before_ref:`, `after_ref:`, `allow_hosts:`).
|
|
59
|
+
# Enum values stay as the runtime's strings (e.g. a `disposition` of
|
|
60
|
+
# `"next start"`, a `state` of `"guest-refused"`).
|
|
61
|
+
class ModificationPlan
|
|
62
|
+
# @return [String] the sandbox the plan applies to
|
|
63
|
+
attr_reader :sandbox
|
|
64
|
+
# @return [String] the sandbox status used to classify the changes
|
|
65
|
+
attr_reader :status
|
|
66
|
+
# @return [Symbol] :no_restart, :next_start, or :restart
|
|
67
|
+
attr_reader :policy
|
|
68
|
+
# @return [Array<Hash>] the planned changes (config and secret)
|
|
69
|
+
attr_reader :changes
|
|
70
|
+
# @return [Array<Hash>] conflicts that must be resolved before applying
|
|
71
|
+
attr_reader :conflicts
|
|
72
|
+
# @return [Array<Hash>] non-fatal warnings about the patch
|
|
73
|
+
attr_reader :warnings
|
|
74
|
+
# @return [Array<Hash>] live resource-resize outcomes (populated by an apply)
|
|
75
|
+
attr_reader :resize_status
|
|
76
|
+
|
|
77
|
+
def initialize(data)
|
|
78
|
+
@sandbox = data["sandbox"]
|
|
79
|
+
@status = data["status"]
|
|
80
|
+
@applied = data["applied"]
|
|
81
|
+
@policy = data["policy"]&.to_sym
|
|
82
|
+
@changes = freeze_items(data["changes"])
|
|
83
|
+
@conflicts = freeze_items(data["conflicts"])
|
|
84
|
+
@warnings = freeze_items(data["warnings"])
|
|
85
|
+
@resize_status = freeze_items(data["resize_status"])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] whether the changes were applied (false for a dry run)
|
|
89
|
+
def applied?
|
|
90
|
+
@applied
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def inspect
|
|
94
|
+
"#<Microsandbox::ModificationPlan sandbox=#{@sandbox.inspect} applied=#{@applied} " \
|
|
95
|
+
"policy=#{@policy} changes=#{@changes.size} conflicts=#{@conflicts.size} " \
|
|
96
|
+
"warnings=#{@warnings.size}>"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Convert a JSON array of plan entries (string-keyed Hashes) into a frozen
|
|
102
|
+
# Array of frozen, symbol-keyed Hashes. The entries are shallow (all values
|
|
103
|
+
# are scalars or string arrays), so a single `transform_keys` suffices.
|
|
104
|
+
def freeze_items(items)
|
|
105
|
+
Array(items).map { |item| item.transform_keys(&:to_sym).freeze }.freeze
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|