@opensoha/contracts 0.1.26 → 0.1.29

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 (213) hide show
  1. package/COMPATIBILITY.md +14 -3
  2. package/README.md +8 -9
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/permission-catalog.json +481 -1
  5. package/compat/consumer-revisions.json +1 -1
  6. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  7. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  8. package/endpointcontrol/decode.go +64 -0
  9. package/endpointcontrol/models.go +138 -0
  10. package/endpointcontrol/models_test.go +51 -0
  11. package/endpointcontrol/protocol.schema.json +397 -0
  12. package/fixtures/json-schema/endpoint-controls/invalid/controls.json +16 -0
  13. package/fixtures/json-schema/endpoint-controls/invalid/missing-channel-decision.json +15 -0
  14. package/fixtures/json-schema/endpoint-controls/invalid/receipt-content.json +36 -0
  15. package/fixtures/json-schema/endpoint-controls/valid/controls.json +16 -0
  16. package/fixtures/json-schema/endpoint-controls/valid/receipt.json +35 -0
  17. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-activate-bad-fingerprint.json +14 -0
  18. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-gateway-request.json +14 -0
  19. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-private-key-request.json +15 -0
  20. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-profile-without-eap-credential.json +22 -0
  21. package/fixtures/json-schema/network-runtime-protocol/invalid/endpoint-dns-observation.json +25 -0
  22. package/fixtures/json-schema/network-runtime-protocol/invalid/gateway-presence.json +22 -0
  23. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-protection-zero-version.json +45 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-raw-key.json +23 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-zero-sequence.json +22 -0
  26. package/fixtures/json-schema/network-runtime-protocol/invalid/workload-wide-vpn.json +17 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-activate.json +14 -0
  28. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-request.json +14 -0
  29. package/fixtures/json-schema/network-runtime-protocol/valid/eap-profile-enrollment.json +24 -0
  30. package/fixtures/json-schema/network-runtime-protocol/valid/endpoint-presence.json +22 -0
  31. package/fixtures/json-schema/network-runtime-protocol/valid/ipv6-configuration.json +102 -0
  32. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-protected-policy.json +45 -0
  33. package/fixtures/json-schema/network-runtime-protocol/valid/site-dns-ipv6.json +25 -0
  34. package/fixtures/json-schema/network-runtime-protocol/valid/workload-direct-ztna.json +20 -0
  35. package/fixtures/openapi/DLPShadowDecision/invalid/step-up-type.json +14 -0
  36. package/fixtures/openapi/DLPShadowDecision/valid/archive-v2.json +14 -0
  37. package/fixtures/openapi/DLPShadowDecision/valid/step-up.json +14 -0
  38. package/fixtures/openapi/DLPShadowInput/invalid/self-reported-result.json +7 -0
  39. package/fixtures/openapi/DLPShadowInput/invalid/too-many-samples.json +14 -0
  40. package/fixtures/openapi/DLPShadowInput/valid/bounded.json +7 -0
  41. package/fixtures/openapi/DockerVolumeDLPAppealInput/invalid/p4-response.json +3 -0
  42. package/fixtures/openapi/DockerVolumeDLPAppealInput/valid/p4-response.json +3 -0
  43. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/invalid/p4-response.json +5 -0
  44. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/valid/p4-response.json +5 -0
  45. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/invalid/p4-response.json +3 -0
  46. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/valid/p4-response.json +3 -0
  47. package/fixtures/openapi/DockerVolumeDLPContent/invalid/archive-unsupported-channel.json +9 -0
  48. package/fixtures/openapi/DockerVolumeDLPContent/invalid/p4.json +6 -0
  49. package/fixtures/openapi/DockerVolumeDLPContent/invalid/step-up-audit.json +9 -0
  50. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unavailable-engine.json +9 -0
  51. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unknown-data-level.json +12 -0
  52. package/fixtures/openapi/DockerVolumeDLPContent/valid/classified-rule.json +16 -0
  53. package/fixtures/openapi/DockerVolumeDLPContent/valid/p4.json +8 -0
  54. package/fixtures/openapi/DockerVolumeDLPContent/valid/step-up.json +9 -0
  55. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/p4.json +9 -0
  56. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/step-up-type.json +12 -0
  57. package/fixtures/openapi/DockerVolumeDLPDecision/valid/archive-v2.json +12 -0
  58. package/fixtures/openapi/DockerVolumeDLPDecision/valid/p4.json +11 -0
  59. package/fixtures/openapi/DockerVolumeDLPDecision/valid/step-up.json +12 -0
  60. package/fixtures/openapi/DockerVolumeDLPSaveInput/invalid/p4.json +11 -0
  61. package/fixtures/openapi/DockerVolumeDLPSaveInput/valid/p4.json +11 -0
  62. package/fixtures/openapi/DockerVolumeFileReadInput/invalid/p4-response.json +5 -0
  63. package/fixtures/openapi/DockerVolumeFileReadInput/valid/p4-response.json +4 -0
  64. package/fixtures/openapi/EndpointDeviceInput/invalid/reported-management.json +5 -0
  65. package/fixtures/openapi/EndpointDeviceInput/valid/management-state.json +5 -0
  66. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/self-promote-management.json +5 -0
  67. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  68. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  69. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  70. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  71. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  72. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  73. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  74. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  75. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  76. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  77. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  78. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  79. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  80. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  81. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  82. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  83. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  84. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  85. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  86. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  87. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  88. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  89. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  90. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  91. package/fixtures/openapi/ManagedContentPasteInput/invalid/boundary.json +5 -0
  92. package/fixtures/openapi/ManagedContentPasteInput/valid/boundary.json +6 -0
  93. package/fixtures/openapi/ManagedContentPolicy/invalid/self-reported-allow.json +8 -0
  94. package/fixtures/openapi/ManagedContentPolicy/invalid/step-up-audit.json +8 -0
  95. package/fixtures/openapi/ManagedContentPolicy/invalid/unknown-extractor.json +8 -0
  96. package/fixtures/openapi/ManagedContentPolicy/valid/archive-v2.json +8 -0
  97. package/fixtures/openapi/ManagedContentPolicy/valid/network-and-level.json +10 -0
  98. package/fixtures/openapi/ManagedContentPolicy/valid/step-up.json +8 -0
  99. package/fixtures/openapi/ManagedContentReviewInput/invalid/boundary.json +5 -0
  100. package/fixtures/openapi/ManagedContentReviewInput/valid/boundary.json +5 -0
  101. package/fixtures/openapi/ManagedContentSaveInput/invalid/boundary.json +14 -0
  102. package/fixtures/openapi/ManagedContentSaveInput/valid/boundary.json +16 -0
  103. package/fixtures/openapi/ManagedContentUploadInput/invalid/boundary.json +7 -0
  104. package/fixtures/openapi/ManagedContentUploadInput/valid/boundary.json +6 -0
  105. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/unknown-management-state.json +35 -0
  106. package/fixtures/openapi/NetworkAccessPolicyInput/valid/managed-device.json +35 -0
  107. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/deviceId-characters.json +4 -0
  108. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/nasBindingId-characters.json +4 -0
  109. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/subject-override.json +1 -0
  110. package/fixtures/openapi/NetworkAdmissionPreviewRequest/valid/device-nas.json +1 -0
  111. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-attributeVersion.json +13 -0
  112. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-evaluatedAt.json +13 -0
  113. package/fixtures/openapi/NetworkAdmissionPreviewResult/valid/versioned.json +14 -0
  114. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/missing-runtime-ranges.json +1 -0
  115. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/unknown-source-type.json +9 -0
  116. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/empty-runtime-ranges.json +3 -0
  117. package/fixtures/openapi/NetworkConflictAnalysisRequest/{invalid → valid}/ipv6-range.json +1 -1
  118. package/fixtures/openapi/NetworkEAPServerTrust/invalid/no-server-validation.json +5 -0
  119. package/fixtures/openapi/NetworkEAPServerTrust/valid/named-server.json +9 -0
  120. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/all-sites-implicit.json +45 -0
  121. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/unbounded-stages.json +54 -0
  122. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/zero-heartbeat.json +47 -0
  123. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/valid/staged-protection.json +47 -0
  124. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/invalid/missing-reason.json +3 -0
  125. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/valid/expected-current.json +4 -0
  126. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/attribute-version.json +1 -0
  127. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/admission-version.json +1 -0
  128. package/fixtures/openapi/NetworkPolicySubjects/invalid/duplicate-role.json +1 -0
  129. package/fixtures/openapi/NetworkPolicySubjects/invalid/role-characters.json +5 -0
  130. package/fixtures/openapi/NetworkPolicySubjects/valid/role-bound.json +1 -0
  131. package/fixtures/openapi/NetworkProxyConfigurationInput/invalid/missing-content.json +1 -0
  132. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/bad-binding-id.json +9 -0
  133. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/empty-content.json +1 -0
  134. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/binding-only.json +11 -0
  135. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/clear-binding-disabled.json +5 -0
  136. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/enabled.json +1 -0
  137. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-governance.json +6 -0
  138. package/fixtures/openapi/NetworkSiteInput/valid/restricted-unmanaged.json +6 -0
  139. package/gen/go/sohaapi/client.go +1 -1
  140. package/gen/go/sohaapi/client_test.go +13 -0
  141. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  142. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  143. package/gen/go/sohaapi/proxy_configuration_test.go +38 -0
  144. package/gen/go/sohaapi/types.go +3222 -260
  145. package/gen/ts/sohaapi/index.ts +15506 -10104
  146. package/helmrelease/runtime/values_upgrade.go +78 -0
  147. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  148. package/helmrelease/values_update.go +31 -0
  149. package/network/README.md +34 -0
  150. package/network/network-radius-accounting.schema.json +4 -1
  151. package/network/network-runtime-protocol.schema.json +2166 -317
  152. package/network/proxy-runtime.schema.json +482 -67
  153. package/openapi/network_control_test.go +64 -0
  154. package/openapi/soha-api.json +50412 -41310
  155. package/openapi/soha-api.yaml +16859 -10699
  156. package/openapi/spec_generated.go +3 -3
  157. package/package.json +6 -3
  158. package/proxytransport/handler.go +271 -0
  159. package/proxytransport/probe.go +30 -0
  160. package/proxytransport/scope.go +89 -0
  161. package/proxytransport/transport_test.go +60 -0
  162. package/resource/agent_profiles.go +9 -15
  163. package/resource/agent_profiles_test.go +9 -3
  164. package/resource/custom_graph_page.go +18 -0
  165. package/resource/models.go +102 -57
  166. package/resource/network_topology.go +22 -0
  167. package/resource/network_topology_projection.go +36 -22
  168. package/resource/network_topology_projection_test.go +4 -0
  169. package/resource/network_topology_test.go +20 -0
  170. package/resource/network_topology_validation.go +5 -0
  171. package/resource/observed_workload.go +18 -0
  172. package/resource/pod_status_test.go +22 -0
  173. package/resource/rbac_graph.go +4 -0
  174. package/resource/rollout_optional_test.go +33 -0
  175. package/resource/runtime/network_relationship_primitives.go +66 -0
  176. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  177. package/resource/runtime/network_topology.go +17 -5
  178. package/resource/runtime/network_topology_builder.go +405 -0
  179. package/resource/runtime/network_topology_builder_backends.go +195 -0
  180. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  181. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  182. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  183. package/resource/runtime/network_topology_builder_providers.go +242 -0
  184. package/resource/runtime/network_topology_builder_test.go +186 -0
  185. package/resource/runtime/network_topology_declarations.go +329 -0
  186. package/resource/runtime/resource_edit.go +22 -0
  187. package/resource/runtime/resource_edit_test.go +32 -0
  188. package/resource/runtime/resource_graph_collect.go +325 -0
  189. package/resource/runtime/resource_graph_configuration.go +59 -0
  190. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  191. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  192. package/resource/runtime/resource_graph_custom_page.go +159 -0
  193. package/resource/runtime/resource_graph_custom_root.go +132 -0
  194. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  195. package/resource/runtime/resource_graph_enrichment.go +417 -0
  196. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  197. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  198. package/resource/runtime/resource_graph_metadata.go +125 -0
  199. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  200. package/resource/runtime/resource_graph_network.go +224 -0
  201. package/resource/runtime/resource_graph_network_collect.go +174 -0
  202. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  203. package/resource/runtime/resource_graph_network_test.go +342 -0
  204. package/resource/runtime/resource_graph_rbac.go +278 -0
  205. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  206. package/resource/runtime/resource_graph_rules.go +834 -0
  207. package/resource/runtime/resource_graph_storage.go +144 -0
  208. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  209. package/resource/runtime/resource_graph_storage_test.go +397 -0
  210. package/resource/runtime/resource_graph_test.go +627 -0
  211. package/resource/runtime/secret_certificate.go +44 -0
  212. package/resource/runtime/secret_certificate_test.go +87 -0
  213. package/resource/storage_graph.go +4 -0
@@ -0,0 +1,329 @@
1
+ package runtime
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "strings"
7
+
8
+ cr "github.com/opensoha/soha-contracts/resource"
9
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10
+ "k8s.io/apimachinery/pkg/labels"
11
+ )
12
+
13
+ // declarations adds only structured Kubernetes references. It never interprets
14
+ // arbitrary provider spec, certificate bytes, or policy as live connectivity.
15
+ func (g *topologyGraph) declarations(prioritized string) {
16
+ for _, o := range g.ordered {
17
+ if g.budgetReached() || g.ctx.Err() != nil {
18
+ return
19
+ }
20
+ if o.id() != prioritized {
21
+ g.objectDeclarations(o)
22
+ }
23
+ }
24
+ }
25
+
26
+ func (g *topologyGraph) objectDeclarations(o topologyObject) {
27
+ switch o.ref.Kind {
28
+ case "Ingress":
29
+ g.declaredRef(o, "networking.k8s.io/v1", "IngressClass", "", text(at(o.body, "spec", "ingressClassName")), "uses-class", "spec.ingressClassName", "")
30
+ for i, tls := range items(at(o.body, "spec", "tls")) {
31
+ g.declaredRef(o, "v1", "Secret", o.ref.Namespace, text(tls["secretName"]), "uses-secret", fmt.Sprintf("spec.tls[%d].secretName", i), "")
32
+ }
33
+ case "Gateway":
34
+ g.declaredRef(o, "gateway.networking.k8s.io/v1", "GatewayClass", "", text(at(o.body, "spec", "gatewayClassName")), "uses-class", "spec.gatewayClassName", "")
35
+ g.gatewayCertificates(o)
36
+ case "IngressClass":
37
+ g.classParameters(o, object(at(o.body, "spec", "parameters")), "spec.parameters")
38
+ case "GatewayClass":
39
+ g.classParameters(o, object(at(o.body, "spec", "parametersRef")), "spec.parametersRef")
40
+ case "BackendTLSPolicy":
41
+ g.backendTLSPolicy(o)
42
+ case "NetworkPolicy":
43
+ g.networkPolicy(o)
44
+ }
45
+ }
46
+
47
+ func topologySourceFor(kind, api string) (cr.NetworkTopologySourceDefinition, bool) {
48
+ for _, d := range cr.NetworkTopologySources() {
49
+ if d.Kind == kind && topologyDefinitionAPI(d, api) {
50
+ return d, true
51
+ }
52
+ }
53
+ return cr.NetworkTopologySourceDefinition{}, false
54
+ }
55
+
56
+ func (g *topologyGraph) declaredRef(o topologyObject, api, kind, namespace, name, relation, path, section string) *cr.NetworkTopologyGraphEdge {
57
+ if name == "" {
58
+ return &cr.NetworkTopologyGraphEdge{}
59
+ }
60
+ source := g.objectNode(o, "backend-group", topologyProvider(o.source), "")
61
+ ref := cr.ResourceRef{ClusterID: g.view.ClusterID, APIVersion: api, Kind: kind, Namespace: namespace, Name: name, ScopeMode: cr.ResourceScopeModeNamespace}
62
+ if namespace == "" {
63
+ ref.ScopeMode = cr.ResourceScopeModeCluster
64
+ }
65
+ target := topologyObject{ref: ref}
66
+ state := "unknown"
67
+ if api == "" {
68
+ state = "unsupported_fragment"
69
+ }
70
+ if d, ok := topologySourceFor(kind, api); ok {
71
+ if g.completeSource(d.ID, namespace) {
72
+ state = "not_found"
73
+ }
74
+ if actual, ok := g.objects[topologyKey(kind, namespace, name)]; ok && actual.ref.APIVersion == api {
75
+ target = actual
76
+ state = "resolved"
77
+ }
78
+ }
79
+ // Secret existence is never inferred from an ordinary object projection.
80
+ if kind == "Secret" && api == "v1" {
81
+ state = "unresolved"
82
+ }
83
+ id := ""
84
+ if state == "unsupported_fragment" {
85
+ id = g.unknown(source, kind, namespace, name, state)
86
+ } else if state == "unknown" || kind == "Secret" && api == "v1" {
87
+ // The public topology has no per-node product permission filter. Keep a
88
+ // declaration identity private for Graph's later authorization pass only.
89
+ id = g.unknown(source, kind, namespace, name, state)
90
+ g.hiddenRefs[id] = target.ref
91
+ } else {
92
+ id = g.objectNode(target, "backend-group", topologyProvider(o.source), "")
93
+ if state != "resolved" {
94
+ g.state(id, state)
95
+ }
96
+ }
97
+ edge := g.link(source, id, relation, "", "")
98
+ edge.SourceRef = &o.ref
99
+ edge.FieldPath = path
100
+ edge.SectionName = section
101
+ edge.ResolutionState = state
102
+ return edge
103
+ }
104
+ func topologyProvider(source string) string {
105
+ d, _ := cr.LookupNetworkTopologySource(source)
106
+ return firstTopology(d.Provider, "kubernetes")
107
+ }
108
+
109
+ func (g *topologyGraph) classParameters(o topologyObject, ref map[string]any, path string) {
110
+ kind, name := text(ref["kind"]), text(ref["name"])
111
+ group := firstTopology(text(ref["group"]), text(ref["apiGroup"]))
112
+ if kind == "" || name == "" {
113
+ return
114
+ }
115
+ namespace := text(ref["namespace"])
116
+ if text(ref["scope"]) == "Namespace" && namespace == "" {
117
+ return
118
+ }
119
+ api := topologyReferenceAPI(group, kind)
120
+ g.declaredRef(o, api, kind, namespace, name, "parameters", path, "")
121
+ }
122
+ func topologyReferenceAPI(group, kind string) string {
123
+ for _, d := range cr.NetworkTopologySources() {
124
+ if d.Group == group && d.Kind == kind {
125
+ return strings.TrimPrefix(d.Group+"/"+d.Version, "/")
126
+ }
127
+ }
128
+ if group == "" && kind == "Secret" {
129
+ return "v1"
130
+ }
131
+ // No version is guessed for an unsupported parameter kind.
132
+ return ""
133
+ }
134
+ func (g *topologyGraph) gatewayCertificates(o topologyObject) {
135
+ for i, l := range items(at(o.body, "spec", "listeners")) {
136
+ for j, r := range items(at(l, "tls", "certificateRefs")) {
137
+ group, kind := text(r["group"]), firstTopology(text(r["kind"]), "Secret")
138
+ ns := firstTopology(text(r["namespace"]), o.ref.Namespace)
139
+ authorized := ns == o.ref.Namespace || g.referenceAuthorized(o, group, kind, ns, text(r["name"]))
140
+ edge := g.declaredRef(o, topologyReferenceAPI(group, kind), kind, ns, text(r["name"]), "uses-secret", fmt.Sprintf("spec.listeners[%d].tls.certificateRefs[%d]", i, j), text(l["name"]))
141
+ edge.Protocol = text(l["protocol"])
142
+ edge.Port = text(l["port"])
143
+ if !authorized {
144
+ edge.ResolutionState = g.grantResolution(ns)
145
+ }
146
+ if group != "" || kind != "Secret" {
147
+ edge.ResolutionState = "unsupported_fragment"
148
+ }
149
+ edge.Note = "Certificate reference only; TLS activation is unverified"
150
+ }
151
+ }
152
+ }
153
+ func (g *topologyGraph) backendTLSPolicy(o topologyObject) {
154
+ refs := items(at(o.body, "spec", "targetRefs"))
155
+ path := "spec.targetRefs"
156
+ if len(refs) == 0 {
157
+ if ref := object(at(o.body, "spec", "targetRef")); len(ref) > 0 {
158
+ refs = append(refs, ref)
159
+ path = "spec.targetRef"
160
+ }
161
+ }
162
+ for i, r := range refs {
163
+ targetPath := path
164
+ if path == "spec.targetRefs" {
165
+ targetPath = fmt.Sprintf("%s[%d]", path, i)
166
+ }
167
+ kind, name, section := text(r["kind"]), text(r["name"]), text(r["sectionName"])
168
+ api := topologyReferenceAPI(text(r["group"]), kind)
169
+ edge := g.declaredRef(o, api, kind, o.ref.Namespace, name, "targets", targetPath, section)
170
+ if api == "v1" && kind == "Service" && section != "" && edge.ResolutionState == "resolved" {
171
+ service := g.objects[topologyKey(kind, o.ref.Namespace, name)]
172
+ resolveBackendTLSServiceSection(edge, service, section)
173
+ }
174
+ }
175
+ for i, r := range items(at(o.body, "spec", "validation", "caCertificateRefs")) {
176
+ group, kind := text(r["group"]), firstTopology(text(r["kind"]), "ConfigMap")
177
+ edge := g.declaredRef(o, topologyReferenceAPI(group, kind), kind, o.ref.Namespace, text(r["name"]), "uses-config", fmt.Sprintf("spec.validation.caCertificateRefs[%d]", i), "")
178
+ if group != "" || kind != "ConfigMap" {
179
+ edge.ResolutionState = "unsupported_fragment"
180
+ }
181
+ edge.Note = "CA reference only; trust and certificate validity are unverified"
182
+ }
183
+ }
184
+
185
+ func resolveBackendTLSServiceSection(edge *cr.NetworkTopologyGraphEdge, service topologyObject, section string) {
186
+ for _, port := range items(at(service.body, "spec", "ports")) {
187
+ if text(port["name"]) == section {
188
+ edge.Port = text(port["port"])
189
+ edge.Protocol = firstTopology(text(port["protocol"]), "TCP")
190
+ return
191
+ }
192
+ }
193
+ edge.ResolutionState = "unresolved"
194
+ edge.Note = "Declared Service port name was not observed"
195
+ }
196
+
197
+ // A grant is an OR of from entries AND an OR of to entries, scoped to
198
+ // the target namespace. Partial/unavailable grant coverage never confirms it.
199
+ func (g *topologyGraph) referenceAuthorized(o topologyObject, group, kind, namespace, name string) bool {
200
+ if namespace == o.ref.Namespace {
201
+ return true
202
+ }
203
+ if !g.completeSource("gateway.referencegrants", namespace) {
204
+ return false
205
+ }
206
+ fromGroup := strings.Split(o.ref.APIVersion, "/")[0]
207
+ for _, grant := range g.ordered {
208
+ if g.ctx.Err() != nil || g.budgetReached() {
209
+ return false
210
+ }
211
+ if grant.source != "gateway.referencegrants" || grant.ref.Namespace != namespace {
212
+ continue
213
+ }
214
+ from, to := false, false
215
+ for _, r := range items(at(grant.body, "spec", "from")) {
216
+ if text(r["namespace"]) == o.ref.Namespace && text(r["kind"]) == o.ref.Kind && text(r["group"]) == fromGroup {
217
+ from = true
218
+ }
219
+ }
220
+ for _, r := range items(at(grant.body, "spec", "to")) {
221
+ if text(r["group"]) == group && text(r["kind"]) == kind && (text(r["name"]) == "" || text(r["name"]) == name) {
222
+ to = true
223
+ }
224
+ }
225
+ if from && to {
226
+ edge := g.link(g.objectNode(grant, "backend-group", "gateway", ""), g.objectNode(o, "entry", "gateway", ""), "authorizes-reference", "", "")
227
+ edge.SourceRef = &grant.ref
228
+ edge.FieldPath = "spec.from+spec.to"
229
+ edge.Note = "Authorizes a declared reference; does not carry traffic"
230
+ return true
231
+ }
232
+ }
233
+ return false
234
+ }
235
+ func (g *topologyGraph) grantResolution(namespace string) string {
236
+ if g.completeSource("gateway.referencegrants", namespace) {
237
+ return "unresolved"
238
+ }
239
+ return "unknown"
240
+ }
241
+
242
+ func topologySelectorMatches(selector, values map[string]any) bool {
243
+ if selector == nil {
244
+ return false
245
+ }
246
+ raw, err := json.Marshal(selector)
247
+ if err != nil {
248
+ return false
249
+ }
250
+ var s metav1.LabelSelector
251
+ if json.Unmarshal(raw, &s) != nil {
252
+ return false
253
+ }
254
+ compiled, err := metav1.LabelSelectorAsSelector(&s)
255
+ if err != nil {
256
+ return false
257
+ }
258
+ set := labels.Set{}
259
+ for k, v := range values {
260
+ if value, ok := v.(string); ok {
261
+ set[k] = value
262
+ }
263
+ }
264
+ return compiled.Matches(set)
265
+ }
266
+
267
+ func (g *topologyGraph) networkPolicy(o topologyObject) {
268
+ id := g.objectNode(o, "backend-group", "kubernetes", "")
269
+ selected := object(at(o.body, "spec", "podSelector"))
270
+ for _, pod := range g.ordered {
271
+ if g.budgetReached() || g.ctx.Err() != nil {
272
+ return
273
+ }
274
+ if pod.source == "kubernetes.pods" && pod.ref.Namespace == o.ref.Namespace && topologySelectorMatches(selected, object(at(pod.body, "metadata", "labels"))) {
275
+ edge := g.link(id, g.objectNode(pod, "pod", "kubernetes", ""), "selects", "", "")
276
+ edge.FieldPath = "spec.podSelector"
277
+ edge.Note = "Selected Pod only; policy enforcement and connectivity are unverified"
278
+ }
279
+ }
280
+ for _, direction := range []string{"ingress", "egress"} {
281
+ peerField := "from"
282
+ if direction == "egress" {
283
+ peerField = "to"
284
+ }
285
+ for i, rule := range items(at(o.body, "spec", direction)) {
286
+ if len(items(rule[peerField])) == 0 {
287
+ g.properties(id, cr.NetworkTopologyProperty{Name: fmt.Sprintf("spec.%s[%d].%s", direction, i, peerField), Value: "All peers declared; connectivity is unverified"})
288
+ }
289
+ for j, peer := range items(rule[peerField]) {
290
+ path := fmt.Sprintf("spec.%s[%d].%s[%d]", direction, i, peerField, j)
291
+ if block := object(peer["ipBlock"]); len(block) > 0 {
292
+ g.properties(id, cr.NetworkTopologyProperty{Name: path + ".ipBlock", Value: text(block["cidr"]) + " except " + strings.Join(texts(block["except"]), ",") + "; declared CIDRs only"})
293
+ continue
294
+ }
295
+ g.policyPeers(o, id, peer, path)
296
+ }
297
+ }
298
+ }
299
+ }
300
+ func (g *topologyGraph) policyPeers(o topologyObject, id string, peer map[string]any, path string) {
301
+ ps, hasPod := peer["podSelector"]
302
+ ns, hasNS := peer["namespaceSelector"]
303
+ if !hasPod && !hasNS {
304
+ g.properties(id, cr.NetworkTopologyProperty{Name: path, Value: "All peers declared; connectivity is unverified"})
305
+ return
306
+ }
307
+ for _, pod := range g.ordered {
308
+ if g.budgetReached() || g.ctx.Err() != nil {
309
+ return
310
+ }
311
+ if pod.source != "kubernetes.pods" {
312
+ continue
313
+ }
314
+ if hasNS {
315
+ namespace, ok := g.objects[topologyKey("Namespace", "", pod.ref.Namespace)]
316
+ if !ok || !g.completeSource("kubernetes.namespaces", "") || !topologySelectorMatches(object(ns), object(at(namespace.body, "metadata", "labels"))) {
317
+ continue
318
+ }
319
+ } else if pod.ref.Namespace != o.ref.Namespace {
320
+ continue
321
+ }
322
+ if hasPod && !topologySelectorMatches(object(ps), object(at(pod.body, "metadata", "labels"))) {
323
+ continue
324
+ }
325
+ edge := g.link(id, g.objectNode(pod, "pod", "kubernetes", ""), "policy-peer", "", "")
326
+ edge.FieldPath = path
327
+ edge.Note = "Peer selector match (namespace AND pod within this entry; entries OR); no live connectivity inferred"
328
+ }
329
+ }
@@ -10,6 +10,7 @@ import (
10
10
  apierrors "k8s.io/apimachinery/pkg/api/errors"
11
11
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12
12
  "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13
+ "k8s.io/apimachinery/pkg/runtime/schema"
13
14
  "k8s.io/apimachinery/pkg/types"
14
15
  "k8s.io/apimachinery/pkg/util/sets"
15
16
  "k8s.io/client-go/dynamic"
@@ -17,6 +18,27 @@ import (
17
18
  "k8s.io/utils/ptr"
18
19
  )
19
20
 
21
+ // BindResourceEditIdentity keeps legacy name-based edits and validates observed edits
22
+ // before attaching the live object's optimistic concurrency preconditions.
23
+ func BindResourceEditIdentity(current, desired *unstructured.Unstructured, resource schema.GroupResource, requireObserved bool) error {
24
+ if requireObserved {
25
+ if desired.GetUID() == "" || desired.GetResourceVersion() == "" {
26
+ return apierrors.NewBadRequest("observed YAML must include metadata.uid and metadata.resourceVersion")
27
+ }
28
+ if desired.GetUID() != current.GetUID() || desired.GetResourceVersion() != current.GetResourceVersion() {
29
+ return apierrors.NewConflict(resource, current.GetName(), errors.New("resource changed since the YAML was read"))
30
+ }
31
+ }
32
+ desired.SetUID(current.GetUID())
33
+ desired.SetResourceVersion(current.GetResourceVersion())
34
+ return nil
35
+ }
36
+
37
+ // IsResourceEditOwnershipConflict separates a dry-run ownership report from stale identity.
38
+ func IsResourceEditOwnershipConflict(err error) bool {
39
+ return apierrors.HasStatusCause(err, metav1.CauseTypeFieldManagerConflict)
40
+ }
41
+
20
42
  // ApplyResourceEdit preserves SSA conflicts while migrating this editor's own Create/Update fields.
21
43
  // Dry-run migration validates a complete manifest with Update; it never changes live ownership.
22
44
  func ApplyResourceEdit(ctx context.Context, resource dynamic.ResourceInterface, current, desired *unstructured.Unstructured, manager string, dryRun bool) (*unstructured.Unstructured, error) {
@@ -30,6 +30,38 @@ func editConflict(manager string) error {
30
30
  return apierrors.NewApplyConflict([]metav1.StatusCause{{Type: metav1.CauseTypeFieldManagerConflict, Field: ".data.phase", Message: fmt.Sprintf("conflict with %q using v1", manager)}}, "field conflict")
31
31
  }
32
32
 
33
+ func TestBindResourceEditIdentity(t *testing.T) {
34
+ resource := schema.GroupResource{Resource: "configmaps"}
35
+ current := editFixture()
36
+ for _, tc := range []struct {
37
+ name, uid, version string
38
+ strict bool
39
+ wantError func(error) bool
40
+ }{
41
+ {name: "legacy name only"},
42
+ {name: "observed current object", uid: "original", version: "1", strict: true},
43
+ {name: "missing observed identity", uid: "original", strict: true, wantError: apierrors.IsBadRequest},
44
+ {name: "same name replacement", uid: "deleted", version: "1", strict: true, wantError: apierrors.IsConflict},
45
+ {name: "same UID newer version", uid: "original", version: "0", strict: true, wantError: apierrors.IsConflict},
46
+ } {
47
+ t.Run(tc.name, func(t *testing.T) {
48
+ desired := current.DeepCopy()
49
+ desired.SetUID(types.UID(tc.uid))
50
+ desired.SetResourceVersion(tc.version)
51
+ err := BindResourceEditIdentity(current, desired, resource, tc.strict)
52
+ if tc.wantError != nil {
53
+ if !tc.wantError(err) || string(desired.GetUID()) != tc.uid || desired.GetResourceVersion() != tc.version {
54
+ t.Fatalf("stale request unexpectedly rebound: uid=%q version=%q err=%v", desired.GetUID(), desired.GetResourceVersion(), err)
55
+ }
56
+ return
57
+ }
58
+ if err != nil || desired.GetUID() != current.GetUID() || desired.GetResourceVersion() != current.GetResourceVersion() {
59
+ t.Fatalf("current identity not preserved: uid=%q version=%q err=%v", desired.GetUID(), desired.GetResourceVersion(), err)
60
+ }
61
+ })
62
+ }
63
+ }
64
+
33
65
  func TestApplyResourceEditMigratesOnlyItsOwnCreationFields(t *testing.T) {
34
66
  for _, test := range []struct {
35
67
  name string