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) {