microsandbox-rb 0.12.0 → 0.14.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 +152 -1
- data/Cargo.lock +1035 -731
- data/DESIGN.md +107 -18
- data/README.md +124 -16
- data/ext/microsandbox/Cargo.toml +23 -7
- data/ext/microsandbox/src/backend.rs +22 -0
- data/ext/microsandbox/src/error.rs +4 -0
- data/ext/microsandbox/src/sandbox.rs +198 -9
- data/ext/microsandbox/src/snapshot.rs +10 -3
- data/ext/microsandbox/src/volume.rs +9 -0
- data/lib/microsandbox/backend_info.rb +46 -0
- data/lib/microsandbox/errors.rb +4 -0
- data/lib/microsandbox/root_disk.rb +24 -0
- data/lib/microsandbox/sandbox.rb +181 -9
- 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 +144 -13
- data/sig/microsandbox.rbs +29 -2
- metadata +2 -1
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -288,7 +288,14 @@ module Microsandbox
|
|
|
288
288
|
# @param hostname [String, nil] guest hostname
|
|
289
289
|
# @param labels [Hash, nil] metadata labels
|
|
290
290
|
# @param scripts [Hash, nil] named scripts to install
|
|
291
|
-
# @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.
|
|
292
299
|
# @param ports [Hash, nil] host_port => guest_port TCP publications
|
|
293
300
|
# @param ports_udp [Hash, nil] host_port => guest_port UDP publications
|
|
294
301
|
# @param volumes [Hash, nil] guest_path => mount spec. Each value is a host
|
|
@@ -322,6 +329,17 @@ module Microsandbox
|
|
|
322
329
|
# @param ipv6_pool [String, nil] guest IPv6 address pool CIDR
|
|
323
330
|
# @param max_connections [Integer, nil] cap on concurrent proxied connections
|
|
324
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.
|
|
325
343
|
# @param from_snapshot [String, nil] boot from a snapshot name or digest
|
|
326
344
|
# instead of an image (mutually exclusive with `image:`)
|
|
327
345
|
# @param fstype [String, nil] inner filesystem type (e.g. "ext4") when
|
|
@@ -343,9 +361,12 @@ module Microsandbox
|
|
|
343
361
|
# disk (runtime v0.6.7). An Integer is the managed ext4 upper's size cap
|
|
344
362
|
# in MiB (default kind, 4 GiB when unset); a Hash picks a kind via the
|
|
345
363
|
# {RootDisk} factory — `RootDisk.managed(8192)`, `RootDisk.tmpfs(2048)`
|
|
346
|
-
# (RAM-backed, pristine on every boot),
|
|
364
|
+
# (RAM-backed, pristine on every boot),
|
|
347
365
|
# `RootDisk.disk("./scratch.img", format: "raw", fstype: "ext4")`
|
|
348
|
-
# (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).
|
|
349
370
|
# @param oci_upper_size [Integer, nil] deprecated alias for
|
|
350
371
|
# `root_disk: <Integer>` (the managed kind); warns, and conflicts with
|
|
351
372
|
# `root_disk:`
|
|
@@ -418,9 +439,9 @@ module Microsandbox
|
|
|
418
439
|
def build_create_opts(image: nil, cpus: nil, max_cpus: nil, memory: nil, max_memory: nil,
|
|
419
440
|
env: nil, workdir: nil,
|
|
420
441
|
shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
|
|
421
|
-
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,
|
|
422
443
|
dns: nil, tls: nil, ipv4_pool: nil, ipv6_pool: nil,
|
|
423
|
-
max_connections: nil, trust_host_cas: nil,
|
|
444
|
+
max_connections: nil, trust_host_cas: nil, rate_limiter: nil, vsock: nil,
|
|
424
445
|
patches: nil,
|
|
425
446
|
from_snapshot: nil, fstype: nil, init: nil, ephemeral: false,
|
|
426
447
|
log_level: nil, quiet_logs: false, security: nil,
|
|
@@ -465,7 +486,11 @@ module Microsandbox
|
|
|
465
486
|
opts["env"] = stringify(env) if env
|
|
466
487
|
opts["labels"] = stringify(labels) if labels
|
|
467
488
|
opts["scripts"] = stringify(scripts) if scripts
|
|
468
|
-
|
|
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?
|
|
469
494
|
opts["ports"] = intify_ports(ports) if ports
|
|
470
495
|
opts["ports_udp"] = intify_ports(ports_udp) if ports_udp
|
|
471
496
|
opts["volumes"] = normalize_volumes(volumes) if volumes
|
|
@@ -477,6 +502,8 @@ module Microsandbox
|
|
|
477
502
|
opts["ipv6_pool"] = ipv6_pool.to_s if ipv6_pool
|
|
478
503
|
opts["max_connections"] = Integer(max_connections) if max_connections
|
|
479
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
|
|
480
507
|
opts["log_level"] = log_level.to_s if log_level
|
|
481
508
|
opts["quiet_logs"] = true if quiet_logs
|
|
482
509
|
opts["security"] = security.to_s if security
|
|
@@ -638,6 +665,7 @@ module Microsandbox
|
|
|
638
665
|
# always set (defaulting to "no_restart"); everything else is included only
|
|
639
666
|
# when provided so an unset option means "leave unchanged".
|
|
640
667
|
def build_modify_opts(cpus: nil, max_cpus: nil, memory: nil, max_memory: nil,
|
|
668
|
+
root_disk_size: nil,
|
|
641
669
|
env: nil, remove_env: nil, labels: nil, remove_labels: nil, workdir: nil,
|
|
642
670
|
secrets: nil, remove_secrets: nil, policy: nil, dry_run: false)
|
|
643
671
|
opts = {}
|
|
@@ -645,6 +673,7 @@ module Microsandbox
|
|
|
645
673
|
opts["max_cpus"] = Integer(max_cpus) if max_cpus
|
|
646
674
|
opts["memory"] = Integer(memory) if memory
|
|
647
675
|
opts["max_memory"] = Integer(max_memory) if max_memory
|
|
676
|
+
opts["root_disk_size"] = Integer(root_disk_size) if root_disk_size
|
|
648
677
|
opts["env"] = stringify(env) if env
|
|
649
678
|
opts["remove_env"] = Array(remove_env).map(&:to_s) if remove_env
|
|
650
679
|
opts["labels"] = stringify(labels) if labels
|
|
@@ -831,6 +860,71 @@ module Microsandbox
|
|
|
831
860
|
end
|
|
832
861
|
|
|
833
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
|
+
|
|
834
928
|
def normalize_dns(dns)
|
|
835
929
|
raise ArgumentError, "dns: must be a Hash" unless dns.is_a?(Hash)
|
|
836
930
|
out = {}
|
|
@@ -928,9 +1022,29 @@ module Microsandbox
|
|
|
928
1022
|
h["format"] = spec["format"].to_s if spec["format"]
|
|
929
1023
|
h["fstype"] = spec["fstype"].to_s if spec["fstype"]
|
|
930
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
|
|
931
1045
|
else
|
|
932
1046
|
raise ArgumentError,
|
|
933
|
-
"unknown root_disk kind #{kind.inspect} (expected managed/tmpfs/disk)"
|
|
1047
|
+
"unknown root_disk kind #{kind.inspect} (expected managed/tmpfs/disk/flat)"
|
|
934
1048
|
end
|
|
935
1049
|
end
|
|
936
1050
|
|
|
@@ -1116,6 +1230,21 @@ module Microsandbox
|
|
|
1116
1230
|
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
1117
1231
|
end
|
|
1118
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
|
+
|
|
1119
1248
|
# Run a command and stream its output as it arrives.
|
|
1120
1249
|
#
|
|
1121
1250
|
# Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
|
|
@@ -1142,6 +1271,17 @@ module Microsandbox
|
|
|
1142
1271
|
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
1143
1272
|
end
|
|
1144
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
|
+
|
|
1145
1285
|
# Attach an interactive terminal to a command in the sandbox.
|
|
1146
1286
|
#
|
|
1147
1287
|
# Puts the **host** terminal into raw mode and forwards keystrokes (and
|
|
@@ -1181,6 +1321,32 @@ module Microsandbox
|
|
|
1181
1321
|
@native.attach_shell
|
|
1182
1322
|
end
|
|
1183
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
|
+
|
|
1184
1350
|
# Guest filesystem operations.
|
|
1185
1351
|
# @return [FS]
|
|
1186
1352
|
def fs
|
|
@@ -1241,6 +1407,11 @@ module Microsandbox
|
|
|
1241
1407
|
# @param max_cpus [Integer, nil] desired boot-time maximum vCPU ceiling
|
|
1242
1408
|
# @param memory [Integer, nil] desired effective guest memory in MiB
|
|
1243
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
|
|
1244
1415
|
# @param env [Hash, nil] environment variables to set for future execs
|
|
1245
1416
|
# @param remove_env [Array<String>, nil] environment variable names to remove
|
|
1246
1417
|
# @param labels [Hash, nil] labels to set
|
|
@@ -1406,8 +1577,9 @@ module Microsandbox
|
|
|
1406
1577
|
when :pipe
|
|
1407
1578
|
unless pipe_ok
|
|
1408
1579
|
raise ArgumentError,
|
|
1409
|
-
"stdin: :pipe is only valid for
|
|
1410
|
-
"
|
|
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"
|
|
1411
1583
|
end
|
|
1412
1584
|
opts["stdin_pipe"] = true
|
|
1413
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.14.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"
|
|
@@ -63,12 +64,13 @@ module Microsandbox
|
|
|
63
64
|
# Download and install the `msb` runtime + `libkrunfw` into
|
|
64
65
|
# `~/.microsandbox` (idempotent).
|
|
65
66
|
#
|
|
66
|
-
#
|
|
67
|
-
# runtime
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
67
|
+
# This gem is SDK-only: nothing is provisioned at build/install time. The
|
|
68
|
+
# runtime comes from the companion `microsandbox-rb-binaries` gem when it is
|
|
69
|
+
# installed (see {ensure_runtime!}); otherwise it is fetched into
|
|
70
|
+
# `~/.microsandbox` on first use. Call this explicitly to provision ahead of
|
|
71
|
+
# time (e.g. while baking a container image) so the first {Sandbox.create}
|
|
72
|
+
# doesn't pay the download. Not needed — and not used — when the binaries
|
|
73
|
+
# gem supplies the runtime.
|
|
72
74
|
# @return [nil]
|
|
73
75
|
def install
|
|
74
76
|
Native.install
|
|
@@ -101,10 +103,14 @@ module Microsandbox
|
|
|
101
103
|
|
|
102
104
|
# Ensure the `msb` runtime + `libkrunfw` are present *and version-matched*,
|
|
103
105
|
# provisioning them on first use if not. Called automatically by
|
|
104
|
-
# {Sandbox.create}/{Sandbox.start} so
|
|
105
|
-
#
|
|
106
|
+
# {Sandbox.create}/{Sandbox.start} so a freshly installed gem gets a working
|
|
107
|
+
# runtime without a manual {install} step.
|
|
106
108
|
#
|
|
107
|
-
# Runs at most once per process.
|
|
109
|
+
# Runs at most once per process. When the companion `microsandbox-rb-binaries`
|
|
110
|
+
# gem supplies the runtime (it was activated at load time and its `msb` is
|
|
111
|
+
# what the resolver now returns from {runtime_path}), nothing is downloaded
|
|
112
|
+
# or touched in `~/.microsandbox` — the bundled binaries are already the
|
|
113
|
+
# matching version. Opt out of the download by setting
|
|
108
114
|
# `MICROSANDBOX_NO_AUTO_INSTALL` (e.g. air-gapped hosts that provision the
|
|
109
115
|
# runtime out of band); the runtime is then left untouched and a missing or
|
|
110
116
|
# stale one surfaces at the operation itself.
|
|
@@ -128,6 +134,14 @@ module Microsandbox
|
|
|
128
134
|
# uses the same lazy env/profile/config ladder every operation already
|
|
129
135
|
# consults, so this adds no work for local hosts (the common case).
|
|
130
136
|
return if default_backend_kind == :cloud
|
|
137
|
+
# The binaries gem won the resolver: its vendored msb (and the libkrunfw
|
|
138
|
+
# beside it) are exactly the version this gem was built for, so there is
|
|
139
|
+
# nothing to verify or download. (If `MSB_PATH` overrides it, the user owns
|
|
140
|
+
# the runtime and we fall through to the existing behaviour.)
|
|
141
|
+
if bundled_runtime_active?
|
|
142
|
+
@runtime_ready = true
|
|
143
|
+
return
|
|
144
|
+
end
|
|
131
145
|
# Opted out: the caller manages the runtime out of band, so don't fetch,
|
|
132
146
|
# verify, or repair it here. Memoize the decision (the env var is stable for
|
|
133
147
|
# the process); the operation resolves `msb` itself and surfaces any problem.
|
|
@@ -138,7 +152,8 @@ module Microsandbox
|
|
|
138
152
|
|
|
139
153
|
unless installed?
|
|
140
154
|
warn "[microsandbox] runtime (msb + libkrunfw) not found; " \
|
|
141
|
-
"downloading to ~/.microsandbox (set MICROSANDBOX_NO_AUTO_INSTALL to skip
|
|
155
|
+
"downloading to ~/.microsandbox (set MICROSANDBOX_NO_AUTO_INSTALL to skip, " \
|
|
156
|
+
"or install the microsandbox-rb-binaries gem to ship it with your bundle)..."
|
|
142
157
|
end
|
|
143
158
|
install
|
|
144
159
|
@runtime_ready = true
|
|
@@ -154,9 +169,17 @@ module Microsandbox
|
|
|
154
169
|
# resolver, below only the `MSB_PATH` environment variable). Process-level
|
|
155
170
|
# and set-once: a second call is silently ignored, and the `MSB_PATH`
|
|
156
171
|
# environment variable still wins. Mirrors {libkrunfw_path=}.
|
|
172
|
+
#
|
|
173
|
+
# The companion `microsandbox-rb-binaries` gem claims this same slot when
|
|
174
|
+
# `require "microsandbox"` activates it, so with that gem installed this
|
|
175
|
+
# setter is a no-op — use `MSB_PATH` to override a bundled runtime.
|
|
157
176
|
# @param path [String]
|
|
158
177
|
# @return [void]
|
|
159
178
|
def runtime_path=(path)
|
|
179
|
+
if @bundled_msb_path && path.to_s != @bundled_msb_path
|
|
180
|
+
warn "[microsandbox] runtime_path= ignored: the microsandbox-rb-binaries gem already " \
|
|
181
|
+
"claimed the set-once SDK slot (#{@bundled_msb_path}); set MSB_PATH to override it"
|
|
182
|
+
end
|
|
160
183
|
Native.set_runtime_msb_path(path.to_s)
|
|
161
184
|
end
|
|
162
185
|
|
|
@@ -172,9 +195,13 @@ module Microsandbox
|
|
|
172
195
|
|
|
173
196
|
# Install a process-wide default backend (v0.5.8 backend routing). Without a
|
|
174
197
|
# call to this, operations use a local libkrun backend; the env/profile
|
|
175
|
-
# ladder (`MSB_BACKEND
|
|
176
|
-
#
|
|
177
|
-
#
|
|
198
|
+
# ladder (`MSB_BACKEND` → `MSB_PROFILE` → `~/.microsandbox/config.json`) is
|
|
199
|
+
# resolved lazily on first use. Since runtime v0.6.9 a bare `MSB_API_KEY`
|
|
200
|
+
# no longer selects the cloud — cloud intent must be explicit via
|
|
201
|
+
# `MSB_BACKEND=cloud` (paired with `MSB_API_URL`/`MSB_API_KEY`), a cloud
|
|
202
|
+
# profile, or this method; invalid cloud config raises
|
|
203
|
+
# {InvalidConfigError} instead of falling back to local. Call once at
|
|
204
|
+
# startup, before any sandbox operations.
|
|
178
205
|
#
|
|
179
206
|
# @param kind ["local","cloud", Symbol] backend kind
|
|
180
207
|
# @param url [String, nil] cloud control-plane URL (cloud, unless `profile:`)
|
|
@@ -216,6 +243,14 @@ module Microsandbox
|
|
|
216
243
|
Native.default_backend_kind.to_sym
|
|
217
244
|
end
|
|
218
245
|
|
|
246
|
+
# Secret-safe description of the active default backend (runtime v0.6.9).
|
|
247
|
+
# Like {default_backend_kind}, the first call freezes ambient env/profile
|
|
248
|
+
# resolution for the process. The API key is never included.
|
|
249
|
+
# @return [BackendInfo]
|
|
250
|
+
def default_backend_info
|
|
251
|
+
BackendInfo.new(Native.default_backend_info)
|
|
252
|
+
end
|
|
253
|
+
|
|
219
254
|
# Latest resource-usage snapshot for every running sandbox, keyed by name.
|
|
220
255
|
# Mirrors the official `all_sandbox_metrics`/`allSandboxMetrics` helpers.
|
|
221
256
|
# @return [Hash{String => Metrics}]
|
|
@@ -244,5 +279,101 @@ module Microsandbox
|
|
|
244
279
|
v = ENV["MICROSANDBOX_NO_AUTO_INSTALL"]
|
|
245
280
|
!v.nil? && !v.empty? && !%w[0 false no].include?(v.downcase)
|
|
246
281
|
end
|
|
282
|
+
|
|
283
|
+
# Wire the companion `microsandbox-rb-binaries` gem into the core resolver.
|
|
284
|
+
# Runs once, at `require "microsandbox"` time (like the Node SDK, which pushes
|
|
285
|
+
# its platform package's msb into the same set-once SDK slot at module load)
|
|
286
|
+
# so every entry point that spawns msb — not just {Sandbox.create} — sees it.
|
|
287
|
+
#
|
|
288
|
+
# The gem is optional and has no dependency edge to this one (cloud-only
|
|
289
|
+
# users skip the ~50 MB download; RubyGems has no optional dependencies), so
|
|
290
|
+
# discovery is by require: absent → nothing happens and the resolver's lower
|
|
291
|
+
# tiers (`~/.microsandbox`, then `PATH`) plus the first-use download take
|
|
292
|
+
# over. Present but built for a different upstream runtime → warn and skip
|
|
293
|
+
# it rather than hand the core a mismatched msb (a stale runtime passes an
|
|
294
|
+
# exists-check and then fails every create on a wire-protocol mismatch).
|
|
295
|
+
# Only `MSB_PATH` (env) outranks the slot claimed here.
|
|
296
|
+
#
|
|
297
|
+
# Never raises: a broken companion gem must not take `require "microsandbox"`
|
|
298
|
+
# down with it.
|
|
299
|
+
# @return [String, nil] the activated msb path
|
|
300
|
+
def activate_bundled_runtime!
|
|
301
|
+
@bundled_msb_path = nil
|
|
302
|
+
begin
|
|
303
|
+
# Pin the lockstep version when RubyGems (not Bundler) picks the gem, so
|
|
304
|
+
# a newer/older companion left around doesn't get activated over the
|
|
305
|
+
# matching one. Under Bundler the Gemfile already decides; a companion
|
|
306
|
+
# that isn't in the bundle raises here and `require` then fails below.
|
|
307
|
+
gem "microsandbox-rb-binaries", "= #{VERSION}"
|
|
308
|
+
rescue Gem::LoadError
|
|
309
|
+
# Not installed at this version — `require` settles it.
|
|
310
|
+
end
|
|
311
|
+
begin
|
|
312
|
+
require "microsandbox/binaries"
|
|
313
|
+
rescue LoadError
|
|
314
|
+
return nil
|
|
315
|
+
end
|
|
316
|
+
# A bare require activates the newest gem of ANY name that ships this
|
|
317
|
+
# feature path; only accept the companion gem itself, this gem's own tree
|
|
318
|
+
# (a source checkout, where Bundler's path gem spans the whole repo and so
|
|
319
|
+
# owns binaries/lib too), or a plain load path (RUBYLIB/-I) no gem owns.
|
|
320
|
+
owner = bundled_runtime_owner
|
|
321
|
+
if owner && !TRUSTED_BINARIES_OWNERS.include?(owner)
|
|
322
|
+
warn "[microsandbox] ignoring microsandbox/binaries provided by the #{owner} gem " \
|
|
323
|
+
"(only microsandbox-rb-binaries is trusted for the bundled runtime)"
|
|
324
|
+
return nil
|
|
325
|
+
end
|
|
326
|
+
# Lockstep gate on BOTH constants. The runtime tag is what the wire
|
|
327
|
+
# protocol depends on; the gem version is the documented contract
|
|
328
|
+
# ("install both at the same version") and also covers this file's own
|
|
329
|
+
# API/packaging — the `gem "…", "= VERSION"` pin above is best-effort (it
|
|
330
|
+
# raises under Bundler whenever the bundle picked any other version, and
|
|
331
|
+
# that is swallowed), so it cannot be what enforces it.
|
|
332
|
+
unless Binaries::VERSION == VERSION && Binaries::RUNTIME_VERSION == RUNTIME_VERSION
|
|
333
|
+
warn "[microsandbox] ignoring microsandbox-rb-binaries #{Binaries::VERSION} " \
|
|
334
|
+
"(runtime #{Binaries::RUNTIME_VERSION}): microsandbox-rb #{VERSION} " \
|
|
335
|
+
"(runtime #{RUNTIME_VERSION}) needs the companion gem at the same version. " \
|
|
336
|
+
"Install both gems at the same version; falling back to ~/.microsandbox."
|
|
337
|
+
return nil
|
|
338
|
+
end
|
|
339
|
+
msb = Binaries.msb_path
|
|
340
|
+
unless msb && Binaries.libkrunfw_path
|
|
341
|
+
warn "[microsandbox] microsandbox-rb-binaries #{Binaries::VERSION} is installed but " \
|
|
342
|
+
"carries no runtime under #{Binaries.root}; falling back to ~/.microsandbox."
|
|
343
|
+
return nil
|
|
344
|
+
end
|
|
345
|
+
Native.set_runtime_msb_path(msb)
|
|
346
|
+
@bundled_msb_path = msb
|
|
347
|
+
rescue StandardError, ScriptError => e
|
|
348
|
+
# ScriptError too: a corrupt or newer-syntax binaries.rb raises
|
|
349
|
+
# SyntaxError (not a StandardError) out of `require`, and the optional
|
|
350
|
+
# companion must never take `require "microsandbox"` down with it.
|
|
351
|
+
warn "[microsandbox] could not activate microsandbox-rb-binaries: #{e.class}: #{e.message}"
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Gems allowed to provide `microsandbox/binaries` (see activate_bundled_runtime!).
|
|
356
|
+
TRUSTED_BINARIES_OWNERS = %w[microsandbox-rb-binaries microsandbox-rb].freeze
|
|
357
|
+
private_constant :TRUSTED_BINARIES_OWNERS
|
|
358
|
+
|
|
359
|
+
# Name of the loaded gem whose files define Microsandbox::Binaries, or nil
|
|
360
|
+
# when it came from a bare load path.
|
|
361
|
+
def bundled_runtime_owner
|
|
362
|
+
source = Binaries.method(:msb_path).source_location&.first
|
|
363
|
+
return nil unless source
|
|
364
|
+
spec = Gem.loaded_specs.values.find { |s| source.start_with?(File.join(s.full_gem_path, "")) }
|
|
365
|
+
spec&.name
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Whether the binaries gem's msb is what the resolver actually returns — i.e.
|
|
369
|
+
# it was activated and nothing higher in the ladder (`MSB_PATH`) overrides it.
|
|
370
|
+
def bundled_runtime_active?
|
|
371
|
+
return false unless @bundled_msb_path
|
|
372
|
+
runtime_path == @bundled_msb_path
|
|
373
|
+
rescue Microsandbox::Error
|
|
374
|
+
false
|
|
375
|
+
end
|
|
247
376
|
end
|
|
377
|
+
|
|
378
|
+
send(:activate_bundled_runtime!)
|
|
248
379
|
end
|