"""What has to go with a deleted stack, besides its containers and files. Several features keep rows keyed by stack id, and deleting the stack used to leave all of them behind: * a **Git source**: the poll loop and the webhook kept syncing it — which recreated the deleted stack's folder from the repository and, with auto deploy on, started it again; * an **auto-update policy**, which a new stack of the same name then silently inherited; * **backup schedules** naming it, which from then on failed on every run with "stack not found" and paged somebody each time. """ from __future__ import annotations import logging from sqlmodel import Session, select from models.auto_update import AutoUpdate from models.backup_schedule import BackupSchedule, set_stacks, stacks_of from models.git_source import GitSource from services import git_service logger = logging.getLogger("stackpilot.stack_cleanup") def forget_stack(session: Session, stack_id: str) -> dict: """Drop everything that refers to ``stack_id``. The caller deletes the Stack. A schedule that covered only this stack is disabled rather than deleted: it may carry a destination and retention somebody wants to point at another stack. All-stacks schedules need nothing — they resolve the stack list when they run. """ report = {"git": False, "auto_update": 0, "schedules_changed": [], "schedules_disabled": []} source = session.exec(select(GitSource).where(GitSource.stack_id == stack_id)).first() if source is not None: session.delete(source) report["git"] = True for policy in session.exec(select(AutoUpdate).where(AutoUpdate.stack_id == stack_id)).all(): session.delete(policy) report["auto_update"] += 1 for schedule in session.exec(select(BackupSchedule)).all(): if schedule.all_stacks: continue ids = stacks_of(schedule) if stack_id not in ids: continue remaining = [sid for sid in ids if sid != stack_id] set_stacks(schedule, remaining) if remaining: report["schedules_changed"].append(schedule.id) else: schedule.enabled = False schedule.last_status = f"disabled: its stack '{stack_id}' was deleted" report["schedules_disabled"].append(schedule.id) session.add(schedule) session.commit() if report["git"]: git_service.forget(stack_id) if any(report[k] for k in report): logger.info("Cleaned up after deleted stack '%s': %s", stack_id, report) return report def prune_orphans(session: Session) -> list[str]: """Forget every stack that has rows here but no Stack any more. Run at startup, after on-disk stacks have been registered: installs that deleted stacks before this existed still carry their Git sources and policies. Returns the stack ids cleaned up. """ from models.stack import Stack known = {s.id for s in session.exec(select(Stack)).all()} referenced = {g.stack_id for g in session.exec(select(GitSource)).all()} referenced |= {p.stack_id for p in session.exec(select(AutoUpdate)).all()} orphans = sorted(referenced - known) for stack_id in orphans: forget_stack(session, stack_id) return orphans