fix(integrations): фильтровать не-IP и наследовать VPS по сервису
Docker / build (push) Failing after 24s

CNAME с hostname в ips не матчились; unmatched bindings того же CFDM-сервиса наследуют уникальный VPS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-08-01 01:30:26 +07:00
co-authored by Cursor
parent 619385981c
commit 0177f5d539
2 changed files with 139 additions and 12 deletions
@@ -67,6 +67,86 @@ describe('vpsDomainsRepository', () => {
expect(result.unmatched).toBe(1)
expect(vpsDomainsRepository.listUnmatched()).toHaveLength(1)
})
it('привязывает CNAME по cnameTarget → VPS.dns', () => {
const vps = vpsRepository.create({
ip: '203.0.113.20',
dns: 'ihome.rkns.top',
providerId: 'p1',
providerAccountId: 'a1',
status: 'active',
tariffType: 'monthly',
currency: 'RUB',
vcpu: 1,
ramGb: 1,
diskGb: 10,
})
const created = Array.isArray(vps) ? vps[0]! : vps
const result = vpsDomainsRepository.syncBindings([
{
bindingId: 3,
serviceId: 12,
serviceName: 'MSK IHOR Node',
serviceSlug: 'msk-ihor',
fqdn: 'imsk.rkns.top',
zoneName: 'rkns.top',
hostname: 'imsk',
ips: ['ihome.rkns.top'],
cnameTarget: 'ihome.rkns.top',
},
])
expect(result.matched).toBe(1)
expect(result.unmatched).toBe(0)
const domains = vpsDomainsRepository.listByVpsId(created.id)
expect(domains[0]?.fqdn).toBe('imsk.rkns.top')
expect(domains[0]?.matchStatus).toBe('matched')
})
it('наследует VPS от sibling binding того же сервиса', () => {
const vps = vpsRepository.create({
ip: '203.0.113.30',
providerId: 'p1',
providerAccountId: 'a1',
status: 'active',
tariffType: 'monthly',
currency: 'RUB',
vcpu: 1,
ramGb: 1,
diskGb: 10,
})
const created = Array.isArray(vps) ? vps[0]! : vps
const result = vpsDomainsRepository.syncBindings([
{
bindingId: 4,
serviceId: 20,
serviceName: 'MSK Macloud',
serviceSlug: 'msk-macloud',
fqdn: 'home.rkns.top',
zoneName: 'rkns.top',
hostname: 'home',
ips: ['203.0.113.30'],
},
{
bindingId: 5,
serviceId: 20,
serviceName: 'MSK Macloud',
serviceSlug: 'msk-macloud',
fqdn: 'mhome.rkns.top',
zoneName: 'rkns.top',
hostname: 'mhome',
ips: [],
cnameTarget: 'somewhere.else',
},
])
expect(result.matched).toBe(2)
expect(result.unmatched).toBe(0)
const domains = vpsDomainsRepository.listByVpsId(created.id)
expect(domains.map((d) => d.fqdn).sort()).toEqual(['home.rkns.top', 'mhome.rkns.top'])
})
})
describe('settingsRepository integration fields', () => {
+59 -12
View File
@@ -18,6 +18,19 @@ function normalizeHost(host: string): string {
return host.trim().toLowerCase().replace(/\.+$/, '')
}
function isIpLiteral(value: string): boolean {
const v = value.trim()
if (!v) return false
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(v)) {
return v.split('.').every((p) => {
const n = Number(p)
return Number.isInteger(n) && n >= 0 && n <= 255
})
}
// грубый IPv6 — отсекает hostname вроде ihome.rkns.top
return v.includes(':') && !v.includes(' ')
}
function collectVpsIps(vps: { ip?: string | null; additionalIps?: string[] }): string[] {
const ips: string[] = []
if (vps.ip?.trim()) ips.push(normalizeIp(vps.ip))
@@ -31,7 +44,9 @@ function findVpsIdByIps(
allVps: ReturnType<typeof vpsRepository.list>,
ips: string[],
): string | null {
const normalized = [...new Set(ips.map(normalizeIp).filter(Boolean))]
const normalized = [
...new Set(ips.map(normalizeIp).filter((ip) => ip && isIpLiteral(ip))),
]
if (normalized.length === 0) return null
const matches: string[] = []
@@ -51,7 +66,7 @@ function findVpsIdByDns(
hostname: string,
): string | null {
const key = normalizeHost(hostname)
if (!key) return null
if (!key || isIpLiteral(key)) return null
const matches = allVps.filter((v) => normalizeHost(v.dns ?? '') === key)
if (matches.length === 1) return matches[0]!.id
return null
@@ -72,6 +87,20 @@ function resolveMatchStatus(vpsId: string | null): 'matched' | 'unmatched' {
return vpsId ? 'matched' : 'unmatched'
}
function applyInheritedMatch(
db: ReturnType<typeof getDb>,
rowId: string,
vpsId: string,
counters: { matched: number; unmatched: number },
): void {
db.update(schema.vpsDomains)
.set({ vpsId, matchStatus: 'matched' })
.where(eq(schema.vpsDomains.id, rowId))
.run()
counters.matched++
counters.unmatched = Math.max(0, counters.unmatched - 1)
}
export const vpsDomainsRepository = {
list(): VpsDomainDto[] {
const spaceId = getCurrentSpaceId()
@@ -174,6 +203,8 @@ export const vpsDomainsRepository = {
let upserted = 0
const keptBindingIds = new Set<number>()
const fqdnToVpsId = new Map<string, string>()
const serviceMatchedVps = new Map<number, Set<string>>()
const counters = { matched: 0, unmatched: 0 }
for (const item of items) {
if (item.deleted) {
@@ -186,10 +217,13 @@ export const vpsDomainsRepository = {
const vpsId = findVpsIdForBinding(allVps, item)
const matchStatus = resolveMatchStatus(vpsId)
if (matchStatus === 'matched') {
matched++
counters.matched++
fqdnToVpsId.set(normalizeHost(item.fqdn), vpsId!)
const set = serviceMatchedVps.get(item.serviceId) ?? new Set<string>()
set.add(vpsId!)
serviceMatchedVps.set(item.serviceId, set)
} else {
unmatched++
counters.unmatched++
}
const existing = this.getByCfdmBindingId(item.bindingId)
@@ -205,7 +239,7 @@ export const vpsDomainsRepository = {
cfdmBindingId: item.bindingId,
source: 'cfdm' as const,
matchStatus,
targetIps: JSON.stringify(item.ips),
targetIps: JSON.stringify(item.ips.filter(isIpLiteral)),
syncedAt: now,
}
@@ -217,7 +251,7 @@ export const vpsDomainsRepository = {
upserted++
}
// CNAME → уже matched FQDN (например imsk → ihome, а ihome привязан по IP/dns)
// CNAME → уже matched FQDN (например imsk → ihome)
let inherited = true
while (inherited) {
inherited = false
@@ -227,17 +261,30 @@ export const vpsDomainsRepository = {
if (!existing || existing.vpsId) continue
const parentVpsId = fqdnToVpsId.get(normalizeHost(item.cnameTarget))
if (!parentVpsId) continue
db.update(schema.vpsDomains)
.set({ vpsId: parentVpsId, matchStatus: 'matched' })
.where(eq(schema.vpsDomains.id, existing.id))
.run()
applyInheritedMatch(db, existing.id, parentVpsId, counters)
fqdnToVpsId.set(normalizeHost(item.fqdn), parentVpsId)
matched++
unmatched = Math.max(0, unmatched - 1)
const set = serviceMatchedVps.get(item.serviceId) ?? new Set<string>()
set.add(parentVpsId)
serviceMatchedVps.set(item.serviceId, set)
inherited = true
}
}
// Sibling bindings того же CFDM-сервиса → один уникальный VPS
for (const item of items) {
if (item.deleted) continue
const existing = this.getByCfdmBindingId(item.bindingId)
if (!existing || existing.vpsId) continue
const matched = serviceMatchedVps.get(item.serviceId)
if (!matched || matched.size !== 1) continue
const siblingVpsId = [...matched][0]!
applyInheritedMatch(db, existing.id, siblingVpsId, counters)
fqdnToVpsId.set(normalizeHost(item.fqdn), siblingVpsId)
}
matched = counters.matched
unmatched = counters.unmatched
if (opts?.fullSync) {
const rows = db
.select()