@nitra/cursor 3.12.0 → 3.14.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.14.0] - 2026-06-02
4
+
5
+ ### Changed
6
+
7
+ - k8s hasura_configmap: base/dev ConfigMap Hasura-Deployment має містити HASURA_GRAPHQL_ENABLED_APIS="metadata,graphql,pgdump" (точний рядок). Кожен не-base overlay (k8s/<env>/, env≠base/dev), що успадковує Hasura-base, має у kustomization.yaml перевизначати цей ключ до "metadata,graphql" (без pgdump) патчем JSON6902/Strategic Merge на ConfigMap — нова cross-file перевірка validateHasuraOverlayEnabledApisOverride
8
+
9
+ ## [3.13.0] - 2026-06-02
10
+
11
+ ### Changed
12
+
13
+ - k8s hasura_configmap: розширено перелік обов'язкових env у ConfigMap Hasura-Deployment — додатково до HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS="true" тепер вимагаються HASURA_GRAPHQL_ENABLE_RELAY="false", HASURA_GRAPHQL_ENABLE_TELEMETRY="false", HASURA_GRAPHQL_ENABLED_LOG_TYPES="startup,http-log" (точний рядок) і HASURA_GRAPHQL_DISABLE_EVENTING (ключ обов'язковий, значення довільне, за замовчуванням "true")
14
+
3
15
  ## [3.12.0] - 2026-06-02
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.12.0",
3
+ "version": "3.14.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -2349,47 +2349,18 @@ export function isHasuraDeploymentManifest(manifest) {
2349
2349
  }
2350
2350
 
2351
2351
  /**
2352
- * Обов'язковий ключ у **`data`** ConfigMap для Hasura-Deployment (узгоджено з k8s.mdc).
2353
- */
2354
- export const HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY = 'HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS'
2355
-
2356
- /**
2357
- * Чи значення поля `data.<key>` у ConfigMap читається як логічне **true**.
2358
- * ConfigMap у Kubernetes тримає значення як рядки, але в YAML часто пишуть без лапок —
2359
- * тому приймаємо і булевий **true**, і рядок **"true"** (без регістрової залежності).
2360
- * @param {unknown} v значення з `data[HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY]`
2361
- * @returns {boolean} true, якщо значення — `true` або рядок `'true'`
2362
- */
2363
- function isConfigMapValueTrue(v) {
2364
- if (v === true) return true
2365
- if (typeof v === 'string' && v.trim().toLowerCase() === 'true') return true
2366
- return false
2367
- }
2368
-
2369
- /**
2370
- * Чи порушує ConfigMap вимогу щодо **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: "true"`** (k8s.mdc).
2371
- * Перевірка застосовна, коли в тому ж каталозі є Hasura-Deployment (див. `isHasuraDeploymentManifest`).
2372
- * @param {unknown} manifest корінь YAML-документа ConfigMap
2373
- * @returns {string | null} текст порушення або null, якщо не ConfigMap / ключ є і значення `true`
2374
- */
2375
- export function hasuraConfigMapRemoteSchemaPermissionsViolation(manifest) {
2376
- if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
2377
- return null
2378
- const rec = /** @type {Record<string, unknown>} */ (manifest)
2379
- if (rec.kind !== 'ConfigMap') return null
2380
- const data = rec.data
2381
- if (data === null || data === undefined || typeof data !== 'object' || Array.isArray(data)) {
2382
- return `data.${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}: додай ключ зі значенням "true" (Deployment з hasura/graphql-engine — див. k8s.mdc)`
2383
- }
2384
- const d = /** @type {Record<string, unknown>} */ (data)
2385
- if (!Object.hasOwn(d, HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY)) {
2386
- return `data.${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}: додай ключ зі значенням "true" (Deployment з hasura/graphql-engine — див. k8s.mdc)`
2387
- }
2388
- if (!isConfigMapValueTrue(d[HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY])) {
2389
- return `data.${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}: значення має бути "true" (зараз: ${JSON.stringify(d[HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY])}) (див. k8s.mdc)`
2390
- }
2391
- return null
2392
- }
2352
+ * Обов'язкові env-ключі у **`data`** ConfigMap для Hasura-Deployment (узгоджено з
2353
+ * rego-пакетом `k8s.hasura_configmap` та k8s.mdc). Лише для людиночитного pass-повідомлення —
2354
+ * авторитетна пер-документна валідація (наявність ключів і значення) живе в rego.
2355
+ */
2356
+ export const HASURA_REQUIRED_ENV_KEYS = [
2357
+ 'HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS',
2358
+ 'HASURA_GRAPHQL_ENABLE_RELAY',
2359
+ 'HASURA_GRAPHQL_ENABLE_TELEMETRY',
2360
+ 'HASURA_GRAPHQL_ENABLED_LOG_TYPES',
2361
+ 'HASURA_GRAPHQL_ENABLED_APIS',
2362
+ 'HASURA_GRAPHQL_DISABLE_EVENTING'
2363
+ ]
2393
2364
 
2394
2365
  const K8S_YAML_EXT_RE = /\.ya?ml$/iu
2395
2366
 
@@ -3676,7 +3647,10 @@ async function validateConfigMapNameMatchesDeployment(root, yamlFilesAbs, fail,
3676
3647
 
3677
3648
  /**
3678
3649
  * Для кожного `k8s/base/configmap.yaml`, у каталозі якого поруч є Hasura-Deployment,
3679
- * вимагає у `data` ключ **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`** (k8s.mdc).
3650
+ * вимагає у `data` обов'язкові env-ключі (`HASURA_REQUIRED_ENV_KEYS`) з очікуваними
3651
+ * значеннями (`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS="true"`,
3652
+ * `HASURA_GRAPHQL_ENABLE_RELAY="false"`, `HASURA_GRAPHQL_ENABLE_TELEMETRY="false"`,
3653
+ * `HASURA_GRAPHQL_ENABLED_LOG_TYPES="startup,http-log"`, `HASURA_GRAPHQL_DISABLE_EVENTING` — будь-яке) (k8s.mdc).
3680
3654
  * @param {string} root корінь репозиторію
3681
3655
  * @param {string[]} yamlFilesAbs yaml під k8s
3682
3656
  * @param {(msg: string) => void} fail callback при помилці
@@ -3688,8 +3662,8 @@ async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs
3688
3662
  return CONFIGMAP_BASE_PATH_RE.test(`/${rel}`) || rel === 'k8s/base/configmap.yaml'
3689
3663
  })
3690
3664
  // JS gating: відберемо ConfigMap-файли, у каталозі яких поруч є Hasura-Deployment.
3691
- // Per-document валідація `data.HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS == "true"`
3692
- // — у rego-пакеті `k8s.hasura_configmap`.
3665
+ // Per-document валідація обов'язкових `data.HASURA_GRAPHQL_*` env — у rego-пакеті
3666
+ // `k8s.hasura_configmap`.
3693
3667
  const paired = []
3694
3668
  for (const cmAbs of cmFiles) {
3695
3669
  const deployment = await findDeploymentDocInDir(dirname(cmAbs))
@@ -3708,7 +3682,7 @@ async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs
3708
3682
  fail(`${rel}: ${v.message}`)
3709
3683
  }
3710
3684
  if (violations.length === 0) {
3711
- passFn(`Hasura-ConfigMap (${paired.length}) відповідає ${HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY}="true" (rego)`)
3685
+ passFn(`Hasura-ConfigMap (${paired.length}) містить обов'язкові env [${HASURA_REQUIRED_ENV_KEYS.join(', ')}] (rego)`)
3712
3686
  }
3713
3687
  }
3714
3688
 
@@ -4956,6 +4930,130 @@ async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, pass
4956
4930
  }
4957
4931
  }
4958
4932
 
4933
+ /** Очікуване `HASURA_GRAPHQL_ENABLED_APIS` у non-base/dev overlay (без `pgdump` — він лише для base/dev). */
4934
+ const HASURA_OVERLAY_ENABLED_APIS = 'metadata,graphql'
4935
+
4936
+ /** JSON-Pointer ключа `HASURA_GRAPHQL_ENABLED_APIS` у `data` ConfigMap (для JSON6902-патчів). */
4937
+ const HASURA_ENABLED_APIS_DATA_POINTER = '/data/HASURA_GRAPHQL_ENABLED_APIS'
4938
+
4939
+ /**
4940
+ * Чи дерево kustomization (`resources` / `bases` / `components` / `crds`, рекурсивно) містить
4941
+ * **Hasura-Deployment** у шарі base (образ `hasura/graphql-engine`). Маркер того, що overlay успадковує
4942
+ * Hasura-ConfigMap з pgdump-значенням `ENABLED_APIS` і має його перевизначити.
4943
+ * @param {string} kustAbs kustomization.yaml
4944
+ * @param {string} rootNorm нормалізований корінь репо
4945
+ * @returns {Promise<boolean>} true, якщо в успадкованому base є Hasura-Deployment
4946
+ */
4947
+ export async function kustomizationTreeHasHasuraDeployment(kustAbs, rootNorm) {
4948
+ const visited = new Set()
4949
+ const paths = await collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visited)
4950
+ const rootResolved = resolve(rootNorm)
4951
+ for (const abs of paths) {
4952
+ const rel = (relative(rootResolved, abs) || '').replaceAll('\\', '/')
4953
+ if (!isK8sYamlUnderBaseDirectory(rel)) continue
4954
+ const roots = await readK8sYamlDocumentRootsForInventory(abs)
4955
+ if (roots.some(o => isHasuraDeploymentManifest(o))) return true
4956
+ }
4957
+ return false
4958
+ }
4959
+
4960
+ /**
4961
+ * Значення, яке inline-`patch` присвоює `data.HASURA_GRAPHQL_ENABLED_APIS`. Підтримка двох форматів:
4962
+ * **JSON6902** (`op` add/replace на `/data/HASURA_GRAPHQL_ENABLED_APIS`) і **Strategic Merge**
4963
+ * (`data.HASURA_GRAPHQL_ENABLED_APIS`). Зовнішні patch-файли (`patches[].path`) не охоплені — Plan B trade-off.
4964
+ * @param {string} patchText вміст поля `patch`
4965
+ * @returns {string | null} присвоєне значення (рядок) або null, якщо patch не чіпає цей ключ
4966
+ */
4967
+ export function enabledApisValueFromPatchText(patchText) {
4968
+ const t = typeof patchText === 'string' ? patchText.trim() : ''
4969
+ if (t === '') return null
4970
+ let parsed
4971
+ try {
4972
+ for (const d of parseAllDocuments(t)) {
4973
+ if (d.errors.length === 0) {
4974
+ parsed = d.toJSON()
4975
+ break
4976
+ }
4977
+ }
4978
+ } catch {
4979
+ return null
4980
+ }
4981
+ if (Array.isArray(parsed)) {
4982
+ for (const item of parsed) {
4983
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
4984
+ const rec = /** @type {Record<string, unknown>} */ (item)
4985
+ const op = typeof rec.op === 'string' ? rec.op.trim().toLowerCase() : ''
4986
+ const path = typeof rec.path === 'string' ? normalizeJsonPatchPath(rec.path) : ''
4987
+ if ((op === 'add' || op === 'replace') && path === HASURA_ENABLED_APIS_DATA_POINTER) {
4988
+ return typeof rec.value === 'string' ? rec.value : JSON.stringify(rec.value)
4989
+ }
4990
+ }
4991
+ return null
4992
+ }
4993
+ if (parsed === null || typeof parsed !== 'object') return null
4994
+ const data = /** @type {Record<string, unknown>} */ (parsed).data
4995
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) return null
4996
+ const d = /** @type {Record<string, unknown>} */ (data)
4997
+ if (!Object.hasOwn(d, 'HASURA_GRAPHQL_ENABLED_APIS')) return null
4998
+ const v = d.HASURA_GRAPHQL_ENABLED_APIS
4999
+ return typeof v === 'string' ? v : JSON.stringify(v)
5000
+ }
5001
+
5002
+ /**
5003
+ * Значення, яке `patches[]` kustomization присвоюють `data.HASURA_GRAPHQL_ENABLED_APIS` на цілі **ConfigMap**.
5004
+ * Повертає значення першого patch-а, що чіпає цей ключ, або null, якщо такого немає.
5005
+ * @param {Record<string, unknown>} kust об'єкт kustomization.yaml
5006
+ * @returns {string | null} присвоєне значення або null
5007
+ */
5008
+ export function hasuraEnabledApisOverrideValue(kust) {
5009
+ const patches = kust.patches
5010
+ if (!Array.isArray(patches)) return null
5011
+ for (const p of patches) {
5012
+ if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
5013
+ const pr = /** @type {Record<string, unknown>} */ (p)
5014
+ if (typeof pr.patch !== 'string') continue
5015
+ if (resolvePatchTargetKind(pr) !== 'ConfigMap') continue
5016
+ const v = enabledApisValueFromPatchText(pr.patch)
5017
+ if (v !== null) return v
5018
+ }
5019
+ return null
5020
+ }
5021
+
5022
+ /**
5023
+ * Для кожного **non-base/dev** overlay `kustomization.yaml`, що успадковує Hasura-base (Deployment з
5024
+ * `hasura/graphql-engine`), вимагає у `patches[]` перевизначення **`data.HASURA_GRAPHQL_ENABLED_APIS`**
5025
+ * до **`"metadata,graphql"`** (pgdump лишається строго для base/dev) (k8s.mdc). `kind: Component`
5026
+ * пропускається (env-неутральне джерело, не overlay).
5027
+ * @param {string} root корінь репозиторію
5028
+ * @param {string[]} yamlFilesAbs yaml під k8s
5029
+ * @param {(msg: string) => void} fail callback при помилці
5030
+ * @param {(msg: string) => void} passFn callback при успіху
5031
+ */
5032
+ async function validateHasuraOverlayEnabledApisOverride(root, yamlFilesAbs, fail, passFn) {
5033
+ const rootNorm = resolve(root)
5034
+ const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
5035
+ for (const kustAbs of kustFiles) {
5036
+ const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
5037
+ const segment = k8sEnvSegmentFromRelPath(rel)
5038
+ if (segment === null || segment === 'base' || segment === 'dev') continue
5039
+ const kust = await readFirstYamlObject(kustAbs)
5040
+ if (kust === null || kust.kind === 'Component') continue
5041
+ if (!(await kustomizationTreeHasHasuraDeployment(kustAbs, rootNorm))) continue
5042
+ const value = hasuraEnabledApisOverrideValue(kust)
5043
+ if (value === HASURA_OVERLAY_ENABLED_APIS) {
5044
+ passFn(`${rel}: overlay '${segment}' перевизначає HASURA_GRAPHQL_ENABLED_APIS="${HASURA_OVERLAY_ENABLED_APIS}" (k8s.mdc)`)
5045
+ } else if (value === null) {
5046
+ fail(
5047
+ `${rel}: overlay '${segment}' має у patches[] перевизначати data.HASURA_GRAPHQL_ENABLED_APIS до "${HASURA_OVERLAY_ENABLED_APIS}" (pgdump лише для base/dev) (k8s.mdc)`
5048
+ )
5049
+ } else {
5050
+ fail(
5051
+ `${rel}: overlay '${segment}' patch data.HASURA_GRAPHQL_ENABLED_APIS має бути "${HASURA_OVERLAY_ENABLED_APIS}" (зараз: ${JSON.stringify(value)}) (k8s.mdc)`
5052
+ )
5053
+ }
5054
+ }
5055
+ }
5056
+
4959
5057
  /**
4960
5058
  * Шукає HPA за `scaleTargetRef.name` серед документів.
4961
5059
  * @param {Record<string, unknown>[]} hpaDocs масив HPA-документів
@@ -6653,5 +6751,7 @@ export async function check(cwd = process.cwd()) {
6653
6751
 
6654
6752
  await validateProdKustomizationOverrides(root, yamlFiles, fail, pass)
6655
6753
 
6754
+ await validateHasuraOverlayEnabledApisOverride(root, yamlFiles, fail, pass)
6755
+
6656
6756
  return reporter.getExitCode()
6657
6757
  }
package/rules/k8s/k8s.mdc CHANGED
@@ -297,11 +297,40 @@ spec:
297
297
 
298
298
  ## ConfigMap для Hasura-Deployment
299
299
 
300
- Якщо в `k8s/base/` поруч із **`configmap.yaml`** є **Deployment** з образом **`hasura/graphql-engine`**, у `data` ConfigMap **обов'язково** має бути ключ **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`**. Точні умови перевірки — **`rules/k8s/fix.mjs`**.
300
+ Якщо в `k8s/base/` поруч із **`configmap.yaml`** є **Deployment** з образом **`hasura/graphql-engine`**, у `data` ConfigMap **обов'язково** мають бути env-ключі:
301
+
302
+ - **`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`** зі значенням **`"true"`**;
303
+ - **`HASURA_GRAPHQL_ENABLE_RELAY`** зі значенням **`"false"`**;
304
+ - **`HASURA_GRAPHQL_ENABLE_TELEMETRY`** зі значенням **`"false"`**;
305
+ - **`HASURA_GRAPHQL_ENABLED_LOG_TYPES`** зі значенням **`"startup,http-log"`** (точний рядок);
306
+ - **`HASURA_GRAPHQL_ENABLED_APIS`** зі значенням **`"metadata,graphql,pgdump"`** (точний рядок) — **значення для base/dev**;
307
+ - **`HASURA_GRAPHQL_DISABLE_EVENTING`** — ключ обов'язковий, значення довільне (за замовчуванням **`"true"`**).
308
+
309
+ Точні умови перевірки — rego-пакет **`k8s.hasura_configmap`** (cross-file прив'язка ConfigMap↔Deployment — у `rules/k8s/js/manifests.mjs`).
301
310
 
302
311
  ```yaml
303
312
  data:
304
313
  HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: 'true'
314
+ HASURA_GRAPHQL_ENABLE_RELAY: 'false'
315
+ HASURA_GRAPHQL_ENABLE_TELEMETRY: 'false'
316
+ HASURA_GRAPHQL_ENABLED_LOG_TYPES: 'startup,http-log'
317
+ HASURA_GRAPHQL_ENABLED_APIS: 'metadata,graphql,pgdump'
318
+ HASURA_GRAPHQL_DISABLE_EVENTING: 'true'
319
+ ```
320
+
321
+ ### `HASURA_GRAPHQL_ENABLED_APIS` поза base/dev
322
+
323
+ `pgdump` дозволено **лише** для **base**/**dev**. Кожен **не-base** overlay (`k8s/<env>/`, де `<env>` ≠ `base`/`dev`), що успадковує Hasura-base, **зобов'язаний** у своєму **`kustomization.yaml`** перевизначити `HASURA_GRAPHQL_ENABLED_APIS` до **`"metadata,graphql"`** (без `pgdump`) — патчем JSON6902 або Strategic Merge на ціль **ConfigMap**. Перевірка — cross-file у `rules/k8s/js/manifests.mjs` (`validateHasuraOverlayEnabledApisOverride`); `kind: Component` пропускається.
324
+
325
+ ```yaml title="k8s/prod/kustomization.yaml"
326
+ patches:
327
+ - target:
328
+ kind: ConfigMap
329
+ name: db-h
330
+ patch: |
331
+ - op: replace
332
+ path: /data/HASURA_GRAPHQL_ENABLED_APIS
333
+ value: metadata,graphql
305
334
  ```
306
335
 
307
336
  ## Kustomize: структура каталогів (`base` / overlays)
@@ -1,18 +1,33 @@
1
1
  # Порт перевірки ConfigMap для Hasura-Deployment з
2
2
  # `npm/scripts/rules/k8s/fix.mjs` (k8s.mdc): у ConfigMap, що сусідствує з
3
- # Hasura-Deployment, у `data` обов'язково має бути ключ
4
- # `HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS` зі значенням `"true"`.
3
+ # Hasura-Deployment, у `data` обов'язково мають бути env-ключі зі списку
4
+ # `required_env` з очікуваними значеннями:
5
+ # "HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS" → "true"
6
+ # "HASURA_GRAPHQL_ENABLE_RELAY" → "false"
7
+ # "HASURA_GRAPHQL_ENABLE_TELEMETRY" → "false"
8
+ # "HASURA_GRAPHQL_ENABLED_LOG_TYPES" → "startup,http-log" (точний рядок)
9
+ # "HASURA_GRAPHQL_ENABLED_APIS" → "metadata,graphql,pgdump" (точний рядок;
10
+ # це значення для base/dev. Не-base overlay-и мають зводити його до "metadata,graphql"
11
+ # патчем у kustomization.yaml — перевіряє JS `validateHasuraOverlayEnabledApisOverride`)
12
+ # "HASURA_GRAPHQL_DISABLE_EVENTING" → null (ключ обов'язковий,
13
+ # значення довільне; за замовчуванням рекомендовано "true")
14
+ #
15
+ # Семантика очікуваного значення у `required_env`:
16
+ # "true" — має читатись як логічне true (boolean true або рядок "true", case-insensitive);
17
+ # "false" — має читатись як логічне false (boolean false або рядок "false", case-insensitive);
18
+ # null — ключ обов'язковий, значення довільне (за замовчуванням "true");
19
+ # інший рядок — значення має точно дорівнювати рядку (exact match).
5
20
  #
6
21
  # Запуск (локально, лише для ConfigMap у каталозі з Hasura-Deployment):
7
22
  # conftest test path/to/k8s/.../configmap.yaml \
8
23
  # -p npm/policy/k8s/hasura_configmap \
9
24
  # --namespace k8s.hasura_configmap
10
25
  #
11
- # Прив'язка ConfigMap-Deployment cross-file — у JS (`rules/k8s/fix.mjs`:
26
+ # Прив'язка ConfigMap-Deployment cross-file — у JS (`rules/k8s/js/manifests.mjs`:
12
27
  # `validateHasuraConfigMapRemoteSchemaPermissions` шукає Hasura-Deployment
13
28
  # у тому ж dir-у і викликає conftest з цією намеспейс лише для відповідних
14
- # ConfigMap-ів). JS authoritative (`hasuraConfigMapRemoteSchemaPermissionsViolation`,
15
- # константа `HASURA_REMOTE_SCHEMA_PERMISSIONS_KEY`).
29
+ # ConfigMap-ів). Rego authoritative для пер-документної валідації; JS лишає
30
+ # лише cross-file orchestration.
16
31
  #
17
32
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
18
33
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
@@ -20,45 +35,83 @@ package k8s.hasura_configmap
20
35
 
21
36
  import rego.v1
22
37
 
23
- # Обов'язковий ключ у `data` (узгоджено з `rules/k8s/fix.mjs`).
24
- required_key := "HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS"
38
+ # Обов'язкові env-ключі у `data` (узгоджено з `rules/k8s/js/manifests.mjs` та k8s.mdc).
39
+ # Значення — очікуваний стан ключа (семантика — у шапці файлу).
40
+ required_env := {
41
+ "HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS": "true",
42
+ "HASURA_GRAPHQL_ENABLE_RELAY": "false",
43
+ "HASURA_GRAPHQL_ENABLE_TELEMETRY": "false",
44
+ "HASURA_GRAPHQL_ENABLED_LOG_TYPES": "startup,http-log",
45
+ "HASURA_GRAPHQL_ENABLED_APIS": "metadata,graphql,pgdump",
46
+ "HASURA_GRAPHQL_DISABLE_EVENTING": null,
47
+ }
48
+
49
+ # Множина "boolean-подібних" очікувань — для них значення читається як логічне,
50
+ # а не звіряється точним рядком.
51
+ bool_expected := {"true", "false"}
52
+
53
+ # Підказка про очікуване значення для повідомлення про відсутній ключ.
54
+ expected_hint(null) := "(значення довільне, за замовчуванням \"true\")"
25
55
 
26
- key_missing_template := concat(" ", [
27
- "data.%s: додай ключ зі значенням \"true\"",
28
- "(Deployment з hasura/graphql-engine — k8s.mdc)",
29
- ])
56
+ expected_hint(expected) := sprintf("зі значенням \"%s\"", [expected]) if is_string(expected)
30
57
 
31
- key_value_wrong_template := concat(" ", ["data.%s: значення має бути \"true\" (зараз: %v) (k8s.mdc)"])
58
+ key_value_wrong_template := concat(" ", ["data.%s: значення має бути \"%s\" (зараз: %v) (k8s.mdc)"])
59
+
60
+ # Ключ відсутній: `data` не об'єкт або в ньому немає обов'язкового ключа.
61
+ deny contains msg if {
62
+ input.kind == "ConfigMap"
63
+ some key, expected in required_env
64
+ not key_present(key)
65
+ msg := sprintf(
66
+ "data.%s: додай ключ %s (Deployment з hasura/graphql-engine — k8s.mdc)",
67
+ [key, expected_hint(expected)],
68
+ )
69
+ }
32
70
 
71
+ # Очікуване "true", а значення не читається як логічне true.
33
72
  deny contains msg if {
34
73
  input.kind == "ConfigMap"
35
- not is_object(object.get(input, "data", null))
36
- msg := sprintf(key_missing_template, [required_key])
74
+ d := object.get(input, "data", null)
75
+ is_object(d)
76
+ some key, expected in required_env
77
+ expected == "true"
78
+ key in object.keys(d)
79
+ not is_value_true(d[key])
80
+ msg := sprintf(key_value_wrong_template, [key, "true", d[key]])
37
81
  }
38
82
 
83
+ # Очікуване "false", а значення не читається як логічне false.
39
84
  deny contains msg if {
40
85
  input.kind == "ConfigMap"
41
86
  d := object.get(input, "data", null)
42
87
  is_object(d)
43
- not key_present(d)
44
- msg := sprintf(key_missing_template, [required_key])
88
+ some key, expected in required_env
89
+ expected == "false"
90
+ key in object.keys(d)
91
+ not is_value_false(d[key])
92
+ msg := sprintf(key_value_wrong_template, [key, "false", d[key]])
45
93
  }
46
94
 
95
+ # Очікуване — точний рядок (не "true"/"false"/null), а значення не збігається.
47
96
  deny contains msg if {
48
97
  input.kind == "ConfigMap"
49
98
  d := object.get(input, "data", null)
50
99
  is_object(d)
51
- key_present(d)
52
- value := d[required_key]
53
- not is_value_true(value)
54
- msg := sprintf(key_value_wrong_template, [required_key, value])
100
+ some key, expected in required_env
101
+ is_string(expected)
102
+ not expected in bool_expected
103
+ key in object.keys(d)
104
+ d[key] != expected
105
+ msg := sprintf(key_value_wrong_template, [key, expected, d[key]])
55
106
  }
56
107
 
57
- key_present(d) if {
58
- required_key in object.keys(d)
108
+ key_present(key) if {
109
+ d := object.get(input, "data", null)
110
+ is_object(d)
111
+ key in object.keys(d)
59
112
  }
60
113
 
61
- # Значення вважається "true", якщо це boolean true або рядок "true"
114
+ # Значення вважається "true"/"false", якщо це відповідний boolean або рядок
62
115
  # (case-insensitive). ConfigMap у Kubernetes тримає рядки, але YAML без лапок
63
116
  # дає boolean — приймаємо обидва варіанти.
64
117
  is_value_true(true)
@@ -67,3 +120,10 @@ is_value_true(v) if {
67
120
  is_string(v)
68
121
  lower(trim_space(v)) == "true"
69
122
  }
123
+
124
+ is_value_false(false)
125
+
126
+ is_value_false(v) if {
127
+ is_string(v)
128
+ lower(trim_space(v)) == "false"
129
+ }