@opensoha/contracts 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/COMPATIBILITY.md +4 -3
  2. package/README.md +8 -9
  3. package/compat/consumer-revisions.json +1 -1
  4. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  5. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  6. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  7. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  8. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  9. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  10. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  11. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  12. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  13. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  14. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  15. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  16. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  17. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  18. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  19. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  20. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  21. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  22. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  23. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  24. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  25. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  26. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  27. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  28. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  29. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  30. package/gen/go/sohaapi/client.go +1 -1
  31. package/gen/go/sohaapi/client_test.go +13 -0
  32. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  33. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  34. package/gen/go/sohaapi/types.go +421 -88
  35. package/gen/ts/sohaapi/index.ts +756 -4
  36. package/helmrelease/runtime/values_upgrade.go +78 -0
  37. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  38. package/helmrelease/values_update.go +31 -0
  39. package/openapi/soha-api.json +1191 -14
  40. package/openapi/soha-api.yaml +588 -28
  41. package/openapi/spec_generated.go +3 -3
  42. package/package.json +1 -1
  43. package/resource/agent_profiles.go +9 -15
  44. package/resource/agent_profiles_test.go +9 -3
  45. package/resource/custom_graph_page.go +18 -0
  46. package/resource/models.go +102 -57
  47. package/resource/network_topology.go +22 -0
  48. package/resource/network_topology_projection.go +36 -22
  49. package/resource/network_topology_projection_test.go +4 -0
  50. package/resource/network_topology_test.go +20 -0
  51. package/resource/network_topology_validation.go +5 -0
  52. package/resource/observed_workload.go +18 -0
  53. package/resource/pod_status_test.go +22 -0
  54. package/resource/rbac_graph.go +4 -0
  55. package/resource/rollout_optional_test.go +33 -0
  56. package/resource/runtime/network_relationship_primitives.go +66 -0
  57. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  58. package/resource/runtime/network_topology.go +17 -5
  59. package/resource/runtime/network_topology_builder.go +405 -0
  60. package/resource/runtime/network_topology_builder_backends.go +195 -0
  61. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  62. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  63. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  64. package/resource/runtime/network_topology_builder_providers.go +242 -0
  65. package/resource/runtime/network_topology_builder_test.go +186 -0
  66. package/resource/runtime/network_topology_declarations.go +329 -0
  67. package/resource/runtime/resource_edit.go +22 -0
  68. package/resource/runtime/resource_edit_test.go +32 -0
  69. package/resource/runtime/resource_graph_collect.go +325 -0
  70. package/resource/runtime/resource_graph_configuration.go +59 -0
  71. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  72. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  73. package/resource/runtime/resource_graph_custom_page.go +159 -0
  74. package/resource/runtime/resource_graph_custom_root.go +132 -0
  75. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  76. package/resource/runtime/resource_graph_enrichment.go +417 -0
  77. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  78. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  79. package/resource/runtime/resource_graph_metadata.go +125 -0
  80. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  81. package/resource/runtime/resource_graph_network.go +224 -0
  82. package/resource/runtime/resource_graph_network_collect.go +174 -0
  83. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  84. package/resource/runtime/resource_graph_network_test.go +342 -0
  85. package/resource/runtime/resource_graph_rbac.go +278 -0
  86. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  87. package/resource/runtime/resource_graph_rules.go +834 -0
  88. package/resource/runtime/resource_graph_storage.go +144 -0
  89. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  90. package/resource/runtime/resource_graph_storage_test.go +397 -0
  91. package/resource/runtime/resource_graph_test.go +627 -0
  92. package/resource/runtime/secret_certificate.go +44 -0
  93. package/resource/runtime/secret_certificate_test.go +87 -0
  94. package/resource/storage_graph.go +4 -0
@@ -0,0 +1,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
+ }