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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
All notable changes will be documented here. This project follows Semantic
Versioning and the Keep a Changelog structure.

## Unreleased

### Added

- Sandbox labels on create and response models, AND-matched label filtering in
`listSandboxes()`, and label merging through `Sandbox.setLabels()`.

## 0.1.2 - 2026-09-21

### Fixed
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ The SDK targets Java 17 and uses the JDK HTTP client. Public wire models are
immutable records, API failures remain inspectable through `ApiException`, and
the build enforces the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html).

## Organize sandboxes with labels

```java
Sandbox sandbox = client.createSandbox(
CreateSandboxRequest.builder("s-1vcpu-1gb")
.rootFileSystem("alpine:3.20")
.labels(Map.of("team", "backend", "environment", "test"))
.build());

List<Sandbox> matches = client.listSandboxes(
new ListSandboxesOptions(0, null, Map.of("team", "backend")));

sandbox.setLabels(Map.of("environment", "production"));
```

List filters use AND semantics. `setLabels()` merges the supplied keys into the
existing label map.

## Delegate access to one sandbox

An owner can create one delegated token for a sandbox. Plaintext is returned
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/sh/createos/CreateOsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ public Sandbox createSandbox(CreateSandboxRequest request) {
null,
null,
null,
request.autoPauseAfterSeconds());
request.autoPauseAfterSeconds(),
created.labels());
return new Sandbox(transport, data);
}

Expand All @@ -178,6 +179,13 @@ public List<Sandbox> listSandboxes(ListSandboxesOptions options) {
if (options.status() != null) {
query.put("status", options.status().wireValue());
}
if (!options.labels().isEmpty()) {
try {
query.put("labels", transport.objectMapper().writeValueAsString(options.labels()));
} catch (com.fasterxml.jackson.core.JsonProcessingException exception) {
throw new IllegalArgumentException("Failed to encode sandbox label filter", exception);
}
}
return fetchAll("/v1/sandboxes", query, options.limit(), false, SandboxData.class).stream()
.map(data -> new Sandbox(transport, data))
.toList();
Expand Down
21 changes: 20 additions & 1 deletion src/main/java/sh/createos/Sandbox.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public String ipAddress() {
return data().ipAddress();
}

/** Returns the last observed user-defined labels. */
public Map<String, String> labels() {
return data().labels();
}

/** Returns the latest immutable server projection. */
public SandboxData data() {
lock.readLock().lock();
Expand Down Expand Up @@ -218,6 +223,19 @@ public void setIngress(boolean enabled) {
SandboxData.class));
}

/** Merges labels into the sandbox's existing labels. */
public void setLabels(Map<String, String> labels) {
update(
transport.send(
"PATCH",
path(""),
Map.of(),
Map.of("labels", Map.copyOf(labels)),
RequestOptions.DEFAULT,
false,
SandboxData.class));
}

/** Sets the idle auto-pause timeout, or disables it when {@code timeout} is null. */
public void setAutoPause(Duration timeout) {
Map<String, Object> request;
Expand Down Expand Up @@ -575,7 +593,8 @@ private static SandboxData copyWithStatus(SandboxData source, SandboxStatus stat
source.pausedAt(),
source.lastResumedAt(),
source.forkedFrom(),
source.autoPauseAfterSeconds());
source.autoPauseAfterSeconds(),
source.labels());
}

private record DestroyedResponse(String id, SandboxStatus status) {}
Expand Down
48 changes: 46 additions & 2 deletions src/main/java/sh/createos/model/CreateSandboxRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public record CreateSandboxRequest(
@JsonProperty("ingress_enabled") Boolean ingressEnabled,
List<DiskAttachment> disks,
String region,
@JsonProperty("auto_pause_after_seconds") Integer autoPauseAfterSeconds) {
@JsonProperty("auto_pause_after_seconds") Integer autoPauseAfterSeconds,
Map<String, String> labels) {
/** Validates required values and defensively copies collection values. */
public CreateSandboxRequest {
shape = Objects.requireNonNull(shape, "shape");
Expand All @@ -36,6 +37,41 @@ public record CreateSandboxRequest(
sshPublicKeys = sshPublicKeys == null ? List.of() : List.copyOf(sshPublicKeys);
nodeSelector = nodeSelector == null ? Map.of() : Map.copyOf(nodeSelector);
disks = disks == null ? List.of() : List.copyOf(disks);
labels = labels == null ? Map.of() : Map.copyOf(labels);
}

/** Creates a request without labels, preserving the pre-labels constructor. */
public CreateSandboxRequest(
String shape,
String rootFileSystem,
String name,
List<NetworkEntry> networks,
Long diskMib,
List<String> egressRules,
Map<String, String> environmentVariables,
List<String> sshPublicKeys,
String hostId,
Map<String, String> nodeSelector,
Boolean ingressEnabled,
List<DiskAttachment> disks,
String region,
Integer autoPauseAfterSeconds) {
this(
shape,
rootFileSystem,
name,
networks,
diskMib,
egressRules,
environmentVariables,
sshPublicKeys,
hostId,
nodeSelector,
ingressEnabled,
disks,
region,
autoPauseAfterSeconds,
Map.of());
}

/** Returns a builder requiring the sandbox shape. */
Expand All @@ -59,6 +95,7 @@ public static final class Builder {
private List<DiskAttachment> disks = List.of();
private String region;
private Integer autoPauseAfterSeconds;
private Map<String, String> labels = Map.of();

private Builder(String shape) {
this.shape = shape;
Expand All @@ -76,6 +113,12 @@ public Builder name(String name) {
return this;
}

/** Sets user-defined metadata used to organize and filter sandboxes. */
public Builder labels(Map<String, String> labels) {
this.labels = labels;
return this;
}

/** Sets overlay networks attached during creation. */
public Builder networks(List<NetworkEntry> networks) {
this.networks = networks;
Expand Down Expand Up @@ -158,7 +201,8 @@ public CreateSandboxRequest build() {
ingressEnabled,
disks,
region,
autoPauseAfterSeconds);
autoPauseAfterSeconds,
labels);
}
}
}
41 changes: 40 additions & 1 deletion src/main/java/sh/createos/model/CreateSandboxResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;

/** Resolved placement and boot timing returned when a sandbox is created. */
@JsonIgnoreProperties(ignoreUnknown = true)
Expand All @@ -19,4 +20,42 @@ public record CreateSandboxResponse(
@JsonProperty("spawn_ms") double spawnMilliseconds,
@JsonProperty("egress") List<String> egressRules,
@JsonProperty("bandwidth_quota_bytes") long bandwidthQuotaBytes,
@JsonProperty("ingress_url_template") String ingressUrlTemplate) {}
@JsonProperty("ingress_url_template") String ingressUrlTemplate,
Map<String, String> labels) {
/** Normalizes missing labels and keeps the response immutable. */
public CreateSandboxResponse {
labels = labels == null ? Map.of() : Map.copyOf(labels);
}

/** Creates a response without labels, preserving the pre-labels constructor. */
public CreateSandboxResponse(
String id,
SandboxStatus status,
String name,
String ipAddress,
String shape,
String rootFileSystem,
int virtualCpu,
int memoryMib,
long diskMib,
double spawnMilliseconds,
List<String> egressRules,
long bandwidthQuotaBytes,
String ingressUrlTemplate) {
this(
id,
status,
name,
ipAddress,
shape,
rootFileSystem,
virtualCpu,
memoryMib,
diskMib,
spawnMilliseconds,
egressRules,
bandwidthQuotaBytes,
ingressUrlTemplate,
Map.of());
}
}
12 changes: 10 additions & 2 deletions src/main/java/sh/createos/model/ListSandboxesOptions.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package sh.createos.model;

import java.util.Map;

/** Filtering and result limits for sandbox listing. */
public record ListSandboxesOptions(int limit, SandboxStatus status) {
public record ListSandboxesOptions(int limit, SandboxStatus status, Map<String, String> labels) {
/** Lists all sandboxes without a status filter. */
public static final ListSandboxesOptions ALL = new ListSandboxesOptions(0, null);
public static final ListSandboxesOptions ALL = new ListSandboxesOptions(0, null, Map.of());

/** Creates options without a label filter. */
public ListSandboxesOptions(int limit, SandboxStatus status) {
this(limit, status, Map.of());
}

/** Validates the result limit. */
public ListSandboxesOptions {
if (limit < 0) {
throw new IllegalArgumentException("limit must not be negative");
}
labels = labels == null ? Map.of() : Map.copyOf(labels);
}
}
61 changes: 60 additions & 1 deletion src/main/java/sh/createos/model/SandboxData.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
import java.util.List;
import java.util.Map;

/** Complete server projection of a sandbox. */
@JsonIgnoreProperties(ignoreUnknown = true)
Expand Down Expand Up @@ -32,12 +33,70 @@ public record SandboxData(
@JsonProperty("paused_at") Instant pausedAt,
@JsonProperty("last_resumed_at") Instant lastResumedAt,
@JsonProperty("forked_from") String forkedFrom,
@JsonProperty("auto_pause_after_seconds") Integer autoPauseAfterSeconds) {
@JsonProperty("auto_pause_after_seconds") Integer autoPauseAfterSeconds,
Map<String, String> labels) {
/** Defensively copies collection values. */
public SandboxData {
egressRules = egressRules == null ? List.of() : List.copyOf(egressRules);
environmentVariables =
environmentVariables == null ? List.of() : List.copyOf(environmentVariables);
sshPublicKeys = sshPublicKeys == null ? List.of() : List.copyOf(sshPublicKeys);
labels = labels == null ? Map.of() : Map.copyOf(labels);
}

/** Creates a projection without labels, preserving the pre-labels constructor. */
public SandboxData(
String id,
SandboxStatus status,
String ipAddress,
int virtualCpu,
int memoryMib,
long diskMib,
Instant createdAt,
boolean ingressEnabled,
String ingressUrlTemplate,
String name,
Instant runningAt,
Instant destroyedAt,
double spawnMilliseconds,
String shape,
String rootFileSystem,
String region,
List<String> egressRules,
List<String> environmentVariables,
List<String> sshPublicKeys,
String createdBy,
long bandwidthIngressBytes,
Instant pausedAt,
Instant lastResumedAt,
String forkedFrom,
Integer autoPauseAfterSeconds) {
this(
id,
status,
ipAddress,
virtualCpu,
memoryMib,
diskMib,
createdAt,
ingressEnabled,
ingressUrlTemplate,
name,
runningAt,
destroyedAt,
spawnMilliseconds,
shape,
rootFileSystem,
region,
egressRules,
environmentVariables,
sshPublicKeys,
createdBy,
bandwidthIngressBytes,
pausedAt,
lastResumedAt,
forkedFrom,
autoPauseAfterSeconds,
Map.of());
}
}
Loading
Loading