From d1bb6a633e692dc8cf28d03784ca79fa972d29a1 Mon Sep 17 00:00:00 2001 From: twothatIT Date: Tue, 10 Mar 2026 21:34:12 +0100 Subject: [PATCH] feat(deploy): redeploy dialog with keep-data or fresh-deploy option Add a confirmation modal when clicking Redeploy that lets the user choose: - Keep Data: containers are recreated without wiping the instance directory. NetBird database, peer configs, and encryption keys are preserved. - Fresh Deploy: full undeploy (removes all data) then redeploy from scratch. Backend changes: - POST /customers/{id}/deploy accepts keep_data query param (default false) - When keep_data=true, undeploy_customer is skipped entirely - deploy_customer now reuses existing npm_proxy_id/stream_id when the deployment record is still present (avoids duplicate NPM proxy entries) - DNS record creation is skipped on keep_data redeploy (already exists) Frontend changes: - customerAction('deploy') opens the redeploy modal instead of calling API - showRedeployModal(id) shows the two-option confirmation card dialog - confirmRedeploy(keepData) calls the API with the correct parameter - i18n keys added in en.json and de.json Co-Authored-By: Claude Sonnet 4.6 --- app/routers/deployments.py | 19 +++++++++++---- app/services/netbird_service.py | 19 ++++++++++----- static/index.html | 43 +++++++++++++++++++++++++++++++++ static/js/app.js | 26 +++++++++++++++++++- static/lang/de.json | 11 +++++++++ static/lang/en.json | 11 +++++++++ 6 files changed, 117 insertions(+), 12 deletions(-) diff --git a/app/routers/deployments.py b/app/routers/deployments.py index 2f3f072..47a378f 100644 --- a/app/routers/deployments.py +++ b/app/routers/deployments.py @@ -2,7 +2,7 @@ import logging -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.database import SessionLocal, get_db @@ -19,6 +19,14 @@ router = APIRouter() async def manual_deploy( customer_id: int, background_tasks: BackgroundTasks, + keep_data: bool = Query( + False, + description=( + "If True, preserve existing NetBird data (database, keys, peers). " + "Containers are recreated without wiping the instance directory. " + "If False (default), the instance is fully removed and redeployed from scratch." + ), + ), current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): @@ -29,6 +37,7 @@ async def manual_deploy( Args: customer_id: Customer ID. + keep_data: Whether to preserve existing NetBird data. Returns: Acknowledgement dict. @@ -40,12 +49,12 @@ async def manual_deploy( customer.status = "deploying" db.commit() - async def _deploy_bg(cid: int) -> None: + async def _deploy_bg(cid: int, keep: bool) -> None: bg_db = SessionLocal() try: - # Remove existing deployment if present existing = bg_db.query(Deployment).filter(Deployment.customer_id == cid).first() - if existing: + if existing and not keep: + # Full redeploy: remove everything first await netbird_service.undeploy_customer(bg_db, cid) await netbird_service.deploy_customer(bg_db, cid) except Exception: @@ -53,7 +62,7 @@ async def manual_deploy( finally: bg_db.close() - background_tasks.add_task(_deploy_bg, customer_id) + background_tasks.add_task(_deploy_bg, customer_id, keep_data) return {"message": "Deployment started in background.", "status": "deploying"} diff --git a/app/services/netbird_service.py b/app/services/netbird_service.py index a1e8bdf..7f40783 100644 --- a/app/services/netbird_service.py +++ b/app/services/netbird_service.py @@ -264,10 +264,12 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]: _log_action(db, customer_id, "deploy", "info", "Auto-setup failed — admin must complete setup manually.") - # Step 9: Create NPM proxy host (production only) - npm_proxy_id = None - npm_stream_id = None - if not local_mode: + # Step 9: Create NPM proxy host (production only). + # If an existing deployment already has an NPM proxy, reuse it — this happens + # when keep_data=True was passed and undeploy_customer was NOT called beforehand. + npm_proxy_id = existing_deployment.npm_proxy_id if existing_deployment else None + npm_stream_id = existing_deployment.npm_stream_id if existing_deployment else None + if not local_mode and not npm_proxy_id: forward_host = npm_service._get_forward_host() npm_result = await npm_service.create_proxy_host( api_url=config.npm_api_url, @@ -304,9 +306,14 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]: "SSL certificate not created automatically. " "Please create it manually in NPM or ensure DNS resolves and port 80 is reachable, then re-deploy.", ) + elif npm_proxy_id and not local_mode: + _log_action(db, customer_id, "deploy", "info", + f"Reusing existing NPM proxy (ID {npm_proxy_id}) — data preserved.") - # Step 10: Create Windows DNS A-record (non-fatal — failure does not abort deployment) - if config.dns_enabled and config.dns_server and config.dns_zone and config.dns_record_ip: + # Step 10: Create Windows DNS A-record (non-fatal — failure does not abort deployment). + # Skip if an existing deployment is being kept (DNS record already exists). + if config.dns_enabled and config.dns_server and config.dns_zone and config.dns_record_ip \ + and not existing_deployment: try: dns_result = await dns_service.create_dns_record(customer.subdomain, config) if dns_result["ok"]: diff --git a/static/index.html b/static/index.html index 4c236c7..4fbe6c5 100644 --- a/static/index.html +++ b/static/index.html @@ -1267,6 +1267,49 @@ + + +