Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Versioning and the Keep a Changelog structure.

### Added

- Reusable disk and full snapshot create, get, list, and delete operations,
plus sandbox restoration through `CreateSandboxRequest.fromSnapshot()`.
- Sandbox labels on create and response models, AND-matched label filtering in
`listSandboxes()`, and label merging through `Sandbox.setLabels()`.

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ sandbox.setLabels(Map.of("environment", "production"));
List filters use AND semantics. `setLabels()` merges the supplied keys into the
existing label map.

## Capture and restore snapshots

```java
Snapshot snapshot =
sandbox.createSnapshot(new CreateSnapshotRequest(SnapshotType.DISK, "configured-workspace"));
snapshot = client.snapshots().get(snapshot.id()); // poll until READY
Sandbox restored =
client.createSandbox(CreateSandboxRequest.fromSnapshot(snapshot.id()).build());
```

Disk snapshots capture the filesystem. Full snapshots also retain memory and
must be restored with their captured CPU, memory, and disk sizes. Snapshot
capture requires a running source sandbox with no attached external disks.
Each account may retain up to five snapshots across both types. See the compile-checked
[snapshot example](examples/src/main/java/sh/createos/examples/snapshots/Snapshots.java).

## Delegate access to one sandbox

An owner can create one delegated token for a sandbox. Plaintext is returned
Expand Down Expand Up @@ -384,6 +400,7 @@ validated by `ExamplesDocumentationTest`:
- [HTTP execution server](examples/src/main/java/sh/createos/examples/executionserver/README.md)
- [Command streaming](examples/src/main/java/sh/createos/examples/commandstreaming/README.md)
- [Files and snapshots](examples/src/main/java/sh/createos/examples/filesandsnapshots/README.md)
- [Reusable snapshots](examples/src/main/java/sh/createos/examples/snapshots/README.md)
- [Ingress preview](examples/src/main/java/sh/createos/examples/ingresspreview/README.md)
- [Private overlay network](examples/src/main/java/sh/createos/examples/network/README.md)
- [Custom template](examples/src/main/java/sh/createos/examples/customtemplate/README.md)
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Replace the final class with one of these:
| `ingress-preview` | `sh.createos.examples.ingresspreview.IngressPreview` |
| `managed-process` | `sh.createos.examples.managedprocess.ManagedProcessExample` |
| `network` | `sh.createos.examples.network.NetworkExample` |
| `snapshots` | `sh.createos.examples.snapshots.Snapshots` |

Examples create billable resources. Their `finally` blocks make a best effort
to destroy sandboxes and delete temporary account resources.
Expand Down
12 changes: 12 additions & 0 deletions examples/src/main/java/sh/createos/examples/snapshots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Reusable snapshots

Capture a reusable disk snapshot, wait for it to become ready, and restore a
new sandbox from it.

```bash
export CREATEOS_API_KEY="..."
mvn verify
mvn dependency:build-classpath -Dmdep.outputFile=/tmp/createos-java-classpath
java -cp "target/classes:target/example-classes:$(cat /tmp/createos-java-classpath)" \
sh.createos.examples.snapshots.Snapshots
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package sh.createos.examples.snapshots;

import java.time.Duration;
import java.time.Instant;
import sh.createos.CreateOsClient;
import sh.createos.Sandbox;
import sh.createos.model.CreateSandboxRequest;
import sh.createos.model.CreateSnapshotRequest;
import sh.createos.model.Snapshot;
import sh.createos.model.SnapshotStatus;
import sh.createos.model.SnapshotType;

/** Captures a reusable disk snapshot and restores a second sandbox from it. */
public final class Snapshots {
private Snapshots() {}

/** Runs the example. */
public static void main(String[] arguments) throws InterruptedException {
CreateOsClient client =
CreateOsClient.builder().apiKey(System.getenv("CREATEOS_API_KEY")).build();
Sandbox source =
client.createSandbox(
CreateSandboxRequest.builder("s-1vcpu-1gb").rootFileSystem("alpine:3.20").build());
Snapshot snapshot = null;
Sandbox restored = null;
try {
snapshot =
source.createSnapshot(
new CreateSnapshotRequest(SnapshotType.DISK, "configured-workspace"));
snapshot = waitUntilReady(client, snapshot.id(), Duration.ofMinutes(5));
restored = client.createSandbox(CreateSandboxRequest.fromSnapshot(snapshot.id()).build());
System.out.printf("restored sandbox %s from snapshot %s%n", restored.id(), snapshot.id());
} finally {
if (restored != null) {
restored.destroy();
}
source.destroy();
if (snapshot != null && snapshot.status() != SnapshotStatus.CREATING) {
client.snapshots().delete(snapshot.id());
}
}
}

private static Snapshot waitUntilReady(CreateOsClient client, String id, Duration timeout)
throws InterruptedException {
Instant deadline = Instant.now().plus(timeout);
while (Instant.now().isBefore(deadline)) {
Snapshot snapshot = client.snapshots().get(id);
if (snapshot.status() == SnapshotStatus.READY) {
return snapshot;
}
if (snapshot.status() == SnapshotStatus.FAILED) {
throw new IllegalStateException("snapshot capture failed: " + id);
}
Thread.sleep(1000);
}
throw new IllegalStateException("timed out waiting for snapshot: " + id);
}
}
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 @@ -31,6 +31,7 @@ public final class CreateOsClient {
private final TemplatesService templates;
private final NetworksService networks;
private final DisksService disks;
private final SnapshotsService snapshots;

private CreateOsClient(Builder builder) {
this.baseUri = builder.baseUri;
Expand All @@ -45,6 +46,7 @@ private CreateOsClient(Builder builder) {
this.templates = new TemplatesService(this);
this.networks = new NetworksService(this);
this.disks = new DisksService(this);
this.snapshots = new SnapshotsService(this);
}

/**
Expand Down Expand Up @@ -75,6 +77,11 @@ public DisksService disks() {
return disks;
}

/** Returns account-level reusable snapshot operations. */
public SnapshotsService snapshots() {
return snapshots;
}

/** Returns the unauthenticated control-plane liveness state. */
public HealthResponse health() {
return transport.send(
Expand Down Expand Up @@ -159,7 +166,8 @@ public Sandbox createSandbox(CreateSandboxRequest request) {
null,
null,
request.autoPauseAfterSeconds(),
created.labels());
created.labels(),
created.snapshotId());
return new Sandbox(transport, data);
}

Expand Down
17 changes: 16 additions & 1 deletion src/main/java/sh/createos/Sandbox.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sh.createos.internal.RawResponse;
import sh.createos.model.Bandwidth;
import sh.createos.model.CommandStreamFrame;
import sh.createos.model.CreateSnapshotRequest;
import sh.createos.model.DiskAttachment;
import sh.createos.model.Egress;
import sh.createos.model.ForkSandboxRequest;
Expand All @@ -24,6 +25,7 @@
import sh.createos.model.SandboxData;
import sh.createos.model.SandboxDisk;
import sh.createos.model.SandboxStatus;
import sh.createos.model.Snapshot;

/** Stateful handle to one CreateOS sandbox. */
public final class Sandbox {
Expand Down Expand Up @@ -166,6 +168,18 @@ public void resume() {
lifecycle("/resume");
}

/** Starts a reusable snapshot capture from this running sandbox. */
public Snapshot createSnapshot(CreateSnapshotRequest request) {
return transport.send(
"POST",
path("/snapshots"),
Map.of(),
request,
RequestOptions.DEFAULT,
false,
Snapshot.class);
}

/** Creates an independent sandbox fork. */
public Sandbox fork(ForkSandboxRequest request) {
SandboxData forked =
Expand Down Expand Up @@ -594,7 +608,8 @@ private static SandboxData copyWithStatus(SandboxData source, SandboxStatus stat
source.lastResumedAt(),
source.forkedFrom(),
source.autoPauseAfterSeconds(),
source.labels());
source.labels(),
source.snapshotId());
}

private record DestroyedResponse(String id, SandboxStatus status) {}
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/sh/createos/SnapshotsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package sh.createos;

import java.util.List;
import java.util.Map;
import sh.createos.internal.HttpTransport;
import sh.createos.model.PaginationOptions;
import sh.createos.model.Snapshot;
import sh.createos.model.SnapshotDeletedResponse;

/** Account-level reusable sandbox snapshot operations. */
public final class SnapshotsService {
private final CreateOsClient client;

SnapshotsService(CreateOsClient client) {
this.client = client;
}

/** Lists reusable snapshots owned by the caller. */
public List<Snapshot> list(PaginationOptions options) {
return client.fetchAll(
"/v1/snapshots",
options.offset() == 0 ? Map.of() : Map.of("offset", Integer.toString(options.offset())),
options.limit(),
false,
Snapshot.class);
}

/** Returns an owned reusable snapshot by identifier. */
public Snapshot get(String snapshotId) {
return client
.transport()
.send(
"GET", path(snapshotId), Map.of(), null, RequestOptions.DEFAULT, false, Snapshot.class);
}

/** Removes an owned ready or failed snapshot from API listings. */
public SnapshotDeletedResponse delete(String snapshotId) {
return client
.transport()
.send(
"DELETE",
path(snapshotId),
Map.of(),
null,
RequestOptions.DEFAULT,
false,
SnapshotDeletedResponse.class);
}

private static String path(String snapshotId) {
return "/v1/snapshots/" + HttpTransport.encodePathSegment(snapshotId);
}
}
63 changes: 56 additions & 7 deletions src/main/java/sh/createos/model/CreateSandboxRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/** Request used to create a sandbox. */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
Expand All @@ -23,12 +22,12 @@ public record CreateSandboxRequest(
List<DiskAttachment> disks,
String region,
@JsonProperty("auto_pause_after_seconds") Integer autoPauseAfterSeconds,
Map<String, String> labels) {
Map<String, String> labels,
@JsonProperty("snapshot_id") String snapshotId) {
/** Validates required values and defensively copies collection values. */
public CreateSandboxRequest {
shape = Objects.requireNonNull(shape, "shape");
if (shape.isBlank()) {
throw new IllegalArgumentException("shape must not be blank");
if ((shape == null || shape.isBlank()) && (snapshotId == null || snapshotId.isBlank())) {
throw new IllegalArgumentException("shape or snapshotId must not be blank");
}
networks = networks == null ? List.of() : List.copyOf(networks);
egressRules = egressRules == null ? List.of() : List.copyOf(egressRules);
Expand Down Expand Up @@ -71,14 +70,56 @@ public CreateSandboxRequest(
disks,
region,
autoPauseAfterSeconds,
Map.of());
Map.of(),
null);
}

/** Creates a request with labels, preserving the pre-snapshots 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,
Map<String, String> labels) {
this(
shape,
rootFileSystem,
name,
networks,
diskMib,
egressRules,
environmentVariables,
sshPublicKeys,
hostId,
nodeSelector,
ingressEnabled,
disks,
region,
autoPauseAfterSeconds,
labels,
null);
}

/** Returns a builder requiring the sandbox shape. */
public static Builder builder(String shape) {
return new Builder(shape);
}

/** Returns a builder that restores an owned ready reusable snapshot. */
public static Builder fromSnapshot(String snapshotId) {
return new Builder(null).snapshotId(snapshotId);
}

/** Builder for optional sandbox creation settings. */
public static final class Builder {
private final String shape;
Expand All @@ -96,6 +137,7 @@ public static final class Builder {
private String region;
private Integer autoPauseAfterSeconds;
private Map<String, String> labels = Map.of();
private String snapshotId;

private Builder(String shape) {
this.shape = shape;
Expand All @@ -119,6 +161,12 @@ public Builder labels(Map<String, String> labels) {
return this;
}

/** Restores an owned ready reusable snapshot instead of a root filesystem. */
public Builder snapshotId(String snapshotId) {
this.snapshotId = snapshotId;
return this;
}

/** Sets overlay networks attached during creation. */
public Builder networks(List<NetworkEntry> networks) {
this.networks = networks;
Expand Down Expand Up @@ -202,7 +250,8 @@ public CreateSandboxRequest build() {
disks,
region,
autoPauseAfterSeconds,
labels);
labels,
snapshotId);
}
}
}
Loading
Loading