be3bfd0513
F1: cap the image-status cache TTL at 5m (was 24h) — the cache is keyed by the
LOCAL imageID, which doesn't change when upstream pushes a new image under the
same tag, so the 24h TTL hid new images from both the badge and the auto-update
daemon; a short TTL re-resolves the remote digest within the poll window.
F2: document that the update->rollback guard map is in-memory (restart implication).
F3: skip auto-update for an unnamed container when rollback is on (the endpoint+name
keyed guard can't record it, so it would loop) — pure skipUnnamedForRollback + test.
F4: wrap the pre-update ContainerInspect in context.WithTimeout(endpointTimeout).
F5: document Reload() does not interrupt an in-flight tick.
F6: floor auto-heal CheckInterval at 1s (mirrors auto-update) + test.
F7: wontfix — migration is currently correct; namespace rework is out of scope.
F8: correct the misleading SSRF/AllowList comment (no filter is applied).
F9: front auto-heal interval floor + test; dedup STALE_TIME; fix invalidation comment.
Also refresh three stale '24h/long-lived cache' comments to match the 5m TTL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { SchemaOf, boolean, mixed, object, string } from 'yup';
|
|
|
|
import { AutoUpdateScope } from '../../types';
|
|
|
|
import { Values } from './types';
|
|
|
|
// Matches Go's time.ParseDuration units (e.g. "6h", "30m", "1h30m").
|
|
const durationPattern = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
|
|
|
|
// Lower bound for the poll interval, kept in sync with the backend
|
|
// (minAutoUpdatePollInterval). Polling more often than this only adds registry
|
|
// load: the image-status cache (~5m) bounds detection latency, so a sub-minute
|
|
// interval is wasteful.
|
|
const minPollIntervalSeconds = 60;
|
|
|
|
// Lower bound for the rollback timeout, kept in sync with the backend
|
|
// (minAutoUpdateRollbackTimeout). A near-zero timeout would roll back before any
|
|
// container can pass its healthcheck, defeating the gate.
|
|
const minRollbackTimeoutSeconds = 10;
|
|
|
|
// Seconds per Go duration unit, used to evaluate the configured interval against
|
|
// the floor. Mirrors the units accepted by durationPattern.
|
|
const unitSeconds: Record<string, number> = {
|
|
ns: 1e-9,
|
|
us: 1e-6,
|
|
µs: 1e-6,
|
|
ms: 1e-3,
|
|
s: 1,
|
|
m: 60,
|
|
h: 3600,
|
|
};
|
|
|
|
// parseGoDurationSeconds converts a Go-style duration (e.g. "1h30m") to seconds,
|
|
// or returns null when the string is not a well-formed duration.
|
|
function parseGoDurationSeconds(value: string): number | null {
|
|
if (!durationPattern.test(value)) {
|
|
return null;
|
|
}
|
|
|
|
let total = 0;
|
|
const componentPattern = /(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g;
|
|
let match = componentPattern.exec(value);
|
|
while (match !== null) {
|
|
total += parseFloat(match[1]) * unitSeconds[match[2]];
|
|
match = componentPattern.exec(value);
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
export function validation(): SchemaOf<Values> {
|
|
return object({
|
|
enabled: boolean().default(false),
|
|
pollInterval: string()
|
|
.required('Poll interval is required')
|
|
.matches(durationPattern, 'Must be a valid duration (e.g. 6h, 30m, 1h30m)')
|
|
.test(
|
|
'min-poll-interval',
|
|
'Poll interval must be at least 1m',
|
|
(value) => {
|
|
if (!value) {
|
|
return true; // let required/matches report the error first
|
|
}
|
|
|
|
const seconds = parseGoDurationSeconds(value);
|
|
if (seconds === null) {
|
|
return true; // let matches report the format error first
|
|
}
|
|
|
|
return seconds >= minPollIntervalSeconds;
|
|
}
|
|
),
|
|
scope: mixed<AutoUpdateScope>()
|
|
.oneOf(['labeled', 'all'])
|
|
.required('Scope is required'),
|
|
cleanup: boolean().default(false),
|
|
rollbackOnFailure: boolean().default(false),
|
|
rollbackTimeout: string()
|
|
.required('Rollback timeout is required')
|
|
.matches(durationPattern, 'Must be a valid duration (e.g. 120s, 2m)')
|
|
.test(
|
|
'min-rollback-timeout',
|
|
'Rollback timeout must be at least 10s',
|
|
(value) => {
|
|
if (!value) {
|
|
return true; // let required/matches report the error first
|
|
}
|
|
|
|
const seconds = parseGoDurationSeconds(value);
|
|
if (seconds === null) {
|
|
return true; // let matches report the format error first
|
|
}
|
|
|
|
return seconds >= minRollbackTimeoutSeconds;
|
|
}
|
|
),
|
|
});
|
|
}
|