service

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrPreferredHostPortUnavailable = errors.New("preferred host port unavailable on this node; exposure parked")

ErrPreferredHostPortUnavailable is returned by exposePort when a TCP replay supplied a specific preferredHostPort that's already reserved (cluster-wide or on this node) and so cannot be re-bound. The allocator deliberately does NOT silently fall through to a fresh random port: cluster-stable TCP endpoints are the entire point of B6 — clients addressing host:40123 must not be invisibly rerouted to host:55555 after a failover-recreate. Park is the policy; the FSM record (with the original HostPort) stays intact and the watcher / operator surfaces the parked state instead of mutating the contract behind the client's back.

Functions

func GenerateSandboxID added in v0.2.1

func GenerateSandboxID() (string, error)

GenerateSandboxID is the exported entry point for the cluster handler's reservation-first create path: the router (Node A) needs to mint a sandbox ID before opReserve so the chosen target (Node T) can accept the forward with X-Cluster-Create-ID and run CreateSandboxWithID against the same reservation. Routes through the package-private generateSandboxID so the format stays in lockstep with the local create path.

func ImageRequiresLocalPlacement added in v0.2.1

func ImageRequiresLocalPlacement(req models.CreateSandboxRequest) bool

func IngressInstalledVersion added in v0.2.1

func IngressInstalledVersion() uint64

IngressInstalledVersion returns the highest placement.Version this node's ingress reconciler has finished installing routes for. The /v1/cluster/ placements/{id} convergence-status read compares this against the per- placement Version to answer "has this node bound the cluster-stable TCP route yet?" without reaching into Caddy state. Zero means the reconciler has not yet run a successful pass (fresh boot) — treat as "not converged" for any non-zero placement.Version.

func NormalizeCreateFailover added in v0.2.2

func NormalizeCreateFailover(req *models.CreateSandboxRequest) error

func RecordCreateReservationState added in v0.2.1

func RecordCreateReservationState(state string)

func RecordExpiredReservations added in v0.2.1

func RecordExpiredReservations(count int)

func RecordFacadeIdempotencyConflict added in v0.2.1

func RecordFacadeIdempotencyConflict(scope string)

func RecordFacadeIdempotencyReplay added in v0.2.1

func RecordFacadeIdempotencyReplay(scope string)

func RecordRouteMiss added in v0.2.1

func RecordRouteMiss(reasons ...string)

RecordRouteMiss bumps the route-miss counter from the API layer. Exported so pkg/api/v1 can wire it in without exposing the expvar directly (keeps the metric name owned by this package). Service has no logical role here — this is a package-level counter the v1 wrap layer pokes when it observes the no-usable-URL case.

func RedactClusterSecrets added in v0.2.1

func RedactClusterSecrets(req models.CreateSandboxRequest) models.CreateSandboxRequest

RedactClusterSecrets returns a copy of req with credentials stripped — safe to replicate via raft. The Registry field's Server/Username are preserved (not secret) but Password is cleared; mount Credentials maps are dropped per-entry. Maps and slices that the caller might mutate are deep-copied so the original req is left untouched.

Lives next to SealClusterSecrets because the two are always called as a pair: seal returns the encrypted bag, redact returns the safe-to-replicate spec, and writing one without the other would either leak secrets (no redact) or lose them on failover (no seal).

func SetIngressRouteLag added in v0.2.1

func SetIngressRouteLag(fsmVersion uint64)

SetIngressRouteLag is the post-tick hook the reconciler calls with the FSM's current PlacementVersion. Lag is computed as max(0, fsmVersion - ingressPlacementVersionMax). Computed here (not at recordIngressReconcile) so callers that only have the FSM version can still publish the lag without needing the reconciler's maxVersion.

Types

type ImageDistributionProvider added in v0.2.1

type ImageDistributionProvider interface {
	ClassifyImage(ctx context.Context, image string) (models.ImageDistributionMetadata, error)
}

ImageDistributionProvider is the control-plane contract for image availability. Providers may verify digests or talk to an external registry cache, but the core daemon only needs the resulting placement metadata.

type Service

type Service struct {
	// contains filtered or unexported fields
}

func New

func New(cfg config.Config, logger *slog.Logger, db *store.Store, runtimeDriver runtime.Runtime, eventsClient *docker.Client, caddyClient *caddy.Client, cipher *secrets.Cipher, mountManager *mounts.Manager, admitter *capacity.Admitter) *Service

func (*Service) AttachCluster added in v0.2.1

func (s *Service) AttachCluster(c cluster.Client)

AttachCluster swaps in a cluster.Client. Called from cmd/sandboxd/main after service.New when SB_ENABLE_CLUSTER=true. Idempotent.

func (*Service) Capacity

func (s *Service) Capacity() capacity.Snapshot

Capacity returns the admitter's current snapshot. Returns the zero value when no admitter is configured (e.g. in tests).

func (*Service) ClaimIdempotentRequest added in v0.1.7

func (s *Service) ClaimIdempotentRequest(ctx context.Context, scope, fingerprint string, now time.Time, pendingTTL time.Duration) (*models.IdempotentRequestRecord, bool, error)

func (*Service) Cluster added in v0.2.1

func (s *Service) Cluster() cluster.Client

Cluster returns the attached cluster.Client. Always non-nil.

func (*Service) ClusterTopologyError added in v0.2.2

func (s *Service) ClusterTopologyError() error

ClusterTopologyError returns a production-topology violation for the current live member set. It is intentionally a runtime check so rolling membership, old nodes that still gossip empty roles, and explicit hybrid roles are all evaluated from the same source of truth the scheduler uses.

func (*Service) CompleteIdempotentRequest added in v0.1.7

func (s *Service) CompleteIdempotentRequest(ctx context.Context, scope, fingerprint, targetID string, now time.Time, replayTTL time.Duration) error

func (*Service) CreateSandbox

func (*Service) CreateSandboxWithID added in v0.2.1

func (s *Service) CreateSandboxWithID(ctx context.Context, req models.CreateSandboxRequest, id string) (*models.CreateSandboxResponse, error)

CreateSandboxWithID is the failover-recreate entry point: it behaves like CreateSandbox but uses the supplied ID instead of generating a fresh one. Idempotent at the cluster boundary — if a sandbox with this ID already exists locally we return the existing record without touching docker. Used by the cluster owner watcher to re-materialize a sandbox after its previous owner died.

func (*Service) CreateSnapshot added in v0.1.7

func (s *Service) CreateSnapshot(ctx context.Context, sandboxID string, req models.CreateSandboxSnapshotRequest) (*models.SandboxSnapshot, error)

CreateSnapshot commits the sandbox container into a reusable local image. Idempotency is by snapshot name: repeated requests for the same sandbox + name return the stored snapshot metadata, while a different sandbox trying to claim the same name is rejected with a conflict.

func (*Service) CreateSnapshotWithOwnership added in v0.1.7

func (s *Service) CreateSnapshotWithOwnership(ctx context.Context, sandboxID string, req models.CreateSandboxSnapshotRequest) (*models.SandboxSnapshot, bool, error)

CreateSnapshotWithOwnership commits a sandbox image and reports whether this call created the native snapshot row. Callers that add companion metadata can use the flag to avoid rolling back a snapshot that already existed.

func (*Service) DeleteClusterSecrets added in v0.2.1

func (s *Service) DeleteClusterSecrets(ctx context.Context, sandboxID string) error

func (*Service) DeleteIdempotentRequest added in v0.1.7

func (s *Service) DeleteIdempotentRequest(ctx context.Context, scope, fingerprint string) error

func (*Service) DeleteSnapshot added in v0.1.7

func (s *Service) DeleteSnapshot(ctx context.Context, idOrName string) error

func (*Service) DeleteSnapshotAlias added in v0.1.7

func (s *Service) DeleteSnapshotAlias(ctx context.Context, alias string) error

func (*Service) DestroySandbox

func (s *Service) DestroySandbox(ctx context.Context, id string) error

func (*Service) EnsureClusterReady added in v0.2.1

func (s *Service) EnsureClusterReady(ctx context.Context) error

EnsureClusterReady blocks until the cluster has elected a leader, mirroring the EnsureLayer4Ready single-flight latch shape. Single-node mode latches immediately. The API wrapper calls this before any RecordPlacement so a just-booted node doesn't 503 a CreateSandbox while raft is still catching up.

func (*Service) EnsureLayer4Ready added in v0.1.4

func (s *Service) EnsureLayer4Ready(ctx context.Context) error

EnsureLayer4Ready bootstraps the caddy-l4 app under a single-flight mutex and latches success. Safe to call from boot AND from each L4 exposure path: the atomic fast-path turns it into a single load on the steady state, and a failed boot is recovered by the very next TCP/TLS expose call instead of surfacing as a confusing "layer4 app missing" error from caddy.

func (*Service) EnsureNetstatsReady added in v0.1.7

func (s *Service) EnsureNetstatsReady(ctx context.Context) error

EnsureNetstatsReady boots the per-sandbox network byte-counter poller under a single-flight latch. Same lazy-bootstrap shape as EnsureLayer4Ready: caller pays the bootstrap cost only once across the daemon's lifetime, and the poller goroutine survives until ctx — typically the daemon's signal context — is cancelled.

Failure here is non-fatal: callers log and continue. Without the poller, network counters stay at zero and quotas never trigger; both Create and reads of /network/usage still work.

func (*Service) ExposePort

func (s *Service) ExposePort(ctx context.Context, id string, port int, protocol string) (models.ExposePortResponse, error)

ExposePort publishes a sandbox container port through one of three caddy surfaces, selected by protocol:

  • "" / "http": existing Caddy HTTP reverse-proxy route, returns https://<id>-<port>.<domain> (or the path-mode equivalent).
  • "tcp": allocates a parent-host TCP port from the [SB_L4_PORT_RANGE_START, SB_L4_PORT_RANGE_END] pool, points caddy-l4 at it, and returns tcp://<public-host>:<host-port>. This is what unblocks native Postgres / Redis / MySQL DSNs in the spawn-postgres docs.
  • "tls": adds a TLS-SNI route to the shared layer4 server. Requires --domain (so the SNI hostname has a place to resolve) and a non-empty SB_L4_TLS_LISTEN. Returns tls://<id>-<port>.<domain>:<l4-port>.

func (*Service) GetCompatState added in v0.1.7

func (s *Service) GetCompatState(ctx context.Context, sandboxID, facade string) (*models.SandboxCompatState, error)

func (*Service) GetIdempotentRequest added in v0.1.7

func (s *Service) GetIdempotentRequest(ctx context.Context, scope, fingerprint string) (*models.IdempotentRequestRecord, error)

func (*Service) GetNetworkUsage added in v0.1.7

func (s *Service) GetNetworkUsage(ctx context.Context, id string) (*models.NetworkUsage, error)

GetNetworkUsage returns the current cumulative byte counters and configured limits for a sandbox. Callers handle ErrNotFound translation.

Best-effort lazy bootstrap of the netstats poller: if boot's EnsureNetstatsReady failed (cold-start race against the docker daemon, etc.) this call retries it under the same single-flight latch. Failure is logged and swallowed — the caller still gets back whatever counters the store has, and the next call will retry again.

func (*Service) GetSandbox

func (s *Service) GetSandbox(ctx context.Context, id string) (*models.Sandbox, error)

func (*Service) GetSnapshot added in v0.1.7

func (s *Service) GetSnapshot(ctx context.Context, idOrName string) (*models.SandboxSnapshot, error)

func (*Service) GetSnapshotAlias added in v0.1.7

func (s *Service) GetSnapshotAlias(ctx context.Context, alias string) (*models.SnapshotAlias, error)

func (*Service) Health

func (s *Service) Health(ctx context.Context) (models.HealthStatus, error)

func (*Service) ListCompatState added in v0.1.7

func (s *Service) ListCompatState(ctx context.Context, facade string) (map[string]models.SandboxCompatState, error)

func (*Service) ListMounts

func (s *Service) ListMounts(ctx context.Context, sandboxID string) ([]models.MountSpecRedacted, error)

ListMounts returns the redacted mount config for a sandbox. Credentials are never included in the response — they are write-only via CreateSandbox.

func (*Service) ListSandboxes

func (s *Service) ListSandboxes(ctx context.Context, tagFilter map[string]string) ([]*models.Sandbox, error)

ListSandboxes returns sandboxes whose Tags match every entry in tagFilter. A nil or empty filter returns every sandbox on this node. Filtering happens in-memory after the store read because Tags is JSON-encoded; pushing the filter into SQL via json_extract is a follow-up once row counts make the extra hop worth it. The filter exists so an external control plane can ask "give me the sandboxes belonging to user X" without round-tripping every sandbox in the cluster (see plans/multi-tenancy-via-control-plane.md).

func (*Service) ListSnapshotAliases added in v0.1.7

func (s *Service) ListSnapshotAliases(ctx context.Context, facade string) (map[string]models.SnapshotAlias, error)

func (*Service) ListSnapshots added in v0.1.7

func (s *Service) ListSnapshots(ctx context.Context) ([]*models.SandboxSnapshot, error)

func (*Service) NormalizeCreateImageDistribution added in v0.2.1

func (s *Service) NormalizeCreateImageDistribution(ctx context.Context, req *models.CreateSandboxRequest) error

NormalizeCreateImageDistribution resolves snapshot aliases and fills the create request's image-distribution metadata before placement decisions are made. The method is intentionally side-effect free: it does not pull, push, or verify image contents.

func (*Service) OpenClusterSecrets added in v0.2.1

func (*Service) OpenClusterSecretsForNode added in v0.2.1

func (s *Service) OpenClusterSecretsForNode(ctx context.Context, redacted models.CreateSandboxRequest, secrets cluster.PlacementSecrets, nodeID string) (out models.CreateSandboxRequest, err error)

OpenClusterSecretsForNode resolves a replicated secret handle and merges the decrypted credentials back into a redacted spec. LegacySealed is still honored for placements written before the ref model.

func (*Service) PutClusterSecretsForRecipient added in v0.2.1

func (s *Service) PutClusterSecretsForRecipient(ctx context.Context, sandboxID string, req models.CreateSandboxRequest, recipient string) (cluster.PlacementSecrets, error)

PutClusterSecretsForRecipient stores the credential-bearing parts of req behind a provider ref and returns the handle safe to replicate through Raft. The local provider stores an encrypted recipient-bound envelope in SQLite; external KMS/secret-store providers can replace this boundary without changing cluster placement state.

func (*Service) Reconcile

func (s *Service) Reconcile(ctx context.Context) error

func (*Service) ReconcileClusterIngress added in v0.2.1

func (s *Service) ReconcileClusterIngress(ctx context.Context) error

func (*Service) RecreateSandbox added in v0.2.1

func (s *Service) RecreateSandbox(ctx context.Context, id string, spec models.CreateSandboxRequest, secrets cluster.PlacementSecrets, exposedPorts map[int]cluster.ExposedPortRoute) error

RecreateSandbox satisfies cluster.SandboxRecreator. The cluster owner watcher invokes this for any FSM placement that points to self. If the sandbox already exists locally, we still replay the replicated port intents: a previous recreate attempt may have created the container and then failed while restoring Caddy/L4 ingress.

secrets is the provider handle that can rehydrate the redacted spec; we resolve and re-merge it here via OpenClusterSecretsForNode so the recreated container can pull from the same private registry / mount the same external storage. A decrypt failure is fatal to this attempt but non-fatal globally — the watcher's retry loop (now with reassign-after-K-failures) will eventually move the placement to a node whose key matches.

Port replay tries every port but returns an error when any replay failed so the owner watcher keeps retrying and can eventually reassign the placement. ExposePort is idempotent, so a partial replay is safe to resume.

Only placements whose create spec opted into failover.policy=recreate reach this path; default sandboxes remain non-HA and are orphaned on owner death.

func (*Service) RegisterSnapshot added in v0.1.7

func (s *Service) RegisterSnapshot(ctx context.Context, snapshot *models.SandboxSnapshot) (*models.SandboxSnapshot, error)

RegisterSnapshot persists a snapshot row whose Image was resolved out-of-band — either a pre-existing registry image the caller supplied by name, or a freshly built local tag produced by the image builder (e.g. the daytona facade's buildInfo path). It does NOT call docker.CreateSnapshot; the image is assumed to already be runnable. Idempotency is by snapshot name; a re-register with matching image is treated as a no-op so SDK retries don't fail. A different image under the same name is a conflict.

func (*Service) ReplayReservations

func (s *Service) ReplayReservations(ctx context.Context)

ReplayReservations re-populates the admitter from persistent state. Without this, after a daemon restart the admitter sees zero reservations and the host can be overcommitted on the first wave of new sandboxes. Destroyed AND stopped sandboxes are skipped — neither holds host CPU/RAM (the stop path releases the slot, and StartSandbox re-Admits on the way back up), so counting them here would re-introduce the overcommit-budget bug we fixed when stop began releasing capacity. Best-effort: a store error is logged, not returned, since admission control degrading to "unaware" is preferable to refusing to boot.

func (*Service) ResizeSandbox

func (s *Service) ResizeSandbox(ctx context.Context, id string, req models.ResizeSandboxRequest) (*models.Sandbox, error)

func (*Service) ResolveSandboxIDByName added in v0.1.7

func (s *Service) ResolveSandboxIDByName(ctx context.Context, name string) (string, error)

ResolveSandboxIDByName looks up the sandbox owning the given unique name. Empty name returns ErrNotFound (handled inside the store).

func (*Service) SealClusterSecrets added in v0.2.1

func (s *Service) SealClusterSecrets(req models.CreateSandboxRequest) ([]byte, error)

SealClusterSecrets extracts the secret-bearing portions of req, marshals them as JSON, and encrypts the result with the service cipher. The output is opaque bytes safe to put in the raft log. Returns nil/nil when there are no secrets to seal so the FSM column stays empty for sandboxes that don't need it.

The legacy method emits a wildcard-recipient v2 envelope for compatibility. New cluster placement paths should prefer SealClusterSecretsForRecipient so the encrypted payload is authenticated to the specific owner node ID.

func (*Service) SealClusterSecretsForRecipient added in v0.2.1

func (s *Service) SealClusterSecretsForRecipient(req models.CreateSandboxRequest, recipient string) ([]byte, error)

func (*Service) SetNetworkLimits added in v0.1.7

func (s *Service) SetNetworkLimits(ctx context.Context, id string, bytesInLimit, bytesOutLimit int64) (*models.NetworkUsage, error)

SetNetworkLimits writes new caps (0 = unlimited) and re-evaluates the quota state. If the new limit moves the sandbox back under-quota, the matching ingress/egress block is cleared. If it leaves the sandbox over, the matching block is (re-)applied.

Egress clears are conditional: NetworkBlockAll uses the same DOCKER-USER row as the quota egress block, so we must not lift it here when the operator's blanket egress block is still on. See pkg/docker/netrules commentary on the shared rule.

func (*Service) StartBuiltImageGC added in v0.1.7

func (s *Service) StartBuiltImageGC(ctx context.Context)

StartBuiltImageGC launches the periodic janitor that removes locally-built images (BuiltImageNamespace, i.e. "aerolvm-build/*") that are no longer referenced by any active sandbox AND were created more than the configured TTL ago. Without this, two failure modes leak images forever:

  • POST /v1/images/build called standalone (no follow-up CreateSandbox).
  • Build succeeded, CreateSandbox failed AND the daytona facade's inline rollback couldn't reach the daemon (e.g. server-side panic, dropped connection between build success and rollback call).

The TTL keeps the janitor from racing the dominant build+create flow: an image built moments ago must clear ImageBuildGCTTL before it's eligible, so a transient network hiccup between build and create can't have the janitor yanking an image the client is about to consume.

No-op if ImageBuildGCEnabled is false or ImageBuildGCInterval <= 0.

func (*Service) StartClusterIngressReconcile added in v0.2.1

func (s *Service) StartClusterIngressReconcile(ctx context.Context)

func (*Service) StartEventMonitor

func (s *Service) StartEventMonitor(ctx context.Context)

StartEventMonitor launches the Docker event consumer goroutine. It is the realtime counterpart to Reconcile() — when a container dies, OOM-kills, or is destroyed out-of-band, this loop updates the DB and tears down routes within ~1s instead of waiting for the next reconcile tick.

func (*Service) StartLifecycleSweep

func (s *Service) StartLifecycleSweep(ctx context.Context)

StartLifecycleSweep launches the per-sandbox lifecycle ticker. Every minute it evaluates each sandbox's Lifecycle timers (StopIfIdleFor / DestroyIfIdleFor / StopAtAge / DestroyAtAge) plus the legacy global SB_IDLE_TIMEOUT_MIN fallback for sandboxes that don't declare any per-sandbox timers. Without either configured, the sweep still runs but is a no-op — kept on so a later UpdateLifecycle call doesn't need to start a goroutine.

func (*Service) StartReconcileLoop

func (s *Service) StartReconcileLoop(ctx context.Context)

func (*Service) StartSandbox

func (s *Service) StartSandbox(ctx context.Context, id string) (*models.Sandbox, error)

func (*Service) StopSandbox

func (s *Service) StopSandbox(ctx context.Context, id string) (*models.Sandbox, error)

func (*Service) ToolboxTarget

func (s *Service) ToolboxTarget(ctx context.Context, id string) (ToolboxEndpoint, error)

func (*Service) TouchSandbox

func (s *Service) TouchSandbox(ctx context.Context, id string) error

func (*Service) UnexposePort

func (s *Service) UnexposePort(ctx context.Context, id string, port int) error

func (*Service) UnsealClusterSecrets added in v0.2.1

func (s *Service) UnsealClusterSecrets(redacted models.CreateSandboxRequest, sealed []byte) (models.CreateSandboxRequest, error)

UnsealClusterSecrets opens a sealed bag and merges the credentials back into the previously-redacted spec. Returns the merged spec; the input is not mutated. An empty sealed payload returns redacted unchanged so callers don't have to short-circuit themselves.

The merge prefers the redacted spec for non-secret fields (Registry Server/Username, mount Source/Target/Options) and overlays only the secret bits — so a future credential rotation that re-seals doesn't stomp on whatever the latest replicated metadata is.

func (*Service) UnsealClusterSecretsForNode added in v0.2.1

func (s *Service) UnsealClusterSecretsForNode(redacted models.CreateSandboxRequest, sealed []byte, nodeID string) (models.CreateSandboxRequest, error)

func (*Service) UnsealRegistry added in v0.2.1

func (s *Service) UnsealRegistry(sealed []byte) (*models.RegistryAuth, error)

UnsealRegistry decrypts a previously sealed RegistryAuth. Returns nil/nil when the input is empty (no credentials persisted). Exported for the boot-time backfill in cmd/sandboxd that rebuilds CreateSandboxRequest from the persisted Sandbox row.

func (*Service) UpdateLifecycle

func (s *Service) UpdateLifecycle(ctx context.Context, id string, l models.Lifecycle) (*models.Sandbox, error)

UpdateLifecycle replaces the lifecycle timers on an existing sandbox. Full-replacement semantics: pass zero in any field to clear that timer. The sweep picks up the new values on its next tick (within ~1 minute), so a tightened deadline can fire as soon as the next sweep runs.

func (*Service) UpdateTags added in v0.1.7

func (s *Service) UpdateTags(ctx context.Context, sandboxID string, tags map[string]string) error

UpdateTags replaces sandboxes.tags_json for the given sandbox. Tags are the native key/value bag — facades use it for label-style metadata (Daytona labels, E2B metadata).

func (*Service) UpsertCompatState added in v0.1.7

func (s *Service) UpsertCompatState(ctx context.Context, sandboxID, facade, stateJSON string) error

func (*Service) UpsertSnapshotAlias added in v0.1.7

func (s *Service) UpsertSnapshotAlias(ctx context.Context, alias models.SnapshotAlias) error

type ToolboxEndpoint

type ToolboxEndpoint struct {
	URL   string
	Token string
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL