@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,132 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// BuildCustomResourceGraph builds the metadata relationships for an observed
|
|
12
|
+
// CRD or custom instance. Callers authorize every observation and establish the
|
|
13
|
+
// definition's served GVK, plural and scope. An input may omit TargetID, which
|
|
14
|
+
// is derived from its metadata. An observed built-in may omit Definition.
|
|
15
|
+
func BuildCustomResourceGraph(ctx context.Context, root cr.ResourceRef, definition cr.ResourceRef, inputs []ResourceGraphMetadataEnrichment) (cr.ResourceGraph, error) {
|
|
16
|
+
ctx, cancel := context.WithTimeout(ctx, ResourceGraphTimeout)
|
|
17
|
+
defer cancel()
|
|
18
|
+
if err := ctx.Err(); err != nil {
|
|
19
|
+
return cr.ResourceGraph{}, err
|
|
20
|
+
}
|
|
21
|
+
if len(inputs) > ResourceGraphMetadataEnrichmentLimit {
|
|
22
|
+
return cr.ResourceGraph{}, ErrResourceGraphBudget
|
|
23
|
+
}
|
|
24
|
+
if root.ClusterID == "" || root.APIVersion == "" || root.Kind == "" || root.Name == "" || root.UID == "" || root.ScopeMode != topologyScope(root.Namespace != "") {
|
|
25
|
+
return cr.ResourceGraph{}, invalidGraphRoot("custom graph requires an exact observed root")
|
|
26
|
+
}
|
|
27
|
+
plural, group, _ := strings.Cut(definition.Name, ".")
|
|
28
|
+
if definition.ClusterID != root.ClusterID || !customGraphIsDefinition(definition) || definition.UID == "" || definition.Namespace != "" || definition.ScopeMode != cr.ResourceScopeModeCluster || plural == "" || group == "" {
|
|
29
|
+
return cr.ResourceGraph{}, invalidGraphRoot("custom graph requires an exact observed definition")
|
|
30
|
+
}
|
|
31
|
+
rootID := graphNodeID(root)
|
|
32
|
+
definitionRoot := customGraphIsDefinition(root)
|
|
33
|
+
if definitionRoot && rootID != graphNodeID(definition) {
|
|
34
|
+
return cr.ResourceGraph{}, invalidGraphRoot("definition root identity mismatch")
|
|
35
|
+
}
|
|
36
|
+
definitions := map[string]cr.ResourceRef{definition.Name: definition}
|
|
37
|
+
observations := make(map[string]ResourceGraphEnrichment, len(inputs))
|
|
38
|
+
nodes := make(map[string]cr.ResourceGraphNode, len(inputs)+1)
|
|
39
|
+
for _, input := range inputs {
|
|
40
|
+
if err := ctx.Err(); err != nil {
|
|
41
|
+
return cr.ResourceGraph{}, err
|
|
42
|
+
}
|
|
43
|
+
if len(input.Content) > ResourceGraphMetadataJSONLimit {
|
|
44
|
+
return cr.ResourceGraph{}, ErrResourceGraphBudget
|
|
45
|
+
}
|
|
46
|
+
o, err := decodeEnrichmentMetadata(input.Content)
|
|
47
|
+
if err != nil {
|
|
48
|
+
return cr.ResourceGraph{}, err
|
|
49
|
+
}
|
|
50
|
+
ref := enrichmentRef(root.ClusterID, o)
|
|
51
|
+
if ref.APIVersion == "" || ref.Kind == "" || ref.Name == "" || ref.UID == "" || customGraphIsDefinition(ref) {
|
|
52
|
+
return cr.ResourceGraph{}, invalidGraphEnrichment("observation requires exact instance identity")
|
|
53
|
+
}
|
|
54
|
+
id := graphNodeID(ref)
|
|
55
|
+
if input.TargetID != "" && input.TargetID != id {
|
|
56
|
+
return cr.ResourceGraph{}, invalidGraphEnrichment("observation target identity mismatch")
|
|
57
|
+
}
|
|
58
|
+
if graphBuiltinSpec(o) {
|
|
59
|
+
if input.Definition != (cr.ResourceRef{}) {
|
|
60
|
+
return cr.ResourceGraph{}, invalidGraphEnrichment("built-in observation has a custom definition")
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
if err := validateEnrichmentDefinition(root.ClusterID, ref.APIVersion, input.Definition); err != nil {
|
|
64
|
+
return cr.ResourceGraph{}, err
|
|
65
|
+
}
|
|
66
|
+
apiGroup, version, _ := strings.Cut(ref.APIVersion, "/")
|
|
67
|
+
_, definitionGroup, _ := strings.Cut(input.Definition.Name, ".")
|
|
68
|
+
if version == "" || strings.Contains(version, "/") || definitionGroup != apiGroup {
|
|
69
|
+
return cr.ResourceGraph{}, invalidGraphEnrichment("definition requires exact group and instance version")
|
|
70
|
+
}
|
|
71
|
+
if prior, exists := definitions[input.Definition.Name]; exists {
|
|
72
|
+
if prior.UID != input.Definition.UID {
|
|
73
|
+
return cr.ResourceGraph{}, invalidGraphEnrichment("conflicting definition UID")
|
|
74
|
+
}
|
|
75
|
+
input.Definition = prior
|
|
76
|
+
} else {
|
|
77
|
+
definitions[input.Definition.Name] = input.Definition
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
key := graphIdentity(ref.APIVersion, ref.Kind, ref.Namespace, ref.Name)
|
|
81
|
+
observation := ResourceGraphEnrichment{TargetID: id, Object: o, Definition: input.Definition}
|
|
82
|
+
if prior, exists := observations[key]; exists {
|
|
83
|
+
if prior.TargetID != id || !sameEnrichment(prior, observation) {
|
|
84
|
+
return cr.ResourceGraph{}, invalidGraphEnrichment("conflicting duplicate observation")
|
|
85
|
+
}
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
observations[key] = observation
|
|
89
|
+
nodes[id] = cr.ResourceGraphNode{ID: id, Resource: ref}
|
|
90
|
+
}
|
|
91
|
+
rootObservation := observations[graphIdentity(root.APIVersion, root.Kind, root.Namespace, root.Name)]
|
|
92
|
+
if !definitionRoot && (rootObservation.TargetID != rootID || rootObservation.Definition != definition) {
|
|
93
|
+
return cr.ResourceGraph{}, invalidGraphRoot("custom root metadata identity mismatch")
|
|
94
|
+
}
|
|
95
|
+
for _, ref := range definitions {
|
|
96
|
+
id := graphNodeID(ref)
|
|
97
|
+
nodes[id] = cr.ResourceGraphNode{ID: id, Resource: ref}
|
|
98
|
+
}
|
|
99
|
+
nodes[rootID] = cr.ResourceGraphNode{ID: rootID, Resource: root}
|
|
100
|
+
ordered := make([]ResourceGraphEnrichment, 0, len(observations))
|
|
101
|
+
for _, input := range observations {
|
|
102
|
+
ordered = append(ordered, input)
|
|
103
|
+
}
|
|
104
|
+
sort.Slice(ordered, func(i, j int) bool { return ordered[i].TargetID < ordered[j].TargetID })
|
|
105
|
+
b := enrichmentBuilder(ctx, root.ClusterID, nodes, ordered)
|
|
106
|
+
b.nodes = nodes
|
|
107
|
+
for _, input := range ordered {
|
|
108
|
+
if input.Definition.UID != "" {
|
|
109
|
+
b.edge(graphNodeID(input.Definition), input.TargetID, "defines")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if rootObservation.Object != nil {
|
|
113
|
+
b.ownerRelations(rootObservation.Object, rootID)
|
|
114
|
+
}
|
|
115
|
+
for _, input := range ordered {
|
|
116
|
+
if input.TargetID != rootID {
|
|
117
|
+
b.ownerRelations(input.Object, input.TargetID)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if b.err != nil {
|
|
121
|
+
return cr.ResourceGraph{}, b.err
|
|
122
|
+
}
|
|
123
|
+
graph, err := b.bounded(rootID, root.Namespace)
|
|
124
|
+
if b.partial {
|
|
125
|
+
graph.Warnings = append(graph.Warnings, "resource graph working set truncated at 10000 nodes or 10000 edges")
|
|
126
|
+
}
|
|
127
|
+
return graph, err
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func customGraphIsDefinition(ref cr.ResourceRef) bool {
|
|
131
|
+
return ref.APIVersion == "apiextensions.k8s.io/v1" && ref.Kind == "CustomResourceDefinition"
|
|
132
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
12
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
13
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func customRootObservation(t *testing.T, object *unstructured.Unstructured, definition cr.ResourceRef) ResourceGraphMetadataEnrichment {
|
|
17
|
+
t.Helper()
|
|
18
|
+
content, err := object.MarshalJSON()
|
|
19
|
+
if err != nil {
|
|
20
|
+
t.Fatal(err)
|
|
21
|
+
}
|
|
22
|
+
return ResourceGraphMetadataEnrichment{Content: content, Definition: definition}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestBuildCustomResourceGraphDefinitionRoot(t *testing.T) {
|
|
26
|
+
_, fixture := graphEnrichmentFixture()
|
|
27
|
+
definition := fixture.Definition
|
|
28
|
+
graph, err := BuildCustomResourceGraph(context.Background(), definition, definition, nil)
|
|
29
|
+
if err != nil || graph.RootID != graphNodeID(definition) || len(graph.Nodes) != 1 || len(graph.Edges) != 0 || graph.Nodes[0].Resource != definition {
|
|
30
|
+
t.Fatalf("empty definition graph: %+v, %v", graph, err)
|
|
31
|
+
}
|
|
32
|
+
instance := customRootObservation(t, fixture.Object, definition)
|
|
33
|
+
other := graphTestObject("other.example/v1", "Widget", "owner", "unrelated-uid")
|
|
34
|
+
otherDefinition := definition
|
|
35
|
+
otherDefinition.Name, otherDefinition.UID = "widgets.other.example", "unrelated-crd-uid"
|
|
36
|
+
graph, err = BuildCustomResourceGraph(context.Background(), definition, definition, []ResourceGraphMetadataEnrichment{instance, customRootObservation(t, other, otherDefinition), instance})
|
|
37
|
+
if err != nil || graph.RootID != graphNodeID(definition) || len(graph.Nodes) != 2 || len(graph.Edges) != 1 || !graphTestEdge(graph, "defines", definition.Name, fixture.Object.GetName()) {
|
|
38
|
+
t.Fatalf("definition graph included a non-instance, duplicate or omitted the exact instance: %+v, %v", graph, err)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestBuildCustomResourceGraphOwnerReferences(t *testing.T) {
|
|
43
|
+
_, fixture := graphEnrichmentFixture()
|
|
44
|
+
definition := fixture.Definition
|
|
45
|
+
root := fixture.Object
|
|
46
|
+
root.SetName("root")
|
|
47
|
+
root.SetUID("root-uid")
|
|
48
|
+
root.Object["spec"] = map[string]interface{}{"secretRef": "MUST-NOT-LEAK"}
|
|
49
|
+
root.SetLabels(map[string]string{"targetRef": "MUST-NOT-LEAK"})
|
|
50
|
+
parent := graphTestObject("parents.example/v1", "Parent", "parent", "parent-uid")
|
|
51
|
+
parent.SetNamespace("")
|
|
52
|
+
parentDefinition := definition
|
|
53
|
+
parentDefinition.Name, parentDefinition.UID = "parents.parents.example", "parent-definition-uid"
|
|
54
|
+
graphTestOwner(root, parent.GetAPIVersion(), parent.GetKind(), parent.GetName(), string(parent.GetUID()), true)
|
|
55
|
+
child := graphTestObject("v1", "Pod", "child", "child-uid")
|
|
56
|
+
graphTestOwner(child, root.GetAPIVersion(), root.GetKind(), root.GetName(), string(root.GetUID()), false)
|
|
57
|
+
stale := graphTestObject("v1", "Pod", "stale-child", "stale-child-uid")
|
|
58
|
+
graphTestOwner(stale, root.GetAPIVersion(), root.GetKind(), root.GetName(), "replaced-root-uid", false)
|
|
59
|
+
otherNamespace := graphTestObject("v1", "Pod", "other-namespace-child", "other-namespace-child-uid")
|
|
60
|
+
otherNamespace.SetNamespace("other")
|
|
61
|
+
graphTestOwner(otherNamespace, root.GetAPIVersion(), root.GetKind(), root.GetName(), string(root.GetUID()), false)
|
|
62
|
+
rootRef := enrichmentRef(definition.ClusterID, root)
|
|
63
|
+
rootRef.ResourceVersion = "9"
|
|
64
|
+
rootInput := customRootObservation(t, root, definition)
|
|
65
|
+
rootInput.TargetID = graphNodeID(rootRef)
|
|
66
|
+
graph, err := BuildCustomResourceGraph(context.Background(), rootRef, definition, []ResourceGraphMetadataEnrichment{
|
|
67
|
+
rootInput, customRootObservation(t, parent, parentDefinition), customRootObservation(t, child, cr.ResourceRef{}), customRootObservation(t, stale, cr.ResourceRef{}), customRootObservation(t, otherNamespace, cr.ResourceRef{}),
|
|
68
|
+
})
|
|
69
|
+
if err != nil {
|
|
70
|
+
t.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
if graph.RootID != graphNodeID(rootRef) || len(graph.Nodes) != 5 || len(graph.Edges) != 4 || !graphTestEdge(graph, "defines", definition.Name, "root") || !graphTestEdge(graph, "defines", parentDefinition.Name, "parent") || !graphTestEdge(graph, "controls", "parent", "root") || !graphTestEdge(graph, "owns", "root", "child") {
|
|
73
|
+
t.Fatalf("unexpected custom graph: %+v", graph)
|
|
74
|
+
}
|
|
75
|
+
for _, node := range graph.Nodes {
|
|
76
|
+
if node.ID == graph.RootID && node.Resource != rootRef {
|
|
77
|
+
t.Fatalf("root identity changed: %+v", node)
|
|
78
|
+
}
|
|
79
|
+
if node.Resource.Name == "parent" && (node.Status == "unresolved" || node.Resource.Namespace != "" || node.Resource.ScopeMode != cr.ResourceScopeModeCluster) {
|
|
80
|
+
t.Fatalf("cluster-scoped owner failed exact resolution: %+v", node)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
content, err := json.Marshal(graph)
|
|
84
|
+
if err != nil || strings.Contains(string(content), "MUST-NOT-LEAK") {
|
|
85
|
+
t.Fatalf("spec or labels reached graph output: %s, %v", content, err)
|
|
86
|
+
}
|
|
87
|
+
parent.SetUID("new-parent-uid")
|
|
88
|
+
graph, err = BuildCustomResourceGraph(context.Background(), rootRef, definition, []ResourceGraphMetadataEnrichment{rootInput, customRootObservation(t, parent, parentDefinition)})
|
|
89
|
+
if err != nil || len(graph.Nodes) != 3 || !graphTestEdge(graph, "controls", "parent", "root") {
|
|
90
|
+
t.Fatalf("stale owner declaration missing: %+v, %v", graph, err)
|
|
91
|
+
}
|
|
92
|
+
for _, node := range graph.Nodes {
|
|
93
|
+
if node.Resource.Name == "parent" && (node.Status != "unresolved" || node.Resource.UID != "parent-uid") {
|
|
94
|
+
t.Fatalf("replacement owner incorrectly resolved: %+v", node)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func TestBuildCustomResourceGraphExactIdentity(t *testing.T) {
|
|
100
|
+
_, fixture := graphEnrichmentFixture()
|
|
101
|
+
for _, test := range []struct {
|
|
102
|
+
name string
|
|
103
|
+
edit func(*cr.ResourceRef, *cr.ResourceRef, *[]ResourceGraphMetadataEnrichment)
|
|
104
|
+
want error
|
|
105
|
+
}{
|
|
106
|
+
{"missing root UID", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) { root.UID = "" }, ErrInvalidResourceGraphRoot},
|
|
107
|
+
{"wrong root UID", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) { root.UID = "stale" }, ErrInvalidResourceGraphRoot},
|
|
108
|
+
{"wrong root GVK", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
|
|
109
|
+
root.APIVersion = "custom.example/v2"
|
|
110
|
+
}, ErrInvalidResourceGraphRoot},
|
|
111
|
+
{"wrong root scope", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
|
|
112
|
+
root.ScopeMode = cr.ResourceScopeModeCluster
|
|
113
|
+
}, ErrInvalidResourceGraphRoot},
|
|
114
|
+
{"wrong root namespace", func(root, _ *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) { root.Namespace = "other" }, ErrInvalidResourceGraphRoot},
|
|
115
|
+
{"missing root metadata", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) { *inputs = nil }, ErrInvalidResourceGraphRoot},
|
|
116
|
+
{"wrong definition cluster", func(_, definition *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
|
|
117
|
+
definition.ClusterID = "other"
|
|
118
|
+
}, ErrInvalidResourceGraphRoot},
|
|
119
|
+
{"wrong definition root UID", func(root, definition *cr.ResourceRef, _ *[]ResourceGraphMetadataEnrichment) {
|
|
120
|
+
*root = *definition
|
|
121
|
+
root.UID = "stale"
|
|
122
|
+
}, ErrInvalidResourceGraphRoot},
|
|
123
|
+
{"wrong observation ID", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
|
|
124
|
+
(*inputs)[0].TargetID = "unrelated"
|
|
125
|
+
}, ErrInvalidResourceGraphEnrichment},
|
|
126
|
+
{"conflicting definition UID", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
|
|
127
|
+
(*inputs)[0].Definition.UID = "stale"
|
|
128
|
+
}, ErrInvalidResourceGraphEnrichment},
|
|
129
|
+
{"lookalike definition group", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
|
|
130
|
+
(*inputs)[0].Definition.Name = "widgets.fake.custom.example"
|
|
131
|
+
}, ErrInvalidResourceGraphEnrichment},
|
|
132
|
+
{"missing observation UID", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
|
|
133
|
+
(*inputs)[0].Content = []byte(`{"apiVersion":"custom.example/v1","kind":"Widget","metadata":{"name":"owner","namespace":"ns"}}`)
|
|
134
|
+
}, ErrInvalidResourceGraphEnrichment},
|
|
135
|
+
{"invalid metadata", func(_, _ *cr.ResourceRef, inputs *[]ResourceGraphMetadataEnrichment) {
|
|
136
|
+
(*inputs)[0].Content = []byte("{")
|
|
137
|
+
}, ErrInvalidResourceGraphEnrichment},
|
|
138
|
+
} {
|
|
139
|
+
t.Run(test.name, func(t *testing.T) {
|
|
140
|
+
definition := fixture.Definition
|
|
141
|
+
root := enrichmentRef(definition.ClusterID, fixture.Object)
|
|
142
|
+
inputs := []ResourceGraphMetadataEnrichment{customRootObservation(t, fixture.Object, definition)}
|
|
143
|
+
test.edit(&root, &definition, &inputs)
|
|
144
|
+
_, err := BuildCustomResourceGraph(context.Background(), root, definition, inputs)
|
|
145
|
+
if !errors.Is(err, test.want) {
|
|
146
|
+
t.Fatalf("expected %v, got %v", test.want, err)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func TestBuildCustomResourceGraphBudgets(t *testing.T) {
|
|
153
|
+
_, fixture := graphEnrichmentFixture()
|
|
154
|
+
definition := fixture.Definition
|
|
155
|
+
root := enrichmentRef(definition.ClusterID, fixture.Object)
|
|
156
|
+
for _, inputs := range [][]ResourceGraphMetadataEnrichment{
|
|
157
|
+
make([]ResourceGraphMetadataEnrichment, ResourceGraphMetadataEnrichmentLimit+1),
|
|
158
|
+
{{Content: make([]byte, ResourceGraphMetadataJSONLimit+1)}},
|
|
159
|
+
} {
|
|
160
|
+
_, err := BuildCustomResourceGraph(context.Background(), root, definition, inputs)
|
|
161
|
+
if !errors.Is(err, ErrResourceGraphBudget) {
|
|
162
|
+
t.Fatalf("expected budget error, got %v", err)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
166
|
+
cancel()
|
|
167
|
+
if _, err := BuildCustomResourceGraph(ctx, definition, definition, nil); !errors.Is(err, context.Canceled) {
|
|
168
|
+
t.Fatalf("expected context cancellation, got %v", err)
|
|
169
|
+
}
|
|
170
|
+
owners := make([]metav1.OwnerReference, ResourceGraphNodeLimit)
|
|
171
|
+
for i := range owners {
|
|
172
|
+
owners[i] = metav1.OwnerReference{APIVersion: "custom.example/v1", Kind: "Parent", Name: fmt.Sprintf("parent-%03d", i), UID: "unobserved"}
|
|
173
|
+
}
|
|
174
|
+
fixture.Object.SetOwnerReferences(owners)
|
|
175
|
+
graph, err := BuildCustomResourceGraph(context.Background(), root, definition, []ResourceGraphMetadataEnrichment{customRootObservation(t, fixture.Object, definition)})
|
|
176
|
+
if err != nil || len(graph.Nodes) != ResourceGraphNodeLimit || len(graph.Edges) >= ResourceGraphEdgeLimit || !strings.Contains(strings.Join(graph.Warnings, " "), "truncated") {
|
|
177
|
+
t.Fatalf("budget was not enforced: nodes=%d edges=%d warnings=%v err=%v", len(graph.Nodes), len(graph.Edges), graph.Warnings, err)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func TestBuildCustomResourceGraphDepth(t *testing.T) {
|
|
182
|
+
_, fixture := graphEnrichmentFixture()
|
|
183
|
+
definition := fixture.Definition
|
|
184
|
+
root := enrichmentRef(definition.ClusterID, fixture.Object)
|
|
185
|
+
inputs := []ResourceGraphMetadataEnrichment{customRootObservation(t, fixture.Object, definition)}
|
|
186
|
+
owner := fixture.Object
|
|
187
|
+
for i := 1; i <= ResourceGraphDepth+1; i++ {
|
|
188
|
+
child := graphTestObject("v1", "Pod", fmt.Sprintf("child-%d", i), fmt.Sprintf("child-uid-%d", i))
|
|
189
|
+
graphTestOwner(child, owner.GetAPIVersion(), owner.GetKind(), owner.GetName(), string(owner.GetUID()), true)
|
|
190
|
+
inputs = append(inputs, customRootObservation(t, child, cr.ResourceRef{}))
|
|
191
|
+
owner = child
|
|
192
|
+
}
|
|
193
|
+
graph, err := BuildCustomResourceGraph(context.Background(), root, definition, inputs)
|
|
194
|
+
if err != nil || len(graph.Nodes) != ResourceGraphDepth+2 || len(graph.Edges) != ResourceGraphDepth+1 || !strings.Contains(strings.Join(graph.Warnings, " "), "depth") {
|
|
195
|
+
t.Fatalf("depth was not enforced: %+v, %v", graph, err)
|
|
196
|
+
}
|
|
197
|
+
}
|