@nitra/cursor 1.8.144 → 1.8.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +1 -0
- package/bin/rename-yaml-extensions.mjs +0 -1
- package/mdc/js-bun-db.mdc +118 -0
- package/mdc/js-lint.mdc +6 -4
- package/mdc/js-mssql.mdc +27 -1
- package/mdc/k8s.mdc +2 -2
- package/mdc/vue.mdc +3 -3
- package/package.json +3 -3
- package/scripts/auto-rules.mjs +104 -26
- package/scripts/check-ga.mjs +27 -15
- package/scripts/check-js-bun-db.mjs +213 -0
- package/scripts/check-js-lint.mjs +110 -27
- package/scripts/check-js-mssql.mjs +156 -86
- package/scripts/check-k8s.mjs +161 -32
- package/scripts/check-nginx-default-tpl.mjs +1 -1
- package/scripts/check-vue.mjs +82 -45
- package/scripts/cli-entry.mjs +2 -5
- package/scripts/upgrade-nitra-cursor-and-install.mjs +1 -1
- package/scripts/utils/bun-sql-scan.mjs +294 -0
- package/scripts/utils/mssql-pool-scan.mjs +188 -1
- package/scripts/utils/oxlint-canonical-skeleton.json +27 -0
- package/scripts/utils/oxlint-canonical.json +387 -0
- package/scripts/utils/oxlint-rules.tsv +359 -0
- package/scripts/utils/rebuild-oxlint-canonical.mjs +29 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
findSharedMssqlRequestInText,
|
|
19
19
|
findUnsafeMssqlQueryTemplateCallInText,
|
|
20
20
|
findUnsafeMssqlDynamicSqlListInText,
|
|
21
|
+
findUnsafeMssqlInListUnparsedInText,
|
|
21
22
|
isMssqlScanSourceFile
|
|
22
23
|
} from './utils/mssql-pool-scan.mjs'
|
|
23
24
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -100,8 +101,8 @@ function parseLeadingSemver(range) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
/**
|
|
103
|
-
* @param {{ major: number, minor: number, patch: number }} a
|
|
104
|
-
* @param {{ major: number, minor: number, patch: number }} b
|
|
104
|
+
* @param {{ major: number, minor: number, patch: number }} a перша semver
|
|
105
|
+
* @param {{ major: number, minor: number, patch: number }} b друга semver
|
|
105
106
|
* @returns {boolean} true, якщо a >= b
|
|
106
107
|
*/
|
|
107
108
|
function semverGte(a, b) {
|
|
@@ -110,6 +111,153 @@ function semverGte(a, b) {
|
|
|
110
111
|
return a.patch >= b.patch
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Аудит одного package.json на dependencies.mssql: версія має бути >=12.5.0.
|
|
116
|
+
* Повертає інкремент для лічильників `{ found, bad }`.
|
|
117
|
+
* @param {string} rel шлях у людино-читабельному вигляді
|
|
118
|
+
* @param {unknown} parsed розпарений package.json
|
|
119
|
+
* @param {(msg: string) => void} pass pass callback
|
|
120
|
+
* @param {(msg: string) => void} fail fail callback
|
|
121
|
+
* @returns {{ found: 0 | 1, bad: 0 | 1 }} прирости лічильників
|
|
122
|
+
*/
|
|
123
|
+
function auditMssqlVersionInPackageJson(rel, parsed, pass, fail) {
|
|
124
|
+
const range = getMssqlDependencyRange(asObject(parsed).dependencies)
|
|
125
|
+
if (!range) return { found: 0, bad: 0 }
|
|
126
|
+
|
|
127
|
+
const parsedVer = parseLeadingSemver(range)
|
|
128
|
+
if (!parsedVer) {
|
|
129
|
+
fail(`js-mssql: ${rel}: dependencies.mssql має нечитабельну версію: ${JSON.stringify(range)} (js-mssql.mdc)`)
|
|
130
|
+
return { found: 1, bad: 1 }
|
|
131
|
+
}
|
|
132
|
+
if (semverGte(parsedVer, MIN_MSSQL_VERSION)) {
|
|
133
|
+
pass(`js-mssql: ${rel}: dependencies.mssql ${JSON.stringify(range)} (>=12.5.0)`)
|
|
134
|
+
return { found: 1, bad: 0 }
|
|
135
|
+
}
|
|
136
|
+
fail(`js-mssql: ${rel}: dependencies.mssql ${JSON.stringify(range)} — має бути >=12.5.0 (js-mssql.mdc)`)
|
|
137
|
+
return { found: 1, bad: 1 }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Прогін усіх package.json: рахує знайдені mssql і ті, що не задовольняють мінімум.
|
|
142
|
+
* @param {string} repoRoot корінь репозиторію
|
|
143
|
+
* @param {string[]} pkgJsonPaths абсолютні шляхи до package.json
|
|
144
|
+
* @param {(msg: string) => void} pass pass callback
|
|
145
|
+
* @param {(msg: string) => void} fail fail callback
|
|
146
|
+
* @returns {Promise<{ found: number, bad: number }>} підсумкові лічильники
|
|
147
|
+
*/
|
|
148
|
+
async function aggregateMssqlVersionsAcrossPackages(repoRoot, pkgJsonPaths, pass, fail) {
|
|
149
|
+
let found = 0
|
|
150
|
+
let bad = 0
|
|
151
|
+
for (const absPath of pkgJsonPaths) {
|
|
152
|
+
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
153
|
+
let parsed
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(await readFile(absPath, 'utf8'))
|
|
156
|
+
} catch {
|
|
157
|
+
fail(`js-mssql: ${rel} — невалідний JSON`)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
const inc = auditMssqlVersionInPackageJson(rel, parsed, pass, fail)
|
|
161
|
+
found += inc.found
|
|
162
|
+
bad += inc.bad
|
|
163
|
+
}
|
|
164
|
+
return { found, bad }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Підрахунок порушень у одному файлі джерела mssql.
|
|
169
|
+
* Кожен лічильник інкрементується відповідним сканером, повідомлення йдуть у `fail`.
|
|
170
|
+
* @param {string} rel relative-шлях файлу
|
|
171
|
+
* @param {string} content вихідний код
|
|
172
|
+
* @param {Record<string, number>} counters агрегатор лічильників
|
|
173
|
+
* @param {(msg: string) => void} fail fail callback
|
|
174
|
+
*/
|
|
175
|
+
function scanMssqlOneSourceFile(rel, content, counters, fail) {
|
|
176
|
+
for (const v of findMssqlPerRequestConnectionInText(content, rel)) {
|
|
177
|
+
counters.violations++
|
|
178
|
+
fail(
|
|
179
|
+
`js-mssql: ${rel}:${v.line} — не створюй new sql.ConnectionPool(...) на кожен запит; використовуй singleton sql.ConnectionPool: ${v.snippet}`
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
for (const v of findSharedMssqlRequestInText(content, rel)) {
|
|
183
|
+
counters.sharedRequestViolations++
|
|
184
|
+
fail(
|
|
185
|
+
`js-mssql: ${rel}:${v.line} — заборонено шарити Request (наприклад export const request = pool.request()); створюй pool.request() щоразу заново (js-mssql.mdc): ${v.snippet}`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
for (const v of findUnsafeMssqlQueryTemplateCallInText(content, rel)) {
|
|
189
|
+
counters.unsafeQueryCalls++
|
|
190
|
+
fail(
|
|
191
|
+
`js-mssql: ${rel}:${v.line} — заборонено query(\`...\`): це не tagged template; використовуй pool.request().query\`...\` (js-mssql.mdc): ${v.snippet}`
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
for (const v of findUnsafeMssqlDynamicSqlListInText(content, rel)) {
|
|
195
|
+
counters.unsafeDynamicSqlLists++
|
|
196
|
+
fail(
|
|
197
|
+
`js-mssql: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') (типово IN (...) / VALUES (...)); використовуй TVP (sql.Table) + JOIN/INSERT (js-mssql.mdc): ${v.snippet}`
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
for (const v of findUnsafeMssqlInListUnparsedInText(content, rel)) {
|
|
201
|
+
counters.unparsedInLists++
|
|
202
|
+
fail(
|
|
203
|
+
`js-mssql: ${rel}:${v.line} — у SQL IN (\${...}) значення мають бути попередньо приведені числовим парсером (parseInt/Number/BigInt/parseFloat) і відфільтровані від NaN, інакше можливий SQL injection (js-mssql.mdc): ${v.snippet}`
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Звіт про відсутність порушень у джерелах mssql: кожен лічильник із 0 → один pass-рядок.
|
|
210
|
+
* @param {Record<string, number>} counters лічильники після проходу всіх файлів
|
|
211
|
+
* @param {(msg: string) => void} pass pass callback
|
|
212
|
+
*/
|
|
213
|
+
function reportZeroMssqlSourceViolations(counters, pass) {
|
|
214
|
+
if (counters.violations === 0) {
|
|
215
|
+
pass('js-mssql: немає створення new sql.ConnectionPool(...) всередині функцій (singleton pool)')
|
|
216
|
+
}
|
|
217
|
+
if (counters.sharedRequestViolations === 0) {
|
|
218
|
+
pass('js-mssql: немає shared Request (export const request = pool.request())')
|
|
219
|
+
}
|
|
220
|
+
if (counters.unsafeQueryCalls === 0) {
|
|
221
|
+
pass('js-mssql: немає небезпечних викликів query(`...`) (потрібно query`...`)')
|
|
222
|
+
}
|
|
223
|
+
if (counters.unsafeDynamicSqlLists === 0) {
|
|
224
|
+
pass("js-mssql: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
|
|
225
|
+
}
|
|
226
|
+
if (counters.unparsedInLists === 0) {
|
|
227
|
+
pass(`js-mssql: немає підстановок IN (\${...}) без числового парсера значень`)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Аудит усіх JS/TS-джерел репо щодо безпечного використання mssql.
|
|
233
|
+
* @param {string} repoRoot корінь репозиторію
|
|
234
|
+
* @param {(msg: string) => void} pass pass callback
|
|
235
|
+
* @param {(msg: string) => void} fail fail callback
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async function auditMssqlSources(repoRoot, pass, fail) {
|
|
239
|
+
const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot)
|
|
240
|
+
if (sourcePaths.length === 0) {
|
|
241
|
+
pass('js-mssql: немає JS/TS файлів для скану singleton ConnectionPool')
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const counters = {
|
|
246
|
+
violations: 0,
|
|
247
|
+
sharedRequestViolations: 0,
|
|
248
|
+
unsafeQueryCalls: 0,
|
|
249
|
+
unsafeDynamicSqlLists: 0,
|
|
250
|
+
unparsedInLists: 0
|
|
251
|
+
}
|
|
252
|
+
for (const absPath of sourcePaths) {
|
|
253
|
+
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
254
|
+
const content = await readFile(absPath, 'utf8')
|
|
255
|
+
scanMssqlOneSourceFile(rel, content, counters, fail)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
reportZeroMssqlSourceViolations(counters, pass)
|
|
259
|
+
}
|
|
260
|
+
|
|
113
261
|
/**
|
|
114
262
|
* Перевіряє відповідність проєкту правилу js-mssql.mdc
|
|
115
263
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -119,8 +267,7 @@ export async function check() {
|
|
|
119
267
|
const { pass, fail } = reporter
|
|
120
268
|
|
|
121
269
|
const repoRoot = process.cwd()
|
|
122
|
-
|
|
123
|
-
if (!existsSync(rootPkg)) {
|
|
270
|
+
if (!existsSync(join(repoRoot, 'package.json'))) {
|
|
124
271
|
pass('js-mssql: package.json у корені відсутній — перевірку пропущено')
|
|
125
272
|
return reporter.getExitCode()
|
|
126
273
|
}
|
|
@@ -131,94 +278,17 @@ export async function check() {
|
|
|
131
278
|
return reporter.getExitCode()
|
|
132
279
|
}
|
|
133
280
|
|
|
134
|
-
|
|
135
|
-
let bad = 0
|
|
136
|
-
for (const absPath of pkgJsonPaths) {
|
|
137
|
-
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
138
|
-
let parsed
|
|
139
|
-
try {
|
|
140
|
-
parsed = JSON.parse(await readFile(absPath, 'utf8'))
|
|
141
|
-
} catch {
|
|
142
|
-
fail(`js-mssql: ${rel} — невалідний JSON`)
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
const range = getMssqlDependencyRange(parsed.dependencies)
|
|
146
|
-
if (range) {
|
|
147
|
-
found++
|
|
148
|
-
const parsedVer = parseLeadingSemver(range)
|
|
149
|
-
if (!parsedVer) {
|
|
150
|
-
bad++
|
|
151
|
-
fail(`js-mssql: ${rel}: dependencies.mssql має нечитабельну версію: ${JSON.stringify(range)} (js-mssql.mdc)`)
|
|
152
|
-
continue
|
|
153
|
-
}
|
|
154
|
-
if (semverGte(parsedVer, MIN_MSSQL_VERSION)) {
|
|
155
|
-
pass(`js-mssql: ${rel}: dependencies.mssql ${JSON.stringify(range)} (>=12.5.0)`)
|
|
156
|
-
} else {
|
|
157
|
-
bad++
|
|
158
|
-
fail(`js-mssql: ${rel}: dependencies.mssql ${JSON.stringify(range)} — має бути >=12.5.0 (js-mssql.mdc)`)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
281
|
+
const { found, bad } = await aggregateMssqlVersionsAcrossPackages(repoRoot, pkgJsonPaths, pass, fail)
|
|
162
282
|
|
|
163
283
|
if (found === 0) {
|
|
164
284
|
pass('js-mssql: пакет mssql не знайдено в dependencies жодного package.json')
|
|
165
|
-
|
|
285
|
+
return reporter.getExitCode()
|
|
286
|
+
}
|
|
287
|
+
if (bad === 0) {
|
|
166
288
|
pass(`js-mssql: всі знайдені dependencies.mssql відповідають мінімальній версії 12.5.0 (${found})`)
|
|
167
289
|
}
|
|
168
290
|
|
|
169
|
-
|
|
170
|
-
const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot)
|
|
171
|
-
if (sourcePaths.length === 0) {
|
|
172
|
-
pass('js-mssql: немає JS/TS файлів для скану singleton ConnectionPool')
|
|
173
|
-
return reporter.getExitCode()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let violations = 0
|
|
177
|
-
let sharedRequestViolations = 0
|
|
178
|
-
let unsafeQueryCalls = 0
|
|
179
|
-
let unsafeDynamicSqlLists = 0
|
|
180
|
-
for (const absPath of sourcePaths) {
|
|
181
|
-
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
182
|
-
const content = await readFile(absPath, 'utf8')
|
|
183
|
-
for (const v of findMssqlPerRequestConnectionInText(content, rel)) {
|
|
184
|
-
violations++
|
|
185
|
-
fail(
|
|
186
|
-
`js-mssql: ${rel}:${v.line} — не створюй new sql.ConnectionPool(...) на кожен запит; використовуй singleton sql.ConnectionPool: ${v.snippet}`
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
for (const v of findSharedMssqlRequestInText(content, rel)) {
|
|
190
|
-
sharedRequestViolations++
|
|
191
|
-
fail(
|
|
192
|
-
`js-mssql: ${rel}:${v.line} — заборонено шарити Request (наприклад export const request = pool.request()); створюй pool.request() щоразу заново (js-mssql.mdc): ${v.snippet}`
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
for (const v of findUnsafeMssqlQueryTemplateCallInText(content, rel)) {
|
|
196
|
-
unsafeQueryCalls++
|
|
197
|
-
fail(
|
|
198
|
-
`js-mssql: ${rel}:${v.line} — заборонено query(\`...\`): це не tagged template; використовуй pool.request().query\`...\` (js-mssql.mdc): ${v.snippet}`
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
for (const v of findUnsafeMssqlDynamicSqlListInText(content, rel)) {
|
|
202
|
-
unsafeDynamicSqlLists++
|
|
203
|
-
fail(
|
|
204
|
-
`js-mssql: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') (типово IN (...) / VALUES (...)); використовуй TVP (sql.Table) + JOIN/INSERT (js-mssql.mdc): ${v.snippet}`
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (violations === 0) {
|
|
210
|
-
pass('js-mssql: немає створення new sql.ConnectionPool(...) всередині функцій (singleton pool)')
|
|
211
|
-
}
|
|
212
|
-
if (sharedRequestViolations === 0) {
|
|
213
|
-
pass('js-mssql: немає shared Request (export const request = pool.request())')
|
|
214
|
-
}
|
|
215
|
-
if (unsafeQueryCalls === 0) {
|
|
216
|
-
pass('js-mssql: немає небезпечних викликів query(`...`) (потрібно query`...`)')
|
|
217
|
-
}
|
|
218
|
-
if (unsafeDynamicSqlLists === 0) {
|
|
219
|
-
pass("js-mssql: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
|
|
220
|
-
}
|
|
221
|
-
}
|
|
291
|
+
await auditMssqlSources(repoRoot, pass, fail)
|
|
222
292
|
|
|
223
293
|
return reporter.getExitCode()
|
|
224
294
|
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -67,9 +67,10 @@
|
|
|
67
67
|
* **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`** (приймається булеве `true`
|
|
68
68
|
* або рядок `"true"`, без регістрової залежності).
|
|
69
69
|
*
|
|
70
|
-
* **HPA / PDB / topologySpreadConstraints для кожного Deployment
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* **HPA / PDB / topologySpreadConstraints:** для кожного **`Deployment`** у шарі **`…/k8s/…/base/`** (будь-який
|
|
71
|
+
* `.yaml` у цьому каталозі, наприклад **`deploy.yaml`**, **`deployment.yaml`**) у тому ж каталозі поруч обов'язкові
|
|
72
|
+
* **`hpa.yaml`**, **`pdb.yaml`** та канонічні **topologySpreadConstraints**. Workload-и без Deployment (**CronJob**
|
|
73
|
+
* тощо) та каталоги поза **`…/base/`** цим блоком не охоплюються.
|
|
73
74
|
* Env-залежні межі за сегментом після `/k8s/`: **dev-like** (`base`, `dev`, `*-qa`) — `minReplicas === 1`,
|
|
74
75
|
* `maxReplicas === 1`, `minAvailable === 0`; **прод** (решта) — `minReplicas >= 2`, `maxReplicas >= 2`,
|
|
75
76
|
* `minAvailable >= 1`. Сам Deployment має мати у `spec.template.spec.topologySpreadConstraints` запис
|
|
@@ -88,11 +89,11 @@
|
|
|
88
89
|
* `replacements[].path` має вказувати на наявний у репозиторії файл (`.yaml` / `.yml`) або каталог; інакше
|
|
89
90
|
* помилка `check k8s` (k8s.mdc).
|
|
90
91
|
*
|
|
91
|
-
* **HPA / PDB тільки з Deployment у
|
|
92
|
+
* **HPA / PDB тільки з Deployment у шарі base:** у дереві Kustomize з `…/k8s/…/base/kustomization.yaml` не
|
|
92
93
|
* дозволяти `HorizontalPodAutoscaler` / `PodDisruptionBudget` у `resources` / `bases` / `components` / `crds`
|
|
93
|
-
* (рекурсивно), якщо в цьому ж дереві немає
|
|
94
|
-
* каталог `…/k8s/…/base`, не додавай окремі YAML-файли з HPA / PDB,
|
|
95
|
-
* не з’явиться
|
|
94
|
+
* (рекурсивно), якщо в цьому ж дереві немає документа **`Deployment`** у жодному YAML під **`…/k8s/…/base/`**. У
|
|
95
|
+
* `kustomization.yaml` overlay, який підключає каталог `…/k8s/…/base`, не додавай окремі YAML-файли з HPA / PDB,
|
|
96
|
+
* поки в наслідуваному `base` у дереві не з’явиться такий Deployment (k8s.mdc).
|
|
96
97
|
*/
|
|
97
98
|
import { existsSync } from 'node:fs'
|
|
98
99
|
import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
@@ -118,6 +119,21 @@ const HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES = new Set([
|
|
|
118
119
|
`docker.io/${HASURA_GRAPHQL_ENGINE_IMAGE}`
|
|
119
120
|
])
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Чи відносний POSIX-шлях від кореня репо вказує на YAML під **`…/k8s/…/base/…`** (після сегмента **`k8s`** у шляху
|
|
124
|
+
* є каталог **`base`**). Тут очікуються маніфести шару **base**, включно з будь-яким файлом із **`kind: Deployment`**
|
|
125
|
+
* (наприклад **`deploy.yaml`**, **`deployment.yaml`**).
|
|
126
|
+
* @param {string} relPosix шлях через `/`
|
|
127
|
+
* @returns {boolean} true, якщо шлях лежить у каталозі `…/k8s/…/base/`
|
|
128
|
+
*/
|
|
129
|
+
export function isK8sYamlUnderBaseDirectory(relPosix) {
|
|
130
|
+
const parts = relPosix.replaceAll('\\', '/').split('/').filter(Boolean)
|
|
131
|
+
const k = parts.indexOf('k8s')
|
|
132
|
+
if (k === -1) return false
|
|
133
|
+
const dirs = parts.slice(k + 1, -1)
|
|
134
|
+
return dirs.includes('base')
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
/**
|
|
122
138
|
* Ключі анотацій GKE (NEG / BackendConfig) у **Service** — заборонені (узгоджено з k8s.mdc).
|
|
123
139
|
* @type {readonly string[]}
|
|
@@ -280,6 +296,7 @@ const K8S_BASE_SEGMENT_RE = /(^|\/)k8s\/base\//u
|
|
|
280
296
|
const OXLINT_SCHEMA_MODELINE_RE = /^\s*#\s*yaml-language-server:\s*\$schema=\S+/u
|
|
281
297
|
const HTTPS_SCHEMA_RE = /^https:/iu
|
|
282
298
|
const HASURA_GRAPHQL_ENGINE_RE = /(^|\/)hasura\/graphql-engine(?::|$)/u
|
|
299
|
+
const BATCH_V1BETA1_API_VERSION_LINE_RE = /^(\s*apiVersion:\s*)["']?batch\/v1beta1["']?(\s*)$/u
|
|
283
300
|
|
|
284
301
|
/**
|
|
285
302
|
* Чи містить шлях сегмент директорії `k8s` (рівно ця назва компонента).
|
|
@@ -1013,6 +1030,110 @@ async function readK8sYamlDocumentRootsForInventory(abs) {
|
|
|
1013
1030
|
return out
|
|
1014
1031
|
}
|
|
1015
1032
|
|
|
1033
|
+
/**
|
|
1034
|
+
* Збирає абсолютні шляхи до YAML-файлів із дерева **`resources` / `bases` / `components` / `crds`** (рекурсивно
|
|
1035
|
+
* через вкладені **kustomization.yaml**). Дублює обхід **`collectResourceDescriptorsForKustomizationWalk`**, але
|
|
1036
|
+
* повертає лише шляхи файлів — для перевірки наявності **`Deployment`** у YAML під **`…/k8s/…/base/`**.
|
|
1037
|
+
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
1038
|
+
* @param {string} rootNorm нормалізований абсолютний корінь репозиторію
|
|
1039
|
+
* @param {Set<string>} visitedKustomization нормалізовані абсолютні шляхи відвіданих **kustomization.yaml**
|
|
1040
|
+
* @returns {Promise<string[]>} список абсолютних шляхів до `.yaml` / `.yml`
|
|
1041
|
+
*/
|
|
1042
|
+
async function collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visitedKustomization) {
|
|
1043
|
+
const normKust = resolve(kustAbs)
|
|
1044
|
+
if (visitedKustomization.has(normKust)) {
|
|
1045
|
+
return []
|
|
1046
|
+
}
|
|
1047
|
+
visitedKustomization.add(normKust)
|
|
1048
|
+
|
|
1049
|
+
let raw
|
|
1050
|
+
try {
|
|
1051
|
+
raw = await readFile(normKust, 'utf8')
|
|
1052
|
+
} catch {
|
|
1053
|
+
return []
|
|
1054
|
+
}
|
|
1055
|
+
const lines = toLines(raw)
|
|
1056
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
1057
|
+
|
|
1058
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
1059
|
+
let docs
|
|
1060
|
+
try {
|
|
1061
|
+
docs = parseAllDocuments(body)
|
|
1062
|
+
} catch {
|
|
1063
|
+
return []
|
|
1064
|
+
}
|
|
1065
|
+
const first = docs[0]?.toJSON()
|
|
1066
|
+
if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) {
|
|
1067
|
+
return []
|
|
1068
|
+
}
|
|
1069
|
+
const kustDir = dirname(normKust)
|
|
1070
|
+
const pathRefs = resourcePathRefsFromKustomizationObject(first)
|
|
1071
|
+
|
|
1072
|
+
/** @type {string[]} */
|
|
1073
|
+
const out = []
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* @param {string} ref шлях з resources/bases/…
|
|
1077
|
+
* @returns {Promise<void>}
|
|
1078
|
+
*/
|
|
1079
|
+
async function handleResourcePathRef(ref) {
|
|
1080
|
+
if (typeof ref !== 'string' || ref.includes('://')) {
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
const resolved = resolve(kustDir, ref)
|
|
1084
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
1085
|
+
return
|
|
1086
|
+
}
|
|
1087
|
+
/** @type {import('node:fs').Stats | undefined} */
|
|
1088
|
+
let st
|
|
1089
|
+
try {
|
|
1090
|
+
st = await stat(resolved)
|
|
1091
|
+
} catch {
|
|
1092
|
+
st = undefined
|
|
1093
|
+
}
|
|
1094
|
+
if (st === undefined) {
|
|
1095
|
+
return
|
|
1096
|
+
}
|
|
1097
|
+
if (st.isFile() && YAML_EXTENSION_RE.test(resolved)) {
|
|
1098
|
+
out.push(resolved)
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
if (!st.isDirectory()) {
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
const childK = existsSync(join(resolved, 'kustomization.yaml')) ? join(resolved, 'kustomization.yaml') : null
|
|
1105
|
+
if (childK !== null) {
|
|
1106
|
+
const sub = await collectYamlAbsPathsFromKustomizationTree(childK, rootNorm, visitedKustomization)
|
|
1107
|
+
out.push(...sub)
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
for (const ref of pathRefs) {
|
|
1112
|
+
await handleResourcePathRef(ref)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return out
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Чи в дереві kustomization є **`Deployment`** у будь-якому YAML під **`…/k8s/…/base/`** (умова для HPA/PDB у k8s.mdc).
|
|
1120
|
+
* @param {string} kustAbs kustomization.yaml
|
|
1121
|
+
* @param {string} rootNorm корінь репо
|
|
1122
|
+
* @returns {Promise<boolean>} true, якщо дерево містить Deployment у шарі base
|
|
1123
|
+
*/
|
|
1124
|
+
async function kustomizationTreeHasDeploymentUnderK8sBase(kustAbs, rootNorm) {
|
|
1125
|
+
const visited = new Set()
|
|
1126
|
+
const paths = await collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visited)
|
|
1127
|
+
const rootResolved = resolve(rootNorm)
|
|
1128
|
+
for (const abs of paths) {
|
|
1129
|
+
const rel = (relative(rootResolved, abs) || '').replaceAll('\\', '/')
|
|
1130
|
+
if (!isK8sYamlUnderBaseDirectory(rel)) continue
|
|
1131
|
+
const roots = await readK8sYamlDocumentRootsForInventory(abs)
|
|
1132
|
+
if (roots.some(o => o.kind === 'Deployment')) return true
|
|
1133
|
+
}
|
|
1134
|
+
return false
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1016
1137
|
/**
|
|
1017
1138
|
* Збирає дескриптори ресурсів з **`resources` / `bases` / `components` / `crds`** для одного дерева kustomization.
|
|
1018
1139
|
* Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок (як у **`collectKustomizeManagedRelPaths`**).
|
|
@@ -1243,7 +1364,7 @@ async function resolveExistingYamlFileUnderRoot(kustDir, pathStr, rootNorm) {
|
|
|
1243
1364
|
return null
|
|
1244
1365
|
}
|
|
1245
1366
|
/** @type {import('node:fs').Stats | null} */
|
|
1246
|
-
let st
|
|
1367
|
+
let st
|
|
1247
1368
|
try {
|
|
1248
1369
|
st = await stat(resolved)
|
|
1249
1370
|
} catch {
|
|
@@ -1375,8 +1496,8 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
|
|
|
1375
1496
|
}
|
|
1376
1497
|
const lines = toLines(raw)
|
|
1377
1498
|
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
1378
|
-
/** @type {import('yaml').Document[]
|
|
1379
|
-
let docs
|
|
1499
|
+
/** @type {import('yaml').Document[]} */
|
|
1500
|
+
let docs
|
|
1380
1501
|
try {
|
|
1381
1502
|
docs = parseAllDocuments(body)
|
|
1382
1503
|
} catch {
|
|
@@ -1567,15 +1688,15 @@ async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
|
|
|
1567
1688
|
* Один рядок YAML: якщо це `apiVersion` зі значенням **`batch/v1beta1`**, повертає той самий рядок із **`batch/v1`**
|
|
1568
1689
|
* (з тими самими відступами/пробілами після `apiVersion:`, крім випадків з лапками — нормалізується до `apiVersion: batch/v1`).
|
|
1569
1690
|
* Рядки, що після trim починаються з `#`, не змінюються.
|
|
1570
|
-
* @param {string} line
|
|
1571
|
-
* @returns {string}
|
|
1691
|
+
* @param {string} line один рядок YAML
|
|
1692
|
+
* @returns {string} той самий рядок або з заміною apiVersion batch/v1beta1 на batch/v1
|
|
1572
1693
|
*/
|
|
1573
1694
|
function rewriteLineBatchV1beta1ApiVersion(line) {
|
|
1574
1695
|
const t = line.trimStart()
|
|
1575
1696
|
if (t.startsWith('#')) {
|
|
1576
1697
|
return line
|
|
1577
1698
|
}
|
|
1578
|
-
const m = line.match(
|
|
1699
|
+
const m = line.match(BATCH_V1BETA1_API_VERSION_LINE_RE)
|
|
1579
1700
|
if (m) {
|
|
1580
1701
|
return `${m[1]}batch/v1${m[2]}`
|
|
1581
1702
|
}
|
|
@@ -1586,11 +1707,11 @@ function rewriteLineBatchV1beta1ApiVersion(line) {
|
|
|
1586
1707
|
* У повному тексті YAML замінює всі **цілі** рядки `apiVersion: batch/v1beta1` (за потреби в лапках) на `apiVersion: batch/v1`.
|
|
1587
1708
|
* Зберігає **CRLF** / **LF** як у вихідному рядку.
|
|
1588
1709
|
* @param {string} raw вміст файлу
|
|
1589
|
-
* @returns {{ changed: boolean, content: string }}
|
|
1710
|
+
* @returns {{ changed: boolean, content: string }} прапорець зміни та оновлений текст
|
|
1590
1711
|
*/
|
|
1591
1712
|
export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
|
|
1592
1713
|
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
1593
|
-
const lines = raw.split(
|
|
1714
|
+
const lines = raw.split(YAML_LINE_SPLIT_RE)
|
|
1594
1715
|
let changed = false
|
|
1595
1716
|
const out = lines.map(line => {
|
|
1596
1717
|
const n = rewriteLineBatchV1beta1ApiVersion(line)
|
|
@@ -1608,8 +1729,8 @@ export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
|
|
|
1608
1729
|
/**
|
|
1609
1730
|
* Проходить усі `*.yaml` / `*.yml` під сегментом `k8s` і на диску застосовує **`replaceBatchV1beta1ApiVersionInYamlText`**.
|
|
1610
1731
|
* @param {string} root корінь репозиторію
|
|
1611
|
-
* @param {(msg: string) => void} fail
|
|
1612
|
-
* @param {(msg: string) => void} pass
|
|
1732
|
+
* @param {(msg: string) => void} fail колбек повідомлення про помилку
|
|
1733
|
+
* @param {(msg: string) => void} pass колбек успішного повідомлення
|
|
1613
1734
|
* @returns {Promise<void>}
|
|
1614
1735
|
*/
|
|
1615
1736
|
async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass) {
|
|
@@ -1893,7 +2014,7 @@ function failIfJson6902RemoveAddConflictOnSamePath(rel, label, patchText, fail)
|
|
|
1893
2014
|
*/
|
|
1894
2015
|
async function auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail) {
|
|
1895
2016
|
/** @type {import('node:fs').Stats | null} */
|
|
1896
|
-
let st
|
|
2017
|
+
let st
|
|
1897
2018
|
try {
|
|
1898
2019
|
st = await stat(resolved)
|
|
1899
2020
|
} catch {
|
|
@@ -2010,8 +2131,8 @@ async function auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fa
|
|
|
2010
2131
|
}
|
|
2011
2132
|
const lines = toLines(raw)
|
|
2012
2133
|
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
2013
|
-
/** @type {import('yaml').Document[]
|
|
2014
|
-
let docs
|
|
2134
|
+
/** @type {import('yaml').Document[]} */
|
|
2135
|
+
let docs
|
|
2015
2136
|
try {
|
|
2016
2137
|
docs = parseAllDocuments(body)
|
|
2017
2138
|
} catch {
|
|
@@ -4480,8 +4601,9 @@ export async function kustomizeResourceTreeHpaPdbDeploymentFlags(kustAbs, rootNo
|
|
|
4480
4601
|
/** @type {Set<string>} */
|
|
4481
4602
|
const visitedKustomization = new Set()
|
|
4482
4603
|
const desc = await collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visitedKustomization)
|
|
4604
|
+
const hasDeployment = await kustomizationTreeHasDeploymentUnderK8sBase(kustAbs, rootNorm)
|
|
4483
4605
|
return {
|
|
4484
|
-
hasDeployment
|
|
4606
|
+
hasDeployment,
|
|
4485
4607
|
hasHpa: desc.some(d => d.kind === 'HorizontalPodAutoscaler'),
|
|
4486
4608
|
hasPdb: desc.some(d => d.kind === 'PodDisruptionBudget')
|
|
4487
4609
|
}
|
|
@@ -4854,9 +4976,9 @@ async function extractDeploymentsFromFile(filePath) {
|
|
|
4854
4976
|
}
|
|
4855
4977
|
|
|
4856
4978
|
/**
|
|
4857
|
-
* Для кожного **Deployment**
|
|
4858
|
-
* `hpa.yaml` (валідний `autoscaling/v2`) і `pdb.yaml` (валідний `policy/v1`),
|
|
4859
|
-
*
|
|
4979
|
+
* Для кожного **Deployment** у шарі **`…/k8s/…/base/`** (будь-який YAML у відповідному каталозі) перевіряє:
|
|
4980
|
+
* у тому ж каталозі повинні бути `hpa.yaml` (валідний `autoscaling/v2`) і `pdb.yaml` (валідний `policy/v1`),
|
|
4981
|
+
* а сам Deployment — канонічні **topologySpreadConstraints**. Env-залежні межі (`minReplicas`,
|
|
4860
4982
|
* `minAvailable`) — за сегментом після `/k8s/`: `base` / `dev` / `*-qa` = dev-like, решта — прод.
|
|
4861
4983
|
* @param {string} root корінь репозиторію
|
|
4862
4984
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
@@ -4864,18 +4986,25 @@ async function extractDeploymentsFromFile(filePath) {
|
|
|
4864
4986
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
4865
4987
|
*/
|
|
4866
4988
|
async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, passFn) {
|
|
4867
|
-
|
|
4868
|
-
|
|
4989
|
+
const rootNorm = resolve(root)
|
|
4990
|
+
/** @type {Map<string, Record<string, unknown>[]>} */
|
|
4991
|
+
const deploymentsByDir = new Map()
|
|
4869
4992
|
for (const abs of yamlFilesAbs) {
|
|
4993
|
+
const rel = (relative(rootNorm, abs) || abs).replaceAll('\\', '/')
|
|
4994
|
+
if (!isK8sYamlUnderBaseDirectory(rel)) continue
|
|
4995
|
+
const deployments = await extractDeploymentsFromFile(abs)
|
|
4996
|
+
if (deployments.length === 0) continue
|
|
4870
4997
|
const dir = dirname(abs)
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
}
|
|
4998
|
+
const merged = deploymentsByDir.get(dir)
|
|
4999
|
+
if (merged === undefined) {
|
|
5000
|
+
deploymentsByDir.set(dir, [...deployments])
|
|
5001
|
+
} else {
|
|
5002
|
+
merged.push(...deployments)
|
|
4877
5003
|
}
|
|
4878
5004
|
}
|
|
5005
|
+
for (const [dir, deployments] of deploymentsByDir) {
|
|
5006
|
+
await validateDeploymentsInDir(deployments, dir, rootNorm, fail, passFn)
|
|
5007
|
+
}
|
|
4879
5008
|
}
|
|
4880
5009
|
|
|
4881
5010
|
/**
|