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
13 changes: 13 additions & 0 deletions porter_sandbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
SandboxNetworkingSpec,
SandboxResourcesSpec,
SandboxSpec,
Snapshot,
SnapshotListResponse,
SnapshotSpec,
StatusResponse,
VolumeFileEntry,
VolumeFileListResponse,
Expand All @@ -54,6 +57,8 @@
SandboxDomainSpecVisibility,
SandboxesPhase,
SandboxMetric,
SnapshotMode,
SnapshotStatus,
StatusResponsePhase,
VolumeFileEntryType,
VolumeObjectSpecAccess,
Expand All @@ -66,6 +71,7 @@
from .readyz import AsyncReadyz, Readyz
from .sandbox import AsyncSandbox, Sandbox
from .sandboxes import AsyncSandboxes, Sandboxes
from .snapshots import AsyncSnapshots, Snapshots
from .volume import AsyncObjectVolume, AsyncVolume, ObjectVolume, Volume, VolumeFile
from .volumes import AsyncVolumes, Volumes

Expand All @@ -77,6 +83,7 @@
"AsyncReadyz",
"AsyncSandbox",
"AsyncSandboxes",
"AsyncSnapshots",
"AsyncVolume",
"AsyncVolumes",
"AuthenticationError",
Expand Down Expand Up @@ -124,6 +131,12 @@
"Sandboxes",
"SandboxesPhase",
"ServerError",
"Snapshot",
"SnapshotListResponse",
"SnapshotMode",
"SnapshotSpec",
"SnapshotStatus",
"Snapshots",
"StatusResponse",
"StatusResponsePhase",
"Volume",
Expand Down
3 changes: 3 additions & 0 deletions porter_sandbox/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .resources.healthz import AsyncHealthz, Healthz
from .resources.readyz import AsyncReadyz, Readyz
from .resources.sandboxes import AsyncSandboxes, Sandboxes
from .resources.snapshots import AsyncSnapshots, Snapshots
from .resources.volumes import AsyncVolumes, Volumes


Expand All @@ -26,6 +27,7 @@ def __init__(
config=Config.resolve(api_key=api_key, base_url=base_url, timeout=timeout),
)
self.sandboxes: Sandboxes = Sandboxes(self._base)
self.snapshots: Snapshots = Snapshots(self._base)
self.volumes: Volumes = Volumes(self._base)
self.healthz: Healthz = Healthz(self._base)
self.readyz: Readyz = Readyz(self._base)
Expand Down Expand Up @@ -55,6 +57,7 @@ def __init__(
config=Config.resolve(api_key=api_key, base_url=base_url, timeout=timeout),
)
self.sandboxes: AsyncSandboxes = AsyncSandboxes(self._base)
self.snapshots: AsyncSnapshots = AsyncSnapshots(self._base)
self.volumes: AsyncVolumes = AsyncVolumes(self._base)
self.healthz: AsyncHealthz = AsyncHealthz(self._base)
self.readyz: AsyncReadyz = AsyncReadyz(self._base)
Expand Down
32 changes: 28 additions & 4 deletions porter_sandbox/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
FilterValuesResponsePhases,
LogLineLevel,
SandboxDomainSpecVisibility,
SnapshotMode,
SnapshotStatus,
StatusResponsePhase,
VolumeFileEntryType,
VolumeObjectSpecAccess,
Expand Down Expand Up @@ -117,7 +119,7 @@ class ReadinessResponse(BaseModel):

class SandboxDomainSpec(BaseModel):
domain: str | None = Field(default=None, description="Fully qualified hostname for the sandbox, one label under the\ntarget ingress's domain. Unlike name it need not be unique, so\nsuccessive sandboxes can reuse one hostname (only one may be live\nat a time). Defaults to <name>.<ingress domain>, then\n<id>.<ingress domain>, when omitted.\n")
visibility: SandboxDomainSpecVisibility | None = Field(default=None, description="Which sandbox ingress serves the domain when the cluster has both a\npublic and a private one. Omit to default to whichever is\nconfigured, public winning. Rejected when the sandbox exposes a\nport but the requested ingress is not configured on the cluster.\n")
visibility: SandboxDomainSpecVisibility | None = Field(default=None, description="Which sandbox ingress serves the domain when the cluster has both a\npublic and a private one. Omit to default to whichever is\nconfigured, public winning; with neither configured, the sandbox is\nserved at its cluster-internal address only. Rejected when the\nsandbox exposes a port but the requested ingress is not configured\non the cluster.\n")


class SandboxEgressSpec(BaseModel):
Expand Down Expand Up @@ -146,7 +148,8 @@ class SandboxMetricsSeries(BaseModel):

class SandboxNetworkingSpec(BaseModel):
port: int = Field(description="Port the workload listens on; the per-sandbox Service targets it on\nthe pod. Privileged ports (1-1023) are not allowed.\n")
domains: list[SandboxDomainSpec] | None = Field(default=None, description="Domains the port is served on through a sandbox ingress. Omit to\nserve the port at the default hostname through the default ingress.\nCurrently only one entry is supported.\n")
domains: list[SandboxDomainSpec] | None = Field(default=None, description="Domains the port is served on through a sandbox ingress. Omit to\nserve the port at the default hostname through the default ingress,\nor - on a cluster with no sandbox ingress - at the cluster-internal\naddress only. Currently only one entry is supported.\n")
internal: bool | None = Field(default=None, description="Serve the port inside the cluster only: the sandbox gets no public\nhostname and is reachable at the cluster-internal address surfaced\nas internal_address in its status. Cannot be combined with domains.\n")


class SandboxResourcesSpec(BaseModel):
Expand All @@ -159,7 +162,8 @@ class SandboxResourcesSpec(BaseModel):


class SandboxSpec(BaseModel):
image: str = Field(description="Container image to run")
image: str = Field(description="Container image to run. Empty when snapshot_id is set, since the\nsnapshot is the image.\n")
snapshot_id: str | None = Field(default=None, description="Start the sandbox from this snapshot's filesystem instead of an image,\nso image must be left unset. The snapshot carries no command or volumes:\nan omitted command runs the base image's, and the sandbox mounts only\nthe volumes this request asks for.\n")
name: str | None = Field(default=None, description="Sandbox name, unique within the cluster. Must be a valid DNS label\n(lowercase alphanumeric and dashes). Defaults to the sandbox's id\nwhen omitted.\n")
tags: dict[str, str] | None = Field(default=None, description="Arbitrary key/value labels for identifying and filtering sandboxes")
command: list[str] | None = Field(default=None, description="Override image entrypoint")
Expand All @@ -173,6 +177,25 @@ class SandboxSpec(BaseModel):
ttl_seconds: int | None = Field(default=None, description="Maximum lifetime in seconds, counted from when the sandbox starts\nrunning (from creation while it waits to start). The sandbox is\nterminated once it elapses. Omit for no limit.\n")


class Snapshot(BaseModel):
id: str = Field(description="Snapshot id")
sandbox_id: str = Field(description="Sandbox the snapshot was captured from")
mode: SnapshotMode
status: SnapshotStatus = Field(description="Capture state. A sandbox can be started from a snapshot once it is ready. A failed capture keeps its record so the reason stays visible.\n")
t_created_unix_ms: int | None = Field(default=None, description="When the capture started, in unix milliseconds")
t_ready_unix_ms: int | None = Field(default=None, description="When the capture completed, in unix milliseconds")
failure_reason: str | None = Field(default=None, description="Why the capture failed, when it did")
size_bytes: int | None = Field(default=None, description="Total size of what was captured")


class SnapshotListResponse(BaseModel):
snapshots: list[Snapshot]


class SnapshotSpec(BaseModel):
mode: SnapshotMode | None = Field(default=None, description="What to capture. Defaults to capturing the filesystem.")


class StatusResponse(BaseModel):
id: str = Field(description="Sandbox ID")
name: str = Field(description="Sandbox name (the id when no name was given)")
Expand All @@ -184,6 +207,7 @@ class StatusResponse(BaseModel):
started_at: str | None = Field(default=None, description="When the sandbox pod started running")
finished_at: str | None = Field(default=None, description="When the sandbox reached a terminal phase (succeeded, failed, or terminated)")
host: str = Field(description="Public hostname the sandbox is reachable at. Empty when the sandbox\nexposes no port or the cluster has no sandbox ingress configured.\n")
internal_address: str | None = Field(default=None, description="Cluster-internal host:port the sandbox's exposed port is served on,\nreachable from workloads inside the cluster subject to their own\nnetwork policy. Empty when the sandbox exposes no port.\n")
volume_mounts: dict[str, str] | None = Field(default=None, description="Volumes the sandbox mounts, keyed by mount path")
exec_target: ExecTarget | None = Field(default=None, description="Where a client addresses an interactive exec into the running sandbox. Absent until the sandbox has a pod.")

Expand Down Expand Up @@ -237,4 +261,4 @@ class VolumeSpec(BaseModel):
object: VolumeObjectSpec | None = Field(default=None)


__all__ = ["CountPoint", "CountResponse", "CreateResponse", "Error", "ExecRequest", "ExecResponse", "ExecTarget", "FilterValuesResponse", "HealthResponse", "ListResponse", "LogLine", "LogsResponse", "LookupResult", "MetricSummaryResponse", "Pagination", "ReadinessResponse", "SandboxDomainSpec", "SandboxEgressSpec", "SandboxMetricsPoint", "SandboxMetricsResponse", "SandboxMetricsResult", "SandboxMetricsSeries", "SandboxNetworkingSpec", "SandboxResourcesSpec", "SandboxSpec", "StatusResponse", "Volume", "VolumeFileEntry", "VolumeFileListResponse", "VolumeFileMoveRequest", "VolumeListResponse", "VolumeObjectSpec", "VolumeSpec"]
__all__ = ["CountPoint", "CountResponse", "CreateResponse", "Error", "ExecRequest", "ExecResponse", "ExecTarget", "FilterValuesResponse", "HealthResponse", "ListResponse", "LogLine", "LogsResponse", "LookupResult", "MetricSummaryResponse", "Pagination", "ReadinessResponse", "SandboxDomainSpec", "SandboxEgressSpec", "SandboxMetricsPoint", "SandboxMetricsResponse", "SandboxMetricsResult", "SandboxMetricsSeries", "SandboxNetworkingSpec", "SandboxResourcesSpec", "SandboxSpec", "Snapshot", "SnapshotListResponse", "SnapshotSpec", "StatusResponse", "Volume", "VolumeFileEntry", "VolumeFileListResponse", "VolumeFileMoveRequest", "VolumeListResponse", "VolumeObjectSpec", "VolumeSpec"]
13 changes: 12 additions & 1 deletion porter_sandbox/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ class SandboxMetric(str, Enum):
NETWORK_TX = "network_tx"


class SnapshotMode(str, Enum):
FILESYSTEM = "filesystem"
FULL = "full"


class SnapshotStatus(str, Enum):
PENDING = "pending"
READY = "ready"
FAILED = "failed"


class StatusResponsePhase(str, Enum):
QUEUED = "queued"
CREATING = "creating"
Expand Down Expand Up @@ -80,4 +91,4 @@ class VolumeType(str, Enum):
OBJECT = "object"


__all__ = ["FilterValuesResponsePhases", "LogLineLevel", "SandboxDomainSpecVisibility", "SandboxesPhase", "SandboxMetric", "StatusResponsePhase", "VolumeFileEntryType", "VolumeObjectSpecAccess", "VolumePhase", "VolumeSpecType", "VolumeType"]
__all__ = ["FilterValuesResponsePhases", "LogLineLevel", "SandboxDomainSpecVisibility", "SandboxesPhase", "SandboxMetric", "SnapshotMode", "SnapshotStatus", "StatusResponsePhase", "VolumeFileEntryType", "VolumeObjectSpecAccess", "VolumePhase", "VolumeSpecType", "VolumeType"]
3 changes: 3 additions & 0 deletions porter_sandbox/porter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from porter_sandbox.healthz import AsyncHealthz, Healthz
from porter_sandbox.readyz import AsyncReadyz, Readyz
from porter_sandbox.sandboxes import AsyncSandboxes, Sandboxes
from porter_sandbox.snapshots import AsyncSnapshots, Snapshots
from porter_sandbox.volumes import AsyncVolumes, Volumes


Expand All @@ -28,6 +29,7 @@ def __init__(
self.healthz: Healthz = Healthz(self._client.healthz)
self.readyz: Readyz = Readyz(self._client.readyz)
self.sandboxes: Sandboxes = Sandboxes(self._client.sandboxes)
self.snapshots: Snapshots = Snapshots(self._client.snapshots)
self.volumes: Volumes = Volumes(self._client.volumes)

@property
Expand Down Expand Up @@ -64,6 +66,7 @@ def __init__(
self.healthz: AsyncHealthz = AsyncHealthz(self._client.healthz)
self.readyz: AsyncReadyz = AsyncReadyz(self._client.readyz)
self.sandboxes: AsyncSandboxes = AsyncSandboxes(self._client.sandboxes)
self.snapshots: AsyncSnapshots = AsyncSnapshots(self._client.snapshots)
self.volumes: AsyncVolumes = AsyncVolumes(self._client.volumes)

@property
Expand Down
3 changes: 2 additions & 1 deletion porter_sandbox/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .healthz import AsyncHealthz, Healthz
from .readyz import AsyncReadyz, Readyz
from .sandboxes import AsyncSandboxes, Sandboxes
from .snapshots import AsyncSnapshots, Snapshots
from .volumes import AsyncVolumes, Volumes

__all__ = ["Healthz", "AsyncHealthz", "Readyz", "AsyncReadyz", "Sandboxes", "AsyncSandboxes", "Volumes", "AsyncVolumes"]
__all__ = ["Healthz", "AsyncHealthz", "Readyz", "AsyncReadyz", "Sandboxes", "AsyncSandboxes", "Snapshots", "AsyncSnapshots", "Volumes", "AsyncVolumes"]
101 changes: 101 additions & 0 deletions porter_sandbox/resources/snapshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Generated by oagen. DO NOT EDIT.


from __future__ import annotations

from typing import Any, TypeVar

from pydantic import BaseModel

from .._async_base_client import _AsyncBaseClient
from .._base_client import _BaseClient
from .._models import Snapshot, SnapshotListResponse, SnapshotSpec

_M = TypeVar("_M", bound=BaseModel)


def _coerce(model_cls: type[_M], data: Any) -> _M:
"""Validate API response data into the generated model type."""
return model_cls.model_validate(data)


class Snapshots:
"""Snapshots resource."""

def __init__(self, client: _BaseClient) -> None:
self._client = client

def create_snapshot(self, id: str, body: SnapshotSpec) -> Snapshot:
"""
Snapshot a sandbox

Capture a running sandbox so a new sandbox can be started from it later.
The sandbox keeps running.
"""
path = f"/v1/sandbox/{id}/snapshot"
response = self._client._request(method="POST", path=path, json=body.model_dump(by_alias=True, exclude_none=True) if hasattr(body, "model_dump") else body)
return _coerce(Snapshot, response)

def list_snapshots(self, sandbox_id: str | None = None) -> SnapshotListResponse:
"""
List snapshots

List captured snapshots, most recent first.
"""
path = "/v1/snapshot"
params: dict[str, Any] = {}
if sandbox_id is not None:
params["sandbox_id"] = sandbox_id
response = self._client._request(method="GET", path=path, params=params)
return _coerce(SnapshotListResponse, response)

def delete_snapshot(self, id: str) -> None:
"""
Delete snapshot

Delete a snapshot and everything it captured.
"""
path = f"/v1/snapshot/{id}"
self._client._request(method="DELETE", path=path)
return None


class AsyncSnapshots:
"""Snapshots resource."""

def __init__(self, client: _AsyncBaseClient) -> None:
self._client = client

async def create_snapshot(self, id: str, body: SnapshotSpec) -> Snapshot:
"""
Snapshot a sandbox

Capture a running sandbox so a new sandbox can be started from it later.
The sandbox keeps running.
"""
path = f"/v1/sandbox/{id}/snapshot"
response = await self._client._request(method="POST", path=path, json=body.model_dump(by_alias=True, exclude_none=True) if hasattr(body, "model_dump") else body)
return _coerce(Snapshot, response)

async def list_snapshots(self, sandbox_id: str | None = None) -> SnapshotListResponse:
"""
List snapshots

List captured snapshots, most recent first.
"""
path = "/v1/snapshot"
params: dict[str, Any] = {}
if sandbox_id is not None:
params["sandbox_id"] = sandbox_id
response = await self._client._request(method="GET", path=path, params=params)
return _coerce(SnapshotListResponse, response)

async def delete_snapshot(self, id: str) -> None:
"""
Delete snapshot

Delete a snapshot and everything it captured.
"""
path = f"/v1/snapshot/{id}"
await self._client._request(method="DELETE", path=path)
return None
4 changes: 4 additions & 0 deletions porter_sandbox/sandboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def create(
self,
image: str,
*,
snapshot_id: str | None = None,
name: str | None = None,
tags: dict[str, str] | None = None,
command: list[str] | None = None,
Expand All @@ -46,6 +47,7 @@ def create(
) -> Sandbox:
spec = SandboxSpec(
image=image,
snapshot_id=snapshot_id,
name=name,
tags=tags,
command=command,
Expand Down Expand Up @@ -98,6 +100,7 @@ async def create(
self,
image: str,
*,
snapshot_id: str | None = None,
name: str | None = None,
tags: dict[str, str] | None = None,
command: list[str] | None = None,
Expand All @@ -112,6 +115,7 @@ async def create(
) -> AsyncSandbox:
spec = SandboxSpec(
image=image,
snapshot_id=snapshot_id,
name=name,
tags=tags,
command=command,
Expand Down
52 changes: 52 additions & 0 deletions porter_sandbox/snapshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Generated by oagen. DO NOT EDIT.


from __future__ import annotations

from porter_sandbox._models import Snapshot, SnapshotListResponse, SnapshotSpec
from porter_sandbox.resources.snapshots import AsyncSnapshots as AsyncSnapshotsResource
from porter_sandbox.resources.snapshots import Snapshots as SnapshotsResource


class Snapshots:
"""User-facing snapshots namespace."""

def __init__(self, resource: SnapshotsResource) -> None:
self._resource = resource

@property
def raw(self) -> SnapshotsResource:
"""Direct access to the generated low-level snapshots resource."""
return self._resource

def create(self, id: str, body: SnapshotSpec) -> Snapshot:
return self._resource.create_snapshot(id=id, body=body)

def list(self, sandbox_id: str | None = None) -> SnapshotListResponse:
return self._resource.list_snapshots(sandbox_id=sandbox_id)

def delete(self, id: str) -> None:
self._resource.delete_snapshot(id=id)
return None


class AsyncSnapshots:
"""User-facing snapshots namespace."""

def __init__(self, resource: AsyncSnapshotsResource) -> None:
self._resource = resource

@property
def raw(self) -> AsyncSnapshotsResource:
"""Direct access to the generated low-level snapshots resource."""
return self._resource

async def create(self, id: str, body: SnapshotSpec) -> Snapshot:
return await self._resource.create_snapshot(id=id, body=body)

async def list(self, sandbox_id: str | None = None) -> SnapshotListResponse:
return await self._resource.list_snapshots(sandbox_id=sandbox_id)

async def delete(self, id: str) -> None:
await self._resource.delete_snapshot(id=id)
return None
Loading
Loading