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
6 changes: 6 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,12 @@ async def api_deploy(request: Request, slug: str, body: DeployRequest, worker_id
if raw_command:
spec["command"] = re.sub(r"\$\{(\w+)\}", lambda m: env.get(m.group(1), m.group(0)), raw_command)

# Durable resource limits (mem_limit / mem_reservation / oom_score_adj),
# declared in the service YAML. Only forwarded when present.
resources = docker_conf.get("resources")
if resources:
spec["resources"] = resources

result = await _proxy_worker_deploy(worker_id, slug, spec)
container_id = result.get("container_id", "remote")
await database.save_deployment(slug=slug, container_id=container_id)
Expand Down
30 changes: 29 additions & 1 deletion app/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ def _find_container(slug: str):
raise ValueError(f"Container for {slug} not found")


def _normalize_resources(resources: Any) -> dict[str, Any]:
"""Coerce a resources spec (Pydantic model, dict, or None) into a plain dict.

Only non-None values are kept, so callers can splat the result into
``containers.run`` without clobbering Docker's own defaults.
"""
if resources is None:
return {}
if hasattr(resources, "model_dump"):
data = resources.model_dump()
elif isinstance(resources, dict):
data = dict(resources)
else:
return {}
return {k: v for k, v in data.items() if v is not None}


def deploy_raw(
slug: str,
image: str,
Expand All @@ -114,12 +131,14 @@ def deploy_raw(
command: str | None = None,
hostname: str | None = None,
labels: dict[str, str] | None = None,
resources: Any = None,
category: str = "bandwidth",
) -> str:
"""Deploy a container from a raw spec (no catalog lookup).

Used by CashPilot Worker when the UI sends a full container spec.
Returns the container ID.
``resources`` (mem_limit / mem_reservation / oom_score_adj) makes the
container's cgroup limits durable across recreates. Returns the container ID.
"""
client = _get_client()
name = _container_name(slug)
Expand Down Expand Up @@ -148,6 +167,14 @@ def deploy_raw(
except APIError as exc:
logger.warning("Failed to pull image %s: %s (trying local)", image, exc)

# Durable resource limits (only passed when explicitly set, so Docker
# defaults are preserved otherwise). memswap is deliberately left unset:
# at create time mem_limit alone avoids the cgroup-v2 swap validation issue.
res = _normalize_resources(resources)
resource_kwargs = {
key: res[key] for key in ("mem_limit", "mem_reservation", "oom_score_adj") if res.get(key) is not None
}

logger.info("Creating container %s from %s", name, image)
container = client.containers.run(
image=image,
Expand All @@ -163,6 +190,7 @@ def deploy_raw(
hostname=hostname or f"cashpilot-{slug}",
detach=True,
restart_policy={"Name": "unless-stopped"},
**resource_kwargs,
)

logger.info("Container %s started: %s", name, container.short_id)
Expand Down
34 changes: 34 additions & 0 deletions app/worker_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import logging
import os
import platform
import re
import socket
from contextlib import asynccontextmanager
from datetime import UTC, datetime
Expand Down Expand Up @@ -258,6 +259,18 @@ async def worker_status_page():
# ---------------------------------------------------------------------------


class ResourceSpec(BaseModel):
"""Optional Docker resource limits applied when the container is created.

mem_limit / mem_reservation follow Docker's size syntax ("768m", "2g");
oom_score_adj biases the kernel OOM killer (-1000 = sacrificed last).
"""

mem_limit: str | None = None
mem_reservation: str | None = None
oom_score_adj: int | None = None


class DeploySpec(BaseModel):
image: str
env: dict[str, str] = {}
Expand All @@ -269,10 +282,13 @@ class DeploySpec(BaseModel):
command: str | None = None
hostname: str | None = None
labels: dict[str, str] = {}
resources: ResourceSpec | None = None


_BLOCKED_VOLUME_SOURCES = {"/", "/etc", "/var/run/docker.sock", "/root", "/proc", "/sys"}
_BLOCKED_CAPS = {"ALL", "SYS_ADMIN", "SYS_PTRACE"}
# Docker memory size syntax: a positive integer with an optional b/k/m/g unit.
_MEM_LIMIT_RE = re.compile(r"^\d+[bkmgBKMG]?$")


def _validate_deploy_spec(spec: DeploySpec) -> None:
Expand All @@ -289,6 +305,23 @@ def _validate_deploy_spec(spec: DeploySpec) -> None:
for blocked in _BLOCKED_VOLUME_SOURCES:
if normalized == blocked or normalized.startswith(blocked + "/"):
raise HTTPException(status_code=403, detail=f"Volume mount '{source}' is blocked")
_validate_resources(spec.resources)


def _validate_resources(resources: ResourceSpec | None) -> None:
if resources is None:
return
for field, value in (("mem_limit", resources.mem_limit), ("mem_reservation", resources.mem_reservation)):
if value is not None and not _MEM_LIMIT_RE.match(value):
raise HTTPException(
status_code=400,
detail=f"Invalid {field} '{value}': expected a size like '768m' or '2g'",
)
if resources.oom_score_adj is not None and not (-1000 <= resources.oom_score_adj <= 1000):
raise HTTPException(
status_code=400,
detail=f"Invalid oom_score_adj '{resources.oom_score_adj}': must be between -1000 and 1000",
)


@app.get("/api/status")
Expand Down Expand Up @@ -338,6 +371,7 @@ async def api_deploy_container(request: Request, slug: str, spec: DeploySpec) ->
command=spec.command,
hostname=spec.hostname,
labels=spec.labels,
resources=spec.resources,
)
return {"status": "deployed", "container_id": container_id}
except Exception:
Expand Down
4 changes: 4 additions & 0 deletions services/_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
# docker:
# image: Docker Hub image
# platforms: [linux/amd64, linux/arm64]
# resources: (optional, durable Docker resource limits applied at container create)
# mem_limit: "768m" # hard memory ceiling (str: <number><b|k|m|g>, e.g. "512m", "2g")
# mem_reservation: "512m" # (optional) soft memory reservation, same format as mem_limit
# oom_score_adj: -100 # kernel OOM-killer bias, int -1000..1000 (lower = sacrificed last)
# env:
# - key: ENV_VAR_NAME
# label: "Human label"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/bitping.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: bitping/bitpingd
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 200
env: []
ports: []
volumes:
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/earnapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ referral:
docker:
image: fazalfarhan01/earnapp:lite
platforms: [linux/amd64]
resources:
mem_limit: "256m"
oom_score_adj: 300
env:
- key: EARNAPP_UUID
label: "Node UUID"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/earnfm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ referral:
docker:
image: earnfm/earnfm-client
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 200
env:
- key: EARNFM_TOKEN
label: "API Key"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/honeygain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ referral:
docker:
image: honeygain/honeygain
platforms: [linux/amd64]
resources:
mem_limit: "256m"
oom_score_adj: 200
env:
- key: HONEYGAIN_EMAIL
label: "Email"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/iproyal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: iproyal/pawns-cli
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: IPROYALPAWNS_EMAIL
label: "Email"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/mysterium.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: mysteriumnetwork/myst
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "768m"
oom_score_adj: -100
env: []
ports:
- "4449:4449"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/packetstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: packetstream/psclient
platforms: [linux/amd64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: CID
label: "Client ID"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/proxybase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ referral:
docker:
image: proxybase/proxybase
platforms: [linux/amd64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: USER_ID
label: "User ID"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/proxylite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ referral:
docker:
image: proxylite/proxyservice
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: USER_ID
label: "User ID"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/proxyrack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: proxyrack/pop
platforms: [linux/amd64]
resources:
mem_limit: "256m"
oom_score_adj: 300
env:
- key: UUID
label: "Device UUID"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/repocket.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: repocket/repocket
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: RP_EMAIL
label: "Email"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/traffmonetizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ referral:
docker:
image: traffmonetizer/cli_v2
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: TRAFFMONETIZER_TOKEN
label: "Token"
Expand Down
3 changes: 3 additions & 0 deletions services/bandwidth/urnetwork.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ referral:
docker:
image: bringyour/community-provider
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "128m"
oom_score_adj: 300
env:
- key: UR_AUTH_TOKEN
label: "Auth Token"
Expand Down
3 changes: 3 additions & 0 deletions services/depin/anyone-protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ docker:
image: ghcr.io/anyone-protocol/ator-protocol
tag: latest
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "1536m"
oom_score_adj: -100
env: []
ports: ["9001:9001"]
volumes:
Expand Down
3 changes: 3 additions & 0 deletions services/storage/storj.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ referral:
docker:
image: storjlabs/storagenode
platforms: [linux/amd64, linux/arm64]
resources:
mem_limit: "2g"
oom_score_adj: -100
env:
- key: WALLET
label: "Wallet address"
Expand Down
Loading
Loading