Skip to content

Commit 13e7659

Browse files
committed
Sync FS operations
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
1 parent c721b31 commit 13e7659

File tree

3 files changed

+285
-0
lines changed

3 files changed

+285
-0
lines changed

docs/reference/api.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,30 @@ Add a host with a custom GitHub user to override the SSH keys:
176176
}
177177
```
178178

179+
### Block until ready (`?wait=` and `?timeout=`)
180+
181+
By default, `POST /hostgroup/NAME/nodes` returns as soon as the VM is
182+
scheduled — the in-guest agent may still be booting. Two optional query
183+
parameters let the server hold the response until readiness, so callers
184+
don't need their own poll loop on `/vm/{hostname}/health`:
185+
186+
- `wait=agent` — block until `agent_uptime > 0` (the agent is up and can
187+
accept `exec` calls).
188+
- `wait=userdata` — additionally block until `userdata_ran == true`
189+
(the cloud-init-style userdata script has completed).
190+
- `timeout=30s` — Go-style duration. Defaults to `5m` when `wait` is
191+
set. On timeout the VM is cleaned up and the endpoint returns **408**.
192+
193+
```bash
194+
curl --unix-socket ~/slicer-mac/slicer.sock \
195+
-X POST "http://localhost/hostgroup/sbox/nodes?wait=agent&timeout=30s" \
196+
-H "Content-Type: application/json" \
197+
-d '{"cpus":1,"ram_bytes":1073741824}'
198+
```
199+
200+
Typical response latency on Slicer-for-Mac is ~450 ms for `wait=agent`
201+
on a 1 vCPU / 1 GiB sandbox.
202+
179203
## List nodes in a Host Group
180204

181205
HTTP GET
@@ -366,11 +390,18 @@ Query parameters:
366390
- `stdout` (optional): enable stdout capture (true/false)
367391
- `stderr` (optional): enable stderr capture (true/false)
368392
- `permissions` (optional): permissions for the command
393+
- `buffered` (optional): see **Buffered exec** below.
369394

370395
Body: stdin data (if `stdin=true`)
371396

372397
Response: streaming newline-delimited JSON with stdout/stderr/exit_code
373398

399+
Newer agents emit typed frames on the streaming response — `started`
400+
(first frame, carries `pid` and `started_at`), `stdout`, `stderr`, and
401+
`exit` (last frame, carries `exit_code` and `ended_at`). Older agents
402+
emit untyped frames that mix `stdout`/`stderr`/`exit_code` on a single
403+
object; clients should tolerate both shapes.
404+
374405
Example response stream:
375406

376407
```json
@@ -389,6 +420,87 @@ The `error` variable may also be populated if there was a problem running, start
389420
{"error": "Some error message", "exit_code": 1}
390421
```
391422

423+
### Buffered exec (`?buffered=true`)
424+
425+
For callers that don't need live output, add `buffered=true` to get a
426+
single JSON response instead of an NDJSON stream:
427+
428+
```bash
429+
curl --unix-socket ~/slicer-mac/slicer.sock \
430+
-X POST "http://localhost/vm/sbox-1/exec?buffered=true&cmd=/bin/bash&args=-lc&args=echo%20hello" \
431+
-H "Content-Type: application/json"
432+
```
433+
434+
Response:
435+
436+
```json
437+
{ "stdout": "hello\n", "stderr": "", "exit_code": 0 }
438+
```
439+
440+
The request shape is the same (query parameters, not a JSON body). The
441+
response does not include `started_at` / `ended_at` — those are only on
442+
streaming frames. `stdin` is not supported with `buffered=true`; use
443+
the streaming endpoint if you need to pipe stdin.
444+
445+
## Filesystem operations
446+
447+
HTTP endpoints under `/vm/{hostname}/fs/*` read and mutate the guest
448+
filesystem directly via the in-guest agent. Prefer these over shelling
449+
out through `/exec` — they're faster, don't depend on `ls`/`stat`/`mkdir`
450+
being on the guest PATH, and avoid shell-quoting hazards on unusual
451+
filenames.
452+
453+
### List a directory
454+
455+
HTTP GET
456+
457+
`/vm/{hostname}/fs/readdir?path=/etc`
458+
459+
Response:
460+
461+
```json
462+
[
463+
{"name":"hostname","type":"file","size":7,"mtime":1776024825,"mode":"0644"},
464+
{"name":"ssh","type":"directory","size":4096,"mtime":1776024825,"mode":"0755"},
465+
{"name":"localtime","type":"symlink","size":27,"mtime":1775348015,"mode":"0777"}
466+
]
467+
```
468+
469+
`type` is one of `file`, `directory`, or `symlink`. `mtime` is a Unix
470+
timestamp in seconds; `mode` is the octal file mode.
471+
472+
### Stat a single entry
473+
474+
HTTP GET
475+
476+
`/vm/{hostname}/fs/stat?path=/etc/hostname`
477+
478+
Response: a single `SlicerFSInfo` object (same shape as one entry from
479+
`readdir`). Returns **404** if the path does not exist — useful as a
480+
cheap `exists` check.
481+
482+
### Create a directory
483+
484+
HTTP POST
485+
486+
`/vm/{hostname}/fs/mkdir`
487+
488+
Body:
489+
490+
```json
491+
{ "path": "/tmp/work", "recursive": true, "mode": "0755" }
492+
```
493+
494+
`recursive` and `mode` are optional.
495+
496+
### Remove a file or directory
497+
498+
HTTP DELETE
499+
500+
`/vm/{hostname}/fs/remove?path=/tmp/work&recursive=true`
501+
502+
Returns 200 on success.
503+
392504
## Manage secrets
393505

394506
HTTP GET
@@ -497,6 +609,50 @@ HTTP POST
497609

498610
Response 200: text/plain
499611

612+
## Suspend a VM to disk (Slicer-for-Mac only, for now)
613+
614+
HTTP POST
615+
616+
`/vm/{hostname}/suspend`
617+
618+
Takes a Firecracker snapshot of the VM's memory and disk state, then
619+
shuts down the underlying VM process. Memory is released. Use
620+
`/restore` to bring the VM back up from the snapshot.
621+
622+
> Availability: currently supported on Slicer-for-Mac only. Not
623+
> available on the Linux daemon yet.
624+
625+
Response 200: text/plain
626+
627+
## Restore a VM from a snapshot (Slicer-for-Mac only, for now)
628+
629+
HTTP POST
630+
631+
`/vm/{hostname}/restore`
632+
633+
Restores a previously-suspended VM from its Firecracker snapshot. The
634+
VM resumes with its memory and disk state intact.
635+
636+
> Availability: currently supported on Slicer-for-Mac only. Not
637+
> available on the Linux daemon yet.
638+
639+
Response 200: text/plain
640+
641+
## Relaunch an API-launched VM
642+
643+
HTTP POST
644+
645+
`/vm/{hostname}/relaunch`
646+
647+
Bring a previously-created API-launched VM back up using its existing
648+
disk image, without going through a fresh `POST /hostgroup/NAME/nodes`.
649+
Useful after an intentional shutdown when you want to resume the same
650+
sandbox identity rather than spin up a new one.
651+
652+
Available on both Slicer-for-Mac and the Linux daemon.
653+
654+
Response 200: `SlicerCreateNodeResponse`
655+
500656
## Forward TCP connections
501657

502658
HTTP GET

docs/tasks/execute-commands.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,30 @@ Combine with local commands using pipes and STDIO:
4545
# Pipe local file content to VM command
4646
cat /etc/hostname | slicer vm exec vm-1 -- base64 --wrap 9999
4747
```
48+
49+
## Streaming vs buffered exec via the REST API
50+
51+
The `/vm/{hostname}/exec` REST endpoint ships two response shapes:
52+
53+
- **Streaming** (default) — NDJSON frames as output arrives. Preferred
54+
for long-running commands where you want live stdout/stderr, or when
55+
you want to measure process-start latency separately from first-byte
56+
latency using the typed `started` frame.
57+
- **Buffered** — add `buffered=true` to get a single JSON document with
58+
`stdout`, `stderr`, and `exit_code` once the process exits. Preferred
59+
for short "run one thing, give me the result" calls; avoids the need
60+
for client-side NDJSON parsing.
61+
62+
```bash
63+
# Streaming (default)
64+
curl --unix-socket ~/slicer-mac/slicer.sock \
65+
-X POST "http://localhost/vm/vm-1/exec?cmd=uname&args=-a"
66+
67+
# Buffered
68+
curl --unix-socket ~/slicer-mac/slicer.sock \
69+
-X POST "http://localhost/vm/vm-1/exec?buffered=true&cmd=uname&args=-a"
70+
# → {"stdout":"Linux vm-1 ...\n","stderr":"","exit_code":0}
71+
```
72+
73+
`buffered=true` does not accept `stdin`; use the streaming endpoint if
74+
you need to pipe stdin data.

openapi.yaml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,48 @@ components:
311311

312312
SlicerExecWriteResult:
313313
type: object
314+
description: |
315+
One NDJSON frame from a streaming exec. Typed exec frames add
316+
`started` (first frame, emitted when the process is spawned) and
317+
`exit` (last frame, carries exit_code). Intermediate frames carry
318+
stdout or stderr chunks. Older agents may emit untyped frames that
319+
mix stdout/stderr/exit_code; clients should tolerate both shapes.
314320
properties:
321+
type:
322+
type: string
323+
enum: [started, stdout, stderr, exit]
324+
description: Frame type (newer agents). Absent on older agents.
315325
timestamp:
316326
type: string
317327
format: date-time
328+
started_at:
329+
type: string
330+
format: date-time
331+
description: Present on the `started` frame only.
332+
ended_at:
333+
type: string
334+
format: date-time
335+
description: Present on the `exit` frame only.
336+
pid:
337+
type: integer
338+
description: Present on the `started` frame only.
339+
stdout:
340+
type: string
341+
stderr:
342+
type: string
343+
exit_code:
344+
type: integer
345+
error:
346+
type: string
347+
348+
ExecResult:
349+
type: object
350+
description: |
351+
Buffered exec response — returned when the caller passes
352+
`?buffered=true` to `POST /vm/{hostname}/exec`. A single JSON
353+
document instead of an NDJSON stream.
354+
required: [stdout, stderr, exit_code]
355+
properties:
318356
stdout:
319357
type: string
320358
stderr:
@@ -323,6 +361,41 @@ components:
323361
type: integer
324362
error:
325363
type: string
364+
description: Non-empty when the agent could not run the command.
365+
366+
SlicerFSInfo:
367+
type: object
368+
description: Filesystem entry metadata returned by `/fs/readdir` and `/fs/stat`.
369+
required: [name, type, size, mtime, mode]
370+
properties:
371+
name:
372+
type: string
373+
type:
374+
type: string
375+
enum: [file, directory, symlink]
376+
size:
377+
type: integer
378+
format: int64
379+
mtime:
380+
type: integer
381+
format: int64
382+
description: Modification time as a Unix timestamp (seconds).
383+
mode:
384+
type: string
385+
description: Octal file mode, zero-padded (e.g. "0644").
386+
387+
SlicerFSMkdirRequest:
388+
type: object
389+
required: [path]
390+
properties:
391+
path:
392+
type: string
393+
recursive:
394+
type: boolean
395+
default: false
396+
mode:
397+
type: string
398+
description: Octal mode for the created directory (e.g. "0755").
326399

327400
SlicerAgentHealthResponse:
328401
type: object
@@ -420,13 +493,37 @@ paths:
420493

421494
post:
422495
summary: Create a node in a host group
496+
description: |
497+
Optionally block the response until the in-guest agent reports ready
498+
(`wait=agent`) or until cloud-init-style userdata finishes
499+
(`wait=userdata`). Server-side wait removes the need for client-side
500+
`/health` polling and returns a 408 on timeout (the VM is cleaned up
501+
before the error is returned).
423502
security:
424503
- BearerAuth: []
425504
parameters:
426505
- in: path
427506
name: name
428507
required: true
429508
schema: { type: string }
509+
- in: query
510+
name: wait
511+
description: |
512+
Block the response until readiness. `agent` waits for agent_uptime
513+
> 0 (process is ready to receive exec). `userdata` additionally
514+
waits for userdata_ran == true. Omit to return as soon as the VM
515+
is scheduled.
516+
schema:
517+
type: string
518+
enum: [agent, userdata]
519+
- in: query
520+
name: timeout
521+
description: |
522+
Go-style duration (e.g. `30s`, `2m`). Only meaningful when `wait`
523+
is set. Defaults to `5m`.
524+
schema:
525+
type: string
526+
default: "5m"
430527
requestBody:
431528
required: true
432529
content:
@@ -458,6 +555,11 @@ paths:
458555
content:
459556
application/json:
460557
schema: { $ref: '#/components/schemas/ErrorResponse' }
558+
"408":
559+
description: Wait timed out before readiness; VM is cleaned up
560+
content:
561+
application/json:
562+
schema: { $ref: '#/components/schemas/ErrorResponse' }
461563
"500":
462564
description: Internal error
463565
content:

0 commit comments

Comments
 (0)