@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,144 @@
1
+ package runtime
2
+
3
+ import (
4
+ cr "github.com/opensoha/soha-contracts/resource"
5
+ corev1 "k8s.io/api/core/v1"
6
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
7
+ )
8
+
9
+ const (
10
+ ResourceGraphRelationMounts = "mounts"
11
+ ResourceGraphRelationBindsTo = "binds-to"
12
+ ResourceGraphRelationUsesStorageClass = "uses-storage-class"
13
+ )
14
+
15
+ func graphStorageRootKind(kind string) bool {
16
+ switch kind {
17
+ case "PersistentVolumeClaim", "PersistentVolume", "StorageClass":
18
+ return true
19
+ }
20
+ return false
21
+ }
22
+
23
+ func graphStorageClassName(o *unstructured.Unstructured) string {
24
+ name, _, _ := unstructured.NestedString(o.Object, "spec", "storageClassName")
25
+ return name
26
+ }
27
+
28
+ func graphClaimVolumeName(claim *unstructured.Unstructured) string {
29
+ name, _, _ := unstructured.NestedString(claim.Object, "spec", "volumeName")
30
+ return name
31
+ }
32
+
33
+ func graphVolumeClaimName(volume map[string]interface{}) string {
34
+ return graphString(graphMap(volume, "persistentVolumeClaim"), "claimName")
35
+ }
36
+
37
+ func graphVolumeClaimRef(volume *unstructured.Unstructured) (graphNamespacedName, string) {
38
+ ref, _, _ := graphNestedMap(volume.Object, "spec", "claimRef")
39
+ if api := graphString(ref, "apiVersion"); api != "" && api != "v1" {
40
+ return graphNamespacedName{}, ""
41
+ }
42
+ if kind := graphString(ref, "kind"); kind != "" && kind != "PersistentVolumeClaim" {
43
+ return graphNamespacedName{}, ""
44
+ }
45
+ return graphNamespacedName{namespace: graphString(ref, "namespace"), name: graphString(ref, "name")}, graphString(ref, "uid")
46
+ }
47
+
48
+ func graphStorageBound(claim, volume *unstructured.Unstructured) bool {
49
+ if claim == nil || volume == nil || claim.GetDeletionTimestamp() != nil || volume.GetDeletionTimestamp() != nil {
50
+ return false
51
+ }
52
+ if graphStatus(claim) != string(corev1.ClaimBound) || graphStatus(volume) != string(corev1.VolumeBound) {
53
+ return false
54
+ }
55
+ ref, uid := graphVolumeClaimRef(volume)
56
+ return ref.namespace != "" && ref.name != "" && uid != "" && uid == string(claim.GetUID()) &&
57
+ ref.namespace == claim.GetNamespace() && ref.name == claim.GetName() && graphClaimVolumeName(claim) == volume.GetName()
58
+ }
59
+
60
+ func (b *graphRules) storageRelations(o *unstructured.Unstructured, id string) {
61
+ if !graphBuiltinSpec(o) {
62
+ return
63
+ }
64
+ switch o.GetKind() {
65
+ case "Pod":
66
+ for _, raw := range graphSlice(graphPodSpec(o), "volumes") {
67
+ if !b.active() {
68
+ return
69
+ }
70
+ volume, _ := raw.(map[string]interface{})
71
+ b.storageMountEdge(id, o.GetNamespace(), graphVolumeClaimName(volume))
72
+ }
73
+ return
74
+ case "PersistentVolumeClaim":
75
+ if o.GetNamespace() == "" {
76
+ return
77
+ }
78
+ if volumeName := graphClaimVolumeName(o); volumeName != "" {
79
+ volume := b.objects[graphIdentity("v1", "PersistentVolume", "", volumeName)]
80
+ edge := cr.ResourceGraphEdge{SourceID: id, Relation: ResourceGraphRelationBindsTo, FieldPath: "spec.volumeName"}
81
+ if graphStorageBound(o, volume) {
82
+ edge.TargetID = b.node(b.ref(volume), graphStatus(volume))
83
+ edge.ResolutionState = "resolved"
84
+ edge.Note = "PVC and PV report Bound with matching references"
85
+ } else {
86
+ edge.TargetID = b.unresolvedStorageNode("PersistentVolume", "", volumeName, "")
87
+ edge.ResolutionState = "unresolved"
88
+ edge.Note = "PVC volume reference requires binding confirmation"
89
+ }
90
+ b.addEdge(edge)
91
+ }
92
+ case "PersistentVolume":
93
+ if o.GetNamespace() != "" {
94
+ return
95
+ }
96
+ ref, uid := graphVolumeClaimRef(o)
97
+ if ref.namespace != "" && ref.name != "" {
98
+ claim := b.objects[graphIdentity("v1", "PersistentVolumeClaim", ref.namespace, ref.name)]
99
+ if !graphStorageBound(claim, o) {
100
+ declaration := b.ref(o)
101
+ b.addEdge(cr.ResourceGraphEdge{
102
+ SourceID: b.unresolvedStorageNode("PersistentVolumeClaim", ref.namespace, ref.name, uid),
103
+ TargetID: id, Relation: ResourceGraphRelationBindsTo, ResolutionState: "unresolved",
104
+ FieldPath: "spec.claimRef", Note: "PV claim reference requires binding confirmation", SourceRef: &declaration,
105
+ })
106
+ }
107
+ }
108
+ default:
109
+ return
110
+ }
111
+ b.storageReferenceEdge(id, "storage.k8s.io/v1", "StorageClass", "", graphStorageClassName(o),
112
+ ResourceGraphRelationUsesStorageClass, "spec.storageClassName", "StorageClass reference declared in spec")
113
+ }
114
+
115
+ func (b *graphRules) storageMountEdge(source, namespace, claim string) {
116
+ if namespace != "" {
117
+ b.storageReferenceEdge(source, "v1", "PersistentVolumeClaim", namespace, claim,
118
+ ResourceGraphRelationMounts, "spec.volumes", "PVC reference declared in Pod spec")
119
+ }
120
+ }
121
+
122
+ func (b *graphRules) unresolvedStorageNode(kind, namespace, name, uid string) string {
123
+ ref := cr.ResourceRef{ClusterID: b.cluster, APIVersion: "v1", Kind: kind, Namespace: namespace, Name: name, UID: uid, ScopeMode: topologyScope(namespace != "")}
124
+ id := graphNodeID(ref)
125
+ if observed := b.objects[graphIdentity(ref.APIVersion, kind, namespace, name)]; observed != nil && graphNodeID(b.ref(observed)) == id {
126
+ // A one-sided declaration can name an observed object without binding it.
127
+ id += ":unresolved"
128
+ }
129
+ return b.addNode(id, ref, "unresolved")
130
+ }
131
+
132
+ func (b *graphRules) storageReferenceEdge(source, api, kind, namespace, name, relation, fieldPath, note string) {
133
+ if name == "" {
134
+ return
135
+ }
136
+ state := "unresolved"
137
+ if b.objects[graphIdentity(api, kind, namespace, name)] != nil {
138
+ state = "resolved"
139
+ }
140
+ b.addEdge(cr.ResourceGraphEdge{
141
+ SourceID: source, TargetID: b.target(api, kind, namespace, name, ""), Relation: relation,
142
+ ResolutionState: state, FieldPath: fieldPath, Note: note,
143
+ })
144
+ }
@@ -0,0 +1,163 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+
7
+ corev1 "k8s.io/api/core/v1"
8
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10
+ kruntime "k8s.io/apimachinery/pkg/runtime"
11
+ "k8s.io/apimachinery/pkg/runtime/schema"
12
+ "k8s.io/client-go/kubernetes"
13
+ )
14
+
15
+ func graphStorageProjection(obj kruntime.Object, projected *unstructured.Unstructured) error {
16
+ kind := projected.GetKind()
17
+ _, metadataOnly := obj.(*metav1.PartialObjectMetadata)
18
+ if gvk := obj.GetObjectKind().GroupVersionKind(); !metadataOnly && !gvk.Empty() && gvk != schema.FromAPIVersionAndKind(projected.GetAPIVersion(), kind) {
19
+ return errors.New("storage relationship observation GVK mismatch")
20
+ }
21
+ if projected.GetName() == "" || (kind == "PersistentVolumeClaim") != (projected.GetNamespace() != "") {
22
+ return errors.New("storage relationship observation scope mismatch")
23
+ }
24
+ switch item := obj.(type) {
25
+ case *corev1.PersistentVolumeClaim:
26
+ spec := map[string]interface{}{"volumeName": item.Spec.VolumeName}
27
+ if item.Spec.StorageClassName != nil {
28
+ spec["storageClassName"] = *item.Spec.StorageClassName
29
+ }
30
+ projected.Object["spec"] = spec
31
+ projected.Object["status"] = map[string]interface{}{"phase": string(item.Status.Phase)}
32
+ if item.DeletionTimestamp != nil {
33
+ projected.SetDeletionTimestamp(item.DeletionTimestamp)
34
+ }
35
+ case *corev1.PersistentVolume:
36
+ spec := map[string]interface{}{"storageClassName": item.Spec.StorageClassName}
37
+ if ref := item.Spec.ClaimRef; ref != nil {
38
+ spec["claimRef"] = map[string]interface{}{"apiVersion": ref.APIVersion, "kind": ref.Kind, "namespace": ref.Namespace, "name": ref.Name, "uid": string(ref.UID)}
39
+ }
40
+ projected.Object["spec"] = spec
41
+ projected.Object["status"] = map[string]interface{}{"phase": string(item.Status.Phase)}
42
+ if item.DeletionTimestamp != nil {
43
+ projected.SetDeletionTimestamp(item.DeletionTimestamp)
44
+ }
45
+ }
46
+ return nil
47
+ }
48
+
49
+ func collectGraphStorage(ctx context.Context, client kubernetes.Interface, sources map[string]graphSourceDefinition, root *unstructured.Unstructured, objects *[]*unstructured.Unstructured, warnings *[]string) error {
50
+ var claim, volume *unstructured.Unstructured
51
+ var err error
52
+ switch root.GetKind() {
53
+ case "PersistentVolumeClaim":
54
+ claim = root
55
+ volume, err = collectGraphStorageTarget(ctx, "PersistentVolume", "", graphClaimVolumeName(claim), client.CoreV1().PersistentVolumes().Get, objects, warnings)
56
+ case "PersistentVolume":
57
+ volume = root
58
+ ref, _ := graphVolumeClaimRef(volume)
59
+ if ref.namespace != "" {
60
+ claim, err = collectGraphStorageTarget(ctx, "PersistentVolumeClaim", ref.namespace, ref.name, client.CoreV1().PersistentVolumeClaims(ref.namespace).Get, objects, warnings)
61
+ }
62
+ case "StorageClass":
63
+ return collectGraphStorageClassSources(ctx, client, sources, root.GetName(), objects, warnings)
64
+ default:
65
+ return nil
66
+ }
67
+ if err != nil {
68
+ return err
69
+ }
70
+ class := graphStorageClassName(root)
71
+ getClass := client.StorageV1().StorageClasses().Get
72
+ if _, err = collectGraphStorageTarget(ctx, "StorageClass", "", class, getClass, objects, warnings); err != nil {
73
+ return err
74
+ }
75
+ bound := graphStorageBound(claim, volume)
76
+ if bound && root == claim && graphStorageClassName(volume) != class {
77
+ if _, err = collectGraphStorageTarget(ctx, "StorageClass", "", graphStorageClassName(volume), getClass, objects, warnings); err != nil {
78
+ return err
79
+ }
80
+ }
81
+ if root != claim && !bound {
82
+ return ctx.Err()
83
+ }
84
+ ref := graphNamespacedName{namespace: claim.GetNamespace(), name: claim.GetName()}
85
+ return collectGraphStoragePods(ctx, client, ref.namespace, map[graphNamespacedName]bool{ref: true}, objects, warnings)
86
+ }
87
+
88
+ func collectGraphStorageTarget[T kruntime.Object](ctx context.Context, kind, namespace, name string, get func(context.Context, string, metav1.GetOptions) (T, error), objects *[]*unstructured.Unstructured, warnings *[]string) (*unstructured.Unstructured, error) {
89
+ if err := ctx.Err(); err != nil || name == "" {
90
+ return nil, err
91
+ }
92
+ item, err := get(ctx, name, metav1.GetOptions{})
93
+ if err != nil {
94
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
95
+ return nil, err
96
+ }
97
+ *warnings = append(*warnings, kind+" reference source "+graphSourceErrorClass(err))
98
+ return nil, ctx.Err()
99
+ }
100
+ projected, err := graphObjectProjection(item, graphBuiltinAPIs[kind], kind)
101
+ if err != nil || projected.GetName() != name || projected.GetNamespace() != namespace {
102
+ *warnings = append(*warnings, kind+" reference source projection unavailable")
103
+ return nil, ctx.Err()
104
+ }
105
+ *objects = append(*objects, projected)
106
+ return projected, ctx.Err()
107
+ }
108
+
109
+ func collectGraphStorageClassSources(ctx context.Context, client kubernetes.Interface, sources map[string]graphSourceDefinition, name string, objects *[]*unstructured.Unstructured, warnings *[]string) error {
110
+ volumesStart := len(*objects)
111
+ if err := sources["PersistentVolume"].collect(ctx, objects, warnings); err != nil {
112
+ return err
113
+ }
114
+ classVolumes := map[string]*unstructured.Unstructured{}
115
+ for _, volume := range (*objects)[volumesStart:] {
116
+ if graphStorageClassName(volume) == name && classVolumes[volume.GetName()] == nil {
117
+ classVolumes[volume.GetName()] = volume
118
+ }
119
+ }
120
+ claimsStart := len(*objects)
121
+ if err := sources["PersistentVolumeClaim"].collect(ctx, objects, warnings); err != nil {
122
+ return err
123
+ }
124
+ claims := map[graphNamespacedName]bool{}
125
+ for _, claim := range (*objects)[claimsStart:] {
126
+ if graphStorageClassName(claim) == name || graphStorageBound(claim, classVolumes[graphClaimVolumeName(claim)]) {
127
+ claims[graphNamespacedName{namespace: claim.GetNamespace(), name: claim.GetName()}] = true
128
+ }
129
+ }
130
+ if len(claims) == 0 {
131
+ return ctx.Err()
132
+ }
133
+ return collectGraphStoragePods(ctx, client, "", claims, objects, warnings)
134
+ }
135
+
136
+ func collectGraphStoragePods(ctx context.Context, client kubernetes.Interface, namespace string, claims map[graphNamespacedName]bool, objects *[]*unstructured.Unstructured, warnings *[]string) error {
137
+ project := func(obj kruntime.Object) (*unstructured.Unstructured, error) {
138
+ pod, ok := obj.(*corev1.Pod)
139
+ if !ok || pod.Namespace == "" || (namespace != "" && pod.Namespace != namespace) {
140
+ return nil, errors.New("storage relationship Pod scope mismatch")
141
+ }
142
+ volumes := []interface{}{}
143
+ for _, volume := range pod.Spec.Volumes {
144
+ if err := ctx.Err(); err != nil {
145
+ return nil, err
146
+ }
147
+ if claim := volume.PersistentVolumeClaim; claim != nil && claims[graphNamespacedName{namespace: pod.Namespace, name: claim.ClaimName}] {
148
+ volumes = append(volumes, map[string]interface{}{"persistentVolumeClaim": map[string]interface{}{"claimName": claim.ClaimName}})
149
+ }
150
+ }
151
+ if len(volumes) == 0 {
152
+ return nil, nil
153
+ }
154
+ projected, err := graphObjectMetadataProjection(pod, "v1", "Pod", false)
155
+ if err != nil {
156
+ return nil, err
157
+ }
158
+ projected.Object["spec"] = map[string]interface{}{"volumes": volumes}
159
+ projected.Object["status"] = map[string]interface{}{"phase": string(pod.Status.Phase)}
160
+ return projected, nil
161
+ }
162
+ return collectGraphSource(ctx, client.CoreV1().Pods(namespace).List, "Pod", project, objects, warnings)
163
+ }