fix(sync): не считать сервис с одним IP резервированием
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m20s
quality / api (push) Successful in 1m4s
CD / quality (push) Successful in 2m38s
CD / publish (push) Successful in 56s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m20s
quality / api (push) Successful in 1m4s
CD / quality (push) Successful in 2m38s
CD / publish (push) Successful in 56s
В payload для VPS Tracker lbMode не отдаём без пула уникальных IP; в UI показываем без резервирования вместо Round robin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { CfdmBindingSyncItem, LbMode, ServiceBindingView } from "@cfdm/shar
|
|||||||
import { isIpLiteral } from "@cfdm/shared";
|
import { isIpLiteral } from "@cfdm/shared";
|
||||||
import type { Db } from "@cfdm/db";
|
import type { Db } from "@cfdm/db";
|
||||||
import { repos, getAppSettingsSecrets, touchVpsTrackerSync } from "@cfdm/db";
|
import { repos, getAppSettingsSecrets, touchVpsTrackerSync } from "@cfdm/db";
|
||||||
|
import { isSharedPool } from "./routing/pool.js";
|
||||||
|
|
||||||
export function isLbMode(value: unknown): value is LbMode {
|
export function isLbMode(value: unknown): value is LbMode {
|
||||||
return value === "round_robin" || value === "failover" || value === "weighted";
|
return value === "round_robin" || value === "failover" || value === "weighted";
|
||||||
@@ -18,6 +19,16 @@ export function resolveLbModeForSync(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Effective HA only when the service has two or more unique origin IPs. */
|
||||||
|
export function effectiveLbModeForSync(
|
||||||
|
bindingLbMode: string | undefined | null,
|
||||||
|
groupLbMode: string | undefined | null,
|
||||||
|
serviceIps: readonly string[],
|
||||||
|
): LbMode | undefined {
|
||||||
|
if (!isSharedPool(serviceIps)) return undefined;
|
||||||
|
return resolveLbModeForSync(bindingLbMode, groupLbMode);
|
||||||
|
}
|
||||||
|
|
||||||
function groupLbModeForService(
|
function groupLbModeForService(
|
||||||
db: Db,
|
db: Db,
|
||||||
serviceId: number,
|
serviceId: number,
|
||||||
@@ -166,6 +177,7 @@ export async function buildServiceSyncBindingsAsync(
|
|||||||
const items: CfdmBindingSyncItem[] = [];
|
const items: CfdmBindingSyncItem[] = [];
|
||||||
for (const binding of bindings) {
|
for (const binding of bindings) {
|
||||||
const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
|
const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
|
||||||
|
const lbMode = effectiveLbModeForSync(binding.lb_mode, groupLb, serviceIps);
|
||||||
items.push({
|
items.push({
|
||||||
bindingId: binding.id,
|
bindingId: binding.id,
|
||||||
serviceId: service.id,
|
serviceId: service.id,
|
||||||
@@ -176,7 +188,7 @@ export async function buildServiceSyncBindingsAsync(
|
|||||||
hostname: binding.hostname,
|
hostname: binding.hostname,
|
||||||
ips,
|
ips,
|
||||||
cnameTarget: cnameTargetForSync(binding),
|
cnameTarget: cnameTargetForSync(binding),
|
||||||
lbMode: resolveLbModeForSync(binding.lb_mode, groupLb),
|
...(lbMode ? { lbMode } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +226,7 @@ export async function buildAllSyncBindings(
|
|||||||
}
|
}
|
||||||
const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
|
const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
|
||||||
const groupLb = groupLbModeForService(db, binding.service_id, groupLbCache);
|
const groupLb = groupLbModeForService(db, binding.service_id, groupLbCache);
|
||||||
|
const lbMode = effectiveLbModeForSync(binding.lb_mode, groupLb, serviceIps);
|
||||||
items.push({
|
items.push({
|
||||||
bindingId: binding.id,
|
bindingId: binding.id,
|
||||||
serviceId: binding.service_id,
|
serviceId: binding.service_id,
|
||||||
@@ -224,7 +237,7 @@ export async function buildAllSyncBindings(
|
|||||||
hostname: binding.hostname,
|
hostname: binding.hostname,
|
||||||
ips,
|
ips,
|
||||||
cnameTarget: cnameTargetForSync(binding),
|
cnameTarget: cnameTargetForSync(binding),
|
||||||
lbMode: resolveLbModeForSync(binding.lb_mode, groupLb),
|
...(lbMode ? { lbMode } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
resolveBindingIpsForSync,
|
resolveBindingIpsForSync,
|
||||||
resolveLbModeForSync,
|
resolveLbModeForSync,
|
||||||
|
effectiveLbModeForSync,
|
||||||
} from "../src/services/vps-tracker-sync.js";
|
} from "../src/services/vps-tracker-sync.js";
|
||||||
|
|
||||||
function binding(
|
function binding(
|
||||||
@@ -183,6 +184,35 @@ describe("resolveLbModeForSync", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("effectiveLbModeForSync", () => {
|
||||||
|
it("omits mode when unique origin IPs are below two", () => {
|
||||||
|
expect(
|
||||||
|
effectiveLbModeForSync("round_robin", "failover", ["203.0.113.10"]),
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
effectiveLbModeForSync("round_robin", "failover", [
|
||||||
|
"203.0.113.10",
|
||||||
|
"203.0.113.10",
|
||||||
|
]),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits configured mode when the service has a pool", () => {
|
||||||
|
expect(
|
||||||
|
effectiveLbModeForSync("failover", "round_robin", [
|
||||||
|
"203.0.113.10",
|
||||||
|
"203.0.113.20",
|
||||||
|
]),
|
||||||
|
).toBe("failover");
|
||||||
|
expect(
|
||||||
|
effectiveLbModeForSync("off", "weighted", [
|
||||||
|
"203.0.113.10",
|
||||||
|
"198.51.100.1",
|
||||||
|
]),
|
||||||
|
).toBe("weighted");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("cfdmBindingSyncItemSchema lbMode", () => {
|
describe("cfdmBindingSyncItemSchema lbMode", () => {
|
||||||
const base = {
|
const base = {
|
||||||
bindingId: 1,
|
bindingId: 1,
|
||||||
|
|||||||
@@ -75,18 +75,25 @@ function alertDescription(events: { kind: string; fqdns: string[] }[]): string {
|
|||||||
*/
|
*/
|
||||||
export function ServiceFailoverPanel({
|
export function ServiceFailoverPanel({
|
||||||
lbMode = 'round_robin',
|
lbMode = 'round_robin',
|
||||||
|
hasPool = true,
|
||||||
ipHealth,
|
ipHealth,
|
||||||
bindings,
|
bindings,
|
||||||
history,
|
history,
|
||||||
probes = [],
|
probes = [],
|
||||||
}: {
|
}: {
|
||||||
lbMode?: LbMode
|
lbMode?: LbMode
|
||||||
|
hasPool?: boolean
|
||||||
ipHealth: readonly FailoverHealthInput[]
|
ipHealth: readonly FailoverHealthInput[]
|
||||||
bindings: readonly FailoverBindingPool[]
|
bindings: readonly FailoverBindingPool[]
|
||||||
history: readonly FailoverLogEntry[]
|
history: readonly FailoverLogEntry[]
|
||||||
probes?: readonly HealthLogProbe[]
|
probes?: readonly HealthLogProbe[]
|
||||||
}) {
|
}) {
|
||||||
const copy = PANEL_COPY[lbMode] ?? PANEL_COPY.round_robin
|
const copy = hasPool
|
||||||
|
? (PANEL_COPY[lbMode] ?? PANEL_COPY.round_robin)
|
||||||
|
: {
|
||||||
|
title: 'без резервирования',
|
||||||
|
description: 'Один origin IP — балансировка не применяется',
|
||||||
|
}
|
||||||
const liveByIp = latestHealthByIp(probes)
|
const liveByIp = latestHealthByIp(probes)
|
||||||
const overlayHealth = ipHealth.map((row) => {
|
const overlayHealth = ipHealth.map((row) => {
|
||||||
const live = liveByIp.get(row.ip)
|
const live = liveByIp.get(row.ip)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Repeat2Icon,
|
Repeat2Icon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
UnplugIcon,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
ServiceIpList,
|
ServiceIpList,
|
||||||
} from '@/components/services/service-fqdn-list'
|
} from '@/components/services/service-fqdn-list'
|
||||||
import { serviceDisplayFqdn, serviceDisplayFqdns } from '@/lib/service-utils'
|
import { serviceDisplayFqdn, serviceDisplayFqdns } from '@/lib/service-utils'
|
||||||
|
import { uniqueIpCount } from '@/lib/failover-events'
|
||||||
import type { ServiceView } from '@/lib/schemas'
|
import type { ServiceView } from '@/lib/schemas'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import {
|
import {
|
||||||
@@ -75,7 +77,35 @@ const LB_MODE_META: Record<
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LbModeTile({ mode }: { mode: LbMode }) {
|
export function LbModeTile({
|
||||||
|
mode,
|
||||||
|
hasPool = true,
|
||||||
|
}: {
|
||||||
|
mode: LbMode
|
||||||
|
hasPool?: boolean
|
||||||
|
}) {
|
||||||
|
if (!hasPool) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<IconTile
|
||||||
|
variant="elevated"
|
||||||
|
size="xs"
|
||||||
|
className="shrink-0 text-muted-foreground"
|
||||||
|
aria-label="без резервирования"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UnplugIcon aria-hidden="true" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>без резервирования</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const meta = LB_MODE_META[mode]
|
const meta = LB_MODE_META[mode]
|
||||||
const Icon = meta.icon
|
const Icon = meta.icon
|
||||||
|
|
||||||
@@ -148,7 +178,10 @@ export function ServiceUnitCard({
|
|||||||
{service.name}
|
{service.name}
|
||||||
</Link>
|
</Link>
|
||||||
</FrameTitle>
|
</FrameTitle>
|
||||||
<LbModeTile mode={service.lb_mode} />
|
<LbModeTile
|
||||||
|
mode={service.lb_mode}
|
||||||
|
hasPool={uniqueIpCount(service.ips) >= 2}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-1">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
<FrameDescription className="min-w-0 truncate font-mono text-xs">
|
<FrameDescription className="min-w-0 truncate font-mono text-xs">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
ServiceHealthMonitor,
|
ServiceHealthMonitor,
|
||||||
} from '@/components/reui-kit'
|
} from '@/components/reui-kit'
|
||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
import { hasSharedPool } from '@/lib/failover-events'
|
import { hasSharedPool, uniqueIpCount } from '@/lib/failover-events'
|
||||||
import {
|
import {
|
||||||
enabledHealthProviders,
|
enabledHealthProviders,
|
||||||
providerHealthStatuses,
|
providerHealthStatuses,
|
||||||
@@ -268,7 +268,10 @@ function ServiceDetailPage() {
|
|||||||
description="Domain → Service → Node → Health → Failover"
|
description="Domain → Service → Node → Health → Failover"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<LbModeTile mode={service.lb_mode} />
|
<LbModeTile
|
||||||
|
mode={service.lb_mode}
|
||||||
|
hasPool={uniqueIpCount(service.ips) >= 2}
|
||||||
|
/>
|
||||||
<HealthCheckBadge status={displayHealth} />
|
<HealthCheckBadge status={displayHealth} />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
@@ -360,6 +363,7 @@ function ServiceDetailPage() {
|
|||||||
{showPoolPanel ? (
|
{showPoolPanel ? (
|
||||||
<ServiceFailoverPanel
|
<ServiceFailoverPanel
|
||||||
lbMode={service.lb_mode}
|
lbMode={service.lb_mode}
|
||||||
|
hasPool={uniqueIpCount(service.ips) >= 2}
|
||||||
ipHealth={service.ip_health}
|
ipHealth={service.ip_health}
|
||||||
bindings={failoverBindings}
|
bindings={failoverBindings}
|
||||||
history={failoverHistory}
|
history={failoverHistory}
|
||||||
|
|||||||
Reference in New Issue
Block a user