@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
@@ -5,9 +5,9 @@ package openapi
5
5
  import _ "embed"
6
6
 
7
7
  const (
8
- Version = "0.1.17"
9
- YAMLSHA256 = "43c055eeb546b837a15cdd39bacd5c086d5257bfccc8f9fc137943200e4ea72e"
10
- JSONSHA256 = "11f96a23fa4302d4518af62b39446ff48204e97c4b9d07e561d81124c2a5e754"
8
+ Version = "0.1.19"
9
+ YAMLSHA256 = "a1edda91ecc623423ca1875777dd500f12f239ca1571d7276a23d5e6c00c4c40"
10
+ JSONSHA256 = "40f61307c9379b1496b43ff31fe8aebd82f1353100fe799f6d1ebbaefb1c3cd3"
11
11
  )
12
12
 
13
13
  //go:embed soha-api.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensoha/contracts",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "private": false,
5
5
  "description": "Shared OpenSoha API, agent, MCP, skill, event, and auth contracts.",
6
6
  "license": "Apache-2.0",
@@ -28,6 +28,7 @@
28
28
  "test": "npm run check:version-sync && npm run check:generated && npm run validate && npm run lint:openapi && npm run check:openapi-breaking && npm run check:openapi-loose-boundaries && npm run check:json-schema-compat && npm run typecheck && go test ./..."
29
29
  },
30
30
  "exports": {
31
+ "./delivery/document.schema.json": "./delivery/document.schema.json",
31
32
  "./gen/ts/sohaapi": "./gen/ts/sohaapi/index.ts",
32
33
  "./openapi/soha-api.yaml": "./openapi/soha-api.yaml",
33
34
  "./openapi/soha-api.json": "./openapi/soha-api.json",
@@ -90,6 +91,7 @@
90
91
  "./memory/memory-policy.schema.json": "./memory/memory-policy.schema.json"
91
92
  },
92
93
  "files": [
94
+ "delivery",
93
95
  "COMPATIBILITY.md",
94
96
  "go.mod",
95
97
  "go.sum",
@@ -0,0 +1,155 @@
1
+ package resource
2
+
3
+ import "encoding/json"
4
+
5
+ // ManifestHealth gives Direct and Agent execution the same conservative health
6
+ // interpretation. Successful persistence alone is health evidence only for
7
+ // built-in configuration objects, never for an unknown custom resource.
8
+ func ManifestHealth(object map[string]any) string {
9
+ if object == nil {
10
+ return "unknown"
11
+ }
12
+ encoded, err := json.Marshal(object)
13
+ if err != nil {
14
+ return "unknown"
15
+ }
16
+ var item manifestHealthObject
17
+ if err := json.Unmarshal(encoded, &item); err != nil {
18
+ return "unknown"
19
+ }
20
+ if item.Metadata.DeletionTimestamp != "" {
21
+ return "degraded"
22
+ }
23
+ kind := item.APIVersion + "/" + item.Kind
24
+ switch kind {
25
+ case "workloads.soha.io/v1alpha1/WorkloadCronJob":
26
+ // Ready describes configuration synchronization. Its source and owned
27
+ // CronJob must also be observed by the runtime before declaring health.
28
+ if item.conditionHealth(true) == "degraded" {
29
+ return "degraded"
30
+ }
31
+ return "unknown"
32
+ case "v1/ConfigMap", "v1/Secret", "v1/ServiceAccount", "rbac.authorization.k8s.io/v1/Role", "rbac.authorization.k8s.io/v1/RoleBinding", "networking.k8s.io/v1/NetworkPolicy":
33
+ return "healthy"
34
+ case "apps/v1/Deployment", "apps/v1/StatefulSet", "apps/v1/DaemonSet":
35
+ return item.workloadHealth()
36
+ case "v1/PersistentVolumeClaim":
37
+ if item.Status.Phase == "Bound" {
38
+ return "healthy"
39
+ }
40
+ if item.Status.Phase == "Lost" {
41
+ return "degraded"
42
+ }
43
+ return "progressing"
44
+ case "v1/Service":
45
+ if item.Spec.Type != "LoadBalancer" || len(item.Status.LoadBalancer.Ingress) > 0 {
46
+ return "healthy"
47
+ }
48
+ return "progressing"
49
+ case "v1/Pod", "batch/v1/Job":
50
+ if item.Status.Phase == "Succeeded" {
51
+ return "healthy"
52
+ }
53
+ if item.Status.Phase == "Failed" {
54
+ return "degraded"
55
+ }
56
+ return item.conditionHealth(false)
57
+ default:
58
+ return item.conditionHealth(true)
59
+ }
60
+ }
61
+
62
+ type manifestHealthObject struct {
63
+ APIVersion string `json:"apiVersion"`
64
+ Kind string `json:"kind"`
65
+ Metadata struct {
66
+ Generation int64 `json:"generation"`
67
+ DeletionTimestamp string `json:"deletionTimestamp"`
68
+ } `json:"metadata"`
69
+ Spec struct {
70
+ Replicas *int64 `json:"replicas"`
71
+ Type string `json:"type"`
72
+ UpdateStrategy struct {
73
+ RollingUpdate struct {
74
+ Partition int64 `json:"partition"`
75
+ } `json:"rollingUpdate"`
76
+ } `json:"updateStrategy"`
77
+ } `json:"spec"`
78
+ Status struct {
79
+ ObservedGeneration int64 `json:"observedGeneration"`
80
+ Replicas int64 `json:"replicas"`
81
+ ReadyReplicas int64 `json:"readyReplicas"`
82
+ UpdatedReplicas int64 `json:"updatedReplicas"`
83
+ AvailableReplicas int64 `json:"availableReplicas"`
84
+ DesiredNumberScheduled int64 `json:"desiredNumberScheduled"`
85
+ UpdatedNumberScheduled int64 `json:"updatedNumberScheduled"`
86
+ NumberAvailable int64 `json:"numberAvailable"`
87
+ Phase string `json:"phase"`
88
+ LoadBalancer struct {
89
+ Ingress []json.RawMessage `json:"ingress"`
90
+ } `json:"loadBalancer"`
91
+ Conditions []struct {
92
+ Type string `json:"type"`
93
+ Status string `json:"status"`
94
+ Reason string `json:"reason"`
95
+ ObservedGeneration int64 `json:"observedGeneration"`
96
+ } `json:"conditions"`
97
+ } `json:"status"`
98
+ }
99
+
100
+ func (item manifestHealthObject) workloadHealth() string {
101
+ if item.Metadata.Generation < 1 || item.Status.ObservedGeneration < item.Metadata.Generation {
102
+ return "progressing"
103
+ }
104
+ if item.conditionHealth(false) == "degraded" {
105
+ return "degraded"
106
+ }
107
+ desired := int64(1)
108
+ if item.Spec.Replicas != nil {
109
+ desired = *item.Spec.Replicas
110
+ }
111
+ healthy := false
112
+ switch item.Kind {
113
+ case "Deployment":
114
+ healthy = item.Status.UpdatedReplicas == desired && item.Status.Replicas == desired && item.Status.AvailableReplicas >= desired
115
+ case "StatefulSet":
116
+ updated := max(int64(0), desired-item.Spec.UpdateStrategy.RollingUpdate.Partition)
117
+ healthy = item.Status.ReadyReplicas >= desired && item.Status.UpdatedReplicas >= updated
118
+ case "DaemonSet":
119
+ healthy = item.Status.UpdatedNumberScheduled == item.Status.DesiredNumberScheduled && item.Status.NumberAvailable >= item.Status.DesiredNumberScheduled
120
+ }
121
+ if healthy {
122
+ return "healthy"
123
+ }
124
+ return "progressing"
125
+ }
126
+
127
+ func (item manifestHealthObject) conditionHealth(requireCurrentGeneration bool) string {
128
+ result := "progressing"
129
+ notReady := false
130
+ if requireCurrentGeneration {
131
+ result = "unknown"
132
+ }
133
+ for _, condition := range item.Status.Conditions {
134
+ if requireCurrentGeneration && (item.Metadata.Generation < 1 || max(condition.ObservedGeneration, item.Status.ObservedGeneration) < item.Metadata.Generation) {
135
+ continue
136
+ }
137
+ if condition.Status == "True" && (condition.Type == "Failed" || condition.Type == "Degraded" || condition.Type == "ReplicaFailure") {
138
+ return "degraded"
139
+ }
140
+ if condition.Type == "Progressing" && condition.Status == "False" && condition.Reason == "ProgressDeadlineExceeded" {
141
+ return "degraded"
142
+ }
143
+ if condition.Type == "Ready" || condition.Type == "Available" || condition.Type == "Complete" || condition.Type == "Established" {
144
+ if condition.Status == "True" {
145
+ result = "healthy"
146
+ } else {
147
+ notReady = true
148
+ }
149
+ }
150
+ }
151
+ if notReady {
152
+ return "progressing"
153
+ }
154
+ return result
155
+ }
@@ -0,0 +1,37 @@
1
+ package resource
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ )
7
+
8
+ func TestManifestHealth(t *testing.T) {
9
+ for _, test := range []struct{ name, object, want string }{
10
+ {"configuration", `{"apiVersion":"v1","kind":"ConfigMap"}`, "healthy"},
11
+ {"unknown CR", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2}}`, "unknown"},
12
+ {"custom kind cannot impersonate built-in", `{"apiVersion":"example.com/v1","kind":"ConfigMap"}`, "unknown"},
13
+ {"unobserved generation", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":1,"replicas":1,"updatedReplicas":1,"availableReplicas":1,"conditions":[{"type":"Available","status":"True"}]}}`, "progressing"},
14
+ {"old replicas still available", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":2,"replicas":2,"updatedReplicas":1,"availableReplicas":2}}`, "progressing"},
15
+ {"available but not updated", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":2,"replicas":1,"updatedReplicas":0,"availableReplicas":1}}`, "progressing"},
16
+ {"deployment converged", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":2,"replicas":1,"updatedReplicas":1,"availableReplicas":1}}`, "healthy"},
17
+ {"deployment failed after available", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"status":{"observedGeneration":2,"replicas":1,"updatedReplicas":1,"availableReplicas":1,"conditions":[{"type":"Available","status":"True"},{"type":"Progressing","status":"False","reason":"ProgressDeadlineExceeded"}]}}`, "degraded"},
18
+ {"stale custom ready", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"conditions":[{"type":"Ready","status":"True","observedGeneration":1}]}}`, "unknown"},
19
+ {"current custom ready", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"conditions":[{"type":"Ready","status":"True","observedGeneration":2}]}}`, "healthy"},
20
+ {"conflicting current ready conditions", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"observedGeneration":2,"conditions":[{"type":"Available","status":"True"},{"type":"Ready","status":"False"}]}}`, "progressing"},
21
+ {"current custom failure", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"observedGeneration":2,"conditions":[{"type":"Ready","status":"True"},{"type":"Failed","status":"True"}]}}`, "degraded"},
22
+ {"deleting object", `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"deletionTimestamp":"2026-09-12T00:00:00Z"}}`, "degraded"},
23
+ {"pod not ready", `{"apiVersion":"v1","kind":"Pod","status":{"phase":"Running","conditions":[{"type":"Ready","status":"False"}]}}`, "progressing"},
24
+ {"statefulset partition", `{"apiVersion":"apps/v1","kind":"StatefulSet","metadata":{"generation":2},"spec":{"replicas":3,"updateStrategy":{"rollingUpdate":{"partition":2}}},"status":{"observedGeneration":2,"readyReplicas":3,"updatedReplicas":1}}`, "healthy"},
25
+ {"daemonset rolling", `{"apiVersion":"apps/v1","kind":"DaemonSet","metadata":{"generation":2},"status":{"observedGeneration":2,"desiredNumberScheduled":3,"updatedNumberScheduled":2,"numberAvailable":3}}`, "progressing"},
26
+ } {
27
+ t.Run(test.name, func(t *testing.T) {
28
+ var object map[string]any
29
+ if err := json.Unmarshal([]byte(test.object), &object); err != nil {
30
+ t.Fatal(err)
31
+ }
32
+ if got := ManifestHealth(object); got != test.want {
33
+ t.Fatalf("health = %s, want %s", got, test.want)
34
+ }
35
+ })
36
+ }
37
+ }
@@ -1097,6 +1097,10 @@ type CRDView struct {
1097
1097
  }
1098
1098
 
1099
1099
  type CustomResourceView struct {
1100
+ UID string `json:"uid,omitempty"`
1101
+ Generation int64 `json:"generation,omitempty"`
1102
+ DeletingAt string `json:"deletingAt,omitempty"`
1103
+ Finalizers []string `json:"finalizers,omitempty"`
1100
1104
  APIVersion string `json:"apiVersion,omitempty"`
1101
1105
  Kind string `json:"kind"`
1102
1106
  Name string `json:"name"`
@@ -1107,6 +1111,16 @@ type CustomResourceView struct {
1107
1111
  AllowedActions []string `json:"allowedActions,omitempty"`
1108
1112
  }
1109
1113
 
1114
+ // CustomResourceYAMLRequest is the Core/Agent custom-resource transport.
1115
+ // ExpectedUID fences deletion against replacement after a user viewed the object.
1116
+ type CustomResourceYAMLRequest struct {
1117
+ Definition CRDResourceDefinition `json:"definition"`
1118
+ Namespace string `json:"namespace"`
1119
+ Name string `json:"name,omitempty"`
1120
+ Content string `json:"content,omitempty"`
1121
+ ExpectedUID string `json:"expectedUid,omitempty"`
1122
+ }
1123
+
1110
1124
  type HelmReleaseView struct {
1111
1125
  Name string `json:"name"`
1112
1126
  Namespace string `json:"namespace"`
@@ -0,0 +1,166 @@
1
+ package runtime
2
+
3
+ import (
4
+ "encoding/hex"
5
+ "fmt"
6
+ "net/url"
7
+ "path"
8
+ "reflect"
9
+ "regexp"
10
+ "slices"
11
+ "strings"
12
+
13
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14
+ "k8s.io/apimachinery/pkg/runtime/schema"
15
+ )
16
+
17
+ const (
18
+ ArgoOwnerAnnotation = "delivery.soha.io/argocd-owner"
19
+ ArgoOperationAnnotation = "delivery.soha.io/argocd-operation"
20
+ )
21
+
22
+ var argoApplications = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
23
+ var argoImageOverride = regexp.MustCompile(`^(?:([A-Za-z0-9][A-Za-z0-9._:/-]*)=)?([A-Za-z0-9][A-Za-z0-9._:/-]*)@sha256:[a-f0-9]{64}$`)
24
+
25
+ // ParseArgoImageOverride accepts Kustomize's image@digest and name=image@digest forms.
26
+ func ParseArgoImageOverride(value string) (name, image string, err error) {
27
+ parts := argoImageOverride.FindStringSubmatch(value)
28
+ if len(parts) != 3 || strings.LastIndex(parts[2], ":") > strings.LastIndex(parts[2], "/") {
29
+ return "", "", fmt.Errorf("GitOps image overrides require an immutable image digest without a tag")
30
+ }
31
+ name = parts[1]
32
+ if name == "" {
33
+ name = parts[2]
34
+ }
35
+ _, image, found := strings.Cut(value, "=")
36
+ if !found {
37
+ image = value
38
+ }
39
+ return name, image, nil
40
+ }
41
+
42
+ func IsArgoApplication(object *unstructured.Unstructured) bool {
43
+ return object != nil && object.GetAPIVersion() == "argoproj.io/v1alpha1" && object.GetKind() == "Application"
44
+ }
45
+
46
+ // ValidateArgoApplication defines the supported, single-namespace GitOps path.
47
+ // AppProject and repository credentials remain administrator-managed in Argo CD.
48
+ func ValidateArgoApplication(object *unstructured.Unstructured) error {
49
+ if !IsArgoApplication(object) || object.GetNamespace() == "" || object.GetName() == "" {
50
+ return fmt.Errorf("GitOps requires a namespaced Argo CD Application")
51
+ }
52
+ if object.GetDeletionTimestamp() != nil || len(object.GetOwnerReferences()) != 0 || len(object.GetFinalizers()) != 0 {
53
+ return fmt.Errorf("GitOps Application must be an independent, non-deleting root without cascading deletion")
54
+ }
55
+ for key := range object.GetAnnotations() {
56
+ if strings.HasPrefix(key, "argocd.argoproj.io/") {
57
+ return fmt.Errorf("GitOps Application must not override controller behavior with annotations")
58
+ }
59
+ }
60
+ spec, _, err := unstructured.NestedMap(object.Object, "spec")
61
+ if err != nil {
62
+ return err
63
+ }
64
+ if err := argoAllowedFields(spec, "project", "source", "destination", "syncPolicy"); err != nil {
65
+ return err
66
+ }
67
+ if argoString(object, "spec", "project") == "" || argoString(object, "spec", "project") == "default" {
68
+ return fmt.Errorf("GitOps requires a dedicated restricted AppProject")
69
+ }
70
+ destination, _, err := unstructured.NestedStringMap(object.Object, "spec", "destination")
71
+ if err != nil || len(destination) != 2 || destination["namespace"] != object.GetNamespace() || destination["server"] != "https://kubernetes.default.svc" {
72
+ return fmt.Errorf("GitOps destination must be this cluster and the Application namespace")
73
+ }
74
+ source, _, err := unstructured.NestedMap(object.Object, "spec", "source")
75
+ if err != nil {
76
+ return err
77
+ }
78
+ if err := argoAllowedFields(source, "repoURL", "targetRevision", "path", "kustomize"); err != nil {
79
+ return err
80
+ }
81
+ revision := argoString(object, "spec", "source", "targetRevision")
82
+ if decoded, err := hex.DecodeString(revision); err != nil || len(decoded) != 20 && len(decoded) != 32 {
83
+ return fmt.Errorf("GitOps targetRevision must be a full immutable Git commit")
84
+ }
85
+ repository, err := url.Parse(argoString(object, "spec", "source", "repoURL"))
86
+ if err != nil || repository.Host == "" || repository.User != nil || repository.RawQuery != "" || repository.Fragment != "" || (repository.Scheme != "https" && repository.Scheme != "http" && repository.Scheme != "ssh") {
87
+ return fmt.Errorf("GitOps repository must be an HTTP(S) or SSH URL without embedded credentials")
88
+ }
89
+ directory := argoString(object, "spec", "source", "path")
90
+ if directory == "" || path.IsAbs(directory) || path.Clean(directory) != directory || directory == ".." || strings.HasPrefix(directory, "../") || strings.Contains(directory, "\\") {
91
+ return fmt.Errorf("GitOps path must stay within the pinned repository")
92
+ }
93
+ kustomize, _, err := unstructured.NestedMap(object.Object, "spec", "source", "kustomize")
94
+ if err != nil {
95
+ return err
96
+ }
97
+ if err := argoAllowedFields(kustomize, "images", "namespace"); err != nil {
98
+ return err
99
+ }
100
+ if argoString(object, "spec", "source", "kustomize", "namespace") != object.GetNamespace() {
101
+ return fmt.Errorf("GitOps Kustomize namespace must be explicitly fixed to the delivery namespace")
102
+ }
103
+ images, _, err := unstructured.NestedStringSlice(object.Object, "spec", "source", "kustomize", "images")
104
+ if err != nil || len(images) == 0 || len(images) > 16 {
105
+ return fmt.Errorf("GitOps requires 1 to 16 fixed Kustomize image overrides")
106
+ }
107
+ imageNames := map[string]bool{}
108
+ for _, image := range images {
109
+ name, _, err := ParseArgoImageOverride(image)
110
+ if err != nil {
111
+ return err
112
+ }
113
+ if imageNames[name] {
114
+ return fmt.Errorf("GitOps image overrides must use unique image names")
115
+ }
116
+ imageNames[name] = true
117
+ }
118
+ policy, _, err := unstructured.NestedMap(object.Object, "spec", "syncPolicy")
119
+ if err != nil {
120
+ return err
121
+ }
122
+ if err := argoAllowedFields(policy, "syncOptions"); err != nil {
123
+ return err
124
+ }
125
+ options, _, err := unstructured.NestedStringSlice(object.Object, "spec", "syncPolicy", "syncOptions")
126
+ if err != nil || !reflect.DeepEqual(options, []string{"FailOnSharedResource=true"}) {
127
+ return fmt.Errorf("GitOps requires manual sync and FailOnSharedResource=true")
128
+ }
129
+ return nil
130
+ }
131
+
132
+ func argoAllowedFields(object map[string]any, allowed ...string) error {
133
+ for key := range object {
134
+ if !slices.Contains(allowed, key) {
135
+ return fmt.Errorf("unsupported GitOps field %q", key)
136
+ }
137
+ }
138
+ return nil
139
+ }
140
+
141
+ func argoString(object *unstructured.Unstructured, fields ...string) string {
142
+ value, _, _ := unstructured.NestedString(object.Object, fields...)
143
+ return value
144
+ }
145
+
146
+ func argoOperationID(object *unstructured.Unstructured, fields ...string) string {
147
+ items, _, _ := unstructured.NestedSlice(object.Object, append(fields, "info")...)
148
+ for _, raw := range items {
149
+ item, _ := raw.(map[string]any)
150
+ if item["name"] == ArgoOperationAnnotation {
151
+ value, _ := item["value"].(string)
152
+ return value
153
+ }
154
+ }
155
+ return ""
156
+ }
157
+
158
+ var argoResourceTypes = map[string]schema.GroupVersionResource{
159
+ "apps/Deployment": {Group: "apps", Version: "v1", Resource: "deployments"},
160
+ "apps/StatefulSet": {Group: "apps", Version: "v1", Resource: "statefulsets"},
161
+ "/Service": {Version: "v1", Resource: "services"},
162
+ "/ConfigMap": {Version: "v1", Resource: "configmaps"},
163
+ "/Secret": {Version: "v1", Resource: "secrets"},
164
+ "/ServiceAccount": {Version: "v1", Resource: "serviceaccounts"},
165
+ "/PersistentVolumeClaim": {Version: "v1", Resource: "persistentvolumeclaims"},
166
+ }
@@ -0,0 +1,142 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "net/url"
8
+ "os"
9
+ "strings"
10
+ "testing"
11
+ "time"
12
+
13
+ "github.com/google/uuid"
14
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
15
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17
+ "k8s.io/apimachinery/pkg/runtime/schema"
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 TestArgoCDWithKubernetes(t *testing.T) {
25
+ kubeconfig := os.Getenv("SOHA_ARGOCD_TEST_KUBECONFIG")
26
+ if kubeconfig == "" {
27
+ t.Skip("set explicit loopback kubeconfig and SOHA_ARGOCD_TEST_STATE for the private GitOps fixture")
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 loopback fixture cluster is supported")
34
+ }
35
+ config.Timeout = 15 * time.Second
36
+ client, err := dynamic.NewForConfig(config)
37
+ operatorRequire(t, err)
38
+ kube, err := kubernetes.NewForConfig(config)
39
+ operatorRequire(t, err)
40
+ data, err := os.ReadFile(os.Getenv("SOHA_ARGOCD_TEST_STATE"))
41
+ operatorRequire(t, err)
42
+ var state map[string]json.RawMessage
43
+ operatorRequire(t, json.Unmarshal(data, &state))
44
+ value := func(key string) string {
45
+ var value string
46
+ operatorRequire(t, json.Unmarshal(state[key], &value))
47
+ return value
48
+ }
49
+ namespace, name := "soha-workflow-r6-gitops", "r6-gitops-runtime"
50
+ app := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "argoproj.io/v1alpha1", "kind": "Application", "metadata": map[string]any{"name": name, "namespace": namespace}, "spec": map[string]any{"project": "r6-gitops", "source": map[string]any{"repoURL": value("repositoryURL"), "path": ".", "targetRevision": value("v1"), "kustomize": map[string]any{"namespace": namespace, "images": []any{"app=" + value("image")}}}, "destination": map[string]any{"server": "https://kubernetes.default.svc", "namespace": namespace}, "syncPolicy": map[string]any{"syncOptions": []any{"FailOnSharedResource=true"}}}}}
51
+ children := func(version string) []*unstructured.Unstructured {
52
+ var documents map[string][]*unstructured.Unstructured
53
+ operatorRequire(t, json.Unmarshal(state["documents"], &documents))
54
+ if len(documents[version]) != 2 {
55
+ t.Fatal("frozen fixture requires two source-derived child documents")
56
+ }
57
+ return documents[version]
58
+ }
59
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
60
+ defer cancel()
61
+ resources := client.Resource(argoApplications).Namespace(namespace)
62
+ options := metav1.PatchOptions{FieldManager: "soha-r6-gitops-integration"}
63
+ id := uuid.NewString()
64
+ preflight := options
65
+ preflight.DryRun = []string{metav1.DryRunAll}
66
+ _, err = ApplyArgoApplication(ctx, client, app, children("v1"), preflight, id+":preflight")
67
+ operatorRequire(t, err)
68
+ if _, err = resources.Get(ctx, name, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
69
+ t.Fatal("preflight created an Application or fixture already exists")
70
+ }
71
+ t.Cleanup(func() {
72
+ if t.Failed() {
73
+ t.Log("failed fixture retained for diagnosis in its isolated namespace")
74
+ return
75
+ }
76
+ cleanup, stop := context.WithTimeout(context.Background(), 15*time.Second)
77
+ defer stop()
78
+ _ = resources.Delete(cleanup, name, metav1.DeleteOptions{})
79
+ for _, gvr := range []schema.GroupVersionResource{{Group: "apps", Version: "v1", Resource: "deployments"}, {Version: "v1", Resource: "services"}} {
80
+ _ = client.Resource(gvr).Namespace(namespace).Delete(cleanup, "r6-gitops-http", metav1.DeleteOptions{})
81
+ }
82
+ })
83
+ apply := func(version, operation string) {
84
+ operatorRequire(t, unstructured.SetNestedField(app.Object, value(version), "spec", "source", "targetRevision"))
85
+ live, err := ApplyArgoApplication(ctx, client, app, children(version), options, id+operation)
86
+ if err != nil {
87
+ var status any
88
+ if live != nil {
89
+ status = live.Object["status"]
90
+ }
91
+ t.Fatalf("sync %s: %v; status=%v", version, err, status)
92
+ }
93
+ err = wait.PollUntilContextTimeout(ctx, time.Second, 45*time.Second, true, func(ctx context.Context) (bool, error) {
94
+ live, err := resources.Get(ctx, name, metav1.GetOptions{})
95
+ if err != nil {
96
+ return false, err
97
+ }
98
+ observed, err := ObserveArgoApplication(ctx, client, app, live, children(version), options.FieldManager)
99
+ if err != nil {
100
+ return false, err
101
+ }
102
+ return observed.Health == "healthy" && observed.OperationID == id+operation && observed.Revision == value(version), nil
103
+ })
104
+ operatorRequire(t, err)
105
+ body, err := kube.CoreV1().RESTClient().Get().Namespace(namespace).Resource("services").Name("r6-gitops-http:8080").SubResource("proxy").DoRaw(ctx)
106
+ if err != nil || strings.TrimSpace(string(body)) != "gitops-"+version {
107
+ t.Fatalf("actual HTTP %s: %q %v", version, body, err)
108
+ }
109
+ t.Logf("Argo operation %s applied commit %s with owned, healthy resources and actual HTTP %s", operation, value(version), version)
110
+ }
111
+ apply("v1", ":deploy")
112
+ apply("v2", ":upgrade")
113
+ apply("v1", ":rollback")
114
+ operatorRequire(t, unstructured.SetNestedField(app.Object, value("hold"), "spec", "source", "targetRevision"))
115
+ stopping, stop := context.WithCancel(ctx)
116
+ observedHold := make(chan error, 1)
117
+ go func() {
118
+ err := wait.PollUntilContextTimeout(ctx, time.Second, 45*time.Second, true, func(ctx context.Context) (bool, error) {
119
+ live, err := resources.Get(ctx, name, metav1.GetOptions{})
120
+ if err != nil {
121
+ return false, err
122
+ }
123
+ return argoOperationID(live, "status", "operationState", "operation") == id+":cancel" && argoString(live, "status", "operationState", "phase") == "Running", nil
124
+ })
125
+ observedHold <- err
126
+ stop()
127
+ }()
128
+ live, err := ApplyArgoApplication(stopping, client, app, children("hold"), options, id+":cancel")
129
+ operatorRequire(t, <-observedHold)
130
+ if !errors.Is(err, context.Canceled) || live == nil || argoOperationActive(live) {
131
+ t.Fatalf("cancel was not confirmed: %v", err)
132
+ }
133
+ t.Log("controller confirmed cancellation; restoring through a new operation")
134
+ apply("v1", ":restore")
135
+ // The direct writer cannot adopt a resource managed by Argo CD, even with force.
136
+ deployment, err := client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace(namespace).Get(ctx, "r6-gitops-http", metav1.GetOptions{})
137
+ operatorRequire(t, err)
138
+ if ValidateDirectManifestOwner(deployment) == nil {
139
+ t.Fatal("direct writer accepted Argo CD-owned resource")
140
+ }
141
+ t.Logf("real Argo CD GitOps lifecycle passed for %s/%s", namespace, name)
142
+ }