@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,132 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "sort"
6
+ "strings"
7
+
8
+ cr "github.com/opensoha/soha-contracts/resource"
9
+ )
10
+
11
+ // BuildCustomResourceGraph builds the metadata relationships for an observed
12
+ // CRD or custom instance. Callers authorize every observation and establish the
13
+ // definition's served GVK, plural and scope. An input may omit TargetID, which
14
+ // is derived from its metadata. An observed built-in may omit Definition.
15
+ func BuildCustomResourceGraph(ctx context.Context, root cr.ResourceRef, definition cr.ResourceRef, inputs []ResourceGraphMetadataEnrichment) (cr.ResourceGraph, error) {
16
+ ctx, cancel := context.WithTimeout(ctx, ResourceGraphTimeout)
17
+ defer cancel()
18
+ if err := ctx.Err(); err != nil {
19
+ return cr.ResourceGraph{}, err
20
+ }
21
+ if len(inputs) > ResourceGraphMetadataEnrichmentLimit {
22
+ return cr.ResourceGraph{}, ErrResourceGraphBudget
23
+ }
24
+ if root.ClusterID == "" || root.APIVersion == "" || root.Kind == "" || root.Name == "" || root.UID == "" || root.ScopeMode != topologyScope(root.Namespace != "") {
25
+ return cr.ResourceGraph{}, invalidGraphRoot("custom graph requires an exact observed root")
26
+ }
27
+ plural, group, _ := strings.Cut(definition.Name, ".")
28
+ if definition.ClusterID != root.ClusterID || !customGraphIsDefinition(definition) || definition.UID == "" || definition.Namespace != "" || definition.ScopeMode != cr.ResourceScopeModeCluster || plural == "" || group == "" {
29
+ return cr.ResourceGraph{}, invalidGraphRoot("custom graph requires an exact observed definition")
30
+ }
31
+ rootID := graphNodeID(root)
32
+ definitionRoot := customGraphIsDefinition(root)
33
+ if definitionRoot && rootID != graphNodeID(definition) {
34
+ return cr.ResourceGraph{}, invalidGraphRoot("definition root identity mismatch")
35
+ }
36
+ definitions := map[string]cr.ResourceRef{definition.Name: definition}
37
+ observations := make(map[string]ResourceGraphEnrichment, len(inputs))
38
+ nodes := make(map[string]cr.ResourceGraphNode, len(inputs)+1)
39
+ for _, input := range inputs {
40
+ if err := ctx.Err(); err != nil {
41
+ return cr.ResourceGraph{}, err
42
+ }
43
+ if len(input.Content) > ResourceGraphMetadataJSONLimit {
44
+ return cr.ResourceGraph{}, ErrResourceGraphBudget
45
+ }
46
+ o, err := decodeEnrichmentMetadata(input.Content)
47
+ if err != nil {
48
+ return cr.ResourceGraph{}, err
49
+ }
50
+ ref := enrichmentRef(root.ClusterID, o)
51
+ if ref.APIVersion == "" || ref.Kind == "" || ref.Name == "" || ref.UID == "" || customGraphIsDefinition(ref) {
52
+ return cr.ResourceGraph{}, invalidGraphEnrichment("observation requires exact instance identity")
53
+ }
54
+ id := graphNodeID(ref)
55
+ if input.TargetID != "" && input.TargetID != id {
56
+ return cr.ResourceGraph{}, invalidGraphEnrichment("observation target identity mismatch")
57
+ }
58
+ if graphBuiltinSpec(o) {
59
+ if input.Definition != (cr.ResourceRef{}) {
60
+ return cr.ResourceGraph{}, invalidGraphEnrichment("built-in observation has a custom definition")
61
+ }
62
+ } else {
63
+ if err := validateEnrichmentDefinition(root.ClusterID, ref.APIVersion, input.Definition); err != nil {
64
+ return cr.ResourceGraph{}, err
65
+ }
66
+ apiGroup, version, _ := strings.Cut(ref.APIVersion, "/")
67
+ _, definitionGroup, _ := strings.Cut(input.Definition.Name, ".")
68
+ if version == "" || strings.Contains(version, "/") || definitionGroup != apiGroup {
69
+ return cr.ResourceGraph{}, invalidGraphEnrichment("definition requires exact group and instance version")
70
+ }
71
+ if prior, exists := definitions[input.Definition.Name]; exists {
72
+ if prior.UID != input.Definition.UID {
73
+ return cr.ResourceGraph{}, invalidGraphEnrichment("conflicting definition UID")
74
+ }
75
+ input.Definition = prior
76
+ } else {
77
+ definitions[input.Definition.Name] = input.Definition
78
+ }
79
+ }
80
+ key := graphIdentity(ref.APIVersion, ref.Kind, ref.Namespace, ref.Name)
81
+ observation := ResourceGraphEnrichment{TargetID: id, Object: o, Definition: input.Definition}
82
+ if prior, exists := observations[key]; exists {
83
+ if prior.TargetID != id || !sameEnrichment(prior, observation) {
84
+ return cr.ResourceGraph{}, invalidGraphEnrichment("conflicting duplicate observation")
85
+ }
86
+ continue
87
+ }
88
+ observations[key] = observation
89
+ nodes[id] = cr.ResourceGraphNode{ID: id, Resource: ref}
90
+ }
91
+ rootObservation := observations[graphIdentity(root.APIVersion, root.Kind, root.Namespace, root.Name)]
92
+ if !definitionRoot && (rootObservation.TargetID != rootID || rootObservation.Definition != definition) {
93
+ return cr.ResourceGraph{}, invalidGraphRoot("custom root metadata identity mismatch")
94
+ }
95
+ for _, ref := range definitions {
96
+ id := graphNodeID(ref)
97
+ nodes[id] = cr.ResourceGraphNode{ID: id, Resource: ref}
98
+ }
99
+ nodes[rootID] = cr.ResourceGraphNode{ID: rootID, Resource: root}
100
+ ordered := make([]ResourceGraphEnrichment, 0, len(observations))
101
+ for _, input := range observations {
102
+ ordered = append(ordered, input)
103
+ }
104
+ sort.Slice(ordered, func(i, j int) bool { return ordered[i].TargetID < ordered[j].TargetID })
105
+ b := enrichmentBuilder(ctx, root.ClusterID, nodes, ordered)
106
+ b.nodes = nodes
107
+ for _, input := range ordered {
108
+ if input.Definition.UID != "" {
109
+ b.edge(graphNodeID(input.Definition), input.TargetID, "defines")
110
+ }
111
+ }
112
+ if rootObservation.Object != nil {
113
+ b.ownerRelations(rootObservation.Object, rootID)
114
+ }
115
+ for _, input := range ordered {
116
+ if input.TargetID != rootID {
117
+ b.ownerRelations(input.Object, input.TargetID)
118
+ }
119
+ }
120
+ if b.err != nil {
121
+ return cr.ResourceGraph{}, b.err
122
+ }
123
+ graph, err := b.bounded(rootID, root.Namespace)
124
+ if b.partial {
125
+ graph.Warnings = append(graph.Warnings, "resource graph working set truncated at 10000 nodes or 10000 edges")
126
+ }
127
+ return graph, err
128
+ }
129
+
130
+ func customGraphIsDefinition(ref cr.ResourceRef) bool {
131
+ return ref.APIVersion == "apiextensions.k8s.io/v1" && ref.Kind == "CustomResourceDefinition"
132
+ }
@@ -0,0 +1,197 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "strings"
9
+ "testing"
10
+
11
+ cr "github.com/opensoha/soha-contracts/resource"
12
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14
+ )
15
+
16
+ func customRootObservation(t *testing.T, object *unstructured.Unstructured, definition cr.ResourceRef) ResourceGraphMetadataEnrichment {
17
+ t.Helper()
18
+ content, err := object.MarshalJSON()
19
+ if err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ return ResourceGraphMetadataEnrichment{Content: content, Definition: definition}
23
+ }
24
+
25
+ func TestBuildCustomResourceGraphDefinitionRoot(t *testing.T) {
26
+ _, fixture := graphEnrichmentFixture()
27
+ definition := fixture.Definition
28
+ graph, err := BuildCustomResourceGraph(context.Background(), definition, definition, nil)
29
+ if err != nil || graph.RootID != graphNodeID(definition) || len(graph.Nodes) != 1 || len(graph.Edges) != 0 || graph.Nodes[0].Resource != definition {
30
+ t.Fatalf("empty definition graph: %+v, %v", graph, err)
31
+ }
32
+ instance := customRootObservation(t, fixture.Object, definition)
33
+ other := graphTestObject("other.example/v1", "Widget", "owner", "unrelated-uid")
34
+ otherDefinition := definition
35
+ otherDefinition.Name, otherDefinition.UID = "widgets.other.example", "unrelated-crd-uid"
36
+ graph, err = BuildCustomResourceGraph(context.Background(), definition, definition, []ResourceGraphMetadataEnrichment{instance, customRootObservation(t, other, otherDefinition), instance})
37
+ if err != nil || graph.RootID != graphNodeID(definition) || len(graph.Nodes) != 2 || len(graph.Edges) != 1 || !graphTestEdge(graph, "defines", definition.Name, fixture.Object.GetName()) {
38
+ t.Fatalf("definition graph included a non-instance, duplicate or omitted the exact instance: %+v, %v", graph, err)
39
+ }
40
+ }
41
+
42
+ func TestBuildCustomResourceGraphOwnerReferences(t *testing.T) {
43
+ _, fixture := graphEnrichmentFixture()
44
+ definition := fixture.Definition
45
+ root := fixture.Object
46
+ root.SetName("root")
47
+ root.SetUID("root-uid")
48
+ root.Object["spec"] = map[string]interface{}{"secretRef": "MUST-NOT-LEAK"}
49
+ root.SetLabels(map[string]string{"targetRef": "MUST-NOT-LEAK"})
50
+ parent := graphTestObject("parents.example/v1", "Parent", "parent", "parent-uid")
51
+ parent.SetNamespace("")
52
+ parentDefinition := definition
53
+ parentDefinition.Name, parentDefinition.UID = "parents.parents.example", "parent-definition-uid"
54
+ graphTestOwner(root, parent.GetAPIVersion(), parent.GetKind(), parent.GetName(), string(parent.GetUID()), true)
55
+ child := graphTestObject("v1", "Pod", "child", "child-uid")
56
+ graphTestOwner(child, root.GetAPIVersion(), root.GetKind(), root.GetName(), string(root.GetUID()), false)
57
+ stale := graphTestObject("v1", "Pod", "stale-child", "stale-child-uid")
58
+ graphTestOwner(stale, root.GetAPIVersion(), root.GetKind(), root.GetName(), "replaced-root-uid", false)
59
+ otherNamespace := graphTestObject("v1", "Pod", "other-namespace-child", "other-namespace-child-uid")
60
+ otherNamespace.SetNamespace("other")
61
+ graphTestOwner(otherNamespace, root.GetAPIVersion(), root.GetKind(), root.GetName(), string(root.GetUID()), false)
62
+ rootRef := enrichmentRef(definition.ClusterID, root)
63
+ rootRef.ResourceVersion = "9"
64
+ rootInput := customRootObservation(t, root, definition)
65
+ rootInput.TargetID = graphNodeID(rootRef)
66
+ graph, err := BuildCustomResourceGraph(context.Background(), rootRef, definition, []ResourceGraphMetadataEnrichment{
67
+ rootInput, customRootObservation(t, parent, parentDefinition), customRootObservation(t, child, cr.ResourceRef{}), customRootObservation(t, stale, cr.ResourceRef{}), customRootObservation(t, otherNamespace, cr.ResourceRef{}),
68
+ })
69
+ if err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ if graph.RootID != graphNodeID(rootRef) || len(graph.Nodes) != 5 || len(graph.Edges) != 4 || !graphTestEdge(graph, "defines", definition.Name, "root") || !graphTestEdge(graph, "defines", parentDefinition.Name, "parent") || !graphTestEdge(graph, "controls", "parent", "root") || !graphTestEdge(graph, "owns", "root", "child") {
73
+ t.Fatalf("unexpected custom graph: %+v", graph)
74
+ }
75
+ for _, node := range graph.Nodes {
76
+ if node.ID == graph.RootID && node.Resource != rootRef {
77
+ t.Fatalf("root identity changed: %+v", node)
78
+ }
79
+ if node.Resource.Name == "parent" && (node.Status == "unresolved" || node.Resource.Namespace != "" || node.Resource.ScopeMode != cr.ResourceScopeModeCluster) {
80
+ t.Fatalf("cluster-scoped owner failed exact resolution: %+v", node)
81
+ }
82
+ }
83
+ content, err := json.Marshal(graph)
84
+ if err != nil || strings.Contains(string(content), "MUST-NOT-LEAK") {
85
+ t.Fatalf("spec or labels reached graph output: %s, %v", content, err)
86
+ }
87
+ parent.SetUID("new-parent-uid")
88
+ graph, err = BuildCustomResourceGraph(context.Background(), rootRef, definition, []ResourceGraphMetadataEnrichment{rootInput, customRootObservation(t, parent, parentDefinition)})
89
+ if err != nil || len(graph.Nodes) != 3 || !graphTestEdge(graph, "controls", "parent", "root") {
90
+ t.Fatalf("stale owner declaration missing: %+v, %v", graph, err)
91
+ }
92
+ for _, node := range graph.Nodes {
93
+ if node.Resource.Name == "parent" && (node.Status != "unresolved" || node.Resource.UID != "parent-uid") {
94
+ t.Fatalf("replacement owner incorrectly resolved: %+v", node)
95
+ }
96
+ }
97
+ }
98
+
99
+ func TestBuildCustomResourceGraphExactIdentity(t *testing.T) {
100
+ _, fixture := graphEnrichmentFixture()
101
+ for _, test := range []struct {
102
+ name string
103
+ edit func(*cr.ResourceRef, *cr.ResourceRef, *[]ResourceGraphMetadataEnrichment)
104
+ want error
105
+ }{
106
+ {"missing root UID", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) { root.UID = "" }, ErrInvalidResourceGraphRoot},
107
+ {"wrong root UID", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) { root.UID = "stale" }, ErrInvalidResourceGraphRoot},
108
+ {"wrong root GVK", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
109
+ root.APIVersion = "custom.example/v2"
110
+ }, ErrInvalidResourceGraphRoot},
111
+ {"wrong root scope", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
112
+ root.ScopeMode = cr.ResourceScopeModeCluster
113
+ }, ErrInvalidResourceGraphRoot},
114
+ {"wrong root namespace", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) { root.Namespace = "other" }, ErrInvalidResourceGraphRoot},
115
+ {"missing root metadata", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) { *inputs = nil }, ErrInvalidResourceGraphRoot},
116
+ {"wrong definition cluster", func(_, definition *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
117
+ definition.ClusterID = "other"
118
+ }, ErrInvalidResourceGraphRoot},
119
+ {"wrong definition root UID", func(root, definition *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
120
+ *root = *definition
121
+ root.UID = "stale"
122
+ }, ErrInvalidResourceGraphRoot},
123
+ {"wrong observation ID", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
124
+ (*inputs)[0].TargetID = "unrelated"
125
+ }, ErrInvalidResourceGraphEnrichment},
126
+ {"conflicting definition UID", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
127
+ (*inputs)[0].Definition.UID = "stale"
128
+ }, ErrInvalidResourceGraphEnrichment},
129
+ {"lookalike definition group", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
130
+ (*inputs)[0].Definition.Name = "widgets.fake.custom.example"
131
+ }, ErrInvalidResourceGraphEnrichment},
132
+ {"missing observation UID", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
133
+ (*inputs)[0].Content = []byte(`{"apiVersion":"custom.example/v1","kind":"Widget","metadata":{"name":"owner","namespace":"ns"}}`)
134
+ }, ErrInvalidResourceGraphEnrichment},
135
+ {"invalid metadata", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
136
+ (*inputs)[0].Content = []byte("{")
137
+ }, ErrInvalidResourceGraphEnrichment},
138
+ } {
139
+ t.Run(test.name, func(t *testing.T) {
140
+ definition := fixture.Definition
141
+ root := enrichmentRef(definition.ClusterID, fixture.Object)
142
+ inputs := []ResourceGraphMetadataEnrichment{customRootObservation(t, fixture.Object, definition)}
143
+ test.edit(&root, &definition, &inputs)
144
+ _, err := BuildCustomResourceGraph(context.Background(), root, definition, inputs)
145
+ if !errors.Is(err, test.want) {
146
+ t.Fatalf("expected %v, got %v", test.want, err)
147
+ }
148
+ })
149
+ }
150
+ }
151
+
152
+ func TestBuildCustomResourceGraphBudgets(t *testing.T) {
153
+ _, fixture := graphEnrichmentFixture()
154
+ definition := fixture.Definition
155
+ root := enrichmentRef(definition.ClusterID, fixture.Object)
156
+ for _, inputs := range [][]ResourceGraphMetadataEnrichment{
157
+ make([]ResourceGraphMetadataEnrichment, ResourceGraphMetadataEnrichmentLimit+1),
158
+ {{Content: make([]byte, ResourceGraphMetadataJSONLimit+1)}},
159
+ } {
160
+ _, err := BuildCustomResourceGraph(context.Background(), root, definition, inputs)
161
+ if !errors.Is(err, ErrResourceGraphBudget) {
162
+ t.Fatalf("expected budget error, got %v", err)
163
+ }
164
+ }
165
+ ctx, cancel := context.WithCancel(context.Background())
166
+ cancel()
167
+ if _, err := BuildCustomResourceGraph(ctx, definition, definition, nil); !errors.Is(err, context.Canceled) {
168
+ t.Fatalf("expected context cancellation, got %v", err)
169
+ }
170
+ owners := make([]metav1.OwnerReference, ResourceGraphNodeLimit)
171
+ for i := range owners {
172
+ owners[i] = metav1.OwnerReference{APIVersion: "custom.example/v1", Kind: "Parent", Name: fmt.Sprintf("parent-%03d", i), UID: "unobserved"}
173
+ }
174
+ fixture.Object.SetOwnerReferences(owners)
175
+ graph, err := BuildCustomResourceGraph(context.Background(), root, definition, []ResourceGraphMetadataEnrichment{customRootObservation(t, fixture.Object, definition)})
176
+ if err != nil || len(graph.Nodes) != ResourceGraphNodeLimit || len(graph.Edges) >= ResourceGraphEdgeLimit || !strings.Contains(strings.Join(graph.Warnings, " "), "truncated") {
177
+ t.Fatalf("budget was not enforced: nodes=%d edges=%d warnings=%v err=%v", len(graph.Nodes), len(graph.Edges), graph.Warnings, err)
178
+ }
179
+ }
180
+
181
+ func TestBuildCustomResourceGraphDepth(t *testing.T) {
182
+ _, fixture := graphEnrichmentFixture()
183
+ definition := fixture.Definition
184
+ root := enrichmentRef(definition.ClusterID, fixture.Object)
185
+ inputs := []ResourceGraphMetadataEnrichment{customRootObservation(t, fixture.Object, definition)}
186
+ owner := fixture.Object
187
+ for i := 1; i <= ResourceGraphDepth+1; i++ {
188
+ child := graphTestObject("v1", "Pod", fmt.Sprintf("child-%d", i), fmt.Sprintf("child-uid-%d", i))
189
+ graphTestOwner(child, owner.GetAPIVersion(), owner.GetKind(), owner.GetName(), string(owner.GetUID()), true)
190
+ inputs = append(inputs, customRootObservation(t, child, cr.ResourceRef{}))
191
+ owner = child
192
+ }
193
+ graph, err := BuildCustomResourceGraph(context.Background(), root, definition, inputs)
194
+ if err != nil || len(graph.Nodes) != ResourceGraphDepth+2 || len(graph.Edges) != ResourceGraphDepth+1 || !strings.Contains(strings.Join(graph.Warnings, " "), "depth") {
195
+ t.Fatalf("depth was not enforced: %+v, %v", graph, err)
196
+ }
197
+ }