fix(integrations): фильтровать не-IP и наследовать VPS по сервису
Docker / build (push) Failing after 24s
Docker / build (push) Failing after 24s
CNAME с hostname в ips не матчились; unmatched bindings того же CFDM-сервиса наследуют уникальный VPS. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -67,6 +67,86 @@ describe('vpsDomainsRepository', () => {
|
|||||||
expect(result.unmatched).toBe(1)
|
expect(result.unmatched).toBe(1)
|
||||||
expect(vpsDomainsRepository.listUnmatched()).toHaveLength(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', () => {
|
describe('settingsRepository integration fields', () => {
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ function normalizeHost(host: string): string {
|
|||||||
return host.trim().toLowerCase().replace(/\.+$/, '')
|
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[] {
|
function collectVpsIps(vps: { ip?: string | null; additionalIps?: string[] }): string[] {
|
||||||
const ips: string[] = []
|
const ips: string[] = []
|
||||||
if (vps.ip?.trim()) ips.push(normalizeIp(vps.ip))
|
if (vps.ip?.trim()) ips.push(normalizeIp(vps.ip))
|
||||||
@@ -31,7 +44,9 @@ function findVpsIdByIps(
|
|||||||
allVps: ReturnType<typeof vpsRepository.list>,
|
allVps: ReturnType<typeof vpsRepository.list>,
|
||||||
ips: string[],
|
ips: string[],
|
||||||
): string | null {
|
): 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
|
if (normalized.length === 0) return null
|
||||||
|
|
||||||
const matches: string[] = []
|
const matches: string[] = []
|
||||||
@@ -51,7 +66,7 @@ function findVpsIdByDns(
|
|||||||
hostname: string,
|
hostname: string,
|
||||||
): string | null {
|
): string | null {
|
||||||
const key = normalizeHost(hostname)
|
const key = normalizeHost(hostname)
|
||||||
if (!key) return null
|
if (!key || isIpLiteral(key)) return null
|
||||||
const matches = allVps.filter((v) => normalizeHost(v.dns ?? '') === key)
|
const matches = allVps.filter((v) => normalizeHost(v.dns ?? '') === key)
|
||||||
if (matches.length === 1) return matches[0]!.id
|
if (matches.length === 1) return matches[0]!.id
|
||||||
return null
|
return null
|
||||||
@@ -72,6 +87,20 @@ function resolveMatchStatus(vpsId: string | null): 'matched' | 'unmatched' {
|
|||||||
return vpsId ? '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 = {
|
export const vpsDomainsRepository = {
|
||||||
list(): VpsDomainDto[] {
|
list(): VpsDomainDto[] {
|
||||||
const spaceId = getCurrentSpaceId()
|
const spaceId = getCurrentSpaceId()
|
||||||
@@ -174,6 +203,8 @@ export const vpsDomainsRepository = {
|
|||||||
let upserted = 0
|
let upserted = 0
|
||||||
const keptBindingIds = new Set<number>()
|
const keptBindingIds = new Set<number>()
|
||||||
const fqdnToVpsId = new Map<string, string>()
|
const fqdnToVpsId = new Map<string, string>()
|
||||||
|
const serviceMatchedVps = new Map<number, Set<string>>()
|
||||||
|
const counters = { matched: 0, unmatched: 0 }
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.deleted) {
|
if (item.deleted) {
|
||||||
@@ -186,10 +217,13 @@ export const vpsDomainsRepository = {
|
|||||||
const vpsId = findVpsIdForBinding(allVps, item)
|
const vpsId = findVpsIdForBinding(allVps, item)
|
||||||
const matchStatus = resolveMatchStatus(vpsId)
|
const matchStatus = resolveMatchStatus(vpsId)
|
||||||
if (matchStatus === 'matched') {
|
if (matchStatus === 'matched') {
|
||||||
matched++
|
counters.matched++
|
||||||
fqdnToVpsId.set(normalizeHost(item.fqdn), vpsId!)
|
fqdnToVpsId.set(normalizeHost(item.fqdn), vpsId!)
|
||||||
|
const set = serviceMatchedVps.get(item.serviceId) ?? new Set<string>()
|
||||||
|
set.add(vpsId!)
|
||||||
|
serviceMatchedVps.set(item.serviceId, set)
|
||||||
} else {
|
} else {
|
||||||
unmatched++
|
counters.unmatched++
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = this.getByCfdmBindingId(item.bindingId)
|
const existing = this.getByCfdmBindingId(item.bindingId)
|
||||||
@@ -205,7 +239,7 @@ export const vpsDomainsRepository = {
|
|||||||
cfdmBindingId: item.bindingId,
|
cfdmBindingId: item.bindingId,
|
||||||
source: 'cfdm' as const,
|
source: 'cfdm' as const,
|
||||||
matchStatus,
|
matchStatus,
|
||||||
targetIps: JSON.stringify(item.ips),
|
targetIps: JSON.stringify(item.ips.filter(isIpLiteral)),
|
||||||
syncedAt: now,
|
syncedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +251,7 @@ export const vpsDomainsRepository = {
|
|||||||
upserted++
|
upserted++
|
||||||
}
|
}
|
||||||
|
|
||||||
// CNAME → уже matched FQDN (например imsk → ihome, а ihome привязан по IP/dns)
|
// CNAME → уже matched FQDN (например imsk → ihome)
|
||||||
let inherited = true
|
let inherited = true
|
||||||
while (inherited) {
|
while (inherited) {
|
||||||
inherited = false
|
inherited = false
|
||||||
@@ -227,17 +261,30 @@ export const vpsDomainsRepository = {
|
|||||||
if (!existing || existing.vpsId) continue
|
if (!existing || existing.vpsId) continue
|
||||||
const parentVpsId = fqdnToVpsId.get(normalizeHost(item.cnameTarget))
|
const parentVpsId = fqdnToVpsId.get(normalizeHost(item.cnameTarget))
|
||||||
if (!parentVpsId) continue
|
if (!parentVpsId) continue
|
||||||
db.update(schema.vpsDomains)
|
applyInheritedMatch(db, existing.id, parentVpsId, counters)
|
||||||
.set({ vpsId: parentVpsId, matchStatus: 'matched' })
|
|
||||||
.where(eq(schema.vpsDomains.id, existing.id))
|
|
||||||
.run()
|
|
||||||
fqdnToVpsId.set(normalizeHost(item.fqdn), parentVpsId)
|
fqdnToVpsId.set(normalizeHost(item.fqdn), parentVpsId)
|
||||||
matched++
|
const set = serviceMatchedVps.get(item.serviceId) ?? new Set<string>()
|
||||||
unmatched = Math.max(0, unmatched - 1)
|
set.add(parentVpsId)
|
||||||
|
serviceMatchedVps.set(item.serviceId, set)
|
||||||
inherited = true
|
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) {
|
if (opts?.fullSync) {
|
||||||
const rows = db
|
const rows = db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
Reference in New Issue
Block a user