Fix NPM: reuse existing proxy host and SSL cert on redeployment
When a proxy host already exists in NPM (domain "already in use"), the code now finds the existing host, updates it, and requests SSL instead of failing with an error. Also checks if the host already has a valid certificate before requesting a new one from Let's Encrypt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,29 @@ async def test_npm_connection(api_url: str, email: str, password: str) -> dict[s
|
|||||||
return {"ok": False, "message": f"Unexpected error: {exc}"}
|
return {"ok": False, "message": f"Unexpected error: {exc}"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_proxy_host_by_domain(
|
||||||
|
client: httpx.AsyncClient, api_url: str, headers: dict, domain: str
|
||||||
|
) -> int | None:
|
||||||
|
"""Find an existing proxy host by domain name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: httpx client (already authenticated).
|
||||||
|
api_url: NPM API base URL.
|
||||||
|
headers: Auth headers with Bearer token.
|
||||||
|
domain: Domain to search for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Proxy host ID if found, None otherwise.
|
||||||
|
"""
|
||||||
|
resp = await client.get(f"{api_url}/nginx/proxy-hosts", headers=headers)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
for host in resp.json():
|
||||||
|
domains = host.get("domain_names", [])
|
||||||
|
if domain in domains:
|
||||||
|
return host.get("id")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def create_proxy_host(
|
async def create_proxy_host(
|
||||||
api_url: str,
|
api_url: str,
|
||||||
npm_email: str,
|
npm_email: str,
|
||||||
@@ -182,15 +205,32 @@ async def create_proxy_host(
|
|||||||
proxy_id = data.get("id")
|
proxy_id = data.get("id")
|
||||||
logger.info("Created NPM proxy host %s -> %s:%d (id=%s)",
|
logger.info("Created NPM proxy host %s -> %s:%d (id=%s)",
|
||||||
domain, forward_host, forward_port, proxy_id)
|
domain, forward_host, forward_port, proxy_id)
|
||||||
|
elif resp.status_code == 400 and "already in use" in resp.text:
|
||||||
# Step 2: Request SSL certificate and enable HTTPS
|
# Domain already exists — find and update the existing proxy host
|
||||||
ssl_ok = await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
proxy_id = await _find_proxy_host_by_domain(client, api_url, headers, domain)
|
||||||
|
if not proxy_id:
|
||||||
return {"proxy_id": proxy_id, "ssl": ssl_ok}
|
return {"error": f"Domain {domain} already in use but could not find existing proxy host"}
|
||||||
|
logger.info("Found existing NPM proxy host for %s (id=%s), updating...", domain, proxy_id)
|
||||||
|
update_resp = await client.put(
|
||||||
|
f"{api_url}/nginx/proxy-hosts/{proxy_id}",
|
||||||
|
json=payload,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if update_resp.status_code in (200, 201):
|
||||||
|
logger.info("Updated NPM proxy host %s -> %s:%d (id=%s)",
|
||||||
|
domain, forward_host, forward_port, proxy_id)
|
||||||
|
else:
|
||||||
|
logger.warning("Could not update proxy host %s: HTTP %s — %s",
|
||||||
|
proxy_id, update_resp.status_code, update_resp.text[:300])
|
||||||
else:
|
else:
|
||||||
error_msg = f"NPM returned {resp.status_code}: {resp.text[:300]}"
|
error_msg = f"NPM returned {resp.status_code}: {resp.text[:300]}"
|
||||||
logger.error("Failed to create proxy host: %s", error_msg)
|
logger.error("Failed to create proxy host: %s", error_msg)
|
||||||
return {"error": error_msg}
|
return {"error": error_msg}
|
||||||
|
|
||||||
|
# Step 2: Request SSL certificate and enable HTTPS
|
||||||
|
ssl_ok = await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
||||||
|
|
||||||
|
return {"proxy_id": proxy_id, "ssl": ssl_ok}
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
logger.error("NPM login failed: %s", exc)
|
logger.error("NPM login failed: %s", exc)
|
||||||
return {"error": f"NPM login failed: {exc}"}
|
return {"error": f"NPM login failed: {exc}"}
|
||||||
@@ -229,6 +269,32 @@ async def _request_ssl(
|
|||||||
logger.warning("No admin email set — skipping SSL certificate for %s", domain)
|
logger.warning("No admin email set — skipping SSL certificate for %s", domain)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check if proxy host already has a valid certificate
|
||||||
|
try:
|
||||||
|
host_resp = await client.get(f"{api_url}/nginx/proxy-hosts/{proxy_id}", headers=headers)
|
||||||
|
if host_resp.status_code == 200:
|
||||||
|
host_data = host_resp.json()
|
||||||
|
existing_cert_id = host_data.get("certificate_id", 0)
|
||||||
|
if existing_cert_id and existing_cert_id > 0:
|
||||||
|
logger.info("Proxy host %s already has certificate (id=%s), ensuring SSL is enabled",
|
||||||
|
proxy_id, existing_cert_id)
|
||||||
|
ssl_update = {
|
||||||
|
"certificate_id": existing_cert_id,
|
||||||
|
"ssl_forced": True,
|
||||||
|
"hsts_enabled": True,
|
||||||
|
"http2_support": True,
|
||||||
|
}
|
||||||
|
update_resp = await client.put(
|
||||||
|
f"{api_url}/nginx/proxy-hosts/{proxy_id}",
|
||||||
|
json=ssl_update,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if update_resp.status_code in (200, 201):
|
||||||
|
logger.info("SSL enabled on existing proxy host %s (cert_id=%s)", proxy_id, existing_cert_id)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Could not check existing cert for proxy host %s: %s", proxy_id, exc)
|
||||||
|
|
||||||
ssl_payload = {
|
ssl_payload = {
|
||||||
"domain_names": [domain],
|
"domain_names": [domain],
|
||||||
"provider": "letsencrypt",
|
"provider": "letsencrypt",
|
||||||
|
|||||||
Reference in New Issue
Block a user