@opensoha/contracts 0.1.26 → 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 +14 -3
- package/README.md +8 -9
- package/auth/catalog_generated.go +2 -2
- package/auth/permission-catalog.json +481 -1
- 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/endpointcontrol/decode.go +64 -0
- package/endpointcontrol/models.go +138 -0
- package/endpointcontrol/models_test.go +51 -0
- package/endpointcontrol/protocol.schema.json +397 -0
- package/fixtures/json-schema/endpoint-controls/invalid/controls.json +16 -0
- package/fixtures/json-schema/endpoint-controls/invalid/missing-channel-decision.json +15 -0
- package/fixtures/json-schema/endpoint-controls/invalid/receipt-content.json +36 -0
- package/fixtures/json-schema/endpoint-controls/valid/controls.json +16 -0
- package/fixtures/json-schema/endpoint-controls/valid/receipt.json +35 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/eap-activate-bad-fingerprint.json +14 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/eap-gateway-request.json +14 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/eap-private-key-request.json +15 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/eap-profile-without-eap-credential.json +22 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/endpoint-dns-observation.json +25 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/gateway-presence.json +22 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-protection-zero-version.json +45 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/presence-raw-key.json +23 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/presence-zero-sequence.json +22 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/workload-wide-vpn.json +17 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-activate.json +14 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-request.json +14 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/eap-profile-enrollment.json +24 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/endpoint-presence.json +22 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/ipv6-configuration.json +102 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-protected-policy.json +45 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/site-dns-ipv6.json +25 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/workload-direct-ztna.json +20 -0
- package/fixtures/openapi/DLPShadowDecision/invalid/step-up-type.json +14 -0
- package/fixtures/openapi/DLPShadowDecision/valid/archive-v2.json +14 -0
- package/fixtures/openapi/DLPShadowDecision/valid/step-up.json +14 -0
- package/fixtures/openapi/DLPShadowInput/invalid/self-reported-result.json +7 -0
- package/fixtures/openapi/DLPShadowInput/invalid/too-many-samples.json +14 -0
- package/fixtures/openapi/DLPShadowInput/valid/bounded.json +7 -0
- package/fixtures/openapi/DockerVolumeDLPAppealInput/invalid/p4-response.json +3 -0
- package/fixtures/openapi/DockerVolumeDLPAppealInput/valid/p4-response.json +3 -0
- package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/invalid/p4-response.json +5 -0
- package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/valid/p4-response.json +5 -0
- package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/invalid/p4-response.json +3 -0
- package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/valid/p4-response.json +3 -0
- package/fixtures/openapi/DockerVolumeDLPContent/invalid/archive-unsupported-channel.json +9 -0
- package/fixtures/openapi/DockerVolumeDLPContent/invalid/p4.json +6 -0
- package/fixtures/openapi/DockerVolumeDLPContent/invalid/step-up-audit.json +9 -0
- package/fixtures/openapi/DockerVolumeDLPContent/invalid/unavailable-engine.json +9 -0
- package/fixtures/openapi/DockerVolumeDLPContent/invalid/unknown-data-level.json +12 -0
- package/fixtures/openapi/DockerVolumeDLPContent/valid/classified-rule.json +16 -0
- package/fixtures/openapi/DockerVolumeDLPContent/valid/p4.json +8 -0
- package/fixtures/openapi/DockerVolumeDLPContent/valid/step-up.json +9 -0
- package/fixtures/openapi/DockerVolumeDLPDecision/invalid/p4.json +9 -0
- package/fixtures/openapi/DockerVolumeDLPDecision/invalid/step-up-type.json +12 -0
- package/fixtures/openapi/DockerVolumeDLPDecision/valid/archive-v2.json +12 -0
- package/fixtures/openapi/DockerVolumeDLPDecision/valid/p4.json +11 -0
- package/fixtures/openapi/DockerVolumeDLPDecision/valid/step-up.json +12 -0
- package/fixtures/openapi/DockerVolumeDLPSaveInput/invalid/p4.json +11 -0
- package/fixtures/openapi/DockerVolumeDLPSaveInput/valid/p4.json +11 -0
- package/fixtures/openapi/DockerVolumeFileReadInput/invalid/p4-response.json +5 -0
- package/fixtures/openapi/DockerVolumeFileReadInput/valid/p4-response.json +4 -0
- package/fixtures/openapi/EndpointDeviceInput/invalid/reported-management.json +5 -0
- package/fixtures/openapi/EndpointDeviceInput/valid/management-state.json +5 -0
- package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/self-promote-management.json +5 -0
- 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/fixtures/openapi/ManagedContentPasteInput/invalid/boundary.json +5 -0
- package/fixtures/openapi/ManagedContentPasteInput/valid/boundary.json +6 -0
- package/fixtures/openapi/ManagedContentPolicy/invalid/self-reported-allow.json +8 -0
- package/fixtures/openapi/ManagedContentPolicy/invalid/step-up-audit.json +8 -0
- package/fixtures/openapi/ManagedContentPolicy/invalid/unknown-extractor.json +8 -0
- package/fixtures/openapi/ManagedContentPolicy/valid/archive-v2.json +8 -0
- package/fixtures/openapi/ManagedContentPolicy/valid/network-and-level.json +10 -0
- package/fixtures/openapi/ManagedContentPolicy/valid/step-up.json +8 -0
- package/fixtures/openapi/ManagedContentReviewInput/invalid/boundary.json +5 -0
- package/fixtures/openapi/ManagedContentReviewInput/valid/boundary.json +5 -0
- package/fixtures/openapi/ManagedContentSaveInput/invalid/boundary.json +14 -0
- package/fixtures/openapi/ManagedContentSaveInput/valid/boundary.json +16 -0
- package/fixtures/openapi/ManagedContentUploadInput/invalid/boundary.json +7 -0
- package/fixtures/openapi/ManagedContentUploadInput/valid/boundary.json +6 -0
- package/fixtures/openapi/NetworkAccessPolicyInput/invalid/unknown-management-state.json +35 -0
- package/fixtures/openapi/NetworkAccessPolicyInput/valid/managed-device.json +35 -0
- package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/deviceId-characters.json +4 -0
- package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/nasBindingId-characters.json +4 -0
- package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/subject-override.json +1 -0
- package/fixtures/openapi/NetworkAdmissionPreviewRequest/valid/device-nas.json +1 -0
- package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-attributeVersion.json +13 -0
- package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-evaluatedAt.json +13 -0
- package/fixtures/openapi/NetworkAdmissionPreviewResult/valid/versioned.json +14 -0
- package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/missing-runtime-ranges.json +1 -0
- package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/unknown-source-type.json +9 -0
- package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/empty-runtime-ranges.json +3 -0
- package/fixtures/openapi/NetworkConflictAnalysisRequest/{invalid → valid}/ipv6-range.json +1 -1
- package/fixtures/openapi/NetworkEAPServerTrust/invalid/no-server-validation.json +5 -0
- package/fixtures/openapi/NetworkEAPServerTrust/valid/named-server.json +9 -0
- package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/all-sites-implicit.json +45 -0
- package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/unbounded-stages.json +54 -0
- package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/zero-heartbeat.json +47 -0
- package/fixtures/openapi/NetworkEndpointResponsePolicyInput/valid/staged-protection.json +47 -0
- package/fixtures/openapi/NetworkEndpointResponseRestoreInput/invalid/missing-reason.json +3 -0
- package/fixtures/openapi/NetworkEndpointResponseRestoreInput/valid/expected-current.json +4 -0
- package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/attribute-version.json +1 -0
- package/fixtures/openapi/NetworkPolicyPreviewResult/valid/admission-version.json +1 -0
- package/fixtures/openapi/NetworkPolicySubjects/invalid/duplicate-role.json +1 -0
- package/fixtures/openapi/NetworkPolicySubjects/invalid/role-characters.json +5 -0
- package/fixtures/openapi/NetworkPolicySubjects/valid/role-bound.json +1 -0
- package/fixtures/openapi/NetworkProxyConfigurationInput/invalid/missing-content.json +1 -0
- package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/bad-binding-id.json +9 -0
- package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/empty-content.json +1 -0
- package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/binding-only.json +11 -0
- package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/clear-binding-disabled.json +5 -0
- package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/enabled.json +1 -0
- package/fixtures/openapi/NetworkSiteInput/invalid/unknown-governance.json +6 -0
- package/fixtures/openapi/NetworkSiteInput/valid/restricted-unmanaged.json +6 -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/proxy_configuration_test.go +38 -0
- package/gen/go/sohaapi/types.go +3222 -260
- package/gen/ts/sohaapi/index.ts +15506 -10104
- 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/network/README.md +34 -0
- package/network/network-radius-accounting.schema.json +4 -1
- package/network/network-runtime-protocol.schema.json +2166 -317
- package/network/proxy-runtime.schema.json +482 -67
- package/openapi/network_control_test.go +64 -0
- package/openapi/soha-api.json +50412 -41310
- package/openapi/soha-api.yaml +16859 -10699
- package/openapi/spec_generated.go +3 -3
- package/package.json +6 -3
- package/proxytransport/handler.go +271 -0
- package/proxytransport/probe.go +30 -0
- package/proxytransport/scope.go +89 -0
- package/proxytransport/transport_test.go +60 -0
- 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,417 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
9
|
+
"reflect"
|
|
10
|
+
"sort"
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
14
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
15
|
+
"k8s.io/apimachinery/pkg/types"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// ResourceGraphEnrichment contains an authorized observation, not a discovery
|
|
19
|
+
// request. Callers must establish the CRD's group/kind/version from its spec.
|
|
20
|
+
// Only the instance's GVK and metadata are read; unknown payload is ignored.
|
|
21
|
+
type ResourceGraphEnrichment struct {
|
|
22
|
+
TargetID string
|
|
23
|
+
Object *unstructured.Unstructured
|
|
24
|
+
Definition cr.ResourceRef
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var ErrInvalidResourceGraphEnrichment = errors.New("invalid resource graph enrichment")
|
|
28
|
+
|
|
29
|
+
func invalidGraphEnrichment(reason string) error {
|
|
30
|
+
return fmt.Errorf("%w: %s", ErrInvalidResourceGraphEnrichment, reason)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// EnrichResourceGraph resolves authorized custom owner observations and adds
|
|
34
|
+
// metadata owner relationships and CRD defines edges. Existing IDs, edges,
|
|
35
|
+
// evidence and statuses are retained; no discovery, payload parsing or I/O occurs.
|
|
36
|
+
func EnrichResourceGraph(ctx context.Context, graph cr.ResourceGraph, enrichments []ResourceGraphEnrichment) (cr.ResourceGraph, error) {
|
|
37
|
+
ctx, cancel := context.WithTimeout(ctx, ResourceGraphTimeout)
|
|
38
|
+
defer cancel()
|
|
39
|
+
if err := ctx.Err(); err != nil {
|
|
40
|
+
return cr.ResourceGraph{}, err
|
|
41
|
+
}
|
|
42
|
+
if len(graph.Nodes) > ResourceGraphNodeLimit || len(graph.Edges) > ResourceGraphEdgeLimit || len(enrichments) > ResourceGraphWorkingNodeLimit {
|
|
43
|
+
return cr.ResourceGraph{}, ErrResourceGraphBudget
|
|
44
|
+
}
|
|
45
|
+
result := cloneEnrichmentGraph(graph)
|
|
46
|
+
nodes, err := enrichmentNodes(result)
|
|
47
|
+
if err != nil {
|
|
48
|
+
return cr.ResourceGraph{}, err
|
|
49
|
+
}
|
|
50
|
+
inputs, err := validateEnrichments(ctx, result.ClusterID, nodes, enrichments)
|
|
51
|
+
if err != nil {
|
|
52
|
+
return cr.ResourceGraph{}, err
|
|
53
|
+
}
|
|
54
|
+
for _, input := range inputs {
|
|
55
|
+
node := nodes[input.TargetID]
|
|
56
|
+
node.Resource = enrichmentRef(result.ClusterID, input.Object)
|
|
57
|
+
if node.Status == "unresolved" {
|
|
58
|
+
node.Status = ""
|
|
59
|
+
}
|
|
60
|
+
nodes[input.TargetID] = node
|
|
61
|
+
}
|
|
62
|
+
builder := enrichmentBuilder(ctx, result.ClusterID, nodes, inputs)
|
|
63
|
+
for _, input := range inputs {
|
|
64
|
+
if err := ctx.Err(); err != nil {
|
|
65
|
+
return cr.ResourceGraph{}, err
|
|
66
|
+
}
|
|
67
|
+
builder.ownerRelations(input.Object, input.TargetID)
|
|
68
|
+
definitionID := builder.node(input.Definition, "")
|
|
69
|
+
builder.edge(definitionID, input.TargetID, "defines")
|
|
70
|
+
}
|
|
71
|
+
if builder.err != nil {
|
|
72
|
+
return cr.ResourceGraph{}, builder.err
|
|
73
|
+
}
|
|
74
|
+
return mergeEnrichment(ctx, result, nodes, builder)
|
|
75
|
+
}
|
|
76
|
+
func cloneEnrichmentGraph(graph cr.ResourceGraph) cr.ResourceGraph {
|
|
77
|
+
graph.Nodes = append([]cr.ResourceGraphNode{}, graph.Nodes...)
|
|
78
|
+
graph.Edges = append([]cr.ResourceGraphEdge{}, graph.Edges...)
|
|
79
|
+
graph.Evidence = append([]cr.ResourceEvidence{}, graph.Evidence...)
|
|
80
|
+
graph.Warnings = append([]string{}, graph.Warnings...)
|
|
81
|
+
return graph
|
|
82
|
+
}
|
|
83
|
+
func enrichmentNodes(graph cr.ResourceGraph) (map[string]cr.ResourceGraphNode, error) {
|
|
84
|
+
nodes := map[string]cr.ResourceGraphNode{}
|
|
85
|
+
for _, node := range graph.Nodes {
|
|
86
|
+
if node.ID == "" || node.Resource.ClusterID != graph.ClusterID {
|
|
87
|
+
return nil, invalidGraphEnrichment("node identity or cluster mismatch")
|
|
88
|
+
}
|
|
89
|
+
if _, exists := nodes[node.ID]; exists {
|
|
90
|
+
return nil, invalidGraphEnrichment("duplicate node ID")
|
|
91
|
+
}
|
|
92
|
+
nodes[node.ID] = node
|
|
93
|
+
}
|
|
94
|
+
if graph.ClusterID == "" || graph.RootID == "" {
|
|
95
|
+
return nil, invalidGraphEnrichment("graph requires cluster and root")
|
|
96
|
+
}
|
|
97
|
+
if _, ok := nodes[graph.RootID]; !ok {
|
|
98
|
+
return nil, invalidGraphEnrichment("root not present")
|
|
99
|
+
}
|
|
100
|
+
return nodes, nil
|
|
101
|
+
}
|
|
102
|
+
func validateEnrichments(ctx context.Context, cluster string, nodes map[string]cr.ResourceGraphNode, inputs []ResourceGraphEnrichment) ([]ResourceGraphEnrichment, error) {
|
|
103
|
+
byID := map[string]ResourceGraphEnrichment{}
|
|
104
|
+
for _, input := range inputs {
|
|
105
|
+
if err := ctx.Err(); err != nil {
|
|
106
|
+
return nil, err
|
|
107
|
+
}
|
|
108
|
+
node, exists := nodes[input.TargetID]
|
|
109
|
+
if !exists {
|
|
110
|
+
return nil, invalidGraphEnrichment("target not present")
|
|
111
|
+
}
|
|
112
|
+
if err := validateEnrichment(cluster, node, input); err != nil {
|
|
113
|
+
return nil, err
|
|
114
|
+
}
|
|
115
|
+
if previous, exists := byID[input.TargetID]; exists {
|
|
116
|
+
if !sameEnrichment(previous, input) {
|
|
117
|
+
return nil, invalidGraphEnrichment("conflicting duplicate observation")
|
|
118
|
+
}
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
byID[input.TargetID] = input
|
|
122
|
+
}
|
|
123
|
+
ids := make([]string, 0, len(byID))
|
|
124
|
+
for id := range byID {
|
|
125
|
+
ids = append(ids, id)
|
|
126
|
+
}
|
|
127
|
+
sort.Strings(ids)
|
|
128
|
+
result := make([]ResourceGraphEnrichment, 0, len(ids))
|
|
129
|
+
for _, id := range ids {
|
|
130
|
+
result = append(result, byID[id])
|
|
131
|
+
}
|
|
132
|
+
return result, nil
|
|
133
|
+
}
|
|
134
|
+
func validateEnrichment(cluster string, node cr.ResourceGraphNode, input ResourceGraphEnrichment) error {
|
|
135
|
+
o := input.Object
|
|
136
|
+
if o == nil {
|
|
137
|
+
return invalidGraphEnrichment("object required")
|
|
138
|
+
}
|
|
139
|
+
if graphBuiltinSpec(o) {
|
|
140
|
+
return invalidGraphEnrichment("built-in GVK is not a custom instance")
|
|
141
|
+
}
|
|
142
|
+
expected := node.Resource
|
|
143
|
+
if expected.APIVersion != o.GetAPIVersion() || expected.Kind != o.GetKind() || expected.Name != o.GetName() {
|
|
144
|
+
return invalidGraphEnrichment("GVK or name mismatch")
|
|
145
|
+
}
|
|
146
|
+
if expected.UID == "" || string(o.GetUID()) != expected.UID {
|
|
147
|
+
return invalidGraphEnrichment("UID mismatch")
|
|
148
|
+
}
|
|
149
|
+
if o.GetNamespace() != expected.Namespace && o.GetNamespace() != "" {
|
|
150
|
+
return invalidGraphEnrichment("namespace mismatch")
|
|
151
|
+
}
|
|
152
|
+
if node.Status != "unresolved" && o.GetNamespace() != expected.Namespace {
|
|
153
|
+
return invalidGraphEnrichment("resolved scope changed")
|
|
154
|
+
}
|
|
155
|
+
return validateEnrichmentDefinition(cluster, o.GetAPIVersion(), input.Definition)
|
|
156
|
+
}
|
|
157
|
+
func validateEnrichmentDefinition(cluster, api string, definition cr.ResourceRef) error {
|
|
158
|
+
group, _, ok := strings.Cut(api, "/")
|
|
159
|
+
if !ok || group == "" {
|
|
160
|
+
return invalidGraphEnrichment("custom API group required")
|
|
161
|
+
}
|
|
162
|
+
if definition.ClusterID != cluster || definition.APIVersion != "apiextensions.k8s.io/v1" || definition.Kind != "CustomResourceDefinition" {
|
|
163
|
+
return invalidGraphEnrichment("definition identity mismatch")
|
|
164
|
+
}
|
|
165
|
+
if definition.Namespace != "" || definition.ScopeMode != cr.ResourceScopeModeCluster || definition.UID == "" {
|
|
166
|
+
return invalidGraphEnrichment("definition requires observed cluster identity")
|
|
167
|
+
}
|
|
168
|
+
suffix := "." + group
|
|
169
|
+
if !strings.HasSuffix(definition.Name, suffix) || len(definition.Name) <= len(suffix) {
|
|
170
|
+
return invalidGraphEnrichment("definition group mismatch")
|
|
171
|
+
}
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
func sameEnrichment(a, b ResourceGraphEnrichment) bool {
|
|
175
|
+
return a.Definition == b.Definition && a.Object.GetNamespace() == b.Object.GetNamespace() && reflect.DeepEqual(a.Object.GetOwnerReferences(), b.Object.GetOwnerReferences())
|
|
176
|
+
}
|
|
177
|
+
func enrichmentRef(cluster string, o *unstructured.Unstructured) cr.ResourceRef {
|
|
178
|
+
scope := cr.ResourceScopeModeNamespace
|
|
179
|
+
if o.GetNamespace() == "" {
|
|
180
|
+
scope = cr.ResourceScopeModeCluster
|
|
181
|
+
}
|
|
182
|
+
return cr.ResourceRef{ClusterID: cluster, APIVersion: o.GetAPIVersion(), Kind: o.GetKind(), Name: o.GetName(), Namespace: o.GetNamespace(), ScopeMode: scope, UID: string(o.GetUID()), ResourceVersion: o.GetResourceVersion()}
|
|
183
|
+
}
|
|
184
|
+
func enrichmentBuilder(ctx context.Context, cluster string, nodes map[string]cr.ResourceGraphNode, inputs []ResourceGraphEnrichment) *graphRules {
|
|
185
|
+
objects := map[string]*unstructured.Unstructured{}
|
|
186
|
+
for _, node := range nodes {
|
|
187
|
+
ref := node.Resource
|
|
188
|
+
if node.Status == "unresolved" || ref.UID == "" {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
o := &unstructured.Unstructured{}
|
|
192
|
+
o.SetAPIVersion(ref.APIVersion)
|
|
193
|
+
o.SetKind(ref.Kind)
|
|
194
|
+
o.SetName(ref.Name)
|
|
195
|
+
o.SetNamespace(ref.Namespace)
|
|
196
|
+
o.SetUID(types.UID(ref.UID))
|
|
197
|
+
objects[graphIdentity(ref.APIVersion, ref.Kind, ref.Namespace, ref.Name)] = o
|
|
198
|
+
}
|
|
199
|
+
for _, input := range inputs {
|
|
200
|
+
o := input.Object
|
|
201
|
+
objects[graphIdentity(o.GetAPIVersion(), o.GetKind(), o.GetNamespace(), o.GetName())] = o
|
|
202
|
+
}
|
|
203
|
+
return &graphRules{ctx: ctx, cluster: cluster, objects: objects, nodes: map[string]cr.ResourceGraphNode{}, edges: map[string]cr.ResourceGraphEdge{}}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func mergeEnrichment(ctx context.Context, result cr.ResourceGraph, nodes map[string]cr.ResourceGraphNode, builder *graphRules) (cr.ResourceGraph, error) {
|
|
207
|
+
aliases, candidates, err := enrichmentCandidates(nodes, builder.nodes)
|
|
208
|
+
if err != nil {
|
|
209
|
+
return cr.ResourceGraph{}, err
|
|
210
|
+
}
|
|
211
|
+
existingEdges := map[string]bool{}
|
|
212
|
+
edges := append([]cr.ResourceGraphEdge{}, result.Edges...)
|
|
213
|
+
for _, edge := range result.Edges {
|
|
214
|
+
existingEdges[edge.Relation+":"+edge.SourceID+":"+edge.TargetID] = true
|
|
215
|
+
}
|
|
216
|
+
generated := make([]cr.ResourceGraphEdge, 0, len(builder.edges))
|
|
217
|
+
for _, edge := range builder.edges {
|
|
218
|
+
if alias, ok := aliases[edge.SourceID]; ok {
|
|
219
|
+
edge.SourceID = alias
|
|
220
|
+
}
|
|
221
|
+
if alias, ok := aliases[edge.TargetID]; ok {
|
|
222
|
+
edge.TargetID = alias
|
|
223
|
+
}
|
|
224
|
+
if edge.SourceID == edge.TargetID {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
edge.ID = edge.Relation + ":" + edge.SourceID + ":" + edge.TargetID
|
|
228
|
+
generated = append(generated, edge)
|
|
229
|
+
}
|
|
230
|
+
sort.Slice(generated, func(i, j int) bool { return generated[i].ID < generated[j].ID })
|
|
231
|
+
edges = append(edges, generated...)
|
|
232
|
+
sort.Slice(edges, func(i, j int) bool { return edges[i].ID < edges[j].ID })
|
|
233
|
+
selected, err := graphTraverse(ctx, result.RootID, edges)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return cr.ResourceGraph{}, err
|
|
236
|
+
}
|
|
237
|
+
if err := includeRootScaleDefinitions(ctx, result, nodes, generated, selected); err != nil {
|
|
238
|
+
return cr.ResourceGraph{}, err
|
|
239
|
+
}
|
|
240
|
+
acceptedEdges := append([]cr.ResourceGraphEdge{}, result.Edges...)
|
|
241
|
+
partial := builder.partial
|
|
242
|
+
for _, edge := range generated {
|
|
243
|
+
if err := ctx.Err(); err != nil {
|
|
244
|
+
return cr.ResourceGraph{}, err
|
|
245
|
+
}
|
|
246
|
+
if existingEdges[edge.ID] {
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
if !selected.chosen[edge.ID] {
|
|
250
|
+
partial = true
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
if !includeEnrichmentEdge(edge, nodes, candidates, &acceptedEdges) {
|
|
254
|
+
partial = true
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
existingEdges[edge.Relation+":"+edge.SourceID+":"+edge.TargetID] = true
|
|
258
|
+
}
|
|
259
|
+
result.Nodes = result.Nodes[:0]
|
|
260
|
+
for _, node := range nodes {
|
|
261
|
+
result.Nodes = append(result.Nodes, node)
|
|
262
|
+
}
|
|
263
|
+
result.Edges = acceptedEdges
|
|
264
|
+
sort.Slice(result.Nodes, func(i, j int) bool { return result.Nodes[i].ID < result.Nodes[j].ID })
|
|
265
|
+
sort.Slice(result.Edges, func(i, j int) bool { return result.Edges[i].ID < result.Edges[j].ID })
|
|
266
|
+
if partial || selected.depthTruncated || selected.limitTruncated {
|
|
267
|
+
result.Warnings = appendUniqueWarning(result.Warnings, "resource graph enrichment is partial due to depth or node/edge budget")
|
|
268
|
+
}
|
|
269
|
+
return result, ctx.Err()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A direct HPA scale target also exposes its observed definition, without
|
|
273
|
+
// reversing general traversal or following the definition's other instances.
|
|
274
|
+
func includeRootScaleDefinitions(ctx context.Context, graph cr.ResourceGraph, nodes map[string]cr.ResourceGraphNode, generated []cr.ResourceGraphEdge, selected *graphTraversal) error {
|
|
275
|
+
root := nodes[graph.RootID].Resource
|
|
276
|
+
if root.APIVersion != "autoscaling/v2" || root.Kind != "HorizontalPodAutoscaler" || root.Namespace == "" {
|
|
277
|
+
return nil
|
|
278
|
+
}
|
|
279
|
+
if root.ScopeMode != "" && root.ScopeMode != cr.ResourceScopeModeNamespace {
|
|
280
|
+
return nil
|
|
281
|
+
}
|
|
282
|
+
targets := map[string]bool{}
|
|
283
|
+
for _, edge := range graph.Edges {
|
|
284
|
+
if err := ctx.Err(); err != nil {
|
|
285
|
+
return err
|
|
286
|
+
}
|
|
287
|
+
if edge.Relation == "scales" && edge.SourceID == graph.RootID && selected.chosen[edge.ID] && nodes[edge.TargetID].Resource.Namespace == root.Namespace {
|
|
288
|
+
targets[edge.TargetID] = true
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for _, edge := range generated {
|
|
292
|
+
if err := ctx.Err(); err != nil {
|
|
293
|
+
return err
|
|
294
|
+
}
|
|
295
|
+
if edge.Relation == "defines" && targets[edge.TargetID] {
|
|
296
|
+
selected.include(edge, graphTraversalState{id: edge.SourceID, depth: 2, direction: -1})
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return nil
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func enrichmentCandidates(existing, generated map[string]cr.ResourceGraphNode) (map[string]string, map[string]cr.ResourceGraphNode, error) {
|
|
303
|
+
aliases := map[string]string{}
|
|
304
|
+
candidates := map[string]cr.ResourceGraphNode{}
|
|
305
|
+
// Sorted existing IDs make alias resolution stable for opaque IDs.
|
|
306
|
+
ids := make([]string, 0, len(existing))
|
|
307
|
+
for id := range existing {
|
|
308
|
+
ids = append(ids, id)
|
|
309
|
+
}
|
|
310
|
+
sort.Strings(ids)
|
|
311
|
+
for _, id := range ids {
|
|
312
|
+
key := graphNodeID(existing[id].Resource)
|
|
313
|
+
if _, found := aliases[key]; !found {
|
|
314
|
+
aliases[key] = id
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for id, node := range generated {
|
|
318
|
+
if _, found := aliases[id]; found {
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
if current, collision := existing[id]; collision && graphNodeID(current.Resource) != graphNodeID(node.Resource) {
|
|
322
|
+
return nil, nil, invalidGraphEnrichment("opaque node ID collision")
|
|
323
|
+
}
|
|
324
|
+
candidates[id] = node
|
|
325
|
+
}
|
|
326
|
+
return aliases, candidates, nil
|
|
327
|
+
}
|
|
328
|
+
func includeEnrichmentEdge(edge cr.ResourceGraphEdge, nodes, candidates map[string]cr.ResourceGraphNode, edges *[]cr.ResourceGraphEdge) bool {
|
|
329
|
+
missing := []cr.ResourceGraphNode{}
|
|
330
|
+
for _, id := range []string{edge.SourceID, edge.TargetID} {
|
|
331
|
+
if _, exists := nodes[id]; exists {
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
node, ok := candidates[id]
|
|
335
|
+
if !ok {
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
missing = append(missing, node)
|
|
339
|
+
}
|
|
340
|
+
if len(nodes)+len(missing) > ResourceGraphNodeLimit || len(*edges) >= ResourceGraphEdgeLimit {
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
for _, node := range missing {
|
|
344
|
+
nodes[node.ID] = node
|
|
345
|
+
}
|
|
346
|
+
*edges = append(*edges, edge)
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
func appendUniqueWarning(warnings []string, message string) []string {
|
|
350
|
+
for _, existing := range warnings {
|
|
351
|
+
if existing == message {
|
|
352
|
+
return warnings
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return append(warnings, message)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ResourceGraphMetadataEnrichment keeps Kubernetes SDK types out of callers.
|
|
359
|
+
type ResourceGraphMetadataEnrichment struct {
|
|
360
|
+
TargetID string
|
|
361
|
+
Content []byte
|
|
362
|
+
Definition cr.ResourceRef
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const ResourceGraphMetadataEnrichmentLimit = 16
|
|
366
|
+
const ResourceGraphMetadataJSONLimit = 1 << 20
|
|
367
|
+
|
|
368
|
+
// EnrichResourceGraphFromMetadataJSON decodes only the relationship metadata
|
|
369
|
+
// fields. All other JSON, including spec/data/annotations, is ignored.
|
|
370
|
+
func EnrichResourceGraphFromMetadataJSON(ctx context.Context, graph cr.ResourceGraph, inputs []ResourceGraphMetadataEnrichment) (cr.ResourceGraph, error) {
|
|
371
|
+
ctx, cancel := context.WithTimeout(ctx, ResourceGraphTimeout)
|
|
372
|
+
defer cancel()
|
|
373
|
+
if err := ctx.Err(); err != nil {
|
|
374
|
+
return cr.ResourceGraph{}, err
|
|
375
|
+
}
|
|
376
|
+
if len(inputs) > ResourceGraphMetadataEnrichmentLimit {
|
|
377
|
+
return cr.ResourceGraph{}, ErrResourceGraphBudget
|
|
378
|
+
}
|
|
379
|
+
objects := make([]ResourceGraphEnrichment, 0, len(inputs))
|
|
380
|
+
for _, input := range inputs {
|
|
381
|
+
if err := ctx.Err(); err != nil {
|
|
382
|
+
return cr.ResourceGraph{}, err
|
|
383
|
+
}
|
|
384
|
+
if len(input.Content) > ResourceGraphMetadataJSONLimit {
|
|
385
|
+
return cr.ResourceGraph{}, ErrResourceGraphBudget
|
|
386
|
+
}
|
|
387
|
+
object, err := decodeEnrichmentMetadata(input.Content)
|
|
388
|
+
if err != nil {
|
|
389
|
+
return cr.ResourceGraph{}, err
|
|
390
|
+
}
|
|
391
|
+
objects = append(objects, ResourceGraphEnrichment{TargetID: input.TargetID, Object: object, Definition: input.Definition})
|
|
392
|
+
}
|
|
393
|
+
return EnrichResourceGraph(ctx, graph, objects)
|
|
394
|
+
}
|
|
395
|
+
func decodeEnrichmentMetadata(content []byte) (*unstructured.Unstructured, error) {
|
|
396
|
+
var decoded struct {
|
|
397
|
+
APIVersion string `json:"apiVersion"`
|
|
398
|
+
Kind string `json:"kind"`
|
|
399
|
+
Metadata struct {
|
|
400
|
+
Name string `json:"name"`
|
|
401
|
+
Namespace string `json:"namespace"`
|
|
402
|
+
UID types.UID `json:"uid"`
|
|
403
|
+
OwnerReferences []metav1.OwnerReference `json:"ownerReferences"`
|
|
404
|
+
} `json:"metadata"`
|
|
405
|
+
}
|
|
406
|
+
if json.Unmarshal(content, &decoded) != nil {
|
|
407
|
+
return nil, invalidGraphEnrichment("invalid metadata JSON")
|
|
408
|
+
}
|
|
409
|
+
object := &unstructured.Unstructured{}
|
|
410
|
+
object.SetAPIVersion(decoded.APIVersion)
|
|
411
|
+
object.SetKind(decoded.Kind)
|
|
412
|
+
object.SetName(decoded.Metadata.Name)
|
|
413
|
+
object.SetNamespace(decoded.Metadata.Namespace)
|
|
414
|
+
object.SetUID(decoded.Metadata.UID)
|
|
415
|
+
object.SetOwnerReferences(decoded.Metadata.OwnerReferences)
|
|
416
|
+
return object, nil
|
|
417
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func graphHPAScaleEnrichmentFixture() (cr.ResourceGraph, ResourceGraphEnrichment) {
|
|
13
|
+
graph, input := graphEnrichmentFixture()
|
|
14
|
+
graph.Nodes[0].Resource.APIVersion = "autoscaling/v2"
|
|
15
|
+
graph.Nodes[0].Resource.Kind = "HorizontalPodAutoscaler"
|
|
16
|
+
graph.Edges[0].SourceID, graph.Edges[0].TargetID = graph.RootID, input.TargetID
|
|
17
|
+
graph.Edges[0].Relation = "scales"
|
|
18
|
+
return graph, input
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func TestResourceGraphEnrichmentHPADirectScaleDefinition(t *testing.T) {
|
|
22
|
+
graph, input := graphHPAScaleEnrichmentFixture()
|
|
23
|
+
result, err := EnrichResourceGraph(context.Background(), graph, []ResourceGraphEnrichment{input})
|
|
24
|
+
if err != nil || len(result.Nodes) != 3 || len(result.Edges) != 2 || !graphTestEdge(result, "defines", "widgets.custom.example", "owner") {
|
|
25
|
+
t.Fatalf("direct scale definition missing: graph=%+v err=%v", result, err)
|
|
26
|
+
}
|
|
27
|
+
if strings.Contains(strings.Join(result.Warnings, " "), "enrichment is partial") {
|
|
28
|
+
t.Fatalf("complete definition reported partial: %+v", result.Warnings)
|
|
29
|
+
}
|
|
30
|
+
if result.Edges[0] != graph.Edges[0] && result.Edges[1] != graph.Edges[0] {
|
|
31
|
+
t.Fatal("opaque scales edge changed")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func TestResourceGraphEnrichmentHPADefinitionAdjacencyIsLimited(t *testing.T) {
|
|
36
|
+
for _, tc := range []struct {
|
|
37
|
+
name string
|
|
38
|
+
edit func(*cr.ResourceGraph, *ResourceGraphEnrichment)
|
|
39
|
+
}{
|
|
40
|
+
{"other source kind", func(g *cr.ResourceGraph, _ *ResourceGraphEnrichment) {
|
|
41
|
+
g.Nodes[0].Resource.APIVersion, g.Nodes[0].Resource.Kind = "v1", "Pod"
|
|
42
|
+
}},
|
|
43
|
+
{"custom HPA source", func(g *cr.ResourceGraph, _ *ResourceGraphEnrichment) {
|
|
44
|
+
g.Nodes[0].Resource.APIVersion = "custom.example/v1"
|
|
45
|
+
}},
|
|
46
|
+
{"invalid root scope", func(g *cr.ResourceGraph, _ *ResourceGraphEnrichment) {
|
|
47
|
+
g.Nodes[0].Resource.ScopeMode = cr.ResourceScopeModeCluster
|
|
48
|
+
}},
|
|
49
|
+
{"cross namespace target", func(g *cr.ResourceGraph, e *ResourceGraphEnrichment) {
|
|
50
|
+
g.Nodes[1].Resource.Namespace = "other"
|
|
51
|
+
e.Object.SetNamespace("other")
|
|
52
|
+
}},
|
|
53
|
+
{"indirect scale target", func(g *cr.ResourceGraph, _ *ResourceGraphEnrichment) {
|
|
54
|
+
middle := g.Nodes[0]
|
|
55
|
+
middle.ID, middle.Resource.APIVersion, middle.Resource.Kind, middle.Resource.Name = "middle", "v1", "Pod", "middle"
|
|
56
|
+
g.Nodes = append(g.Nodes, middle)
|
|
57
|
+
g.Edges[0].SourceID = "middle"
|
|
58
|
+
g.Edges = append(g.Edges, cr.ResourceGraphEdge{ID: "root-middle", SourceID: g.RootID, TargetID: "middle", Relation: "uses-configmap"})
|
|
59
|
+
}},
|
|
60
|
+
} {
|
|
61
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
62
|
+
graph, input := graphHPAScaleEnrichmentFixture()
|
|
63
|
+
tc.edit(&graph, &input)
|
|
64
|
+
result, err := EnrichResourceGraph(context.Background(), graph, []ResourceGraphEnrichment{input})
|
|
65
|
+
if err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
if graphTestHas(result, "widgets.custom.example") || graphTestEdge(result, "defines", "widgets.custom.example", "owner") {
|
|
69
|
+
t.Fatalf("definition adjacency expanded outside direct namespaced HPA target: %+v", result)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func TestResourceGraphEnrichmentHPADefinitionDoesNotFanOut(t *testing.T) {
|
|
76
|
+
graph, input := graphHPAScaleEnrichmentFixture()
|
|
77
|
+
other := graphTestObject("custom.example/v1", "Widget", "other-instance", "other-uid")
|
|
78
|
+
graph.Nodes = append(graph.Nodes, cr.ResourceGraphNode{ID: "unrelated-instance", Resource: enrichmentRef(graph.ClusterID, other), Status: "unresolved"})
|
|
79
|
+
unrelated := ResourceGraphEnrichment{TargetID: "unrelated-instance", Object: other, Definition: input.Definition}
|
|
80
|
+
result, err := EnrichResourceGraph(context.Background(), graph, []ResourceGraphEnrichment{input, unrelated})
|
|
81
|
+
if err != nil {
|
|
82
|
+
t.Fatal(err)
|
|
83
|
+
}
|
|
84
|
+
if !graphTestEdge(result, "defines", "widgets.custom.example", "owner") {
|
|
85
|
+
t.Fatal("root's direct scale definition missing")
|
|
86
|
+
}
|
|
87
|
+
if graphTestEdge(result, "defines", "widgets.custom.example", "other-instance") {
|
|
88
|
+
t.Fatalf("definition expanded to an unrelated observed instance: %+v", result)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestResourceGraphEnrichmentHPADefinitionRespectsOutputBudgets(t *testing.T) {
|
|
93
|
+
for _, budget := range []string{"nodes", "edges"} {
|
|
94
|
+
t.Run(budget, func(t *testing.T) {
|
|
95
|
+
graph, input := graphHPAScaleEnrichmentFixture()
|
|
96
|
+
if budget == "nodes" {
|
|
97
|
+
for len(graph.Nodes) < ResourceGraphNodeLimit {
|
|
98
|
+
id := fmt.Sprintf("unrelated-%03d", len(graph.Nodes))
|
|
99
|
+
graph.Nodes = append(graph.Nodes, cr.ResourceGraphNode{ID: id, Resource: cr.ResourceRef{ClusterID: graph.ClusterID, APIVersion: "v1", Kind: "Pod", Name: id, Namespace: "ns", ScopeMode: cr.ResourceScopeModeNamespace}})
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
for len(graph.Edges) < ResourceGraphEdgeLimit {
|
|
103
|
+
edge := graph.Edges[0]
|
|
104
|
+
edge.ID = fmt.Sprintf("opaque-scale-%04d", len(graph.Edges))
|
|
105
|
+
graph.Edges = append(graph.Edges, edge)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
result, err := EnrichResourceGraph(context.Background(), graph, []ResourceGraphEnrichment{input})
|
|
109
|
+
if err != nil || len(result.Nodes) > ResourceGraphNodeLimit || len(result.Edges) > ResourceGraphEdgeLimit {
|
|
110
|
+
t.Fatalf("budget overflow: nodes=%d edges=%d err=%v", len(result.Nodes), len(result.Edges), err)
|
|
111
|
+
}
|
|
112
|
+
if graphTestHas(result, "widgets.custom.example") || !strings.Contains(strings.Join(result.Warnings, " "), "enrichment is partial") {
|
|
113
|
+
t.Fatalf("definition bypassed exhausted %s budget: %+v", budget, result)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|