@opensoha/contracts 0.1.25 → 0.1.27
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 +34 -0
- package/README.md +1 -1
- package/agent/environment/export.go +1 -0
- package/agent/environment/protocol_test.go +1 -1
- package/agent/guest/protocol_test.go +2 -2
- package/auth/catalog_generated.go +2 -2
- package/auth/permission-catalog.json +481 -1
- package/compat/json-schema-ecosystem-0.1.0.json +1 -1
- package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
- package/delivery/document.schema.json +127 -9
- 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/examples/relations_compat_test.go +18 -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/DeliveryDocument/invalid/migration-missing-guide-build-template.json +30 -0
- package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-deployment-template.json +30 -0
- package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-workflow-template.json +27 -0
- package/fixtures/openapi/DeliveryDocument/valid/migration-build-template.json +31 -0
- package/fixtures/openapi/DeliveryDocument/valid/migration-deployment-template.json +31 -0
- package/fixtures/openapi/DeliveryDocument/valid/migration-workflow-template.json +28 -0
- package/fixtures/openapi/DeliveryPlanInput/invalid/manual-plan.json +7 -0
- package/fixtures/openapi/DeliveryPlanInput/valid/manual-plan.json +6 -0
- package/fixtures/openapi/DeliveryTargetInput/invalid/candidate-on-runtime-deploy.json +9 -0
- package/fixtures/openapi/DeliveryTargetInput/invalid/config-update-missing-service.json +7 -0
- package/fixtures/openapi/DeliveryTargetInput/valid/template-configuration-only.json +8 -0
- package/fixtures/openapi/DeliveryTargetInput/valid/template-configuration-without-runtime-environment.json +1 -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/KubernetesImportCandidateListEnvelope/invalid/source-with-spec.json +33 -0
- package/fixtures/openapi/KubernetesImportCandidateListEnvelope/invalid/source-without-uid.json +29 -0
- package/fixtures/openapi/KubernetesImportCandidateListEnvelope/valid/stable-source.json +30 -0
- package/fixtures/openapi/KubernetesPod/invalid/pod-uid.json +9 -0
- package/fixtures/openapi/KubernetesPod/valid/pod-uid.json +9 -0
- package/fixtures/openapi/KubernetesPodDetail/invalid/pod-uid.json +6 -0
- package/fixtures/openapi/KubernetesPodDetail/valid/pod-uid.json +6 -0
- package/fixtures/openapi/KubernetesServiceImportRequest/invalid/empty-source-uid.json +48 -0
- package/fixtures/openapi/KubernetesServiceImportRequest/valid/pinned-workloads-and-cr.json +48 -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/ManifestExecutionTaskPayload/invalid/initialization.json +7 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/valid/initialization.json +7 -0
- package/fixtures/openapi/ManifestRootResource/invalid/root.json +1 -0
- package/fixtures/openapi/ManifestRootResource/valid/root.json +1 -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/fixtures/openapi/ResourceRelationConfig/invalid/executable-rule.json +17 -0
- package/fixtures/openapi/ResourceRelationConfig/valid/unknown-kind.json +13 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/migration-missing-guide-valid-yaml.json +38 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/migration-valid-yaml.json +39 -0
- package/fixtures/openapi/TemplateMigrationAssessment/invalid/initialization.json +9 -0
- package/fixtures/openapi/TemplateMigrationAssessment/valid/initialization.json +9 -0
- package/fixtures/openapi/TemplateMigrationPolicy/invalid/blank-guide.json +5 -0
- package/fixtures/openapi/TemplateMigrationPolicy/invalid/missing-guide.json +4 -0
- package/fixtures/openapi/TemplateMigrationPolicy/invalid/unknown-escape-hatch.json +6 -0
- package/fixtures/openapi/TemplateMigrationPolicy/valid/legacy-safe.json +4 -0
- package/fixtures/openapi/TemplateMigrationPolicy/valid/ordered-data-change.json +5 -0
- package/fixtures/openapi/TemplateUpdateBatchInput/invalid/duplicate-candidates.json +11 -0
- package/fixtures/openapi/TemplateUpdateBatchInput/invalid/empty-identities.json +10 -0
- package/fixtures/openapi/TemplateUpdateBatchInput/valid/staged-adoption.json +10 -0
- package/fixtures/openapi/TemplateUpdatePolicyInput/invalid/unknown-mode.json +8 -0
- package/fixtures/openapi/TemplateUpdatePolicyInput/valid/explicit-automatic.json +8 -0
- package/fixtures/openapi/TemplateUpdatePreviewInput/invalid/empty-identities.json +10 -0
- package/fixtures/openapi/TemplateUpdatePreviewInput/invalid/empty-targets.json +6 -0
- package/fixtures/openapi/TemplateUpdatePreviewInput/valid/cross-version-manual.json +14 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/client_test.go +2 -2
- package/gen/go/sohaapi/import_source_compat_test.go +12 -0
- package/gen/go/sohaapi/proxy_configuration_test.go +38 -0
- package/gen/go/sohaapi/types.go +3760 -345
- package/gen/ts/sohaapi/index.ts +5573 -181
- package/go.mod +2 -2
- package/helmrelease/delivery.go +4 -1
- package/helmrelease/initialization.go +10 -0
- package/helmrelease/runtime/delivery.go +40 -10
- package/helmrelease/runtime/delivery_test.go +4 -0
- package/helmrelease/runtime/initialization.go +186 -0
- package/helmrelease/runtime/initialization_test.go +203 -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 +53524 -43459
- package/openapi/soha-api.yaml +26431 -20491
- package/openapi/spec_generated.go +3 -3
- package/openapi/spec_test.go +42 -0
- 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/models.go +2 -0
- package/resource/relations.go +225 -0
- package/resource/relations_test.go +47 -0
- package/resource/root_cr.go +38 -0
- package/resource/runtime/argocd_integration_test.go +1 -1
- package/resource/runtime/initialization.go +66 -0
- package/resource/runtime/initialization_test.go +80 -0
- package/resource/runtime/relations.go +161 -0
- package/resource/runtime/relations_test.go +100 -0
- package/resource/runtime/root_cr.go +79 -0
- package/resource/runtime/root_cr_test.go +118 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
package resource
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const RelationSnapshotFeature = "resource-relations.metadata.v1"
|
|
12
|
+
const RelationMaxQueries = 32
|
|
13
|
+
const RelationMaxItems = 200
|
|
14
|
+
const RelationMaxNodes = 500
|
|
15
|
+
const RelationMaxDepth = 8
|
|
16
|
+
|
|
17
|
+
var ErrRelationSnapshotUnsupported = errors.New("resource relation metadata snapshot unsupported")
|
|
18
|
+
|
|
19
|
+
func DefaultRelationSources(namespace string) []RelationSource {
|
|
20
|
+
return []RelationSource{
|
|
21
|
+
{APIVersion: "v1", Resource: "pods", Kind: "Pod", Namespace: namespace},
|
|
22
|
+
{APIVersion: "apps/v1", Resource: "deployments", Kind: "Deployment", Namespace: namespace},
|
|
23
|
+
{APIVersion: "apps/v1", Resource: "replicasets", Kind: "ReplicaSet", Namespace: namespace},
|
|
24
|
+
{APIVersion: "apps/v1", Resource: "statefulsets", Kind: "StatefulSet", Namespace: namespace},
|
|
25
|
+
{APIVersion: "apps/v1", Resource: "daemonsets", Kind: "DaemonSet", Namespace: namespace},
|
|
26
|
+
{APIVersion: "batch/v1", Resource: "jobs", Kind: "Job", Namespace: namespace},
|
|
27
|
+
{APIVersion: "batch/v1", Resource: "cronjobs", Kind: "CronJob", Namespace: namespace},
|
|
28
|
+
{APIVersion: "v1", Resource: "services", Kind: "Service", Namespace: namespace},
|
|
29
|
+
{APIVersion: "v1", Resource: "persistentvolumeclaims", Kind: "PersistentVolumeClaim", Namespace: namespace},
|
|
30
|
+
{APIVersion: "v1", Resource: "configmaps", Kind: "ConfigMap", Namespace: namespace},
|
|
31
|
+
{APIVersion: "v1", Resource: "secrets", Kind: "Secret", Namespace: namespace},
|
|
32
|
+
{APIVersion: "networking.k8s.io/v1", Resource: "ingresses", Kind: "Ingress", Namespace: namespace},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func IsBuiltinRelationSource(source RelationSource) bool {
|
|
37
|
+
for _, builtin := range DefaultRelationSources(source.Namespace) {
|
|
38
|
+
if builtin == source {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// RelationSource always names an explicit namespaced API collection. Discovery
|
|
46
|
+
// must verify the resource/kind pair before reading it.
|
|
47
|
+
type RelationSource struct {
|
|
48
|
+
APIVersion string `json:"apiVersion"`
|
|
49
|
+
Resource string `json:"resource"`
|
|
50
|
+
Kind string `json:"kind"`
|
|
51
|
+
Namespace string `json:"namespace"`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type RelationRule struct {
|
|
55
|
+
ID string `json:"id"`
|
|
56
|
+
Type string `json:"type"`
|
|
57
|
+
Target RelationSource `json:"target"`
|
|
58
|
+
Name string `json:"name,omitempty"`
|
|
59
|
+
UID string `json:"uid,omitempty"`
|
|
60
|
+
Selector map[string]string `json:"selector,omitempty"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type RelationConfig struct {
|
|
64
|
+
Version string `json:"version"`
|
|
65
|
+
RootUID string `json:"rootUid"`
|
|
66
|
+
Sources []RelationSource `json:"sources"`
|
|
67
|
+
Rules []RelationRule `json:"rules"`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type RelationQuery struct {
|
|
71
|
+
ID string `json:"id"`
|
|
72
|
+
Source RelationSource `json:"source"`
|
|
73
|
+
Name string `json:"name,omitempty"`
|
|
74
|
+
Selector map[string]string `json:"selector,omitempty"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type RelationSnapshotRequest struct {
|
|
78
|
+
Queries []RelationQuery `json:"queries"`
|
|
79
|
+
}
|
|
80
|
+
type RelationOwner struct {
|
|
81
|
+
APIVersion string `json:"apiVersion"`
|
|
82
|
+
Kind string `json:"kind"`
|
|
83
|
+
Name string `json:"name"`
|
|
84
|
+
UID string `json:"uid"`
|
|
85
|
+
Controller bool `json:"controller"`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type RelationObject struct {
|
|
89
|
+
Resource ResourceRef `json:"resource"`
|
|
90
|
+
Owners []RelationOwner `json:"owners,omitempty"`
|
|
91
|
+
}
|
|
92
|
+
type RelationCoverage struct {
|
|
93
|
+
QueryID string `json:"queryId"`
|
|
94
|
+
State string `json:"state"`
|
|
95
|
+
ReasonCode string `json:"reasonCode,omitempty"`
|
|
96
|
+
Truncated bool `json:"truncated"`
|
|
97
|
+
}
|
|
98
|
+
type RelationSourceResult struct {
|
|
99
|
+
Coverage RelationCoverage `json:"coverage"`
|
|
100
|
+
Objects []RelationObject `json:"objects"`
|
|
101
|
+
}
|
|
102
|
+
type RelationSnapshot struct {
|
|
103
|
+
Version string `json:"version"`
|
|
104
|
+
Sources []RelationSourceResult `json:"sources"`
|
|
105
|
+
}
|
|
106
|
+
type RelationNode struct {
|
|
107
|
+
Resource ResourceRef `json:"resource"`
|
|
108
|
+
Role string `json:"role"`
|
|
109
|
+
Depth int `json:"depth"`
|
|
110
|
+
}
|
|
111
|
+
type RelationEdge struct {
|
|
112
|
+
FromUID string `json:"fromUid"`
|
|
113
|
+
ToUID string `json:"toUid"`
|
|
114
|
+
Type string `json:"type"`
|
|
115
|
+
RuleID string `json:"ruleId,omitempty"`
|
|
116
|
+
}
|
|
117
|
+
type ResourceRelationsView struct {
|
|
118
|
+
Root ResourceRef `json:"root"`
|
|
119
|
+
Config RelationConfig `json:"config"`
|
|
120
|
+
Nodes []RelationNode `json:"nodes"`
|
|
121
|
+
Edges []RelationEdge `json:"edges"`
|
|
122
|
+
Coverage []RelationCoverage `json:"coverage"`
|
|
123
|
+
Partial bool `json:"partial"`
|
|
124
|
+
ObservedAt string `json:"observedAt"`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var relationDNS = regexp.MustCompile(`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$`)
|
|
128
|
+
var relationKind = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*$`)
|
|
129
|
+
var relationToken = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
|
|
130
|
+
var relationLabelKey = regexp.MustCompile(`^[a-zA-Z0-9_.-]+(/[a-zA-Z0-9_.-]+)?$`)
|
|
131
|
+
|
|
132
|
+
func ValidateRelationSource(s RelationSource) error {
|
|
133
|
+
parts := strings.Split(s.APIVersion, "/")
|
|
134
|
+
if len(parts) > 2 || len(parts) == 0 || len(s.APIVersion) > 320 {
|
|
135
|
+
return fmt.Errorf("invalid resource API version")
|
|
136
|
+
}
|
|
137
|
+
for _, part := range parts {
|
|
138
|
+
if !relationDNS.MatchString(part) {
|
|
139
|
+
return fmt.Errorf("invalid resource API version")
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if !relationDNS.MatchString(s.Resource) || len(s.Resource) > 63 || strings.Contains(s.Resource, ".") || !relationKind.MatchString(s.Kind) || len(s.Kind) > 128 {
|
|
143
|
+
return fmt.Errorf("invalid resource definition")
|
|
144
|
+
}
|
|
145
|
+
if !relationDNS.MatchString(s.Namespace) || strings.Contains(s.Namespace, ".") || len(s.Namespace) > 63 {
|
|
146
|
+
return fmt.Errorf("an explicit namespace is required")
|
|
147
|
+
}
|
|
148
|
+
return nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func validateRelationSelector(selector map[string]string) error {
|
|
152
|
+
if len(selector) > 16 {
|
|
153
|
+
return fmt.Errorf("too many label terms")
|
|
154
|
+
}
|
|
155
|
+
for key, value := range selector {
|
|
156
|
+
if len(key) > 317 || !relationLabelKey.MatchString(key) || len(value) > 63 || (value != "" && !relationToken.MatchString(value)) {
|
|
157
|
+
return fmt.Errorf("invalid label selector")
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return nil
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func ValidateRelationConfig(config RelationConfig) error {
|
|
164
|
+
if config.Sources == nil || config.Rules == nil {
|
|
165
|
+
return fmt.Errorf("sources and rules must be arrays")
|
|
166
|
+
}
|
|
167
|
+
data, err := json.Marshal(config)
|
|
168
|
+
if err != nil || len(data) > 16<<10 || config.Version != "v1" || strings.TrimSpace(config.RootUID) == "" || len(config.RootUID) > 128 || len(config.Sources) > 8 || len(config.Rules) > 8 {
|
|
169
|
+
return fmt.Errorf("invalid relation configuration or budget")
|
|
170
|
+
}
|
|
171
|
+
for _, source := range config.Sources {
|
|
172
|
+
if err := ValidateRelationSource(source); err != nil {
|
|
173
|
+
return err
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
seen := map[string]bool{}
|
|
177
|
+
for _, rule := range config.Rules {
|
|
178
|
+
if !relationToken.MatchString(rule.ID) || len(rule.ID) > 64 || seen[rule.ID] {
|
|
179
|
+
return fmt.Errorf("invalid or duplicate rule ID")
|
|
180
|
+
}
|
|
181
|
+
seen[rule.ID] = true
|
|
182
|
+
if err := ValidateRelationSource(rule.Target); err != nil {
|
|
183
|
+
return err
|
|
184
|
+
}
|
|
185
|
+
switch rule.Type {
|
|
186
|
+
case "reference", "manual":
|
|
187
|
+
if !relationDNS.MatchString(rule.Name) || len(rule.Name) > 253 || len(rule.Selector) != 0 || len(rule.UID) > 128 || (rule.Type == "manual" && strings.TrimSpace(rule.UID) == "") {
|
|
188
|
+
return fmt.Errorf("named manual relations require a fixed UID")
|
|
189
|
+
}
|
|
190
|
+
case "selector":
|
|
191
|
+
if rule.Name != "" || rule.UID != "" || len(rule.Selector) == 0 {
|
|
192
|
+
return fmt.Errorf("selector rules must declare labels only")
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return fmt.Errorf("unknown relation rule type")
|
|
196
|
+
}
|
|
197
|
+
if err := validateRelationSelector(rule.Selector); err != nil {
|
|
198
|
+
return err
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return nil
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func ValidateRelationSnapshotRequest(input RelationSnapshotRequest) error {
|
|
205
|
+
if len(input.Queries) == 0 || len(input.Queries) > RelationMaxQueries {
|
|
206
|
+
return fmt.Errorf("invalid relation query budget")
|
|
207
|
+
}
|
|
208
|
+
seen := map[string]bool{}
|
|
209
|
+
for _, q := range input.Queries {
|
|
210
|
+
if q.ID == "" || len(q.ID) > 80 || !relationToken.MatchString(q.ID) || seen[q.ID] {
|
|
211
|
+
return fmt.Errorf("invalid query identity")
|
|
212
|
+
}
|
|
213
|
+
seen[q.ID] = true
|
|
214
|
+
if err := ValidateRelationSource(q.Source); err != nil {
|
|
215
|
+
return err
|
|
216
|
+
}
|
|
217
|
+
if q.Name != "" && (!relationDNS.MatchString(q.Name) || len(q.Name) > 253 || len(q.Selector) != 0) {
|
|
218
|
+
return fmt.Errorf("invalid named query")
|
|
219
|
+
}
|
|
220
|
+
if err := validateRelationSelector(q.Selector); err != nil {
|
|
221
|
+
return err
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return nil
|
|
225
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package resource
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
func TestRelationConfigSupportsUnknownKindsWithoutCodeRegistration(t *testing.T) {
|
|
6
|
+
source := RelationSource{APIVersion: "widgets.example.test/v1", Resource: "widgets", Kind: "Widget", Namespace: "team"}
|
|
7
|
+
config := RelationConfig{Version: "v1", RootUID: "root", Sources: []RelationSource{source}, Rules: []RelationRule{{ID: "shared", Type: "reference", Target: source, Name: "runtime"}}}
|
|
8
|
+
if err := ValidateRelationConfig(config); err != nil {
|
|
9
|
+
t.Fatal(err)
|
|
10
|
+
}
|
|
11
|
+
config.Rules[0].Type = "manual"
|
|
12
|
+
if ValidateRelationConfig(config) == nil {
|
|
13
|
+
t.Fatal("manual association accepted without UID")
|
|
14
|
+
}
|
|
15
|
+
config.Rules[0].UID = "fixed-runtime"
|
|
16
|
+
if err := ValidateRelationConfig(config); err != nil {
|
|
17
|
+
t.Fatal(err)
|
|
18
|
+
}
|
|
19
|
+
config.Rules[0].Target.Resource = "widgets/../../secrets"
|
|
20
|
+
if ValidateRelationConfig(config) == nil {
|
|
21
|
+
t.Fatal("path traversal accepted")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestRelationQueryBoundsAndSelectors(t *testing.T) {
|
|
26
|
+
q := RelationQuery{ID: "pods", Source: RelationSource{APIVersion: "v1", Resource: "pods", Kind: "Pod", Namespace: "team"}, Selector: map[string]string{"app": "demo"}}
|
|
27
|
+
for _, test := range []struct {
|
|
28
|
+
name string
|
|
29
|
+
change func(*RelationQuery)
|
|
30
|
+
}{
|
|
31
|
+
{"all namespaces", func(q *RelationQuery) { q.Source.Namespace = "" }},
|
|
32
|
+
{"selector injection", func(q *RelationQuery) { q.Selector["app"] = "demo,secret=yes" }},
|
|
33
|
+
{"named selector", func(q *RelationQuery) { q.Name = "pod" }},
|
|
34
|
+
} {
|
|
35
|
+
t.Run(test.name, func(t *testing.T) {
|
|
36
|
+
copy := q
|
|
37
|
+
copy.Selector = map[string]string{"app": "demo"}
|
|
38
|
+
test.change(©)
|
|
39
|
+
if ValidateRelationSnapshotRequest(RelationSnapshotRequest{Queries: []RelationQuery{copy}}) == nil {
|
|
40
|
+
t.Fatal("invalid query accepted")
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
if ValidateRelationSnapshotRequest(RelationSnapshotRequest{Queries: []RelationQuery{q, q}}) == nil {
|
|
45
|
+
t.Fatal("duplicate query accepted")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package resource
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ValidateRootCRDocument prevents a root publication from changing runtime metadata
|
|
10
|
+
// or writing generated children. Operator reconciliation remains independent.
|
|
11
|
+
func ValidateRootCRDocument(object map[string]any, root sohaapi.ManifestRootResource) error {
|
|
12
|
+
metadata, ok := object["metadata"].(map[string]any)
|
|
13
|
+
if !ok {
|
|
14
|
+
return fmt.Errorf("root CR metadata is required")
|
|
15
|
+
}
|
|
16
|
+
if root.UID == "" || root.Generation < 1 || root.CrdName == "" || !strings.Contains(root.APIVersion, "/") || root.Namespace == "" {
|
|
17
|
+
return fmt.Errorf("root CR requires a frozen namespaced identity")
|
|
18
|
+
}
|
|
19
|
+
if object["apiVersion"] != root.APIVersion || object["kind"] != root.Kind || metadata["namespace"] != root.Namespace || metadata["name"] != root.Name {
|
|
20
|
+
return fmt.Errorf("root CR identity differs from the approved resource")
|
|
21
|
+
}
|
|
22
|
+
for key := range object {
|
|
23
|
+
switch key {
|
|
24
|
+
case "apiVersion", "kind", "metadata", "spec":
|
|
25
|
+
default:
|
|
26
|
+
return fmt.Errorf("root CR updates only support spec")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for key := range metadata {
|
|
30
|
+
if key != "name" && key != "namespace" {
|
|
31
|
+
return fmt.Errorf("root CR runtime metadata cannot be published")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if _, ok := object["spec"].(map[string]any); !ok {
|
|
35
|
+
return fmt.Errorf("root CR spec must be an object")
|
|
36
|
+
}
|
|
37
|
+
return nil
|
|
38
|
+
}
|
|
@@ -37,7 +37,7 @@ func TestArgoCDWithKubernetes(t *testing.T) {
|
|
|
37
37
|
operatorRequire(t, err)
|
|
38
38
|
kube, err := kubernetes.NewForConfig(config)
|
|
39
39
|
operatorRequire(t, err)
|
|
40
|
-
data, err := os.ReadFile(os.Getenv("SOHA_ARGOCD_TEST_STATE"))
|
|
40
|
+
data, err := os.ReadFile(os.Getenv("SOHA_ARGOCD_TEST_STATE")) // #nosec G703 -- Opt-in integration test reads the operator-selected local fixture path; no server request controls it.
|
|
41
41
|
operatorRequire(t, err)
|
|
42
42
|
var state map[string]json.RawMessage
|
|
43
43
|
operatorRequire(t, json.Unmarshal(data, &state))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
9
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
10
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
11
|
+
"k8s.io/client-go/dynamic"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const ManifestInitializationCapability = "manifest.initialize.v1"
|
|
15
|
+
const ManifestInitializationProviderPrefix = "manifest_initialize_v1."
|
|
16
|
+
|
|
17
|
+
// ValidateManifestInitialization fences the new mode from legacy/controller writes.
|
|
18
|
+
// Callers still own current authorization, immutable Plan validation and admission.
|
|
19
|
+
func ValidateManifestInitialization(p sohaapi.ManifestExecutionTaskPayload) error {
|
|
20
|
+
if p.InitializationPlanID == "" {
|
|
21
|
+
return nil
|
|
22
|
+
}
|
|
23
|
+
if strings.TrimSpace(p.InitializationPlanID) != p.InitializationPlanID || len(p.InitializationPlanID) > 128 || p.ForceConflicts || p.RootResource != nil || p.RolloutControl != nil || len(p.GitOpsDocuments) != 0 {
|
|
24
|
+
return fmt.Errorf("%w: invalid initialization boundary", ErrResourceOwnership)
|
|
25
|
+
}
|
|
26
|
+
if p.Action != sohaapi.ManifestTaskActionPreflight && p.Action != sohaapi.ManifestTaskActionApply && p.Action != sohaapi.ManifestTaskActionObserve {
|
|
27
|
+
return fmt.Errorf("%w: initialization requires preflight, apply or observation", ErrResourceOwnership)
|
|
28
|
+
}
|
|
29
|
+
if IsRolloutTask(p) {
|
|
30
|
+
return fmt.Errorf("%w: controller delivery cannot guarantee create-only initialization", ErrResourceOwnership)
|
|
31
|
+
}
|
|
32
|
+
for _, document := range p.Documents {
|
|
33
|
+
if initializationController(document.APIVersion, document.Kind) {
|
|
34
|
+
return fmt.Errorf("%w: controller delivery cannot guarantee create-only initialization", ErrResourceOwnership)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func initializationController(apiVersion, kind string) bool {
|
|
41
|
+
return strings.HasPrefix(apiVersion, "kustomize.toolkit.fluxcd.io/") || strings.HasPrefix(apiVersion, "helm.toolkit.fluxcd.io/") || strings.HasPrefix(apiVersion, "argoproj.io/") && (kind == "Application" || kind == "Rollout")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// CreateManifest never falls back to SSA or adopts a concurrent object. Kubernetes
|
|
45
|
+
// validates non-existence atomically for both dry-run and the actual POST.
|
|
46
|
+
func CreateManifest(ctx context.Context, client dynamic.ResourceInterface, desired *unstructured.Unstructured, options metav1.CreateOptions) (*unstructured.Unstructured, error) {
|
|
47
|
+
if desired.GetUID() != "" || desired.GetResourceVersion() != "" || desired.GetGenerateName() != "" {
|
|
48
|
+
return nil, fmt.Errorf("%w: initialization requires a new named resource", ErrResourceOwnership)
|
|
49
|
+
}
|
|
50
|
+
if initializationController(desired.GetAPIVersion(), desired.GetKind()) {
|
|
51
|
+
return nil, fmt.Errorf("%w: controller delivery requires its existing publication path", ErrResourceOwnership)
|
|
52
|
+
}
|
|
53
|
+
if err := ValidateDirectManifestOwner(desired); err != nil {
|
|
54
|
+
return nil, err
|
|
55
|
+
}
|
|
56
|
+
return client.Create(ctx, desired, options)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ValidateInitializedUID prevents a healthy same-name replacement from confirming
|
|
60
|
+
// the baseline of a different object created by the approved initialization.
|
|
61
|
+
func ValidateInitializedUID(live metav1.Object, expectedUID string) error {
|
|
62
|
+
if live == nil || expectedUID == "" || live.GetUID() == "" || string(live.GetUID()) != expectedUID {
|
|
63
|
+
return fmt.Errorf("%w: initialized resource identity is missing or changed", ErrResourceOwnership)
|
|
64
|
+
}
|
|
65
|
+
return nil
|
|
66
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
8
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
9
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
10
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
11
|
+
"k8s.io/apimachinery/pkg/runtime"
|
|
12
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
13
|
+
"k8s.io/client-go/dynamic/fake"
|
|
14
|
+
ktesting "k8s.io/client-go/testing"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func TestManifestInitializationCreatesWithoutAdoption(t *testing.T) {
|
|
18
|
+
object := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "settings", "namespace": "test"}}}
|
|
19
|
+
gvr := schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
|
|
20
|
+
for _, existing := range []bool{false, true} {
|
|
21
|
+
t.Run(map[bool]string{false: "new", true: "concurrently-created"}[existing], func(t *testing.T) {
|
|
22
|
+
client := fake.NewSimpleDynamicClient(runtime.NewScheme())
|
|
23
|
+
if existing {
|
|
24
|
+
if err := client.Tracker().Create(gvr, object.DeepCopy(), "test"); err != nil {
|
|
25
|
+
t.Fatal(err)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
_, err := CreateManifest(t.Context(), client.Resource(gvr).Namespace("test"), object.DeepCopy(), metav1.CreateOptions{})
|
|
29
|
+
if existing && !apierrors.IsAlreadyExists(err) || !existing && err != nil {
|
|
30
|
+
t.Fatalf("create existing=%v: %v", existing, err)
|
|
31
|
+
}
|
|
32
|
+
for _, action := range client.Actions() {
|
|
33
|
+
if action.GetVerb() != "create" {
|
|
34
|
+
t.Fatalf("initialization used %s instead of atomic create", action.GetVerb())
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
client := fake.NewSimpleDynamicClient(runtime.NewScheme())
|
|
40
|
+
client.PrependReactor("create", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
41
|
+
typed, ok := action.(interface{ GetCreateOptions() metav1.CreateOptions })
|
|
42
|
+
if !ok {
|
|
43
|
+
t.Fatalf("missing create options: %T", action)
|
|
44
|
+
}
|
|
45
|
+
if options := typed.GetCreateOptions(); len(options.DryRun) != 1 || options.DryRun[0] != metav1.DryRunAll {
|
|
46
|
+
t.Fatal("preflight lost server dry-run")
|
|
47
|
+
}
|
|
48
|
+
return true, object.DeepCopy(), nil
|
|
49
|
+
})
|
|
50
|
+
if _, err := CreateManifest(context.Background(), client.Resource(gvr).Namespace("test"), object, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}}); err != nil {
|
|
51
|
+
t.Fatal(err)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func TestManifestInitializationRejectsUnsafeModesAndReplacedUID(t *testing.T) {
|
|
56
|
+
p := sohaapi.ManifestExecutionTaskPayload{InitializationPlanID: "plan", Action: sohaapi.ManifestTaskActionApply}
|
|
57
|
+
if err := ValidateManifestInitialization(p); err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
for _, action := range []sohaapi.ManifestTaskAction{sohaapi.ManifestTaskActionRepair, sohaapi.ManifestTaskActionRollback, sohaapi.ManifestTaskActionAdopt} {
|
|
61
|
+
p.Action = action
|
|
62
|
+
if err := ValidateManifestInitialization(p); err == nil {
|
|
63
|
+
t.Fatalf("accepted initialization %s", action)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
p.Action, p.ForceConflicts = sohaapi.ManifestTaskActionApply, true
|
|
67
|
+
if ValidateManifestInitialization(p) == nil {
|
|
68
|
+
t.Fatal("initialization accepted forced conflicts")
|
|
69
|
+
}
|
|
70
|
+
live := &unstructured.Unstructured{}
|
|
71
|
+
live.SetUID("created-uid")
|
|
72
|
+
if err := ValidateInitializedUID(live, "created-uid"); err != nil {
|
|
73
|
+
t.Fatal(err)
|
|
74
|
+
}
|
|
75
|
+
for _, expected := range []string{"", "replaced-uid"} {
|
|
76
|
+
if ValidateInitializedUID(live, expected) == nil {
|
|
77
|
+
t.Fatal("unproven initialization UID was accepted")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"sort"
|
|
10
|
+
"strings"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
resource "github.com/opensoha/soha-contracts/resource"
|
|
14
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
15
|
+
"k8s.io/client-go/rest"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// ReadRelationSnapshot reads metadata only. Its caller must authorize every
|
|
19
|
+
// collection and namespace; Kubernetes RBAC still applies to the actual reads.
|
|
20
|
+
func ReadRelationSnapshot(ctx context.Context, client rest.Interface, clusterID string, input resource.RelationSnapshotRequest) (resource.RelationSnapshot, error) {
|
|
21
|
+
if err := resource.ValidateRelationSnapshotRequest(input); err != nil {
|
|
22
|
+
return resource.RelationSnapshot{}, err
|
|
23
|
+
}
|
|
24
|
+
if client == nil {
|
|
25
|
+
return resource.RelationSnapshot{}, fmt.Errorf("resource metadata client unavailable")
|
|
26
|
+
}
|
|
27
|
+
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
28
|
+
defer cancel()
|
|
29
|
+
out := resource.RelationSnapshot{Version: resource.RelationSnapshotFeature, Sources: []resource.RelationSourceResult{}}
|
|
30
|
+
budget := &topologyReadBudget{remaining: 8 << 20}
|
|
31
|
+
for _, query := range input.Queries {
|
|
32
|
+
out.Sources = append(out.Sources, readRelationSource(ctx, client, clusterID, query, budget))
|
|
33
|
+
}
|
|
34
|
+
return out, nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func readRelationSource(ctx context.Context, client rest.Interface, clusterID string, q resource.RelationQuery, budget *topologyReadBudget) resource.RelationSourceResult {
|
|
38
|
+
out := resource.RelationSourceResult{Coverage: resource.RelationCoverage{QueryID: q.ID, State: "available"}, Objects: []resource.RelationObject{}}
|
|
39
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
40
|
+
defer cancel()
|
|
41
|
+
group, version := relationGroupVersion(q.Source.APIVersion)
|
|
42
|
+
base := topologyAPIBase(group, version)
|
|
43
|
+
data, err := boundedTopologyGET(ctx, client, base, false, budget)
|
|
44
|
+
if err == nil {
|
|
45
|
+
err = validateRelationDiscovery(data, q.Source)
|
|
46
|
+
}
|
|
47
|
+
if err != nil {
|
|
48
|
+
out.Coverage.State, out.Coverage.ReasonCode = relationReadError(err)
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
path := base + "/namespaces/" + q.Source.Namespace + "/" + q.Source.Resource
|
|
52
|
+
if q.Name != "" {
|
|
53
|
+
path += "/" + q.Name
|
|
54
|
+
}
|
|
55
|
+
request := client.Get().AbsPath(path)
|
|
56
|
+
if q.Name == "" {
|
|
57
|
+
request = request.Param("limit", "200").SetHeader("Accept", "application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1")
|
|
58
|
+
} else {
|
|
59
|
+
request = request.SetHeader("Accept", "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1")
|
|
60
|
+
}
|
|
61
|
+
if len(q.Selector) > 0 {
|
|
62
|
+
terms := []string{}
|
|
63
|
+
for k, v := range q.Selector {
|
|
64
|
+
terms = append(terms, k+"="+v)
|
|
65
|
+
}
|
|
66
|
+
sort.Strings(terms)
|
|
67
|
+
request = request.Param("labelSelector", strings.Join(terms, ","))
|
|
68
|
+
}
|
|
69
|
+
stream, err := request.Stream(ctx)
|
|
70
|
+
if err != nil {
|
|
71
|
+
out.Coverage.State, out.Coverage.ReasonCode = relationReadError(err)
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
defer func() { _ = stream.Close() }()
|
|
75
|
+
data, err = io.ReadAll(io.LimitReader(&topologyBudgetReader{reader: stream, budget: budget}, (512<<10)+1))
|
|
76
|
+
if err != nil || len(data) > 512<<10 {
|
|
77
|
+
out.Coverage.State = "partial"
|
|
78
|
+
out.Coverage.ReasonCode = "byte_budget"
|
|
79
|
+
out.Coverage.Truncated = true
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
return relationMetadataResult(out, clusterID, q, data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func relationMetadataResult(out resource.RelationSourceResult, clusterID string, q resource.RelationQuery, data []byte) resource.RelationSourceResult {
|
|
86
|
+
var objects []metav1.PartialObjectMetadata
|
|
87
|
+
if q.Name != "" {
|
|
88
|
+
var object metav1.PartialObjectMetadata
|
|
89
|
+
if json.Unmarshal(data, &object) != nil {
|
|
90
|
+
out.Coverage.State = "error"
|
|
91
|
+
out.Coverage.ReasonCode = "invalid_response"
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
objects = append(objects, object)
|
|
95
|
+
} else {
|
|
96
|
+
var list metav1.PartialObjectMetadataList
|
|
97
|
+
if json.Unmarshal(data, &list) != nil {
|
|
98
|
+
out.Coverage.State = "error"
|
|
99
|
+
out.Coverage.ReasonCode = "invalid_response"
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
objects = list.Items
|
|
103
|
+
if list.Continue != "" || len(objects) > resource.RelationMaxItems {
|
|
104
|
+
out.Coverage.State = "partial"
|
|
105
|
+
out.Coverage.ReasonCode = "item_budget"
|
|
106
|
+
out.Coverage.Truncated = true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for i, object := range objects {
|
|
110
|
+
if i >= resource.RelationMaxItems {
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
if object.Name == "" || object.UID == "" || object.Namespace != q.Source.Namespace || (q.Name != "" && q.Name != object.Name) || len(object.OwnerReferences) > 32 {
|
|
114
|
+
out.Coverage.State = "partial"
|
|
115
|
+
out.Coverage.ReasonCode = "invalid_identity"
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
item := resource.RelationObject{Resource: resource.ResourceRef{ClusterID: clusterID, APIVersion: q.Source.APIVersion, Kind: q.Source.Kind, Namespace: object.Namespace, Name: object.Name, UID: string(object.UID), ResourceVersion: object.ResourceVersion, ScopeMode: resource.ResourceScopeModeNamespace}}
|
|
119
|
+
for _, owner := range object.OwnerReferences {
|
|
120
|
+
item.Owners = append(item.Owners, resource.RelationOwner{APIVersion: owner.APIVersion, Kind: owner.Kind, Name: owner.Name, UID: string(owner.UID), Controller: owner.Controller != nil && *owner.Controller})
|
|
121
|
+
}
|
|
122
|
+
out.Objects = append(out.Objects, item)
|
|
123
|
+
}
|
|
124
|
+
return out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func relationGroupVersion(apiVersion string) (string, string) {
|
|
128
|
+
parts := strings.SplitN(apiVersion, "/", 2)
|
|
129
|
+
if len(parts) == 1 {
|
|
130
|
+
return "", parts[0]
|
|
131
|
+
}
|
|
132
|
+
return parts[0], parts[1]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func validateRelationDiscovery(data []byte, source resource.RelationSource) error {
|
|
136
|
+
var list metav1.APIResourceList
|
|
137
|
+
if json.Unmarshal(data, &list) != nil {
|
|
138
|
+
return fmt.Errorf("invalid discovery response")
|
|
139
|
+
}
|
|
140
|
+
for _, item := range list.APIResources {
|
|
141
|
+
if item.Name == source.Resource && item.Kind == source.Kind && item.Namespaced {
|
|
142
|
+
return nil
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return errRelationSourceNotServed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var errRelationSourceNotServed = errors.New("resource definition not served")
|
|
149
|
+
|
|
150
|
+
func relationReadError(err error) (string, string) {
|
|
151
|
+
if errors.Is(err, context.DeadlineExceeded) {
|
|
152
|
+
return "error", "timeout"
|
|
153
|
+
}
|
|
154
|
+
if errors.Is(err, context.Canceled) {
|
|
155
|
+
return "error", "cancelled"
|
|
156
|
+
}
|
|
157
|
+
if errors.Is(err, errRelationSourceNotServed) {
|
|
158
|
+
return "not_installed", "source_not_served"
|
|
159
|
+
}
|
|
160
|
+
return topologyReadError(err)
|
|
161
|
+
}
|