From a228febc2720614fb9d20046c294ac86b89ba8fa Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 31 Aug 2026 12:19:53 +0700 Subject: [PATCH] =?UTF-8?q?fix(services):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BF.=20FQDN=20=D1=83?= =?UTF-8?q?=20IP=20=D0=BF=D1=80=D0=B8=20=D0=BF=D1=83=D0=BB=D0=B5=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При одном IP общий и доп. FQDN неотличимы по target_ips; после сохранения гидратация относила оба к общим доменам. Co-authored-by: Cursor --- apps/web/src/lib/service-address.test.ts | 33 ++++++++++++++++ apps/web/src/lib/service-address.ts | 48 +++++++++++++++++++++--- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/apps/web/src/lib/service-address.test.ts b/apps/web/src/lib/service-address.test.ts index 0e6dec3..aee6c50 100644 --- a/apps/web/src/lib/service-address.test.ts +++ b/apps/web/src/lib/service-address.test.ts @@ -84,6 +84,21 @@ describe('hydrateAddressBlock', () => { expect(state.preservedBindings[0]?.fqdn).toBe('edge.example.com') }) + it('при одном IP пула отделяет второй A в extraFqdn узла', () => { + const drafts = [ + aRecord('dns.shnt.top', ['130.49.213.176']), + aRecord('ndns.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', extraFqdn: 'ndns.shnt.top' }, + ]) + expect(state.preservedBindings).toEqual([]) + }) + it('поднимает веса и приоритеты с общего FQDN', () => { const drafts = [ aRecord('gt.rkns.top', ['130.49.213.153', '93.115.203.183'], { @@ -143,6 +158,24 @@ describe('toDomainsPayload', () => { expect(second.nodes).toEqual(first.nodes) expect(second.preservedBindings).toEqual([]) }) + + it('круг hydrate → payload → hydrate сохраняет extra FQDN при одном IP', () => { + const drafts = [ + aRecord('dns.shnt.top', ['130.49.213.176']), + aRecord('ndns.shnt.top', ['130.49.213.176']), + ] + const first = hydrateAddressBlock(drafts, ['130.49.213.176']) + expect(first.commonFqdns).toEqual(['dns.shnt.top']) + expect(first.nodes).toEqual([ + { ip: '130.49.213.176', extraFqdn: 'ndns.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', () => { diff --git a/apps/web/src/lib/service-address.ts b/apps/web/src/lib/service-address.ts index 1689400..9fdc6cf 100644 --- a/apps/web/src/lib/service-address.ts +++ b/apps/web/src/lib/service-address.ts @@ -145,6 +145,23 @@ function isFullPoolA(draft: ServiceBindingDraft, pool: string[]): boolean { return draft.record_type === 'A' && sameIpSet(draft.target_ips, pool) } +function takeAsCommon( + draft: ServiceBindingDraft, + fqdn: string, + commonFqdns: string[], + weights: Record, + priorities: Record, +): { weights: Record; priorities: Record } { + if (fqdn) commonFqdns.push(draft.fqdn) + if (Object.keys(weights).length === 0) { + return { + weights: { ...draft.target_ip_weights }, + priorities: { ...draft.target_ip_priorities }, + } + } + return { weights, priorities } +} + export function hydrateAddressBlock( drafts: ServiceBindingDraft[], pool: string[] = [], @@ -166,15 +183,36 @@ export function hydrateAddressBlock( const preservedBindings: ServiceBindingDraft[] = [] let weights: Record = {} let priorities: Record = {} + const splitSinglePool = + ips.length === 1 && + drafts.filter((draft) => isFullPoolA(draft, ips)).length > 1 + let assignedFirstSinglePoolCommon = false 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 } + if (splitSinglePool && isFullPoolA(draft, ips)) { + if (!assignedFirstSinglePoolCommon) { + assignedFirstSinglePoolCommon = true + const next = takeAsCommon(draft, fqdn, commonFqdns, weights, priorities) + weights = next.weights + priorities = next.priorities + continue } + 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 + } + const overflow = takeAsCommon(draft, fqdn, commonFqdns, weights, priorities) + weights = overflow.weights + priorities = overflow.priorities + continue + } + if (isFullPoolA(draft, ips)) { + const next = takeAsCommon(draft, fqdn, commonFqdns, weights, priorities) + weights = next.weights + priorities = next.priorities continue } if (draft.record_type === 'A' && draft.target_ips.length === 1) {