@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,397 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"reflect"
|
|
8
|
+
"slices"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
|
|
12
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
13
|
+
corev1 "k8s.io/api/core/v1"
|
|
14
|
+
storagev1 "k8s.io/api/storage/v1"
|
|
15
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
16
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
17
|
+
kruntime "k8s.io/apimachinery/pkg/runtime"
|
|
18
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
19
|
+
"k8s.io/client-go/kubernetes/fake"
|
|
20
|
+
ktesting "k8s.io/client-go/testing"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
func storageGraphFixture() (*corev1.Pod, *corev1.PersistentVolumeClaim, *corev1.PersistentVolume, *storagev1.StorageClass) {
|
|
24
|
+
className := "fast"
|
|
25
|
+
claim := &corev1.PersistentVolumeClaim{
|
|
26
|
+
ObjectMeta: metav1.ObjectMeta{Name: "data", Namespace: "team", UID: "claim-uid"},
|
|
27
|
+
Spec: corev1.PersistentVolumeClaimSpec{VolumeName: "disk", StorageClassName: &className},
|
|
28
|
+
Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimBound},
|
|
29
|
+
}
|
|
30
|
+
volume := &corev1.PersistentVolume{
|
|
31
|
+
ObjectMeta: metav1.ObjectMeta{Name: "disk", UID: "volume-uid"},
|
|
32
|
+
Spec: corev1.PersistentVolumeSpec{
|
|
33
|
+
StorageClassName: className,
|
|
34
|
+
ClaimRef: &corev1.ObjectReference{APIVersion: "v1", Kind: "PersistentVolumeClaim", Namespace: "team", Name: "data", UID: claim.UID},
|
|
35
|
+
},
|
|
36
|
+
Status: corev1.PersistentVolumeStatus{Phase: corev1.VolumeBound},
|
|
37
|
+
}
|
|
38
|
+
pod := &corev1.Pod{
|
|
39
|
+
ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "team", UID: "pod-uid"},
|
|
40
|
+
Spec: corev1.PodSpec{Volumes: []corev1.Volume{
|
|
41
|
+
{Name: "first", VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: claim.Name}}},
|
|
42
|
+
{Name: "duplicate", VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: claim.Name, ReadOnly: true}}},
|
|
43
|
+
}},
|
|
44
|
+
Status: corev1.PodStatus{Phase: corev1.PodPending},
|
|
45
|
+
}
|
|
46
|
+
class := &storagev1.StorageClass{ObjectMeta: metav1.ObjectMeta{Name: className, UID: "class-uid"}}
|
|
47
|
+
return pod, claim, volume, class
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func collectStorageGraph(t *testing.T, client *fake.Clientset, namespace, kind, name string) cr.ResourceGraph {
|
|
51
|
+
t.Helper()
|
|
52
|
+
graph, err := BuildKubernetesResourceGraph(context.Background(), client, cr.ResourceRef{ClusterID: "cluster", Namespace: namespace, Kind: kind, Name: name})
|
|
53
|
+
if err != nil {
|
|
54
|
+
t.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
return graph
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func assertStorageGraphActions(t *testing.T, client *fake.Clientset, want []string) {
|
|
60
|
+
t.Helper()
|
|
61
|
+
got := []string{}
|
|
62
|
+
for _, action := range client.Actions() {
|
|
63
|
+
record := action.GetVerb() + " " + action.GetResource().Resource + "/" + action.GetNamespace()
|
|
64
|
+
switch action.GetVerb() {
|
|
65
|
+
case "get":
|
|
66
|
+
get, ok := action.(ktesting.GetAction)
|
|
67
|
+
if !ok {
|
|
68
|
+
t.Fatalf("get action has no resource name: %+v", action)
|
|
69
|
+
}
|
|
70
|
+
record += "/" + get.GetName()
|
|
71
|
+
case "list":
|
|
72
|
+
list, ok := action.(interface{ GetListOptions() metav1.ListOptions })
|
|
73
|
+
if !ok {
|
|
74
|
+
t.Fatalf("list action has no list options: %+v", action)
|
|
75
|
+
}
|
|
76
|
+
options := list.GetListOptions()
|
|
77
|
+
if options.Limit != ResourceGraphSourceLimit || options.Continue != "" {
|
|
78
|
+
t.Fatalf("unbounded source options for %s: %+v", record, options)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
got = append(got, record)
|
|
82
|
+
}
|
|
83
|
+
if !slices.Equal(got, want) {
|
|
84
|
+
t.Fatalf("actions = %v, want %v", got, want)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func storageGraphNodes(graph cr.ResourceGraph) map[string]cr.ResourceGraphNode {
|
|
89
|
+
nodes := map[string]cr.ResourceGraphNode{}
|
|
90
|
+
for _, node := range graph.Nodes {
|
|
91
|
+
nodes[node.ID] = node
|
|
92
|
+
}
|
|
93
|
+
return nodes
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func TestResourceGraphStorageCollectorRootsAndScopes(t *testing.T) {
|
|
97
|
+
for _, tc := range []struct {
|
|
98
|
+
kind, name, namespace string
|
|
99
|
+
calls []string
|
|
100
|
+
}{
|
|
101
|
+
{"persistentvolumeclaim", "data", "team", []string{"get persistentvolumeclaims/team/data", "get persistentvolumes//disk", "get storageclasses//fast", "list pods/team"}},
|
|
102
|
+
{"PersistentVolume", "disk", "", []string{"get persistentvolumes//disk", "get persistentvolumeclaims/team/data", "get storageclasses//fast", "list pods/team"}},
|
|
103
|
+
{"storageclass", "fast", "", []string{"get storageclasses//fast", "list persistentvolumes/", "list persistentvolumeclaims/", "list pods/"}},
|
|
104
|
+
} {
|
|
105
|
+
t.Run(tc.kind, func(t *testing.T) {
|
|
106
|
+
pod, claim, volume, class := storageGraphFixture()
|
|
107
|
+
pod.Spec.NodeName = "unrelated-node"
|
|
108
|
+
pod.OwnerReferences = []metav1.OwnerReference{{APIVersion: "custom.example/v1", Kind: "UnrelatedOwner", Name: "unrelated-owner", UID: "owner-uid"}}
|
|
109
|
+
claim.OwnerReferences = pod.OwnerReferences
|
|
110
|
+
foreignPod := pod.DeepCopy()
|
|
111
|
+
foreignPod.Name, foreignPod.Namespace, foreignPod.UID = "foreign-pod", "other", "foreign-pod-uid"
|
|
112
|
+
foreignClaim := claim.DeepCopy()
|
|
113
|
+
foreignClaim.Namespace, foreignClaim.UID, foreignClaim.Spec.StorageClassName = "other", "foreign-claim-uid", nil
|
|
114
|
+
client := fake.NewClientset(pod, claim, volume, class, foreignPod, foreignClaim)
|
|
115
|
+
graph := collectStorageGraph(t, client, tc.namespace, tc.kind, tc.name)
|
|
116
|
+
assertStorageGraphActions(t, client, tc.calls)
|
|
117
|
+
if len(graph.Nodes) != 4 || len(graph.Warnings) != 0 || graphTestHas(graph, "foreign-pod") {
|
|
118
|
+
t.Fatalf("unexpected graph: %+v", graph)
|
|
119
|
+
}
|
|
120
|
+
nodes := storageGraphNodes(graph)
|
|
121
|
+
if nodes[graph.RootID].Resource.Name != tc.name || nodes[graph.RootID].Resource.Namespace != tc.namespace {
|
|
122
|
+
t.Fatalf("wrong root: %+v", nodes[graph.RootID])
|
|
123
|
+
}
|
|
124
|
+
matches := map[string]int{}
|
|
125
|
+
for _, edge := range graph.Edges {
|
|
126
|
+
if edge.ResolutionState != "resolved" {
|
|
127
|
+
t.Fatalf("unexpected resolution: %+v", edge)
|
|
128
|
+
}
|
|
129
|
+
matches[edge.Relation+"/"+nodes[edge.SourceID].Resource.Name+"/"+nodes[edge.TargetID].Resource.Name]++
|
|
130
|
+
if edge.Relation == ResourceGraphRelationMounts && (edge.Note != "PVC reference declared in Pod spec" || nodes[edge.SourceID].Status != "Pending") {
|
|
131
|
+
t.Fatalf("spec reference became mount success: %+v", edge)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for _, expected := range []string{"mounts/app/data", "binds-to/data/disk", "uses-storage-class/disk/fast"} {
|
|
135
|
+
if matches[expected] != 1 {
|
|
136
|
+
t.Errorf("%s count = %d, want 1: %v", expected, matches[expected], matches)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if !strings.EqualFold(tc.kind, "PersistentVolume") && matches["uses-storage-class/data/fast"] != 1 {
|
|
140
|
+
t.Errorf("missing declared claim class: %v", matches)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func TestResourceGraphStorageClassFollowsConfirmedVolumeToClaimWithoutClass(t *testing.T) {
|
|
147
|
+
pod, claim, volume, class := storageGraphFixture()
|
|
148
|
+
claim.Spec.StorageClassName = nil
|
|
149
|
+
client := fake.NewClientset(pod, claim, volume, class)
|
|
150
|
+
graph := collectStorageGraph(t, client, "", "StorageClass", "fast")
|
|
151
|
+
assertStorageGraphActions(t, client, []string{"get storageclasses//fast", "list persistentvolumes/", "list persistentvolumeclaims/", "list pods/"})
|
|
152
|
+
if !graphTestEdge(graph, "mounts", "app", "data") || !graphTestEdge(graph, "binds-to", "data", "disk") || graphTestEdge(graph, "uses-storage-class", "data", "fast") {
|
|
153
|
+
t.Fatalf("class root lost the binding chain or invented a claim class: %+v", graph)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func TestResourceGraphStorageCollectorStaleAndOneSidedBindings(t *testing.T) {
|
|
158
|
+
for _, tc := range []struct {
|
|
159
|
+
name string
|
|
160
|
+
edit func(*corev1.PersistentVolumeClaim, *corev1.PersistentVolume)
|
|
161
|
+
}{
|
|
162
|
+
{"recreated claim", func(claim *corev1.PersistentVolumeClaim, _ *corev1.PersistentVolume) { claim.UID = "new-uid" }},
|
|
163
|
+
{"another namespace", func(_ *corev1.PersistentVolumeClaim, volume *corev1.PersistentVolume) {
|
|
164
|
+
volume.Spec.ClaimRef.Namespace = "other"
|
|
165
|
+
}},
|
|
166
|
+
{"another name", func(_ *corev1.PersistentVolumeClaim, volume *corev1.PersistentVolume) {
|
|
167
|
+
volume.Spec.ClaimRef.Name = "other"
|
|
168
|
+
}},
|
|
169
|
+
{"no UID", func(_ *corev1.PersistentVolumeClaim, volume *corev1.PersistentVolume) { volume.Spec.ClaimRef.UID = "" }},
|
|
170
|
+
{"no reciprocal volume", func(claim *corev1.PersistentVolumeClaim, _ *corev1.PersistentVolume) { claim.Spec.VolumeName = "" }},
|
|
171
|
+
{"another reciprocal volume", func(claim *corev1.PersistentVolumeClaim, _ *corev1.PersistentVolume) { claim.Spec.VolumeName = "other" }},
|
|
172
|
+
{"terminating claim", func(claim *corev1.PersistentVolumeClaim, _ *corev1.PersistentVolume) {
|
|
173
|
+
now := metav1.Now()
|
|
174
|
+
claim.DeletionTimestamp = &now
|
|
175
|
+
}},
|
|
176
|
+
} {
|
|
177
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
178
|
+
pod, claim, volume, class := storageGraphFixture()
|
|
179
|
+
tc.edit(claim, volume)
|
|
180
|
+
client := fake.NewClientset(pod, claim, volume, class)
|
|
181
|
+
graph := collectStorageGraph(t, client, "", "PersistentVolume", "disk")
|
|
182
|
+
for _, action := range client.Actions() {
|
|
183
|
+
if action.GetResource().Resource == "pods" {
|
|
184
|
+
t.Fatalf("unconfirmed claim triggered a Pod read: %+v", action)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
nodes := storageGraphNodes(graph)
|
|
188
|
+
count := 0
|
|
189
|
+
for _, edge := range graph.Edges {
|
|
190
|
+
if edge.Relation != ResourceGraphRelationBindsTo {
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
count++
|
|
194
|
+
source := nodes[edge.SourceID]
|
|
195
|
+
ref := volume.Spec.ClaimRef
|
|
196
|
+
if edge.TargetID != graph.RootID || edge.ResolutionState != "unresolved" || source.Status != "unresolved" || source.Resource.UID != string(ref.UID) || source.Resource.Namespace != ref.Namespace || source.Resource.Name != ref.Name {
|
|
197
|
+
t.Fatalf("binding attached an observed claim or wrong declaration: %+v, %+v", edge, source)
|
|
198
|
+
}
|
|
199
|
+
if edge.FieldPath != "spec.claimRef" || edge.SourceRef == nil || *edge.SourceRef != nodes[graph.RootID].Resource {
|
|
200
|
+
t.Fatalf("reversed edge does not identify the PV declaration: %+v", edge)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if count != 1 || graphTestHas(graph, pod.Name) {
|
|
204
|
+
t.Fatalf("unexpected graph: %+v", graph)
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
t.Run("claim side excludes an observed mismatched volume", func(t *testing.T) {
|
|
209
|
+
pod, claim, volume, class := storageGraphFixture()
|
|
210
|
+
volume.Spec.ClaimRef.UID = "old-claim-uid"
|
|
211
|
+
client := fake.NewClientset(pod, claim, volume, class)
|
|
212
|
+
graph := collectStorageGraph(t, client, "team", "PersistentVolumeClaim", "data")
|
|
213
|
+
nodes := storageGraphNodes(graph)
|
|
214
|
+
for _, edge := range graph.Edges {
|
|
215
|
+
if edge.Relation != ResourceGraphRelationBindsTo {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
target := nodes[edge.TargetID]
|
|
219
|
+
if edge.SourceID != graph.RootID || edge.ResolutionState != "unresolved" || target.Status != "unresolved" || target.Resource.UID != "" || target.Resource.Name != volume.Name {
|
|
220
|
+
t.Fatalf("binding attached an unrelated observed volume: %+v, %+v", edge, target)
|
|
221
|
+
}
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
t.Fatal("PVC volume declaration missing")
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func TestResourceGraphStorageBindingRequiresBothBoundPhases(t *testing.T) {
|
|
229
|
+
for _, tc := range []struct {
|
|
230
|
+
name string
|
|
231
|
+
claimPhase corev1.PersistentVolumeClaimPhase
|
|
232
|
+
volumePhase corev1.PersistentVolumePhase
|
|
233
|
+
}{
|
|
234
|
+
{"pending claim", corev1.ClaimPending, corev1.VolumeBound},
|
|
235
|
+
{"lost claim", corev1.ClaimLost, corev1.VolumeBound},
|
|
236
|
+
{"missing claim phase", "", corev1.VolumeBound},
|
|
237
|
+
{"released volume", corev1.ClaimBound, corev1.VolumeReleased},
|
|
238
|
+
{"failed volume", corev1.ClaimBound, corev1.VolumeFailed},
|
|
239
|
+
{"missing volume phase", corev1.ClaimBound, ""},
|
|
240
|
+
} {
|
|
241
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
242
|
+
pod, claim, volume, class := storageGraphFixture()
|
|
243
|
+
claim.Status.Phase = tc.claimPhase
|
|
244
|
+
volume.Status.Phase = tc.volumePhase
|
|
245
|
+
for _, root := range []struct {
|
|
246
|
+
namespace, kind, name, phase string
|
|
247
|
+
}{
|
|
248
|
+
{claim.Namespace, "PersistentVolumeClaim", claim.Name, string(tc.claimPhase)},
|
|
249
|
+
{"", "PersistentVolume", volume.Name, string(tc.volumePhase)},
|
|
250
|
+
} {
|
|
251
|
+
graph := collectStorageGraph(t, fake.NewClientset(pod, claim, volume, class), root.namespace, root.kind, root.name)
|
|
252
|
+
if node := storageGraphNodes(graph)[graph.RootID]; node.Status != root.phase {
|
|
253
|
+
t.Errorf("%s phase = %q, want observed phase %q", root.kind, node.Status, root.phase)
|
|
254
|
+
}
|
|
255
|
+
if edges := networkEdges(graph, ResourceGraphRelationBindsTo); len(edges) != 1 || edges[0].ResolutionState != "unresolved" {
|
|
256
|
+
t.Errorf("%s needs one unresolved declaration, got %+v", root.kind, edges)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func TestResourceGraphStorageCollectorUnboundAndUnavailableSources(t *testing.T) {
|
|
264
|
+
t.Run("explicit empty or nil class stays absent", func(t *testing.T) {
|
|
265
|
+
empty := ""
|
|
266
|
+
for _, className := range []*string{nil, &empty} {
|
|
267
|
+
pod, claim, _, class := storageGraphFixture()
|
|
268
|
+
claim.Spec.VolumeName, claim.Spec.StorageClassName, claim.Status.Phase = "", className, corev1.ClaimPending
|
|
269
|
+
class.Annotations = map[string]string{"storageclass.kubernetes.io/is-default-class": "true"}
|
|
270
|
+
client := fake.NewClientset(pod, claim, class)
|
|
271
|
+
graph := collectStorageGraph(t, client, "team", "PersistentVolumeClaim", "data")
|
|
272
|
+
assertStorageGraphActions(t, client, []string{"get persistentvolumeclaims/team/data", "list pods/team"})
|
|
273
|
+
if storageGraphNodes(graph)[graph.RootID].Status != "Pending" || len(graph.Warnings) != 0 || len(graph.Edges) != 1 || graph.Edges[0].Relation != ResourceGraphRelationMounts {
|
|
274
|
+
t.Fatalf("unbound claim state lost: %+v", graph)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
t.Run("missing volume and forbidden class", func(t *testing.T) {
|
|
279
|
+
pod, claim, _, _ := storageGraphFixture()
|
|
280
|
+
client := fake.NewClientset(pod, claim)
|
|
281
|
+
client.PrependReactor("get", "storageclasses", func(ktesting.Action) (bool, kruntime.Object, error) {
|
|
282
|
+
return true, nil, apierrors.NewForbidden(schema.GroupResource{Group: "storage.k8s.io", Resource: "storageclasses"}, "private-name", errors.New("private error"))
|
|
283
|
+
})
|
|
284
|
+
graph := collectStorageGraph(t, client, "team", "PersistentVolumeClaim", "data")
|
|
285
|
+
want := []string{"PersistentVolume reference source NotFound", "StorageClass reference source Forbidden"}
|
|
286
|
+
if !slices.Equal(graph.Warnings, want) {
|
|
287
|
+
t.Fatalf("warnings = %v, want %v", graph.Warnings, want)
|
|
288
|
+
}
|
|
289
|
+
nodes := storageGraphNodes(graph)
|
|
290
|
+
for _, edge := range graph.Edges {
|
|
291
|
+
if edge.Relation != ResourceGraphRelationMounts && (edge.ResolutionState != "unresolved" || nodes[edge.TargetID].Status != "unresolved" || nodes[edge.TargetID].Resource.UID != "") {
|
|
292
|
+
t.Fatalf("unavailable target resolved: %+v", edge)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
t.Run("truncated class claims do not trigger an unrelated Pod scan", func(t *testing.T) {
|
|
297
|
+
pod, _, volume, class := storageGraphFixture()
|
|
298
|
+
client := fake.NewClientset(pod, volume, class)
|
|
299
|
+
client.PrependReactor("list", "persistentvolumeclaims", func(ktesting.Action) (bool, kruntime.Object, error) {
|
|
300
|
+
return true, &corev1.PersistentVolumeClaimList{ListMeta: metav1.ListMeta{Continue: "second-page"}}, nil
|
|
301
|
+
})
|
|
302
|
+
graph := collectStorageGraph(t, client, "", "StorageClass", "fast")
|
|
303
|
+
assertStorageGraphActions(t, client, []string{"get storageclasses//fast", "list persistentvolumes/", "list persistentvolumeclaims/"})
|
|
304
|
+
if !slices.Equal(graph.Warnings, []string{"PersistentVolumeClaim source truncated at 500 objects"}) || !graphTestEdge(graph, "binds-to", "data", "disk") {
|
|
305
|
+
t.Fatalf("truncated source or declared claim missing: %+v", graph)
|
|
306
|
+
}
|
|
307
|
+
for _, node := range graph.Nodes {
|
|
308
|
+
if node.Resource.Kind == "PersistentVolumeClaim" && (node.Resource.UID != "claim-uid" || node.Status != "unresolved") {
|
|
309
|
+
t.Fatalf("unobserved claim resolved: %+v", node)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
t.Run("canceled sources fail the request", func(t *testing.T) {
|
|
314
|
+
_, claim, volume, class := storageGraphFixture()
|
|
315
|
+
client := fake.NewClientset(claim, volume, class)
|
|
316
|
+
client.PrependReactor("list", "pods", func(ktesting.Action) (bool, kruntime.Object, error) { return true, nil, context.DeadlineExceeded })
|
|
317
|
+
_, err := BuildKubernetesResourceGraph(context.Background(), client, cr.ResourceRef{ClusterID: "cluster", Namespace: "team", Kind: "PersistentVolumeClaim", Name: "data"})
|
|
318
|
+
if !errors.Is(err, context.DeadlineExceeded) {
|
|
319
|
+
t.Fatalf("cancellation did not stop graph: %v", err)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
func TestResourceGraphStorageCollectorRejectsInvalidRoots(t *testing.T) {
|
|
325
|
+
for _, root := range []cr.ResourceRef{
|
|
326
|
+
{Kind: "PersistentVolumeClaim", Name: "data"},
|
|
327
|
+
{Kind: "PersistentVolumeClaim", Name: "data", Namespace: "team", ScopeMode: cr.ResourceScopeModeCluster},
|
|
328
|
+
{Kind: "PersistentVolume", Name: "disk", Namespace: "team"},
|
|
329
|
+
{Kind: "StorageClass", Name: "fast", Namespace: "team"},
|
|
330
|
+
{Kind: "StorageClass", Name: "fast", ScopeMode: cr.ResourceScopeModeNamespace},
|
|
331
|
+
{Kind: "StorageClass", Name: "fast", APIVersion: "v1"},
|
|
332
|
+
} {
|
|
333
|
+
root.ClusterID = "cluster"
|
|
334
|
+
client := fake.NewClientset()
|
|
335
|
+
_, err := BuildKubernetesResourceGraph(context.Background(), client, root)
|
|
336
|
+
if !errors.Is(err, ErrInvalidResourceGraphRoot) || len(client.Actions()) != 0 {
|
|
337
|
+
t.Errorf("invalid root %+v triggered provider actions: %v, %v", root, err, client.Actions())
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
_, claim, volume, class := storageGraphFixture()
|
|
341
|
+
for _, mutate := range []func(*corev1.PersistentVolume){
|
|
342
|
+
func(item *corev1.PersistentVolume) { item.Name = "different" },
|
|
343
|
+
func(item *corev1.PersistentVolume) { item.Namespace = "team" },
|
|
344
|
+
func(item *corev1.PersistentVolume) {
|
|
345
|
+
item.APIVersion, item.Kind = "custom.example/v1", "PersistentVolume"
|
|
346
|
+
},
|
|
347
|
+
} {
|
|
348
|
+
mismatched := volume.DeepCopy()
|
|
349
|
+
mutate(mismatched)
|
|
350
|
+
client := fake.NewClientset(claim, volume, class)
|
|
351
|
+
client.PrependReactor("get", "persistentvolumes", func(ktesting.Action) (bool, kruntime.Object, error) { return true, mismatched, nil })
|
|
352
|
+
_, err := BuildKubernetesResourceGraph(context.Background(), client, cr.ResourceRef{ClusterID: "cluster", Kind: "PersistentVolume", Name: "disk"})
|
|
353
|
+
if !errors.Is(err, ErrInvalidResourceGraphRoot) {
|
|
354
|
+
t.Errorf("changed named root was accepted: %v", err)
|
|
355
|
+
}
|
|
356
|
+
assertStorageGraphActions(t, client, []string{"get persistentvolumes//disk"})
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func TestResourceGraphStorageProjectionAndExistingPodRoot(t *testing.T) {
|
|
361
|
+
pod, claim, volume, class := storageGraphFixture()
|
|
362
|
+
volume.Labels = map[string]string{"private": "private-label"}
|
|
363
|
+
volume.Spec.CSI = &corev1.CSIPersistentVolumeSource{
|
|
364
|
+
Driver: "private-driver", VolumeHandle: "private-handle", VolumeAttributes: map[string]string{"token": "private-token"},
|
|
365
|
+
NodePublishSecretRef: &corev1.SecretReference{Name: "private-secret", Namespace: "private-namespace"},
|
|
366
|
+
}
|
|
367
|
+
volume.Spec.ClaimRef.FieldPath = "private-field"
|
|
368
|
+
class.Provisioner = "private-provisioner"
|
|
369
|
+
class.Parameters = map[string]string{"key": "private-value"}
|
|
370
|
+
for _, tc := range []struct {
|
|
371
|
+
obj kruntime.Object
|
|
372
|
+
kind string
|
|
373
|
+
}{
|
|
374
|
+
{volume, "PersistentVolume"}, {class, "StorageClass"},
|
|
375
|
+
} {
|
|
376
|
+
projection, err := graphObjectProjection(tc.obj, graphBuiltinAPIs[tc.kind], tc.kind)
|
|
377
|
+
if err != nil {
|
|
378
|
+
t.Fatal(err)
|
|
379
|
+
}
|
|
380
|
+
content, err := json.Marshal(projection)
|
|
381
|
+
if err != nil || strings.Contains(string(content), "private-") {
|
|
382
|
+
t.Fatalf("storage projection retained provider payload: %s, %v", content, err)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
client := fake.NewClientset(pod, claim, volume, class)
|
|
386
|
+
graph := collectStorageGraph(t, client, "team", "Pod", "app")
|
|
387
|
+
for _, action := range client.Actions() {
|
|
388
|
+
if slices.Contains([]string{"persistentvolumes", "storageclasses", "secrets"}, action.GetResource().Resource) {
|
|
389
|
+
t.Fatalf("existing Pod root issued new storage or Secret read: %+v", action)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if !reflect.DeepEqual(graph.Edges, []cr.ResourceGraphEdge{{
|
|
393
|
+
ID: "mounts:v1:Pod:team/app:pod-uid:v1:PersistentVolumeClaim:team/data:claim-uid", SourceID: graph.RootID, TargetID: "v1:PersistentVolumeClaim:team/data:claim-uid", Relation: "mounts",
|
|
394
|
+
}}) {
|
|
395
|
+
t.Fatalf("existing Pod PVC edge changed: %+v", graph.Edges)
|
|
396
|
+
}
|
|
397
|
+
}
|