@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,147 @@
1
+ package runtime
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+
7
+ cr "github.com/opensoha/soha-contracts/resource"
8
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9
+ )
10
+
11
+ func TestNetworkRelationshipServiceSelectorParityAndMalformedValues(t *testing.T) {
12
+ for _, tc := range []struct {
13
+ name, selector string
14
+ match bool
15
+ }{
16
+ {"missing", "null", false}, {"empty", "{}", false},
17
+ {"match", `{"app":"web"}`, true}, {"different", `{"app":"other"}`, false},
18
+ {"allLabelsRequired", `{"app":"web","tier":"api"}`, false},
19
+ {"missingEmptyLabel", `{"missing":""}`, false},
20
+ {"nonString", `{"app":42}`, false},
21
+ {"doNotDropMalformedTerm", `{"app":"web","malformed":42}`, false},
22
+ } {
23
+ t.Run(tc.name, func(t *testing.T) {
24
+ pod := graphTestObject("v1", "Pod", "pod", "pod-uid")
25
+ pod.SetLabels(map[string]string{"app": "web"})
26
+ service := graphTestObject("v1", "Service", "svc", "svc-uid")
27
+ var selector any
28
+ if err := json.Unmarshal([]byte(tc.selector), &selector); err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ service.Object["spec"] = map[string]any{"selector": selector}
32
+ old, err := BuildResourceGraphFromObjects(graphTestRoot("Pod", "pod"), []*unstructured.Unstructured{pod, service})
33
+ if err != nil {
34
+ t.Fatal(err)
35
+ }
36
+ if got := graphTestEdge(old, "selects", "svc", "pod"); got != tc.match {
37
+ t.Fatalf("Workload selector match=%v want=%v", got, tc.match)
38
+ }
39
+ networkService := topologyFixture("kubernetes.services", "ns", "svc", `{"spec":{"selector":`+tc.selector+`}}`)
40
+ networkPod := topologyFixture("kubernetes.pods", "ns", "pod", `{"metadata":{"labels":{"app":"web"}}}`)
41
+ network := networkFixtureGraph(t, networkService, networkPod)
42
+ if got := len(networkEdges(network, "selects")) > 0; got != tc.match {
43
+ t.Fatalf("Network selector match=%v want=%v", got, tc.match)
44
+ }
45
+ })
46
+ }
47
+ }
48
+
49
+ func TestNetworkRelationshipIngressDefaultAndPathsPreserveBothViews(t *testing.T) {
50
+ for _, tc := range []struct {
51
+ name, spec string
52
+ names, paths []string
53
+ }{
54
+ {"defaultOnly", `{"defaultBackend":{"service":{"name":"default","port":{"name":"http"}}}}`, []string{"default"}, []string{"spec.defaultBackend"}},
55
+ {"pathsOnly", `{"rules":[{"host":"a.test","http":{"paths":[{"path":"/a","pathType":"Prefix","backend":{"service":{"name":"first","port":{"number":80}}}},{"path":"/b","backend":{"service":{"name":"second","port":{"number":80}}}}]}},{"host":"b.test","http":{"paths":[{"backend":{"service":{"name":"last","port":{"number":80}}}}]}}]}`, []string{"first", "second", "last"}, []string{"spec.rules[0].http.paths[0].backend", "spec.rules[0].http.paths[1].backend", "spec.rules[1].http.paths[0].backend"}},
56
+ {"defaultAndPath", `{"defaultBackend":{"service":{"name":"default","port":{"name":"http"}}},"rules":[{"host":"a.test","http":{"paths":[{"path":"/a","backend":{"service":{"name":"first","port":{"number":80}}}}]}}]}`, []string{"default", "first"}, []string{"spec.defaultBackend", "spec.rules[0].http.paths[0].backend"}},
57
+ } {
58
+ t.Run(tc.name, func(t *testing.T) {
59
+ var spec map[string]any
60
+ if err := json.Unmarshal([]byte(tc.spec), &spec); err != nil {
61
+ t.Fatal(err)
62
+ }
63
+ pod := graphTestObject("v1", "Pod", "pod", "pod-uid")
64
+ pod.SetLabels(map[string]string{"app": "web"})
65
+ ingress := graphTestObject("networking.k8s.io/v1", "Ingress", "ing", "ing-uid")
66
+ ingress.Object["spec"] = spec
67
+ objects := []*unstructured.Unstructured{pod, ingress}
68
+ sources := []cr.NetworkTopologySourceResult{}
69
+ for _, name := range tc.names {
70
+ service := graphTestObject("v1", "Service", name, name+"-uid")
71
+ service.Object["spec"] = map[string]any{"selector": map[string]any{"app": "web"}}
72
+ objects = append(objects, service)
73
+ sources = append(sources, topologyFixture("kubernetes.services", "ns", name, `{"spec":{"ports":[{"name":"http","port":80}]}}`))
74
+ }
75
+ workload, err := BuildResourceGraphFromObjects(graphTestRoot("Pod", "pod"), objects)
76
+ if err != nil {
77
+ t.Fatal(err)
78
+ }
79
+ for _, name := range tc.names {
80
+ if !graphTestEdge(workload, "routes-to", "ing", name) {
81
+ t.Fatalf("Workload dropped backend %s: %+v", name, workload.Edges)
82
+ }
83
+ }
84
+ if edges := networkEdges(workload, "routes-to"); len(edges) != len(tc.names) {
85
+ t.Fatalf("Workload changed name-only edge count: %+v", edges)
86
+ } else {
87
+ for _, edge := range edges {
88
+ if edge.Port != "" || edge.FieldPath != "" {
89
+ t.Fatalf("Workload adopted network-only facts: %+v", edge)
90
+ }
91
+ }
92
+ }
93
+ networkIngress := topologyFixture("kubernetes.ingresses", "ns", "ing", `{"spec":`+tc.spec+`}`)
94
+ network := networkFixtureGraph(t, networkIngress, sources...)
95
+ actual := map[string]bool{}
96
+ for _, edge := range networkEdges(network, "routes-to") {
97
+ actual[edge.FieldPath] = true
98
+ if edge.Port == "" {
99
+ t.Fatal("Network dropped backend port")
100
+ }
101
+ }
102
+ if len(actual) != len(tc.paths) {
103
+ t.Fatalf("Network changed backend declarations: %+v", actual)
104
+ }
105
+ for _, path := range tc.paths {
106
+ if !actual[path] {
107
+ t.Fatalf("Network dropped declaration %s", path)
108
+ }
109
+ }
110
+ view := fixtureView(append([]cr.NetworkTopologySourceResult{networkIngress}, sources...)...)
111
+ if tc.name != "defaultOnly" {
112
+ found := false
113
+ for _, node := range view.Nodes {
114
+ if node.Rule != nil {
115
+ for _, group := range node.Rule.MatchGroups {
116
+ for _, match := range group {
117
+ if match.Type == "host" && match.Value == "a.test" {
118
+ found = true
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if !found {
125
+ t.Fatal("Topology lost match context")
126
+ }
127
+ }
128
+ })
129
+ }
130
+ }
131
+
132
+ func TestNetworkRelationshipIngressIterationHonorsBudgetAndUnsupportedBackend(t *testing.T) {
133
+ body := map[string]any{}
134
+ if err := json.Unmarshal([]byte(`{"spec":{"defaultBackend":{"resource":{"apiGroup":"example.io","kind":"Bucket","name":"custom"}},"rules":[{"http":{"paths":[{"backend":{"service":{"name":"later"}}}]}}]}}`), &body); err != nil {
135
+ t.Fatal(err)
136
+ }
137
+ count := 0
138
+ forEachIngressBackendDeclaration(body, func() bool { return count == 0 }, func(ref ingressBackendDeclaration) {
139
+ count++
140
+ if ref.fieldPath != "spec.defaultBackend" || ref.serviceDeclared || ref.serviceName != "" {
141
+ t.Fatalf("unsupported backend fabricated a Service: %+v", ref)
142
+ }
143
+ })
144
+ if count != 1 {
145
+ t.Fatalf("iterator ignored caller budget: %d", count)
146
+ }
147
+ }
@@ -29,6 +29,12 @@ func ReadNetworkTopologySnapshot(ctx context.Context, client rest.Interface, clu
29
29
  ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
30
30
  defer cancel()
31
31
  output := resource.NetworkTopologySnapshot{Version: resource.NetworkTopologySnapshotFeature, Sources: make([]resource.NetworkTopologySourceResult, len(input.Queries))}
32
+ for _, query := range input.Queries {
33
+ if query.Name != "" {
34
+ output.Version = resource.NetworkTopologyNamedRootFeature
35
+ break
36
+ }
37
+ }
32
38
  budget := &topologyReadBudget{remaining: resource.NetworkTopologyMaxTotalBytes}
33
39
  sem := make(chan struct{}, 4)
34
40
  var wg sync.WaitGroup
@@ -47,6 +53,9 @@ func ReadNetworkTopologySnapshot(ctx context.Context, client rest.Interface, clu
47
53
  }(index, query)
48
54
  }
49
55
  wg.Wait()
56
+ if err := ctx.Err(); err != nil {
57
+ return resource.NetworkTopologySnapshot{}, err
58
+ }
50
59
  total := 0
51
60
  for i := range output.Sources {
52
61
  source := &output.Sources[i]
@@ -69,10 +78,13 @@ func ReadNetworkTopologySnapshot(ctx context.Context, client rest.Interface, clu
69
78
 
70
79
  func topologySourceFailure(query resource.NetworkTopologySourceQuery, state, reason string) resource.NetworkTopologySourceResult {
71
80
  definition, _ := resource.LookupNetworkTopologySource(query.SourceID)
72
- return resource.NetworkTopologySourceResult{Coverage: resource.NetworkTopologyCoverage{Provider: definition.Provider, SourceID: query.SourceID, Namespace: query.Namespace, State: state, ReasonCode: reason, CheckedAt: time.Now().UTC().Format(time.RFC3339), SupportedKinds: []string{definition.Kind}}, Objects: []resource.NetworkTopologySourceObject{}}
81
+ return resource.NetworkTopologySourceResult{Coverage: resource.NetworkTopologyCoverage{Provider: definition.Provider, SourceID: query.SourceID, Name: query.Name, Namespace: query.Namespace, State: state, ReasonCode: reason, CheckedAt: time.Now().UTC().Format(time.RFC3339), SupportedKinds: []string{definition.Kind}}, Objects: []resource.NetworkTopologySourceObject{}}
73
82
  }
74
83
 
75
84
  func readTopologySource(ctx context.Context, client rest.Interface, clusterID string, query resource.NetworkTopologySourceQuery, budget *topologyReadBudget) resource.NetworkTopologySourceResult {
85
+ if query.Name != "" {
86
+ return readNetworkGraphNamedSource(ctx, client, clusterID, query, budget)
87
+ }
76
88
  definition, _ := resource.LookupNetworkTopologySource(query.SourceID)
77
89
  result := topologySourceFailure(query, "available", "")
78
90
  version, err := discoverTopologySource(ctx, client, definition, budget)
@@ -142,10 +154,10 @@ func topologyAPIBase(group, version string) string {
142
154
  }
143
155
 
144
156
  func discoverTopologySource(ctx context.Context, client rest.Interface, definition resource.NetworkTopologySourceDefinition, budget *topologyReadBudget) (string, error) {
145
- versions := []string{definition.Version}
146
- if definition.FallbackVersion != "" {
147
- versions = append(versions, definition.FallbackVersion)
148
- }
157
+ return discoverTopologySourceVersions(ctx, client, definition, budget, resource.NetworkTopologySourceVersions(definition))
158
+ }
159
+
160
+ func discoverTopologySourceVersions(ctx context.Context, client rest.Interface, definition resource.NetworkTopologySourceDefinition, budget *topologyReadBudget, versions []string) (string, error) {
149
161
  for _, version := range versions {
150
162
  raw, err := boundedTopologyGET(ctx, client, topologyAPIBase(definition.Group, version), false, budget)
151
163
  if apierrors.IsNotFound(err) {
@@ -0,0 +1,405 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "sort"
8
+ "strings"
9
+ "time"
10
+
11
+ cr "github.com/opensoha/soha-contracts/resource"
12
+ )
13
+
14
+ type topologyObject struct {
15
+ source string
16
+ ref cr.ResourceRef
17
+ generation int64
18
+ body map[string]any
19
+ }
20
+ type topologyGraph struct {
21
+ view cr.NetworkTopologyView
22
+ objects map[string]topologyObject
23
+ ordered []topologyObject
24
+ nodes map[string]cr.NetworkTopologyGraphNode
25
+ expanded map[string]bool
26
+ legacyWalks int
27
+ ctx context.Context
28
+ nodeLimit int
29
+ edgeLimit int
30
+ hiddenRefs map[string]cr.ResourceRef
31
+ }
32
+
33
+ // The public topology permits coverage for both issued and denied source queries.
34
+ const topologyCoverageLimit = 128
35
+
36
+ // BuildNetworkTopology joins authorized, projected sources. It never treats absent coverage as an empty resource list.
37
+ func BuildNetworkTopology(clusterID, namespace string, snapshot cr.NetworkTopologySnapshot) cr.NetworkTopologyView {
38
+ return buildNetworkTopology(context.Background(), clusterID, namespace, snapshot, cr.NetworkTopologyDefaultNodes, cr.NetworkTopologyDefaultEdges, nil).view
39
+ }
40
+
41
+ func buildNetworkTopology(ctx context.Context, clusterID, namespace string, snapshot cr.NetworkTopologySnapshot, nodeLimit, edgeLimit int, root *cr.ResourceRef) *topologyGraph {
42
+ now := time.Now().UTC().Format(time.RFC3339)
43
+ g := &topologyGraph{hiddenRefs: map[string]cr.ResourceRef{}, ctx: ctx, nodeLimit: nodeLimit, edgeLimit: edgeLimit, view: cr.NetworkTopologyView{ClusterID: clusterID, Namespace: namespace, GraphVersion: "v1", Source: "kubernetes-snapshot", GeneratedAt: now, Nodes: []cr.NetworkTopologyGraphNode{}, Edges: []cr.NetworkTopologyGraphEdge{}, Traces: []cr.NetworkTopologyTraceView{}}, objects: map[string]topologyObject{}, nodes: map[string]cr.NetworkTopologyGraphNode{}, expanded: map[string]bool{}}
44
+ totalBytes := 0
45
+ for sourceIndex, source := range snapshot.Sources {
46
+ if sourceIndex >= topologyCoverageLimit {
47
+ g.view.Truncated = true
48
+ break
49
+ }
50
+ g.view.Coverage = append(g.view.Coverage, source.Coverage)
51
+ g.view.Truncated = g.view.Truncated || source.Coverage.Truncated
52
+ if source.Coverage.ObservedAt > g.view.ObservedAt {
53
+ g.view.ObservedAt = source.Coverage.ObservedAt
54
+ }
55
+ for objectIndex, item := range source.Objects {
56
+ if objectIndex >= cr.NetworkTopologyMaxSourceItems {
57
+ g.view.Truncated = true
58
+ break
59
+ }
60
+ if _, exists := g.objects[topologyKey(item.Resource.Kind, item.Resource.Namespace, item.Resource.Name)]; exists {
61
+ continue
62
+ }
63
+ totalBytes += len(item.Content)
64
+ if totalBytes > cr.NetworkTopologyMaxTotalBytes {
65
+ g.view.Truncated = true
66
+ break
67
+ }
68
+ if ctx.Err() != nil || len(g.ordered) >= ResourceGraphWorkingNodeLimit {
69
+ g.view.Truncated = true
70
+ break
71
+ }
72
+ if object, ok := decodeTopologyObject(clusterID, source, item); ok {
73
+ key := topologyKey(object.ref.Kind, object.ref.Namespace, object.ref.Name)
74
+ if _, exists := g.objects[key]; !exists {
75
+ g.objects[key] = object
76
+ g.ordered = append(g.ordered, object)
77
+ if root != nil && object.ref.Kind == root.Kind && object.ref.Namespace == root.Namespace && object.ref.Name == root.Name && object.ref.APIVersion == root.APIVersion {
78
+ g.objectNode(object, "backend-group", topologyProvider(object.source), "")
79
+ }
80
+ }
81
+ } else if source.Coverage.State == "available" || source.Coverage.State == "partial" {
82
+ index := len(g.view.Coverage) - 1
83
+ g.view.Coverage[index].State = "partial"
84
+ g.view.Coverage[index].ReasonCode = "invalid_projection"
85
+ g.view.Truncated = true
86
+ }
87
+ }
88
+ }
89
+ sort.Slice(g.ordered, func(i, j int) bool { return g.ordered[i].id() < g.ordered[j].id() })
90
+ prioritized := g.prioritizeRoot(root)
91
+ for _, object := range g.ordered {
92
+ if object.id() == prioritized {
93
+ continue
94
+ }
95
+ g.addRoute(object)
96
+ }
97
+ for _, object := range g.ordered {
98
+ if object.ref.Kind == "Service" && object.id() != prioritized {
99
+ g.service(object, "")
100
+ }
101
+ }
102
+ for _, o := range g.ordered {
103
+ if o.source == "kubernetes.endpointslices" && !g.expanded["slice:"+o.id()] {
104
+ g.endpointSlice(topologyObject{}, o, "", "")
105
+ }
106
+ }
107
+ g.declarations(prioritized)
108
+ for i := range g.view.Edges {
109
+ e := &g.view.Edges[i]
110
+ e.ID = strings.Join([]string{e.Source, e.Target, e.Relation, e.Protocol, e.Port, e.SectionName, e.FieldPath, fmt.Sprint(i)}, "|")
111
+ }
112
+ for _, node := range g.nodes {
113
+ g.view.Nodes = append(g.view.Nodes, node)
114
+ g.countNode(node)
115
+ }
116
+ sort.Slice(g.view.Nodes, func(i, j int) bool { return g.view.Nodes[i].ID < g.view.Nodes[j].ID })
117
+ sort.Slice(g.view.Edges, func(i, j int) bool { return g.view.Edges[i].ID < g.view.Edges[j].ID })
118
+ if root == nil {
119
+ g.projectLegacy()
120
+ }
121
+ return g
122
+ }
123
+
124
+ func (o topologyObject) id() string {
125
+ return strings.Join([]string{o.ref.ClusterID, o.ref.APIVersion, o.ref.Kind, o.ref.Namespace, o.ref.Name, o.ref.UID}, "|")
126
+ }
127
+ func topologyKey(kind, namespace, name string) string { return kind + "/" + namespace + "/" + name }
128
+ func (g *topologyGraph) addNode(node cr.NetworkTopologyGraphNode) string {
129
+ if g.ctx.Err() != nil {
130
+ return ""
131
+ }
132
+ if _, ok := g.nodes[node.ID]; ok {
133
+ return node.ID
134
+ }
135
+ if len(g.nodes) >= g.nodeLimit {
136
+ g.view.Truncated = true
137
+ return ""
138
+ }
139
+ g.nodes[node.ID] = node
140
+ return node.ID
141
+ }
142
+ func (g *topologyGraph) objectNode(o topologyObject, nodeType, provider, protocol string) string {
143
+ ref := o.ref
144
+ return g.addNode(cr.NetworkTopologyGraphNode{ID: o.id(), Type: nodeType, Label: ref.Name, Provider: provider, Protocol: protocol, Resource: &ref, Generation: o.generation, ResolutionState: "resolved"})
145
+ }
146
+ func (g *topologyGraph) link(source, target, relation, protocol, port string) *cr.NetworkTopologyGraphEdge {
147
+ if source == "" || target == "" {
148
+ return &cr.NetworkTopologyGraphEdge{}
149
+ }
150
+ if _, ok := g.nodes[source]; !ok {
151
+ return &cr.NetworkTopologyGraphEdge{}
152
+ }
153
+ if _, ok := g.nodes[target]; !ok {
154
+ return &cr.NetworkTopologyGraphEdge{}
155
+ }
156
+ if len(g.view.Edges) >= g.edgeLimit {
157
+ g.view.Truncated = true
158
+ return &cr.NetworkTopologyGraphEdge{}
159
+ }
160
+ edge := cr.NetworkTopologyGraphEdge{ID: fmt.Sprintf("%s>%s:%s:%d", source, target, relation, len(g.view.Edges)), Source: source, Target: target, Relation: relation, Protocol: protocol, Port: port, SourceRef: g.nodes[source].Resource, ResolutionState: g.nodes[target].ResolutionState}
161
+ if ref := g.nodes[target].Resource; ref != nil && ref.Kind == "Service" {
162
+ if service, ok := g.objects[topologyKey("Service", ref.Namespace, ref.Name)]; ok && target == service.id() {
163
+ edge.ResolutionState = "resolved"
164
+ if port != "" && !servicePortExists(service, port, protocol) {
165
+ edge.ResolutionState = "unresolved"
166
+ }
167
+ }
168
+ }
169
+ g.view.Edges = append(g.view.Edges, edge)
170
+ return &g.view.Edges[len(g.view.Edges)-1]
171
+ }
172
+ func (g *topologyGraph) unknown(parent, kind, namespace, name, state string) string {
173
+ if state == "" {
174
+ state = g.referenceState(kind, namespace)
175
+ }
176
+ if state == "unsupported_fragment" {
177
+ return g.addNode(cr.NetworkTopologyGraphNode{ID: fmt.Sprintf("%s:unsupported:%d", parent, len(g.nodes)), Type: "backend-group", Label: "Unsupported reference", Provider: g.nodes[parent].Provider, ResolutionState: state})
178
+ }
179
+ label := name
180
+ api := "v1"
181
+ for _, d := range cr.NetworkTopologySources() {
182
+ if d.Kind == kind {
183
+ api = strings.TrimPrefix(d.Group+"/"+d.Version, "/")
184
+ break
185
+ }
186
+ }
187
+ ref := &cr.ResourceRef{ClusterID: g.view.ClusterID, Kind: kind, Namespace: namespace, Name: name, APIVersion: api, ScopeMode: cr.ResourceScopeModeNamespace}
188
+ if namespace == "" {
189
+ ref.ScopeMode = cr.ResourceScopeModeCluster
190
+ }
191
+ id := fmt.Sprintf("%s:unresolved:%d", parent, len(g.nodes))
192
+ visibility := g.referenceState(kind, namespace)
193
+ if state == "not_visible" || state == "unknown" || visibility == "not_visible" || visibility == "unknown" {
194
+ g.hiddenRefs[id] = *ref
195
+ ref = nil
196
+ label = "Unresolved reference"
197
+ }
198
+ return g.addNode(cr.NetworkTopologyGraphNode{ID: id, Type: "backend-group", Label: label, Provider: g.nodes[parent].Provider, ResolutionState: state, Resource: ref})
199
+ }
200
+
201
+ func (g *topologyGraph) referenceState(kind, namespace string) string {
202
+ definition := map[string]string{"Service": "kubernetes.services", "Pod": "kubernetes.pods", "Gateway": "gateway.gateways", "TraefikService": "traefik.traefikservices", "Middleware": "traefik.middlewares", "MiddlewareTCP": "traefik.middlewaretcps"}[kind]
203
+ state := "not_visible"
204
+ for _, coverage := range g.view.Coverage {
205
+ if coverage.Name != "" || coverage.SourceID != definition || (coverage.Namespace != "" && coverage.Namespace != namespace) {
206
+ continue
207
+ }
208
+ if coverage.State == "available" {
209
+ return "not_found"
210
+ }
211
+ if coverage.State == "partial" {
212
+ state = "unknown"
213
+ }
214
+ }
215
+ return state
216
+ }
217
+ func (g *topologyGraph) addRoute(object topologyObject) {
218
+ switch object.ref.Kind {
219
+ case "Ingress":
220
+ g.ingress(object)
221
+ case "HTTPRoute", "GRPCRoute", "TCPRoute", "TLSRoute", "UDPRoute":
222
+ g.gatewayRoute(object)
223
+ case "IngressRoute", "IngressRouteTCP", "IngressRouteUDP":
224
+ g.traefikRoute(object)
225
+ case "ApisixRoute":
226
+ g.apisixRoute(object)
227
+ case "ApisixTls":
228
+ id := g.objectNode(object, "entry", "apisix", "TLS")
229
+ g.properties(id, cr.NetworkTopologyProperty{Name: "SNI", Value: strings.Join(texts(at(object.body, "spec", "hosts")), ", ")})
230
+ if text(at(object.body, "spec", "secret", "name")) != "" {
231
+ g.properties(id, cr.NetworkTopologyProperty{Name: "Certificate reference", Value: "Configured; Secret existence and TLS activation unverified"})
232
+ }
233
+ }
234
+ }
235
+ func (g *topologyGraph) rule(object topologyObject, provider, protocol string, index int, name string, groups [][]cr.NetworkTopologyMatch) string {
236
+ suffix := fmt.Sprint(index)
237
+ if name != "" {
238
+ suffix = name
239
+ }
240
+ ref := object.ref
241
+ return g.addNode(cr.NetworkTopologyGraphNode{ID: object.id() + "/rule/" + suffix, Type: "rule", Label: firstTopology(name, fmt.Sprintf("Rule %d", index+1)), Provider: provider, Protocol: protocol, ResolutionState: "resolved", Resource: &ref, Generation: object.generation, Rule: &cr.NetworkTopologyRule{Name: name, Index: index, MatchGroups: groups}})
242
+ }
243
+ func (g *topologyGraph) properties(id string, values ...cr.NetworkTopologyProperty) {
244
+ node, ok := g.nodes[id]
245
+ if !ok {
246
+ return
247
+ }
248
+ node.Properties = append(node.Properties, values...)
249
+ g.nodes[id] = node
250
+ }
251
+ func (g *topologyGraph) state(id, state string) {
252
+ node, ok := g.nodes[id]
253
+ if ok {
254
+ node.ResolutionState = state
255
+ g.nodes[id] = node
256
+ }
257
+ }
258
+ func (g *topologyGraph) countNode(node cr.NetworkTopologyGraphNode) {
259
+ switch node.Type {
260
+ case "entry":
261
+ g.view.Summary.EntryCount++
262
+ case "rule":
263
+ g.view.Summary.RouteCount++
264
+ if node.ResolutionState != "resolved" {
265
+ g.view.Summary.PendingRouteCount++
266
+ }
267
+ case "service":
268
+ g.view.Summary.ServiceCount++
269
+ case "pod":
270
+ g.view.Summary.BackendPodCount++
271
+ case "backend-group":
272
+ if node.ResolutionState == "not_found" {
273
+ g.view.Summary.MissingServiceCount++
274
+ }
275
+ }
276
+ }
277
+ func at(object map[string]any, path ...string) any {
278
+ var value any = object
279
+ for _, key := range path {
280
+ item, ok := value.(map[string]any)
281
+ if !ok {
282
+ return nil
283
+ }
284
+ value = item[key]
285
+ }
286
+ return value
287
+ }
288
+ func object(value any) map[string]any { item, _ := value.(map[string]any); return item }
289
+ func items(value any) []map[string]any {
290
+ array, _ := value.([]any)
291
+ result := []map[string]any{}
292
+ for _, item := range array {
293
+ if row, ok := item.(map[string]any); ok {
294
+ result = append(result, row)
295
+ }
296
+ }
297
+ return result
298
+ }
299
+ func texts(value any) []string {
300
+ array, _ := value.([]any)
301
+ result := []string{}
302
+ for _, item := range array {
303
+ if text, ok := item.(string); ok {
304
+ result = append(result, text)
305
+ }
306
+ }
307
+ return result
308
+ }
309
+ func text(value any) string {
310
+ if value == nil {
311
+ return ""
312
+ }
313
+ return fmt.Sprint(value)
314
+ }
315
+ func integer(value any, defaultValue int64) int64 {
316
+ if number, ok := value.(float64); ok {
317
+ return int64(number)
318
+ }
319
+ return defaultValue
320
+ }
321
+ func firstTopology(values ...string) string {
322
+ for _, value := range values {
323
+ if value != "" {
324
+ return value
325
+ }
326
+ }
327
+ return ""
328
+ }
329
+
330
+ func (g *topologyGraph) budgetReached() bool {
331
+ if len(g.nodes) >= g.nodeLimit || len(g.view.Edges) >= g.edgeLimit {
332
+ g.view.Truncated = true
333
+ return true
334
+ }
335
+ return false
336
+ }
337
+
338
+ // decodeTopologyObject enforces the fixed source's exact GVK and re-applies the
339
+ // allowlist before any relationship interpretation (also for local snapshots).
340
+ func decodeTopologyObject(cluster string, source cr.NetworkTopologySourceResult, item cr.NetworkTopologySourceObject) (topologyObject, bool) {
341
+ d, ok := cr.LookupNetworkTopologySource(source.Coverage.SourceID)
342
+
343
+ ref := item.Resource
344
+ ref.APIVersion = strings.TrimPrefix(ref.APIVersion, "/")
345
+ if !ok || ref.ClusterID != cluster || ref.Kind != d.Kind || !topologyDefinitionAPI(d, ref.APIVersion) || ref.Name == "" || len(item.Content) > cr.NetworkTopologyMaxObjectBytes {
346
+ return topologyObject{}, false
347
+ }
348
+ if d.Namespaced && ref.Namespace == "" || !d.Namespaced && ref.Namespace != "" || source.Coverage.Namespace != "" && ref.Namespace != source.Coverage.Namespace {
349
+ return topologyObject{}, false
350
+ }
351
+ if source.Coverage.Name != "" && source.Coverage.Name != ref.Name {
352
+ return topologyObject{}, false
353
+ }
354
+ if source.Coverage.State != "available" && source.Coverage.State != "partial" {
355
+ return topologyObject{}, false
356
+ }
357
+ var body map[string]any
358
+ if json.Unmarshal([]byte(item.Content), &body) != nil {
359
+ return topologyObject{}, false
360
+ }
361
+ for key, want := range map[string]string{"name": ref.Name, "namespace": ref.Namespace, "uid": ref.UID} {
362
+ if actual := text(at(body, "metadata", key)); actual != "" && actual != want {
363
+ return topologyObject{}, false
364
+ }
365
+ }
366
+ body, err := cr.ProjectNetworkTopologyObject(d.ID, body)
367
+ if err != nil {
368
+ return topologyObject{}, false
369
+ }
370
+ ref.ScopeMode = cr.ResourceScopeModeCluster
371
+ if d.Namespaced {
372
+ ref.ScopeMode = cr.ResourceScopeModeNamespace
373
+ }
374
+ return topologyObject{source: d.ID, ref: ref, generation: item.Generation, body: body}, true
375
+ }
376
+
377
+ func topologyDefinitionAPI(d cr.NetworkTopologySourceDefinition, api string) bool {
378
+ for _, version := range cr.NetworkTopologySourceVersions(d) {
379
+ if api == strings.TrimPrefix(d.Group+"/"+version, "/") {
380
+ return true
381
+ }
382
+ }
383
+ return false
384
+ }
385
+
386
+ // Interpret the selected root before unrelated high-fanout sources can consume
387
+ // the bounded relationship set. Reuse the same rule methods; do not rescan input.
388
+ func (g *topologyGraph) prioritizeRoot(root *cr.ResourceRef) string {
389
+ if root == nil {
390
+ return ""
391
+ }
392
+ o, ok := g.objects[topologyKey(root.Kind, root.Namespace, root.Name)]
393
+ if !ok {
394
+ return ""
395
+ }
396
+ g.addRoute(o)
397
+ switch o.source {
398
+ case "kubernetes.services":
399
+ g.service(o, "")
400
+ case "kubernetes.endpointslices":
401
+ g.endpointSlice(topologyObject{}, o, "", "")
402
+ }
403
+ g.objectDeclarations(o)
404
+ return o.id()
405
+ }