fix(auto-update): paid-gate execute route and harden image-check watchdog (#1257)

* fix(auto-update): paid-gate execute route and harden image-check watchdog

Auto-update execution is a paid capability, but POST /api/auto-update/execute
was reachable by any admin regardless of license. Add the paid guard so it
matches the rest of the surface (scheduled-task management and fleet refresh).
The scheduler dispatch to remote nodes still works because the controlling
instance forwards its tier with the request.

Restrict GET /api/image-updates/fleet to admins. The single-node status
endpoint that drives the sidebar update dot stays open to all roles.

Replace the image-check watchdog timer that released the run lock after five
minutes. On a healthy but slow scan it let a manual refresh start a second
concurrent check, duplicating notifications and racing the status writes. The
scan now owns its lock for its full duration, and every Docker socket and
filesystem read is bounded so a wedged daemon or mount cannot stall a scan
forever.

* test(auto-update): assert the debug skip-log branch in the image-check guard

The concurrency-guard test covered the warn branch for a trigger arriving past
the long-run threshold but never exercised the developer-mode debug skip log.
Add a case that enables developer mode and asserts the debug line fires for a
mid-scan trigger under the threshold.
This commit is contained in:
Anso
2026-05-31 16:00:00 -04:00
committed by GitHub
parent 7d4e61625f
commit ca346916c1
4 changed files with 248 additions and 38 deletions
+3 -1
View File
@@ -52,7 +52,8 @@ imageUpdatesRouter.get('/status', authMiddleware, (_req: Request, res: Response)
res.json({ checking: ImageUpdateService.getInstance().isChecking() });
});
imageUpdatesRouter.get('/fleet', authMiddleware, async (_req: Request, res: Response): Promise<void> => {
imageUpdatesRouter.get('/fleet', authMiddleware, async (req: Request, res: Response): Promise<void> => {
if (!requireAdmin(req, res)) return;
try {
const result = await CacheService.getInstance().getOrFetch<Record<number, Record<string, boolean>>>(
FLEET_UPDATE_CACHE_KEY,
@@ -197,6 +198,7 @@ export const autoUpdateRouter = Router();
autoUpdateRouter.post('/execute', authMiddleware, async (req: Request, res: Response): Promise<void> => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
try {
const { target } = req.body as { target?: string };
console.log(`[AutoUpdate] Execute requested: target="${sanitizeForLog(target || '')}"`);