@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевірка кластерного DNS у abie env-файлах (`*.dev.env`, `*.ua.env`).
|
|
3
|
+
*
|
|
4
|
+
* abie живе у двох GKE-кластерах (`abie-dev.internal`, `abie-ua.internal`); внутрішньокластерні
|
|
5
|
+
* URL у env-файлі мусять відповідати кластеру за іменем файла. `validateAbieEnvInternalUrls`
|
|
6
|
+
* сканує всі URL виду `http://<svc>.<ns>.svc.<dns>` і вимагає коректний `<dns>` + namespace-префікс.
|
|
7
|
+
* Файл `.env` без імені (локальний для розробника) виключено.
|
|
8
|
+
*/
|
|
9
|
+
import { basename } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
12
|
+
|
|
13
|
+
const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua)\.env$/u
|
|
14
|
+
|
|
15
|
+
const ABIE_INTERNAL_URL_GLOBAL_RE =
|
|
16
|
+
/\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.([a-z0-9][a-z0-9-]*\.internal)(?::\d+)?(?:\/[^\s"'`]*)?/giu
|
|
17
|
+
|
|
18
|
+
const ABIE_ENV_CLUSTER_DNS_MAP = Object.freeze({
|
|
19
|
+
dev: Object.freeze({ clusterDns: 'abie-dev.internal', namespacePrefix: 'dev-' }),
|
|
20
|
+
ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Дістає `dev` / `ua` з basename env-файлу abie.
|
|
25
|
+
* Не-abie env-файли (`production.env`, `.env` без імені) → null.
|
|
26
|
+
* @param {string} basenameOfEnvFile
|
|
27
|
+
* @returns {('dev' | 'ua') | null}
|
|
28
|
+
*/
|
|
29
|
+
export function abieEnvNameFromBasename(basenameOfEnvFile) {
|
|
30
|
+
const m = basenameOfEnvFile.match(ABIE_ENV_FILE_BASENAME_RE)
|
|
31
|
+
return m ? /** @type {'dev' | 'ua'} */ (m[1]) : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Сканує вміст env-файла, повертає помилки невідповідності кластерного DNS / namespace
|
|
36
|
+
* для кожного internal URL (один URL у двох змінних = дві окремі помилки).
|
|
37
|
+
* @param {string} content вміст env-файла (UTF-8)
|
|
38
|
+
* @param {'dev' | 'ua'} envName
|
|
39
|
+
* @returns {string[]} порожній масив, якщо все OK
|
|
40
|
+
*/
|
|
41
|
+
export function validateAbieEnvInternalUrls(content, envName) {
|
|
42
|
+
const expected = ABIE_ENV_CLUSTER_DNS_MAP[envName]
|
|
43
|
+
if (!expected) return []
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
const errors = []
|
|
46
|
+
for (const match of content.matchAll(ABIE_INTERNAL_URL_GLOBAL_RE)) {
|
|
47
|
+
const [fullUrl, , namespace, clusterDns] = match
|
|
48
|
+
if (clusterDns !== expected.clusterDns) {
|
|
49
|
+
errors.push(
|
|
50
|
+
`${fullUrl}: кластерний DNS "${clusterDns}" не відповідає env "${envName}" (очікується "${expected.clusterDns}")`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
if (!namespace.startsWith(expected.namespacePrefix)) {
|
|
54
|
+
errors.push(
|
|
55
|
+
`${fullUrl}: namespace "${namespace}" не починається з "${expected.namespacePrefix}" (env "${envName}")`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return errors
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Збирає `*.env` файли, які є abie env (`dev.env`/`ua.env`, опц. з провідною крапкою).
|
|
64
|
+
* @param {string} root корінь репозиторію
|
|
65
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів-виключень
|
|
66
|
+
* @returns {Promise<string[]>}
|
|
67
|
+
*/
|
|
68
|
+
export async function collectAbieEnvFiles(root, ignorePaths) {
|
|
69
|
+
/** @type {string[]} */
|
|
70
|
+
const out = []
|
|
71
|
+
await walkDir(
|
|
72
|
+
root,
|
|
73
|
+
absPath => {
|
|
74
|
+
if (abieEnvNameFromBasename(basename(absPath)) !== null) {
|
|
75
|
+
out.push(absPath)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
ignorePaths
|
|
79
|
+
)
|
|
80
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
81
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Валідація modeline у `hc.yaml` для abie.
|
|
3
|
+
* Per-document структурна валідація `HealthCheckPolicy` живе у
|
|
4
|
+
* `policy/health_check_policy/health_check_policy.rego` (CLI прогонить через target.json).
|
|
5
|
+
*/
|
|
6
|
+
import { LINE_SPLIT_RE, MODELINE_RE, stripBom } from './yaml.mjs'
|
|
7
|
+
|
|
8
|
+
/** Очікуваний URL `$schema` для **hc.yaml** (abie.mdc). */
|
|
9
|
+
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Перевіряє modeline (`# yaml-language-server: $schema=...`) у `hc.yaml`.
|
|
13
|
+
* @param {string} raw вміст файла
|
|
14
|
+
* @param {string} relPath відносний шлях (для повідомлень)
|
|
15
|
+
* @returns {string | null} текст помилки або null, якщо OK
|
|
16
|
+
*/
|
|
17
|
+
export function validateAbieHcModeline(raw, relPath) {
|
|
18
|
+
const body = stripBom(raw)
|
|
19
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
20
|
+
if (lines.length === 0 || lines[0].trim() === '') {
|
|
21
|
+
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
22
|
+
}
|
|
23
|
+
const m = lines[0].match(MODELINE_RE)
|
|
24
|
+
if (!m) return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
25
|
+
if (m[1] !== ABIE_HC_SCHEMA_URL) return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-документна аналітика abie HTTPRoute: підрахунок `backendRefs` до спільних
|
|
3
|
+
* сервісів (`auth-run-hl`, `file-link-hl`) у base-маніфестах пакета (поза overlay `ua`).
|
|
4
|
+
* Використовується ua_http_route-концерном для синхронізації числа patch-ів namespace
|
|
5
|
+
* у overlay із кількістю base-referencе.
|
|
6
|
+
*/
|
|
7
|
+
import { relative } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { isK8sYamlInAbiePackageExcludingUaOverlay } from './overlay-paths.mjs'
|
|
10
|
+
import { readAndParseYamlDocs, silentFail } from './yaml.mjs'
|
|
11
|
+
|
|
12
|
+
export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'file-link-hl'])
|
|
13
|
+
const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NAMES)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Перевіряє один `backendRef`: якщо це спільний `-hl` сервіс, має бути `namespace: dev`.
|
|
17
|
+
* @param {unknown} br
|
|
18
|
+
* @param {string} rel rel-шлях файла
|
|
19
|
+
* @param {string[]} errors мутабельний список помилок
|
|
20
|
+
* @returns {number} 1 — це shared backend, 0 — інакше
|
|
21
|
+
*/
|
|
22
|
+
function checkSharedBackendRef(br, rel, errors) {
|
|
23
|
+
if (br === null || typeof br !== 'object' || Array.isArray(br)) return 0
|
|
24
|
+
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
25
|
+
const name = brRec.name
|
|
26
|
+
if (typeof name !== 'string' || !ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) return 0
|
|
27
|
+
if (typeof brRec.namespace !== 'string' || brRec.namespace !== 'dev') {
|
|
28
|
+
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
29
|
+
}
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Збирає по HTTPRoute-документу кількість посилань на shared backends і порушення namespace.
|
|
35
|
+
* @param {unknown} obj корінь YAML
|
|
36
|
+
* @param {string} rel
|
|
37
|
+
* @returns {{ refCount: number, errors: string[] }}
|
|
38
|
+
*/
|
|
39
|
+
function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
40
|
+
/** @type {string[]} */
|
|
41
|
+
const errors = []
|
|
42
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return { refCount: 0, errors }
|
|
43
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
44
|
+
if (rec.kind !== 'HTTPRoute') return { refCount: 0, errors }
|
|
45
|
+
const spec = rec.spec
|
|
46
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return { refCount: 0, errors }
|
|
47
|
+
const rules = /** @type {Record<string, unknown>} */ (spec).rules
|
|
48
|
+
if (!Array.isArray(rules)) return { refCount: 0, errors }
|
|
49
|
+
let refCount = 0
|
|
50
|
+
for (const rule of rules) {
|
|
51
|
+
if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
52
|
+
const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
|
|
53
|
+
if (Array.isArray(brs)) {
|
|
54
|
+
for (const br of brs) {
|
|
55
|
+
refCount += checkSharedBackendRef(br, rel, errors)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { refCount, errors }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Збирає по yaml-файлах пакета (поза overlay ua) кількість shared-`-hl` `backendRefs`
|
|
65
|
+
* і базові помилки (без `namespace: dev`).
|
|
66
|
+
* @param {string} root корінь репозиторію
|
|
67
|
+
* @param {string} pkgAbs абсолютний шлях каталогу пакета
|
|
68
|
+
* @param {string[]} yamlFilesAbs усі yaml під k8s
|
|
69
|
+
* @returns {Promise<{ refCount: number, baseErrors: string[] }>}
|
|
70
|
+
*/
|
|
71
|
+
export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
|
|
72
|
+
const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
73
|
+
let refCount = 0
|
|
74
|
+
/** @type {string[]} */
|
|
75
|
+
const baseErrors = []
|
|
76
|
+
for (const abs of yamlFilesAbs) {
|
|
77
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
78
|
+
if (isK8sYamlInAbiePackageExcludingUaOverlay(rel, pkgRel)) {
|
|
79
|
+
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
80
|
+
if (docs) {
|
|
81
|
+
for (const doc of docs) {
|
|
82
|
+
if (doc.errors.length === 0) {
|
|
83
|
+
const json = doc.toJSON()
|
|
84
|
+
const st = httpRouteDocSharedCrossNsBackendStats(json, rel)
|
|
85
|
+
refCount += st.refCount
|
|
86
|
+
baseErrors.push(...st.errors)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { refCount, baseErrors }
|
|
93
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Обхід k8s-дерева abie з кешуванням на час одного прогону:
|
|
3
|
+
* - `findK8sYamlFiles(root, ignorePaths)` — yaml/yml файли під сегментом `k8s/`.
|
|
4
|
+
* - `collectDeploymentDirs(root, yamlAbs)` — каталоги, де знайдено `kind: Deployment`.
|
|
5
|
+
*
|
|
6
|
+
* Кеш — module-level singleton, ключований за `(root, ignorePaths)`. Перший виклик
|
|
7
|
+
* платить за обхід; наступні концерни в межах того ж прогону отримують готове.
|
|
8
|
+
* Для тестів — `resetAbieK8sTreeCache()` (інакше withTmpCwd-фікстури злипатимуться).
|
|
9
|
+
*/
|
|
10
|
+
import { dirname, relative } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { pathHasK8sSegment } from '../../k8s/js/check.mjs'
|
|
13
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
14
|
+
import { isDeploymentDoc, readAndParseYamlDocs } from './yaml.mjs'
|
|
15
|
+
|
|
16
|
+
const YAML_EXTENSION_RE = /\.ya?ml$/iu
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, Promise<string[]>>} */
|
|
19
|
+
const yamlCache = new Map()
|
|
20
|
+
/** @type {Map<string, Promise<Set<string>>>} */
|
|
21
|
+
const deploymentCache = new Map()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Скидає кеш — тести мусять викликати між фікстурами.
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
export function resetAbieK8sTreeCache() {
|
|
28
|
+
yamlCache.clear()
|
|
29
|
+
deploymentCache.clear()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Стабільний ключ кешу за (root, ignorePaths).
|
|
34
|
+
* @param {string} root
|
|
35
|
+
* @param {string[]} ignorePaths
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
function cacheKey(root, ignorePaths) {
|
|
39
|
+
return `${root}|${[...ignorePaths].toSorted((a, b) => a.localeCompare(b)).join(':')}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Збирає абсолютні шляхи до `.yaml`/`.yml` під деревом, де є сегмент `k8s/`.
|
|
44
|
+
* Каталог `.github/` свідомо пропускається (належить `ga.mdc`).
|
|
45
|
+
* @param {string} root корінь репозиторію
|
|
46
|
+
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів-виключень
|
|
47
|
+
* @returns {Promise<string[]>}
|
|
48
|
+
*/
|
|
49
|
+
export function findK8sYamlFiles(root, ignorePaths = []) {
|
|
50
|
+
const key = cacheKey(root, ignorePaths)
|
|
51
|
+
const cached = yamlCache.get(key)
|
|
52
|
+
if (cached) return cached
|
|
53
|
+
const promise = (async () => {
|
|
54
|
+
/** @type {string[]} */
|
|
55
|
+
const out = []
|
|
56
|
+
await walkDir(
|
|
57
|
+
root,
|
|
58
|
+
p => {
|
|
59
|
+
const rel = relative(root, p).replaceAll('\\', '/')
|
|
60
|
+
if (rel.startsWith('.github/')) return
|
|
61
|
+
if (!pathHasK8sSegment(p, root)) return
|
|
62
|
+
if (!YAML_EXTENSION_RE.test(p)) return
|
|
63
|
+
out.push(p)
|
|
64
|
+
},
|
|
65
|
+
ignorePaths
|
|
66
|
+
)
|
|
67
|
+
return [...out].toSorted((a, b) => a.localeCompare(b))
|
|
68
|
+
})()
|
|
69
|
+
yamlCache.set(key, promise)
|
|
70
|
+
return promise
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Каталоги, де є хоча б один `kind: Deployment` у YAML під `k8s/`.
|
|
75
|
+
* @param {string} root корінь репозиторію
|
|
76
|
+
* @param {string[]} yamlAbs абсолютні шляхи `.yaml`/`.yml` під `k8s/` (як з `findK8sYamlFiles`)
|
|
77
|
+
* @param {(msg: string) => void} [fail] репортер помилок парсингу (опц.)
|
|
78
|
+
* @returns {Promise<Set<string>>} абсолютні шляхи директорій
|
|
79
|
+
*/
|
|
80
|
+
export function collectDeploymentDirs(root, yamlAbs, fail = () => {}) {
|
|
81
|
+
const key = `${root}|${[...yamlAbs].toSorted((a, b) => a.localeCompare(b)).join(':')}`
|
|
82
|
+
const cached = deploymentCache.get(key)
|
|
83
|
+
if (cached) return cached
|
|
84
|
+
const promise = (async () => {
|
|
85
|
+
/** @type {Set<string>} */
|
|
86
|
+
const dirs = new Set()
|
|
87
|
+
for (const abs of yamlAbs) {
|
|
88
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
89
|
+
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
90
|
+
if (docs) {
|
|
91
|
+
for (const doc of docs) {
|
|
92
|
+
if (doc.errors.length === 0 && isDeploymentDoc(doc.toJSON())) {
|
|
93
|
+
dirs.add(dirname(abs))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return dirs
|
|
99
|
+
})()
|
|
100
|
+
deploymentCache.set(key, promise)
|
|
101
|
+
return promise
|
|
102
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Парсинг inline JSON6902-патчів у abie ua-kustomization:
|
|
3
|
+
* - **nodeSelector** patch на `Deployment` (preem: false);
|
|
4
|
+
* - **HTTPRoute** patch (hostnames, parentRefs namespace, backendRefs namespace).
|
|
5
|
+
*
|
|
6
|
+
* Regex використовуються, бо `patch:` — це YAML-string з вкладеним JSON6902, який ми не парсимо
|
|
7
|
+
* вдруге; підрядки на кшталт `path: /spec/hostnames` і `value: ua` достатньо інформативні.
|
|
8
|
+
*/
|
|
9
|
+
import { parseAllDocuments } from 'yaml'
|
|
10
|
+
|
|
11
|
+
import { LINE_SPLIT_RE, MODELINE_RE, stripBom } from './yaml.mjs'
|
|
12
|
+
|
|
13
|
+
const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
|
|
14
|
+
const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
15
|
+
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
16
|
+
// Overlay namespaces: дозволено `ua` і `ua-*` (наприклад `ua-b2b`).
|
|
17
|
+
const PATCH_PARENT_REF_NS_UA_RE =
|
|
18
|
+
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
19
|
+
|
|
20
|
+
/** Домени `hostnames` для overlay `ua` (підрядки у JSON6902-тексті patch). */
|
|
21
|
+
const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
|
|
22
|
+
|
|
23
|
+
// ── nodeSelector (ua) ─────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Чи patch-рядок містить очікуваний ua nodeSelector (preem: false).
|
|
27
|
+
* @param {string} patchText
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
31
|
+
if (typeof patchText !== 'string' || patchText.trim() === '') return false
|
|
32
|
+
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) return false
|
|
33
|
+
if (!PATCH_PREEM_FALSE_RE.test(patchText)) return false
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Чи один елемент `patches` відповідає abie nodeSelector для `mode`.
|
|
39
|
+
* @param {unknown} p
|
|
40
|
+
* @param {'ua'} mode
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function inlineKustomizationPatchMatchesAbieMode(p, mode) {
|
|
44
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) return false
|
|
45
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
46
|
+
const target = pr.target
|
|
47
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return false
|
|
48
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
49
|
+
if (tg.kind !== 'Deployment') return false
|
|
50
|
+
const patchStr = pr.patch
|
|
51
|
+
if (typeof patchStr !== 'string') return false
|
|
52
|
+
if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) return true
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Чи документ Kustomization містить відповідний inline patch на Deployment.
|
|
58
|
+
* @param {import('yaml').Document} doc
|
|
59
|
+
* @param {'ua'} mode
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
63
|
+
if (doc.errors.length > 0) return false
|
|
64
|
+
const root = doc.toJSON()
|
|
65
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return false
|
|
66
|
+
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
67
|
+
if (rec.kind !== 'Kustomization') return false
|
|
68
|
+
const patches = rec.patches
|
|
69
|
+
if (!Array.isArray(patches)) return false
|
|
70
|
+
for (const p of patches) {
|
|
71
|
+
if (inlineKustomizationPatchMatchesAbieMode(p, mode)) return true
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Чи `kustomization.yaml` містить валідні inline patch для Deployment nodeSelector (ua).
|
|
78
|
+
* @param {string} raw повний текст файла
|
|
79
|
+
* @param {'ua'} mode
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
83
|
+
const body = stripBom(raw)
|
|
84
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
85
|
+
const first = lines[0] ?? ''
|
|
86
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
87
|
+
/** @type {import('yaml').Document[]} */
|
|
88
|
+
let docs
|
|
89
|
+
try {
|
|
90
|
+
docs = parseAllDocuments(rest)
|
|
91
|
+
} catch {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
for (const doc of docs) {
|
|
95
|
+
if (kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode)) return true
|
|
96
|
+
}
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── HTTPRoute (ua) ────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {unknown} p
|
|
104
|
+
* @returns {string | null}
|
|
105
|
+
*/
|
|
106
|
+
function extractHttpRoutePatchString(p) {
|
|
107
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) return null
|
|
108
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
109
|
+
const target = pr.target
|
|
110
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
111
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
112
|
+
if (tg.kind !== 'HTTPRoute' || typeof tg.name !== 'string' || tg.name.trim() === '') return null
|
|
113
|
+
const patchStr = pr.patch
|
|
114
|
+
return typeof patchStr === 'string' && patchStr.trim() !== '' ? patchStr : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Збирає inline `patch`-рядки для HTTPRoute (непорожній `target.name`) з одного Kustomization-документа.
|
|
119
|
+
* @param {import('yaml').Document} doc
|
|
120
|
+
* @returns {string[]}
|
|
121
|
+
*/
|
|
122
|
+
function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
123
|
+
if (doc.errors.length > 0) return []
|
|
124
|
+
const root = doc.toJSON()
|
|
125
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return []
|
|
126
|
+
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
127
|
+
if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return []
|
|
128
|
+
/** @type {string[]} */
|
|
129
|
+
const out = []
|
|
130
|
+
for (const p of rec.patches) {
|
|
131
|
+
const s = extractHttpRoutePatchString(p)
|
|
132
|
+
if (s !== null) out.push(s)
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Збирає всі inline JSON6902-фрагменти HTTPRoute (непорожній `target.name`) у kustomization.yaml.
|
|
139
|
+
* @param {string} raw повний текст файла
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
143
|
+
const body = stripBom(raw)
|
|
144
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
145
|
+
const first = lines[0] ?? ''
|
|
146
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
147
|
+
/** @type {import('yaml').Document[]} */
|
|
148
|
+
let docs
|
|
149
|
+
try {
|
|
150
|
+
docs = parseAllDocuments(rest)
|
|
151
|
+
} catch {
|
|
152
|
+
return ''
|
|
153
|
+
}
|
|
154
|
+
/** @type {string[]} */
|
|
155
|
+
const chunks = []
|
|
156
|
+
for (const doc of docs) {
|
|
157
|
+
chunks.push(...collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc))
|
|
158
|
+
}
|
|
159
|
+
return chunks.join('\n')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Рахує операції JSON6902 з `path: /spec/rules/.../backendRefs/.../namespace` і `value: ua[-…]`.
|
|
164
|
+
* @param {string} combined сукупний текст patch
|
|
165
|
+
* @param {'ua'} mode
|
|
166
|
+
* @returns {number}
|
|
167
|
+
*/
|
|
168
|
+
function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
|
|
169
|
+
if (mode !== 'ua') return 0
|
|
170
|
+
const re =
|
|
171
|
+
/path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
172
|
+
return [...combined.matchAll(re)].length
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Перевіряє сукупний текст patch(ів) HTTPRoute на відповідність abie.mdc.
|
|
177
|
+
* @param {string} combined сукупний текст patch
|
|
178
|
+
* @param {'ua'} mode
|
|
179
|
+
* @param {string} [_fullKustomizationRaw] зберігається для API-сумісності, не використовується
|
|
180
|
+
* @param {number} [sharedCrossNsBackendRefCount] кількість `auth-run-hl`/`file-link-hl` у base HTTPRoute
|
|
181
|
+
* @returns {string | null} повідомлення про помилку або null
|
|
182
|
+
*/
|
|
183
|
+
export function validateAbieNginxRunHttpRoutePatches(
|
|
184
|
+
combined,
|
|
185
|
+
mode,
|
|
186
|
+
_fullKustomizationRaw,
|
|
187
|
+
sharedCrossNsBackendRefCount = 0
|
|
188
|
+
) {
|
|
189
|
+
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
190
|
+
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}) — abie.mdc`
|
|
191
|
+
}
|
|
192
|
+
if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
|
|
193
|
+
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
194
|
+
}
|
|
195
|
+
const markers = ABIE_UA_HTTPROUTE_HOST_MARKERS
|
|
196
|
+
if (!markers.some(m => combined.includes(m))) {
|
|
197
|
+
return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
198
|
+
}
|
|
199
|
+
if (!PATCH_PARENT_REF_NS_UA_RE.test(combined)) {
|
|
200
|
+
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
201
|
+
}
|
|
202
|
+
const sharedCount =
|
|
203
|
+
typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
|
|
204
|
+
? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
|
|
205
|
+
: 0
|
|
206
|
+
if (sharedCount > 0) {
|
|
207
|
+
const patchHits = countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode)
|
|
208
|
+
if (patchHits < sharedCount) {
|
|
209
|
+
return `HTTPRoute: для backendRefs до спільних сервісів auth-run-hl, file-link-hl очікується ${sharedCount} JSON6902 patch(ів) з path /spec/rules/…/backendRefs/…/namespace та value ${mode} (зараз ${patchHits}) — abie.mdc`
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Чи kustomization містить валідні patch для HTTPRoute (ua).
|
|
217
|
+
* @param {string} raw повний текст kustomization.yaml
|
|
218
|
+
* @param {'ua'} mode
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
222
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
223
|
+
return validateAbieNginxRunHttpRoutePatches(combined, mode, raw) === null
|
|
224
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-хелпери для overlay-перевірок abie:
|
|
3
|
+
* - класифікація шляхів (`isUaKustomizationPath`, `isAbieK8sBaseYamlPath`),
|
|
4
|
+
* - вилучення каталогу пакета з overlay-шляху (`abiePackageDirFromK8sOverlay`),
|
|
5
|
+
* - умовний gate для HTTPRoute через наявність `vite.config.*` (`abieOverlayRequiresHttpRouteByVite`),
|
|
6
|
+
* - перевірка наявності `Deployment` у дереві пакета (`abieOverlayK8sTreeHasDeployment`),
|
|
7
|
+
* - чи yaml належить base-шару пакета (`isK8sYamlInAbiePackageExcludingUaOverlay`).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { join, relative } from 'node:path'
|
|
11
|
+
|
|
12
|
+
const UA_KUSTOMIZATION_PATH_RE = /(^|\/)ua\/kustomization\.yaml$/u
|
|
13
|
+
const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/ua\/kustomization\.yaml$/u
|
|
14
|
+
const BASE_SEGMENT_RE = /(^|\/)base\//u
|
|
15
|
+
const TRAILING_SLASH_RE = /\/$/u
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Чи `rel` — це `…/ua/kustomization.yaml` (abie overlay).
|
|
19
|
+
* @param {string} rel посі від кореня репозиторію
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isUaKustomizationPath(rel) {
|
|
23
|
+
const norm = rel.replaceAll('\\', '/')
|
|
24
|
+
return UA_KUSTOMIZATION_PATH_RE.test(norm)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Каталог пакета (батько `k8s/`) для overlay `…/k8s/ua/kustomization.yaml`.
|
|
29
|
+
* @param {string} root корінь репозиторію
|
|
30
|
+
* @param {string} kustomizationAbs абсолютний шлях до ua kustomization
|
|
31
|
+
* @returns {string | null}
|
|
32
|
+
*/
|
|
33
|
+
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
34
|
+
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
35
|
+
const m = rel.match(OVERLAY_PACKAGE_DIR_RE)
|
|
36
|
+
return m ? join(root, m[1]) : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Чи у каталозі пакета (батько `k8s/`) є `vite.config.{js,mjs,ts}` — HTTPRoute-вимога abie
|
|
41
|
+
* застосовується лише до Vite-пакетів.
|
|
42
|
+
* @param {string} root корінь репозиторію
|
|
43
|
+
* @param {string} kustomizationAbs абсолютний шлях до ua kustomization
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
47
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
48
|
+
if (!pkg) return false
|
|
49
|
+
return (
|
|
50
|
+
existsSync(join(pkg, 'vite.config.js')) ||
|
|
51
|
+
existsSync(join(pkg, 'vite.config.mjs')) ||
|
|
52
|
+
existsSync(join(pkg, 'vite.config.ts'))
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Чи у дереві `k8s/` пакета є `Deployment` (за каталогами з `collectDeploymentDirs`).
|
|
58
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги з Deployment
|
|
59
|
+
* @param {string} root корінь репозиторію
|
|
60
|
+
* @param {string} kustomizationAbs абсолютний шлях до ua kustomization
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
|
|
64
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
65
|
+
if (!pkg) return false
|
|
66
|
+
const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
|
|
67
|
+
for (const dir of deploymentDirs) {
|
|
68
|
+
const norm = dir.replaceAll('\\', '/')
|
|
69
|
+
if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) return true
|
|
70
|
+
}
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Чи rel-шлях `…/k8s/base/…` (base-шар abie, не overlay).
|
|
76
|
+
* @param {string} rel
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
export function isAbieK8sBaseYamlPath(rel) {
|
|
80
|
+
const norm = rel.replaceAll('\\', '/')
|
|
81
|
+
return BASE_SEGMENT_RE.test(norm)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Чи yaml належить до `<pkgRel>/k8s/**` поза `ua/` піддеревом (base-шар abie).
|
|
86
|
+
* @param {string} relFromRoot шлях від кореня
|
|
87
|
+
* @param {string} pkgRelFromRoot каталог пакета від кореня
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function isK8sYamlInAbiePackageExcludingUaOverlay(relFromRoot, pkgRelFromRoot) {
|
|
91
|
+
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
92
|
+
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
|
|
93
|
+
const prefix = `${pkg}/k8s/`
|
|
94
|
+
if (!normRel.startsWith(prefix)) return false
|
|
95
|
+
const after = normRel.slice(prefix.length)
|
|
96
|
+
return !after.startsWith('ua/')
|
|
97
|
+
}
|