Bsdkrun.Client (bsdkrun_ex v0.2.0)

Copy Markdown View Source

A remote client for a bsdkrund daemon's GraphQL API — talks straight to POST <url> (queries/mutations) and a graphql-transport-ws socket (subscriptions), instead of shelling out to a local bsdkrun binary like Bsdkrun.Sandbox does. Same contract the web frontend speaks (web/src/lib/graphql.ts, web/src/lib/api.ts) and the daemon documents in daemon/README.md.

{:ok, client} = Bsdkrun.Client.from_env()
{:ok, machines} = Bsdkrun.Client.list(client, true)
{:ok, %{exit_code: 0, output: out}} = Bsdkrun.Client.exec(client, "abc123", ["uname", "-a"])

Bsdkrun.Client.new/1 builds a client lazily — no connection is made until the first call. Queries and mutations go straight over HTTP (Bsdkrun.GraphQL, built on :httpc); anything that streams (exec/4, shell/3, follow_logs/3, subscribe/4) shares one graphql-transport-ws socket per {url, token} pair — a Bsdkrun.GraphQLSocket GenServer, started lazily under Bsdkrun.Client.SocketSupervisor on first use and found again via Bsdkrun.Client.Registry (see ensure_conn/1). A %Client{} itself stays a plain, immutable struct — callers never see the GenServer.

Live output: messages, not callbacks, by default

follow_logs/3 and shell/3 deliver output by sending messages to the calling process's mailbox — {:bsdkrun_logs, subscription_id, event} and {:bsdkrun_shell, session_id, event}} respectively, where event is {:data, binary}, {:exit, exit_code}, {:error, %Bsdkrun.Error{}} or :complete. That is the idiomatic default for this codebase — every other blocking call in this SDK (Bsdkrun.Sandbox.exec/3 included) already returns to an Elixir process, so a mailbox is the natural sink, no process-owned closure required. Pass on_data: fn id, event -> ... end in opts to receive a callback instead of messages (and owner: pid to target a different process's mailbox than the caller's). The escape hatch subscribe/4 follows the same convention with {:bsdkrun_subscription, id, event}, event being the raw {:next, data} | {:error, _} | :complete.

exec/4 is built on the same shellOutput subscription internally, but blocks the calling process until the command exits (a receive loop, not a GenServer.call — see await_exec/3), matching this SDK's otherwise synchronous feel (Bsdkrun.Sandbox.exec/3 blocks too).

Every fallible function returns {:ok, value} | {:error, %Bsdkrun.Error{}}; list/2 and get/2 (mirroring Bsdkrun.Sandbox) and from_env/0 also have bang counterparts that unwrap or raise.

Summary

Functions

Snapshot a machine into a named flavor, like docker commit.

Run a command to completion and collect its output. Blocks the calling process until the command exits (opts[:timeout], default 300_000ms). command is an argv list or a bare program name string. Opts: :env (a map or "K=V" list), :rows/:cols (pty size, default 24x80), :timeout.

Follow the machine's console log live, over the machineLogs subscription. Opts: :boot (bsdkrun's own boot log, default false), :on_data (fn subscription_id, event -> end) and :owner — see the module doc. With no :on_data, events arrive as {:bsdkrun_logs, subscription_id, event} messages, event being {:data, binary} | {:exit, exit_code} | {:error, %Bsdkrun.Error{}} | :complete.

Build a client from BSDKRUN_URL / BSDKRUN_TOKEN. BSDKRUN_URL unset is an error; BSDKRUN_URL set without BSDKRUN_TOKEN is also an error — this never silently proceeds unauthenticated (mirrors daemon/src/client.rs's RemoteConfig::from_env for the gRPC client; different env vars, same philosophy, since a GraphQL endpoint is a different port and URL shape).

Like from_env/0, but returns the client or raises Bsdkrun.Error.

Fetch a single machine by id or name (a unique prefix is enough), or nil if there is no such machine.

Like get/2, but returns the machine (or nil) or raises Bsdkrun.Error.

List machines. all: true includes exited ones (default: running only).

Like list/2, but returns the list or raises Bsdkrun.Error.

Read the machine's console log as a single string, one-shot. Pass boot: true for bsdkrun's own boot log.

Build a client. Does not connect — connections are made lazily, on first use.

Normalize what a person actually pastes into the daemon's GraphQL endpoint URL: trim, default to http:// when no scheme is given, strip trailing slashes, and append /graphql unless it is already there. Mirrors web/src/lib/connection.ts's normalizeUrl.

Remove one or more machines and their state. force: true stops them first if running.

Run a raw query or mutation. Returns {:ok, data} (the response's data field) or {:error, %Bsdkrun.Error{}}.

Boot a FreeBSD/NetBSD machine. opts maps to RunBsdInput, :os being :freebsd or :netbsd.

Boot a named flavor. opts maps to RunFlavorInput.

Boot a Linux (OCI) machine. opts maps to RunLinuxInput; see the module doc for the input shape.

Boot a Nanos unikernel. opts maps to RunNanosInput. No agent — no exec/4/commit/4.

Boot an OSv unikernel. opts maps to RunOsvInput. No agent — no exec/4/commit/4.

Boot a Unikraft unikernel. opts maps to RunUnikraftInput. No disk, no agent — no exec/4/commit/4.

Open a live interactive session on a machine (or run opts[:command] if given, instead of a login shell) and return a Shell.t() handle. Output is delivered as it arrives, exactly as follow_logs/3 does: opts[:on_data] (fn session_id, event -> end) or, with no callback, {:bsdkrun_shell, session_id, event} messages to opts[:owner] (default: the caller). event is {:data, binary} | {:exit, exit_code} | {:error, _} | :complete.

Restart a stopped machine in place — same id, disk/rootfs, resources.

Stop the machine (BSD guests clean-poweroff; Linux is SIGTERM'd).

Start a raw subscription. opts[:on_data] (arity 2, fn id, event -> end) receives {:next, data} | {:error, %Bsdkrun.Error{}} | :complete; with no callback, the same arrives as {:bsdkrun_subscription, id, event} messages to opts[:owner] (default: the calling process). Returns a Subscription.t() — cancel it with Subscription.cancel/1.

Change a machine's recorded vCPU / RAM (:cpus, :mem). Applies on next start/2.

Types

t()

@type t() :: %Bsdkrun.Client{token: String.t(), url: String.t()}

Functions

commit(client, id, name, description \\ "")

@spec commit(t(), String.t(), String.t(), String.t()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Snapshot a machine into a named flavor, like docker commit.

exec(client, machine_id, command, opts \\ [])

@spec exec(t(), String.t(), String.t() | [String.t()], keyword()) ::
  {:ok, %{exit_code: integer() | nil, output: binary()}}
  | {:error, Bsdkrun.Error.t()}

Run a command to completion and collect its output. Blocks the calling process until the command exits (opts[:timeout], default 300_000ms). command is an argv list or a bare program name string. Opts: :env (a map or "K=V" list), :rows/:cols (pty size, default 24x80), :timeout.

follow_logs(client, id, opts \\ [])

@spec follow_logs(t(), String.t(), keyword()) ::
  {:ok, Bsdkrun.Client.Subscription.t()} | {:error, Bsdkrun.Error.t()}

Follow the machine's console log live, over the machineLogs subscription. Opts: :boot (bsdkrun's own boot log, default false), :on_data (fn subscription_id, event -> end) and :owner — see the module doc. With no :on_data, events arrive as {:bsdkrun_logs, subscription_id, event} messages, event being {:data, binary} | {:exit, exit_code} | {:error, %Bsdkrun.Error{}} | :complete.

from_env()

@spec from_env() :: {:ok, t()} | {:error, Bsdkrun.Error.t()}

Build a client from BSDKRUN_URL / BSDKRUN_TOKEN. BSDKRUN_URL unset is an error; BSDKRUN_URL set without BSDKRUN_TOKEN is also an error — this never silently proceeds unauthenticated (mirrors daemon/src/client.rs's RemoteConfig::from_env for the gRPC client; different env vars, same philosophy, since a GraphQL endpoint is a different port and URL shape).

from_env!()

@spec from_env!() :: t()

Like from_env/0, but returns the client or raises Bsdkrun.Error.

get(client, id)

@spec get(t(), String.t()) ::
  {:ok, Bsdkrun.Types.SandboxInfo.t() | nil} | {:error, Bsdkrun.Error.t()}

Fetch a single machine by id or name (a unique prefix is enough), or nil if there is no such machine.

get!(client, id)

@spec get!(t(), String.t()) :: Bsdkrun.Types.SandboxInfo.t() | nil

Like get/2, but returns the machine (or nil) or raises Bsdkrun.Error.

list(client, all \\ false)

@spec list(t(), boolean()) ::
  {:ok, [Bsdkrun.Types.SandboxInfo.t()]} | {:error, Bsdkrun.Error.t()}

List machines. all: true includes exited ones (default: running only).

list!(client, all \\ false)

@spec list!(t(), boolean()) :: [Bsdkrun.Types.SandboxInfo.t()]

Like list/2, but returns the list or raises Bsdkrun.Error.

logs(client, id, boot \\ false)

@spec logs(t(), String.t(), boolean()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Read the machine's console log as a single string, one-shot. Pass boot: true for bsdkrun's own boot log.

new(opts)

@spec new(keyword()) :: t()

Build a client. Does not connect — connections are made lazily, on first use.

normalize_url(input)

@spec normalize_url(String.t()) :: String.t()

Normalize what a person actually pastes into the daemon's GraphQL endpoint URL: trim, default to http:// when no scheme is given, strip trailing slashes, and append /graphql unless it is already there. Mirrors web/src/lib/connection.ts's normalizeUrl.

remove(client, ids, force \\ false)

@spec remove(t(), String.t() | [String.t()], boolean()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Remove one or more machines and their state. force: true stops them first if running.

request(client, query, variables \\ %{})

@spec request(t(), String.t(), map()) :: {:ok, term()} | {:error, Bsdkrun.Error.t()}

Run a raw query or mutation. Returns {:ok, data} (the response's data field) or {:error, %Bsdkrun.Error{}}.

run_bsd(client, opts)

@spec run_bsd(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a FreeBSD/NetBSD machine. opts maps to RunBsdInput, :os being :freebsd or :netbsd.

run_flavor(client, opts)

@spec run_flavor(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a named flavor. opts maps to RunFlavorInput.

run_linux(client, opts)

@spec run_linux(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a Linux (OCI) machine. opts maps to RunLinuxInput; see the module doc for the input shape.

run_nanos(client, opts)

@spec run_nanos(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a Nanos unikernel. opts maps to RunNanosInput. No agent — no exec/4/commit/4.

run_osv(client, opts)

@spec run_osv(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot an OSv unikernel. opts maps to RunOsvInput. No agent — no exec/4/commit/4.

run_unikraft(client, opts)

@spec run_unikraft(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a Unikraft unikernel. opts maps to RunUnikraftInput. No disk, no agent — no exec/4/commit/4.

shell(client, machine_id, opts \\ [])

@spec shell(t(), String.t(), keyword()) ::
  {:ok, Bsdkrun.Client.Shell.t()} | {:error, Bsdkrun.Error.t()}

Open a live interactive session on a machine (or run opts[:command] if given, instead of a login shell) and return a Shell.t() handle. Output is delivered as it arrives, exactly as follow_logs/3 does: opts[:on_data] (fn session_id, event -> end) or, with no callback, {:bsdkrun_shell, session_id, event} messages to opts[:owner] (default: the caller). event is {:data, binary} | {:exit, exit_code} | {:error, _} | :complete.

start(client, id)

@spec start(t(), String.t()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Restart a stopped machine in place — same id, disk/rootfs, resources.

stop(client, id)

@spec stop(t(), String.t()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Stop the machine (BSD guests clean-poweroff; Linux is SIGTERM'd).

subscribe(client, query, variables \\ %{}, opts \\ [])

@spec subscribe(t(), String.t(), map(), keyword()) ::
  {:ok, Bsdkrun.Client.Subscription.t()} | {:error, Bsdkrun.Error.t()}

Start a raw subscription. opts[:on_data] (arity 2, fn id, event -> end) receives {:next, data} | {:error, %Bsdkrun.Error{}} | :complete; with no callback, the same arrives as {:bsdkrun_subscription, id, event} messages to opts[:owner] (default: the calling process). Returns a Subscription.t() — cancel it with Subscription.cancel/1.

update(client, id, opts \\ [])

@spec update(t(), String.t(), keyword()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Change a machine's recorded vCPU / RAM (:cpus, :mem). Applies on next start/2.