@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,100 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"net/http"
|
|
9
|
+
"net/http/httptest"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
|
|
13
|
+
cr "github.com/opensoha/soha-contracts/resource"
|
|
14
|
+
"k8s.io/client-go/kubernetes"
|
|
15
|
+
"k8s.io/client-go/rest"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func TestRelationMetadataProjectionAndPartialCoverage(t *testing.T) {
|
|
19
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
20
|
+
w.Header().Set("Content-Type", "application/json")
|
|
21
|
+
if r.URL.Path == "/apis/widgets.test/v1" {
|
|
22
|
+
_, _ = w.Write([]byte(`{"resources":[{"name":"widgets","kind":"Widget","namespaced":true}]}`))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
if strings.Contains(r.Header.Get("Accept"), "q=0.9") {
|
|
26
|
+
t.Error("full JSON fallback must not be negotiated")
|
|
27
|
+
}
|
|
28
|
+
if r.URL.Query().Get("limit") != "200" || r.URL.Query().Get("labelSelector") != "app=demo" {
|
|
29
|
+
t.Errorf("unbounded or different query: %s", r.URL.String())
|
|
30
|
+
}
|
|
31
|
+
_, _ = w.Write([]byte(`{"metadata":{"continue":"next"},"items":[{"metadata":{"name":"child","namespace":"team","uid":"child-uid","annotations":{"password":"synthetic-private"},"labels":{"token":"synthetic-private"},"ownerReferences":[{"apiVersion":"widgets.test/v1","kind":"Widget","name":"root","uid":"root-uid"}]},"spec":{"password":"synthetic-private"},"status":{"secret":"synthetic-private"}}]}`))
|
|
32
|
+
}))
|
|
33
|
+
defer server.Close()
|
|
34
|
+
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL})
|
|
35
|
+
if err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
q := cr.RelationQuery{ID: "unknown", Source: cr.RelationSource{APIVersion: "widgets.test/v1", Resource: "widgets", Kind: "Widget", Namespace: "team"}, Selector: map[string]string{"app": "demo"}}
|
|
39
|
+
view, err := ReadRelationSnapshot(context.Background(), client.CoreV1().RESTClient(), "cluster", cr.RelationSnapshotRequest{Queries: []cr.RelationQuery{q}})
|
|
40
|
+
if err != nil {
|
|
41
|
+
t.Fatal(err)
|
|
42
|
+
}
|
|
43
|
+
if len(view.Sources) != 1 || view.Sources[0].Coverage.State != "partial" || !view.Sources[0].Coverage.Truncated || len(view.Sources[0].Objects) != 1 {
|
|
44
|
+
t.Fatalf("incorrect result: %+v", view)
|
|
45
|
+
}
|
|
46
|
+
data, _ := json.Marshal(view)
|
|
47
|
+
if strings.Contains(string(data), "synthetic-private") {
|
|
48
|
+
t.Fatal("configuration value leaked")
|
|
49
|
+
}
|
|
50
|
+
object := view.Sources[0].Objects[0]
|
|
51
|
+
if object.Resource.UID != "child-uid" || object.Owners[0].UID != "root-uid" {
|
|
52
|
+
t.Fatal("UID evidence lost")
|
|
53
|
+
}
|
|
54
|
+
q.Source.Kind = "DifferentKind"
|
|
55
|
+
view, err = ReadRelationSnapshot(context.Background(), client.CoreV1().RESTClient(), "cluster", cr.RelationSnapshotRequest{Queries: []cr.RelationQuery{q}})
|
|
56
|
+
if err != nil || len(view.Sources[0].Objects) != 0 || view.Sources[0].Coverage.State == "available" {
|
|
57
|
+
t.Fatal("unverified GVR/kind accepted")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func TestRelationMetadataNegotiationFailsClosed(t *testing.T) {
|
|
62
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
63
|
+
w.Header().Set("Content-Type", "application/json")
|
|
64
|
+
if r.URL.Path == "/api/v1" {
|
|
65
|
+
_, _ = w.Write([]byte(`{"resources":[{"name":"secrets","kind":"Secret","namespaced":true}]}`))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if strings.Contains(r.Header.Get("Accept"), ",application/json") {
|
|
69
|
+
t.Error("full Secret requested")
|
|
70
|
+
}
|
|
71
|
+
w.WriteHeader(http.StatusNotAcceptable)
|
|
72
|
+
}))
|
|
73
|
+
defer server.Close()
|
|
74
|
+
client, err := kubernetes.NewForConfig(&rest.Config{Host: server.URL})
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
query := cr.RelationQuery{ID: "secrets", Source: cr.RelationSource{APIVersion: "v1", Resource: "secrets", Kind: "Secret", Namespace: "team"}}
|
|
79
|
+
snapshot, err := ReadRelationSnapshot(context.Background(), client.CoreV1().RESTClient(), "cluster", cr.RelationSnapshotRequest{Queries: []cr.RelationQuery{query}})
|
|
80
|
+
if err != nil || len(snapshot.Sources[0].Objects) != 0 || snapshot.Sources[0].Coverage.State == "available" {
|
|
81
|
+
t.Fatalf("metadata refusal not closed: %+v %v", snapshot, err)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func TestRelationReadErrorsRetainCoverageReasons(t *testing.T) {
|
|
86
|
+
for _, tc := range []struct {
|
|
87
|
+
err error
|
|
88
|
+
state, reason string
|
|
89
|
+
}{
|
|
90
|
+
{fmt.Errorf("request: %w", context.DeadlineExceeded), "error", "timeout"},
|
|
91
|
+
{context.Canceled, "error", "cancelled"},
|
|
92
|
+
{errRelationSourceNotServed, "not_installed", "source_not_served"},
|
|
93
|
+
{errors.New("opaque server response"), "error", "source_unavailable"},
|
|
94
|
+
} {
|
|
95
|
+
state, reason := relationReadError(tc.err)
|
|
96
|
+
if state != tc.state || reason != tc.reason {
|
|
97
|
+
t.Fatalf("%v became %s/%s", tc.err, state, reason)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
contractresource "github.com/opensoha/soha-contracts/resource"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
12
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
13
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
14
|
+
"k8s.io/client-go/dynamic"
|
|
15
|
+
"k8s.io/client-go/util/retry"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const RootCRAgentCapability = "manifest.root-cr.v1"
|
|
19
|
+
const RootCRAgentProviderPrefix = "manifest_root_cr_v1."
|
|
20
|
+
|
|
21
|
+
// ApplyRootCR updates only spec after an explicit publication handoff. It reuses
|
|
22
|
+
// the workbench optimistic update semantics and preserves runtime metadata/status.
|
|
23
|
+
// A retry may acknowledge the same desired spec already applied to the same UID.
|
|
24
|
+
func ApplyRootCR(ctx context.Context, client dynamic.ResourceInterface, desired *unstructured.Unstructured, root sohaapi.ManifestRootResource, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
|
|
25
|
+
if err := contractresource.ValidateRootCRDocument(desired.Object, root); err != nil {
|
|
26
|
+
return nil, err
|
|
27
|
+
}
|
|
28
|
+
if options.Force != nil && *options.Force {
|
|
29
|
+
return nil, fmt.Errorf("root CR cannot force field ownership")
|
|
30
|
+
}
|
|
31
|
+
var applied *unstructured.Unstructured
|
|
32
|
+
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
|
33
|
+
live, err := client.Get(ctx, root.Name, metav1.GetOptions{})
|
|
34
|
+
if err != nil {
|
|
35
|
+
return err
|
|
36
|
+
}
|
|
37
|
+
if string(live.GetUID()) != root.UID {
|
|
38
|
+
return fmt.Errorf("root CR UID changed; create a new plan")
|
|
39
|
+
}
|
|
40
|
+
if err := validateRootCROwner(live); err != nil {
|
|
41
|
+
return err
|
|
42
|
+
}
|
|
43
|
+
sameSpec := rootSpecEqual(live.Object["spec"], desired.Object["spec"])
|
|
44
|
+
if live.GetGeneration() != root.Generation && !sameSpec {
|
|
45
|
+
return fmt.Errorf("root CR configuration changed; create a new plan")
|
|
46
|
+
}
|
|
47
|
+
if sameSpec {
|
|
48
|
+
applied = live
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
write := live.DeepCopy()
|
|
52
|
+
write.Object["spec"] = desired.DeepCopy().Object["spec"]
|
|
53
|
+
applied, err = client.Update(ctx, write, metav1.UpdateOptions{FieldManager: options.FieldManager, DryRun: options.DryRun, FieldValidation: options.FieldValidation})
|
|
54
|
+
return err
|
|
55
|
+
})
|
|
56
|
+
return applied, err
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func validateRootCROwner(live *unstructured.Unstructured) error {
|
|
60
|
+
if err := ValidateDirectManifestOwner(live); err != nil {
|
|
61
|
+
return err
|
|
62
|
+
}
|
|
63
|
+
manager := strings.ToLower(live.GetLabels()["app.kubernetes.io/managed-by"])
|
|
64
|
+
if len(live.GetOwnerReferences()) > 0 || (manager != "" && manager != "soha") {
|
|
65
|
+
return fmt.Errorf("%w: root CR has an external owner", ErrResourceOwnership)
|
|
66
|
+
}
|
|
67
|
+
for key := range live.GetAnnotations() {
|
|
68
|
+
if strings.HasPrefix(key, "meta.helm.sh/") || strings.HasPrefix(key, "argocd.argoproj.io/") {
|
|
69
|
+
return fmt.Errorf("%w: root CR has an external publisher", ErrResourceOwnership)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func rootSpecEqual(left, right any) bool {
|
|
76
|
+
a, aErr := json.Marshal(left)
|
|
77
|
+
b, bErr := json.Marshal(right)
|
|
78
|
+
return aErr == nil && bErr == nil && bytes.Equal(a, b)
|
|
79
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"reflect"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
9
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
10
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
11
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
12
|
+
"k8s.io/apimachinery/pkg/runtime"
|
|
13
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
14
|
+
"k8s.io/client-go/dynamic/fake"
|
|
15
|
+
ktesting "k8s.io/client-go/testing"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func TestRootCRPublicationFences(t *testing.T) {
|
|
19
|
+
for _, mode := range []string{"apply", "dry-run", "retry", "replaced", "changed", "deleted", "deleting", "helm", "git", "controller", "status", "finalizer", "force", "conflict-replaced"} {
|
|
20
|
+
t.Run(mode, func(t *testing.T) {
|
|
21
|
+
root, live, desired, options := rootPublicationFixture(mode)
|
|
22
|
+
client := fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
|
|
23
|
+
gvr := schema.GroupVersionResource{Group: "unknown.example", Version: "v1", Resource: "databases"}
|
|
24
|
+
resources := client.Resource(gvr).Namespace("team")
|
|
25
|
+
if mode == "deleted" {
|
|
26
|
+
if err := resources.Delete(context.Background(), root.Name, metav1.DeleteOptions{}); err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
client.ClearActions()
|
|
30
|
+
}
|
|
31
|
+
writes := rootPublicationUpdates(t, client, root, live, mode, gvr)
|
|
32
|
+
_, err := ApplyRootCR(context.Background(), resources, desired, root, options)
|
|
33
|
+
valid := mode == "apply" || mode == "dry-run" || mode == "retry"
|
|
34
|
+
if valid && err != nil || !valid && err == nil {
|
|
35
|
+
t.Fatalf("error=%v valid=%v", err, valid)
|
|
36
|
+
}
|
|
37
|
+
expected := 0
|
|
38
|
+
if mode == "apply" || mode == "dry-run" || mode == "conflict-replaced" {
|
|
39
|
+
expected = 1
|
|
40
|
+
}
|
|
41
|
+
if *writes != expected {
|
|
42
|
+
t.Fatalf("writes=%d want=%d", *writes, expected)
|
|
43
|
+
}
|
|
44
|
+
for _, action := range client.Actions() {
|
|
45
|
+
if action.GetVerb() != "get" && action.GetVerb() != "update" {
|
|
46
|
+
t.Fatalf("unexpected mutation: %s", action.GetVerb())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func rootPublicationFixture(mode string) (sohaapi.ManifestRootResource, *unstructured.Unstructured, *unstructured.Unstructured, metav1.PatchOptions) {
|
|
54
|
+
root := sohaapi.ManifestRootResource{APIVersion: "unknown.example/v1", Kind: "Database", Namespace: "team", Name: "db", UID: "uid-1", Generation: 2, CrdName: "databases.unknown.example"}
|
|
55
|
+
live := &unstructured.Unstructured{Object: map[string]any{"apiVersion": root.APIVersion, "kind": root.Kind, "metadata": map[string]any{"namespace": root.Namespace, "name": root.Name, "uid": root.UID, "resourceVersion": "12", "generation": int64(2)}, "spec": map[string]any{"size": int64(1)}}}
|
|
56
|
+
desired := &unstructured.Unstructured{Object: map[string]any{"apiVersion": root.APIVersion, "kind": root.Kind, "metadata": map[string]any{"namespace": root.Namespace, "name": root.Name}, "spec": map[string]any{"size": int64(2)}}}
|
|
57
|
+
force := mode == "force"
|
|
58
|
+
options := metav1.PatchOptions{FieldManager: "opensoha-manifest/binding", Force: &force}
|
|
59
|
+
switch mode {
|
|
60
|
+
case "dry-run":
|
|
61
|
+
options.DryRun = []string{metav1.DryRunAll}
|
|
62
|
+
case "retry":
|
|
63
|
+
live.Object["spec"] = desired.Object["spec"]
|
|
64
|
+
live.SetGeneration(3)
|
|
65
|
+
case "replaced":
|
|
66
|
+
live.SetUID("uid-2")
|
|
67
|
+
case "changed":
|
|
68
|
+
live.SetGeneration(3)
|
|
69
|
+
case "deleting":
|
|
70
|
+
now := metav1.Now()
|
|
71
|
+
live.SetDeletionTimestamp(&now)
|
|
72
|
+
case "helm":
|
|
73
|
+
live.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "Helm"})
|
|
74
|
+
case "git":
|
|
75
|
+
live.SetManagedFields([]metav1.ManagedFieldsEntry{{Manager: "argocd-controller"}})
|
|
76
|
+
case "controller":
|
|
77
|
+
yes := true
|
|
78
|
+
live.SetOwnerReferences([]metav1.OwnerReference{{UID: "parent", Controller: &yes}})
|
|
79
|
+
case "status":
|
|
80
|
+
desired.Object["status"] = map[string]any{"ready": true}
|
|
81
|
+
case "finalizer":
|
|
82
|
+
desired.SetFinalizers([]string{"operator/hold"})
|
|
83
|
+
}
|
|
84
|
+
return root, live, desired, options
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func rootPublicationUpdates(t *testing.T, client *fake.FakeDynamicClient, root sohaapi.ManifestRootResource, live *unstructured.Unstructured, mode string, gvr schema.GroupVersionResource) *int {
|
|
88
|
+
t.Helper()
|
|
89
|
+
writes := new(int)
|
|
90
|
+
client.PrependReactor("update", "databases", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
91
|
+
*writes++
|
|
92
|
+
update, ok := action.(ktesting.UpdateActionImpl)
|
|
93
|
+
if !ok {
|
|
94
|
+
t.Fatalf("expected update action, got %T", action)
|
|
95
|
+
}
|
|
96
|
+
object, ok := update.GetObject().(*unstructured.Unstructured)
|
|
97
|
+
if !ok {
|
|
98
|
+
t.Fatalf("expected unstructured root, got %T", update.GetObject())
|
|
99
|
+
}
|
|
100
|
+
if mode == "dry-run" && len(update.GetUpdateOptions().DryRun) != 1 {
|
|
101
|
+
t.Fatal("preflight performed a real update")
|
|
102
|
+
}
|
|
103
|
+
body := object.Object
|
|
104
|
+
if string(object.GetUID()) != root.UID || object.GetResourceVersion() != "12" || !reflect.DeepEqual(object.Object["status"], live.Object["status"]) {
|
|
105
|
+
t.Fatalf("unsafe patch: %v", body)
|
|
106
|
+
}
|
|
107
|
+
if mode == "conflict-replaced" {
|
|
108
|
+
replaced := live.DeepCopy()
|
|
109
|
+
replaced.SetUID("uid-2")
|
|
110
|
+
if err := client.Tracker().Update(gvr, replaced, "team"); err != nil {
|
|
111
|
+
t.Fatal(err)
|
|
112
|
+
}
|
|
113
|
+
return true, nil, apierrors.NewConflict(gvr.GroupResource(), root.Name, nil)
|
|
114
|
+
}
|
|
115
|
+
return true, object, nil
|
|
116
|
+
})
|
|
117
|
+
return writes
|
|
118
|
+
}
|