fix(services): вернуть вес и приоритет в режиме балансировки
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
CD / quality (push) Successful in 1m5s
CD / publish (push) Successful in 1m43s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
CD / quality (push) Successful in 1m5s
CD / publish (push) Successful in 1m43s
После объединения адресов в форме пропали per-IP поля. Weighted снова задаёт вес, failover — основной и запасной. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@cfdm/ui/components/select'
|
} from '@cfdm/ui/components/select'
|
||||||
import { Switch } from '@cfdm/ui/components/switch'
|
import { Switch } from '@cfdm/ui/components/switch'
|
||||||
import { FieldGroup } from '@cfdm/ui/components/field'
|
import { Field, FieldGroup, FieldLabel } from '@cfdm/ui/components/field'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import { ButtonGroup } from '@cfdm/ui/components/button-group'
|
import { ButtonGroup } from '@cfdm/ui/components/button-group'
|
||||||
import { CableIcon, GlobeIcon } from 'lucide-react'
|
import { CableIcon, GlobeIcon } from 'lucide-react'
|
||||||
@@ -62,6 +62,11 @@ const defaultLbModeOptions = [
|
|||||||
{ value: 'weighted', label: 'Weighted (веса)' },
|
{ value: 'weighted', label: 'Weighted (веса)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export interface LbPoolMetaChange {
|
||||||
|
weight?: number
|
||||||
|
priority?: number
|
||||||
|
}
|
||||||
|
|
||||||
function CompactNumberField({
|
function CompactNumberField({
|
||||||
id,
|
id,
|
||||||
value,
|
value,
|
||||||
@@ -95,6 +100,89 @@ function CompactNumberField({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PoolLbMetaFields({
|
||||||
|
idPrefix,
|
||||||
|
mode,
|
||||||
|
ips,
|
||||||
|
weights,
|
||||||
|
priorities,
|
||||||
|
onMetaChange,
|
||||||
|
}: {
|
||||||
|
idPrefix: string
|
||||||
|
mode: Exclude<LbMode, 'round_robin'>
|
||||||
|
ips: readonly string[]
|
||||||
|
weights: Record<string, number>
|
||||||
|
priorities: Record<string, number>
|
||||||
|
onMetaChange?: (ip: string, meta: LbPoolMetaChange) => void
|
||||||
|
}) {
|
||||||
|
const isWeighted = mode === 'weighted'
|
||||||
|
const minPriority =
|
||||||
|
ips.length === 0
|
||||||
|
? 1
|
||||||
|
: Math.min(...ips.map((ip) => priorities[ip] ?? 1))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingRow
|
||||||
|
title={isWeighted ? 'Вес IP' : 'Приоритет IP'}
|
||||||
|
description={
|
||||||
|
isWeighted
|
||||||
|
? 'Больше — чаще в пуле'
|
||||||
|
: '1 — основной, больше — запасной'
|
||||||
|
}
|
||||||
|
compact
|
||||||
|
stacked
|
||||||
|
className="gap-3 px-0 py-3"
|
||||||
|
contentClassName="min-w-0"
|
||||||
|
>
|
||||||
|
{ips.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">Сначала добавьте IP выше</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
{ips.map((ip) => {
|
||||||
|
const isPrimary = (priorities[ip] ?? 1) === minPriority
|
||||||
|
const fieldId = isWeighted
|
||||||
|
? `${idPrefix}-weight-${ip}`
|
||||||
|
: `${idPrefix}-priority-${ip}`
|
||||||
|
return (
|
||||||
|
<div key={ip} className="flex items-center gap-3">
|
||||||
|
<span className="min-w-0 flex-1 truncate font-mono text-sm">{ip}</span>
|
||||||
|
{!isWeighted ? (
|
||||||
|
<Badge
|
||||||
|
variant={isPrimary ? 'success-light' : 'outline'}
|
||||||
|
size="xs"
|
||||||
|
radius="full"
|
||||||
|
>
|
||||||
|
{isPrimary ? 'Основной' : 'Запасной'}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
<Field className="w-28 gap-0">
|
||||||
|
<FieldLabel htmlFor={fieldId} className="sr-only">
|
||||||
|
{isWeighted ? `Вес ${ip}` : `Приоритет ${ip}`}
|
||||||
|
</FieldLabel>
|
||||||
|
<CompactNumberField
|
||||||
|
id={fieldId}
|
||||||
|
value={isWeighted ? (weights[ip] ?? 1) : (priorities[ip] ?? 1)}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
onValueChange={(next) =>
|
||||||
|
onMetaChange?.(
|
||||||
|
ip,
|
||||||
|
isWeighted
|
||||||
|
? { weight: next ?? 1 }
|
||||||
|
: { priority: next ?? 1 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SettingRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function HealthCheckConfigFields({
|
export function HealthCheckConfigFields({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -102,6 +190,10 @@ export function HealthCheckConfigFields({
|
|||||||
lbModeOptions = defaultLbModeOptions,
|
lbModeOptions = defaultLbModeOptions,
|
||||||
idPrefix = 'health',
|
idPrefix = 'health',
|
||||||
showLbMode = true,
|
showLbMode = true,
|
||||||
|
ips = [],
|
||||||
|
weights = {},
|
||||||
|
priorities = {},
|
||||||
|
onMetaChange,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
value: LbAndHealthConfig
|
value: LbAndHealthConfig
|
||||||
@@ -110,6 +202,10 @@ export function HealthCheckConfigFields({
|
|||||||
lbModeOptions?: { value: string; label: string }[]
|
lbModeOptions?: { value: string; label: string }[]
|
||||||
idPrefix?: string
|
idPrefix?: string
|
||||||
showLbMode?: boolean
|
showLbMode?: boolean
|
||||||
|
ips?: readonly string[]
|
||||||
|
weights?: Record<string, number>
|
||||||
|
priorities?: Record<string, number>
|
||||||
|
onMetaChange?: (ip: string, meta: LbPoolMetaChange) => void
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
function patch(next: Partial<LbAndHealthConfig>) {
|
function patch(next: Partial<LbAndHealthConfig>) {
|
||||||
@@ -153,6 +249,17 @@ export function HealthCheckConfigFields({
|
|||||||
</SettingRow>
|
</SettingRow>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showLbMode && value.lb_mode !== 'round_robin' ? (
|
||||||
|
<PoolLbMetaFields
|
||||||
|
idPrefix={idPrefix}
|
||||||
|
mode={value.lb_mode}
|
||||||
|
ips={ips}
|
||||||
|
weights={weights}
|
||||||
|
priorities={priorities}
|
||||||
|
onMetaChange={onMetaChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
title="Провайдер health-check"
|
title="Провайдер health-check"
|
||||||
description="Кто пробирует цель. Можно выбрать несколько источников."
|
description="Кто пробирует цель. Можно выбрать несколько источников."
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
DEFAULT_BINDING_HEALTH,
|
DEFAULT_BINDING_HEALTH,
|
||||||
emptyAddressBlock,
|
emptyAddressBlock,
|
||||||
hydrateAddressBlock,
|
hydrateAddressBlock,
|
||||||
|
patchAddressIpMeta,
|
||||||
toBindingDrafts,
|
toBindingDrafts,
|
||||||
toDomainsPayload,
|
toDomainsPayload,
|
||||||
type AddressBlockState,
|
type AddressBlockState,
|
||||||
@@ -282,6 +283,12 @@ export function ServiceEditSheet({
|
|||||||
idPrefix="service-health"
|
idPrefix="service-health"
|
||||||
value={primaryHealthValue}
|
value={primaryHealthValue}
|
||||||
onChange={handlePrimaryHealthChange}
|
onChange={handlePrimaryHealthChange}
|
||||||
|
ips={address.nodes.map((node) => node.ip)}
|
||||||
|
weights={address.target_ip_weights}
|
||||||
|
priorities={address.target_ip_priorities}
|
||||||
|
onMetaChange={(ip, meta) =>
|
||||||
|
setAddress((current) => patchAddressIpMeta(current, ip, meta))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
emptyAddressBlock,
|
emptyAddressBlock,
|
||||||
emptyBindingDraft,
|
emptyBindingDraft,
|
||||||
hydrateAddressBlock,
|
hydrateAddressBlock,
|
||||||
|
patchAddressIpMeta,
|
||||||
removeAddressNode,
|
removeAddressNode,
|
||||||
toAddressBindings,
|
toAddressBindings,
|
||||||
toDomainsPayload,
|
toDomainsPayload,
|
||||||
@@ -82,6 +83,26 @@ describe('hydrateAddressBlock', () => {
|
|||||||
expect(state.preservedBindings).toHaveLength(1)
|
expect(state.preservedBindings).toHaveLength(1)
|
||||||
expect(state.preservedBindings[0]?.fqdn).toBe('edge.example.com')
|
expect(state.preservedBindings[0]?.fqdn).toBe('edge.example.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('поднимает веса и приоритеты с общего FQDN', () => {
|
||||||
|
const drafts = [
|
||||||
|
aRecord('gt.rkns.top', ['130.49.213.153', '93.115.203.183'], {
|
||||||
|
target_ip_weights: { '130.49.213.153': 3, '93.115.203.183': 1 },
|
||||||
|
target_ip_priorities: { '130.49.213.153': 2, '93.115.203.183': 1 },
|
||||||
|
}),
|
||||||
|
aRecord('nsgt.rkns.top', ['130.49.213.153']),
|
||||||
|
]
|
||||||
|
const state = hydrateAddressBlock(drafts, ['130.49.213.153', '93.115.203.183'])
|
||||||
|
expect(state.target_ip_weights).toEqual({
|
||||||
|
'130.49.213.153': 3,
|
||||||
|
'93.115.203.183': 1,
|
||||||
|
})
|
||||||
|
expect(state.target_ip_priorities).toEqual({
|
||||||
|
'130.49.213.153': 2,
|
||||||
|
'93.115.203.183': 1,
|
||||||
|
})
|
||||||
|
expect(state.nodes[0]?.extraFqdn).toBe('nsgt.rkns.top')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('toDomainsPayload', () => {
|
describe('toDomainsPayload', () => {
|
||||||
@@ -161,6 +182,35 @@ describe('addAddressNode / addCommonFqdn', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('patchAddressIpMeta', () => {
|
||||||
|
it('меняет вес одного IP и не трогает extraFqdn', () => {
|
||||||
|
const state = {
|
||||||
|
...addAddressNode(addAddressNode(emptyAddressBlock(), '1.1.1.1'), '2.2.2.2'),
|
||||||
|
nodes: [
|
||||||
|
{ ip: '1.1.1.1', extraFqdn: 'msk.example.com' },
|
||||||
|
{ ip: '2.2.2.2', extraFqdn: '' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const next = patchAddressIpMeta(state, '1.1.1.1', { weight: 7 })
|
||||||
|
expect(next.target_ip_weights).toEqual({ '1.1.1.1': 7, '2.2.2.2': 1 })
|
||||||
|
expect(next.target_ip_priorities).toEqual(state.target_ip_priorities)
|
||||||
|
expect(next.nodes).toEqual(state.nodes)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamp веса и приоритета в 1–100', () => {
|
||||||
|
const state = addAddressNode(emptyAddressBlock(), '10.0.0.1')
|
||||||
|
expect(patchAddressIpMeta(state, '10.0.0.1', { weight: 0 }).target_ip_weights['10.0.0.1']).toBe(1)
|
||||||
|
expect(
|
||||||
|
patchAddressIpMeta(state, '10.0.0.1', { priority: 999 }).target_ip_priorities['10.0.0.1'],
|
||||||
|
).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('игнорирует IP вне пула', () => {
|
||||||
|
const state = addAddressNode(emptyAddressBlock(), '10.0.0.1')
|
||||||
|
expect(patchAddressIpMeta(state, '8.8.8.8', { weight: 5 })).toBe(state)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('CNAME / preservedBindings', () => {
|
describe('CNAME / preservedBindings', () => {
|
||||||
it('сохраняет CNAME в preserved при круге hydrate → payload', () => {
|
it('сохраняет CNAME в preserved при круге hydrate → payload', () => {
|
||||||
const cname: ServiceBindingDraft = {
|
const cname: ServiceBindingDraft = {
|
||||||
|
|||||||
@@ -243,6 +243,33 @@ export function addAddressNode(state: AddressBlockState, ip: string): AddressBlo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LB_META_MIN = 1
|
||||||
|
const LB_META_MAX = 100
|
||||||
|
|
||||||
|
function clampLbMeta(value: number): number {
|
||||||
|
if (!Number.isFinite(value)) return LB_META_MIN
|
||||||
|
return Math.min(LB_META_MAX, Math.max(LB_META_MIN, Math.round(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchAddressIpMeta(
|
||||||
|
state: AddressBlockState,
|
||||||
|
ip: string,
|
||||||
|
meta: { weight?: number; priority?: number },
|
||||||
|
): AddressBlockState {
|
||||||
|
if (!state.nodes.some((node) => node.ip === ip)) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
target_ip_weights:
|
||||||
|
meta.weight === undefined
|
||||||
|
? state.target_ip_weights
|
||||||
|
: { ...state.target_ip_weights, [ip]: clampLbMeta(meta.weight) },
|
||||||
|
target_ip_priorities:
|
||||||
|
meta.priority === undefined
|
||||||
|
? state.target_ip_priorities
|
||||||
|
: { ...state.target_ip_priorities, [ip]: clampLbMeta(meta.priority) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fqdnKey(value: string): string {
|
function fqdnKey(value: string): string {
|
||||||
return value.trim().toLowerCase()
|
return value.trim().toLowerCase()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user