fix(services): разрешить несколько доп. FQDN на IP включая wildcard
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 10s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m16s
quality / api (push) Successful in 1m14s
CD / quality (push) Successful in 2m44s
CD / publish (push) Successful in 1m50s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 10s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m16s
quality / api (push) Successful in 1m14s
CD / quality (push) Successful in 2m44s
CD / publish (push) Successful in 1m50s
У IP был один extra FQDN; wildcard вида *.mdns.shnt.top не матчился с именем из Cloudflare. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -16,10 +16,13 @@ import { parseFqdn } from '@/lib/parse-fqdn'
|
|||||||
import {
|
import {
|
||||||
addAddressNode,
|
addAddressNode,
|
||||||
addCommonFqdn,
|
addCommonFqdn,
|
||||||
|
addExtraFqdn,
|
||||||
addressHasFqdn,
|
addressHasFqdn,
|
||||||
removeAddressNode,
|
removeAddressNode,
|
||||||
removeCommonFqdn,
|
removeCommonFqdn,
|
||||||
|
removeExtraFqdn,
|
||||||
updateCommonFqdn,
|
updateCommonFqdn,
|
||||||
|
updateExtraFqdn,
|
||||||
type AddressBlockState,
|
type AddressBlockState,
|
||||||
} from '@/lib/service-address'
|
} from '@/lib/service-address'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
@@ -67,7 +70,7 @@ function ZoneAddon({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Единый блок адресов сервиса: список общих FQDN на весь пул + IP с доп. доменом.
|
* Единый блок адресов сервиса: список общих FQDN на весь пул + IP с доп. доменами.
|
||||||
* Preview: https://reui.io/preview/base/settings-3
|
* Preview: https://reui.io/preview/base/settings-3
|
||||||
* Preview: https://reui.io/preview/base/list-9
|
* Preview: https://reui.io/preview/base/list-9
|
||||||
* Preview: https://reui.io/preview/base/form-7
|
* Preview: https://reui.io/preview/base/form-7
|
||||||
@@ -88,6 +91,8 @@ export function ServiceAddressBlock({
|
|||||||
const [ipInvalid, setIpInvalid] = useState(false)
|
const [ipInvalid, setIpInvalid] = useState(false)
|
||||||
const [pendingFqdn, setPendingFqdn] = useState('')
|
const [pendingFqdn, setPendingFqdn] = useState('')
|
||||||
const [fqdnInvalid, setFqdnInvalid] = useState(false)
|
const [fqdnInvalid, setFqdnInvalid] = useState(false)
|
||||||
|
const [pendingExtraByIp, setPendingExtraByIp] = useState<Record<string, string>>({})
|
||||||
|
const [extraInvalidByIp, setExtraInvalidByIp] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
const pool = value.nodes.map((node) => node.ip)
|
const pool = value.nodes.map((node) => node.ip)
|
||||||
const pendingIpTrimmed = pendingIp.trim()
|
const pendingIpTrimmed = pendingIp.trim()
|
||||||
@@ -141,13 +146,26 @@ export function ServiceAddressBlock({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNodeFqdn(ip: string, extraFqdn: string) {
|
function tryAddExtra(ip: string, raw: string) {
|
||||||
onChange({
|
const trimmed = raw.trim()
|
||||||
...value,
|
if (!trimmed) {
|
||||||
nodes: value.nodes.map((node) =>
|
setExtraInvalidByIp((current) => ({ ...current, [ip]: false }))
|
||||||
node.ip === ip ? { ...node, extraFqdn } : node,
|
return
|
||||||
),
|
}
|
||||||
})
|
if (addressHasFqdn(value, trimmed)) {
|
||||||
|
setExtraInvalidByIp((current) => ({ ...current, [ip]: true }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onChange(addExtraFqdn(value, ip, trimmed))
|
||||||
|
setPendingExtraByIp((current) => ({ ...current, [ip]: '' }))
|
||||||
|
setExtraInvalidByIp((current) => ({ ...current, [ip]: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExtraKeyDown(ip: string, event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
tryAddExtra(ip, pendingExtraByIp[ip] ?? '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -156,7 +174,7 @@ export function ServiceAddressBlock({
|
|||||||
<FrameHeader className="px-0 pt-0">
|
<FrameHeader className="px-0 pt-0">
|
||||||
<FrameTitle>Адреса</FrameTitle>
|
<FrameTitle>Адреса</FrameTitle>
|
||||||
<FrameDescription>
|
<FrameDescription>
|
||||||
Общие FQDN — на весь пул · у IP свой доп. домен
|
Общие FQDN — на весь пул · у IP свои доп. домены
|
||||||
</FrameDescription>
|
</FrameDescription>
|
||||||
</FrameHeader>
|
</FrameHeader>
|
||||||
<Field>
|
<Field>
|
||||||
@@ -224,7 +242,7 @@ export function ServiceAddressBlock({
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
icon={ServerIcon}
|
icon={ServerIcon}
|
||||||
title="Добавьте IP пула"
|
title="Добавьте IP пула"
|
||||||
description="IPv4 сервиса. Для каждого адреса можно указать доп. FQDN."
|
description="IPv4 сервиса. Для каждого адреса можно указать несколько доп. FQDN, в том числе wildcard."
|
||||||
stackedIcon={false}
|
stackedIcon={false}
|
||||||
centered={false}
|
centered={false}
|
||||||
/>
|
/>
|
||||||
@@ -265,27 +283,98 @@ export function ServiceAddressBlock({
|
|||||||
</div>
|
</div>
|
||||||
<Field className="gap-1.5">
|
<Field className="gap-1.5">
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
htmlFor={`service-ip-extra-${node.ip}`}
|
htmlFor={`service-ip-extra-add-${node.ip}`}
|
||||||
className="text-muted-foreground text-xs"
|
className="text-muted-foreground text-xs"
|
||||||
>
|
>
|
||||||
Доп. FQDN
|
Доп. FQDN
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<InputGroup>
|
<div className="flex w-full flex-col gap-2">
|
||||||
<InputGroupInput
|
{node.extraFqdns.map((fqdn, index) => (
|
||||||
id={`service-ip-extra-${node.ip}`}
|
<InputGroup key={`extra-fqdn-${node.ip}-${index}`}>
|
||||||
className="font-mono"
|
<InputGroupInput
|
||||||
value={node.extraFqdn}
|
id={`service-ip-extra-${node.ip}-${index}`}
|
||||||
placeholder={
|
className="font-mono"
|
||||||
zoneHints[0]
|
value={fqdn}
|
||||||
? `необязательно · spb.${zoneHints[0]}`
|
placeholder={
|
||||||
: 'необязательно · spb.example.com'
|
zoneHints[0]
|
||||||
}
|
? `*.mdns.${zoneHints[0]}`
|
||||||
onChange={(event) =>
|
: '*.mdns.example.com'
|
||||||
handleNodeFqdn(node.ip, event.target.value)
|
}
|
||||||
}
|
onChange={(event) =>
|
||||||
/>
|
onChange(
|
||||||
<ZoneAddon fqdn={node.extraFqdn} zoneHints={zoneHints} />
|
updateExtraFqdn(
|
||||||
</InputGroup>
|
value,
|
||||||
|
node.ip,
|
||||||
|
index,
|
||||||
|
event.target.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ZoneAddon
|
||||||
|
fqdn={fqdn}
|
||||||
|
zoneHints={zoneHints}
|
||||||
|
trailing={
|
||||||
|
<InputGroupButton
|
||||||
|
size="icon-xs"
|
||||||
|
aria-label={`Удалить ${fqdn || 'FQDN'}`}
|
||||||
|
onClick={() =>
|
||||||
|
onChange(removeExtraFqdn(value, node.ip, index))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</InputGroupButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
))}
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput
|
||||||
|
id={`service-ip-extra-add-${node.ip}`}
|
||||||
|
className="font-mono"
|
||||||
|
value={pendingExtraByIp[node.ip] ?? ''}
|
||||||
|
placeholder={
|
||||||
|
zoneHints[0]
|
||||||
|
? `необязательно · *.mdns.${zoneHints[0]}`
|
||||||
|
: 'необязательно · *.mdns.example.com'
|
||||||
|
}
|
||||||
|
aria-invalid={
|
||||||
|
extraInvalidByIp[node.ip] &&
|
||||||
|
(pendingExtraByIp[node.ip] ?? '').trim().length > 0
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
setPendingExtraByIp((current) => ({
|
||||||
|
...current,
|
||||||
|
[node.ip]: event.target.value,
|
||||||
|
}))
|
||||||
|
setExtraInvalidByIp((current) => ({
|
||||||
|
...current,
|
||||||
|
[node.ip]: false,
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => handleExtraKeyDown(node.ip, event)}
|
||||||
|
onBlur={() =>
|
||||||
|
tryAddExtra(node.ip, pendingExtraByIp[node.ip] ?? '')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ZoneAddon
|
||||||
|
fqdn={pendingExtraByIp[node.ip] ?? ''}
|
||||||
|
zoneHints={zoneHints}
|
||||||
|
trailing={
|
||||||
|
<InputGroupButton
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
tryAddExtra(node.ip, pendingExtraByIp[node.ip] ?? '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</InputGroupButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ export function ServiceEditSheet({
|
|||||||
<SheetHeader className="shrink-0 border-b pb-4">
|
<SheetHeader className="shrink-0 border-b pb-4">
|
||||||
<SheetTitle>{isCreate ? 'Новый сервис' : 'Редактирование сервиса'}</SheetTitle>
|
<SheetTitle>{isCreate ? 'Новый сервис' : 'Редактирование сервиса'}</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
Общие FQDN на весь пул IP. У каждого адреса можно указать свой доп.
|
Общие FQDN на весь пул IP. У каждого адреса можно указать несколько доп.
|
||||||
FQDN.
|
FQDN, в том числе wildcard.
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
DEFAULT_BINDING_HEALTH,
|
DEFAULT_BINDING_HEALTH,
|
||||||
addAddressNode,
|
addAddressNode,
|
||||||
addCommonFqdn,
|
addCommonFqdn,
|
||||||
|
addExtraFqdn,
|
||||||
emptyAddressBlock,
|
emptyAddressBlock,
|
||||||
emptyBindingDraft,
|
emptyBindingDraft,
|
||||||
hydrateAddressBlock,
|
hydrateAddressBlock,
|
||||||
@@ -45,8 +46,8 @@ describe('hydrateAddressBlock', () => {
|
|||||||
|
|
||||||
expect(state.commonFqdns).toEqual(['rutg.rkns.top'])
|
expect(state.commonFqdns).toEqual(['rutg.rkns.top'])
|
||||||
expect(state.nodes).toEqual([
|
expect(state.nodes).toEqual([
|
||||||
{ ip: '93.115.203.183', extraFqdn: 'msk.rutg.rkns.top' },
|
{ ip: '93.115.203.183', extraFqdns: ['msk.rutg.rkns.top'] },
|
||||||
{ ip: '185.244.181.61', extraFqdn: '' },
|
{ ip: '185.244.181.61', extraFqdns: [] },
|
||||||
])
|
])
|
||||||
expect(state.preservedBindings).toEqual([])
|
expect(state.preservedBindings).toEqual([])
|
||||||
})
|
})
|
||||||
@@ -66,7 +67,7 @@ describe('hydrateAddressBlock', () => {
|
|||||||
const state = hydrateAddressBlock(drafts, ['1.1.1.1', '2.2.2.2'])
|
const state = hydrateAddressBlock(drafts, ['1.1.1.1', '2.2.2.2'])
|
||||||
|
|
||||||
expect(state.commonFqdns).toEqual(['rutg.rkns.top', 'both.rkns.top'])
|
expect(state.commonFqdns).toEqual(['rutg.rkns.top', 'both.rkns.top'])
|
||||||
expect(state.nodes.every((node) => node.extraFqdn === '')).toBe(true)
|
expect(state.nodes.every((node) => node.extraFqdns.length === 0)).toBe(true)
|
||||||
expect(state.preservedBindings.map((item) => item.fqdn)).toEqual(['alias.rkns.top'])
|
expect(state.preservedBindings.map((item) => item.fqdn)).toEqual(['alias.rkns.top'])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ describe('hydrateAddressBlock', () => {
|
|||||||
|
|
||||||
const state = hydrateAddressBlock(drafts, ['10.0.0.1'])
|
const state = hydrateAddressBlock(drafts, ['10.0.0.1'])
|
||||||
|
|
||||||
expect(state.nodes).toEqual([{ ip: '10.0.0.1', extraFqdn: '' }])
|
expect(state.nodes).toEqual([{ ip: '10.0.0.1', extraFqdns: [] }])
|
||||||
expect(state.commonFqdns).toEqual(['gw.example.com'])
|
expect(state.commonFqdns).toEqual(['gw.example.com'])
|
||||||
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')
|
||||||
@@ -94,7 +95,26 @@ describe('hydrateAddressBlock', () => {
|
|||||||
|
|
||||||
expect(state.commonFqdns).toEqual(['dns.shnt.top'])
|
expect(state.commonFqdns).toEqual(['dns.shnt.top'])
|
||||||
expect(state.nodes).toEqual([
|
expect(state.nodes).toEqual([
|
||||||
{ ip: '130.49.213.176', extraFqdn: 'ndns.shnt.top' },
|
{ ip: '130.49.213.176', extraFqdns: ['ndns.shnt.top'] },
|
||||||
|
])
|
||||||
|
expect(state.preservedBindings).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('кладёт несколько extra A на один IP в extraFqdns, включая wildcard', () => {
|
||||||
|
const drafts = [
|
||||||
|
aRecord('dns.shnt.top', ['130.49.213.176']),
|
||||||
|
aRecord('ndns.shnt.top', ['130.49.213.176']),
|
||||||
|
aRecord('*.mdns.shnt.top', ['130.49.213.176']),
|
||||||
|
]
|
||||||
|
|
||||||
|
const state = hydrateAddressBlock(drafts, ['130.49.213.176'])
|
||||||
|
|
||||||
|
expect(state.commonFqdns).toEqual(['dns.shnt.top'])
|
||||||
|
expect(state.nodes).toEqual([
|
||||||
|
{
|
||||||
|
ip: '130.49.213.176',
|
||||||
|
extraFqdns: ['ndns.shnt.top', '*.mdns.shnt.top'],
|
||||||
|
},
|
||||||
])
|
])
|
||||||
expect(state.preservedBindings).toEqual([])
|
expect(state.preservedBindings).toEqual([])
|
||||||
})
|
})
|
||||||
@@ -116,7 +136,7 @@ describe('hydrateAddressBlock', () => {
|
|||||||
'130.49.213.153': 2,
|
'130.49.213.153': 2,
|
||||||
'93.115.203.183': 1,
|
'93.115.203.183': 1,
|
||||||
})
|
})
|
||||||
expect(state.nodes[0]?.extraFqdn).toBe('nsgt.rkns.top')
|
expect(state.nodes[0]?.extraFqdns).toEqual(['nsgt.rkns.top'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -167,7 +187,7 @@ describe('toDomainsPayload', () => {
|
|||||||
const first = hydrateAddressBlock(drafts, ['130.49.213.176'])
|
const first = hydrateAddressBlock(drafts, ['130.49.213.176'])
|
||||||
expect(first.commonFqdns).toEqual(['dns.shnt.top'])
|
expect(first.commonFqdns).toEqual(['dns.shnt.top'])
|
||||||
expect(first.nodes).toEqual([
|
expect(first.nodes).toEqual([
|
||||||
{ ip: '130.49.213.176', extraFqdn: 'ndns.shnt.top' },
|
{ ip: '130.49.213.176', extraFqdns: ['ndns.shnt.top'] },
|
||||||
])
|
])
|
||||||
const rebound = toAddressBindings(first, primaryMeta)
|
const rebound = toAddressBindings(first, primaryMeta)
|
||||||
const second = hydrateAddressBlock(rebound, ['130.49.213.176'])
|
const second = hydrateAddressBlock(rebound, ['130.49.213.176'])
|
||||||
@@ -176,6 +196,22 @@ describe('toDomainsPayload', () => {
|
|||||||
expect(second.nodes).toEqual(first.nodes)
|
expect(second.nodes).toEqual(first.nodes)
|
||||||
expect(second.preservedBindings).toEqual([])
|
expect(second.preservedBindings).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('круг hydrate → payload → hydrate сохраняет несколько extra и wildcard при одном IP', () => {
|
||||||
|
const drafts = [
|
||||||
|
aRecord('dns.shnt.top', ['130.49.213.176']),
|
||||||
|
aRecord('ndns.shnt.top', ['130.49.213.176']),
|
||||||
|
aRecord('*.mdns.shnt.top', ['130.49.213.176']),
|
||||||
|
]
|
||||||
|
const first = hydrateAddressBlock(drafts, ['130.49.213.176'])
|
||||||
|
expect(first.nodes[0]?.extraFqdns).toEqual(['ndns.shnt.top', '*.mdns.shnt.top'])
|
||||||
|
const rebound = toAddressBindings(first, primaryMeta)
|
||||||
|
const second = hydrateAddressBlock(rebound, ['130.49.213.176'])
|
||||||
|
|
||||||
|
expect(second.commonFqdns).toEqual(first.commonFqdns)
|
||||||
|
expect(second.nodes).toEqual(first.nodes)
|
||||||
|
expect(second.preservedBindings).toEqual([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('removeAddressNode', () => {
|
describe('removeAddressNode', () => {
|
||||||
@@ -191,7 +227,7 @@ describe('removeAddressNode', () => {
|
|||||||
|
|
||||||
const next = removeAddressNode(state, '10.0.0.1')
|
const next = removeAddressNode(state, '10.0.0.1')
|
||||||
|
|
||||||
expect(next.nodes).toEqual([{ ip: '10.0.0.2', extraFqdn: '' }])
|
expect(next.nodes).toEqual([{ ip: '10.0.0.2', extraFqdns: [] }])
|
||||||
expect(next.preservedBindings).toHaveLength(1)
|
expect(next.preservedBindings).toHaveLength(1)
|
||||||
expect(next.preservedBindings[0]?.target_ips).toEqual(['9.9.9.9'])
|
expect(next.preservedBindings[0]?.target_ips).toEqual(['9.9.9.9'])
|
||||||
})
|
})
|
||||||
@@ -200,7 +236,7 @@ describe('removeAddressNode', () => {
|
|||||||
describe('addAddressNode / addCommonFqdn', () => {
|
describe('addAddressNode / addCommonFqdn', () => {
|
||||||
it('не добавляет дубликат IP', () => {
|
it('не добавляет дубликат IP', () => {
|
||||||
const withIp = addAddressNode(
|
const withIp = addAddressNode(
|
||||||
{ ...emptyAddressBlock(), nodes: [{ ip: '1.1.1.1', extraFqdn: '' }] },
|
{ ...emptyAddressBlock(), nodes: [{ ip: '1.1.1.1', extraFqdns: [] }] },
|
||||||
'1.1.1.1',
|
'1.1.1.1',
|
||||||
)
|
)
|
||||||
expect(withIp.nodes).toHaveLength(1)
|
expect(withIp.nodes).toHaveLength(1)
|
||||||
@@ -213,15 +249,22 @@ describe('addAddressNode / addCommonFqdn', () => {
|
|||||||
)
|
)
|
||||||
expect(state.commonFqdns).toEqual(['gt.rkns.top'])
|
expect(state.commonFqdns).toEqual(['gt.rkns.top'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('добавляет extra FQDN к IP и отклоняет дубликат', () => {
|
||||||
|
const withIp = addAddressNode(emptyAddressBlock(), '1.1.1.1')
|
||||||
|
const withExtra = addExtraFqdn(withIp, '1.1.1.1', 'mdns.shnt.top')
|
||||||
|
expect(withExtra.nodes[0]?.extraFqdns).toEqual(['mdns.shnt.top'])
|
||||||
|
expect(addExtraFqdn(withExtra, '1.1.1.1', 'MDNS.shnt.top')).toBe(withExtra)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('patchAddressIpMeta', () => {
|
describe('patchAddressIpMeta', () => {
|
||||||
it('меняет вес одного IP и не трогает extraFqdn', () => {
|
it('меняет вес одного IP и не трогает extraFqdns', () => {
|
||||||
const state = {
|
const state = {
|
||||||
...addAddressNode(addAddressNode(emptyAddressBlock(), '1.1.1.1'), '2.2.2.2'),
|
...addAddressNode(addAddressNode(emptyAddressBlock(), '1.1.1.1'), '2.2.2.2'),
|
||||||
nodes: [
|
nodes: [
|
||||||
{ ip: '1.1.1.1', extraFqdn: 'msk.example.com' },
|
{ ip: '1.1.1.1', extraFqdns: ['msk.example.com'] },
|
||||||
{ ip: '2.2.2.2', extraFqdn: '' },
|
{ ip: '2.2.2.2', extraFqdns: [] },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
const next = patchAddressIpMeta(state, '1.1.1.1', { weight: 7 })
|
const next = patchAddressIpMeta(state, '1.1.1.1', { weight: 7 })
|
||||||
@@ -257,7 +300,7 @@ describe('CNAME / preservedBindings', () => {
|
|||||||
cname,
|
cname,
|
||||||
]
|
]
|
||||||
const state = hydrateAddressBlock(drafts, ['1.1.1.1', '2.2.2.2'])
|
const state = hydrateAddressBlock(drafts, ['1.1.1.1', '2.2.2.2'])
|
||||||
expect(state.nodes[0]?.extraFqdn).toBe('msk.rkns.top')
|
expect(state.nodes[0]?.extraFqdns).toEqual(['msk.rkns.top'])
|
||||||
expect(state.preservedBindings).toHaveLength(1)
|
expect(state.preservedBindings).toHaveLength(1)
|
||||||
|
|
||||||
const payload = toDomainsPayload(state, primaryMeta)
|
const payload = toDomainsPayload(state, primaryMeta)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface ServiceBindingDraft {
|
|||||||
|
|
||||||
export interface AddressNode {
|
export interface AddressNode {
|
||||||
ip: string
|
ip: string
|
||||||
extraFqdn: string
|
extraFqdns: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddressBlockState {
|
export interface AddressBlockState {
|
||||||
@@ -178,8 +178,7 @@ export function hydrateAddressBlock(
|
|||||||
: uniqueIps(...(multiIpTargets.length > 0 ? multiIpTargets : allAIps))
|
: uniqueIps(...(multiIpTargets.length > 0 ? multiIpTargets : allAIps))
|
||||||
const poolSet = new Set(ips)
|
const poolSet = new Set(ips)
|
||||||
const commonFqdns: string[] = []
|
const commonFqdns: string[] = []
|
||||||
const claimed = new Set<string>()
|
const extraByIp = new Map<string, string[]>()
|
||||||
const extraByIp = new Map<string, string>()
|
|
||||||
const preservedBindings: ServiceBindingDraft[] = []
|
const preservedBindings: ServiceBindingDraft[] = []
|
||||||
let weights: Record<string, number> = {}
|
let weights: Record<string, number> = {}
|
||||||
let priorities: Record<string, number> = {}
|
let priorities: Record<string, number> = {}
|
||||||
@@ -188,6 +187,12 @@ export function hydrateAddressBlock(
|
|||||||
drafts.filter((draft) => isFullPoolA(draft, ips)).length > 1
|
drafts.filter((draft) => isFullPoolA(draft, ips)).length > 1
|
||||||
let assignedFirstSinglePoolCommon = false
|
let assignedFirstSinglePoolCommon = false
|
||||||
|
|
||||||
|
function pushExtra(ip: string, fqdn: string) {
|
||||||
|
const list = extraByIp.get(ip) ?? []
|
||||||
|
list.push(fqdn)
|
||||||
|
extraByIp.set(ip, list)
|
||||||
|
}
|
||||||
|
|
||||||
for (const draft of drafts) {
|
for (const draft of drafts) {
|
||||||
const fqdn = draft.fqdn.trim()
|
const fqdn = draft.fqdn.trim()
|
||||||
if (splitSinglePool && isFullPoolA(draft, ips)) {
|
if (splitSinglePool && isFullPoolA(draft, ips)) {
|
||||||
@@ -199,15 +204,10 @@ export function hydrateAddressBlock(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const ip = draft.target_ips[0]?.trim() ?? ''
|
const ip = draft.target_ips[0]?.trim() ?? ''
|
||||||
if (ip && poolSet.has(ip) && fqdn && !claimed.has(ip)) {
|
if (ip && poolSet.has(ip) && fqdn) {
|
||||||
claimed.add(ip)
|
pushExtra(ip, draft.fqdn)
|
||||||
extraByIp.set(ip, draft.fqdn)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const overflow = takeAsCommon(draft, fqdn, commonFqdns, weights, priorities)
|
|
||||||
weights = overflow.weights
|
|
||||||
priorities = overflow.priorities
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if (isFullPoolA(draft, ips)) {
|
if (isFullPoolA(draft, ips)) {
|
||||||
const next = takeAsCommon(draft, fqdn, commonFqdns, weights, priorities)
|
const next = takeAsCommon(draft, fqdn, commonFqdns, weights, priorities)
|
||||||
@@ -217,9 +217,8 @@ export function hydrateAddressBlock(
|
|||||||
}
|
}
|
||||||
if (draft.record_type === 'A' && draft.target_ips.length === 1) {
|
if (draft.record_type === 'A' && draft.target_ips.length === 1) {
|
||||||
const ip = draft.target_ips[0]?.trim() ?? ''
|
const ip = draft.target_ips[0]?.trim() ?? ''
|
||||||
if (ip && poolSet.has(ip) && fqdn && !claimed.has(ip)) {
|
if (ip && poolSet.has(ip) && fqdn) {
|
||||||
claimed.add(ip)
|
pushExtra(ip, draft.fqdn)
|
||||||
extraByIp.set(ip, draft.fqdn)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +229,7 @@ export function hydrateAddressBlock(
|
|||||||
commonFqdns,
|
commonFqdns,
|
||||||
nodes: ips.map((ip) => ({
|
nodes: ips.map((ip) => ({
|
||||||
ip,
|
ip,
|
||||||
extraFqdn: extraByIp.get(ip) ?? '',
|
extraFqdns: extraByIp.get(ip) ?? [],
|
||||||
})),
|
})),
|
||||||
preservedBindings,
|
preservedBindings,
|
||||||
target_ip_weights: weights,
|
target_ip_weights: weights,
|
||||||
@@ -275,7 +274,7 @@ export function addAddressNode(state: AddressBlockState, ip: string): AddressBlo
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
nodes: [...state.nodes, { ip: trimmed, extraFqdn: '' }],
|
nodes: [...state.nodes, { ip: trimmed, extraFqdns: [] }],
|
||||||
target_ip_weights: { ...state.target_ip_weights, [trimmed]: 1 },
|
target_ip_weights: { ...state.target_ip_weights, [trimmed]: 1 },
|
||||||
target_ip_priorities: { ...state.target_ip_priorities, [trimmed]: 1 },
|
target_ip_priorities: { ...state.target_ip_priorities, [trimmed]: 1 },
|
||||||
}
|
}
|
||||||
@@ -316,7 +315,9 @@ export function addressHasFqdn(state: AddressBlockState, fqdn: string): boolean
|
|||||||
const key = fqdnKey(fqdn)
|
const key = fqdnKey(fqdn)
|
||||||
if (!key) return false
|
if (!key) return false
|
||||||
if (state.commonFqdns.some((item) => fqdnKey(item) === key)) return true
|
if (state.commonFqdns.some((item) => fqdnKey(item) === key)) return true
|
||||||
if (state.nodes.some((node) => fqdnKey(node.extraFqdn) === key)) return true
|
if (state.nodes.some((node) => node.extraFqdns.some((item) => fqdnKey(item) === key))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +345,56 @@ export function updateCommonFqdn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addExtraFqdn(
|
||||||
|
state: AddressBlockState,
|
||||||
|
ip: string,
|
||||||
|
fqdn: string,
|
||||||
|
): AddressBlockState {
|
||||||
|
const trimmed = fqdn.trim()
|
||||||
|
if (!trimmed || addressHasFqdn(state, trimmed)) return state
|
||||||
|
if (!state.nodes.some((node) => node.ip === ip)) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: state.nodes.map((node) =>
|
||||||
|
node.ip === ip ? { ...node, extraFqdns: [...node.extraFqdns, trimmed] } : node,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeExtraFqdn(
|
||||||
|
state: AddressBlockState,
|
||||||
|
ip: string,
|
||||||
|
index: number,
|
||||||
|
): AddressBlockState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: state.nodes.map((node) =>
|
||||||
|
node.ip === ip
|
||||||
|
? { ...node, extraFqdns: node.extraFqdns.filter((_, i) => i !== index) }
|
||||||
|
: node,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateExtraFqdn(
|
||||||
|
state: AddressBlockState,
|
||||||
|
ip: string,
|
||||||
|
index: number,
|
||||||
|
fqdn: string,
|
||||||
|
): AddressBlockState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: state.nodes.map((node) =>
|
||||||
|
node.ip === ip
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
extraFqdns: node.extraFqdns.map((item, i) => (i === index ? fqdn : item)),
|
||||||
|
}
|
||||||
|
: node,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function toAddressBindings(
|
export function toAddressBindings(
|
||||||
state: AddressBlockState,
|
state: AddressBlockState,
|
||||||
primary: AddressPrimaryMeta,
|
primary: AddressPrimaryMeta,
|
||||||
@@ -373,18 +424,20 @@ export function toAddressBindings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const node of state.nodes) {
|
for (const node of state.nodes) {
|
||||||
const extraFqdn = node.extraFqdn.trim()
|
for (const raw of node.extraFqdns) {
|
||||||
if (!extraFqdn) continue
|
const extraFqdn = raw.trim()
|
||||||
drafts.push({
|
if (!extraFqdn) continue
|
||||||
fqdn: extraFqdn,
|
drafts.push({
|
||||||
record_type: 'A',
|
fqdn: extraFqdn,
|
||||||
target_ips: [node.ip],
|
record_type: 'A',
|
||||||
target_cname: '',
|
target_ips: [node.ip],
|
||||||
lb_mode: primary.lb_mode,
|
target_cname: '',
|
||||||
health: { ...primary.health },
|
lb_mode: primary.lb_mode,
|
||||||
target_ip_weights: { [node.ip]: weights[node.ip] ?? 1 },
|
health: { ...primary.health },
|
||||||
target_ip_priorities: { [node.ip]: priorities[node.ip] ?? 1 },
|
target_ip_weights: { [node.ip]: weights[node.ip] ?? 1 },
|
||||||
})
|
target_ip_priorities: { [node.ip]: priorities[node.ip] ?? 1 },
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drafts.push(...state.preservedBindings)
|
drafts.push(...state.preservedBindings)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function dnsNameToSubdomainLabel(
|
|||||||
return prefix || "@";
|
return prefix || "@";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rn.startsWith("*.")) return rn;
|
||||||
if (!rn.includes(".")) return rn;
|
if (!rn.includes(".")) return rn;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ describe("normalizeDnsRecordName", () => {
|
|||||||
expect(normalizeDnsRecordName("@", ZONE)).toBe("rkns.top");
|
expect(normalizeDnsRecordName("@", ZONE)).toBe("rkns.top");
|
||||||
expect(normalizeDnsRecordName("rkns.top", ZONE)).toBe("rkns.top");
|
expect(normalizeDnsRecordName("rkns.top", ZONE)).toBe("rkns.top");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes nested wildcard relative name to FQDN", () => {
|
||||||
|
expect(normalizeDnsRecordName("*.mdns", ZONE)).toBe("*.mdns.rkns.top");
|
||||||
|
expect(normalizeDnsRecordName("*.mdns.rkns.top", ZONE)).toBe(
|
||||||
|
"*.mdns.rkns.top",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("dnsRecordNamesMatch", () => {
|
describe("dnsRecordNamesMatch", () => {
|
||||||
@@ -34,10 +41,19 @@ describe("dnsRecordNamesMatch", () => {
|
|||||||
it("does not match different hosts", () => {
|
it("does not match different hosts", () => {
|
||||||
expect(dnsRecordNamesMatch("de", "mhome.rkns.top", ZONE)).toBe(false);
|
expect(dnsRecordNamesMatch("de", "mhome.rkns.top", ZONE)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches nested wildcard relative name and FQDN", () => {
|
||||||
|
expect(dnsRecordNamesMatch("*.mdns", "*.mdns.rkns.top", ZONE)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("dnsNameToSubdomainLabel", () => {
|
describe("dnsNameToSubdomainLabel", () => {
|
||||||
it("extracts label from FQDN", () => {
|
it("extracts label from FQDN", () => {
|
||||||
expect(dnsNameToSubdomainLabel("de.rkns.top", ZONE)).toBe("de");
|
expect(dnsNameToSubdomainLabel("de.rkns.top", ZONE)).toBe("de");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps nested wildcard relative names", () => {
|
||||||
|
expect(dnsNameToSubdomainLabel("*.mdns", ZONE)).toBe("*.mdns");
|
||||||
|
expect(dnsNameToSubdomainLabel("*.mdns.rkns.top", ZONE)).toBe("*.mdns");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user