microsandbox-rb 0.11.0 → 0.13.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 +126 -0
- data/Cargo.lock +1006 -557
- data/README.md +48 -10
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/backend.rs +29 -9
- data/ext/microsandbox/src/error.rs +89 -1
- data/ext/microsandbox/src/image.rs +18 -8
- data/ext/microsandbox/src/lib.rs +9 -9
- data/ext/microsandbox/src/sandbox.rs +236 -32
- data/ext/microsandbox/src/snapshot.rs +10 -3
- data/ext/microsandbox/src/volume.rs +14 -3
- data/lib/microsandbox/backend_info.rb +46 -0
- data/lib/microsandbox/errors.rb +16 -0
- data/lib/microsandbox/root_disk.rb +24 -0
- data/lib/microsandbox/sandbox.rb +247 -17
- data/lib/microsandbox/snapshot.rb +12 -8
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +16 -0
- data/lib/microsandbox.rb +16 -3
- data/sig/microsandbox.rbs +48 -5
- metadata +2 -1
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -202,6 +202,48 @@ module Microsandbox
|
|
|
202
202
|
end
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
+
# One page of sandbox handles, returned by {Sandbox.list} and
|
|
206
|
+
# {Sandbox.list_with} — sandbox listing is cursor-paginated as of runtime
|
|
207
|
+
# v0.6.8. Enumerable over its {SandboxHandle}s; fetch the following page by
|
|
208
|
+
# passing {#next_cursor} to {Sandbox.list_with}.
|
|
209
|
+
#
|
|
210
|
+
# @example Walk every page
|
|
211
|
+
# page = Microsandbox::Sandbox.list
|
|
212
|
+
# loop do
|
|
213
|
+
# page.each { |h| puts h.name }
|
|
214
|
+
# break if page.last_page?
|
|
215
|
+
# page = Microsandbox::Sandbox.list_with(cursor: page.next_cursor)
|
|
216
|
+
# end
|
|
217
|
+
class SandboxPage
|
|
218
|
+
include Enumerable
|
|
219
|
+
|
|
220
|
+
# @return [Array<SandboxHandle>] the sandboxes in this page
|
|
221
|
+
attr_reader :sandboxes
|
|
222
|
+
# @return [String, nil] opaque cursor for the next page; nil on the final page
|
|
223
|
+
attr_reader :next_cursor
|
|
224
|
+
|
|
225
|
+
# @api private — construct via {Sandbox.list}/{Sandbox.list_with}.
|
|
226
|
+
def initialize(sandboxes, next_cursor)
|
|
227
|
+
@sandboxes = sandboxes
|
|
228
|
+
@next_cursor = next_cursor
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def each(&) = @sandboxes.each(&)
|
|
232
|
+
|
|
233
|
+
# @return [Integer]
|
|
234
|
+
def size = @sandboxes.size
|
|
235
|
+
alias_method :length, :size
|
|
236
|
+
|
|
237
|
+
def empty? = @sandboxes.empty?
|
|
238
|
+
|
|
239
|
+
# Whether this is the final page (no cursor to continue from).
|
|
240
|
+
def last_page? = @next_cursor.nil?
|
|
241
|
+
|
|
242
|
+
def inspect
|
|
243
|
+
"#<Microsandbox::SandboxPage size=#{size} next_cursor=#{@next_cursor.inspect}>"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
205
247
|
# A running sandbox (microVM) — the primary entry point of the SDK.
|
|
206
248
|
#
|
|
207
249
|
# @example Block form (auto-stops on exit)
|
|
@@ -246,7 +288,14 @@ module Microsandbox
|
|
|
246
288
|
# @param hostname [String, nil] guest hostname
|
|
247
289
|
# @param labels [Hash, nil] metadata labels
|
|
248
290
|
# @param scripts [Hash, nil] named scripts to install
|
|
249
|
-
# @param entrypoint [Array<String>, nil] image
|
|
291
|
+
# @param entrypoint [Array<String>, nil] image ENTRYPOINT override. An
|
|
292
|
+
# explicit `[]` clears the image's ENTRYPOINT (so `exec_default` runs
|
|
293
|
+
# the CMD alone); `nil` (the default) inherits it.
|
|
294
|
+
# @param cmd [Array<String>, nil] image CMD override used by
|
|
295
|
+
# default-workload execution ({Sandbox#exec_default} et al., runtime
|
|
296
|
+
# v0.6.9). Durable configuration — it does **not** execute anything at
|
|
297
|
+
# create time (`create` boots the VM only). An explicit `[]` clears the
|
|
298
|
+
# image CMD; `nil` (the default) inherits it.
|
|
250
299
|
# @param ports [Hash, nil] host_port => guest_port TCP publications
|
|
251
300
|
# @param ports_udp [Hash, nil] host_port => guest_port UDP publications
|
|
252
301
|
# @param volumes [Hash, nil] guest_path => mount spec. Each value is a host
|
|
@@ -280,6 +329,17 @@ module Microsandbox
|
|
|
280
329
|
# @param ipv6_pool [String, nil] guest IPv6 address pool CIDR
|
|
281
330
|
# @param max_connections [Integer, nil] cap on concurrent proxied connections
|
|
282
331
|
# @param trust_host_cas [Boolean, nil] trust the host's CA bundle for upstream TLS
|
|
332
|
+
# @param rate_limiter [Hash, nil] per-sandbox egress/ingress token-bucket
|
|
333
|
+
# limits (runtime v0.6.9, local backend):
|
|
334
|
+
# `{ egress: { bandwidth: { size: 1_048_576, refill_time_ms: 1000,
|
|
335
|
+
# one_time_burst: 0 }, ops: { size: 1000, refill_time_ms: 1000 } },
|
|
336
|
+
# ingress: { ... } }`. `bandwidth` buckets meter bytes, `ops` buckets
|
|
337
|
+
# meter network frames; an omitted bucket or direction is unlimited.
|
|
338
|
+
# @param vsock [Hash, Array, nil] host sockets exposed on guest-to-host
|
|
339
|
+
# vsock ports (runtime v0.6.9): `{ "/host/api.sock" => 5000 }` (stream
|
|
340
|
+
# sockets), or an Array of
|
|
341
|
+
# `{ host_socket:, port:, socket_type: :stream|:dgram }` Hashes. Guests
|
|
342
|
+
# connect to host CID 2 on the given port; no in-guest proxy required.
|
|
283
343
|
# @param from_snapshot [String, nil] boot from a snapshot name or digest
|
|
284
344
|
# instead of an image (mutually exclusive with `image:`)
|
|
285
345
|
# @param fstype [String, nil] inner filesystem type (e.g. "ext4") when
|
|
@@ -301,9 +361,12 @@ module Microsandbox
|
|
|
301
361
|
# disk (runtime v0.6.7). An Integer is the managed ext4 upper's size cap
|
|
302
362
|
# in MiB (default kind, 4 GiB when unset); a Hash picks a kind via the
|
|
303
363
|
# {RootDisk} factory — `RootDisk.managed(8192)`, `RootDisk.tmpfs(2048)`
|
|
304
|
-
# (RAM-backed, pristine on every boot),
|
|
364
|
+
# (RAM-backed, pristine on every boot),
|
|
305
365
|
# `RootDisk.disk("./scratch.img", format: "raw", fstype: "ext4")`
|
|
306
|
-
# (user-supplied image attached writable)
|
|
366
|
+
# (user-supplied image attached writable), or
|
|
367
|
+
# `RootDisk.flat(8192, clone: :auto)` (a single complete ext4 root disk
|
|
368
|
+
# materialized from the OCI image, runtime v0.6.9 — skips the overlay
|
|
369
|
+
# stack; content-addressed and cached across sandboxes).
|
|
307
370
|
# @param oci_upper_size [Integer, nil] deprecated alias for
|
|
308
371
|
# `root_disk: <Integer>` (the managed kind); warns, and conflicts with
|
|
309
372
|
# `root_disk:`
|
|
@@ -376,9 +439,9 @@ module Microsandbox
|
|
|
376
439
|
def build_create_opts(image: nil, cpus: nil, max_cpus: nil, memory: nil, max_memory: nil,
|
|
377
440
|
env: nil, workdir: nil,
|
|
378
441
|
shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
|
|
379
|
-
entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
442
|
+
entrypoint: nil, cmd: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
380
443
|
dns: nil, tls: nil, ipv4_pool: nil, ipv6_pool: nil,
|
|
381
|
-
max_connections: nil, trust_host_cas: nil,
|
|
444
|
+
max_connections: nil, trust_host_cas: nil, rate_limiter: nil, vsock: nil,
|
|
382
445
|
patches: nil,
|
|
383
446
|
from_snapshot: nil, fstype: nil, init: nil, ephemeral: false,
|
|
384
447
|
log_level: nil, quiet_logs: false, security: nil,
|
|
@@ -423,7 +486,11 @@ module Microsandbox
|
|
|
423
486
|
opts["env"] = stringify(env) if env
|
|
424
487
|
opts["labels"] = stringify(labels) if labels
|
|
425
488
|
opts["scripts"] = stringify(scripts) if scripts
|
|
426
|
-
|
|
489
|
+
# entrypoint/cmd: an explicit empty Array clears the image's value
|
|
490
|
+
# (blocking the image-config merge), so presence is keyed on the kwarg
|
|
491
|
+
# itself — nil (the default) inherits from the image.
|
|
492
|
+
opts["entrypoint"] = Array(entrypoint).map(&:to_s) unless entrypoint.nil?
|
|
493
|
+
opts["cmd"] = Array(cmd).map(&:to_s) unless cmd.nil?
|
|
427
494
|
opts["ports"] = intify_ports(ports) if ports
|
|
428
495
|
opts["ports_udp"] = intify_ports(ports_udp) if ports_udp
|
|
429
496
|
opts["volumes"] = normalize_volumes(volumes) if volumes
|
|
@@ -435,6 +502,8 @@ module Microsandbox
|
|
|
435
502
|
opts["ipv6_pool"] = ipv6_pool.to_s if ipv6_pool
|
|
436
503
|
opts["max_connections"] = Integer(max_connections) if max_connections
|
|
437
504
|
set_bool(opts, "trust_host_cas", trust_host_cas)
|
|
505
|
+
opts["rate_limiter"] = normalize_rate_limiter(rate_limiter) if rate_limiter
|
|
506
|
+
opts["vsock"] = normalize_vsock(vsock) if vsock
|
|
438
507
|
opts["log_level"] = log_level.to_s if log_level
|
|
439
508
|
opts["quiet_logs"] = true if quiet_logs
|
|
440
509
|
opts["security"] = security.to_s if security
|
|
@@ -478,18 +547,25 @@ module Microsandbox
|
|
|
478
547
|
SandboxHandle.new(Native::Sandbox.get(name.to_s))
|
|
479
548
|
end
|
|
480
549
|
|
|
481
|
-
# List
|
|
482
|
-
#
|
|
550
|
+
# List the first page of sandboxes (default page size 20) as controllable
|
|
551
|
+
# handles. Cursor-paginated as of runtime v0.6.8 — follow
|
|
552
|
+
# {SandboxPage#next_cursor} via {.list_with} for subsequent pages.
|
|
553
|
+
# @return [SandboxPage]
|
|
483
554
|
def list
|
|
484
|
-
Native::Sandbox.list
|
|
555
|
+
build_page(Native::Sandbox.list)
|
|
485
556
|
end
|
|
486
557
|
|
|
487
|
-
# List
|
|
488
|
-
# @param labels [Hash] required key => value labels
|
|
489
|
-
# @
|
|
490
|
-
|
|
558
|
+
# List one configured page of sandboxes.
|
|
559
|
+
# @param labels [Hash] required key => value labels (AND-matched)
|
|
560
|
+
# @param limit [Integer, nil] page size, 1..100 (upstream default 20)
|
|
561
|
+
# @param cursor [String, nil] opaque cursor from a previous page's
|
|
562
|
+
# {SandboxPage#next_cursor}
|
|
563
|
+
# @return [SandboxPage]
|
|
564
|
+
def list_with(labels: {}, limit: nil, cursor: nil)
|
|
491
565
|
opts = {"labels" => stringify(labels)}
|
|
492
|
-
|
|
566
|
+
opts["limit"] = Integer(limit) if limit
|
|
567
|
+
opts["cursor"] = cursor.to_s if cursor
|
|
568
|
+
build_page(Native::Sandbox.list_with(opts))
|
|
493
569
|
end
|
|
494
570
|
|
|
495
571
|
# Remove a (stopped) sandbox by name.
|
|
@@ -518,6 +594,15 @@ module Microsandbox
|
|
|
518
594
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
519
595
|
end
|
|
520
596
|
|
|
597
|
+
# Wrap a native page Hash ({"sandboxes" => [...], "next_cursor" => ...})
|
|
598
|
+
# into a {SandboxPage} of {SandboxHandle}s.
|
|
599
|
+
def build_page(data)
|
|
600
|
+
SandboxPage.new(
|
|
601
|
+
data["sandboxes"].map { |h| SandboxHandle.new(h) },
|
|
602
|
+
data["next_cursor"]
|
|
603
|
+
)
|
|
604
|
+
end
|
|
605
|
+
|
|
521
606
|
def intify_ports(ports)
|
|
522
607
|
ports.each_with_object({}) { |(k, v), acc| acc[Integer(k)] = Integer(v) }
|
|
523
608
|
end
|
|
@@ -580,6 +665,7 @@ module Microsandbox
|
|
|
580
665
|
# always set (defaulting to "no_restart"); everything else is included only
|
|
581
666
|
# when provided so an unset option means "leave unchanged".
|
|
582
667
|
def build_modify_opts(cpus: nil, max_cpus: nil, memory: nil, max_memory: nil,
|
|
668
|
+
root_disk_size: nil,
|
|
583
669
|
env: nil, remove_env: nil, labels: nil, remove_labels: nil, workdir: nil,
|
|
584
670
|
secrets: nil, remove_secrets: nil, policy: nil, dry_run: false)
|
|
585
671
|
opts = {}
|
|
@@ -587,6 +673,7 @@ module Microsandbox
|
|
|
587
673
|
opts["max_cpus"] = Integer(max_cpus) if max_cpus
|
|
588
674
|
opts["memory"] = Integer(memory) if memory
|
|
589
675
|
opts["max_memory"] = Integer(max_memory) if max_memory
|
|
676
|
+
opts["root_disk_size"] = Integer(root_disk_size) if root_disk_size
|
|
590
677
|
opts["env"] = stringify(env) if env
|
|
591
678
|
opts["remove_env"] = Array(remove_env).map(&:to_s) if remove_env
|
|
592
679
|
opts["labels"] = stringify(labels) if labels
|
|
@@ -773,6 +860,71 @@ module Microsandbox
|
|
|
773
860
|
end
|
|
774
861
|
|
|
775
862
|
# Normalize the `dns:` config Hash for the native layer.
|
|
863
|
+
# Normalize the `rate_limiter:` Hash (v0.6.9) — per-direction
|
|
864
|
+
# bandwidth/ops token buckets — for the native layer. Mirrors the Python
|
|
865
|
+
# SDK's `NetworkRateLimiter`/`RateLimiter`/`TokenBucket` shapes as plain
|
|
866
|
+
# Hashes: `{egress: {bandwidth: {size:, refill_time_ms:, one_time_burst:},
|
|
867
|
+
# ops: {...}}, ingress: {...}}`.
|
|
868
|
+
def normalize_rate_limiter(rl)
|
|
869
|
+
raise ArgumentError, "rate_limiter: must be a Hash" unless rl.is_a?(Hash)
|
|
870
|
+
out = {}
|
|
871
|
+
%i[egress ingress].each do |dir|
|
|
872
|
+
spec = fetch_opt(rl, dir)
|
|
873
|
+
next if spec.nil?
|
|
874
|
+
unless spec.is_a?(Hash)
|
|
875
|
+
raise ArgumentError, "rate_limiter #{dir}: must be a Hash"
|
|
876
|
+
end
|
|
877
|
+
dout = {}
|
|
878
|
+
%i[bandwidth ops].each do |dim|
|
|
879
|
+
bucket = fetch_opt(spec, dim)
|
|
880
|
+
next if bucket.nil?
|
|
881
|
+
unless bucket.is_a?(Hash)
|
|
882
|
+
raise ArgumentError, "rate_limiter #{dir} #{dim}: must be a Hash"
|
|
883
|
+
end
|
|
884
|
+
size = fetch_opt(bucket, :size)
|
|
885
|
+
refill = fetch_opt(bucket, :refill_time_ms)
|
|
886
|
+
if size.nil? || refill.nil?
|
|
887
|
+
raise ArgumentError,
|
|
888
|
+
"rate_limiter #{dir} #{dim}: requires size: and refill_time_ms:"
|
|
889
|
+
end
|
|
890
|
+
bout = {"size" => Integer(size), "refill_time_ms" => Integer(refill)}
|
|
891
|
+
burst = fetch_opt(bucket, :one_time_burst)
|
|
892
|
+
bout["one_time_burst"] = Integer(burst) if burst
|
|
893
|
+
dout[dim.to_s] = bout
|
|
894
|
+
end
|
|
895
|
+
out[dir.to_s] = dout
|
|
896
|
+
end
|
|
897
|
+
out
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# Normalize the `vsock:` create option (v0.6.9) into the native array of
|
|
901
|
+
# route Hashes. Accepts the Python SDK's two shapes: a Hash of
|
|
902
|
+
# `{ host_socket => port }` (stream sockets), or an Array of
|
|
903
|
+
# `{host_socket:, port:, socket_type: :stream|:dgram}` Hashes.
|
|
904
|
+
def normalize_vsock(vsock)
|
|
905
|
+
if vsock.is_a?(Hash)
|
|
906
|
+
vsock.map { |path, port| {"host_socket" => path.to_s, "port" => Integer(port)} }
|
|
907
|
+
elsif vsock.is_a?(Array)
|
|
908
|
+
vsock.map do |route|
|
|
909
|
+
unless route.is_a?(Hash)
|
|
910
|
+
raise ArgumentError, "vsock: array entries must be Hashes {host_socket:, port:, socket_type:}"
|
|
911
|
+
end
|
|
912
|
+
path = fetch_opt(route, :host_socket)
|
|
913
|
+
port = fetch_opt(route, :port)
|
|
914
|
+
if path.nil? || port.nil?
|
|
915
|
+
raise ArgumentError, "vsock route requires host_socket: and port:"
|
|
916
|
+
end
|
|
917
|
+
out = {"host_socket" => path.to_s, "port" => Integer(port)}
|
|
918
|
+
st = fetch_opt(route, :socket_type)
|
|
919
|
+
out["socket_type"] = st.to_s if st
|
|
920
|
+
out
|
|
921
|
+
end
|
|
922
|
+
else
|
|
923
|
+
raise ArgumentError,
|
|
924
|
+
"vsock: must be a Hash of {host_socket => port} or an Array of route Hashes"
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
776
928
|
def normalize_dns(dns)
|
|
777
929
|
raise ArgumentError, "dns: must be a Hash" unless dns.is_a?(Hash)
|
|
778
930
|
out = {}
|
|
@@ -870,9 +1022,29 @@ module Microsandbox
|
|
|
870
1022
|
h["format"] = spec["format"].to_s if spec["format"]
|
|
871
1023
|
h["fstype"] = spec["fstype"].to_s if spec["fstype"]
|
|
872
1024
|
h
|
|
1025
|
+
when "flat"
|
|
1026
|
+
%w[path format].each do |key|
|
|
1027
|
+
if spec[key]
|
|
1028
|
+
raise ArgumentError, "root_disk #{key}: is only valid for the disk kind"
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
h = {"kind" => "flat"}
|
|
1032
|
+
if spec["size_mib"]
|
|
1033
|
+
h["size_mib"] = coerce_root_disk_size(spec["size_mib"], "root_disk size_mib:")
|
|
1034
|
+
end
|
|
1035
|
+
h["fstype"] = spec["fstype"].to_s if spec["fstype"]
|
|
1036
|
+
if spec["clone"]
|
|
1037
|
+
clone = spec["clone"].to_s
|
|
1038
|
+
unless %w[auto copy reflink].include?(clone)
|
|
1039
|
+
raise ArgumentError,
|
|
1040
|
+
"unknown root_disk clone strategy #{clone.inspect} (expected auto/copy/reflink)"
|
|
1041
|
+
end
|
|
1042
|
+
h["clone"] = clone
|
|
1043
|
+
end
|
|
1044
|
+
h
|
|
873
1045
|
else
|
|
874
1046
|
raise ArgumentError,
|
|
875
|
-
"unknown root_disk kind #{kind.inspect} (expected managed/tmpfs/disk)"
|
|
1047
|
+
"unknown root_disk kind #{kind.inspect} (expected managed/tmpfs/disk/flat)"
|
|
876
1048
|
end
|
|
877
1049
|
end
|
|
878
1050
|
|
|
@@ -1058,6 +1230,21 @@ module Microsandbox
|
|
|
1058
1230
|
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
1059
1231
|
end
|
|
1060
1232
|
|
|
1233
|
+
# Run the image's resolved OCI `ENTRYPOINT` and `CMD` — the **default
|
|
1234
|
+
# workload** (runtime v0.6.9) — and collect output. {Sandbox.create} is
|
|
1235
|
+
# strictly boot-only, so this is how the image's own command gets
|
|
1236
|
+
# executed; override the durable CMD at create time via `cmd:`.
|
|
1237
|
+
#
|
|
1238
|
+
# Options match {#exec} minus the command itself.
|
|
1239
|
+
# @return [ExecOutput]
|
|
1240
|
+
# @raise [NoDefaultCommandError] when the image's effective entrypoint and
|
|
1241
|
+
# CMD resolve to no executable command
|
|
1242
|
+
def exec_default(cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
1243
|
+
ExecOutput.new(@native.exec_default(
|
|
1244
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)
|
|
1245
|
+
))
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1061
1248
|
# Run a command and stream its output as it arrives.
|
|
1062
1249
|
#
|
|
1063
1250
|
# Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
|
|
@@ -1084,6 +1271,17 @@ module Microsandbox
|
|
|
1084
1271
|
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
1085
1272
|
end
|
|
1086
1273
|
|
|
1274
|
+
# Run the default workload (see {#exec_default}) and stream its output.
|
|
1275
|
+
# @note Like {#exec_stream}, +timeout:+ is accepted but **not applied** on
|
|
1276
|
+
# the streaming path.
|
|
1277
|
+
# @return [ExecHandle]
|
|
1278
|
+
# @raise [NoDefaultCommandError] when no executable default command resolves
|
|
1279
|
+
def exec_default_stream(cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
1280
|
+
ExecHandle.new(@native.exec_default_stream(
|
|
1281
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)
|
|
1282
|
+
))
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1087
1285
|
# Attach an interactive terminal to a command in the sandbox.
|
|
1088
1286
|
#
|
|
1089
1287
|
# Puts the **host** terminal into raw mode and forwards keystrokes (and
|
|
@@ -1123,6 +1321,32 @@ module Microsandbox
|
|
|
1123
1321
|
@native.attach_shell
|
|
1124
1322
|
end
|
|
1125
1323
|
|
|
1324
|
+
# Attach an interactive terminal to the image's resolved OCI `ENTRYPOINT`
|
|
1325
|
+
# and `CMD` — the default workload (runtime v0.6.9). See {#attach} for the
|
|
1326
|
+
# host-TTY requirements and {#exec_default} for default-workload semantics.
|
|
1327
|
+
#
|
|
1328
|
+
# @param cwd [String, nil] working directory
|
|
1329
|
+
# @param user [String, nil] user to run as
|
|
1330
|
+
# @param env [Hash, nil] extra environment variables
|
|
1331
|
+
# @param detach_keys [String, nil] detach sequence (default "ctrl-]")
|
|
1332
|
+
# @param rlimits [Hash, nil] resource limits (see {#exec})
|
|
1333
|
+
# @return [Integer] the workload's exit code (or the code at detach)
|
|
1334
|
+
# @raise [NoDefaultCommandError] when no executable default command resolves
|
|
1335
|
+
def attach_default(cwd: nil, user: nil, env: nil, detach_keys: nil, rlimits: nil)
|
|
1336
|
+
opts = {}
|
|
1337
|
+
opts["cwd"] = cwd.to_s if cwd
|
|
1338
|
+
opts["user"] = user.to_s if user
|
|
1339
|
+
opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
|
|
1340
|
+
opts["detach_keys"] = detach_keys.to_s if detach_keys
|
|
1341
|
+
if rlimits
|
|
1342
|
+
opts["rlimits"] = rlimits.map do |resource, limit|
|
|
1343
|
+
soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
|
|
1344
|
+
[resource.to_s, Integer(soft), Integer(hard)]
|
|
1345
|
+
end
|
|
1346
|
+
end
|
|
1347
|
+
@native.attach_default(opts)
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1126
1350
|
# Guest filesystem operations.
|
|
1127
1351
|
# @return [FS]
|
|
1128
1352
|
def fs
|
|
@@ -1183,6 +1407,11 @@ module Microsandbox
|
|
|
1183
1407
|
# @param max_cpus [Integer, nil] desired boot-time maximum vCPU ceiling
|
|
1184
1408
|
# @param memory [Integer, nil] desired effective guest memory in MiB
|
|
1185
1409
|
# @param max_memory [Integer, nil] desired boot-time maximum memory (MiB)
|
|
1410
|
+
# @param root_disk_size [Integer, nil] desired root-disk size in MiB
|
|
1411
|
+
# (runtime v0.6.9): grows the sandbox-owned managed upper or flat root
|
|
1412
|
+
# disk. Growth-only, applied while the sandbox is stopped —
|
|
1413
|
+
# restart/next-start semantics and backing-specific limits are enforced
|
|
1414
|
+
# by the runtime
|
|
1186
1415
|
# @param env [Hash, nil] environment variables to set for future execs
|
|
1187
1416
|
# @param remove_env [Array<String>, nil] environment variable names to remove
|
|
1188
1417
|
# @param labels [Hash, nil] labels to set
|
|
@@ -1348,8 +1577,9 @@ module Microsandbox
|
|
|
1348
1577
|
when :pipe
|
|
1349
1578
|
unless pipe_ok
|
|
1350
1579
|
raise ArgumentError,
|
|
1351
|
-
"stdin: :pipe is only valid for
|
|
1352
|
-
"
|
|
1580
|
+
"stdin: :pipe is only valid for the streaming variants (exec_stream/" \
|
|
1581
|
+
"shell_stream/exec_default_stream) — a blocking exec cannot expose a " \
|
|
1582
|
+
"writable stdin sink; pass a String to feed bytes"
|
|
1353
1583
|
end
|
|
1354
1584
|
opts["stdin_pipe"] = true
|
|
1355
1585
|
when Symbol
|
|
@@ -115,19 +115,21 @@ module Microsandbox
|
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
# The result of {Snapshot.verify}.
|
|
119
|
-
#
|
|
120
|
-
#
|
|
118
|
+
# The result of {Snapshot.verify}. Since runtime v0.6.9 payload integrity is
|
|
119
|
+
# recorded only when the snapshot was created with `record_integrity: true`:
|
|
120
|
+
# such snapshots report `:verified` (an integrity mismatch raises
|
|
121
|
+
# {SnapshotIntegrityError} instead of returning), snapshots without recorded
|
|
122
|
+
# integrity report `:not_recorded` with `algorithm`/`content_digest` nil.
|
|
121
123
|
class SnapshotVerifyReport
|
|
122
124
|
# @return [String] descriptor digest
|
|
123
125
|
attr_reader :digest
|
|
124
126
|
# @return [String] artifact directory path
|
|
125
127
|
attr_reader :path
|
|
126
|
-
# @return [Symbol] :verified
|
|
128
|
+
# @return [Symbol] :verified or :not_recorded
|
|
127
129
|
attr_reader :status
|
|
128
|
-
# @return [String] digest algorithm
|
|
130
|
+
# @return [String, nil] digest algorithm (nil when :not_recorded)
|
|
129
131
|
attr_reader :algorithm
|
|
130
|
-
# @return [String] matched content digest
|
|
132
|
+
# @return [String, nil] matched content digest (nil when :not_recorded)
|
|
131
133
|
attr_reader :content_digest
|
|
132
134
|
|
|
133
135
|
def initialize(data)
|
|
@@ -161,8 +163,10 @@ module Microsandbox
|
|
|
161
163
|
# is written at `dest_dir/<name>` (default: the snapshots dir)
|
|
162
164
|
# @param labels [Hash, nil] user labels
|
|
163
165
|
# @param force [Boolean] overwrite an existing artifact at the destination
|
|
164
|
-
# @param record_integrity [Boolean]
|
|
165
|
-
#
|
|
166
|
+
# @param record_integrity [Boolean] record persistent payload integrity
|
|
167
|
+
# (a Merkle content digest) in the artifact — opt-in since runtime
|
|
168
|
+
# v0.6.9 because hashing large allocated uppers is expensive; without
|
|
169
|
+
# it {Snapshot.verify} reports `:not_recorded`
|
|
166
170
|
# @param resumable [Boolean] request a resumable (memory+device) snapshot;
|
|
167
171
|
# raises {UnsupportedError} until VM pause/resume lands upstream
|
|
168
172
|
# @return [SnapshotInfo]
|
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.13.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.9"
|
|
19
19
|
end
|
data/lib/microsandbox/volume.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Microsandbox
|
|
|
13
13
|
@name = data["name"]
|
|
14
14
|
@path = data["path"]
|
|
15
15
|
@kind = data["kind"]
|
|
16
|
+
@default = data["default"]
|
|
16
17
|
@quota_mib = data["quota_mib"]
|
|
17
18
|
@used_bytes = data["used_bytes"]
|
|
18
19
|
@capacity_bytes = data["capacity_bytes"]
|
|
@@ -28,6 +29,13 @@ module Microsandbox
|
|
|
28
29
|
@kind&.to_sym
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# Whether this is the backend's default volume (runtime v0.6.9, see
|
|
33
|
+
# {Volume.get_default}).
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def default?
|
|
36
|
+
!!@default
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
# @return [Time, nil]
|
|
32
40
|
def created_at
|
|
33
41
|
@created_at_ms && Time.at(@created_at_ms / 1000.0)
|
|
@@ -154,6 +162,14 @@ module Microsandbox
|
|
|
154
162
|
VolumeInfo.new(Native::Volume.get(name.to_s))
|
|
155
163
|
end
|
|
156
164
|
|
|
165
|
+
# The backend's default volume (runtime v0.6.9). Cloud backend only —
|
|
166
|
+
# the full {VolumeInfo#fs} surface works against it; the local backend
|
|
167
|
+
# raises {UnsupportedError} to avoid accidental host access.
|
|
168
|
+
# @return [VolumeInfo]
|
|
169
|
+
def get_default
|
|
170
|
+
VolumeInfo.new(Native::Volume.get_default)
|
|
171
|
+
end
|
|
172
|
+
|
|
157
173
|
# All volumes.
|
|
158
174
|
# @return [Array<VolumeInfo>]
|
|
159
175
|
def list
|
data/lib/microsandbox.rb
CHANGED
|
@@ -17,6 +17,7 @@ rescue LoadError
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
require_relative "microsandbox/errors"
|
|
20
|
+
require_relative "microsandbox/backend_info"
|
|
20
21
|
require_relative "microsandbox/exec_output"
|
|
21
22
|
require_relative "microsandbox/exec_handle"
|
|
22
23
|
require_relative "microsandbox/fs"
|
|
@@ -172,9 +173,13 @@ module Microsandbox
|
|
|
172
173
|
|
|
173
174
|
# Install a process-wide default backend (v0.5.8 backend routing). Without a
|
|
174
175
|
# call to this, operations use a local libkrun backend; the env/profile
|
|
175
|
-
# ladder (`MSB_BACKEND
|
|
176
|
-
#
|
|
177
|
-
#
|
|
176
|
+
# ladder (`MSB_BACKEND` → `MSB_PROFILE` → `~/.microsandbox/config.json`) is
|
|
177
|
+
# resolved lazily on first use. Since runtime v0.6.9 a bare `MSB_API_KEY`
|
|
178
|
+
# no longer selects the cloud — cloud intent must be explicit via
|
|
179
|
+
# `MSB_BACKEND=cloud` (paired with `MSB_API_URL`/`MSB_API_KEY`), a cloud
|
|
180
|
+
# profile, or this method; invalid cloud config raises
|
|
181
|
+
# {InvalidConfigError} instead of falling back to local. Call once at
|
|
182
|
+
# startup, before any sandbox operations.
|
|
178
183
|
#
|
|
179
184
|
# @param kind ["local","cloud", Symbol] backend kind
|
|
180
185
|
# @param url [String, nil] cloud control-plane URL (cloud, unless `profile:`)
|
|
@@ -216,6 +221,14 @@ module Microsandbox
|
|
|
216
221
|
Native.default_backend_kind.to_sym
|
|
217
222
|
end
|
|
218
223
|
|
|
224
|
+
# Secret-safe description of the active default backend (runtime v0.6.9).
|
|
225
|
+
# Like {default_backend_kind}, the first call freezes ambient env/profile
|
|
226
|
+
# resolution for the process. The API key is never included.
|
|
227
|
+
# @return [BackendInfo]
|
|
228
|
+
def default_backend_info
|
|
229
|
+
BackendInfo.new(Native.default_backend_info)
|
|
230
|
+
end
|
|
231
|
+
|
|
219
232
|
# Latest resource-usage snapshot for every running sandbox, keyed by name.
|
|
220
233
|
# Mirrors the official `all_sandbox_metrics`/`allSandboxMetrics` helpers.
|
|
221
234
|
# @return [Hash{String => Metrics}]
|