@nitra/cursor 1.13.34 → 1.13.40
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/CHANGELOG.md +41 -1
- package/bin/n-cursor.js +4 -0
- package/package.json +1 -1
- package/rules/changelog/fix/consistency/check.mjs +100 -85
- package/rules/ci4/ci4.mdc +7 -7
- package/rules/ga/lint/lint.mjs +23 -3
- package/rules/ga/policy/lint_ga/lint_ga.rego +6 -0
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +6 -0
- package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/js-run/fix/runtime/check.mjs +3 -0
- package/rules/js-run/js-run.mdc +16 -1
- package/rules/js-run/policy/package_json/package_json.rego +17 -0
- package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
- package/rules/k8s/fix/manifests/check.mjs +775 -139
- package/rules/k8s/k8s.mdc +52 -6
- package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
- package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
- package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
- package/rules/security/fix/trufflehog/check.mjs +3 -0
- package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
- package/rules/text/fix/formatting/check.mjs +1 -1
- package/rules/text/lint/lint.mjs +113 -5
- package/rules/text/policy/cspell/cspell.rego +1 -1
- package/rules/text/policy/lint_text/lint_text.rego +100 -0
- package/rules/text/policy/lint_text/target.json +4 -0
- package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +61 -0
- package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
- package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
- package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/text/text.mdc +3 -57
- package/rules/vue/vue.mdc +1 -0
- package/scripts/sync-claude-config.mjs +2 -2
- package/scripts/utils/check-mdc-template-refs.mjs +15 -5
- package/scripts/utils/inline-template-links.mjs +15 -8
- package/scripts/utils/package-manifest.mjs +24 -19
- package/scripts/utils/run-conftest-batch.mjs +22 -15
- 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
|
|
93
|
-
* каталозі **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу `components`). У `base/`
|
|
94
|
-
* заборонено тримати локальні `hpa.yaml` і `pdb.yaml` (file-existence error) і також у дереві
|
|
95
|
-
* не повинно бути HPA/PDB через `resources` / `components` / `bases`.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
1147
|
+
/**
|
|
1148
|
+
@type {unknown[]}
|
|
1149
|
+
*/
|
|
1122
1150
|
const roots = parseK8sYamlDocumentObjectRoots(body)
|
|
1123
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
1282
|
-
* @
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
2048
|
+
/**
|
|
2049
|
+
@type {string | undefined}
|
|
2050
|
+
*/
|
|
1989
2051
|
let apiVersion
|
|
1990
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
2899
|
+
/**
|
|
2900
|
+
@type {string[]}
|
|
2901
|
+
*/
|
|
2824
2902
|
const out = []
|
|
2825
2903
|
|
|
2904
|
+
/*
|
|
2905
|
+
* @param {unknown} node вузол для обходу
|
|
2906
|
+
|
|
2907
|
+
* @returns {void} результат
|
|
2908
|
+
*/
|
|
2826
2909
|
/**
|
|
2827
|
-
*
|
|
2828
|
-
* @
|
|
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
|
-
/**
|
|
2943
|
+
/**
|
|
2944
|
+
@type {string[]}
|
|
2945
|
+
*/
|
|
2862
2946
|
const out = []
|
|
2863
2947
|
|
|
2948
|
+
/*
|
|
2949
|
+
* @param {unknown} node вузол для обходу
|
|
2950
|
+
|
|
2951
|
+
* @returns {void} результат
|
|
2952
|
+
*/
|
|
2864
2953
|
/**
|
|
2865
|
-
*
|
|
2866
|
-
* @
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
3382
|
+
/**
|
|
3383
|
+
@type {Map<string, Set<string>>}
|
|
3384
|
+
*/
|
|
3291
3385
|
const hasuraByDir = new Map()
|
|
3292
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
5696
|
+
/**
|
|
5697
|
+
@type {Map<number, string>}
|
|
5698
|
+
*/
|
|
5211
5699
|
const replacements = new Map()
|
|
5212
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
5780
|
+
/**
|
|
5781
|
+
@type {string | null}
|
|
5782
|
+
*/
|
|
5287
5783
|
let strippedTag = null
|
|
5288
5784
|
let nameProcessed = false
|
|
5289
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
6171
|
+
/**
|
|
6172
|
+
@type {Array<{ index: number, opIndex: number, totalOps: number, name: string, newName: string, newTag: string | null }>}
|
|
6173
|
+
*/
|
|
5670
6174
|
const conversions = []
|
|
5671
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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()
|