@nitra/cursor 1.8.221 → 1.8.228
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/.claude-template/npm-CLAUDE.md +4 -0
- package/CHANGELOG.md +69 -0
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +3 -2
- package/mdc/abie.mdc +13 -0
- package/mdc/ci4.mdc +8 -0
- package/mdc/tauri.mdc +20 -0
- package/package.json +1 -1
- package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
- package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
- package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
- package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
- package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
- package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
- package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
- package/policy/k8s/base_manifest/base_manifest.rego +154 -0
- package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
- package/policy/k8s/gateway/gateway.rego +151 -0
- package/policy/k8s/gateway/gateway_test.rego +122 -0
- package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
- package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
- package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
- package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
- package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
- package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
- package/policy/k8s/kustomization/kustomization.rego +220 -0
- package/policy/k8s/kustomization/kustomization_test.rego +128 -0
- package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
- package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
- package/policy/k8s/manifest/manifest.rego +151 -4
- package/policy/k8s/manifest/manifest_test.rego +309 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
- package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
- package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
- package/scripts/check-abie.mjs +102 -369
- package/scripts/check-ga.mjs +89 -9
- package/scripts/check-k8s.mjs +128 -569
- package/scripts/lint-conftest.mjs +98 -3
- package/scripts/lint-ga.mjs +18 -132
- package/scripts/lint-rego.mjs +19 -4
- package/scripts/utils/run-conftest-batch.mjs +117 -0
package/scripts/check-k8s.mjs
CHANGED
|
@@ -131,6 +131,7 @@ import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
|
|
|
131
131
|
|
|
132
132
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
133
133
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
134
|
+
import { runConftestBatch } from './utils/run-conftest-batch.mjs'
|
|
134
135
|
import { walkDir } from './utils/walkDir.mjs'
|
|
135
136
|
|
|
136
137
|
/** Версія набору схем yannh — узгоджено з k8s.mdc */
|
|
@@ -477,25 +478,9 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
|
|
|
477
478
|
return null
|
|
478
479
|
}
|
|
479
480
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
484
|
-
* @param {(msg: string) => void} fail функція для фіксації порушення
|
|
485
|
-
* @returns {Promise<void>} завершується після перевірки всіх kustomization.yaml
|
|
486
|
-
*/
|
|
487
|
-
async function validateKustomizationResourcesSortedAlphabetically(root, yamlFilesAbs, fail) {
|
|
488
|
-
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
489
|
-
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
490
|
-
const kust = await readFirstYamlObject(kustAbs)
|
|
491
|
-
if (kust !== null) {
|
|
492
|
-
const v = kustomizationResourcesSortedAlphabeticallyViolation(kust)
|
|
493
|
-
if (v !== null) {
|
|
494
|
-
fail(`${rel}: ${v}`)
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
481
|
+
// Plan B: per-document `resources[]` sort у Kustomization — у rego-пакеті
|
|
482
|
+
// `k8s.kustomization`, викликається з `runAllK8sRego` на початку `check()`.
|
|
483
|
+
// JS-orchestrator validateKustomizationResourcesSortedAlphabetically видалено.
|
|
499
484
|
|
|
500
485
|
/**
|
|
501
486
|
* Лексичне порівняння двох tuple рядків через `localeCompare('en', { sensitivity: 'base' })`.
|
|
@@ -662,42 +647,9 @@ export function kustomizationInlinePatchOpsSortedViolation(patchText) {
|
|
|
662
647
|
return `inline patch (JSON6902) має бути за алфавітом по path. Зараз: ${paths.join(', ')}; очікувано: ${want.join(', ')} (k8s.mdc)`
|
|
663
648
|
}
|
|
664
649
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
* @param {string} root корінь репо
|
|
669
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
670
|
-
* @param {(msg: string) => void} fail функція для фіксації порушення
|
|
671
|
-
* @returns {Promise<void>} завершується після перевірки всіх kustomization.yaml
|
|
672
|
-
*/
|
|
673
|
-
async function validateKustomizationPatchesStructuralSort(root, yamlFilesAbs, fail) {
|
|
674
|
-
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
675
|
-
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
676
|
-
const kust = await readFirstYamlObject(kustAbs)
|
|
677
|
-
if (kust === null) continue
|
|
678
|
-
const outer = kustomizationPatchesSortedViolation(kust)
|
|
679
|
-
if (outer !== null) fail(`${rel}: ${outer}`)
|
|
680
|
-
if (!Array.isArray(kust.patches)) continue
|
|
681
|
-
validateInlinePatchesSorted(rel, kust.patches, fail)
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Перевіряє, що inline-`patch:` (рядок YAML/JSON) у кожному `patches[i]` має ops у канонічному порядку
|
|
687
|
-
* (`add`/`replace` за `path`). Чужі форми (без `patch`-стрічки, з `target` без inline-блока) пропускаються.
|
|
688
|
-
* @param {string} rel відносний шлях `kustomization.yaml` для повідомлень
|
|
689
|
-
* @param {unknown[]} patches масив `kust.patches` (рекордів)
|
|
690
|
-
* @param {(msg: string) => void} fail callback при порушенні
|
|
691
|
-
*/
|
|
692
|
-
function validateInlinePatchesSorted(rel, patches, fail) {
|
|
693
|
-
for (const [i, p] of patches.entries()) {
|
|
694
|
-
if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
|
|
695
|
-
const rec = /** @type {Record<string, unknown>} */ (p)
|
|
696
|
-
if (typeof rec.patch !== 'string') continue
|
|
697
|
-
const v = kustomizationInlinePatchOpsSortedViolation(rec.patch)
|
|
698
|
-
if (v !== null) fail(`${rel}: patches[${i}] (${kustomizationPatchLabel(p, i)}): ${v}`)
|
|
699
|
-
}
|
|
700
|
-
}
|
|
650
|
+
// Plan B: validateKustomizationPatchesStructuralSort видалено. Per-document
|
|
651
|
+
// `patches[]` sort + inline JSON6902 ops sort — у rego-пакеті `k8s.kustomization`,
|
|
652
|
+
// викликається з `runAllK8sRego`.
|
|
701
653
|
|
|
702
654
|
/**
|
|
703
655
|
* Шляхи з полів Kustomization для resolve відносно каталогу **`kustomization.yaml`**.
|
|
@@ -2285,111 +2237,11 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
|
2285
2237
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
2286
2238
|
}
|
|
2287
2239
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2294
|
-
* @returns {void}
|
|
2295
|
-
*/
|
|
2296
|
-
function failIfJson6902RemoveAddConflictOnSamePath(rel, label, patchText, fail) {
|
|
2297
|
-
const ops = collectJson6902OperationsFromPatchText(patchText)
|
|
2298
|
-
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
2299
|
-
if (bad.length > 0) {
|
|
2300
|
-
fail(`${rel}: ${label}: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`)
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
/**
|
|
2305
|
-
* Зовнішній patch-файл (масив JSON6902): remove+add на один path.
|
|
2306
|
-
* @param {string} rel відносний шлях до kustomization.yaml
|
|
2307
|
-
* @param {string} resolved абсолютний шлях до файлу patch
|
|
2308
|
-
* @param {string} root корінь репо
|
|
2309
|
-
* @param {string} patchRef відносне посилання з kustomization
|
|
2310
|
-
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2311
|
-
* @returns {Promise<void>}
|
|
2312
|
-
*/
|
|
2313
|
-
async function auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail) {
|
|
2314
|
-
/** @type {import('node:fs').Stats | null} */
|
|
2315
|
-
let st
|
|
2316
|
-
try {
|
|
2317
|
-
st = await stat(resolved)
|
|
2318
|
-
} catch {
|
|
2319
|
-
st = null
|
|
2320
|
-
}
|
|
2321
|
-
if (st === null || !st.isFile()) {
|
|
2322
|
-
return
|
|
2323
|
-
}
|
|
2324
|
-
let pRaw
|
|
2325
|
-
try {
|
|
2326
|
-
pRaw = await readFile(resolved, 'utf8')
|
|
2327
|
-
} catch {
|
|
2328
|
-
return
|
|
2329
|
-
}
|
|
2330
|
-
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
2331
|
-
if (ops.length === 0) {
|
|
2332
|
-
return
|
|
2333
|
-
}
|
|
2334
|
-
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
2335
|
-
if (bad.length === 0) {
|
|
2336
|
-
return
|
|
2337
|
-
}
|
|
2338
|
-
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
2339
|
-
fail(
|
|
2340
|
-
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
2341
|
-
)
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
/**
|
|
2345
|
-
* Один елемент **`patches[]`**: inline JSON6902 або зовнішній patch-файл.
|
|
2346
|
-
* @param {string} rel відносний шлях до kustomization.yaml
|
|
2347
|
-
* @param {Record<string, unknown>} pr об’єкт patch
|
|
2348
|
-
* @param {number} patchIdx 1-based індекс у масиві
|
|
2349
|
-
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
2350
|
-
* @param {string} rootNorm нормалізований корінь репо
|
|
2351
|
-
* @param {string} root корінь репо
|
|
2352
|
-
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2353
|
-
* @returns {Promise<void>}
|
|
2354
|
-
*/
|
|
2355
|
-
async function auditOneKustomizationJson6902Patch(rel, pr, patchIdx, kustAbs, rootNorm, root, fail) {
|
|
2356
|
-
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
2357
|
-
failIfJson6902RemoveAddConflictOnSamePath(rel, `patches[${patchIdx}] inline JSON6902`, pr.patch, fail)
|
|
2358
|
-
}
|
|
2359
|
-
if (typeof pr.path !== 'string' || pr.path.trim() === '') {
|
|
2360
|
-
return
|
|
2361
|
-
}
|
|
2362
|
-
const patchRef = pr.path.trim()
|
|
2363
|
-
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
2364
|
-
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved) || !existsSync(resolved)) {
|
|
2365
|
-
return
|
|
2366
|
-
}
|
|
2367
|
-
await auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail)
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
/**
|
|
2371
|
-
* Усі **`patches[]`** у Kustomization: inline та зовнішні файли.
|
|
2372
|
-
* @param {string} rel відносний шлях до kustomization.yaml
|
|
2373
|
-
* @param {unknown} patches поле **patches**
|
|
2374
|
-
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
2375
|
-
* @param {string} rootNorm нормалізований корінь репо
|
|
2376
|
-
* @param {string} root корінь репо
|
|
2377
|
-
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2378
|
-
* @returns {Promise<void>}
|
|
2379
|
-
*/
|
|
2380
|
-
async function auditKustomizationPatchesJson6902(rel, patches, kustAbs, rootNorm, root, fail) {
|
|
2381
|
-
if (!Array.isArray(patches)) {
|
|
2382
|
-
return
|
|
2383
|
-
}
|
|
2384
|
-
let patchIdx = 0
|
|
2385
|
-
for (const p of patches) {
|
|
2386
|
-
patchIdx++
|
|
2387
|
-
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
2388
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
2389
|
-
await auditOneKustomizationJson6902Patch(rel, pr, patchIdx, kustAbs, rootNorm, root, fail)
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2240
|
+
// Plan B: вся audit-ланка JSON6902 (failIfJson6902RemoveAddConflictOnSamePath,
|
|
2241
|
+
// auditJson6902PatchExternalFile, auditOneKustomizationJson6902Patch,
|
|
2242
|
+
// auditKustomizationPatchesJson6902) видалена. Per-document inline JSON6902
|
|
2243
|
+
// remove+add conflict — у rego-пакеті `k8s.kustomization`. Зовнішні patch-файли
|
|
2244
|
+
// не охоплені rego-кроком (потребує FS-доступу) — це trade-off Plan B.
|
|
2393
2245
|
|
|
2394
2246
|
/**
|
|
2395
2247
|
* Один YAML-документ: якщо це Kustomization — перевірка **patches** на JSON6902 remove+add.
|
|
@@ -2401,140 +2253,18 @@ async function auditKustomizationPatchesJson6902(rel, patches, kustAbs, rootNorm
|
|
|
2401
2253
|
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2402
2254
|
* @returns {Promise<void>}
|
|
2403
2255
|
*/
|
|
2404
|
-
async function auditJson6902ForKustomizationYamlDoc(rel, rootObj, kustAbs, rootNorm, root, fail) {
|
|
2405
|
-
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
2406
|
-
if (rec.kind !== 'Kustomization') {
|
|
2407
|
-
return
|
|
2408
|
-
}
|
|
2409
|
-
await auditKustomizationPatchesJson6902(rel, rec.patches, kustAbs, rootNorm, root, fail)
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
2256
|
/**
|
|
2413
|
-
*
|
|
2414
|
-
*
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2418
|
-
* @returns {Promise<void>}
|
|
2257
|
+
* Plan B: per-document JSON6902 remove+add conflict — у rego-пакеті
|
|
2258
|
+
* `k8s.kustomization`, виклик через `runAllK8sRego`. JS-функції
|
|
2259
|
+
* auditJson6902ForKustomizationYamlDoc, auditJson6902OneKustomizationYamlFile,
|
|
2260
|
+
* validateKustomizationJson6902NoRemoveAddSamePath видалено.
|
|
2419
2261
|
*/
|
|
2420
|
-
async function auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fail) {
|
|
2421
|
-
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
2422
|
-
let raw
|
|
2423
|
-
try {
|
|
2424
|
-
raw = await readFile(kustAbs, 'utf8')
|
|
2425
|
-
} catch (error) {
|
|
2426
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
2427
|
-
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
2428
|
-
return
|
|
2429
|
-
}
|
|
2430
|
-
const lines = toLines(raw)
|
|
2431
|
-
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
2432
|
-
/** @type {import('yaml').Document[]} */
|
|
2433
|
-
let docs
|
|
2434
|
-
try {
|
|
2435
|
-
docs = parseAllDocuments(body)
|
|
2436
|
-
} catch {
|
|
2437
|
-
return
|
|
2438
|
-
}
|
|
2439
|
-
for (const doc of docs) {
|
|
2440
|
-
if (doc.errors.length === 0) {
|
|
2441
|
-
const rootObj = doc.toJSON()
|
|
2442
|
-
if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
|
|
2443
|
-
await auditJson6902ForKustomizationYamlDoc(rel, rootObj, kustAbs, rootNorm, root, fail)
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
2262
|
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
* @returns {Promise<void>}
|
|
2455
|
-
*/
|
|
2456
|
-
async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
|
|
2457
|
-
const rootNorm = resolve(root)
|
|
2458
|
-
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
2459
|
-
await auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fail)
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
/**
|
|
2464
|
-
* Заборонений **kind: Ingress** у документі.
|
|
2465
|
-
* @param {string} rel відносний шлях до файлу
|
|
2466
|
-
* @param {number} docIndex 1-based індекс документа
|
|
2467
|
-
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
2468
|
-
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2469
|
-
* @returns {void}
|
|
2470
|
-
*/
|
|
2471
|
-
function failIfIngressInDocument(rel, docIndex, rec, fail) {
|
|
2472
|
-
if (rec.kind !== 'Ingress') {
|
|
2473
|
-
return
|
|
2474
|
-
}
|
|
2475
|
-
fail(
|
|
2476
|
-
`${rel}: знайдено kind: Ingress (документ ${docIndex}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml) (див. k8s.mdc)`
|
|
2477
|
-
)
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
/**
|
|
2481
|
-
* Чи маніфест використовує заборонений **`apiVersion: autoscaling/v1`** (HPA).
|
|
2482
|
-
* Канон — **`autoscaling/v2`** (див. k8s.mdc).
|
|
2483
|
-
* @param {unknown} manifest корінь YAML-документа
|
|
2484
|
-
* @returns {boolean} true, якщо `apiVersion === 'autoscaling/v1'`
|
|
2485
|
-
*/
|
|
2486
|
-
export function isForbiddenAutoscalingV1Manifest(manifest) {
|
|
2487
|
-
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
2488
|
-
return false
|
|
2489
|
-
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
2490
|
-
return rec.apiVersion === 'autoscaling/v1'
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
/**
|
|
2494
|
-
* Заборонена група **`apiVersion: autoscaling/v1`** (HPA) — вимагається міграція на **`autoscaling/v2`**.
|
|
2495
|
-
* @param {string} rel відносний шлях до файлу
|
|
2496
|
-
* @param {number} docIndex 1-based індекс документа
|
|
2497
|
-
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
2498
|
-
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2499
|
-
* @returns {void}
|
|
2500
|
-
*/
|
|
2501
|
-
function failIfAutoscalingV1InDocument(rel, docIndex, rec, fail) {
|
|
2502
|
-
if (!isForbiddenAutoscalingV1Manifest(rec)) {
|
|
2503
|
-
return
|
|
2504
|
-
}
|
|
2505
|
-
const kind = typeof rec.kind === 'string' ? rec.kind : '(невідомо)'
|
|
2506
|
-
fail(
|
|
2507
|
-
`${rel}: знайдено apiVersion: autoscaling/v1 (документ ${docIndex}, kind: ${kind}) — мігруй на autoscaling/v2 (див. k8s.mdc)`
|
|
2508
|
-
)
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
/**
|
|
2512
|
-
* Шукає заборонені маніфести у розібраних документах: **kind: Ingress** і **apiVersion: autoscaling/v1**.
|
|
2513
|
-
* @param {string} rel відносний шлях до файлу
|
|
2514
|
-
* @param {string} body YAML після modeline
|
|
2515
|
-
* @param {(msg: string) => void} fail callback для помилки
|
|
2516
|
-
* @returns {void}
|
|
2517
|
-
*/
|
|
2518
|
-
function scanForbiddenManifestsInYamlDocuments(rel, body, fail) {
|
|
2519
|
-
/** @type {import('yaml').Document[]} */
|
|
2520
|
-
let docs
|
|
2521
|
-
try {
|
|
2522
|
-
docs = parseAllDocuments(body)
|
|
2523
|
-
} catch {
|
|
2524
|
-
return
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
for (const [di, doc] of docs.entries()) {
|
|
2528
|
-
if (doc.errors.length === 0) {
|
|
2529
|
-
const obj = doc.toJSON()
|
|
2530
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
2531
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
2532
|
-
failIfIngressInDocument(rel, di + 1, rec, fail)
|
|
2533
|
-
failIfAutoscalingV1InDocument(rel, di + 1, rec, fail)
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2263
|
+
// Plan B: per-document `kind: Ingress` і `apiVersion: autoscaling/v1` заборонено —
|
|
2264
|
+
// у rego-пакеті `k8s.manifest`, виклик через `runAllK8sRego`. JS-функції
|
|
2265
|
+
// failIfIngressInDocument, failIfAutoscalingV1InDocument, scanForbiddenManifestsInYamlDocuments
|
|
2266
|
+
// видалено. `isForbiddenAutoscalingV1Manifest` як публічний predicate теж видалено
|
|
2267
|
+
// (rego — авторитативне джерело).
|
|
2538
2268
|
|
|
2539
2269
|
/**
|
|
2540
2270
|
* Рекомендоване **`resources.requests.cpu`** поза шарем base для підказок у повідомленнях (k8s.mdc).
|
|
@@ -3086,15 +2816,6 @@ export function serviceForbiddenGcpAnnotationsViolation(manifest) {
|
|
|
3086
2816
|
/** Суфікс **`metadata.name`** headless-сервісу поруч із **`svc.yaml`** (див. k8s.mdc). */
|
|
3087
2817
|
const SVC_HL_NAME_SUFFIX = '-hl'
|
|
3088
2818
|
|
|
3089
|
-
/**
|
|
3090
|
-
* Kind маршрутів Gateway API, у **`spec`** яких шукаємо **`backendRefs`** / **`backendRef`** до **Service**.
|
|
3091
|
-
* @type {Set<string>}
|
|
3092
|
-
*/
|
|
3093
|
-
const GATEWAY_API_ROUTE_KINDS = new Set(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute', 'UDPRoute'])
|
|
3094
|
-
|
|
3095
|
-
/** Префікс **`apiVersion`** стандартних ресурсів Gateway API. */
|
|
3096
|
-
const GATEWAY_API_GROUP_PREFIX = 'gateway.networking.k8s.io/'
|
|
3097
|
-
|
|
3098
2819
|
/**
|
|
3099
2820
|
* Чи **Service** у **`svc.yaml`** має **`spec.type: ClusterIP`** (k8s.mdc).
|
|
3100
2821
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -3281,64 +3002,9 @@ export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, ro
|
|
|
3281
3002
|
* @param {(msg: string) => void} fail callback помилки
|
|
3282
3003
|
* @returns {void}
|
|
3283
3004
|
*/
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
if (
|
|
3288
|
-
typeof av !== 'string' ||
|
|
3289
|
-
!av.startsWith(GATEWAY_API_GROUP_PREFIX) ||
|
|
3290
|
-
typeof kind !== 'string' ||
|
|
3291
|
-
!GATEWAY_API_ROUTE_KINDS.has(kind)
|
|
3292
|
-
) {
|
|
3293
|
-
return
|
|
3294
|
-
}
|
|
3295
|
-
const names = collectGatewayApiRouteBackendServiceNames(rec.spec)
|
|
3296
|
-
for (const svcName of names) {
|
|
3297
|
-
if (!svcName.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
3298
|
-
fail(
|
|
3299
|
-
`${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef до Service має вказувати headless-сервіс з суфіксом «${SVC_HL_NAME_SUFFIX}» у name (зараз: «${svcName}»; див. k8s.mdc)`
|
|
3300
|
-
)
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
const meta = rec.metadata
|
|
3304
|
-
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
3305
|
-
const routeNs = /** @type {Record<string, unknown>} */ (meta).namespace
|
|
3306
|
-
if (typeof routeNs === 'string' && routeNs !== '') {
|
|
3307
|
-
const redundant = collectGatewayApiRouteBackendRefsWithRedundantNamespace(rec.spec, routeNs)
|
|
3308
|
-
for (const svcName of redundant) {
|
|
3309
|
-
fail(
|
|
3310
|
-
`${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef «${svcName}» має namespace «${routeNs}», що збігається з metadata.namespace маршруту — прибери поле namespace з backendRef (див. k8s.mdc)`
|
|
3311
|
-
)
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
/**
|
|
3318
|
-
* Реєструє порушення: маршрути Gateway API мають посилатися на **Service** з суфіксом **`-hl`**.
|
|
3319
|
-
* @param {string} rel відносний шлях до файлу
|
|
3320
|
-
* @param {string} body YAML після modeline
|
|
3321
|
-
* @param {(msg: string) => void} fail callback помилки
|
|
3322
|
-
* @returns {void}
|
|
3323
|
-
*/
|
|
3324
|
-
function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
3325
|
-
/** @type {import('yaml').Document[]} */
|
|
3326
|
-
let docs
|
|
3327
|
-
try {
|
|
3328
|
-
docs = parseAllDocuments(body)
|
|
3329
|
-
} catch {
|
|
3330
|
-
return
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
for (const [di, doc] of docs.entries()) {
|
|
3334
|
-
if (doc.errors.length === 0) {
|
|
3335
|
-
const obj = doc.toJSON()
|
|
3336
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3337
|
-
failIfGatewayRouteUsesNonHeadlessService(rel, di + 1, /** @type {Record<string, unknown>} */ (obj), fail)
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3005
|
+
// Plan B: Gateway API маршрут backendRef з суфіксом `-hl` і redundant namespace —
|
|
3006
|
+
// у rego-пакеті `k8s.gateway`, виклик через `runAllK8sRego`. JS-функції
|
|
3007
|
+
// failIfGatewayRouteUsesNonHeadlessService, scanGatewayApiRouteBackendRefsInYamlBody видалено.
|
|
3342
3008
|
|
|
3343
3009
|
/**
|
|
3344
3010
|
* Звузити `unknown` до `Record<string, unknown>` (`null`, масиви, примітиви → null).
|
|
@@ -3815,20 +3481,28 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
|
3815
3481
|
const { hasuraByDir, httpRoutes } = await collectHasuraDeploymentsAndHttpRoutes(yamlFiles)
|
|
3816
3482
|
if (hasuraByDir.size === 0 || httpRoutes.length === 0) return
|
|
3817
3483
|
|
|
3484
|
+
// JS gating: відберемо файли HTTPRoute, що paired з Hasura-Deployment у тому ж каталозі
|
|
3485
|
+
// (за `metadata.name` HTTPRoute === metadata.name Hasura-Deployment). Per-document валідація
|
|
3486
|
+
// канону 4 правил Hasura — у rego-пакеті `k8s.hasura_httproute`.
|
|
3487
|
+
const pairedFiles = new Set()
|
|
3818
3488
|
for (const hr of httpRoutes) {
|
|
3819
3489
|
const meta = asPlainRecord(hr.obj.metadata)
|
|
3820
3490
|
const name = meta === null ? undefined : meta.name
|
|
3821
3491
|
const set = typeof name === 'string' && name !== '' ? hasuraByDir.get(hr.dir) : undefined
|
|
3822
3492
|
if (set !== undefined && typeof name === 'string' && set.has(name)) {
|
|
3823
|
-
|
|
3824
|
-
if (v !== null) {
|
|
3825
|
-
const rel = (relative(root, hr.abs) || hr.abs).replaceAll('\\', '/')
|
|
3826
|
-
fail(
|
|
3827
|
-
`${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`
|
|
3828
|
-
)
|
|
3829
|
-
}
|
|
3493
|
+
pairedFiles.add(hr.abs)
|
|
3830
3494
|
}
|
|
3831
3495
|
}
|
|
3496
|
+
if (pairedFiles.size === 0) return
|
|
3497
|
+
const violations = runConftestBatch({
|
|
3498
|
+
policyDirRel: 'k8s/hasura_httproute',
|
|
3499
|
+
namespace: 'k8s.hasura_httproute',
|
|
3500
|
+
files: [...pairedFiles]
|
|
3501
|
+
})
|
|
3502
|
+
for (const v of violations) {
|
|
3503
|
+
const rel = (relative(root, v.filename) || v.filename).replaceAll('\\', '/')
|
|
3504
|
+
fail(`${rel}: ${v.message}`)
|
|
3505
|
+
}
|
|
3832
3506
|
}
|
|
3833
3507
|
|
|
3834
3508
|
/**
|
|
@@ -3898,117 +3572,11 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
|
3898
3572
|
return K8S_BASE_SEGMENT_RE.test(n)
|
|
3899
3573
|
}
|
|
3900
3574
|
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
* @param {boolean} skipMetaNs пропуск для **kustomization.yaml**
|
|
3907
|
-
* @param {boolean} inBaseManifest файл у **k8s/base/**
|
|
3908
|
-
* @param {boolean} kustomizeManaged файл у графі kustomization
|
|
3909
|
-
* @param {(msg: string) => void} fail реєстрація помилки
|
|
3910
|
-
* @returns {void}
|
|
3911
|
-
*/
|
|
3912
|
-
function failIfK8sPolicyNamespaceRulesViolated(rel, docIndex, obj, skipMetaNs, inBaseManifest, kustomizeManaged, fail) {
|
|
3913
|
-
if (skipMetaNs) {
|
|
3914
|
-
return
|
|
3915
|
-
}
|
|
3916
|
-
if (inBaseManifest) {
|
|
3917
|
-
const req = metadataNamespaceRequiredViolation(obj, true)
|
|
3918
|
-
if (req !== null) {
|
|
3919
|
-
fail(`${rel}: документ ${docIndex}: ${req}`)
|
|
3920
|
-
}
|
|
3921
|
-
return
|
|
3922
|
-
}
|
|
3923
|
-
if (kustomizeManaged) {
|
|
3924
|
-
const ns = metadataNamespaceForbiddenViolation(obj)
|
|
3925
|
-
if (ns !== null) {
|
|
3926
|
-
fail(`${rel}: документ ${docIndex}: ${ns}`)
|
|
3927
|
-
}
|
|
3928
|
-
return
|
|
3929
|
-
}
|
|
3930
|
-
const req = metadataNamespaceRequiredViolation(obj, false)
|
|
3931
|
-
if (req !== null) {
|
|
3932
|
-
fail(`${rel}: документ ${docIndex}: ${req}`)
|
|
3933
|
-
}
|
|
3934
|
-
}
|
|
3935
|
-
|
|
3936
|
-
/**
|
|
3937
|
-
* Deployment / Service / HealthCheckPolicy — політики для одного документа.
|
|
3938
|
-
* @param {string} rel відносний шлях
|
|
3939
|
-
* @param {string} baseLower basename (нижній регістр)
|
|
3940
|
-
* @param {number} docIndex 1-based
|
|
3941
|
-
* @param {unknown} obj корінь документа
|
|
3942
|
-
* @param {(msg: string) => void} fail реєстрація помилки
|
|
3943
|
-
* @returns {void}
|
|
3944
|
-
*/
|
|
3945
|
-
function failIfK8sPolicyResourceRulesViolated(rel, baseLower, docIndex, obj, fail) {
|
|
3946
|
-
const relPosix = rel.replaceAll('\\', '/')
|
|
3947
|
-
const inK8sBaseLayer = isK8sYamlUnderBaseDirectory(relPosix)
|
|
3948
|
-
const resV = deploymentResourcesViolation(obj, inK8sBaseLayer)
|
|
3949
|
-
if (resV !== null) {
|
|
3950
|
-
fail(`${rel}: Deployment (документ ${docIndex}): ${resV}`)
|
|
3951
|
-
}
|
|
3952
|
-
const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
|
|
3953
|
-
if (hasuraV !== null) {
|
|
3954
|
-
fail(`${rel}: Deployment (документ ${docIndex}): ${hasuraV}`)
|
|
3955
|
-
}
|
|
3956
|
-
const svcGcpV = serviceForbiddenGcpAnnotationsViolation(obj)
|
|
3957
|
-
if (svcGcpV !== null) {
|
|
3958
|
-
fail(`${rel}: Service (документ ${docIndex}): ${svcGcpV}`)
|
|
3959
|
-
}
|
|
3960
|
-
if (baseLower === 'svc.yaml') {
|
|
3961
|
-
const svcT = serviceSvcYamlClusterIpTypeViolation(obj)
|
|
3962
|
-
if (svcT !== null) {
|
|
3963
|
-
fail(`${rel}: Service (документ ${docIndex}): ${svcT}`)
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
if (baseLower === 'svc-hl.yaml') {
|
|
3967
|
-
const svcH = serviceSvcHlYamlHeadlessViolation(obj)
|
|
3968
|
-
if (svcH !== null) {
|
|
3969
|
-
fail(`${rel}: Service (документ ${docIndex}): ${svcH}`)
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
const hcpHl = healthCheckPolicyTargetRefHeadlessServiceViolation(obj)
|
|
3973
|
-
if (hcpHl !== null) {
|
|
3974
|
-
fail(`${rel}: документ ${docIndex}: ${hcpHl}`)
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
|
|
3978
|
-
/**
|
|
3979
|
-
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
|
|
3980
|
-
* **Service — заборонені GKE-анотації**, **`svc.yaml`** (**`spec.type: ClusterIP`**), **`svc-hl.yaml`**
|
|
3981
|
-
* (**headless**, суфікс **`-hl`** у **`metadata.name`**), **HealthCheckPolicy** (**`targetRef.name`** з **`-hl`**).
|
|
3982
|
-
* @param {string} rel відносний шлях
|
|
3983
|
-
* @param {string} baseLower basename файлу (нижній регістр)
|
|
3984
|
-
* @param {string} body вміст після modeline
|
|
3985
|
-
* @param {(msg: string) => void} fail реєстрація помилки
|
|
3986
|
-
* @param {boolean} kustomizeManaged чи файл досяжний з kustomization.yaml (resources / patches / …)
|
|
3987
|
-
*/
|
|
3988
|
-
function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged) {
|
|
3989
|
-
/** @type {import('yaml').Document[]} */
|
|
3990
|
-
let docs
|
|
3991
|
-
try {
|
|
3992
|
-
docs = parseAllDocuments(body)
|
|
3993
|
-
} catch (error) {
|
|
3994
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
3995
|
-
fail(`${rel}: не вдалося розібрати YAML для перевірок маніфестів (${msg})`)
|
|
3996
|
-
return
|
|
3997
|
-
}
|
|
3998
|
-
|
|
3999
|
-
const skipMetaNs = isKustomizationFileName(baseLower)
|
|
4000
|
-
const inBaseManifest = isK8sBaseManifestYamlPath(rel, baseLower)
|
|
4001
|
-
|
|
4002
|
-
for (const [di, doc] of docs.entries()) {
|
|
4003
|
-
if (doc.errors.length > 0) {
|
|
4004
|
-
fail(`${rel}: YAML (документ ${di + 1}): ${doc.errors.map(e => e.message).join('; ')}`)
|
|
4005
|
-
} else {
|
|
4006
|
-
const obj = doc.toJSON()
|
|
4007
|
-
failIfK8sPolicyNamespaceRulesViolated(rel, di + 1, obj, skipMetaNs, inBaseManifest, kustomizeManaged, fail)
|
|
4008
|
-
failIfK8sPolicyResourceRulesViolated(rel, baseLower, di + 1, obj, fail)
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
4011
|
-
}
|
|
3575
|
+
// Plan B: per-document валідаційне ядро для k8s YAML повністю в rego —
|
|
3576
|
+
// `k8s.manifest`, `k8s.gateway`, `k8s.svc_yaml`, `k8s.svc_hl_yaml`,
|
|
3577
|
+
// `k8s.kustomize_managed`, `k8s.base_manifest`. Виклик через `runAllK8sRego`.
|
|
3578
|
+
// JS-функції failIfK8sPolicyNamespaceRulesViolated, failIfK8sPolicyResourceRulesViolated,
|
|
3579
|
+
// validateK8sYamlPolicyDocuments видалено.
|
|
4012
3580
|
|
|
4013
3581
|
/**
|
|
4014
3582
|
* Kind для імен файлів yannh/datree: лише літери та цифри, нижній регістр (Service → service, HTTPRoute → httproute).
|
|
@@ -4103,20 +3671,6 @@ function countSchemaModelines(lines) {
|
|
|
4103
3671
|
return lines.filter(l => OXLINT_SCHEMA_MODELINE_RE.test(l.trim())).length
|
|
4104
3672
|
}
|
|
4105
3673
|
|
|
4106
|
-
/**
|
|
4107
|
-
* Політики маніфестів і Gateway backendRefs після розбору тіла.
|
|
4108
|
-
* @param {string} rel відносний шлях
|
|
4109
|
-
* @param {string} baseLower basename (нижній регістр)
|
|
4110
|
-
* @param {string} body YAML після modeline
|
|
4111
|
-
* @param {(msg: string) => void} fail реєстрація помилки
|
|
4112
|
-
* @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
|
|
4113
|
-
* @returns {void}
|
|
4114
|
-
*/
|
|
4115
|
-
function runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel) {
|
|
4116
|
-
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
4117
|
-
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
4118
|
-
scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
|
|
4119
|
-
}
|
|
4120
3674
|
|
|
4121
3675
|
/**
|
|
4122
3676
|
* Файл з першим документом **HttpBackendGroup** (ALB Yandex): без modeline **$schema**.
|
|
@@ -4128,11 +3682,11 @@ function runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeMa
|
|
|
4128
3682
|
* @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
|
|
4129
3683
|
* @returns {void}
|
|
4130
3684
|
*/
|
|
4131
|
-
function checkK8sYamlHttpBackendGroupFile(rel,
|
|
4132
|
-
|
|
4133
|
-
|
|
3685
|
+
function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass, _kustomizeManagedRel) {
|
|
3686
|
+
// Per-document валідація (Ingress/autoscaling/v1 заборонено, Gateway API backendRef,
|
|
3687
|
+
// metadata.namespace правила) — у rego (`k8s.manifest`, `k8s.gateway`, `k8s.kustomize_managed`,
|
|
3688
|
+
// `k8s.base_manifest`), батч-виклик з `runAllK8sRego` на початку `check()`.
|
|
4134
3689
|
pass(`${rel}: HttpBackendGroup (alb.yc.io/v1alpha1) — modeline $schema не застосовується (k8s.mdc)`)
|
|
4135
|
-
runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel)
|
|
4136
3690
|
}
|
|
4137
3691
|
|
|
4138
3692
|
/**
|
|
@@ -4160,7 +3714,9 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
|
|
|
4160
3714
|
|
|
4161
3715
|
const body = yamlBodyAfterModeline(lines)
|
|
4162
3716
|
|
|
4163
|
-
|
|
3717
|
+
// Per-document валідація (Ingress/autoscaling/v1 заборонено, Gateway API backendRef,
|
|
3718
|
+
// metadata.namespace правила, Service GCP-анотації, Deployment resources/Hasura image,
|
|
3719
|
+
// topologySpread, HCP, svc/svc-hl) — делегована rego, виконано у `runAllK8sRego` вище.
|
|
4164
3720
|
|
|
4165
3721
|
if (schemaUrl.startsWith('file:')) {
|
|
4166
3722
|
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
@@ -4181,10 +3737,7 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
|
|
|
4181
3737
|
pass(`${rel}: $schema узгоджено (${reason})`)
|
|
4182
3738
|
} else {
|
|
4183
3739
|
fail(`${rel}: $schema має бути https URL або file: (див. k8s.mdc)`)
|
|
4184
|
-
return
|
|
4185
3740
|
}
|
|
4186
|
-
|
|
4187
|
-
runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel)
|
|
4188
3741
|
}
|
|
4189
3742
|
|
|
4190
3743
|
/**
|
|
@@ -4272,46 +3825,9 @@ function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
|
|
|
4272
3825
|
* @param {(msg: string) => void} fail реєстрація порушення
|
|
4273
3826
|
* @returns {Promise<void>}
|
|
4274
3827
|
*/
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
const raw = await readFile(abs, 'utf8')
|
|
4279
|
-
const lines = toLines(raw)
|
|
4280
|
-
const body = yamlBodyAfterModeline(lines)
|
|
4281
|
-
/** @type {import('yaml').Document[] | undefined} */
|
|
4282
|
-
let docs
|
|
4283
|
-
try {
|
|
4284
|
-
docs = parseAllDocuments(body)
|
|
4285
|
-
} catch {
|
|
4286
|
-
fail(`${rel}: не вдалося розпарсити YAML для перевірки namespace у base (див. k8s.mdc)`)
|
|
4287
|
-
return
|
|
4288
|
-
}
|
|
4289
|
-
const first = docs[0]?.toJSON()
|
|
4290
|
-
const v = baseKustomizationNamespaceViolation(first)
|
|
4291
|
-
if (v) {
|
|
4292
|
-
fail(`${rel}: ${v}`)
|
|
4293
|
-
}
|
|
4294
|
-
} catch (error) {
|
|
4295
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
4296
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
4297
|
-
}
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
|
-
/**
|
|
4301
|
-
* Якщо є **`k8s/base/kustomization.yaml`**, у ньому **завжди** має бути непорожній **`namespace:`**.
|
|
4302
|
-
* @param {string} root корінь репозиторію
|
|
4303
|
-
* @param {string[]} yamlFiles абсолютні шляхи
|
|
4304
|
-
* @param {(msg: string) => void} fail callback для реєстрації порушення
|
|
4305
|
-
* @returns {Promise<void>}
|
|
4306
|
-
*/
|
|
4307
|
-
async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
|
|
4308
|
-
for (const abs of yamlFiles) {
|
|
4309
|
-
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
4310
|
-
if (isBaseKustomizationPath(rel)) {
|
|
4311
|
-
await verifyBaseKustomizationNamespaceOnFile(root, abs, fail)
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
3828
|
+
// Plan B: per-document `k8s/base/kustomization.yaml` має непорожнє поле `namespace:` —
|
|
3829
|
+
// у rego-пакеті `k8s.base_kustomization`, виклик через `runAllK8sRego`.
|
|
3830
|
+
// JS-функції verifyBaseKustomizationNamespaceOnFile, ensureBaseKustomizationHasNamespace видалено.
|
|
4315
3831
|
|
|
4316
3832
|
const CONFIGMAP_BASE_PATH_RE = /\/k8s\/base\/configmap\.yaml$/u
|
|
4317
3833
|
|
|
@@ -4374,19 +3890,6 @@ async function validateConfigMapNameMatchesDeployment(root, yamlFilesAbs, fail,
|
|
|
4374
3890
|
}
|
|
4375
3891
|
}
|
|
4376
3892
|
|
|
4377
|
-
/**
|
|
4378
|
-
* Знаходить перший документ **ConfigMap** у файлі (з `metadata.name`).
|
|
4379
|
-
* @param {string} absPath абсолютний шлях до YAML-файлу
|
|
4380
|
-
* @returns {Promise<Record<string, unknown> | null>} об'єкт ConfigMap або null
|
|
4381
|
-
*/
|
|
4382
|
-
async function readFirstConfigMapDoc(absPath) {
|
|
4383
|
-
const raw = await tryReadFileUtf8(absPath)
|
|
4384
|
-
if (raw === undefined) return null
|
|
4385
|
-
const docs = tryParseAllYamlDocs(raw)
|
|
4386
|
-
if (docs === undefined) return null
|
|
4387
|
-
return findFirstDocByKind(docs, 'ConfigMap')
|
|
4388
|
-
}
|
|
4389
|
-
|
|
4390
3893
|
/**
|
|
4391
3894
|
* Для кожного `k8s/base/configmap.yaml`, у каталозі якого поруч є Hasura-Deployment,
|
|
4392
3895
|
* вимагає у `data` ключ **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`** (k8s.mdc).
|
|
@@ -4400,21 +3903,29 @@ async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs
|
|
|
4400
3903
|
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
4401
3904
|
return CONFIGMAP_BASE_PATH_RE.test(`/${rel}`) || rel === 'k8s/base/configmap.yaml'
|
|
4402
3905
|
})
|
|
3906
|
+
// JS gating: відберемо ConfigMap-файли, у каталозі яких поруч є Hasura-Deployment.
|
|
3907
|
+
// Per-document валідація `data.HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS == "true"`
|
|
3908
|
+
// — у rego-пакеті `k8s.hasura_configmap`.
|
|
3909
|
+
const paired = []
|
|
4403
3910
|
for (const cmAbs of cmFiles) {
|
|
4404
|
-
const rel = relative(root, cmAbs).replaceAll('\\', '/') || cmAbs
|
|
4405
3911
|
const deployment = await findDeploymentDocInDir(dirname(cmAbs))
|
|
4406
3912
|
if (deployment !== null && isHasuraDeploymentManifest(deployment)) {
|
|
4407
|
-
|
|
4408
|
-
if (cm !== null) {
|
|
4409
|
-
const violation = hasuraConfigMapRemoteSchemaPermissionsViolation(cm)
|
|
4410
|
-
if (violation === null) {
|
|
4411
|
-
passFn(`${rel}: ${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}="true" для Hasura-Deployment (k8s.mdc)`)
|
|
4412
|
-
} else {
|
|
4413
|
-
fail(`${rel}: ${violation}`)
|
|
4414
|
-
}
|
|
4415
|
-
}
|
|
3913
|
+
paired.push(cmAbs)
|
|
4416
3914
|
}
|
|
4417
3915
|
}
|
|
3916
|
+
if (paired.length === 0) return
|
|
3917
|
+
const violations = runConftestBatch({
|
|
3918
|
+
policyDirRel: 'k8s/hasura_configmap',
|
|
3919
|
+
namespace: 'k8s.hasura_configmap',
|
|
3920
|
+
files: paired
|
|
3921
|
+
})
|
|
3922
|
+
for (const v of violations) {
|
|
3923
|
+
const rel = (relative(root, v.filename) || v.filename).replaceAll('\\', '/')
|
|
3924
|
+
fail(`${rel}: ${v.message}`)
|
|
3925
|
+
}
|
|
3926
|
+
if (violations.length === 0) {
|
|
3927
|
+
passFn(`Hasura-ConfigMap (${paired.length}) відповідає ${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}="true" (rego)`)
|
|
3928
|
+
}
|
|
4418
3929
|
}
|
|
4419
3930
|
|
|
4420
3931
|
/**
|
|
@@ -6518,6 +6029,56 @@ async function runKustomizationImagesCleanup(kustAbs, rel, fail, pass) {
|
|
|
6518
6029
|
* Перевіряє відповідність проєкту правилам k8s.mdc.
|
|
6519
6030
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
6520
6031
|
*/
|
|
6032
|
+
/**
|
|
6033
|
+
* Plan B (rego-authoritative): на початку `check()` батч-викликаємо всі 9 path-фільтрованих
|
|
6034
|
+
* rego-пакетів з `npm/policy/k8s/` через `runConftestBatch`. Пакети hasura_configmap і
|
|
6035
|
+
* hasura_httproute мають cross-file gating (паруються з Hasura-Deployment) — вони запускаються
|
|
6036
|
+
* з відповідних orchestrator-функцій (`validateHasuraConfigMapRemoteSchemaPermissions`,
|
|
6037
|
+
* `validateHasuraHttpRouteCanon`). Структурна частина HPA/PDB (`k8s.hpa_pdb`) тут на всіх yaml,
|
|
6038
|
+
* env-залежні межі min/maxReplicas і expected-name — JS-cross-file у `validateDeploymentHpaPdbAndTopology`.
|
|
6039
|
+
* @param {string} root корінь репозиторію (cwd)
|
|
6040
|
+
* @param {string[]} yamlFiles абсолютні шляхи знайдених *.yaml під `…/k8s/`
|
|
6041
|
+
* @param {Set<string>} kustomizeManagedRel відносні posix-шляхи kustomize-managed файлів
|
|
6042
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
6043
|
+
* @returns {void}
|
|
6044
|
+
*/
|
|
6045
|
+
function runAllK8sRego(root, yamlFiles, kustomizeManagedRel, fail) {
|
|
6046
|
+
const relOf = abs => relative(root, abs).replaceAll('\\', '/') || abs
|
|
6047
|
+
|
|
6048
|
+
const allYaml = yamlFiles
|
|
6049
|
+
const kustYaml = yamlFiles.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')
|
|
6050
|
+
const svcYaml = yamlFiles.filter(p => basename(p) === 'svc.yaml')
|
|
6051
|
+
const svcHlYaml = yamlFiles.filter(p => basename(p) === 'svc-hl.yaml')
|
|
6052
|
+
const baseKustYaml = yamlFiles.filter(p => isBaseKustomizationPath(relOf(p)))
|
|
6053
|
+
const baseResourceYaml = yamlFiles.filter(p => {
|
|
6054
|
+
const r = relOf(p)
|
|
6055
|
+
if (!K8S_BASE_SEGMENT_RE.test(r)) return false
|
|
6056
|
+
return basename(p).toLowerCase() !== 'kustomization.yaml'
|
|
6057
|
+
})
|
|
6058
|
+
const kustomizeManagedFiles = yamlFiles.filter(p => kustomizeManagedRel.has(relOf(p)))
|
|
6059
|
+
|
|
6060
|
+
/** @type {Array<{ ns: string, dir: string, files: string[] }>} */
|
|
6061
|
+
const targets = [
|
|
6062
|
+
{ ns: 'k8s.manifest', dir: 'k8s/manifest', files: allYaml },
|
|
6063
|
+
{ ns: 'k8s.gateway', dir: 'k8s/gateway', files: allYaml },
|
|
6064
|
+
{ ns: 'k8s.hpa_pdb', dir: 'k8s/hpa_pdb', files: allYaml },
|
|
6065
|
+
{ ns: 'k8s.kustomization', dir: 'k8s/kustomization', files: kustYaml },
|
|
6066
|
+
{ ns: 'k8s.svc_yaml', dir: 'k8s/svc_yaml', files: svcYaml },
|
|
6067
|
+
{ ns: 'k8s.svc_hl_yaml', dir: 'k8s/svc_hl_yaml', files: svcHlYaml },
|
|
6068
|
+
{ ns: 'k8s.base_kustomization', dir: 'k8s/base_kustomization', files: baseKustYaml },
|
|
6069
|
+
{ ns: 'k8s.base_manifest', dir: 'k8s/base_manifest', files: baseResourceYaml },
|
|
6070
|
+
{ ns: 'k8s.kustomize_managed', dir: 'k8s/kustomize_managed', files: kustomizeManagedFiles }
|
|
6071
|
+
]
|
|
6072
|
+
|
|
6073
|
+
for (const t of targets) {
|
|
6074
|
+
if (t.files.length === 0) continue
|
|
6075
|
+
const violations = runConftestBatch({ policyDirRel: t.dir, namespace: t.ns, files: t.files })
|
|
6076
|
+
for (const v of violations) {
|
|
6077
|
+
fail(`${relOf(v.filename)}: ${v.message}`)
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
|
|
6521
6082
|
export async function check() {
|
|
6522
6083
|
const reporter = createCheckReporter()
|
|
6523
6084
|
const { pass, fail } = reporter
|
|
@@ -6544,6 +6105,12 @@ export async function check() {
|
|
|
6544
6105
|
|
|
6545
6106
|
const kustomizeManagedRel = await collectKustomizeManagedRelPaths(root, yamlFiles)
|
|
6546
6107
|
|
|
6108
|
+
// Plan B: пер-документні структурні правила — у rego-полісі `npm/policy/k8s/*`,
|
|
6109
|
+
// викликаємо одним батчем на namespace через runConftestBatch. JS нижче робить
|
|
6110
|
+
// лише cross-file orchestration, modeline та FS-existence перевірки.
|
|
6111
|
+
runAllK8sRego(root, yamlFiles, kustomizeManagedRel, fail)
|
|
6112
|
+
pass(`Rego-полісі (npm/policy/k8s/*) виконано на ${yamlFiles.length} файл(ах)`)
|
|
6113
|
+
|
|
6547
6114
|
for (const abs of yamlFiles) {
|
|
6548
6115
|
await checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel)
|
|
6549
6116
|
}
|
|
@@ -6554,20 +6121,12 @@ export async function check() {
|
|
|
6554
6121
|
|
|
6555
6122
|
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
6556
6123
|
|
|
6557
|
-
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|
|
6558
|
-
|
|
6559
6124
|
await validateKustomizationPathRefsExistOnDisk(root, yamlFiles, fail)
|
|
6560
6125
|
|
|
6561
|
-
await validateKustomizationResourcesSortedAlphabetically(root, yamlFiles, fail)
|
|
6562
|
-
|
|
6563
|
-
await validateKustomizationPatchesStructuralSort(root, yamlFiles, fail)
|
|
6564
|
-
|
|
6565
6126
|
await validateKustomizationPatchTargetsResolved(root, yamlFiles, fail)
|
|
6566
6127
|
|
|
6567
6128
|
await validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFiles, fail, pass)
|
|
6568
6129
|
|
|
6569
|
-
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
6570
|
-
|
|
6571
6130
|
await validateConfigMapNameMatchesDeployment(root, yamlFiles, fail, pass)
|
|
6572
6131
|
|
|
6573
6132
|
await validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFiles, fail, pass)
|