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:
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_BINDING_HEALTH,
|
||||
addAddressNode,
|
||||
addCommonFqdn,
|
||||
emptyAddressBlock,
|
||||
emptyBindingDraft,
|
||||
hydrateAddressBlock,
|
||||
@@ -41,15 +42,15 @@ describe('hydrateAddressBlock', () => {
|
||||
|
||||
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([
|
||||
{ ip: '93.115.203.183', extraFqdn: 'msk.rutg.rkns.top' },
|
||||
{ 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 = {
|
||||
...emptyBindingDraft('alias.rkns.top'),
|
||||
record_type: 'CNAME',
|
||||
@@ -63,14 +64,12 @@ describe('hydrateAddressBlock', () => {
|
||||
|
||||
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.otherBindings.map((item) => item.fqdn)).toEqual([
|
||||
'both.rkns.top',
|
||||
'alias.rkns.top',
|
||||
])
|
||||
expect(state.preservedBindings.map((item) => item.fqdn)).toEqual(['alias.rkns.top'])
|
||||
})
|
||||
|
||||
it('кладёт extra A с IP вне пула в otherBindings', () => {
|
||||
it('кладёт extra A с IP вне пула в preservedBindings', () => {
|
||||
const drafts = [
|
||||
aRecord('gw.example.com', ['10.0.0.1']),
|
||||
aRecord('edge.example.com', ['8.8.8.8']),
|
||||
@@ -79,13 +78,14 @@ describe('hydrateAddressBlock', () => {
|
||||
const state = hydrateAddressBlock(drafts, ['10.0.0.1'])
|
||||
|
||||
expect(state.nodes).toEqual([{ ip: '10.0.0.1', extraFqdn: '' }])
|
||||
expect(state.otherBindings).toHaveLength(1)
|
||||
expect(state.otherBindings[0]?.fqdn).toBe('edge.example.com')
|
||||
expect(state.commonFqdns).toEqual(['gw.example.com'])
|
||||
expect(state.preservedBindings).toHaveLength(1)
|
||||
expect(state.preservedBindings[0]?.fqdn).toBe('edge.example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('toDomainsPayload', () => {
|
||||
it('собирает primary на весь пул и extra binding на один IP', () => {
|
||||
it('собирает каждый common на весь пул и extra binding на один IP', () => {
|
||||
const drafts = [
|
||||
aRecord('rutg.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
||||
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 = [
|
||||
aRecord('rutg.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
||||
aRecord('msk.rutg.rkns.top', ['93.115.203.183']),
|
||||
aRecord('gt.rkns.top', ['93.115.203.183', '185.244.181.61']),
|
||||
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'])
|
||||
expect(first.commonFqdns).toEqual(['gt.rkns.top', 'msk.rkns.top'])
|
||||
const rebound = toAddressBindings(first, primaryMeta)
|
||||
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.otherBindings).toEqual([])
|
||||
expect(second.preservedBindings).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeAddressNode', () => {
|
||||
it('удаляет extra FQDN узла и IP из other A-bindings', () => {
|
||||
it('удаляет extra FQDN узла и IP из preserved A-bindings', () => {
|
||||
const state = hydrateAddressBlock(
|
||||
[
|
||||
aRecord('gw.example.com', ['10.0.0.1', '10.0.0.2']),
|
||||
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'],
|
||||
)
|
||||
@@ -136,12 +138,12 @@ describe('removeAddressNode', () => {
|
||||
const next = removeAddressNode(state, '10.0.0.1')
|
||||
|
||||
expect(next.nodes).toEqual([{ ip: '10.0.0.2', extraFqdn: '' }])
|
||||
expect(next.otherBindings).toHaveLength(1)
|
||||
expect(next.otherBindings[0]?.target_ips).toEqual(['10.0.0.2'])
|
||||
expect(next.preservedBindings).toHaveLength(1)
|
||||
expect(next.preservedBindings[0]?.target_ips).toEqual(['9.9.9.9'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('addAddressNode', () => {
|
||||
describe('addAddressNode / addCommonFqdn', () => {
|
||||
it('не добавляет дубликат IP', () => {
|
||||
const withIp = addAddressNode(
|
||||
{ ...emptyAddressBlock(), nodes: [{ ip: '1.1.1.1', extraFqdn: '' }] },
|
||||
@@ -149,23 +151,31 @@ describe('addAddressNode', () => {
|
||||
)
|
||||
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', () => {
|
||||
it('сохраняет CNAME в otherBindings при круге hydrate → payload', () => {
|
||||
describe('CNAME / preservedBindings', () => {
|
||||
it('сохраняет CNAME в preserved при круге hydrate → payload', () => {
|
||||
const cname: ServiceBindingDraft = {
|
||||
...emptyBindingDraft('alias.rkns.top'),
|
||||
record_type: 'CNAME',
|
||||
target_cname: 'rutg.rkns.top',
|
||||
}
|
||||
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']),
|
||||
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.otherBindings).toHaveLength(1)
|
||||
expect(state.preservedBindings).toHaveLength(1)
|
||||
|
||||
const payload = toDomainsPayload(state, primaryMeta)
|
||||
expect(payload.map((item) => item.fqdn)).toEqual([
|
||||
|
||||
@@ -37,9 +37,9 @@ export interface AddressNode {
|
||||
}
|
||||
|
||||
export interface AddressBlockState {
|
||||
commonFqdn: string
|
||||
commonFqdns: string[]
|
||||
nodes: AddressNode[]
|
||||
otherBindings: ServiceBindingDraft[]
|
||||
preservedBindings: ServiceBindingDraft[]
|
||||
target_ip_weights: Record<string, number>
|
||||
target_ip_priorities: Record<string, number>
|
||||
}
|
||||
@@ -78,31 +78,14 @@ export function emptyBindingDraft(fqdn = ''): ServiceBindingDraft {
|
||||
|
||||
export function emptyAddressBlock(): AddressBlockState {
|
||||
return {
|
||||
commonFqdn: '',
|
||||
commonFqdns: [],
|
||||
nodes: [],
|
||||
otherBindings: [],
|
||||
preservedBindings: [],
|
||||
target_ip_weights: {},
|
||||
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[] {
|
||||
const seen = new Set<string>()
|
||||
const out: string[] = []
|
||||
@@ -123,6 +106,13 @@ function omitKey(record: Record<string, number>, key: string): Record<string, nu
|
||||
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[] {
|
||||
return (service.domains ?? []).map((binding) => ({
|
||||
fqdn: bindingToFqdn(binding),
|
||||
@@ -151,49 +141,62 @@ export function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function canCollapseToNode(
|
||||
extra: ServiceBindingDraft,
|
||||
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
|
||||
function isFullPoolA(draft: ServiceBindingDraft, pool: string[]): boolean {
|
||||
return draft.record_type === 'A' && sameIpSet(draft.target_ips, pool)
|
||||
}
|
||||
|
||||
export function hydrateAddressBlock(
|
||||
drafts: ServiceBindingDraft[],
|
||||
pool: string[] = [],
|
||||
): AddressBlockState {
|
||||
const primary = drafts[0]
|
||||
const ips = uniqueIps(pool, primary?.target_ips ?? [])
|
||||
const multiIpTargets = drafts
|
||||
.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 commonFqdns: string[] = []
|
||||
const claimed = new Set<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)) {
|
||||
const ip = canCollapseToNode(extra, poolSet, claimed)
|
||||
if (ip) {
|
||||
claimed.add(ip)
|
||||
extraByIp.set(ip, extra.fqdn)
|
||||
for (const draft of drafts) {
|
||||
const fqdn = draft.fqdn.trim()
|
||||
if (isFullPoolA(draft, ips)) {
|
||||
if (fqdn) commonFqdns.push(draft.fqdn)
|
||||
if (Object.keys(weights).length === 0) {
|
||||
weights = { ...draft.target_ip_weights }
|
||||
priorities = { ...draft.target_ip_priorities }
|
||||
}
|
||||
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 {
|
||||
commonFqdn: primary?.fqdn ?? '',
|
||||
commonFqdns,
|
||||
nodes: ips.map((ip) => ({
|
||||
ip,
|
||||
extraFqdn: extraByIp.get(ip) ?? '',
|
||||
})),
|
||||
otherBindings,
|
||||
target_ip_weights: { ...(primary?.target_ip_weights ?? {}) },
|
||||
target_ip_priorities: { ...(primary?.target_ip_priorities ?? {}) },
|
||||
preservedBindings,
|
||||
target_ip_weights: weights,
|
||||
target_ip_priorities: priorities,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +224,7 @@ export function removeAddressNode(state: AddressBlockState, ip: string): Address
|
||||
return {
|
||||
...state,
|
||||
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_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(
|
||||
state: AddressBlockState,
|
||||
primary: AddressPrimaryMeta,
|
||||
@@ -253,10 +292,11 @@ export function toAddressBindings(
|
||||
)
|
||||
|
||||
const drafts: ServiceBindingDraft[] = []
|
||||
const hasPrimary = Boolean(state.commonFqdn.trim()) || ips.length > 0
|
||||
if (hasPrimary) {
|
||||
for (const raw of state.commonFqdns) {
|
||||
const fqdn = raw.trim()
|
||||
if (!fqdn || ips.length === 0) continue
|
||||
drafts.push({
|
||||
fqdn: state.commonFqdn,
|
||||
fqdn,
|
||||
record_type: 'A',
|
||||
target_ips: ips,
|
||||
target_cname: '',
|
||||
@@ -282,7 +322,7 @@ export function toAddressBindings(
|
||||
})
|
||||
}
|
||||
|
||||
drafts.push(...state.otherBindings)
|
||||
drafts.push(...state.preservedBindings)
|
||||
return drafts
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user