@nitra/cursor 1.13.51 → 1.13.54

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 CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.54] - 2026-05-19
8
+
9
+ ### Changed
10
+
11
+ - `check k8s` / `lint-k8s`: правила під `npm/rules/k8s/` спрощено — за каноном `k8s.mdc` тримаємо лише `.yaml`, тож **rego-цілі** і **rego-вирази** очищено від `.yml`. Зачеплено: глоби `walkGlob` у `npm/rules/k8s/policy/{manifest,base_manifest,gateway,hpa_pdb}/target.json` — лише `**/*.yaml`; у [base_kustomization.rego](rules/k8s/policy/base_kustomization/base_kustomization.rego) `is_hpa_or_pdb_filename` більше не містить `hpa.yml` / `pdb.yml`; тест `test_deny_hpa_yml_in_subdir` → `test_deny_hpa_yaml_in_subdir`. **Safety-net** у [check.mjs](rules/k8s/fix/manifests/check.mjs) **збережено**: `findK8sYamlFiles` та `checkK8sYamlFile` все ще пропускають `.yml` далі, але одразу падають з повідомленням `розширення .yml — перейменуй на .yaml (див. k8s.mdc)` — щоб випадково створений `*.yml` під `k8s/` не залишився непоміченим (автоматичне перейменування — окрема ручна команда `npx @nitra/cursor rename-yaml-extensions`, яка з `check k8s` не викликається). Згадки про `.github/workflows/*.yml` у JSDoc лишилися (це чуже правило `ga.mdc`, де канон — `.yml`). Bump `k8s.mdc` `1.40` → `1.41`.
12
+
13
+ ## [1.13.53] - 2026-05-19
14
+
15
+ ### Changed
16
+
17
+ - `check k8s`: **NetworkPolicy переїхав з `components/` у `base/`**. Раніше канон вимагав `…/k8s/<pkg>/components/networkpolicy.yaml` (Kustomize Component, sibling до `base/`), а локальний `networkpolicy.yaml` у base був забороненим (file-existence error) — через що **dev-середовище** (рендер лише з base без overlay → без components) **не отримувало жодних мережевих обмежень** і pod'и були відкриті для будь-якого трафіку. Тепер NP лежить у `base/networkpolicy.yaml` поруч з workload-маніфестом і підключений через `base/kustomization.yaml` `resources:` — обмеження діють і на dev, і на всіх overlays через звичайний `resources: [- ../base]`. Канон `components/`: лише `hpa.yaml` + `pdb.yaml` (HPA/PDB лишаються env-залежними й підключаються тільки прод-overlays). У не-base overlays `networkpolicy.yaml` поруч з workload — опційний overlay-specific override. Зачеплено: [npm/rules/k8s/k8s.mdc](rules/k8s/k8s.mdc) (нова секція «NetworkPolicy у `base/`», оновлені приклади `components/kustomization.yaml` без NP і новий приклад `base/networkpolicy.yaml`), [npm/rules/k8s/fix/manifests/check.mjs](rules/k8s/fix/manifests/check.mjs) (видалено `failIfBaseLayerHasLocalNetworkPolicy` і `validateComponentsNetworkPolicyFile`; `validateNetworkPoliciesForK8sWorkloads` і `ensureNetworkPoliciesForWorkloadsInDir` тепер завжди шукають `networkpolicy.yaml` у `dir`, autofix додає його у `base/kustomization.yaml` `resources:`; `validateComponentsKustomizationManifest` більше не вимагає NP у resources), [npm/rules/k8s/policy/base_kustomization/base_kustomization.rego](rules/k8s/policy/base_kustomization/base_kustomization.rego) (deny прибирає `networkpolicy.yaml` зі списку заборонених у base resources — лишаються тільки HPA/PDB), [npm/rules/k8s/lint/lint.mjs](rules/k8s/lint/lint.mjs) (оновлено JSDoc про C-0260: NP тепер у base і kustomize-збірка нормалізує namespace природньо). Bump `k8s.mdc` `1.39` → `1.40`.
18
+
19
+ ## [1.13.52] - 2026-05-19
20
+
21
+ ### Added
22
+
23
+ - `check bun`: **зворотній інваріант** для `lint-<id>`-скриптів. Раніше `checkCursorRuleScripts` ([npm/rules/bun/fix/layout/check.mjs](rules/bun/fix/layout/check.mjs)) перевіряв лише пряму імплікацію — «правило в `.n-cursor.json:rules` → скрипт у `package.json`». Тепер також fail-имо, коли правило **відсутнє** в `rules` (або явно перенесене в **`disable-rules`**), але в кореневому `package.json` залишилися: (а) сам скрипт `lint-<id>`, або (б) виклик `bun run lint-<id>` у агрегованому `scripts.lint`. Причина: `n-cursor lint-<id>` запускається напряму й **ігнорує** `.n-cursor.json`, тож `bun run lint` падає на вимкненому правилі (як було з `disable-rules: ["k8s"]` у cursor-репо, де `lint-k8s` обходив template-сорці власного правила). Покриті скрипти і їхні правила-власники: `lint-docker` ← `docker`, `lint-k8s` ← `k8s`, `lint-image` ← `image-avif`/`image-compress` (multi-owner — скрипт лишається дозволеним, поки активний **хоч один** власник). Розпізнавання згадки `bun run lint-<id>` у chain'і — через токен-границі (regex `\\bbun run <script>\\b`), щоб не матчити префікси (`lint-k8s-foo` ≠ `lint-k8s`). Bump `bun.mdc` `1.8` → `1.9`.
24
+
7
25
  ## [1.13.51] - 2026-05-19
8
26
 
9
27
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.51",
3
+ "version": "1.13.54",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
package/rules/bun/bun.mdc CHANGED
@@ -2,7 +2,7 @@
2
2
  description: Bun як єдиний package manager у монорепо
3
3
  globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"
4
4
  alwaysApply: false
5
- version: '1.8'
5
+ version: '1.9'
6
6
  ---
7
7
 
8
8
  Проект використовує тільки Bun для керування залежностями та запуску скриптів.
@@ -70,4 +70,6 @@ FROM oven/bun:alpine AS build-env
70
70
 
71
71
  У кінці скрипта `lint` додай `&& oxfmt .`.
72
72
 
73
- Якщо в **`.n-cursor.json`** у масиві **`rules`** є **`docker`**, у кореневому `package.json` **обов'язково** скрипт **`lint-docker`** (див. **`docker.mdc`**) і рядок **`bun run lint-docker`** у **`lint`**. Якщо є **`k8s`** — **обов'язково** **`lint-k8s`** і **`bun run lint-k8s`** у **`lint`** (див. **`k8s.mdc`**). Перевірка — **`npx @nitra/cursor check bun`**.
73
+ Якщо в **`.n-cursor.json`** у масиві **`rules`** є **`docker`**, у кореневому `package.json` **обов'язково** скрипт **`lint-docker`** (див. **`docker.mdc`**) і рядок **`bun run lint-docker`** у **`lint`**. Якщо є **`k8s`** — **обов'язково** **`lint-k8s`** і **`bun run lint-k8s`** у **`lint`** (див. **`k8s.mdc`**).
74
+
75
+ **Зворотній інваріант:** якщо правила **немає** в `rules` (або воно явно перенесене в **`disable-rules`**), скрипту **`lint-<id>`** у кореневому `package.json` бути **не може**, і ланцюжок агрегованого **`scripts.lint`** не має містити **`bun run lint-<id>`**. Інакше `bun run lint` падатиме на вимкненому правилі — `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево незалежно від `rules`/`disable-rules`. Для скриптів із кількома власниками (як **`lint-image`** — обслуговує і **`image-avif`**, і **`image-compress`**) скрипт лишається дозволеним, поки активний **хоч один** власник; зворотній інваріант тригериться лише коли в `rules` немає **жодного** з них. Перевірка — **`npx @nitra/cursor check bun`**.
@@ -5,9 +5,11 @@
5
5
  * - наявність `bun.lock`, `bunfig.toml`, `package.json` у корені (FS-existence);
6
6
  * - заборонені lockfile та артефакти yarn/pnpm (`package-lock.json`, `yarn.lock`,
7
7
  * `pnpm-lock.yaml`, `.yarnrc.yml`, директорія `.yarn/`);
8
- * - якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, у кореневому
9
- * `package.json` має бути відповідний скрипт `lint-docker` / `lint-k8s`
10
- * (cross-file: два JSON-файли).
8
+ * - двосторонній зв'язок `.n-cursor.json:rules` `package.json:scripts` для правил із
9
+ * `lint-<id>` (`docker`, `k8s`): rule увімкнено скрипт мусить існувати; rule
10
+ * відсутнє (або в `disable-rules`) скрипту та згадки `bun run lint-<id>` у
11
+ * агрегованому `scripts.lint` бути **не може** (інакше `bun run lint` падатиме
12
+ * на правилі, яке у конфізі вимкнено).
11
13
  *
12
14
  * **Що покрила Rego** (`npx \@nitra/cursor check`):
13
15
  * - `npm/policy/bun/bunfig/` — `[install].linker == "hoisted"` у `bunfig.toml`;
@@ -25,46 +27,116 @@ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mj
25
27
  // видалено, щоб не було двох джерел істини.
26
28
 
27
29
  /**
28
- * Зчитує ідентифікатори правил з `.n-cursor.json` (поле `rules`).
29
- * @returns {Promise<Set<string>>} множина рядків id правил або порожня, якщо файлу/поля немає
30
+ * Зчитує `rules` та `disable-rules` з `.n-cursor.json`.
31
+ * @returns {Promise<{ rules: Set<string>, disabled: Set<string> }>} активні правила і явно вимкнені
30
32
  */
31
33
  async function loadNCursorRules() {
32
- if (!existsSync('.n-cursor.json')) {
33
- return new Set()
34
- }
34
+ const empty = { rules: new Set(), disabled: new Set() }
35
+ if (!existsSync('.n-cursor.json')) return empty
35
36
  try {
36
37
  const raw = JSON.parse(await readFile('.n-cursor.json', 'utf8'))
37
- const list = raw?.rules
38
- if (!Array.isArray(list)) {
39
- return new Set()
40
- }
41
- return new Set(list.map(String))
38
+ const list = Array.isArray(raw?.rules) ? raw.rules.map(String) : []
39
+ const disabled = Array.isArray(raw?.['disable-rules']) ? raw['disable-rules'].map(String) : []
40
+ return { rules: new Set(list), disabled: new Set(disabled) }
42
41
  } catch {
43
- return new Set()
42
+ return empty
44
43
  }
45
44
  }
46
45
 
47
46
  /**
48
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
47
+ * Чи містить `scripts.lint` виклик `bun run <script>` у chain'і. Шукаємо саме `bun run <script>`
48
+ * як окремий токен (між пробілами/`&&`), щоб уникнути false-positive на префіксах
49
+ * (`bun run lint-k8s-foo` не матчиться як `bun run lint-k8s`).
50
+ * @param {string} lintScript значення `scripts.lint` (порожній рядок — якщо нема)
51
+ * @param {string} target ім'я скрипта (без префіксів)
52
+ * @returns {boolean} true, якщо chain згадує `bun run <target>`
53
+ */
54
+ function lintChainHasScript(lintScript, target) {
55
+ if (!lintScript) return false
56
+ const escaped = target.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')
57
+ return new RegExp(`(?:^|\\s)bun\\s+run\\s+${escaped}(?:$|\\s)`, 'u').test(lintScript)
58
+ }
59
+
60
+ /**
61
+ * Описує `lint-<id>`-обгортку та правила, що нею володіють. Один скрипт може мати кілька
62
+ * власників (`lint-image` — обслуговує і `image-avif`, і `image-compress`); скрипт вважається
63
+ * «потрібним», якщо **хоч одне** з власних правил активне у `.n-cursor.json:rules`.
64
+ * @typedef {object} RuleScript
65
+ * @property {string[]} rules id правил-власників (>=1); скрипт зобов'язаний існувати, поки активне хоч одне з них
66
+ * @property {string} script ім'я скрипта в `package.json:scripts`
67
+ * @property {string} doc `.mdc`-файл (або кома-список), на який посилається повідомлення check-у
68
+ */
69
+
70
+ /** @type {RuleScript[]} */
71
+ const RULE_SCRIPTS = [
72
+ { rules: ['docker'], script: 'lint-docker', doc: 'docker.mdc' },
73
+ { rules: ['k8s'], script: 'lint-k8s', doc: 'k8s.mdc' },
74
+ { rules: ['image-avif', 'image-compress'], script: 'lint-image', doc: 'image-avif.mdc / image-compress.mdc' }
75
+ ]
76
+
77
+ /**
78
+ * Описує стан правил-власників скрипта для повідомлень про reason. Повертає або список увімкнених
79
+ * правил (для passing-кейсу «правило є»), або компактний опис, чому всі вимкнені (для inverse-fail).
80
+ * @param {string[]} owners id правил-власників (>=1)
81
+ * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
82
+ * @returns {{ enabled: string[], reason: string }} `enabled` — список з `cursorRules.rules`; `reason` — текст для лога
83
+ */
84
+ function ownerStatus(owners, cursorRules) {
85
+ const enabled = owners.filter(r => cursorRules.rules.has(r))
86
+ if (enabled.length > 0) {
87
+ return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${enabled.map(r => `\`${r}\``).join(', ')}` }
88
+ }
89
+ if (owners.length === 1) {
90
+ const [only] = owners
91
+ const where = cursorRules.disabled.has(only) ? 'в disable-rules' : 'відсутнє в rules'
92
+ return { enabled, reason: `правило \`${only}\` ${where}` }
93
+ }
94
+ const disabledCount = owners.filter(r => cursorRules.disabled.has(r)).length
95
+ const note = disabledCount === owners.length ? 'усі власники в disable-rules' : 'жоден власник не активний у rules'
96
+ return { enabled, reason: `${owners.map(r => `\`${r}\``).join('/')} — ${note}` }
97
+ }
98
+
99
+ /**
100
+ * Перевіряє двосторонній зв'язок `rules` ↔ `scripts.lint-<id>` для правил із `lint-<id>`-обгорткою
101
+ * (див. `RULE_SCRIPTS`). Якщо активне хоч одне правило-власник — скрипт мусить існувати; якщо
102
+ * жодне з власників не активне (відсутнє у `rules` або є в `disable-rules`), скрипту і згадки
103
+ * `bun run <script>` у `scripts.lint` бути **не може**. Інакше `bun run lint` падатиме на
104
+ * вимкненому правилі: `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево
105
+ * незалежно від конфігу (як було в cursor-репо: `disable-rules: ["k8s"]` + залишений `lint-k8s`
106
+ * ламав chain на template-сорцях власного правила).
107
+ * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter
49
108
  * @param {Record<string, string>} scripts scripts з package.json
50
- * @param {Set<string>} cursorRules активні правила з .n-cursor.json
109
+ * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
51
110
  */
52
111
  function checkCursorRuleScripts(reporter, scripts, cursorRules) {
53
112
  const { pass, fail } = reporter
54
- /** @type {Array<{rule: string, script: string, doc: string}>} */
55
- const ruleScripts = [
56
- { rule: 'docker', script: 'lint-docker', doc: 'docker.mdc' },
57
- { rule: 'k8s', script: 'lint-k8s', doc: 'k8s.mdc' }
58
- ]
59
- for (const { rule, script, doc } of ruleScripts) {
60
- if (cursorRules.has(rule)) {
61
- if (scripts[script]) {
62
- pass(`package.json: є \`${script}\` (правило ${rule} у .n-cursor.json)`)
113
+ const lintScript = typeof scripts.lint === 'string' ? scripts.lint : ''
114
+ for (const { rules: owners, script, doc } of RULE_SCRIPTS) {
115
+ const status = ownerStatus(owners, cursorRules)
116
+ const present = Boolean(scripts[script])
117
+ const inChain = lintChainHasScript(lintScript, script)
118
+ if (status.enabled.length > 0) {
119
+ if (present) {
120
+ pass(`package.json: є \`${script}\` (${status.reason} у .n-cursor.json)`)
63
121
  } else {
64
122
  fail(
65
- `У .n-cursor.json є правило \`${rule}\` — додай скрипт \`${script}\` у кореневий package.json (див. ${doc})`
123
+ `У .n-cursor.json увімкнено ${status.reason} — додай скрипт \`${script}\` у кореневий package.json (див. ${doc})`
66
124
  )
67
125
  }
126
+ continue
127
+ }
128
+ if (present) {
129
+ fail(
130
+ `У .n-cursor.json немає активних власників ${owners.map(r => `\`${r}\``).join('/')} — прибери скрипт \`${script}\` з кореневого package.json (див. ${doc})`
131
+ )
132
+ }
133
+ if (inChain) {
134
+ fail(
135
+ `У \`scripts.lint\` є \`bun run ${script}\`, але серед \`${owners.join('/')}\` жоден не активний у .n-cursor.json — прибери з ланцюжка lint (див. ${doc})`
136
+ )
137
+ }
138
+ if (!present && !inChain) {
139
+ pass(`package.json: \`${script}\` відсутній (${status.reason})`)
68
140
  }
69
141
  }
70
142
  }
@@ -413,9 +413,7 @@ export async function check() {
413
413
  const ignorePaths = await loadCursorIgnorePaths(process.cwd())
414
414
 
415
415
  if (!(await hasAnyVueRasterReference(ignorePaths))) {
416
- pass(
417
- 'image-avif: у .vue/.html немає raster-посилань для переписування — AVIF-генерація і cleanup пропущені'
418
- )
416
+ pass('image-avif: у .vue/.html немає raster-посилань для переписування — AVIF-генерація і cleanup пропущені')
419
417
  return reporter.getExitCode()
420
418
  }
421
419
 
@@ -91,14 +91,16 @@
91
91
  * **HPA / PDB / topologySpreadConstraints:** для кожного **`Deployment`** у шарі **`…/k8s/…/base/`**
92
92
  * (будь-який `.yaml` у цьому каталозі) обов'язкові канонічні **topologySpreadConstraints**, а HPA і PDB
93
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`.
94
+ * заборонено тримати локальні `hpa.yaml` і `pdb.yaml` (file-existence error) і також у дереві
95
+ * base-kustomize не повинно бути HPA/PDB через `resources` / `components` / `bases`.
96
96
  * **NetworkPolicy:** для кожного **`Deployment`**, **`StatefulSet`**, **`DaemonSet`**, **`Job`**, **`CronJob`** під `k8s`
97
- * обов'язковий канонічний NetworkPolicy (base `components/networkpolicy.yaml`, інші шари `networkpolicy.yaml` поруч).
97
+ * обов'язковий канонічний NetworkPolicy у `networkpolicy.yaml` поруч з workload-маніфестом у base
98
+ * (`base/networkpolicy.yaml`, підключений через `base/kustomization.yaml` `resources:` — обмеження діють і на dev)
99
+ * і у не-base overlay (опційно — overlay-specific override).
98
100
  * Egress: kube-dns; **TCP 80/443** на `0.0.0.0/0`; інші порти — `namespaceSelector: {}` (in-cluster / `*.svc`). Заборонено `egress: [{}]`.
99
101
  * Відсутні документи **`check k8s`** створює автоматично (multi-doc у одному файлі, якщо workload-ів кілька).
100
102
  * Структура `components/`: `kustomization.yaml` з `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`,
101
- * `resources` що містять `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml`, `hpa.yaml` (валідний `autoscaling/v2`
103
+ * `resources` що містять `hpa.yaml` і `pdb.yaml`, `hpa.yaml` (валідний `autoscaling/v2`
102
104
  * HorizontalPodAutoscaler з `scaleTargetRef.name` = ім'я Deployment, dev-like `min=max=1`), `pdb.yaml` (валідний
103
105
  * `policy/v1` PodDisruptionBudget з `selector.matchLabels.app` = мітка `app` Deployment, dev-like `minAvailable=0`).
104
106
  * Overlays (`ua/`, прод-overlays) підключають `components: [- ../components]` і додають JSON6902-патчі для
@@ -133,7 +135,7 @@
133
135
  * поки в наслідуваному `base` у дереві не з'явиться такий Deployment (k8s.mdc).
134
136
  */
135
137
  import { existsSync } from 'node:fs'
136
- import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
138
+ import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
137
139
  import { basename, dirname, join, relative, resolve } from 'node:path'
138
140
 
139
141
  import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
@@ -1776,7 +1778,7 @@ export function baseKustomizationNamespaceViolation(obj) {
1776
1778
  }
1777
1779
 
1778
1780
  /**
1779
- * Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
1781
+ * Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — fail з порадою перейменувати на `.yaml`; k8s.mdc).
1780
1782
  * @param {string} root корінь репозиторію (cwd)
1781
1783
  * @param {string[]} [ignorePaths] шляхи каталогів, повністю виключених з обходу
1782
1784
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
@@ -5155,6 +5157,8 @@ function validateNetworkPolicyForWorkload(npDocs, workloadName, appLabel, worklo
5155
5157
  * що дорівнює `metadata.name` цього Deployment, з dev-like значеннями `min=max=1`.
5156
5158
  * - `components/pdb.yaml` — валідний `policy/v1` `PodDisruptionBudget` зі `selector.matchLabels.app`,
5157
5159
  * що дорівнює мітці `app` Deployment, з dev-like `minAvailable=0`.
5160
+ * - **NetworkPolicy** в components не живе — він підключений з `base/networkpolicy.yaml` через
5161
+ * `base/kustomization.yaml` `resources:` (див. `validateNetworkPoliciesForK8sWorkloads`).
5158
5162
  * @param {string} baseDir абсолютний шлях до `…/k8s/…/base/`
5159
5163
  * @param {string} deployName ім'я Deployment з base
5160
5164
  * @param {string} appLabel мітка `app` з `spec.selector.matchLabels.app`
@@ -5168,7 +5172,7 @@ export async function validateComponentsForBaseDeployment(baseDir, deployName, a
5168
5172
  const componentsRel = (relative(root, componentsDir) || componentsDir).replaceAll('\\', '/')
5169
5173
  if (!existsSync(componentsDir)) {
5170
5174
  fail(
5171
- `${componentsRel}: для Deployment '${deployName}' з sibling base/ обов'язковий каталог components/ з hpa.yaml, networkpolicy.yaml і pdb.yaml (Kustomize Component) (k8s.mdc)`
5175
+ `${componentsRel}: для Deployment '${deployName}' з sibling base/ обов'язковий каталог components/ з hpa.yaml і pdb.yaml (Kustomize Component) (k8s.mdc)`
5172
5176
  )
5173
5177
  return
5174
5178
  }
@@ -5184,21 +5188,13 @@ export async function validateComponentsForBaseDeployment(baseDir, deployName, a
5184
5188
  }
5185
5189
  await validateComponentsKustomizationManifest(componentsDir, componentsRel, fail, passFn)
5186
5190
  await validateComponentsHpaFile(componentsDir, componentsRel, deployName, fail, passFn)
5187
- await validateComponentsNetworkPolicyFile(
5188
- componentsDir,
5189
- componentsRel,
5190
- deployName,
5191
- appLabel,
5192
- 'Deployment',
5193
- fail,
5194
- passFn
5195
- )
5196
5191
  await validateComponentsPdbFile(componentsDir, componentsRel, deployName, appLabel, fail, passFn)
5197
5192
  }
5198
5193
 
5199
5194
  /**
5200
5195
  * Перевіряє `components/kustomization.yaml`: `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`,
5201
- * `resources` містить `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` (як мінімум).
5196
+ * `resources` містить `hpa.yaml` і `pdb.yaml` (як мінімум). NetworkPolicy у components вже не живе —
5197
+ * він підключений з `base/networkpolicy.yaml`.
5202
5198
  * @param {string} componentsDir абсолютний шлях до каталогу `components/`
5203
5199
  * @param {string} componentsRel відносний шлях для повідомлень
5204
5200
  * @param {(msg: string) => void} fail callback при помилці
@@ -5228,21 +5224,15 @@ async function validateComponentsKustomizationManifest(componentsDir, components
5228
5224
  }
5229
5225
  const resources = Array.isArray(obj.resources) ? obj.resources.filter(x => typeof x === 'string') : []
5230
5226
  const hasHpa = resources.includes(HPA_FILENAME)
5231
- const hasNp = resources.includes(NETWORK_POLICY_FILENAME)
5232
5227
  const hasPdb = resources.includes(PDB_FILENAME)
5233
5228
  if (!hasHpa) {
5234
5229
  fail(`${componentsRel}/kustomization.yaml: у resources має бути '${HPA_FILENAME}' (k8s.mdc)`)
5235
5230
  }
5236
- if (!hasNp) {
5237
- fail(`${componentsRel}/kustomization.yaml: у resources має бути '${NETWORK_POLICY_FILENAME}' (k8s.mdc)`)
5238
- }
5239
5231
  if (!hasPdb) {
5240
5232
  fail(`${componentsRel}/kustomization.yaml: у resources має бути '${PDB_FILENAME}' (k8s.mdc)`)
5241
5233
  }
5242
- if (obj.apiVersion === KUSTOMIZE_COMPONENT_API_VERSION && obj.kind === 'Component' && hasHpa && hasNp && hasPdb) {
5243
- passFn(
5244
- `${componentsRel}/kustomization.yaml: канонічний Kustomize Component з hpa.yaml, networkpolicy.yaml і pdb.yaml (k8s.mdc)`
5245
- )
5234
+ if (obj.apiVersion === KUSTOMIZE_COMPONENT_API_VERSION && obj.kind === 'Component' && hasHpa && hasPdb) {
5235
+ passFn(`${componentsRel}/kustomization.yaml: канонічний Kustomize Component з hpa.yaml і pdb.yaml (k8s.mdc)`)
5246
5236
  }
5247
5237
  }
5248
5238
 
@@ -5266,36 +5256,6 @@ async function validateComponentsHpaFile(componentsDir, componentsRel, deployNam
5266
5256
  validateHpaForDeployment(hpaDocs, deployName, true, hpaRel, fail, passFn)
5267
5257
  }
5268
5258
 
5269
- /**
5270
- * Перевіряє `components/networkpolicy.yaml`: NetworkPolicy для Deployment.
5271
- * @param {string} componentsDir абсолютний шлях до каталогу `components/`
5272
- * @param {string} componentsRel відносний шлях для повідомлень
5273
- * @param {string} deployName ім'я Deployment з base
5274
- * @param {string} appLabel мітка `app` Deployment
5275
- * @param {string} workloadKind вид workload для повідомлень
5276
- * @param {(msg: string) => void} fail callback при помилці
5277
- * @param {(msg: string) => void} passFn callback при успіху
5278
- * @returns {Promise<void>} результат
5279
- */
5280
- async function validateComponentsNetworkPolicyFile(
5281
- componentsDir,
5282
- componentsRel,
5283
- deployName,
5284
- appLabel,
5285
- workloadKind,
5286
- fail,
5287
- passFn
5288
- ) {
5289
- const npAbs = join(componentsDir, NETWORK_POLICY_FILENAME)
5290
- const npRel = `${componentsRel}/${NETWORK_POLICY_FILENAME}`
5291
- if (!existsSync(npAbs)) {
5292
- fail(`${npRel}: відсутній — додай NetworkPolicy для ${workloadKind} '${deployName}' (k8s.mdc)`)
5293
- return
5294
- }
5295
- const npDocs = await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy')
5296
- validateNetworkPolicyForWorkload(npDocs, deployName, appLabel, workloadKind, npRel, fail, passFn)
5297
- }
5298
-
5299
5259
  /**
5300
5260
  * Перевіряє `components/pdb.yaml`: PDB для Deployment, dev-like `minAvailable=0`.
5301
5261
  * @param {string} componentsDir абсолютний шлях до каталогу `components/`
@@ -5380,7 +5340,6 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
5380
5340
  const deployRel = relDir === '' ? '.' : relDir
5381
5341
  if (isK8sBaseLayer && deployments.length > 0) {
5382
5342
  failIfBaseLayerHasLocalHpaOrPdb(dir, deployRel, fail)
5383
- failIfBaseLayerHasLocalNetworkPolicy(dir, deployRel, fail)
5384
5343
  }
5385
5344
  const hpaDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'HorizontalPodAutoscaler', HPA_FILENAME)
5386
5345
  const pdbDocs = isK8sBaseLayer ? [] : await readDocsByKindInDir(dir, 'PodDisruptionBudget', PDB_FILENAME)
@@ -5420,20 +5379,6 @@ function failIfBaseLayerHasLocalHpaOrPdb(dir, deployRel, fail) {
5420
5379
  }
5421
5380
  }
5422
5381
 
5423
- /**
5424
- * У шарі `…/k8s/…/base/` забороняє локальний `networkpolicy.yaml` (має жити у sibling `components/`).
5425
- * @param {string} dir абсолютний каталог Deployment-маніфесту
5426
- * @param {string} deployRel відносний шлях для повідомлень
5427
- * @param {(msg: string) => void} fail callback при порушенні
5428
- */
5429
- function failIfBaseLayerHasLocalNetworkPolicy(dir, deployRel, fail) {
5430
- if (existsSync(join(dir, NETWORK_POLICY_FILENAME))) {
5431
- fail(
5432
- `${deployRel}/${NETWORK_POLICY_FILENAME}: у шарі k8s/.../base не тримай локальний networkpolicy.yaml — NetworkPolicy живе у sibling components/ (k8s.mdc)`
5433
- )
5434
- }
5435
- }
5436
-
5437
5382
  /**
5438
5383
  * Якщо у Deployment є `metadata.name` і `spec.selector.matchLabels.app` — викликає
5439
5384
  * `validateComponentsForBaseDeployment` для звірки sibling-`components/`. Без цих ключів
@@ -5545,7 +5490,7 @@ async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, pas
5545
5490
 
5546
5491
  /**
5547
5492
  * Перевіряє NetworkPolicy для **Deployment**, **StatefulSet**, **DaemonSet**, **Job**, **CronJob**
5548
- * під `k8s` (base `components/networkpolicy.yaml`, інші шари `networkpolicy.yaml` поруч).
5493
+ * під `k8s` у `networkpolicy.yaml` поруч з workload-маніфестом base, у не-base — як overlay-specific override).
5549
5494
  * @param {string} root корінь репозиторію
5550
5495
  * @param {string[]} yamlFilesAbs yaml під k8s
5551
5496
  * @param {(msg: string) => void} fail callback при помилці
@@ -5558,11 +5503,7 @@ async function validateNetworkPoliciesForK8sWorkloads(root, yamlFilesAbs, fail,
5558
5503
  for (const [dir, workloads] of workloadsByDir) {
5559
5504
  const relDir = (relative(rootNorm, dir) || dir).replaceAll('\\', '/')
5560
5505
  const deployRel = relDir === '' ? '.' : relDir
5561
- const isBase = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
5562
- if (isBase && workloads.length > 0) {
5563
- failIfBaseLayerHasLocalNetworkPolicy(dir, deployRel, fail)
5564
- }
5565
- const npAbs = isBase ? join(dir, '..', COMPONENTS_DIR, NETWORK_POLICY_FILENAME) : join(dir, NETWORK_POLICY_FILENAME)
5506
+ const npAbs = join(dir, NETWORK_POLICY_FILENAME)
5566
5507
  const npRel = (relative(rootNorm, npAbs) || npAbs).replaceAll('\\', '/')
5567
5508
  const npDocs = existsSync(npAbs) ? await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy') : []
5568
5509
  for (const workload of workloads) {
@@ -6486,7 +6427,8 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs) {
6486
6427
  }
6487
6428
 
6488
6429
  /**
6489
- * Створює відсутні NetworkPolicy для workload-ів у каталозі (base → `components/`, інакше поруч).
6430
+ * Створює відсутні NetworkPolicy для workload-ів у каталозі (`networkpolicy.yaml` поруч з workload-маніфестом).
6431
+ * Якщо каталог — base, додатково додає `networkpolicy.yaml` у `kustomization.yaml` `resources:` (якщо файл існує).
6490
6432
  * @param {string} dir абсолютний каталог workload-маніфесту
6491
6433
  * @param {Record<string, unknown>[]} workloads workload-документи з цього каталогу
6492
6434
  * @param {string} rootNorm корінь репо
@@ -6496,8 +6438,7 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs) {
6496
6438
  */
6497
6439
  async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm, fail, passFn) {
6498
6440
  const relDir = (relative(rootNorm, dir) || dir).replaceAll('\\', '/')
6499
- const isBase = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
6500
- const npAbs = isBase ? join(dir, '..', COMPONENTS_DIR, NETWORK_POLICY_FILENAME) : join(dir, NETWORK_POLICY_FILENAME)
6441
+ const npAbs = join(dir, NETWORK_POLICY_FILENAME)
6501
6442
  const npRel = (relative(rootNorm, npAbs) || npAbs).replaceAll('\\', '/')
6502
6443
  if (existsSync(npAbs)) {
6503
6444
  const migrated = await regenerateLegacyNetworkPolicyDocsInFile(npAbs)
@@ -6519,19 +6460,15 @@ async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm,
6519
6460
  }
6520
6461
  if (toAdd.length === 0) return
6521
6462
  try {
6522
- if (isBase) await mkdir(dirname(npAbs), { recursive: true })
6523
6463
  await appendNetworkPolicyDocuments(npAbs, toAdd, npRel, passFn)
6524
- if (isBase) {
6525
- const componentsDir = dirname(npAbs)
6526
- const componentsRel = (relative(rootNorm, componentsDir) || componentsDir).replaceAll('\\', '/')
6527
- const kustAbs = join(componentsDir, 'kustomization.yaml')
6528
- if (existsSync(kustAbs)) {
6529
- const raw = await readFile(kustAbs, 'utf8')
6530
- const { changed, content } = ensureResourceInKustomizationYaml(raw, NETWORK_POLICY_FILENAME)
6531
- if (changed) {
6532
- await writeFile(kustAbs, content, 'utf8')
6533
- passFn(`${componentsRel}/kustomization.yaml: додано '${NETWORK_POLICY_FILENAME}' у resources (k8s.mdc)`)
6534
- }
6464
+ const kustAbs = join(dir, 'kustomization.yaml')
6465
+ if (existsSync(kustAbs)) {
6466
+ const raw = await readFile(kustAbs, 'utf8')
6467
+ const { changed, content } = ensureResourceInKustomizationYaml(raw, NETWORK_POLICY_FILENAME)
6468
+ if (changed) {
6469
+ await writeFile(kustAbs, content, 'utf8')
6470
+ const kustRel = relDir === '' ? 'kustomization.yaml' : `${relDir}/kustomization.yaml`
6471
+ passFn(`${kustRel}: додано '${NETWORK_POLICY_FILENAME}' у resources (k8s.mdc)`)
6535
6472
  }
6536
6473
  }
6537
6474
  } catch (error) {
@@ -6542,7 +6479,7 @@ async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm,
6542
6479
 
6543
6480
  /**
6544
6481
  * Автоматично створює відсутні **NetworkPolicy** для Deployment, StatefulSet, DaemonSet, Job і CronJob
6545
- * під `k8s` (base `components/`, інші шари → поруч).
6482
+ * під `k8s` (`networkpolicy.yaml` поруч з workload-маніфестом, у base додаток у `base/kustomization.yaml` resources).
6546
6483
  * @param {string} root корінь репозиторію
6547
6484
  * @param {string[]} yamlFilesAbs абсолютні шляхи yaml під `k8s`
6548
6485
  * @param {(msg: string) => void} fail callback при помилці
package/rules/k8s/k8s.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
3
- version: '1.39'
3
+ version: '1.41'
4
4
  globs: "**/k8s/**/*.yaml"
5
5
  alwaysApply: false
6
6
  ---
@@ -33,7 +33,7 @@ alwaysApply: false
33
33
 
34
34
  **Версія Kubernetes для kubeconform** має відповідати PIN yannh у цьому правилі та в **`check-k8s.mjs`** (зараз **`-kubernetes-version 1.33.9`** — semver без префікса `v`, еквівалент релізу **v1.33.9**; набір схем **`v1.33.9-standalone-strict`**). Для CRD додатково підключай реєстр [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog) другим **`-schema-location`**, як у [прикладах kubeconform](https://github.com/yannh/kubeconform#readme). За потреби **`-ignore-missing-schemas`**, якщо частина CRD ще без публічної схеми.
35
35
 
36
- **kubescape — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kubectl kustomize <dir>`** і передає stdout у **`kubescape scan <tmp-file>`** з порогом **`--severity-threshold high`** (вбудована в kubectl підкоманда `kustomize` — окремий бінарник `kustomize` не потрібен; рендеринг локальний і не потребує доступу до кластера). Маніфест проходить через тимчасовий файл, бо **`kubescape scan` у v4.x не читає stdin** (`-` як шлях → `no resources found to scan`; прапорця `--input`/`--stdin` немає); тимчасова директорія створюється під `os.tmpdir()` і прибирається після скану. Це усуває false-positive **C-0260** (`Missing network policy`) у каноні з sibling **`components/networkpolicy.yaml`** без `metadata.namespace`: сирий dir-скан не виконує kustomize-збірку й бачить порожній namespace у NetworkPolicy проти непорожнього у Deployment з `base/`, тож `podSelector` не матчиться. Якщо в дереві **`…/k8s`** немає жодного `kustomization.yaml` (проєкт без Kustomize) — fallback на старий dir-скан **`kubescape scan <каталог-k8s>`**. Перший запуск kubescape може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
36
+ **kubescape — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kubectl kustomize <dir>`** і передає stdout у **`kubescape scan <tmp-file>`** з порогом **`--severity-threshold high`** (вбудована в kubectl підкоманда `kustomize` — окремий бінарник `kustomize` не потрібен; рендеринг локальний і не потребує доступу до кластера). Маніфест проходить через тимчасовий файл, бо **`kubescape scan` у v4.x не читає stdin** (`-` як шлях → `no resources found to scan`; прапорця `--input`/`--stdin` немає); тимчасова директорія створюється під `os.tmpdir()` і прибирається після скану. Збірка через kustomize нормалізує namespace на workload-маніфестах і **NetworkPolicy у `base/networkpolicy.yaml`** (через `base/kustomization.yaml` `namespace:`), що дає коректний матчинг `podSelector` у `C-0260` (`Missing network policy`) і дозволяє kubescape бачити дерево overlays / components зі справжніми ресурсами. Якщо в дереві **`…/k8s`** немає жодного `kustomization.yaml` (проєкт без Kustomize) — fallback на старий dir-скан **`kubescape scan <каталог-k8s>`**. Перший запуск kubescape може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
37
37
 
38
38
  ### Винятки kubescape: `.kubescape-exceptions.json`
39
39
 
@@ -121,7 +121,7 @@ resources:
121
121
  memory: '128Mi'
122
122
  ```
123
123
 
124
- **HPA, PDB і NetworkPolicy у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` / `networkpolicy.yaml` поруч із workload-маніфестами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/` (див. розділ нижче).
124
+ **HPA і PDB у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` поруч із workload-маніфестами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/`, який підключають лише прод-overlays (див. розділ нижче). **NetworkPolicy** — навпаки: **обов'язковий і у `base/`**, у вигляді `base/networkpolicy.yaml` поруч з workload-маніфестом, підключений через `base/kustomization.yaml` `resources:` — щоб обмеження діяли і на dev-середовищі.
125
125
 
126
126
  ### Поза base (оверлеї, окремі каталоги)
127
127
 
@@ -381,33 +381,34 @@ images:
381
381
 
382
382
  **`check k8s`:** заборонено **`kind: Ingress`**.
383
383
 
384
- ## Deployment: `topologySpreadConstraints`, HPA / PDB через `components/`
384
+ ## Deployment: `topologySpreadConstraints`, HPA / PDB через `components/`, NetworkPolicy у `base/`
385
385
 
386
- Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB / NetworkPolicy також **не дозволені** через `resources` / `components` / `bases`.
386
+ Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB також **не дозволені** через `resources` / `components` / `bases`.
387
387
 
388
- Для **кожного** з **`Deployment`**, **`StatefulSet`**, **`DaemonSet`**, **`Job`**, **`CronJob`** під `k8s` обов'язковий **NetworkPolicy**: у **`…/k8s/…/base/`** — у **`components/networkpolicy.yaml`** (multi-doc, якщо workload-ів кілька); у **не-base** — **`networkpolicy.yaml`** поруч із маніфестом workload у тому ж каталозі. `metadata.name` NetworkPolicy **= `metadata.name`** workload; `spec.podSelector.matchLabels.app` **= мітка `app`** з `spec.selector.matchLabels` (для **CronJob** — з `spec.jobTemplate.spec.selector.matchLabels`). Відсутні документи **`check k8s`** створює автоматично.
388
+ **NetworkPolicy** — інша історія: оскільки обмеження мережі мають діяти **і на dev**, NP лежить **у `base/`** (а не в `components/`). Для **кожного** з **`Deployment`**, **`StatefulSet`**, **`DaemonSet`**, **`Job`**, **`CronJob`** під `k8s` обов'язковий **NetworkPolicy**: у **`…/k8s/…/base/`** — у **`base/networkpolicy.yaml`** поруч з workload-маніфестом (multi-doc, якщо workload-ів кілька); у **не-base** — **`networkpolicy.yaml`** поруч із маніфестом workload у тому ж каталозі (overlay-specific override). `metadata.name` NetworkPolicy **= `metadata.name`** workload; `spec.podSelector.matchLabels.app` **= мітка `app`** з `spec.selector.matchLabels` (для **CronJob** — з `spec.jobTemplate.spec.selector.matchLabels`). У `base/kustomization.yaml` `resources:` має бути `networkpolicy.yaml`. Відсутні документи **`check k8s`** створює автоматично і додає `networkpolicy.yaml` у `base/kustomization.yaml` `resources:`.
389
389
 
390
- **Канонічна структура `<pkg>/k8s/components/`** (sibling до `base/`):
390
+ **Канонічна структура `<pkg>/k8s/components/`** (sibling до `base/`) — лише HPA і PDB:
391
391
 
392
- - **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, networkpolicy.yaml, pdb.yaml]` (відсортовано за алфавітом).
392
+ - **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, pdb.yaml]` (відсортовано за алфавітом).
393
393
  - **`hpa.yaml`** — `autoscaling/v2`, `HorizontalPodAutoscaler`, **без** `metadata.namespace` (namespace задає kustomization-споживач), `spec.scaleTargetRef.name` **= `metadata.name`** Deployment з base, dev-like значення `minReplicas: 1`, `maxReplicas: 1`.
394
- - **`networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace`; один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у sibling `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns (UDP/TCP 53); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні, включно з metadata `169.254.169.254:80`); **in-cluster** — `to.namespaceSelector: {}` з **явним списком TCP-портів** (`80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318`; трафік на `*.svc` / Pod-и в кластері). Заборонено: `egress: [{}]`; `to.namespaceSelector: {}` без `ports:` (catch-all). Додаткові in-cluster порти можна додати вручну у `ports:` цього rule. Канон: [networkpolicy.snippet.yaml](./policy/network_policy/template/networkpolicy.snippet.yaml). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
395
394
  - **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
396
395
 
396
+ **Канонічний `base/networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace` (namespace додає `base/kustomization.yaml`); один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у тому ж `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns (UDP/TCP 53); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні, включно з metadata `169.254.169.254:80`); **in-cluster** — `to.namespaceSelector: {}` з **явним списком TCP-портів** (`80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318`; трафік на `*.svc` / Pod-и в кластері). Заборонено: `egress: [{}]`; `to.namespaceSelector: {}` без `ports:` (catch-all). Додаткові in-cluster порти можна додати вручну у `ports:` цього rule. Канон: [networkpolicy.snippet.yaml](./policy/network_policy/template/networkpolicy.snippet.yaml). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
397
+
397
398
  Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
398
399
 
399
- **Overlays** (`ua/`, прод-overlays) підключають `components: [- ../components]` і додають JSON6902-патчі для прод-значень: `/spec/minReplicas`, `/spec/maxReplicas` (HPA), `/spec/minAvailable` (PDB). Dev-середовище (`base`) HPA/PDB не отримує — як і потрібно.
400
+ **Overlays** (`ua/`, прод-overlays) підключають `components: [- ../components]` і додають JSON6902-патчі для прод-значень: `/spec/minReplicas`, `/spec/maxReplicas` (HPA), `/spec/minAvailable` (PDB). NetworkPolicy успадковується з base через `resources: [- ../base]` — окремий `networkpolicy.yaml` у overlay не обов'язковий і додається тільки для overlay-specific правил. Dev-середовище (`base`) HPA/PDB не отримує — як і потрібно — а NetworkPolicy діє з тим самим каноном.
400
401
 
401
402
  **`<pkg>/k8s/components/kustomization.yaml`** має `kind: Component` (не `kind: Kustomization`) — це **джерело** канонічних HPA/PDB для всіх overlays, а не overlay сам по собі. Прод-перезаписи (`/spec/minReplicas`, `/spec/maxReplicas`, `/spec/minAvailable`) живуть у `<env>/kustomization.yaml`, що підключає Component через `components:`. У самому Component patches не потрібні — він env-неутральний; **`check k8s`** не вимагає прод-патчів від `components/kustomization.yaml`.
402
403
 
403
- У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема: окремі **`hpa.yaml`**, **`networkpolicy.yaml`** і **`pdb.yaml`**, якщо такі потрібні для цього середовища. Для **StatefulSet**, **DaemonSet**, **Job**, **CronJob** у не-base — обов'язковий лише **`networkpolicy.yaml`** (HPA/PDB лишаються прив'язаними до **Deployment**, якщо є). **`check k8s`** звіряє прив'язку за іменами (і **дописує** відсутні NetworkPolicy-документи автоматично):
404
+ У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема: окремі **`hpa.yaml`**, **`networkpolicy.yaml`** і **`pdb.yaml`**, якщо такі потрібні для цього середовища (NP overlay-specific override). Для **StatefulSet**, **DaemonSet**, **Job**, **CronJob** у не-base — обов'язкового локального `networkpolicy.yaml` немає, бо NP вже успадковується з base; додавайте лише якщо overlay змінює правила. **`check k8s`** звіряє прив'язку за іменами (і **дописує** відсутні NetworkPolicy-документи автоматично у файл поруч):
404
405
 
405
406
  - **`hpa.yaml`** (поза **`…/base/`**) — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
406
- - **`networkpolicy.yaml`** — той самий канон egress/ingress, що в `components/` (multi-doc, якщо у каталозі кілька workload-ів).
407
+ - **`networkpolicy.yaml`** (overlay-specific, опціональний) — той самий канон egress/ingress, що в `base/` (multi-doc, якщо у каталозі кілька workload-ів).
407
408
  - **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
408
409
  - **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
409
410
 
410
- **Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
411
+ **Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
411
412
 
412
413
  **Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
413
414
 
@@ -462,18 +463,28 @@ patches:
462
463
  value: 1
463
464
  ```
464
465
 
465
- ### Приклади `components/`
466
+ ### Приклади `components/` і `base/networkpolicy.yaml`
466
467
 
467
468
  ```yaml title="k8s/components/kustomization.yaml"
468
469
  apiVersion: kustomize.config.k8s.io/v1alpha1
469
470
  kind: Component
470
471
  resources:
471
472
  - hpa.yaml
472
- - networkpolicy.yaml
473
473
  - pdb.yaml
474
474
  ```
475
475
 
476
- ```yaml title="k8s/components/networkpolicy.yaml"
476
+ ```yaml title="k8s/base/kustomization.yaml (фрагмент)"
477
+ apiVersion: kustomize.config.k8s.io/v1beta1
478
+ kind: Kustomization
479
+ namespace: dev
480
+ resources:
481
+ - deploy.yaml
482
+ - networkpolicy.yaml
483
+ - svc.yaml
484
+ - svc-hl.yaml
485
+ ```
486
+
487
+ ```yaml title="k8s/base/networkpolicy.yaml"
477
488
  # yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/networkpolicy-networking-v1.json
478
489
  apiVersion: networking.k8s.io/v1
479
490
  kind: NetworkPolicy
@@ -227,10 +227,10 @@ function runKubescapeManifest(kubescapePath, manifest, exceptionsArgs) {
227
227
  * Запускає kubescape по зібраному kustomize-маніфесту для кожного `…/k8s`-кореня. Для кожного
228
228
  * dir-у з `kustomization.yaml` (крім `kind: Component`) робимо `kubectl kustomize <dir>` і
229
229
  * передаємо stdout у `kubescape scan <tmp-file>` через тимчасовий файл (kubescape v4.x не читає
230
- * stdin — див. `runKubescapeManifest`). Це усуває false-positive C-0260 (`Missing network policy`)
231
- * у випадках, коли NetworkPolicy живе у sibling `components/` без `metadata.namespace` (намспейс
232
- * інжектить overlay через `kustomization.namespace`); сирий dir-скан не виконує kustomize й бачить
233
- * порожній `namespace` у NetworkPolicy проти непорожнього у Deployment, через що `podSelector` не матчиться.
230
+ * stdin — див. `runKubescapeManifest`). Збірка через kustomize нормалізує namespace на workload-маніфестах
231
+ * і `base/networkpolicy.yaml` (через `base/kustomization.yaml` `namespace:`), що дає коректний
232
+ * матчинг `podSelector` у C-0260 (`Missing network policy`) і дозволяє kubescape бачити дерево
233
+ * overlays / components зі справжніми ресурсами.
234
234
  *
235
235
  * Якщо в `…/k8s`-корені немає жодного білдабельного kustomization.yaml (проєкт без Kustomize) —
236
236
  * fallback на старий dir-скан, щоб не блокувати чистий YAML-only набір маніфестів.
@@ -41,16 +41,20 @@ deny contains base_namespace_required_msg if {
41
41
  # (із зануренням у вкладені kustomization.yaml) — JS-оркестратор
42
42
  # `verifyK8sBaseKustomizeHasNoHpaPdb` у `check-k8s.mjs` (потребує fs-доступу). Цей
43
43
  # rego-deny — defense-in-depth: спрацює навіть якщо JS-крок упаде з винятку раніше.
44
- deny contains hpa_pdb_np_in_base_resources_msg(r) if {
44
+ #
45
+ # NetworkPolicy у base — навпаки, обов'язковий (k8s.mdc): обмеження мережі мають
46
+ # діяти і на dev-середовищі, тож `base/networkpolicy.yaml` підключений через
47
+ # `base/kustomization.yaml` `resources:` і не блокується цим правилом.
48
+ deny contains hpa_pdb_in_base_resources_msg(r) if {
45
49
  is_kustomization
46
50
  some r in object.get(input, "resources", [])
47
51
  is_string(r)
48
- is_hpa_pdb_or_np_filename(r)
52
+ is_hpa_or_pdb_filename(r)
49
53
  }
50
54
 
51
- hpa_pdb_np_in_base_resources_msg(file) := sprintf(
55
+ hpa_pdb_in_base_resources_msg(file) := sprintf(
52
56
  concat("", [
53
- "у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB/NetworkPolicy заборонені у base, ",
57
+ "у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB заборонені у base, ",
54
58
  "перенесіть у sibling каталог components/ і підключайте з overlay (k8s.mdc)",
55
59
  ]),
56
60
  [file],
@@ -61,14 +65,10 @@ is_kustomization if {
61
65
  startswith(object.get(input, "apiVersion", ""), "kustomize.config.k8s.io/")
62
66
  }
63
67
 
64
- is_hpa_pdb_or_np_filename(p) if {
68
+ is_hpa_or_pdb_filename(p) if {
65
69
  basename(p) in {
66
70
  "hpa.yaml",
67
71
  "pdb.yaml",
68
- "networkpolicy.yaml",
69
- "hpa.yml",
70
- "pdb.yml",
71
- "networkpolicy.yml",
72
72
  }
73
73
  }
74
74
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
3
  "files": {
4
- "walkGlob": ["**/k8s/**/base/**/*.yaml", "**/k8s/**/base/**/*.yml", "!**/k8s/**/base/**/kustomization.yaml"]
4
+ "walkGlob": ["**/k8s/**/base/**/*.yaml", "!**/k8s/**/base/**/kustomization.yaml"]
5
5
  }
6
6
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
- "files": { "walkGlob": ["**/k8s/**/*.yaml", "**/k8s/**/*.yml"] }
3
+ "files": { "walkGlob": ["**/k8s/**/*.yaml"] }
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
- "files": { "walkGlob": ["**/k8s/**/*.yaml", "**/k8s/**/*.yml"] }
3
+ "files": { "walkGlob": ["**/k8s/**/*.yaml"] }
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
- "files": { "walkGlob": ["**/k8s/**/*.yaml", "**/k8s/**/*.yml"] }
3
+ "files": { "walkGlob": ["**/k8s/**/*.yaml"] }
4
4
  }