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

У IP был один extra FQDN; wildcard вида *.mdns.shnt.top не матчился с именем из Cloudflare.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-31 12:40:27 +07:00
co-authored by Cursor
parent a228febc27
commit 0d567379fa
6 changed files with 272 additions and 70 deletions
@@ -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>
+56 -13
View File
@@ -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)
+81 -28
View File
@@ -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)
+1
View File
@@ -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;
+16
View File
@@ -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");
});
}); });