diff --git a/README.md b/README.md index 558e47f..6be3645 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,11 @@ sandbox, at the address in `$sandbox->url` — read secondary ports out of **Treat those URLs as secrets.** Reaching one is sufficient to run commands in the sandbox, which is why the hostname carries an unguessable token instead of the id. +A read tells you what the sandbox is, not just where it is: `$sandbox->image`, +`$sandbox->cpu`, and `$sandbox->memory` are the shape it is running in, recorded +when its pod was created, so you never have to keep a record of what you asked +for. They are null for a sandbox created before the orchestrator recorded it. + A sandbox that fails to materialize is not an error response: `create()` returns a status with `SandboxState::Failed` and an `error`, because the sandbox exists as a record you can read and delete. diff --git a/src/Model/Data.php b/src/Model/Data.php index 3965bf1..1c3e70b 100644 --- a/src/Model/Data.php +++ b/src/Model/Data.php @@ -70,6 +70,23 @@ public static function optionalInt(array $data, string $key, string $context): ? return $value; } + /** + * @param array $data + */ + public static function optionalFloat(array $data, string $key, string $context): ?float + { + $value = $data[$key] ?? null; + if ($value === null) { + return null; + } + + if (! \is_float($value) && ! \is_int($value)) { + throw new ClientException("Invalid {$context}: {$key} must be a number."); + } + + return (float) $value; + } + /** * @param array $data * @return list diff --git a/src/Model/SandboxStatus.php b/src/Model/SandboxStatus.php index 66a205a..a120d5d 100644 --- a/src/Model/SandboxStatus.php +++ b/src/Model/SandboxStatus.php @@ -15,6 +15,8 @@ { /** * @param array $urls Every port the sandbox serves, keyed by port number. + * @param string|null $image The shape it is running in, recorded when its pod was created. + * Absent on a sandbox created before the orchestrator recorded it. */ public function __construct( public string $id, @@ -22,6 +24,9 @@ public function __construct( public ?string $poolId = null, public ?string $url = null, public array $urls = [], + public ?string $image = null, + public ?float $cpu = null, + public ?int $memory = null, public ?string $error = null, ) {} @@ -36,6 +41,9 @@ public static function fromArray(array $data): self poolId: Data::optionalString($data, 'poolId', 'sandbox status'), url: Data::optionalString($data, 'url', 'sandbox status'), urls: Data::stringMap($data, 'urls', 'sandbox status'), + image: Data::optionalString($data, 'image', 'sandbox status'), + cpu: Data::optionalFloat($data, 'cpu', 'sandbox status'), + memory: Data::optionalInt($data, 'memory', 'sandbox status'), error: Data::optionalString($data, 'error', 'sandbox status'), ); } diff --git a/tests/SandboxesTest.php b/tests/SandboxesTest.php index e391b52..6b29824 100644 --- a/tests/SandboxesTest.php +++ b/tests/SandboxesTest.php @@ -109,6 +109,28 @@ public function test_a_failed_sandbox_is_a_status_not_an_error(): void $this->assertNull($sandbox->url); } + public function test_a_read_reports_the_shape_the_sandbox_runs_in(): void + { + $http = new Client([ + new Response(200, body: new Stream( + '{"id":"py-1","poolId":"py","status":"ready","image":"python:3.12-slim","cpu":2,"memory":2048}' + )), + new Response(200, body: new Stream('{"id":"old-1","status":"ready"}')), + ]); + $sandboxes = new Sandboxes($http); + + $sandbox = $sandboxes->get('py-1'); + $this->assertSame('python:3.12-slim', $sandbox->image); + $this->assertEqualsWithDelta(2.0, $sandbox->cpu, PHP_FLOAT_EPSILON); + $this->assertSame(2048, $sandbox->memory); + + // A sandbox created before the orchestrator recorded a shape reports none. + $legacy = $sandboxes->get('old-1'); + $this->assertNull($legacy->image); + $this->assertNull($legacy->cpu); + $this->assertNull($legacy->memory); + } + public function test_get_list_and_delete(): void { $http = new Client([