@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.
- package/COMPATIBILITY.md +4 -3
- package/README.md +8 -9
- package/compat/consumer-revisions.json +1 -1
- package/compat/json-schema-ecosystem-0.1.0.json +1 -1
- package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
- package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
- package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
- package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
- package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
- package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
- package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
- package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
- package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
- package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
- package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
- package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
- package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
- package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
- package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
- package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
- package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
- package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/client_test.go +13 -0
- package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
- package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
- package/gen/go/sohaapi/types.go +421 -88
- package/gen/ts/sohaapi/index.ts +756 -4
- package/helmrelease/runtime/values_upgrade.go +78 -0
- package/helmrelease/runtime/values_upgrade_test.go +123 -0
- package/helmrelease/values_update.go +31 -0
- package/openapi/soha-api.json +1191 -14
- package/openapi/soha-api.yaml +588 -28
- package/openapi/spec_generated.go +3 -3
- package/package.json +1 -1
- package/resource/agent_profiles.go +9 -15
- package/resource/agent_profiles_test.go +9 -3
- package/resource/custom_graph_page.go +18 -0
- package/resource/models.go +102 -57
- package/resource/network_topology.go +22 -0
- package/resource/network_topology_projection.go +36 -22
- package/resource/network_topology_projection_test.go +4 -0
- package/resource/network_topology_test.go +20 -0
- package/resource/network_topology_validation.go +5 -0
- package/resource/observed_workload.go +18 -0
- package/resource/pod_status_test.go +22 -0
- package/resource/rbac_graph.go +4 -0
- package/resource/rollout_optional_test.go +33 -0
- package/resource/runtime/network_relationship_primitives.go +66 -0
- package/resource/runtime/network_relationship_primitives_test.go +147 -0
- package/resource/runtime/network_topology.go +17 -5
- package/resource/runtime/network_topology_builder.go +405 -0
- package/resource/runtime/network_topology_builder_backends.go +195 -0
- package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
- package/resource/runtime/network_topology_builder_ingress.go +302 -0
- package/resource/runtime/network_topology_builder_legacy.go +66 -0
- package/resource/runtime/network_topology_builder_providers.go +242 -0
- package/resource/runtime/network_topology_builder_test.go +186 -0
- package/resource/runtime/network_topology_declarations.go +329 -0
- package/resource/runtime/resource_edit.go +22 -0
- package/resource/runtime/resource_edit_test.go +32 -0
- package/resource/runtime/resource_graph_collect.go +325 -0
- package/resource/runtime/resource_graph_configuration.go +59 -0
- package/resource/runtime/resource_graph_configuration_collect.go +198 -0
- package/resource/runtime/resource_graph_configuration_test.go +470 -0
- package/resource/runtime/resource_graph_custom_page.go +159 -0
- package/resource/runtime/resource_graph_custom_root.go +132 -0
- package/resource/runtime/resource_graph_custom_root_test.go +197 -0
- package/resource/runtime/resource_graph_enrichment.go +417 -0
- package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
- package/resource/runtime/resource_graph_enrichment_test.go +201 -0
- package/resource/runtime/resource_graph_metadata.go +125 -0
- package/resource/runtime/resource_graph_metadata_test.go +204 -0
- package/resource/runtime/resource_graph_network.go +224 -0
- package/resource/runtime/resource_graph_network_collect.go +174 -0
- package/resource/runtime/resource_graph_network_collect_test.go +147 -0
- package/resource/runtime/resource_graph_network_test.go +342 -0
- package/resource/runtime/resource_graph_rbac.go +278 -0
- package/resource/runtime/resource_graph_rbac_test.go +372 -0
- package/resource/runtime/resource_graph_rules.go +834 -0
- package/resource/runtime/resource_graph_storage.go +144 -0
- package/resource/runtime/resource_graph_storage_collect.go +163 -0
- package/resource/runtime/resource_graph_storage_test.go +397 -0
- package/resource/runtime/resource_graph_test.go +627 -0
- package/resource/runtime/secret_certificate.go +44 -0
- package/resource/runtime/secret_certificate_test.go +87 -0
- package/resource/storage_graph.go +4 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
6
|
+
"k8s.io/apimachinery/pkg/labels"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// servicePodSelector preserves Service's nonempty equality-selector semantics.
|
|
10
|
+
// Malformed values invalidate the selector rather than silently broadening it.
|
|
11
|
+
func servicePodSelector(selector map[string]any) labels.Selector {
|
|
12
|
+
if len(selector) == 0 {
|
|
13
|
+
return nil
|
|
14
|
+
}
|
|
15
|
+
set := labels.Set{}
|
|
16
|
+
for key, value := range selector {
|
|
17
|
+
label, ok := value.(string)
|
|
18
|
+
if !ok {
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
set[key] = label
|
|
22
|
+
}
|
|
23
|
+
return labels.SelectorFromSet(set)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ingressBackendDeclaration struct {
|
|
27
|
+
host string
|
|
28
|
+
path map[string]any
|
|
29
|
+
serviceName string
|
|
30
|
+
port string
|
|
31
|
+
fieldPath string
|
|
32
|
+
serviceDeclared bool
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// forEachIngressBackendDeclaration extracts configured references once. Callers
|
|
36
|
+
// retain their own graph projection, resolution, authorization and budget rules.
|
|
37
|
+
func forEachIngressBackendDeclaration(body map[string]any, active func() bool, visit func(ingressBackendDeclaration)) {
|
|
38
|
+
if !active() {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if backend := object(at(body, "spec", "defaultBackend")); len(backend) > 0 {
|
|
42
|
+
visit(ingressBackendFact("", map[string]any{"path": "/", "pathType": "Default"}, backend, "spec.defaultBackend"))
|
|
43
|
+
}
|
|
44
|
+
rules, _ := at(body, "spec", "rules").([]any)
|
|
45
|
+
for ruleIndex, raw := range rules {
|
|
46
|
+
if !active() {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
rule := object(raw)
|
|
50
|
+
paths, _ := at(rule, "http", "paths").([]any)
|
|
51
|
+
for pathIndex, rawPath := range paths {
|
|
52
|
+
if !active() {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
path := object(rawPath)
|
|
56
|
+
visit(ingressBackendFact(text(rule["host"]), path, object(path["backend"]), fmt.Sprintf("spec.rules[%d].http.paths[%d].backend", ruleIndex, pathIndex)))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func ingressBackendFact(host string, path, backend map[string]any, fieldPath string) ingressBackendDeclaration {
|
|
62
|
+
service := object(backend["service"])
|
|
63
|
+
name, _ := service["name"].(string)
|
|
64
|
+
port := object(service["port"])
|
|
65
|
+
return ingressBackendDeclaration{host: host, path: path, serviceName: name, port: firstTopology(text(port["name"]), text(port["number"])), fieldPath: fieldPath, serviceDeclared: len(service) > 0}
|
|
66
|
+
}
|
|
@@ -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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|