fix: resolve circular import, async blocking, SELinux and delete timeout issues

- Extract shared SlowAPI limiter to app/limiter.py to break circular
  import between app.main and app.routers.auth
- Seed default SystemConfig row (id=1) on first DB init so settings
  page works out of the box
- Make all docker_service.compose_* functions async (run_in_executor)
  so long docker pulls/stops no longer block the async event loop
- Propagate async to netbird_service stop/start/restart and await
  callers in deployments router
- Move customer delete to BackgroundTasks so the HTTP response returns
  immediately and avoids frontend "Network error" on slow machines
- docker-compose: add :z SELinux labels, mount docker.sock directly,
  add security_opt label:disable for socket access, extra_hosts for
  host.docker.internal, enable DELETE/VOLUMES on socket proxy
- npm_service: auto-detect outbound host IP via UDP socket when
  HOST_IP env var is not set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 00:30:25 +01:00
parent 0ac15e4db9
commit 1bbe4904a7
10 changed files with 102 additions and 53 deletions

View File

@@ -27,8 +27,7 @@ from app.utils.validators import ChangePasswordRequest, LoginRequest, MfaTokenRe
logger = logging.getLogger(__name__)
router = APIRouter()
# Import the shared rate limiter from main
from app.main import limiter
from app.limiter import limiter
@router.post("/login")

View File

@@ -211,12 +211,14 @@ async def update_customer(
@router.delete("/{customer_id}")
async def delete_customer(
customer_id: int,
background_tasks: BackgroundTasks,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Delete a customer and clean up all resources.
Removes containers, NPM proxy, instance directory, and database records.
Cleanup runs in background so the response returns immediately.
Args:
customer_id: Customer ID.
@@ -231,15 +233,23 @@ async def delete_customer(
detail="Customer not found.",
)
# Undeploy first (containers, NPM, files)
try:
await netbird_service.undeploy_customer(db, customer_id)
except Exception:
logger.exception("Undeploy error for customer %d (continuing with delete)", customer_id)
# Delete customer record (cascades to deployment + logs)
db.delete(customer)
# Mark as deleting immediately so UI reflects the state
customer.status = "inactive"
db.commit()
logger.info("Customer %d deleted by %s.", customer_id, current_user.username)
return {"message": f"Customer {customer_id} deleted successfully."}
async def _delete_in_background(cid: int) -> None:
bg_db = SessionLocal()
try:
await netbird_service.undeploy_customer(bg_db, cid)
c = bg_db.query(Customer).filter(Customer.id == cid).first()
if c:
bg_db.delete(c)
bg_db.commit()
logger.info("Customer %d deleted by %s.", cid, current_user.username)
except Exception:
logger.exception("Background delete failed for customer %d", cid)
finally:
bg_db.close()
background_tasks.add_task(_delete_in_background, customer_id)
return {"message": f"Customer {customer_id} deletion started."}

View File

@@ -72,7 +72,7 @@ async def start_customer(
Result dict.
"""
_require_customer(db, customer_id)
result = netbird_service.start_customer(db, customer_id)
result = await netbird_service.start_customer(db, customer_id)
if not result.get("success"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -96,7 +96,7 @@ async def stop_customer(
Result dict.
"""
_require_customer(db, customer_id)
result = netbird_service.stop_customer(db, customer_id)
result = await netbird_service.stop_customer(db, customer_id)
if not result.get("success"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -120,7 +120,7 @@ async def restart_customer(
Result dict.
"""
_require_customer(db, customer_id)
result = netbird_service.restart_customer(db, customer_id)
result = await netbird_service.restart_customer(db, customer_id)
if not result.get("success"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,