install: preflight + teardown tooling + CCP tunnel cleanup on delete

Fixes surfaced by three rounds of fresh-install testing on marcelle:

- config.sh: add host-port preflight check (ss -tln) to catch
  cockpit-on-9090 style collisions before compose up; add
  --skip-port-check escape hatch; add --install-watcher /
  --no-install-watcher / --install-backup-timer /
  --no-install-backup-timer flags; -y --enable-all now installs both
  systemd units by default (previously silently skipped); print
  resolved admin email in Configuration Complete block.

- scripts/validate-env.sh: new section 5b "Host Port Availability"
  using ss-based detection, with process-name surfacing when run as
  root.

- scripts/pangolin-teardown.sh: new wrapper. Reads credentials from
  .env or takes --api-url/--api-key/--org-id flags. Dry-run by
  default; --yes to execute. Deletes resources before sites (avoids
  orphans). --keep-site-ids for safety.

- scripts/build-release.sh: include validate-env.sh and
  pangolin-teardown.sh in release tarball whitelist.

- CCP instances.service.ts: deleteInstance() now calls
  teardownTunnel() before composeDown when pangolinSiteId is set.
  Previously an admin clicking "Delete Instance" orphaned the
  Pangolin site + all its resources. Best-effort with try/catch
  matching the existing Docker-cleanup tolerance pattern.

- CLAUDE.md: sync drift — 44 → 50 migrations, 186 → 192 models,
  40 → 44 modules.

Bunker Admin
This commit is contained in:
2026-04-16 12:50:48 -06:00
parent 13513aeca5
commit f9d566bd84
6 changed files with 315 additions and 22 deletions

View File

@@ -12,6 +12,7 @@ import { getDriverForInstance, AgentUnreachableError } from '../../services/exec
import { provision } from './provisioner';
import { CreateInstanceInput, UpdateInstanceInput, RegisterInstanceInput, ReconfigureInstanceInput, ConfigureTunnelInput } from './instances.schemas';
import { buildTemplateContext, renderAllTemplates, clearTemplateCache } from '../../services/template-engine';
import { teardownTunnel } from '../../services/tunnel.service';
import { logger } from '../../utils/logger';
import path from 'path';
@@ -283,6 +284,18 @@ export async function deleteInstance(id: string, userId: string, ipAddress?: str
data: { status: InstanceStatus.DESTROYING, statusMessage: 'Shutting down containers...' },
});
// Tear down Pangolin site + resources first. If we crashed after composeDown
// but before this, the Pangolin entities would leak for the lifetime of the org.
// Best effort — matches the Docker-cleanup tolerance below.
if (instance.pangolinSiteId) {
try {
await teardownTunnel(id, userId, ipAddress ?? null);
logger.info(`[instances] ${instance.slug}: Pangolin tunnel torn down`);
} catch (err) {
logger.warn(`[instances] ${instance.slug}: Pangolin teardown warning: ${(err as Error).message}`);
}
}
// Stop containers and remove volumes
try {
const driver = await getDriverForInstance(instance);