@nitra/cursor 1.13.31 → 1.13.38

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/package.json +1 -1
  3. package/rules/changelog/fix/consistency/check.mjs +100 -85
  4. package/rules/ci4/ci4.mdc +7 -7
  5. package/rules/image-avif/image-avif.mdc +2 -2
  6. package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
  7. package/rules/js-run/fix/runtime/check.mjs +3 -0
  8. package/rules/js-run/js-run.mdc +16 -1
  9. package/rules/js-run/policy/package_json/package_json.rego +17 -0
  10. package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
  11. package/rules/k8s/fix/kubescape_exceptions/template/.kubescape-exceptions.json.snippet.json +21 -0
  12. package/rules/k8s/fix/manifests/check.mjs +775 -139
  13. package/rules/k8s/k8s.mdc +60 -6
  14. package/rules/k8s/lint/lint.mjs +29 -4
  15. package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
  16. package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
  17. package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
  18. package/rules/security/fix/trufflehog/check.mjs +3 -0
  19. package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
  20. package/rules/style-lint/style-lint.mdc +20 -1
  21. package/rules/text/policy/cspell/cspell.rego +1 -1
  22. package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
  23. package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
  24. package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
  25. package/rules/vue/vue.mdc +1 -0
  26. package/scripts/sync-claude-config.mjs +2 -2
  27. package/scripts/utils/check-mdc-template-refs.mjs +15 -5
  28. package/scripts/utils/inline-template-links.mjs +15 -8
  29. package/scripts/utils/package-manifest.mjs +24 -19
  30. package/scripts/utils/run-conftest-batch.mjs +22 -15
  31. package/scripts/utils/template.mjs +89 -21
@@ -88,13 +88,17 @@
88
88
  * **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`** (приймається булеве `true`
89
89
  * або рядок `"true"`, без регістрової залежності).
90
90
  *
91
- * **HPA / PDB / topologySpreadConstraints:** для кожного **`Deployment`** у шарі **`…/k8s/…/base/`** (будь-який
92
- * `.yaml` у цьому каталозі) обов'язкові канонічні **topologySpreadConstraints**, а HPA і PDB живуть у sibling
93
- * каталозі **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу `components`). У `base/`
94
- * заборонено тримати локальні `hpa.yaml` і `pdb.yaml` (file-existence error) і також у дереві base-kustomize
95
- * не повинно бути HPA/PDB через `resources` / `components` / `bases`. Структура `components/`:
96
- * `kustomization.yaml` з `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources` що містять
97
- * `hpa.yaml` і `pdb.yaml` (як єдині або принаймні обов'язкові), `hpa.yaml` (валідний `autoscaling/v2`
91
+ * **HPA / PDB / topologySpreadConstraints:** для кожного **`Deployment`** у шарі **`…/k8s/…/base/`**
92
+ * (будь-який `.yaml` у цьому каталозі) обов'язкові канонічні **topologySpreadConstraints**, а HPA і PDB
93
+ * живуть у sibling каталозі **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу `components`). У `base/`
94
+ * заборонено тримати локальні `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` (file-existence error) і також у дереві
95
+ * base-kustomize не повинно бути HPA/PDB/NetworkPolicy через `resources` / `components` / `bases`.
96
+ * **NetworkPolicy:** для кожного **`Deployment`**, **`StatefulSet`**, **`DaemonSet`**, **`Job`**, **`CronJob`** під `k8s`
97
+ * обов'язковий канонічний NetworkPolicy (base → `components/networkpolicy.yaml`, інші шари `networkpolicy.yaml` поруч).
98
+ * Egress: kube-dns; **TCP 80/443** на `0.0.0.0/0`; інші порти — `namespaceSelector: {}` (in-cluster / `*.svc`). Заборонено `egress: [{}]`.
99
+ * Відсутні документи **`check k8s`** створює автоматично (multi-doc у одному файлі, якщо workload-ів кілька).
100
+ * Структура `components/`: `kustomization.yaml` з `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`,
101
+ * `resources` що містять `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml`, `hpa.yaml` (валідний `autoscaling/v2`
98
102
  * HorizontalPodAutoscaler з `scaleTargetRef.name` = ім'я Deployment, dev-like `min=max=1`), `pdb.yaml` (валідний
99
103
  * `policy/v1` PodDisruptionBudget з `selector.matchLabels.app` = мітка `app` Deployment, dev-like `minAvailable=0`).
100
104
  * Overlays (`ua/`, прод-overlays) підключають `components: [- ../components]` і додають JSON6902-патчі для
@@ -129,7 +133,7 @@
129
133
  * поки в наслідуваному `base` у дереві не з'явиться такий Deployment (k8s.mdc).
130
134
  */
131
135
  import { existsSync } from 'node:fs'
132
- import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
136
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
133
137
  import { basename, dirname, join, relative, resolve } from 'node:path'
134
138
 
135
139
  import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
@@ -140,6 +144,8 @@ import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.m
140
144
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
141
145
 
142
146
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
147
+ const YAML_LS_MODELINE_RE = /^# yaml-language-server: \$schema=.*\n/
148
+
143
149
  const YANNH_PIN = 'v1.33.9-standalone-strict'
144
150
 
145
151
  /**
@@ -148,7 +154,9 @@ const YANNH_PIN = 'v1.33.9-standalone-strict'
148
154
  */
149
155
  export const HASURA_GRAPHQL_ENGINE_IMAGE = 'hasura/graphql-engine:v2.48.15.ubi.amd64'
150
156
 
151
- /** Набір прийнятних рядків `image` без digest (`@sha256:…`). */
157
+ /**
158
+ Набір прийнятних рядків `image` без digest (`@sha256:…`).
159
+ */
152
160
  const HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES = new Set([
153
161
  HASURA_GRAPHQL_ENGINE_IMAGE,
154
162
  `docker.io/${HASURA_GRAPHQL_ENGINE_IMAGE}`
@@ -464,7 +472,9 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
464
472
  if (!Array.isArray(res)) {
465
473
  return 'Kustomization.resources має бути масивом (k8s.mdc)'
466
474
  }
467
- /** @type {string[]} */
475
+ /**
476
+ @type {string[]}
477
+ */
468
478
  const paths = []
469
479
  for (const [i, item] of res.entries()) {
470
480
  if (typeof item !== 'string') {
@@ -530,7 +540,9 @@ function kustomizationPatchSortKey(patchItem) {
530
540
  }
531
541
  const rec = /** @type {Record<string, unknown>} */ (patchItem)
532
542
  const t = rec.target
533
- /** @type {Record<string, unknown>} */
543
+ /**
544
+ @type {Record<string, unknown>}
545
+ */
534
546
  const target =
535
547
  t !== null && typeof t === 'object' && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {}
536
548
  const kind = typeof target.kind === 'string' ? target.kind : ''
@@ -614,7 +626,9 @@ function parseJson6902OpsFromText(raw) {
614
626
  return null
615
627
  }
616
628
  if (!Array.isArray(parsed)) return null
617
- /** @type {{ op: string, path: string }[]} */
629
+ /**
630
+ @type {{ op: string, path: string }[]}
631
+ */
618
632
  const out = []
619
633
  for (const item of parsed) {
620
634
  if (item === null || typeof item !== 'object' || Array.isArray(item)) return null
@@ -643,7 +657,9 @@ export function kustomizationInlinePatchOpsSortedViolation(patchText) {
643
657
  }
644
658
  const paths = ops.map(o => o.path)
645
659
  if (!jsonPointerPathsAreDisjoint(paths)) return null
646
- /** @type {string[][]} */
660
+ /**
661
+ @type {string[][]}
662
+ */
647
663
  const keys = paths.map(p => [p])
648
664
  if (stringTuplesAreSortedEn(keys)) return null
649
665
  const want = paths.toSorted((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))
@@ -662,7 +678,9 @@ export function kustomizationInlinePatchOpsSortedViolation(patchText) {
662
678
  function pathsFromKustomizationObject(obj) {
663
679
  if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
664
680
  const rec = /** @type {Record<string, unknown>} */ (obj)
665
- /** @type {string[]} */
681
+ /**
682
+ @type {string[]}
683
+ */
666
684
  const out = []
667
685
  pushStringPaths(rec.resources, out)
668
686
  pushStringPaths(rec.bases, out)
@@ -687,8 +705,8 @@ function pathsFromKustomizationObject(obj) {
687
705
  }
688
706
 
689
707
  /**
690
- * @param {unknown} arr масив (може бути не масивом)
691
- * @param {string[]} out вихідний масив
708
+ * @param {unknown} arr масив об'єктів із полем `path` (може бути не масивом)
709
+ * @param {string[]} out вихідний масив для накопичення значень `path`
692
710
  */
693
711
  function collectObjectPathFields(arr, out) {
694
712
  if (!Array.isArray(arr)) return
@@ -703,8 +721,8 @@ function collectObjectPathFields(arr, out) {
703
721
  }
704
722
 
705
723
  /**
706
- * @param {unknown} arr масив (може бути не масивом)
707
- * @param {string[]} out вихідний масив
724
+ * @param {unknown} arr масив рядків (може бути не масивом)
725
+ * @param {string[]} out вихідний масив для накопичення непорожніх рядків
708
726
  */
709
727
  function collectStringPaths(arr, out) {
710
728
  if (!Array.isArray(arr)) return
@@ -739,8 +757,8 @@ export function kustomizePathRefsForExistenceCheck(obj) {
739
757
  * @param {string} r посилання з kustomization
740
758
  * @param {string} kustDir каталог kustomization.yaml
741
759
  * @param {string} rootNorm нормалізований корінь
742
- * @param {(msg: string) => void} fail callback
743
- * @returns {Promise<void>}
760
+ * @param {(msg: string) => void} fail callback при помилці
761
+ * @returns {Promise<void>} резолвиться по завершенню перевірки
744
762
  */
745
763
  async function validateKustomizationRef(rel, r, kustDir, rootNorm, fail) {
746
764
  const target = resolve(kustDir, r.trim())
@@ -752,7 +770,9 @@ async function validateKustomizationRef(rel, r, kustDir, rootNorm, fail) {
752
770
  )
753
771
  return
754
772
  }
755
- /** @type {import('node:fs').Stats | undefined} */
773
+ /**
774
+ @type {import('node:fs').Stats | undefined}
775
+ */
756
776
  let st
757
777
  try {
758
778
  st = await stat(target)
@@ -778,7 +798,7 @@ async function validateKustomizationRef(rel, r, kustDir, rootNorm, fail) {
778
798
  * @param {string} kustAbs kustomization.yaml
779
799
  * @param {string} rootNorm нормалізований корінь
780
800
  * @param {(msg: string) => void} fail callback
781
- * @returns {Promise<void>}
801
+ * @returns {Promise<void>} результат
782
802
  */
783
803
  async function validateOneKustomizationPathRefsExist(root, kustAbs, rootNorm, fail) {
784
804
  const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
@@ -800,7 +820,7 @@ async function validateOneKustomizationPathRefsExist(root, kustAbs, rootNorm, fa
800
820
  * @param {string} root корінь репозиторію
801
821
  * @param {string[]} yamlFilesAbs абсолютні шляхи YAML-файлів у k8s
802
822
  * @param {(msg: string) => void} fail callback для повідомлень про помилки
803
- * @returns {Promise<void>}
823
+ * @returns {Promise<void>} результат
804
824
  */
805
825
  async function validateKustomizationPathRefsExistOnDisk(root, yamlFilesAbs, fail) {
806
826
  const rootNorm = resolve(root)
@@ -817,7 +837,9 @@ async function validateKustomizationPathRefsExistOnDisk(root, yamlFilesAbs, fail
817
837
  * @returns {string | null} текст порушення або null, якщо ок
818
838
  */
819
839
  export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, pathRefs) {
820
- /** @type {Set<string>} */
840
+ /**
841
+ @type {Set<string>}
842
+ */
821
843
  const resolved = new Set()
822
844
  for (const ref of pathRefs) {
823
845
  if (typeof ref === 'string' && !ref.includes('://')) {
@@ -843,7 +865,7 @@ export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, path
843
865
  * @param {string} root корінь репозиторію
844
866
  * @param {string} kustAbs абсолютний шлях до kustomization.yaml
845
867
  * @param {(msg: string) => void} fail реєстрація помилки
846
- * @returns {Promise<void>}
868
+ * @returns {Promise<void>} результат
847
869
  */
848
870
  async function validateOneKustomizationSvcHlWithSvc(root, kustAbs, fail) {
849
871
  const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
@@ -857,7 +879,9 @@ async function validateOneKustomizationSvcHlWithSvc(root, kustAbs, fail) {
857
879
  }
858
880
  const lines = toLines(raw)
859
881
  const body = yamlBodyAfterModeline(lines)
860
- /** @type {import('yaml').Document[] | undefined} */
882
+ /**
883
+ @type {import('yaml').Document[] | undefined}
884
+ */
861
885
  let docs
862
886
  try {
863
887
  docs = parseAllDocuments(body)
@@ -882,7 +906,7 @@ async function validateOneKustomizationSvcHlWithSvc(root, kustAbs, fail) {
882
906
  * @param {string} root корінь репозиторію
883
907
  * @param {string[]} yamlFiles абсолютні шляхи до yaml під k8s
884
908
  * @param {(msg: string) => void} fail callback помилки
885
- * @returns {Promise<void>}
909
+ * @returns {Promise<void>} результат
886
910
  */
887
911
  async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail) {
888
912
  for (const kustAbs of yamlFiles.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
@@ -898,7 +922,9 @@ async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
898
922
  function resourcePathRefsFromKustomizationObject(obj) {
899
923
  if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
900
924
  const rec = /** @type {Record<string, unknown>} */ (obj)
901
- /** @type {string[]} */
925
+ /**
926
+ @type {string[]}
927
+ */
902
928
  const out = []
903
929
  pushStringPaths(rec.resources, out)
904
930
  pushStringPaths(rec.bases, out)
@@ -1118,9 +1144,13 @@ async function readK8sYamlDocumentRootsForInventory(abs) {
1118
1144
  }
1119
1145
  const lines = toLines(raw)
1120
1146
  const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
1121
- /** @type {unknown[]} */
1147
+ /**
1148
+ @type {unknown[]}
1149
+ */
1122
1150
  const roots = parseK8sYamlDocumentObjectRoots(body)
1123
- /** @type {Record<string, unknown>[]} */
1151
+ /**
1152
+ @type {Record<string, unknown>[]}
1153
+ */
1124
1154
  const out = []
1125
1155
  for (const r of roots) {
1126
1156
  if (r !== null && typeof r === 'object' && !Array.isArray(r)) {
@@ -1155,7 +1185,9 @@ async function collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visit
1155
1185
  const lines = toLines(raw)
1156
1186
  const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
1157
1187
 
1158
- /** @type {import('yaml').Document[] | undefined} */
1188
+ /**
1189
+ @type {import('yaml').Document[] | undefined}
1190
+ */
1159
1191
  let docs
1160
1192
  try {
1161
1193
  docs = parseAllDocuments(body)
@@ -1169,12 +1201,14 @@ async function collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visit
1169
1201
  const kustDir = dirname(normKust)
1170
1202
  const pathRefs = resourcePathRefsFromKustomizationObject(first)
1171
1203
 
1172
- /** @type {string[]} */
1204
+ /**
1205
+ @type {string[]}
1206
+ */
1173
1207
  const out = []
1174
1208
 
1175
1209
  /**
1176
1210
  * @param {string} ref шлях з resources/bases/…
1177
- * @returns {Promise<void>}
1211
+ * @returns {Promise<void>} результат
1178
1212
  */
1179
1213
  async function handleResourcePathRef(ref) {
1180
1214
  if (typeof ref !== 'string' || ref.includes('://')) {
@@ -1184,7 +1218,9 @@ async function collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visit
1184
1218
  if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
1185
1219
  return
1186
1220
  }
1187
- /** @type {import('node:fs').Stats | undefined} */
1221
+ /**
1222
+ @type {import('node:fs').Stats | undefined}
1223
+ */
1188
1224
  let st
1189
1225
  try {
1190
1226
  st = await stat(resolved)
@@ -1258,7 +1294,9 @@ export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, ro
1258
1294
  const lines = toLines(raw)
1259
1295
  const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
1260
1296
 
1261
- /** @type {import('yaml').Document[] | undefined} */
1297
+ /**
1298
+ @type {import('yaml').Document[] | undefined}
1299
+ */
1262
1300
  let docs
1263
1301
  try {
1264
1302
  docs = parseAllDocuments(body)
@@ -1274,14 +1312,20 @@ export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, ro
1274
1312
  const kustDir = dirname(normKust)
1275
1313
  const pathRefs = resourcePathRefsFromKustomizationObject(first)
1276
1314
 
1277
- /** @type {KustomizeResourceDescriptor[]} */
1315
+ /**
1316
+ @type {KustomizeResourceDescriptor[]}
1317
+ */
1278
1318
  const out = []
1279
1319
 
1320
+ /*
1321
+ * @param {string} ref шлях з resources/bases/…
1322
+
1323
+ * @returns {Promise<void>} результат
1324
+ */
1280
1325
  /**
1281
- * @param {string} ref шлях з resources/bases/…
1282
- * @returns {Promise<void>}
1283
- */
1284
- async function handleResourceDescriptorPathRef(ref) {
1326
+ *
1327
+ * @param {*} ref параметр
1328
+ */ async function handleResourceDescriptorPathRef(ref) {
1285
1329
  if (typeof ref !== 'string' || ref.includes('://')) {
1286
1330
  return
1287
1331
  }
@@ -1289,7 +1333,9 @@ export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, ro
1289
1333
  if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
1290
1334
  return
1291
1335
  }
1292
- /** @type {import('node:fs').Stats | undefined} */
1336
+ /**
1337
+ @type {import('node:fs').Stats | undefined}
1338
+ */
1293
1339
  let st
1294
1340
  try {
1295
1341
  st = await stat(resolved)
@@ -1336,13 +1382,17 @@ function extractExplicitPatchTargetsFromKustomization(obj) {
1336
1382
  return []
1337
1383
  }
1338
1384
  const rec = /** @type {Record<string, unknown>} */ (obj)
1339
- /** @type {Array<{ section: string, index: number, target: unknown }>} */
1340
- const out = []
1341
1385
  /**
1342
- * @param {string} section ім’я поля
1343
- * @param {unknown} arr масив з YAML
1344
- * @returns {void}
1386
+ @type {Array<{ section: string, index: number, target: unknown }>}
1345
1387
  */
1388
+ const out = []
1389
+ /*
1390
+ * @param {string} section ім’я поля
1391
+
1392
+ * @param {unknown} arr масив з YAML
1393
+
1394
+ * @returns {void} результат
1395
+ */
1346
1396
  const push = (section, arr) => {
1347
1397
  if (!Array.isArray(arr)) {
1348
1398
  return
@@ -1403,7 +1453,7 @@ function formatKustomizePatchTargetForMessage(target) {
1403
1453
  * @param {Record<string, unknown>} first корінь Kustomization
1404
1454
  * @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
1405
1455
  * @param {(msg: string) => void} fail реєстрація помилки
1406
- * @returns {void}
1456
+ * @returns {void} результат
1407
1457
  */
1408
1458
  function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
1409
1459
  for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
@@ -1421,7 +1471,7 @@ function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
1421
1471
  * @param {Record<string, unknown>} first корінь Kustomization
1422
1472
  * @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
1423
1473
  * @param {(msg: string) => void} fail реєстрація помилки
1424
- * @returns {void}
1474
+ * @returns {void} результат
1425
1475
  */
1426
1476
  function failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail) {
1427
1477
  for (const entry of extractExplicitPatchTargetsFromKustomization(first)) {
@@ -1455,7 +1505,9 @@ function describePatchTargetRedundancy(entry, catalog) {
1455
1505
  const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
1456
1506
  const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
1457
1507
  if (distinctGvk.size > 1) return null
1458
- /** @type {string[]} */
1508
+ /**
1509
+ @type {string[]}
1510
+ */
1459
1511
  const redundant = []
1460
1512
  if (tgtGroup !== '') redundant.push('group')
1461
1513
  if (tgtVersion !== '') redundant.push('version')
@@ -1472,7 +1524,7 @@ function describePatchTargetRedundancy(entry, catalog) {
1472
1524
  * @param {KustomizeResourceDescriptor[]} catalog інвентар
1473
1525
  * @param {string} kustNs default namespace
1474
1526
  * @param {(msg: string) => void} fail реєстрація помилки
1475
- * @returns {Promise<void>}
1527
+ * @returns {Promise<void>} результат
1476
1528
  */
1477
1529
  async function failIfYamlFileRootsMissingFromCatalog(
1478
1530
  rel,
@@ -1510,7 +1562,9 @@ async function resolveExistingYamlFileUnderRoot(kustDir, pathStr, rootNorm) {
1510
1562
  if (!resolvedFilePathIsUnderRoot(rootNorm, resolved) || !existsSync(resolved)) {
1511
1563
  return null
1512
1564
  }
1513
- /** @type {import('node:fs').Stats | null} */
1565
+ /**
1566
+ @type {import('node:fs').Stats | null}
1567
+ */
1514
1568
  let st
1515
1569
  try {
1516
1570
  st = await stat(resolved)
@@ -1534,7 +1588,7 @@ async function resolveExistingYamlFileUnderRoot(kustDir, pathStr, rootNorm) {
1534
1588
  * @param {KustomizeResourceDescriptor[]} catalog інвентар
1535
1589
  * @param {string} kustNs default namespace з kustomization
1536
1590
  * @param {(msg: string) => void} fail реєстрація помилки
1537
- * @returns {Promise<void>}
1591
+ * @returns {Promise<void>} результат
1538
1592
  */
1539
1593
  async function failIfOnePathOnlyPatchNotInCatalog(rel, p, pIdx, kustDir, rootNorm, root, catalog, kustNs, fail) {
1540
1594
  if (p === null || typeof p !== 'object' || Array.isArray(p)) {
@@ -1573,7 +1627,7 @@ async function failIfOnePathOnlyPatchNotInCatalog(rel, p, pIdx, kustDir, rootNor
1573
1627
  * @param {KustomizeResourceDescriptor[]} catalog інвентар
1574
1628
  * @param {string} kustNs default namespace з kustomization
1575
1629
  * @param {(msg: string) => void} fail реєстрація помилки
1576
- * @returns {Promise<void>}
1630
+ * @returns {Promise<void>} результат
1577
1631
  */
1578
1632
  async function failIfPathOnlyPatchesNotInCatalog(rel, patches, kustDir, rootNorm, root, catalog, kustNs, fail) {
1579
1633
  if (!Array.isArray(patches)) {
@@ -1596,7 +1650,7 @@ async function failIfPathOnlyPatchesNotInCatalog(rel, patches, kustDir, rootNorm
1596
1650
  * @param {KustomizeResourceDescriptor[]} catalog інвентар
1597
1651
  * @param {string} kustNs default namespace з kustomization
1598
1652
  * @param {(msg: string) => void} fail реєстрація помилки
1599
- * @returns {Promise<void>}
1653
+ * @returns {Promise<void>} результат
1600
1654
  */
1601
1655
  async function failIfStrategicMergePatchesNotInCatalog(rel, sm, kustDir, rootNorm, root, catalog, kustNs, fail) {
1602
1656
  if (!Array.isArray(sm)) {
@@ -1629,7 +1683,7 @@ async function failIfStrategicMergePatchesNotInCatalog(rel, sm, kustDir, rootNor
1629
1683
  * @param {string} kustAbs абсолютний шлях до файлу
1630
1684
  * @param {string} rootNorm нормалізований корінь
1631
1685
  * @param {(msg: string) => void} fail реєстрація помилки
1632
- * @returns {Promise<void>}
1686
+ * @returns {Promise<void>} результат
1633
1687
  */
1634
1688
  async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm, fail) {
1635
1689
  const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
@@ -1643,7 +1697,9 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
1643
1697
  }
1644
1698
  const lines = toLines(raw)
1645
1699
  const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
1646
- /** @type {import('yaml').Document[]} */
1700
+ /**
1701
+ @type {import('yaml').Document[]}
1702
+ */
1647
1703
  let docs
1648
1704
  try {
1649
1705
  docs = parseAllDocuments(body)
@@ -1683,7 +1739,7 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
1683
1739
  * @param {string} root корінь репозиторію
1684
1740
  * @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
1685
1741
  * @param {(msg: string) => void} fail реєстрація помилки
1686
- * @returns {Promise<void>}
1742
+ * @returns {Promise<void>} результат
1687
1743
  */
1688
1744
  async function validateKustomizationPatchTargetsResolved(root, yamlFilesAbs, fail) {
1689
1745
  const rootNorm = resolve(root)
@@ -1726,7 +1782,9 @@ export function baseKustomizationNamespaceViolation(obj) {
1726
1782
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
1727
1783
  */
1728
1784
  async function findK8sYamlFiles(root, ignorePaths = []) {
1729
- /** @type {string[]} */
1785
+ /**
1786
+ @type {string[]}
1787
+ */
1730
1788
  const out = []
1731
1789
  await walkDir(
1732
1790
  root,
@@ -1761,7 +1819,7 @@ function k8sYamlBodyForDocumentParse(lines) {
1761
1819
  * Оновлює прапорці наявності **BackendConfig** / інших **kind** у документі.
1762
1820
  * @param {unknown} kind значення **kind**
1763
1821
  * @param {{ hasBc: boolean, hasOther: boolean }} acc накопичувач
1764
- * @returns {void}
1822
+ * @returns {void} результат
1765
1823
  */
1766
1824
  function updateBackendConfigKindFlags(kind, acc) {
1767
1825
  if (kind === 'BackendConfig') {
@@ -1779,7 +1837,9 @@ function updateBackendConfigKindFlags(kind, acc) {
1779
1837
  * @returns {'none' | 'only' | 'mixed' | 'unparsed'} unparsed — не вдалося розпарсити YAML
1780
1838
  */
1781
1839
  export function classifyBackendConfigManifestPresence(body) {
1782
- /** @type {import('yaml').Document[]} */
1840
+ /**
1841
+ @type {import('yaml').Document[]}
1842
+ */
1783
1843
  let docs
1784
1844
  try {
1785
1845
  docs = parseAllDocuments(body)
@@ -1812,7 +1872,7 @@ export function classifyBackendConfigManifestPresence(body) {
1812
1872
  * @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
1813
1873
  * @param {(msg: string) => void} fail реєстрація порушення
1814
1874
  * @param {(msg: string) => void} pass реєстрація успіху
1815
- * @returns {Promise<void>}
1875
+ * @returns {Promise<void>} результат
1816
1876
  */
1817
1877
  async function removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass) {
1818
1878
  const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
@@ -1890,7 +1950,7 @@ export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
1890
1950
  * @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
1891
1951
  * @param {(msg: string) => void} fail колбек повідомлення про помилку
1892
1952
  * @param {(msg: string) => void} pass колбек успішного повідомлення
1893
- * @returns {Promise<void>}
1953
+ * @returns {Promise<void>} результат
1894
1954
  */
1895
1955
  async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass) {
1896
1956
  const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
@@ -1985,9 +2045,13 @@ function firstYamlDocument(body) {
1985
2045
  * @returns {{ apiVersion?: string, kind?: string }} знайдені поля або властивості відсутні
1986
2046
  */
1987
2047
  function extractApiVersionAndKind(doc) {
1988
- /** @type {string | undefined} */
2048
+ /**
2049
+ @type {string | undefined}
2050
+ */
1989
2051
  let apiVersion
1990
- /** @type {string | undefined} */
2052
+ /**
2053
+ @type {string | undefined}
2054
+ */
1991
2055
  let kind
1992
2056
  for (const line of doc.split(YAML_LINE_SPLIT_RE)) {
1993
2057
  if (apiVersion === undefined) {
@@ -2052,7 +2116,9 @@ function normalizeJsonPatchPath(p) {
2052
2116
  * @returns {Array<{ op: string, path: string }>} **op** у нижньому регістрі
2053
2117
  */
2054
2118
  function extractJson6902OpsFromArray(arr) {
2055
- /** @type {Array<{ op: string, path: string }>} */
2119
+ /**
2120
+ @type {Array<{ op: string, path: string }>}
2121
+ */
2056
2122
  const out = []
2057
2123
  for (const item of arr) {
2058
2124
  if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
@@ -2113,7 +2179,9 @@ export function collectJson6902OperationsFromPatchText(patchText) {
2113
2179
  * @returns {string[]} унікальні **path** з порушенням (відсортовано)
2114
2180
  */
2115
2181
  export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
2116
- /** @type {Map<string, Set<string>>} */
2182
+ /**
2183
+ @type {Map<string, Set<string>>}
2184
+ */
2117
2185
  const byPath = new Map()
2118
2186
  for (const { op, path } of ops) {
2119
2187
  if (path) {
@@ -2123,7 +2191,9 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
2123
2191
  byPath.get(path).add(op)
2124
2192
  }
2125
2193
  }
2126
- /** @type {string[]} */
2194
+ /**
2195
+ @type {string[]}
2196
+ */
2127
2197
  const out = []
2128
2198
  for (const [path, set] of byPath) {
2129
2199
  if (set.has('remove') && set.has('add')) {
@@ -2147,7 +2217,7 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
2147
2217
  * @param {string} rootNorm нормалізований корінь репо
2148
2218
  * @param {string} root корінь репо
2149
2219
  * @param {(msg: string) => void} fail реєстрація порушення
2150
- * @returns {Promise<void>}
2220
+ * @returns {Promise<void>} результат
2151
2221
  */
2152
2222
  /**
2153
2223
  * Plan B: per-document JSON6902 remove+add conflict — у rego-пакеті
@@ -2538,7 +2608,9 @@ function findFirstDocByKind(docs, kind) {
2538
2608
  * @returns {Record<string, unknown>[]} знайдені об'єкти
2539
2609
  */
2540
2610
  function collectDocsByKind(docs, kind) {
2541
- /** @type {Record<string, unknown>[]} */
2611
+ /**
2612
+ @type {Record<string, unknown>[]}
2613
+ */
2542
2614
  const out = []
2543
2615
  for (const doc of docs) {
2544
2616
  if (doc.errors.length === 0) {
@@ -2669,7 +2741,9 @@ function collectConfigMapRefsFromVolumes(volumes, names) {
2669
2741
  * @returns {Set<string>} унікальні імена ConfigMap
2670
2742
  */
2671
2743
  export function collectDeploymentConfigMapRefs(deployment) {
2672
- /** @type {Set<string>} */
2744
+ /**
2745
+ @type {Set<string>}
2746
+ */
2673
2747
  const names = new Set()
2674
2748
  const ps = extractPodSpec(deployment)
2675
2749
  if (ps === null) return names
@@ -2698,7 +2772,9 @@ export function serviceForbiddenGcpAnnotationsViolation(manifest) {
2698
2772
  const ann = m.annotations
2699
2773
  if (ann === null || ann === undefined || typeof ann !== 'object' || Array.isArray(ann)) return null
2700
2774
  const a = /** @type {Record<string, unknown>} */ (ann)
2701
- /** @type {string[]} */
2775
+ /**
2776
+ @type {string[]}
2777
+ */
2702
2778
  const found = []
2703
2779
  for (const key of SERVICE_FORBIDDEN_GCP_ANNOTATION_KEYS) {
2704
2780
  if (Object.hasOwn(a, key)) {
@@ -2820,14 +2896,20 @@ function isGatewayApiBackendRefToService(obj) {
2820
2896
  * @returns {string[]} імена backend-сервісів (можливі дублікати)
2821
2897
  */
2822
2898
  export function collectGatewayApiRouteBackendServiceNames(spec) {
2823
- /** @type {string[]} */
2899
+ /**
2900
+ @type {string[]}
2901
+ */
2824
2902
  const out = []
2825
2903
 
2904
+ /*
2905
+ * @param {unknown} node вузол для обходу
2906
+
2907
+ * @returns {void} результат
2908
+ */
2826
2909
  /**
2827
- * @param {unknown} node вузол для обходу
2828
- * @returns {void}
2829
- */
2830
- function walk(node) {
2910
+ *
2911
+ * @param {*} node параметр
2912
+ */ function walk(node) {
2831
2913
  if (node === null || node === undefined) return
2832
2914
  if (Array.isArray(node)) {
2833
2915
  for (const x of node) {
@@ -2858,14 +2940,20 @@ export function collectGatewayApiRouteBackendServiceNames(spec) {
2858
2940
  * @returns {string[]} імена backend-сервісів з надлишковим **`namespace`** (можливі дублікати)
2859
2941
  */
2860
2942
  export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs) {
2861
- /** @type {string[]} */
2943
+ /**
2944
+ @type {string[]}
2945
+ */
2862
2946
  const out = []
2863
2947
 
2948
+ /*
2949
+ * @param {unknown} node вузол для обходу
2950
+
2951
+ * @returns {void} результат
2952
+ */
2864
2953
  /**
2865
- * @param {unknown} node вузол для обходу
2866
- * @returns {void}
2867
- */
2868
- function walk(node) {
2954
+ *
2955
+ * @param {*} node параметр
2956
+ */ function walk(node) {
2869
2957
  if (node === null || node === undefined) return
2870
2958
  if (Array.isArray(node)) {
2871
2959
  for (const x of node) {
@@ -3167,7 +3255,7 @@ function appendServiceNamesFromSvcRoots(roots, relForMsg, fileLabel, names, fail
3167
3255
  * @param {string[]} svcNames імена з **svc.yaml**
3168
3256
  * @param {string[]} hlNames імена з **svc-hl.yaml**
3169
3257
  * @param {(msg: string) => void} fail реєстрація помилки
3170
- * @returns {void}
3258
+ * @returns {void} результат
3171
3259
  */
3172
3260
  function validateSvcHlServiceNamePairing(relSvc, relHl, svcNames, hlNames, fail) {
3173
3261
  if (svcNames.length === 0) {
@@ -3209,7 +3297,7 @@ function validateSvcHlServiceNamePairing(relSvc, relHl, svcNames, hlNames, fail)
3209
3297
  * @param {string[]} yamlFiles абсолютні шляхи
3210
3298
  * @param {Set<string>} absSet той самий набір шляхів
3211
3299
  * @param {(msg: string) => void} fail реєстрація помилки
3212
- * @returns {void}
3300
+ * @returns {void} результат
3213
3301
  */
3214
3302
  function failIfSvcHlWithoutSiblingSvc(root, yamlFiles, absSet, fail) {
3215
3303
  for (const abs of yamlFiles.filter(p => basename(p).toLowerCase() === 'svc-hl.yaml')) {
@@ -3227,7 +3315,7 @@ function failIfSvcHlWithoutSiblingSvc(root, yamlFiles, absSet, fail) {
3227
3315
  * @param {Set<string>} absSet наявні yaml під k8s
3228
3316
  * @param {string} svcAbs абсолютний шлях до **svc.yaml**
3229
3317
  * @param {(msg: string) => void} fail реєстрація помилки
3230
- * @returns {Promise<void>}
3318
+ * @returns {Promise<void>} результат
3231
3319
  */
3232
3320
  async function validateOneSvcYamlHlPair(root, absSet, svcAbs, fail) {
3233
3321
  const rel = (relative(root, svcAbs) || svcAbs).replaceAll('\\', '/')
@@ -3249,12 +3337,16 @@ async function validateOneSvcYamlHlPair(root, absSet, svcAbs, fail) {
3249
3337
  }
3250
3338
  const svcRoots = parseK8sYamlDocumentObjectRoots(svcBody)
3251
3339
  const hlRoots = parseK8sYamlDocumentObjectRoots(hlBody)
3252
- /** @type {string[]} */
3340
+ /**
3341
+ @type {string[]}
3342
+ */
3253
3343
  const svcNames = []
3254
3344
  if (!appendServiceNamesFromSvcRoots(svcRoots, rel, 'svc.yaml', svcNames, fail)) {
3255
3345
  return
3256
3346
  }
3257
- /** @type {string[]} */
3347
+ /**
3348
+ @type {string[]}
3349
+ */
3258
3350
  const hlNames = []
3259
3351
  if (!appendServiceNamesFromSvcRoots(hlRoots, hlRel, 'svc-hl.yaml', hlNames, fail)) {
3260
3352
  return
@@ -3267,7 +3359,7 @@ async function validateOneSvcYamlHlPair(root, absSet, svcAbs, fail) {
3267
3359
  * @param {string} root корінь репозиторію
3268
3360
  * @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
3269
3361
  * @param {(msg: string) => void} fail callback помилки
3270
- * @returns {Promise<void>}
3362
+ * @returns {Promise<void>} результат
3271
3363
  */
3272
3364
  async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
3273
3365
  const absSet = new Set(yamlFiles)
@@ -3287,9 +3379,13 @@ async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
3287
3379
  * }>} індекс Hasura-Deployment-ів за каталогом і список HTTPRoute-документів
3288
3380
  */
3289
3381
  async function collectHasuraDeploymentsAndHttpRoutes(yamlFiles) {
3290
- /** @type {Map<string, Set<string>>} */
3382
+ /**
3383
+ @type {Map<string, Set<string>>}
3384
+ */
3291
3385
  const hasuraByDir = new Map()
3292
- /** @type {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]} */
3386
+ /**
3387
+ @type {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]}
3388
+ */
3293
3389
  const httpRoutes = []
3294
3390
 
3295
3391
  for (const abs of yamlFiles) {
@@ -3304,7 +3400,7 @@ async function collectHasuraDeploymentsAndHttpRoutes(yamlFiles) {
3304
3400
  * @param {string} abs абсолютний шлях до файлу
3305
3401
  * @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом
3306
3402
  * @param {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]} httpRoutes колектор HTTPRoute-документів
3307
- * @returns {Promise<void>}
3403
+ * @returns {Promise<void>} результат
3308
3404
  */
3309
3405
  async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
3310
3406
  let raw
@@ -3315,7 +3411,9 @@ async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
3315
3411
  }
3316
3412
  const lines = toLines(raw)
3317
3413
  const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
3318
- /** @type {import('yaml').Document[]} */
3414
+ /**
3415
+ @type {import('yaml').Document[]}
3416
+ */
3319
3417
  let docs
3320
3418
  try {
3321
3419
  docs = parseAllDocuments(body)
@@ -3343,7 +3441,7 @@ async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
3343
3441
  * @param {Record<string, unknown>} rec корінь YAML-документа
3344
3442
  * @param {string} dir абсолютний шлях до каталогу файлу
3345
3443
  * @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (під час обходу в нього додаються імена)
3346
- * @returns {void}
3444
+ * @returns {void} результат
3347
3445
  */
3348
3446
  function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
3349
3447
  if (!isHasuraDeploymentManifest(rec)) return
@@ -3364,7 +3462,7 @@ function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
3364
3462
  * @param {string} root корінь репозиторію
3365
3463
  * @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
3366
3464
  * @param {(msg: string) => void} fail callback реєстрації помилки
3367
- * @returns {Promise<void>}
3465
+ * @returns {Promise<void>} результат
3368
3466
  */
3369
3467
  async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
3370
3468
  const { hasuraByDir, httpRoutes } = await collectHasuraDeploymentsAndHttpRoutes(yamlFiles)
@@ -3551,7 +3649,7 @@ function countSchemaModelines(lines) {
3551
3649
  * @param {string[]} _lines рядки файлу (лишені з тієї ж причини)
3552
3650
  * @param {(msg: string) => void} _fail реєстрація помилки (rego гейтує per-document)
3553
3651
  * @param {(msg: string) => void} pass реєстрація успіху
3554
- * @returns {void}
3652
+ * @returns {void} результат
3555
3653
  */
3556
3654
  function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass) {
3557
3655
  // Per-document валідація (Ingress/autoscaling/v1 заборонено, Gateway API backendRef,
@@ -3568,7 +3666,7 @@ function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass)
3568
3666
  * @param {string[]} lines рядки файлу
3569
3667
  * @param {(msg: string) => void} fail реєстрація помилки
3570
3668
  * @param {(msg: string) => void} pass реєстрація успіху
3571
- * @returns {void}
3669
+ * @returns {void} результат
3572
3670
  */
3573
3671
  function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass) {
3574
3672
  const match = lines[0].match(MODELINE_RE)
@@ -3623,7 +3721,7 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
3623
3721
  * @param {string} root корінь репозиторію
3624
3722
  * @param {(msg: string) => void} fail реєстрація помилки
3625
3723
  * @param {(msg: string) => void} pass реєстрація успіху
3626
- * @returns {Promise<void>}
3724
+ * @returns {Promise<void>} результат
3627
3725
  */
3628
3726
  async function checkK8sYamlFile(abs, root, fail, pass) {
3629
3727
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
@@ -3685,7 +3783,7 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
3685
3783
  * @param {string[]} yamlFiles абсолютні шляхи
3686
3784
  * @param {string} root корінь репозиторію
3687
3785
  * @param {(msg: string) => void} fail callback для реєстрації порушення
3688
- * @returns {void}
3786
+ * @returns {void} результат
3689
3787
  */
3690
3788
  function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
3691
3789
  for (const abs of yamlFiles) {
@@ -3701,7 +3799,7 @@ function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
3701
3799
  * @param {string} root корінь репозиторію
3702
3800
  * @param {string} abs абсолютний шлях до файлу
3703
3801
  * @param {(msg: string) => void} fail реєстрація порушення
3704
- * @returns {Promise<void>}
3802
+ * @returns {Promise<void>} результат
3705
3803
  */
3706
3804
  // Plan B: per-document `k8s/base/kustomization.yaml` має непорожнє поле `namespace:` —
3707
3805
  // у rego-пакеті `k8s.base_kustomization`, виклик через `runAllK8sRego`.
@@ -3816,6 +3914,23 @@ export const HPA_FILENAME = 'hpa.yaml'
3816
3914
  */
3817
3915
  export const PDB_FILENAME = 'pdb.yaml'
3818
3916
 
3917
+ /**
3918
+ * Ім'я файлу NetworkPolicy поруч із Deployment або в `components/` (див. k8s.mdc).
3919
+ */
3920
+ export const NETWORK_POLICY_FILENAME = 'networkpolicy.yaml'
3921
+
3922
+ /**
3923
+ * Workload-типи, для яких обов'язковий **NetworkPolicy** (див. k8s.mdc).
3924
+ * @type {readonly string[]}
3925
+ */
3926
+ export const WORKLOAD_KINDS_WITH_NETWORK_POLICY = Object.freeze([
3927
+ 'Deployment',
3928
+ 'StatefulSet',
3929
+ 'DaemonSet',
3930
+ 'Job',
3931
+ 'CronJob'
3932
+ ])
3933
+
3819
3934
  /**
3820
3935
  * Фіксована назва каталогу Kustomize Component, sibling до `base/`, де живуть HPA і PDB
3821
3936
  * (за каноном — `hpa.yaml` і `pdb.yaml` з `kind: Component` у `kustomization.yaml`). Інші назви
@@ -3885,6 +4000,40 @@ export function deploymentAppLabel(deployment) {
3885
4000
  return typeof app === 'string' && app.trim() !== '' ? app : null
3886
4001
  }
3887
4002
 
4003
+ /**
4004
+ * Витягує мітку `app` з `spec.selector.matchLabels.app` об'єкта з полем `spec.selector`.
4005
+ * @param {Record<string, unknown>} spec об'єкт `spec` workload
4006
+ * @returns {string | null} результат
4007
+ */
4008
+ function appLabelFromSpecSelector(spec) {
4009
+ const selector = getNestedObject(spec, 'selector')
4010
+ if (selector === null) return null
4011
+ const matchLabels = getNestedObject(selector, 'matchLabels')
4012
+ if (matchLabels === null) return null
4013
+ const app = matchLabels.app
4014
+ return typeof app === 'string' && app.trim() !== '' ? app : null
4015
+ }
4016
+
4017
+ /**
4018
+ * Витягує мітку `app` для workload, для якого потрібен NetworkPolicy.
4019
+ * Deployment / StatefulSet / DaemonSet / Job — `spec.selector.matchLabels.app`;
4020
+ * CronJob — `spec.jobTemplate.spec.selector.matchLabels.app`.
4021
+ * @param {Record<string, unknown>} manifest AST workload
4022
+ * @returns {string | null} непорожнє значення `app` або null
4023
+ */
4024
+ export function workloadAppLabel(manifest) {
4025
+ const kind = manifest.kind
4026
+ if (typeof kind !== 'string') return null
4027
+ if (kind === 'CronJob') {
4028
+ const jobTemplate = getNestedObject(getNestedObject(manifest, 'spec'), 'jobTemplate')
4029
+ const jobSpec = jobTemplate === null ? null : getNestedObject(jobTemplate, 'spec')
4030
+ return jobSpec === null ? null : appLabelFromSpecSelector(jobSpec)
4031
+ }
4032
+ const spec = getNestedObject(manifest, 'spec')
4033
+ if (spec === null) return null
4034
+ return appLabelFromSpecSelector(spec)
4035
+ }
4036
+
3888
4037
  /**
3889
4038
  * Перетворює значення на ціле число (приймає число або числовий рядок).
3890
4039
  * @param {unknown} v значення з YAML
@@ -3995,7 +4144,9 @@ function validateHpaBehavior(spec, errs) {
3995
4144
  * @returns {string[]} список порушень (порожній — ок)
3996
4145
  */
3997
4146
  export function hpaManifestViolations(manifest, expectedDeployName, isDevLike) {
3998
- /** @type {string[]} */
4147
+ /**
4148
+ @type {string[]}
4149
+ */
3999
4150
  const errs = []
4000
4151
  if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest)) {
4001
4152
  errs.push('HPA має бути обʼєктом YAML')
@@ -4073,7 +4224,9 @@ function validatePdbSelector(spec, expectedAppLabel, errs) {
4073
4224
  * @returns {string[]} список порушень (порожній — ок)
4074
4225
  */
4075
4226
  export function pdbManifestViolations(manifest, expectedAppLabel, isDevLike) {
4076
- /** @type {string[]} */
4227
+ /**
4228
+ @type {string[]}
4229
+ */
4077
4230
  const errs = []
4078
4231
  if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest)) {
4079
4232
  errs.push('PDB має бути обʼєктом YAML')
@@ -4095,6 +4248,143 @@ export function pdbManifestViolations(manifest, expectedAppLabel, isDevLike) {
4095
4248
  return errs
4096
4249
  }
4097
4250
 
4251
+ /**
4252
+ * Канонічний блок `spec.egress` NetworkPolicy (k8s.mdc): kube-dns; TCP 80/443 на 0.0.0.0/0;
4253
+ * інші порти — `namespaceSelector: {}` (in-cluster, зокрема `*.svc`).
4254
+ */
4255
+ const NETWORK_POLICY_EGRESS_YAML = ` egress:
4256
+ - to:
4257
+ - namespaceSelector:
4258
+ matchLabels:
4259
+ kubernetes.io/metadata.name: kube-system
4260
+ podSelector:
4261
+ matchLabels:
4262
+ k8s-app: kube-dns
4263
+ ports:
4264
+ - protocol: UDP
4265
+ port: 53
4266
+ - protocol: TCP
4267
+ port: 53
4268
+ - to:
4269
+ - ipBlock:
4270
+ cidr: 0.0.0.0/0
4271
+ ports:
4272
+ - protocol: TCP
4273
+ port: 80
4274
+ - protocol: TCP
4275
+ port: 443
4276
+ - to:
4277
+ - namespaceSelector: {}
4278
+ `
4279
+
4280
+ /**
4281
+ * Канонічний YAML **NetworkPolicy** для workload з іменем `workloadName` і міткою `app`.
4282
+ * @param {string} deployName `metadata.name` workload (Deployment, StatefulSet, …)
4283
+ * @param {string} appLabel `spec.selector.matchLabels.app` (або selector у `jobTemplate` для CronJob)
4284
+ * @returns {string} вміст `networkpolicy.yaml`
4285
+ */
4286
+ export function buildNetworkPolicyYaml(deployName, appLabel) {
4287
+ const schemaUrl = `${YANNH_BASE}networkpolicy-networking-k8s-io-v1.json`
4288
+ return `# yaml-language-server: $schema=${schemaUrl}
4289
+ apiVersion: networking.k8s.io/v1
4290
+ kind: NetworkPolicy
4291
+ metadata:
4292
+ name: ${deployName}
4293
+ spec:
4294
+ podSelector:
4295
+ matchLabels:
4296
+ app: ${appLabel}
4297
+ policyTypes:
4298
+ - Ingress
4299
+ - Egress
4300
+ ingress:
4301
+ - from:
4302
+ - podSelector: {}
4303
+ ${NETWORK_POLICY_EGRESS_YAML}`
4304
+ }
4305
+
4306
+ /**
4307
+ * Перевіряє **NetworkPolicy** (`networking.k8s.io/v1`): структура й прив'язка до workload.
4308
+ * @param {unknown} manifest корінь YAML-документа NetworkPolicy
4309
+ * @param {string} expectedDeployName очікуване `metadata.name` workload
4310
+ * @param {string} expectedAppLabel очікувана мітка `app` у `podSelector.matchLabels`
4311
+ * @returns {string[]} список порушень (порожній — ок)
4312
+ */
4313
+ export function networkPolicyManifestViolations(manifest, expectedDeployName, expectedAppLabel) {
4314
+ /**
4315
+ @type {string[]}
4316
+ */
4317
+ const errs = []
4318
+ if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest)) {
4319
+ errs.push('NetworkPolicy має бути обʼєктом YAML')
4320
+ return errs
4321
+ }
4322
+ const rec = /** @type {Record<string, unknown>} */ (manifest)
4323
+ if (rec.kind !== 'NetworkPolicy') errs.push(`kind має бути NetworkPolicy (зараз: ${JSON.stringify(rec.kind)})`)
4324
+ if (rec.apiVersion !== 'networking.k8s.io/v1')
4325
+ errs.push(`apiVersion має бути networking.k8s.io/v1 (зараз: ${JSON.stringify(rec.apiVersion)})`)
4326
+ const name = manifestMetadataName(rec)
4327
+ if (name !== expectedDeployName)
4328
+ errs.push(`metadata.name має бути '${expectedDeployName}' (зараз: ${JSON.stringify(name)})`)
4329
+ const spec = rec.spec
4330
+ if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) {
4331
+ errs.push('spec відсутній або некоректний')
4332
+ return errs
4333
+ }
4334
+ const s = /** @type {Record<string, unknown>} */ (spec)
4335
+ const podSelector = s.podSelector
4336
+ if (
4337
+ podSelector === null ||
4338
+ podSelector === undefined ||
4339
+ typeof podSelector !== 'object' ||
4340
+ Array.isArray(podSelector)
4341
+ ) {
4342
+ errs.push('spec.podSelector відсутній')
4343
+ return errs
4344
+ }
4345
+ const matchLabels = /** @type {Record<string, unknown>} */ (podSelector).matchLabels
4346
+ if (
4347
+ matchLabels === null ||
4348
+ matchLabels === undefined ||
4349
+ typeof matchLabels !== 'object' ||
4350
+ Array.isArray(matchLabels)
4351
+ ) {
4352
+ errs.push('spec.podSelector.matchLabels відсутній')
4353
+ return errs
4354
+ }
4355
+ const app = /** @type {Record<string, unknown>} */ (matchLabels).app
4356
+ if (app !== expectedAppLabel)
4357
+ errs.push(`spec.podSelector.matchLabels.app має бути '${expectedAppLabel}' (зараз: ${JSON.stringify(app)})`)
4358
+ return errs
4359
+ }
4360
+
4361
+ /**
4362
+ * Додає `resourceName` у `resources:` kustomization/Component YAML, якщо ще немає; сортує за алфавітом (en).
4363
+ * @param {string} raw вміст `kustomization.yaml`
4364
+ * @param {string} resourceName ім'я файлу ресурсу (наприклад `networkpolicy.yaml`)
4365
+ * @returns {{ changed: boolean, content: string }} результат
4366
+ */
4367
+ export function ensureResourceInKustomizationYaml(raw, resourceName) {
4368
+ const doc = parseDocument(raw)
4369
+ const resourcesNode = doc.get('resources')
4370
+ /**
4371
+ @type {string[]}
4372
+ */
4373
+ let items = []
4374
+ if (resourcesNode && isSeq(resourcesNode)) {
4375
+ items = resourcesNode.items
4376
+ .map(n => (n && typeof n === 'object' && 'value' in n ? String(n.value) : ''))
4377
+ .filter(s => s !== '')
4378
+ }
4379
+ if (items.includes(resourceName)) {
4380
+ return { changed: false, content: raw }
4381
+ }
4382
+ items.push(resourceName)
4383
+ items.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))
4384
+ doc.set('resources', doc.createNode(items))
4385
+ return { changed: true, content: String(doc) }
4386
+ }
4387
+
4098
4388
  /**
4099
4389
  * Чи елемент `topologySpreadConstraints` відповідає канону (maxSkew=1, topologyKey, whenUnsatisfiable, app label).
4100
4390
  * @param {unknown} item елемент масиву topologySpreadConstraints
@@ -4170,7 +4460,9 @@ function matchesYamlFilter(entry, filenameFilter) {
4170
4460
  * @returns {Promise<Record<string, unknown>[]>} список знайдених документів
4171
4461
  */
4172
4462
  async function readDocsByKindInDir(dirPath, kind, filenameFilter) {
4173
- /** @type {Record<string, unknown>[]} */
4463
+ /**
4464
+ @type {Record<string, unknown>[]}
4465
+ */
4174
4466
  const out = []
4175
4467
  const entries = await tryReaddir(dirPath)
4176
4468
  for (const entry of entries) {
@@ -4192,7 +4484,9 @@ async function readDocsByKindInDir(dirPath, kind, filenameFilter) {
4192
4484
  * @returns {Set<string>} шляхи JSON Pointer (наприклад `/spec/minReplicas`)
4193
4485
  */
4194
4486
  export function kustomizePatchModifiedPaths(patchText) {
4195
- /** @type {Set<string>} */
4487
+ /**
4488
+ @type {Set<string>}
4489
+ */
4196
4490
  const out = new Set()
4197
4491
  const t = typeof patchText === 'string' ? patchText.trim() : ''
4198
4492
  if (t === '') return out
@@ -4296,7 +4590,9 @@ function processSingleKustomizePatch(p, byKind) {
4296
4590
  * @returns {Map<string, Set<string>>} `kind` → шляхи JSON Pointer, які overrides змінюють
4297
4591
  */
4298
4592
  export function kustomizationPatchPathsByTargetKind(kust) {
4299
- /** @type {Map<string, Set<string>>} */
4593
+ /**
4594
+ @type {Map<string, Set<string>>}
4595
+ */
4300
4596
  const byKind = new Map()
4301
4597
  const patches = kust.patches
4302
4598
  if (!Array.isArray(patches)) return byKind
@@ -4401,7 +4697,9 @@ async function isK8sBaseDir(resolved, rootNorm) {
4401
4697
  * @returns {Promise<string[]>} абсолютні шляхи (без дедуплікації, якщо кілька однакових ref)
4402
4698
  */
4403
4699
  async function k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootNorm) {
4404
- /** @type {string[]} */
4700
+ /**
4701
+ @type {string[]}
4702
+ */
4405
4703
  const out = []
4406
4704
  for (const ref of pathRefs) {
4407
4705
  if (typeof ref === 'string' && !ref.includes('://') && ref.trim() !== '') {
@@ -4422,7 +4720,9 @@ async function k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootN
4422
4720
  * @returns {Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} прапорці
4423
4721
  */
4424
4722
  export async function kustomizeResourceTreeHpaPdbDeploymentFlags(kustAbs, rootNorm) {
4425
- /** @type {Set<string>} */
4723
+ /**
4724
+ @type {Set<string>}
4725
+ */
4426
4726
  const visitedKustomization = new Set()
4427
4727
  const desc = await collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visitedKustomization)
4428
4728
  const hasDeployment = await kustomizationTreeHasDeploymentUnderK8sBase(kustAbs, rootNorm)
@@ -4465,7 +4765,7 @@ async function yamlFileContainsHpaOrPdbDocument(fileAbs) {
4465
4765
  * @param {(msg: string) => void} fail callback при помилці
4466
4766
  * @param {(msg: string) => void} passFn callback при успіху
4467
4767
  * @param {(kust: string) => Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} getTreeFlags мемоізований аналіз дерева
4468
- * @returns {Promise<void>}
4768
+ * @returns {Promise<void>} результат
4469
4769
  */
4470
4770
  async function verifyK8sBaseKustomizeHasNoHpaPdb(kustAbs, rel, fail, passFn, getTreeFlags) {
4471
4771
  const { hasHpa, hasPdb } = await getTreeFlags(kustAbs)
@@ -4488,7 +4788,7 @@ async function verifyK8sBaseKustomizeHasNoHpaPdb(kustAbs, rel, fail, passFn, get
4488
4788
  * @param {(msg: string) => void} fail callback
4489
4789
  * @param {(msg: string) => void} passFn success
4490
4790
  * @param {(kust: string) => Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} getTreeFlags функція отримання прапорців дерева kustomize
4491
- * @returns {Promise<void>}
4791
+ * @returns {Promise<void>} результат
4492
4792
  */
4493
4793
  async function verifyOverlayHpaPdbFileRefsRespectBaseDeployment(
4494
4794
  root,
@@ -4523,9 +4823,9 @@ async function verifyOverlayHpaPdbFileRefsRespectBaseDeployment(
4523
4823
  * @param {string} ref посилання з pathRefs
4524
4824
  * @param {string[]} baseDirs масив base-каталогів
4525
4825
  * @param {boolean} anyBaseHasDep чи є Deployment у base
4526
- * @param {(msg: string) => void} fail callback
4527
- * @param {(msg: string) => void} passFn callback
4528
- * @returns {Promise<void>}
4826
+ * @param {(msg: string) => void} fail callback при помилці
4827
+ * @param {(msg: string) => void} passFn callback при успіху
4828
+ * @returns {Promise<void>} резолвиться по завершенню перевірки
4529
4829
  */
4530
4830
  async function checkOverlayRefHpaPdb(root, kustDir, rel, ref, baseDirs, anyBaseHasDep, fail, passFn) {
4531
4831
  const fAbs = resolve(kustDir, ref.trim())
@@ -4559,16 +4859,19 @@ async function checkOverlayRefHpaPdb(root, kustDir, rel, ref, baseDirs, anyBaseH
4559
4859
  * @param {string[]} yamlFilesAbs yaml у k8s
4560
4860
  * @param {(msg: string) => void} fail callback
4561
4861
  * @param {(msg: string) => void} passFn pass
4562
- * @returns {Promise<void>}
4862
+ * @returns {Promise<void>} результат
4563
4863
  */
4564
4864
  async function validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFilesAbs, fail, passFn) {
4565
4865
  const rootNorm = resolve(root)
4566
- /** @type {Map<string, Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>>} */
4567
- const treeFlagsMemo = new Map()
4568
4866
  /**
4569
- * @param {string} kustPath абсолютний шлях до kustomization.yaml
4570
- * @returns {Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} прапорці наявності ресурсів у дереві
4867
+ @type {Map<string, Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>>}
4571
4868
  */
4869
+ const treeFlagsMemo = new Map()
4870
+ /*
4871
+ * @param {string} kustPath абсолютний шлях до kustomization.yaml
4872
+
4873
+ * @returns {Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} прапорці наявності ресурсів у дереві
4874
+ */
4572
4875
  const getTreeFlags = kustPath => {
4573
4876
  const k = resolve(kustPath)
4574
4877
  let p = treeFlagsMemo.get(k)
@@ -4792,6 +5095,42 @@ function validatePdbForDeployment(pdbDocs, deployName, appLabel, isDevLike, pdbR
4792
5095
  }
4793
5096
  }
4794
5097
 
5098
+ /**
5099
+ * Шукає NetworkPolicy за `metadata.name`.
5100
+ * @param {Record<string, unknown>[]} npDocs документи NetworkPolicy
5101
+ * @param {string} deployName очікуване `metadata.name`
5102
+ * @returns {Record<string, unknown> | undefined} результат
5103
+ */
5104
+ function findNetworkPolicyByDeployName(npDocs, deployName) {
5105
+ return npDocs.find(doc => manifestMetadataName(doc) === deployName)
5106
+ }
5107
+
5108
+ /**
5109
+ * Перевіряє NetworkPolicy для одного workload: наявність і прив'язка за іменем / міткою `app`.
5110
+ * @param {Record<string, unknown>[]} npDocs масив NetworkPolicy-документів каталогу
5111
+ * @param {string} workloadName `metadata.name` workload
5112
+ * @param {string} appLabel мітка `app` у selector workload
5113
+ * @param {string} workloadKind `kind` workload (Deployment, StatefulSet, …)
5114
+ * @param {string} npRel відносний шлях до networkpolicy.yaml для повідомлень
5115
+ * @param {(msg: string) => void} fail callback при помилці
5116
+ * @param {(msg: string) => void} passFn callback при успіху
5117
+ */
5118
+ function validateNetworkPolicyForWorkload(npDocs, workloadName, appLabel, workloadKind, npRel, fail, passFn) {
5119
+ const matchedNp = findNetworkPolicyByDeployName(npDocs, workloadName)
5120
+ if (matchedNp === undefined) {
5121
+ fail(
5122
+ `${npRel}: відсутній або не знайдено NetworkPolicy з metadata.name='${workloadName}' для ${workloadKind} (k8s.mdc)`
5123
+ )
5124
+ return
5125
+ }
5126
+ const npErrs = networkPolicyManifestViolations(matchedNp, workloadName, appLabel)
5127
+ if (npErrs.length === 0) {
5128
+ passFn(`${npRel}: NetworkPolicy для ${workloadKind} '${workloadName}' валідний (k8s.mdc)`)
5129
+ } else {
5130
+ for (const e of npErrs) fail(`${npRel}: ${e} (k8s.mdc)`)
5131
+ }
5132
+ }
5133
+
4795
5134
  /**
4796
5135
  * Перевіряє sibling каталог `…/k8s/…/components/` для одного **Deployment** з шару `…/k8s/…/base/`.
4797
5136
  *
@@ -4809,14 +5148,14 @@ function validatePdbForDeployment(pdbDocs, deployName, appLabel, isDevLike, pdbR
4809
5148
  * @param {string} root корінь репозиторію
4810
5149
  * @param {(msg: string) => void} fail callback при помилці
4811
5150
  * @param {(msg: string) => void} passFn callback при успіху
4812
- * @returns {Promise<void>}
5151
+ * @returns {Promise<void>} результат
4813
5152
  */
4814
5153
  export async function validateComponentsForBaseDeployment(baseDir, deployName, appLabel, root, fail, passFn) {
4815
5154
  const componentsDir = resolve(baseDir, '..', COMPONENTS_DIR)
4816
5155
  const componentsRel = (relative(root, componentsDir) || componentsDir).replaceAll('\\', '/')
4817
5156
  if (!existsSync(componentsDir)) {
4818
5157
  fail(
4819
- `${componentsRel}: для Deployment '${deployName}' з sibling base/ обов'язковий каталог components/ з hpa.yaml і pdb.yaml (Kustomize Component) (k8s.mdc)`
5158
+ `${componentsRel}: для Deployment '${deployName}' з sibling base/ обов'язковий каталог components/ з hpa.yaml, networkpolicy.yaml і pdb.yaml (Kustomize Component) (k8s.mdc)`
4820
5159
  )
4821
5160
  return
4822
5161
  }
@@ -4832,17 +5171,26 @@ export async function validateComponentsForBaseDeployment(baseDir, deployName, a
4832
5171
  }
4833
5172
  await validateComponentsKustomizationManifest(componentsDir, componentsRel, fail, passFn)
4834
5173
  await validateComponentsHpaFile(componentsDir, componentsRel, deployName, fail, passFn)
5174
+ await validateComponentsNetworkPolicyFile(
5175
+ componentsDir,
5176
+ componentsRel,
5177
+ deployName,
5178
+ appLabel,
5179
+ 'Deployment',
5180
+ fail,
5181
+ passFn
5182
+ )
4835
5183
  await validateComponentsPdbFile(componentsDir, componentsRel, deployName, appLabel, fail, passFn)
4836
5184
  }
4837
5185
 
4838
5186
  /**
4839
5187
  * Перевіряє `components/kustomization.yaml`: `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`,
4840
- * `resources` містить `hpa.yaml` і `pdb.yaml` (як мінімум).
5188
+ * `resources` містить `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` (як мінімум).
4841
5189
  * @param {string} componentsDir абсолютний шлях до каталогу `components/`
4842
5190
  * @param {string} componentsRel відносний шлях для повідомлень
4843
5191
  * @param {(msg: string) => void} fail callback при помилці
4844
5192
  * @param {(msg: string) => void} passFn callback при успіху
4845
- * @returns {Promise<void>}
5193
+ * @returns {Promise<void>} результат
4846
5194
  */
4847
5195
  async function validateComponentsKustomizationManifest(componentsDir, componentsRel, fail, passFn) {
4848
5196
  const kustAbs = join(componentsDir, 'kustomization.yaml')
@@ -4867,15 +5215,21 @@ async function validateComponentsKustomizationManifest(componentsDir, components
4867
5215
  }
4868
5216
  const resources = Array.isArray(obj.resources) ? obj.resources.filter(x => typeof x === 'string') : []
4869
5217
  const hasHpa = resources.includes(HPA_FILENAME)
5218
+ const hasNp = resources.includes(NETWORK_POLICY_FILENAME)
4870
5219
  const hasPdb = resources.includes(PDB_FILENAME)
4871
5220
  if (!hasHpa) {
4872
5221
  fail(`${componentsRel}/kustomization.yaml: у resources має бути '${HPA_FILENAME}' (k8s.mdc)`)
4873
5222
  }
5223
+ if (!hasNp) {
5224
+ fail(`${componentsRel}/kustomization.yaml: у resources має бути '${NETWORK_POLICY_FILENAME}' (k8s.mdc)`)
5225
+ }
4874
5226
  if (!hasPdb) {
4875
5227
  fail(`${componentsRel}/kustomization.yaml: у resources має бути '${PDB_FILENAME}' (k8s.mdc)`)
4876
5228
  }
4877
- if (obj.apiVersion === KUSTOMIZE_COMPONENT_API_VERSION && obj.kind === 'Component' && hasHpa && hasPdb) {
4878
- passFn(`${componentsRel}/kustomization.yaml: канонічний Kustomize Component з hpa.yaml і pdb.yaml (k8s.mdc)`)
5229
+ if (obj.apiVersion === KUSTOMIZE_COMPONENT_API_VERSION && obj.kind === 'Component' && hasHpa && hasNp && hasPdb) {
5230
+ passFn(
5231
+ `${componentsRel}/kustomization.yaml: канонічний Kustomize Component з hpa.yaml, networkpolicy.yaml і pdb.yaml (k8s.mdc)`
5232
+ )
4879
5233
  }
4880
5234
  }
4881
5235
 
@@ -4886,7 +5240,7 @@ async function validateComponentsKustomizationManifest(componentsDir, components
4886
5240
  * @param {string} deployName ім'я Deployment з base
4887
5241
  * @param {(msg: string) => void} fail callback при помилці
4888
5242
  * @param {(msg: string) => void} passFn callback при успіху
4889
- * @returns {Promise<void>}
5243
+ * @returns {Promise<void>} результат
4890
5244
  */
4891
5245
  async function validateComponentsHpaFile(componentsDir, componentsRel, deployName, fail, passFn) {
4892
5246
  const hpaAbs = join(componentsDir, HPA_FILENAME)
@@ -4899,6 +5253,36 @@ async function validateComponentsHpaFile(componentsDir, componentsRel, deployNam
4899
5253
  validateHpaForDeployment(hpaDocs, deployName, true, hpaRel, fail, passFn)
4900
5254
  }
4901
5255
 
5256
+ /**
5257
+ * Перевіряє `components/networkpolicy.yaml`: NetworkPolicy для Deployment.
5258
+ * @param {string} componentsDir абсолютний шлях до каталогу `components/`
5259
+ * @param {string} componentsRel відносний шлях для повідомлень
5260
+ * @param {string} deployName ім'я Deployment з base
5261
+ * @param {string} appLabel мітка `app` Deployment
5262
+ * @param {string} workloadKind вид workload для повідомлень
5263
+ * @param {(msg: string) => void} fail callback при помилці
5264
+ * @param {(msg: string) => void} passFn callback при успіху
5265
+ * @returns {Promise<void>} результат
5266
+ */
5267
+ async function validateComponentsNetworkPolicyFile(
5268
+ componentsDir,
5269
+ componentsRel,
5270
+ deployName,
5271
+ appLabel,
5272
+ workloadKind,
5273
+ fail,
5274
+ passFn
5275
+ ) {
5276
+ const npAbs = join(componentsDir, NETWORK_POLICY_FILENAME)
5277
+ const npRel = `${componentsRel}/${NETWORK_POLICY_FILENAME}`
5278
+ if (!existsSync(npAbs)) {
5279
+ fail(`${npRel}: відсутній — додай NetworkPolicy для ${workloadKind} '${deployName}' (k8s.mdc)`)
5280
+ return
5281
+ }
5282
+ const npDocs = await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy')
5283
+ validateNetworkPolicyForWorkload(npDocs, deployName, appLabel, workloadKind, npRel, fail, passFn)
5284
+ }
5285
+
4902
5286
  /**
4903
5287
  * Перевіряє `components/pdb.yaml`: PDB для Deployment, dev-like `minAvailable=0`.
4904
5288
  * @param {string} componentsDir абсолютний шлях до каталогу `components/`
@@ -4907,7 +5291,7 @@ async function validateComponentsHpaFile(componentsDir, componentsRel, deployNam
4907
5291
  * @param {string} appLabel мітка `app` Deployment
4908
5292
  * @param {(msg: string) => void} fail callback при помилці
4909
5293
  * @param {(msg: string) => void} passFn callback при успіху
4910
- * @returns {Promise<void>}
5294
+ * @returns {Promise<void>} результат
4911
5295
  */
4912
5296
  async function validateComponentsPdbFile(componentsDir, componentsRel, deployName, appLabel, fail, passFn) {
4913
5297
  const pdbAbs = join(componentsDir, PDB_FILENAME)
@@ -4968,7 +5352,7 @@ function validateSingleDeploymentHpaPdbTopology(
4968
5352
  }
4969
5353
 
4970
5354
  /**
4971
- * Обробляє один каталог з Deployment: читає HPA/PDB і перевіряє кожен Deployment.
5355
+ * Обробляє один каталог з Deployment: читає HPA/PDB/NetworkPolicy і перевіряє кожен Deployment.
4972
5356
  * @param {Record<string, unknown>[]} deployments масив Deployment-документів
4973
5357
  * @param {string} dir абсолютний шлях до каталогу
4974
5358
  * @param {string} root корінь репозиторію
@@ -4983,6 +5367,7 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
4983
5367
  const deployRel = relDir === '' ? '.' : relDir
4984
5368
  if (isK8sBaseLayer && deployments.length > 0) {
4985
5369
  failIfBaseLayerHasLocalHpaOrPdb(dir, deployRel, fail)
5370
+ failIfBaseLayerHasLocalNetworkPolicy(dir, deployRel, fail)
4986
5371
  }
4987
5372
  const hpaDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'HorizontalPodAutoscaler', HPA_FILENAME)
4988
5373
  const pdbDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'PodDisruptionBudget', PDB_FILENAME)
@@ -5022,6 +5407,20 @@ function failIfBaseLayerHasLocalHpaOrPdb(dir, deployRel, fail) {
5022
5407
  }
5023
5408
  }
5024
5409
 
5410
+ /**
5411
+ * У шарі `…/k8s/…/base/` забороняє локальний `networkpolicy.yaml` (має жити у sibling `components/`).
5412
+ * @param {string} dir абсолютний каталог Deployment-маніфесту
5413
+ * @param {string} deployRel відносний шлях для повідомлень
5414
+ * @param {(msg: string) => void} fail callback при порушенні
5415
+ */
5416
+ function failIfBaseLayerHasLocalNetworkPolicy(dir, deployRel, fail) {
5417
+ if (existsSync(join(dir, NETWORK_POLICY_FILENAME))) {
5418
+ fail(
5419
+ `${deployRel}/${NETWORK_POLICY_FILENAME}: у шарі k8s/.../base не тримай локальний networkpolicy.yaml — NetworkPolicy живе у sibling components/ (k8s.mdc)`
5420
+ )
5421
+ }
5422
+ }
5423
+
5025
5424
  /**
5026
5425
  * Якщо у Deployment є `metadata.name` і `spec.selector.matchLabels.app` — викликає
5027
5426
  * `validateComponentsForBaseDeployment` для звірки sibling-`components/`. Без цих ключів
@@ -5052,6 +5451,50 @@ async function extractDeploymentsFromFile(filePath) {
5052
5451
  return collectDocsByKind(docs, 'Deployment')
5053
5452
  }
5054
5453
 
5454
+ /**
5455
+ * Витягує workload-документи, для яких потрібен NetworkPolicy (Deployment, StatefulSet, …).
5456
+ * @param {string} filePath абсолютний шлях до YAML-файлу
5457
+ * @returns {Promise<Record<string, unknown>[]>} результат
5458
+ */
5459
+ async function extractNetworkPolicyWorkloadsFromFile(filePath) {
5460
+ const raw = await tryReadFileUtf8(filePath)
5461
+ if (raw === undefined) return []
5462
+ const docs = tryParseAllYamlDocs(raw)
5463
+ if (docs === undefined) return []
5464
+ /**
5465
+ @type {Record<string, unknown>[]}
5466
+ */
5467
+ const out = []
5468
+ for (const kind of WORKLOAD_KINDS_WITH_NETWORK_POLICY) {
5469
+ out.push(...collectDocsByKind(docs, kind))
5470
+ }
5471
+ return out
5472
+ }
5473
+
5474
+ /**
5475
+ * Групує workload-и з NetworkPolicy за каталогом маніфесту.
5476
+ * @param {string[]} yamlFilesAbs абсолютні шляхи yaml під `k8s`
5477
+ * @returns {Promise<Map<string, Record<string, unknown>[]>>} результат
5478
+ */
5479
+ async function collectNetworkPolicyWorkloadsByDir(yamlFilesAbs) {
5480
+ /**
5481
+ @type {Map<string, Record<string, unknown>[]>}
5482
+ */
5483
+ const byDir = new Map()
5484
+ for (const abs of yamlFilesAbs) {
5485
+ const workloads = await extractNetworkPolicyWorkloadsFromFile(abs)
5486
+ if (workloads.length === 0) continue
5487
+ const dir = dirname(abs)
5488
+ const merged = byDir.get(dir)
5489
+ if (merged === undefined) {
5490
+ byDir.set(dir, [...workloads])
5491
+ } else {
5492
+ merged.push(...workloads)
5493
+ }
5494
+ }
5495
+ return byDir
5496
+ }
5497
+
5055
5498
  /**
5056
5499
  * Для кожного **Deployment** у шарі **`…/k8s/…/base/`** (будь-який YAML у відповідному каталозі) перевіряє:
5057
5500
  * заборона локальних **`hpa.yaml`** і **`pdb.yaml`** (file-existence); канонічні **topologySpreadConstraints**;
@@ -5065,7 +5508,9 @@ async function extractDeploymentsFromFile(filePath) {
5065
5508
  */
5066
5509
  async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, passFn) {
5067
5510
  const rootNorm = resolve(root)
5068
- /** @type {Map<string, Record<string, unknown>[]>} */
5511
+ /**
5512
+ @type {Map<string, Record<string, unknown>[]>}
5513
+ */
5069
5514
  const deploymentsByDir = new Map()
5070
5515
  for (const abs of yamlFilesAbs) {
5071
5516
  const rel = (relative(rootNorm, abs) || abs).replaceAll('\\', '/')
@@ -5085,6 +5530,47 @@ async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, pas
5085
5530
  }
5086
5531
  }
5087
5532
 
5533
+ /**
5534
+ * Перевіряє NetworkPolicy для **Deployment**, **StatefulSet**, **DaemonSet**, **Job**, **CronJob**
5535
+ * під `k8s` (base → `components/networkpolicy.yaml`, інші шари → `networkpolicy.yaml` поруч).
5536
+ * @param {string} root корінь репозиторію
5537
+ * @param {string[]} yamlFilesAbs yaml під k8s
5538
+ * @param {(msg: string) => void} fail callback при помилці
5539
+ * @param {(msg: string) => void} passFn callback при успіху
5540
+ * @returns {Promise<void>} результат
5541
+ */
5542
+ async function validateNetworkPoliciesForK8sWorkloads(root, yamlFilesAbs, fail, passFn) {
5543
+ const rootNorm = resolve(root)
5544
+ const workloadsByDir = await collectNetworkPolicyWorkloadsByDir(yamlFilesAbs)
5545
+ for (const [dir, workloads] of workloadsByDir) {
5546
+ const relDir = (relative(rootNorm, dir) || dir).replaceAll('\\', '/')
5547
+ const deployRel = relDir === '' ? '.' : relDir
5548
+ const isBase = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
5549
+ if (isBase && workloads.length > 0) {
5550
+ failIfBaseLayerHasLocalNetworkPolicy(dir, deployRel, fail)
5551
+ }
5552
+ const npAbs = isBase ? join(dir, '..', COMPONENTS_DIR, NETWORK_POLICY_FILENAME) : join(dir, NETWORK_POLICY_FILENAME)
5553
+ const npRel = (relative(rootNorm, npAbs) || npAbs).replaceAll('\\', '/')
5554
+ const npDocs = existsSync(npAbs) ? await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy') : []
5555
+ for (const workload of workloads) {
5556
+ const workloadName = manifestMetadataName(workload)
5557
+ const appLabel = workloadAppLabel(workload)
5558
+ const workloadKind = typeof workload.kind === 'string' ? workload.kind : 'workload'
5559
+ if (workloadName === null) {
5560
+ fail(`${deployRel}: ${workloadKind} без metadata.name — не можу перевірити NetworkPolicy (k8s.mdc)`)
5561
+ continue
5562
+ }
5563
+ if (appLabel === null) {
5564
+ fail(
5565
+ `${deployRel}: ${workloadKind} '${workloadName}' без мітки app у selector (spec.selector.matchLabels.app або jobTemplate для CronJob) (k8s.mdc)`
5566
+ )
5567
+ continue
5568
+ }
5569
+ validateNetworkPolicyForWorkload(npDocs, workloadName, appLabel, workloadKind, npRel, fail, passFn)
5570
+ }
5571
+ }
5572
+ }
5573
+
5088
5574
  /**
5089
5575
  * Розбирає рядок image на ім'я і тег, з виявленням digest.
5090
5576
  *
@@ -5207,9 +5693,13 @@ export function cleanupKustomizationImagesInYamlText(raw) {
5207
5693
 
5208
5694
  const entries = splitImagesBlockEntries(lines, imagesRange.start, imagesRange.end)
5209
5695
 
5210
- /** @type {Map<number, string>} */
5696
+ /**
5697
+ @type {Map<number, string>}
5698
+ */
5211
5699
  const replacements = new Map()
5212
- /** @type {Set<number>} */
5700
+ /**
5701
+ @type {Set<number>}
5702
+ */
5213
5703
  const removals = new Set()
5214
5704
  let changed = false
5215
5705
 
@@ -5219,7 +5709,9 @@ export function cleanupKustomizationImagesInYamlText(raw) {
5219
5709
 
5220
5710
  if (!changed) return { changed: false, content: raw }
5221
5711
 
5222
- /** @type {string[]} */
5712
+ /**
5713
+ @type {string[]}
5714
+ */
5223
5715
  const out = []
5224
5716
  for (const [i, line] of lines.entries()) {
5225
5717
  if (removals.has(i)) continue
@@ -5261,7 +5753,9 @@ function findImagesBlockRange(lines) {
5261
5753
  * @returns {Array<{ start: number, end: number }>} діапазони рядків кожного елемента
5262
5754
  */
5263
5755
  function splitImagesBlockEntries(lines, blockStart, blockEnd) {
5264
- /** @type {Array<{ start: number, end: number }>} */
5756
+ /**
5757
+ @type {Array<{ start: number, end: number }>}
5758
+ */
5265
5759
  const entries = []
5266
5760
  let curStart = -1
5267
5761
  for (let i = blockStart; i < blockEnd; i++) {
@@ -5283,10 +5777,14 @@ function splitImagesBlockEntries(lines, blockStart, blockEnd) {
5283
5777
  * @returns {boolean} true, якщо для цього елемента запланована хоча б одна зміна
5284
5778
  */
5285
5779
  function processImagesEntry(lines, entry, replacements, removals) {
5286
- /** @type {string | null} */
5780
+ /**
5781
+ @type {string | null}
5782
+ */
5287
5783
  let strippedTag = null
5288
5784
  let nameProcessed = false
5289
- /** @type {{ lineIdx: number, value: string } | null} */
5785
+ /**
5786
+ @type {{ lineIdx: number, value: string } | null}
5787
+ */
5290
5788
  let newTagInfo = null
5291
5789
  let newTagProcessed = false
5292
5790
  let changed = false
@@ -5368,7 +5866,9 @@ export function imageReplaceDeploymentPatchInfo(patchObj) {
5368
5866
  const parsedArr = tryParseJson6902Array(pr.patch)
5369
5867
  if (parsedArr === null) return null
5370
5868
 
5371
- /** @type {Array<{ containerIndex: number, newImage: string, opIndex: number }>} */
5869
+ /**
5870
+ @type {Array<{ containerIndex: number, newImage: string, opIndex: number }>}
5871
+ */
5372
5872
  const ops = []
5373
5873
  for (const [i, element] of parsedArr.entries()) {
5374
5874
  const op = asPlainObject(element)
@@ -5635,7 +6135,9 @@ function parseKustomizationWithPatches(raw) {
5635
6135
  if (typeof rec.apiVersion !== 'string' || !rec.apiVersion.startsWith(KUSTOMIZE_CONFIG_API_PREFIX)) return null
5636
6136
  if (!Array.isArray(rec.patches)) return null
5637
6137
 
5638
- /** @type {Array<{ index: number, totalOps: number, info: { deployName: string, containerIndex: number, newImage: string, opIndex: number } }>} */
6138
+ /**
6139
+ @type {Array<{ index: number, totalOps: number, info: { deployName: string, containerIndex: number, newImage: string, opIndex: number } }>}
6140
+ */
5639
6141
  const candidates = []
5640
6142
  for (const [i, p] of rec.patches.entries()) {
5641
6143
  const info = imageReplaceDeploymentPatchInfo(p)
@@ -5666,9 +6168,13 @@ function parseKustomizationWithPatches(raw) {
5666
6168
  * результати конвертації та зібрані нефатальні помилки
5667
6169
  */
5668
6170
  async function buildPatchToImageConversions(kustAbs, rootNorm, candidates) {
5669
- /** @type {Array<{ index: number, opIndex: number, totalOps: number, name: string, newName: string, newTag: string | null }>} */
6171
+ /**
6172
+ @type {Array<{ index: number, opIndex: number, totalOps: number, name: string, newName: string, newTag: string | null }>}
6173
+ */
5670
6174
  const conversions = []
5671
- /** @type {string[]} */
6175
+ /**
6176
+ @type {string[]}
6177
+ */
5672
6178
  const errors = []
5673
6179
 
5674
6180
  for (const { index, totalOps, info } of candidates) {
@@ -5746,7 +6252,9 @@ function applyConversionsToDoc(doc, conversions) {
5746
6252
  * @returns {Map<number, { totalOps: number, opIdx: number[] }>} згруповане
5747
6253
  */
5748
6254
  function groupConversionsByPatchIndex(conversions) {
5749
- /** @type {Map<number, { totalOps: number, opIdx: number[] }>} */
6255
+ /**
6256
+ @type {Map<number, { totalOps: number, opIdx: number[] }>}
6257
+ */
5750
6258
  const byPatch = new Map()
5751
6259
  for (const c of conversions) {
5752
6260
  const slot = byPatch.get(c.index) ?? { totalOps: c.totalOps, opIdx: [] }
@@ -5839,6 +6347,127 @@ function rewriteInlinePatchWithoutOps(patchText, opIndices) {
5839
6347
  return stripTrailingNewlines(inner.toString())
5840
6348
  }
5841
6349
 
6350
+ /**
6351
+ * Прибирає рядок modeline з блоку YAML (для multi-doc `networkpolicy.yaml`).
6352
+ * @param {string} yamlText фрагмент YAML
6353
+ * @returns {string} результат
6354
+ */
6355
+ function stripYamlLanguageServerModeline(yamlText) {
6356
+ return yamlText.replace(YAML_LS_MODELINE_RE, '')
6357
+ }
6358
+
6359
+ /**
6360
+ * Імена NetworkPolicy, уже присутні у файлі.
6361
+ * @param {string} npAbs абсолютний шлях до `networkpolicy.yaml`
6362
+ * @returns {Promise<Set<string>>} результат
6363
+ */
6364
+ async function existingNetworkPolicyNames(npAbs) {
6365
+ if (!existsSync(npAbs)) return new Set()
6366
+ const docs = await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy')
6367
+ /**
6368
+ @type {Set<string>}
6369
+ */
6370
+ const names = new Set()
6371
+ for (const doc of docs) {
6372
+ const n = manifestMetadataName(doc)
6373
+ if (n !== null) names.add(n)
6374
+ }
6375
+ return names
6376
+ }
6377
+
6378
+ /**
6379
+ * Дописує відсутні NetworkPolicy-документи у `networkpolicy.yaml` (multi-doc через `---`).
6380
+ * @param {string} npAbs абсолютний шлях до файлу
6381
+ * @param {Array<{ name: string, appLabel: string, kind: string }>} toAdd workload-и без NP
6382
+ * @param {string} npRel відносний шлях для повідомлень
6383
+ * @param {(msg: string) => void} passFn callback при успіху
6384
+ * @returns {Promise<void>} результат
6385
+ */
6386
+ async function appendNetworkPolicyDocuments(npAbs, toAdd, npRel, passFn) {
6387
+ if (toAdd.length === 0) return
6388
+ let content = ''
6389
+ if (existsSync(npAbs)) {
6390
+ const raw = await readFile(npAbs, 'utf8')
6391
+ content = raw.trimEnd()
6392
+ }
6393
+ const blocks = toAdd.map(({ name, appLabel }, i) => {
6394
+ const block = buildNetworkPolicyYaml(name, appLabel)
6395
+ return i === 0 && content === '' ? block.trimEnd() : stripYamlLanguageServerModeline(block).trimEnd()
6396
+ })
6397
+ const joined = blocks.join('\n---\n')
6398
+ content = content === '' ? `${joined}\n` : `${content}\n---\n${joined}\n`
6399
+ await writeFile(npAbs, content, 'utf8')
6400
+ for (const { name, kind } of toAdd) {
6401
+ passFn(`${npRel}: додано NetworkPolicy для ${kind} '${name}' (k8s.mdc)`)
6402
+ }
6403
+ }
6404
+
6405
+ /**
6406
+ * Створює відсутні NetworkPolicy для workload-ів у каталозі (base → `components/`, інакше — поруч).
6407
+ * @param {string} dir абсолютний каталог workload-маніфесту
6408
+ * @param {Record<string, unknown>[]} workloads workload-документи з цього каталогу
6409
+ * @param {string} rootNorm корінь репо
6410
+ * @param {(msg: string) => void} fail callback при помилці
6411
+ * @param {(msg: string) => void} passFn callback при успіху
6412
+ * @returns {Promise<void>} результат
6413
+ */
6414
+ async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm, fail, passFn) {
6415
+ const relDir = (relative(rootNorm, dir) || dir).replaceAll('\\', '/')
6416
+ const isBase = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
6417
+ const npAbs = isBase ? join(dir, '..', COMPONENTS_DIR, NETWORK_POLICY_FILENAME) : join(dir, NETWORK_POLICY_FILENAME)
6418
+ const npRel = (relative(rootNorm, npAbs) || npAbs).replaceAll('\\', '/')
6419
+ const existing = await existingNetworkPolicyNames(npAbs)
6420
+ /**
6421
+ @type {Array<{ name: string, appLabel: string, kind: string }>}
6422
+ */
6423
+ const toAdd = []
6424
+ for (const workload of workloads) {
6425
+ const name = manifestMetadataName(workload)
6426
+ const appLabel = workloadAppLabel(workload)
6427
+ const kind = typeof workload.kind === 'string' ? workload.kind : 'workload'
6428
+ if (name === null || appLabel === null) continue
6429
+ if (!existing.has(name)) toAdd.push({ name, appLabel, kind })
6430
+ }
6431
+ if (toAdd.length === 0) return
6432
+ try {
6433
+ if (isBase) await mkdir(dirname(npAbs), { recursive: true })
6434
+ await appendNetworkPolicyDocuments(npAbs, toAdd, npRel, passFn)
6435
+ if (isBase) {
6436
+ const componentsDir = dirname(npAbs)
6437
+ const componentsRel = (relative(rootNorm, componentsDir) || componentsDir).replaceAll('\\', '/')
6438
+ const kustAbs = join(componentsDir, 'kustomization.yaml')
6439
+ if (existsSync(kustAbs)) {
6440
+ const raw = await readFile(kustAbs, 'utf8')
6441
+ const { changed, content } = ensureResourceInKustomizationYaml(raw, NETWORK_POLICY_FILENAME)
6442
+ if (changed) {
6443
+ await writeFile(kustAbs, content, 'utf8')
6444
+ passFn(`${componentsRel}/kustomization.yaml: додано '${NETWORK_POLICY_FILENAME}' у resources (k8s.mdc)`)
6445
+ }
6446
+ }
6447
+ }
6448
+ } catch (error) {
6449
+ const msg = error instanceof Error ? error.message : String(error)
6450
+ fail(`${npRel}: не вдалося створити/оновити NetworkPolicy (${msg})`)
6451
+ }
6452
+ }
6453
+
6454
+ /**
6455
+ * Автоматично створює відсутні **NetworkPolicy** для Deployment, StatefulSet, DaemonSet, Job і CronJob
6456
+ * під `k8s` (base → `components/`, інші шари → поруч).
6457
+ * @param {string} root корінь репозиторію
6458
+ * @param {string[]} yamlFilesAbs абсолютні шляхи yaml під `k8s`
6459
+ * @param {(msg: string) => void} fail callback при помилці
6460
+ * @param {(msg: string) => void} passFn callback при успіху
6461
+ * @returns {Promise<void>} результат
6462
+ */
6463
+ async function ensureNetworkPoliciesForK8sWorkloads(root, yamlFilesAbs, fail, passFn) {
6464
+ const rootNorm = resolve(root)
6465
+ const workloadsByDir = await collectNetworkPolicyWorkloadsByDir(yamlFilesAbs)
6466
+ for (const [dir, workloads] of workloadsByDir) {
6467
+ await ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm, fail, passFn)
6468
+ }
6469
+ }
6470
+
5842
6471
  /**
5843
6472
  * Прохід для всіх `kustomization.yaml`: конвертує image-replace patches у `images:`,
5844
6473
  * потім чистить `images:` (зрізає теги в `name`, видаляє надлишкові `newTag`).
@@ -5846,7 +6475,7 @@ function rewriteInlinePatchWithoutOps(patchText, opIndices) {
5846
6475
  * @param {string[]} yamlFilesAbs всі yaml під k8s
5847
6476
  * @param {(msg: string) => void} fail колбек повідомлення про помилку
5848
6477
  * @param {(msg: string) => void} pass колбек успішного повідомлення
5849
- * @returns {Promise<void>}
6478
+ * @returns {Promise<void>} результат
5850
6479
  */
5851
6480
  async function autofixKustomizationImagesYaml(root, yamlFilesAbs, fail, pass) {
5852
6481
  const rootNorm = resolve(root)
@@ -5917,7 +6546,7 @@ async function runKustomizationImagesCleanup(kustAbs, rel, fail, pass) {
5917
6546
  * @param {string} root корінь репозиторію (cwd)
5918
6547
  * @param {string[]} yamlFiles абсолютні шляхи знайдених *.yaml під `…/k8s/`
5919
6548
  * @param {(msg: string) => void} fail callback при помилці
5920
- * @returns {void}
6549
+ * @returns {void} результат
5921
6550
  */
5922
6551
  function runAllK8sRego(root, yamlFiles, fail) {
5923
6552
  const relOf = abs => relative(root, abs).replaceAll('\\', '/') || abs
@@ -5933,11 +6562,14 @@ function runAllK8sRego(root, yamlFiles, fail) {
5933
6562
  return basename(p).toLowerCase() !== 'kustomization.yaml'
5934
6563
  })
5935
6564
 
5936
- /** @type {Array<{ ns: string, dir: string, files: string[] }>} */
6565
+ /**
6566
+ @type {Array<{ ns: string, dir: string, files: string[] }>}
6567
+ */
5937
6568
  const targets = [
5938
6569
  { ns: 'k8s.manifest', dir: 'k8s/manifest', files: allYaml },
5939
6570
  { ns: 'k8s.gateway', dir: 'k8s/gateway', files: allYaml },
5940
6571
  { ns: 'k8s.hpa_pdb', dir: 'k8s/hpa_pdb', files: allYaml },
6572
+ { ns: 'k8s.network_policy', dir: 'k8s/network_policy', files: allYaml },
5941
6573
  { ns: 'k8s.kustomization', dir: 'k8s/kustomization', files: kustYaml },
5942
6574
  { ns: 'k8s.svc_yaml', dir: 'k8s/svc_yaml', files: svcYaml },
5943
6575
  { ns: 'k8s.svc_hl_yaml', dir: 'k8s/svc_hl_yaml', files: svcHlYaml },
@@ -5980,6 +6612,8 @@ export async function check() {
5980
6612
 
5981
6613
  await autofixKustomizationImagesYaml(root, yamlFiles, fail, pass)
5982
6614
 
6615
+ await ensureNetworkPoliciesForK8sWorkloads(root, yamlFiles, fail, pass)
6616
+
5983
6617
  assertNoForbiddenK8sDevPaths(yamlFiles, root, fail)
5984
6618
 
5985
6619
  // Plan B: пер-документні структурні правила — у rego-полісі `npm/policy/k8s/*`,
@@ -6010,6 +6644,8 @@ export async function check() {
6010
6644
 
6011
6645
  await validateDeploymentHpaPdbAndTopology(root, yamlFiles, fail, pass)
6012
6646
 
6647
+ await validateNetworkPoliciesForK8sWorkloads(root, yamlFiles, fail, pass)
6648
+
6013
6649
  await validateProdKustomizationOverrides(root, yamlFiles, fail, pass)
6014
6650
 
6015
6651
  return reporter.getExitCode()