microsandbox-rb 0.5.9 → 0.5.10
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 +77 -0
- data/Cargo.lock +90 -45
- data/DESIGN.md +7 -3
- data/README.md +91 -26
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/extconf.rb +6 -2
- data/ext/microsandbox/src/backend.rs +170 -0
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/image.rs +7 -7
- data/ext/microsandbox/src/lib.rs +27 -4
- data/ext/microsandbox/src/sandbox.rs +172 -58
- data/ext/microsandbox/src/volume.rs +6 -1
- data/lib/microsandbox/errors.rb +6 -0
- data/lib/microsandbox/exec_handle.rb +14 -11
- data/lib/microsandbox/fs.rb +7 -7
- data/lib/microsandbox/image.rb +1 -1
- data/lib/microsandbox/network.rb +19 -19
- data/lib/microsandbox/patch.rb +8 -8
- data/lib/microsandbox/sandbox.rb +199 -75
- data/lib/microsandbox/snapshot.rb +2 -2
- data/lib/microsandbox/ssh.rb +2 -2
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +3 -3
- data/lib/microsandbox.rb +61 -0
- data/sig/microsandbox.rbs +31 -13
- metadata +2 -1
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -1,41 +1,119 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Microsandbox
|
|
4
|
-
#
|
|
5
|
-
# {Sandbox.
|
|
6
|
-
|
|
4
|
+
# A controllable handle to a sandbox, returned by {Sandbox.get}, {Sandbox.list},
|
|
5
|
+
# and {Sandbox.list_with}. Carries a metadata snapshot (captured when fetched)
|
|
6
|
+
# plus the fine-grained lifecycle surface — `stop_with_timeout`, `request_stop`,
|
|
7
|
+
# `request_kill`, `request_drain`, `wait_until_stopped` — that mirrors the
|
|
8
|
+
# official SDKs' `SandboxHandle`. (The live {Sandbox} from {Sandbox.create}/
|
|
9
|
+
# {Sandbox.start} carries only the high-level `stop`/`kill`/`drain`/`wait`.)
|
|
10
|
+
#
|
|
11
|
+
# As of v0.5.8 this replaces the old read-only `SandboxInfo` (kept as a
|
|
12
|
+
# deprecated constant alias); `#status` here is a synchronous snapshot.
|
|
13
|
+
class SandboxHandle
|
|
14
|
+
def initialize(native)
|
|
15
|
+
@native = native
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
# @return [String]
|
|
8
|
-
|
|
9
|
-
# @return [Symbol] :running, :draining, :paused, :stopped, or :crashed
|
|
10
|
-
attr_reader :status
|
|
19
|
+
def name = @native.name
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@created_at_ms = data["created_at_ms"]
|
|
16
|
-
@updated_at_ms = data["updated_at_ms"]
|
|
17
|
-
end
|
|
21
|
+
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
22
|
+
# :stopped, or :crashed (a snapshot, captured when this handle was fetched)
|
|
23
|
+
def status = @native.status.to_sym
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# Whether the fetch-time {#status} snapshot is `:running` / `:stopped`.
|
|
26
|
+
# Like {#status}, these do NOT refresh: to observe a state change after
|
|
27
|
+
# {#request_stop}/{#request_kill}/{#request_drain}, use {#wait_until_stopped}
|
|
28
|
+
# or re-fetch the handle with {Sandbox.get}.
|
|
29
|
+
def running? = status == :running
|
|
30
|
+
def stopped? = status == :stopped
|
|
21
31
|
|
|
22
32
|
# @return [Time, nil]
|
|
23
33
|
def created_at
|
|
24
|
-
|
|
34
|
+
ms = @native.created_at_ms
|
|
35
|
+
ms && Time.at(ms / 1000.0)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# @return [Time, nil]
|
|
28
39
|
def updated_at
|
|
29
|
-
|
|
40
|
+
ms = @native.updated_at_ms
|
|
41
|
+
ms && Time.at(ms / 1000.0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default).
|
|
45
|
+
# @return [nil]
|
|
46
|
+
def stop
|
|
47
|
+
@native.stop
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Gracefully stop with a custom escalation timeout.
|
|
52
|
+
# @param timeout [Numeric] seconds to wait before escalating to SIGKILL
|
|
53
|
+
# @return [nil]
|
|
54
|
+
def stop_with_timeout(timeout)
|
|
55
|
+
@native.stop_with_timeout(Sandbox.send(:coerce_duration, timeout, "timeout"))
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Force-kill the sandbox (SIGKILL).
|
|
60
|
+
# @return [nil]
|
|
61
|
+
def kill
|
|
62
|
+
@native.kill
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Force-kill, waiting up to `timeout` seconds for the process to disappear.
|
|
67
|
+
# @param timeout [Numeric]
|
|
68
|
+
# @return [nil]
|
|
69
|
+
def kill_with_timeout(timeout)
|
|
70
|
+
@native.kill_with_timeout(Sandbox.send(:coerce_duration, timeout, "timeout"))
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Send the graceful-shutdown request and return immediately, without waiting.
|
|
75
|
+
# Pair with {#wait_until_stopped}.
|
|
76
|
+
# @return [nil]
|
|
77
|
+
def request_stop
|
|
78
|
+
@native.request_stop
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Send the force-kill request and return immediately, without waiting.
|
|
83
|
+
# @return [nil]
|
|
84
|
+
def request_kill
|
|
85
|
+
@native.request_kill
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Request a graceful drain (SIGUSR1) and return immediately, without waiting.
|
|
90
|
+
# @return [nil]
|
|
91
|
+
def request_drain
|
|
92
|
+
@native.request_drain
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Block until the sandbox is observed in a terminal (non-running) state.
|
|
97
|
+
# @return [SandboxStopResult]
|
|
98
|
+
def wait_until_stopped
|
|
99
|
+
SandboxStopResult.new(@native.wait_until_stopped)
|
|
30
100
|
end
|
|
31
101
|
|
|
32
102
|
def inspect
|
|
33
|
-
"#<Microsandbox::
|
|
103
|
+
"#<Microsandbox::SandboxHandle name=#{name.inspect} status=#{status}>"
|
|
34
104
|
end
|
|
35
105
|
end
|
|
36
106
|
|
|
107
|
+
# @deprecated since v0.5.8. {Sandbox.get}/{Sandbox.list} now return a
|
|
108
|
+
# controllable {SandboxHandle}; this constant remains as an alias so code
|
|
109
|
+
# that referenced the old read-only metadata type by name (e.g. `is_a?`
|
|
110
|
+
# checks) still resolves. Note it is now the same class as {SandboxHandle},
|
|
111
|
+
# whose constructor takes a native handle, not the metadata Hash the old
|
|
112
|
+
# `SandboxInfo.new` accepted — construct via {Sandbox.get}/{Sandbox.list}.
|
|
113
|
+
SandboxInfo = SandboxHandle
|
|
114
|
+
|
|
37
115
|
# The terminal observation of a stopped sandbox, returned by
|
|
38
|
-
# {
|
|
116
|
+
# {SandboxHandle#wait_until_stopped}. Mirrors the official SDKs' `SandboxStopResult`.
|
|
39
117
|
class SandboxStopResult
|
|
40
118
|
# @return [String]
|
|
41
119
|
attr_reader :name
|
|
@@ -67,7 +145,7 @@ module Microsandbox
|
|
|
67
145
|
|
|
68
146
|
def inspect
|
|
69
147
|
"#<Microsandbox::SandboxStopResult name=#{@name.inspect} status=#{@status}" \
|
|
70
|
-
"#{
|
|
148
|
+
"#{" exit_code=#{@exit_code}" if @exit_code}#{" signal=#{@signal}" if @signal}>"
|
|
71
149
|
end
|
|
72
150
|
end
|
|
73
151
|
|
|
@@ -143,15 +221,21 @@ module Microsandbox
|
|
|
143
221
|
# @yieldparam sandbox [Sandbox]
|
|
144
222
|
# @return [Sandbox, Object] the sandbox, or the block's return value
|
|
145
223
|
def create(name,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
224
|
+
image: nil, cpus: nil, memory: nil, env: nil, workdir: nil,
|
|
225
|
+
shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
|
|
226
|
+
entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
227
|
+
patches: nil,
|
|
228
|
+
from_snapshot: nil, log_level: nil, quiet_logs: false, security: nil,
|
|
229
|
+
oci_upper_size: nil, max_duration: nil, idle_timeout: nil, rlimits: nil,
|
|
230
|
+
pull_policy: nil, registry_auth: nil, registry_insecure: false,
|
|
231
|
+
registry_ca_certs: nil, secrets: nil,
|
|
232
|
+
detached: false, replace: false, replace_with_timeout: nil)
|
|
233
|
+
# A sandbox boots from exactly one rootfs source. The core would reject a
|
|
234
|
+
# contradictory pair, but only after a runtime round-trip; fail fast and
|
|
235
|
+
# clearly here (the Python SDK validates this the same way).
|
|
236
|
+
if image && from_snapshot
|
|
237
|
+
raise ArgumentError, "provide either image: or from_snapshot:, not both"
|
|
238
|
+
end
|
|
155
239
|
Microsandbox.ensure_runtime!
|
|
156
240
|
opts = {}
|
|
157
241
|
opts["image"] = image.to_s if image
|
|
@@ -183,7 +267,7 @@ module Microsandbox
|
|
|
183
267
|
opts["secrets"] = normalize_secrets(secrets) if secrets
|
|
184
268
|
opts["detached"] = true if detached
|
|
185
269
|
if replace_with_timeout
|
|
186
|
-
opts["replace_with_timeout"] =
|
|
270
|
+
opts["replace_with_timeout"] = coerce_duration(replace_with_timeout, "replace_with_timeout")
|
|
187
271
|
elsif replace
|
|
188
272
|
opts["replace"] = true
|
|
189
273
|
end
|
|
@@ -206,27 +290,27 @@ module Microsandbox
|
|
|
206
290
|
# @return [Sandbox]
|
|
207
291
|
def start(name, detached: false)
|
|
208
292
|
Microsandbox.ensure_runtime!
|
|
209
|
-
new(Native::Sandbox.start(name.to_s, {
|
|
293
|
+
new(Native::Sandbox.start(name.to_s, {"detached" => detached}))
|
|
210
294
|
end
|
|
211
295
|
|
|
212
|
-
# Fetch
|
|
213
|
-
# @return [
|
|
296
|
+
# Fetch a controllable handle for a sandbox by name (running or not).
|
|
297
|
+
# @return [SandboxHandle]
|
|
214
298
|
def get(name)
|
|
215
|
-
|
|
299
|
+
SandboxHandle.new(Native::Sandbox.get(name.to_s))
|
|
216
300
|
end
|
|
217
301
|
|
|
218
|
-
# List all sandboxes.
|
|
219
|
-
# @return [Array<
|
|
302
|
+
# List all sandboxes as controllable handles.
|
|
303
|
+
# @return [Array<SandboxHandle>]
|
|
220
304
|
def list
|
|
221
|
-
Native::Sandbox.list.map { |
|
|
305
|
+
Native::Sandbox.list.map { |h| SandboxHandle.new(h) }
|
|
222
306
|
end
|
|
223
307
|
|
|
224
308
|
# List sandboxes carrying all of the given labels (AND-matched).
|
|
225
309
|
# @param labels [Hash] required key => value labels
|
|
226
|
-
# @return [Array<
|
|
310
|
+
# @return [Array<SandboxHandle>]
|
|
227
311
|
def list_with(labels: {})
|
|
228
|
-
opts = {
|
|
229
|
-
Native::Sandbox.list_with(opts).map { |
|
|
312
|
+
opts = {"labels" => stringify(labels)}
|
|
313
|
+
Native::Sandbox.list_with(opts).map { |h| SandboxHandle.new(h) }
|
|
230
314
|
end
|
|
231
315
|
|
|
232
316
|
# Remove a (stopped) sandbox by name.
|
|
@@ -238,6 +322,19 @@ module Microsandbox
|
|
|
238
322
|
|
|
239
323
|
private
|
|
240
324
|
|
|
325
|
+
# Coerce a seconds value to a finite, non-negative Float. Rejects negatives,
|
|
326
|
+
# NaN, and infinities *here* (a clean ArgumentError) rather than letting them
|
|
327
|
+
# reach the native layer, where `Duration::from_secs_f64` panics across the
|
|
328
|
+
# FFI boundary on exactly those inputs. Shared by every duration option.
|
|
329
|
+
def coerce_duration(value, label)
|
|
330
|
+
seconds = Float(value)
|
|
331
|
+
unless seconds.finite? && seconds >= 0
|
|
332
|
+
raise ArgumentError,
|
|
333
|
+
"#{label} must be a finite, non-negative number of seconds (got #{value.inspect})"
|
|
334
|
+
end
|
|
335
|
+
seconds
|
|
336
|
+
end
|
|
337
|
+
|
|
241
338
|
def stringify(hash)
|
|
242
339
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
243
340
|
end
|
|
@@ -256,7 +353,7 @@ module Microsandbox
|
|
|
256
353
|
unless username && password
|
|
257
354
|
# Report only the keys given, never the values — auth carries secrets.
|
|
258
355
|
raise ArgumentError,
|
|
259
|
-
|
|
356
|
+
"registry_auth needs :username and :password (got keys: #{auth.keys.inspect})"
|
|
260
357
|
end
|
|
261
358
|
opts["registry_username"] = username.to_s
|
|
262
359
|
opts["registry_password"] = password.to_s
|
|
@@ -358,33 +455,39 @@ module Microsandbox
|
|
|
358
455
|
# @param env [Hash, nil] extra environment variables
|
|
359
456
|
# @param timeout [Numeric, nil] kill after N seconds
|
|
360
457
|
# @param tty [Boolean] allocate a pseudo-terminal
|
|
361
|
-
# @param stdin [String, nil]
|
|
458
|
+
# @param stdin [String, Symbol, nil] bytes to feed to stdin, or +:pipe+ to
|
|
459
|
+
# open a streaming stdin pipe (write/close it via {ExecHandle#stdin}; only
|
|
460
|
+
# useful with the streaming variants)
|
|
362
461
|
# @return [ExecOutput]
|
|
363
462
|
def exec(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
364
463
|
ExecOutput.new(@native.exec(command.to_s, Array(args).map(&:to_s),
|
|
365
|
-
|
|
464
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
366
465
|
end
|
|
367
466
|
|
|
368
467
|
# Run a shell script (pipes, redirects, etc. allowed) and collect output.
|
|
369
468
|
# @return [ExecOutput]
|
|
370
469
|
def shell(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
371
470
|
ExecOutput.new(@native.shell(script.to_s,
|
|
372
|
-
|
|
471
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
373
472
|
end
|
|
374
473
|
|
|
375
474
|
# Run a command and stream its output as it arrives.
|
|
475
|
+
#
|
|
476
|
+
# Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
|
|
477
|
+
# then returns a writable sink; close it to send EOF (a process like +cat+
|
|
478
|
+
# that reads until EOF will otherwise block forever).
|
|
376
479
|
# @return [ExecHandle]
|
|
377
480
|
# @see ExecHandle
|
|
378
481
|
def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
379
482
|
ExecHandle.new(@native.exec_stream(command.to_s, Array(args).map(&:to_s),
|
|
380
|
-
|
|
483
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
381
484
|
end
|
|
382
485
|
|
|
383
486
|
# Run a shell script and stream its output as it arrives.
|
|
384
487
|
# @return [ExecHandle]
|
|
385
488
|
def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
386
489
|
ExecHandle.new(@native.shell_stream(script.to_s,
|
|
387
|
-
|
|
490
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
388
491
|
end
|
|
389
492
|
|
|
390
493
|
# Attach an interactive terminal to a command in the sandbox.
|
|
@@ -469,7 +572,7 @@ module Microsandbox
|
|
|
469
572
|
# @param interval [Numeric] seconds between snapshots
|
|
470
573
|
# @return [MetricsStream] an {Enumerable} of {Metrics}
|
|
471
574
|
def metrics_stream(interval: 1.0)
|
|
472
|
-
MetricsStream.new(@native.metrics_stream(
|
|
575
|
+
MetricsStream.new(@native.metrics_stream(coerce_duration(interval, "interval")))
|
|
473
576
|
end
|
|
474
577
|
|
|
475
578
|
# Stream captured logs as they appear.
|
|
@@ -492,48 +595,46 @@ module Microsandbox
|
|
|
492
595
|
LogStream.new(@native.log_stream(opts))
|
|
493
596
|
end
|
|
494
597
|
|
|
495
|
-
# Gracefully stop the sandbox (
|
|
496
|
-
#
|
|
598
|
+
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default) and
|
|
599
|
+
# wait for it to terminate. For a custom timeout or fire-and-return
|
|
600
|
+
# `request_*` control, fetch a {SandboxHandle} via {Sandbox.get}.
|
|
497
601
|
# @return [nil]
|
|
498
|
-
def stop
|
|
499
|
-
@native.stop
|
|
602
|
+
def stop
|
|
603
|
+
@native.stop
|
|
500
604
|
nil
|
|
501
605
|
end
|
|
502
606
|
|
|
503
|
-
#
|
|
504
|
-
# @
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
@native.kill(timeout && Float(timeout))
|
|
508
|
-
nil
|
|
607
|
+
# Gracefully stop, then wait for the process to exit.
|
|
608
|
+
# @return [ExitStatus]
|
|
609
|
+
def stop_and_wait
|
|
610
|
+
ExitStatus.new(@native.stop_and_wait)
|
|
509
611
|
end
|
|
510
612
|
|
|
511
|
-
#
|
|
512
|
-
# for the sandbox to terminate. Pair with {#wait_until_stopped}.
|
|
613
|
+
# Force-kill the sandbox (SIGKILL).
|
|
513
614
|
# @return [nil]
|
|
514
|
-
def
|
|
515
|
-
@native.
|
|
615
|
+
def kill
|
|
616
|
+
@native.kill
|
|
516
617
|
nil
|
|
517
618
|
end
|
|
518
619
|
|
|
519
|
-
#
|
|
620
|
+
# Trigger a graceful drain (SIGUSR1).
|
|
520
621
|
# @return [nil]
|
|
521
|
-
def
|
|
522
|
-
@native.
|
|
622
|
+
def drain
|
|
623
|
+
@native.drain
|
|
523
624
|
nil
|
|
524
625
|
end
|
|
525
626
|
|
|
526
|
-
#
|
|
527
|
-
# @return [
|
|
528
|
-
def
|
|
529
|
-
@native.
|
|
530
|
-
nil
|
|
627
|
+
# Wait for the sandbox process to exit.
|
|
628
|
+
# @return [ExitStatus]
|
|
629
|
+
def wait
|
|
630
|
+
ExitStatus.new(@native.wait)
|
|
531
631
|
end
|
|
532
632
|
|
|
533
|
-
#
|
|
534
|
-
# @return [
|
|
535
|
-
|
|
536
|
-
|
|
633
|
+
# The live status, fetched from the backend (a round-trip per call).
|
|
634
|
+
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
635
|
+
# :stopped, or :crashed
|
|
636
|
+
def status
|
|
637
|
+
@native.status.to_sym
|
|
537
638
|
end
|
|
538
639
|
|
|
539
640
|
# @return [Boolean] whether this handle owns the sandbox process lifecycle
|
|
@@ -556,14 +657,37 @@ module Microsandbox
|
|
|
556
657
|
|
|
557
658
|
private
|
|
558
659
|
|
|
559
|
-
|
|
660
|
+
# Instance-side shim for the shared, class-private duration validator (used
|
|
661
|
+
# by #exec/#shell timeouts, #stop/#kill, and #metrics_stream).
|
|
662
|
+
def coerce_duration(value, label)
|
|
663
|
+
self.class.send(:coerce_duration, value, label)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: false)
|
|
560
667
|
opts = {}
|
|
561
668
|
opts["cwd"] = cwd.to_s if cwd
|
|
562
669
|
opts["user"] = user.to_s if user
|
|
563
670
|
opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
|
|
564
|
-
opts["timeout"] =
|
|
671
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
565
672
|
opts["tty"] = true if tty
|
|
566
|
-
|
|
673
|
+
# `stdin: :pipe` opens a streaming stdin pipe — write to it via
|
|
674
|
+
# {ExecHandle#stdin} and close to send EOF. It is only meaningful for the
|
|
675
|
+
# streaming variants (which return an ExecHandle); a blocking exec/shell
|
|
676
|
+
# collects to completion and has nowhere to hand back the sink, so a piped
|
|
677
|
+
# process that reads stdin would block forever waiting for EOF. Reject it
|
|
678
|
+
# there. Any other truthy value is fed as a fixed byte buffer (closed
|
|
679
|
+
# automatically). nil means no stdin.
|
|
680
|
+
case stdin
|
|
681
|
+
when nil then nil
|
|
682
|
+
when :pipe
|
|
683
|
+
unless pipe_ok
|
|
684
|
+
raise ArgumentError,
|
|
685
|
+
"stdin: :pipe is only valid for exec_stream/shell_stream — a blocking " \
|
|
686
|
+
"exec/shell cannot expose a writable stdin sink; pass a String to feed bytes"
|
|
687
|
+
end
|
|
688
|
+
opts["stdin_pipe"] = true
|
|
689
|
+
else opts["stdin"] = stdin.to_s
|
|
690
|
+
end
|
|
567
691
|
if rlimits
|
|
568
692
|
opts["rlimits"] = rlimits.map do |resource, limit|
|
|
569
693
|
soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
|
|
@@ -43,7 +43,7 @@ module Microsandbox
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def inspect
|
|
46
|
-
"#<Microsandbox::SnapshotInfo digest=#{@digest.inspect}#{
|
|
46
|
+
"#<Microsandbox::SnapshotInfo digest=#{@digest.inspect}#{" name=#{@name.inspect}" if @name}>"
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -142,7 +142,7 @@ module Microsandbox
|
|
|
142
142
|
# @param dest [String, nil] explicit destination directory
|
|
143
143
|
# @return [SnapshotInfo]
|
|
144
144
|
def import(archive_path, dest: nil)
|
|
145
|
-
SnapshotInfo.new(Native::Snapshot.import(archive_path.to_s, dest
|
|
145
|
+
SnapshotInfo.new(Native::Snapshot.import(archive_path.to_s, dest&.to_s))
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
private
|
data/lib/microsandbox/ssh.rb
CHANGED
|
@@ -218,7 +218,7 @@ module Microsandbox
|
|
|
218
218
|
# @yieldparam client [SshClient]
|
|
219
219
|
# @return [SshClient, Object]
|
|
220
220
|
def open_client(user: "root", term: nil, sftp: true)
|
|
221
|
-
opts = {
|
|
221
|
+
opts = {"user" => user.to_s, "sftp" => sftp ? true : false}
|
|
222
222
|
opts["term"] = term.to_s if term
|
|
223
223
|
client = SshClient.new(@native.ssh_open_client(opts))
|
|
224
224
|
return client unless block_given?
|
|
@@ -237,7 +237,7 @@ module Microsandbox
|
|
|
237
237
|
# @param sftp [Boolean] enable the SFTP subsystem (default true)
|
|
238
238
|
# @return [SshServer]
|
|
239
239
|
def prepare_server(host_key_path: nil, authorized_keys_path: nil, user: nil, sftp: true)
|
|
240
|
-
opts = {
|
|
240
|
+
opts = {"sftp" => sftp ? true : false}
|
|
241
241
|
opts["host_key_path"] = host_key_path.to_s if host_key_path
|
|
242
242
|
opts["authorized_keys_path"] = authorized_keys_path.to_s if authorized_keys_path
|
|
243
243
|
opts["user"] = user.to_s if user
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Microsandbox
|
|
4
|
-
# Gem version. Tracks the upstream microsandbox runtime (currently `v0.5.
|
|
4
|
+
# Gem version. Tracks the upstream microsandbox runtime (currently `v0.5.8`,
|
|
5
5
|
# the pinned core-crate tag); the patch segment advances for gem-only revisions
|
|
6
6
|
# that add bindings atop the same core. Must equal the native ext's Cargo crate
|
|
7
7
|
# version (`Native.version`), enforced by spec/unit/version_spec.rb.
|
|
8
|
-
VERSION = "0.5.
|
|
8
|
+
VERSION = "0.5.10"
|
|
9
9
|
end
|
data/lib/microsandbox/volume.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Microsandbox
|
|
|
7
7
|
# (`kind`, `used_bytes`, …) are populated when returned from {Volume.get}/{Volume.list}.
|
|
8
8
|
class VolumeInfo
|
|
9
9
|
attr_reader :name, :path, :quota_mib, :used_bytes, :capacity_bytes,
|
|
10
|
-
|
|
10
|
+
:disk_format, :disk_fstype, :labels
|
|
11
11
|
|
|
12
12
|
def initialize(data)
|
|
13
13
|
@name = data["name"]
|
|
@@ -33,7 +33,7 @@ module Microsandbox
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def inspect
|
|
36
|
-
"#<Microsandbox::VolumeInfo name=#{@name.inspect}#{
|
|
36
|
+
"#<Microsandbox::VolumeInfo name=#{@name.inspect}#{" kind=#{@kind}" if @kind}>"
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -49,7 +49,7 @@ module Microsandbox
|
|
|
49
49
|
# @param labels [Hash, nil]
|
|
50
50
|
# @return [VolumeInfo]
|
|
51
51
|
def create(name, kind: "dir", size_mib: nil, quota_mib: nil, labels: nil)
|
|
52
|
-
opts = {
|
|
52
|
+
opts = {"kind" => kind.to_s}
|
|
53
53
|
opts["size_mib"] = Integer(size_mib) if size_mib
|
|
54
54
|
opts["quota_mib"] = Integer(quota_mib) if quota_mib
|
|
55
55
|
opts["labels"] = labels.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if labels
|
data/lib/microsandbox.rb
CHANGED
|
@@ -78,6 +78,11 @@ module Microsandbox
|
|
|
78
78
|
# @return [nil]
|
|
79
79
|
def ensure_runtime!
|
|
80
80
|
return if @runtime_ready
|
|
81
|
+
# A cloud backend has no local msb/libkrunfw runtime to provision: skip the
|
|
82
|
+
# presence check and the first-use download entirely. Resolving the kind
|
|
83
|
+
# uses the same lazy env/profile/config ladder every operation already
|
|
84
|
+
# consults, so this adds no work for local hosts (the common case).
|
|
85
|
+
return if default_backend_kind == :cloud
|
|
81
86
|
if installed?
|
|
82
87
|
@runtime_ready = true
|
|
83
88
|
return
|
|
@@ -104,6 +109,62 @@ module Microsandbox
|
|
|
104
109
|
Native.set_runtime_msb_path(path.to_s)
|
|
105
110
|
end
|
|
106
111
|
|
|
112
|
+
# Override the `libkrunfw` shared-library path (SDK tier of the resolver,
|
|
113
|
+
# below the `MSB_LIBKRUNFW_PATH` environment variable). Process-level and
|
|
114
|
+
# set-once: a second call is silently ignored, and the env var still wins.
|
|
115
|
+
# Mirrors {runtime_path=} for libkrunfw.
|
|
116
|
+
# @param path [String]
|
|
117
|
+
# @return [void]
|
|
118
|
+
def libkrunfw_path=(path)
|
|
119
|
+
Native.set_runtime_libkrunfw_path(path.to_s)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Install a process-wide default backend (v0.5.8 backend routing). Without a
|
|
123
|
+
# call to this, operations use a local libkrun backend; the env/profile
|
|
124
|
+
# ladder (`MSB_BACKEND`, `MSB_API_URL`+`MSB_API_KEY`, `MSB_PROFILE`,
|
|
125
|
+
# `~/.microsandbox/config.json`) is resolved lazily on first use. Call once
|
|
126
|
+
# at startup, before any sandbox operations.
|
|
127
|
+
#
|
|
128
|
+
# @param kind ["local","cloud", Symbol] backend kind
|
|
129
|
+
# @param url [String, nil] cloud control-plane URL (cloud, unless `profile:`)
|
|
130
|
+
# @param api_key [String, nil] cloud API key (cloud, unless `profile:`)
|
|
131
|
+
# @param profile [String, nil] named profile from `~/.microsandbox/config.json`
|
|
132
|
+
# @return [void]
|
|
133
|
+
def set_default_backend(kind, url: nil, api_key: nil, profile: nil)
|
|
134
|
+
Native.set_default_backend(kind.to_s, url&.to_s, api_key&.to_s, profile&.to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Run the given block with a temporary default backend, restoring the
|
|
138
|
+
# previous one afterward (even on error). NOTE: the swap is process-wide
|
|
139
|
+
# while the block runs, not fiber/thread-local — concurrent threads observe
|
|
140
|
+
# the temporary backend. It is also NOT safe to call from multiple threads
|
|
141
|
+
# at once: two interleaved `with_backend` calls can restore each other's
|
|
142
|
+
# saved backend out of order and leave a temporary backend installed
|
|
143
|
+
# permanently. Use it only when no other thread is changing the backend, and
|
|
144
|
+
# avoid calling {set_default_backend} inside the block (the restore on exit
|
|
145
|
+
# would overwrite that change). Mirrors the official SDKs' scoped-backend helper.
|
|
146
|
+
#
|
|
147
|
+
# @param kind ["local","cloud", Symbol]
|
|
148
|
+
# @param url [String, nil]
|
|
149
|
+
# @param api_key [String, nil]
|
|
150
|
+
# @param profile [String, nil]
|
|
151
|
+
# @yield with the temporary backend installed
|
|
152
|
+
# @return [Object] the block's return value
|
|
153
|
+
def with_backend(kind, url: nil, api_key: nil, profile: nil)
|
|
154
|
+
token = Native.push_default_backend(kind.to_s, url&.to_s, api_key&.to_s, profile&.to_s)
|
|
155
|
+
begin
|
|
156
|
+
yield
|
|
157
|
+
ensure
|
|
158
|
+
Native.pop_default_backend(token)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [Symbol] the active default backend kind, :local or :cloud.
|
|
163
|
+
# The first call resolves the env/profile/config ladder.
|
|
164
|
+
def default_backend_kind
|
|
165
|
+
Native.default_backend_kind.to_sym
|
|
166
|
+
end
|
|
167
|
+
|
|
107
168
|
# Latest resource-usage snapshot for every running sandbox, keyed by name.
|
|
108
169
|
# Mirrors the official `all_sandbox_metrics`/`allSandboxMetrics` helpers.
|
|
109
170
|
# @return [Hash{String => Metrics}]
|