🌳
pt0/deployF/testsF/nodePortValidationAI.mts
5import { basename } from 'path'
6import { type absFileDirPath } from '../../ptDirF.mts'
7import { portInOpenA, type OpenPortNoA } from '../../sharedF/portInOpenAAI.mts'
9import { type AcornNode } from '../acornF/libAcornF.mts'
10import * as _ from 'lodash-es'
12export type NodePortAssign = {
13 cluster: string
14 svcName: string
15 port: number
16 ptPath: string
19const isNodePortFile = (ptPath: string) => {
20 const parts = ptPath.split('/')
21 const dir = parts[parts.length - 2]
22 return (dir === 'nodeports' || dir === 'nodeportsF') && ptPath.endsWith('.mjs') && !ptPath.endsWith('index.mjs')
25const rangeConstNameA = ['openPortNoA', 'routerFwdPortNoA'] as const
26type RangeConstName = typeof rangeConstNameA[number]
27export type ClusterPortRanges = {cluster: string; ptPath: string} & Partial<Record<RangeConstName, OpenPortNoA>>
29const evalPortRangeArray = (node: AcornNode): OpenPortNoA => {
30 const out: OpenPortNoA = []
31 for (const el of node.elements ?? []) {
32 if (el?.type === 'Literal' && typeof el.value === 'number') out.push(el.value)
33 else if (el?.type === 'ArrayExpression') {
34 const nums = (el.elements ?? []).filter((e: AcornNode) => e?.type === 'Literal' && typeof e.value === 'number').map((e: AcornNode) => e.value as number)
35 if (nums.length === 2) out.push([nums[0], nums[1]])
36 }
37 }
38 return out
41type ParsedNodePortFile = {assigns: NodePortAssign[]; ranges: ClusterPortRanges}
43const parseNodePortFile = async (ptPath: string): Promise<ParsedNodePortFile> => {
44 const content = await read1File(`${ptDir}/${ptPath}` as absFileDirPath)
45 const cluster = basename(ptPath, '.mjs')
46 const assigns: NodePortAssign[] = []
47 const ranges: ClusterPortRanges = {cluster, ptPath}
48 const ast = acornParse({contents: content, ptPath: ptPath as absFileDirPath}) as unknown as AcornNode
49 for (const stmt of ast.body ?? []) {
50 if (stmt.type !== 'ExportNamedDeclaration' || stmt.declaration?.type !== 'VariableDeclaration') continue
51 for (const decl of stmt.declaration.declarations ?? []) {
52 if (decl.id?.type !== 'Identifier' || !decl.init) continue
53 const name = decl.id.name as string
54 const init = decl.init
55 if (init.type === 'Literal' && typeof init.value === 'number') {
56 assigns.push({cluster, svcName: name, port: init.value, ptPath})
57 } else if (init.type === 'ObjectExpression') {
58 const portProp = (init.properties ?? []).find((p: AcornNode) => p?.type === 'Property' && p.key?.type === 'Identifier' && p.key.name === 'ptNodePortNo')
59 if (portProp?.value?.type === 'Literal' && typeof portProp.value.value === 'number') {
60 assigns.push({cluster, svcName: name, port: portProp.value.value, ptPath})
61 }
62 } else if (init.type === 'ArrayExpression' && (rangeConstNameA as readonly string[]).includes(name)) {
63 ranges[name as RangeConstName] = evalPortRangeArray(init)
64 }
65 }
66 }
67 return {assigns, ranges}
70const collectParsed = async (): Promise<ParsedNodePortFile[]> => {
71 const allFiles = await lsFilePathsRec(ptDir as absFileDirPath)
72 const nodePortFiles = allFiles.map(absPathToPtPath).filter(isNodePortFile)
73 return Promise.all(nodePortFiles.map(parseNodePortFile))
76export const collectNodePorts = async (): Promise<NodePortAssign[]> => (await collectParsed()).flatMap(p => p.assigns)
78export const collectPortRanges = async (): Promise<ClusterPortRanges[]> => (await collectParsed()).map(p => p.ranges)
80export type PortDupe = {
81 port: number
82 entries: { cluster: string; svcName: string; ptPath: string }[]
85export const findWithinClusterDupes = (assigns: NodePortAssign[]): PortDupe[] => {
86 const dupes: PortDupe[] = []
87 for (const cluster of _.uniq(assigns.map(a => a.cluster))) {
88 const byPort = _.groupBy(assigns.filter(a => a.cluster === cluster), 'port')
89 for (const [port, group] of Object.entries(byPort)) {
90 const distinctSvcs = _.uniqBy(group, 'svcName')
91 if (distinctSvcs.length > 1) {
92 dupes.push({port: parseInt(port), entries: distinctSvcs.map(e => ({cluster: e.cluster, svcName: e.svcName, ptPath: e.ptPath}))})
93 }
94 }
95 }
96 return dupes
99export const findCrossClusterDupes = (assigns: NodePortAssign[]): PortDupe[] => {
100 const byPort = _.groupBy(assigns, 'port')
101 const dupes: PortDupe[] = []
102 for (const [port, group] of Object.entries(byPort)) {
103 const distinctClusters = _.uniq(group.map(a => a.cluster))
104 if (distinctClusters.length > 1) {
105 dupes.push({port: parseInt(port), entries: group.map(e => ({cluster: e.cluster, svcName: e.svcName, ptPath: e.ptPath}))})
106 }
107 }
108 return dupes
111export type OpenPortViol = { cluster: string; ptPath: string; detail: string }
113export const findOpenNotInFwd = (ranges: ClusterPortRanges[]): OpenPortViol[] => {
114 const viols: OpenPortViol[] = []
115 for (const {cluster, ptPath, openPortNoA, routerFwdPortNoA} of ranges) {
116 if (!openPortNoA) continue
117 if (!routerFwdPortNoA) { viols.push({cluster, ptPath, detail: 'openPortNoA declared but routerFwdPortNoA missing'}); continue }
118 const uncovered: number[] = []
119 for (const item of openPortNoA) {
120 const [lo, hi] = Array.isArray(item) ? item : [item, item]
121 for (let p = lo!; p <= hi!; p++) if (!portInOpenA(p, routerFwdPortNoA)) uncovered.push(p)
122 }
123 if (uncovered.length) viols.push({cluster, ptPath, detail: `${uncovered.length} port(s) in openPortNoA not forwarded by router: ${uncovered.slice(0, 10).join(',')}${uncovered.length > 10 ? '…' : ''}`})
124 }
125 return viols