@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
@@ -0,0 +1,99 @@
1
+ # Тести для `abie.health_check_policy`. Запуск:
2
+ # conftest verify -p npm/policy/abie/health_check_policy
3
+ package abie.health_check_policy_test
4
+
5
+ import rego.v1
6
+
7
+ import data.abie.health_check_policy
8
+
9
+ valid_hcp := {
10
+ "apiVersion": "networking.gke.io/v1",
11
+ "kind": "HealthCheckPolicy",
12
+ "metadata": {"name": "api"},
13
+ "spec": {
14
+ "default": {"config": {
15
+ "type": "HTTP",
16
+ "httpHealthCheck": {"requestPath": "/healthz", "port": 8080},
17
+ }},
18
+ "targetRef": {"group": "", "kind": "Service", "name": "api-hl"},
19
+ },
20
+ }
21
+
22
+ # ── happy path ────────────────────────────────────────────────────────────
23
+
24
+ test_allow_canonical if {
25
+ count(health_check_policy.deny) == 0 with input as valid_hcp
26
+ }
27
+
28
+ # ── apiVersion ────────────────────────────────────────────────────────────
29
+
30
+ test_deny_wrong_api_version if {
31
+ bad := json.patch(valid_hcp, [{"op": "replace", "path": "/apiVersion", "value": "networking.gke.io/v1beta1"}])
32
+ count(health_check_policy.deny) > 0 with input as bad
33
+ }
34
+
35
+ # ── metadata.name ─────────────────────────────────────────────────────────
36
+
37
+ test_deny_empty_name if {
38
+ bad := json.patch(valid_hcp, [{"op": "replace", "path": "/metadata/name", "value": ""}])
39
+ count(health_check_policy.deny) > 0 with input as bad
40
+ }
41
+
42
+ # ── spec.default.config.type ──────────────────────────────────────────────
43
+
44
+ test_deny_config_type_not_http if {
45
+ bad := json.patch(valid_hcp, [{"op": "replace", "path": "/spec/default/config/type", "value": "TCP"}])
46
+ count(health_check_policy.deny) > 0 with input as bad
47
+ }
48
+
49
+ # ── requestPath ───────────────────────────────────────────────────────────
50
+
51
+ test_deny_empty_request_path if {
52
+ bad := json.patch(valid_hcp, [{
53
+ "op": "replace",
54
+ "path": "/spec/default/config/httpHealthCheck/requestPath",
55
+ "value": "",
56
+ }])
57
+ count(health_check_policy.deny) > 0 with input as bad
58
+ }
59
+
60
+ test_deny_request_path_without_slash if {
61
+ bad := json.patch(valid_hcp, [{
62
+ "op": "replace",
63
+ "path": "/spec/default/config/httpHealthCheck/requestPath",
64
+ "value": "healthz",
65
+ }])
66
+ count(health_check_policy.deny) > 0 with input as bad
67
+ }
68
+
69
+ # ── port ──────────────────────────────────────────────────────────────────
70
+
71
+ test_deny_port_not_8080 if {
72
+ bad := json.patch(valid_hcp, [{
73
+ "op": "replace",
74
+ "path": "/spec/default/config/httpHealthCheck/port",
75
+ "value": 9090,
76
+ }])
77
+ count(health_check_policy.deny) > 0 with input as bad
78
+ }
79
+
80
+ # ── targetRef ─────────────────────────────────────────────────────────────
81
+
82
+ test_deny_target_ref_kind_not_service if {
83
+ bad := json.patch(valid_hcp, [{"op": "replace", "path": "/spec/targetRef/kind", "value": "Gateway"}])
84
+ count(health_check_policy.deny) > 0 with input as bad
85
+ }
86
+
87
+ test_deny_target_ref_name_without_hl if {
88
+ bad := json.patch(valid_hcp, [{"op": "replace", "path": "/spec/targetRef/name", "value": "api"}])
89
+ count(health_check_policy.deny) > 0 with input as bad
90
+ }
91
+
92
+ # Не HCP — пакет не діє.
93
+ test_allow_other_kind if {
94
+ count(health_check_policy.deny) == 0 with input as {
95
+ "apiVersion": "v1",
96
+ "kind": "ConfigMap",
97
+ "metadata": {"name": "x"},
98
+ }
99
+ }
@@ -0,0 +1,64 @@
1
+ # Тести для `abie.http_route_base`. Запуск:
2
+ # conftest verify -p npm/policy/abie/http_route_base
3
+ package abie.http_route_base_test
4
+
5
+ import rego.v1
6
+
7
+ import data.abie.http_route_base
8
+
9
+ mk_route(hostnames) := {
10
+ "apiVersion": "gateway.networking.k8s.io/v1",
11
+ "kind": "HTTPRoute",
12
+ "metadata": {"name": "r", "namespace": "dev"},
13
+ "spec": {"hostnames": hostnames},
14
+ }
15
+
16
+ # ── allow ────────────────────────────────────────────────────────────────
17
+
18
+ test_allow_apex if {
19
+ count(http_route_base.deny) == 0 with input as mk_route(["aiml.live"])
20
+ }
21
+
22
+ test_allow_subdomain if {
23
+ count(http_route_base.deny) == 0 with input as mk_route(["api.aiml.live"])
24
+ }
25
+
26
+ test_allow_wildcard if {
27
+ count(http_route_base.deny) == 0 with input as mk_route(["*.aiml.live"])
28
+ }
29
+
30
+ test_allow_uppercase_apex if {
31
+ count(http_route_base.deny) == 0 with input as mk_route(["AIML.LIVE"])
32
+ }
33
+
34
+ test_allow_multiple_subdomains if {
35
+ count(http_route_base.deny) == 0 with input as mk_route(["api.aiml.live", "admin.aiml.live"])
36
+ }
37
+
38
+ # ── deny ─────────────────────────────────────────────────────────────────
39
+
40
+ test_deny_other_apex if {
41
+ count(http_route_base.deny) > 0 with input as mk_route(["example.com"])
42
+ }
43
+
44
+ test_deny_wrong_subdomain if {
45
+ count(http_route_base.deny) > 0 with input as mk_route(["api.example.com"])
46
+ }
47
+
48
+ test_deny_mixed_one_bad if {
49
+ count(http_route_base.deny) > 0 with input as mk_route(["api.aiml.live", "evil.com"])
50
+ }
51
+
52
+ test_deny_aiml_live_substring if {
53
+ # "aiml.live.example.com" не має закінчуватись на ".aiml.live" — це інший домен.
54
+ count(http_route_base.deny) > 0 with input as mk_route(["aiml.live.example.com"])
55
+ }
56
+
57
+ # Не HTTPRoute — пакет не діє.
58
+ test_allow_non_httproute if {
59
+ count(http_route_base.deny) == 0 with input as {
60
+ "apiVersion": "v1",
61
+ "kind": "Service",
62
+ "metadata": {"name": "x"},
63
+ }
64
+ }
@@ -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"