Files
portainer/app/react/portainer/settings/SettingsView/AutoHealPanel/validation.ts
T
claude code agent be3bfd0513 fix(automation): maintainer pre-merge review — stale detection, daemon edge cases, parity (F1-F9)
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>
2026-06-29 19:51:15 +03:00

73 lines
2.2 KiB
TypeScript

import { SchemaOf, boolean, mixed, object, string } from 'yup';
import { AutoHealScope } from '../../types';
import { Values } from './types';
// Matches Go's time.ParseDuration units (e.g. "30s", "1m30s", "2h").
const durationPattern = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;
// Lower bound for the check interval, kept in sync with the backend
// (minAutoHealCheckInterval). A near-zero interval would hammer Docker with
// list/inspect calls for no benefit, and the backend rejects it (400), so the
// front-end must reject it too rather than letting a sub-second value through.
const minCheckIntervalSeconds = 1;
// 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),
checkInterval: string()
.required('Check interval is required')
.matches(durationPattern, 'Must be a valid duration (e.g. 30s, 1m, 2h)')
.test(
'min-check-interval',
'Check interval must be at least 1s',
(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 >= minCheckIntervalSeconds;
}
),
scope: mixed<AutoHealScope>()
.oneOf(['labeled', 'all'])
.required('Scope is required'),
});
}