@nitra/cursor 1.8.221 → 1.8.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.claude-template/npm-CLAUDE.md +4 -0
  2. package/CHANGELOG.md +15 -0
  3. package/bin/auto-rules.md +2 -0
  4. package/mdc/tauri.mdc +20 -0
  5. package/package.json +1 -1
  6. package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
  7. package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
  8. package/policy/k8s/base_manifest/base_manifest.rego +154 -0
  9. package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
  10. package/policy/k8s/gateway/gateway.rego +151 -0
  11. package/policy/k8s/gateway/gateway_test.rego +122 -0
  12. package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
  13. package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
  14. package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
  15. package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
  16. package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
  17. package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
  18. package/policy/k8s/kustomization/kustomization.rego +220 -0
  19. package/policy/k8s/kustomization/kustomization_test.rego +128 -0
  20. package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
  21. package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
  22. package/policy/k8s/manifest/manifest.rego +151 -4
  23. package/policy/k8s/manifest/manifest_test.rego +309 -0
  24. package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
  25. package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
  26. package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
  27. package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
  28. package/scripts/lint-conftest.mjs +73 -1
  29. package/scripts/lint-rego.mjs +19 -4
@@ -21,6 +21,10 @@ npx @nitra/cursor check changelog
21
21
  npx @nitra/cursor check npm-module
22
22
  ```
23
23
 
24
+ ## Перш ніж писати `check-*.mjs`
25
+
26
+ Перед створенням нового `npm/scripts/check-<rule>.mjs` оціни, чи задача лягає на rego-полісі. **Default — Rego**: пер-документні структурні перевірки (kind/apiVersion, поля, форма масивів) пишуться у `npm/policy/<rule>/<name>/<name>.rego` + `_test.rego`. JS — тільки для cross-file resolution, file-system access (`readdir`/`stat`), autofix/rewrite або парсингу до YAML-body. Гібрид (rego як швидкий gate + JS-оркестратор для cross-file) — нормальний патерн; референс — `npm/policy/k8s/*` ↔ `npm/scripts/check-k8s.mjs`. Деталі алгоритму рішення — `.cursor/rules/conftest.mdc` (alwaysApply).
27
+
24
28
  ## Джерело правил
25
29
 
26
30
  - `.cursor/rules/n-changelog.mdc` — правило про CHANGELOG (PR-scoped, для всіх воркспейсів)
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.222] - 2026-05-10
8
+
9
+ ### Added
10
+
11
+ - **k8s / rego-полісі:** розширено `npm/policy/k8s/manifest/manifest.rego` (Deployment cpu+memory у `requests`, Hasura image pin із білим списком тегів, канонічний `topologySpreadConstraints` з мітки `app` самого Deployment). Додано `manifest_test.rego` із вхідними фікстурами; rego тестується через `conftest verify` (опційний крок у `bun run lint-rego`). JS у `check-k8s.mjs` лишається authoritative — нові правила Rego — швидкий gate для одиничного маніфеста.
12
+ - **k8s / нові rego-пакети:** `npm/policy/k8s/gateway/` (Gateway API: backendRef з суфіксом `-hl`, redundant `namespace` у backendRef, HCP `targetRef.name` `-hl`); `npm/policy/k8s/kustomization/` (resources/patches алфавітне сортування, JSON6902 `remove`+`add` на той самий `path`); `npm/policy/k8s/svc_yaml/` (`Service.spec.type: ClusterIP`); `npm/policy/k8s/svc_hl_yaml/` (headless Service з суфіксом `-hl` і `clusterIP: None`); `npm/policy/k8s/base_kustomization/` (обов'язковий `namespace:`); `npm/policy/k8s/base_manifest/` (`metadata.namespace` у base, base-canon `cpu='0.02'`/`memory='128Mi'`); `npm/policy/k8s/kustomize_managed/` (заборона `metadata.namespace` у kustomize-managed файлах); `npm/policy/k8s/hasura_configmap/` (`HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: "true"`); `npm/policy/k8s/hasura_httproute/` (канон 4 правил Hasura: `/ql` Exact + `/ql/` Exact + PathPrefix + WebSocket); `npm/policy/k8s/hpa_pdb/` (структурний gate HPA/PDB: `apiVersion`, `behavior.scaleUp/Down`, `metrics`, `selector.matchLabels`). До кожного пакета додано `*_test.rego` фікстури.
13
+ - **lint-rego:** додано опційний крок `conftest verify` у `npm/scripts/lint-rego.mjs` після `regal lint` для виконання `*_test.rego`. Якщо `conftest` не у PATH — крок мовчки пропускається з install-hint.
14
+
15
+ ### Changed
16
+
17
+ - **lint-conftest:** `npm/scripts/lint-conftest.mjs` — `k8s.manifest` target тепер указує на `policyDir: 'k8s/manifest'` (вужчий policy-tree), додано targets для нових пакетів `k8s.gateway`, `k8s.hpa_pdb`, `k8s.kustomization`, `k8s.svc_yaml`, `k8s.svc_hl_yaml`, `k8s.base_kustomization`, `k8s.base_manifest`. Шляхові регекспи: `K8S_KUSTOMIZATION_PATH_RE`, `K8S_BASE_KUSTOMIZATION_PATH_RE`, `K8S_BASE_MANIFEST_PATH_RE`, `K8S_SVC_YAML_PATH_RE`, `K8S_SVC_HL_YAML_PATH_RE`. Пакети `kustomize_managed`, `hasura_configmap`, `hasura_httproute` потребують cross-file gating з `check-k8s.mjs` і не входять у `lint-conftest` walk-targets.
18
+ - **n-k8s.mdc:** додано розділ «Швидкий gate через conftest (Rego)» зі списком rego-пакетів і опису того, що cross-file логіка (резолюція kustomize-tree, парність svc.yaml/svc-hl.yaml, прив'язка ConfigMap/HTTPRoute до Hasura-Deployment, HPA/PDB by directory, env-залежні межі min/maxReplicas) лишається у `check-k8s.mjs`.
19
+ - **conftest.mdc (alwaysApply):** замість одного абзацу про «пріоритет conftest» — повний алгоритм рішення для нової перевірки: декізія-дерево (single-document → Rego за замовчуванням; cross-file/FS/autofix/text-pre-YAML → JS), workflow «спершу намалюй вхід → rego або гібрид», список «червоних прапорів» (Rego не вміє X — звір зі списком винятків). Мета: робити Rego default-вибором для нових перевірок.
20
+ - **npm/.claude-template/npm-CLAUDE.md:** додано path-scoped нагадування «Перш ніж писати `check-*.mjs`» з посиланням на алгоритм у `conftest.mdc`. Регенеровано `npm/CLAUDE.md`.
21
+
7
22
  ## [1.8.221] - 2026-05-10
8
23
 
9
24
  ### Changed
package/bin/auto-rules.md CHANGED
@@ -48,6 +48,8 @@ rego - якщо в проекті є хоч один rego
48
48
 
49
49
  style-lint - якщо присутній хоч один vue або css файл
50
50
 
51
+ tauri - - якщо в хоч в package.json в секції dependencies присутній пакет @tauri-apps/api
52
+
51
53
  text - завжди
52
54
 
53
55
  vue - якщо присутній хоч один vue файл
package/mdc/tauri.mdc ADDED
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Tauri
3
+ alwaysApply: true
4
+ version: '1.0'
5
+ ---
6
+
7
+
8
+ в файлі .vscode/extensions.json є налаштування для Vue:
9
+
10
+ ```json title=".vscode/extensions.json"
11
+ {
12
+ "recommendations": ["tauri-apps.tauri-vscode",
13
+ "rust-lang.rust-analyzer"]
14
+ }
15
+ ```
16
+
17
+
18
+ ## Перевірка
19
+
20
+ `npx @nitra/cursor check tauri`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.221",
3
+ "version": "1.8.222",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,40 @@
1
+ # Порт перевірки `k8s/base/kustomization.yaml` з `npm/scripts/check-k8s.mjs`
2
+ # (k8s.mdc): у base-kustomization обов'язково має бути непорожнє поле
3
+ # `namespace:`.
4
+ #
5
+ # Запуск (локально, лише для одного `k8s/base/kustomization.yaml`):
6
+ # conftest test path/to/k8s/base/kustomization.yaml \
7
+ # -p npm/policy/k8s/base_kustomization \
8
+ # --namespace k8s.base_kustomization
9
+ #
10
+ # JS authoritative (`check-k8s.mjs`: `baseKustomizationNamespaceViolation`,
11
+ # `isBaseKustomizationPath` для відбору файла, `ensureBaseKustomizationHasNamespace`
12
+ # як оркестратор).
13
+ #
14
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
15
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
16
+ package k8s.base_kustomization
17
+
18
+ import rego.v1
19
+
20
+ base_namespace_required_msg := concat(" ", [
21
+ "у base/kustomization.yaml завжди додай непорожній namespace:",
22
+ "(наприклад namespace: dev; k8s.mdc)",
23
+ ])
24
+
25
+ deny contains base_namespace_required_msg if {
26
+ is_kustomization
27
+ not is_string(object.get(input, "namespace", null))
28
+ }
29
+
30
+ deny contains base_namespace_required_msg if {
31
+ is_kustomization
32
+ ns := object.get(input, "namespace", "")
33
+ is_string(ns)
34
+ trim_space(ns) == ""
35
+ }
36
+
37
+ is_kustomization if {
38
+ input.kind == "Kustomization"
39
+ startswith(object.get(input, "apiVersion", ""), "kustomize.config.k8s.io/")
40
+ }
@@ -0,0 +1,36 @@
1
+ # Тести для `k8s.base_kustomization`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/base_kustomization --namespace k8s.base_kustomization
3
+ package k8s.base_kustomization_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.base_kustomization
8
+
9
+ base_kust := {
10
+ "apiVersion": "kustomize.config.k8s.io/v1beta1",
11
+ "kind": "Kustomization",
12
+ }
13
+
14
+ test_deny_missing_namespace if {
15
+ count(base_kustomization.deny) > 0 with input as base_kust
16
+ }
17
+
18
+ test_deny_empty_namespace if {
19
+ count(base_kustomization.deny) > 0 with input as object.union(base_kust, {"namespace": ""})
20
+ }
21
+
22
+ test_deny_whitespace_namespace if {
23
+ count(base_kustomization.deny) > 0 with input as object.union(base_kust, {"namespace": " "})
24
+ }
25
+
26
+ test_allow_with_namespace if {
27
+ count(base_kustomization.deny) == 0 with input as object.union(base_kust, {"namespace": "dev"})
28
+ }
29
+
30
+ test_allow_non_kustomization if {
31
+ count(base_kustomization.deny) == 0 with input as {
32
+ "apiVersion": "v1",
33
+ "kind": "ConfigMap",
34
+ "metadata": {"name": "cm"},
35
+ }
36
+ }
@@ -0,0 +1,154 @@
1
+ # Порт пер-документних структурних перевірок для маніфестів у шарі
2
+ # `…/k8s/.../base/...` (k8s.mdc).
3
+ #
4
+ # Запуск (локально, лише для одного файлу під base/):
5
+ # conftest test path/to/k8s/base/deployment.yaml -p npm/policy/k8s/base_manifest \
6
+ # --namespace k8s.base_manifest
7
+ #
8
+ # JS відбирає файли під `…/k8s/.../base/…` (окрім `kustomization.yaml`) і
9
+ # викликає conftest з цією намеспейс. JS authoritative
10
+ # (`check-k8s.mjs`: `metadataNamespaceRequiredViolation` з `inBaseDir=true`,
11
+ # `deploymentResourcesViolation` з `inK8sBaseLayer=true`,
12
+ # `isK8sBaseManifestYamlPath`).
13
+ #
14
+ # Перевіряє:
15
+ # - namespaced kind має непорожній `metadata.namespace` (cluster-scoped kind
16
+ # і Kustomization/List виняток);
17
+ # - Deployment у base має фіксовані `resources.requests.cpu == "0.02"` (або
18
+ # число `0.02`) і `resources.requests.memory == "128Mi"` (case-insensitive
19
+ # суфікс Mi).
20
+ #
21
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
22
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
23
+ package k8s.base_manifest
24
+
25
+ import rego.v1
26
+
27
+ # Cluster-scoped kind (не вимагають metadata.namespace) — узгоджено з
28
+ # `CLUSTER_SCOPED_KINDS` у `check-k8s.mjs`.
29
+ cluster_scoped_kinds := {
30
+ "APIService",
31
+ "CertificateSigningRequest",
32
+ "ClusterCIDR",
33
+ "ClusterRole",
34
+ "ClusterRoleBinding",
35
+ "ComponentStatus",
36
+ "CSIDriver",
37
+ "CSINode",
38
+ "CustomResourceDefinition",
39
+ "FlowSchema",
40
+ "IPAddress",
41
+ "IngressClass",
42
+ "MutatingWebhookConfiguration",
43
+ "Namespace",
44
+ "Node",
45
+ "PersistentVolume",
46
+ "PriorityClass",
47
+ "PriorityLevelConfiguration",
48
+ "RuntimeClass",
49
+ "ServiceCIDR",
50
+ "StorageClass",
51
+ "StorageVersionMigration",
52
+ "ValidatingAdmissionPolicy",
53
+ "ValidatingAdmissionPolicyBinding",
54
+ "ValidatingWebhookConfiguration",
55
+ "VolumeAttachment",
56
+ }
57
+
58
+ # Жорстко зафіксовані значення resources.requests у base-шарі (k8s.mdc).
59
+ base_cpu_request := "0.02"
60
+
61
+ base_memory_request := "128Mi"
62
+
63
+ base_metadata_missing_msg := concat(" ", [
64
+ "додай metadata з непорожнім metadata.namespace —",
65
+ "у k8s/base у кожному ресурсному YAML має бути явний namespace (k8s.mdc)",
66
+ ])
67
+
68
+ base_namespace_required_msg := concat(" ", [
69
+ "metadata.namespace обов'язковий у k8s/base —",
70
+ "додай явний namespace у маніфесті (k8s.mdc)",
71
+ ])
72
+
73
+ base_canon_cpu_template := concat(" ", [
74
+ "контейнер %q: у k8s/.../base resources.requests.cpu має бути рівно %q",
75
+ "(допускається число 0.02) — зараз %v (k8s.mdc)",
76
+ ])
77
+
78
+ base_canon_memory_template := concat(" ", [
79
+ "контейнер %q: у k8s/.../base resources.requests.memory має бути рівно %q",
80
+ "(суфікс Mi без урахування регістру) — зараз %v (k8s.mdc)",
81
+ ])
82
+
83
+ # ── deny: namespaced kind у base/ — обов'язковий metadata.namespace ──────
84
+
85
+ deny contains base_metadata_missing_msg if {
86
+ is_namespaced_kind
87
+ not is_object(object.get(input, "metadata", null))
88
+ }
89
+
90
+ deny contains base_namespace_required_msg if {
91
+ is_namespaced_kind
92
+ meta := object.get(input, "metadata", null)
93
+ is_object(meta)
94
+ ns := object.get(meta, "namespace", "")
95
+ trim_space(ns) == ""
96
+ }
97
+
98
+ # ── deny: Deployment у base — точне cpu='0.02' / memory='128Mi' ──────────
99
+
100
+ deny contains msg if {
101
+ input.kind == "Deployment"
102
+ some container in deployment_all_containers
103
+ cpu := object.get(object.get(container, "resources", {}), "requests", {}).cpu
104
+ cpu != null
105
+ not is_base_canon_cpu(cpu)
106
+ msg := sprintf(base_canon_cpu_template, [container.name, base_cpu_request, cpu])
107
+ }
108
+
109
+ deny contains msg if {
110
+ input.kind == "Deployment"
111
+ some container in deployment_all_containers
112
+ mem := object.get(object.get(container, "resources", {}), "requests", {}).memory
113
+ mem != null
114
+ not is_base_canon_memory(mem)
115
+ msg := sprintf(base_canon_memory_template, [container.name, base_memory_request, mem])
116
+ }
117
+
118
+ # ── helpers ───────────────────────────────────────────────────────────────
119
+
120
+ # Це namespaced ресурс, на який має застосовуватись правило metadata.namespace.
121
+ is_namespaced_kind if {
122
+ is_string(input.kind)
123
+ input.kind != ""
124
+ input.kind != "List"
125
+ input.kind != "Kustomization"
126
+ is_string(input.apiVersion)
127
+ input.apiVersion != ""
128
+ not input.kind in cluster_scoped_kinds
129
+ }
130
+
131
+ deployment_all_containers contains container if {
132
+ some container in object.get(object.get(input.spec.template, "spec", {}), "containers", [])
133
+ }
134
+
135
+ deployment_all_containers contains container if {
136
+ some container in object.get(object.get(input.spec.template, "spec", {}), "initContainers", [])
137
+ }
138
+
139
+ # Канон cpu='0.02' — рядок (точно "0.02") або число 0.02.
140
+ is_base_canon_cpu(v) if {
141
+ is_string(v)
142
+ trim_space(v) == base_cpu_request
143
+ }
144
+
145
+ is_base_canon_cpu(v) if {
146
+ is_number(v)
147
+ v == 0.02
148
+ }
149
+
150
+ # Канон memory='128Mi' (суфікс Mi без урахування регістру).
151
+ is_base_canon_memory(v) if {
152
+ is_string(v)
153
+ lower(trim_space(v)) == "128mi"
154
+ }
@@ -0,0 +1,94 @@
1
+ # Тести для `k8s.base_manifest`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/base_manifest --namespace k8s.base_manifest
3
+ package k8s.base_manifest_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.base_manifest
8
+
9
+ # ── metadata.namespace required ─────────────────────────────────────────
10
+
11
+ test_deny_namespaced_kind_without_metadata if {
12
+ count(base_manifest.deny) > 0 with input as {
13
+ "apiVersion": "v1",
14
+ "kind": "ConfigMap",
15
+ }
16
+ }
17
+
18
+ test_deny_namespaced_kind_empty_namespace if {
19
+ count(base_manifest.deny) > 0 with input as {
20
+ "apiVersion": "v1",
21
+ "kind": "ConfigMap",
22
+ "metadata": {"name": "cm", "namespace": ""},
23
+ }
24
+ }
25
+
26
+ test_allow_cluster_scoped_kind_without_namespace if {
27
+ count(base_manifest.deny) == 0 with input as {
28
+ "apiVersion": "v1",
29
+ "kind": "Namespace",
30
+ "metadata": {"name": "dev"},
31
+ }
32
+ }
33
+
34
+ test_allow_namespaced_kind_with_namespace if {
35
+ count(base_manifest.deny) == 0 with input as {
36
+ "apiVersion": "v1",
37
+ "kind": "ConfigMap",
38
+ "metadata": {"name": "cm", "namespace": "dev"},
39
+ }
40
+ }
41
+
42
+ # ── base canon resources ─────────────────────────────────────────────────
43
+
44
+ test_deny_deployment_cpu_not_base_canon if {
45
+ count(base_manifest.deny) > 0 with input as {
46
+ "apiVersion": "apps/v1",
47
+ "kind": "Deployment",
48
+ "metadata": {"name": "api", "namespace": "dev"},
49
+ "spec": {"template": {"spec": {"containers": [{
50
+ "name": "main",
51
+ "image": "x",
52
+ "resources": {"requests": {"cpu": "100m", "memory": "128Mi"}},
53
+ }]}}},
54
+ }
55
+ }
56
+
57
+ test_deny_deployment_memory_not_base_canon if {
58
+ count(base_manifest.deny) > 0 with input as {
59
+ "apiVersion": "apps/v1",
60
+ "kind": "Deployment",
61
+ "metadata": {"name": "api", "namespace": "dev"},
62
+ "spec": {"template": {"spec": {"containers": [{
63
+ "name": "main",
64
+ "image": "x",
65
+ "resources": {"requests": {"cpu": "0.02", "memory": "256Mi"}},
66
+ }]}}},
67
+ }
68
+ }
69
+
70
+ test_allow_deployment_with_base_canon_string if {
71
+ count(base_manifest.deny) == 0 with input as {
72
+ "apiVersion": "apps/v1",
73
+ "kind": "Deployment",
74
+ "metadata": {"name": "api", "namespace": "dev"},
75
+ "spec": {"template": {"spec": {"containers": [{
76
+ "name": "main",
77
+ "image": "x",
78
+ "resources": {"requests": {"cpu": "0.02", "memory": "128Mi"}},
79
+ }]}}},
80
+ }
81
+ }
82
+
83
+ test_allow_deployment_with_base_canon_number_cpu if {
84
+ count(base_manifest.deny) == 0 with input as {
85
+ "apiVersion": "apps/v1",
86
+ "kind": "Deployment",
87
+ "metadata": {"name": "api", "namespace": "dev"},
88
+ "spec": {"template": {"spec": {"containers": [{
89
+ "name": "main",
90
+ "image": "x",
91
+ "resources": {"requests": {"cpu": 0.02, "memory": "128mi"}},
92
+ }]}}},
93
+ }
94
+ }
@@ -0,0 +1,151 @@
1
+ # Порт пер-документних перевірок для Gateway API і HealthCheckPolicy з
2
+ # `npm/scripts/check-k8s.mjs` (k8s.mdc).
3
+ #
4
+ # Запуск (локально, по одному файлу або по дереву):
5
+ # conftest test path/to/manifest.yaml -p npm/policy/k8s/gateway \
6
+ # --namespace k8s.gateway
7
+ #
8
+ # Перевіряє:
9
+ # - HealthCheckPolicy (`networking.gke.io/v1`): `spec.targetRef.name` має
10
+ # закінчуватися на `-hl` (headless Service);
11
+ # - HTTPRoute / GRPCRoute / TCPRoute / TLSRoute / UDPRoute (`gateway.networking.k8s.io/*`):
12
+ # backendRef до Service має ім'я з суфіксом `-hl` (headless);
13
+ # - той самий маршрут: backendRef з полем `namespace`, що збігається з
14
+ # `metadata.namespace` маршруту, — заборонено (надлишкове поле, ламається при
15
+ # overlay-перенесеннях).
16
+ #
17
+ # JS authoritative (`check-k8s.mjs` — функції `failIfGatewayRouteUsesNonHeadlessService`,
18
+ # `healthCheckPolicyTargetRefHeadlessServiceViolation`,
19
+ # `collectGatewayApiRouteBackendServiceNames`,
20
+ # `collectGatewayApiRouteBackendRefsWithRedundantNamespace`); ця Rego — швидкий
21
+ # gate для одиничного маніфеста.
22
+ #
23
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
24
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
25
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
26
+ package k8s.gateway
27
+
28
+ import rego.v1
29
+
30
+ # Kind-и маршрутів Gateway API, у `spec` яких шукаємо backendRefs.
31
+ route_kinds := {"HTTPRoute", "GRPCRoute", "TCPRoute", "TLSRoute", "UDPRoute"}
32
+
33
+ # Префікс apiVersion стандартних ресурсів Gateway API.
34
+ api_group_prefix := "gateway.networking.k8s.io/"
35
+
36
+ # Суфікс metadata.name для headless-сервісу (k8s.mdc).
37
+ svc_hl_name_suffix := "-hl"
38
+
39
+ hcp_target_ref_template := concat(" ", [
40
+ "HealthCheckPolicy: spec.targetRef.name має закінчуватись на %q",
41
+ "(зараз: %q) (k8s.mdc)",
42
+ ])
43
+
44
+ route_backend_hl_template := concat(" ", [
45
+ "Gateway API %s: backendRef до Service має вказувати headless-сервіс",
46
+ "з суфіксом %q (зараз: %q) (k8s.mdc)",
47
+ ])
48
+
49
+ route_backend_redundant_ns_template := concat(" ", [
50
+ "Gateway API %s: backendRef %q має namespace %q,",
51
+ "що збігається з metadata.namespace маршруту —",
52
+ "прибери поле namespace (k8s.mdc)",
53
+ ])
54
+
55
+ # ── deny: HealthCheckPolicy — targetRef.name має закінчуватись на `-hl` ───
56
+
57
+ deny contains msg if {
58
+ is_health_check_policy
59
+ target_ref_kind_is_service
60
+ name := object.get(object.get(input.spec, "targetRef", {}), "name", "")
61
+ name != ""
62
+ not endswith(name, svc_hl_name_suffix)
63
+ msg := sprintf(hcp_target_ref_template, [svc_hl_name_suffix, name])
64
+ }
65
+
66
+ deny contains msg if {
67
+ is_health_check_policy
68
+ not has_target_ref
69
+ msg := "HealthCheckPolicy: відсутній spec.targetRef (k8s.mdc)"
70
+ }
71
+
72
+ # ── deny: Gateway API маршрут — backendRef має суфікс `-hl` ──────────────
73
+
74
+ deny contains msg if {
75
+ is_gateway_api_route
76
+ some backend_name in route_service_backend_names
77
+ not endswith(backend_name, svc_hl_name_suffix)
78
+ msg := sprintf(route_backend_hl_template, [input.kind, svc_hl_name_suffix, backend_name])
79
+ }
80
+
81
+ # ── deny: Gateway API маршрут — backendRef з redundant namespace ─────────
82
+
83
+ deny contains msg if {
84
+ is_gateway_api_route
85
+ route_ns := object.get(input.metadata, "namespace", "")
86
+ route_ns != ""
87
+ some redundant in redundant_namespace_backend_names(route_ns)
88
+ msg := sprintf(route_backend_redundant_ns_template, [input.kind, redundant, route_ns])
89
+ }
90
+
91
+ # ── helpers ───────────────────────────────────────────────────────────────
92
+
93
+ is_health_check_policy if {
94
+ input.kind == "HealthCheckPolicy"
95
+ startswith(object.get(input, "apiVersion", ""), "networking.gke.io/")
96
+ }
97
+
98
+ # targetRef.kind не задано або дорівнює "Service" — звіряємо суфікс імені.
99
+ target_ref_kind_is_service if {
100
+ target_ref_kind == ""
101
+ }
102
+
103
+ target_ref_kind_is_service if {
104
+ target_ref_kind == "Service"
105
+ }
106
+
107
+ target_ref_kind := object.get(object.get(input.spec, "targetRef", {}), "kind", "")
108
+
109
+ has_target_ref if {
110
+ object.get(input, "spec", {}).targetRef
111
+ }
112
+
113
+ is_gateway_api_route if {
114
+ startswith(object.get(input, "apiVersion", ""), api_group_prefix)
115
+ input.kind in route_kinds
116
+ }
117
+
118
+ # Усі імена backend-ів у `spec` що виглядають як backendRef до Service: вузол
119
+ # має `name` (string) і `port` (number); якщо поле `kind`/`group` явне — лише
120
+ # `Service`/`core` (без явного group теж приймаємо).
121
+ route_service_backend_names contains node.name if {
122
+ walk(object.get(input, "spec", {}), [_, node])
123
+ is_gateway_api_backend_ref_to_service(node)
124
+ }
125
+
126
+ # Тільки ті backendRef, у яких `namespace` збігається з namespace маршруту.
127
+ redundant_namespace_backend_names(route_ns) := {node.name |
128
+ walk(object.get(input, "spec", {}), [_, node])
129
+ is_gateway_api_backend_ref_to_service(node)
130
+ node.namespace == route_ns
131
+ }
132
+
133
+ is_gateway_api_backend_ref_to_service(obj) if {
134
+ is_object(obj)
135
+ is_string(obj.name)
136
+ is_number(obj.port)
137
+ kind_ok(obj)
138
+ group_ok(obj)
139
+ }
140
+
141
+ # Якщо `kind` не вказано — приймаємо як Service (Gateway API дефолт).
142
+ kind_ok(obj) if not obj.kind
143
+
144
+ kind_ok(obj) if obj.kind == "Service"
145
+
146
+ # Якщо `group` не вказано / порожній / "core" — приймаємо як Service.
147
+ group_ok(obj) if not obj.group
148
+
149
+ group_ok(obj) if obj.group == ""
150
+
151
+ group_ok(obj) if obj.group == "core"
@@ -0,0 +1,122 @@
1
+ # Тести для `k8s.gateway`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/gateway --namespace k8s.gateway
3
+ package k8s.gateway_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.gateway
8
+
9
+ # ── HealthCheckPolicy ─────────────────────────────────────────────────────
10
+
11
+ test_deny_hcp_targetref_without_hl_suffix if {
12
+ count(gateway.deny) > 0 with input as {
13
+ "apiVersion": "networking.gke.io/v1",
14
+ "kind": "HealthCheckPolicy",
15
+ "metadata": {"name": "hc"},
16
+ "spec": {"targetRef": {
17
+ "group": "",
18
+ "kind": "Service",
19
+ "name": "auth",
20
+ }},
21
+ }
22
+ }
23
+
24
+ test_allow_hcp_targetref_with_hl_suffix if {
25
+ count(gateway.deny) == 0 with input as {
26
+ "apiVersion": "networking.gke.io/v1",
27
+ "kind": "HealthCheckPolicy",
28
+ "metadata": {"name": "hc"},
29
+ "spec": {"targetRef": {
30
+ "group": "",
31
+ "kind": "Service",
32
+ "name": "auth-hl",
33
+ }},
34
+ }
35
+ }
36
+
37
+ test_deny_hcp_missing_targetref if {
38
+ count(gateway.deny) > 0 with input as {
39
+ "apiVersion": "networking.gke.io/v1",
40
+ "kind": "HealthCheckPolicy",
41
+ "metadata": {"name": "hc"},
42
+ "spec": {},
43
+ }
44
+ }
45
+
46
+ # Без kind=Service у targetRef правило не діє (інші kind не оцінюємо).
47
+ test_allow_hcp_targetref_other_kind if {
48
+ count(gateway.deny) == 0 with input as {
49
+ "apiVersion": "networking.gke.io/v1",
50
+ "kind": "HealthCheckPolicy",
51
+ "metadata": {"name": "hc"},
52
+ "spec": {"targetRef": {
53
+ "kind": "Gateway",
54
+ "name": "gw",
55
+ }},
56
+ }
57
+ }
58
+
59
+ # ── HTTPRoute backendRef → Service з суфіксом `-hl` ──────────────────────
60
+
61
+ test_deny_httproute_backend_without_hl if {
62
+ count(gateway.deny) > 0 with input as {
63
+ "apiVersion": "gateway.networking.k8s.io/v1",
64
+ "kind": "HTTPRoute",
65
+ "metadata": {"name": "r", "namespace": "dev"},
66
+ "spec": {"rules": [{"backendRefs": [{"name": "auth", "port": 8080}]}]},
67
+ }
68
+ }
69
+
70
+ test_allow_httproute_backend_with_hl if {
71
+ count(gateway.deny) == 0 with input as {
72
+ "apiVersion": "gateway.networking.k8s.io/v1",
73
+ "kind": "HTTPRoute",
74
+ "metadata": {"name": "r", "namespace": "dev"},
75
+ "spec": {"rules": [{"backendRefs": [{"name": "auth-hl", "port": 8080}]}]},
76
+ }
77
+ }
78
+
79
+ # ── HTTPRoute backendRef з redundant namespace ───────────────────────────
80
+
81
+ test_deny_httproute_backend_redundant_namespace if {
82
+ count(gateway.deny) > 0 with input as {
83
+ "apiVersion": "gateway.networking.k8s.io/v1",
84
+ "kind": "HTTPRoute",
85
+ "metadata": {"name": "r", "namespace": "dev"},
86
+ "spec": {"rules": [{"backendRefs": [{
87
+ "name": "auth-hl",
88
+ "namespace": "dev",
89
+ "port": 8080,
90
+ }]}]},
91
+ }
92
+ }
93
+
94
+ test_allow_httproute_backend_different_namespace if {
95
+ count(gateway.deny) == 0 with input as {
96
+ "apiVersion": "gateway.networking.k8s.io/v1",
97
+ "kind": "HTTPRoute",
98
+ "metadata": {"name": "r", "namespace": "dev"},
99
+ "spec": {"rules": [{"backendRefs": [{
100
+ "name": "auth-hl",
101
+ "namespace": "other",
102
+ "port": 8080,
103
+ }]}]},
104
+ }
105
+ }
106
+
107
+ # Перевірка не діє на HTTPHeaderMatch (немає `port`).
108
+ test_allow_httproute_header_match_without_port if {
109
+ count(gateway.deny) == 0 with input as {
110
+ "apiVersion": "gateway.networking.k8s.io/v1",
111
+ "kind": "HTTPRoute",
112
+ "metadata": {"name": "r", "namespace": "dev"},
113
+ "spec": {"rules": [{
114
+ "matches": [{"headers": [{
115
+ "type": "Exact",
116
+ "name": "X-Tenant",
117
+ "value": "acme",
118
+ }]}],
119
+ "backendRefs": [{"name": "auth-hl", "port": 8080}],
120
+ }]},
121
+ }
122
+ }