feat: add node-scoped opt-out for image update detection (#1715)

* feat: add node-scoped opt-out for image update detection

Operators who use an external update authority can disable Sencho registry
polling per node without losing explicit stack Update, pull, or redeploy.

* test: fix mocks and lint for image-update checks opt-out

Scheduler tests need isChecksEnabled on the ImageUpdateService mock, and the UpdatesSection older-node fixture must not leave an unused binding.

* fix: gate update-preview and recheck when detection is off

Anatomy was still calling stack update-preview (and contacting registries)
while checks were disabled. Short-circuit those routes and skip recheckStack
writes so disabled nodes stay quiet until detection is re-enabled.
This commit is contained in:
Anso
2026-07-28 10:10:04 -04:00
committed by GitHub
parent e175db8e62
commit fa503ddf27
23 changed files with 722 additions and 80 deletions
+16 -1
View File
@@ -17,7 +17,11 @@ import { StackUpdateOrchestrator, shortImageId, type OrchestratorResult } from '
import DockerController, { type BulkStackInfo } from '../services/DockerController';
import { DatabaseService, type StackDossierFields } from '../services/DatabaseService';
import { CacheService, type CacheFetchOutcome } from '../services/CacheService';
import { UpdatePreviewService, isAuthoritativeNegativePreview } from '../services/UpdatePreviewService';
import {
UpdatePreviewService,
isAuthoritativeNegativePreview,
buildDetectionDisabledPreview,
} from '../services/UpdatePreviewService';
import { GitSourceService, GitSourceError, repoHost as gitRepoHost } from '../services/GitSourceService';
import { enforcePolicyPreDeploy } from '../services/PolicyEnforcement';
import { buildStackDriftReport, type DriftFindingKind, type StackDriftReport } from '../services/DriftDetectionService';
@@ -2248,6 +2252,12 @@ stacksRouter.post('/:stackName/services/:serviceName/restore', async (req: Reque
stacksRouter.get('/:stackName/update-preview', async (req: Request, res: Response) => {
const stackName = req.params.stackName as string;
try {
// Anatomy and other GET consumers must not contact registries while
// node-scoped detection is off.
if (!ImageUpdateService.isChecksEnabled()) {
res.json(buildDetectionDisabledPreview(stackName));
return;
}
// Read-only: sticky reconciliation lives on POST so UpdateGuard and other
// GET consumers never mutate persisted scanner state.
const preview = await UpdatePreviewService.getInstance().getPreview(req.nodeId, stackName);
@@ -2261,6 +2271,11 @@ stacksRouter.get('/:stackName/update-preview', async (req: Request, res: Respons
stacksRouter.post('/:stackName/update-preview', async (req: Request, res: Response) => {
const stackName = req.params.stackName as string;
try {
if (!ImageUpdateService.isChecksEnabled()) {
// No registry I/O and no sticky reconcile on a synthetic disabled preview.
res.json({ ...buildDetectionDisabledPreview(stackName), reconciled: false });
return;
}
// Snapshot write-generation watermarks before the read-only preview so a
// later clear can erase older confirmed/sticky rows without racing a
// scanner that reserved or rewrote the row after this observation.