@opensoha/contracts 0.1.27 → 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 (94) hide show
  1. package/COMPATIBILITY.md +4 -3
  2. package/README.md +8 -9
  3. package/compat/consumer-revisions.json +1 -1
  4. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  5. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  6. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  7. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  8. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  9. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  10. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  11. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  12. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  13. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  14. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  15. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  16. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  17. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  18. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  19. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  20. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  21. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  22. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  23. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  24. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  25. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  26. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  27. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  28. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  29. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  30. package/gen/go/sohaapi/client.go +1 -1
  31. package/gen/go/sohaapi/client_test.go +13 -0
  32. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  33. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  34. package/gen/go/sohaapi/types.go +421 -88
  35. package/gen/ts/sohaapi/index.ts +756 -4
  36. package/helmrelease/runtime/values_upgrade.go +78 -0
  37. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  38. package/helmrelease/values_update.go +31 -0
  39. package/openapi/soha-api.json +1191 -14
  40. package/openapi/soha-api.yaml +588 -28
  41. package/openapi/spec_generated.go +3 -3
  42. package/package.json +1 -1
  43. package/resource/agent_profiles.go +9 -15
  44. package/resource/agent_profiles_test.go +9 -3
  45. package/resource/custom_graph_page.go +18 -0
  46. package/resource/models.go +102 -57
  47. package/resource/network_topology.go +22 -0
  48. package/resource/network_topology_projection.go +36 -22
  49. package/resource/network_topology_projection_test.go +4 -0
  50. package/resource/network_topology_test.go +20 -0
  51. package/resource/network_topology_validation.go +5 -0
  52. package/resource/observed_workload.go +18 -0
  53. package/resource/pod_status_test.go +22 -0
  54. package/resource/rbac_graph.go +4 -0
  55. package/resource/rollout_optional_test.go +33 -0
  56. package/resource/runtime/network_relationship_primitives.go +66 -0
  57. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  58. package/resource/runtime/network_topology.go +17 -5
  59. package/resource/runtime/network_topology_builder.go +405 -0
  60. package/resource/runtime/network_topology_builder_backends.go +195 -0
  61. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  62. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  63. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  64. package/resource/runtime/network_topology_builder_providers.go +242 -0
  65. package/resource/runtime/network_topology_builder_test.go +186 -0
  66. package/resource/runtime/network_topology_declarations.go +329 -0
  67. package/resource/runtime/resource_edit.go +22 -0
  68. package/resource/runtime/resource_edit_test.go +32 -0
  69. package/resource/runtime/resource_graph_collect.go +325 -0
  70. package/resource/runtime/resource_graph_configuration.go +59 -0
  71. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  72. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  73. package/resource/runtime/resource_graph_custom_page.go +159 -0
  74. package/resource/runtime/resource_graph_custom_root.go +132 -0
  75. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  76. package/resource/runtime/resource_graph_enrichment.go +417 -0
  77. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  78. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  79. package/resource/runtime/resource_graph_metadata.go +125 -0
  80. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  81. package/resource/runtime/resource_graph_network.go +224 -0
  82. package/resource/runtime/resource_graph_network_collect.go +174 -0
  83. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  84. package/resource/runtime/resource_graph_network_test.go +342 -0
  85. package/resource/runtime/resource_graph_rbac.go +278 -0
  86. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  87. package/resource/runtime/resource_graph_rules.go +834 -0
  88. package/resource/runtime/resource_graph_storage.go +144 -0
  89. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  90. package/resource/runtime/resource_graph_storage_test.go +397 -0
  91. package/resource/runtime/resource_graph_test.go +627 -0
  92. package/resource/runtime/secret_certificate.go +44 -0
  93. package/resource/runtime/secret_certificate_test.go +87 -0
  94. package/resource/storage_graph.go +4 -0
@@ -0,0 +1,325 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "sort"
8
+ "time"
9
+
10
+ cr "github.com/opensoha/soha-contracts/resource"
11
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
12
+ "k8s.io/apimachinery/pkg/api/meta"
13
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
15
+ kruntime "k8s.io/apimachinery/pkg/runtime"
16
+ "k8s.io/apimachinery/pkg/runtime/schema"
17
+ "k8s.io/client-go/kubernetes"
18
+ "k8s.io/client-go/metadata"
19
+ )
20
+
21
+ const ResourceGraphSourceLimit int64 = 500
22
+ const ResourceGraphNodeLimit = 500
23
+ const ResourceGraphEdgeLimit = 1000
24
+ const ResourceGraphDepth = 3
25
+ const ResourceGraphTimeout = 8 * time.Second
26
+
27
+ var graphSecretResource = schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
28
+
29
+ // BuildKubernetesResourceGraph is the common Direct/Agent read-only entry point.
30
+ // Root failures remain wrapped Kubernetes errors. Partial source failures are
31
+ // explicit warnings; cancellation and the total collection deadline are errors.
32
+ func BuildKubernetesResourceGraph(ctx context.Context, client kubernetes.Interface, root cr.ResourceRef) (cr.ResourceGraph, error) {
33
+ return BuildKubernetesResourceGraphWithMetadata(ctx, client, nil, root)
34
+ }
35
+
36
+ // BuildKubernetesResourceGraphWithMetadata optionally confirms Secret identities
37
+ // using metadata-only HTTP negotiation. Callers must supply a metadata client
38
+ // whose transport excludes the client-go full-JSON fallback from Accept headers.
39
+ // It never calls the typed Secret API.
40
+ func BuildKubernetesResourceGraphWithMetadata(ctx context.Context, client kubernetes.Interface, metadataClient metadata.Interface, root cr.ResourceRef) (cr.ResourceGraph, error) {
41
+ root, err := NormalizeResourceGraphRoot(root)
42
+ if err != nil {
43
+ return cr.ResourceGraph{}, err
44
+ }
45
+ ctx, cancel := context.WithTimeout(ctx, ResourceGraphTimeout)
46
+ defer cancel()
47
+ sources := graphSources(client, root.Namespace)
48
+ source, ok := sources[root.Kind]
49
+ if !ok && (root.Kind != "Secret" || metadataClient == nil) {
50
+ return cr.ResourceGraph{}, fmt.Errorf("%w: %s; Secret roots require metadata-only collection", ErrUnsupportedResourceGraphRoot, root.Kind)
51
+ }
52
+ if root.APIVersion != "" && root.APIVersion != graphBuiltinAPIs[root.Kind] {
53
+ return cr.ResourceGraph{}, invalidGraphRoot("resource graph root apiVersion mismatch")
54
+ }
55
+ var object kruntime.Object
56
+ if root.Kind == "Secret" {
57
+ object, err = metadataClient.Resource(graphSecretResource).Namespace(root.Namespace).Get(ctx, root.Name, metav1.GetOptions{})
58
+ } else {
59
+ object, err = source.get(ctx, root.Name, metav1.GetOptions{})
60
+ }
61
+ if err != nil {
62
+ return cr.ResourceGraph{}, fmt.Errorf("get resource graph root %s/%s: %w", root.Kind, root.Name, err)
63
+ }
64
+ if err = ctx.Err(); err != nil {
65
+ return cr.ResourceGraph{}, err
66
+ }
67
+ projected, err := graphObjectProjection(object, graphBuiltinAPIs[root.Kind], root.Kind)
68
+ if err != nil {
69
+ if graphStorageRootKind(root.Kind) {
70
+ return cr.ResourceGraph{}, invalidGraphRoot("storage resource graph root projection unavailable")
71
+ }
72
+ return cr.ResourceGraph{}, err
73
+ }
74
+ if root.UID != "" && root.UID != string(projected.GetUID()) {
75
+ return cr.ResourceGraph{}, invalidGraphRoot("resource graph root UID changed")
76
+ }
77
+ if (graphStorageRootKind(root.Kind) || graphRBACRootKind(root.Kind)) && (projected.GetName() != root.Name || projected.GetNamespace() != root.Namespace) {
78
+ return cr.ResourceGraph{}, invalidGraphRoot("resource graph root identity changed")
79
+ }
80
+ objects := []*unstructured.Unstructured{projected}
81
+ warnings := []string{}
82
+ kinds := make([]string, 0, len(sources))
83
+ for kind := range sources {
84
+ if graphCollectKind(root.Kind, kind) {
85
+ kinds = append(kinds, kind)
86
+ }
87
+ }
88
+ sort.Strings(kinds)
89
+ for _, kind := range kinds {
90
+ if err = sources[kind].collect(ctx, &objects, &warnings); err != nil {
91
+ return cr.ResourceGraph{}, err
92
+ }
93
+ }
94
+ if err = collectGraphCrossNamespaceAccounts(ctx, client, projected, &objects, &warnings); err != nil {
95
+ return cr.ResourceGraph{}, err
96
+ }
97
+ if err = collectGraphStorage(ctx, client, sources, projected, &objects, &warnings); err != nil {
98
+ return cr.ResourceGraph{}, err
99
+ }
100
+ metadataReads := 0
101
+ if err = collectGraphConfigurationTargets(ctx, metadataClient, root, projected, &metadataReads, &objects, &warnings); err != nil {
102
+ return cr.ResourceGraph{}, err
103
+ }
104
+ graph, err := buildResourceGraphWithContext(ctx, root, objects)
105
+ if err != nil {
106
+ return cr.ResourceGraph{}, err
107
+ }
108
+ if metadataClient != nil {
109
+ changed, metadataErr := collectGraphSecretMetadata(ctx, metadataClient, graph, &metadataReads, &objects, &warnings)
110
+ if metadataErr != nil {
111
+ return cr.ResourceGraph{}, metadataErr
112
+ }
113
+ if changed {
114
+ graph, err = buildResourceGraphWithContext(ctx, root, objects)
115
+ }
116
+ }
117
+ if canceled := ctx.Err(); canceled != nil {
118
+ return cr.ResourceGraph{}, canceled
119
+ }
120
+ graph.Warnings = append(graph.Warnings, warnings...)
121
+ for _, node := range graph.Nodes {
122
+ if node.Resource.APIVersion == "v1" && node.Resource.Kind == "Secret" && node.Status == "unresolved" {
123
+ graph.Warnings = append(graph.Warnings, "Secret targets are declared references only; metadata was not read and existence is unverified")
124
+ break
125
+ }
126
+ }
127
+ return graph, err
128
+ }
129
+
130
+ type graphSourceDefinition struct {
131
+ get func(context.Context, string, metav1.GetOptions) (kruntime.Object, error)
132
+ collect func(context.Context, *[]*unstructured.Unstructured, *[]string) error
133
+ }
134
+
135
+ func graphSource[O kruntime.Object, L kruntime.Object](kind string, get func(context.Context, string, metav1.GetOptions) (O, error), list func(context.Context, metav1.ListOptions) (L, error)) graphSourceDefinition {
136
+ return graphSourceDefinition{
137
+ get: func(ctx context.Context, name string, options metav1.GetOptions) (kruntime.Object, error) {
138
+ return get(ctx, name, options)
139
+ },
140
+ collect: func(ctx context.Context, objects *[]*unstructured.Unstructured, warnings *[]string) error {
141
+ return collectGraphSource(ctx, list, kind, func(obj kruntime.Object) (*unstructured.Unstructured, error) {
142
+ return graphObjectProjection(obj, graphBuiltinAPIs[kind], kind)
143
+ }, objects, warnings)
144
+ },
145
+ }
146
+ }
147
+ func graphSources(client kubernetes.Interface, namespace string) map[string]graphSourceDefinition {
148
+ return map[string]graphSourceDefinition{
149
+ "Pod": graphSource("Pod", client.CoreV1().Pods(namespace).Get, client.CoreV1().Pods(namespace).List),
150
+ "Deployment": graphSource("Deployment", client.AppsV1().Deployments(namespace).Get, client.AppsV1().Deployments(namespace).List),
151
+ "StatefulSet": graphSource("StatefulSet", client.AppsV1().StatefulSets(namespace).Get, client.AppsV1().StatefulSets(namespace).List),
152
+ "DaemonSet": graphSource("DaemonSet", client.AppsV1().DaemonSets(namespace).Get, client.AppsV1().DaemonSets(namespace).List),
153
+ "Job": graphSource("Job", client.BatchV1().Jobs(namespace).Get, client.BatchV1().Jobs(namespace).List),
154
+ "CronJob": graphSource("CronJob", client.BatchV1().CronJobs(namespace).Get, client.BatchV1().CronJobs(namespace).List),
155
+ "ReplicaSet": graphSource("ReplicaSet", client.AppsV1().ReplicaSets(namespace).Get, client.AppsV1().ReplicaSets(namespace).List),
156
+ "ReplicationController": graphSource("ReplicationController", client.CoreV1().ReplicationControllers(namespace).Get, client.CoreV1().ReplicationControllers(namespace).List),
157
+ "Service": graphSource("Service", client.CoreV1().Services(namespace).Get, client.CoreV1().Services(namespace).List),
158
+ "Ingress": graphSource("Ingress", client.NetworkingV1().Ingresses(namespace).Get, client.NetworkingV1().Ingresses(namespace).List),
159
+ "HorizontalPodAutoscaler": graphSource("HorizontalPodAutoscaler", client.AutoscalingV2().HorizontalPodAutoscalers(namespace).Get, client.AutoscalingV2().HorizontalPodAutoscalers(namespace).List),
160
+ "PodDisruptionBudget": graphSource("PodDisruptionBudget", client.PolicyV1().PodDisruptionBudgets(namespace).Get, client.PolicyV1().PodDisruptionBudgets(namespace).List),
161
+ "ConfigMap": graphSource("ConfigMap", client.CoreV1().ConfigMaps(namespace).Get, client.CoreV1().ConfigMaps(namespace).List),
162
+ "ResourceQuota": graphSource("ResourceQuota", client.CoreV1().ResourceQuotas(namespace).Get, client.CoreV1().ResourceQuotas(namespace).List),
163
+ "LimitRange": graphSource("LimitRange", client.CoreV1().LimitRanges(namespace).Get, client.CoreV1().LimitRanges(namespace).List),
164
+ "PriorityClass": graphSource("PriorityClass", client.SchedulingV1().PriorityClasses().Get, client.SchedulingV1().PriorityClasses().List),
165
+ "RuntimeClass": graphSource("RuntimeClass", client.NodeV1().RuntimeClasses().Get, client.NodeV1().RuntimeClasses().List),
166
+ "Lease": graphSource("Lease", client.CoordinationV1().Leases(namespace).Get, client.CoordinationV1().Leases(namespace).List),
167
+ "MutatingWebhookConfiguration": graphSource("MutatingWebhookConfiguration", client.AdmissionregistrationV1().MutatingWebhookConfigurations().Get, client.AdmissionregistrationV1().MutatingWebhookConfigurations().List),
168
+ "ValidatingWebhookConfiguration": graphSource("ValidatingWebhookConfiguration", client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get, client.AdmissionregistrationV1().ValidatingWebhookConfigurations().List),
169
+ "PersistentVolumeClaim": graphSource("PersistentVolumeClaim", client.CoreV1().PersistentVolumeClaims(namespace).Get, client.CoreV1().PersistentVolumeClaims(namespace).List),
170
+ "PersistentVolume": graphSource("PersistentVolume", client.CoreV1().PersistentVolumes().Get, client.CoreV1().PersistentVolumes().List),
171
+ "StorageClass": graphSource("StorageClass", client.StorageV1().StorageClasses().Get, client.StorageV1().StorageClasses().List),
172
+ "ServiceAccount": graphSource("ServiceAccount", client.CoreV1().ServiceAccounts(namespace).Get, client.CoreV1().ServiceAccounts(namespace).List),
173
+ "Role": graphSource("Role", client.RbacV1().Roles(namespace).Get, client.RbacV1().Roles(namespace).List),
174
+ "RoleBinding": graphSource("RoleBinding", client.RbacV1().RoleBindings(namespace).Get, client.RbacV1().RoleBindings(namespace).List),
175
+ "ClusterRole": graphSource("ClusterRole", client.RbacV1().ClusterRoles().Get, client.RbacV1().ClusterRoles().List),
176
+ "ClusterRoleBinding": graphSource("ClusterRoleBinding", client.RbacV1().ClusterRoleBindings().Get, client.RbacV1().ClusterRoleBindings().List),
177
+ "Node": graphSource("Node", client.CoreV1().Nodes().Get, client.CoreV1().Nodes().List),
178
+ "Event": graphSource("Event", client.CoreV1().Events(namespace).Get, client.CoreV1().Events(namespace).List),
179
+ }
180
+ }
181
+
182
+ // Projection immediately discards payloads outside relationship inputs; Secret
183
+ // data, ConfigMap data and annotations are never inspected or retained.
184
+ func graphObjectProjection(obj kruntime.Object, api, kind string) (*unstructured.Unstructured, error) {
185
+ if _, metadataOnly := obj.(*metav1.PartialObjectMetadata); graphRBACRootKind(kind) && !metadataOnly {
186
+ if gvk := obj.GetObjectKind().GroupVersionKind(); !gvk.Empty() && (gvk.GroupVersion().String() != api || gvk.Kind != kind) {
187
+ return nil, invalidGraphRoot("RBAC resource graph object apiVersion or kind mismatch")
188
+ }
189
+ }
190
+ storage := graphStorageRootKind(kind)
191
+ u, err := graphObjectMetadataProjection(obj, api, kind, !storage || kind == "PersistentVolumeClaim")
192
+ if err != nil {
193
+ return nil, err
194
+ }
195
+ if storage {
196
+ return u, graphStorageProjection(obj, u)
197
+ }
198
+ if graphRBACRootKind(kind) {
199
+ return u, graphRBACProjection(obj, u)
200
+ }
201
+ if kind == "Secret" || kind == "ConfigMap" || kind == "Node" || kind == "Namespace" {
202
+ return u, nil
203
+ }
204
+ full, err := kruntime.DefaultUnstructuredConverter.ToUnstructured(obj)
205
+ if err != nil {
206
+ return nil, err
207
+ }
208
+ if spec, ok := full["spec"]; ok {
209
+ u.Object["spec"] = spec
210
+ }
211
+ if status, ok := full["status"]; ok {
212
+ u.Object["status"] = status
213
+ }
214
+ if kind == "Event" {
215
+ for _, key := range []string{"involvedObject", "reason", "message", "type", "lastTimestamp", "eventTime"} {
216
+ u.Object[key] = full[key]
217
+ }
218
+ }
219
+ if kind == "MutatingWebhookConfiguration" || kind == "ValidatingWebhookConfiguration" {
220
+ // Admission rules, CA bundles and expressions are not relationship inputs.
221
+ u.Object["webhooks"] = graphWebhookProjection(full)
222
+ }
223
+ return u, nil
224
+ }
225
+ func graphObjectMetadataProjection(obj kruntime.Object, api, kind string, includeRelationships bool) (*unstructured.Unstructured, error) {
226
+ accessor, err := metav1Object(obj)
227
+ if err != nil {
228
+ return nil, err
229
+ }
230
+ metadata := map[string]interface{}{"name": accessor.GetName(), "namespace": accessor.GetNamespace(), "uid": string(accessor.GetUID()), "resourceVersion": accessor.GetResourceVersion()}
231
+ if created := accessor.GetCreationTimestamp(); kind == "Event" && !created.IsZero() {
232
+ metadata["creationTimestamp"] = created.UTC().Format(time.RFC3339Nano)
233
+ }
234
+ if includeRelationships {
235
+ labels := map[string]interface{}{}
236
+ for k, v := range accessor.GetLabels() {
237
+ labels[k] = v
238
+ }
239
+ metadata["labels"] = labels
240
+ owners := []interface{}{}
241
+ for _, owner := range accessor.GetOwnerReferences() {
242
+ projected, err := kruntime.DefaultUnstructuredConverter.ToUnstructured(&owner)
243
+ if err != nil {
244
+ return nil, err
245
+ }
246
+ owners = append(owners, projected)
247
+ }
248
+ metadata["ownerReferences"] = owners
249
+ }
250
+ return &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": api, "kind": kind, "metadata": metadata}}, nil
251
+ }
252
+ func metav1Object(obj kruntime.Object) (metav1.Object, error) {
253
+ o, ok := obj.(metav1.Object)
254
+ if !ok {
255
+ return nil, fmt.Errorf("resource lacks metadata")
256
+ }
257
+ return o, nil
258
+ }
259
+
260
+ // Only one bounded page is read. A continuation token is an explicit partial
261
+ // observation, never permission to issue an unbounded namespace scan.
262
+ func collectGraphSource[T kruntime.Object](ctx context.Context, list func(context.Context, metav1.ListOptions) (T, error), kind string, project func(kruntime.Object) (*unstructured.Unstructured, error), objects *[]*unstructured.Unstructured, warnings *[]string) error {
263
+ if err := ctx.Err(); err != nil {
264
+ return err
265
+ }
266
+ result, err := list(ctx, metav1.ListOptions{Limit: ResourceGraphSourceLimit})
267
+ if err != nil {
268
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
269
+ return err
270
+ }
271
+ if canceled := ctx.Err(); canceled != nil {
272
+ return canceled
273
+ }
274
+ *warnings = append(*warnings, kind+" source "+graphSourceErrorClass(err))
275
+ return nil
276
+ }
277
+ if err := ctx.Err(); err != nil {
278
+ return err
279
+ }
280
+ items, err := meta.ExtractList(result)
281
+ if err != nil {
282
+ *warnings = append(*warnings, kind+" source projection unavailable")
283
+ return nil
284
+ }
285
+ accessor, err := meta.ListAccessor(result)
286
+ if err != nil {
287
+ return err
288
+ }
289
+ count := len(items)
290
+ if count > int(ResourceGraphSourceLimit) {
291
+ count = int(ResourceGraphSourceLimit)
292
+ }
293
+ for _, item := range items[:count] {
294
+ if err := ctx.Err(); err != nil {
295
+ return err
296
+ }
297
+ projected, err := project(item)
298
+ if err != nil {
299
+ *warnings = append(*warnings, kind+" source projection unavailable")
300
+ continue
301
+ }
302
+ if projected != nil {
303
+ *objects = append(*objects, projected)
304
+ }
305
+ }
306
+ if accessor.GetContinue() != "" || len(items) > int(ResourceGraphSourceLimit) || graphHasRemaining(accessor) {
307
+ *warnings = append(*warnings, kind+" source truncated at 500 objects")
308
+ }
309
+ return nil
310
+ }
311
+
312
+ func graphHasRemaining(accessor metav1.ListInterface) bool {
313
+ remaining := accessor.GetRemainingItemCount()
314
+ return remaining != nil && *remaining > 0
315
+ }
316
+ func graphSourceErrorClass(err error) string {
317
+ switch {
318
+ case apierrors.IsForbidden(err):
319
+ return "Forbidden"
320
+ case apierrors.IsNotFound(err):
321
+ return "NotFound"
322
+ default:
323
+ return "unavailable"
324
+ }
325
+ }
@@ -0,0 +1,59 @@
1
+ package runtime
2
+
3
+ import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
4
+
5
+ // Scope and declared references have separate tokens from ownership and traffic.
6
+ const (
7
+ ResourceGraphRelationAppliesIn = "applies-in"
8
+ ResourceGraphRelationUsesPriorityClass = "uses-priority-class"
9
+ ResourceGraphRelationUsesRuntimeClass = "uses-runtime-class"
10
+ ResourceGraphRelationWebhookService = "webhook-service"
11
+ )
12
+
13
+ func graphClusterConfigurationKind(kind string) bool {
14
+ switch kind {
15
+ case "PriorityClass", "RuntimeClass", "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration":
16
+ return true
17
+ }
18
+ return false
19
+ }
20
+
21
+ func (b *graphRules) configurationRelations(o *unstructured.Unstructured, id string) {
22
+ if !graphBuiltinSpec(o) {
23
+ return
24
+ }
25
+ switch o.GetKind() {
26
+ case "ResourceQuota", "LimitRange":
27
+ // Namespace scope is a declaration; selectors/limits do not prove ownership.
28
+ if o.GetNamespace() != "" {
29
+ b.edge(id, b.target("v1", "Namespace", "", o.GetNamespace(), ""), ResourceGraphRelationAppliesIn)
30
+ }
31
+ case "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration":
32
+ for _, target := range graphWebhookServices(o, b.active) {
33
+ b.edge(id, b.target("v1", "Service", target.namespace, target.name, ""), ResourceGraphRelationWebhookService)
34
+ }
35
+ }
36
+ // Lease holderIdentity is opaque. Only metadata ownerReferences add edges.
37
+ }
38
+
39
+ type graphNamespacedName struct{ namespace, name string }
40
+
41
+ func graphWebhookServices(o *unstructured.Unstructured, active func() bool) []graphNamespacedName {
42
+ if !graphBuiltinSpec(o) || (o.GetKind() != "MutatingWebhookConfiguration" && o.GetKind() != "ValidatingWebhookConfiguration") {
43
+ return nil
44
+ }
45
+ result := []graphNamespacedName{}
46
+ for _, raw := range graphSlice(o.Object, "webhooks") {
47
+ if !active() {
48
+ break
49
+ }
50
+ webhook, _ := raw.(map[string]interface{})
51
+ config := graphMap(webhook, "clientConfig")
52
+ service := graphMap(config, "service")
53
+ namespace, name := graphString(service, "namespace"), graphString(service, "name")
54
+ if graphString(config, "url") == "" && namespace != "" && name != "" {
55
+ result = append(result, graphNamespacedName{namespace, name})
56
+ }
57
+ }
58
+ return result
59
+ }
@@ -0,0 +1,198 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "sort"
7
+
8
+ cr "github.com/opensoha/soha-contracts/resource"
9
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11
+ "k8s.io/apimachinery/pkg/runtime/schema"
12
+ "k8s.io/client-go/metadata"
13
+ )
14
+
15
+ const ResourceGraphMetadataTargetLimit = 16
16
+
17
+ func graphWorkloadKind(kind string) bool {
18
+ switch kind {
19
+ case "Pod", "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", "ReplicaSet", "ReplicationController":
20
+ return true
21
+ }
22
+ return false
23
+ }
24
+
25
+ func graphCollectKind(rootKind, kind string) bool {
26
+ if graphRBACRootKind(rootKind) {
27
+ return graphCollectRBACKind(rootKind, kind)
28
+ }
29
+ switch rootKind {
30
+ case "PriorityClass", "RuntimeClass":
31
+ // One cluster-wide page per workload source, never a loop over namespaces.
32
+ return graphWorkloadKind(kind)
33
+ case "ResourceQuota", "LimitRange", "Lease", "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration", "PersistentVolumeClaim", "PersistentVolume", "StorageClass":
34
+ return false
35
+ }
36
+ switch kind {
37
+ case "ResourceQuota", "LimitRange", "Lease", "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration", "PersistentVolume", "StorageClass",
38
+ "Role", "RoleBinding", "ClusterRole", "ClusterRoleBinding":
39
+ return false
40
+ }
41
+ return true
42
+ }
43
+
44
+ func graphWebhookProjection(full map[string]interface{}) []interface{} {
45
+ result := []interface{}{}
46
+ for _, raw := range graphSlice(full, "webhooks") {
47
+ webhook, _ := raw.(map[string]interface{})
48
+ config := graphMap(webhook, "clientConfig")
49
+ service := graphMap(config, "service")
50
+ if graphString(config, "url") != "" || service == nil {
51
+ continue
52
+ }
53
+ result = append(result, map[string]interface{}{"clientConfig": map[string]interface{}{"service": map[string]interface{}{
54
+ "namespace": graphString(service, "namespace"), "name": graphString(service, "name"),
55
+ }}})
56
+ }
57
+ return result
58
+ }
59
+
60
+ func collectGraphConfigurationTargets(ctx context.Context, client metadata.Interface, root cr.ResourceRef, object *unstructured.Unstructured, reads *int, objects *[]*unstructured.Unstructured, warnings *[]string) error {
61
+ targets := []cr.ResourceRef{}
62
+ switch root.Kind {
63
+ case "ResourceQuota", "LimitRange":
64
+ targets = append(targets, cr.ResourceRef{APIVersion: "v1", Kind: "Namespace", Name: root.Namespace})
65
+ case "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration":
66
+ for _, target := range graphWebhookServices(object, func() bool { return ctx.Err() == nil }) {
67
+ targets = append(targets, cr.ResourceRef{APIVersion: "v1", Kind: "Service", Namespace: target.namespace, Name: target.name})
68
+ }
69
+ case "Lease":
70
+ for _, owner := range object.GetOwnerReferences() {
71
+ if owner.Name == "" || owner.UID == "" || graphBuiltinAPIs[owner.Kind] != owner.APIVersion {
72
+ continue
73
+ }
74
+ namespace := root.Namespace
75
+ if owner.Kind == "Node" || owner.Kind == "Namespace" || graphClusterConfigurationKind(owner.Kind) {
76
+ namespace = ""
77
+ }
78
+ targets = append(targets, cr.ResourceRef{APIVersion: owner.APIVersion, Kind: owner.Kind, Namespace: namespace, Name: owner.Name, UID: string(owner.UID)})
79
+ }
80
+ }
81
+ seen := map[string]bool{}
82
+ for _, target := range targets {
83
+ if err := ctx.Err(); err != nil {
84
+ return err
85
+ }
86
+ key := graphIdentity(target.APIVersion, target.Kind, target.Namespace, target.Name)
87
+ if seen[key] {
88
+ continue
89
+ }
90
+ if *reads >= ResourceGraphMetadataTargetLimit {
91
+ *warnings = append(*warnings, "configuration reference metadata truncated at 16 targets")
92
+ break
93
+ }
94
+ seen[key] = true
95
+ resource, ok := graphMetadataResource(target.APIVersion, target.Kind)
96
+ if !ok {
97
+ continue
98
+ }
99
+ if client == nil {
100
+ *warnings = append(*warnings, target.Kind+" reference metadata unavailable")
101
+ continue
102
+ }
103
+ *reads += 1
104
+ obj, err := client.Resource(resource).Namespace(target.Namespace).Get(ctx, target.Name, metav1.GetOptions{})
105
+ if err != nil {
106
+ if fatal := graphConfigurationReadError(ctx, err, target.Kind, warnings); fatal != nil {
107
+ return fatal
108
+ }
109
+ continue
110
+ }
111
+ projected, err := graphObjectProjection(obj, target.APIVersion, target.Kind)
112
+ if err != nil {
113
+ *warnings = append(*warnings, target.Kind+" reference metadata projection unavailable")
114
+ continue
115
+ }
116
+ if projected.GetName() != target.Name || projected.GetNamespace() != target.Namespace || (target.UID != "" && string(projected.GetUID()) != target.UID) {
117
+ *warnings = append(*warnings, target.Kind+" reference metadata identity changed")
118
+ continue
119
+ }
120
+ *objects = append(*objects, projected)
121
+ }
122
+ return ctx.Err()
123
+ }
124
+
125
+ func graphConfigurationReadError(ctx context.Context, err error, kind string, warnings *[]string) error {
126
+ if canceled := ctx.Err(); canceled != nil {
127
+ return canceled
128
+ }
129
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
130
+ return err
131
+ }
132
+ *warnings = append(*warnings, kind+" reference metadata "+graphSourceErrorClass(err))
133
+ return nil
134
+ }
135
+
136
+ func collectGraphSecretMetadata(ctx context.Context, client metadata.Interface, graph cr.ResourceGraph, reads *int, objects *[]*unstructured.Unstructured, warnings *[]string) (bool, error) {
137
+ targets := []cr.ResourceRef{}
138
+ for _, node := range graph.Nodes {
139
+ if node.Status == "unresolved" && node.Resource.APIVersion == "v1" && node.Resource.Kind == "Secret" && node.Resource.Namespace != "" {
140
+ targets = append(targets, node.Resource)
141
+ }
142
+ }
143
+ sort.Slice(targets, func(i, j int) bool {
144
+ return graphNodeID(targets[i]) < graphNodeID(targets[j])
145
+ })
146
+ changed := false
147
+ seen := map[string]bool{}
148
+ for _, target := range targets {
149
+ if err := ctx.Err(); err != nil {
150
+ return changed, err
151
+ }
152
+ key := graphIdentity("v1", "Secret", target.Namespace, target.Name)
153
+ if seen[key] {
154
+ continue
155
+ }
156
+ if *reads >= ResourceGraphMetadataTargetLimit {
157
+ *warnings = append(*warnings, "Secret reference metadata truncated at 16 targets")
158
+ break
159
+ }
160
+ seen[key] = true
161
+ *reads += 1
162
+ object, err := client.Resource(graphSecretResource).Namespace(target.Namespace).Get(ctx, target.Name, metav1.GetOptions{})
163
+ if err != nil {
164
+ if fatal := graphConfigurationReadError(ctx, err, "Secret", warnings); fatal != nil {
165
+ return changed, fatal
166
+ }
167
+ continue
168
+ }
169
+ projected, err := graphObjectProjection(object, "v1", "Secret")
170
+ if err != nil {
171
+ *warnings = append(*warnings, "Secret reference metadata projection unavailable")
172
+ continue
173
+ }
174
+ if projected.GetName() != target.Name || projected.GetNamespace() != target.Namespace || (target.UID != "" && string(projected.GetUID()) != target.UID) {
175
+ *warnings = append(*warnings, "Secret reference metadata identity changed")
176
+ continue
177
+ }
178
+ *objects = append(*objects, projected)
179
+ changed = true
180
+ }
181
+ return changed, ctx.Err()
182
+ }
183
+
184
+ func graphMetadataResource(api, kind string) (schema.GroupVersionResource, bool) {
185
+ // Exact built-in resource names only; CR owners use the existing discovery path.
186
+ resources := map[string]string{
187
+ "Pod": "pods", "Deployment": "deployments", "StatefulSet": "statefulsets", "DaemonSet": "daemonsets", "ReplicaSet": "replicasets", "ReplicationController": "replicationcontrollers", "Job": "jobs", "CronJob": "cronjobs",
188
+ "ConfigMap": "configmaps", "Secret": "secrets", "Service": "services", "PersistentVolumeClaim": "persistentvolumeclaims", "ServiceAccount": "serviceaccounts", "Node": "nodes", "Namespace": "namespaces",
189
+ "Ingress": "ingresses", "HorizontalPodAutoscaler": "horizontalpodautoscalers", "PodDisruptionBudget": "poddisruptionbudgets", "ResourceQuota": "resourcequotas", "LimitRange": "limitranges", "PriorityClass": "priorityclasses", "RuntimeClass": "runtimeclasses", "Lease": "leases",
190
+ "MutatingWebhookConfiguration": "mutatingwebhookconfigurations", "ValidatingWebhookConfiguration": "validatingwebhookconfigurations",
191
+ }
192
+ resource, ok := resources[kind]
193
+ gv, err := schema.ParseGroupVersion(api)
194
+ if !ok || err != nil || graphBuiltinAPIs[kind] != api {
195
+ return schema.GroupVersionResource{}, false
196
+ }
197
+ return gv.WithResource(resource), true
198
+ }