@opensoha/contracts 0.1.17 → 0.1.18
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 +45 -0
- package/README.md +37 -0
- package/auth/catalog_generated.go +2 -2
- package/auth/outpost_headers.go +17 -0
- package/auth/outpost_headers_test.go +16 -0
- package/auth/permission-catalog.json +383 -1
- package/compat/consumer-revisions.json +3 -0
- package/compat/json-schema-ecosystem-0.1.0.json +1 -1
- package/compat/openapi-loose-boundaries-0.1.0.json +78 -46
- package/delivery/canonical.go +136 -0
- package/delivery/document.go +203 -0
- package/delivery/document.schema.json +1527 -0
- package/delivery/document_test.go +191 -0
- package/delivery/parser.go +155 -0
- package/evaluation/evaluation-run.schema.json +64 -10
- package/fixtures/json-schema/gateway-manifest/invalid/bad-execution-mode.json +73 -0
- package/fixtures/json-schema/gateway-manifest/invalid/unversioned-check.json +85 -0
- package/fixtures/json-schema/gateway-manifest/valid/checks.json +86 -0
- package/fixtures/json-schema/gateway-manifest/valid/versioned-task.json +73 -0
- package/fixtures/json-schema/network-ingest-event/invalid/vpn-probe-negative-rtt.json +34 -0
- package/fixtures/json-schema/network-ingest-event/invalid/vpn-stats-secret.json +27 -0
- package/fixtures/json-schema/network-ingest-event/valid/vpn-probe-batch.json +45 -0
- package/fixtures/json-schema/network-ingest-event/valid/vpn-tunnel-stats.json +26 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-bad-token.json +15 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-mode-override.json +16 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/managed-vpn-macos-enrollment.json +27 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/vpn-managed-connect.json +15 -0
- package/fixtures/openapi/AgentChatInput/invalid/empty-question.json +4 -0
- package/fixtures/openapi/AgentChatInput/invalid/system-role.json +9 -0
- package/fixtures/openapi/AgentChatInput/valid/authorized-background.json +5 -0
- package/fixtures/openapi/AgentChatInput/valid/conversation.json +15 -0
- package/fixtures/openapi/AgentRunnerToolCallRequest/invalid/model-identity.json +1 -0
- package/fixtures/openapi/AgentRunnerToolCallRequest/valid/search.json +1 -0
- package/fixtures/openapi/ApplicationInput/invalid/negative-version.json +6 -0
- package/fixtures/openapi/ApplicationInput/invalid/zero-version.json +6 -0
- package/fixtures/openapi/ApplicationInput/valid/versioned.json +6 -0
- package/fixtures/openapi/ApprovalRequest/invalid/resolved-scopes.json +20 -0
- package/fixtures/openapi/ApprovalRequest/valid/resolved-scopes.json +21 -0
- package/fixtures/openapi/BuildTemplateInput/invalid/negative-revision.json +6 -0
- package/fixtures/openapi/BuildTemplateInput/valid/draft.json +11 -0
- package/fixtures/openapi/BuildTriggerRequest/invalid/no-ref.json +1 -0
- package/fixtures/openapi/BuildTriggerRequest/valid/no-environment.json +1 -0
- package/fixtures/openapi/BuildpacksConfiguration/invalid/mutable-builder.json +5 -0
- package/fixtures/openapi/BuildpacksConfiguration/invalid/privilege-override.json +6 -0
- package/fixtures/openapi/BuildpacksConfiguration/invalid/unsupported-platform.json +5 -0
- package/fixtures/openapi/BuildpacksConfiguration/valid/pinned.json +5 -0
- package/fixtures/openapi/CapabilityAssessment/invalid/contract.json +16 -0
- package/fixtures/openapi/CapabilityAssessment/valid/contract.json +16 -0
- package/fixtures/openapi/CapabilityPlan/invalid/contract.json +16 -0
- package/fixtures/openapi/CapabilityPlan/valid/contract.json +19 -0
- package/fixtures/openapi/CapabilityPlanValidation/invalid/contract.json +5 -0
- package/fixtures/openapi/CapabilityPlanValidation/valid/contract.json +11 -0
- package/fixtures/openapi/CapabilityTask/invalid/contract.json +33 -0
- package/fixtures/openapi/CapabilityTask/valid/contract.json +34 -0
- package/fixtures/openapi/CapabilityTaskInput/invalid/contract.json +22 -0
- package/fixtures/openapi/CapabilityTaskInput/valid/contract.json +22 -0
- package/fixtures/openapi/CapabilityValueSemantic/invalid/contract.json +4 -0
- package/fixtures/openapi/CapabilityValueSemantic/valid/contract.json +7 -0
- package/fixtures/openapi/DeliveryAccessResult/invalid/release-verification.json +8 -0
- package/fixtures/openapi/DeliveryAccessResult/valid/release-verification.json +8 -0
- package/fixtures/openapi/DeliveryBatchAssessment/invalid/release-verification.json +11 -0
- package/fixtures/openapi/DeliveryBatchAssessment/valid/release-verification.json +11 -0
- package/fixtures/openapi/DeliveryBatchAssessmentInput/invalid/release-verification.json +9 -0
- package/fixtures/openapi/DeliveryBatchAssessmentInput/valid/release-verification.json +8 -0
- package/fixtures/openapi/DeliveryBatchInput/invalid/ambiguous-source.json +19 -0
- package/fixtures/openapi/DeliveryBatchInput/invalid/missing-idempotency.json +16 -0
- package/fixtures/openapi/DeliveryBatchInput/valid/quick-deploy.json +17 -0
- package/fixtures/openapi/DeliveryBatchInput/valid/saved-workflow.json +5 -0
- package/fixtures/openapi/DeliveryBlueprintInput/invalid/unversioned-preset.json +31 -0
- package/fixtures/openapi/DeliveryBlueprintInput/valid/service-preset.json +32 -0
- package/fixtures/openapi/DeliveryDocument/invalid/unknown-field.json +24 -0
- package/fixtures/openapi/DeliveryDocument/invalid/unknown-kind.json +23 -0
- package/fixtures/openapi/DeliveryDocument/invalid/unknown-version.json +23 -0
- package/fixtures/openapi/DeliveryDocument/valid/build-template.json +26 -0
- package/fixtures/openapi/DeliveryDocument/valid/deployment-template.json +26 -0
- package/fixtures/openapi/DeliveryDocument/valid/legacy-dag.json +39 -0
- package/fixtures/openapi/DeliveryDocument/valid/workflow-template.json +23 -0
- package/fixtures/openapi/DeliveryDocument/valid/workflow.json +20 -0
- package/fixtures/openapi/DeliveryDraftInput/invalid/short-idempotency-key.json +30 -0
- package/fixtures/openapi/DeliveryDraftInput/valid/idempotent-create.json +30 -0
- package/fixtures/openapi/DeliveryDraftService/invalid/zero-version.json +7 -0
- package/fixtures/openapi/DeliveryDraftService/valid/versioned-template.json +9 -0
- package/fixtures/openapi/DeliveryExecutionHistoryPage/invalid-kind.json +9 -0
- package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-build.json +16 -0
- package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-empty.json +3 -0
- package/fixtures/openapi/DeliveryPlanApprovalInput/invalid/confirm.json +1 -0
- package/fixtures/openapi/DeliveryPlanApprovalInput/valid/approve.json +1 -0
- package/fixtures/openapi/DeliveryPlanInput/invalid/draft-manifest-revision.json +1 -0
- package/fixtures/openapi/DeliveryPlanInput/valid/manifest-revision.json +1 -0
- package/fixtures/openapi/DeliveryTargetInput/invalid/deploy-without-environment.json +7 -0
- package/fixtures/openapi/DeliveryTargetInput/valid/build-without-environment.json +6 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/duplicate-kind.json +13 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/embedded-credentials.json +16 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/empty-kinds.json +10 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/negative-generation.json +15 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/unknown-kind.json +12 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/valid/manual-source.json +15 -0
- package/fixtures/openapi/DeliveryWorkflowDefinition/invalid/unversioned-template.json +15 -0
- package/fixtures/openapi/DeliveryWorkflowDefinition/valid/cross-app.json +24 -0
- package/fixtures/openapi/DeliveryWorkflowInput/invalid/short-key.json +1 -0
- package/fixtures/openapi/DeliveryWorkflowInput/valid/idempotent-create.json +1 -0
- package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-target-override.json +7 -0
- package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-zero-version.json +6 -0
- package/fixtures/openapi/DeploymentTemplatePreviewInput/valid/valid.json +6 -0
- package/fixtures/openapi/DockerDeliveryConfiguration/invalid/empty-mappings.json +5 -0
- package/fixtures/openapi/DockerDeliveryConfiguration/valid/compose.json +7 -0
- package/fixtures/openapi/DockerDeliverySnapshot/invalid/missing-required-field.json +19 -0
- package/fixtures/openapi/DockerDeliverySnapshot/valid/frozen-project.json +20 -0
- package/fixtures/openapi/DockerOperationCallbackRequest/invalid/cancellation-acknowledged-not-boolean.json +13 -0
- package/fixtures/openapi/DockerOperationCallbackRequest/valid/cancellation-acknowledged.json +13 -0
- package/fixtures/openapi/DockerProjectInput/invalid/empty-host.json +4 -0
- package/fixtures/openapi/DockerProjectInput/valid/inline-idempotent.json +7 -0
- package/fixtures/openapi/DockerQuickCreateHostInput/invalid/capacity-type.json +1 -0
- package/fixtures/openapi/DockerQuickCreateHostInput/valid/capacity.json +1 -0
- package/fixtures/openapi/HelmDeliveryConfiguration/invalid/plain-credential.json +21 -0
- package/fixtures/openapi/HelmDeliveryConfiguration/invalid/release-name.json +20 -0
- package/fixtures/openapi/HelmDeliveryConfiguration/valid/private-oci.json +20 -0
- package/fixtures/openapi/HelmDeliverySnapshot/invalid/secret-plan.json +31 -0
- package/fixtures/openapi/HelmDeliverySnapshot/invalid/unknown-operation.json +28 -0
- package/fixtures/openapi/HelmDeliverySnapshot/valid/first-install.json +28 -0
- package/fixtures/openapi/IdentityApplication/invalid/outpost-status.json +1 -0
- package/fixtures/openapi/IdentityApplicationOnboardingInput/invalid/missing-access.json +12 -0
- package/fixtures/openapi/IdentityApplicationOnboardingInput/valid/link.json +13 -0
- package/fixtures/openapi/IdentityOutpost/invalid/missing-runtime-status.json +9 -0
- package/fixtures/openapi/IdentityOutpost/valid/healthy.json +17 -0
- package/fixtures/openapi/IdentityOutpost/valid/unregistered.json +12 -0
- package/fixtures/openapi/IdentityOutpostDeployment/invalid/missing-protocol.json +3 -0
- package/fixtures/openapi/IdentityOutpostDeployment/valid/remote.json +6 -0
- package/fixtures/openapi/IdentityOutpostInput/invalid/forged-observation.json +7 -0
- package/fixtures/openapi/IdentityOutpostInput/valid/forward-auth.json +6 -0
- package/fixtures/openapi/IdentityOutpostInput/valid/legacy.json +8 -0
- package/fixtures/openapi/IdentityProviderSetup/invalid/fake-verification.json +1 -0
- package/fixtures/openapi/IdentityProviderSetup/valid/incomplete.json +1 -0
- package/fixtures/openapi/IdentityProviderSetup/valid/remote.json +1 -0
- package/fixtures/openapi/IdentityProviderUserMetadata/invalid/attributes.json +8 -0
- package/fixtures/openapi/IdentityProviderUserMetadata/valid/attributes.json +13 -0
- package/fixtures/openapi/KubernetesCustomResource/invalid/negative-generation.json +1 -0
- package/fixtures/openapi/KubernetesCustomResource/valid/deleting-operator.json +1 -0
- package/fixtures/openapi/KubernetesResourceCreationReceipt/invalid/manifest-content.json +20 -0
- package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/confirmed.json +20 -0
- package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/unknown.json +19 -0
- package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/negative-resource-observation.json +57 -0
- package/fixtures/openapi/ManifestDeploymentEnvelope/valid/operator-deleting.json +57 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/gitops-empty.json +7 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/valid/gitops-frozen-children.json +18 -0
- package/fixtures/openapi/ManifestKustomizeOptions/invalid/escaping-entry.json +1 -0
- package/fixtures/openapi/ManifestKustomizeOptions/invalid/mutable-image.json +1 -0
- package/fixtures/openapi/ManifestKustomizeOptions/valid/overlay-images.json +7 -0
- package/fixtures/openapi/ManifestRevisionInput/invalid/missing-version.json +1 -0
- package/fixtures/openapi/ManifestRevisionInput/valid/current-draft.json +1 -0
- package/fixtures/openapi/ManifestServiceTemplateInputs/invalid/mutable-image.json +8 -0
- package/fixtures/openapi/ManifestServiceTemplateInputs/valid/frozen-service-image.json +9 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/auto-with-gateway.json +6 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/missing-manual-gateway.json +5 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/mode-override.json +7 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/auto-intent.json +5 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/manual-intent.json +6 -0
- package/fixtures/openapi/NetworkVPNProfileInput/invalid/unknown-private-key.json +25 -0
- package/fixtures/openapi/NetworkVPNProfileInput/valid/assigned-profile.json +24 -0
- package/fixtures/openapi/NetworkVPNSelectionPolicyInput/invalid/unbounded-attempts.json +21 -0
- package/fixtures/openapi/NetworkVPNSelectionPolicyInput/valid/provider-policy.json +21 -0
- package/fixtures/openapi/ObservabilityMetricAssessmentInput/invalid/arbitrary-expression.json +10 -0
- package/fixtures/openapi/ObservabilityMetricAssessmentInput/valid/metric-window.json +10 -0
- package/fixtures/openapi/PluginManifest/valid/hermes-api.json +53 -0
- package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/missing-identity.json +5 -0
- package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/skip-analysis.json +5 -0
- package/fixtures/openapi/ProgressiveRolloutControlInput/valid/pause.json +5 -0
- package/fixtures/openapi/ReleaseTargetInput/invalid/missing-required-field.json +15 -0
- package/fixtures/openapi/ReleaseTargetInput/valid/docker-compose.json +16 -0
- package/fixtures/openapi/RepositoryAnalysis/invalid/moving-ref.json +11 -0
- package/fixtures/openapi/RepositoryAnalysis/valid/go.json +29 -0
- package/fixtures/openapi/RepositoryAnalysisInput/invalid/arbitrary-source-url.json +6 -0
- package/fixtures/openapi/RepositoryAnalysisInput/invalid/missing-ref.json +4 -0
- package/fixtures/openapi/RepositoryAnalysisInput/valid/nested-project.json +6 -0
- package/fixtures/openapi/ServiceDeploymentTemplateBinding/invalid/invalid-unpinned.json +4 -0
- package/fixtures/openapi/ServiceDeploymentTemplateBinding/valid/valid.json +7 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-executable-schema.json +25 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-missing-schema.json +21 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-mixed-renderers.json +40 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-nested-parameters.json +41 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-yaml.json +34 -0
- package/fixtures/openapi/TemplatePublishInput/invalid/missing-revision.json +1 -0
- package/fixtures/openapi/TemplatePublishInput/invalid/unknown-field.json +4 -0
- package/fixtures/openapi/TemplatePublishInput/invalid/zero-revision.json +3 -0
- package/fixtures/openapi/TemplatePublishInput/valid/revision.json +3 -0
- package/fixtures/openapi/ToolCapability/invalid/bad-idempotency.json +35 -0
- package/fixtures/openapi/ToolCapability/invalid/unversioned-check.json +47 -0
- package/fixtures/openapi/ToolCapability/valid/checks.json +48 -0
- package/fixtures/openapi/ToolCapability/valid/versioned-task.json +35 -0
- package/fixtures/openapi/ToolInvocationRequest/invalid/empty-version.json +8 -0
- package/fixtures/openapi/ToolInvocationRequest/valid/versioned.json +8 -0
- package/fixtures/openapi/ToolInvocationResult/invalid/missing-status-call.json +13 -0
- package/fixtures/openapi/ToolInvocationResult/valid/task.json +20 -0
- package/fixtures/openapi/VirtualMachineCreateInput/invalid/capacity-type.json +1 -0
- package/fixtures/openapi/VirtualMachineCreateInput/valid/capacity.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityInput/invalid/missing-disk.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityInput/valid/running-vm.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityResult/invalid/invented-status.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityResult/valid/available.json +1 -0
- package/fixtures/openapi/VirtualizationWorkerCreateInput/invalid/secret-input.json +5 -0
- package/fixtures/openapi/VirtualizationWorkerCreateInput/valid/one-worker.json +4 -0
- package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/external-owner.json +27 -0
- package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/zero-budget.json +27 -0
- package/fixtures/openapi/VirtualizationWorkerPoolInput/valid/registered.json +27 -0
- package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/invalid/nested-data.json +5 -0
- package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/valid/empty.json +3 -0
- package/fixtures/openapi/WorkbenchContextSelection/invalid/missing-file-content.json +8 -0
- package/fixtures/openapi/WorkbenchContextSelection/invalid/unknown-reference-kind.json +8 -0
- package/fixtures/openapi/WorkbenchContextSelection/valid/text-and-resource.json +17 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/trigger-without-plan.json +10 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/unknown-trigger.json +28 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/zero-interval.json +28 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/alert-capability.json +30 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/scheduled-capability.json +28 -0
- package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/missing-rule.json +3 -0
- package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/schedule-alert-selector.json +4 -0
- package/fixtures/openapi/WorkbenchInspectionTrigger/valid/schedule.json +3 -0
- package/fixtures/openapi/WorkbenchModelPreferences/invalid/unknown-effort.json +1 -0
- package/fixtures/openapi/WorkbenchModelPreferences/valid/selected.json +1 -0
- package/fixtures/openapi/WorkflowCatalogPage/invalid/unsupported-kind.json +6 -0
- package/fixtures/openapi/WorkflowCatalogPage/valid/build-without-environment.json +6 -0
- package/fixtures/openapi/WorkflowTemplateInput/invalid/zero-revision.json +6 -0
- package/fixtures/openapi/WorkflowTemplateInput/valid/draft.json +10 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/types.go +8070 -859
- package/gen/go/sohaapi/types_test.go +100 -1
- package/gen/go/sohaapi/vpn_models_test.go +21 -0
- package/gen/ts/sohaapi/index.ts +20343 -13162
- package/go.mod +107 -5
- package/go.sum +379 -16
- package/helmrelease/delivery.go +283 -0
- package/helmrelease/delivery_test.go +102 -0
- package/helmrelease/frozen_chart.go +91 -0
- package/helmrelease/result.go +27 -0
- package/helmrelease/runtime/delivery.go +508 -0
- package/helmrelease/runtime/delivery_test.go +236 -0
- package/helmrelease/runtime/resources_test.go +60 -0
- package/helmrelease/values.go +99 -0
- package/knowledge/context-envelope.schema.json +1 -0
- package/mcp/gateway-manifest.schema.json +387 -72
- package/network/README.md +40 -0
- package/network/network-ingest-event.schema.json +228 -0
- package/network/network-runtime-protocol.schema.json +457 -1
- package/openapi/soha-api.json +54958 -38899
- package/openapi/soha-api.yaml +12143 -4984
- package/openapi/spec_generated.go +3 -3
- package/package.json +3 -1
- package/resource/manifest_health.go +155 -0
- package/resource/manifest_health_test.go +37 -0
- package/resource/models.go +14 -0
- package/resource/runtime/argocd.go +166 -0
- package/resource/runtime/argocd_integration_test.go +142 -0
- package/resource/runtime/argocd_observation.go +143 -0
- package/resource/runtime/argocd_operation.go +253 -0
- package/resource/runtime/argocd_resources.go +115 -0
- package/resource/runtime/argocd_resources_test.go +107 -0
- package/resource/runtime/argocd_test.go +386 -0
- package/resource/runtime/health.go +115 -0
- package/resource/runtime/health_test.go +78 -0
- package/resource/runtime/operator_integration_test.go +174 -0
- package/resource/runtime/ownership.go +178 -0
- package/resource/runtime/ownership_test.go +178 -0
- package/resource/runtime/rollout_integration_test.go +287 -0
- package/resource/runtime/rollout_observation.go +252 -0
- package/resource/runtime/rollout_operation.go +398 -0
- package/resource/runtime/rollout_resources.go +385 -0
- package/resource/runtime/rollout_task.go +165 -0
- package/resource/runtime/rollout_test.go +450 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"errors"
|
|
9
|
+
"fmt"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
15
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
16
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
17
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
18
|
+
"k8s.io/apimachinery/pkg/runtime"
|
|
19
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
20
|
+
"k8s.io/apimachinery/pkg/types"
|
|
21
|
+
"k8s.io/client-go/dynamic/fake"
|
|
22
|
+
ktesting "k8s.io/client-go/testing"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
func rolloutFixture(t *testing.T, strategy string) []*unstructured.Unstructured {
|
|
26
|
+
t.Helper()
|
|
27
|
+
var documents []*unstructured.Unstructured
|
|
28
|
+
err := json.Unmarshal([]byte(`[
|
|
29
|
+
{"apiVersion":"argoproj.io/v1alpha1","kind":"Rollout","metadata":{"name":"web","namespace":"demo"},"spec":{"replicas":2,"selector":{"matchLabels":{"app":"web"}},"template":{"metadata":{"labels":{"app":"web"}},"spec":{"containers":[{"name":"app","image":"registry.example/app@sha256:`+strings.Repeat("a", 64)+`"}]}},"strategy":{"canary":{"stableService":"web-stable","canaryService":"web-preview","trafficRouting":{"traefik":{"weightedTraefikServiceName":"web-traffic"}},"steps":[{"setWeight":20},{"pause":{}},{"analysis":{"templates":[{"templateName":"web-metrics"}]}},{"setWeight":100}]}}}},
|
|
30
|
+
{"apiVersion":"v1","kind":"Service","metadata":{"name":"web-stable","namespace":"demo"},"spec":{"selector":{"app":"web"},"ports":[{"port":8080,"targetPort":8080}]}},
|
|
31
|
+
{"apiVersion":"v1","kind":"Service","metadata":{"name":"web-preview","namespace":"demo"},"spec":{"selector":{"app":"web"},"ports":[{"port":8080,"targetPort":8080}]}},
|
|
32
|
+
{"apiVersion":"argoproj.io/v1alpha1","kind":"AnalysisTemplate","metadata":{"name":"web-metrics","namespace":"demo"},"spec":{"metrics":[{"name":"success","interval":"2s","count":3,"successCondition":"result >= 0.99","failureLimit":0,"provider":{"web":{"url":"http://web-preview.demo.svc:8080/metrics","jsonPath":"{$.successRate}"}}}]}},
|
|
33
|
+
{"apiVersion":"traefik.io/v1alpha1","kind":"TraefikService","metadata":{"name":"web-traffic","namespace":"demo"},"spec":{"weighted":{"services":[{"name":"web-stable","port":8080,"weight":100},{"name":"web-preview","port":8080,"weight":0}]}}},
|
|
34
|
+
{"apiVersion":"traefik.io/v1alpha1","kind":"IngressRoute","metadata":{"name":"web-route","namespace":"demo"},"spec":{"entryPoints":["web"],"routes":[{"kind":"Rule","match":"PathPrefix(\"/\")","services":[{"name":"web-traffic","kind":"TraefikService"}]}]}}
|
|
35
|
+
]`), &documents)
|
|
36
|
+
if err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
if strategy == "blueGreen" {
|
|
40
|
+
documents = documents[:4]
|
|
41
|
+
_ = unstructured.SetNestedMap(documents[0].Object, map[string]any{"blueGreen": map[string]any{"activeService": "web-stable", "previewService": "web-preview", "autoPromotionEnabled": false, "prePromotionAnalysis": map[string]any{"templates": []any{map[string]any{"templateName": "web-metrics"}}}}}, "spec", "strategy")
|
|
42
|
+
}
|
|
43
|
+
return documents
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func rolloutLiveFixture(t *testing.T) ([]*unstructured.Unstructured, *fake.FakeDynamicClient) {
|
|
47
|
+
documents := rolloutFixture(t, "canary")
|
|
48
|
+
objects := make([]runtime.Object, 0, len(documents))
|
|
49
|
+
for index, document := range documents {
|
|
50
|
+
live := document.DeepCopy()
|
|
51
|
+
live.SetUID(types.UID(fmt.Sprintf("uid-%d", index)))
|
|
52
|
+
live.SetResourceVersion("42")
|
|
53
|
+
live.SetAnnotations(map[string]string{RolloutOwnerAnnotation: "opensoha-manifest/test:web"})
|
|
54
|
+
if IsArgoRollout(live) {
|
|
55
|
+
live.SetGeneration(3)
|
|
56
|
+
annotations := live.GetAnnotations()
|
|
57
|
+
annotations[RolloutOperationAnnotation] = "operation-1"
|
|
58
|
+
live.SetAnnotations(annotations)
|
|
59
|
+
live.Object["status"] = map[string]any{"observedGeneration": "3", "phase": "Paused", "currentPodHash": "new", "stableRS": "old", "currentStepIndex": int64(1), "pauseConditions": []any{map[string]any{"reason": "CanaryPauseStep"}}}
|
|
60
|
+
}
|
|
61
|
+
if live.GetKind() == "Service" {
|
|
62
|
+
hash := "old"
|
|
63
|
+
if live.GetName() == "web-preview" {
|
|
64
|
+
hash = "new"
|
|
65
|
+
}
|
|
66
|
+
_ = unstructured.SetNestedField(live.Object, hash, "spec", "selector", rolloutPodHash)
|
|
67
|
+
}
|
|
68
|
+
if live.GetKind() == "TraefikService" {
|
|
69
|
+
_ = unstructured.SetNestedSlice(live.Object, []any{map[string]any{"name": "web-stable", "port": int64(8080), "weight": int64(80)}, map[string]any{"name": "web-preview", "port": int64(8080), "weight": int64(20)}}, "spec", "weighted", "services")
|
|
70
|
+
}
|
|
71
|
+
objects = append(objects, live)
|
|
72
|
+
}
|
|
73
|
+
return documents, fake.NewSimpleDynamicClient(runtime.NewScheme(), objects...)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestRolloutRestoreWaitsForCurrentGenerationBeforeAcceptingFailure(t *testing.T) {
|
|
77
|
+
for _, phase := range []string{"Degraded", "Aborted"} {
|
|
78
|
+
t.Run(phase, func(t *testing.T) {
|
|
79
|
+
documents, client := rolloutLiveFixture(t)
|
|
80
|
+
root, err := client.Resource(rollouts).Namespace("demo").Get(t.Context(), "web", metav1.GetOptions{})
|
|
81
|
+
operatorRequire(t, err)
|
|
82
|
+
root.SetGeneration(4)
|
|
83
|
+
if phase == "Aborted" {
|
|
84
|
+
_ = unstructured.SetNestedField(root.Object, true, "status", "abort")
|
|
85
|
+
} else {
|
|
86
|
+
_ = unstructured.SetNestedField(root.Object, phase, "status", "phase")
|
|
87
|
+
}
|
|
88
|
+
_, err = client.Resource(rollouts).Namespace("demo").Update(t.Context(), root, metav1.UpdateOptions{})
|
|
89
|
+
operatorRequire(t, err)
|
|
90
|
+
observed, err := ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
|
|
91
|
+
if err != nil || observed.Health != "progressing" {
|
|
92
|
+
t.Fatalf("previous generation failure must not abort a new restore: %+v, %v", observed, err)
|
|
93
|
+
}
|
|
94
|
+
_ = unstructured.SetNestedField(root.Object, "4", "status", "observedGeneration")
|
|
95
|
+
_, err = client.Resource(rollouts).Namespace("demo").Update(t.Context(), root, metav1.UpdateOptions{})
|
|
96
|
+
operatorRequire(t, err)
|
|
97
|
+
observed, err = ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
|
|
98
|
+
if err != nil || observed.Health != "degraded" {
|
|
99
|
+
t.Fatalf("current generation failure must remain terminal: %+v, %v", observed, err)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func TestRolloutObservationAcceptsOmittedEmptyContainerLists(t *testing.T) {
|
|
106
|
+
documents, client := rolloutLiveFixture(t)
|
|
107
|
+
containers, _, _ := unstructured.NestedSlice(documents[0].Object, "spec", "template", "spec", "containers")
|
|
108
|
+
container, ok := containers[0].(map[string]any)
|
|
109
|
+
if !ok {
|
|
110
|
+
t.Fatal("unexpected container")
|
|
111
|
+
}
|
|
112
|
+
container["env"] = []any{}
|
|
113
|
+
_ = unstructured.SetNestedSlice(documents[0].Object, containers, "spec", "template", "spec", "containers")
|
|
114
|
+
observed, err := ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
|
|
115
|
+
if err != nil || observed.Drifted || observed.Health != "progressing" {
|
|
116
|
+
t.Fatalf("omitted empty env must not abort a healthy progression: %+v, %v", observed, err)
|
|
117
|
+
}
|
|
118
|
+
container, ok = containers[0].(map[string]any)
|
|
119
|
+
if !ok {
|
|
120
|
+
t.Fatal("unexpected container")
|
|
121
|
+
}
|
|
122
|
+
container["env"] = []any{map[string]any{"name": "REQUIRED", "value": "yes"}}
|
|
123
|
+
_ = unstructured.SetNestedSlice(documents[0].Object, containers, "spec", "template", "spec", "containers")
|
|
124
|
+
observed, err = ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
|
|
125
|
+
if err != nil || !observed.Drifted || observed.Health != "degraded" {
|
|
126
|
+
t.Fatalf("missing required env must still fail: %+v, %v", observed, err)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func TestRolloutFrozenResources(t *testing.T) {
|
|
131
|
+
for _, strategy := range []string{"canary", "blueGreen"} {
|
|
132
|
+
if _, err := ValidateRolloutResources(rolloutFixture(t, strategy)); err != nil {
|
|
133
|
+
t.Fatal(strategy, err)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for _, scenario := range []string{"missing service", "namespace", "mutable image", "job analysis", "external metric", "unbounded metric", "invalid port", "skip analysis", "second root", "controller owner", "backend port", "other traffic", "unreferenced", "preset pause"} {
|
|
137
|
+
t.Run(scenario, func(t *testing.T) {
|
|
138
|
+
documents := rolloutFixture(t, "canary")
|
|
139
|
+
switch scenario {
|
|
140
|
+
case "missing service":
|
|
141
|
+
documents = append(documents[:1], documents[2:]...)
|
|
142
|
+
case "namespace":
|
|
143
|
+
documents[1].SetNamespace("outside")
|
|
144
|
+
case "mutable image":
|
|
145
|
+
_ = unstructured.SetNestedSlice(documents[0].Object, []any{map[string]any{"name": "app", "image": "app:latest"}}, "spec", "template", "spec", "containers")
|
|
146
|
+
case "job analysis", "external metric", "unbounded metric":
|
|
147
|
+
metrics, _, _ := unstructured.NestedSlice(documents[3].Object, "spec", "metrics")
|
|
148
|
+
metric, ok := metrics[0].(map[string]any)
|
|
149
|
+
if !ok {
|
|
150
|
+
t.Fatal("unexpected metric")
|
|
151
|
+
}
|
|
152
|
+
switch scenario {
|
|
153
|
+
case "job analysis":
|
|
154
|
+
metric["provider"] = map[string]any{"job": map[string]any{}}
|
|
155
|
+
case "external metric":
|
|
156
|
+
metric["provider"] = map[string]any{"web": map[string]any{"url": "http://other.demo.svc/metrics"}}
|
|
157
|
+
case "unbounded metric":
|
|
158
|
+
metric["interval"] = "24h"
|
|
159
|
+
}
|
|
160
|
+
_ = unstructured.SetNestedSlice(documents[3].Object, metrics, "spec", "metrics")
|
|
161
|
+
case "skip analysis":
|
|
162
|
+
_ = unstructured.SetNestedSlice(documents[0].Object, []any{map[string]any{"setWeight": int64(100)}}, "spec", "strategy", "canary", "steps")
|
|
163
|
+
case "invalid port":
|
|
164
|
+
documents = rolloutFixture(t, "blueGreen")
|
|
165
|
+
_ = unstructured.SetNestedSlice(documents[1].Object, []any{"invalid"}, "spec", "ports")
|
|
166
|
+
case "second root":
|
|
167
|
+
other := documents[0].DeepCopy()
|
|
168
|
+
other.SetName("other")
|
|
169
|
+
documents = append(documents, other)
|
|
170
|
+
case "controller owner":
|
|
171
|
+
documents[1].SetAnnotations(map[string]string{RolloutOwnerAnnotation: "another"})
|
|
172
|
+
case "backend port":
|
|
173
|
+
_ = unstructured.SetNestedSlice(documents[1].Object, []any{map[string]any{"port": int64(9090)}}, "spec", "ports")
|
|
174
|
+
case "other traffic":
|
|
175
|
+
_ = unstructured.SetNestedMap(documents[0].Object, map[string]any{"nginx": map[string]any{}}, "spec", "strategy", "canary", "trafficRouting")
|
|
176
|
+
case "unreferenced":
|
|
177
|
+
other := documents[1].DeepCopy()
|
|
178
|
+
other.SetName("other")
|
|
179
|
+
documents = append(documents, other)
|
|
180
|
+
case "preset pause":
|
|
181
|
+
_ = unstructured.SetNestedField(documents[0].Object, true, "spec", "paused")
|
|
182
|
+
}
|
|
183
|
+
if _, err := ValidateRolloutResources(documents); err == nil {
|
|
184
|
+
t.Fatal("unsafe bundle accepted")
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func TestRolloutControlsUseIdentityAndCannotSkipGates(t *testing.T) {
|
|
191
|
+
for _, scenario := range []string{"promote", "pause", "abort", "stale UID", "stale RV", "inconclusive", "timed pause", "other owner", "other operation"} {
|
|
192
|
+
t.Run(scenario, func(t *testing.T) {
|
|
193
|
+
documents, client := rolloutLiveFixture(t)
|
|
194
|
+
root, _ := client.Resource(rollouts).Namespace("demo").Get(context.Background(), "web", metav1.GetOptions{})
|
|
195
|
+
input := sohaapi.ProgressiveRolloutControlInput{Action: "promote", UID: "uid-0", ResourceVersion: "42"}
|
|
196
|
+
switch scenario {
|
|
197
|
+
case "pause", "abort":
|
|
198
|
+
input.Action = sohaapi.ProgressiveRolloutControlInputAction(scenario)
|
|
199
|
+
case "stale UID":
|
|
200
|
+
input.UID = "replaced"
|
|
201
|
+
case "stale RV":
|
|
202
|
+
input.ResourceVersion = "41"
|
|
203
|
+
case "inconclusive":
|
|
204
|
+
_ = unstructured.SetNestedSlice(root.Object, []any{map[string]any{"reason": "InconclusiveAnalysis"}}, "status", "pauseConditions")
|
|
205
|
+
case "timed pause":
|
|
206
|
+
steps, _, _ := unstructured.NestedSlice(root.Object, "spec", "strategy", "canary", "steps")
|
|
207
|
+
steps[1] = map[string]any{"pause": map[string]any{"duration": "1m"}}
|
|
208
|
+
_ = unstructured.SetNestedSlice(root.Object, steps, "spec", "strategy", "canary", "steps")
|
|
209
|
+
_ = unstructured.SetNestedSlice(documents[0].Object, steps, "spec", "strategy", "canary", "steps")
|
|
210
|
+
case "other owner":
|
|
211
|
+
annotations := root.GetAnnotations()
|
|
212
|
+
annotations[RolloutOwnerAnnotation] = "opensoha-manifest/test:other-root"
|
|
213
|
+
root.SetAnnotations(annotations)
|
|
214
|
+
case "other operation":
|
|
215
|
+
annotations := root.GetAnnotations()
|
|
216
|
+
annotations[RolloutOperationAnnotation] = "new-operation"
|
|
217
|
+
root.SetAnnotations(annotations)
|
|
218
|
+
}
|
|
219
|
+
_ = client.Tracker().Update(rollouts, root, "demo")
|
|
220
|
+
writes := 0
|
|
221
|
+
client.PrependReactor("patch", "rollouts", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
222
|
+
writes++
|
|
223
|
+
patch, ok := action.(ktesting.PatchAction)
|
|
224
|
+
if !ok {
|
|
225
|
+
t.Fatal("unexpected patch action")
|
|
226
|
+
}
|
|
227
|
+
var body map[string]any
|
|
228
|
+
_ = json.Unmarshal(patch.GetPatch(), &body)
|
|
229
|
+
metadata, ok := body["metadata"].(map[string]any)
|
|
230
|
+
if !ok {
|
|
231
|
+
t.Fatal("unexpected patch metadata")
|
|
232
|
+
}
|
|
233
|
+
if metadata["uid"] != "uid-0" || metadata["resourceVersion"] != "42" {
|
|
234
|
+
t.Fatal("control lost its CAS")
|
|
235
|
+
}
|
|
236
|
+
if status, ok := body["status"].(map[string]any); ok {
|
|
237
|
+
if status["currentStepIndex"] != nil || status["promoteFull"] != nil {
|
|
238
|
+
t.Fatal("control skipped native analysis")
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return true, root, nil
|
|
242
|
+
})
|
|
243
|
+
_, err := ControlRollout(context.Background(), client, documents, "opensoha-manifest/test", "operation-1", input)
|
|
244
|
+
accepted := scenario == "promote" || scenario == "pause" || scenario == "abort"
|
|
245
|
+
if accepted && (err != nil || writes != 1) || !accepted && (err == nil || writes != 0) {
|
|
246
|
+
t.Fatalf("writes=%d err=%v", writes, err)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
func TestRolloutStopRequiresActualStableTraffic(t *testing.T) {
|
|
253
|
+
documents, client := rolloutLiveFixture(t)
|
|
254
|
+
root, _ := client.Resource(rollouts).Namespace("demo").Get(context.Background(), "web", metav1.GetOptions{})
|
|
255
|
+
_ = unstructured.SetNestedField(root.Object, true, "status", "abort")
|
|
256
|
+
_ = unstructured.SetNestedField(root.Object, "Degraded", "status", "phase")
|
|
257
|
+
_ = client.Tracker().Update(rollouts, root, "demo")
|
|
258
|
+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
259
|
+
defer cancel()
|
|
260
|
+
if err := StopRollout(ctx, client, documents, "opensoha-manifest/test", "operation-1", "uid-0"); !errors.Is(err, ErrRolloutStopUnconfirmed) {
|
|
261
|
+
t.Fatalf("unconfirmed traffic stop: %v", err)
|
|
262
|
+
}
|
|
263
|
+
gvr := rolloutResourceTypes["traefik.io/TraefikService"]
|
|
264
|
+
weighted, _ := client.Resource(gvr).Namespace("demo").Get(context.Background(), "web-traffic", metav1.GetOptions{})
|
|
265
|
+
_ = unstructured.SetNestedSlice(weighted.Object, []any{map[string]any{"name": "web-stable", "port": int64(8080), "weight": int64(100)}, map[string]any{"name": "web-preview", "port": int64(8080), "weight": int64(0)}}, "spec", "weighted", "services")
|
|
266
|
+
_ = client.Tracker().Update(gvr, weighted, "demo")
|
|
267
|
+
if err := StopRollout(context.Background(), client, documents, "opensoha-manifest/test", "operation-1", "uid-0"); err != nil {
|
|
268
|
+
t.Fatal(err)
|
|
269
|
+
}
|
|
270
|
+
if err := ValidateDirectManifestOwner(root); !errors.Is(err, ErrResourceOwnership) {
|
|
271
|
+
t.Fatal("native mutation accepted a Rollout")
|
|
272
|
+
}
|
|
273
|
+
if _, err := ApplyManifest(context.Background(), client.Resource(rollouts).Namespace("demo"), documents[0], metav1.PatchOptions{}); !errors.Is(err, ErrResourceOwnership) {
|
|
274
|
+
t.Fatal("generic apply accepted a Rollout")
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func TestRolloutPreflightPreservesControllerFields(t *testing.T) {
|
|
279
|
+
documents, client := rolloutLiveFixture(t)
|
|
280
|
+
root, _ := client.Resource(rollouts).Namespace("demo").Get(context.Background(), "web", metav1.GetOptions{})
|
|
281
|
+
_ = unstructured.SetNestedField(root.Object, "Healthy", "status", "phase")
|
|
282
|
+
_ = client.Tracker().Update(rollouts, root, "demo")
|
|
283
|
+
writes := 0
|
|
284
|
+
client.PrependReactor("update", "*", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
285
|
+
update, ok := action.(ktesting.UpdateAction)
|
|
286
|
+
if !ok {
|
|
287
|
+
t.Fatal("unexpected update action")
|
|
288
|
+
}
|
|
289
|
+
write, ok := update.GetObject().(*unstructured.Unstructured)
|
|
290
|
+
if !ok {
|
|
291
|
+
t.Fatal("unexpected update object")
|
|
292
|
+
}
|
|
293
|
+
updateOptions, ok := action.(interface{ GetUpdateOptions() metav1.UpdateOptions })
|
|
294
|
+
if !ok {
|
|
295
|
+
t.Fatal("unexpected update options")
|
|
296
|
+
}
|
|
297
|
+
if write.GetUID() == "" || write.GetResourceVersion() != "42" || len(updateOptions.GetUpdateOptions().DryRun) != 1 {
|
|
298
|
+
t.Fatal("preflight lost identity or dry-run")
|
|
299
|
+
}
|
|
300
|
+
if write.GetKind() == "Service" && argoString(write, "spec", "selector", rolloutPodHash) == "" {
|
|
301
|
+
t.Fatal("preflight removed the native selector hash")
|
|
302
|
+
}
|
|
303
|
+
if write.GetKind() == "TraefikService" {
|
|
304
|
+
t.Fatal("preflight rewrote native traffic weights")
|
|
305
|
+
}
|
|
306
|
+
writes++
|
|
307
|
+
return true, write, nil
|
|
308
|
+
})
|
|
309
|
+
_, err := ApplyRollout(context.Background(), client, documents, metav1.PatchOptions{FieldManager: "opensoha-manifest/test", DryRun: []string{metav1.DryRunAll}}, "operation-2")
|
|
310
|
+
if err != nil || writes != 5 {
|
|
311
|
+
t.Fatalf("preflight writes=%d err=%v", writes, err)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
func TestRolloutDependencyUpdateRetriesAndSkipsUnchanged(t *testing.T) {
|
|
316
|
+
documents, client := rolloutLiveFixture(t)
|
|
317
|
+
resources := client.Resource(rolloutResourceTypes["/Service"]).Namespace("demo")
|
|
318
|
+
live, err := resources.Get(t.Context(), "web-preview", metav1.GetOptions{})
|
|
319
|
+
if err != nil {
|
|
320
|
+
t.Fatal(err)
|
|
321
|
+
}
|
|
322
|
+
wanted := documents[2].DeepCopy()
|
|
323
|
+
_ = unstructured.SetNestedSlice(live.Object, []any{map[string]any{"port": int64(8080), "targetPort": int64(8080), "protocol": "TCP"}}, "spec", "ports")
|
|
324
|
+
_ = client.Tracker().Update(rolloutResourceTypes["/Service"], live, "demo")
|
|
325
|
+
wanted.SetAnnotations(live.GetAnnotations())
|
|
326
|
+
options := metav1.PatchOptions{FieldManager: "opensoha-manifest/test"}
|
|
327
|
+
if _, err := updateRolloutResource(t.Context(), resources, wanted, live, options, "web"); err != nil {
|
|
328
|
+
t.Fatal(err)
|
|
329
|
+
}
|
|
330
|
+
for _, action := range client.Actions() {
|
|
331
|
+
if action.GetVerb() == "update" {
|
|
332
|
+
t.Fatal("unchanged Service was rewritten")
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
_ = unstructured.SetNestedSlice(wanted.Object, []any{map[string]any{"port": int64(8080), "targetPort": int64(9090)}}, "spec", "ports")
|
|
336
|
+
writes := 0
|
|
337
|
+
client.PrependReactor("update", "services", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
338
|
+
writes++
|
|
339
|
+
if writes < 4 {
|
|
340
|
+
return true, nil, apierrors.NewConflict(rolloutResourceTypes["/Service"].GroupResource(), "web-preview", errors.New("native controller changed selector"))
|
|
341
|
+
}
|
|
342
|
+
update, ok := action.(ktesting.UpdateAction)
|
|
343
|
+
if !ok {
|
|
344
|
+
t.Fatal("unexpected update action")
|
|
345
|
+
}
|
|
346
|
+
write, ok := update.GetObject().(*unstructured.Unstructured)
|
|
347
|
+
if !ok {
|
|
348
|
+
t.Fatal("unexpected update object")
|
|
349
|
+
}
|
|
350
|
+
if argoString(write, "spec", "selector", rolloutPodHash) != "new" {
|
|
351
|
+
t.Fatal("native selector lost during retry")
|
|
352
|
+
}
|
|
353
|
+
return true, write, nil
|
|
354
|
+
})
|
|
355
|
+
if _, err := updateRolloutResource(t.Context(), resources, wanted, live, options, "web"); err != nil || writes != 4 {
|
|
356
|
+
t.Fatalf("writes=%d err=%v", writes, err)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func TestRolloutTaskValidatesFrozenPayloadAndReturnsNativeEvidence(t *testing.T) {
|
|
361
|
+
for _, scenario := range []string{"observe", "digest", "namespace", "owner", "mixed GitOps", "adopt"} {
|
|
362
|
+
t.Run(scenario, func(t *testing.T) {
|
|
363
|
+
documents, client := rolloutLiveFixture(t)
|
|
364
|
+
payload := sohaapi.ManifestExecutionTaskPayload{Action: sohaapi.ManifestTaskActionObserve, PackageID: "package", BindingID: "test", DeploymentID: "deployment", Generation: 7, Namespace: "demo", FieldManager: "opensoha-manifest/test", IdempotencyKey: "operation-1", RenderedDigest: "frozen"}
|
|
365
|
+
for _, item := range documents {
|
|
366
|
+
content, _ := item.MarshalJSON()
|
|
367
|
+
sum := sha256.Sum256(content)
|
|
368
|
+
payload.Documents = append(payload.Documents, sohaapi.ManifestRenderedDocument{APIVersion: item.GetAPIVersion(), Kind: item.GetKind(), Namespace: item.GetNamespace(), Name: item.GetName(), Content: string(content), ContentDigest: hex.EncodeToString(sum[:])})
|
|
369
|
+
}
|
|
370
|
+
switch scenario {
|
|
371
|
+
case "digest":
|
|
372
|
+
payload.Documents[1].Content += " "
|
|
373
|
+
case "namespace":
|
|
374
|
+
payload.Namespace = "outside"
|
|
375
|
+
case "owner":
|
|
376
|
+
payload.FieldManager = "opensoha-manifest/another"
|
|
377
|
+
case "mixed GitOps":
|
|
378
|
+
payload.GitOpsDocuments = payload.Documents[:1]
|
|
379
|
+
case "adopt":
|
|
380
|
+
payload.Action = sohaapi.ManifestTaskActionAdopt
|
|
381
|
+
}
|
|
382
|
+
result, err := ExecuteRolloutTask(t.Context(), client, payload)
|
|
383
|
+
if scenario != "observe" {
|
|
384
|
+
if err == nil || len(client.Actions()) != 0 {
|
|
385
|
+
t.Fatalf("invalid task reached Kubernetes: %v", err)
|
|
386
|
+
}
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if err != nil {
|
|
390
|
+
t.Fatal(err)
|
|
391
|
+
}
|
|
392
|
+
if result.Rollout == nil || *result.Rollout.CanaryWeight != 20 || len(result.Inventory) != 6 || result.Drift == nil || result.Drift.Drifted {
|
|
393
|
+
t.Fatalf("missing native evidence: %+v", result)
|
|
394
|
+
}
|
|
395
|
+
for _, item := range result.Inventory {
|
|
396
|
+
if item.UID == "" || item.Generation != 7 || item.DesiredObjectDigest != item.ObservedObjectDigest {
|
|
397
|
+
t.Fatalf("bad inventory: %+v", item)
|
|
398
|
+
}
|
|
399
|
+
if item.Kind != "Service" && item.Health != "progressing" {
|
|
400
|
+
t.Fatalf("native policy readiness must follow the paused rollout: %+v", item)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
func TestRolloutAnalysisEvidenceSurvivesStepCompletion(t *testing.T) {
|
|
408
|
+
root := rolloutFixture(t, "canary")[0]
|
|
409
|
+
root.SetUID("current-owner")
|
|
410
|
+
root.SetAnnotations(map[string]string{"rollout.argoproj.io/revision": "2"})
|
|
411
|
+
base := &unstructured.Unstructured{Object: map[string]any{
|
|
412
|
+
"apiVersion": "argoproj.io/v1alpha1", "kind": "AnalysisRun",
|
|
413
|
+
"metadata": map[string]any{"name": "current", "namespace": "demo", "uid": "run-uid", "labels": map[string]any{rolloutPodHash: "current-hash"}, "annotations": map[string]any{"rollout.argoproj.io/revision": "2"}},
|
|
414
|
+
"spec": map[string]any{"metrics": []any{map[string]any{"name": "success", "count": int64(2), "interval": "3s", "successCondition": "result >= 0.99"}}},
|
|
415
|
+
"status": map[string]any{"phase": "Successful", "metricResults": []any{map[string]any{"name": "success", "phase": "Successful", "count": int64(2), "successful": int64(2), "measurements": []any{
|
|
416
|
+
map[string]any{"startedAt": "2026-09-14T00:00:00Z", "finishedAt": "2026-09-14T00:00:01Z", "value": "0.99"},
|
|
417
|
+
map[string]any{"startedAt": "2026-09-14T00:00:03Z", "finishedAt": "2026-09-14T00:00:04Z", "value": "1"},
|
|
418
|
+
}}}},
|
|
419
|
+
}}
|
|
420
|
+
base.SetOwnerReferences([]metav1.OwnerReference{*metav1.NewControllerRef(root, root.GroupVersionKind())})
|
|
421
|
+
objects := []runtime.Object{base}
|
|
422
|
+
for _, mode := range []string{"old-owner", "old-revision", "old-hash"} {
|
|
423
|
+
run := base.DeepCopy()
|
|
424
|
+
run.SetName(mode)
|
|
425
|
+
run.SetUID(types.UID(mode))
|
|
426
|
+
switch mode {
|
|
427
|
+
case "old-owner":
|
|
428
|
+
refs := run.GetOwnerReferences()
|
|
429
|
+
refs[0].UID = "old-owner"
|
|
430
|
+
run.SetOwnerReferences(refs)
|
|
431
|
+
case "old-revision":
|
|
432
|
+
run.SetAnnotations(map[string]string{"rollout.argoproj.io/revision": "1"})
|
|
433
|
+
case "old-hash":
|
|
434
|
+
run.SetLabels(map[string]string{rolloutPodHash: "old-hash"})
|
|
435
|
+
}
|
|
436
|
+
objects = append(objects, run)
|
|
437
|
+
}
|
|
438
|
+
client := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{analysisRuns: "AnalysisRunList"}, objects...)
|
|
439
|
+
state := sohaapi.ProgressiveRolloutStatus{CurrentRevision: "current-hash"}
|
|
440
|
+
if err := observeRolloutAnalyses(t.Context(), client, root, &state); err != nil {
|
|
441
|
+
t.Fatal(err)
|
|
442
|
+
}
|
|
443
|
+
if len(state.Metrics) != 1 {
|
|
444
|
+
t.Fatalf("included unrelated analysis: %+v", state.Metrics)
|
|
445
|
+
}
|
|
446
|
+
metric := state.Metrics[0]
|
|
447
|
+
if metric.AnalysisRun != "current" || metric.Count != 2 || metric.Value != "1" || metric.StartedAt != "2026-09-14T00:00:00Z" || metric.FinishedAt != "2026-09-14T00:00:04Z" {
|
|
448
|
+
t.Fatalf("completed observation evidence lost: %+v", metric)
|
|
449
|
+
}
|
|
450
|
+
}
|