@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,385 @@
1
+ package runtime
2
+
3
+ import (
4
+ "fmt"
5
+ "net/url"
6
+ "reflect"
7
+ "slices"
8
+ "strings"
9
+ "time"
10
+
11
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12
+ "k8s.io/apimachinery/pkg/runtime/schema"
13
+ )
14
+
15
+ const (
16
+ RolloutOwnerAnnotation = "delivery.soha.io/rollout-owner"
17
+ RolloutOperationAnnotation = "delivery.soha.io/rollout-operation"
18
+ rolloutPodHash = "rollouts-pod-template-hash"
19
+ )
20
+
21
+ var rollouts = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "rollouts"}
22
+ var analysisRuns = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "analysisruns"}
23
+ var rolloutResourceTypes = map[string]schema.GroupVersionResource{
24
+ "argoproj.io/Rollout": rollouts,
25
+ "argoproj.io/AnalysisTemplate": {Group: "argoproj.io", Version: "v1alpha1", Resource: "analysistemplates"},
26
+ "/Service": {Version: "v1", Resource: "services"},
27
+ "traefik.io/TraefikService": {Group: "traefik.io", Version: "v1alpha1", Resource: "traefikservices"},
28
+ "traefik.io/IngressRoute": {Group: "traefik.io", Version: "v1alpha1", Resource: "ingressroutes"},
29
+ }
30
+
31
+ func IsArgoRollout(object *unstructured.Unstructured) bool {
32
+ return object != nil && object.GetAPIVersion() == "argoproj.io/v1alpha1" && object.GetKind() == "Rollout"
33
+ }
34
+
35
+ // ValidateRolloutResources admits native blue/green and Traefik canaries. Every
36
+ // traffic and analysis reference belongs to the same frozen namespace bundle.
37
+ func ValidateRolloutResources(documents []*unstructured.Unstructured) (*unstructured.Unstructured, error) {
38
+ objects := map[string]*unstructured.Unstructured{}
39
+ var root *unstructured.Unstructured
40
+ for _, object := range documents {
41
+ if object == nil {
42
+ return nil, fmt.Errorf("rollout document is missing")
43
+ }
44
+ gvk := object.GroupVersionKind()
45
+ gvr, ok := rolloutResourceTypes[gvk.Group+"/"+gvk.Kind]
46
+ key := object.GetKind() + "/" + object.GetName()
47
+ if !ok || gvk.Version != gvr.Version || object.GetName() == "" || object.GetNamespace() == "" || objects[key] != nil {
48
+ return nil, fmt.Errorf("unsupported or repeated rollout document")
49
+ }
50
+ if object.GetUID() != "" || object.GetResourceVersion() != "" || object.GetGenerateName() != "" || object.GetDeletionTimestamp() != nil || len(object.GetOwnerReferences()) != 0 || len(object.GetFinalizers()) != 0 || object.Object["status"] != nil {
51
+ return nil, fmt.Errorf("rollout documents must contain independent desired resources")
52
+ }
53
+ if IsArgoRollout(object) {
54
+ if root != nil {
55
+ return nil, fmt.Errorf("rollout bundle requires exactly one Rollout")
56
+ }
57
+ root = object
58
+ }
59
+ objects[key] = object
60
+ }
61
+ if root == nil || len(objects) > 20 {
62
+ return nil, fmt.Errorf("rollout bundle requires one Rollout and at most 19 dependencies")
63
+ }
64
+ for _, object := range documents {
65
+ if object.GetNamespace() != root.GetNamespace() {
66
+ return nil, fmt.Errorf("rollout dependencies must stay in the target namespace")
67
+ }
68
+ clean := object.DeepCopy()
69
+ clean.SetKind("DesiredResource")
70
+ if err := ValidateDirectManifestOwner(clean); err != nil {
71
+ return nil, err
72
+ }
73
+ for key := range object.GetAnnotations() {
74
+ if strings.HasPrefix(key, "rollout.argoproj.io/") || strings.HasPrefix(key, "rollouts.argoproj.io/") || key == RolloutOwnerAnnotation || key == RolloutOperationAnnotation {
75
+ return nil, fmt.Errorf("rollout controller annotations are executor-owned")
76
+ }
77
+ }
78
+ }
79
+ spec, _, _ := unstructured.NestedMap(root.Object, "spec")
80
+ if err := rolloutFields(spec, "replicas", "revisionHistoryLimit", "selector", "template", "strategy", "progressDeadlineSeconds", "progressDeadlineAbort", "minReadySeconds"); err != nil {
81
+ return nil, err
82
+ }
83
+ selector, _, _ := unstructured.NestedStringMap(root.Object, "spec", "selector", "matchLabels")
84
+ selectorSpec, _, _ := unstructured.NestedMap(root.Object, "spec", "selector")
85
+ if len(selector) == 0 || len(selectorSpec) != 1 || selector[rolloutPodHash] != "" {
86
+ return nil, fmt.Errorf("rollout requires fixed matchLabels without a controller hash")
87
+ }
88
+ labels, _, _ := unstructured.NestedStringMap(root.Object, "spec", "template", "metadata", "labels")
89
+ for key, value := range selector {
90
+ if labels[key] != value {
91
+ return nil, fmt.Errorf("rollout Pod labels do not match its selector")
92
+ }
93
+ }
94
+ for _, field := range []string{"containers", "initContainers"} {
95
+ containers, _, err := unstructured.NestedSlice(root.Object, "spec", "template", "spec", field)
96
+ if err != nil || field == "containers" && len(containers) == 0 {
97
+ return nil, fmt.Errorf("rollout requires container images")
98
+ }
99
+ for _, raw := range containers {
100
+ container, _ := raw.(map[string]any)
101
+ image, _ := container["image"].(string)
102
+ if _, parsed, err := ParseArgoImageOverride(image); err != nil || parsed != image {
103
+ return nil, fmt.Errorf("rollout images require immutable digests")
104
+ }
105
+ }
106
+ }
107
+ used := map[string]bool{"Rollout/" + root.GetName(): true}
108
+ strategy, _, _ := unstructured.NestedMap(root.Object, "spec", "strategy")
109
+ if len(strategy) != 1 {
110
+ return nil, fmt.Errorf("choose exactly one native rollout strategy")
111
+ }
112
+ var serviceNames []string
113
+ if raw, ok := strategy["blueGreen"]; ok {
114
+ blue, _ := raw.(map[string]any)
115
+ if err := rolloutFields(blue, "activeService", "previewService", "autoPromotionEnabled", "prePromotionAnalysis", "postPromotionAnalysis", "scaleDownDelaySeconds", "abortScaleDownDelaySeconds", "previewReplicaCount"); err != nil {
116
+ return nil, err
117
+ }
118
+ if blue["autoPromotionEnabled"] != false {
119
+ return nil, fmt.Errorf("blue/green requires explicit promotion")
120
+ }
121
+ serviceNames = []string{argoString(root, "spec", "strategy", "blueGreen", "activeService"), argoString(root, "spec", "strategy", "blueGreen", "previewService")}
122
+ if err := rolloutAnalysisReference(blue["prePromotionAnalysis"], objects, used); err != nil {
123
+ return nil, err
124
+ }
125
+ if raw := blue["postPromotionAnalysis"]; raw != nil {
126
+ if err := rolloutAnalysisReference(raw, objects, used); err != nil {
127
+ return nil, err
128
+ }
129
+ }
130
+ } else if raw, ok := strategy["canary"]; ok {
131
+ canary, _ := raw.(map[string]any)
132
+ if err := rolloutFields(canary, "stableService", "canaryService", "trafficRouting", "steps", "maxSurge", "maxUnavailable", "scaleDownDelaySeconds", "abortScaleDownDelaySeconds"); err != nil {
133
+ return nil, err
134
+ }
135
+ serviceNames = []string{argoString(root, "spec", "strategy", "canary", "stableService"), argoString(root, "spec", "strategy", "canary", "canaryService")}
136
+ routing, _, _ := unstructured.NestedMap(root.Object, "spec", "strategy", "canary", "trafficRouting")
137
+ traefik, _ := routing["traefik"].(map[string]any)
138
+ weighted, _ := traefik["weightedTraefikServiceName"].(string)
139
+ if len(routing) != 1 || len(traefik) != 1 || weighted == "" {
140
+ return nil, fmt.Errorf("canary requires a frozen Traefik weighted service")
141
+ }
142
+ if err := validateRolloutTraffic(objects, used, serviceNames, weighted); err != nil {
143
+ return nil, err
144
+ }
145
+ steps, _ := canary["steps"].([]any)
146
+ weight, analyses := int64(-1), 0
147
+ if len(steps) == 0 || len(steps) > 30 {
148
+ return nil, fmt.Errorf("canary requires 1 to 30 explicit steps")
149
+ }
150
+ for _, raw := range steps {
151
+ step, _ := raw.(map[string]any)
152
+ if len(step) != 1 {
153
+ return nil, fmt.Errorf("canary step requires one action")
154
+ }
155
+ switch {
156
+ case step["setWeight"] != nil:
157
+ next, ok := step["setWeight"].(int64)
158
+ if !ok || next <= weight || next < 0 || next > 100 {
159
+ return nil, fmt.Errorf("canary weights must increase from 0 to 100")
160
+ }
161
+ weight = next
162
+ case step["pause"] != nil:
163
+ pause, ok := step["pause"].(map[string]any)
164
+ if !ok || rolloutFields(pause, "duration") != nil {
165
+ return nil, fmt.Errorf("invalid canary pause")
166
+ }
167
+ if value := pause["duration"]; value != nil {
168
+ if !validRolloutDuration(value) {
169
+ return nil, fmt.Errorf("pause duration must be positive and at most 24 hours")
170
+ }
171
+ }
172
+ case step["analysis"] != nil:
173
+ if err := rolloutAnalysisReference(step["analysis"], objects, used); err != nil {
174
+ return nil, err
175
+ }
176
+ analyses++
177
+ default:
178
+ return nil, fmt.Errorf("unsupported canary step")
179
+ }
180
+ }
181
+ if weight != 100 || analyses == 0 {
182
+ return nil, fmt.Errorf("canary requires an analysis window and a final 100 percent step")
183
+ }
184
+ } else {
185
+ return nil, fmt.Errorf("unsupported rollout strategy")
186
+ }
187
+ if serviceNames[0] == "" || serviceNames[0] == serviceNames[1] {
188
+ return nil, fmt.Errorf("rollout requires two distinct frozen Services")
189
+ }
190
+ for _, name := range serviceNames {
191
+ service := objects["Service/"+name]
192
+ if service == nil {
193
+ return nil, fmt.Errorf("rollout Service %q is not frozen", name)
194
+ }
195
+ actual, _, _ := unstructured.NestedStringMap(service.Object, "spec", "selector")
196
+ if !reflect.DeepEqual(selector, actual) || argoString(service, "spec", "type") != "" && argoString(service, "spec", "type") != "ClusterIP" || argoString(service, "spec", "externalName") != "" {
197
+ return nil, fmt.Errorf("rollout Services require the fixed Pod selector and ClusterIP routing")
198
+ }
199
+ ports, _, _ := unstructured.NestedSlice(service.Object, "spec", "ports")
200
+ if len(ports) == 0 {
201
+ return nil, fmt.Errorf("rollout Service ports are required")
202
+ }
203
+ for _, raw := range ports {
204
+ port, ok := raw.(map[string]any)
205
+ if !ok || len(port) == 0 {
206
+ return nil, fmt.Errorf("rollout Service port must be an object")
207
+ }
208
+ }
209
+ used["Service/"+name] = true
210
+ }
211
+ for key, object := range objects {
212
+ if !used[key] {
213
+ return nil, fmt.Errorf("unreferenced rollout dependency %s", key)
214
+ }
215
+ if object.GetKind() == "AnalysisTemplate" {
216
+ if err := validateRolloutMetrics(object, serviceNames); err != nil {
217
+ return nil, err
218
+ }
219
+ }
220
+ }
221
+ return root, nil
222
+ }
223
+
224
+ func rolloutFields(object map[string]any, allowed ...string) error {
225
+ for key := range object {
226
+ if !slices.Contains(allowed, key) {
227
+ return fmt.Errorf("unsupported rollout field %q", key)
228
+ }
229
+ }
230
+ return nil
231
+ }
232
+
233
+ func validRolloutDuration(value any) bool {
234
+ var duration time.Duration
235
+ switch value := value.(type) {
236
+ case int64:
237
+ if value > 0 && value <= 86400 {
238
+ return true
239
+ }
240
+ case string:
241
+ duration, _ = time.ParseDuration(value)
242
+ }
243
+ return duration > 0 && duration <= 24*time.Hour
244
+ }
245
+
246
+ func rolloutAnalysisReference(raw any, objects map[string]*unstructured.Unstructured, used map[string]bool) error {
247
+ analysis, _ := raw.(map[string]any)
248
+ if len(analysis) != 1 {
249
+ return fmt.Errorf("rollout analysis requires only frozen namespaced templates")
250
+ }
251
+ templates, _ := analysis["templates"].([]any)
252
+ if len(templates) == 0 {
253
+ return fmt.Errorf("rollout analysis template is required")
254
+ }
255
+ for _, raw := range templates {
256
+ template, _ := raw.(map[string]any)
257
+ name, _ := template["templateName"].(string)
258
+ if len(template) != 1 || objects["AnalysisTemplate/"+name] == nil {
259
+ return fmt.Errorf("analysis must reference a frozen namespaced AnalysisTemplate")
260
+ }
261
+ used["AnalysisTemplate/"+name] = true
262
+ }
263
+ return nil
264
+ }
265
+
266
+ func validateRolloutMetrics(object *unstructured.Unstructured, services []string) error {
267
+ spec, _, _ := unstructured.NestedMap(object.Object, "spec")
268
+ if len(spec) != 1 {
269
+ return fmt.Errorf("AnalysisTemplate supports only frozen web metrics")
270
+ }
271
+ metrics, _ := spec["metrics"].([]any)
272
+ if len(metrics) == 0 || len(metrics) > 10 {
273
+ return fmt.Errorf("analysis requires 1 to 10 metrics")
274
+ }
275
+ names := map[string]bool{}
276
+ for _, raw := range metrics {
277
+ metric, _ := raw.(map[string]any)
278
+ if err := rolloutFields(metric, "name", "interval", "count", "successCondition", "failureCondition", "failureLimit", "inconclusiveLimit", "consecutiveErrorLimit", "provider"); err != nil {
279
+ return err
280
+ }
281
+ name, _ := metric["name"].(string)
282
+ count, _ := metric["count"].(int64)
283
+ success, _ := metric["successCondition"].(string)
284
+ interval, _ := time.ParseDuration(fmt.Sprint(metric["interval"]))
285
+ if name == "" || names[name] || count < 2 || count > 1000 || interval <= 0 || interval > 24*time.Hour/time.Duration(count) || success == "" {
286
+ return fmt.Errorf("each metric requires a unique name, a finite observation window and a success condition")
287
+ }
288
+ names[name] = true
289
+ provider, _ := metric["provider"].(map[string]any)
290
+ web, _ := provider["web"].(map[string]any)
291
+ if len(provider) != 1 || len(web) == 0 || rolloutFields(web, "url", "jsonPath", "timeoutSeconds") != nil {
292
+ return fmt.Errorf("analysis supports read-only web metrics without embedded credentials")
293
+ }
294
+ endpoint, _ := web["url"].(string)
295
+ parsed, err := url.Parse(endpoint)
296
+ if err != nil || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Scheme != "http" && parsed.Scheme != "https" {
297
+ return fmt.Errorf("metric URL must reference a frozen Service without credentials")
298
+ }
299
+ valid := false
300
+ for _, service := range services {
301
+ prefix := service + "." + object.GetNamespace() + ".svc"
302
+ valid = valid || parsed.Hostname() == prefix || parsed.Hostname() == prefix+".cluster.local"
303
+ }
304
+ if !valid {
305
+ return fmt.Errorf("metric URL must stay within the frozen rollout Services")
306
+ }
307
+ }
308
+ return nil
309
+ }
310
+
311
+ func validateRolloutTraffic(objects map[string]*unstructured.Unstructured, used map[string]bool, services []string, name string) error {
312
+ if len(services) != 2 {
313
+ return fmt.Errorf("canary requires stable and preview Services")
314
+ }
315
+ stableService, previewService := services[0], services[1]
316
+ weighted := objects["TraefikService/"+name]
317
+ if weighted == nil {
318
+ return fmt.Errorf("weighted TraefikService is not frozen")
319
+ }
320
+ spec, _, _ := unstructured.NestedMap(weighted.Object, "spec")
321
+ config, _ := spec["weighted"].(map[string]any)
322
+ backends, _ := config["services"].([]any)
323
+ if len(spec) != 1 || len(config) != 1 || len(backends) != 2 {
324
+ return fmt.Errorf("canary requires two direct weighted Service backends")
325
+ }
326
+ seen := map[string]bool{}
327
+ for _, raw := range backends {
328
+ backend, _ := raw.(map[string]any)
329
+ if err := rolloutFields(backend, "name", "port", "weight", "kind"); err != nil {
330
+ return err
331
+ }
332
+ name, _ := backend["name"].(string)
333
+ weight, _ := backend["weight"].(int64)
334
+ port, _ := backend["port"].(int64)
335
+ if !slices.Contains(services, name) || seen[name] || port < 1 || port > 65535 || name == stableService && weight != 100 || name == previewService && weight != 0 || backend["kind"] != nil && backend["kind"] != "Service" {
336
+ return fmt.Errorf("weighted backends must start at stable 100 / canary 0")
337
+ }
338
+ seen[name] = true
339
+ service := objects["Service/"+name]
340
+ if service == nil {
341
+ return fmt.Errorf("weighted backend Service is not frozen")
342
+ }
343
+ ports, _, _ := unstructured.NestedSlice(service.Object, "spec", "ports")
344
+ matched := false
345
+ for _, raw := range ports {
346
+ item, _ := raw.(map[string]any)
347
+ matched = matched || item["port"] == port && (item["protocol"] == nil || item["protocol"] == "TCP")
348
+ }
349
+ if !matched {
350
+ return fmt.Errorf("weighted backend port does not match its frozen Service")
351
+ }
352
+ }
353
+ used["TraefikService/"+name] = true
354
+ routeCount := 0
355
+ for key, object := range objects {
356
+ if object.GetKind() != "IngressRoute" {
357
+ continue
358
+ }
359
+ spec, _, _ := unstructured.NestedMap(object.Object, "spec")
360
+ if err := rolloutFields(spec, "entryPoints", "routes"); err != nil {
361
+ return err
362
+ }
363
+ routes, _ := spec["routes"].([]any)
364
+ if len(routes) != 1 {
365
+ return fmt.Errorf("canary requires one explicit HTTP route")
366
+ }
367
+ route, _ := routes[0].(map[string]any)
368
+ if err := rolloutFields(route, "kind", "match", "services"); err != nil {
369
+ return err
370
+ }
371
+ backends, _ := route["services"].([]any)
372
+ if route["kind"] != "Rule" || len(backends) != 1 {
373
+ return fmt.Errorf("canary route must use its frozen weighted service")
374
+ }
375
+ backend, _ := backends[0].(map[string]any)
376
+ if len(backend) != 2 || backend["name"] != name || backend["kind"] != "TraefikService" {
377
+ return fmt.Errorf("canary route must use its frozen weighted service")
378
+ }
379
+ used[key], routeCount = true, routeCount+1
380
+ }
381
+ if routeCount != 1 {
382
+ return fmt.Errorf("canary requires exactly one frozen IngressRoute")
383
+ }
384
+ return nil
385
+ }
@@ -0,0 +1,165 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "crypto/sha256"
6
+ "encoding/hex"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/opensoha/soha-contracts/gen/go/sohaapi"
14
+ contractresource "github.com/opensoha/soha-contracts/resource"
15
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17
+ "k8s.io/client-go/dynamic"
18
+ )
19
+
20
+ type rolloutDriftResource = struct {
21
+ APIVersion string `json:"apiVersion"`
22
+ Fields []sohaapi.ManifestDriftField `json:"fields"`
23
+ Kind string `json:"kind"`
24
+ Name string `json:"name"`
25
+ Namespace string `json:"namespace"`
26
+ }
27
+
28
+ func IsRolloutTask(payload sohaapi.ManifestExecutionTaskPayload) bool {
29
+ if payload.RolloutControl != nil || payload.Action == sohaapi.ManifestTaskActionRolloutControl {
30
+ return true
31
+ }
32
+ for _, document := range payload.Documents {
33
+ if document.APIVersion == "argoproj.io/v1alpha1" && document.Kind == "Rollout" {
34
+ return true
35
+ }
36
+ }
37
+ return false
38
+ }
39
+
40
+ // ExecuteRolloutTask is shared by the control plane and Agent. It verifies the
41
+ // frozen wire payload before any Kubernetes write and uses native reconciliation.
42
+ func ExecuteRolloutTask(ctx context.Context, client dynamic.Interface, payload sohaapi.ManifestExecutionTaskPayload) (sohaapi.ManifestExecutionTaskResult, error) {
43
+ result := sohaapi.ManifestExecutionTaskResult{Action: payload.Action, DeploymentID: payload.DeploymentID, Generation: payload.Generation, RenderedDigest: payload.RenderedDigest, Inventory: []sohaapi.ManifestResourceInventory{}, Diagnostics: []sohaapi.ManifestDiagnostic{}}
44
+ if payload.BindingID == "" || payload.FieldManager != "opensoha-manifest/"+payload.BindingID || payload.Namespace == "" || payload.IdempotencyKey == "" || payload.ForceConflicts || len(payload.GitOpsDocuments) > 0 {
45
+ return result, fmt.Errorf("rollout requires a frozen namespace, stable binding owner and operation; force and mixed GitOps are disabled")
46
+ }
47
+ documents := make([]*unstructured.Unstructured, 0, len(payload.Documents))
48
+ for _, document := range payload.Documents {
49
+ desired := &unstructured.Unstructured{}
50
+ if err := desired.UnmarshalJSON([]byte(document.Content)); err != nil {
51
+ return result, fmt.Errorf("decode frozen rollout: %w", err)
52
+ }
53
+ sum := sha256.Sum256([]byte(document.Content))
54
+ if desired.GetAPIVersion() != document.APIVersion || desired.GetKind() != document.Kind || desired.GetNamespace() != document.Namespace || desired.GetName() != document.Name || document.Namespace != payload.Namespace || !strings.EqualFold(document.ContentDigest, hex.EncodeToString(sum[:])) {
55
+ return result, fmt.Errorf("rollout document identity, namespace or digest does not match its frozen content")
56
+ }
57
+ documents = append(documents, desired)
58
+ }
59
+ if _, err := ValidateRolloutResources(documents); err != nil {
60
+ return result, err
61
+ }
62
+ options := metav1.PatchOptions{FieldManager: payload.FieldManager}
63
+ var observation RolloutObservation
64
+ var err error
65
+ switch payload.Action {
66
+ case sohaapi.ManifestTaskActionPreflight:
67
+ options.DryRun = []string{metav1.DryRunAll}
68
+ _, err = ApplyRollout(ctx, client, documents, options, payload.IdempotencyKey)
69
+ if err != nil {
70
+ result.Diagnostics = append(result.Diagnostics, sohaapi.ManifestDiagnostic{Stage: "dry_run", Severity: "error", Code: "rollout_preflight", Message: err.Error()})
71
+ }
72
+ result.Preflight = &sohaapi.ManifestPreflightResult{Ready: err == nil, Capability: "available", RenderedDigest: payload.RenderedDigest, ResourceCount: len(documents), Diagnostics: result.Diagnostics}
73
+ return result, nil
74
+ case sohaapi.ManifestTaskActionApply, sohaapi.ManifestTaskActionRepair, sohaapi.ManifestTaskActionRollback:
75
+ _, err = ApplyRollout(ctx, client, documents, options, payload.IdempotencyKey)
76
+ case sohaapi.ManifestTaskActionObserve:
77
+ case sohaapi.ManifestTaskActionRolloutControl:
78
+ if payload.RolloutControl == nil {
79
+ return result, fmt.Errorf("rollout control is required")
80
+ }
81
+ observation, err = ControlRollout(ctx, client, documents, payload.FieldManager, payload.IdempotencyKey, *payload.RolloutControl)
82
+ default:
83
+ return result, fmt.Errorf("unsupported rollout action %q", payload.Action)
84
+ }
85
+ if payload.Action != sohaapi.ManifestTaskActionRolloutControl {
86
+ query, stop := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
87
+ defer stop()
88
+ var observeErr error
89
+ operation := payload.IdempotencyKey
90
+ if payload.Action == sohaapi.ManifestTaskActionObserve {
91
+ operation = ""
92
+ }
93
+ observation, observeErr = ObserveRollout(query, client, documents, payload.FieldManager, operation)
94
+ err = errors.Join(err, observeErr)
95
+ }
96
+ if err != nil {
97
+ stage := sohaapi.ManifestValidationStageApply
98
+ if payload.Action == sohaapi.ManifestTaskActionObserve {
99
+ stage = sohaapi.ManifestValidationStageObserve
100
+ }
101
+ result.Diagnostics = append(result.Diagnostics, sohaapi.ManifestDiagnostic{Stage: stage, Severity: "error", Code: "rollout_runtime", Message: err.Error()})
102
+ }
103
+ if observation.State.UID == "" {
104
+ return result, err
105
+ }
106
+ result.Rollout = &observation.State
107
+ result.Drift = &sohaapi.ManifestDriftReport{Drifted: observation.Drifted, ObservedAt: time.Now().UTC(), Resources: []rolloutDriftResource{}, EvidenceRefs: []string{}}
108
+ for index, desired := range documents {
109
+ for _, live := range observation.Resources {
110
+ if desired.GroupVersionKind() != live.GroupVersionKind() || desired.GetName() != live.GetName() {
111
+ continue
112
+ }
113
+ document := payload.Documents[index]
114
+ projected := projectRolloutObject(desired.Object, live.Object)
115
+ encoded, _ := json.Marshal(projected)
116
+ sum := sha256.Sum256(encoded)
117
+ item := sohaapi.ManifestResourceInventory{DeploymentID: payload.DeploymentID, Generation: payload.Generation, APIVersion: document.APIVersion, Kind: document.Kind, Namespace: live.GetNamespace(), Name: live.GetName(), UID: string(live.GetUID()), ResourceVersion: live.GetResourceVersion(), ResourceGeneration: live.GetGeneration(), DesiredObjectDigest: document.ContentDigest, ObservedObjectDigest: hex.EncodeToString(sum[:]), Health: contractresource.ManifestHealth(live.Object), LastObservedAt: time.Now().UTC(), Finalizers: live.GetFinalizers()}
118
+ if RolloutMatchesDesired(desired, live) {
119
+ // The immutable policy is matched; live controller weights are
120
+ // separately exposed in rollout rather than treated as spec drift.
121
+ item.ObservedObjectDigest = document.ContentDigest
122
+ } else {
123
+ result.Drift.Resources = append(result.Drift.Resources, rolloutDriftResource{APIVersion: document.APIVersion, Kind: document.Kind, Namespace: document.Namespace, Name: document.Name, Fields: []sohaapi.ManifestDriftField{{Path: "/", DesiredValue: desired.Object, ObservedValue: projected}}})
124
+ }
125
+ if IsArgoRollout(live) {
126
+ item.Health, item.ObservedResourceGeneration = observation.Health, observation.State.ObservedGeneration
127
+ }
128
+ if live.GetKind() == "AnalysisTemplate" || live.GetKind() == "TraefikService" || live.GetKind() == "IngressRoute" {
129
+ // These configuration resources have no Ready condition. Their
130
+ // frozen policy and owning rollout's traffic gate determine readiness.
131
+ item.Health = observation.Health
132
+ }
133
+ if deleting := live.GetDeletionTimestamp(); deleting != nil {
134
+ item.DeletingAt = &deleting.Time
135
+ }
136
+ result.Inventory = append(result.Inventory, item)
137
+ break
138
+ }
139
+ }
140
+ result.EvidenceRefs = []string{"rollout:" + observation.State.Namespace + "/" + observation.State.Name + ":" + observation.State.UID + ":" + observation.State.OperationID}
141
+ return result, err
142
+ }
143
+
144
+ func projectRolloutObject(desired, live any) any {
145
+ switch wanted := desired.(type) {
146
+ case map[string]any:
147
+ actual, _ := live.(map[string]any)
148
+ result := make(map[string]any, len(wanted))
149
+ for key, value := range wanted {
150
+ result[key] = projectRolloutObject(value, actual[key])
151
+ }
152
+ return result
153
+ case []any:
154
+ actual, _ := live.([]any)
155
+ result := make([]any, len(wanted))
156
+ for index, value := range wanted {
157
+ if index < len(actual) {
158
+ result[index] = projectRolloutObject(value, actual[index])
159
+ }
160
+ }
161
+ return result
162
+ default:
163
+ return live
164
+ }
165
+ }