Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ Create one Blaxel sandbox with an explicit memory size in MiB. Blaxel derives CP

## What this means

Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Blaxel requires at least 256 MiB when memory is set.
Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Omitted memory keeps the Blaxel default.

## Edge cases

- Omitted memory keeps the Blaxel default.
- Fractional or sub-256 MiB values fail validation.
- Memory values that Blaxel rejects are returned as provider errors.

## Run

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ Create one Blaxel sandbox with an explicit memory size in MiB. Blaxel derives CP

## What this means

Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Blaxel requires at least 256 MiB when memory is set.
Only `resources.memoryMib` is mapped. CPU cores, limits, and disk MiB overrides are rejected. Omitted memory keeps the Blaxel default.

## Edge cases

- Omitted memory keeps the Blaxel default.
- Fractional or sub-256 MiB values fail validation.
- Memory values that Blaxel rejects are returned as provider errors.

## Run

Expand Down
8 changes: 4 additions & 4 deletions sdks/go/providers/blaxel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ rejected. `Scope` fields other than `Workspace` are rejected.
Create from a registry image with an explicit name. You can set labels,
environment variables, memory, ports, outbound domains, maximum lifetime, and
idle terminate. Max-age TTL must be at least 5m; idle TTL must be at least 1m.
Create provisioning timeout is not mapped (the Go Blaxel SDK has no create
timeout option).
Ports 80, 443, and 8080 are rejected (Blaxel-reserved). Create provisioning
timeout is not mapped (the Go Blaxel SDK has no create timeout option).

On the handle: `Get`, `Refresh`, `Delete`, and `Close`. `Get` and `Refresh` look
up the sandbox by name. `Attach`, `Start`, `Stop`, `Pause`, `Archive`, and
Expand All @@ -85,8 +85,8 @@ exit code is success for the SDK—read `GetExitCode`. Blaxel returns stdout and
stderr. Omit or zero `Timeout` for no Blaxel command limit. Command timeouts
must be whole seconds. Cancel the context to stop waiting.

Memory is allocated in MiB and must be at least 256 when set. Blaxel derives
CPU from memory, so `cpu_cores` and disk MiB overrides are rejected.
Memory is allocated in MiB. Blaxel derives CPU from memory, so `cpu_cores` and
disk MiB overrides are rejected.

## License

Expand Down
3 changes: 3 additions & 0 deletions sdks/go/providers/blaxel/creation.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ func (c *creation) network() error {
if binding == nil {
continue
}
if binding.Port == 80 || binding.Port == 443 || binding.Port == 8080 {
return providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldNetworkPortsPort, "ports 80, 443, and 8080 are reserved by Blaxel")
}
protocol, err := blaxelPortProtocol(binding.Transport)
if err != nil {
return err
Expand Down
3 changes: 3 additions & 0 deletions sdks/go/providers/blaxel/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func mapProviderError(err error, operation string) error {
kind = sandbox.ErrorKindResourceExhausted
case sdkblaxel.ErrBadRequest, sdkblaxel.ErrPolicyViolation:
kind = sandbox.ErrorKindInvalidArgument
case "SANDBOX_ALREADY_EXISTS", "VOLUME_ALREADY_ATTACHED":
// Control-plane codes (not in gateway apierror constants).
kind = sandbox.ErrorKindAlreadyExists
}
}
if kind == sandbox.ErrorKindUnknown {
Expand Down
17 changes: 17 additions & 0 deletions sdks/go/providers/blaxel/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,20 @@ func TestMapProviderErrorPreservesContext(t *testing.T) {
t.Fatalf("error = %v detail = %#v", err, detail)
}
}

func TestMapProviderErrorSandboxAlreadyExists(t *testing.T) {
parsed, err := url.Parse("https://example.invalid/sandboxes/demo")
if err != nil {
t.Fatal(err)
}
mapped := mapProviderError(&sdkblaxel.Error{
StatusCode: http.StatusConflict,
ErrorCode: "SANDBOX_ALREADY_EXISTS",
Request: &http.Request{Method: http.MethodPost, URL: parsed},
Response: &http.Response{StatusCode: http.StatusConflict},
}, "create")
var detail *sandbox.Error
if !errors.As(mapped, &detail) || detail.Kind != sandbox.ErrorKindAlreadyExists {
t.Fatalf("error = %v detail = %#v", mapped, detail)
}
}
3 changes: 3 additions & 0 deletions sdks/go/providers/blaxel/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ func (b *backend) Stat(ctx context.Context, id, path string, _ *sandbox.StatOpti
if err != nil {
return nil, err
}
// SandboxInstanceFSService has no public Get/Stat; metadata is on the lower
// SandboxFilesystemService. Rebuild with the instance base URL so Stat uses
// the same sandbox endpoint as instance.FS without private-field access.
fs := b.filesystemService(instance)
res, err := fs.Get(ctx, path, blaxel.SandboxFilesystemGetParams{})
if err != nil {
Expand Down
3 changes: 0 additions & 3 deletions sdks/go/providers/blaxel/provider_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ func validateProviderCreate(request *sandbox.CreateOptions) error {
if resources.CpuLimitCores != nil {
return providerCheckError(sandbox.ErrorKindUnsupported, sandbox.CreateFieldResourcesCpuLimitCores, "Blaxel resource limits are not mapped")
}
if resources.MemoryMib != nil && *resources.MemoryMib < 256 {
return providerCheckError(sandbox.ErrorKindInvalidArgument, sandbox.CreateFieldResourcesMemoryMib, "resources.memory_mib is outside supported values")
}
if resources.MemoryLimitMib != nil {
return providerCheckError(sandbox.ErrorKindUnsupported, sandbox.CreateFieldResourcesMemoryLimitMib, "Blaxel resource limits are not mapped")
}
Expand Down
18 changes: 11 additions & 7 deletions sdks/go/providers/blaxel/rejection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,16 @@ func TestRejectedCreateSettings(t *testing.T) {
field: sandbox.CreateFieldLifetimeIdleTerminate,
},
{
name: "memory below floor",
name: "reserved port",
request: namedImage(&sandbox.CreateOptions{
Resources: &sandbox.Resources{MemoryMib: sandbox.Value(uint64(255))},
Network: &sandbox.NetworkConfig{
Ports: []*sandbox.PortBinding{
{Port: 443, Transport: sandbox.PortTransportTLS},
},
},
}),
kind: sandbox.ErrorKindInvalidArgument,
field: sandbox.CreateFieldResourcesMemoryMib,
field: sandbox.CreateFieldNetworkPortsPort,
},
} {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -215,8 +219,8 @@ func TestMappedNetworkPortsAndDomains(t *testing.T) {
Network: &sandbox.NetworkConfig{
OutboundDomains: &sandbox.StringAllowlist{Entries: []string{"example.com"}},
Ports: []*sandbox.PortBinding{
{Port: 443, Transport: sandbox.PortTransportTLS},
{Port: 8080, Transport: sandbox.PortTransportPlain},
{Port: 3000, Transport: sandbox.PortTransportTLS},
{Port: 3001, Transport: sandbox.PortTransportPlain},
},
},
}), "us-pdx-1")
Expand All @@ -227,10 +231,10 @@ func TestMappedNetworkPortsAndDomains(t *testing.T) {
t.Fatalf("AllowedDomains = %#v", got)
}
ports := plan.params.Sandbox.Spec.Runtime.Ports
if len(ports) != 2 || ports[0].Target != 443 || ports[0].Protocol != blaxel.PortProtocolTls {
if len(ports) != 2 || ports[0].Target != 3000 || ports[0].Protocol != blaxel.PortProtocolTls {
t.Fatalf("Ports = %#v", ports)
}
if ports[1].Target != 8080 || ports[1].Protocol != blaxel.PortProtocolTcp {
if ports[1].Target != 3001 || ports[1].Protocol != blaxel.PortProtocolTcp {
t.Fatalf("Ports[1] = %#v", ports[1])
}
}
15 changes: 10 additions & 5 deletions sdks/typescript/providers/blaxel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ than `workspace` are rejected.
**Create** requires a nonblank `name` and an image source. Optional memory
(MiB; Blaxel CPU follows memory), labels, environment variables, ports, outbound
domains, max-age TTL (at least 5m), idle-delete policy (at least 1m; explicit
disable is rejected), and a create timeout (1–50 whole seconds). Creation
calls native `SandboxInstance.create`, not `createIfNotExists`.
disable is rejected), and a create timeout (1–50 whole seconds).
`provisioning.waitFor` Ready is unsupported (`sandbox.wait` is a deprecated
no-op in `@blaxel/core`). Creation calls native `SandboxInstance.create`, not
`createIfNotExists`. Ports 80, 443, and 8080 are rejected (Blaxel-reserved).

**Lifecycle** on the handle: `get` / `refresh` (lookup by name), `delete`, and
`close`. `close` does not delete the sandbox. Start, stop, pause, archive,
Expand All @@ -78,9 +80,12 @@ command limit. Command timeouts must be whole seconds (`2000` is two seconds;
`1500` is rejected). Aborting the signal rejects the `await`; the remote
command may keep running.

When you set memory, Blaxel requires at least 256 MiB. Omitted memory keeps
the Blaxel default. CPU, CPU limit, memory limit, and disk overrides are
rejected.
Omitted memory keeps the Blaxel default. CPU, CPU limit, memory limit, and
disk overrides are rejected.

`@blaxel/core` applies credentials through process-global `settings`. Two Kit
Blaxel clients in one process overwrite each other; construct one client per
process or reconfigure before each use.

## License

Expand Down
48 changes: 40 additions & 8 deletions sdks/typescript/providers/blaxel/src/blaxel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
CreateFieldLifetimeIdleTerminate,
CreateFieldLifetimeMaximumLifetime,
CreateFieldName,
CreateFieldResourcesMemoryMib,
CreateFieldNetworkPortsPort,
CreateFieldProvisioningWaitFor,
FileFieldLocalPath,
} from "@sandbox-kit/sandbox";
import { mapClientConfig, mapCreateRequest } from "#mappings/mappings.js";
Expand Down Expand Up @@ -116,16 +117,13 @@ describe("Blaxel mappings", () => {
},
provisioning: {
timeout: 30_000,
waitFor: WaitCondition.Ready,
},
labels: { purpose: "test" },
environment: { NODE_ENV: "test" },
},
{ scope: { workspace: "ws" }, region: "us-pdx-1" },
);
expect(plan).toMatchObject({
waitUntilReady: true,
readyTimeoutMs: 30_000,
options: { timeout: 30 },
params: {
name: "example",
Expand All @@ -146,23 +144,57 @@ describe("Blaxel mappings", () => {
});
});

it("rejects memory below the Blaxel floor", () => {
expect(() =>
it("maps memory without a Kit-side floor", () => {
expect(
mapCreateRequest(
{
name: "example",
source: { image: { reference: "blaxel/base-image:latest" } },
resources: { memoryMib: 128n },
},
{ scope: { workspace: "ws" } },
).params.memory,
).toBe(128);
expect(mapResources({ memoryMib: 4096n })).toEqual({ memory: 4096 });
});

it("rejects WaitCondition.Ready", () => {
expect(() =>
mapCreateRequest(
{
name: "example",
source: { image: { reference: "blaxel/base-image:latest" } },
provisioning: {
timeout: 30_000,
waitFor: WaitCondition.Ready,
},
},
{ scope: { workspace: "ws" } },
),
).toThrowError(
expect.objectContaining({
kind: ErrorKind.Unsupported,
field: CreateFieldProvisioningWaitFor,
}),
);
});

it("rejects Blaxel-reserved ports", () => {
expect(() =>
mapCreateRequest(
{
name: "example",
source: { image: { reference: "blaxel/base-image:latest" } },
network: { ports: [{ port: 443, transport: PortTransport.Tls }] },
},
{ scope: { workspace: "ws" } },
),
).toThrowError(
expect.objectContaining({
kind: ErrorKind.InvalidArgument,
field: CreateFieldResourcesMemoryMib,
field: CreateFieldNetworkPortsPort,
}),
);
expect(mapResources({ memoryMib: 4096n })).toEqual({ memory: 4096 });
});

it("rejects lifetime durations that are not whole seconds", () => {
Expand Down
12 changes: 0 additions & 12 deletions sdks/typescript/providers/blaxel/src/blaxel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ interface NativeSandboxHandle {
stderr?: string;
}>;
};
wait(options?: {
maxWait?: number;
interval?: number;
}): Promise<NativeSandboxHandle>;
delete(): Promise<unknown>;
}

Expand Down Expand Up @@ -155,14 +151,6 @@ class BlaxelBackend extends BackendBase implements Backend {
const plan = mapCreateRequest(request, this.config);
throwIfAborted(options.signal);
const sandbox = await this.native.create(plan.params, plan.options);
if (plan.waitUntilReady) {
throwIfAborted(options.signal);
await sandbox.wait({
...(plan.readyTimeoutMs === undefined
? {}
: { maxWait: plan.readyTimeoutMs }),
});
}
return { sandbox: mapSandboxInfo(sandbox, request) };
} catch (error) {
throw mapBlaxelError(error, "create");
Expand Down
10 changes: 0 additions & 10 deletions sdks/typescript/providers/blaxel/src/checks/provider_checks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
ErrorKind,
CreateFieldEnvironment,
CreateFieldResourcesMemoryMib,
validEnvName,
} from "@sandbox-kit/sandbox";
import type { CreateOptions } from "@sandbox-kit/sandbox";
Expand All @@ -10,15 +9,6 @@ import { providerError } from "#errors.js";
// validateProviderCreate rejects creation settings this provider cannot honor.
// Rules are inventoried in specs/providers/blaxel.yaml checks when present.
export function validateProviderCreate(request: CreateOptions): void {
const memoryMib = request.resources?.memoryMib;
if (memoryMib !== undefined && memoryMib < BigInt("256")) {
providerError({
kind: ErrorKind.InvalidArgument,
operation: "create",
field: CreateFieldResourcesMemoryMib,
message: "resources.memory_mib is outside supported values",
});
}
for (const value of Object.keys(request.environment ?? {})) {
if (!validEnvName(value)) {
providerError({
Expand Down
Loading