microsandbox-rb 0.15.0 → 0.17.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 +197 -0
- data/Cargo.lock +219 -171
- data/DESIGN.md +1 -1
- data/README.md +83 -4
- data/ext/microsandbox/Cargo.toml +5 -5
- data/ext/microsandbox/src/error.rs +12 -0
- data/ext/microsandbox/src/sandbox.rs +264 -16
- data/lib/microsandbox/errors.rb +7 -0
- data/lib/microsandbox/outbound_proxy.rb +332 -0
- data/lib/microsandbox/sandbox.rb +316 -3
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox.rb +1 -0
- data/sig/microsandbox.rbs +56 -1
- metadata +2 -1
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -20,6 +20,14 @@ module Microsandbox
|
|
|
20
20
|
# @return [String]
|
|
21
21
|
def name = @native.name
|
|
22
22
|
|
|
23
|
+
# The opaque, backend-assigned identity of the persisted sandbox this handle
|
|
24
|
+
# is bound to (runtime v0.6.16). Unlike {#name} — a reusable label — it is
|
|
25
|
+
# stable for the sandbox's lifetime and changes once the same name is
|
|
26
|
+
# removed and recreated; the convergent operations below compare against it
|
|
27
|
+
# before acting.
|
|
28
|
+
# @return [String]
|
|
29
|
+
def id = @native.id
|
|
30
|
+
|
|
23
31
|
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
24
32
|
# :stopped, or :crashed (a snapshot, captured when this handle was fetched)
|
|
25
33
|
def status = @native.status.to_sym
|
|
@@ -95,6 +103,68 @@ module Microsandbox
|
|
|
95
103
|
nil
|
|
96
104
|
end
|
|
97
105
|
|
|
106
|
+
# Converge on a live sandbox for this handle (runtime v0.6.16): connect
|
|
107
|
+
# when it is already running, wait while it is starting, or start it when it
|
|
108
|
+
# is created/stopped/crashed. A draining or paused sandbox is rejected
|
|
109
|
+
# rather than raced.
|
|
110
|
+
# @param detached [Boolean] applies only when a start is actually required
|
|
111
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
112
|
+
# @return [Sandbox] the live sandbox
|
|
113
|
+
def connect_or_start(detached: false)
|
|
114
|
+
# May boot a microVM (the start arm), so it needs a provisioned runtime
|
|
115
|
+
# just like {Sandbox.create}/{Sandbox.start}. Memoized after the first
|
|
116
|
+
# call, so the extra check costs nothing on the connect arm.
|
|
117
|
+
Microsandbox.ensure_runtime!
|
|
118
|
+
opts = detached ? {"detached" => true} : {}
|
|
119
|
+
Sandbox.new(@native.connect_or_start(opts))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Block until *this exact* sandbox reaches +status+ (runtime v0.6.16).
|
|
123
|
+
#
|
|
124
|
+
# The wait is uninterruptible from Ruby: the native call blocks with the GVL
|
|
125
|
+
# released and no unblock function, so Ctrl-C (and any other signal) only
|
|
126
|
+
# lands once it returns. There is no built-in timeout either — the core
|
|
127
|
+
# polls every 100ms forever, by design, leaving the deadline to the caller.
|
|
128
|
+
# So wait only for a state this *backend* can actually reach (`:created` and
|
|
129
|
+
# `:paused` are cloud-side states, unreachable locally), and if you need a
|
|
130
|
+
# deadline, run this on a Thread of its own and enforce the timeout there
|
|
131
|
+
# (`Timeout.timeout` around it would not fire).
|
|
132
|
+
# @param status [Symbol, String] :created, :starting, :running, :draining,
|
|
133
|
+
# :paused, :stopped, or :crashed
|
|
134
|
+
# @raise [ArgumentError] on an unknown status name
|
|
135
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
136
|
+
# @return [SandboxHandle] a fresh handle observed in that state
|
|
137
|
+
def wait_for_status(status)
|
|
138
|
+
SandboxHandle.new(@native.wait_for_status(Sandbox.send(:coerce_status, status)))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Stop and start *this exact* sandbox, returning the new live sandbox
|
|
142
|
+
# (runtime v0.6.16). A created/stopped/crashed sandbox is started directly.
|
|
143
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
144
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
145
|
+
# (the core's default is 10)
|
|
146
|
+
# @param detached [Boolean] start the replacement detached
|
|
147
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
148
|
+
# @return [Sandbox]
|
|
149
|
+
def restart(force: false, timeout: nil, detached: false)
|
|
150
|
+
Microsandbox.ensure_runtime!
|
|
151
|
+
opts = Sandbox.send(:build_restart_opts, force: force, timeout: timeout, detached: detached)
|
|
152
|
+
Sandbox.new(@native.restart(opts))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Stop and remove *this exact* sandbox (runtime v0.6.16) — {#stop} plus
|
|
156
|
+
# {Sandbox.remove} in one convergent step, with an identity check that
|
|
157
|
+
# refuses to remove a same-name replacement.
|
|
158
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
159
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
160
|
+
# (the core's default is 10)
|
|
161
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
162
|
+
# @return [nil]
|
|
163
|
+
def destroy(force: false, timeout: nil)
|
|
164
|
+
@native.destroy(Sandbox.send(:build_destroy_opts, force: force, timeout: timeout))
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
98
168
|
# Block until the sandbox is observed in a terminal (non-running) state.
|
|
99
169
|
# @return [SandboxStopResult]
|
|
100
170
|
def wait_until_stopped
|
|
@@ -265,6 +335,11 @@ module Microsandbox
|
|
|
265
335
|
# to gate the `fstype:`-vs-OCI check; keep in sync on a runtime-tag bump.
|
|
266
336
|
DISK_IMAGE_EXTENSIONS = %w[raw qcow2 vmdk].freeze
|
|
267
337
|
|
|
338
|
+
# The seven lifecycle states a sandbox can be observed in — the spelling
|
|
339
|
+
# {Sandbox#status} / {SandboxHandle#status} return, and the accepted
|
|
340
|
+
# arguments of {Sandbox#wait_for_status} / {SandboxHandle#wait_for_status}.
|
|
341
|
+
STATUSES = %i[created starting running draining paused stopped crashed].freeze
|
|
342
|
+
|
|
268
343
|
class << self
|
|
269
344
|
# Create and boot a sandbox.
|
|
270
345
|
#
|
|
@@ -309,7 +384,11 @@ module Microsandbox
|
|
|
309
384
|
# (4 GiB as of `v0.5.10`); the core rejects it on tmpfs/disk/named (for a
|
|
310
385
|
# named volume, set its quota via {Volume.create}). Bind/named mounts
|
|
311
386
|
# also accept `follow_root_symlinks: true` to opt out of the default-on
|
|
312
|
-
# mount-root symlink protection (runtime v0.6.7)
|
|
387
|
+
# mount-root symlink protection (runtime v0.6.7), and `uid:`/`gid:`
|
|
388
|
+
# (runtime v0.6.15) to pin the guest owner presented for host files that
|
|
389
|
+
# carry no per-file stat override — both must be given together, each an
|
|
390
|
+
# Integer in 0..4294967295, and they conflict with
|
|
391
|
+
# `stat_virtualization: :off`.
|
|
313
392
|
# @param network [Array, String, Symbol, NetworkPolicy, Hash, nil] network
|
|
314
393
|
# policy. Composable profiles (`[:public]` (the default), `[:public,
|
|
315
394
|
# :private]`, `:host`, …), a terminal preset (:none, :allow_all), a
|
|
@@ -329,12 +408,28 @@ module Microsandbox
|
|
|
329
408
|
# @param ipv6_pool [String, nil] guest IPv6 address pool CIDR
|
|
330
409
|
# @param max_connections [Integer, nil] cap on concurrent proxied connections
|
|
331
410
|
# @param trust_host_cas [Boolean, nil] trust the host's CA bundle for upstream TLS
|
|
411
|
+
# @param strict [Boolean, nil] strict hostname-policy mode (runtime
|
|
412
|
+
# v0.6.18, default false): a hostname-rule allow must be backed by an
|
|
413
|
+
# inspectable request authority — plain-HTTP `Host`, or SNI/authority
|
|
414
|
+
# under TLS interception. Without that visibility (e.g. bypassed or
|
|
415
|
+
# non-intercepted HTTPS) a flow allowed only by a hostname rule is
|
|
416
|
+
# denied before the upstream dial. Create-only; `modify` cannot change it.
|
|
332
417
|
# @param rate_limiter [Hash, nil] per-sandbox egress/ingress token-bucket
|
|
333
418
|
# limits (runtime v0.6.9, local backend):
|
|
334
419
|
# `{ egress: { bandwidth: { size: 1_048_576, refill_time_ms: 1000,
|
|
335
420
|
# one_time_burst: 0 }, ops: { size: 1000, refill_time_ms: 1000 } },
|
|
336
421
|
# ingress: { ... } }`. `bandwidth` buckets meter bytes, `ops` buckets
|
|
337
422
|
# meter network frames; an omitted bucket or direction is unlimited.
|
|
423
|
+
# @param proxy [OutboundProxy, Hash, nil] outbound SOCKS proxy for the
|
|
424
|
+
# sandbox's egress traffic (runtime v0.6.17, local backend):
|
|
425
|
+
# {OutboundProxy.socks4}(address, user_id:) or {OutboundProxy.socks5}
|
|
426
|
+
# (address), optionally `.credentials(username, SecretSource.env("VAR"))`
|
|
427
|
+
# — or the equivalent Hash `{ protocol: :socks5, address: "IP:port",
|
|
428
|
+
# credentials: { username:, password: { env: "VAR" } } }`. The address is
|
|
429
|
+
# dialed from the host; only the password's env var *name* is sent. The
|
|
430
|
+
# cloud backend rejects it with {UnsupportedError}. An unparseable
|
|
431
|
+
# address is rejected by the core at create time with
|
|
432
|
+
# {InvalidConfigError}, before any boot.
|
|
338
433
|
# @param vsock [Hash, Array, nil] host sockets exposed on guest-to-host
|
|
339
434
|
# vsock ports (runtime v0.6.9): `{ "/host/api.sock" => 5000 }` (stream
|
|
340
435
|
# sockets), or an Array of
|
|
@@ -414,6 +509,62 @@ module Microsandbox
|
|
|
414
509
|
end
|
|
415
510
|
end
|
|
416
511
|
|
|
512
|
+
# Converge on a live sandbox called +name+ (runtime v0.6.16, upstream
|
|
513
|
+
# #1462): connect to the persisted sandbox with that name — starting it
|
|
514
|
+
# when it is created/stopped/crashed, waiting when it is already starting
|
|
515
|
+
# — or create it when none exists. Concurrent callers converge on the
|
|
516
|
+
# winning identity instead of one of them losing to a name clash.
|
|
517
|
+
#
|
|
518
|
+
# The keyword options are exactly {create}'s and are used **only when a
|
|
519
|
+
# create actually happens**: an existing sandbox keeps its persisted
|
|
520
|
+
# configuration, so passing `memory:` here will not resize one that is
|
|
521
|
+
# already there. `replace:`/`replace_with_timeout:` are accepted for
|
|
522
|
+
# kwargs parity with {create} but are rejected by the core — replacing a
|
|
523
|
+
# sandbox is the opposite of converging on it — and raise
|
|
524
|
+
# {InvalidConfigError}.
|
|
525
|
+
#
|
|
526
|
+
# The block form yields the sandbox and then stops it **only when this
|
|
527
|
+
# call owns its lifecycle** ({Sandbox#owns_lifecycle?}). That is a
|
|
528
|
+
# deliberate divergence from {create}, whose block form always stops:
|
|
529
|
+
# `connect_or_create` may hand back a sandbox this process did not start,
|
|
530
|
+
# and tearing down someone else's long-lived service on the way out of a
|
|
531
|
+
# block is never what the caller meant. Concretely:
|
|
532
|
+
#
|
|
533
|
+
# - created here (the common case) — yielded, then stopped, exactly like
|
|
534
|
+
# {create};
|
|
535
|
+
# - *connected* to a sandbox that was already running — left running;
|
|
536
|
+
# - created here with `detached: true` — left running, which is what
|
|
537
|
+
# `detached:` asks for (this is where the divergence from {create} bites:
|
|
538
|
+
# `create(..., detached: true) { }` still stops on block exit).
|
|
539
|
+
#
|
|
540
|
+
# Either way, stop it yourself with {Sandbox#stop}/{Sandbox#destroy} when
|
|
541
|
+
# you do want it gone.
|
|
542
|
+
#
|
|
543
|
+
# The teardown is safe against name reuse from either side: it skips the
|
|
544
|
+
# stop entirely for a sandbox this call did not start, and the stop it does
|
|
545
|
+
# issue is scoped to this sandbox's {Sandbox#id} (runtime v0.6.16), so a
|
|
546
|
+
# name removed and recreated while the block ran raises inside the ensure
|
|
547
|
+
# — swallowed as a best-effort teardown failure — rather than taking the
|
|
548
|
+
# replacement down.
|
|
549
|
+
# @param name [String]
|
|
550
|
+
# @yieldparam sandbox [Sandbox]
|
|
551
|
+
# @return [Sandbox, Object] the live sandbox, or the block's return value
|
|
552
|
+
def connect_or_create(name, **kwargs, &block)
|
|
553
|
+
opts = build_create_opts(**kwargs)
|
|
554
|
+
sandbox = new(Native::Sandbox.connect_or_create(name.to_s, opts))
|
|
555
|
+
return sandbox unless block_given?
|
|
556
|
+
|
|
557
|
+
begin
|
|
558
|
+
yield sandbox
|
|
559
|
+
ensure
|
|
560
|
+
begin
|
|
561
|
+
sandbox.stop if sandbox.owns_lifecycle?
|
|
562
|
+
rescue Microsandbox::Error
|
|
563
|
+
# best-effort cleanup; ignore stop failures during teardown
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
417
568
|
# Create a sandbox while streaming image-pull progress. Accepts the same
|
|
418
569
|
# options as {create}; returns a {PullSession} — iterate it (an
|
|
419
570
|
# {Enumerable} of progress-event Hashes, each with a "kind"), then call
|
|
@@ -441,7 +592,8 @@ module Microsandbox
|
|
|
441
592
|
shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
|
|
442
593
|
entrypoint: nil, cmd: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
443
594
|
dns: nil, tls: nil, ipv4_pool: nil, ipv6_pool: nil,
|
|
444
|
-
max_connections: nil, trust_host_cas: nil, rate_limiter: nil,
|
|
595
|
+
max_connections: nil, trust_host_cas: nil, strict: nil, rate_limiter: nil, proxy: nil,
|
|
596
|
+
vsock: nil,
|
|
445
597
|
patches: nil,
|
|
446
598
|
from_snapshot: nil, fstype: nil, init: nil, ephemeral: false,
|
|
447
599
|
log_level: nil, quiet_logs: false, security: nil,
|
|
@@ -502,7 +654,9 @@ module Microsandbox
|
|
|
502
654
|
opts["ipv6_pool"] = ipv6_pool.to_s if ipv6_pool
|
|
503
655
|
opts["max_connections"] = Integer(max_connections) if max_connections
|
|
504
656
|
set_bool(opts, "trust_host_cas", trust_host_cas)
|
|
657
|
+
set_bool(opts, "strict", strict)
|
|
505
658
|
opts["rate_limiter"] = normalize_rate_limiter(rate_limiter) if rate_limiter
|
|
659
|
+
opts["proxy"] = OutboundProxy.coerce(proxy) unless proxy.nil?
|
|
506
660
|
opts["vsock"] = normalize_vsock(vsock) if vsock
|
|
507
661
|
opts["log_level"] = log_level.to_s if log_level
|
|
508
662
|
opts["quiet_logs"] = true if quiet_logs
|
|
@@ -590,6 +744,40 @@ module Microsandbox
|
|
|
590
744
|
seconds
|
|
591
745
|
end
|
|
592
746
|
|
|
747
|
+
# Validate a `wait_for_status` target and lower it to the wire name.
|
|
748
|
+
# Accepts a Symbol or String in the {STATUSES} spelling; anything else is
|
|
749
|
+
# an ArgumentError (an unreachable typo would otherwise block forever —
|
|
750
|
+
# `wait_for_status` has no built-in timeout).
|
|
751
|
+
def coerce_status(status)
|
|
752
|
+
name = status.to_s
|
|
753
|
+
unless STATUSES.include?(name.to_sym)
|
|
754
|
+
raise ArgumentError,
|
|
755
|
+
"unknown sandbox status #{status.inspect} " \
|
|
756
|
+
"(expected one of: #{STATUSES.join(", ")})"
|
|
757
|
+
end
|
|
758
|
+
name
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Shared option builder for `restart`, on both {Sandbox} and
|
|
762
|
+
# {SandboxHandle}. An omitted `timeout:` keeps the core's ten-second
|
|
763
|
+
# graceful-shutdown default.
|
|
764
|
+
def build_restart_opts(force:, timeout:, detached:)
|
|
765
|
+
opts = {}
|
|
766
|
+
opts["force"] = true if force
|
|
767
|
+
opts["detached"] = true if detached
|
|
768
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
769
|
+
opts
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Shared option builder for `destroy`, on both {Sandbox} and
|
|
773
|
+
# {SandboxHandle}.
|
|
774
|
+
def build_destroy_opts(force:, timeout:)
|
|
775
|
+
opts = {}
|
|
776
|
+
opts["force"] = true if force
|
|
777
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
778
|
+
opts
|
|
779
|
+
end
|
|
780
|
+
|
|
593
781
|
def stringify(hash)
|
|
594
782
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
595
783
|
end
|
|
@@ -987,7 +1175,8 @@ module Microsandbox
|
|
|
987
1175
|
# { tmpfs: true, size_mib: 64 } # memory-backed
|
|
988
1176
|
# { disk: "/img.raw", format: "raw", fstype: "ext4" } # disk-image mount
|
|
989
1177
|
# Any mount may also carry stat_virtualization: (:strict/:relaxed/:off) and
|
|
990
|
-
# host_permissions: (:private/:mirror); a bind mount may carry
|
|
1178
|
+
# host_permissions: (:private/:mirror); a bind/named mount may carry
|
|
1179
|
+
# uid:/gid: (the fallback guest owner); a bind mount may carry quota_mib:.
|
|
991
1180
|
# Coerce the `root_disk:` argument — an Integer (managed size in MiB), a
|
|
992
1181
|
# {RootDisk} factory Hash, or an equivalent hand-written Hash — into the
|
|
993
1182
|
# wire shape, validating kind/field combinations up front (mirrors the
|
|
@@ -1115,6 +1304,8 @@ module Microsandbox
|
|
|
1115
1304
|
# `follow_root_symlinks:` opts out of the default-on mount-root symlink
|
|
1116
1305
|
# protection (runtime v0.6.7; bind/named only — the core silently ignores
|
|
1117
1306
|
# it on tmpfs/disk mounts, so reject those here instead).
|
|
1307
|
+
# `uid:`/`gid:` pin the fallback guest owner (runtime v0.6.15) — see
|
|
1308
|
+
# {apply_mount_owner}.
|
|
1118
1309
|
def apply_mount_flags(mount, spec)
|
|
1119
1310
|
mount["readonly"] = true if spec[:ro] || spec["ro"] || spec[:readonly] || spec["readonly"]
|
|
1120
1311
|
mount["noexec"] = true if spec[:noexec] || spec["noexec"]
|
|
@@ -1134,6 +1325,58 @@ module Microsandbox
|
|
|
1134
1325
|
end
|
|
1135
1326
|
mount["follow_root_symlinks"] = !!follow
|
|
1136
1327
|
end
|
|
1328
|
+
apply_mount_owner(mount, spec)
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
# Apply a volume spec Hash's fallback mount ownership (runtime v0.6.15,
|
|
1332
|
+
# upstream #1451). Host files carrying no per-file stat override surface in
|
|
1333
|
+
# the guest as `uid:`/`gid:` instead of the runtime's fallback owner; the
|
|
1334
|
+
# pair travels on the wire as `override_uid`/`override_gid` (the core's
|
|
1335
|
+
# `MountBuilder#owner`), mirroring the Python SDK's concise `uid:`/`gid:`
|
|
1336
|
+
# public names over the same wire fields.
|
|
1337
|
+
#
|
|
1338
|
+
# Validation mirrors the Python SDK exactly: the two must be given
|
|
1339
|
+
# together, each must be a plain Integer in the u32 range, they need stat
|
|
1340
|
+
# virtualization (so `stat_virtualization: :off` conflicts), and they only
|
|
1341
|
+
# apply to bind/named mounts. The one Python rule with no Ruby counterpart
|
|
1342
|
+
# is "not supported for disk-backed named volumes": Python knows the
|
|
1343
|
+
# volume kind because its `Volume.named` carries it, while a Ruby
|
|
1344
|
+
# `{ named: "vol" }` spec only references an existing volume by name — the
|
|
1345
|
+
# core rejects that combination at create() instead.
|
|
1346
|
+
def apply_mount_owner(mount, spec)
|
|
1347
|
+
uid = spec.fetch(:uid, spec["uid"])
|
|
1348
|
+
gid = spec.fetch(:gid, spec["gid"])
|
|
1349
|
+
return if uid.nil? && gid.nil?
|
|
1350
|
+
|
|
1351
|
+
if uid.nil? || gid.nil?
|
|
1352
|
+
raise ArgumentError, "mount uid: and gid: must be set together"
|
|
1353
|
+
end
|
|
1354
|
+
unless %w[bind named].include?(mount["kind"])
|
|
1355
|
+
raise ArgumentError,
|
|
1356
|
+
"uid:/gid: (mount owner) only applies to bind/named mounts " \
|
|
1357
|
+
"(got a #{mount["kind"]} mount), like stat_virtualization:/host_permissions:"
|
|
1358
|
+
end
|
|
1359
|
+
if mount["stat_virtualization"] == "off"
|
|
1360
|
+
raise ArgumentError,
|
|
1361
|
+
"uid:/gid: (mount owner) cannot be combined with stat_virtualization: :off — " \
|
|
1362
|
+
"`off` exposes literal host metadata, leaving no overlay to rewrite the owner in"
|
|
1363
|
+
end
|
|
1364
|
+
mount["override_uid"] = coerce_mount_owner_id(uid, "uid:")
|
|
1365
|
+
mount["override_gid"] = coerce_mount_owner_id(gid, "gid:")
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
# Range-check one mount owner ID. Deliberately stricter than the repo's
|
|
1369
|
+
# usual `Integer(value)` coercion: `Integer("1000")` and a truncating
|
|
1370
|
+
# `Integer(1000.7)` would both silently accept input that never named a
|
|
1371
|
+
# real owner. `is_a?(Integer)` also rejects `true`/`false`, matching the
|
|
1372
|
+
# Python SDK's `_mount_owner_id` (`type(value) is not int`, which excludes
|
|
1373
|
+
# `bool`).
|
|
1374
|
+
def coerce_mount_owner_id(value, label)
|
|
1375
|
+
unless value.is_a?(Integer) && value >= 0 && value <= 0xFFFF_FFFF
|
|
1376
|
+
raise ArgumentError,
|
|
1377
|
+
"#{label} must be an Integer between 0 and 4294967295 (got #{value.inspect})"
|
|
1378
|
+
end
|
|
1379
|
+
value
|
|
1137
1380
|
end
|
|
1138
1381
|
|
|
1139
1382
|
# Translate the pre-0.7.0 `options:` array form (e.g. options: %w[ro noexec])
|
|
@@ -1201,6 +1444,16 @@ module Microsandbox
|
|
|
1201
1444
|
@native.name
|
|
1202
1445
|
end
|
|
1203
1446
|
|
|
1447
|
+
# The opaque, backend-assigned identity of the persisted sandbox (runtime
|
|
1448
|
+
# v0.6.16). Unlike {#name} — a reusable label — this is stable for the
|
|
1449
|
+
# sandbox's lifetime and changes once the same name is removed and
|
|
1450
|
+
# recreated, which is exactly what {#wait_for_status}/{#restart}/{#destroy}
|
|
1451
|
+
# check before acting.
|
|
1452
|
+
# @return [String]
|
|
1453
|
+
def id
|
|
1454
|
+
@native.id
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1204
1457
|
# Run a command (no shell interpretation) and collect its output.
|
|
1205
1458
|
#
|
|
1206
1459
|
# @param command [String] the executable
|
|
@@ -1488,6 +1741,13 @@ module Microsandbox
|
|
|
1488
1741
|
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default) and
|
|
1489
1742
|
# wait for it to terminate. For a custom timeout or fire-and-return
|
|
1490
1743
|
# `request_*` control, fetch a {SandboxHandle} via {Sandbox.get}.
|
|
1744
|
+
#
|
|
1745
|
+
# Scoped to *this exact* sandbox: as of runtime v0.6.16 the stop carries this
|
|
1746
|
+
# object's {#id}, so if the name has since been removed and recreated the
|
|
1747
|
+
# call raises {SandboxReplacedError} (or {SandboxNotFoundError} when nothing
|
|
1748
|
+
# holds the name any more) instead of terminating whatever sandbox now
|
|
1749
|
+
# answers to it. Same for {#kill}, {#drain} and {#stop_and_wait}.
|
|
1750
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
1491
1751
|
# @return [nil]
|
|
1492
1752
|
def stop
|
|
1493
1753
|
@native.stop
|
|
@@ -1520,6 +1780,59 @@ module Microsandbox
|
|
|
1520
1780
|
ExitStatus.new(@native.wait)
|
|
1521
1781
|
end
|
|
1522
1782
|
|
|
1783
|
+
# Block until *this exact* sandbox reaches +status+ (runtime v0.6.16). If
|
|
1784
|
+
# the name has since been recreated as a different sandbox, this raises
|
|
1785
|
+
# {SandboxReplacedError} rather than silently redirecting the wait to the
|
|
1786
|
+
# replacement.
|
|
1787
|
+
#
|
|
1788
|
+
# The wait is uninterruptible from Ruby: the native call blocks with the GVL
|
|
1789
|
+
# released and no unblock function, so Ctrl-C (and any other signal) only
|
|
1790
|
+
# lands once it returns. There is no built-in timeout either — the core
|
|
1791
|
+
# polls every 100ms forever, by design, leaving the deadline to the caller.
|
|
1792
|
+
# So wait only for a state this *backend* can actually reach (`:created` and
|
|
1793
|
+
# `:paused` are cloud-side states, unreachable locally), and if you need a
|
|
1794
|
+
# deadline, run this on a Thread of its own and enforce the timeout there
|
|
1795
|
+
# (`Timeout.timeout` around it would not fire).
|
|
1796
|
+
# @param status [Symbol, String] :created, :starting, :running, :draining,
|
|
1797
|
+
# :paused, :stopped, or :crashed
|
|
1798
|
+
# @raise [ArgumentError] on an unknown status name
|
|
1799
|
+
# @return [SandboxHandle] a fresh handle observed in that state (mirrors the
|
|
1800
|
+
# official SDKs, which return a handle from both `Sandbox` and
|
|
1801
|
+
# `SandboxHandle`)
|
|
1802
|
+
def wait_for_status(status)
|
|
1803
|
+
SandboxHandle.new(@native.wait_for_status(self.class.send(:coerce_status, status)))
|
|
1804
|
+
end
|
|
1805
|
+
|
|
1806
|
+
# Stop and start *this exact* sandbox, returning the new live sandbox
|
|
1807
|
+
# (runtime v0.6.16). A created/stopped/crashed sandbox is started directly.
|
|
1808
|
+
# This handle is spent afterwards — use the returned one.
|
|
1809
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
1810
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
1811
|
+
# (the core's default is 10)
|
|
1812
|
+
# @param detached [Boolean] start the replacement detached
|
|
1813
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
1814
|
+
# @return [Sandbox]
|
|
1815
|
+
def restart(force: false, timeout: nil, detached: false)
|
|
1816
|
+
# Boots a microVM again, so it needs a provisioned runtime — the sandbox
|
|
1817
|
+
# this object came from may have been created in a different process.
|
|
1818
|
+
Microsandbox.ensure_runtime!
|
|
1819
|
+
opts = self.class.send(:build_restart_opts, force: force, timeout: timeout, detached: detached)
|
|
1820
|
+
self.class.new(@native.restart(opts))
|
|
1821
|
+
end
|
|
1822
|
+
|
|
1823
|
+
# Stop and remove *this exact* sandbox (runtime v0.6.16) — the convergent
|
|
1824
|
+
# equivalent of {#stop} followed by {Sandbox.remove}, with an identity check
|
|
1825
|
+
# that refuses to remove a same-name replacement.
|
|
1826
|
+
# @param force [Boolean] SIGKILL instead of requesting a graceful shutdown
|
|
1827
|
+
# @param timeout [Numeric, nil] graceful-shutdown seconds before escalating
|
|
1828
|
+
# (the core's default is 10)
|
|
1829
|
+
# @raise [SandboxReplacedError] if the name now refers to a different sandbox
|
|
1830
|
+
# @return [nil]
|
|
1831
|
+
def destroy(force: false, timeout: nil)
|
|
1832
|
+
@native.destroy(self.class.send(:build_destroy_opts, force: force, timeout: timeout))
|
|
1833
|
+
nil
|
|
1834
|
+
end
|
|
1835
|
+
|
|
1523
1836
|
# The live status, fetched from the backend (a round-trip per call).
|
|
1524
1837
|
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
1525
1838
|
# :stopped, or :crashed
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -8,12 +8,12 @@ module Microsandbox
|
|
|
8
8
|
# Versioning section of the README for the full gem-to-runtime map. Must equal
|
|
9
9
|
# the native ext's Cargo crate version (`Native.version`), enforced by
|
|
10
10
|
# spec/unit/version_spec.rb.
|
|
11
|
-
VERSION = "0.
|
|
11
|
+
VERSION = "0.17.0"
|
|
12
12
|
|
|
13
13
|
# The upstream microsandbox runtime release this gem build embeds — the `tag`
|
|
14
14
|
# pinned on the `microsandbox`/`microsandbox-network` git deps in
|
|
15
15
|
# ext/microsandbox/Cargo.toml. Exposed at runtime as
|
|
16
16
|
# {Microsandbox.runtime_version}. spec/unit/version_spec.rb asserts it stays in
|
|
17
17
|
# sync with the Cargo tag so it can't silently drift out of date.
|
|
18
|
-
RUNTIME_VERSION = "v0.6.
|
|
18
|
+
RUNTIME_VERSION = "v0.6.18"
|
|
19
19
|
end
|
data/lib/microsandbox.rb
CHANGED
|
@@ -30,6 +30,7 @@ require_relative "microsandbox/snapshot"
|
|
|
30
30
|
require_relative "microsandbox/patch"
|
|
31
31
|
require_relative "microsandbox/root_disk"
|
|
32
32
|
require_relative "microsandbox/network"
|
|
33
|
+
require_relative "microsandbox/outbound_proxy"
|
|
33
34
|
require_relative "microsandbox/agent"
|
|
34
35
|
require_relative "microsandbox/ssh"
|
|
35
36
|
require_relative "microsandbox/modification"
|
data/sig/microsandbox.rbs
CHANGED
|
@@ -39,6 +39,7 @@ module Microsandbox
|
|
|
39
39
|
class SandboxNotRunningError < Error end
|
|
40
40
|
class SandboxAlreadyExistsError < Error end
|
|
41
41
|
class SandboxStillRunningError < Error end
|
|
42
|
+
class SandboxReplacedError < Error end
|
|
42
43
|
class ExecTimeoutError < Error end
|
|
43
44
|
class ExecFailedError < Error end
|
|
44
45
|
class NoDefaultCommandError < Error end
|
|
@@ -163,6 +164,7 @@ module Microsandbox
|
|
|
163
164
|
|
|
164
165
|
class SandboxHandle
|
|
165
166
|
def name: () -> String
|
|
167
|
+
def id: () -> String
|
|
166
168
|
def status: () -> Symbol
|
|
167
169
|
def running?: () -> bool
|
|
168
170
|
def stopped?: () -> bool
|
|
@@ -176,6 +178,10 @@ module Microsandbox
|
|
|
176
178
|
def request_kill: () -> nil
|
|
177
179
|
def request_drain: () -> nil
|
|
178
180
|
def wait_until_stopped: () -> SandboxStopResult
|
|
181
|
+
def connect_or_start: (?detached: bool) -> Sandbox
|
|
182
|
+
def wait_for_status: (Symbol | String status) -> SandboxHandle
|
|
183
|
+
def restart: (?force: bool, ?timeout: Numeric?, ?detached: bool) -> Sandbox
|
|
184
|
+
def destroy: (?force: bool, ?timeout: Numeric?) -> nil
|
|
179
185
|
def config_json: () -> String
|
|
180
186
|
def config: () -> Hash[String, untyped]
|
|
181
187
|
def snapshot: (String name) -> SnapshotInfo
|
|
@@ -243,6 +249,8 @@ module Microsandbox
|
|
|
243
249
|
end
|
|
244
250
|
|
|
245
251
|
class Sandbox
|
|
252
|
+
STATUSES: Array[Symbol]
|
|
253
|
+
|
|
246
254
|
def self.create: (String name, ?image: String?, ?cpus: Integer?, ?max_cpus: Integer?,
|
|
247
255
|
?memory: Integer?, ?max_memory: Integer?,
|
|
248
256
|
?env: Hash[untyped, untyped]?, ?workdir: String?, ?shell: String?,
|
|
@@ -253,8 +261,9 @@ module Microsandbox
|
|
|
253
261
|
?volumes: Hash[untyped, untyped]?, ?network: untyped?,
|
|
254
262
|
?dns: Hash[untyped, untyped]?, ?tls: Hash[untyped, untyped]?,
|
|
255
263
|
?ipv4_pool: String?, ?ipv6_pool: String?,
|
|
256
|
-
?max_connections: Integer?, ?trust_host_cas: bool?,
|
|
264
|
+
?max_connections: Integer?, ?trust_host_cas: bool?, ?strict: bool?,
|
|
257
265
|
?rate_limiter: Hash[untyped, untyped]?,
|
|
266
|
+
?proxy: (OutboundProxy | Hash[untyped, untyped])?,
|
|
258
267
|
?vsock: (Hash[untyped, untyped] | Array[Hash[untyped, untyped]])?,
|
|
259
268
|
?patches: Array[Hash[untyped, untyped]]?, ?from_snapshot: String?,
|
|
260
269
|
?fstype: String?, ?init: (String | Symbol | Hash[untyped, untyped])?, ?ephemeral: bool,
|
|
@@ -269,6 +278,10 @@ module Microsandbox
|
|
|
269
278
|
?detached: bool,
|
|
270
279
|
?replace: bool, ?replace_with_timeout: Numeric?)
|
|
271
280
|
?{ (Sandbox) -> untyped } -> untyped
|
|
281
|
+
# Accepts the same keyword options as {create} (used only when a create is
|
|
282
|
+
# actually necessary); `replace:`/`replace_with_timeout:` are rejected by
|
|
283
|
+
# the core.
|
|
284
|
+
def self.connect_or_create: (String name, **untyped) ?{ (Sandbox) -> untyped } -> untyped
|
|
272
285
|
# Accepts the same keyword options as {create}.
|
|
273
286
|
def self.create_with_progress: (String name, **untyped) -> PullSession
|
|
274
287
|
def self.start: (String name, ?detached: bool) -> Sandbox
|
|
@@ -278,6 +291,7 @@ module Microsandbox
|
|
|
278
291
|
def self.remove: (String name) -> nil
|
|
279
292
|
|
|
280
293
|
def name: () -> String
|
|
294
|
+
def id: () -> String
|
|
281
295
|
def exec: (String command, ?Array[String] args, ?cwd: String?, ?user: String?,
|
|
282
296
|
?env: Hash[untyped, untyped]?, ?timeout: Numeric?, ?tty: bool, ?stdin: (String | :null)?,
|
|
283
297
|
?rlimits: Hash[untyped, untyped]?) -> ExecOutput
|
|
@@ -322,6 +336,9 @@ module Microsandbox
|
|
|
322
336
|
def kill: () -> nil
|
|
323
337
|
def drain: () -> nil
|
|
324
338
|
def wait: () -> ExitStatus
|
|
339
|
+
def wait_for_status: (Symbol | String status) -> SandboxHandle
|
|
340
|
+
def restart: (?force: bool, ?timeout: Numeric?, ?detached: bool) -> Sandbox
|
|
341
|
+
def destroy: (?force: bool, ?timeout: Numeric?) -> nil
|
|
325
342
|
def status: () -> Symbol
|
|
326
343
|
def owns_lifecycle?: () -> bool
|
|
327
344
|
def detach: () -> nil
|
|
@@ -560,6 +577,44 @@ module Microsandbox
|
|
|
560
577
|
def cloud?: () -> bool
|
|
561
578
|
end
|
|
562
579
|
|
|
580
|
+
# Host-side secret source (runtime v0.6.17): only `env` exists.
|
|
581
|
+
class SecretSource
|
|
582
|
+
KINDS: Array[String]
|
|
583
|
+
attr_reader kind: String
|
|
584
|
+
attr_reader var: String
|
|
585
|
+
def self.env: ((String | Symbol) variable) -> SecretSource
|
|
586
|
+
def self.coerce: (untyped value, ?String context) -> SecretSource
|
|
587
|
+
def initialize: ((String | Symbol) kind, (String | Symbol) var) -> void
|
|
588
|
+
def to_h: () -> Hash[String, String]
|
|
589
|
+
def ==: (untyped other) -> bool
|
|
590
|
+
def eql?: (untyped other) -> bool
|
|
591
|
+
def hash: () -> Integer
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Outbound SOCKS4/SOCKS5 proxy for the `proxy:` create option (runtime
|
|
595
|
+
# v0.6.17). Immutable: `credentials` returns a new proxy.
|
|
596
|
+
class OutboundProxy
|
|
597
|
+
PROTOCOLS: Array[String]
|
|
598
|
+
attr_reader protocol: String
|
|
599
|
+
attr_reader address: String
|
|
600
|
+
attr_reader user_id: String?
|
|
601
|
+
attr_reader username: String?
|
|
602
|
+
attr_reader password: SecretSource?
|
|
603
|
+
def self.socks4: (String address, ?user_id: String?) -> OutboundProxy
|
|
604
|
+
def self.socks5: (String address) -> OutboundProxy
|
|
605
|
+
def self.coerce: (untyped value) -> Hash[String, untyped]
|
|
606
|
+
def self.from_hash: (Hash[untyped, untyped] hash) -> OutboundProxy
|
|
607
|
+
def initialize: (protocol: (String | Symbol), address: String, ?user_id: String?,
|
|
608
|
+
?username: String?, ?password: SecretSource?) -> void
|
|
609
|
+
def credentials: (String username, (SecretSource | Hash[untyped, untyped]) password) -> OutboundProxy
|
|
610
|
+
def socks4?: () -> bool
|
|
611
|
+
def socks5?: () -> bool
|
|
612
|
+
def to_h: () -> Hash[String, untyped]
|
|
613
|
+
def ==: (untyped other) -> bool
|
|
614
|
+
def eql?: (untyped other) -> bool
|
|
615
|
+
def hash: () -> Integer
|
|
616
|
+
end
|
|
617
|
+
|
|
563
618
|
class NetworkPolicy
|
|
564
619
|
PROFILES: Array[String]
|
|
565
620
|
PRESET_ALIASES: Hash[String, String]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: microsandbox-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ya-luotao
|
|
@@ -71,6 +71,7 @@ files:
|
|
|
71
71
|
- lib/microsandbox/metrics.rb
|
|
72
72
|
- lib/microsandbox/modification.rb
|
|
73
73
|
- lib/microsandbox/network.rb
|
|
74
|
+
- lib/microsandbox/outbound_proxy.rb
|
|
74
75
|
- lib/microsandbox/patch.rb
|
|
75
76
|
- lib/microsandbox/root_disk.rb
|
|
76
77
|
- lib/microsandbox/sandbox.rb
|