@opensoha/contracts 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/COMPATIBILITY.md +72 -0
  2. package/README.md +38 -0
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/outpost_headers.go +17 -0
  5. package/auth/outpost_headers_test.go +16 -0
  6. package/auth/permission-catalog.json +383 -1
  7. package/compat/consumer-revisions.json +7 -0
  8. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  9. package/compat/openapi-loose-boundaries-0.1.0.json +78 -46
  10. package/delivery/canonical.go +136 -0
  11. package/delivery/document.go +203 -0
  12. package/delivery/document.schema.json +1527 -0
  13. package/delivery/document_test.go +191 -0
  14. package/delivery/parser.go +155 -0
  15. package/evaluation/evaluation-run.schema.json +64 -10
  16. package/fixtures/json-schema/gateway-manifest/invalid/bad-execution-mode.json +73 -0
  17. package/fixtures/json-schema/gateway-manifest/invalid/unversioned-check.json +85 -0
  18. package/fixtures/json-schema/gateway-manifest/valid/checks.json +86 -0
  19. package/fixtures/json-schema/gateway-manifest/valid/versioned-task.json +73 -0
  20. package/fixtures/json-schema/network-ingest-event/invalid/vpn-probe-negative-rtt.json +34 -0
  21. package/fixtures/json-schema/network-ingest-event/invalid/vpn-stats-secret.json +27 -0
  22. package/fixtures/json-schema/network-ingest-event/valid/vpn-probe-batch.json +45 -0
  23. package/fixtures/json-schema/network-ingest-event/valid/vpn-tunnel-stats.json +26 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-bad-token.json +15 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-mode-override.json +16 -0
  26. package/fixtures/json-schema/network-runtime-protocol/valid/managed-vpn-macos-enrollment.json +27 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-managed-connect.json +15 -0
  28. package/fixtures/openapi/AgentChatInput/invalid/empty-question.json +4 -0
  29. package/fixtures/openapi/AgentChatInput/invalid/system-role.json +9 -0
  30. package/fixtures/openapi/AgentChatInput/valid/authorized-background.json +5 -0
  31. package/fixtures/openapi/AgentChatInput/valid/conversation.json +15 -0
  32. package/fixtures/openapi/AgentRunnerToolCallRequest/invalid/model-identity.json +1 -0
  33. package/fixtures/openapi/AgentRunnerToolCallRequest/valid/search.json +1 -0
  34. package/fixtures/openapi/ApplicationInput/invalid/negative-version.json +6 -0
  35. package/fixtures/openapi/ApplicationInput/invalid/zero-version.json +6 -0
  36. package/fixtures/openapi/ApplicationInput/valid/versioned.json +6 -0
  37. package/fixtures/openapi/ApprovalRequest/invalid/resolved-scopes.json +20 -0
  38. package/fixtures/openapi/ApprovalRequest/valid/resolved-scopes.json +21 -0
  39. package/fixtures/openapi/BuildTemplateInput/invalid/negative-revision.json +6 -0
  40. package/fixtures/openapi/BuildTemplateInput/valid/draft.json +11 -0
  41. package/fixtures/openapi/BuildTriggerRequest/invalid/no-ref.json +1 -0
  42. package/fixtures/openapi/BuildTriggerRequest/valid/no-environment.json +1 -0
  43. package/fixtures/openapi/BuildpacksConfiguration/invalid/mutable-builder.json +5 -0
  44. package/fixtures/openapi/BuildpacksConfiguration/invalid/privilege-override.json +6 -0
  45. package/fixtures/openapi/BuildpacksConfiguration/invalid/unsupported-platform.json +5 -0
  46. package/fixtures/openapi/BuildpacksConfiguration/valid/pinned.json +5 -0
  47. package/fixtures/openapi/CapabilityAssessment/invalid/contract.json +16 -0
  48. package/fixtures/openapi/CapabilityAssessment/valid/contract.json +16 -0
  49. package/fixtures/openapi/CapabilityPlan/invalid/contract.json +16 -0
  50. package/fixtures/openapi/CapabilityPlan/valid/contract.json +19 -0
  51. package/fixtures/openapi/CapabilityPlanValidation/invalid/contract.json +5 -0
  52. package/fixtures/openapi/CapabilityPlanValidation/valid/contract.json +11 -0
  53. package/fixtures/openapi/CapabilityTask/invalid/contract.json +33 -0
  54. package/fixtures/openapi/CapabilityTask/valid/contract.json +34 -0
  55. package/fixtures/openapi/CapabilityTaskInput/invalid/contract.json +22 -0
  56. package/fixtures/openapi/CapabilityTaskInput/valid/contract.json +22 -0
  57. package/fixtures/openapi/CapabilityValueSemantic/invalid/contract.json +4 -0
  58. package/fixtures/openapi/CapabilityValueSemantic/valid/contract.json +7 -0
  59. package/fixtures/openapi/ConnectionCheckResult/invalid/missing-health.json +1 -0
  60. package/fixtures/openapi/ConnectionCheckResult/invalid/task-id.json +1 -0
  61. package/fixtures/openapi/ConnectionCheckResult/valid/failure.json +1 -0
  62. package/fixtures/openapi/ConnectionCheckResult/valid/healthy.json +1 -0
  63. package/fixtures/openapi/DeliveryAccessResult/invalid/release-verification.json +8 -0
  64. package/fixtures/openapi/DeliveryAccessResult/valid/release-verification.json +8 -0
  65. package/fixtures/openapi/DeliveryBatchAssessment/invalid/release-verification.json +11 -0
  66. package/fixtures/openapi/DeliveryBatchAssessment/valid/release-verification.json +11 -0
  67. package/fixtures/openapi/DeliveryBatchAssessmentInput/invalid/release-verification.json +9 -0
  68. package/fixtures/openapi/DeliveryBatchAssessmentInput/valid/release-verification.json +8 -0
  69. package/fixtures/openapi/DeliveryBatchInput/invalid/ambiguous-source.json +19 -0
  70. package/fixtures/openapi/DeliveryBatchInput/invalid/missing-idempotency.json +16 -0
  71. package/fixtures/openapi/DeliveryBatchInput/valid/quick-deploy.json +17 -0
  72. package/fixtures/openapi/DeliveryBatchInput/valid/saved-workflow.json +5 -0
  73. package/fixtures/openapi/DeliveryBlueprintInput/invalid/unversioned-preset.json +31 -0
  74. package/fixtures/openapi/DeliveryBlueprintInput/valid/service-preset.json +32 -0
  75. package/fixtures/openapi/DeliveryDocument/invalid/unknown-field.json +24 -0
  76. package/fixtures/openapi/DeliveryDocument/invalid/unknown-kind.json +23 -0
  77. package/fixtures/openapi/DeliveryDocument/invalid/unknown-version.json +23 -0
  78. package/fixtures/openapi/DeliveryDocument/valid/build-template.json +26 -0
  79. package/fixtures/openapi/DeliveryDocument/valid/deployment-template.json +26 -0
  80. package/fixtures/openapi/DeliveryDocument/valid/legacy-dag.json +39 -0
  81. package/fixtures/openapi/DeliveryDocument/valid/workflow-template.json +23 -0
  82. package/fixtures/openapi/DeliveryDocument/valid/workflow.json +20 -0
  83. package/fixtures/openapi/DeliveryDraftInput/invalid/short-idempotency-key.json +30 -0
  84. package/fixtures/openapi/DeliveryDraftInput/valid/idempotent-create.json +30 -0
  85. package/fixtures/openapi/DeliveryDraftService/invalid/zero-version.json +7 -0
  86. package/fixtures/openapi/DeliveryDraftService/valid/versioned-template.json +9 -0
  87. package/fixtures/openapi/DeliveryExecutionHistoryPage/invalid-kind.json +9 -0
  88. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-build.json +16 -0
  89. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-empty.json +3 -0
  90. package/fixtures/openapi/DeliveryPlanApprovalInput/invalid/confirm.json +1 -0
  91. package/fixtures/openapi/DeliveryPlanApprovalInput/valid/approve.json +1 -0
  92. package/fixtures/openapi/DeliveryPlanInput/invalid/draft-manifest-revision.json +1 -0
  93. package/fixtures/openapi/DeliveryPlanInput/valid/manifest-revision.json +1 -0
  94. package/fixtures/openapi/DeliveryTargetInput/invalid/deploy-without-environment.json +7 -0
  95. package/fixtures/openapi/DeliveryTargetInput/valid/build-without-environment.json +6 -0
  96. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/duplicate-kind.json +13 -0
  97. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/embedded-credentials.json +16 -0
  98. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/empty-kinds.json +10 -0
  99. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/negative-generation.json +15 -0
  100. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/unknown-kind.json +12 -0
  101. package/fixtures/openapi/DeliveryTemplateSourceInput/valid/manual-source.json +15 -0
  102. package/fixtures/openapi/DeliveryWorkflowDefinition/invalid/unversioned-template.json +15 -0
  103. package/fixtures/openapi/DeliveryWorkflowDefinition/valid/cross-app.json +24 -0
  104. package/fixtures/openapi/DeliveryWorkflowInput/invalid/short-key.json +1 -0
  105. package/fixtures/openapi/DeliveryWorkflowInput/valid/idempotent-create.json +1 -0
  106. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-target-override.json +7 -0
  107. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-zero-version.json +6 -0
  108. package/fixtures/openapi/DeploymentTemplatePreviewInput/valid/valid.json +6 -0
  109. package/fixtures/openapi/DockerDeliveryConfiguration/invalid/empty-mappings.json +5 -0
  110. package/fixtures/openapi/DockerDeliveryConfiguration/valid/compose.json +7 -0
  111. package/fixtures/openapi/DockerDeliverySnapshot/invalid/missing-required-field.json +19 -0
  112. package/fixtures/openapi/DockerDeliverySnapshot/valid/frozen-project.json +20 -0
  113. package/fixtures/openapi/DockerOperationCallbackRequest/invalid/cancellation-acknowledged-not-boolean.json +13 -0
  114. package/fixtures/openapi/DockerOperationCallbackRequest/valid/cancellation-acknowledged.json +13 -0
  115. package/fixtures/openapi/DockerProjectInput/invalid/empty-host.json +4 -0
  116. package/fixtures/openapi/DockerProjectInput/valid/inline-idempotent.json +7 -0
  117. package/fixtures/openapi/DockerQuickCreateHostInput/invalid/capacity-type.json +1 -0
  118. package/fixtures/openapi/DockerQuickCreateHostInput/valid/capacity.json +1 -0
  119. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/plain-credential.json +21 -0
  120. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/release-name.json +20 -0
  121. package/fixtures/openapi/HelmDeliveryConfiguration/valid/private-oci.json +20 -0
  122. package/fixtures/openapi/HelmDeliverySnapshot/invalid/secret-plan.json +31 -0
  123. package/fixtures/openapi/HelmDeliverySnapshot/invalid/unknown-operation.json +28 -0
  124. package/fixtures/openapi/HelmDeliverySnapshot/valid/first-install.json +28 -0
  125. package/fixtures/openapi/IdentityApplication/invalid/outpost-status.json +1 -0
  126. package/fixtures/openapi/IdentityApplicationOnboardingInput/invalid/missing-access.json +12 -0
  127. package/fixtures/openapi/IdentityApplicationOnboardingInput/valid/link.json +13 -0
  128. package/fixtures/openapi/IdentityOutpost/invalid/missing-runtime-status.json +9 -0
  129. package/fixtures/openapi/IdentityOutpost/valid/healthy.json +17 -0
  130. package/fixtures/openapi/IdentityOutpost/valid/unregistered.json +12 -0
  131. package/fixtures/openapi/IdentityOutpostDeployment/invalid/missing-protocol.json +3 -0
  132. package/fixtures/openapi/IdentityOutpostDeployment/valid/remote.json +6 -0
  133. package/fixtures/openapi/IdentityOutpostInput/invalid/forged-observation.json +7 -0
  134. package/fixtures/openapi/IdentityOutpostInput/valid/forward-auth.json +6 -0
  135. package/fixtures/openapi/IdentityOutpostInput/valid/legacy.json +8 -0
  136. package/fixtures/openapi/IdentityProviderSetup/invalid/fake-verification.json +1 -0
  137. package/fixtures/openapi/IdentityProviderSetup/valid/incomplete.json +1 -0
  138. package/fixtures/openapi/IdentityProviderSetup/valid/remote.json +1 -0
  139. package/fixtures/openapi/IdentityProviderUserMetadata/invalid/attributes.json +8 -0
  140. package/fixtures/openapi/IdentityProviderUserMetadata/valid/attributes.json +13 -0
  141. package/fixtures/openapi/KubernetesCustomResource/invalid/negative-generation.json +1 -0
  142. package/fixtures/openapi/KubernetesCustomResource/valid/deleting-operator.json +1 -0
  143. package/fixtures/openapi/KubernetesResourceCreationReceipt/invalid/manifest-content.json +20 -0
  144. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/confirmed.json +20 -0
  145. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/unknown.json +19 -0
  146. package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/negative-resource-observation.json +57 -0
  147. package/fixtures/openapi/ManifestDeploymentEnvelope/valid/operator-deleting.json +57 -0
  148. package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/gitops-empty.json +7 -0
  149. package/fixtures/openapi/ManifestExecutionTaskPayload/valid/gitops-frozen-children.json +18 -0
  150. package/fixtures/openapi/ManifestKustomizeOptions/invalid/escaping-entry.json +1 -0
  151. package/fixtures/openapi/ManifestKustomizeOptions/invalid/mutable-image.json +1 -0
  152. package/fixtures/openapi/ManifestKustomizeOptions/valid/overlay-images.json +7 -0
  153. package/fixtures/openapi/ManifestRevisionInput/invalid/missing-version.json +1 -0
  154. package/fixtures/openapi/ManifestRevisionInput/valid/current-draft.json +1 -0
  155. package/fixtures/openapi/ManifestServiceTemplateInputs/invalid/mutable-image.json +8 -0
  156. package/fixtures/openapi/ManifestServiceTemplateInputs/valid/frozen-service-image.json +9 -0
  157. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/auto-with-gateway.json +6 -0
  158. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/missing-manual-gateway.json +5 -0
  159. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/mode-override.json +7 -0
  160. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/auto-intent.json +5 -0
  161. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/manual-intent.json +6 -0
  162. package/fixtures/openapi/NetworkVPNProfileInput/invalid/unknown-private-key.json +25 -0
  163. package/fixtures/openapi/NetworkVPNProfileInput/valid/assigned-profile.json +24 -0
  164. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/invalid/unbounded-attempts.json +21 -0
  165. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/valid/provider-policy.json +21 -0
  166. package/fixtures/openapi/ObservabilityMetricAssessmentInput/invalid/arbitrary-expression.json +10 -0
  167. package/fixtures/openapi/ObservabilityMetricAssessmentInput/valid/metric-window.json +10 -0
  168. package/fixtures/openapi/PluginManifest/valid/hermes-api.json +53 -0
  169. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/missing-identity.json +5 -0
  170. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/skip-analysis.json +5 -0
  171. package/fixtures/openapi/ProgressiveRolloutControlInput/valid/pause.json +5 -0
  172. package/fixtures/openapi/ReleaseTargetInput/invalid/missing-required-field.json +15 -0
  173. package/fixtures/openapi/ReleaseTargetInput/valid/docker-compose.json +16 -0
  174. package/fixtures/openapi/RepositoryAnalysis/invalid/moving-ref.json +11 -0
  175. package/fixtures/openapi/RepositoryAnalysis/valid/go.json +29 -0
  176. package/fixtures/openapi/RepositoryAnalysisInput/invalid/arbitrary-source-url.json +6 -0
  177. package/fixtures/openapi/RepositoryAnalysisInput/invalid/missing-ref.json +4 -0
  178. package/fixtures/openapi/RepositoryAnalysisInput/valid/nested-project.json +6 -0
  179. package/fixtures/openapi/ServiceDeploymentTemplateBinding/invalid/invalid-unpinned.json +4 -0
  180. package/fixtures/openapi/ServiceDeploymentTemplateBinding/valid/valid.json +7 -0
  181. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-executable-schema.json +25 -0
  182. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-missing-schema.json +21 -0
  183. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-mixed-renderers.json +40 -0
  184. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-nested-parameters.json +41 -0
  185. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-yaml.json +34 -0
  186. package/fixtures/openapi/TemplatePublishInput/invalid/missing-revision.json +1 -0
  187. package/fixtures/openapi/TemplatePublishInput/invalid/unknown-field.json +4 -0
  188. package/fixtures/openapi/TemplatePublishInput/invalid/zero-revision.json +3 -0
  189. package/fixtures/openapi/TemplatePublishInput/valid/revision.json +3 -0
  190. package/fixtures/openapi/ToolCapability/invalid/bad-idempotency.json +35 -0
  191. package/fixtures/openapi/ToolCapability/invalid/unversioned-check.json +47 -0
  192. package/fixtures/openapi/ToolCapability/valid/checks.json +48 -0
  193. package/fixtures/openapi/ToolCapability/valid/versioned-task.json +35 -0
  194. package/fixtures/openapi/ToolInvocationRequest/invalid/empty-version.json +8 -0
  195. package/fixtures/openapi/ToolInvocationRequest/valid/versioned.json +8 -0
  196. package/fixtures/openapi/ToolInvocationResult/invalid/missing-status-call.json +13 -0
  197. package/fixtures/openapi/ToolInvocationResult/valid/task.json +20 -0
  198. package/fixtures/openapi/UpdateBrandingSettingsRequest/invalid/dark-logo-type.json +9 -0
  199. package/fixtures/openapi/UpdateBrandingSettingsRequest/valid/dark-logos.json +10 -0
  200. package/fixtures/openapi/VirtualMachineCreateInput/invalid/capacity-type.json +1 -0
  201. package/fixtures/openapi/VirtualMachineCreateInput/valid/capacity.json +1 -0
  202. package/fixtures/openapi/VirtualizationCapacityInput/invalid/missing-disk.json +1 -0
  203. package/fixtures/openapi/VirtualizationCapacityInput/valid/running-vm.json +1 -0
  204. package/fixtures/openapi/VirtualizationCapacityResult/invalid/invented-status.json +1 -0
  205. package/fixtures/openapi/VirtualizationCapacityResult/valid/available.json +1 -0
  206. package/fixtures/openapi/VirtualizationWorkerCreateInput/invalid/secret-input.json +5 -0
  207. package/fixtures/openapi/VirtualizationWorkerCreateInput/valid/one-worker.json +4 -0
  208. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/external-owner.json +27 -0
  209. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/zero-budget.json +27 -0
  210. package/fixtures/openapi/VirtualizationWorkerPoolInput/valid/registered.json +27 -0
  211. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/invalid/nested-data.json +5 -0
  212. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/valid/empty.json +3 -0
  213. package/fixtures/openapi/WorkbenchContextSelection/invalid/missing-file-content.json +8 -0
  214. package/fixtures/openapi/WorkbenchContextSelection/invalid/unknown-reference-kind.json +8 -0
  215. package/fixtures/openapi/WorkbenchContextSelection/valid/text-and-resource.json +17 -0
  216. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/trigger-without-plan.json +10 -0
  217. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/unknown-trigger.json +28 -0
  218. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/zero-interval.json +28 -0
  219. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/alert-capability.json +30 -0
  220. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/scheduled-capability.json +28 -0
  221. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/missing-rule.json +3 -0
  222. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/schedule-alert-selector.json +4 -0
  223. package/fixtures/openapi/WorkbenchInspectionTrigger/valid/schedule.json +3 -0
  224. package/fixtures/openapi/WorkbenchModelPreferences/invalid/unknown-effort.json +1 -0
  225. package/fixtures/openapi/WorkbenchModelPreferences/valid/selected.json +1 -0
  226. package/fixtures/openapi/WorkflowCatalogPage/invalid/unsupported-kind.json +6 -0
  227. package/fixtures/openapi/WorkflowCatalogPage/valid/build-without-environment.json +6 -0
  228. package/fixtures/openapi/WorkflowTemplateInput/invalid/zero-revision.json +6 -0
  229. package/fixtures/openapi/WorkflowTemplateInput/valid/draft.json +10 -0
  230. package/gen/go/sohaapi/client.go +11 -3
  231. package/gen/go/sohaapi/client_test.go +26 -3
  232. package/gen/go/sohaapi/types.go +8103 -863
  233. package/gen/go/sohaapi/types_test.go +100 -1
  234. package/gen/go/sohaapi/vpn_models_test.go +21 -0
  235. package/gen/ts/sohaapi/index.ts +20390 -13160
  236. package/go.mod +107 -5
  237. package/go.sum +379 -16
  238. package/helmrelease/delivery.go +283 -0
  239. package/helmrelease/delivery_test.go +102 -0
  240. package/helmrelease/frozen_chart.go +91 -0
  241. package/helmrelease/result.go +27 -0
  242. package/helmrelease/runtime/delivery.go +508 -0
  243. package/helmrelease/runtime/delivery_test.go +236 -0
  244. package/helmrelease/runtime/resources_test.go +60 -0
  245. package/helmrelease/values.go +99 -0
  246. package/knowledge/context-envelope.schema.json +1 -0
  247. package/mcp/gateway-manifest.schema.json +387 -72
  248. package/network/README.md +40 -0
  249. package/network/network-ingest-event.schema.json +228 -0
  250. package/network/network-runtime-protocol.schema.json +457 -1
  251. package/openapi/soha-api.json +54874 -38722
  252. package/openapi/soha-api.yaml +12199 -4982
  253. package/openapi/spec_generated.go +3 -3
  254. package/package.json +3 -1
  255. package/resource/manifest_health.go +155 -0
  256. package/resource/manifest_health_test.go +37 -0
  257. package/resource/models.go +14 -0
  258. package/resource/runtime/argocd.go +166 -0
  259. package/resource/runtime/argocd_integration_test.go +142 -0
  260. package/resource/runtime/argocd_observation.go +143 -0
  261. package/resource/runtime/argocd_operation.go +253 -0
  262. package/resource/runtime/argocd_resources.go +115 -0
  263. package/resource/runtime/argocd_resources_test.go +107 -0
  264. package/resource/runtime/argocd_test.go +386 -0
  265. package/resource/runtime/health.go +115 -0
  266. package/resource/runtime/health_test.go +78 -0
  267. package/resource/runtime/operator_integration_test.go +174 -0
  268. package/resource/runtime/ownership.go +178 -0
  269. package/resource/runtime/ownership_test.go +178 -0
  270. package/resource/runtime/rollout_integration_test.go +287 -0
  271. package/resource/runtime/rollout_observation.go +252 -0
  272. package/resource/runtime/rollout_operation.go +398 -0
  273. package/resource/runtime/rollout_resources.go +385 -0
  274. package/resource/runtime/rollout_task.go +165 -0
  275. package/resource/runtime/rollout_test.go +450 -0
@@ -0,0 +1,287 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "net/url"
9
+ "os"
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/util/wait"
19
+ "k8s.io/client-go/dynamic"
20
+ "k8s.io/client-go/kubernetes"
21
+ "k8s.io/client-go/tools/clientcmd"
22
+ )
23
+
24
+ func TestRolloutWithKubernetes(t *testing.T) {
25
+ kubeconfig := os.Getenv("SOHA_ROLLOUT_TEST_KUBECONFIG")
26
+ if kubeconfig == "" {
27
+ t.Skip("set the explicit loopback Rollouts/Traefik fixture kubeconfig and SOHA_ROLLOUT_TEST_IMAGE")
28
+ }
29
+ config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
30
+ operatorRequire(t, err)
31
+ endpoint, err := url.Parse(config.Host)
32
+ if err != nil || endpoint.Hostname() != "127.0.0.1" {
33
+ t.Fatal("only the isolated loopback fixture cluster is supported")
34
+ }
35
+ config.Timeout = 10 * time.Second
36
+ client, err := dynamic.NewForConfig(config)
37
+ operatorRequire(t, err)
38
+ kube, err := kubernetes.NewForConfig(config)
39
+ operatorRequire(t, err)
40
+ namespace := "soha-workflow-r6-progressive"
41
+ for _, strategy := range []string{"blueGreen", "canary"} {
42
+ t.Run(strategy, func(t *testing.T) {
43
+ name := "r6-runtime-" + strings.ToLower(strategy)
44
+ base := rolloutFixture(t, strategy)
45
+ encoded, _ := json.Marshal(base)
46
+ encoded = []byte(strings.ReplaceAll(strings.ReplaceAll(string(encoded), "web", name), "demo", namespace))
47
+ // The web provider key is a protocol field, not a resource name.
48
+ encoded = []byte(strings.ReplaceAll(string(encoded), `"`+name+`":{`, `"web":{`))
49
+ operatorRequire(t, json.Unmarshal(encoded, &base))
50
+ for _, document := range base {
51
+ if document.GetKind() == "IngressRoute" {
52
+ operatorRequire(t, unstructured.SetNestedStringSlice(document.Object, []string{"web"}, "spec", "entryPoints"))
53
+ }
54
+ }
55
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
56
+ defer cancel()
57
+ owner := "opensoha-manifest/" + name
58
+ operation := name + "-" + time.Now().UTC().Format("150405.000000")
59
+ options := metav1.PatchOptions{FieldManager: owner}
60
+ documents := func(version string) []*unstructured.Unstructured {
61
+ items := make([]*unstructured.Unstructured, len(base))
62
+ for i, item := range base {
63
+ items[i] = item.DeepCopy()
64
+ }
65
+ rate := 1
66
+ if version == "bad" {
67
+ rate = 0
68
+ }
69
+ response := fmt.Sprintf(`{"version":%q,"successRate":%d}`, version, rate)
70
+ command := fmt.Sprintf("cat > /tmp/serve-http <<'SERVER'\n#!/bin/sh\nwhile IFS= read -r line; do [ \"$line\" = \"$(printf '\\r')\" ] && break; done\nprintf 'HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: %d\\r\\nConnection: close\\r\\n\\r\\n%s'\nSERVER\nchmod 700 /tmp/serve-http\nexec nc -lk -p 8080 -e /tmp/serve-http", len(response), response)
71
+ containers := []any{map[string]any{"name": "app", "image": os.Getenv("SOHA_ROLLOUT_TEST_IMAGE"), "imagePullPolicy": "IfNotPresent", "env": []any{}, "command": []any{"/bin/sh", "-c"}, "args": []any{command}, "ports": []any{map[string]any{"containerPort": int64(8080)}}, "readinessProbe": map[string]any{"httpGet": map[string]any{"path": "/", "port": int64(8080)}, "periodSeconds": int64(2)}}}
72
+ operatorRequire(t, unstructured.SetNestedSlice(items[0].Object, containers, "spec", "template", "spec", "containers"))
73
+ return items
74
+ }
75
+ t.Cleanup(func() {
76
+ if t.Failed() {
77
+ t.Log("failed fixture retained:", name)
78
+ return
79
+ }
80
+ cleanup, stop := context.WithTimeout(context.Background(), 20*time.Second)
81
+ defer stop()
82
+ for _, document := range base {
83
+ gvk := document.GroupVersionKind()
84
+ _ = client.Resource(rolloutResourceTypes[gvk.Group+"/"+gvk.Kind]).Namespace(namespace).Delete(cleanup, document.GetName(), metav1.DeleteOptions{})
85
+ }
86
+ })
87
+ readHTTP := func(service string) (string, error) {
88
+ body, err := kube.CoreV1().RESTClient().Get().Namespace(namespace).Resource("services").Name(service + ":8080").SubResource("proxy").DoRaw(ctx)
89
+ if err != nil {
90
+ return "", err
91
+ }
92
+ var response struct {
93
+ Version string `json:"version"`
94
+ }
95
+ err = json.Unmarshal(body, &response)
96
+ return response.Version, err
97
+ }
98
+ checkHTTP := func(service, version string) {
99
+ err := wait.PollUntilContextTimeout(ctx, 200*time.Millisecond, 15*time.Second, true, func(context.Context) (bool, error) {
100
+ actual, err := readHTTP(service)
101
+ return err == nil && actual == version, nil
102
+ })
103
+ operatorRequire(t, err)
104
+ }
105
+ control := func(items []*unstructured.Unstructured, op, action string) {
106
+ err := wait.PollUntilContextTimeout(ctx, 200*time.Millisecond, 10*time.Second, true, func(context.Context) (bool, error) {
107
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
108
+ if err != nil {
109
+ return false, err
110
+ }
111
+ _, err = ControlRollout(ctx, client, items, owner, op, sohaapi.ProgressiveRolloutControlInput{Action: sohaapi.ProgressiveRolloutControlInputAction(action), UID: observed.State.UID, ResourceVersion: observed.State.ResourceVersion})
112
+ if apierrors.IsConflict(err) {
113
+ return false, nil
114
+ }
115
+ return err == nil, err
116
+ })
117
+ operatorRequire(t, err)
118
+ }
119
+ preflight := options
120
+ preflight.DryRun = []string{metav1.DryRunAll}
121
+ _, err := ApplyRollout(ctx, client, documents("v1"), preflight, operation+"-preflight")
122
+ operatorRequire(t, err)
123
+ live, err := ApplyRollout(ctx, client, documents("v1"), options, operation+"-v1")
124
+ if err != nil {
125
+ t.Fatalf("initial rollout: %v; live=%v", err, live)
126
+ }
127
+ uid := live.GetUID()
128
+ checkHTTP(name+"-stable", "v1")
129
+ if strategy == "canary" {
130
+ checkHTTP("r6-traefik", "v1")
131
+ }
132
+ for _, version := range []string{"v2", "v1"} {
133
+ items := documents(version)
134
+ op := operation + "-" + version + "-update"
135
+ finished := make(chan error, 1)
136
+ go func() { _, err := ApplyRollout(ctx, client, items, options, op); finished <- err }()
137
+ var paused RolloutObservation
138
+ completed := false
139
+ err := wait.PollUntilContextTimeout(ctx, 250*time.Millisecond, 70*time.Second, true, func(context.Context) (bool, error) {
140
+ select {
141
+ case err := <-finished:
142
+ if err == nil && version == "v1" {
143
+ completed = true
144
+ return true, nil
145
+ }
146
+ if err == nil {
147
+ return false, fmt.Errorf("update completed without a manual pause")
148
+ }
149
+ return false, err
150
+ default:
151
+ }
152
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
153
+ if err != nil {
154
+ return false, nil
155
+ }
156
+ paused = observed
157
+ if !paused.State.Paused || paused.State.ObservedGeneration == nil || *paused.State.ObservedGeneration != paused.State.Generation {
158
+ return false, nil
159
+ }
160
+ if strategy == "blueGreen" {
161
+ if len(paused.State.Metrics) == 0 {
162
+ return false, nil
163
+ }
164
+ for _, metric := range paused.State.Metrics {
165
+ if metric.Phase != "Successful" {
166
+ return false, nil
167
+ }
168
+ }
169
+ }
170
+ return true, nil
171
+ })
172
+ operatorRequire(t, err)
173
+ if completed {
174
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
175
+ operatorRequire(t, err)
176
+ if observed.Health != "healthy" || observed.State.UID != string(uid) {
177
+ t.Fatal("native fast rollback lost health or identity")
178
+ }
179
+ checkHTTP(name+"-stable", version)
180
+ if strategy == "canary" {
181
+ checkHTTP("r6-traefik", version)
182
+ }
183
+ t.Logf("%s native fast rollback restored known healthy v1 and actual HTTP", strategy)
184
+ continue
185
+ }
186
+ previous := "v1"
187
+ if version == "v1" {
188
+ previous = "v2"
189
+ }
190
+ checkHTTP(name+"-stable", previous)
191
+ checkHTTP(name+"-preview", version)
192
+ if strategy == "canary" {
193
+ if paused.State.CanaryWeight == nil || *paused.State.CanaryWeight != 20 || paused.State.StableWeight == nil || *paused.State.StableWeight != 80 {
194
+ t.Fatalf("wrong native traffic weights: %+v", paused.State)
195
+ }
196
+ counts := map[string]int{}
197
+ for i := 0; i < 80; i++ {
198
+ version, err := readHTTP("r6-traefik")
199
+ operatorRequire(t, err)
200
+ counts[version]++
201
+ }
202
+ if counts[previous] == 0 || counts[version] == 0 || len(counts) != 2 {
203
+ t.Fatalf("actual routed traffic did not reach both revisions: %v", counts)
204
+ }
205
+ t.Logf("actual routed traffic at 20 percent: %v", counts)
206
+ }
207
+ control(items, op, "promote")
208
+ select {
209
+ case err := <-finished:
210
+ operatorRequire(t, err)
211
+ case <-ctx.Done():
212
+ t.Fatal(ctx.Err())
213
+ }
214
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
215
+ operatorRequire(t, err)
216
+ if observed.Health != "healthy" || observed.State.UID != string(uid) {
217
+ t.Fatalf("updated rollout is not healthy with the same UID: %+v", observed)
218
+ }
219
+ if len(observed.State.Metrics) == 0 {
220
+ t.Fatal("completed rollout lost its native analysis evidence")
221
+ }
222
+ for _, metric := range observed.State.Metrics {
223
+ if metric.Phase != "Successful" || metric.Count != metric.TargetCount || metric.StartedAt == "" || metric.FinishedAt == "" || metric.StartedAt >= metric.FinishedAt {
224
+ t.Fatalf("completed rollout lost its full successful analysis window: %+v", metric)
225
+ }
226
+ }
227
+ checkHTTP(name+"-stable", version)
228
+ if strategy == "canary" {
229
+ checkHTTP("r6-traefik", version)
230
+ }
231
+ t.Logf("%s version %s passed actual HTTP, analysis and same-UID reconciliation", strategy, version)
232
+ }
233
+ for _, mode := range []string{"bad", "cancel"} {
234
+ items, op := documents(mode), operation+"-"+mode
235
+ runCtx, stop := context.WithCancel(ctx)
236
+ finished := make(chan error, 1)
237
+ go func() { _, err := ApplyRollout(runCtx, client, items, options, op); finished <- err }()
238
+ if mode == "cancel" || strategy == "canary" {
239
+ err := wait.PollUntilContextTimeout(ctx, 250*time.Millisecond, 70*time.Second, true, func(context.Context) (bool, error) {
240
+ select {
241
+ case err := <-finished:
242
+ return false, fmt.Errorf("rollout finished before control: %v", err)
243
+ default:
244
+ }
245
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
246
+ return err == nil && observed.State.Paused && observed.State.ObservedGeneration != nil && *observed.State.ObservedGeneration == observed.State.Generation, nil
247
+ })
248
+ operatorRequire(t, err)
249
+ if mode == "bad" {
250
+ control(items, op, "promote")
251
+ } else {
252
+ control(items, op, "pause")
253
+ operatorRequire(t, wait.PollUntilContextTimeout(ctx, 200*time.Millisecond, 10*time.Second, true, func(context.Context) (bool, error) {
254
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
255
+ return err == nil && observed.State.ObservedGeneration != nil && *observed.State.ObservedGeneration == observed.State.Generation, nil
256
+ }))
257
+ control(items, op, "promote") // Remove spec.paused; retain the native promotion pause.
258
+ stop()
259
+ }
260
+ }
261
+ select {
262
+ case err := <-finished:
263
+ stop()
264
+ if err == nil || errors.Is(err, ErrRolloutStopUnconfirmed) {
265
+ t.Fatalf("%s did not fail with confirmed native stop: %v", mode, err)
266
+ }
267
+ case <-ctx.Done():
268
+ stop()
269
+ t.Fatal(ctx.Err())
270
+ }
271
+ observed, err := ObserveRollout(ctx, client, items, owner, op)
272
+ operatorRequire(t, err)
273
+ if !rolloutStopped(observed.State) || observed.State.UID != string(uid) {
274
+ t.Fatalf("%s left active traffic: %+v", mode, observed.State)
275
+ }
276
+ checkHTTP(name+"-stable", "v1")
277
+ if strategy == "canary" {
278
+ checkHTTP("r6-traefik", "v1")
279
+ }
280
+ _, err = ApplyRollout(ctx, client, documents("v1"), options, op+"-restore")
281
+ operatorRequire(t, err)
282
+ checkHTTP(name+"-stable", "v1")
283
+ t.Logf("%s %s confirmed stable traffic, retained failed desired version, then explicit v1 restore", strategy, mode)
284
+ }
285
+ })
286
+ }
287
+ }
@@ -0,0 +1,252 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "slices"
7
+ "strconv"
8
+ "strings"
9
+
10
+ "github.com/opensoha/soha-contracts/gen/go/sohaapi"
11
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13
+ "k8s.io/client-go/dynamic"
14
+ )
15
+
16
+ type RolloutObservation struct {
17
+ State sohaapi.ProgressiveRolloutStatus
18
+ Health string
19
+ Drifted bool
20
+ Resources []*unstructured.Unstructured
21
+ }
22
+
23
+ func ObserveRollout(ctx context.Context, client dynamic.Interface, documents []*unstructured.Unstructured, owner, operation string) (RolloutObservation, error) {
24
+ result := RolloutObservation{Health: "unknown"}
25
+ desired, err := ValidateRolloutResources(documents)
26
+ if err != nil {
27
+ return result, err
28
+ }
29
+ root, err := client.Resource(rollouts).Namespace(desired.GetNamespace()).Get(ctx, desired.GetName(), metav1.GetOptions{})
30
+ if err != nil {
31
+ return result, err
32
+ }
33
+ if err := validateRolloutOwner(root, owner, desired.GetName()); err != nil {
34
+ return result, err
35
+ }
36
+ if operation != "" && root.GetAnnotations()[RolloutOperationAnnotation] != operation {
37
+ return result, fmt.Errorf("rollout operation was superseded")
38
+ }
39
+ result.State = rolloutState(root)
40
+ actual := map[string]*unstructured.Unstructured{}
41
+ for _, document := range documents {
42
+ live := root
43
+ if !IsArgoRollout(document) {
44
+ gvk := document.GroupVersionKind()
45
+ live, err = client.Resource(rolloutResourceTypes[gvk.Group+"/"+gvk.Kind]).Namespace(document.GetNamespace()).Get(ctx, document.GetName(), metav1.GetOptions{})
46
+ if err != nil {
47
+ return result, err
48
+ }
49
+ if err := validateRolloutOwner(live, owner, desired.GetName()); err != nil {
50
+ return result, err
51
+ }
52
+ }
53
+ result.Resources = append(result.Resources, live)
54
+ actual[document.GetKind()+"/"+document.GetName()] = live
55
+ result.Drifted = result.Drifted || !RolloutMatchesDesired(document, live)
56
+ }
57
+ state := &result.State
58
+ active, preview := actual["Service/"+state.ActiveService], actual["Service/"+state.PreviewService]
59
+ if active == nil || preview == nil {
60
+ return result, fmt.Errorf("rollout traffic Services are missing")
61
+ }
62
+ state.ActiveRevision = argoString(active, "spec", "selector", rolloutPodHash)
63
+ state.PreviewRevision = argoString(preview, "spec", "selector", rolloutPodHash)
64
+ if state.Strategy == "canary" {
65
+ name := argoString(root, "spec", "strategy", "canary", "trafficRouting", "traefik", "weightedTraefikServiceName")
66
+ weighted := actual["TraefikService/"+name]
67
+ if weighted == nil {
68
+ return result, fmt.Errorf("weighted traffic resource is missing")
69
+ }
70
+ backends, _, _ := unstructured.NestedSlice(weighted.Object, "spec", "weighted", "services")
71
+ for _, raw := range backends {
72
+ backend, _ := raw.(map[string]any)
73
+ weight, ok := backend["weight"].(int64)
74
+ if !ok || weight < 0 || weight > 100 {
75
+ return result, fmt.Errorf("invalid observed traffic weight")
76
+ }
77
+ value := int(weight)
78
+ if backend["name"] == state.ActiveService {
79
+ state.StableWeight = &value
80
+ }
81
+ if backend["name"] == state.PreviewService {
82
+ state.CanaryWeight = &value
83
+ }
84
+ }
85
+ if state.StableWeight == nil || state.CanaryWeight == nil || *state.StableWeight+*state.CanaryWeight != 100 {
86
+ return result, fmt.Errorf("observed traffic weights are incomplete")
87
+ }
88
+ }
89
+ if err := observeRolloutAnalyses(ctx, client, root, state); err != nil {
90
+ return result, err
91
+ }
92
+ result.Health = "progressing"
93
+ current := state.ObservedGeneration != nil && *state.ObservedGeneration == state.Generation
94
+ if result.Drifted || current && (state.Phase == "Degraded" || state.Aborted) {
95
+ result.Health = "degraded"
96
+ return result, nil
97
+ }
98
+ if current && state.Phase == "Healthy" && !state.Paused && state.CurrentRevision != "" && state.StableRevision == state.CurrentRevision && state.ActiveRevision == state.CurrentRevision {
99
+ result.Health = "healthy"
100
+ if state.Strategy == "canary" && (*state.StableWeight != 100 || *state.CanaryWeight != 0) {
101
+ result.Health = "progressing"
102
+ }
103
+ }
104
+ return result, nil
105
+ }
106
+
107
+ func rolloutState(root *unstructured.Unstructured) sohaapi.ProgressiveRolloutStatus {
108
+ state := sohaapi.ProgressiveRolloutStatus{Name: root.GetName(), Namespace: root.GetNamespace(), UID: string(root.GetUID()), ResourceVersion: root.GetResourceVersion(), Generation: root.GetGeneration(), OperationID: root.GetAnnotations()[RolloutOperationAnnotation], Phase: argoString(root, "status", "phase"), StableRevision: argoString(root, "status", "stableRS"), CurrentRevision: argoString(root, "status", "currentPodHash"), Metrics: []sohaapi.ProgressiveRolloutMetric{}, PauseReasons: []string{}}
109
+ if generation, err := strconv.ParseInt(argoString(root, "status", "observedGeneration"), 10, 64); err == nil && generation >= 0 {
110
+ state.ObservedGeneration = &generation
111
+ }
112
+ state.Paused, _, _ = unstructured.NestedBool(root.Object, "spec", "paused")
113
+ state.Aborted, _, _ = unstructured.NestedBool(root.Object, "status", "abort")
114
+ pauses, _, _ := unstructured.NestedSlice(root.Object, "status", "pauseConditions")
115
+ for _, raw := range pauses {
116
+ pause, _ := raw.(map[string]any)
117
+ reason, _ := pause["reason"].(string)
118
+ state.PauseReasons = append(state.PauseReasons, reason)
119
+ }
120
+ state.Paused = state.Paused || len(pauses) > 0
121
+ if argoString(root, "spec", "strategy", "blueGreen", "activeService") != "" {
122
+ state.Strategy = "blueGreen"
123
+ state.ActiveService = argoString(root, "spec", "strategy", "blueGreen", "activeService")
124
+ state.PreviewService = argoString(root, "spec", "strategy", "blueGreen", "previewService")
125
+ } else {
126
+ state.Strategy = "canary"
127
+ state.ActiveService = argoString(root, "spec", "strategy", "canary", "stableService")
128
+ state.PreviewService = argoString(root, "spec", "strategy", "canary", "canaryService")
129
+ steps, _, _ := unstructured.NestedSlice(root.Object, "spec", "strategy", "canary", "steps")
130
+ count := len(steps)
131
+ state.TotalSteps = &count
132
+ if step, found, _ := unstructured.NestedInt64(root.Object, "status", "currentStepIndex"); found && step >= 0 {
133
+ index := int(step)
134
+ state.CurrentStep = &index
135
+ }
136
+ }
137
+ return state
138
+ }
139
+
140
+ // RolloutMatchesDesired ignores only the native traffic controller's weights.
141
+ // Service hash selectors and spec.paused are absent from the approved template.
142
+ func RolloutMatchesDesired(desired, live *unstructured.Unstructured) bool {
143
+ wanted := desired.DeepCopy()
144
+ if wanted.GetKind() == "TraefikService" {
145
+ backends, _, _ := unstructured.NestedSlice(wanted.Object, "spec", "weighted", "services")
146
+ for _, raw := range backends {
147
+ backend, _ := raw.(map[string]any)
148
+ delete(backend, "weight")
149
+ }
150
+ _ = unstructured.SetNestedSlice(wanted.Object, backends, "spec", "weighted", "services")
151
+ }
152
+ return argoMatchesDesired(wanted.Object, live.Object)
153
+ }
154
+
155
+ func observeRolloutAnalyses(ctx context.Context, client dynamic.Interface, root *unstructured.Unstructured, state *sohaapi.ProgressiveRolloutStatus) error {
156
+ runs := []*unstructured.Unstructured{}
157
+ seen := map[string]bool{}
158
+ for _, path := range [][]string{{"canary", "currentStepAnalysisRunStatus"}, {"canary", "currentBackgroundAnalysisRunStatus"}, {"blueGreen", "prePromotionAnalysisRunStatus"}, {"blueGreen", "postPromotionAnalysisRunStatus"}} {
159
+ name := argoString(root, "status", path[0], path[1], "name")
160
+ if name == "" {
161
+ continue
162
+ }
163
+ run, err := client.Resource(analysisRuns).Namespace(root.GetNamespace()).Get(ctx, name, metav1.GetOptions{})
164
+ if err != nil {
165
+ return err
166
+ }
167
+ owner := metav1.GetControllerOf(run)
168
+ if owner == nil || owner.Kind != "Rollout" || owner.APIVersion != root.GetAPIVersion() || owner.Name != root.GetName() || owner.UID != root.GetUID() || run.GetUID() == "" || run.GetLabels()[rolloutPodHash] != state.CurrentRevision {
169
+ return fmt.Errorf("analysis observation does not belong to this Rollout revision")
170
+ }
171
+ runs = append(runs, run)
172
+ seen[run.GetName()] = true
173
+ }
174
+ // Rollouts clears currentStepAnalysisRunStatus when a canary completes.
175
+ // Retained runs keep the controller owner, Pod hash and native revision, so
176
+ // the completed task can preserve this revision's full observation evidence.
177
+ revision := root.GetAnnotations()["rollout.argoproj.io/revision"]
178
+ if revision != "" && state.CurrentRevision != "" {
179
+ options := metav1.ListOptions{LabelSelector: rolloutPodHash + "=" + state.CurrentRevision, Limit: 100}
180
+ for {
181
+ page, err := client.Resource(analysisRuns).Namespace(root.GetNamespace()).List(ctx, options)
182
+ if err != nil {
183
+ return err
184
+ }
185
+ for _, candidate := range page.Items {
186
+ owner := metav1.GetControllerOf(&candidate)
187
+ if seen[candidate.GetName()] || owner == nil || owner.Kind != "Rollout" || owner.APIVersion != root.GetAPIVersion() || owner.Name != root.GetName() || owner.UID != root.GetUID() || candidate.GetUID() == "" || candidate.GetAnnotations()["rollout.argoproj.io/revision"] != revision {
188
+ continue
189
+ }
190
+ runs = append(runs, candidate.DeepCopy())
191
+ seen[candidate.GetName()] = true
192
+ }
193
+ if page.GetContinue() == "" {
194
+ break
195
+ }
196
+ options.Continue = page.GetContinue()
197
+ }
198
+ }
199
+ slices.SortFunc(runs, func(a, b *unstructured.Unstructured) int { return strings.Compare(a.GetName(), b.GetName()) })
200
+ for _, run := range runs {
201
+ metrics, _, _ := unstructured.NestedSlice(run.Object, "spec", "metrics")
202
+ results, _, _ := unstructured.NestedSlice(run.Object, "status", "metricResults")
203
+ for _, raw := range metrics {
204
+ metric, _ := raw.(map[string]any)
205
+ name, _ := metric["name"].(string)
206
+ count, _ := metric["count"].(int64)
207
+ condition, _ := metric["successCondition"].(string)
208
+ item := sohaapi.ProgressiveRolloutMetric{AnalysisRun: run.GetName(), UID: string(run.GetUID()), Name: name, Phase: argoString(run, "status", "phase"), Interval: fmt.Sprint(metric["interval"]), TargetCount: int(count), SuccessCondition: condition}
209
+ for _, raw := range results {
210
+ result, _ := raw.(map[string]any)
211
+ if result["name"] != name {
212
+ continue
213
+ }
214
+ item.Phase, _ = result["phase"].(string)
215
+ for key, target := range map[string]*int{"count": &item.Count, "successful": &item.Successful, "failed": &item.Failed} {
216
+ value, _ := result[key].(int64)
217
+ *target = int(value)
218
+ }
219
+ measurements, _ := result["measurements"].([]any)
220
+ if len(measurements) > 0 {
221
+ last, _ := measurements[len(measurements)-1].(map[string]any)
222
+ item.Value, _ = last["value"].(string)
223
+ first, _ := measurements[0].(map[string]any)
224
+ item.StartedAt, _ = first["startedAt"].(string)
225
+ item.FinishedAt, _ = last["finishedAt"].(string)
226
+ }
227
+ }
228
+ state.Metrics = append(state.Metrics, item)
229
+ }
230
+ }
231
+ return nil
232
+ }
233
+
234
+ func validateRolloutOwner(object *unstructured.Unstructured, owner, rootName string) error {
235
+ if owner == "" || object.GetAnnotations()[RolloutOwnerAnnotation] != owner+":"+rootName || object.GetUID() == "" || object.GetResourceVersion() == "" || object.GetDeletionTimestamp() != nil {
236
+ return fmt.Errorf("%w: rollout resource has another owner or is being deleted", ErrResourceOwnership)
237
+ }
238
+ clean := object.DeepCopy()
239
+ clean.SetKind("OwnedRolloutResource")
240
+ annotations := clean.GetAnnotations()
241
+ delete(annotations, RolloutOwnerAnnotation)
242
+ clean.SetAnnotations(annotations)
243
+ if err := ValidateDirectManifestOwner(clean); err != nil {
244
+ return err
245
+ }
246
+ for _, field := range object.GetManagedFields() {
247
+ if strings.HasPrefix(field.Manager, "opensoha-") && field.Manager != owner {
248
+ return fmt.Errorf("%w: rollout resource has another Soha field owner", ErrResourceOwnership)
249
+ }
250
+ }
251
+ return nil
252
+ }