@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 +18 -0
- package/package.json +1 -1
- package/rules/bun/bun.mdc +4 -2
- package/rules/bun/fix/layout/check.mjs +98 -26
- package/rules/image-avif/fix/avif_generation/check.mjs +1 -3
- package/rules/k8s/fix/manifests/check.mjs +29 -92
- package/rules/k8s/k8s.mdc +27 -16
- package/rules/k8s/lint/lint.mjs +4 -4
- package/rules/k8s/policy/base_kustomization/base_kustomization.rego +9 -9
- package/rules/k8s/policy/base_manifest/target.json +1 -1
- package/rules/k8s/policy/gateway/target.json +1 -1
- package/rules/k8s/policy/hpa_pdb/target.json +1 -1
- package/rules/k8s/policy/manifest/target.json +1 -1
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
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.
|
|
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`**).
|
|
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
|
-
* -
|
|
9
|
-
* `
|
|
10
|
-
* (
|
|
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
|
-
* Зчитує
|
|
29
|
-
* @returns {Promise<Set<string
|
|
30
|
+
* Зчитує `rules` та `disable-rules` з `.n-cursor.json`.
|
|
31
|
+
* @returns {Promise<{ rules: Set<string>, disabled: Set<string> }>} активні правила і явно вимкнені
|
|
30
32
|
*/
|
|
31
33
|
async function loadNCursorRules() {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
42
|
+
return empty
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
/**
|
|
48
|
-
*
|
|
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
|
|
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
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
95
|
-
* base-kustomize не повинно бути HPA/PDB
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 &&
|
|
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`
|
|
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
|
|
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-ів у каталозі (
|
|
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
|
|
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
|
-
|
|
6525
|
-
|
|
6526
|
-
const
|
|
6527
|
-
const
|
|
6528
|
-
if (
|
|
6529
|
-
|
|
6530
|
-
const
|
|
6531
|
-
|
|
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
|
|
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.
|
|
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()` і прибирається після скану.
|
|
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
|
|
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
|
|
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/`** — у **`
|
|
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,
|
|
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`**, якщо такі потрібні для цього
|
|
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, що в `
|
|
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
|
|
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/
|
|
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
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -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`).
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
52
|
+
is_hpa_or_pdb_filename(r)
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
hpa_pdb_in_base_resources_msg(file) := sprintf(
|
|
52
56
|
concat("", [
|
|
53
|
-
"у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB
|
|
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
|
-
|
|
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", "
|
|
4
|
+
"walkGlob": ["**/k8s/**/base/**/*.yaml", "!**/k8s/**/base/**/kustomization.yaml"]
|
|
5
5
|
}
|
|
6
6
|
}
|