microsandbox-rb 0.5.7

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.
@@ -0,0 +1,461 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Microsandbox
4
+ # Lightweight metadata about a sandbox, returned by {Sandbox.get} and
5
+ # {Sandbox.list}. This is a snapshot, not a live handle.
6
+ class SandboxInfo
7
+ # @return [String]
8
+ attr_reader :name
9
+ # @return [Symbol] :running, :draining, :paused, :stopped, or :crashed
10
+ attr_reader :status
11
+
12
+ def initialize(data)
13
+ @name = data["name"]
14
+ @status = data["status"].to_sym
15
+ @created_at_ms = data["created_at_ms"]
16
+ @updated_at_ms = data["updated_at_ms"]
17
+ end
18
+
19
+ def running? = @status == :running
20
+ def stopped? = @status == :stopped
21
+
22
+ # @return [Time, nil]
23
+ def created_at
24
+ @created_at_ms && Time.at(@created_at_ms / 1000.0)
25
+ end
26
+
27
+ # @return [Time, nil]
28
+ def updated_at
29
+ @updated_at_ms && Time.at(@updated_at_ms / 1000.0)
30
+ end
31
+
32
+ def inspect
33
+ "#<Microsandbox::SandboxInfo name=#{@name.inspect} status=#{@status}>"
34
+ end
35
+ end
36
+
37
+ # The terminal observation of a stopped sandbox, returned by
38
+ # {Sandbox#wait_until_stopped}. Mirrors the official SDKs' `SandboxStopResult`.
39
+ class SandboxStopResult
40
+ # @return [String]
41
+ attr_reader :name
42
+ # @return [Symbol] :running, :draining, :paused, :stopped, or :crashed
43
+ attr_reader :status
44
+ # @return [Integer, nil] process exit code, when observed from an owned child
45
+ attr_reader :exit_code
46
+ # @return [Integer, nil] terminating signal, when observed from an owned child
47
+ attr_reader :signal
48
+ # @return [String, nil] human description of the observation source
49
+ attr_reader :source
50
+
51
+ def initialize(data)
52
+ @name = data["name"]
53
+ @status = data["status"].to_sym
54
+ @exit_code = data["exit_code"]
55
+ @signal = data["signal"]
56
+ @source = data["source"]
57
+ @observed_at_ms = data["observed_at_ms"]
58
+ end
59
+
60
+ def stopped? = @status == :stopped
61
+ def crashed? = @status == :crashed
62
+
63
+ # @return [Time] when the stopped state was observed
64
+ def observed_at
65
+ Time.at(@observed_at_ms / 1000.0)
66
+ end
67
+
68
+ def inspect
69
+ "#<Microsandbox::SandboxStopResult name=#{@name.inspect} status=#{@status}" \
70
+ "#{@exit_code ? " exit_code=#{@exit_code}" : ""}#{@signal ? " signal=#{@signal}" : ""}>"
71
+ end
72
+ end
73
+
74
+ # A running sandbox (microVM) — the primary entry point of the SDK.
75
+ #
76
+ # @example Block form (auto-stops on exit)
77
+ # Microsandbox::Sandbox.create("hello", image: "python") do |sb|
78
+ # out = sb.exec("python", ["-c", "print('hi')"])
79
+ # puts out.stdout
80
+ # end
81
+ #
82
+ # @example Manual lifecycle
83
+ # sb = Microsandbox::Sandbox.create("hello", image: "python")
84
+ # begin
85
+ # sb.shell("echo hi")
86
+ # ensure
87
+ # sb.stop
88
+ # end
89
+ class Sandbox
90
+ class << self
91
+ # Create and boot a sandbox.
92
+ #
93
+ # When a block is given the sandbox is yielded and stopped automatically
94
+ # when the block returns (the block's value is returned); otherwise the
95
+ # live {Sandbox} is returned and you are responsible for calling {#stop}.
96
+ #
97
+ # @param name [String] sandbox name (max 128 UTF-8 bytes)
98
+ # @param image [String, nil] OCI image reference (e.g. "python")
99
+ # @param cpus [Integer, nil] number of vCPUs
100
+ # @param memory [Integer, nil] memory in MiB
101
+ # @param env [Hash, nil] environment variables
102
+ # @param workdir [String, nil] working directory inside the guest
103
+ # @param shell [String, nil] default shell (for {#shell})
104
+ # @param user [String, nil] default user
105
+ # @param hostname [String, nil] guest hostname
106
+ # @param labels [Hash, nil] metadata labels
107
+ # @param scripts [Hash, nil] named scripts to install
108
+ # @param entrypoint [Array<String>, nil] image entrypoint override
109
+ # @param ports [Hash, nil] host_port => guest_port TCP publications
110
+ # @param ports_udp [Hash, nil] host_port => guest_port UDP publications
111
+ # @param network ["public_only", "none", "allow_all", "non_local", nil] network
112
+ # policy preset (default public_only)
113
+ # @param log_level ["error","warn","info","debug","trace", nil] guest log verbosity
114
+ # @param quiet_logs [Boolean] suppress sandbox process logs
115
+ # @param security ["default", "restricted", nil] exec security profile
116
+ # @param oci_upper_size [Integer, nil] writable upper-layer size cap, in MiB
117
+ # @param max_duration [Integer, nil] hard wall-clock lifetime, in seconds
118
+ # @param idle_timeout [Integer, nil] stop after this many idle seconds
119
+ # @param rlimits [Hash, nil] resource limits: { resource => limit } or
120
+ # { resource => [soft, hard] } (e.g. { nofile: 65_535 })
121
+ # @param pull_policy ["always","if-missing","never", nil] image pull behavior
122
+ # @param secrets [Array<Hash>, nil] placeholder-protected secrets, each
123
+ # { env:, value:, host: } — the value is substituted by the TLS proxy only
124
+ # for the allowed host (auto-enables TLS interception)
125
+ # @param detached [Boolean] keep running after this process exits
126
+ # @param replace [Boolean] replace an existing sandbox with the same name
127
+ # @param replace_with_timeout [Numeric, nil] replace, waiting up to N seconds
128
+ # @yieldparam sandbox [Sandbox]
129
+ # @return [Sandbox, Object] the sandbox, or the block's return value
130
+ def create(name,
131
+ image: nil, cpus: nil, memory: nil, env: nil, workdir: nil,
132
+ shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
133
+ entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
134
+ from_snapshot: nil, log_level: nil, quiet_logs: false, security: nil,
135
+ oci_upper_size: nil, max_duration: nil, idle_timeout: nil, rlimits: nil,
136
+ pull_policy: nil, secrets: nil,
137
+ detached: false, replace: false, replace_with_timeout: nil)
138
+ opts = {}
139
+ opts["image"] = image.to_s if image
140
+ opts["from_snapshot"] = from_snapshot.to_s if from_snapshot
141
+ opts["cpus"] = Integer(cpus) if cpus
142
+ opts["memory"] = Integer(memory) if memory
143
+ opts["workdir"] = workdir.to_s if workdir
144
+ opts["shell"] = shell.to_s if shell
145
+ opts["user"] = user.to_s if user
146
+ opts["hostname"] = hostname.to_s if hostname
147
+ opts["env"] = stringify(env) if env
148
+ opts["labels"] = stringify(labels) if labels
149
+ opts["scripts"] = stringify(scripts) if scripts
150
+ opts["entrypoint"] = Array(entrypoint).map(&:to_s) if entrypoint
151
+ opts["ports"] = intify_ports(ports) if ports
152
+ opts["ports_udp"] = intify_ports(ports_udp) if ports_udp
153
+ opts["volumes"] = normalize_volumes(volumes) if volumes
154
+ opts["network"] = network.to_s if network
155
+ opts["log_level"] = log_level.to_s if log_level
156
+ opts["quiet_logs"] = true if quiet_logs
157
+ opts["security"] = security.to_s if security
158
+ opts["oci_upper_size"] = Integer(oci_upper_size) if oci_upper_size
159
+ opts["max_duration"] = Integer(max_duration) if max_duration
160
+ opts["idle_timeout"] = Integer(idle_timeout) if idle_timeout
161
+ opts["rlimits"] = normalize_rlimits(rlimits) if rlimits
162
+ opts["pull_policy"] = pull_policy.to_s if pull_policy
163
+ opts["secrets"] = normalize_secrets(secrets) if secrets
164
+ opts["detached"] = true if detached
165
+ if replace_with_timeout
166
+ opts["replace_with_timeout"] = Float(replace_with_timeout)
167
+ elsif replace
168
+ opts["replace"] = true
169
+ end
170
+
171
+ sandbox = new(Native::Sandbox.create(name.to_s, opts))
172
+ return sandbox unless block_given?
173
+
174
+ begin
175
+ yield sandbox
176
+ ensure
177
+ begin
178
+ sandbox.stop
179
+ rescue Microsandbox::Error
180
+ # best-effort cleanup; ignore stop failures during teardown
181
+ end
182
+ end
183
+ end
184
+
185
+ # Restart a previously-defined sandbox by name.
186
+ # @return [Sandbox]
187
+ def start(name, detached: false)
188
+ new(Native::Sandbox.start(name.to_s, { "detached" => detached }))
189
+ end
190
+
191
+ # Fetch metadata for a sandbox by name.
192
+ # @return [SandboxInfo]
193
+ def get(name)
194
+ SandboxInfo.new(Native::Sandbox.get(name.to_s))
195
+ end
196
+
197
+ # List all sandboxes.
198
+ # @return [Array<SandboxInfo>]
199
+ def list
200
+ Native::Sandbox.list.map { |info| SandboxInfo.new(info) }
201
+ end
202
+
203
+ # List sandboxes carrying all of the given labels (AND-matched).
204
+ # @param labels [Hash] required key => value labels
205
+ # @return [Array<SandboxInfo>]
206
+ def list_with(labels: {})
207
+ opts = { "labels" => stringify(labels) }
208
+ Native::Sandbox.list_with(opts).map { |info| SandboxInfo.new(info) }
209
+ end
210
+
211
+ # Remove a (stopped) sandbox by name.
212
+ # @return [nil]
213
+ def remove(name)
214
+ Native::Sandbox.remove(name.to_s)
215
+ nil
216
+ end
217
+
218
+ private
219
+
220
+ def stringify(hash)
221
+ hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
222
+ end
223
+
224
+ def intify_ports(ports)
225
+ ports.each_with_object({}) { |(k, v), acc| acc[Integer(k)] = Integer(v) }
226
+ end
227
+
228
+ # Normalize secrets into [env, value, host] triples for the native layer.
229
+ # Each entry is a Hash { env:, value:, host: } (string or symbol keys).
230
+ def normalize_secrets(secrets)
231
+ Array(secrets).map do |spec|
232
+ env = spec[:env] || spec["env"]
233
+ value = spec[:value] || spec["value"]
234
+ host = spec[:host] || spec["host"]
235
+ unless env && value && host
236
+ raise ArgumentError, "secret spec needs :env, :value, and :host (got #{spec.inspect})"
237
+ end
238
+ [env.to_s, value.to_s, host.to_s]
239
+ end
240
+ end
241
+
242
+ # Normalize an rlimits Hash into [resource, soft, hard] triples for the
243
+ # native layer. Each value is either a single limit (soft == hard) or a
244
+ # [soft, hard] pair. Shared by {Sandbox.create} and {Sandbox#exec}.
245
+ def normalize_rlimits(rlimits)
246
+ rlimits.map do |resource, limit|
247
+ soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
248
+ [resource.to_s, Integer(soft), Integer(hard)]
249
+ end
250
+ end
251
+
252
+ # Normalize volumes (Hash of guest_path => spec) into [guest, kind, source]
253
+ # triples for the native layer. A spec is a host path String (bind mount),
254
+ # or a Hash { bind: "/host" } / { named: "volume-name" }.
255
+ def normalize_volumes(volumes)
256
+ volumes.map do |guest, spec|
257
+ guest = guest.to_s
258
+ case spec
259
+ when String
260
+ [guest, "bind", spec]
261
+ when Hash
262
+ if (named = spec[:named] || spec["named"])
263
+ [guest, "named", named.to_s]
264
+ elsif (bind = spec[:bind] || spec["bind"])
265
+ [guest, "bind", bind.to_s]
266
+ else
267
+ raise ArgumentError, "volume spec for #{guest.inspect} needs :bind or :named"
268
+ end
269
+ else
270
+ raise ArgumentError, "invalid volume spec for #{guest.inspect}: #{spec.inspect}"
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ def initialize(native)
277
+ @native = native
278
+ end
279
+
280
+ # @return [String] the sandbox name
281
+ def name
282
+ @native.name
283
+ end
284
+
285
+ # Run a command (no shell interpretation) and collect its output.
286
+ #
287
+ # @param command [String] the executable
288
+ # @param args [Array<String>] arguments
289
+ # @param cwd [String, nil] working directory
290
+ # @param user [String, nil] user to run as
291
+ # @param env [Hash, nil] extra environment variables
292
+ # @param timeout [Numeric, nil] kill after N seconds
293
+ # @param tty [Boolean] allocate a pseudo-terminal
294
+ # @param stdin [String, nil] data to feed to stdin
295
+ # @return [ExecOutput]
296
+ def exec(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
297
+ ExecOutput.new(@native.exec(command.to_s, Array(args).map(&:to_s),
298
+ exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
299
+ end
300
+
301
+ # Run a shell script (pipes, redirects, etc. allowed) and collect output.
302
+ # @return [ExecOutput]
303
+ def shell(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
304
+ ExecOutput.new(@native.shell(script.to_s,
305
+ exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
306
+ end
307
+
308
+ # Run a command and stream its output as it arrives.
309
+ # @return [ExecHandle]
310
+ # @see ExecHandle
311
+ def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
312
+ ExecHandle.new(@native.exec_stream(command.to_s, Array(args).map(&:to_s),
313
+ exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
314
+ end
315
+
316
+ # Run a shell script and stream its output as it arrives.
317
+ # @return [ExecHandle]
318
+ def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
319
+ ExecHandle.new(@native.shell_stream(script.to_s,
320
+ exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
321
+ end
322
+
323
+ # Guest filesystem operations.
324
+ # @return [FS]
325
+ def fs
326
+ @fs ||= FS.new(@native)
327
+ end
328
+
329
+ # Latest resource-usage snapshot.
330
+ # @return [Metrics]
331
+ def metrics
332
+ Metrics.new(@native.metrics)
333
+ end
334
+
335
+ # Read captured logs.
336
+ #
337
+ # @param tail [Integer, nil] only the last N entries
338
+ # @param since_ms [Numeric, nil] only entries at/after this Unix ms
339
+ # @param until_ms [Numeric, nil] only entries before this Unix ms
340
+ # @param sources [Array<String,Symbol>, nil] filter by source
341
+ # ("stdout"/"stderr"/"output"/"system"/"all")
342
+ # @return [Array<LogEntry>]
343
+ def logs(tail: nil, since_ms: nil, until_ms: nil, sources: nil)
344
+ opts = {}
345
+ opts["tail"] = Integer(tail) if tail
346
+ opts["since_ms"] = Float(since_ms) if since_ms
347
+ opts["until_ms"] = Float(until_ms) if until_ms
348
+ opts["sources"] = Array(sources).map(&:to_s) if sources
349
+ @native.logs(opts).map { |entry| LogEntry.new(entry) }
350
+ end
351
+
352
+ # Stream resource-usage snapshots, one per interval tick, until the sandbox
353
+ # stops. Requires metrics to be enabled for the sandbox.
354
+ # @param interval [Numeric] seconds between snapshots
355
+ # @return [MetricsStream] an {Enumerable} of {Metrics}
356
+ def metrics_stream(interval: 1.0)
357
+ MetricsStream.new(@native.metrics_stream(Float(interval)))
358
+ end
359
+
360
+ # Stream captured logs as they appear.
361
+ #
362
+ # @param sources [Array<String,Symbol>, nil] filter by source
363
+ # ("stdout"/"stderr"/"output"/"system")
364
+ # @param since_ms [Numeric, nil] start at the first entry at/after this Unix ms
365
+ # @param from_cursor [String, nil] resume exactly after a prior {LogEntry#cursor}
366
+ # (mutually exclusive with since_ms; takes precedence if both given)
367
+ # @param until_ms [Numeric, nil] stop before any entry at/after this Unix ms
368
+ # @param follow [Boolean] keep the stream open for new entries past current EOF
369
+ # @return [LogStream] an {Enumerable} of {LogEntry}
370
+ def log_stream(sources: nil, since_ms: nil, from_cursor: nil, until_ms: nil, follow: false)
371
+ opts = {}
372
+ opts["sources"] = Array(sources).map(&:to_s) if sources
373
+ opts["since_ms"] = Float(since_ms) if since_ms
374
+ opts["from_cursor"] = from_cursor.to_s if from_cursor
375
+ opts["until_ms"] = Float(until_ms) if until_ms
376
+ opts["follow"] = true if follow
377
+ LogStream.new(@native.log_stream(opts))
378
+ end
379
+
380
+ # Gracefully stop the sandbox (and wait for it to terminate).
381
+ # @param timeout [Numeric, nil] seconds to wait before SIGKILL
382
+ # @return [nil]
383
+ def stop(timeout: nil)
384
+ @native.stop(timeout && Float(timeout))
385
+ nil
386
+ end
387
+
388
+ # Force-kill the sandbox (SIGKILL).
389
+ # @param timeout [Numeric, nil] seconds to wait
390
+ # @return [nil]
391
+ def kill(timeout: nil)
392
+ @native.kill(timeout && Float(timeout))
393
+ nil
394
+ end
395
+
396
+ # Send the graceful-shutdown request and return immediately, without waiting
397
+ # for the sandbox to terminate. Pair with {#wait_until_stopped}.
398
+ # @return [nil]
399
+ def request_stop
400
+ @native.request_stop
401
+ nil
402
+ end
403
+
404
+ # Send the force-kill request and return immediately, without waiting.
405
+ # @return [nil]
406
+ def request_kill
407
+ @native.request_kill
408
+ nil
409
+ end
410
+
411
+ # Request a graceful drain and return immediately, without waiting.
412
+ # @return [nil]
413
+ def request_drain
414
+ @native.request_drain
415
+ nil
416
+ end
417
+
418
+ # Block until the sandbox is observed in a terminal (non-running) state.
419
+ # @return [SandboxStopResult]
420
+ def wait_until_stopped
421
+ SandboxStopResult.new(@native.wait_until_stopped)
422
+ end
423
+
424
+ # @return [Boolean] whether this handle owns the sandbox process lifecycle
425
+ # (i.e. stopping it or dropping the handle terminates the sandbox)
426
+ def owns_lifecycle?
427
+ @native.owns_lifecycle
428
+ end
429
+
430
+ # Detach this handle: disarm the stop-on-drop safety net so the sandbox
431
+ # keeps running after this handle is gone (and after this process exits).
432
+ # @return [nil]
433
+ def detach
434
+ @native.detach
435
+ nil
436
+ end
437
+
438
+ def inspect
439
+ "#<Microsandbox::Sandbox name=#{name.inspect}>"
440
+ end
441
+
442
+ private
443
+
444
+ def exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)
445
+ opts = {}
446
+ opts["cwd"] = cwd.to_s if cwd
447
+ opts["user"] = user.to_s if user
448
+ opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
449
+ opts["timeout"] = Float(timeout) if timeout
450
+ opts["tty"] = true if tty
451
+ opts["stdin"] = stdin.to_s if stdin
452
+ if rlimits
453
+ opts["rlimits"] = rlimits.map do |resource, limit|
454
+ soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
455
+ [resource.to_s, Integer(soft), Integer(hard)]
456
+ end
457
+ end
458
+ opts
459
+ end
460
+ end
461
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Microsandbox
4
+ # Metadata for a snapshot artifact, returned by {Snapshot.create}/{Snapshot.get}/
5
+ # {Snapshot.list}/{Snapshot.import}.
6
+ #
7
+ # `digest` and `path` are always present. `create` additionally populates
8
+ # `size_bytes`; `get`/`list`/`import` additionally populate `name`,
9
+ # `parent_digest`, `image_ref`, `format`, and `created_at`.
10
+ class SnapshotInfo
11
+ # @return [String] manifest digest ("sha256:…") — the canonical identity
12
+ attr_reader :digest
13
+ # @return [String] artifact directory path
14
+ attr_reader :path
15
+ # @return [String, nil] name alias (nil for digest-only entries)
16
+ attr_reader :name
17
+ # @return [String, nil] parent snapshot digest
18
+ attr_reader :parent_digest
19
+ # @return [String, nil] source OCI image reference
20
+ attr_reader :image_ref
21
+ # @return [Integer, nil] artifact size in bytes
22
+ attr_reader :size_bytes
23
+
24
+ def initialize(data)
25
+ @digest = data["digest"]
26
+ @path = data["path"]
27
+ @name = data["name"]
28
+ @parent_digest = data["parent_digest"]
29
+ @image_ref = data["image_ref"]
30
+ @format = data["format"]
31
+ @size_bytes = data["size_bytes"]
32
+ @created_at_ms = data["created_at_ms"]
33
+ end
34
+
35
+ # @return [Symbol, nil] disk format (:raw or :qcow2)
36
+ def format
37
+ @format&.to_sym
38
+ end
39
+
40
+ # @return [Time, nil]
41
+ def created_at
42
+ @created_at_ms && Time.at(@created_at_ms / 1000.0)
43
+ end
44
+
45
+ def inspect
46
+ "#<Microsandbox::SnapshotInfo digest=#{@digest.inspect}#{@name ? " name=#{@name.inspect}" : ""}>"
47
+ end
48
+ end
49
+
50
+ # The result of {Snapshot.verify}.
51
+ class SnapshotVerifyReport
52
+ # @return [String] manifest digest
53
+ attr_reader :digest
54
+ # @return [String] artifact directory path
55
+ attr_reader :path
56
+ # @return [Symbol] :not_recorded or :verified
57
+ attr_reader :status
58
+ # @return [String, nil] digest algorithm (when :verified)
59
+ attr_reader :algorithm
60
+ # @return [String, nil] matched content digest (when :verified)
61
+ attr_reader :content_digest
62
+
63
+ def initialize(data)
64
+ @digest = data["digest"]
65
+ @path = data["path"]
66
+ @status = data["upper_status"].to_sym
67
+ @algorithm = data["upper_algorithm"]
68
+ @content_digest = data["upper_digest"]
69
+ end
70
+
71
+ # @return [Boolean] whether content integrity was recorded and matched
72
+ def verified? = @status == :verified
73
+ # @return [Boolean] whether no integrity descriptor was recorded
74
+ def not_recorded? = @status == :not_recorded
75
+ end
76
+
77
+ # Creation and management of sandbox snapshots. A snapshot captures a stopped
78
+ # sandbox's upper layer into a portable artifact; boot from it with
79
+ # `Sandbox.create(from_snapshot: "name-or-digest")`.
80
+ class Snapshot
81
+ class << self
82
+ # Create a snapshot of a stopped sandbox.
83
+ #
84
+ # @param source_sandbox [String] name of the (stopped) source sandbox
85
+ # @param name [String, nil] destination name under the snapshots dir
86
+ # @param path [String, nil] explicit destination directory (alternative to name)
87
+ # @param labels [Hash, nil] user labels
88
+ # @param force [Boolean] overwrite an existing artifact at the destination
89
+ # @param record_integrity [Boolean] compute + record upper-layer integrity
90
+ # @return [SnapshotInfo]
91
+ def create(source_sandbox, name: nil, path: nil, labels: nil, force: false, record_integrity: false)
92
+ opts = {}
93
+ opts["name"] = name.to_s if name
94
+ opts["path"] = path.to_s if path
95
+ opts["labels"] = stringify(labels) if labels
96
+ opts["force"] = true if force
97
+ opts["record_integrity"] = true if record_integrity
98
+ SnapshotInfo.new(Native::Snapshot.create(source_sandbox.to_s, opts))
99
+ end
100
+
101
+ # Metadata for a snapshot by name or digest.
102
+ # @return [SnapshotInfo]
103
+ def get(name_or_digest)
104
+ SnapshotInfo.new(Native::Snapshot.get(name_or_digest.to_s))
105
+ end
106
+
107
+ # All snapshots.
108
+ # @return [Array<SnapshotInfo>]
109
+ def list
110
+ Native::Snapshot.list.map { |info| SnapshotInfo.new(info) }
111
+ end
112
+
113
+ # Remove a snapshot artifact by name or path.
114
+ # @param force [Boolean] remove even if referenced
115
+ # @return [nil]
116
+ def remove(name_or_path, force: false)
117
+ Native::Snapshot.remove(name_or_path.to_s, force)
118
+ nil
119
+ end
120
+
121
+ # Verify a snapshot's recorded upper-layer integrity.
122
+ # @return [SnapshotVerifyReport]
123
+ def verify(name_or_path)
124
+ SnapshotVerifyReport.new(Native::Snapshot.verify(name_or_path.to_s))
125
+ end
126
+
127
+ # Bundle a snapshot into a `.tar.zst` (or plain `.tar`) archive.
128
+ # @param with_parents [Boolean] include ancestor snapshots
129
+ # @param with_image [Boolean] include OCI image artifacts (boots offline)
130
+ # @param plain_tar [Boolean] write an uncompressed `.tar`
131
+ # @return [nil]
132
+ def export(name_or_path, out_path, with_parents: false, with_image: false, plain_tar: false)
133
+ opts = {}
134
+ opts["with_parents"] = true if with_parents
135
+ opts["with_image"] = true if with_image
136
+ opts["plain_tar"] = true if plain_tar
137
+ Native::Snapshot.export(name_or_path.to_s, out_path.to_s, opts)
138
+ nil
139
+ end
140
+
141
+ # Unpack a snapshot archive into the snapshots dir.
142
+ # @param dest [String, nil] explicit destination directory
143
+ # @return [SnapshotInfo]
144
+ def import(archive_path, dest: nil)
145
+ SnapshotInfo.new(Native::Snapshot.import(archive_path.to_s, dest && dest.to_s))
146
+ end
147
+
148
+ private
149
+
150
+ def stringify(hash)
151
+ hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Microsandbox
4
+ # A live stream of {LogEntry}s, returned by {Sandbox#log_stream}. Enumerable:
5
+ # iterate to consume entries as they are appended. With `follow: true` the
6
+ # iteration blocks for new entries until the sandbox stops; otherwise it ends
7
+ # once the historical log is drained.
8
+ #
9
+ # @example
10
+ # sb.log_stream(follow: true).each { |entry| print entry.text }
11
+ class LogStream
12
+ include Enumerable
13
+
14
+ def initialize(native)
15
+ @native = native
16
+ end
17
+
18
+ # @yieldparam entry [LogEntry]
19
+ # @return [self, Enumerator]
20
+ def each
21
+ return enum_for(:each) unless block_given?
22
+
23
+ while (entry = @native.recv)
24
+ yield LogEntry.new(entry)
25
+ end
26
+ self
27
+ end
28
+ end
29
+
30
+ # A live stream of {Metrics} snapshots, returned by {Sandbox#metrics_stream}.
31
+ # Enumerable: iteration yields one snapshot per interval tick until the
32
+ # sandbox stops.
33
+ #
34
+ # @example
35
+ # sb.metrics_stream(interval: 0.5).each { |m| puts m.cpu_percent }
36
+ class MetricsStream
37
+ include Enumerable
38
+
39
+ def initialize(native)
40
+ @native = native
41
+ end
42
+
43
+ # @yieldparam metrics [Metrics]
44
+ # @return [self, Enumerator]
45
+ def each
46
+ return enum_for(:each) unless block_given?
47
+
48
+ while (snapshot = @native.recv)
49
+ yield Metrics.new(snapshot)
50
+ end
51
+ self
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Microsandbox
4
+ # Gem version. Kept in lock-step with the upstream microsandbox runtime and
5
+ # the official Python/Node/Go SDKs (workspace version in the microsandbox repo).
6
+ VERSION = "0.5.7"
7
+ end