@nitra/cursor 1.8.220 → 1.8.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-template/npm-CLAUDE.md +4 -0
- package/CHANGELOG.md +21 -0
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +25 -4
- package/mdc/ci4.mdc +51 -0
- package/mdc/tauri.mdc +20 -0
- package/package.json +1 -1
- package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
- package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
- package/policy/k8s/base_manifest/base_manifest.rego +154 -0
- package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
- package/policy/k8s/gateway/gateway.rego +151 -0
- package/policy/k8s/gateway/gateway_test.rego +122 -0
- package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
- package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
- package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
- package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
- package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
- package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
- package/policy/k8s/kustomization/kustomization.rego +220 -0
- package/policy/k8s/kustomization/kustomization_test.rego +128 -0
- package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
- package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
- package/policy/k8s/manifest/manifest.rego +151 -4
- package/policy/k8s/manifest/manifest_test.rego +309 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
- package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
- package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
- package/scripts/auto-skills.mjs +8 -1
- package/scripts/check-bun.mjs +3 -3
- package/scripts/check-changelog.mjs +2 -3
- package/scripts/check-image-avif.mjs +14 -6
- package/scripts/check-image-compress.mjs +1 -1
- package/scripts/check-js-run.mjs +58 -47
- package/scripts/check-k8s.mjs +128 -51
- package/scripts/check-npm-module.mjs +1 -4
- package/scripts/check-php.mjs +5 -5
- package/scripts/claude-stop-hook.mjs +2 -2
- package/scripts/lint-conftest.mjs +88 -8
- package/scripts/lint-ga.mjs +1 -1
- package/scripts/lint-rego.mjs +19 -4
- package/scripts/run-shellcheck-text.mjs +94 -64
- package/scripts/sync-claude-config.mjs +1 -1
- package/scripts/utils/ast-scan-utils.mjs +28 -0
- package/scripts/utils/bun-sql-scan.mjs +53 -34
- package/scripts/utils/bunyan-imports.mjs +10 -61
- package/scripts/utils/conn-file-rules.mjs +76 -37
- package/scripts/utils/depcheck-workflow.mjs +27 -6
- package/scripts/utils/redis-imports.mjs +9 -51
- package/skills/llm-patch/SKILL.md +16 -5
|
@@ -21,6 +21,10 @@ npx @nitra/cursor check changelog
|
|
|
21
21
|
npx @nitra/cursor check npm-module
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## Перш ніж писати `check-*.mjs`
|
|
25
|
+
|
|
26
|
+
Перед створенням нового `npm/scripts/check-<rule>.mjs` оціни, чи задача лягає на rego-полісі. **Default — Rego**: пер-документні структурні перевірки (kind/apiVersion, поля, форма масивів) пишуться у `npm/policy/<rule>/<name>/<name>.rego` + `_test.rego`. JS — тільки для cross-file resolution, file-system access (`readdir`/`stat`), autofix/rewrite або парсингу до YAML-body. Гібрид (rego як швидкий gate + JS-оркестратор для cross-file) — нормальний патерн; референс — `npm/policy/k8s/*` ↔ `npm/scripts/check-k8s.mjs`. Деталі алгоритму рішення — `.cursor/rules/conftest.mdc` (alwaysApply).
|
|
27
|
+
|
|
24
28
|
## Джерело правил
|
|
25
29
|
|
|
26
30
|
- `.cursor/rules/n-changelog.mdc` — правило про CHANGELOG (PR-scoped, для всіх воркспейсів)
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.222] - 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **k8s / rego-полісі:** розширено `npm/policy/k8s/manifest/manifest.rego` (Deployment cpu+memory у `requests`, Hasura image pin із білим списком тегів, канонічний `topologySpreadConstraints` з мітки `app` самого Deployment). Додано `manifest_test.rego` із вхідними фікстурами; rego тестується через `conftest verify` (опційний крок у `bun run lint-rego`). JS у `check-k8s.mjs` лишається authoritative — нові правила Rego — швидкий gate для одиничного маніфеста.
|
|
12
|
+
- **k8s / нові rego-пакети:** `npm/policy/k8s/gateway/` (Gateway API: backendRef з суфіксом `-hl`, redundant `namespace` у backendRef, HCP `targetRef.name` `-hl`); `npm/policy/k8s/kustomization/` (resources/patches алфавітне сортування, JSON6902 `remove`+`add` на той самий `path`); `npm/policy/k8s/svc_yaml/` (`Service.spec.type: ClusterIP`); `npm/policy/k8s/svc_hl_yaml/` (headless Service з суфіксом `-hl` і `clusterIP: None`); `npm/policy/k8s/base_kustomization/` (обов'язковий `namespace:`); `npm/policy/k8s/base_manifest/` (`metadata.namespace` у base, base-canon `cpu='0.02'`/`memory='128Mi'`); `npm/policy/k8s/kustomize_managed/` (заборона `metadata.namespace` у kustomize-managed файлах); `npm/policy/k8s/hasura_configmap/` (`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: "true"`); `npm/policy/k8s/hasura_httproute/` (канон 4 правил Hasura: `/ql` Exact + `/ql/` Exact + PathPrefix + WebSocket); `npm/policy/k8s/hpa_pdb/` (структурний gate HPA/PDB: `apiVersion`, `behavior.scaleUp/Down`, `metrics`, `selector.matchLabels`). До кожного пакета додано `*_test.rego` фікстури.
|
|
13
|
+
- **lint-rego:** додано опційний крок `conftest verify` у `npm/scripts/lint-rego.mjs` після `regal lint` для виконання `*_test.rego`. Якщо `conftest` не у PATH — крок мовчки пропускається з install-hint.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **lint-conftest:** `npm/scripts/lint-conftest.mjs` — `k8s.manifest` target тепер указує на `policyDir: 'k8s/manifest'` (вужчий policy-tree), додано targets для нових пакетів `k8s.gateway`, `k8s.hpa_pdb`, `k8s.kustomization`, `k8s.svc_yaml`, `k8s.svc_hl_yaml`, `k8s.base_kustomization`, `k8s.base_manifest`. Шляхові регекспи: `K8S_KUSTOMIZATION_PATH_RE`, `K8S_BASE_KUSTOMIZATION_PATH_RE`, `K8S_BASE_MANIFEST_PATH_RE`, `K8S_SVC_YAML_PATH_RE`, `K8S_SVC_HL_YAML_PATH_RE`. Пакети `kustomize_managed`, `hasura_configmap`, `hasura_httproute` потребують cross-file gating з `check-k8s.mjs` і не входять у `lint-conftest` walk-targets.
|
|
18
|
+
- **n-k8s.mdc:** додано розділ «Швидкий gate через conftest (Rego)» зі списком rego-пакетів і опису того, що cross-file логіка (резолюція kustomize-tree, парність svc.yaml/svc-hl.yaml, прив'язка ConfigMap/HTTPRoute до Hasura-Deployment, HPA/PDB by directory, env-залежні межі min/maxReplicas) лишається у `check-k8s.mjs`.
|
|
19
|
+
- **conftest.mdc (alwaysApply):** замість одного абзацу про «пріоритет conftest» — повний алгоритм рішення для нової перевірки: декізія-дерево (single-document → Rego за замовчуванням; cross-file/FS/autofix/text-pre-YAML → JS), workflow «спершу намалюй вхід → rego або гібрид», список «червоних прапорів» (Rego не вміє X — звір зі списком винятків). Мета: робити Rego default-вибором для нових перевірок.
|
|
20
|
+
- **npm/.claude-template/npm-CLAUDE.md:** додано path-scoped нагадування «Перш ніж писати `check-*.mjs`» з посиланням на алгоритм у `conftest.mdc`. Регенеровано `npm/CLAUDE.md`.
|
|
21
|
+
|
|
22
|
+
## [1.8.221] - 2026-05-10
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **ci4.mdc:** наповнено правило людинозрозумілим описом C4-моделі як джерела істини. Markdown-файли (C4 + ADR + тести + документація) — офіційне джерело істини про проєкт. Перед змінами агент аналізує відповідні C4-файли; кожна зміна, що впливає на модель, супроводжується оновленням C4-схеми у тому ж PR. ADR описує вплив рішення на C4 (які контейнери/компоненти з'являються/зникають/змінюють відповідальність). Кожен C4-компонент має посилання на відповідні тести. C4-схеми — частина користувацької документації, не закритий артефакт. Алгоритмічної `check-ci4.mjs` поки немає — правило процесне; `## Перевірка` залишено для майбутньої формалізації.
|
|
27
|
+
|
|
7
28
|
## [1.8.220] - 2026-05-09
|
|
8
29
|
|
|
9
30
|
### Fixed
|
package/bin/auto-rules.md
CHANGED
package/bin/n-cursor.js
CHANGED
|
@@ -1137,7 +1137,8 @@ async function readBundledVersionAt(packageRoot) {
|
|
|
1137
1137
|
* бо ES-модулі вже завантажені у V8 (RULE_MIGRATIONS, detectAutoRules тощо) і нова логіка
|
|
1138
1138
|
* без повної заміни процесу не підхопиться. Захист від нескінченного циклу — env `NITRA_CURSOR_REEXEC=1`.
|
|
1139
1139
|
* @param {string} effectivePackageRoot шлях, повернутий `upgradeNitraCursorToLatestAndBunInstall`
|
|
1140
|
-
* @returns {Promise<void>} повертається лише якщо re-exec не
|
|
1140
|
+
* @returns {Promise<void>} повертається лише якщо re-exec не потрібен; інакше кидає `ReexecHandoff`,
|
|
1141
|
+
* який ловить top-level catch і прокидає exit-код у `process.exitCode`
|
|
1141
1142
|
*/
|
|
1142
1143
|
async function reexecIfPackageVersionChanged(effectivePackageRoot) {
|
|
1143
1144
|
if (env.NITRA_CURSOR_REEXEC === '1') {
|
|
@@ -1167,7 +1168,23 @@ async function reexecIfPackageVersionChanged(effectivePackageRoot) {
|
|
|
1167
1168
|
if (result.error) {
|
|
1168
1169
|
throw result.error
|
|
1169
1170
|
}
|
|
1170
|
-
|
|
1171
|
+
throw new ReexecHandoff(typeof result.status === 'number' ? result.status : 1)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Сентинельна помилка, яку кидає `reexecIfPackageVersionChanged` після успішного re-exec.
|
|
1176
|
+
* Top-level catch розпізнає її й виставляє `process.exitCode = code` без stack-trace —
|
|
1177
|
+
* процес тоді коректно завершується з тим самим кодом, що й child re-exec-у.
|
|
1178
|
+
*/
|
|
1179
|
+
class ReexecHandoff extends Error {
|
|
1180
|
+
/**
|
|
1181
|
+
* @param {number} code exit-код, який повернув child-процес
|
|
1182
|
+
*/
|
|
1183
|
+
constructor(code) {
|
|
1184
|
+
super('reexec-handoff')
|
|
1185
|
+
this.name = 'ReexecHandoff'
|
|
1186
|
+
this.code = code
|
|
1187
|
+
}
|
|
1171
1188
|
}
|
|
1172
1189
|
|
|
1173
1190
|
/**
|
|
@@ -1329,6 +1346,10 @@ try {
|
|
|
1329
1346
|
process.exitCode = 1
|
|
1330
1347
|
}
|
|
1331
1348
|
}
|
|
1332
|
-
} catch {
|
|
1333
|
-
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
if (error instanceof ReexecHandoff) {
|
|
1351
|
+
process.exitCode = error.code
|
|
1352
|
+
} else {
|
|
1353
|
+
process.exitCode = 1
|
|
1354
|
+
}
|
|
1334
1355
|
}
|
package/mdc/ci4.mdc
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Дизайн проєкту за C4 model (https://c4model.com); Markdown — джерело істини про архітектуру, рішення, тести й документацію
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
C4-діаграми проєкту живуть у Markdown поряд із кодом. Це не довідник «для людей із порталу
|
|
8
|
+
архітектора» — це **джерело істини**, з якого LLM-агент і людина читають намір системи перед
|
|
9
|
+
будь-якою зміною коду. Тому правила нижче — не оформлення, а робочий процес.
|
|
10
|
+
|
|
11
|
+
## Markdown як джерело істини
|
|
12
|
+
|
|
13
|
+
Уся ключова інформація про проєкт — архітектура (C4), рішення (ADR), тести й документація —
|
|
14
|
+
зберігається у `.md`/`.mdc`-файлах і є **офіційним джерелом істини**. Це єдиний спосіб
|
|
15
|
+
тримати знання разом із кодом — версійно, в одному PR і доступно для агентів. Якщо щось
|
|
16
|
+
важливе про систему існує лише у Confluence/Notion/месенджері — для проєкту цього **немає**.
|
|
17
|
+
|
|
18
|
+
## Аналіз перед зміною
|
|
19
|
+
|
|
20
|
+
Перш ніж вносити зміни, агент **читає відповідні C4-файли**: контекст, контейнери, компоненти
|
|
21
|
+
тієї ділянки, до якої входить редагований код. Без цього кроку легко зламати приховані
|
|
22
|
+
залежності між контейнерами або «загубити» зовнішню інтеграцію.
|
|
23
|
+
|
|
24
|
+
## Оновлення синхронно зі змінами
|
|
25
|
+
|
|
26
|
+
Кожна зміна, що впливає на C4-модель — нова інтеграція, новий компонент, перейменування,
|
|
27
|
+
зміна напрямку залежності, видалення сервісу — **супроводжується оновленням C4-схеми у тому ж
|
|
28
|
+
PR**. C4 не оновлюється «потім» окремою задачею: розсинхрон між кодом і моделлю — найчастіша
|
|
29
|
+
причина, чому архітектурні документи перестають читати.
|
|
30
|
+
|
|
31
|
+
## Зв'язок із ADR
|
|
32
|
+
|
|
33
|
+
ADR (`docs/adr/`) описує **вплив рішення на C4**: які контейнери/компоненти з'являються,
|
|
34
|
+
зникають, змінюють відповідальність. Якщо рішення суттєве — у тілі ADR явно пишемо, які
|
|
35
|
+
C4-схеми потрібно оновити, і робимо це у тому ж PR.
|
|
36
|
+
|
|
37
|
+
## Зв'язок із тестами
|
|
38
|
+
|
|
39
|
+
Кожен C4-компонент має **посилання на відповідні тести** — інтеграційні, e2e або юніт залежно
|
|
40
|
+
від рівня. Так перехід «компонент → як його перевіряємо» займає один клік, а не пошук по
|
|
41
|
+
репозиторію.
|
|
42
|
+
|
|
43
|
+
## Зв'язок із документацією
|
|
44
|
+
|
|
45
|
+
C4-схеми — частина **користувацької документації**, а не закритий артефакт для команди.
|
|
46
|
+
Контекстна діаграма (рівень 1) і контейнерна (рівень 2) живуть там, де читач шукає вступ у
|
|
47
|
+
проєкт, а не у відокремленій теці «for-architects».
|
|
48
|
+
|
|
49
|
+
## Перевірка
|
|
50
|
+
|
|
51
|
+
`npx @nitra/cursor check ci4`
|
package/mdc/tauri.mdc
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Tauri
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
в файлі .vscode/extensions.json є налаштування для Vue:
|
|
9
|
+
|
|
10
|
+
```json title=".vscode/extensions.json"
|
|
11
|
+
{
|
|
12
|
+
"recommendations": ["tauri-apps.tauri-vscode",
|
|
13
|
+
"rust-lang.rust-analyzer"]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Перевірка
|
|
19
|
+
|
|
20
|
+
`npx @nitra/cursor check tauri`
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Порт перевірки `k8s/base/kustomization.yaml` з `npm/scripts/check-k8s.mjs`
|
|
2
|
+
# (k8s.mdc): у base-kustomization обов'язково має бути непорожнє поле
|
|
3
|
+
# `namespace:`.
|
|
4
|
+
#
|
|
5
|
+
# Запуск (локально, лише для одного `k8s/base/kustomization.yaml`):
|
|
6
|
+
# conftest test path/to/k8s/base/kustomization.yaml \
|
|
7
|
+
# -p npm/policy/k8s/base_kustomization \
|
|
8
|
+
# --namespace k8s.base_kustomization
|
|
9
|
+
#
|
|
10
|
+
# JS authoritative (`check-k8s.mjs`: `baseKustomizationNamespaceViolation`,
|
|
11
|
+
# `isBaseKustomizationPath` для відбору файла, `ensureBaseKustomizationHasNamespace`
|
|
12
|
+
# як оркестратор).
|
|
13
|
+
#
|
|
14
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
15
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
|
|
16
|
+
package k8s.base_kustomization
|
|
17
|
+
|
|
18
|
+
import rego.v1
|
|
19
|
+
|
|
20
|
+
base_namespace_required_msg := concat(" ", [
|
|
21
|
+
"у base/kustomization.yaml завжди додай непорожній namespace:",
|
|
22
|
+
"(наприклад namespace: dev; k8s.mdc)",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
deny contains base_namespace_required_msg if {
|
|
26
|
+
is_kustomization
|
|
27
|
+
not is_string(object.get(input, "namespace", null))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
deny contains base_namespace_required_msg if {
|
|
31
|
+
is_kustomization
|
|
32
|
+
ns := object.get(input, "namespace", "")
|
|
33
|
+
is_string(ns)
|
|
34
|
+
trim_space(ns) == ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
is_kustomization if {
|
|
38
|
+
input.kind == "Kustomization"
|
|
39
|
+
startswith(object.get(input, "apiVersion", ""), "kustomize.config.k8s.io/")
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Тести для `k8s.base_kustomization`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/k8s/base_kustomization --namespace k8s.base_kustomization
|
|
3
|
+
package k8s.base_kustomization_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.k8s.base_kustomization
|
|
8
|
+
|
|
9
|
+
base_kust := {
|
|
10
|
+
"apiVersion": "kustomize.config.k8s.io/v1beta1",
|
|
11
|
+
"kind": "Kustomization",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test_deny_missing_namespace if {
|
|
15
|
+
count(base_kustomization.deny) > 0 with input as base_kust
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test_deny_empty_namespace if {
|
|
19
|
+
count(base_kustomization.deny) > 0 with input as object.union(base_kust, {"namespace": ""})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test_deny_whitespace_namespace if {
|
|
23
|
+
count(base_kustomization.deny) > 0 with input as object.union(base_kust, {"namespace": " "})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test_allow_with_namespace if {
|
|
27
|
+
count(base_kustomization.deny) == 0 with input as object.union(base_kust, {"namespace": "dev"})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test_allow_non_kustomization if {
|
|
31
|
+
count(base_kustomization.deny) == 0 with input as {
|
|
32
|
+
"apiVersion": "v1",
|
|
33
|
+
"kind": "ConfigMap",
|
|
34
|
+
"metadata": {"name": "cm"},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Порт пер-документних структурних перевірок для маніфестів у шарі
|
|
2
|
+
# `…/k8s/.../base/...` (k8s.mdc).
|
|
3
|
+
#
|
|
4
|
+
# Запуск (локально, лише для одного файлу під base/):
|
|
5
|
+
# conftest test path/to/k8s/base/deployment.yaml -p npm/policy/k8s/base_manifest \
|
|
6
|
+
# --namespace k8s.base_manifest
|
|
7
|
+
#
|
|
8
|
+
# JS відбирає файли під `…/k8s/.../base/…` (окрім `kustomization.yaml`) і
|
|
9
|
+
# викликає conftest з цією намеспейс. JS authoritative
|
|
10
|
+
# (`check-k8s.mjs`: `metadataNamespaceRequiredViolation` з `inBaseDir=true`,
|
|
11
|
+
# `deploymentResourcesViolation` з `inK8sBaseLayer=true`,
|
|
12
|
+
# `isK8sBaseManifestYamlPath`).
|
|
13
|
+
#
|
|
14
|
+
# Перевіряє:
|
|
15
|
+
# - namespaced kind має непорожній `metadata.namespace` (cluster-scoped kind
|
|
16
|
+
# і Kustomization/List виняток);
|
|
17
|
+
# - Deployment у base має фіксовані `resources.requests.cpu == "0.02"` (або
|
|
18
|
+
# число `0.02`) і `resources.requests.memory == "128Mi"` (case-insensitive
|
|
19
|
+
# суфікс Mi).
|
|
20
|
+
#
|
|
21
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
22
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
|
|
23
|
+
package k8s.base_manifest
|
|
24
|
+
|
|
25
|
+
import rego.v1
|
|
26
|
+
|
|
27
|
+
# Cluster-scoped kind (не вимагають metadata.namespace) — узгоджено з
|
|
28
|
+
# `CLUSTER_SCOPED_KINDS` у `check-k8s.mjs`.
|
|
29
|
+
cluster_scoped_kinds := {
|
|
30
|
+
"APIService",
|
|
31
|
+
"CertificateSigningRequest",
|
|
32
|
+
"ClusterCIDR",
|
|
33
|
+
"ClusterRole",
|
|
34
|
+
"ClusterRoleBinding",
|
|
35
|
+
"ComponentStatus",
|
|
36
|
+
"CSIDriver",
|
|
37
|
+
"CSINode",
|
|
38
|
+
"CustomResourceDefinition",
|
|
39
|
+
"FlowSchema",
|
|
40
|
+
"IPAddress",
|
|
41
|
+
"IngressClass",
|
|
42
|
+
"MutatingWebhookConfiguration",
|
|
43
|
+
"Namespace",
|
|
44
|
+
"Node",
|
|
45
|
+
"PersistentVolume",
|
|
46
|
+
"PriorityClass",
|
|
47
|
+
"PriorityLevelConfiguration",
|
|
48
|
+
"RuntimeClass",
|
|
49
|
+
"ServiceCIDR",
|
|
50
|
+
"StorageClass",
|
|
51
|
+
"StorageVersionMigration",
|
|
52
|
+
"ValidatingAdmissionPolicy",
|
|
53
|
+
"ValidatingAdmissionPolicyBinding",
|
|
54
|
+
"ValidatingWebhookConfiguration",
|
|
55
|
+
"VolumeAttachment",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Жорстко зафіксовані значення resources.requests у base-шарі (k8s.mdc).
|
|
59
|
+
base_cpu_request := "0.02"
|
|
60
|
+
|
|
61
|
+
base_memory_request := "128Mi"
|
|
62
|
+
|
|
63
|
+
base_metadata_missing_msg := concat(" ", [
|
|
64
|
+
"додай metadata з непорожнім metadata.namespace —",
|
|
65
|
+
"у k8s/base у кожному ресурсному YAML має бути явний namespace (k8s.mdc)",
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
base_namespace_required_msg := concat(" ", [
|
|
69
|
+
"metadata.namespace обов'язковий у k8s/base —",
|
|
70
|
+
"додай явний namespace у маніфесті (k8s.mdc)",
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
base_canon_cpu_template := concat(" ", [
|
|
74
|
+
"контейнер %q: у k8s/.../base resources.requests.cpu має бути рівно %q",
|
|
75
|
+
"(допускається число 0.02) — зараз %v (k8s.mdc)",
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
base_canon_memory_template := concat(" ", [
|
|
79
|
+
"контейнер %q: у k8s/.../base resources.requests.memory має бути рівно %q",
|
|
80
|
+
"(суфікс Mi без урахування регістру) — зараз %v (k8s.mdc)",
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
# ── deny: namespaced kind у base/ — обов'язковий metadata.namespace ──────
|
|
84
|
+
|
|
85
|
+
deny contains base_metadata_missing_msg if {
|
|
86
|
+
is_namespaced_kind
|
|
87
|
+
not is_object(object.get(input, "metadata", null))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
deny contains base_namespace_required_msg if {
|
|
91
|
+
is_namespaced_kind
|
|
92
|
+
meta := object.get(input, "metadata", null)
|
|
93
|
+
is_object(meta)
|
|
94
|
+
ns := object.get(meta, "namespace", "")
|
|
95
|
+
trim_space(ns) == ""
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ── deny: Deployment у base — точне cpu='0.02' / memory='128Mi' ──────────
|
|
99
|
+
|
|
100
|
+
deny contains msg if {
|
|
101
|
+
input.kind == "Deployment"
|
|
102
|
+
some container in deployment_all_containers
|
|
103
|
+
cpu := object.get(object.get(container, "resources", {}), "requests", {}).cpu
|
|
104
|
+
cpu != null
|
|
105
|
+
not is_base_canon_cpu(cpu)
|
|
106
|
+
msg := sprintf(base_canon_cpu_template, [container.name, base_cpu_request, cpu])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
deny contains msg if {
|
|
110
|
+
input.kind == "Deployment"
|
|
111
|
+
some container in deployment_all_containers
|
|
112
|
+
mem := object.get(object.get(container, "resources", {}), "requests", {}).memory
|
|
113
|
+
mem != null
|
|
114
|
+
not is_base_canon_memory(mem)
|
|
115
|
+
msg := sprintf(base_canon_memory_template, [container.name, base_memory_request, mem])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ── helpers ───────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
# Це namespaced ресурс, на який має застосовуватись правило metadata.namespace.
|
|
121
|
+
is_namespaced_kind if {
|
|
122
|
+
is_string(input.kind)
|
|
123
|
+
input.kind != ""
|
|
124
|
+
input.kind != "List"
|
|
125
|
+
input.kind != "Kustomization"
|
|
126
|
+
is_string(input.apiVersion)
|
|
127
|
+
input.apiVersion != ""
|
|
128
|
+
not input.kind in cluster_scoped_kinds
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
deployment_all_containers contains container if {
|
|
132
|
+
some container in object.get(object.get(input.spec.template, "spec", {}), "containers", [])
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
deployment_all_containers contains container if {
|
|
136
|
+
some container in object.get(object.get(input.spec.template, "spec", {}), "initContainers", [])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Канон cpu='0.02' — рядок (точно "0.02") або число 0.02.
|
|
140
|
+
is_base_canon_cpu(v) if {
|
|
141
|
+
is_string(v)
|
|
142
|
+
trim_space(v) == base_cpu_request
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
is_base_canon_cpu(v) if {
|
|
146
|
+
is_number(v)
|
|
147
|
+
v == 0.02
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Канон memory='128Mi' (суфікс Mi без урахування регістру).
|
|
151
|
+
is_base_canon_memory(v) if {
|
|
152
|
+
is_string(v)
|
|
153
|
+
lower(trim_space(v)) == "128mi"
|
|
154
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Тести для `k8s.base_manifest`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/k8s/base_manifest --namespace k8s.base_manifest
|
|
3
|
+
package k8s.base_manifest_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.k8s.base_manifest
|
|
8
|
+
|
|
9
|
+
# ── metadata.namespace required ─────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
test_deny_namespaced_kind_without_metadata if {
|
|
12
|
+
count(base_manifest.deny) > 0 with input as {
|
|
13
|
+
"apiVersion": "v1",
|
|
14
|
+
"kind": "ConfigMap",
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test_deny_namespaced_kind_empty_namespace if {
|
|
19
|
+
count(base_manifest.deny) > 0 with input as {
|
|
20
|
+
"apiVersion": "v1",
|
|
21
|
+
"kind": "ConfigMap",
|
|
22
|
+
"metadata": {"name": "cm", "namespace": ""},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test_allow_cluster_scoped_kind_without_namespace if {
|
|
27
|
+
count(base_manifest.deny) == 0 with input as {
|
|
28
|
+
"apiVersion": "v1",
|
|
29
|
+
"kind": "Namespace",
|
|
30
|
+
"metadata": {"name": "dev"},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test_allow_namespaced_kind_with_namespace if {
|
|
35
|
+
count(base_manifest.deny) == 0 with input as {
|
|
36
|
+
"apiVersion": "v1",
|
|
37
|
+
"kind": "ConfigMap",
|
|
38
|
+
"metadata": {"name": "cm", "namespace": "dev"},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ── base canon resources ─────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
test_deny_deployment_cpu_not_base_canon if {
|
|
45
|
+
count(base_manifest.deny) > 0 with input as {
|
|
46
|
+
"apiVersion": "apps/v1",
|
|
47
|
+
"kind": "Deployment",
|
|
48
|
+
"metadata": {"name": "api", "namespace": "dev"},
|
|
49
|
+
"spec": {"template": {"spec": {"containers": [{
|
|
50
|
+
"name": "main",
|
|
51
|
+
"image": "x",
|
|
52
|
+
"resources": {"requests": {"cpu": "100m", "memory": "128Mi"}},
|
|
53
|
+
}]}}},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test_deny_deployment_memory_not_base_canon if {
|
|
58
|
+
count(base_manifest.deny) > 0 with input as {
|
|
59
|
+
"apiVersion": "apps/v1",
|
|
60
|
+
"kind": "Deployment",
|
|
61
|
+
"metadata": {"name": "api", "namespace": "dev"},
|
|
62
|
+
"spec": {"template": {"spec": {"containers": [{
|
|
63
|
+
"name": "main",
|
|
64
|
+
"image": "x",
|
|
65
|
+
"resources": {"requests": {"cpu": "0.02", "memory": "256Mi"}},
|
|
66
|
+
}]}}},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test_allow_deployment_with_base_canon_string if {
|
|
71
|
+
count(base_manifest.deny) == 0 with input as {
|
|
72
|
+
"apiVersion": "apps/v1",
|
|
73
|
+
"kind": "Deployment",
|
|
74
|
+
"metadata": {"name": "api", "namespace": "dev"},
|
|
75
|
+
"spec": {"template": {"spec": {"containers": [{
|
|
76
|
+
"name": "main",
|
|
77
|
+
"image": "x",
|
|
78
|
+
"resources": {"requests": {"cpu": "0.02", "memory": "128Mi"}},
|
|
79
|
+
}]}}},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test_allow_deployment_with_base_canon_number_cpu if {
|
|
84
|
+
count(base_manifest.deny) == 0 with input as {
|
|
85
|
+
"apiVersion": "apps/v1",
|
|
86
|
+
"kind": "Deployment",
|
|
87
|
+
"metadata": {"name": "api", "namespace": "dev"},
|
|
88
|
+
"spec": {"template": {"spec": {"containers": [{
|
|
89
|
+
"name": "main",
|
|
90
|
+
"image": "x",
|
|
91
|
+
"resources": {"requests": {"cpu": 0.02, "memory": "128mi"}},
|
|
92
|
+
}]}}},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Порт пер-документних перевірок для Gateway API і HealthCheckPolicy з
|
|
2
|
+
# `npm/scripts/check-k8s.mjs` (k8s.mdc).
|
|
3
|
+
#
|
|
4
|
+
# Запуск (локально, по одному файлу або по дереву):
|
|
5
|
+
# conftest test path/to/manifest.yaml -p npm/policy/k8s/gateway \
|
|
6
|
+
# --namespace k8s.gateway
|
|
7
|
+
#
|
|
8
|
+
# Перевіряє:
|
|
9
|
+
# - HealthCheckPolicy (`networking.gke.io/v1`): `spec.targetRef.name` має
|
|
10
|
+
# закінчуватися на `-hl` (headless Service);
|
|
11
|
+
# - HTTPRoute / GRPCRoute / TCPRoute / TLSRoute / UDPRoute (`gateway.networking.k8s.io/*`):
|
|
12
|
+
# backendRef до Service має ім'я з суфіксом `-hl` (headless);
|
|
13
|
+
# - той самий маршрут: backendRef з полем `namespace`, що збігається з
|
|
14
|
+
# `metadata.namespace` маршруту, — заборонено (надлишкове поле, ламається при
|
|
15
|
+
# overlay-перенесеннях).
|
|
16
|
+
#
|
|
17
|
+
# JS authoritative (`check-k8s.mjs` — функції `failIfGatewayRouteUsesNonHeadlessService`,
|
|
18
|
+
# `healthCheckPolicyTargetRefHeadlessServiceViolation`,
|
|
19
|
+
# `collectGatewayApiRouteBackendServiceNames`,
|
|
20
|
+
# `collectGatewayApiRouteBackendRefsWithRedundantNamespace`); ця Rego — швидкий
|
|
21
|
+
# gate для одиничного маніфеста.
|
|
22
|
+
#
|
|
23
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
24
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
25
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
26
|
+
package k8s.gateway
|
|
27
|
+
|
|
28
|
+
import rego.v1
|
|
29
|
+
|
|
30
|
+
# Kind-и маршрутів Gateway API, у `spec` яких шукаємо backendRefs.
|
|
31
|
+
route_kinds := {"HTTPRoute", "GRPCRoute", "TCPRoute", "TLSRoute", "UDPRoute"}
|
|
32
|
+
|
|
33
|
+
# Префікс apiVersion стандартних ресурсів Gateway API.
|
|
34
|
+
api_group_prefix := "gateway.networking.k8s.io/"
|
|
35
|
+
|
|
36
|
+
# Суфікс metadata.name для headless-сервісу (k8s.mdc).
|
|
37
|
+
svc_hl_name_suffix := "-hl"
|
|
38
|
+
|
|
39
|
+
hcp_target_ref_template := concat(" ", [
|
|
40
|
+
"HealthCheckPolicy: spec.targetRef.name має закінчуватись на %q",
|
|
41
|
+
"(зараз: %q) (k8s.mdc)",
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
route_backend_hl_template := concat(" ", [
|
|
45
|
+
"Gateway API %s: backendRef до Service має вказувати headless-сервіс",
|
|
46
|
+
"з суфіксом %q (зараз: %q) (k8s.mdc)",
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
route_backend_redundant_ns_template := concat(" ", [
|
|
50
|
+
"Gateway API %s: backendRef %q має namespace %q,",
|
|
51
|
+
"що збігається з metadata.namespace маршруту —",
|
|
52
|
+
"прибери поле namespace (k8s.mdc)",
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
# ── deny: HealthCheckPolicy — targetRef.name має закінчуватись на `-hl` ───
|
|
56
|
+
|
|
57
|
+
deny contains msg if {
|
|
58
|
+
is_health_check_policy
|
|
59
|
+
target_ref_kind_is_service
|
|
60
|
+
name := object.get(object.get(input.spec, "targetRef", {}), "name", "")
|
|
61
|
+
name != ""
|
|
62
|
+
not endswith(name, svc_hl_name_suffix)
|
|
63
|
+
msg := sprintf(hcp_target_ref_template, [svc_hl_name_suffix, name])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
deny contains msg if {
|
|
67
|
+
is_health_check_policy
|
|
68
|
+
not has_target_ref
|
|
69
|
+
msg := "HealthCheckPolicy: відсутній spec.targetRef (k8s.mdc)"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# ── deny: Gateway API маршрут — backendRef має суфікс `-hl` ──────────────
|
|
73
|
+
|
|
74
|
+
deny contains msg if {
|
|
75
|
+
is_gateway_api_route
|
|
76
|
+
some backend_name in route_service_backend_names
|
|
77
|
+
not endswith(backend_name, svc_hl_name_suffix)
|
|
78
|
+
msg := sprintf(route_backend_hl_template, [input.kind, svc_hl_name_suffix, backend_name])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ── deny: Gateway API маршрут — backendRef з redundant namespace ─────────
|
|
82
|
+
|
|
83
|
+
deny contains msg if {
|
|
84
|
+
is_gateway_api_route
|
|
85
|
+
route_ns := object.get(input.metadata, "namespace", "")
|
|
86
|
+
route_ns != ""
|
|
87
|
+
some redundant in redundant_namespace_backend_names(route_ns)
|
|
88
|
+
msg := sprintf(route_backend_redundant_ns_template, [input.kind, redundant, route_ns])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# ── helpers ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
is_health_check_policy if {
|
|
94
|
+
input.kind == "HealthCheckPolicy"
|
|
95
|
+
startswith(object.get(input, "apiVersion", ""), "networking.gke.io/")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# targetRef.kind не задано або дорівнює "Service" — звіряємо суфікс імені.
|
|
99
|
+
target_ref_kind_is_service if {
|
|
100
|
+
target_ref_kind == ""
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
target_ref_kind_is_service if {
|
|
104
|
+
target_ref_kind == "Service"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
target_ref_kind := object.get(object.get(input.spec, "targetRef", {}), "kind", "")
|
|
108
|
+
|
|
109
|
+
has_target_ref if {
|
|
110
|
+
object.get(input, "spec", {}).targetRef
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
is_gateway_api_route if {
|
|
114
|
+
startswith(object.get(input, "apiVersion", ""), api_group_prefix)
|
|
115
|
+
input.kind in route_kinds
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Усі імена backend-ів у `spec` що виглядають як backendRef до Service: вузол
|
|
119
|
+
# має `name` (string) і `port` (number); якщо поле `kind`/`group` явне — лише
|
|
120
|
+
# `Service`/`core` (без явного group теж приймаємо).
|
|
121
|
+
route_service_backend_names contains node.name if {
|
|
122
|
+
walk(object.get(input, "spec", {}), [_, node])
|
|
123
|
+
is_gateway_api_backend_ref_to_service(node)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Тільки ті backendRef, у яких `namespace` збігається з namespace маршруту.
|
|
127
|
+
redundant_namespace_backend_names(route_ns) := {node.name |
|
|
128
|
+
walk(object.get(input, "spec", {}), [_, node])
|
|
129
|
+
is_gateway_api_backend_ref_to_service(node)
|
|
130
|
+
node.namespace == route_ns
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
is_gateway_api_backend_ref_to_service(obj) if {
|
|
134
|
+
is_object(obj)
|
|
135
|
+
is_string(obj.name)
|
|
136
|
+
is_number(obj.port)
|
|
137
|
+
kind_ok(obj)
|
|
138
|
+
group_ok(obj)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Якщо `kind` не вказано — приймаємо як Service (Gateway API дефолт).
|
|
142
|
+
kind_ok(obj) if not obj.kind
|
|
143
|
+
|
|
144
|
+
kind_ok(obj) if obj.kind == "Service"
|
|
145
|
+
|
|
146
|
+
# Якщо `group` не вказано / порожній / "core" — приймаємо як Service.
|
|
147
|
+
group_ok(obj) if not obj.group
|
|
148
|
+
|
|
149
|
+
group_ok(obj) if obj.group == ""
|
|
150
|
+
|
|
151
|
+
group_ok(obj) if obj.group == "core"
|