Skip to content

Commit 074d8d2

Browse files
author
Ferran Pons Serra
committed
feat(snapshots): expose snapshot imageUri (spec, server, Kotlin SDK)
Surface the portable OCI restore image reference on the snapshot contract so callers can restore a sandbox from it (e.g. across clusters) through the Kotlin SDK instead of dropping down to the raw REST API. - spec: add optional imageUri to the Snapshot schema in sandbox-lifecycle.yml - server: add image_uri/imageUri to the Snapshot response, populated from the record's restore image only once the snapshot is Ready - kotlin: add imageUri to SnapshotInfo (with a binary-compatibility constructor) and map it in SandboxModelConverter; the lifecycle client regenerates from the spec at build time imageUri is optional and only populated once the snapshot is Ready (null/omitted otherwise), so the change is additive and backward compatible.
1 parent f31696b commit 074d8d2

7 files changed

Lines changed: 57 additions & 2 deletions

File tree

sdks/sandbox/kotlin/sandbox/src/main/kotlin/com/alibaba/opensandbox/sandbox/domain/models/sandboxes/SandboxModels.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,24 @@ class SnapshotInfo(
794794
val name: String? = null,
795795
val status: SnapshotStatus,
796796
val createdAt: OffsetDateTime,
797-
)
797+
/**
798+
* Portable OCI image reference for a Ready snapshot, usable to restore a sandbox (e.g. across
799+
* clusters). Populated once the snapshot reaches [SnapshotState.READY]; null otherwise.
800+
*/
801+
val imageUri: String? = null,
802+
) {
803+
/**
804+
* Binary-compatibility constructor preserving the pre-`imageUri` JVM signature for
805+
* already-compiled (e.g. Java) callers; delegates [imageUri] to null.
806+
*/
807+
constructor(
808+
id: String,
809+
sandboxId: String,
810+
name: String?,
811+
status: SnapshotStatus,
812+
createdAt: OffsetDateTime,
813+
) : this(id, sandboxId, name, status, createdAt, null)
814+
}
798815

799816
class SnapshotFilter private constructor(
800817
val sandboxId: String?,

sdks/sandbox/kotlin/sandbox/src/main/kotlin/com/alibaba/opensandbox/sandbox/infrastructure/adapters/converter/SandboxModelConverter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ internal object SandboxModelConverter {
410410
lastTransitionAt = this.status.lastTransitionAt,
411411
),
412412
createdAt = this.createdAt,
413+
imageUri = this.imageUri,
413414
)
414415
}
415416

sdks/sandbox/kotlin/sandbox/src/test/kotlin/com/alibaba/opensandbox/sandbox/infrastructure/adapters/service/SandboxesAdapterTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ class SandboxesAdapterTest {
369369
"message": null,
370370
"lastTransitionAt": "2023-01-01T10:00:00Z"
371371
},
372-
"createdAt": "2023-01-01T10:00:00Z"
372+
"createdAt": "2023-01-01T10:00:00Z",
373+
"imageUri": "registry.example.com/snapshots/snap-123:latest"
373374
}
374375
""".trimIndent()
375376
mockWebServer.enqueue(MockResponse().setBody(responseBody).setResponseCode(200))
@@ -378,6 +379,7 @@ class SandboxesAdapterTest {
378379

379380
assertEquals(snapshotId, result.id)
380381
assertEquals("sandbox-123", result.sandboxId)
382+
assertEquals("registry.example.com/snapshots/snap-123:latest", result.imageUri)
381383

382384
val request = mockWebServer.takeRequest()
383385
assertEquals("GET", request.method)

server/opensandbox_server/api/schema.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,14 @@ class Snapshot(BaseModel):
672672
alias="createdAt",
673673
description="Snapshot creation timestamp",
674674
)
675+
image_uri: Optional[str] = Field(
676+
None,
677+
alias="imageUri",
678+
description=(
679+
"Portable OCI image reference produced for a Ready snapshot, usable to restore a "
680+
"sandbox (e.g. across clusters). Present once the snapshot is Ready; omitted otherwise."
681+
),
682+
)
675683

676684
class Config:
677685
populate_by_name = True

server/opensandbox_server/services/snapshot_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,13 @@ def _to_snapshot_response(record: SnapshotRecord) -> Snapshot:
549549
lastTransitionAt=record.status.last_transition_at,
550550
),
551551
createdAt=record.created_at,
552+
# The restore image is part of the contract only once the snapshot is Ready; never
553+
# surface it for Creating/Deleting/Failed states (e.g. after deletion is requested).
554+
imageUri=(
555+
record.restore_config.image
556+
if record.status.state == SnapshotState.READY
557+
else None
558+
),
552559
)
553560

554561

server/tests/test_snapshot_service.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ def _snapshot_record(
124124
)
125125

126126

127+
def test_snapshot_response_exposes_image_uri_only_when_ready() -> None:
128+
image = "opensandbox-snapshots:snap-1"
129+
ready = _snapshot_record("snap-ready", SnapshotState.READY, image=image)
130+
deleting = _snapshot_record("snap-deleting", SnapshotState.DELETING, image=image)
131+
132+
assert PersistedSnapshotService._to_snapshot_response(ready).image_uri == image
133+
# Once deletion is requested (or any non-Ready state), the restore image must not leak.
134+
assert PersistedSnapshotService._to_snapshot_response(deleting).image_uri is None
135+
136+
127137
def test_snapshot_service_persists_create_and_get(tmp_path) -> None:
128138
repo = SQLiteSnapshotRepository(tmp_path / "snapshots.db")
129139
runtime = StubSnapshotRuntime()
@@ -221,6 +231,10 @@ def create_snapshot(snapshot_id: str, sandbox_id: str, **kwargs):
221231
assert stored.status.state == SnapshotState.READY
222232
assert stored.restore_config.image == "opensandbox-snapshots:snap-ready"
223233

234+
# The Ready snapshot response exposes the portable restore image via imageUri.
235+
response = service.get_snapshot(created.id)
236+
assert response.image_uri == "opensandbox-snapshots:snap-ready"
237+
224238

225239
def test_snapshot_service_marks_snapshot_failed_from_worker(tmp_path) -> None:
226240
repo = SQLiteSnapshotRepository(tmp_path / "snapshots.db")

specs/sandbox-lifecycle.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,12 @@ components:
915915
format: date-time
916916
description: Snapshot creation timestamp
917917

918+
imageUri:
919+
type: string
920+
description: >-
921+
Portable OCI image reference produced for a Ready snapshot, usable to restore a sandbox
922+
(e.g. across clusters). Present once the snapshot reaches the Ready state; omitted otherwise.
923+
918924
required:
919925
- id
920926
- sandboxId

0 commit comments

Comments
 (0)