Compare commits

...

2 Commits

Author SHA1 Message Date
Sascha Lustenberger | techlan gmbh
27428b69a0 fix(netbird): query customer before use in stop/start/restart
In stop_customer, start_customer and restart_customer the local variable
'customer' was referenced on the instance_dir line before it was assigned
(it was only queried after the docker compose call). This caused an
UnboundLocalError (HTTP 500) on every stop/start/restart action.

Fix: move the customer query to the top of each function alongside the
deployment and config queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 11:12:17 +01:00
Sascha Lustenberger | techlan gmbh
582f92eec4 fix(update): add git safe.directory and fetch --tags after pull
- Register SOURCE_DIR as git safe.directory before pulling so the
  process (root inside container) can access repos owned by a host user
- Run 'git fetch --tags' after pull so git describe always finds the
  latest tag for version.json — git pull does not reliably fetch all tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:58:02 +01:00
2 changed files with 31 additions and 15 deletions

View File

@@ -484,16 +484,15 @@ async def undeploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
async def stop_customer(db: Session, customer_id: int) -> dict[str, Any]: async def stop_customer(db: Session, customer_id: int) -> dict[str, Any]:
"""Stop containers for a customer.""" """Stop containers for a customer."""
deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first() deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first()
customer = db.query(Customer).filter(Customer.id == customer_id).first()
config = get_system_config(db) config = get_system_config(db)
if not deployment or not config: if not deployment or not config or not customer:
return {"success": False, "error": "Deployment or config not found."} return {"success": False, "error": "Deployment, customer or config not found."}
instance_dir = os.path.join(config.data_dir, customer.subdomain) instance_dir = os.path.join(config.data_dir, customer.subdomain)
ok = await docker_service.compose_stop(instance_dir, deployment.container_prefix) ok = await docker_service.compose_stop(instance_dir, deployment.container_prefix)
if ok: if ok:
deployment.deployment_status = "stopped" deployment.deployment_status = "stopped"
customer = db.query(Customer).filter(Customer.id == customer_id).first()
if customer:
customer.status = "inactive" customer.status = "inactive"
db.commit() db.commit()
_log_action(db, customer_id, "stop", "success", "Containers stopped.") _log_action(db, customer_id, "stop", "success", "Containers stopped.")
@@ -505,16 +504,15 @@ async def stop_customer(db: Session, customer_id: int) -> dict[str, Any]:
async def start_customer(db: Session, customer_id: int) -> dict[str, Any]: async def start_customer(db: Session, customer_id: int) -> dict[str, Any]:
"""Start containers for a customer.""" """Start containers for a customer."""
deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first() deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first()
customer = db.query(Customer).filter(Customer.id == customer_id).first()
config = get_system_config(db) config = get_system_config(db)
if not deployment or not config: if not deployment or not config or not customer:
return {"success": False, "error": "Deployment or config not found."} return {"success": False, "error": "Deployment, customer or config not found."}
instance_dir = os.path.join(config.data_dir, customer.subdomain) instance_dir = os.path.join(config.data_dir, customer.subdomain)
ok = await docker_service.compose_start(instance_dir, deployment.container_prefix) ok = await docker_service.compose_start(instance_dir, deployment.container_prefix)
if ok: if ok:
deployment.deployment_status = "running" deployment.deployment_status = "running"
customer = db.query(Customer).filter(Customer.id == customer_id).first()
if customer:
customer.status = "active" customer.status = "active"
db.commit() db.commit()
_log_action(db, customer_id, "start", "success", "Containers started.") _log_action(db, customer_id, "start", "success", "Containers started.")
@@ -526,16 +524,15 @@ async def start_customer(db: Session, customer_id: int) -> dict[str, Any]:
async def restart_customer(db: Session, customer_id: int) -> dict[str, Any]: async def restart_customer(db: Session, customer_id: int) -> dict[str, Any]:
"""Restart containers for a customer.""" """Restart containers for a customer."""
deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first() deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first()
customer = db.query(Customer).filter(Customer.id == customer_id).first()
config = get_system_config(db) config = get_system_config(db)
if not deployment or not config: if not deployment or not config or not customer:
return {"success": False, "error": "Deployment or config not found."} return {"success": False, "error": "Deployment, customer or config not found."}
instance_dir = os.path.join(config.data_dir, customer.subdomain) instance_dir = os.path.join(config.data_dir, customer.subdomain)
ok = await docker_service.compose_restart(instance_dir, deployment.container_prefix) ok = await docker_service.compose_restart(instance_dir, deployment.container_prefix)
if ok: if ok:
deployment.deployment_status = "running" deployment.deployment_status = "running"
customer = db.query(Customer).filter(Customer.id == customer_id).first()
if customer:
customer.status = "active" customer.status = "active"
db.commit() db.commit()
_log_action(db, customer_id, "restart", "success", "Containers restarted.") _log_action(db, customer_id, "restart", "success", "Containers restarted.")

View File

@@ -252,6 +252,16 @@ def trigger_update(config: Any, db_path: str) -> dict:
pull_cmd = ["git", "-C", SOURCE_DIR, "pull", "origin", branch] pull_cmd = ["git", "-C", SOURCE_DIR, "pull", "origin", branch]
# 3. Git pull (synchronous — must complete before rebuild) # 3. Git pull (synchronous — must complete before rebuild)
# Ensure .git directory is owned by the process user (root inside container).
# The .git dir may be owned by the host user after manual operations.
try:
subprocess.run(
["git", "config", "--global", "--add", "safe.directory", SOURCE_DIR],
capture_output=True, timeout=10,
)
except Exception:
pass
try: try:
result = subprocess.run( result = subprocess.run(
pull_cmd, pull_cmd,
@@ -275,6 +285,15 @@ def trigger_update(config: Any, db_path: str) -> dict:
logger.info("git pull succeeded: %s", result.stdout.strip()[:200]) logger.info("git pull succeeded: %s", result.stdout.strip()[:200])
# Fetch tags separately — git pull does not always pull all tags
try:
subprocess.run(
["git", "-C", SOURCE_DIR, "fetch", "--tags"],
capture_output=True, text=True, timeout=30,
)
except Exception as exc:
logger.warning("git fetch --tags failed (non-fatal): %s", exc)
# 4. Read version info from the freshly-pulled source # 4. Read version info from the freshly-pulled source
build_env = os.environ.copy() build_env = os.environ.copy()
try: try: