microsandbox-rb 0.17.0 → 0.17.1
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 +136 -1
- data/Cargo.lock +45 -46
- data/DESIGN.md +6 -37
- data/README.md +176 -667
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +101 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +108 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +66 -0
- data/docs/runtime.md +152 -0
- data/docs/surface.md +70 -0
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/lib.rs +1 -0
- data/ext/microsandbox/src/sandbox.rs +61 -31
- data/ext/microsandbox/src/volume.rs +43 -12
- 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 +53 -9
- 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
- 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,66 @@
|
|
|
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 — `Microsandbox::VERSION` and the matching
|
|
11
|
+
`[package] version` in `ext/microsandbox/Cargo.toml` (they must stay equal) —
|
|
12
|
+
on its independent semver track (see the
|
|
13
|
+
[Versioning](../README.md#versioning) section of the README); **don't** pick
|
|
14
|
+
the number to mirror the upstream tag. If the release also adopts a new
|
|
15
|
+
upstream runtime, bump the `tag = "vX.Y.Z"` on **all three** upstream git
|
|
16
|
+
deps (`microsandbox`, `microsandbox-network`, `microsandbox-runtime`), update
|
|
17
|
+
`Microsandbox::RUNTIME_VERSION` to match, commit the release's
|
|
18
|
+
`checksums.sha256` as `binaries/checksums/<tag>.sha256`, and add a row to the
|
|
19
|
+
Versioning table. Also bump `Microsandbox::Binaries::VERSION` (and, on a
|
|
20
|
+
runtime adoption, `::RUNTIME_VERSION`) in
|
|
21
|
+
`binaries/lib/microsandbox/binaries.rb` — the companion gem ships in
|
|
22
|
+
lockstep and the specs assert both. Update `CHANGELOG.md`.
|
|
23
|
+
2. Push a `vX.Y.Z` tag. CI builds the **source gem** and pushes it to RubyGems
|
|
24
|
+
via `rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
|
|
25
|
+
`RUBYGEMS_API_KEY` secret required — then creates the GitHub Release.
|
|
26
|
+
|
|
27
|
+
## Precompiled per-platform gems
|
|
28
|
+
|
|
29
|
+
Precompiled *extension* gems are built best-effort by the `cross-gems` job,
|
|
30
|
+
gated to manual `workflow_dispatch` so you can iterate
|
|
31
|
+
(`gh workflow run release.yml`) without failing tag releases. They are not
|
|
32
|
+
auto-published on tags yet: a gem that *compiles but can't boot a microVM*
|
|
33
|
+
would be served to users ahead of the source gem, and CI can't boot a VM to
|
|
34
|
+
prove otherwise — so promotion is manual after validating the artifact on each
|
|
35
|
+
platform. A precompiled gem ships the compiled extension (with the guest
|
|
36
|
+
`agentd` baked in by *target* arch); the host-side `msb` + `libkrunfw` runtime
|
|
37
|
+
is **not** in it — that comes from the companion `microsandbox-rb-binaries`
|
|
38
|
+
gem, or, when that isn't installed, is fetched into `~/.microsandbox` on first
|
|
39
|
+
use by `Microsandbox.ensure_runtime!` (libkrunfw is `dlopen`'d by `msb` at
|
|
40
|
+
runtime, never linked into the gem). The real cross-compile work is linking the
|
|
41
|
+
*target* native libraries — `libcap-ng` on Linux (handled via Debian multiarch
|
|
42
|
+
in the workflow) and the Hypervisor + Security frameworks on macOS (via
|
|
43
|
+
osxcross; the one platform left to confirm). Until promoted, users install the
|
|
44
|
+
source gem (which compiles via `rb_sys`).
|
|
45
|
+
|
|
46
|
+
## Runtime binaries gems
|
|
47
|
+
|
|
48
|
+
`microsandbox-rb-binaries` (source in `binaries/`) is a *separate* artifact from
|
|
49
|
+
the precompiled extension gems above: it carries no Ruby extension, only the
|
|
50
|
+
upstream `msb` + `libkrunfw` for one platform, verified against the release's
|
|
51
|
+
published `checksums.sha256` when vendored. CI's `binaries` job vendors and
|
|
52
|
+
builds all three (`arm64-darwin`, `x86_64-linux-gnu`, `aarch64-linux-gnu`) on
|
|
53
|
+
every run and smoke-tests the host one; `release.yml`'s `binaries-gems` job
|
|
54
|
+
does the same on a tag and a separate `publish-binaries` job pushes them after
|
|
55
|
+
the SDK gem is live (a companion failure is its own red job and never blocks
|
|
56
|
+
the SDK release). They use their own RubyGems trusted-publisher entry (same
|
|
57
|
+
repo + workflow, gem name `microsandbox-rb-binaries`). To build them by hand:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
rake -C binaries vendor[arm64-darwin] # download + sha256-verify into binaries/vendor
|
|
61
|
+
rake -C binaries build[arm64-darwin] # package into binaries/pkg/*.gem
|
|
62
|
+
rake -C binaries verify # host-only: run the vendored `msb --version`
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`binaries/vendor` and `binaries/pkg` are gitignored — binaries are never
|
|
66
|
+
committed; CI rebuilds them from the upstream release.
|
data/docs/runtime.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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) { Microsandbox::Sandbox.create("box", image: "alpine") { |sb| ... } }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Resolution order when no backend is set programmatically: `MSB_BACKEND`
|
|
92
|
+
(`local`/`cloud`) → `MSB_PROFILE` → the `active_profile` in
|
|
93
|
+
`~/.microsandbox/config.json` (path overridable via `MSB_CONFIG_PATH`) →
|
|
94
|
+
local. **Cloud intent must be explicit** (since runtime `v0.6.9`): a bare
|
|
95
|
+
`MSB_API_KEY` is treated as credential material, not backend intent, and no
|
|
96
|
+
longer selects the cloud on its own — pair it with `MSB_BACKEND=cloud` (which
|
|
97
|
+
reads `MSB_API_URL`/`MSB_API_KEY`), or select a cloud profile. Invalid cloud
|
|
98
|
+
configuration (e.g. `MSB_BACKEND=cloud` without a usable API key or cloud
|
|
99
|
+
profile) fails closed with `Microsandbox::InvalidConfigError` instead of
|
|
100
|
+
silently running locally. The cloud backend currently supports a subset of
|
|
101
|
+
operations (create/start/stop/remove/get/list, one-shot exec, follow log
|
|
102
|
+
streaming); unsupported operations raise `Microsandbox::UnsupportedError`
|
|
103
|
+
(with `#operation` and `#hint`).
|
|
104
|
+
|
|
105
|
+
## One runtime line per home
|
|
106
|
+
|
|
107
|
+
The runtime home (`MSB_HOME`, default `~/.microsandbox`) belongs to one
|
|
108
|
+
runtime line at a time. This gem embeds runtime `v0.6.18` and, on first use,
|
|
109
|
+
installs that version of `msb` into the home — replacing a different version
|
|
110
|
+
that is already there, including a newer `msb` installed by the official CLI.
|
|
111
|
+
In the other direction, a `0.7.x` `msb` migrates the catalog to a newer schema
|
|
112
|
+
the first time it opens it, after which every local call from this gem fails
|
|
113
|
+
with `database schema is newer than this msb binary`. The migration is one-way.
|
|
114
|
+
|
|
115
|
+
If the official CLI (or another SDK) on a different runtime line shares the
|
|
116
|
+
machine, give this gem its own home:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
export MSB_HOME="$HOME/.microsandbox-rb"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Known issues at the pinned runtime
|
|
123
|
+
|
|
124
|
+
These are fixed upstream in runtime releases this gem has not adopted yet. See
|
|
125
|
+
[SECURITY.md](../SECURITY.md) for the security advisories.
|
|
126
|
+
|
|
127
|
+
- **macOS hosts: guest `fallocate` can truncate a file on a bind mount or a
|
|
128
|
+
directory-backed named volume.** Preallocating a range that ends inside an
|
|
129
|
+
existing file shrinks the file to the end of that range, and the call
|
|
130
|
+
reports success. Fixed upstream in `v0.7.0`. Until then, keep data that
|
|
131
|
+
guest programs preallocate (databases, download tools) on the guest's OCI
|
|
132
|
+
root disk or a `disk:` volume, not on a host directory.
|
|
133
|
+
- **`modify(secrets:)` adding the first secret** to a sandbox created without
|
|
134
|
+
any does not turn on TLS interception, so the value is not substituted.
|
|
135
|
+
Create the sandbox with at least one secret.
|
|
136
|
+
- **`patches:`** resets the owner and mode of parent directories that already
|
|
137
|
+
exist in the image to `root:root 0755`. Write after boot with `sb.fs`, or
|
|
138
|
+
restore ownership yourself.
|
|
139
|
+
|
|
140
|
+
## Environment variables
|
|
141
|
+
|
|
142
|
+
| Variable | Effect |
|
|
143
|
+
|----------|--------|
|
|
144
|
+
| `MSB_PATH` | Path to the `msb` runtime binary; outranks every other source, including the binaries gem |
|
|
145
|
+
| `MSB_LIBKRUNFW_PATH` | Path to the `libkrunfw` shared library; outranks `Microsandbox.libkrunfw_path=` |
|
|
146
|
+
| `MSB_HOME` | Runtime home: the catalog, image cache, sandboxes, volumes, snapshots, and the first-use runtime install. Default `~/.microsandbox` |
|
|
147
|
+
| `MSB_BACKEND` | `local` or `cloud`; cloud additionally reads `MSB_API_URL` / `MSB_API_KEY` |
|
|
148
|
+
| `MSB_PROFILE` | Select a named profile from the config file |
|
|
149
|
+
| `MSB_CONFIG_PATH` | Override the `~/.microsandbox/config.json` location |
|
|
150
|
+
| `MICROSANDBOX_NO_AUTO_INSTALL` | Disable the first-use runtime download |
|
|
151
|
+
| `MICROSANDBOX_INTEGRATION` | Set to `1` to run the integration specs (development only) |
|
|
152
|
+
| `MICROSANDBOX_TEST_IMAGE` | Override the integration-spec image (development only) |
|
data/docs/surface.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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:`, `policy:`, CPU/memory
|
|
27
|
+
resize, root-disk growth, env/labels/workdir, secret rotation.
|
|
28
|
+
- **OCI image cache** — `Image.get`/`list`/`inspect`/`remove`/`prune`, image
|
|
29
|
+
archives (`Image.load`/`Image.save`), streaming pull progress
|
|
30
|
+
(`Sandbox.create_with_progress` → `PullSession`).
|
|
31
|
+
- **Named volumes** — `Volume.create`/`get`/`list`/`remove`/`get_default`, plus
|
|
32
|
+
host-side `Volume.fs`/`VolumeInfo#fs` read/write.
|
|
33
|
+
- **Snapshots** — create/open/list/list_dir/reindex/verify/save/load, boot from
|
|
34
|
+
snapshot, opt-in `record_integrity:`.
|
|
35
|
+
- **Rootfs patches** — `Microsandbox::Patch` (text/file/append/copy_file/
|
|
36
|
+
copy_dir/symlink/mkdir/remove) via `create(patches:)`.
|
|
37
|
+
- **Root disk** — structured `root_disk:` (managed / tmpfs / disk / flat via
|
|
38
|
+
`Microsandbox::RootDisk`), `follow_root_symlinks:`.
|
|
39
|
+
- **Network configuration** — composable profiles, custom per-rule
|
|
40
|
+
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, DNS, TLS interception,
|
|
41
|
+
IPv4/IPv6 pools, `max_connections`, `trust_host_cas`, `strict` hostname mode,
|
|
42
|
+
outbound SOCKS4/SOCKS5 `proxy:` (`OutboundProxy`/`SecretSource`),
|
|
43
|
+
`rate_limiter:`, `vsock:`.
|
|
44
|
+
- **Secrets** — multi-host / wildcard allow-lists, injection toggles,
|
|
45
|
+
per-secret and sandbox-level violation policy.
|
|
46
|
+
- **SSH** — `Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`, with
|
|
47
|
+
`inactivity_timeout:`.
|
|
48
|
+
- **Raw agent client** — `Microsandbox::AgentClient` → `AgentStream`/
|
|
49
|
+
`AgentFrame`.
|
|
50
|
+
- **Runtime provisioning** — `install`/`installed?`/`ensure_runtime!`/
|
|
51
|
+
`runtime_path`/`libkrunfw_path`, customizable via `Microsandbox.setup`; the
|
|
52
|
+
companion `microsandbox-rb-binaries` gem.
|
|
53
|
+
- **Registries** — `registry_auth`/`registry_insecure`/`registry_ca_certs` on
|
|
54
|
+
`create`.
|
|
55
|
+
- **Errors** — the typed hierarchy in [errors.md](errors.md).
|
|
56
|
+
|
|
57
|
+
Create options span resources (`cpus`/`memory`/`max_cpus`/`max_memory`),
|
|
58
|
+
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, `init`/`ephemeral`,
|
|
59
|
+
disk-image `fstype`, `ports`/`ports_udp`, network policy + config,
|
|
60
|
+
`log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
|
|
61
|
+
(bind/named/tmpfs/disk with mount policies, `uid`/`gid`, `quota_mib`),
|
|
62
|
+
`idle_timeout`/`max_duration`, `from_snapshot`, `detached`, and
|
|
63
|
+
`replace`/`replace_with_timeout`.
|
|
64
|
+
|
|
65
|
+
## Not yet exposed
|
|
66
|
+
|
|
67
|
+
A few secondary upstream knobs are not yet exposed: per-published-port host
|
|
68
|
+
bind address (ports always bind loopback), network interface overrides, and
|
|
69
|
+
inline named-volume create-mode (pre-create the volume with `Volume.create`,
|
|
70
|
+
then mount it with `{ named: "…" }`).
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -7,7 +7,7 @@ description = "Ruby SDK native extension for microsandbox — secure, fast micro
|
|
|
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
9
|
# The core-crate dependency below stays pinned at its own tag (v0.6.16).
|
|
10
|
-
version = "0.17.
|
|
10
|
+
version = "0.17.1"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//! Pin caller-supplied host paths when a sandbox is created (GHSA-mcw9-9rf6-p4rm).
|
|
2
|
+
//!
|
|
3
|
+
//! The pinned core stores a relative host path exactly as written and resolves
|
|
4
|
+
//! it against the working directory of whichever process later starts the
|
|
5
|
+
//! sandbox, so restarting from another directory mounts a different host
|
|
6
|
+
//! directory than the one the caller meant. Upstream fixed that in v0.7.6 by
|
|
7
|
+
//! resolving host paths once, at the local-backend boundary, before the
|
|
8
|
+
//! configuration is persisted. This module applies the same resolution in the
|
|
9
|
+
//! binding so it also holds on the runtime this gem pins.
|
|
10
|
+
//!
|
|
11
|
+
//! Rules, matching upstream's `resolve_host_paths`:
|
|
12
|
+
//!
|
|
13
|
+
//! - Local backend only: on a cloud backend the paths belong to the remote
|
|
14
|
+
//! host and are forwarded untouched.
|
|
15
|
+
//! - `std::path::absolute`, never `canonicalize`: anchoring must not follow
|
|
16
|
+
//! symlinks (that would defeat the default refusal to follow a symlinked
|
|
17
|
+
//! bind-mount root) and must not require the target to exist.
|
|
18
|
+
//! - Only create-time input is resolved. A sandbox that was already persisted
|
|
19
|
+
//! with a relative path keeps it; nothing here rewrites saved state.
|
|
20
|
+
//!
|
|
21
|
+
//! A vsock `host_socket` is deliberately not routed through here: the core
|
|
22
|
+
//! refuses a relative one outright ("vsock host path must be absolute") before
|
|
23
|
+
//! anything is persisted, so there is no stored relative path to pin.
|
|
24
|
+
|
|
25
|
+
use std::path::PathBuf;
|
|
26
|
+
|
|
27
|
+
use magnus::Error;
|
|
28
|
+
use microsandbox::sandbox::{IntoImage, RootfsSource};
|
|
29
|
+
use microsandbox::{BackendKind, MicrosandboxError};
|
|
30
|
+
|
|
31
|
+
use crate::error;
|
|
32
|
+
|
|
33
|
+
/// How the `image:` text should be handed to the builder.
|
|
34
|
+
pub enum Image {
|
|
35
|
+
/// An OCI reference, an already-absolute path, or text the core rejects —
|
|
36
|
+
/// passed through unchanged so the core classifies (or reports) it.
|
|
37
|
+
Text(String),
|
|
38
|
+
/// A relative local rootfs path, anchored to the current directory.
|
|
39
|
+
Path(PathBuf),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Resolves host paths for one create call.
|
|
43
|
+
#[derive(Clone, Copy)]
|
|
44
|
+
pub struct HostPaths {
|
|
45
|
+
local: bool,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl HostPaths {
|
|
49
|
+
/// Capture which backend the create call is about to dispatch to.
|
|
50
|
+
pub fn for_default_backend() -> Self {
|
|
51
|
+
Self {
|
|
52
|
+
local: matches!(microsandbox::default_backend().kind(), BackendKind::Local),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Anchor a relative host path to the current directory. Absolute paths
|
|
57
|
+
/// and the empty string (left for the core to reject) pass through.
|
|
58
|
+
pub fn pin(self, raw: String, label: &str) -> Result<PathBuf, Error> {
|
|
59
|
+
let path = PathBuf::from(raw);
|
|
60
|
+
if !self.local || path.as_os_str().is_empty() || path.is_absolute() {
|
|
61
|
+
return Ok(path);
|
|
62
|
+
}
|
|
63
|
+
std::path::absolute(&path).map_err(|e| {
|
|
64
|
+
error::to_ruby(MicrosandboxError::InvalidConfig(format!(
|
|
65
|
+
"cannot resolve {label} {}: {e}",
|
|
66
|
+
path.display()
|
|
67
|
+
)))
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Classify `image:` text the way the core does and pin it when it names a
|
|
72
|
+
/// relative local path (a directory rootfs or a disk image).
|
|
73
|
+
pub fn pin_image(self, text: String) -> Result<Image, Error> {
|
|
74
|
+
if !self.local {
|
|
75
|
+
return Ok(Image::Text(text));
|
|
76
|
+
}
|
|
77
|
+
let relative_local = matches!(
|
|
78
|
+
text.as_str().into_rootfs_source(),
|
|
79
|
+
Ok(RootfsSource::Bind { ref path, .. } | RootfsSource::DiskImage { ref path, .. })
|
|
80
|
+
if path.is_relative()
|
|
81
|
+
);
|
|
82
|
+
if relative_local {
|
|
83
|
+
self.pin(text, "rootfs path").map(Image::Path)
|
|
84
|
+
} else {
|
|
85
|
+
Ok(Image::Text(text))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Pin a `from_snapshot:` value when it is a path rather than a snapshot
|
|
90
|
+
/// name or digest. Uses the core's own name-vs-path split: a value that
|
|
91
|
+
/// contains `/` or starts with `.` or `~` is a path. (The core does not
|
|
92
|
+
/// expand `~`; like any other relative path it is anchored as written.)
|
|
93
|
+
pub fn pin_snapshot(self, raw: String) -> Result<String, Error> {
|
|
94
|
+
let path_like = raw.contains('/') || raw.starts_with('.') || raw.starts_with('~');
|
|
95
|
+
if !self.local || !path_like {
|
|
96
|
+
return Ok(raw);
|
|
97
|
+
}
|
|
98
|
+
self.pin(raw, "snapshot path")?
|
|
99
|
+
.into_os_string()
|
|
100
|
+
.into_string()
|
|
101
|
+
.map_err(|path| {
|
|
102
|
+
error::to_ruby(MicrosandboxError::InvalidConfig(format!(
|
|
103
|
+
"cannot resolve snapshot path {}: the current directory is not valid UTF-8",
|
|
104
|
+
PathBuf::from(path).display()
|
|
105
|
+
)))
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|