feat(services): заменить один общий FQDN списком и убрать Другие FQDN
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m6s
CD / quality (push) Successful in 1m18s
CD / publish (push) Successful in 2m1s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m6s
CD / quality (push) Successful in 1m18s
CD / publish (push) Successful in 2m1s
Каждый общий домен смотрит на весь пул IP; CNAME без UI сохраняются. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
import { useState, type KeyboardEvent } from 'react'
|
import { useState, type KeyboardEvent, type ReactNode } from 'react'
|
||||||
import { PlusIcon, ServerIcon, Trash2Icon } from 'lucide-react'
|
import { ServerIcon, Trash2Icon } from 'lucide-react'
|
||||||
|
|
||||||
import { EmptyState } from '@/components/empty-state'
|
import { EmptyState } from '@/components/empty-state'
|
||||||
import { ServiceBindingIpInput } from '@/components/service-binding-ip-input'
|
|
||||||
import { isValidIpv4 } from '@/components/tagged-input'
|
import { isValidIpv4 } from '@/components/tagged-input'
|
||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import {
|
import {
|
||||||
@@ -16,15 +15,15 @@ import { IconTile } from '@/components/reui/icon-tile'
|
|||||||
import { parseFqdn } from '@/lib/parse-fqdn'
|
import { parseFqdn } from '@/lib/parse-fqdn'
|
||||||
import {
|
import {
|
||||||
addAddressNode,
|
addAddressNode,
|
||||||
emptyBindingDraft,
|
addCommonFqdn,
|
||||||
|
addressHasFqdn,
|
||||||
removeAddressNode,
|
removeAddressNode,
|
||||||
withPoolIps,
|
removeCommonFqdn,
|
||||||
|
updateCommonFqdn,
|
||||||
type AddressBlockState,
|
type AddressBlockState,
|
||||||
type ServiceBindingDraft,
|
|
||||||
} from '@/lib/service-address'
|
} from '@/lib/service-address'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import { Field, FieldLabel } from '@cfdm/ui/components/field'
|
import { Field, FieldLabel } from '@cfdm/ui/components/field'
|
||||||
import { Input } from '@cfdm/ui/components/input'
|
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
@@ -39,16 +38,36 @@ import {
|
|||||||
ItemMedia,
|
ItemMedia,
|
||||||
ItemTitle,
|
ItemTitle,
|
||||||
} from '@cfdm/ui/components/item'
|
} from '@cfdm/ui/components/item'
|
||||||
import {
|
|
||||||
Select,
|
function ZoneAddon({
|
||||||
SelectContent,
|
fqdn,
|
||||||
SelectItem,
|
zoneHints,
|
||||||
SelectTrigger,
|
trailing,
|
||||||
SelectValue,
|
}: {
|
||||||
} from '@cfdm/ui/components/select'
|
fqdn: string
|
||||||
|
zoneHints: string[]
|
||||||
|
trailing?: ReactNode
|
||||||
|
}) {
|
||||||
|
const parsed = parseFqdn(fqdn, zoneHints)
|
||||||
|
if (!parsed && !fqdn.trim() && !trailing) return null
|
||||||
|
return (
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
{parsed ? (
|
||||||
|
<Badge variant="outline" size="xs" className="font-mono">
|
||||||
|
{parsed.zoneName}
|
||||||
|
</Badge>
|
||||||
|
) : fqdn.trim() ? (
|
||||||
|
<Badge variant="warning-light" size="xs">
|
||||||
|
зона не найдена
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{trailing}
|
||||||
|
</InputGroupAddon>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Единый блок адресов сервиса: общий 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
|
||||||
@@ -67,17 +86,30 @@ export function ServiceAddressBlock({
|
|||||||
}) {
|
}) {
|
||||||
const [pendingIp, setPendingIp] = useState('')
|
const [pendingIp, setPendingIp] = useState('')
|
||||||
const [ipInvalid, setIpInvalid] = useState(false)
|
const [ipInvalid, setIpInvalid] = useState(false)
|
||||||
const [otherOpen, setOtherOpen] = useState(value.otherBindings.length > 0)
|
const [pendingFqdn, setPendingFqdn] = useState('')
|
||||||
|
const [fqdnInvalid, setFqdnInvalid] = useState(false)
|
||||||
|
|
||||||
const pool = value.nodes.map((node) => node.ip)
|
const pool = value.nodes.map((node) => node.ip)
|
||||||
const parsedCommon = parseFqdn(value.commonFqdn, zoneHints)
|
const pendingIpTrimmed = pendingIp.trim()
|
||||||
const showOthers = otherOpen || value.otherBindings.length > 0
|
const pendingFqdnTrimmed = pendingFqdn.trim()
|
||||||
const pendingTrimmed = pendingIp.trim()
|
const pendingIpInvalid =
|
||||||
const pendingInvalid =
|
ipInvalid && pendingIpTrimmed.length > 0 && !isValidIpv4(pendingIpTrimmed)
|
||||||
ipInvalid && pendingTrimmed.length > 0 && !isValidIpv4(pendingTrimmed)
|
const pendingFqdnInvalid =
|
||||||
|
fqdnInvalid && pendingFqdnTrimmed.length > 0
|
||||||
|
|
||||||
function handleCommonFqdn(next: string) {
|
function tryAddFqdn(raw: string) {
|
||||||
onChange({ ...value, commonFqdn: next })
|
const trimmed = raw.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
setFqdnInvalid(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (addressHasFqdn(value, trimmed)) {
|
||||||
|
setFqdnInvalid(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onChange(addCommonFqdn(value, trimmed))
|
||||||
|
setPendingFqdn('')
|
||||||
|
setFqdnInvalid(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryAddIp(raw: string) {
|
function tryAddIp(raw: string) {
|
||||||
@@ -95,7 +127,14 @@ export function ServiceAddressBlock({
|
|||||||
setIpInvalid(false)
|
setIpInvalid(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePendingKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
function handleFqdnKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
tryAddFqdn(pendingFqdn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIpKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
tryAddIp(pendingIp)
|
tryAddIp(pendingIp)
|
||||||
@@ -111,64 +150,69 @@ export function ServiceAddressBlock({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveIp(ip: string) {
|
|
||||||
onChange(removeAddressNode(value, ip))
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddOther() {
|
|
||||||
setOtherOpen(true)
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
otherBindings: [...value.otherBindings, withPoolIps(emptyBindingDraft(), pool)],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOtherChange(index: number, next: ServiceBindingDraft) {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
otherBindings: value.otherBindings.map((item, i) => (i === index ? next : item)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRemoveOther(index: number) {
|
|
||||||
const otherBindings = value.otherBindings.filter((_, i) => i !== index)
|
|
||||||
onChange({ ...value, otherBindings })
|
|
||||||
if (otherBindings.length === 0) setOtherOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame stacked dense spacing="sm" className="w-full min-w-0">
|
<Frame stacked dense spacing="sm" className="w-full min-w-0">
|
||||||
<FramePanel fit className="flex flex-col gap-3">
|
<FramePanel fit className="flex flex-col gap-3">
|
||||||
<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>
|
||||||
<FieldLabel htmlFor="service-common-fqdn">Общий домен (FQDN)</FieldLabel>
|
<FieldLabel htmlFor="service-common-fqdn-add">Общие домены (FQDN)</FieldLabel>
|
||||||
<InputGroup>
|
<div className="flex w-full flex-col gap-2">
|
||||||
<InputGroupInput
|
{value.commonFqdns.map((fqdn, index) => (
|
||||||
id="service-common-fqdn"
|
<InputGroup key={`common-fqdn-${index}`}>
|
||||||
className="font-mono"
|
<InputGroupInput
|
||||||
value={value.commonFqdn}
|
id={`service-common-fqdn-${index}`}
|
||||||
placeholder={zoneHints[0] ? `gw.${zoneHints[0]}` : 'gw.ivx.su'}
|
className="font-mono"
|
||||||
onChange={(event) => handleCommonFqdn(event.target.value)}
|
value={fqdn}
|
||||||
/>
|
placeholder={zoneHints[0] ? `gw.${zoneHints[0]}` : 'gw.ivx.su'}
|
||||||
{parsedCommon ? (
|
onChange={(event) =>
|
||||||
<InputGroupAddon align="inline-end">
|
onChange(updateCommonFqdn(value, index, event.target.value))
|
||||||
<Badge variant="outline" size="xs" className="font-mono">
|
}
|
||||||
{parsedCommon.zoneName}
|
/>
|
||||||
</Badge>
|
<ZoneAddon
|
||||||
</InputGroupAddon>
|
fqdn={fqdn}
|
||||||
) : value.commonFqdn.trim() ? (
|
zoneHints={zoneHints}
|
||||||
<InputGroupAddon align="inline-end">
|
trailing={
|
||||||
<Badge variant="warning-light" size="xs">
|
<InputGroupButton
|
||||||
зона не найдена
|
size="icon-xs"
|
||||||
</Badge>
|
aria-label={`Удалить ${fqdn || 'FQDN'}`}
|
||||||
</InputGroupAddon>
|
onClick={() => onChange(removeCommonFqdn(value, index))}
|
||||||
) : null}
|
>
|
||||||
</InputGroup>
|
<Trash2Icon />
|
||||||
|
</InputGroupButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
))}
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput
|
||||||
|
id="service-common-fqdn-add"
|
||||||
|
className="font-mono"
|
||||||
|
value={pendingFqdn}
|
||||||
|
placeholder={zoneHints[0] ? `gw.${zoneHints[0]}` : 'gw.ivx.su'}
|
||||||
|
aria-invalid={pendingFqdnInvalid || undefined}
|
||||||
|
onChange={(event) => {
|
||||||
|
setPendingFqdn(event.target.value)
|
||||||
|
setFqdnInvalid(false)
|
||||||
|
}}
|
||||||
|
onKeyDown={handleFqdnKeyDown}
|
||||||
|
onBlur={() => tryAddFqdn(pendingFqdn)}
|
||||||
|
/>
|
||||||
|
<ZoneAddon
|
||||||
|
fqdn={pendingFqdn}
|
||||||
|
zoneHints={zoneHints}
|
||||||
|
trailing={
|
||||||
|
<InputGroupButton size="sm" onClick={() => tryAddFqdn(pendingFqdn)}>
|
||||||
|
Добавить
|
||||||
|
</InputGroupButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
|
|
||||||
@@ -187,7 +231,6 @@ export function ServiceAddressBlock({
|
|||||||
) : (
|
) : (
|
||||||
<ItemGroup className="gap-2">
|
<ItemGroup className="gap-2">
|
||||||
{value.nodes.map((node) => {
|
{value.nodes.map((node) => {
|
||||||
const parsedExtra = parseFqdn(node.extraFqdn, zoneHints)
|
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
key={node.ip}
|
key={node.ip}
|
||||||
@@ -214,7 +257,7 @@ export function ServiceAddressBlock({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
aria-label={`Удалить ${node.ip}`}
|
aria-label={`Удалить ${node.ip}`}
|
||||||
onClick={() => handleRemoveIp(node.ip)}
|
onClick={() => onChange(removeAddressNode(value, node.ip))}
|
||||||
>
|
>
|
||||||
<Trash2Icon />
|
<Trash2Icon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -241,19 +284,7 @@ export function ServiceAddressBlock({
|
|||||||
handleNodeFqdn(node.ip, event.target.value)
|
handleNodeFqdn(node.ip, event.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{parsedExtra ? (
|
<ZoneAddon fqdn={node.extraFqdn} zoneHints={zoneHints} />
|
||||||
<InputGroupAddon align="inline-end">
|
|
||||||
<Badge variant="outline" size="xs" className="font-mono">
|
|
||||||
{parsedExtra.zoneName}
|
|
||||||
</Badge>
|
|
||||||
</InputGroupAddon>
|
|
||||||
) : node.extraFqdn.trim() ? (
|
|
||||||
<InputGroupAddon align="inline-end">
|
|
||||||
<Badge variant="warning-light" size="xs">
|
|
||||||
зона не найдена
|
|
||||||
</Badge>
|
|
||||||
</InputGroupAddon>
|
|
||||||
) : null}
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Field>
|
</Field>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
@@ -268,12 +299,12 @@ export function ServiceAddressBlock({
|
|||||||
className="font-mono"
|
className="font-mono"
|
||||||
value={pendingIp}
|
value={pendingIp}
|
||||||
placeholder="192.168.1.1"
|
placeholder="192.168.1.1"
|
||||||
aria-invalid={pendingInvalid || undefined}
|
aria-invalid={pendingIpInvalid || undefined}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setPendingIp(event.target.value)
|
setPendingIp(event.target.value)
|
||||||
setIpInvalid(false)
|
setIpInvalid(false)
|
||||||
}}
|
}}
|
||||||
onKeyDown={handlePendingKeyDown}
|
onKeyDown={handleIpKeyDown}
|
||||||
onBlur={() => tryAddIp(pendingIp)}
|
onBlur={() => tryAddIp(pendingIp)}
|
||||||
/>
|
/>
|
||||||
<InputGroupAddon align="inline-end">
|
<InputGroupAddon align="inline-end">
|
||||||
@@ -282,156 +313,7 @@ export function ServiceAddressBlock({
|
|||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{showOthers ? null : (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAddOther}
|
|
||||||
>
|
|
||||||
<PlusIcon data-icon="inline-start" />
|
|
||||||
Другой FQDN
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
|
|
||||||
{showOthers ? (
|
|
||||||
<FramePanel fit className="flex flex-col gap-3">
|
|
||||||
<FrameHeader className="flex flex-row items-start justify-between gap-2 px-0 pt-0">
|
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
|
||||||
<FrameTitle>Другие FQDN</FrameTitle>
|
|
||||||
<FrameDescription>CNAME и A не 1:1 с IP пула</FrameDescription>
|
|
||||||
</div>
|
|
||||||
<Button type="button" variant="outline" size="sm" onClick={handleAddOther}>
|
|
||||||
<PlusIcon data-icon="inline-start" />
|
|
||||||
Добавить
|
|
||||||
</Button>
|
|
||||||
</FrameHeader>
|
|
||||||
{value.otherBindings.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground text-sm">Нет дополнительных FQDN</p>
|
|
||||||
) : (
|
|
||||||
<ItemGroup className="gap-2">
|
|
||||||
{value.otherBindings.map((binding, index) => {
|
|
||||||
const parsedZone = parseFqdn(binding.fqdn, zoneHints)
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
key={`other-binding-${index}`}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="items-stretch"
|
|
||||||
>
|
|
||||||
<ItemContent className="flex w-full min-w-0 flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{parsedZone ? (
|
|
||||||
<Badge variant="outline" size="xs" className="font-mono">
|
|
||||||
{parsedZone.zoneName}
|
|
||||||
</Badge>
|
|
||||||
) : binding.fqdn.trim() ? (
|
|
||||||
<Badge variant="warning-light" size="xs">
|
|
||||||
зона не найдена
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground text-xs">FQDN</span>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
className="ml-auto shrink-0"
|
|
||||||
aria-label="Удалить FQDN"
|
|
||||||
onClick={() => handleRemoveOther(index)}
|
|
||||||
>
|
|
||||||
<Trash2Icon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_7.5rem]">
|
|
||||||
<Input
|
|
||||||
id={`other-fqdn-${index}`}
|
|
||||||
className="font-mono"
|
|
||||||
value={binding.fqdn}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleOtherChange(index, {
|
|
||||||
...binding,
|
|
||||||
fqdn: event.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
zoneHints[0] ? `api.${zoneHints[0]}` : 'api.ivx.su'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
items={[
|
|
||||||
{ label: 'A (IP)', value: 'A' },
|
|
||||||
{ label: 'CNAME', value: 'CNAME' },
|
|
||||||
]}
|
|
||||||
value={binding.record_type}
|
|
||||||
onValueChange={(next) => {
|
|
||||||
const recordType = (next ?? 'A') as 'A' | 'CNAME'
|
|
||||||
handleOtherChange(index, {
|
|
||||||
...binding,
|
|
||||||
record_type: recordType,
|
|
||||||
target_ips: recordType === 'A' ? binding.target_ips : [],
|
|
||||||
target_cname:
|
|
||||||
recordType === 'CNAME' ? binding.target_cname : '',
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id={`other-type-${index}`} className="w-full">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="A">A (IP)</SelectItem>
|
|
||||||
<SelectItem value="CNAME">CNAME</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{binding.record_type === 'CNAME' ? (
|
|
||||||
<Input
|
|
||||||
id={`other-cname-${index}`}
|
|
||||||
value={binding.target_cname}
|
|
||||||
placeholder="mmsk.rkns.top"
|
|
||||||
onChange={(event) =>
|
|
||||||
handleOtherChange(index, {
|
|
||||||
...binding,
|
|
||||||
target_cname: event.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ServiceBindingIpInput
|
|
||||||
id={`other-ip-${index}`}
|
|
||||||
value={binding.target_ips}
|
|
||||||
pool={pool}
|
|
||||||
onChange={(targetIps) =>
|
|
||||||
handleOtherChange(index, {
|
|
||||||
...binding,
|
|
||||||
target_ips: targetIps,
|
|
||||||
target_ip_weights: Object.fromEntries(
|
|
||||||
targetIps.map((ip) => [
|
|
||||||
ip,
|
|
||||||
binding.target_ip_weights[ip] ?? 1,
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
target_ip_priorities: Object.fromEntries(
|
|
||||||
targetIps.map((ip) => [
|
|
||||||
ip,
|
|
||||||
binding.target_ip_priorities[ip] ?? 1,
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ItemContent>
|
|
||||||
</Item>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ItemGroup>
|
|
||||||
)}
|
|
||||||
</FramePanel>
|
|
||||||
) : null}
|
|
||||||
</Frame>
|
</Frame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,8 +218,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>
|
||||||
Общий домен и пул IP — в одном блоке. У каждого адреса можно указать
|
Общие FQDN на весь пул IP. У каждого адреса можно указать свой доп.
|
||||||
свой доп. FQDN.
|
FQDN.
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import {
|
import {
|
||||||
DEFAULT_BINDING_HEALTH,
|
DEFAULT_BINDING_HEALTH,
|
||||||
addAddressNode,
|
addAddressNode,
|
||||||
|
addCommonFqdn,
|
||||||
emptyAddressBlock,
|
emptyAddressBlock,
|
||||||
emptyBindingDraft,
|
emptyBindingDraft,
|
||||||
hydrateAddressBlock,
|
hydrateAddressBlock,
|
||||||
@@ -41,15 +42,15 @@ describe('hydrateAddressBlock', () => {
|
|||||||
|
|
||||||
const state = hydrateAddressBlock(drafts, ['93.115.203.183', '185.244.181.61'])
|
const state = hydrateAddressBlock(drafts, ['93.115.203.183', '185.244.181.61'])
|
||||||
|
|
||||||
expect(state.commonFqdn).toBe('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', extraFqdn: 'msk.rutg.rkns.top' },
|
||||||
{ ip: '185.244.181.61', extraFqdn: '' },
|
{ ip: '185.244.181.61', extraFqdn: '' },
|
||||||
])
|
])
|
||||||
expect(state.otherBindings).toEqual([])
|
expect(state.preservedBindings).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('не схлопывает CNAME и A на несколько IP', () => {
|
it('кладёт A на весь пул в commonFqdns, CNAME — в preserved', () => {
|
||||||
const cname: ServiceBindingDraft = {
|
const cname: ServiceBindingDraft = {
|
||||||
...emptyBindingDraft('alias.rkns.top'),
|
...emptyBindingDraft('alias.rkns.top'),
|
||||||
record_type: 'CNAME',
|
record_type: 'CNAME',
|
||||||
@@ -63,14 +64,12 @@ 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.nodes.every((node) => node.extraFqdn === '')).toBe(true)
|
expect(state.nodes.every((node) => node.extraFqdn === '')).toBe(true)
|
||||||
expect(state.otherBindings.map((item) => item.fqdn)).toEqual([
|
expect(state.preservedBindings.map((item) => item.fqdn)).toEqual(['alias.rkns.top'])
|
||||||
'both.rkns.top',
|
|
||||||
'alias.rkns.top',
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('кладёт extra A с IP вне пула в otherBindings', () => {
|
it('кладёт extra A с IP вне пула в preservedBindings', () => {
|
||||||
const drafts = [
|
const drafts = [
|
||||||
aRecord('gw.example.com', ['10.0.0.1']),
|
aRecord('gw.example.com', ['10.0.0.1']),
|
||||||
aRecord('edge.example.com', ['8.8.8.8']),
|
aRecord('edge.example.com', ['8.8.8.8']),
|
||||||
@@ -79,13 +78,14 @@ 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', extraFqdn: '' }])
|
||||||
expect(state.otherBindings).toHaveLength(1)
|
expect(state.commonFqdns).toEqual(['gw.example.com'])
|
||||||
expect(state.otherBindings[0]?.fqdn).toBe('edge.example.com')
|
expect(state.preservedBindings).toHaveLength(1)
|
||||||
|
expect(state.preservedBindings[0]?.fqdn).toBe('edge.example.com')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('toDomainsPayload', () => {
|
describe('toDomainsPayload', () => {
|
||||||
it('собирает primary на весь пул и extra binding на один IP', () => {
|
it('собирает каждый common на весь пул и extra binding на один IP', () => {
|
||||||
const drafts = [
|
const drafts = [
|
||||||
aRecord('rutg.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
aRecord('rutg.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
||||||
aRecord('msk.rutg.rkns.top', ['93.115.203.183']),
|
aRecord('msk.rutg.rkns.top', ['93.115.203.183']),
|
||||||
@@ -107,28 +107,30 @@ describe('toDomainsPayload', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('круг hydrate → payload → hydrate сохраняет extra FQDN', () => {
|
it('круг hydrate → payload → hydrate сохраняет два common и extra FQDN', () => {
|
||||||
const drafts = [
|
const drafts = [
|
||||||
aRecord('rutg.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
aRecord('gt.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
||||||
aRecord('msk.rutg.rkns.top', ['93.115.203.183']),
|
aRecord('msk.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
||||||
|
aRecord('nsgt.rkns.top', ['93.115.203.183']),
|
||||||
]
|
]
|
||||||
const first = hydrateAddressBlock(drafts, ['93.115.203.183', '185.244.181.61'])
|
const first = hydrateAddressBlock(drafts, ['93.115.203.183', '185.244.181.61'])
|
||||||
|
expect(first.commonFqdns).toEqual(['gt.rkns.top', 'msk.rkns.top'])
|
||||||
const rebound = toAddressBindings(first, primaryMeta)
|
const rebound = toAddressBindings(first, primaryMeta)
|
||||||
const second = hydrateAddressBlock(rebound, rebound[0]?.target_ips ?? [])
|
const second = hydrateAddressBlock(rebound, rebound[0]?.target_ips ?? [])
|
||||||
|
|
||||||
expect(second.commonFqdn).toBe(first.commonFqdn)
|
expect(second.commonFqdns).toEqual(first.commonFqdns)
|
||||||
expect(second.nodes).toEqual(first.nodes)
|
expect(second.nodes).toEqual(first.nodes)
|
||||||
expect(second.otherBindings).toEqual([])
|
expect(second.preservedBindings).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('removeAddressNode', () => {
|
describe('removeAddressNode', () => {
|
||||||
it('удаляет extra FQDN узла и IP из other A-bindings', () => {
|
it('удаляет extra FQDN узла и IP из preserved A-bindings', () => {
|
||||||
const state = hydrateAddressBlock(
|
const state = hydrateAddressBlock(
|
||||||
[
|
[
|
||||||
aRecord('gw.example.com', ['10.0.0.1', '10.0.0.2']),
|
aRecord('gw.example.com', ['10.0.0.1', '10.0.0.2']),
|
||||||
aRecord('msk.example.com', ['10.0.0.1']),
|
aRecord('msk.example.com', ['10.0.0.1']),
|
||||||
aRecord('pair.example.com', ['10.0.0.1', '10.0.0.2']),
|
aRecord('edge.example.com', ['9.9.9.9', '10.0.0.1']),
|
||||||
],
|
],
|
||||||
['10.0.0.1', '10.0.0.2'],
|
['10.0.0.1', '10.0.0.2'],
|
||||||
)
|
)
|
||||||
@@ -136,12 +138,12 @@ 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', extraFqdn: '' }])
|
||||||
expect(next.otherBindings).toHaveLength(1)
|
expect(next.preservedBindings).toHaveLength(1)
|
||||||
expect(next.otherBindings[0]?.target_ips).toEqual(['10.0.0.2'])
|
expect(next.preservedBindings[0]?.target_ips).toEqual(['9.9.9.9'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('addAddressNode', () => {
|
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', extraFqdn: '' }] },
|
||||||
@@ -149,23 +151,31 @@ describe('addAddressNode', () => {
|
|||||||
)
|
)
|
||||||
expect(withIp.nodes).toHaveLength(1)
|
expect(withIp.nodes).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('не добавляет дубликат common FQDN', () => {
|
||||||
|
const state = addCommonFqdn(
|
||||||
|
{ ...emptyAddressBlock(), commonFqdns: ['gt.rkns.top'] },
|
||||||
|
'GT.rkns.top',
|
||||||
|
)
|
||||||
|
expect(state.commonFqdns).toEqual(['gt.rkns.top'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('CNAME / otherBindings', () => {
|
describe('CNAME / preservedBindings', () => {
|
||||||
it('сохраняет CNAME в otherBindings при круге hydrate → payload', () => {
|
it('сохраняет CNAME в preserved при круге hydrate → payload', () => {
|
||||||
const cname: ServiceBindingDraft = {
|
const cname: ServiceBindingDraft = {
|
||||||
...emptyBindingDraft('alias.rkns.top'),
|
...emptyBindingDraft('alias.rkns.top'),
|
||||||
record_type: 'CNAME',
|
record_type: 'CNAME',
|
||||||
target_cname: 'rutg.rkns.top',
|
target_cname: 'rutg.rkns.top',
|
||||||
}
|
}
|
||||||
const drafts = [
|
const drafts = [
|
||||||
aRecord('rutg.rkns.top', ['1.1.1.1']),
|
aRecord('rutg.rkns.top', ['1.1.1.1', '2.2.2.2']),
|
||||||
aRecord('msk.rkns.top', ['1.1.1.1']),
|
aRecord('msk.rkns.top', ['1.1.1.1']),
|
||||||
cname,
|
cname,
|
||||||
]
|
]
|
||||||
const state = hydrateAddressBlock(drafts, ['1.1.1.1'])
|
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]?.extraFqdn).toBe('msk.rkns.top')
|
||||||
expect(state.otherBindings).toHaveLength(1)
|
expect(state.preservedBindings).toHaveLength(1)
|
||||||
|
|
||||||
const payload = toDomainsPayload(state, primaryMeta)
|
const payload = toDomainsPayload(state, primaryMeta)
|
||||||
expect(payload.map((item) => item.fqdn)).toEqual([
|
expect(payload.map((item) => item.fqdn)).toEqual([
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ export interface AddressNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AddressBlockState {
|
export interface AddressBlockState {
|
||||||
commonFqdn: string
|
commonFqdns: string[]
|
||||||
nodes: AddressNode[]
|
nodes: AddressNode[]
|
||||||
otherBindings: ServiceBindingDraft[]
|
preservedBindings: ServiceBindingDraft[]
|
||||||
target_ip_weights: Record<string, number>
|
target_ip_weights: Record<string, number>
|
||||||
target_ip_priorities: Record<string, number>
|
target_ip_priorities: Record<string, number>
|
||||||
}
|
}
|
||||||
@@ -78,31 +78,14 @@ export function emptyBindingDraft(fqdn = ''): ServiceBindingDraft {
|
|||||||
|
|
||||||
export function emptyAddressBlock(): AddressBlockState {
|
export function emptyAddressBlock(): AddressBlockState {
|
||||||
return {
|
return {
|
||||||
commonFqdn: '',
|
commonFqdns: [],
|
||||||
nodes: [],
|
nodes: [],
|
||||||
otherBindings: [],
|
preservedBindings: [],
|
||||||
target_ip_weights: {},
|
target_ip_weights: {},
|
||||||
target_ip_priorities: {},
|
target_ip_priorities: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withPoolIps(
|
|
||||||
draft: ServiceBindingDraft,
|
|
||||||
pool: string[],
|
|
||||||
): ServiceBindingDraft {
|
|
||||||
if (draft.record_type !== 'A' || draft.target_ips.length > 0 || pool.length === 0) {
|
|
||||||
return draft
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...draft,
|
|
||||||
target_ips: pool,
|
|
||||||
target_ip_weights: Object.fromEntries(pool.map((ip) => [ip, draft.target_ip_weights[ip] ?? 1])),
|
|
||||||
target_ip_priorities: Object.fromEntries(
|
|
||||||
pool.map((ip) => [ip, draft.target_ip_priorities[ip] ?? 1]),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniqueIps(...lists: string[][]): string[] {
|
function uniqueIps(...lists: string[][]): string[] {
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const out: string[] = []
|
const out: string[] = []
|
||||||
@@ -123,6 +106,13 @@ function omitKey(record: Record<string, number>, key: string): Record<string, nu
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameIpSet(left: string[], right: string[]): boolean {
|
||||||
|
if (left.length === 0 || left.length !== right.length) return false
|
||||||
|
const set = new Set(left.map((ip) => ip.trim()).filter(Boolean))
|
||||||
|
if (set.size !== left.length) return false
|
||||||
|
return right.every((ip) => set.has(ip.trim()))
|
||||||
|
}
|
||||||
|
|
||||||
export function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] {
|
export function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] {
|
||||||
return (service.domains ?? []).map((binding) => ({
|
return (service.domains ?? []).map((binding) => ({
|
||||||
fqdn: bindingToFqdn(binding),
|
fqdn: bindingToFqdn(binding),
|
||||||
@@ -151,49 +141,62 @@ export function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function canCollapseToNode(
|
function isFullPoolA(draft: ServiceBindingDraft, pool: string[]): boolean {
|
||||||
extra: ServiceBindingDraft,
|
return draft.record_type === 'A' && sameIpSet(draft.target_ips, pool)
|
||||||
pool: Set<string>,
|
|
||||||
claimed: Set<string>,
|
|
||||||
): string | null {
|
|
||||||
if (extra.record_type !== 'A') return null
|
|
||||||
if (extra.target_ips.length !== 1) return null
|
|
||||||
const ip = extra.target_ips[0]?.trim() ?? ''
|
|
||||||
if (!ip || !pool.has(ip) || claimed.has(ip)) return null
|
|
||||||
if (!extra.fqdn.trim()) return null
|
|
||||||
return ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hydrateAddressBlock(
|
export function hydrateAddressBlock(
|
||||||
drafts: ServiceBindingDraft[],
|
drafts: ServiceBindingDraft[],
|
||||||
pool: string[] = [],
|
pool: string[] = [],
|
||||||
): AddressBlockState {
|
): AddressBlockState {
|
||||||
const primary = drafts[0]
|
const multiIpTargets = drafts
|
||||||
const ips = uniqueIps(pool, primary?.target_ips ?? [])
|
.filter((draft) => draft.record_type === 'A' && draft.target_ips.length > 1)
|
||||||
|
.map((draft) => draft.target_ips)
|
||||||
|
const allAIps = drafts
|
||||||
|
.filter((draft) => draft.record_type === 'A')
|
||||||
|
.map((draft) => draft.target_ips)
|
||||||
|
const ips =
|
||||||
|
pool.length > 0
|
||||||
|
? uniqueIps(pool)
|
||||||
|
: uniqueIps(...(multiIpTargets.length > 0 ? multiIpTargets : allAIps))
|
||||||
const poolSet = new Set(ips)
|
const poolSet = new Set(ips)
|
||||||
|
const commonFqdns: string[] = []
|
||||||
const claimed = new Set<string>()
|
const claimed = new Set<string>()
|
||||||
const extraByIp = new Map<string, string>()
|
const extraByIp = new Map<string, string>()
|
||||||
const otherBindings: ServiceBindingDraft[] = []
|
const preservedBindings: ServiceBindingDraft[] = []
|
||||||
|
let weights: Record<string, number> = {}
|
||||||
|
let priorities: Record<string, number> = {}
|
||||||
|
|
||||||
for (const extra of drafts.slice(1)) {
|
for (const draft of drafts) {
|
||||||
const ip = canCollapseToNode(extra, poolSet, claimed)
|
const fqdn = draft.fqdn.trim()
|
||||||
if (ip) {
|
if (isFullPoolA(draft, ips)) {
|
||||||
claimed.add(ip)
|
if (fqdn) commonFqdns.push(draft.fqdn)
|
||||||
extraByIp.set(ip, extra.fqdn)
|
if (Object.keys(weights).length === 0) {
|
||||||
|
weights = { ...draft.target_ip_weights }
|
||||||
|
priorities = { ...draft.target_ip_priorities }
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
otherBindings.push(extra)
|
if (draft.record_type === 'A' && draft.target_ips.length === 1) {
|
||||||
|
const ip = draft.target_ips[0]?.trim() ?? ''
|
||||||
|
if (ip && poolSet.has(ip) && fqdn && !claimed.has(ip)) {
|
||||||
|
claimed.add(ip)
|
||||||
|
extraByIp.set(ip, draft.fqdn)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preservedBindings.push(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commonFqdn: primary?.fqdn ?? '',
|
commonFqdns,
|
||||||
nodes: ips.map((ip) => ({
|
nodes: ips.map((ip) => ({
|
||||||
ip,
|
ip,
|
||||||
extraFqdn: extraByIp.get(ip) ?? '',
|
extraFqdn: extraByIp.get(ip) ?? '',
|
||||||
})),
|
})),
|
||||||
otherBindings,
|
preservedBindings,
|
||||||
target_ip_weights: { ...(primary?.target_ip_weights ?? {}) },
|
target_ip_weights: weights,
|
||||||
target_ip_priorities: { ...(primary?.target_ip_priorities ?? {}) },
|
target_ip_priorities: priorities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +224,7 @@ export function removeAddressNode(state: AddressBlockState, ip: string): Address
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
nodes: state.nodes.filter((node) => node.ip !== ip),
|
nodes: state.nodes.filter((node) => node.ip !== ip),
|
||||||
otherBindings: pruneIpFromBindings(state.otherBindings, ip),
|
preservedBindings: pruneIpFromBindings(state.preservedBindings, ip),
|
||||||
target_ip_weights: omitKey(state.target_ip_weights, ip),
|
target_ip_weights: omitKey(state.target_ip_weights, ip),
|
||||||
target_ip_priorities: omitKey(state.target_ip_priorities, ip),
|
target_ip_priorities: omitKey(state.target_ip_priorities, ip),
|
||||||
}
|
}
|
||||||
@@ -240,6 +243,42 @@ export function addAddressNode(state: AddressBlockState, ip: string): AddressBlo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fqdnKey(value: string): string {
|
||||||
|
return value.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addressHasFqdn(state: AddressBlockState, fqdn: string): boolean {
|
||||||
|
const key = fqdnKey(fqdn)
|
||||||
|
if (!key) return false
|
||||||
|
if (state.commonFqdns.some((item) => fqdnKey(item) === key)) return true
|
||||||
|
if (state.nodes.some((node) => fqdnKey(node.extraFqdn) === key)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addCommonFqdn(state: AddressBlockState, fqdn: string): AddressBlockState {
|
||||||
|
const trimmed = fqdn.trim()
|
||||||
|
if (!trimmed || addressHasFqdn(state, trimmed)) return state
|
||||||
|
return { ...state, commonFqdns: [...state.commonFqdns, trimmed] }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCommonFqdn(state: AddressBlockState, index: number): AddressBlockState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
commonFqdns: state.commonFqdns.filter((_, i) => i !== index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCommonFqdn(
|
||||||
|
state: AddressBlockState,
|
||||||
|
index: number,
|
||||||
|
fqdn: string,
|
||||||
|
): AddressBlockState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
commonFqdns: state.commonFqdns.map((item, i) => (i === index ? fqdn : item)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function toAddressBindings(
|
export function toAddressBindings(
|
||||||
state: AddressBlockState,
|
state: AddressBlockState,
|
||||||
primary: AddressPrimaryMeta,
|
primary: AddressPrimaryMeta,
|
||||||
@@ -253,10 +292,11 @@ export function toAddressBindings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const drafts: ServiceBindingDraft[] = []
|
const drafts: ServiceBindingDraft[] = []
|
||||||
const hasPrimary = Boolean(state.commonFqdn.trim()) || ips.length > 0
|
for (const raw of state.commonFqdns) {
|
||||||
if (hasPrimary) {
|
const fqdn = raw.trim()
|
||||||
|
if (!fqdn || ips.length === 0) continue
|
||||||
drafts.push({
|
drafts.push({
|
||||||
fqdn: state.commonFqdn,
|
fqdn,
|
||||||
record_type: 'A',
|
record_type: 'A',
|
||||||
target_ips: ips,
|
target_ips: ips,
|
||||||
target_cname: '',
|
target_cname: '',
|
||||||
@@ -282,7 +322,7 @@ export function toAddressBindings(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
drafts.push(...state.otherBindings)
|
drafts.push(...state.preservedBindings)
|
||||||
return drafts
|
return drafts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user