@nitra/cursor 1.10.0 → 1.11.0
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 +34 -1
- package/bin/n-cursor.js +29 -29
- package/package.json +2 -1
- package/rules/abie/js/applies/check.mjs +24 -0
- package/rules/abie/js/env_dns/check.mjs +53 -0
- package/rules/abie/js/firebase_hosting/check.mjs +49 -0
- package/rules/abie/js/hc_pairing/check.mjs +58 -0
- package/rules/abie/js/ua_http_route/check.mjs +86 -0
- package/rules/abie/js/ua_node_selector/check.mjs +65 -0
- package/rules/abie/policy/base_deployment_preem/target.json +10 -0
- package/rules/abie/policy/clean_merged_ignore_branches/target.json +4 -0
- package/rules/abie/policy/health_check_policy/target.json +4 -0
- package/rules/abie/policy/http_route_base/target.json +4 -0
- package/rules/abie/utils/enabled.mjs +35 -0
- package/rules/abie/utils/env-dns.mjs +81 -0
- package/rules/abie/utils/hc-yaml.mjs +27 -0
- package/rules/abie/utils/http-route.mjs +93 -0
- package/rules/abie/utils/k8s-tree.mjs +102 -0
- package/rules/abie/utils/kustomization-patches.mjs +224 -0
- package/rules/abie/utils/overlay-paths.mjs +97 -0
- package/rules/abie/utils/yaml.mjs +72 -0
- package/rules/adr/policy/settings_json/target.json +4 -0
- package/rules/adr/policy/settings_local_json/target.json +4 -0
- package/rules/bun/policy/bunfig/target.json +4 -0
- package/rules/bun/policy/package_json/target.json +4 -0
- package/rules/capacitor/policy/package_json/target.json +4 -0
- package/rules/docker/policy/lint_docker_yml/target.json +4 -0
- package/rules/docker/policy/package_json/target.json +4 -0
- package/rules/hasura/policy/svc_hl/target.json +4 -0
- package/rules/image-avif/policy/package_json/target.json +4 -0
- package/rules/image-compress/policy/package_json/target.json +4 -0
- package/rules/js-bun-db/policy/package_json/target.json +4 -0
- package/rules/js-bun-redis/policy/package_json/target.json +4 -0
- package/rules/js-lint/policy/lint_js_yml/target.json +4 -0
- package/rules/js-lint/policy/package_json/target.json +4 -0
- package/rules/js-mssql/policy/package_json/target.json +4 -0
- package/rules/js-run/policy/configmap/target.json +4 -0
- package/rules/js-run/policy/package_json/target.json +4 -0
- package/rules/k8s/policy/base_kustomization/target.json +4 -0
- package/rules/k8s/policy/base_manifest/target.json +10 -0
- package/rules/k8s/policy/gateway/target.json +4 -0
- package/rules/k8s/policy/hpa_pdb/target.json +4 -0
- package/rules/k8s/policy/kustomization/target.json +4 -0
- package/rules/k8s/policy/manifest/target.json +4 -0
- package/rules/k8s/policy/svc_hl_yaml/target.json +4 -0
- package/rules/k8s/policy/svc_yaml/target.json +4 -0
- package/rules/npm-module/policy/emit_types_config/target.json +4 -0
- package/rules/npm-module/policy/npm_package_json/target.json +4 -0
- package/rules/npm-module/policy/npm_publish_yml/target.json +4 -0
- package/rules/npm-module/policy/root_package_json/target.json +4 -0
- package/rules/php/policy/lint_php_yml/target.json +4 -0
- package/rules/php/policy/package_json/target.json +4 -0
- package/rules/rego/js/applies/check.mjs +54 -0
- package/rules/rego/policy/package_json/target.json +5 -0
- package/rules/rego/policy/vscode_extensions/target.json +5 -0
- package/rules/rego/policy/vscode_settings/target.json +5 -0
- package/rules/style-lint/policy/lint_style_yml/target.json +4 -0
- package/rules/style-lint/policy/package_json/target.json +4 -0
- package/rules/style-lint/policy/vscode_extensions/target.json +4 -0
- package/rules/style-lint/policy/vscode_settings/target.json +4 -0
- package/rules/text/policy/cspell/target.json +4 -0
- package/rules/text/policy/markdownlint/target.json +4 -0
- package/rules/text/policy/oxfmtrc/target.json +4 -0
- package/rules/text/policy/package_json/target.json +4 -0
- package/rules/text/policy/vscode_extensions/target.json +4 -0
- package/rules/text/policy/vscode_settings/target.json +4 -0
- package/rules/vue/policy/package_json/target.json +4 -0
- package/schemas/target.json +58 -0
- package/scripts/lint-conftest.mjs +65 -414
- package/scripts/utils/discover-checkable-rules.mjs +123 -0
- package/scripts/utils/resolve-target-files.mjs +109 -0
- package/scripts/utils/run-rule.mjs +131 -0
- package/rules/abie/js/check.mjs +0 -1152
- package/rules/rego/js/check.mjs +0 -106
package/rules/abie/js/check.mjs
DELETED
|
@@ -1,1152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Перевіряє відповідність проєкту правилу abie.mdc (проєкти AbInBev Efes).
|
|
3
|
-
*
|
|
4
|
-
* Застосовується лише якщо у **`.n-cursor.json`** у масиві **`rules`** є **`abie`** — інакше вихід **0**
|
|
5
|
-
* без перевірок (щоб не суперечити типовому **ga.mdc** з **`ignore_branches: main,dev`**).
|
|
6
|
-
*
|
|
7
|
-
* **Гілки:** у **`.github/workflows/clean-merged-branch.yml`** у кроці з
|
|
8
|
-
* **`phpdocker-io/github-actions-delete-abandoned-branches`** у **`with.ignore_branches`** мають бути
|
|
9
|
-
* **dev** та **ua** (разом з іншими гілками, якщо потрібно).
|
|
10
|
-
*
|
|
11
|
-
* **Firebase Hosting:** у **підкаталогах першого рівня** (безпосередні діти кореня репозиторію; `node_modules` / `.git` пропускаються) не має бути
|
|
12
|
-
* **`.firebaserc`**, **`firebase.json`** та каталогу **`.firebase/`**; у **самому** корені репозиторію ці імена не перевіряються.
|
|
13
|
-
*
|
|
14
|
-
* **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
|
|
15
|
-
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
16
|
-
* як у abie.mdc, **`requestPath`** — непорожній шлях від кореня (рядок, що починається з **`/`**: **`/healthz`**, **`/IsAlive`**, **`/api/live`** тощо), порт **8080**, **`targetRef`** на **headless Service** (ім'я з суфіксом **`-hl`**):
|
|
17
|
-
* якщо **`metadata.name`** уже закінчується на **`-hl`**, **`targetRef.name`** має збігатися з ним; інакше **`targetRef.name`** = **`${metadata.name}-hl`**.
|
|
18
|
-
* Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc**.
|
|
19
|
-
*
|
|
20
|
-
* **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
|
|
21
|
-
* у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** далі підміняє селектор.
|
|
22
|
-
*
|
|
23
|
-
* **nodeSelector (overlay ua):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua/kustomization.yaml`** цього пакета — inline patch на **`kind: Deployment`**
|
|
24
|
-
* з **`path: /spec/template/spec/nodeSelector`** та **`preem: false`**.
|
|
25
|
-
* Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
|
|
26
|
-
*
|
|
27
|
-
* **HTTPRoute (overlay ua):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
|
|
28
|
-
* — тоді в **`ua/kustomization.yaml`** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
29
|
-
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua**, також дозволені префікси **`ua-*`**).
|
|
30
|
-
* **HTTPRoute (base / dev):** у маніфесті **HTTPRoute** у шляху з сегментом **`base`** (наприклад **`…/k8s/base/hr.yaml`**) у **`spec.hostnames`** дозволені лише **`aiml.live`**, **`*.aiml.live`** та інші піддомени **aiml.live** (канонічно порівняння без урахування регістру).
|
|
31
|
-
* **Спільні бекенди (`auth-run-hl`, `file-link-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** (шлях не містить **`k8s/ua/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
|
|
32
|
-
* у patch overlay **ua** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
|
|
33
|
-
* Вибір **`op`** — **k8s.mdc**.
|
|
34
|
-
*
|
|
35
|
-
* **env→cluster DNS:** abie живе у двох GKE-кластерах (dev / ua), тож DNS-суфікс і namespace-префікс у будь-якому
|
|
36
|
-
* **внутрішньокластерному** URL виду `http://<svc>.<ns>.svc.<dns>` мають відповідати імені env-файла. Скануються всі `*.env` файли,
|
|
37
|
-
* basename яких збігається з `dev.env` / `ua.env` (опційно з провідною крапкою — `.dev.env` тощо). Для кожного знайденого
|
|
38
|
-
* internal URL у файлі (не лише `HASURA_GRAPHQL_ENDPOINT`, а й KVCMS, auth-run, file-link тощо) валідатор `validateAbieEnvInternalUrls`
|
|
39
|
-
* вимагає: для `dev.env` — DNS `abie-dev.internal` і namespace починається з `dev-`; для `ua.env` — `abie-ua.internal` + `ua-`.
|
|
40
|
-
* Файл `.env` без імені (локальний для розробника) виключено зі сканування — як і у `check-hasura.mjs`.
|
|
41
|
-
*/
|
|
42
|
-
import { existsSync } from 'node:fs'
|
|
43
|
-
import { readdir, readFile } from 'node:fs/promises'
|
|
44
|
-
import { basename, dirname, join, relative } from 'node:path'
|
|
45
|
-
|
|
46
|
-
import { parseAllDocuments } from 'yaml'
|
|
47
|
-
|
|
48
|
-
import { pathHasK8sSegment } from '../../k8s/js/check.mjs'
|
|
49
|
-
import { createCheckReporter } from '../../../scripts/utils/check-reporter.mjs'
|
|
50
|
-
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
51
|
-
import { runConftestBatch } from '../../../scripts/utils/run-conftest-batch.mjs'
|
|
52
|
-
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
53
|
-
|
|
54
|
-
const CONFIG_FILE = '.n-cursor.json'
|
|
55
|
-
|
|
56
|
-
/** Каталоги-діти в корені, які пропускаються при скануванні на артефакти Firebase Hosting (abie). */
|
|
57
|
-
const ABIE_FIREBASE_HOSTING_SCAN_SKIP_TOP_DIR_NAMES = new Set(['.git', 'node_modules'])
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов'язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
|
|
61
|
-
* Експорт для споживачів / тестів.
|
|
62
|
-
*/
|
|
63
|
-
export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'file-link-hl'])
|
|
64
|
-
|
|
65
|
-
const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NAMES)
|
|
66
|
-
|
|
67
|
-
/** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
|
|
68
|
-
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
69
|
-
|
|
70
|
-
/** Кореневий домен **`spec.hostnames`** для **HTTPRoute** у **`…/k8s/base/…`** (середовище dev, abie.mdc). */
|
|
71
|
-
export const ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT = 'aiml.live'
|
|
72
|
-
|
|
73
|
-
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
74
|
-
const LINE_SPLIT_RE = /\r?\n/u
|
|
75
|
-
const UA_KUSTOMIZATION_PATH_RE = /(^|\/)ua\/kustomization\.yaml$/u
|
|
76
|
-
const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/ua\/kustomization\.yaml$/u
|
|
77
|
-
const BASE_SEGMENT_RE = /(^|\/)base\//u
|
|
78
|
-
const YAML_EXTENSION_RE = /\.ya?ml$/iu
|
|
79
|
-
const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
|
|
80
|
-
const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
81
|
-
const TRAILING_SLASH_RE = /\/$/u
|
|
82
|
-
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
83
|
-
// Overlay namespaces: allow `ua` and `ua-*` (e.g. ua-b2b).
|
|
84
|
-
const PATCH_PARENT_REF_NS_UA_RE =
|
|
85
|
-
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Регекс basename env-файлу abie: `dev.env` / `ua.env`, опційно з провідною крапкою (`.dev.env` тощо).
|
|
89
|
-
* Файл рівно `.env` (без імені) — виключення з правила: локальний файл розробника, `check-abie` його не сканує
|
|
90
|
-
* (так само як `check-hasura`, див. `isEnvFile`).
|
|
91
|
-
*/
|
|
92
|
-
const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua)\.env$/u
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Глобальний регекс кластерного internal URL у тексті env-файлу.
|
|
96
|
-
* Використовується з `String.prototype.matchAll`, тому має флаг `g`.
|
|
97
|
-
* Допустимий DNS-формат — `<cluster>.internal` (GKE).
|
|
98
|
-
* Порт необов'язковий — у KVCMS-конфігах інколи лежить URL без порту (8080 додається сервісом за замовчуванням).
|
|
99
|
-
*/
|
|
100
|
-
const ABIE_INTERNAL_URL_GLOBAL_RE =
|
|
101
|
-
/\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.([a-z0-9][a-z0-9-]*\.internal)(?::\d+)?(?:\/[^\s"'`]*)?/giu
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Очікуваний кластерний DNS-суфікс і namespace-префікс для кожного env-файлу abie.
|
|
105
|
-
* `dev` / `ua` живуть у двох GKE-кластерах з власним `<cluster>.internal`.
|
|
106
|
-
*/
|
|
107
|
-
const ABIE_ENV_CLUSTER_DNS_MAP = Object.freeze({
|
|
108
|
-
dev: Object.freeze({ clusterDns: 'abie-dev.internal', namespacePrefix: 'dev-' }),
|
|
109
|
-
ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Дістає ім'я env (`dev` / `ua`) з basename env-файлу abie.
|
|
114
|
-
* Для не-abie env-файлів (наприклад `production.env`, `.env` без імені) повертає `null`.
|
|
115
|
-
* @param {string} basenameOfEnvFile basename файла (без шляху)
|
|
116
|
-
* @returns {('dev' | 'ua') | null} ім'я env або `null`
|
|
117
|
-
*/
|
|
118
|
-
export function abieEnvNameFromBasename(basenameOfEnvFile) {
|
|
119
|
-
const m = basenameOfEnvFile.match(ABIE_ENV_FILE_BASENAME_RE)
|
|
120
|
-
return m ? /** @type {'dev' | 'ua'} */ (m[1]) : null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Сканує вміст env-файлу abie і повертає помилки невідповідності кластерного DNS / namespace
|
|
125
|
-
* для кожного знайденого internal URL. URL шукається глобально (`matchAll`), тож одне й те саме
|
|
126
|
-
* порушення в кількох змінних дасть стільки ж окремих помилок.
|
|
127
|
-
* @param {string} content вміст env-файлу (UTF-8)
|
|
128
|
-
* @param {'dev' | 'ua'} envName ім'я env, отримане з `abieEnvNameFromBasename`
|
|
129
|
-
* @returns {string[]} порожній масив, якщо все OK; інакше — список повідомлень про порушення
|
|
130
|
-
*/
|
|
131
|
-
export function validateAbieEnvInternalUrls(content, envName) {
|
|
132
|
-
const expected = ABIE_ENV_CLUSTER_DNS_MAP[envName]
|
|
133
|
-
if (!expected) return []
|
|
134
|
-
/** @type {string[]} */
|
|
135
|
-
const errors = []
|
|
136
|
-
for (const match of content.matchAll(ABIE_INTERNAL_URL_GLOBAL_RE)) {
|
|
137
|
-
const [fullUrl, , namespace, clusterDns] = match
|
|
138
|
-
if (clusterDns !== expected.clusterDns) {
|
|
139
|
-
errors.push(
|
|
140
|
-
`${fullUrl}: кластерний DNS "${clusterDns}" не відповідає env "${envName}" (очікується "${expected.clusterDns}")`
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
if (!namespace.startsWith(expected.namespacePrefix)) {
|
|
144
|
-
errors.push(
|
|
145
|
-
`${fullUrl}: namespace "${namespace}" не починається з "${expected.namespacePrefix}" (env "${envName}")`
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return errors
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім'ям файлу) — специфіка abie overlay.
|
|
154
|
-
* @param {string} rel шлях від кореня репозиторію
|
|
155
|
-
* @returns {boolean} true, якщо це `…/ua/kustomization.yaml`
|
|
156
|
-
*/
|
|
157
|
-
export function isUaKustomizationPath(rel) {
|
|
158
|
-
const norm = rel.replaceAll('\\', '/')
|
|
159
|
-
return UA_KUSTOMIZATION_PATH_RE.test(norm)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/ua/kustomization.yaml`**.
|
|
164
|
-
* @param {string} root корінь репозиторію
|
|
165
|
-
* @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
|
|
166
|
-
* @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua
|
|
167
|
-
*/
|
|
168
|
-
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
169
|
-
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
170
|
-
const m = rel.match(OVERLAY_PACKAGE_DIR_RE)
|
|
171
|
-
return m ? join(root, m[1]) : null
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
|
|
176
|
-
* @param {string} root корінь репозиторію
|
|
177
|
-
* @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
|
|
178
|
-
* @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
|
|
179
|
-
*/
|
|
180
|
-
export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
181
|
-
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
182
|
-
if (!pkg) {
|
|
183
|
-
return false
|
|
184
|
-
}
|
|
185
|
-
return (
|
|
186
|
-
existsSync(join(pkg, 'vite.config.js')) ||
|
|
187
|
-
existsSync(join(pkg, 'vite.config.mjs')) ||
|
|
188
|
-
existsSync(join(pkg, 'vite.config.ts'))
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Чи в дереві **k8s** того ж пакета, що й overlay **ua**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
|
|
194
|
-
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
195
|
-
* @param {string} root корінь репозиторію
|
|
196
|
-
* @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
|
|
197
|
-
* @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
|
|
198
|
-
*/
|
|
199
|
-
export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
|
|
200
|
-
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
201
|
-
if (!pkg) {
|
|
202
|
-
return false
|
|
203
|
-
}
|
|
204
|
-
const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
|
|
205
|
-
for (const dir of deploymentDirs) {
|
|
206
|
-
const norm = dir.replaceAll('\\', '/')
|
|
207
|
-
if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) {
|
|
208
|
-
return true
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return false
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
|
|
216
|
-
* @param {string} rel шлях від кореня репозиторію
|
|
217
|
-
* @returns {boolean} true, якщо в шляху є **`/base/`**
|
|
218
|
-
*/
|
|
219
|
-
export function isAbieK8sBaseYamlPath(rel) {
|
|
220
|
-
const norm = rel.replaceAll('\\', '/')
|
|
221
|
-
return BASE_SEGMENT_RE.test(norm)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Per-document валідація hostnames у `…/k8s/.../base/.../*.yaml` HTTPRoute
|
|
225
|
-
// (Plan B: Rego-authoritative) — повністю в `npm/policy/abie/http_route_base/`.
|
|
226
|
-
// Per-document валідація `nodeSelector.preem` для Deployment у base — у
|
|
227
|
-
// `npm/policy/abie/base_deployment_preem/`. JS у `check-abie.mjs` робить лише
|
|
228
|
-
// path-фільтрацію + батч-виклик conftest через `runConftestBatch`.
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Чи увімкнено правило **abie** у конфігу репозиторію.
|
|
232
|
-
* @param {string} root корінь репозиторію (cwd)
|
|
233
|
-
* @returns {Promise<boolean>} true, якщо **rules** містить **abie**
|
|
234
|
-
*/
|
|
235
|
-
export async function isAbieRuleEnabled(root) {
|
|
236
|
-
const p = join(root, CONFIG_FILE)
|
|
237
|
-
if (!existsSync(p)) {
|
|
238
|
-
return false
|
|
239
|
-
}
|
|
240
|
-
let raw
|
|
241
|
-
try {
|
|
242
|
-
raw = await readFile(p, 'utf8')
|
|
243
|
-
} catch {
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
246
|
-
let cfg
|
|
247
|
-
try {
|
|
248
|
-
cfg = JSON.parse(raw)
|
|
249
|
-
} catch {
|
|
250
|
-
return false
|
|
251
|
-
}
|
|
252
|
-
const rules = cfg?.rules
|
|
253
|
-
if (!Array.isArray(rules)) {
|
|
254
|
-
return false
|
|
255
|
-
}
|
|
256
|
-
return rules.some(r => String(r).trim().toLowerCase() === 'abie')
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Per-document валідація `clean-merged-branch.yml` (with.ignore_branches з
|
|
260
|
-
// dev/ua) делегована rego-пакету `abie.clean_merged_ignore_branches`
|
|
261
|
-
// (`npm/policy/abie/clean_merged_ignore_branches/`). JS викликає
|
|
262
|
-
// `runConftestBatch` у `checkCleanMergedBranch`.
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Збирає абсолютні шляхи до **.yaml** / **.yml** під деревом, де є сегмент **k8s**.
|
|
266
|
-
* @param {string} root корінь репозиторію
|
|
267
|
-
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
268
|
-
* @returns {Promise<string[]>} відсортовані шляхи
|
|
269
|
-
*/
|
|
270
|
-
async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
271
|
-
/** @type {string[]} */
|
|
272
|
-
const out = []
|
|
273
|
-
await walkDir(
|
|
274
|
-
root,
|
|
275
|
-
p => {
|
|
276
|
-
const rel = relative(root, p).replaceAll('\\', '/')
|
|
277
|
-
// `.github/` належить `ga.mdc`; check-abie не зачіпає workflow-файли.
|
|
278
|
-
if (rel.startsWith('.github/')) {
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
if (!pathHasK8sSegment(p, root)) {
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
if (!YAML_EXTENSION_RE.test(p)) {
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
out.push(p)
|
|
288
|
-
},
|
|
289
|
-
ignorePaths
|
|
290
|
-
)
|
|
291
|
-
return [...out].toSorted((a, b) => a.localeCompare(b))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Чи документ — **Deployment**.
|
|
296
|
-
* @param {unknown} obj корінь YAML-документа
|
|
297
|
-
* @returns {boolean} true, якщо **kind** документа — **Deployment**
|
|
298
|
-
*/
|
|
299
|
-
function isDeploymentDoc(obj) {
|
|
300
|
-
return (
|
|
301
|
-
obj !== null &&
|
|
302
|
-
typeof obj === 'object' &&
|
|
303
|
-
!Array.isArray(obj) &&
|
|
304
|
-
/** @type {Record<string, unknown>} */ (obj).kind === 'Deployment'
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Директорії, де є хоча б один **Deployment** у файлах **k8s**.
|
|
310
|
-
* @param {string} root корінь cwd
|
|
311
|
-
* @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
|
|
312
|
-
* @param {(msg: string) => void} fail реєстрація помилки парсингу
|
|
313
|
-
* @returns {Promise<Set<string>>} абсолютні шляхи директорій
|
|
314
|
-
*/
|
|
315
|
-
async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
316
|
-
/** @type {Set<string>} */
|
|
317
|
-
const dirs = new Set()
|
|
318
|
-
for (const abs of yamlAbs) {
|
|
319
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
320
|
-
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
321
|
-
if (docs) {
|
|
322
|
-
for (const doc of docs) {
|
|
323
|
-
if (doc.errors.length === 0 && isDeploymentDoc(doc.toJSON())) {
|
|
324
|
-
dirs.add(dirname(abs))
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return dirs
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Для кожного **Deployment** у YAML під **`k8s`** з шляхом **`…/base/…`** вимагає **`spec.template.spec.nodeSelector.preem: true`** (abie.mdc).
|
|
334
|
-
*
|
|
335
|
-
* Per-document валідація делегована у rego-пакет **`abie.base_deployment_preem`**
|
|
336
|
-
* (`npm/policy/abie/base_deployment_preem/`) — JS лише фільтрує файли за path-патерном `base/` і батчем спавнить conftest.
|
|
337
|
-
* @param {string} root корінь репозиторію
|
|
338
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
339
|
-
* @param {(msg: string) => void} fail callback
|
|
340
|
-
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
341
|
-
* @returns {void}
|
|
342
|
-
*/
|
|
343
|
-
function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
|
|
344
|
-
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
345
|
-
if (baseFiles.length === 0) {
|
|
346
|
-
passFn('Немає файлів у шляхах …/base/… — перевірку preem у base пропущено')
|
|
347
|
-
return
|
|
348
|
-
}
|
|
349
|
-
const violations = runConftestBatch({
|
|
350
|
-
policyDirRel: 'abie/base_deployment_preem',
|
|
351
|
-
namespace: 'abie.base_deployment_preem',
|
|
352
|
-
files: baseFiles
|
|
353
|
-
})
|
|
354
|
-
for (const v of violations) {
|
|
355
|
-
const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
|
|
356
|
-
fail(`${rel}: ${v.message}`)
|
|
357
|
-
}
|
|
358
|
-
if (violations.length === 0) {
|
|
359
|
-
passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc (rego)')
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Прибирає BOM на початку файлу.
|
|
365
|
-
* @param {string} s вміст
|
|
366
|
-
* @returns {string} той самий рядок без BOM (U+FEFF) на початку
|
|
367
|
-
*/
|
|
368
|
-
function stripBom(s) {
|
|
369
|
-
return s.startsWith('') ? s.slice(1) : s
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Зчитує та парсить YAML-документи з файлу.
|
|
374
|
-
* При помилці читання викликає `failFn` і повертає `null`.
|
|
375
|
-
* При помилці парсингу викликає `failFn` і повертає `null`.
|
|
376
|
-
* Автоматично видаляє BOM та modeline (перший рядок з `$schema`).
|
|
377
|
-
* @param {string} abs абсолютний шлях до файлу
|
|
378
|
-
* @param {string} rel відносний шлях (для повідомлень)
|
|
379
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
380
|
-
* @returns {Promise<import('yaml').Document[] | null>} масив документів або null при помилці
|
|
381
|
-
*/
|
|
382
|
-
async function readAndParseYamlDocs(abs, rel, failFn) {
|
|
383
|
-
let raw
|
|
384
|
-
try {
|
|
385
|
-
raw = await readFile(abs, 'utf8')
|
|
386
|
-
} catch (error) {
|
|
387
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
388
|
-
failFn(`${rel}: не вдалося прочитати (${msg})`)
|
|
389
|
-
return null
|
|
390
|
-
}
|
|
391
|
-
const body = stripBom(raw)
|
|
392
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
393
|
-
const first = lines[0] ?? ''
|
|
394
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
395
|
-
try {
|
|
396
|
-
return parseAllDocuments(rest)
|
|
397
|
-
} catch (error) {
|
|
398
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
399
|
-
failFn(`${rel}: YAML (${msg})`)
|
|
400
|
-
return null
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* No-op fail handler для функцій, що повертають null/порожній масив при помилці.
|
|
406
|
-
* @param {string} _msg повідомлення ігнорується
|
|
407
|
-
*/
|
|
408
|
-
const silentFail = _msg => {
|
|
409
|
-
/* silent — пошкоджені файли ловить check-k8s */
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
|
|
414
|
-
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
415
|
-
* @param {string} patchText поле **patch** у kustomization
|
|
416
|
-
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
417
|
-
*/
|
|
418
|
-
function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
419
|
-
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
420
|
-
return false
|
|
421
|
-
}
|
|
422
|
-
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
|
|
423
|
-
return false
|
|
424
|
-
}
|
|
425
|
-
if (!PATCH_PREEM_FALSE_RE.test(patchText)) {
|
|
426
|
-
return false
|
|
427
|
-
}
|
|
428
|
-
return true
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
|
|
433
|
-
* @param {unknown} p елемент масиву **patches**
|
|
434
|
-
* @param {'ua'} mode який overlay перевіряти
|
|
435
|
-
* @returns {boolean} true, якщо patch відповідає abie для **mode**
|
|
436
|
-
*/
|
|
437
|
-
function inlineKustomizationPatchMatchesAbieMode(p, mode) {
|
|
438
|
-
if (p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
439
|
-
return false
|
|
440
|
-
}
|
|
441
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
442
|
-
const target = pr.target
|
|
443
|
-
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
444
|
-
return false
|
|
445
|
-
}
|
|
446
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
447
|
-
if (tg.kind !== 'Deployment') {
|
|
448
|
-
return false
|
|
449
|
-
}
|
|
450
|
-
const patchStr = pr.patch
|
|
451
|
-
if (typeof patchStr !== 'string') {
|
|
452
|
-
return false
|
|
453
|
-
}
|
|
454
|
-
if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
|
|
455
|
-
return true
|
|
456
|
-
}
|
|
457
|
-
return false
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
|
|
462
|
-
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
463
|
-
* @param {'ua'} mode який overlay перевіряти
|
|
464
|
-
* @returns {boolean} true, якщо знайдено відповідний patch
|
|
465
|
-
*/
|
|
466
|
-
function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
467
|
-
if (doc.errors.length > 0) {
|
|
468
|
-
return false
|
|
469
|
-
}
|
|
470
|
-
const root = doc.toJSON()
|
|
471
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
472
|
-
return false
|
|
473
|
-
}
|
|
474
|
-
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
475
|
-
if (rec.kind !== 'Kustomization') {
|
|
476
|
-
return false
|
|
477
|
-
}
|
|
478
|
-
const patches = rec.patches
|
|
479
|
-
if (!Array.isArray(patches)) {
|
|
480
|
-
return false
|
|
481
|
-
}
|
|
482
|
-
for (const p of patches) {
|
|
483
|
-
if (inlineKustomizationPatchMatchesAbieMode(p, mode)) {
|
|
484
|
-
return true
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return false
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (overlay **ua**).
|
|
492
|
-
* @param {string} raw повний текст файлу
|
|
493
|
-
* @param {'ua'} mode який overlay перевіряти
|
|
494
|
-
* @returns {boolean} true, якщо знайдено відповідний patch
|
|
495
|
-
*/
|
|
496
|
-
export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
497
|
-
const body = stripBom(raw)
|
|
498
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
499
|
-
const first = lines[0] ?? ''
|
|
500
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
501
|
-
/** @type {import('yaml').Document[]} */
|
|
502
|
-
let docs
|
|
503
|
-
try {
|
|
504
|
-
docs = parseAllDocuments(rest)
|
|
505
|
-
} catch {
|
|
506
|
-
return false
|
|
507
|
-
}
|
|
508
|
-
for (const doc of docs) {
|
|
509
|
-
if (kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode)) {
|
|
510
|
-
return true
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return false
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревом **`ua/`** (base-шар abie).
|
|
518
|
-
* @param {string} relFromRoot відносний шлях від кореня
|
|
519
|
-
* @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
|
|
520
|
-
* @returns {boolean} `true`, якщо шлях належить до base-шару abie
|
|
521
|
-
*/
|
|
522
|
-
export function isK8sYamlInAbiePackageExcludingUaOverlay(relFromRoot, pkgRelFromRoot) {
|
|
523
|
-
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
524
|
-
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
|
|
525
|
-
const prefix = `${pkg}/k8s/`
|
|
526
|
-
if (!normRel.startsWith(prefix)) {
|
|
527
|
-
return false
|
|
528
|
-
}
|
|
529
|
-
const after = normRel.slice(prefix.length)
|
|
530
|
-
return !after.startsWith('ua/')
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Перевіряє один backendRef на відповідність abie.mdc.
|
|
535
|
-
* @param {unknown} br параметр br
|
|
536
|
-
* @param {string} rel відносний шлях (для повідомлень)
|
|
537
|
-
* @param {string[]} errors масив для запису помилок
|
|
538
|
-
* @returns {number} 1 якщо знайдено shared backend, 0 інакше
|
|
539
|
-
*/
|
|
540
|
-
function checkSharedBackendRef(br, rel, errors) {
|
|
541
|
-
if (br === null || typeof br !== 'object' || Array.isArray(br)) return 0
|
|
542
|
-
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
543
|
-
const name = brRec.name
|
|
544
|
-
if (typeof name !== 'string' || !ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) return 0
|
|
545
|
-
if (typeof brRec.namespace !== 'string' || brRec.namespace !== 'dev') {
|
|
546
|
-
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
547
|
-
}
|
|
548
|
-
return 1
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`file-link-hl`** і порушення **`namespace: dev`**.
|
|
553
|
-
* @param {unknown} obj корінь YAML
|
|
554
|
-
* @param {string} rel відносний шлях (повідомлення)
|
|
555
|
-
* @returns {{ refCount: number, errors: string[] }} кількість посилань і список порушень
|
|
556
|
-
*/
|
|
557
|
-
function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
558
|
-
/** @type {string[]} */
|
|
559
|
-
const errors = []
|
|
560
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return { refCount: 0, errors }
|
|
561
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
562
|
-
if (rec.kind !== 'HTTPRoute') return { refCount: 0, errors }
|
|
563
|
-
const spec = rec.spec
|
|
564
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return { refCount: 0, errors }
|
|
565
|
-
const rules = /** @type {Record<string, unknown>} */ (spec).rules
|
|
566
|
-
if (!Array.isArray(rules)) return { refCount: 0, errors }
|
|
567
|
-
let refCount = 0
|
|
568
|
-
for (const rule of rules) {
|
|
569
|
-
if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
570
|
-
const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
|
|
571
|
-
if (Array.isArray(brs)) {
|
|
572
|
-
for (const br of brs) {
|
|
573
|
-
refCount += checkSharedBackendRef(br, rel, errors)
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return { refCount, errors }
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* З YAML під **k8s** пакета (без overlay **ua**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
|
|
583
|
-
* @param {string} root корінь репозиторію
|
|
584
|
-
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
585
|
-
* @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
|
|
586
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
587
|
-
*/
|
|
588
|
-
export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
|
|
589
|
-
const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
590
|
-
let refCount = 0
|
|
591
|
-
/** @type {string[]} */
|
|
592
|
-
const baseErrors = []
|
|
593
|
-
for (const abs of yamlFilesAbs) {
|
|
594
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
595
|
-
if (isK8sYamlInAbiePackageExcludingUaOverlay(rel, pkgRel)) {
|
|
596
|
-
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
597
|
-
if (docs) {
|
|
598
|
-
for (const doc of docs) {
|
|
599
|
-
if (doc.errors.length === 0) {
|
|
600
|
-
const json = doc.toJSON()
|
|
601
|
-
const st = httpRouteDocSharedCrossNsBackendStats(json, rel)
|
|
602
|
-
refCount += st.refCount
|
|
603
|
-
baseErrors.push(...st.errors)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return { refCount, baseErrors }
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
|
|
614
|
-
* @param {string} combined сукупний текст patch **HTTPRoute**
|
|
615
|
-
* @param {'ua'} mode overlay
|
|
616
|
-
* @returns {number} кількість знайдених патчів namespace
|
|
617
|
-
*/
|
|
618
|
-
function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
|
|
619
|
-
if (mode !== 'ua') return 0
|
|
620
|
-
const re =
|
|
621
|
-
/path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
622
|
-
return [...combined.matchAll(re)].length
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
|
|
626
|
-
const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Витягує текст patch HTTPRoute з елемента patches.
|
|
630
|
-
* @param {unknown} p елемент масиву patches
|
|
631
|
-
* @returns {string | null} текст patch або null
|
|
632
|
-
*/
|
|
633
|
-
function extractHttpRoutePatchString(p) {
|
|
634
|
-
if (p === null || typeof p !== 'object' || Array.isArray(p)) return null
|
|
635
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
636
|
-
const target = pr.target
|
|
637
|
-
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
638
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
639
|
-
if (tg.kind !== 'HTTPRoute' || typeof tg.name !== 'string' || tg.name.trim() === '') return null
|
|
640
|
-
const patchStr = pr.patch
|
|
641
|
-
return typeof patchStr === 'string' && patchStr.trim() !== '' ? patchStr : null
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Збирає тексти inline **patch** для **HTTPRoute** (будь-який непорожній **target.name**) з одного документа **Kustomization**.
|
|
646
|
-
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
647
|
-
* @returns {string[]} непорожні рядки **patch**
|
|
648
|
-
*/
|
|
649
|
-
function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
650
|
-
if (doc.errors.length > 0) return []
|
|
651
|
-
const root = doc.toJSON()
|
|
652
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root)) return []
|
|
653
|
-
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
654
|
-
if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return []
|
|
655
|
-
/** @type {string[]} */
|
|
656
|
-
const out = []
|
|
657
|
-
for (const p of rec.patches) {
|
|
658
|
-
const s = extractHttpRoutePatchString(p)
|
|
659
|
-
if (s !== null) out.push(s)
|
|
660
|
-
}
|
|
661
|
-
return out
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Збирає всі inline **JSON6902**-фрагменти для **HTTPRoute** (непорожній **target.name**) у **kustomization.yaml** (усі документи у файлі).
|
|
666
|
-
* @param {string} raw повний текст файлу
|
|
667
|
-
* @returns {string} текст для **`validateAbieNginxRunHttpRoutePatches`** (може бути порожнім)
|
|
668
|
-
*/
|
|
669
|
-
export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
670
|
-
const body = stripBom(raw)
|
|
671
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
672
|
-
const first = lines[0] ?? ''
|
|
673
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
674
|
-
/** @type {import('yaml').Document[]} */
|
|
675
|
-
let docs
|
|
676
|
-
try {
|
|
677
|
-
docs = parseAllDocuments(rest)
|
|
678
|
-
} catch {
|
|
679
|
-
return ''
|
|
680
|
-
}
|
|
681
|
-
/** @type {string[]} */
|
|
682
|
-
const chunks = []
|
|
683
|
-
for (const doc of docs) {
|
|
684
|
-
chunks.push(...collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc))
|
|
685
|
-
}
|
|
686
|
-
return chunks.join('\n')
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
|
|
691
|
-
* @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
|
|
692
|
-
* @param {'ua'} mode overlay (наразі лише **ua**)
|
|
693
|
-
* @param {string} [_fullKustomizationRaw] збережено для зворотної сумісності API (не використовується)
|
|
694
|
-
* @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
|
|
695
|
-
* @returns {string | null} повідомлення про помилку або **null**
|
|
696
|
-
*/
|
|
697
|
-
export function validateAbieNginxRunHttpRoutePatches(
|
|
698
|
-
combined,
|
|
699
|
-
mode,
|
|
700
|
-
_fullKustomizationRaw,
|
|
701
|
-
sharedCrossNsBackendRefCount = 0
|
|
702
|
-
) {
|
|
703
|
-
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
704
|
-
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}) — abie.mdc`
|
|
705
|
-
}
|
|
706
|
-
if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
|
|
707
|
-
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
708
|
-
}
|
|
709
|
-
const markers = ABIE_UA_HTTPROUTE_HOST_MARKERS
|
|
710
|
-
if (!markers.some(m => combined.includes(m))) {
|
|
711
|
-
return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
712
|
-
}
|
|
713
|
-
if (!PATCH_PARENT_REF_NS_UA_RE.test(combined)) {
|
|
714
|
-
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
715
|
-
}
|
|
716
|
-
const sharedCount =
|
|
717
|
-
typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
|
|
718
|
-
? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
|
|
719
|
-
: 0
|
|
720
|
-
if (sharedCount > 0) {
|
|
721
|
-
const patchHits = countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode)
|
|
722
|
-
if (patchHits < sharedCount) {
|
|
723
|
-
return `HTTPRoute: для backendRefs до спільних сервісів auth-run-hl, file-link-hl очікується ${sharedCount} JSON6902 patch(ів) з path /spec/rules/…/backendRefs/…/namespace та value ${mode} (зараз ${patchHits}) — abie.mdc`
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return null
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua**).
|
|
731
|
-
* @param {string} raw повний текст **kustomization.yaml**
|
|
732
|
-
* @param {'ua'} mode overlay
|
|
733
|
-
* @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
|
|
734
|
-
*/
|
|
735
|
-
export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
736
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
737
|
-
return validateAbieNginxRunHttpRoutePatches(combined, mode, raw) === null
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Per-document валідація HealthCheckPolicy (apiVersion / spec.default.config /
|
|
741
|
-
// httpHealthCheck / targetRef з суфіксом `-hl` exact match) делегована
|
|
742
|
-
// rego-пакету `abie.health_check_policy` (`npm/policy/abie/health_check_policy/`).
|
|
743
|
-
// JS у `checkHcYamlFiles` робить лише modeline-перевірку (`validateAbieHcModeline`)
|
|
744
|
-
// і батч-виклик conftest.
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* JS-частина перевірки hc.yaml — лише modeline (`# yaml-language-server: $schema=…`).
|
|
748
|
-
* Парсинг YAML і структурна валідація HealthCheckPolicy делеговано в rego-пакет
|
|
749
|
-
* **`abie.health_check_policy`** (`npm/policy/abie/health_check_policy/`),
|
|
750
|
-
* викликається з `checkHcYamlFile` через `runConftestBatch`.
|
|
751
|
-
* @param {string} raw вміст файлу
|
|
752
|
-
* @param {string} relPath відносний шлях (для повідомлень)
|
|
753
|
-
* @returns {string | null} null якщо OK, рядок з помилкою
|
|
754
|
-
*/
|
|
755
|
-
export function validateAbieHcModeline(raw, relPath) {
|
|
756
|
-
const body = stripBom(raw)
|
|
757
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
758
|
-
if (lines.length === 0 || lines[0].trim() === '') {
|
|
759
|
-
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
760
|
-
}
|
|
761
|
-
const m = lines[0].match(MODELINE_RE)
|
|
762
|
-
if (!m) return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
763
|
-
if (m[1] !== ABIE_HC_SCHEMA_URL) return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
764
|
-
return null
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Перевіряє одну kustomization.yaml на nodeSelector patch для заданого overlay.
|
|
769
|
-
* @param {string} abs абсолютний шлях до файлу
|
|
770
|
-
* @param {string} rel відносний шлях (для повідомлень)
|
|
771
|
-
* @param {'ua'} mode параметр mode
|
|
772
|
-
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
773
|
-
* @param {string} root корінь репозиторію
|
|
774
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
775
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
776
|
-
* @returns {Promise<boolean>} false якщо виявлено помилку і слід зупинитись
|
|
777
|
-
*/
|
|
778
|
-
async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, root, fail, passFn) {
|
|
779
|
-
if (!abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
780
|
-
passFn(`${rel}: nodeSelector patch (${mode}) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
781
|
-
return true
|
|
782
|
-
}
|
|
783
|
-
let raw
|
|
784
|
-
try {
|
|
785
|
-
raw = await readFile(abs, 'utf8')
|
|
786
|
-
} catch (error) {
|
|
787
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
788
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
789
|
-
return false
|
|
790
|
-
}
|
|
791
|
-
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode)) {
|
|
792
|
-
fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`)
|
|
793
|
-
return false
|
|
794
|
-
}
|
|
795
|
-
passFn(`${rel}: nodeSelector patch (${mode}) відповідає abie.mdc`)
|
|
796
|
-
return true
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Перевіряє наявність патчів nodeSelector для ua overlay у k8s.
|
|
801
|
-
* @param {string} root корінь репозиторію
|
|
802
|
-
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
|
|
803
|
-
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
804
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
805
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
806
|
-
*/
|
|
807
|
-
async function ensureUaAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
808
|
-
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
809
|
-
if (uaAbsList.length === 0) {
|
|
810
|
-
fail(
|
|
811
|
-
'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
|
|
812
|
-
)
|
|
813
|
-
return
|
|
814
|
-
}
|
|
815
|
-
for (const abs of uaAbsList) {
|
|
816
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
817
|
-
const ok = await checkNodeSelectorKustomization(abs, rel, 'ua', deploymentDirs, root, fail, passFn)
|
|
818
|
-
if (!ok) return
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Перевіряє HTTPRoute patch для одного overlay (ua).
|
|
824
|
-
* @param {string} abs абсолютний шлях до kustomization.yaml
|
|
825
|
-
* @param {string} rel відносний шлях (для повідомлень)
|
|
826
|
-
* @param {'ua'} mode overlay
|
|
827
|
-
* @param {string} root корінь репозиторію
|
|
828
|
-
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
|
|
829
|
-
* @param {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} cache кеш аналізу shared backend refs
|
|
830
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
831
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
832
|
-
* @returns {Promise<boolean>} false якщо виявлено помилку і слід зупинитись
|
|
833
|
-
*/
|
|
834
|
-
async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, cache, fail, passFn) {
|
|
835
|
-
if (!abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
836
|
-
passFn(`${rel}: HTTPRoute patch (${mode}) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
837
|
-
return true
|
|
838
|
-
}
|
|
839
|
-
const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
|
|
840
|
-
if (!pkgAbs) {
|
|
841
|
-
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
842
|
-
return false
|
|
843
|
-
}
|
|
844
|
-
let p = cache.get(pkgAbs)
|
|
845
|
-
if (!p) {
|
|
846
|
-
p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs)
|
|
847
|
-
cache.set(pkgAbs, p)
|
|
848
|
-
}
|
|
849
|
-
const sharedAnalysis = await p
|
|
850
|
-
for (const err of sharedAnalysis.baseErrors) {
|
|
851
|
-
fail(err)
|
|
852
|
-
return false
|
|
853
|
-
}
|
|
854
|
-
let raw
|
|
855
|
-
try {
|
|
856
|
-
raw = await readFile(abs, 'utf8')
|
|
857
|
-
} catch (error) {
|
|
858
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
859
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
860
|
-
return false
|
|
861
|
-
}
|
|
862
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
863
|
-
const v = validateAbieNginxRunHttpRoutePatches(combined, mode, raw, sharedAnalysis.refCount)
|
|
864
|
-
if (v !== null) {
|
|
865
|
-
fail(`${rel}: ${v}`)
|
|
866
|
-
return false
|
|
867
|
-
}
|
|
868
|
-
passFn(`${rel}: HTTPRoute patch (${mode}) відповідає abie.mdc`)
|
|
869
|
-
return true
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Для кожного **HTTPRoute** у **`…/k8s/base/…`** з непорожніми **`spec.hostnames`** — лише **aiml.live** та піддомени (abie.mdc).
|
|
874
|
-
* @param {string} root корінь репозиторію
|
|
875
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
876
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
877
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
878
|
-
* @returns {void}
|
|
879
|
-
*/
|
|
880
|
-
function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
|
|
881
|
-
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
882
|
-
if (baseFiles.length === 0) {
|
|
883
|
-
passFn('Немає файлів у шляхах …/k8s/base/… — перевірку HTTPRoute hostnames пропущено')
|
|
884
|
-
return
|
|
885
|
-
}
|
|
886
|
-
// Per-document валідація делегована rego-пакету `abie.http_route_base`
|
|
887
|
-
// (`npm/policy/abie/http_route_base/`) — rego гейтує по `kind == "HTTPRoute"`.
|
|
888
|
-
const violations = runConftestBatch({
|
|
889
|
-
policyDirRel: 'abie/http_route_base',
|
|
890
|
-
namespace: 'abie.http_route_base',
|
|
891
|
-
files: baseFiles
|
|
892
|
-
})
|
|
893
|
-
for (const v of violations) {
|
|
894
|
-
const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
|
|
895
|
-
fail(`${rel}: ${v.message}`)
|
|
896
|
-
}
|
|
897
|
-
if (violations.length === 0) {
|
|
898
|
-
passFn(
|
|
899
|
-
`HTTPRoute у …/k8s/base/…: spec.hostnames відповідають ${ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT} та піддоменам (rego)`
|
|
900
|
-
)
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
|
|
906
|
-
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
907
|
-
* @param {string} root корінь репозиторію
|
|
908
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
909
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
910
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
911
|
-
*/
|
|
912
|
-
async function ensureUaAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
913
|
-
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
914
|
-
const cache = new Map()
|
|
915
|
-
|
|
916
|
-
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
917
|
-
if (uaAbsList.length === 0) {
|
|
918
|
-
passFn(
|
|
919
|
-
'Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
920
|
-
)
|
|
921
|
-
}
|
|
922
|
-
for (const abs of uaAbsList) {
|
|
923
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
924
|
-
const ok = await checkHttpRouteKustomization(abs, rel, 'ua', root, yamlFilesAbs, cache, fail, passFn)
|
|
925
|
-
if (!ok) return
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Перевіряє відсутність артефактів Firebase Hosting у **кожному** **підкаталозі першого рівня** від кореня
|
|
931
|
-
* (не в самому корені репозиторію) — abie.mdc. Каталоги **`.git`** і **`node_modules`** у скануванні пропускаються.
|
|
932
|
-
* @param {string} root корінь репозиторію
|
|
933
|
-
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
934
|
-
* @param {(msg: string) => void} failFn повідомлення про порушення
|
|
935
|
-
* @returns {Promise<void>}
|
|
936
|
-
*/
|
|
937
|
-
async function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
938
|
-
let entries
|
|
939
|
-
try {
|
|
940
|
-
entries = await readdir(root, { withFileTypes: true })
|
|
941
|
-
} catch (error) {
|
|
942
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
943
|
-
failFn(`Не вдалося прочитати ${root} для перевірки Firebase Hosting: ${msg} (abie.mdc)`)
|
|
944
|
-
return
|
|
945
|
-
}
|
|
946
|
-
const topDirs = entries.filter(e => e.isDirectory() && !ABIE_FIREBASE_HOSTING_SCAN_SKIP_TOP_DIR_NAMES.has(e.name))
|
|
947
|
-
let hasViolation = false
|
|
948
|
-
for (const e of topDirs) {
|
|
949
|
-
for (const name of ['.firebaserc', 'firebase.json']) {
|
|
950
|
-
const rel = join(e.name, name).replaceAll('\\', '/')
|
|
951
|
-
if (existsSync(join(root, e.name, name))) {
|
|
952
|
-
failFn(`Знайдено заборонений файл Firebase Hosting: ${rel} — видали його (abie.mdc)`)
|
|
953
|
-
hasViolation = true
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
if (existsSync(join(root, e.name, '.firebase'))) {
|
|
957
|
-
failFn(`Знайдено заборонену директорію: ${e.name}/.firebase/ — видали її (abie.mdc)`)
|
|
958
|
-
hasViolation = true
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
if (hasViolation) {
|
|
962
|
-
return
|
|
963
|
-
}
|
|
964
|
-
passFn('Підкаталоги кореня (1-й рівень, без .git/node_modules): артефактів Firebase Hosting не знайдено (abie.mdc)')
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* Перевіряє clean-merged-branch.yml на ignore_branches.
|
|
969
|
-
* @param {string} root корінь репозиторію
|
|
970
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
971
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
972
|
-
* @returns {void}
|
|
973
|
-
*/
|
|
974
|
-
function checkCleanMergedBranch(root, pass, fail) {
|
|
975
|
-
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
976
|
-
if (!existsSync(cleanMergedPath)) {
|
|
977
|
-
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
978
|
-
return
|
|
979
|
-
}
|
|
980
|
-
// Per-document валідація делегована у rego-пакет `abie.clean_merged_ignore_branches`
|
|
981
|
-
// (`npm/policy/abie/clean_merged_ignore_branches/`). conftest сам читає та парсить YAML.
|
|
982
|
-
const violations = runConftestBatch({
|
|
983
|
-
policyDirRel: 'abie/clean_merged_ignore_branches',
|
|
984
|
-
namespace: 'abie.clean_merged_ignore_branches',
|
|
985
|
-
files: [cleanMergedPath]
|
|
986
|
-
})
|
|
987
|
-
for (const v of violations) fail(v.message)
|
|
988
|
-
if (violations.length === 0) {
|
|
989
|
-
pass('clean-merged-branch.yml: ignore_branches містить dev, ua (rego)')
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Перевіряє hc.yaml у директоріях з Deployment. JS перевіряє modeline, далі
|
|
995
|
-
* один батч conftest для усіх знайдених hc.yaml — структурна валідація HCP
|
|
996
|
-
* делегується rego (`abie.health_check_policy`).
|
|
997
|
-
* @param {string} root корінь репозиторію
|
|
998
|
-
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
999
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1000
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1001
|
-
*/
|
|
1002
|
-
async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
|
|
1003
|
-
/** @type {string[]} файли, які пройшли modeline-check і йдуть у conftest */
|
|
1004
|
-
const hcFilesForRego = []
|
|
1005
|
-
for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
1006
|
-
const hcAbs = join(dir, 'hc.yaml')
|
|
1007
|
-
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
1008
|
-
if (!existsSync(hcAbs)) {
|
|
1009
|
-
fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
|
|
1010
|
-
continue
|
|
1011
|
-
}
|
|
1012
|
-
let hcRaw
|
|
1013
|
-
try {
|
|
1014
|
-
hcRaw = await readFile(hcAbs, 'utf8')
|
|
1015
|
-
} catch (error) {
|
|
1016
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1017
|
-
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
1018
|
-
continue
|
|
1019
|
-
}
|
|
1020
|
-
const modelineErr = validateAbieHcModeline(hcRaw, relHc)
|
|
1021
|
-
if (modelineErr !== null) {
|
|
1022
|
-
fail(modelineErr)
|
|
1023
|
-
continue
|
|
1024
|
-
}
|
|
1025
|
-
hcFilesForRego.push(hcAbs)
|
|
1026
|
-
}
|
|
1027
|
-
if (hcFilesForRego.length === 0) return
|
|
1028
|
-
const violations = runConftestBatch({
|
|
1029
|
-
policyDirRel: 'abie/health_check_policy',
|
|
1030
|
-
namespace: 'abie.health_check_policy',
|
|
1031
|
-
files: hcFilesForRego
|
|
1032
|
-
})
|
|
1033
|
-
for (const v of violations) {
|
|
1034
|
-
const rel = relative(root, v.filename).replaceAll('\\', '/') || v.filename
|
|
1035
|
-
fail(`${rel}: ${v.message}`)
|
|
1036
|
-
}
|
|
1037
|
-
if (violations.length === 0 && hcFilesForRego.length > 0) {
|
|
1038
|
-
pass(`HealthCheckPolicy: ${hcFilesForRego.length} файл(ів) hc.yaml відповідають abie.mdc (rego)`)
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Збирає всі `*.env` файли в дереві (за виключенням `node_modules`, `.git` та інших службових каталогів),
|
|
1044
|
-
* basename яких — abie env-файл (`dev.env` / `ua.env` опційно з провідною крапкою). Файл `.env`
|
|
1045
|
-
* без імені виключається — як і у `check-hasura.mjs`.
|
|
1046
|
-
* @param {string} root корінь репозиторію
|
|
1047
|
-
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
1048
|
-
* @returns {Promise<string[]>} відсортовані абсолютні шляхи env-файлів abie
|
|
1049
|
-
*/
|
|
1050
|
-
async function collectAbieEnvFiles(root, ignorePaths) {
|
|
1051
|
-
/** @type {string[]} */
|
|
1052
|
-
const out = []
|
|
1053
|
-
await walkDir(
|
|
1054
|
-
root,
|
|
1055
|
-
absPath => {
|
|
1056
|
-
if (abieEnvNameFromBasename(basename(absPath)) !== null) {
|
|
1057
|
-
out.push(absPath)
|
|
1058
|
-
}
|
|
1059
|
-
},
|
|
1060
|
-
ignorePaths
|
|
1061
|
-
)
|
|
1062
|
-
return out.toSorted((a, b) => a.localeCompare(b))
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env`) і для кожного знайденого
|
|
1067
|
-
* **внутрішньокластерного** URL (`http://<svc>.<ns>.svc.<dns>`) перевіряє, що DNS-суфікс і namespace-префікс
|
|
1068
|
-
* відповідають середовищу env-файла. Не лише `HASURA_GRAPHQL_ENDPOINT`, а й будь-який сервіс у env (KVCMS,
|
|
1069
|
-
* `auth-run-hl`, `file-link-hl` тощо) мусить мати кластер, що відповідає env: dev → `abie-dev.internal`,
|
|
1070
|
-
* ua → `abie-ua.internal`.
|
|
1071
|
-
* @param {string} root корінь репозиторію
|
|
1072
|
-
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
1073
|
-
* @param {(msg: string) => void} pass успішне повідомлення
|
|
1074
|
-
* @param {(msg: string) => void} fail повідомлення про порушення
|
|
1075
|
-
* @returns {Promise<void>}
|
|
1076
|
-
*/
|
|
1077
|
-
async function ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail) {
|
|
1078
|
-
const envFiles = await collectAbieEnvFiles(root, ignorePaths)
|
|
1079
|
-
if (envFiles.length === 0) {
|
|
1080
|
-
pass('Не знайдено dev.env / ua.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
|
|
1081
|
-
return
|
|
1082
|
-
}
|
|
1083
|
-
for (const abs of envFiles) {
|
|
1084
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1085
|
-
const envName = abieEnvNameFromBasename(basename(abs))
|
|
1086
|
-
if (envName === null) continue
|
|
1087
|
-
let raw
|
|
1088
|
-
try {
|
|
1089
|
-
raw = await readFile(abs, 'utf8')
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1092
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1093
|
-
continue
|
|
1094
|
-
}
|
|
1095
|
-
const errors = validateAbieEnvInternalUrls(raw, envName)
|
|
1096
|
-
if (errors.length === 0) {
|
|
1097
|
-
pass(`${rel}: усі внутрішні URL відповідають env "${envName}" (abie.mdc)`)
|
|
1098
|
-
} else {
|
|
1099
|
-
for (const err of errors) {
|
|
1100
|
-
fail(`${rel}: ${err} (abie.mdc)`)
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
/**
|
|
1107
|
-
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
1108
|
-
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
1109
|
-
*/
|
|
1110
|
-
export async function check() {
|
|
1111
|
-
const reporter = createCheckReporter()
|
|
1112
|
-
const { pass, fail } = reporter
|
|
1113
|
-
|
|
1114
|
-
const root = process.cwd()
|
|
1115
|
-
const enabled = await isAbieRuleEnabled(root)
|
|
1116
|
-
if (!enabled) {
|
|
1117
|
-
pass(`Правило abie не увімкнено в ${CONFIG_FILE} (rules) — перевірку пропущено`)
|
|
1118
|
-
return reporter.getExitCode()
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
1122
|
-
await ensureNoFirebaseHostingArtifacts(root, pass, fail)
|
|
1123
|
-
await checkCleanMergedBranch(root, pass, fail)
|
|
1124
|
-
|
|
1125
|
-
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
1126
|
-
const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
|
|
1127
|
-
const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
|
|
1128
|
-
|
|
1129
|
-
if (deploymentDirs.size > 0) {
|
|
1130
|
-
pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml`)
|
|
1131
|
-
await checkHcYamlFiles(root, deploymentDirs, pass, fail)
|
|
1132
|
-
pass('Є Deployment — перевіряємо base: spec.template.spec.nodeSelector.preem (abie.mdc)')
|
|
1133
|
-
await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail, pass)
|
|
1134
|
-
} else {
|
|
1135
|
-
pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
pass('Перевіряємо HTTPRoute spec.hostnames у …/k8s/base/… (aiml.live, abie.mdc)')
|
|
1139
|
-
await ensureAbieBaseHttpRouteHostnames(root, yamlFiles, fail, pass)
|
|
1140
|
-
|
|
1141
|
-
if (deploymentDirs.size > 0) {
|
|
1142
|
-
pass('Є Deployment — перевіряємо nodeSelector у ua/kustomization (abie.mdc)')
|
|
1143
|
-
await ensureUaAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
|
|
1144
|
-
pass('Є Deployment — перевіряємо HTTPRoute у ua/kustomization (abie.mdc)')
|
|
1145
|
-
await ensureUaAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
pass('Перевіряємо env→cluster DNS у dev.env / ua.env (abie.mdc)')
|
|
1149
|
-
await ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail)
|
|
1150
|
-
|
|
1151
|
-
return reporter.getExitCode()
|
|
1152
|
-
}
|