@nitra/cursor 1.8.221 → 1.8.228

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 (44) hide show
  1. package/.claude-template/npm-CLAUDE.md +4 -0
  2. package/CHANGELOG.md +69 -0
  3. package/bin/auto-rules.md +2 -0
  4. package/bin/n-cursor.js +3 -2
  5. package/mdc/abie.mdc +13 -0
  6. package/mdc/ci4.mdc +8 -0
  7. package/mdc/tauri.mdc +20 -0
  8. package/package.json +1 -1
  9. package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
  10. package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
  11. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
  12. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
  13. package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
  14. package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
  15. package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
  16. package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
  17. package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
  18. package/policy/k8s/base_manifest/base_manifest.rego +154 -0
  19. package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
  20. package/policy/k8s/gateway/gateway.rego +151 -0
  21. package/policy/k8s/gateway/gateway_test.rego +122 -0
  22. package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
  23. package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
  24. package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
  25. package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
  26. package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
  27. package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
  28. package/policy/k8s/kustomization/kustomization.rego +220 -0
  29. package/policy/k8s/kustomization/kustomization_test.rego +128 -0
  30. package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
  31. package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
  32. package/policy/k8s/manifest/manifest.rego +151 -4
  33. package/policy/k8s/manifest/manifest_test.rego +309 -0
  34. package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
  35. package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
  36. package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
  37. package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
  38. package/scripts/check-abie.mjs +102 -369
  39. package/scripts/check-ga.mjs +89 -9
  40. package/scripts/check-k8s.mjs +128 -569
  41. package/scripts/lint-conftest.mjs +98 -3
  42. package/scripts/lint-ga.mjs +18 -132
  43. package/scripts/lint-rego.mjs +19 -4
  44. package/scripts/utils/run-conftest-batch.mjs +117 -0
@@ -4,7 +4,7 @@
4
4
  # запускає policy на кожен документ окремо.
5
5
  #
6
6
  # Запуск (локально, по одному файлу або по дереву):
7
- # conftest test path/to/k8s/manifest.yaml -p npm/policy/k8s \
7
+ # conftest test path/to/k8s/manifest.yaml -p npm/policy/k8s/manifest \
8
8
  # --namespace k8s.manifest
9
9
  #
10
10
  # Перевіряє:
@@ -13,8 +13,13 @@
13
13
  # - `kind: Service` без `cloud.google.com/neg` /
14
14
  # `cloud.google.com/backend-config` в `metadata.annotations` (k8s.mdc);
15
15
  # - `kind: Deployment` — у кожного контейнера спільно `containers` +
16
- # `initContainers` має бути `resources.requests.cpu` (рядок на кшталт
17
- # `"500m"` чи число), без порожнього значення.
16
+ # `initContainers` має бути `resources.requests.cpu` і
17
+ # `resources.requests.memory` (рядок або додатне число);
18
+ # - `kind: Deployment` з образом `hasura/graphql-engine` — образ має бути
19
+ # у білому списку `allowed_hasura_images` (з digest або без; префікс
20
+ # `docker.io/` дозволено);
21
+ # - `kind: Deployment` — наявність канонічного запису у
22
+ # `spec.template.spec.topologySpreadConstraints` (k8s.mdc).
18
23
  #
19
24
  # CROSS-FILE логіка (Kustomize-резолюція ресурсів, парність svc.yaml/svc-hl.yaml,
20
25
  # HPA/PDB/topologySpreadConstraints за каталогом, BackendConfig-сепарація,
@@ -32,11 +37,25 @@ import rego.v1
32
37
 
33
38
  default_cpu_request := "0.5"
34
39
 
40
+ default_memory_request := "512Mi"
41
+
35
42
  forbidden_service_annotations := {
36
43
  "cloud.google.com/neg",
37
44
  "cloud.google.com/backend-config",
38
45
  }
39
46
 
47
+ # Дозволені посилання на образ `hasura/graphql-engine` (узгоджено з
48
+ # `HASURA_GRAPHQL_ENGINE_IMAGE` у `check-k8s.mjs`). Зараз — один канонічний тег
49
+ # у двох варіантах префіксу (із `docker.io/` і без). Digest (`@sha256:…`)
50
+ # відрізається перед звіркою.
51
+ allowed_hasura_images := {
52
+ "hasura/graphql-engine:v2.48.15.ubi.amd64",
53
+ "docker.io/hasura/graphql-engine:v2.48.15.ubi.amd64",
54
+ }
55
+
56
+ # Канонічне значення `topologyKey` для `topologySpreadConstraints` (k8s.mdc).
57
+ topology_spread_topology_key := "kubernetes.io/hostname"
58
+
40
59
  ingress_template := concat(" ", [
41
60
  "знайдено kind: Ingress — заміни на Gateway API:",
42
61
  "HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml) (k8s.mdc)",
@@ -57,6 +76,27 @@ cpu_empty_template := concat(" ", [
57
76
  "значенням (наприклад \"500m\") (зараз: %v) (k8s.mdc)",
58
77
  ])
59
78
 
79
+ memory_missing_template := concat(" ", [
80
+ "Deployment %q, контейнер %q: відсутнє resources.requests.memory —",
81
+ "додай (за замовчуванням %s) (k8s.mdc)",
82
+ ])
83
+
84
+ memory_empty_template := concat(" ", [
85
+ "Deployment %q, контейнер %q: resources.requests.memory має бути непорожнім",
86
+ "значенням (наприклад \"512Mi\") (зараз: %v) (k8s.mdc)",
87
+ ])
88
+
89
+ hasura_image_template := concat(" ", [
90
+ "Deployment %q, контейнер %q: образ hasura/graphql-engine має бути одним",
91
+ "із дозволених тегів (зараз: %q) (k8s.mdc)",
92
+ ])
93
+
94
+ topology_spread_missing_template := concat(" ", [
95
+ "Deployment %q: відсутній канонічний запис у spec.template.spec.topologySpreadConstraints —",
96
+ "додай maxSkew=1, topologyKey=%s, whenUnsatisfiable=ScheduleAnyway,",
97
+ "labelSelector.matchLabels.app=%q (k8s.mdc)",
98
+ ])
99
+
60
100
  # ── deny: заборонені kind/apiVersion ──────────────────────────────────────
61
101
 
62
102
  deny contains ingress_template if {
@@ -100,11 +140,58 @@ deny contains msg if {
100
140
  msg := sprintf(cpu_empty_template, [deployment_name, container.name, cpu])
101
141
  }
102
142
 
143
+ # ── deny: Deployment — у кожного контейнера resources.requests.memory ─────
144
+
145
+ deny contains msg if {
146
+ input.kind == "Deployment"
147
+ some container in deployment_all_containers
148
+ not has_non_empty_memory_request(container)
149
+ not has_memory_field(container)
150
+ msg := sprintf(memory_missing_template, [deployment_name, container.name, default_memory_request])
151
+ }
152
+
153
+ deny contains msg if {
154
+ input.kind == "Deployment"
155
+ some container in deployment_all_containers
156
+ not has_non_empty_memory_request(container)
157
+ has_memory_field(container)
158
+ mem := container.resources.requests.memory
159
+ msg := sprintf(memory_empty_template, [deployment_name, container.name, mem])
160
+ }
161
+
162
+ # ── deny: Deployment — образ hasura/graphql-engine з білого списку ────────
163
+ #
164
+ # Spec вимагає рівно тег із константи `HASURA_GRAPHQL_ENGINE_IMAGE`
165
+ # (з опційним префіксом `docker.io/`). Digest `@sha256:…` у поточних правилах
166
+ # відрізається перед порівнянням (k8s.mdc допускає, але не вимагає його).
167
+
168
+ deny contains msg if {
169
+ input.kind == "Deployment"
170
+ some container in deployment_all_containers
171
+ is_hasura_graphql_engine_image_ref(container.image)
172
+ stripped := strip_image_digest(container.image)
173
+ not stripped in allowed_hasura_images
174
+ msg := sprintf(hasura_image_template, [deployment_name, container.name, container.image])
175
+ }
176
+
177
+ # ── deny: Deployment — канонічний topologySpreadConstraints ───────────────
178
+ #
179
+ # Перевіряємо лише Deployment-и, що мають мітку `app` у
180
+ # `spec.selector.matchLabels.app` — без неї канон не визначений (так само
181
+ # як у JS-перевірці).
182
+
183
+ deny contains msg if {
184
+ input.kind == "Deployment"
185
+ deployment_app_label != ""
186
+ not has_canonical_topology_spread_constraint(deployment_app_label)
187
+ msg := sprintf(topology_spread_missing_template, [deployment_name, topology_spread_topology_key, deployment_app_label])
188
+ }
189
+
103
190
  # ── helpers ────────────────────────────────────────────────────────────────
104
191
 
105
192
  deployment_name := object.get(object.get(input, "metadata", {}), "name", "<no-name>")
106
193
 
107
- # Усі контейнери (звичайні + ініт) Deployment-а — для перевірки CPU.
194
+ # Усі контейнери (звичайні + ініт) Deployment-а — для перевірки CPU/memory/image.
108
195
  deployment_all_containers contains container if {
109
196
  some container in object.get(object.get(input.spec.template, "spec", {}), "containers", [])
110
197
  }
@@ -130,3 +217,63 @@ has_non_empty_cpu_request(container) if {
130
217
  has_cpu_field(container) if {
131
218
  _ := container.resources.requests.cpu
132
219
  }
220
+
221
+ # Чи у контейнера є непорожнє resources.requests.memory (рядок або число > 0).
222
+ has_non_empty_memory_request(container) if {
223
+ mem := container.resources.requests.memory
224
+ is_string(mem)
225
+ trim_space(mem) != ""
226
+ }
227
+
228
+ has_non_empty_memory_request(container) if {
229
+ mem := container.resources.requests.memory
230
+ is_number(mem)
231
+ mem > 0
232
+ }
233
+
234
+ # Чи у контейнера в реальності присутнє поле resources.requests.memory.
235
+ has_memory_field(container) if {
236
+ _ := container.resources.requests.memory
237
+ }
238
+
239
+ # Чи рядок `image` посилається на репозиторій `hasura/graphql-engine` (з тегом
240
+ # або без). Digest `@sha256:…` ігнорується.
241
+ is_hasura_graphql_engine_image_ref(image) if {
242
+ is_string(image)
243
+ stripped := strip_image_digest(image)
244
+ regex.match(`(^|/)hasura/graphql-engine(:|$)`, stripped)
245
+ }
246
+
247
+ # Прибирає digest (`@sha256:…`) з image-string для звірки тегу.
248
+ strip_image_digest(image) := stripped if {
249
+ at_idx := indexof(image, "@")
250
+ at_idx >= 0
251
+ stripped := substring(image, 0, at_idx)
252
+ }
253
+
254
+ strip_image_digest(image) := image if {
255
+ indexof(image, "@") == -1
256
+ }
257
+
258
+ # Витягує мітку `app` з `spec.selector.matchLabels.app`. Повертає "" якщо немає.
259
+ default deployment_app_label := ""
260
+
261
+ deployment_app_label := app if {
262
+ app := object.get(object.get(object.get(input.spec, "selector", {}), "matchLabels", {}), "app", "")
263
+ is_string(app)
264
+ }
265
+
266
+ # Чи серед `spec.template.spec.topologySpreadConstraints` є запис, який
267
+ # відповідає канону (maxSkew=1, потрібний topologyKey, whenUnsatisfiable,
268
+ # labelSelector.matchLabels.app == очікувана мітка).
269
+ has_canonical_topology_spread_constraint(expected_app) if {
270
+ some item in object.get(object.get(input.spec.template, "spec", {}), "topologySpreadConstraints", [])
271
+ is_canonical_topology_spread_constraint(item, expected_app)
272
+ }
273
+
274
+ is_canonical_topology_spread_constraint(item, expected_app) if {
275
+ item.maxSkew == 1
276
+ item.topologyKey == topology_spread_topology_key
277
+ item.whenUnsatisfiable == "ScheduleAnyway"
278
+ object.get(object.get(item, "labelSelector", {}), "matchLabels", {}).app == expected_app
279
+ }
@@ -0,0 +1,309 @@
1
+ # Тести для `k8s.manifest`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/manifest --namespace k8s.manifest
3
+ #
4
+ # Покриваємо deny-правила: Ingress, autoscaling/v1, Service GCP-анотації,
5
+ # Deployment cpu/memory/image (Hasura), topologySpreadConstraints. Тести
6
+ # перевіряють як спрацювання правила (count(deny) > 0), так і його відсутність
7
+ # для коректного маніфесту.
8
+ package k8s.manifest_test
9
+
10
+ import rego.v1
11
+
12
+ import data.k8s.manifest
13
+
14
+ # ── Ingress / autoscaling/v1 ──────────────────────────────────────────────
15
+
16
+ test_deny_ingress if {
17
+ count(manifest.deny) > 0 with input as {
18
+ "apiVersion": "networking.k8s.io/v1",
19
+ "kind": "Ingress",
20
+ "metadata": {"name": "x"},
21
+ }
22
+ }
23
+
24
+ test_deny_autoscaling_v1 if {
25
+ count(manifest.deny) > 0 with input as {
26
+ "apiVersion": "autoscaling/v1",
27
+ "kind": "HorizontalPodAutoscaler",
28
+ "metadata": {"name": "x"},
29
+ }
30
+ }
31
+
32
+ test_allow_autoscaling_v2 if {
33
+ count(manifest.deny) == 0 with input as {
34
+ "apiVersion": "autoscaling/v2",
35
+ "kind": "HorizontalPodAutoscaler",
36
+ "metadata": {"name": "x"},
37
+ }
38
+ }
39
+
40
+ # ── Service: GCP-анотації ─────────────────────────────────────────────────
41
+
42
+ test_deny_service_neg_annotation if {
43
+ count(manifest.deny) > 0 with input as {
44
+ "apiVersion": "v1",
45
+ "kind": "Service",
46
+ "metadata": {
47
+ "name": "api",
48
+ "annotations": {"cloud.google.com/neg": "{}"},
49
+ },
50
+ }
51
+ }
52
+
53
+ test_deny_service_backend_config_annotation if {
54
+ count(manifest.deny) > 0 with input as {
55
+ "apiVersion": "v1",
56
+ "kind": "Service",
57
+ "metadata": {
58
+ "name": "api",
59
+ "annotations": {"cloud.google.com/backend-config": "{}"},
60
+ },
61
+ }
62
+ }
63
+
64
+ test_allow_service_clean_annotations if {
65
+ count(manifest.deny) == 0 with input as {
66
+ "apiVersion": "v1",
67
+ "kind": "Service",
68
+ "metadata": {
69
+ "name": "api",
70
+ "namespace": "dev",
71
+ "annotations": {"foo": "bar"},
72
+ },
73
+ "spec": {"type": "ClusterIP"},
74
+ }
75
+ }
76
+
77
+ # ── Deployment: resources.requests.cpu ────────────────────────────────────
78
+
79
+ test_deny_deployment_missing_cpu if {
80
+ count(manifest.deny) > 0 with input as {
81
+ "apiVersion": "apps/v1",
82
+ "kind": "Deployment",
83
+ "metadata": {"name": "api", "namespace": "dev"},
84
+ "spec": {
85
+ "selector": {"matchLabels": {"app": "api"}},
86
+ "template": {"spec": {"containers": [{
87
+ "name": "main",
88
+ "image": "registry.example.com/api:1.0",
89
+ "resources": {"requests": {"memory": "64Mi"}},
90
+ }]}},
91
+ },
92
+ }
93
+ }
94
+
95
+ test_deny_deployment_empty_cpu if {
96
+ count(manifest.deny) > 0 with input as {
97
+ "apiVersion": "apps/v1",
98
+ "kind": "Deployment",
99
+ "metadata": {"name": "api", "namespace": "dev"},
100
+ "spec": {
101
+ "selector": {"matchLabels": {"app": "api"}},
102
+ "template": {"spec": {"containers": [{
103
+ "name": "main",
104
+ "image": "registry.example.com/api:1.0",
105
+ "resources": {"requests": {"cpu": "", "memory": "64Mi"}},
106
+ }]}},
107
+ },
108
+ }
109
+ }
110
+
111
+ # ── Deployment: resources.requests.memory ─────────────────────────────────
112
+
113
+ test_deny_deployment_missing_memory if {
114
+ count(manifest.deny) > 0 with input as {
115
+ "apiVersion": "apps/v1",
116
+ "kind": "Deployment",
117
+ "metadata": {"name": "api", "namespace": "dev"},
118
+ "spec": {
119
+ "selector": {"matchLabels": {"app": "api"}},
120
+ "template": {"spec": {"containers": [{
121
+ "name": "main",
122
+ "image": "registry.example.com/api:1.0",
123
+ "resources": {"requests": {"cpu": "100m"}},
124
+ }]}},
125
+ },
126
+ }
127
+ }
128
+
129
+ test_deny_deployment_empty_memory if {
130
+ count(manifest.deny) > 0 with input as {
131
+ "apiVersion": "apps/v1",
132
+ "kind": "Deployment",
133
+ "metadata": {"name": "api", "namespace": "dev"},
134
+ "spec": {
135
+ "selector": {"matchLabels": {"app": "api"}},
136
+ "template": {"spec": {"containers": [{
137
+ "name": "main",
138
+ "image": "registry.example.com/api:1.0",
139
+ "resources": {"requests": {"cpu": "100m", "memory": ""}},
140
+ }]}},
141
+ },
142
+ }
143
+ }
144
+
145
+ test_deny_init_container_missing_resources if {
146
+ count(manifest.deny) > 0 with input as {
147
+ "apiVersion": "apps/v1",
148
+ "kind": "Deployment",
149
+ "metadata": {"name": "api", "namespace": "dev"},
150
+ "spec": {
151
+ "selector": {"matchLabels": {"app": "api"}},
152
+ "template": {"spec": {
153
+ "containers": [{
154
+ "name": "main",
155
+ "image": "registry.example.com/api:1.0",
156
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
157
+ }],
158
+ "initContainers": [{"name": "wait", "image": "busybox:1"}],
159
+ }},
160
+ },
161
+ }
162
+ }
163
+
164
+ # ── Deployment: образ hasura/graphql-engine ──────────────────────────────
165
+
166
+ test_deny_deployment_hasura_unpinned_image if {
167
+ count(manifest.deny) > 0 with input as {
168
+ "apiVersion": "apps/v1",
169
+ "kind": "Deployment",
170
+ "metadata": {"name": "db-h", "namespace": "dev"},
171
+ "spec": {
172
+ "selector": {"matchLabels": {"app": "db-h"}},
173
+ "template": {"spec": {"containers": [{
174
+ "name": "graphql-engine",
175
+ "image": "hasura/graphql-engine:latest",
176
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
177
+ }]}},
178
+ },
179
+ }
180
+ }
181
+
182
+ test_allow_deployment_hasura_canonical_image if {
183
+ count(manifest.deny) == 0 with input as {
184
+ "apiVersion": "apps/v1",
185
+ "kind": "Deployment",
186
+ "metadata": {"name": "db-h", "namespace": "dev"},
187
+ "spec": {
188
+ "selector": {"matchLabels": {"app": "db-h"}},
189
+ "template": {"spec": {
190
+ "containers": [{
191
+ "name": "graphql-engine",
192
+ "image": "hasura/graphql-engine:v2.48.15.ubi.amd64",
193
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
194
+ }],
195
+ "topologySpreadConstraints": [{
196
+ "maxSkew": 1,
197
+ "topologyKey": "kubernetes.io/hostname",
198
+ "whenUnsatisfiable": "ScheduleAnyway",
199
+ "labelSelector": {"matchLabels": {"app": "db-h"}},
200
+ }],
201
+ }},
202
+ },
203
+ }
204
+ }
205
+
206
+ test_allow_deployment_hasura_canonical_image_with_digest if {
207
+ count(manifest.deny) == 0 with input as {
208
+ "apiVersion": "apps/v1",
209
+ "kind": "Deployment",
210
+ "metadata": {"name": "db-h", "namespace": "dev"},
211
+ "spec": {
212
+ "selector": {"matchLabels": {"app": "db-h"}},
213
+ "template": {"spec": {
214
+ "containers": [{
215
+ "name": "graphql-engine",
216
+ "image": "docker.io/hasura/graphql-engine:v2.48.15.ubi.amd64@sha256:0000",
217
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
218
+ }],
219
+ "topologySpreadConstraints": [{
220
+ "maxSkew": 1,
221
+ "topologyKey": "kubernetes.io/hostname",
222
+ "whenUnsatisfiable": "ScheduleAnyway",
223
+ "labelSelector": {"matchLabels": {"app": "db-h"}},
224
+ }],
225
+ }},
226
+ },
227
+ }
228
+ }
229
+
230
+ # ── Deployment: topologySpreadConstraints ────────────────────────────────
231
+
232
+ test_deny_deployment_missing_topology_spread if {
233
+ count(manifest.deny) > 0 with input as {
234
+ "apiVersion": "apps/v1",
235
+ "kind": "Deployment",
236
+ "metadata": {"name": "api", "namespace": "dev"},
237
+ "spec": {
238
+ "selector": {"matchLabels": {"app": "api"}},
239
+ "template": {"spec": {"containers": [{
240
+ "name": "main",
241
+ "image": "registry.example.com/api:1.0",
242
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
243
+ }]}},
244
+ },
245
+ }
246
+ }
247
+
248
+ test_deny_deployment_topology_spread_wrong_app_label if {
249
+ count(manifest.deny) > 0 with input as {
250
+ "apiVersion": "apps/v1",
251
+ "kind": "Deployment",
252
+ "metadata": {"name": "api", "namespace": "dev"},
253
+ "spec": {
254
+ "selector": {"matchLabels": {"app": "api"}},
255
+ "template": {"spec": {
256
+ "containers": [{
257
+ "name": "main",
258
+ "image": "registry.example.com/api:1.0",
259
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
260
+ }],
261
+ "topologySpreadConstraints": [{
262
+ "maxSkew": 1,
263
+ "topologyKey": "kubernetes.io/hostname",
264
+ "whenUnsatisfiable": "ScheduleAnyway",
265
+ "labelSelector": {"matchLabels": {"app": "wrong"}},
266
+ }],
267
+ }},
268
+ },
269
+ }
270
+ }
271
+
272
+ test_allow_deployment_canonical_topology_spread if {
273
+ count(manifest.deny) == 0 with input as {
274
+ "apiVersion": "apps/v1",
275
+ "kind": "Deployment",
276
+ "metadata": {"name": "api", "namespace": "dev"},
277
+ "spec": {
278
+ "selector": {"matchLabels": {"app": "api"}},
279
+ "template": {"spec": {
280
+ "containers": [{
281
+ "name": "main",
282
+ "image": "registry.example.com/api:1.0",
283
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
284
+ }],
285
+ "topologySpreadConstraints": [{
286
+ "maxSkew": 1,
287
+ "topologyKey": "kubernetes.io/hostname",
288
+ "whenUnsatisfiable": "ScheduleAnyway",
289
+ "labelSelector": {"matchLabels": {"app": "api"}},
290
+ }],
291
+ }},
292
+ },
293
+ }
294
+ }
295
+
296
+ # Без app-мітки топологічна перевірка не запускається — JS-парність
297
+ # (k8sEnvSegmentFromRelPath без appLabel skipує перевірку).
298
+ test_allow_deployment_without_app_label_skips_topology if {
299
+ count(manifest.deny) == 0 with input as {
300
+ "apiVersion": "apps/v1",
301
+ "kind": "Deployment",
302
+ "metadata": {"name": "api", "namespace": "dev"},
303
+ "spec": {"template": {"spec": {"containers": [{
304
+ "name": "main",
305
+ "image": "registry.example.com/api:1.0",
306
+ "resources": {"requests": {"cpu": "100m", "memory": "64Mi"}},
307
+ }]}}},
308
+ }
309
+ }
@@ -0,0 +1,51 @@
1
+ # Порт пер-документної структурної перевірки `svc-hl.yaml` з
2
+ # `npm/scripts/check-k8s.mjs` (k8s.mdc): headless Service з суфіксом
3
+ # `metadata.name` `-hl` і `spec.clusterIP: None`.
4
+ #
5
+ # Запуск (локально, лише для одного svc-hl.yaml):
6
+ # conftest test path/to/k8s/.../svc-hl.yaml -p npm/policy/k8s/svc_hl_yaml \
7
+ # --namespace k8s.svc_hl_yaml
8
+ #
9
+ # JS authoritative (`check-k8s.mjs`: `serviceSvcHlYamlHeadlessViolation`,
10
+ # вибір файла `svc-hl.yaml` через walk).
11
+ #
12
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
13
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
14
+ package k8s.svc_hl_yaml
15
+
16
+ import rego.v1
17
+
18
+ svc_hl_name_suffix := "-hl"
19
+
20
+ name_suffix_template := concat(" ", [
21
+ "Service metadata.name має закінчуватися на %q",
22
+ "(svc-hl.yaml; зараз: %q; k8s.mdc)",
23
+ ])
24
+
25
+ deny contains "Service: потрібні metadata.name з суфіксом -hl (svc-hl.yaml; k8s.mdc)" if {
26
+ input.kind == "Service"
27
+ not is_object(object.get(input, "metadata", null))
28
+ }
29
+
30
+ deny contains msg if {
31
+ input.kind == "Service"
32
+ meta := object.get(input, "metadata", null)
33
+ is_object(meta)
34
+ name := object.get(meta, "name", "")
35
+ not endswith(name, svc_hl_name_suffix)
36
+ msg := sprintf(name_suffix_template, [svc_hl_name_suffix, name])
37
+ }
38
+
39
+ deny contains "Service: додай spec.clusterIP: None (svc-hl.yaml; k8s.mdc)" if {
40
+ input.kind == "Service"
41
+ not is_object(object.get(input, "spec", null))
42
+ }
43
+
44
+ deny contains msg if {
45
+ input.kind == "Service"
46
+ spec := object.get(input, "spec", null)
47
+ is_object(spec)
48
+ cluster_ip := object.get(spec, "clusterIP", "<absent>")
49
+ cluster_ip != "None"
50
+ msg := sprintf("Service spec.clusterIP має бути None (headless, svc-hl.yaml; зараз: %v; k8s.mdc)", [cluster_ip])
51
+ }
@@ -0,0 +1,42 @@
1
+ # Тести для `k8s.svc_hl_yaml`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/svc_hl_yaml --namespace k8s.svc_hl_yaml
3
+ package k8s.svc_hl_yaml_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.svc_hl_yaml
8
+
9
+ test_deny_service_name_without_hl if {
10
+ count(svc_hl_yaml.deny) > 0 with input as {
11
+ "apiVersion": "v1",
12
+ "kind": "Service",
13
+ "metadata": {"name": "api"},
14
+ "spec": {"clusterIP": "None"},
15
+ }
16
+ }
17
+
18
+ test_deny_service_clusterip_not_none if {
19
+ count(svc_hl_yaml.deny) > 0 with input as {
20
+ "apiVersion": "v1",
21
+ "kind": "Service",
22
+ "metadata": {"name": "api-hl"},
23
+ "spec": {"clusterIP": "1.2.3.4"},
24
+ }
25
+ }
26
+
27
+ test_allow_headless_service if {
28
+ count(svc_hl_yaml.deny) == 0 with input as {
29
+ "apiVersion": "v1",
30
+ "kind": "Service",
31
+ "metadata": {"name": "api-hl"},
32
+ "spec": {"clusterIP": "None"},
33
+ }
34
+ }
35
+
36
+ test_allow_non_service if {
37
+ count(svc_hl_yaml.deny) == 0 with input as {
38
+ "apiVersion": "apps/v1",
39
+ "kind": "Deployment",
40
+ "metadata": {"name": "api"},
41
+ }
42
+ }
@@ -0,0 +1,31 @@
1
+ # Порт пер-документної структурної перевірки `svc.yaml` з
2
+ # `npm/scripts/check-k8s.mjs` (k8s.mdc): `Service` у файлі `svc.yaml` має
3
+ # мати `spec.type: ClusterIP`.
4
+ #
5
+ # Запуск (локально, лише для одного svc.yaml):
6
+ # conftest test path/to/k8s/.../svc.yaml -p npm/policy/k8s/svc_yaml \
7
+ # --namespace k8s.svc_yaml
8
+ #
9
+ # JS authoritative (`check-k8s.mjs`: `serviceSvcYamlClusterIpTypeViolation`,
10
+ # вибір файла `svc.yaml` через walk). Цю Rego JS викликає окремою таргет-командою
11
+ # лише для basename == `svc.yaml`.
12
+ #
13
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
14
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
15
+ package k8s.svc_yaml
16
+
17
+ import rego.v1
18
+
19
+ deny contains "Service: додай spec.type: ClusterIP (svc.yaml; k8s.mdc)" if {
20
+ input.kind == "Service"
21
+ not is_object(object.get(input, "spec", null))
22
+ }
23
+
24
+ deny contains msg if {
25
+ input.kind == "Service"
26
+ spec := object.get(input, "spec", null)
27
+ is_object(spec)
28
+ type_value := object.get(spec, "type", "<absent>")
29
+ type_value != "ClusterIP"
30
+ msg := sprintf("Service spec.type має бути ClusterIP (svc.yaml; зараз: %v; k8s.mdc)", [type_value])
31
+ }
@@ -0,0 +1,41 @@
1
+ # Тести для `k8s.svc_yaml`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/svc_yaml --namespace k8s.svc_yaml
3
+ package k8s.svc_yaml_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.svc_yaml
8
+
9
+ test_deny_service_missing_spec if {
10
+ count(svc_yaml.deny) > 0 with input as {
11
+ "apiVersion": "v1",
12
+ "kind": "Service",
13
+ "metadata": {"name": "api"},
14
+ }
15
+ }
16
+
17
+ test_deny_service_wrong_type if {
18
+ count(svc_yaml.deny) > 0 with input as {
19
+ "apiVersion": "v1",
20
+ "kind": "Service",
21
+ "metadata": {"name": "api"},
22
+ "spec": {"type": "LoadBalancer"},
23
+ }
24
+ }
25
+
26
+ test_allow_service_clusterip if {
27
+ count(svc_yaml.deny) == 0 with input as {
28
+ "apiVersion": "v1",
29
+ "kind": "Service",
30
+ "metadata": {"name": "api"},
31
+ "spec": {"type": "ClusterIP"},
32
+ }
33
+ }
34
+
35
+ test_allow_non_service if {
36
+ count(svc_yaml.deny) == 0 with input as {
37
+ "apiVersion": "apps/v1",
38
+ "kind": "Deployment",
39
+ "metadata": {"name": "api"},
40
+ }
41
+ }