microsandbox-rb 0.17.0 → 0.17.2
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 +229 -8
- data/Cargo.lock +45 -46
- data/DESIGN.md +90 -72
- data/README.md +178 -669
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +108 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +141 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +117 -0
- data/docs/runtime.md +154 -0
- data/docs/surface.md +75 -0
- data/ext/microsandbox/Cargo.toml +9 -5
- data/ext/microsandbox/src/agent.rs +13 -11
- data/ext/microsandbox/src/backend.rs +1 -1
- data/ext/microsandbox/src/exec.rs +12 -8
- data/ext/microsandbox/src/fs_stream.rs +19 -7
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +3 -2
- data/ext/microsandbox/src/runtime.rs +189 -13
- data/ext/microsandbox/src/sandbox.rs +149 -102
- data/ext/microsandbox/src/snapshot.rs +17 -11
- data/ext/microsandbox/src/ssh.rs +22 -16
- data/ext/microsandbox/src/stream.rs +6 -2
- data/ext/microsandbox/src/volume.rs +50 -17
- data/lib/microsandbox/fs.rb +4 -8
- data/lib/microsandbox/image.rb +2 -2
- data/lib/microsandbox/log_entry.rb +1 -1
- data/lib/microsandbox/metrics.rb +1 -1
- data/lib/microsandbox/outbound_proxy.rb +8 -0
- data/lib/microsandbox/sandbox.rb +128 -13
- data/lib/microsandbox/snapshot.rb +1 -1
- data/lib/microsandbox/ssh.rb +2 -10
- data/lib/microsandbox/streams.rb +8 -5
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +31 -14
- data/lib/microsandbox-rb.rb +7 -0
- data/lib/microsandbox.rb +88 -31
- data/sig/microsandbox.rbs +24 -0
- metadata +14 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Metrics, logs, and live modification
|
|
2
|
+
|
|
3
|
+
## Metrics & logs
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
|
|
7
|
+
# On v0.6.x runtimes the metrics slot goes live a beat after create returns,
|
|
8
|
+
# so `metrics` can briefly raise "no live metrics slot" right after boot —
|
|
9
|
+
# retry for a few hundred ms rather than treating the first failure as fatal.
|
|
10
|
+
m = sb.metrics # => Microsandbox::Metrics
|
|
11
|
+
m.cpu_percent
|
|
12
|
+
m.memory_bytes
|
|
13
|
+
m.uptime_secs
|
|
14
|
+
|
|
15
|
+
sb.logs(tail: 100, sources: ["stdout", "stderr"]).each do |entry|
|
|
16
|
+
puts "[#{entry.source}] #{entry.text}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Streaming counterparts: `sb.metrics_stream` (`MetricsStream` over `Metrics`)
|
|
22
|
+
and `sb.log_stream` (`LogStream` over `LogEntry`) — both single-pass, see
|
|
23
|
+
[execution.md](execution.md#streams-are-single-pass).
|
|
24
|
+
`Microsandbox.all_sandbox_metrics` reports every running sandbox at once.
|
|
25
|
+
|
|
26
|
+
`MetricsDisabledError` means metrics are turned off for the sandbox. The
|
|
27
|
+
metrics slot goes live a moment *after* `create` returns; a call inside that
|
|
28
|
+
window raises the base `Microsandbox::Error` with a message containing
|
|
29
|
+
`no live metrics slot` (not `MetricsUnavailableError`, which the pinned runtime
|
|
30
|
+
never raises). Retry on that message:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
metrics = begin
|
|
34
|
+
sb.metrics
|
|
35
|
+
rescue Microsandbox::Error => e
|
|
36
|
+
raise unless e.message.include?("no live metrics slot")
|
|
37
|
+
sleep 0.1
|
|
38
|
+
retry
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`sb.log_stream(follow: true)` keeps waiting for new entries and **does not end
|
|
43
|
+
when the sandbox stops** — the runtime follows the log file, not the sandbox —
|
|
44
|
+
and the wait cannot be interrupted. A thread blocked in it also keeps the
|
|
45
|
+
process from exiting normally. Read what exists with `sb.logs` or
|
|
46
|
+
`sb.log_stream` (no `follow:`), or follow from a separate process you can kill.
|
|
47
|
+
|
|
48
|
+
## Live modification and health
|
|
49
|
+
|
|
50
|
+
Resize, reconfigure, or rotate secrets on a sandbox **without recreating it**,
|
|
51
|
+
and probe the guest agent's liveness. To leave headroom for a live CPU/memory
|
|
52
|
+
resize, reserve a ceiling at create time with `max_cpus:`/`max_memory:`:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpine:latest",
|
|
56
|
+
cpus: 1, max_cpus: 4, memory: 512, max_memory: 2048) do |sb|
|
|
57
|
+
# Health check — does NOT refresh the idle timer:
|
|
58
|
+
ping = sb.ping # => Microsandbox::PingResult
|
|
59
|
+
ping.latency_ms # round-trip latency
|
|
60
|
+
|
|
61
|
+
# Explicitly refresh the idle-activity timer (resets any idle_timeout:):
|
|
62
|
+
sb.touch.activity_seq # => Microsandbox::TouchResult
|
|
63
|
+
|
|
64
|
+
# Preview a change without applying it:
|
|
65
|
+
plan = sb.modify(cpus: 2, memory: 1024, dry_run: true)
|
|
66
|
+
plan.applied? # => false
|
|
67
|
+
plan.changes # => [{ kind: "config", field: "cpus", ... }, ...]
|
|
68
|
+
|
|
69
|
+
# Live resize — applies to the running VM under the default :no_restart policy:
|
|
70
|
+
sb.modify(cpus: 2, memory: 1024)
|
|
71
|
+
|
|
72
|
+
# Grow the root disk (managed upper or flat, MiB — runtime v0.6.9). Applied
|
|
73
|
+
# while stopped; growth-only:
|
|
74
|
+
sb.modify(root_disk_size: 8192, policy: :next_start)
|
|
75
|
+
|
|
76
|
+
# env/labels/workdir changes on a *running* sandbox require a restart, so the
|
|
77
|
+
# default :no_restart policy rejects the whole apply (it raises rather than
|
|
78
|
+
# partially applying). Persist them for the next start — or restart now —
|
|
79
|
+
# by saying so explicitly:
|
|
80
|
+
sb.modify(env: { "TIER" => "prod" }, remove_env: ["DEBUG"],
|
|
81
|
+
labels: { "role" => "worker" }, policy: :next_start) # or policy: :restart
|
|
82
|
+
|
|
83
|
+
# Rotating/removing an *existing* secret (or updating its allowed hosts) is
|
|
84
|
+
# live; *adding* a new secret is restart-required, like env. Specs are keyed
|
|
85
|
+
# by name; env:/store:/value: are mutually exclusive:
|
|
86
|
+
sb.modify(
|
|
87
|
+
secrets: { "API_KEY" => { env: "HOST_API_KEY", allowed_hosts: ["api.example.com"] } },
|
|
88
|
+
remove_secrets: ["OLD_TOKEN"],
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The apply is **all-or-nothing**: under a given `policy:` every planned change
|
|
94
|
+
must be applicable, or the whole `modify` raises — nothing is partially
|
|
95
|
+
applied. Use `dry_run: true` to inspect each change's `disposition` (`"live"`,
|
|
96
|
+
`"next start"`, `"requires restart"`) before committing.
|
|
97
|
+
|
|
98
|
+
`SandboxHandle` (from `Sandbox.get`/`list`) carries the same `#ping`/`#touch`/
|
|
99
|
+
`#modify`; on a stopped sandbox `#ping`/`#touch` raise `SandboxNotRunningError`.
|
data/docs/releasing.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Releases are automated by `.github/workflows/release.yml` via RubyGems
|
|
4
|
+
**Trusted Publishing** (OIDC) — there is no API key to store as a secret. The
|
|
5
|
+
trusted publisher is already configured for this gem, so no per-release secret
|
|
6
|
+
or credential setup is needed.
|
|
7
|
+
|
|
8
|
+
## Each release
|
|
9
|
+
|
|
10
|
+
1. Bump the gem's **own** version on its independent semver track (see the
|
|
11
|
+
[Versioning](../README.md#versioning) section of the README); **don't** pick
|
|
12
|
+
the number to mirror the upstream tag. The version is written in three
|
|
13
|
+
files that must stay equal (`spec/unit/version_spec.rb` asserts it), and
|
|
14
|
+
recorded in the lockfile:
|
|
15
|
+
- `lib/microsandbox/version.rb` — `Microsandbox::VERSION`;
|
|
16
|
+
- `ext/microsandbox/Cargo.toml` — the `[package] version`;
|
|
17
|
+
- `binaries/lib/microsandbox/binaries.rb` — `Microsandbox::Binaries::VERSION`
|
|
18
|
+
(the companion gem ships in lockstep);
|
|
19
|
+
- `Cargo.lock` — committed and packed into the gem, and it records the
|
|
20
|
+
extension crate's version, so a version bump must commit the regenerated
|
|
21
|
+
lock (`bundle exec rake compile` rewrites it).
|
|
22
|
+
|
|
23
|
+
If the release also adopts a new upstream runtime, move the runtime tag
|
|
24
|
+
everywhere it is named:
|
|
25
|
+
- `ext/microsandbox/Cargo.toml` — the `tag = "vX.Y.Z"` on **all three**
|
|
26
|
+
upstream git deps (`microsandbox`, `microsandbox-network`,
|
|
27
|
+
`microsandbox-runtime`);
|
|
28
|
+
- `lib/microsandbox/version.rb` — `Microsandbox::RUNTIME_VERSION`;
|
|
29
|
+
- `binaries/lib/microsandbox/binaries.rb` —
|
|
30
|
+
`Microsandbox::Binaries::RUNTIME_VERSION`;
|
|
31
|
+
- `binaries/checksums/<tag>.sha256` — the release's `checksums.sha256`,
|
|
32
|
+
committed (vendoring fails closed without it);
|
|
33
|
+
- `Cargo.lock` — regenerated again, since it pins the git deps to the new
|
|
34
|
+
tag's commit.
|
|
35
|
+
|
|
36
|
+
Then add a row to the Versioning table in the README and update
|
|
37
|
+
`CHANGELOG.md`.
|
|
38
|
+
2. Push a `vX.Y.Z` tag. Once the [release preflight](#release-preflight)
|
|
39
|
+
passes, CI pushes the **source gem** to RubyGems via
|
|
40
|
+
`rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
|
|
41
|
+
`RUBYGEMS_API_KEY` secret required — then creates the GitHub Release.
|
|
42
|
+
|
|
43
|
+
## Release preflight
|
|
44
|
+
|
|
45
|
+
`release.yml`'s `preflight` job runs `.github/scripts/release_preflight.sh
|
|
46
|
+
<tag>` and the `publish` job needs it, so nothing reaches RubyGems unless:
|
|
47
|
+
|
|
48
|
+
- the tag is `vX.Y.Z` and the three version constants **in the tagged commit**
|
|
49
|
+
— `Microsandbox::VERSION`, `[package] version` in
|
|
50
|
+
`ext/microsandbox/Cargo.toml`, `Microsandbox::Binaries::VERSION` — all equal
|
|
51
|
+
`X.Y.Z`;
|
|
52
|
+
- the tagged commit is on `main`;
|
|
53
|
+
- the `CI` workflow's push run for that exact commit concluded `success`.
|
|
54
|
+
|
|
55
|
+
Pushing `main` and the tag together is fine: the preflight **waits** for that
|
|
56
|
+
CI run to appear and finish (polling every 30 s, up to 45 min —
|
|
57
|
+
`PREFLIGHT_CI_WAIT_SECONDS` / `PREFLIGHT_CI_POLL_SECONDS`), so a release
|
|
58
|
+
publishes only after CI, including the real-microVM integration lanes, is
|
|
59
|
+
green. A failed or timed-out preflight leaves the tag unpublished. How to
|
|
60
|
+
recover depends on whether the tag has to move:
|
|
61
|
+
|
|
62
|
+
- **The tagged commit is right** (CI flaked, or was still red): re-run the
|
|
63
|
+
`CI` run for that commit until it is green, then re-run the failed `Release`
|
|
64
|
+
run.
|
|
65
|
+
- **The tag must point at a different commit** (wrong version constants, not
|
|
66
|
+
on `main`): delete the tag, fix the commit, and push the tag again — that
|
|
67
|
+
starts a new `Release` run. Never re-run the old run after retagging: a
|
|
68
|
+
re-run keeps the commit it was started for, so it checks the old commit
|
|
69
|
+
again and fails the same way.
|
|
70
|
+
|
|
71
|
+
Dry-run it against an existing tag — nothing is published on a manual
|
|
72
|
+
dispatch — or run the script locally from a full clone:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
gh workflow run release.yml --ref main -f preflight_ref=vX.Y.Z
|
|
76
|
+
GH_TOKEN=$(gh auth token) .github/scripts/release_preflight.sh vX.Y.Z
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It is a workflow-level gate; branch protection and a protected environment for
|
|
80
|
+
the trusted publisher are separate repository settings.
|
|
81
|
+
|
|
82
|
+
CI's `lint` job also runs `cargo deny check advisories` against `Cargo.lock`;
|
|
83
|
+
the exceptions (advisories with no fixed release at the pinned runtime) are in
|
|
84
|
+
`deny.toml` at the repository root — re-check them on every runtime adoption.
|
|
85
|
+
|
|
86
|
+
## Precompiled per-platform gems
|
|
87
|
+
|
|
88
|
+
Precompiled *extension* gems are not built or published. Users install the
|
|
89
|
+
source gem, which compiles the extension via `rb_sys` at install time (so a
|
|
90
|
+
stable Rust toolchain is required on the installing host). The companion
|
|
91
|
+
`microsandbox-rb-binaries` gems below are unaffected: they carry the host
|
|
92
|
+
runtime, not the extension.
|
|
93
|
+
|
|
94
|
+
## Runtime binaries gems
|
|
95
|
+
|
|
96
|
+
`microsandbox-rb-binaries` (source in `binaries/`) is a *separate* artifact from
|
|
97
|
+
the SDK gem: it carries no Ruby extension, only the
|
|
98
|
+
upstream `msb` + `libkrunfw` for one platform, verified against the release's
|
|
99
|
+
published `checksums.sha256` when vendored. CI's `binaries` job vendors and
|
|
100
|
+
builds all three (`arm64-darwin`, `x86_64-linux-gnu`, `aarch64-linux-gnu`) on
|
|
101
|
+
every run and smoke-tests the host one; `release.yml`'s `binaries-gems` job
|
|
102
|
+
does the same on a tag and a separate `publish-binaries` job pushes them after
|
|
103
|
+
the SDK gem is live. The SDK `publish` job needs `binaries-gems`, so a
|
|
104
|
+
companion **build** failure blocks the SDK publish as well. A companion
|
|
105
|
+
**push** failure is its own red job once the SDK gem is live: the release is
|
|
106
|
+
then incomplete until `publish-binaries` is re-run. They use their own RubyGems
|
|
107
|
+
trusted-publisher entry (same repo + workflow, gem name
|
|
108
|
+
`microsandbox-rb-binaries`). To build them by hand:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rake -C binaries vendor[arm64-darwin] # download + sha256-verify into binaries/vendor
|
|
112
|
+
rake -C binaries build[arm64-darwin] # package into binaries/pkg/*.gem
|
|
113
|
+
rake -C binaries verify # host-only: run the vendored `msb --version`
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`binaries/vendor` and `binaries/pkg` are gitignored — binaries are never
|
|
117
|
+
committed; CI rebuilds them from the upstream release.
|
data/docs/runtime.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Runtime binaries, resolution, and backends
|
|
2
|
+
|
|
3
|
+
## The runtime binaries
|
|
4
|
+
|
|
5
|
+
`microsandbox-rb` is **SDK-only** — it wraps the microVM runtime, it doesn't
|
|
6
|
+
carry it. The host-side `msb` runtime and the `libkrunfw` firmware come from an
|
|
7
|
+
optional companion gem, **`microsandbox-rb-binaries`**, published as one gem per
|
|
8
|
+
platform (`arm64-darwin`, `x86_64-linux-gnu`, `aarch64-linux-gnu`) and
|
|
9
|
+
versioned in lockstep with this gem:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile — install both gems at the same version
|
|
13
|
+
gem "microsandbox-rb", require: "microsandbox"
|
|
14
|
+
gem "microsandbox-rb-binaries"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That's all the wiring there is: `require "microsandbox"` finds the companion
|
|
18
|
+
gem, checks it is the same version and built for the same upstream runtime, and
|
|
19
|
+
points the resolver at its vendored `msb` — so your bundle carries the runtime
|
|
20
|
+
and nothing is downloaded at install time or on first call. **Recommended
|
|
21
|
+
whenever you boot local microVMs.** Cloud-only users (`MSB_BACKEND=cloud`)
|
|
22
|
+
should skip it: it is a separate, optional gem precisely so nobody has to fetch
|
|
23
|
+
~50 MB of binaries they won't run. Neither gem depends on the other.
|
|
24
|
+
|
|
25
|
+
> **Availability.** The binaries gems are published on every release tag
|
|
26
|
+
> alongside `microsandbox-rb` since `0.14.0`. On an older SDK version, or a
|
|
27
|
+
> platform without a bundle, use the fallback below (or build them yourself
|
|
28
|
+
> from `binaries/` — see [binaries/README.md](https://github.com/ya-luotao/microsandbox-rb/blob/main/binaries/README.md)).
|
|
29
|
+
|
|
30
|
+
### Fallback — first-use download
|
|
31
|
+
|
|
32
|
+
Without the companion gem, the `msb` runtime and `libkrunfw` firmware are
|
|
33
|
+
provisioned into `~/.microsandbox` automatically on first use (the first
|
|
34
|
+
`Sandbox.create`/`start` downloads them if missing). To provision ahead of time
|
|
35
|
+
— e.g. while baking a container image, or to avoid the first-call latency —
|
|
36
|
+
call `install` explicitly:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Microsandbox.install unless Microsandbox.installed?
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Set `MICROSANDBOX_NO_AUTO_INSTALL` to disable the automatic first-use download
|
|
43
|
+
(e.g. on air-gapped hosts that provision the runtime out of band). None of this
|
|
44
|
+
applies when the companion gem supplies the runtime: its binaries are already
|
|
45
|
+
the matching version, so `ensure_runtime!` skips the installer entirely and
|
|
46
|
+
nothing is written to `~/.microsandbox`. `Microsandbox.setup` customizes the
|
|
47
|
+
provisioning step itself.
|
|
48
|
+
|
|
49
|
+
## Runtime path resolution
|
|
50
|
+
|
|
51
|
+
The `msb` runtime path is resolved in this order: the `MSB_PATH` environment
|
|
52
|
+
variable → the `microsandbox-rb-binaries` gem (or another SDK-set override) →
|
|
53
|
+
the config file → `~/.microsandbox/bin/msb` → `msb` on `PATH`.
|
|
54
|
+
`Microsandbox.runtime_path` reports the winner.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Microsandbox.installed? # => true/false
|
|
58
|
+
Microsandbox.install # download + install the runtime (idempotent)
|
|
59
|
+
Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
|
|
60
|
+
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override (set-once)
|
|
61
|
+
Microsandbox.libkrunfw_path = "/opt/microsandbox/lib/libkrunfw.dylib" # override (set-once)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When `microsandbox-rb-binaries` is installed, `require "microsandbox"` claims
|
|
65
|
+
that SDK-set slot with the gem's vendored `msb` (the firmware is found alongside
|
|
66
|
+
it), and `runtime_path` points into the gem. The two gems are versioned in
|
|
67
|
+
lockstep and the companion gem must be the same version **and** built for the
|
|
68
|
+
same upstream runtime — a mismatch of either is reported with a warning and
|
|
69
|
+
skipped, and the SDK falls back to `~/.microsandbox` rather than driving a
|
|
70
|
+
runtime it doesn't match. Because the slot is **set-once**,
|
|
71
|
+
`Microsandbox.runtime_path=` is then a no-op: use the `MSB_PATH` environment
|
|
72
|
+
variable, which outranks it, to point at a different runtime.
|
|
73
|
+
|
|
74
|
+
## Backend routing
|
|
75
|
+
|
|
76
|
+
Every operation runs through a backend. The default is the local libkrun
|
|
77
|
+
backend; without any configuration nothing changes. A backend can be selected
|
|
78
|
+
programmatically or via the environment:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
Microsandbox.default_backend_kind # => :local (or :cloud)
|
|
82
|
+
Microsandbox.default_backend_info # => Microsandbox::BackendInfo
|
|
83
|
+
Microsandbox.set_default_backend(:cloud, url: "https://api.example.com", api_key: ENV["MSB_API_KEY"])
|
|
84
|
+
# or a named profile from ~/.microsandbox/config.json:
|
|
85
|
+
Microsandbox.set_default_backend(:cloud, profile: "prod")
|
|
86
|
+
|
|
87
|
+
# Scoped override (restored afterward, even on error):
|
|
88
|
+
Microsandbox.with_backend(:local) do
|
|
89
|
+
Microsandbox::Sandbox.create("box", image: "alpine") { |sb| puts sb.shell("uname -a").stdout }
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Resolution order when no backend is set programmatically: `MSB_BACKEND`
|
|
94
|
+
(`local`/`cloud`) → `MSB_PROFILE` → the `active_profile` in
|
|
95
|
+
`~/.microsandbox/config.json` (path overridable via `MSB_CONFIG_PATH`) →
|
|
96
|
+
local. **Cloud intent must be explicit** (since runtime `v0.6.9`): a bare
|
|
97
|
+
`MSB_API_KEY` is treated as credential material, not backend intent, and no
|
|
98
|
+
longer selects the cloud on its own — pair it with `MSB_BACKEND=cloud` (which
|
|
99
|
+
reads `MSB_API_URL`/`MSB_API_KEY`), or select a cloud profile. Invalid cloud
|
|
100
|
+
configuration (e.g. `MSB_BACKEND=cloud` without a usable API key or cloud
|
|
101
|
+
profile) fails closed with `Microsandbox::InvalidConfigError` instead of
|
|
102
|
+
silently running locally. The cloud backend currently supports a subset of
|
|
103
|
+
operations (create/start/stop/remove/get/list, one-shot exec, follow log
|
|
104
|
+
streaming); unsupported operations raise `Microsandbox::UnsupportedError`
|
|
105
|
+
(with `#operation` and `#hint`).
|
|
106
|
+
|
|
107
|
+
## One runtime line per home
|
|
108
|
+
|
|
109
|
+
The runtime home (`MSB_HOME`, default `~/.microsandbox`) belongs to one
|
|
110
|
+
runtime line at a time. This gem embeds runtime `v0.6.18` and, on first use,
|
|
111
|
+
installs that version of `msb` into the home — replacing a different version
|
|
112
|
+
that is already there, including a newer `msb` installed by the official CLI.
|
|
113
|
+
In the other direction, a `0.7.x` `msb` migrates the catalog to a newer schema
|
|
114
|
+
the first time it opens it, after which every local call from this gem fails
|
|
115
|
+
with `database schema is newer than this msb binary`. The migration is one-way.
|
|
116
|
+
|
|
117
|
+
If the official CLI (or another SDK) on a different runtime line shares the
|
|
118
|
+
machine, give this gem its own home:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
export MSB_HOME="$HOME/.microsandbox-rb"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Known issues at the pinned runtime
|
|
125
|
+
|
|
126
|
+
These are fixed upstream in runtime releases this gem has not adopted yet. See
|
|
127
|
+
[SECURITY.md](../SECURITY.md) for the security advisories.
|
|
128
|
+
|
|
129
|
+
- **macOS hosts: guest `fallocate` can truncate a file on a bind mount or a
|
|
130
|
+
directory-backed named volume.** Preallocating a range that ends inside an
|
|
131
|
+
existing file shrinks the file to the end of that range, and the call
|
|
132
|
+
reports success. Fixed upstream in `v0.7.0`. Until then, keep data that
|
|
133
|
+
guest programs preallocate (databases, download tools) on the guest's OCI
|
|
134
|
+
root disk or a `disk:` volume, not on a host directory.
|
|
135
|
+
- **`modify(secrets:)` adding the first secret** to a sandbox created without
|
|
136
|
+
any does not turn on TLS interception, so the value is not substituted.
|
|
137
|
+
Create the sandbox with at least one secret.
|
|
138
|
+
- **`patches:`** resets the owner and mode of parent directories that already
|
|
139
|
+
exist in the image to `root:root 0755`. Write after boot with `sb.fs`, or
|
|
140
|
+
restore ownership yourself.
|
|
141
|
+
|
|
142
|
+
## Environment variables
|
|
143
|
+
|
|
144
|
+
| Variable | Effect |
|
|
145
|
+
|----------|--------|
|
|
146
|
+
| `MSB_PATH` | Path to the `msb` runtime binary; outranks every other source, including the binaries gem |
|
|
147
|
+
| `MSB_LIBKRUNFW_PATH` | Path to the `libkrunfw` shared library; outranks `Microsandbox.libkrunfw_path=` |
|
|
148
|
+
| `MSB_HOME` | Runtime home: the catalog, image cache, sandboxes, volumes, snapshots, and the first-use runtime install. Default `~/.microsandbox` |
|
|
149
|
+
| `MSB_BACKEND` | `local` or `cloud`; cloud additionally reads `MSB_API_URL` / `MSB_API_KEY` |
|
|
150
|
+
| `MSB_PROFILE` | Select a named profile from the config file |
|
|
151
|
+
| `MSB_CONFIG_PATH` | Override the `~/.microsandbox/config.json` location |
|
|
152
|
+
| `MICROSANDBOX_NO_AUTO_INSTALL` | Disable the first-use runtime download |
|
|
153
|
+
| `MICROSANDBOX_INTEGRATION` | Set to `1` to run the integration specs (development only) |
|
|
154
|
+
| `MICROSANDBOX_TEST_IMAGE` | Override the integration-spec image (development only) |
|
data/docs/surface.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Supported surface
|
|
2
|
+
|
|
3
|
+
The binding covers the official-SDK surface of the upstream Rust core. This
|
|
4
|
+
page is the authoritative scope statement; `sig/microsandbox.rbs` is the typed
|
|
5
|
+
counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
6
|
+
|
|
7
|
+
## Implemented
|
|
8
|
+
|
|
9
|
+
- **Sandbox lifecycle** — the live `Sandbox` (`stop`/`stop_and_wait`/`kill`/
|
|
10
|
+
`drain`/`wait`/`status`/`detach`/`owns_lifecycle?`), the `SandboxHandle`
|
|
11
|
+
controls from `Sandbox.get` (`stop_with_timeout`/`request_stop`/
|
|
12
|
+
`request_kill`/`request_drain`/`wait_until_stopped`/`config`/`config_json`/
|
|
13
|
+
`snapshot`), the cursor-paginated `list`/`list_with` with label filters, and
|
|
14
|
+
the convergent lifecycle (`connect_or_create`, `#id`, `wait_for_status`,
|
|
15
|
+
`restart`, `destroy`, `SandboxHandle#connect_or_start`,
|
|
16
|
+
`SandboxReplacedError`).
|
|
17
|
+
- **Backend routing** — `set_default_backend`/`with_backend`/
|
|
18
|
+
`default_backend_kind`/`default_backend_info`.
|
|
19
|
+
- **Execution** — `exec`/`shell` (collected and streaming), the default
|
|
20
|
+
workload (`exec_default`/`exec_default_stream`/`attach_default`, `cmd:`),
|
|
21
|
+
interactive `attach`/`attach_shell`, per-call `rlimits`.
|
|
22
|
+
- **Guest filesystem** — the full API including streaming `read_stream`/
|
|
23
|
+
`write_stream`.
|
|
24
|
+
- **Observability** — per-sandbox `metrics`, `Microsandbox.all_sandbox_metrics`,
|
|
25
|
+
streaming `metrics_stream`/`log_stream`, `logs`, `ping`/`touch`.
|
|
26
|
+
- **Live modification** — `modify` with `dry_run:` and `policy:`: CPU/memory
|
|
27
|
+
resize (`cpus`/`max_cpus`/`memory`/`max_memory`), root-disk growth
|
|
28
|
+
(`root_disk_size`, MiB), `env`/`remove_env`, `labels`/`remove_labels`,
|
|
29
|
+
`workdir`, and secret rotation (`secrets`/`remove_secrets`).
|
|
30
|
+
- **OCI image cache** — `Image.get`/`list`/`inspect`/`remove`/`prune`, image
|
|
31
|
+
archives (`Image.load`/`Image.save`), streaming pull progress
|
|
32
|
+
(`Sandbox.create_with_progress` → `PullSession`).
|
|
33
|
+
- **Named volumes** — `Volume.create`/`get`/`list`/`remove`/`get_default`, plus
|
|
34
|
+
host-side `Volume.fs`/`VolumeInfo#fs` read/write.
|
|
35
|
+
- **Snapshots** — create/open/list/list_dir/reindex/verify/save/load, boot from
|
|
36
|
+
snapshot, opt-in `record_integrity:`.
|
|
37
|
+
- **Rootfs patches** — `Microsandbox::Patch` (text/file/append/copy_file/
|
|
38
|
+
copy_dir/symlink/mkdir/remove) via `create(patches:)`.
|
|
39
|
+
- **Root disk** — structured `root_disk:` (managed / tmpfs / disk / flat via
|
|
40
|
+
`Microsandbox::RootDisk`), `follow_root_symlinks:`; `oci_upper_size:` is the
|
|
41
|
+
deprecated alias for a managed `root_disk:` size.
|
|
42
|
+
- **Network configuration** — composable profiles, custom per-rule
|
|
43
|
+
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, custom DNS (`dns:`),
|
|
44
|
+
TLS-interception tuning (`tls:`), guest address pools (`ipv4_pool:`/
|
|
45
|
+
`ipv6_pool:`), `max_connections`, `trust_host_cas`, `strict` hostname mode,
|
|
46
|
+
outbound SOCKS4/SOCKS5 `proxy:` (`OutboundProxy`/`SecretSource`),
|
|
47
|
+
`rate_limiter:`, `vsock:`.
|
|
48
|
+
- **Secrets** — multi-host / wildcard allow-lists, injection toggles,
|
|
49
|
+
per-secret `on_violation:` and sandbox-level `on_secret_violation:` policy.
|
|
50
|
+
- **SSH** — `Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`, with
|
|
51
|
+
`inactivity_timeout:`.
|
|
52
|
+
- **Raw agent client** — `Microsandbox::AgentClient` → `AgentStream`/
|
|
53
|
+
`AgentFrame`.
|
|
54
|
+
- **Runtime provisioning** — `install`/`installed?`/`ensure_runtime!`/
|
|
55
|
+
`runtime_path`/`libkrunfw_path`, customizable via `Microsandbox.setup`; the
|
|
56
|
+
companion `microsandbox-rb-binaries` gem.
|
|
57
|
+
- **Registries** — `registry_auth`/`registry_insecure`/`registry_ca_certs` on
|
|
58
|
+
`create`.
|
|
59
|
+
- **Errors** — the typed hierarchy in [errors.md](errors.md).
|
|
60
|
+
|
|
61
|
+
Create options span resources (`cpus`/`memory`/`max_cpus`/`max_memory`),
|
|
62
|
+
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, named `scripts`, the image
|
|
63
|
+
`entrypoint`/`cmd` overrides, `init`/`ephemeral`,
|
|
64
|
+
disk-image `fstype`, `ports`/`ports_udp`, network policy + config,
|
|
65
|
+
`log_level`/`quiet_logs`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
|
|
66
|
+
(bind/named/tmpfs/disk with mount policies, `uid`/`gid`, `quota_mib`),
|
|
67
|
+
`idle_timeout`/`max_duration`, `from_snapshot`, `detached`, and
|
|
68
|
+
`replace`/`replace_with_timeout`.
|
|
69
|
+
|
|
70
|
+
## Not yet exposed
|
|
71
|
+
|
|
72
|
+
A few secondary upstream knobs are not yet exposed: per-published-port host
|
|
73
|
+
bind address (ports always bind loopback), network interface overrides, and
|
|
74
|
+
inline named-volume create-mode (pre-create the volume with `Volume.create`,
|
|
75
|
+
then mount it with `{ named: "…" }`).
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -6,8 +6,9 @@ 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
|
|
10
|
-
|
|
9
|
+
# The core-crate dependencies below stay on their own tag, which is
|
|
10
|
+
# Microsandbox::RUNTIME_VERSION and moves independently of this version.
|
|
11
|
+
version = "0.17.2"
|
|
11
12
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
13
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
14
|
license = "Apache-2.0"
|
|
@@ -22,9 +23,12 @@ name = "microsandbox_rb"
|
|
|
22
23
|
crate-type = ["cdylib"]
|
|
23
24
|
|
|
24
25
|
[dependencies]
|
|
25
|
-
magnus
|
|
26
|
-
#
|
|
27
|
-
|
|
26
|
+
# "rb-sys" exposes `magnus::rb_sys::protect` (rb_protect), which the GVL bridge
|
|
27
|
+
# uses to check interrupts without letting Ruby longjmp over Rust frames.
|
|
28
|
+
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
29
|
+
# Low-level Ruby C API — used directly for rb_thread_call_without_gvl2 (GVL
|
|
30
|
+
# release during blocking sandbox calls) and rb_thread_check_ints. Kept in
|
|
31
|
+
# lock-step with magnus's own rb-sys.
|
|
28
32
|
rb-sys = "0.9"
|
|
29
33
|
|
|
30
34
|
# The microsandbox core runtime, wrapped via FFI exactly like the official
|
|
@@ -41,8 +41,8 @@ impl AgentClient {
|
|
|
41
41
|
/// Connect to a running sandbox by name. `timeout` is optional seconds.
|
|
42
42
|
fn connect_sandbox(name: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
|
|
43
43
|
let bridge = match dur(timeout)? {
|
|
44
|
-
Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t))
|
|
45
|
-
None => block_on(AgentBridge::connect_sandbox(&name))
|
|
44
|
+
Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t))?,
|
|
45
|
+
None => block_on(AgentBridge::connect_sandbox(&name))?,
|
|
46
46
|
}
|
|
47
47
|
.map_err(to_ruby_agent)?;
|
|
48
48
|
Ok(AgentClient::from_bridge(bridge))
|
|
@@ -51,8 +51,8 @@ impl AgentClient {
|
|
|
51
51
|
/// Connect to an agentd relay socket by path. `timeout` is optional seconds.
|
|
52
52
|
fn connect_path(path: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
|
|
53
53
|
let bridge = match dur(timeout)? {
|
|
54
|
-
Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t))
|
|
55
|
-
None => block_on(AgentBridge::connect_path(&path))
|
|
54
|
+
Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t))?,
|
|
55
|
+
None => block_on(AgentBridge::connect_path(&path))?,
|
|
56
56
|
}
|
|
57
57
|
.map_err(to_ruby_agent)?;
|
|
58
58
|
Ok(AgentClient::from_bridge(bridge))
|
|
@@ -73,7 +73,7 @@ impl AgentClient {
|
|
|
73
73
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
74
74
|
let inner = Arc::clone(&self.inner);
|
|
75
75
|
let frame =
|
|
76
|
-
block_on(async move { inner.request(flags, body).await })
|
|
76
|
+
block_on(async move { inner.request(flags, body).await })?.map_err(to_ruby_agent)?;
|
|
77
77
|
Ok(frame_to_hash(frame))
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -81,8 +81,8 @@ impl AgentClient {
|
|
|
81
81
|
fn stream_open(&self, flags: u8, body: RString) -> Result<RHash, Error> {
|
|
82
82
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
83
83
|
let inner = Arc::clone(&self.inner);
|
|
84
|
-
let (id, handle) =
|
|
85
|
-
|
|
84
|
+
let (id, handle) = block_on(async move { inner.stream_open(flags, body).await })?
|
|
85
|
+
.map_err(to_ruby_agent)?;
|
|
86
86
|
let hash = ruby().hash_new();
|
|
87
87
|
hash.aset("id", id)?;
|
|
88
88
|
hash.aset("handle", handle)?;
|
|
@@ -93,14 +93,14 @@ impl AgentClient {
|
|
|
93
93
|
fn stream_next(&self, handle: u64) -> Result<Option<RHash>, Error> {
|
|
94
94
|
let inner = Arc::clone(&self.inner);
|
|
95
95
|
let frame =
|
|
96
|
-
block_on(async move { inner.stream_next(handle).await })
|
|
96
|
+
block_on(async move { inner.stream_next(handle).await })?.map_err(to_ruby_agent)?;
|
|
97
97
|
Ok(frame.map(frame_to_hash))
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/// Close a stream handle. Idempotent.
|
|
101
101
|
fn stream_close(&self, handle: u64) -> Result<(), Error> {
|
|
102
102
|
let inner = Arc::clone(&self.inner);
|
|
103
|
-
block_on(async move { inner.stream_close(handle).await })
|
|
103
|
+
block_on(async move { inner.stream_close(handle).await })?;
|
|
104
104
|
Ok(())
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -108,7 +108,7 @@ impl AgentClient {
|
|
|
108
108
|
fn send(&self, id: u32, flags: u8, body: RString) -> Result<(), Error> {
|
|
109
109
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
110
110
|
let inner = Arc::clone(&self.inner);
|
|
111
|
-
block_on(async move { inner.send(id, flags, body).await })
|
|
111
|
+
block_on(async move { inner.send(id, flags, body).await })?.map_err(to_ruby_agent)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/// Cached handshake `core.ready` frame body bytes (CBOR).
|
|
@@ -120,7 +120,7 @@ impl AgentClient {
|
|
|
120
120
|
/// Close the connection. Idempotent.
|
|
121
121
|
fn close(&self) -> Result<(), Error> {
|
|
122
122
|
let inner = Arc::clone(&self.inner);
|
|
123
|
-
block_on(async move { inner.close().await })
|
|
123
|
+
block_on(async move { inner.close().await })?;
|
|
124
124
|
Ok(())
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -157,6 +157,8 @@ fn frame_to_hash(frame: BridgeFrame) -> RHash {
|
|
|
157
157
|
|
|
158
158
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
159
159
|
let class = native.define_class("AgentClient", ruby.class_object())?;
|
|
160
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
161
|
+
crate::runtime::preload_class::<AgentClient>(ruby);
|
|
160
162
|
|
|
161
163
|
class.define_singleton_method(
|
|
162
164
|
"connect_sandbox",
|
|
@@ -43,7 +43,7 @@ impl ExecHandle {
|
|
|
43
43
|
/// Next event as a Hash, or nil when the stream ends.
|
|
44
44
|
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
45
45
|
let inner = Arc::clone(&self.inner);
|
|
46
|
-
let event = block_on(async move { inner.lock().await.recv().await })
|
|
46
|
+
let event = block_on(async move { inner.lock().await.recv().await })?;
|
|
47
47
|
Ok(event.map(exec_event_to_hash))
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -51,7 +51,7 @@ impl ExecHandle {
|
|
|
51
51
|
fn wait(&self) -> Result<RHash, Error> {
|
|
52
52
|
let inner = Arc::clone(&self.inner);
|
|
53
53
|
let status =
|
|
54
|
-
block_on(async move { inner.lock().await.wait().await })
|
|
54
|
+
block_on(async move { inner.lock().await.wait().await })?.map_err(error::to_ruby)?;
|
|
55
55
|
let hash = ruby().hash_new();
|
|
56
56
|
hash.aset("exit_code", status.code)?;
|
|
57
57
|
hash.aset("success", status.success)?;
|
|
@@ -62,20 +62,20 @@ impl ExecHandle {
|
|
|
62
62
|
fn collect(&self) -> Result<RHash, Error> {
|
|
63
63
|
let inner = Arc::clone(&self.inner);
|
|
64
64
|
let output =
|
|
65
|
-
block_on(async move { inner.lock().await.collect().await })
|
|
65
|
+
block_on(async move { inner.lock().await.collect().await })?.map_err(error::to_ruby)?;
|
|
66
66
|
crate::sandbox::exec_output_to_hash(output)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
fn signal(&self, sig: i32) -> Result<(), Error> {
|
|
70
|
-
block_on(self.control.signal(sig))
|
|
70
|
+
block_on(self.control.signal(sig))?.map_err(error::to_ruby)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
fn kill(&self) -> Result<(), Error> {
|
|
74
|
-
block_on(self.control.kill())
|
|
74
|
+
block_on(self.control.kill())?.map_err(error::to_ruby)
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
fn resize(&self, rows: u16, cols: u16) -> Result<(), Error> {
|
|
78
|
-
block_on(self.control.resize(rows, cols))
|
|
78
|
+
block_on(self.control.resize(rows, cols))?.map_err(error::to_ruby)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/// The stdin sink (only the first call returns it; nil afterwards or if
|
|
@@ -95,11 +95,11 @@ impl ExecSink {
|
|
|
95
95
|
fn write(&self, data: RString) -> Result<(), Error> {
|
|
96
96
|
// Copy out while holding the GVL (see sandbox::fs_write for the rationale).
|
|
97
97
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
98
|
-
block_on(self.inner.write(&bytes))
|
|
98
|
+
block_on(self.inner.write(&bytes))?.map_err(error::to_ruby)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
fn close(&self) -> Result<(), Error> {
|
|
102
|
-
block_on(self.inner.close())
|
|
102
|
+
block_on(self.inner.close())?.map_err(error::to_ruby)
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -141,6 +141,8 @@ fn exec_event_to_hash(event: microsandbox::ExecEvent) -> RHash {
|
|
|
141
141
|
|
|
142
142
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
143
143
|
let handle = native.define_class("ExecHandle", ruby.class_object())?;
|
|
144
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
145
|
+
crate::runtime::preload_class::<ExecHandle>(ruby);
|
|
144
146
|
handle.define_method("id", method!(ExecHandle::id, 0))?;
|
|
145
147
|
handle.define_method("recv", method!(ExecHandle::recv, 0))?;
|
|
146
148
|
handle.define_method("wait", method!(ExecHandle::wait, 0))?;
|
|
@@ -151,6 +153,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
151
153
|
handle.define_method("take_stdin", method!(ExecHandle::take_stdin, 0))?;
|
|
152
154
|
|
|
153
155
|
let sink = native.define_class("ExecSink", ruby.class_object())?;
|
|
156
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
157
|
+
crate::runtime::preload_class::<ExecSink>(ruby);
|
|
154
158
|
sink.define_method("write", method!(ExecSink::write, 1))?;
|
|
155
159
|
sink.define_method("close", method!(ExecSink::close, 0))?;
|
|
156
160
|
|