bsdkrun/client
A client for a remote bsdkrund daemon’s GraphQL API — the network
counterpart to bsdkrun/sandbox, which shells out to a local bsdkrun
binary. Point one at a daemon (client.new/client.from_env) and get
the same SandboxInfo records back over HTTP + WebSocket instead of a
subprocess.
import bsdkrun/client
import gleam/option.{None}
let assert Ok(c) = client.from_env()
let assert Ok(machines) = client.list(c, all: True)
let assert Ok(res) = client.exec(c, id: "abc123", command: ["uname", "-a"], env: [])
Every function returns Result(_, GraphqlError) — an alias for
bsdkrun/error’s Error type (see that module), extended with the two
variants this client can produce: GraphqlError (a resolver error, a
malformed response, or the daemon being unreachable) and AuthError
(the daemon rejected the bearer token). Pattern-match on
error.GraphqlError/error.AuthError.
Design notes
- HTTP transport:
bsdkrun/graphql_transport, over:httpc— see that module. WebSocket transport (subscriptions):bsdkrun/ws, hand-rolled RFC 6455 framing over:gen_tcp/:ssl— see that module’s doc for the split between its pure, unit-tested protocol functions and its Erlang-FFI connection process. - One shared socket per
Client:Clientitself is an immutable url/token pair (constructing one does not connect anything). The socket is opened lazily on first use and cached — seebsdkrun/ws.ensure. execblocks;shell/follow_logs/subscribedo not.execruns in the calling process and does a boundedsubject.receiveloop itself (see the module doc onbsdkrun/subjectfor whatSubjectis here, givengleam_erlangis not a dependency this SDK can use). The other three each spawn one small background process (via Erlang’s ownspawn/1, called directly through@external) whose only job is to translate the WebSocket connection’s raw events into this module’s richer event types and forward them to aSubjectthe caller reads at its own pace — an unbounded wait, since an idle interactive shell or a quiet log stream legitimately has nothing to say for a long time and should not be timed out for it. A dead connection still always produces a terminal event (the WS actor notifies every open subscription when its socket closes), so this does not risk an actual forever-hang.
Types
BsdOs (daemon/src/graphql.rs ~line 324): which BSD to boot.
pub type BsdOs {
Freebsd
Netbsd
}
Constructors
-
Freebsd -
Netbsd
A handle to a daemon’s GraphQL endpoint: its URL and bearer token.
Constructing one does not connect to anything — the HTTP transport is a
plain request per call, and the WebSocket connection (only needed for
exec/shell/follow_logs/subscribe) is opened lazily on first use.
pub opaque type Client
A bsdkrun/client error — every variant bsdkrun/error.Error has, but
only GraphqlError and AuthError are ones this module itself produces.
pub type GraphqlError =
error.Error
The net field shared by every run* mutation except runFlavor
(daemon/src/graphql.rs’s NetInput, ~line 360).
pub type NetOptions {
NetOptions(
no_net: Bool,
ports: List(String),
mac: option.Option(String),
network: option.Option(String),
name: option.Option(String),
)
}
Constructors
-
NetOptions( no_net: Bool, ports: List(String), mac: option.Option(String), network: option.Option(String), name: option.Option(String), )
RunBsdInput (daemon/src/graphql.rs ~line 406).
pub type RunBsdOptions {
RunBsdOptions(
os: BsdOs,
version: option.Option(String),
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
volume: option.Option(String),
persist: Bool,
force: Bool,
firmware: option.Option(String),
attach_disk: List(String),
disk_size: option.Option(String),
repo: option.Option(String),
command: List(String),
)
}
Constructors
-
RunBsdOptions( os: BsdOs, version: option.Option(String), cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), volume: option.Option(String), persist: Bool, force: Bool, firmware: option.Option(String), attach_disk: List(String), disk_size: option.Option(String), repo: option.Option(String), command: List(String), )
RunFlavorInput (daemon/src/graphql.rs ~line 506).
pub type RunFlavorOptions {
RunFlavorOptions(
name: String,
cpus: option.Option(Int),
mem: option.Option(Int),
ports: List(String),
volume: option.Option(String),
repo: option.Option(String),
)
}
Constructors
-
RunFlavorOptions( name: String, cpus: option.Option(Int), mem: option.Option(Int), ports: List(String), volume: option.Option(String), repo: option.Option(String), )
RunLinuxInput (daemon/src/graphql.rs ~line 384).
pub type RunLinuxOptions {
RunLinuxOptions(
image: String,
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
volume: option.Option(String),
mounts: List(String),
env: List(String),
entrypoint: option.Option(String),
initramfs: Bool,
kernel: option.Option(String),
kernel_version: option.Option(String),
console: option.Option(String),
repo: option.Option(String),
command: List(String),
)
}
Constructors
-
RunLinuxOptions( image: String, cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), volume: option.Option(String), mounts: List(String), env: List(String), entrypoint: option.Option(String), initramfs: Bool, kernel: option.Option(String), kernel_version: option.Option(String), console: option.Option(String), repo: option.Option(String), command: List(String), )
RunNanosInput (daemon/src/graphql.rs ~line 428). Nanos has no agent
(no exec/shell/commit), but does have a root disk, so persist is
the one disk option it takes.
pub type RunNanosOptions {
RunNanosOptions(
image: String,
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
kernel: option.Option(String),
cmdline: option.Option(String),
persist: Bool,
)
}
Constructors
-
RunNanosOptions( image: String, cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), kernel: option.Option(String), cmdline: option.Option(String), persist: Bool, )
RunOsvInput (daemon/src/graphql.rs ~line 467). Like Nanos, no agent —
but it does have a root filesystem, so unlike Unikraft it takes the disk
options.
pub type RunOsvOptions {
RunOsvOptions(
image: String,
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
cmdline: option.Option(String),
disk: option.Option(String),
no_disk: Bool,
attach_disk: List(String),
gic: option.Option(String),
persist: Bool,
volume: option.Option(String),
)
}
Constructors
-
RunOsvOptions( image: String, cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), cmdline: option.Option(String), disk: option.Option(String), no_disk: Bool, attach_disk: List(String), gic: option.Option(String), persist: Bool, volume: option.Option(String), )
RunSolo5Input (daemon/src/graphql.rs). Solo5 (MirageOS) runs under
the solo5-hvt tender rather than libkrun; the unikernel declares its
own network and block devices in its MFT1 manifest note, so only what
the host alone can know is carried: block backing files ("NAME=FILE")
and the args handed to the unikernel itself. Always a single vCPU —
cpus above 1 is warned about and ignored. No disk, no agent.
pub type RunSolo5Options {
RunSolo5Options(
path: option.Option(String),
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
block: List(String),
args: List(String),
)
}
Constructors
-
RunSolo5Options( path: option.Option(String), cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), block: List(String), args: List(String), )
RunUnikraftInput (daemon/src/graphql.rs ~line 449). A unikernel has
no disk and no agent, so this carries none of the volume/persist/
repo/command fields the other guests take.
pub type RunUnikraftOptions {
RunUnikraftOptions(
path: option.Option(String),
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
cmdline: option.Option(String),
initramfs: option.Option(String),
mounts: List(String),
)
}
Constructors
-
RunUnikraftOptions( path: option.Option(String), cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), cmdline: option.Option(String), initramfs: option.Option(String), mounts: List(String), )
A live, interactive shell session opened with shell. Output streams to
shell_output’s Subject; shell_send/shell_resize/shell_close
drive it — these four function names (there is no single “shell handle”
API in the daemon’s schema to mirror 1:1) are this SDK’s own choice of
shape for the “session ID + a way to write/resize/close it” the design
note asked for.
pub opaque type ShellSession
Values
pub fn commit(
client: Client,
id id: String,
name name: String,
description description: String,
) -> Result(types.CommandResult, error.Error)
Snapshot a machine into a named flavor, like docker commit.
pub fn exec(
client: Client,
id id: String,
command command: List(String),
env env: List(String),
) -> Result(types.ExecResult, error.Error)
One-shot command execution: openShell (with command, so it runs that
instead of a login shell) + shellOutput + closeShell, exactly the
sequence daemon/README.md’s “Interactive shells over GraphQL” section
describes (open, then subscribe, then wait for exit — no input to
send). Blocks the calling process until the command exits.
pub fn follow_logs(
client: Client,
id id: String,
follow follow: Bool,
boot boot: Bool,
) -> Result(subject.Subject(types.ShellEvent), error.Error)
Follow a machine’s console log live — everything buffered since the
subscription started, then new lines as they’re written. Ends
(ShellClosed) when the underlying bsdkrun logs -f exits, which with
follow: True is when the machine stops.
pub fn from_env() -> Result(Client, String)
Build a client from BSDKRUN_URL/BSDKRUN_TOKEN. BSDKRUN_URL unset is
Error (nothing to connect to). BSDKRUN_URL set without
BSDKRUN_TOKEN is also Error, deliberately — not a silent fallback
to running unauthenticated — mirroring the same rule
daemon/src/client.rs’s RemoteConfig::from_env applies to the gRPC
BSDKRUN_HOST/BSDKRUN_TOKEN pair (a different pair of variables: this
is the GraphQL port, not the gRPC one, so it gets its own).
pub fn get(
client: Client,
id id: String,
) -> Result(option.Option(types.SandboxInfo), error.Error)
A single machine by id, name, or unique id prefix — or None if there is
no such machine.
pub fn list(
client: Client,
all all: Bool,
) -> Result(List(types.SandboxInfo), error.Error)
Machines. all: True includes stopped ones, like bsdkrun ps -a.
pub fn logs(
client: Client,
id id: String,
boot boot: Bool,
) -> Result(String, error.Error)
A machine’s console log as a single string, as it stands right now. Use
follow_logs to watch it live.
pub fn net_options() -> NetOptions
Default networking: attached, no forwards, no mac/network/name override.
pub fn new(url url: String, token token: String) -> Client
Build a client explicitly. url is normalized exactly like
bsdkrun/graphql_transport.normalize_url (and the web UI’s connection
setup): a scheme is assumed if missing, trailing slashes are stripped,
and /graphql is appended if the path doesn’t already end with it.
pub fn remove(
client: Client,
ids ids: List(String),
force force: Bool,
) -> Result(types.CommandResult, error.Error)
Remove one or more machines. force stops any that are still running
first.
pub fn request(
client: Client,
query query: String,
variables variables: dynamic.Dynamic,
) -> Result(dynamic.Dynamic, error.Error)
Run any query or mutation bsdkrun/client’s typed API does not cover.
variables is a Dynamic — build one from a Dict/List/literal via
gleam/dynamic.from; see bsdkrun_remote_ffi.erl’s dynamic_to_json/1
doc comment for exactly which shapes it understands. Returns the data
field, for the caller to decode the same way bsdkrun/types’s decoders
do (gleam/dynamic/decode).
pub fn run_bsd(
client: Client,
opts opts: RunBsdOptions,
) -> Result(String, error.Error)
Boot a FreeBSD/NetBSD machine, detached. Returns the new machine’s id.
pub fn run_bsd_options(os: BsdOs) -> RunBsdOptions
Defaults for RunBsdOptions: everything unset/empty/false except os.
pub fn run_flavor(
client: Client,
opts opts: RunFlavorOptions,
) -> Result(String, error.Error)
Boot a saved flavor, detached. Returns the new machine’s id.
pub fn run_flavor_options(name: String) -> RunFlavorOptions
Defaults for RunFlavorOptions: everything unset/empty except name.
pub fn run_linux(
client: Client,
opts opts: RunLinuxOptions,
) -> Result(String, error.Error)
Boot a Linux (OCI) machine, detached. Returns the new machine’s id.
pub fn run_linux_options(image: String) -> RunLinuxOptions
Defaults for RunLinuxOptions: everything unset/empty except image.
pub fn run_nanos(
client: Client,
opts opts: RunNanosOptions,
) -> Result(String, error.Error)
Boot a Nanos unikernel, detached. Returns the new machine’s id.
pub fn run_nanos_options(image: String) -> RunNanosOptions
Defaults for RunNanosOptions: everything unset/false except image.
pub fn run_osv(
client: Client,
opts opts: RunOsvOptions,
) -> Result(String, error.Error)
Boot an OSv unikernel, detached. Returns the new machine’s id.
pub fn run_osv_options(image: String) -> RunOsvOptions
Defaults for RunOsvOptions: everything unset/empty/false except
image.
pub fn run_solo5(
client: Client,
opts opts: RunSolo5Options,
) -> Result(String, error.Error)
Boot a Solo5 (MirageOS) unikernel, detached. Returns the new machine’s id.
pub fn run_solo5_options() -> RunSolo5Options
Defaults for RunSolo5Options: everything unset/empty (path defaults
to "." daemon-side when left None).
pub fn run_unikraft(
client: Client,
opts opts: RunUnikraftOptions,
) -> Result(String, error.Error)
Boot a Unikraft unikernel, detached. Returns the new machine’s id.
pub fn run_unikraft_options() -> RunUnikraftOptions
Defaults for RunUnikraftOptions: everything unset/empty (path
defaults to "." daemon-side when left None).
pub fn shell(
client: Client,
id id: String,
command command: option.Option(List(String)),
env env: List(String),
rows rows: Int,
cols cols: Int,
) -> Result(ShellSession, error.Error)
Open an interactive shell (or, with command, run that command with a
live, writable session rather than blocking — the non-blocking sibling
of exec). Output arrives on shell_output(session) as it’s produced;
this function itself returns as soon as the session and its subscription
are set up.
pub fn shell_close(session: ShellSession) -> Nil
Close the session and kill its command. Idempotent.
pub fn shell_id(session: ShellSession) -> String
The daemon-side session id, if you need it for client.request.
pub fn shell_output(
session: ShellSession,
) -> subject.Subject(types.ShellEvent)
This session’s live output Subject — subject.receive it in a loop.
pub fn shell_resize(
session: ShellSession,
rows rows: Int,
cols cols: Int,
) -> Result(Nil, error.Error)
Apply a terminal resize, so full-screen programs in the guest redraw.
pub fn shell_send(
session: ShellSession,
data: BitArray,
) -> Result(Nil, error.Error)
Send keystrokes/input to the session.
pub fn start(
client: Client,
id id: String,
) -> Result(types.CommandResult, error.Error)
Restart a stopped machine in place.
pub fn stop(
client: Client,
id id: String,
) -> Result(types.CommandResult, error.Error)
Stop a machine.
pub fn subscribe(
client: Client,
query query: String,
variables variables: dynamic.Dynamic,
) -> Result(subject.Subject(types.SubscriptionEvent), error.Error)
Subscribe to any subscription bsdkrun/client’s typed API does not
cover. Each next payload’s data arrives as SubNext(Dynamic) on the
returned Subject, terminated by SubError/SubComplete — decode it
the same way request’s result.
pub fn update(
client: Client,
id id: String,
cpus cpus: option.Option(Int),
mem mem: option.Option(Int),
) -> Result(types.CommandResult, error.Error)
Change a machine’s recorded vCPU/RAM. Applies on the next start.