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.
@@ -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, vsock: 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 quota_mib:.
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
@@ -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.15.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.14"
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.15.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