@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.
- package/.claude-template/npm-CLAUDE.md +4 -0
- package/CHANGELOG.md +69 -0
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +3 -2
- package/mdc/abie.mdc +13 -0
- package/mdc/ci4.mdc +8 -0
- package/mdc/tauri.mdc +20 -0
- package/package.json +1 -1
- package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
- package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
- package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
- package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
- package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
- package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
- package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
- package/policy/k8s/base_manifest/base_manifest.rego +154 -0
- package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
- package/policy/k8s/gateway/gateway.rego +151 -0
- package/policy/k8s/gateway/gateway_test.rego +122 -0
- package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
- package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
- package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
- package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
- package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
- package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
- package/policy/k8s/kustomization/kustomization.rego +220 -0
- package/policy/k8s/kustomization/kustomization_test.rego +128 -0
- package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
- package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
- package/policy/k8s/manifest/manifest.rego +151 -4
- package/policy/k8s/manifest/manifest_test.rego +309 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
- package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
- package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
- package/scripts/check-abie.mjs +102 -369
- package/scripts/check-ga.mjs +89 -9
- package/scripts/check-k8s.mjs +128 -569
- package/scripts/lint-conftest.mjs +98 -3
- package/scripts/lint-ga.mjs +18 -132
- package/scripts/lint-rego.mjs +19 -4
- 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
|
-
# `
|
|
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
|
+
}
|