bsdkrun 0.4.0 → 0.5.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/README.md +31 -0
- data/lib/bsdkrun/client.rb +270 -1
- data/lib/bsdkrun/types.rb +183 -3
- data/lib/bsdkrun/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1b2ffdacfec58cee3714b8ca96ecc133604655d423e264444d8f41ea88393e5
|
|
4
|
+
data.tar.gz: 848ec7c03c5489a41daf404135c3bbfdcfb126449ecb712ddf920ea7377bc822
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a86228909ae198caab0e630bac039179fc115317118f321a1aa193683ee6fb10ccfdc56ae1d05d42cad3ebad712897b5979c0b3b75cd0fe9b64bef0406d1d5ae
|
|
7
|
+
data.tar.gz: aef36636aef92124b1c78695648c929bfee4f60e134b9ef0a2d5588693827994e8023f62c91f39cd2d1a19c74e7e1a6a3d99a99c2aca9df51ba53bf03022368e
|
data/README.md
CHANGED
|
@@ -300,6 +300,37 @@ the new machine's id. `run_solo5` boots a MirageOS unikernel under the
|
|
|
300
300
|
`stop`/`start`/`remove`/`update`/`commit` return a
|
|
301
301
|
`CommandResult` (`exit_code`, `stdout`, `stderr`).
|
|
302
302
|
|
|
303
|
+
### Snapshots
|
|
304
|
+
|
|
305
|
+
A snapshot is a **copy-on-write clone of a machine's disk state** — instant to
|
|
306
|
+
take, free until the two sides diverge. `branch` boots a new machine from one
|
|
307
|
+
(or from a machine, which is snapshotted first); `restore`/`rollback` put one
|
|
308
|
+
back, leaving the machine stopped. A BSD guest is powered off to snapshot it:
|
|
309
|
+
a mounted UFS cannot be cloned consistently.
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
snap = client.snapshot(machine_id, name: "before-upgrade")
|
|
313
|
+
client.snapshots(machine: machine_id) # newest first
|
|
314
|
+
branch_id = client.branch(snap.name, name: "web-test")
|
|
315
|
+
client.restore(machine_id, snap.name) # or client.rollback(machine_id)
|
|
316
|
+
client.remove_snapshots(snap.name)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Docker
|
|
320
|
+
|
|
321
|
+
bsdkrun runs one `docker:dind` microVM and serves its API on a host unix
|
|
322
|
+
socket, so the host's own `docker` CLI drives the same engine these calls do.
|
|
323
|
+
Starting is idempotent — the VM has a fixed name, so it resumes rather than
|
|
324
|
+
creating a second.
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
status = client.docker_start(cpus: 4, mem: 4096) # or just docker_status
|
|
328
|
+
puts status.socket
|
|
329
|
+
client.docker_containers.each { |c| puts "#{c.name} #{c.state} #{c.ports}" }
|
|
330
|
+
client.docker_container("restart", "web")
|
|
331
|
+
puts client.docker_logs("web", tail: 50)
|
|
332
|
+
```
|
|
333
|
+
|
|
303
334
|
For a live terminal instead of a one-shot `exec`, use `shell`:
|
|
304
335
|
|
|
305
336
|
```ruby
|
data/lib/bsdkrun/client.rb
CHANGED
|
@@ -35,10 +35,16 @@ module Bsdkrun
|
|
|
35
35
|
# +web/src/lib/api.ts+'s +MACHINE_FIELDS+ fragment exactly.
|
|
36
36
|
MACHINE_FIELDS = <<~GQL.freeze
|
|
37
37
|
id name image kind command status running exitCode pid detached
|
|
38
|
-
cpus mem volume stateDir createdAt finishedAt network netIp
|
|
38
|
+
cpus mem volume stateDir createdAt finishedAt network netIp origin
|
|
39
39
|
ports { bind host guest }
|
|
40
40
|
GQL
|
|
41
41
|
|
|
42
|
+
# The +Snapshot+ selection, likewise shared by every snapshot document.
|
|
43
|
+
SNAPSHOT_FIELDS = <<~GQL.freeze
|
|
44
|
+
id name machineId machineName kind image path parent description
|
|
45
|
+
cpus mem size createdAt ports { bind host guest }
|
|
46
|
+
GQL
|
|
47
|
+
|
|
42
48
|
# @return [String] the GraphQL endpoint URL (normalized).
|
|
43
49
|
attr_reader :url
|
|
44
50
|
|
|
@@ -242,6 +248,269 @@ module Bsdkrun
|
|
|
242
248
|
)
|
|
243
249
|
end
|
|
244
250
|
|
|
251
|
+
# ---- ai agents -------------------------------------------------------------
|
|
252
|
+
#
|
|
253
|
+
# A sandbox is a machine, so its terminal is the ordinary {#shell} with the
|
|
254
|
+
# argv {#ai_shell_command} returns.
|
|
255
|
+
|
|
256
|
+
AI_AGENT_FIELDS = "id label flavor description installed running"
|
|
257
|
+
AI_SESSION_FIELDS = "id name agent running workspace createdAt"
|
|
258
|
+
|
|
259
|
+
# The coding agents, and whether each one's sandbox image is built.
|
|
260
|
+
# @return [Array<AiAgent>]
|
|
261
|
+
def ai_agents
|
|
262
|
+
data = request("{ aiAgents { #{AI_AGENT_FIELDS} } }")
|
|
263
|
+
(data["aiAgents"] || []).map { |a| AiAgent.from_graphql(a) }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Agent sandboxes, newest first.
|
|
267
|
+
# @return [Array<AiSession>]
|
|
268
|
+
def ai_sessions
|
|
269
|
+
data = request("{ aiSessions { #{AI_SESSION_FIELDS} } }")
|
|
270
|
+
(data["aiSessions"] || []).map { |s| AiSession.from_graphql(s) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Start (or reuse) a sandbox; returns its machine id.
|
|
274
|
+
#
|
|
275
|
+
# @param agent [String] +claude+, +codex+, ...
|
|
276
|
+
# @param workspace [String, nil] a directory **on the engine's host**.
|
|
277
|
+
# @param new [Boolean] boot a second sandbox against the same saved login.
|
|
278
|
+
# @return [String]
|
|
279
|
+
def ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false)
|
|
280
|
+
data = request(
|
|
281
|
+
"mutation($input:AiStartInput!){ aiStart(input:$input) }",
|
|
282
|
+
{ input: { agent: agent, cpus: cpus, mem: mem,
|
|
283
|
+
workspace: workspace, new: new } }
|
|
284
|
+
)
|
|
285
|
+
data["aiStart"].to_s
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# The argv that starts the agent's TUI — pass it to {#shell}.
|
|
289
|
+
# @return [Array<String>]
|
|
290
|
+
def ai_shell_command(agent, machine_id)
|
|
291
|
+
data = request(
|
|
292
|
+
"query($agent:String!,$machineId:String!){ " \
|
|
293
|
+
"aiShellCommand(agent:$agent, machineId:$machineId) }",
|
|
294
|
+
{ agent: agent, machineId: machine_id }
|
|
295
|
+
)
|
|
296
|
+
Array(data["aiShellCommand"])
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Stop an agent's sandboxes. Its saved login survives.
|
|
300
|
+
# @return [CommandResult]
|
|
301
|
+
def ai_stop(agent)
|
|
302
|
+
run_command_mutation(
|
|
303
|
+
"aiStop",
|
|
304
|
+
"mutation($agent:String!){ aiStop(agent:$agent){ exitCode stdout stderr } }",
|
|
305
|
+
{ agent: agent }
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Remove an agent's sandboxes, and unless +keep_home+ its saved login too.
|
|
310
|
+
# @return [CommandResult]
|
|
311
|
+
def ai_remove(agent, keep_home: false)
|
|
312
|
+
run_command_mutation(
|
|
313
|
+
"aiRemove",
|
|
314
|
+
"mutation($agent:String!,$keepHome:Boolean!){ " \
|
|
315
|
+
"aiRemove(agent:$agent, keepHome:$keepHome){ exitCode stdout stderr } }",
|
|
316
|
+
{ agent: agent, keepHome: keep_home }
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# ---- docker --------------------------------------------------------------
|
|
321
|
+
#
|
|
322
|
+
# bsdkrun runs one +docker:dind+ microVM and serves its API on a host unix
|
|
323
|
+
# socket, so these drive the same engine the host's +docker+ CLI does.
|
|
324
|
+
|
|
325
|
+
DOCKER_STATUS_FIELDS = <<~GQL.freeze
|
|
326
|
+
running machineId machineRunning socket socketReady apiPort version
|
|
327
|
+
containers images mounts disk diskSize
|
|
328
|
+
GQL
|
|
329
|
+
|
|
330
|
+
DOCKER_CONTAINER_FIELDS = "id name image command state status ports created"
|
|
331
|
+
|
|
332
|
+
# Is the Docker engine up, and where is its socket?
|
|
333
|
+
# @return [DockerStatus]
|
|
334
|
+
def docker_status
|
|
335
|
+
data = request("{ dockerStatus { #{DOCKER_STATUS_FIELDS} } }")
|
|
336
|
+
DockerStatus.from_graphql(data["dockerStatus"])
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Containers in the engine.
|
|
340
|
+
# @param all [Boolean] include stopped ones (default true).
|
|
341
|
+
# @return [Array<DockerContainer>]
|
|
342
|
+
def docker_containers(all: true)
|
|
343
|
+
data = request(
|
|
344
|
+
"query($all:Boolean!){ dockerContainers(all:$all){ #{DOCKER_CONTAINER_FIELDS} } }",
|
|
345
|
+
{ all: all }
|
|
346
|
+
)
|
|
347
|
+
(data["dockerContainers"] || []).map { |c| DockerContainer.from_graphql(c) }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Start (or resume) the engine, returning its status once it answers.
|
|
351
|
+
#
|
|
352
|
+
# Idempotent: the VM has a fixed name, so this resumes the existing one
|
|
353
|
+
# rather than creating a second.
|
|
354
|
+
#
|
|
355
|
+
# @param cpus [Integer, nil]
|
|
356
|
+
# @param mem [Integer, nil]
|
|
357
|
+
# @param mounts [Array<String>] host dirs to share, +PATH+ or +HOST:GUEST+.
|
|
358
|
+
# @param no_home [Boolean] do not share +$HOME+ (shared by default).
|
|
359
|
+
# @param publish_bind [String, nil] +mirror+ (default) or a fixed address.
|
|
360
|
+
# @param disk_size [String, nil] a dedicated image store, e.g. +60G+.
|
|
361
|
+
# @return [DockerStatus]
|
|
362
|
+
def docker_start(cpus: nil, mem: nil, mounts: [], no_home: false,
|
|
363
|
+
publish_bind: nil, disk_size: nil)
|
|
364
|
+
data = request(
|
|
365
|
+
"mutation($input:DockerStartInput!){ dockerStart(input:$input){ " \
|
|
366
|
+
"#{DOCKER_STATUS_FIELDS} } }",
|
|
367
|
+
{ input: { cpus: cpus, mem: mem, mounts: Array(mounts), noHome: no_home,
|
|
368
|
+
publishBind: publish_bind, diskSize: disk_size } }
|
|
369
|
+
)
|
|
370
|
+
DockerStatus.from_graphql(data["dockerStart"])
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Stop the engine. Images and containers stay on its disk.
|
|
374
|
+
# @return [CommandResult]
|
|
375
|
+
def docker_stop
|
|
376
|
+
run_command_mutation(
|
|
377
|
+
"dockerStop",
|
|
378
|
+
"mutation{ dockerStop{ exitCode stdout stderr } }",
|
|
379
|
+
{}
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# start | stop | restart | kill | pause | unpause | rm.
|
|
384
|
+
# @param action [String]
|
|
385
|
+
# @param ids [String, Array<String>]
|
|
386
|
+
# @return [CommandResult]
|
|
387
|
+
def docker_container(action, ids)
|
|
388
|
+
run_command_mutation(
|
|
389
|
+
"dockerContainer",
|
|
390
|
+
"mutation($action:String!,$ids:[String!]!){ " \
|
|
391
|
+
"dockerContainer(action:$action, ids:$ids){ exitCode stdout stderr } }",
|
|
392
|
+
{ action: action, ids: Array(ids) }
|
|
393
|
+
)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# One container's logs (stdout+stderr, most recent +tail+ lines).
|
|
397
|
+
# @param id [String]
|
|
398
|
+
# @param tail [Integer]
|
|
399
|
+
# @return [String]
|
|
400
|
+
def docker_logs(id, tail: 200)
|
|
401
|
+
data = request(
|
|
402
|
+
"query($id:String!,$tail:Int!){ dockerContainerLogs(id:$id, tail:$tail) }",
|
|
403
|
+
{ id: id, tail: tail }
|
|
404
|
+
)
|
|
405
|
+
data["dockerContainerLogs"].to_s
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# ---- snapshots ---------------------------------------------------------
|
|
409
|
+
#
|
|
410
|
+
# A snapshot is a copy-on-write clone of a machine's disk state: instant to
|
|
411
|
+
# take, free until the two sides diverge. {#branch} boots a new machine
|
|
412
|
+
# from one; {#restore}/{#rollback} put one back.
|
|
413
|
+
|
|
414
|
+
# List snapshots, newest first.
|
|
415
|
+
# @param machine [String, nil] only this machine's, when given.
|
|
416
|
+
# @return [Array<SnapshotInfo>]
|
|
417
|
+
def snapshots(machine: nil)
|
|
418
|
+
data = request(
|
|
419
|
+
"query($machine:String){ snapshots(machine:$machine){ #{SNAPSHOT_FIELDS} } }",
|
|
420
|
+
{ machine: machine }
|
|
421
|
+
)
|
|
422
|
+
(data["snapshots"] || []).map { |s| SnapshotInfo.from_graphql(s) }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Capture a machine's disk state.
|
|
426
|
+
#
|
|
427
|
+
# A BSD guest is powered off first — a mounted UFS cannot be cloned
|
|
428
|
+
# consistently — so the machine is left stopped; {#start} brings it back.
|
|
429
|
+
#
|
|
430
|
+
# @param id [String]
|
|
431
|
+
# @param name [String, nil] defaults to +<machine>-<n>+.
|
|
432
|
+
# @param description [String]
|
|
433
|
+
# @return [SnapshotInfo]
|
|
434
|
+
def snapshot(id, name: nil, description: "")
|
|
435
|
+
data = request(
|
|
436
|
+
"mutation($id:String!,$name:String,$description:String!){ " \
|
|
437
|
+
"snapshotMachine(id:$id, name:$name, description:$description){ #{SNAPSHOT_FIELDS} } }",
|
|
438
|
+
{ id: id, name: name, description: description }
|
|
439
|
+
)
|
|
440
|
+
SnapshotInfo.from_graphql(data["snapshotMachine"])
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Delete snapshots and their data. Machines branched from them are
|
|
444
|
+
# unaffected.
|
|
445
|
+
# @param names [String, Array<String>]
|
|
446
|
+
# @return [CommandResult]
|
|
447
|
+
def remove_snapshots(names)
|
|
448
|
+
run_command_mutation(
|
|
449
|
+
"removeSnapshots",
|
|
450
|
+
"mutation($names:[String!]!){ removeSnapshots(names:$names){ exitCode stdout stderr } }",
|
|
451
|
+
{ names: Array(names) }
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Put a machine's disk state back to one of its snapshots.
|
|
456
|
+
#
|
|
457
|
+
# +force+ stops the machine first (it holds the very files being
|
|
458
|
+
# replaced); +backup+ snapshots the state being overwritten, which is a
|
|
459
|
+
# CoW clone and therefore free. The machine is left stopped.
|
|
460
|
+
#
|
|
461
|
+
# @param id [String]
|
|
462
|
+
# @param snapshot [String]
|
|
463
|
+
# @param force [Boolean]
|
|
464
|
+
# @param backup [Boolean]
|
|
465
|
+
# @return [CommandResult]
|
|
466
|
+
def restore(id, snapshot, force: true, backup: true)
|
|
467
|
+
run_command_mutation(
|
|
468
|
+
"restoreMachine",
|
|
469
|
+
"mutation($id:String!,$snapshot:String!,$force:Boolean!,$backup:Boolean!){ " \
|
|
470
|
+
"restoreMachine(id:$id, snapshot:$snapshot, force:$force, backup:$backup){ " \
|
|
471
|
+
"exitCode stdout stderr } }",
|
|
472
|
+
{ id: id, snapshot: snapshot, force: force, backup: backup }
|
|
473
|
+
)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Restore a machine to its most recent snapshot.
|
|
477
|
+
# @param id [String]
|
|
478
|
+
# @param force [Boolean]
|
|
479
|
+
# @param backup [Boolean]
|
|
480
|
+
# @return [CommandResult]
|
|
481
|
+
def rollback(id, force: true, backup: true)
|
|
482
|
+
run_command_mutation(
|
|
483
|
+
"rollbackMachine",
|
|
484
|
+
"mutation($id:String!,$force:Boolean!,$backup:Boolean!){ " \
|
|
485
|
+
"rollbackMachine(id:$id, force:$force, backup:$backup){ exitCode stdout stderr } }",
|
|
486
|
+
{ id: id, force: force, backup: backup }
|
|
487
|
+
)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Boot a NEW machine from a snapshot — or from a machine, which is
|
|
491
|
+
# snapshotted first — and return the new machine's id.
|
|
492
|
+
#
|
|
493
|
+
# The state is cloned, never booted in place, so the source is untouched
|
|
494
|
+
# and one snapshot can be branched any number of times. With no +ports+,
|
|
495
|
+
# the snapshot's own forwards are inherited, with any host port that is
|
|
496
|
+
# already taken swapped for a free one.
|
|
497
|
+
#
|
|
498
|
+
# @param snapshot [String] snapshot name/id, or a machine id.
|
|
499
|
+
# @param name [String, nil]
|
|
500
|
+
# @param cpus [Integer, nil]
|
|
501
|
+
# @param mem [Integer, nil]
|
|
502
|
+
# @param ports [Array<String>]
|
|
503
|
+
# @param no_ports [Boolean]
|
|
504
|
+
# @return [String] the new machine's id.
|
|
505
|
+
def branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false)
|
|
506
|
+
data = request(
|
|
507
|
+
"mutation($input:BranchInput!){ branchSnapshot(input:$input) }",
|
|
508
|
+
{ input: { snapshot: snapshot, name: name, cpus: cpus, mem: mem,
|
|
509
|
+
ports: Array(ports), noPorts: no_ports } }
|
|
510
|
+
)
|
|
511
|
+
data["branchSnapshot"].to_s
|
|
512
|
+
end
|
|
513
|
+
|
|
245
514
|
# One-shot console log fetch. Use {#follow_logs} to stream instead.
|
|
246
515
|
# @param id [String]
|
|
247
516
|
# @param boot [Boolean] bsdkrun's own boot log instead of the guest console.
|
data/lib/bsdkrun/types.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Bsdkrun
|
|
|
34
34
|
SandboxInfo = Data.define(
|
|
35
35
|
:id, :name, :image, :kind, :command, :status, :running, :exit_code,
|
|
36
36
|
:pid, :detached, :cpus, :mem, :volume, :state_dir, :network, :net_ip,
|
|
37
|
-
:created_at, :finished_at, :ports
|
|
37
|
+
:created_at, :finished_at, :ports, :origin
|
|
38
38
|
) do
|
|
39
39
|
# Map a +ps --json+ row (String keys) to a typed instance.
|
|
40
40
|
# @param row [Hash]
|
|
@@ -60,7 +60,8 @@ module Bsdkrun
|
|
|
60
60
|
net_ip: row["net_ip"],
|
|
61
61
|
created_at: row["created_at"].to_i,
|
|
62
62
|
finished_at: to_i_or_nil(row["finished_at"]),
|
|
63
|
-
ports: (row["ports"] || []).map { |p| PortForward.from_row(p) }
|
|
63
|
+
ports: (row["ports"] || []).map { |p| PortForward.from_row(p) },
|
|
64
|
+
origin: row["origin"]
|
|
64
65
|
)
|
|
65
66
|
end
|
|
66
67
|
|
|
@@ -96,11 +97,190 @@ module Bsdkrun
|
|
|
96
97
|
net_ip: m["netIp"],
|
|
97
98
|
created_at: m["createdAt"].to_i,
|
|
98
99
|
finished_at: to_i_or_nil(m["finishedAt"]),
|
|
99
|
-
ports: (m["ports"] || []).map { |p| PortForward.from_row(p) }
|
|
100
|
+
ports: (m["ports"] || []).map { |p| PortForward.from_row(p) },
|
|
101
|
+
origin: m["origin"]
|
|
100
102
|
)
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
# A machine snapshot: one machine's disk state, captured under a name.
|
|
107
|
+
#
|
|
108
|
+
# A copy-on-write clone rather than a memory image — the files the guest
|
|
109
|
+
# wrote, not what it was executing. {Bsdkrun::Client#branch} boots a new
|
|
110
|
+
# machine from one; {Bsdkrun::Client#restore} puts one back.
|
|
111
|
+
SnapshotInfo = Data.define(
|
|
112
|
+
:id, :name, :machine_id, :machine_name, :kind, :image, :path, :parent,
|
|
113
|
+
:description, :cpus, :mem, :ports, :size, :created_at
|
|
114
|
+
) do
|
|
115
|
+
# Map a GraphQL +Snapshot+ (camelCase) to a typed instance.
|
|
116
|
+
# @param s [Hash]
|
|
117
|
+
# @return [SnapshotInfo]
|
|
118
|
+
def self.from_graphql(s)
|
|
119
|
+
new(
|
|
120
|
+
id: s["id"].to_s,
|
|
121
|
+
name: s["name"].to_s,
|
|
122
|
+
machine_id: s["machineId"].to_s,
|
|
123
|
+
machine_name: (s["machineName"] || "").to_s,
|
|
124
|
+
kind: s["kind"].to_s,
|
|
125
|
+
image: (s["image"] || "").to_s,
|
|
126
|
+
path: (s["path"] || "").to_s,
|
|
127
|
+
parent: s["parent"],
|
|
128
|
+
description: (s["description"] || "").to_s,
|
|
129
|
+
cpus: s["cpus"].to_i,
|
|
130
|
+
mem: s["mem"].to_i,
|
|
131
|
+
ports: (s["ports"] || []).map { |p| PortForward.from_row(p) },
|
|
132
|
+
size: s["size"],
|
|
133
|
+
created_at: s["createdAt"].to_i
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Map a +bsdkrun snapshots --json+ row (snake_case).
|
|
138
|
+
# @param row [Hash]
|
|
139
|
+
# @return [SnapshotInfo]
|
|
140
|
+
def self.from_row(row)
|
|
141
|
+
new(
|
|
142
|
+
id: row["id"].to_s,
|
|
143
|
+
name: row["name"].to_s,
|
|
144
|
+
machine_id: row["machine_id"].to_s,
|
|
145
|
+
machine_name: (row["machine_name"] || "").to_s,
|
|
146
|
+
kind: row["kind"].to_s,
|
|
147
|
+
image: (row["image"] || "").to_s,
|
|
148
|
+
path: (row["path"] || "").to_s,
|
|
149
|
+
parent: row["parent"],
|
|
150
|
+
description: (row["description"] || "").to_s,
|
|
151
|
+
cpus: row["cpus"].to_i,
|
|
152
|
+
mem: row["mem"].to_i,
|
|
153
|
+
ports: (row["ports"] || []).map { |p| PortForward.from_row(p) },
|
|
154
|
+
size: row["size"],
|
|
155
|
+
created_at: row["created_at"].to_i
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A coding agent bsdkrun can sandbox.
|
|
161
|
+
#
|
|
162
|
+
# Each runs in a disposable microVM with a persistent login, a shared skills
|
|
163
|
+
# store, and only the folder you choose to share.
|
|
164
|
+
AiAgent = Data.define(
|
|
165
|
+
:id, :label, :flavor, :description, :installed, :running
|
|
166
|
+
) do
|
|
167
|
+
# @param a [Hash]
|
|
168
|
+
# @return [AiAgent]
|
|
169
|
+
def self.from_graphql(a)
|
|
170
|
+
new(
|
|
171
|
+
id: a["id"].to_s,
|
|
172
|
+
label: a["label"].to_s,
|
|
173
|
+
flavor: a["flavor"].to_s,
|
|
174
|
+
description: (a["description"] || "").to_s,
|
|
175
|
+
installed: !!a["installed"],
|
|
176
|
+
running: a["running"].to_i
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class << self
|
|
181
|
+
alias from_row from_graphql
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# One agent sandbox. It is a machine, so +logs+/+stop+ work on +id+.
|
|
186
|
+
AiSession = Data.define(
|
|
187
|
+
:id, :name, :agent, :running, :workspace, :created_at
|
|
188
|
+
) do
|
|
189
|
+
# @param s [Hash]
|
|
190
|
+
# @return [AiSession]
|
|
191
|
+
def self.from_graphql(s)
|
|
192
|
+
new(
|
|
193
|
+
id: s["id"].to_s,
|
|
194
|
+
name: s["name"].to_s,
|
|
195
|
+
agent: s["agent"].to_s,
|
|
196
|
+
running: !!s["running"],
|
|
197
|
+
workspace: s["workspace"],
|
|
198
|
+
created_at: (s["createdAt"] || s["created_at"]).to_i
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class << self
|
|
203
|
+
alias from_row from_graphql
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The Docker engine VM: whether it is up, and how to reach it.
|
|
208
|
+
#
|
|
209
|
+
# bsdkrun runs one +docker:dind+ microVM and serves its API on a host unix
|
|
210
|
+
# socket, so the host's own +docker+ CLI drives the same engine.
|
|
211
|
+
DockerStatus = Data.define(
|
|
212
|
+
:running, :machine_id, :machine_running, :socket, :socket_ready, :api_port,
|
|
213
|
+
:version, :containers, :images, :mounts, :disk, :disk_size
|
|
214
|
+
) do
|
|
215
|
+
# @param s [Hash] a GraphQL +DockerStatus+ (camelCase).
|
|
216
|
+
# @return [DockerStatus]
|
|
217
|
+
def self.from_graphql(s)
|
|
218
|
+
new(
|
|
219
|
+
running: !!s["running"],
|
|
220
|
+
machine_id: s["machineId"],
|
|
221
|
+
machine_running: !!s["machineRunning"],
|
|
222
|
+
socket: s["socket"].to_s,
|
|
223
|
+
socket_ready: !!s["socketReady"],
|
|
224
|
+
api_port: to_i_or_nil(s["apiPort"]),
|
|
225
|
+
version: s["version"],
|
|
226
|
+
containers: to_i_or_nil(s["containers"]),
|
|
227
|
+
images: to_i_or_nil(s["images"]),
|
|
228
|
+
mounts: Array(s["mounts"]),
|
|
229
|
+
disk: s["disk"],
|
|
230
|
+
disk_size: to_i_or_nil(s["diskSize"])
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @param row [Hash] a +bsdkrun docker status --json+ row (snake_case).
|
|
235
|
+
# @return [DockerStatus]
|
|
236
|
+
def self.from_row(row)
|
|
237
|
+
new(
|
|
238
|
+
running: !!row["running"],
|
|
239
|
+
machine_id: row["machine_id"],
|
|
240
|
+
machine_running: !!row["machine_running"],
|
|
241
|
+
socket: row["socket"].to_s,
|
|
242
|
+
socket_ready: !!row["socket_ready"],
|
|
243
|
+
api_port: to_i_or_nil(row["api_port"]),
|
|
244
|
+
version: row["version"],
|
|
245
|
+
containers: to_i_or_nil(row["containers"]),
|
|
246
|
+
images: to_i_or_nil(row["images"]),
|
|
247
|
+
mounts: Array(row["mounts"]),
|
|
248
|
+
disk: row["disk"],
|
|
249
|
+
disk_size: to_i_or_nil(row["disk_size"])
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A container in the Docker engine VM — a trimmed +docker ps+ row.
|
|
255
|
+
DockerContainer = Data.define(
|
|
256
|
+
:id, :name, :image, :command, :state, :status, :ports, :created
|
|
257
|
+
) do
|
|
258
|
+
# @return [Boolean] whether the container is up.
|
|
259
|
+
def running?
|
|
260
|
+
state == "running"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Both the GraphQL object and +docker ps --json+ use these field names.
|
|
264
|
+
# @param c [Hash]
|
|
265
|
+
# @return [DockerContainer]
|
|
266
|
+
def self.from_graphql(c)
|
|
267
|
+
new(
|
|
268
|
+
id: c["id"].to_s,
|
|
269
|
+
name: c["name"].to_s,
|
|
270
|
+
image: c["image"].to_s,
|
|
271
|
+
command: (c["command"] || "").to_s,
|
|
272
|
+
state: c["state"].to_s,
|
|
273
|
+
status: c["status"].to_s,
|
|
274
|
+
ports: Array(c["ports"]),
|
|
275
|
+
created: c["created"].to_i
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
class << self
|
|
280
|
+
alias from_row from_graphql
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
104
284
|
# An image as reported by +bsdkrun images --json+.
|
|
105
285
|
ImageInfo = Data.define(:id, :reference, :digest, :size, :rootfs, :created_at) do
|
|
106
286
|
# @param row [Hash]
|
data/lib/bsdkrun/version.rb
CHANGED