@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,834 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
8
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
9
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
10
|
+
"k8s.io/apimachinery/pkg/labels"
|
|
11
|
+
kruntime "k8s.io/apimachinery/pkg/runtime"
|
|
12
|
+
"sort"
|
|
13
|
+
"strings"
|
|
14
|
+
"time"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// ErrInvalidResourceGraphRoot identifies invalid scope or stale root identity.
|
|
18
|
+
var ErrInvalidResourceGraphRoot = errors.New("invalid resource graph root")
|
|
19
|
+
|
|
20
|
+
// ErrUnsupportedResourceGraphRoot identifies roots requiring another collector.
|
|
21
|
+
var ErrUnsupportedResourceGraphRoot = errors.New("unsupported resource graph root")
|
|
22
|
+
|
|
23
|
+
func invalidGraphRoot(message string) error {
|
|
24
|
+
return fmt.Errorf("%w: %s", ErrInvalidResourceGraphRoot, message)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var graphBuiltinAPIs = map[string]string{
|
|
28
|
+
"Pod": "v1", "ReplicationController": "v1", "Service": "v1", "Secret": "v1", "ConfigMap": "v1", "PersistentVolumeClaim": "v1", "PersistentVolume": "v1", "ServiceAccount": "v1", "Node": "v1", "Namespace": "v1", "Event": "v1",
|
|
29
|
+
"Deployment": "apps/v1", "StatefulSet": "apps/v1", "DaemonSet": "apps/v1", "ReplicaSet": "apps/v1", "Job": "batch/v1", "CronJob": "batch/v1",
|
|
30
|
+
"Ingress": "networking.k8s.io/v1", "HorizontalPodAutoscaler": "autoscaling/v2", "PodDisruptionBudget": "policy/v1",
|
|
31
|
+
"ResourceQuota": "v1", "LimitRange": "v1", "PriorityClass": "scheduling.k8s.io/v1", "RuntimeClass": "node.k8s.io/v1", "Lease": "coordination.k8s.io/v1",
|
|
32
|
+
"MutatingWebhookConfiguration": "admissionregistration.k8s.io/v1", "ValidatingWebhookConfiguration": "admissionregistration.k8s.io/v1",
|
|
33
|
+
"StorageClass": "storage.k8s.io/v1",
|
|
34
|
+
"Role": graphRBACAPI, "RoleBinding": graphRBACAPI, "ClusterRole": graphRBACAPI, "ClusterRoleBinding": graphRBACAPI,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// NormalizeResourceGraphRoot canonicalizes known wire kinds and trims user input.
|
|
38
|
+
// It never supplies a default namespace or relaxes namespaced scope validation.
|
|
39
|
+
func NormalizeResourceGraphRoot(root cr.ResourceRef) (cr.ResourceRef, error) {
|
|
40
|
+
root.ClusterID = strings.TrimSpace(root.ClusterID)
|
|
41
|
+
root.APIVersion = strings.TrimSpace(root.APIVersion)
|
|
42
|
+
root.Kind = strings.TrimSpace(root.Kind)
|
|
43
|
+
root.Name = strings.TrimSpace(root.Name)
|
|
44
|
+
root.Namespace = strings.TrimSpace(root.Namespace)
|
|
45
|
+
for _, definition := range cr.NetworkTopologySources() {
|
|
46
|
+
if networkGraphRootKind(definition.Kind) && strings.EqualFold(root.Kind, definition.Kind) {
|
|
47
|
+
root.Kind = definition.Kind
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for kind := range graphBuiltinAPIs {
|
|
52
|
+
if strings.EqualFold(root.Kind, kind) {
|
|
53
|
+
root.Kind = kind
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return root, validateGraphRoot(root)
|
|
58
|
+
}
|
|
59
|
+
func validateGraphRoot(root cr.ResourceRef) error {
|
|
60
|
+
if root.ClusterID == "" || root.Kind == "" || root.Name == "" {
|
|
61
|
+
return invalidGraphRoot("resource graph root requires cluster, kind and name")
|
|
62
|
+
}
|
|
63
|
+
if definition, ok := networkGraphRootSource(root); ok {
|
|
64
|
+
return validateNetworkGraphRootScope(root, definition)
|
|
65
|
+
}
|
|
66
|
+
builtinAPI, kindKnown := graphBuiltinAPIs[root.Kind]
|
|
67
|
+
known := kindKnown && (root.APIVersion == "" || root.APIVersion == builtinAPI)
|
|
68
|
+
if known && graphStorageRootKind(root.Kind) && root.ScopeMode != "" && root.ScopeMode != topologyScope(root.Kind == "PersistentVolumeClaim") {
|
|
69
|
+
return invalidGraphRoot("storage resource graph root scope mismatch")
|
|
70
|
+
}
|
|
71
|
+
if known && graphRBACRootKind(root.Kind) && root.ScopeMode != "" && root.ScopeMode != topologyScope(!graphRBACClusterKind(root.Kind)) {
|
|
72
|
+
return invalidGraphRoot("RBAC resource graph root scope mismatch")
|
|
73
|
+
}
|
|
74
|
+
coreAPI := root.APIVersion == "" || root.APIVersion == "v1"
|
|
75
|
+
clusterKind := coreAPI && (root.Kind == "Node" || root.Kind == "Namespace") || known && (graphClusterConfigurationKind(root.Kind) || root.Kind == "PersistentVolume" || root.Kind == "StorageClass" || graphRBACClusterKind(root.Kind))
|
|
76
|
+
if root.Namespace == "" && !clusterKind && (known || root.ScopeMode != cr.ResourceScopeModeCluster) {
|
|
77
|
+
return invalidGraphRoot("namespaced resource graph root requires namespace")
|
|
78
|
+
}
|
|
79
|
+
if clusterKind && root.Namespace != "" {
|
|
80
|
+
return invalidGraphRoot("cluster resource graph root cannot have namespace")
|
|
81
|
+
}
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ResourceGraphWorkingNodeLimit = 10000
|
|
86
|
+
const ResourceGraphWorkingEdgeLimit = 10000
|
|
87
|
+
|
|
88
|
+
var ErrResourceGraphBudget = errors.New("resource graph working set budget exceeded")
|
|
89
|
+
|
|
90
|
+
type graphRules struct {
|
|
91
|
+
ctx context.Context
|
|
92
|
+
err error
|
|
93
|
+
partial bool
|
|
94
|
+
pods []*unstructured.Unstructured
|
|
95
|
+
clusterRoles []*unstructured.Unstructured
|
|
96
|
+
|
|
97
|
+
cluster string
|
|
98
|
+
objects map[string]*unstructured.Unstructured
|
|
99
|
+
nodes map[string]cr.ResourceGraphNode
|
|
100
|
+
edges map[string]cr.ResourceGraphEdge
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func graphIdentity(api, kind, namespace, name string) string {
|
|
104
|
+
return api + ":" + kind + ":" + namespace + "/" + name
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var graphIDSegmentEscaper = strings.NewReplacer("%", "%25", ":", "%3A")
|
|
108
|
+
|
|
109
|
+
func graphNodeID(ref cr.ResourceRef) string {
|
|
110
|
+
id := graphIdentity(ref.APIVersion, ref.Kind, ref.Namespace, graphIDSegmentEscaper.Replace(ref.Name))
|
|
111
|
+
if ref.UID != "" {
|
|
112
|
+
id += ":" + graphIDSegmentEscaper.Replace(ref.UID)
|
|
113
|
+
}
|
|
114
|
+
return id
|
|
115
|
+
}
|
|
116
|
+
func (b *graphRules) ref(o *unstructured.Unstructured) cr.ResourceRef {
|
|
117
|
+
scope := cr.ResourceScopeModeNamespace
|
|
118
|
+
if o.GetNamespace() == "" {
|
|
119
|
+
scope = cr.ResourceScopeModeCluster
|
|
120
|
+
}
|
|
121
|
+
return cr.ResourceRef{ClusterID: b.cluster, APIVersion: o.GetAPIVersion(), Kind: o.GetKind(), Namespace: o.GetNamespace(), Name: o.GetName(), UID: string(o.GetUID()), ResourceVersion: o.GetResourceVersion(), ScopeMode: scope}
|
|
122
|
+
}
|
|
123
|
+
func (b *graphRules) active() bool {
|
|
124
|
+
if b.err != nil {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
if err := b.ctx.Err(); err != nil {
|
|
128
|
+
b.err = err
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
return !b.partial
|
|
132
|
+
}
|
|
133
|
+
func (b *graphRules) node(ref cr.ResourceRef, status string) string {
|
|
134
|
+
return b.addNode(graphNodeID(ref), ref, status)
|
|
135
|
+
}
|
|
136
|
+
func (b *graphRules) addNode(id string, ref cr.ResourceRef, status string) string {
|
|
137
|
+
if !b.active() {
|
|
138
|
+
return ""
|
|
139
|
+
}
|
|
140
|
+
if _, ok := b.nodes[id]; !ok {
|
|
141
|
+
if len(b.nodes) >= ResourceGraphWorkingNodeLimit {
|
|
142
|
+
b.partial = true
|
|
143
|
+
return ""
|
|
144
|
+
}
|
|
145
|
+
b.nodes[id] = cr.ResourceGraphNode{ID: id, Resource: ref, Status: status}
|
|
146
|
+
}
|
|
147
|
+
return id
|
|
148
|
+
}
|
|
149
|
+
func (b *graphRules) target(api, kind, namespace, name, uid string) string {
|
|
150
|
+
if name == "" {
|
|
151
|
+
return ""
|
|
152
|
+
}
|
|
153
|
+
ref := cr.ResourceRef{ClusterID: b.cluster, APIVersion: api, Kind: kind, Namespace: namespace, Name: name, UID: uid, ScopeMode: cr.ResourceScopeModeNamespace}
|
|
154
|
+
if namespace == "" {
|
|
155
|
+
ref.ScopeMode = cr.ResourceScopeModeCluster
|
|
156
|
+
}
|
|
157
|
+
if o := b.objects[graphIdentity(api, kind, namespace, name)]; o != nil && (uid == "" || string(o.GetUID()) == uid) {
|
|
158
|
+
return b.node(b.ref(o), graphStatus(o))
|
|
159
|
+
}
|
|
160
|
+
return b.node(ref, "unresolved")
|
|
161
|
+
}
|
|
162
|
+
func (b *graphRules) edge(source, target, relation string) {
|
|
163
|
+
b.addEdge(cr.ResourceGraphEdge{SourceID: source, TargetID: target, Relation: relation})
|
|
164
|
+
}
|
|
165
|
+
func (b *graphRules) addEdge(edge cr.ResourceGraphEdge) {
|
|
166
|
+
if !b.active() {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
if edge.SourceID == "" || edge.TargetID == "" || edge.SourceID == edge.TargetID {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
edge.ID = edge.Relation + ":" + edge.SourceID + ":" + edge.TargetID
|
|
173
|
+
if _, exists := b.edges[edge.ID]; !exists && len(b.edges) >= ResourceGraphWorkingEdgeLimit {
|
|
174
|
+
b.partial = true
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
b.edges[edge.ID] = edge
|
|
178
|
+
}
|
|
179
|
+
func graphIsEvent(o *unstructured.Unstructured) bool {
|
|
180
|
+
return o.GetAPIVersion() == "v1" && o.GetKind() == "Event"
|
|
181
|
+
}
|
|
182
|
+
func graphStatus(o *unstructured.Unstructured) string {
|
|
183
|
+
if !graphBuiltinSpec(o) {
|
|
184
|
+
return ""
|
|
185
|
+
}
|
|
186
|
+
switch o.GetKind() {
|
|
187
|
+
case "Deployment":
|
|
188
|
+
return graphReplicaStatus(o, "availableReplicas")
|
|
189
|
+
case "ReplicaSet":
|
|
190
|
+
return graphReplicaStatus(o, "readyReplicas")
|
|
191
|
+
case "Ingress":
|
|
192
|
+
ingress, _, _ := graphNestedSlice(o.Object, "status", "loadBalancer", "ingress")
|
|
193
|
+
if len(ingress) > 0 {
|
|
194
|
+
return "ready"
|
|
195
|
+
}
|
|
196
|
+
return "pending"
|
|
197
|
+
case "Service":
|
|
198
|
+
value, _, _ := unstructured.NestedString(o.Object, "spec", "type")
|
|
199
|
+
return value
|
|
200
|
+
default:
|
|
201
|
+
phase, _, _ := unstructured.NestedString(o.Object, "status", "phase")
|
|
202
|
+
return phase
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
func graphReplicaStatus(o *unstructured.Unstructured, readyField string) string {
|
|
206
|
+
replicas, found, _ := unstructured.NestedInt64(o.Object, "status", "replicas")
|
|
207
|
+
ready, readyFound, _ := unstructured.NestedInt64(o.Object, "status", readyField)
|
|
208
|
+
if found && readyFound && replicas > 0 && ready >= replicas {
|
|
209
|
+
return "healthy"
|
|
210
|
+
}
|
|
211
|
+
return "progressing"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// BuildResourceGraphFromObjects applies the same pure rules to typed projections
|
|
215
|
+
// or arbitrary Kubernetes objects. Unknown kinds contribute only metadata
|
|
216
|
+
// ownerReferences; their spec is never inferred. Callers own discovery and scope.
|
|
217
|
+
func BuildResourceGraphFromObjects(root cr.ResourceRef, objects []*unstructured.Unstructured) (cr.ResourceGraph, error) {
|
|
218
|
+
ctx, cancel := context.WithTimeout(context.Background(), ResourceGraphTimeout)
|
|
219
|
+
defer cancel()
|
|
220
|
+
return buildResourceGraphWithContext(ctx, root, objects)
|
|
221
|
+
}
|
|
222
|
+
func buildResourceGraphWithContext(ctx context.Context, root cr.ResourceRef, objects []*unstructured.Unstructured) (cr.ResourceGraph, error) {
|
|
223
|
+
if err := ctx.Err(); err != nil {
|
|
224
|
+
return cr.ResourceGraph{}, err
|
|
225
|
+
}
|
|
226
|
+
if len(objects) > ResourceGraphWorkingNodeLimit {
|
|
227
|
+
return cr.ResourceGraph{}, ErrResourceGraphBudget
|
|
228
|
+
}
|
|
229
|
+
root, err := NormalizeResourceGraphRoot(root)
|
|
230
|
+
if err != nil {
|
|
231
|
+
return cr.ResourceGraph{}, err
|
|
232
|
+
}
|
|
233
|
+
index, err := graphObjectIndex(ctx, root.Namespace, objects)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return cr.ResourceGraph{}, err
|
|
236
|
+
}
|
|
237
|
+
b := &graphRules{ctx: ctx, cluster: root.ClusterID, objects: index, nodes: map[string]cr.ResourceGraphNode{}, edges: map[string]cr.ResourceGraphEdge{}}
|
|
238
|
+
rootObject, err := b.rootObject(root)
|
|
239
|
+
if err != nil {
|
|
240
|
+
return cr.ResourceGraph{}, err
|
|
241
|
+
}
|
|
242
|
+
rootID := b.node(b.ref(rootObject), graphStatus(rootObject))
|
|
243
|
+
keys := b.objectKeys(rootObject)
|
|
244
|
+
storage := graphBuiltinSpec(rootObject) && graphStorageRootKind(root.Kind)
|
|
245
|
+
rbac := graphBuiltinSpec(rootObject) && graphRBACRootKind(root.Kind)
|
|
246
|
+
if err := indexGraphCrossNamespaceAccounts(ctx, rootObject, objects, index); err != nil {
|
|
247
|
+
return cr.ResourceGraph{}, err
|
|
248
|
+
}
|
|
249
|
+
if !storage {
|
|
250
|
+
b.priorityOwners(rootObject)
|
|
251
|
+
for _, key := range keys {
|
|
252
|
+
if !b.active() {
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
o := b.objects[key]
|
|
256
|
+
if graphIsEvent(o) {
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
id := b.node(b.ref(o), graphStatus(o))
|
|
260
|
+
b.ownerRelations(o, id)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for _, key := range keys {
|
|
264
|
+
if !b.active() {
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
o := b.objects[key]
|
|
268
|
+
if graphIsEvent(o) {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
id := b.node(b.ref(o), graphStatus(o))
|
|
272
|
+
if storage {
|
|
273
|
+
b.storageRelations(o, id)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if rbac {
|
|
277
|
+
b.rbacRelations(o, id)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
b.podReferences(o, id)
|
|
281
|
+
b.selectorRelations(o, id)
|
|
282
|
+
b.configurationRelations(o, id)
|
|
283
|
+
}
|
|
284
|
+
if b.err != nil {
|
|
285
|
+
return cr.ResourceGraph{}, b.err
|
|
286
|
+
}
|
|
287
|
+
graph, err := b.bounded(rootID, root.Namespace)
|
|
288
|
+
if err != nil {
|
|
289
|
+
return cr.ResourceGraph{}, err
|
|
290
|
+
}
|
|
291
|
+
if b.partial {
|
|
292
|
+
graph.Warnings = append(graph.Warnings, "resource graph working set truncated at 10000 nodes or 10000 edges")
|
|
293
|
+
}
|
|
294
|
+
if rbac && root.Kind == "ServiceAccount" {
|
|
295
|
+
graph.Warnings = append(graph.Warnings, "RoleBinding source limited to the ServiceAccount namespace")
|
|
296
|
+
}
|
|
297
|
+
for _, key := range keys {
|
|
298
|
+
if err := ctx.Err(); err != nil {
|
|
299
|
+
return cr.ResourceGraph{}, err
|
|
300
|
+
}
|
|
301
|
+
o := b.objects[key]
|
|
302
|
+
if graphIsEvent(o) {
|
|
303
|
+
b.event(&graph, o)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
sort.SliceStable(graph.Evidence, func(i, j int) bool {
|
|
307
|
+
left, _ := time.Parse(time.RFC3339Nano, graph.Evidence[i].ObservedAt)
|
|
308
|
+
right, _ := time.Parse(time.RFC3339Nano, graph.Evidence[j].ObservedAt)
|
|
309
|
+
return left.After(right)
|
|
310
|
+
})
|
|
311
|
+
return graph, ctx.Err()
|
|
312
|
+
}
|
|
313
|
+
func (b *graphRules) objectKeys(root *unstructured.Unstructured) []string {
|
|
314
|
+
rootKey := graphIdentity(root.GetAPIVersion(), root.GetKind(), root.GetNamespace(), root.GetName())
|
|
315
|
+
keys := make([]string, 0, len(b.objects))
|
|
316
|
+
for key := range b.objects {
|
|
317
|
+
if key != rootKey {
|
|
318
|
+
keys = append(keys, key)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
sort.Strings(keys)
|
|
322
|
+
// Pod selector iteration is sorted separately from the root-first rule order.
|
|
323
|
+
for _, key := range keys {
|
|
324
|
+
o := b.objects[key]
|
|
325
|
+
if o.GetKind() == "Pod" && o.GetAPIVersion() == "v1" {
|
|
326
|
+
b.pods = append(b.pods, o)
|
|
327
|
+
}
|
|
328
|
+
if o.GetKind() == "ClusterRole" && o.GetAPIVersion() == graphRBACAPI && o.GetNamespace() == "" {
|
|
329
|
+
b.clusterRoles = append(b.clusterRoles, o)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if root.GetKind() == "Pod" && root.GetAPIVersion() == "v1" {
|
|
333
|
+
b.pods = append(b.pods, root)
|
|
334
|
+
}
|
|
335
|
+
if root.GetKind() == "ClusterRole" && root.GetAPIVersion() == graphRBACAPI && root.GetNamespace() == "" {
|
|
336
|
+
b.clusterRoles = append(b.clusterRoles, root)
|
|
337
|
+
}
|
|
338
|
+
return append([]string{rootKey}, keys...)
|
|
339
|
+
}
|
|
340
|
+
func (b *graphRules) priorityOwners(root *unstructured.Unstructured) {
|
|
341
|
+
queue := []*unstructured.Unstructured{root}
|
|
342
|
+
seen := map[string]bool{}
|
|
343
|
+
for len(queue) > 0 {
|
|
344
|
+
if !b.active() {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
current := queue[0]
|
|
348
|
+
queue = queue[1:]
|
|
349
|
+
id := graphNodeID(b.ref(current))
|
|
350
|
+
if seen[id] {
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
seen[id] = true
|
|
354
|
+
b.ownerRelations(current, b.node(b.ref(current), graphStatus(current)))
|
|
355
|
+
for _, owner := range current.GetOwnerReferences() {
|
|
356
|
+
if !b.active() {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
if actual := b.observedOwner(current.GetNamespace(), owner); actual != nil {
|
|
360
|
+
queue = append(queue, actual)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
func graphObjectIndex(ctx context.Context, namespace string, objects []*unstructured.Unstructured) (map[string]*unstructured.Unstructured, error) {
|
|
366
|
+
index := map[string]*unstructured.Unstructured{}
|
|
367
|
+
for _, o := range objects {
|
|
368
|
+
if err := ctx.Err(); err != nil {
|
|
369
|
+
return nil, err
|
|
370
|
+
}
|
|
371
|
+
if o == nil || o.GetName() == "" || o.GetKind() == "" || o.GetAPIVersion() == "" {
|
|
372
|
+
continue
|
|
373
|
+
}
|
|
374
|
+
if namespace != "" && o.GetNamespace() != "" && o.GetNamespace() != namespace {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
key := graphIdentity(o.GetAPIVersion(), o.GetKind(), o.GetNamespace(), o.GetName())
|
|
378
|
+
if _, exists := index[key]; !exists {
|
|
379
|
+
index[key] = o
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return index, nil
|
|
383
|
+
}
|
|
384
|
+
func (b *graphRules) rootObject(root cr.ResourceRef) (*unstructured.Unstructured, error) {
|
|
385
|
+
var result *unstructured.Unstructured
|
|
386
|
+
for _, o := range b.objects {
|
|
387
|
+
if err := b.ctx.Err(); err != nil {
|
|
388
|
+
return nil, err
|
|
389
|
+
}
|
|
390
|
+
if o.GetKind() != root.Kind || o.GetName() != root.Name || o.GetNamespace() != root.Namespace {
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
if root.APIVersion != "" && o.GetAPIVersion() != root.APIVersion {
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
if result != nil {
|
|
397
|
+
return nil, invalidGraphRoot("ambiguous root apiVersion")
|
|
398
|
+
}
|
|
399
|
+
result = o
|
|
400
|
+
}
|
|
401
|
+
if result == nil {
|
|
402
|
+
return nil, invalidGraphRoot("resource graph root not present")
|
|
403
|
+
}
|
|
404
|
+
if root.UID != "" && string(result.GetUID()) != root.UID {
|
|
405
|
+
return nil, invalidGraphRoot("resource graph root UID changed")
|
|
406
|
+
}
|
|
407
|
+
return result, nil
|
|
408
|
+
}
|
|
409
|
+
func (b *graphRules) ownerRelations(o *unstructured.Unstructured, id string) {
|
|
410
|
+
for _, owner := range o.GetOwnerReferences() {
|
|
411
|
+
if !b.active() {
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
relation := "owns"
|
|
415
|
+
if owner.Controller != nil && *owner.Controller {
|
|
416
|
+
relation = "controls"
|
|
417
|
+
}
|
|
418
|
+
b.edge(b.ownerTarget(o.GetNamespace(), owner), id, relation)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Kubernetes permits a cluster-scoped owner of a namespaced dependent. Only
|
|
423
|
+
// observed exact identities confirm this; another namespace is never searched.
|
|
424
|
+
func (b *graphRules) observedOwner(namespace string, owner metav1.OwnerReference) *unstructured.Unstructured {
|
|
425
|
+
if owner.UID == "" {
|
|
426
|
+
return nil
|
|
427
|
+
}
|
|
428
|
+
for _, scope := range []string{namespace, ""} {
|
|
429
|
+
candidate := b.objects[graphIdentity(owner.APIVersion, owner.Kind, scope, owner.Name)]
|
|
430
|
+
if candidate != nil && candidate.GetUID() == owner.UID {
|
|
431
|
+
return candidate
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return nil
|
|
435
|
+
}
|
|
436
|
+
func (b *graphRules) ownerTarget(namespace string, owner metav1.OwnerReference) string {
|
|
437
|
+
if actual := b.observedOwner(namespace, owner); actual != nil {
|
|
438
|
+
return b.node(b.ref(actual), graphStatus(actual))
|
|
439
|
+
}
|
|
440
|
+
ref := cr.ResourceRef{ClusterID: b.cluster, APIVersion: owner.APIVersion, Kind: owner.Kind, Namespace: namespace, Name: owner.Name, UID: string(owner.UID), ScopeMode: cr.ResourceScopeModeNamespace}
|
|
441
|
+
// The wire requires a scope enum. Use the dependent lookup context for this
|
|
442
|
+
// unresolved candidate; it does not confirm the target or its actual scope.
|
|
443
|
+
if namespace == "" {
|
|
444
|
+
ref.ScopeMode = cr.ResourceScopeModeCluster
|
|
445
|
+
}
|
|
446
|
+
return b.node(ref, "unresolved")
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// A custom object may reuse a built-in kind name; spec rules require exact GVK.
|
|
450
|
+
func graphBuiltinSpec(o *unstructured.Unstructured) bool {
|
|
451
|
+
return graphBuiltinAPIs[o.GetKind()] != "" && graphBuiltinAPIs[o.GetKind()] == o.GetAPIVersion()
|
|
452
|
+
}
|
|
453
|
+
func graphNestedMap(object map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
|
|
454
|
+
value, found, err := unstructured.NestedFieldNoCopy(object, fields...)
|
|
455
|
+
result, _ := value.(map[string]interface{})
|
|
456
|
+
return result, found, err
|
|
457
|
+
}
|
|
458
|
+
func graphNestedSlice(object map[string]interface{}, fields ...string) ([]interface{}, bool, error) {
|
|
459
|
+
value, found, err := unstructured.NestedFieldNoCopy(object, fields...)
|
|
460
|
+
result, _ := value.([]interface{})
|
|
461
|
+
return result, found, err
|
|
462
|
+
}
|
|
463
|
+
func graphMap(m map[string]interface{}, key string) map[string]interface{} {
|
|
464
|
+
value, _ := m[key].(map[string]interface{})
|
|
465
|
+
return value
|
|
466
|
+
}
|
|
467
|
+
func graphString(m map[string]interface{}, key string) string {
|
|
468
|
+
value, _ := m[key].(string)
|
|
469
|
+
return value
|
|
470
|
+
}
|
|
471
|
+
func graphSlice(m map[string]interface{}, key string) []interface{} {
|
|
472
|
+
value, _ := m[key].([]interface{})
|
|
473
|
+
return value
|
|
474
|
+
}
|
|
475
|
+
func graphPodSpec(o *unstructured.Unstructured) map[string]interface{} {
|
|
476
|
+
if !graphBuiltinSpec(o) {
|
|
477
|
+
return nil
|
|
478
|
+
}
|
|
479
|
+
path := []string{"spec", "template", "spec"}
|
|
480
|
+
switch o.GetKind() {
|
|
481
|
+
case "Pod":
|
|
482
|
+
path = []string{"spec"}
|
|
483
|
+
case "CronJob":
|
|
484
|
+
path = []string{"spec", "jobTemplate", "spec", "template", "spec"}
|
|
485
|
+
case "Deployment", "StatefulSet", "DaemonSet", "Job", "ReplicaSet", "ReplicationController":
|
|
486
|
+
default:
|
|
487
|
+
return nil
|
|
488
|
+
}
|
|
489
|
+
spec, _, _ := graphNestedMap(o.Object, path...)
|
|
490
|
+
return spec
|
|
491
|
+
}
|
|
492
|
+
func (b *graphRules) podReferences(o *unstructured.Unstructured, id string) {
|
|
493
|
+
spec := graphPodSpec(o)
|
|
494
|
+
if spec == nil {
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
ref := func(kind, name, relation string) {
|
|
498
|
+
b.edge(id, b.target("v1", kind, o.GetNamespace(), name, ""), relation)
|
|
499
|
+
}
|
|
500
|
+
for _, field := range []string{"containers", "initContainers", "ephemeralContainers"} {
|
|
501
|
+
for _, raw := range graphSlice(spec, field) {
|
|
502
|
+
if !b.active() {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
container, _ := raw.(map[string]interface{})
|
|
506
|
+
graphContainerRefs(container, ref, b.active)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for _, raw := range graphSlice(spec, "volumes") {
|
|
510
|
+
if !b.active() {
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
volume, _ := raw.(map[string]interface{})
|
|
514
|
+
graphVolumeRefs(volume, ref, b.active)
|
|
515
|
+
}
|
|
516
|
+
ref("ServiceAccount", graphString(spec, "serviceAccountName"), ResourceGraphRelationUsesServiceAccount)
|
|
517
|
+
for _, raw := range graphSlice(spec, "imagePullSecrets") {
|
|
518
|
+
if !b.active() {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
entry, _ := raw.(map[string]interface{})
|
|
522
|
+
ref("Secret", graphString(entry, "name"), "uses-secret")
|
|
523
|
+
}
|
|
524
|
+
if node := graphString(spec, "nodeName"); node != "" {
|
|
525
|
+
b.edge(id, b.target("v1", "Node", "", node, ""), "scheduled-on")
|
|
526
|
+
}
|
|
527
|
+
b.edge(id, b.target("scheduling.k8s.io/v1", "PriorityClass", "", graphString(spec, "priorityClassName"), ""), ResourceGraphRelationUsesPriorityClass)
|
|
528
|
+
b.edge(id, b.target("node.k8s.io/v1", "RuntimeClass", "", graphString(spec, "runtimeClassName"), ""), ResourceGraphRelationUsesRuntimeClass)
|
|
529
|
+
}
|
|
530
|
+
func graphContainerRefs(container map[string]interface{}, ref func(string, string, string), active func() bool) {
|
|
531
|
+
for _, raw := range graphSlice(container, "env") {
|
|
532
|
+
if !active() {
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
entry, _ := raw.(map[string]interface{})
|
|
536
|
+
value := graphMap(entry, "valueFrom")
|
|
537
|
+
ref("ConfigMap", graphString(graphMap(value, "configMapKeyRef"), "name"), "uses-config")
|
|
538
|
+
ref("Secret", graphString(graphMap(value, "secretKeyRef"), "name"), "uses-secret")
|
|
539
|
+
}
|
|
540
|
+
for _, raw := range graphSlice(container, "envFrom") {
|
|
541
|
+
if !active() {
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
entry, _ := raw.(map[string]interface{})
|
|
545
|
+
ref("ConfigMap", graphString(graphMap(entry, "configMapRef"), "name"), "uses-config")
|
|
546
|
+
ref("Secret", graphString(graphMap(entry, "secretRef"), "name"), "uses-secret")
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
func graphVolumeRefs(volume map[string]interface{}, ref func(string, string, string), active func() bool) {
|
|
550
|
+
ref("ConfigMap", graphString(graphMap(volume, "configMap"), "name"), "uses-config")
|
|
551
|
+
ref("Secret", graphString(graphMap(volume, "secret"), "secretName"), "uses-secret")
|
|
552
|
+
ref("Secret", graphString(graphMap(graphMap(volume, "csi"), "nodePublishSecretRef"), "name"), "uses-secret")
|
|
553
|
+
ref("PersistentVolumeClaim", graphVolumeClaimName(volume), ResourceGraphRelationMounts)
|
|
554
|
+
for _, raw := range graphSlice(graphMap(volume, "projected"), "sources") {
|
|
555
|
+
if !active() {
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
source, _ := raw.(map[string]interface{})
|
|
559
|
+
ref("ConfigMap", graphString(graphMap(source, "configMap"), "name"), "uses-config")
|
|
560
|
+
ref("Secret", graphString(graphMap(source, "secret"), "name"), "uses-secret")
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
func (b *graphRules) selectorRelations(o *unstructured.Unstructured, id string) {
|
|
565
|
+
if !graphBuiltinSpec(o) {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
switch o.GetKind() {
|
|
569
|
+
case "Service", "PodDisruptionBudget":
|
|
570
|
+
b.podSelectorRelations(o, id)
|
|
571
|
+
case "HorizontalPodAutoscaler":
|
|
572
|
+
target, _, _ := graphNestedMap(o.Object, "spec", "scaleTargetRef")
|
|
573
|
+
if graphString(target, "apiVersion") != "" && graphString(target, "kind") != "" {
|
|
574
|
+
b.edge(id, b.target(graphString(target, "apiVersion"), graphString(target, "kind"), o.GetNamespace(), graphString(target, "name"), ""), "scales")
|
|
575
|
+
}
|
|
576
|
+
case "Ingress":
|
|
577
|
+
b.ingressRelations(o, id)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
func graphPodSelector(o *unstructured.Unstructured) labels.Selector {
|
|
581
|
+
selector, found, _ := graphNestedMap(o.Object, "spec", "selector")
|
|
582
|
+
if !found || selector == nil {
|
|
583
|
+
return nil
|
|
584
|
+
}
|
|
585
|
+
if o.GetKind() == "Service" {
|
|
586
|
+
return servicePodSelector(selector)
|
|
587
|
+
}
|
|
588
|
+
var parsed metav1.LabelSelector
|
|
589
|
+
if kruntime.DefaultUnstructuredConverter.FromUnstructured(selector, &parsed) != nil {
|
|
590
|
+
return nil
|
|
591
|
+
}
|
|
592
|
+
query, err := metav1.LabelSelectorAsSelector(&parsed)
|
|
593
|
+
if err != nil {
|
|
594
|
+
return nil
|
|
595
|
+
}
|
|
596
|
+
return query
|
|
597
|
+
}
|
|
598
|
+
func (b *graphRules) podSelectorRelations(o *unstructured.Unstructured, id string) {
|
|
599
|
+
query := graphPodSelector(o)
|
|
600
|
+
if query == nil {
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
relation := "selects"
|
|
604
|
+
if o.GetKind() == "PodDisruptionBudget" {
|
|
605
|
+
relation = "protects"
|
|
606
|
+
}
|
|
607
|
+
for _, pod := range b.pods {
|
|
608
|
+
if !b.active() {
|
|
609
|
+
return
|
|
610
|
+
}
|
|
611
|
+
if pod.GetAPIVersion() == "v1" && pod.GetKind() == "Pod" && pod.GetNamespace() == o.GetNamespace() && query.Matches(labels.Set(pod.GetLabels())) {
|
|
612
|
+
b.edge(id, b.node(b.ref(pod), graphStatus(pod)), relation)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
func (b *graphRules) ingressRelations(o *unstructured.Unstructured, id string) {
|
|
617
|
+
forEachIngressBackendDeclaration(o.Object, b.active, func(ref ingressBackendDeclaration) {
|
|
618
|
+
b.edge(id, b.target("v1", "Service", o.GetNamespace(), ref.serviceName, ""), "routes-to")
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Traverse each direction independently. In particular, arriving at a shared
|
|
623
|
+
// config/Secret never reverses direction and discovers unrelated consumers.
|
|
624
|
+
type graphTraversalState struct {
|
|
625
|
+
id string
|
|
626
|
+
depth, direction int
|
|
627
|
+
}
|
|
628
|
+
type graphTraversal struct {
|
|
629
|
+
selected, chosen map[string]bool
|
|
630
|
+
depthTruncated, limitTruncated bool
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
func graphNext(current graphTraversalState, edge cr.ResourceGraphEdge) (graphTraversalState, bool) {
|
|
634
|
+
next := graphTraversalState{depth: current.depth + 1, direction: current.direction}
|
|
635
|
+
if current.direction == 1 && edge.SourceID == current.id {
|
|
636
|
+
next.id = edge.TargetID
|
|
637
|
+
}
|
|
638
|
+
if current.direction == -1 && edge.TargetID == current.id {
|
|
639
|
+
next.id = edge.SourceID
|
|
640
|
+
}
|
|
641
|
+
supporting := edge.Relation == "selects" || edge.Relation == "protects" || edge.Relation == "scales"
|
|
642
|
+
if current.direction == 1 && edge.TargetID == current.id && supporting {
|
|
643
|
+
next.id = edge.SourceID
|
|
644
|
+
next.direction = -1
|
|
645
|
+
}
|
|
646
|
+
return next, next.id != ""
|
|
647
|
+
}
|
|
648
|
+
func (t *graphTraversal) include(edge cr.ResourceGraphEdge, next graphTraversalState) bool {
|
|
649
|
+
if (!t.selected[next.id] && len(t.selected) >= ResourceGraphNodeLimit) || (!t.chosen[edge.ID] && len(t.chosen) >= ResourceGraphEdgeLimit) {
|
|
650
|
+
t.limitTruncated = true
|
|
651
|
+
return false
|
|
652
|
+
}
|
|
653
|
+
t.selected[next.id] = true
|
|
654
|
+
t.chosen[edge.ID] = true
|
|
655
|
+
return true
|
|
656
|
+
}
|
|
657
|
+
func graphAdjacency(ctx context.Context, edges []cr.ResourceGraphEdge) (map[string][]cr.ResourceGraphEdge, map[string][]cr.ResourceGraphEdge, error) {
|
|
658
|
+
forward := map[string][]cr.ResourceGraphEdge{}
|
|
659
|
+
reverse := map[string][]cr.ResourceGraphEdge{}
|
|
660
|
+
for _, edge := range edges {
|
|
661
|
+
if err := ctx.Err(); err != nil {
|
|
662
|
+
return nil, nil, err
|
|
663
|
+
}
|
|
664
|
+
forward[edge.SourceID] = append(forward[edge.SourceID], edge)
|
|
665
|
+
reverse[edge.TargetID] = append(reverse[edge.TargetID], edge)
|
|
666
|
+
if edge.Relation == "selects" || edge.Relation == "protects" || edge.Relation == "scales" {
|
|
667
|
+
forward[edge.TargetID] = append(forward[edge.TargetID], edge)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return forward, reverse, nil
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Reserve the root's owner chain before selector fanout consumes output slots.
|
|
674
|
+
func graphSeedOwners(ctx context.Context, rootID string, reverse map[string][]cr.ResourceGraphEdge, result *graphTraversal) error {
|
|
675
|
+
queue := []graphTraversalState{{id: rootID, direction: -1}}
|
|
676
|
+
seen := map[string]bool{}
|
|
677
|
+
for len(queue) > 0 {
|
|
678
|
+
if err := ctx.Err(); err != nil {
|
|
679
|
+
return err
|
|
680
|
+
}
|
|
681
|
+
current := queue[0]
|
|
682
|
+
queue = queue[1:]
|
|
683
|
+
if seen[current.id] || current.depth >= ResourceGraphDepth {
|
|
684
|
+
continue
|
|
685
|
+
}
|
|
686
|
+
seen[current.id] = true
|
|
687
|
+
for _, edge := range reverse[current.id] {
|
|
688
|
+
if err := ctx.Err(); err != nil {
|
|
689
|
+
return err
|
|
690
|
+
}
|
|
691
|
+
if edge.Relation != "owns" && edge.Relation != "controls" {
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
next, ok := graphNext(current, edge)
|
|
695
|
+
if ok && result.include(edge, next) {
|
|
696
|
+
queue = append(queue, next)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return nil
|
|
701
|
+
}
|
|
702
|
+
func graphTraverse(ctx context.Context, rootID string, edges []cr.ResourceGraphEdge) (*graphTraversal, error) {
|
|
703
|
+
forward, reverse, err := graphAdjacency(ctx, edges)
|
|
704
|
+
if err != nil {
|
|
705
|
+
return nil, err
|
|
706
|
+
}
|
|
707
|
+
result := &graphTraversal{selected: map[string]bool{rootID: true}, chosen: map[string]bool{}}
|
|
708
|
+
if err := graphSeedOwners(ctx, rootID, reverse, result); err != nil {
|
|
709
|
+
return nil, err
|
|
710
|
+
}
|
|
711
|
+
queue := []graphTraversalState{{id: rootID, direction: 1}, {id: rootID, direction: -1}}
|
|
712
|
+
seen := map[graphTraversalState]bool{}
|
|
713
|
+
omitted := map[string]bool{}
|
|
714
|
+
for len(queue) > 0 {
|
|
715
|
+
if err := ctx.Err(); err != nil {
|
|
716
|
+
return nil, err
|
|
717
|
+
}
|
|
718
|
+
current := queue[0]
|
|
719
|
+
queue = queue[1:]
|
|
720
|
+
if seen[current] {
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
seen[current] = true
|
|
724
|
+
adjacent := forward[current.id]
|
|
725
|
+
if current.direction == -1 {
|
|
726
|
+
adjacent = reverse[current.id]
|
|
727
|
+
}
|
|
728
|
+
for _, edge := range adjacent {
|
|
729
|
+
if err := ctx.Err(); err != nil {
|
|
730
|
+
return nil, err
|
|
731
|
+
}
|
|
732
|
+
next, ok := graphNext(current, edge)
|
|
733
|
+
if !ok {
|
|
734
|
+
continue
|
|
735
|
+
}
|
|
736
|
+
if current.depth >= ResourceGraphDepth {
|
|
737
|
+
omitted[edge.ID] = true
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
740
|
+
if result.include(edge, next) {
|
|
741
|
+
queue = append(queue, next)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
for id := range omitted {
|
|
746
|
+
if !result.chosen[id] {
|
|
747
|
+
result.depthTruncated = true
|
|
748
|
+
break
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return result, nil
|
|
752
|
+
}
|
|
753
|
+
func (b *graphRules) bounded(rootID, namespace string) (cr.ResourceGraph, error) {
|
|
754
|
+
graph := cr.ResourceGraph{ClusterID: b.cluster, Namespace: namespace, GeneratedAt: time.Now().UTC().Format(time.RFC3339Nano), RootID: rootID, Nodes: []cr.ResourceGraphNode{}, Edges: []cr.ResourceGraphEdge{}, Evidence: []cr.ResourceEvidence{}, Warnings: []string{}}
|
|
755
|
+
edges := make([]cr.ResourceGraphEdge, 0, len(b.edges))
|
|
756
|
+
for _, edge := range b.edges {
|
|
757
|
+
edges = append(edges, edge)
|
|
758
|
+
}
|
|
759
|
+
sort.Slice(edges, func(i, j int) bool { return edges[i].ID < edges[j].ID })
|
|
760
|
+
selected, err := graphTraverse(b.ctx, rootID, edges)
|
|
761
|
+
if err != nil {
|
|
762
|
+
return cr.ResourceGraph{}, err
|
|
763
|
+
}
|
|
764
|
+
for id := range selected.selected {
|
|
765
|
+
graph.Nodes = append(graph.Nodes, b.nodes[id])
|
|
766
|
+
}
|
|
767
|
+
for _, edge := range edges {
|
|
768
|
+
if selected.chosen[edge.ID] {
|
|
769
|
+
graph.Edges = append(graph.Edges, edge)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
sort.Slice(graph.Nodes, func(i, j int) bool { return graph.Nodes[i].ID < graph.Nodes[j].ID })
|
|
773
|
+
if selected.limitTruncated {
|
|
774
|
+
graph.Warnings = append(graph.Warnings, "resource graph truncated at 500 nodes or 1000 edges")
|
|
775
|
+
}
|
|
776
|
+
if selected.depthTruncated {
|
|
777
|
+
graph.Warnings = append(graph.Warnings, "resource graph truncated by depth at 3 hops")
|
|
778
|
+
}
|
|
779
|
+
return graph, b.ctx.Err()
|
|
780
|
+
}
|
|
781
|
+
func (b *graphRules) event(graph *cr.ResourceGraph, o *unstructured.Unstructured) {
|
|
782
|
+
involved, _, _ := graphNestedMap(o.Object, "involvedObject")
|
|
783
|
+
str := func(key string) string { s, _ := involved[key].(string); return s }
|
|
784
|
+
var id string
|
|
785
|
+
for _, node := range graph.Nodes {
|
|
786
|
+
if err := b.ctx.Err(); err != nil {
|
|
787
|
+
b.err = err
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
r := node.Resource
|
|
791
|
+
if r.Kind == str("kind") && r.Name == str("name") && r.Namespace == str("namespace") && (str("apiVersion") == "" || r.APIVersion == str("apiVersion")) && (str("uid") == "" || r.UID == str("uid")) {
|
|
792
|
+
id = node.ID
|
|
793
|
+
break
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if id == "" {
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
reason, _, _ := unstructured.NestedString(o.Object, "reason")
|
|
800
|
+
message, _, _ := unstructured.NestedString(o.Object, "message")
|
|
801
|
+
typ, _, _ := unstructured.NestedString(o.Object, "type")
|
|
802
|
+
severity := "info"
|
|
803
|
+
if typ == "Warning" {
|
|
804
|
+
severity = "warning"
|
|
805
|
+
}
|
|
806
|
+
observed := graphEventObservedAt(o)
|
|
807
|
+
if observed.IsZero() {
|
|
808
|
+
return
|
|
809
|
+
}
|
|
810
|
+
graph.Evidence = append(graph.Evidence, cr.ResourceEvidence{ID: "event/" + o.GetNamespace() + "/" + o.GetName(), Type: "kubernetes-event", Severity: severity, Summary: strings.TrimSpace(reason + " " + message), ObservedAt: observed.UTC().Format(time.RFC3339Nano), ResourceID: id, SourceRef: "Event/" + o.GetNamespace() + "/" + o.GetName()})
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
func graphEventObservedAt(o *unstructured.Unstructured) time.Time {
|
|
814
|
+
for _, field := range []string{"lastTimestamp", "eventTime"} {
|
|
815
|
+
value, _, _ := unstructured.NestedString(o.Object, field)
|
|
816
|
+
if observed, err := time.Parse(time.RFC3339Nano, value); err == nil && !observed.IsZero() {
|
|
817
|
+
return observed
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return o.GetCreationTimestamp().Time
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
func validateNetworkGraphRootScope(root cr.ResourceRef, definition cr.NetworkTopologySourceDefinition) error {
|
|
824
|
+
if root.ScopeMode != "" && root.ScopeMode != topologyScope(definition.Namespaced) {
|
|
825
|
+
return invalidGraphRoot("network resource graph root scope mismatch")
|
|
826
|
+
}
|
|
827
|
+
if definition.Namespaced && root.Namespace == "" {
|
|
828
|
+
return invalidGraphRoot("namespaced resource graph root requires namespace")
|
|
829
|
+
}
|
|
830
|
+
if !definition.Namespaced && root.Namespace != "" {
|
|
831
|
+
return invalidGraphRoot("cluster resource graph root cannot have namespace")
|
|
832
|
+
}
|
|
833
|
+
return nil
|
|
834
|
+
}
|