@opensoha/contracts 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (391) hide show
  1. package/COMPATIBILITY.md +91 -0
  2. package/README.md +49 -3
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/catalog_test.go +32 -0
  5. package/auth/outpost_headers.go +17 -0
  6. package/auth/outpost_headers_test.go +16 -0
  7. package/auth/permission-catalog.json +1097 -187
  8. package/compat/consumer-revisions.json +3 -0
  9. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  10. package/compat/openapi-loose-boundaries-0.1.0.json +78 -46
  11. package/delivery/canonical.go +136 -0
  12. package/delivery/document.go +203 -0
  13. package/delivery/document.schema.json +1527 -0
  14. package/delivery/document_test.go +191 -0
  15. package/delivery/parser.go +155 -0
  16. package/evaluation/evaluation-run.schema.json +64 -10
  17. package/fixtures/json-schema/gateway-manifest/invalid/bad-execution-mode.json +73 -0
  18. package/fixtures/json-schema/gateway-manifest/invalid/unversioned-check.json +85 -0
  19. package/fixtures/json-schema/gateway-manifest/valid/checks.json +86 -0
  20. package/fixtures/json-schema/gateway-manifest/valid/versioned-task.json +73 -0
  21. package/fixtures/json-schema/network-ingest-event/invalid/duplicate-event.json +33 -0
  22. package/fixtures/json-schema/network-ingest-event/invalid/message-payload-mismatch.json +23 -0
  23. package/fixtures/json-schema/network-ingest-event/invalid/password.json +22 -0
  24. package/fixtures/json-schema/network-ingest-event/invalid/proxy-flow-destination.json +28 -0
  25. package/fixtures/json-schema/network-ingest-event/invalid/vpn-probe-negative-rtt.json +34 -0
  26. package/fixtures/json-schema/network-ingest-event/invalid/vpn-stats-secret.json +27 -0
  27. package/fixtures/json-schema/network-ingest-event/valid/endpoint-proxy-flow.json +27 -0
  28. package/fixtures/json-schema/network-ingest-event/valid/freeradius-accounting.json +25 -0
  29. package/fixtures/json-schema/network-ingest-event/valid/gateway.json +53 -0
  30. package/fixtures/json-schema/network-ingest-event/valid/vpn-probe-batch.json +45 -0
  31. package/fixtures/json-schema/network-ingest-event/valid/vpn-tunnel-stats.json +26 -0
  32. package/fixtures/json-schema/network-radius-accounting/invalid/password.json +9 -0
  33. package/fixtures/json-schema/network-radius-accounting/valid/accounting.json +16 -0
  34. package/fixtures/json-schema/network-runtime-protocol/invalid/invalid-cidr.json +31 -0
  35. package/fixtures/json-schema/network-runtime-protocol/invalid/message-payload-mismatch.json +19 -0
  36. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-raw-subscription.json +33 -0
  37. package/fixtures/json-schema/network-runtime-protocol/invalid/nas-authorization-password.json +18 -0
  38. package/fixtures/json-schema/network-runtime-protocol/invalid/nas-eap-without-certificate.json +17 -0
  39. package/fixtures/json-schema/network-runtime-protocol/invalid/private-key.json +20 -0
  40. package/fixtures/json-schema/network-runtime-protocol/invalid/raw-credential-reference.json +16 -0
  41. package/fixtures/json-schema/network-runtime-protocol/invalid/runtime-interval-too-short.json +23 -0
  42. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-bad-token.json +15 -0
  43. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-mode-override.json +16 -0
  44. package/fixtures/json-schema/network-runtime-protocol/invalid/wireguard-capability-without-key.json +19 -0
  45. package/fixtures/json-schema/network-runtime-protocol/invalid/wireguard-default-route.json +31 -0
  46. package/fixtures/json-schema/network-runtime-protocol/invalid/ztna-without-access-grant.json +17 -0
  47. package/fixtures/json-schema/network-runtime-protocol/valid/configuration-desired.json +86 -0
  48. package/fixtures/json-schema/network-runtime-protocol/valid/direct-ztna-connect.json +19 -0
  49. package/fixtures/json-schema/network-runtime-protocol/valid/direct-ztna-result.json +33 -0
  50. package/fixtures/json-schema/network-runtime-protocol/valid/internal-ztna-connect.json +19 -0
  51. package/fixtures/json-schema/network-runtime-protocol/valid/managed-vpn-macos-enrollment.json +27 -0
  52. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-app-subscription-configuration.json +32 -0
  53. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-manual-node-configuration.json +32 -0
  54. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-wireguard-configuration.json +70 -0
  55. package/fixtures/json-schema/network-runtime-protocol/valid/nas-authorization-request.json +19 -0
  56. package/fixtures/json-schema/network-runtime-protocol/valid/nas-authorization-result.json +24 -0
  57. package/fixtures/json-schema/network-runtime-protocol/valid/nas-session-command-result.json +17 -0
  58. package/fixtures/json-schema/network-runtime-protocol/valid/nas-session-command.json +27 -0
  59. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-connect-request.json +17 -0
  60. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-connect-result.json +34 -0
  61. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-managed-connect.json +15 -0
  62. package/fixtures/json-schema/network-runtime-protocol/valid/wireguard-enrollment.json +20 -0
  63. package/fixtures/json-schema/network-runtime-protocol/valid/wireguard-site-link.json +48 -0
  64. package/fixtures/openapi/AgentChatInput/invalid/empty-question.json +4 -0
  65. package/fixtures/openapi/AgentChatInput/invalid/system-role.json +9 -0
  66. package/fixtures/openapi/AgentChatInput/valid/authorized-background.json +5 -0
  67. package/fixtures/openapi/AgentChatInput/valid/conversation.json +15 -0
  68. package/fixtures/openapi/AgentRunnerToolCallRequest/invalid/model-identity.json +1 -0
  69. package/fixtures/openapi/AgentRunnerToolCallRequest/valid/search.json +1 -0
  70. package/fixtures/openapi/AnnouncementReceiptPageEnvelope/invalid/missing-read-count.json +9 -0
  71. package/fixtures/openapi/AnnouncementReceiptPageEnvelope/valid/read-and-unread.json +28 -0
  72. package/fixtures/openapi/ApplicationInput/invalid/negative-version.json +6 -0
  73. package/fixtures/openapi/ApplicationInput/invalid/zero-version.json +6 -0
  74. package/fixtures/openapi/ApplicationInput/valid/versioned.json +6 -0
  75. package/fixtures/openapi/ApprovalRequest/invalid/resolved-scopes.json +20 -0
  76. package/fixtures/openapi/ApprovalRequest/valid/resolved-scopes.json +21 -0
  77. package/fixtures/openapi/BuildTemplateInput/invalid/negative-revision.json +6 -0
  78. package/fixtures/openapi/BuildTemplateInput/valid/draft.json +11 -0
  79. package/fixtures/openapi/BuildTriggerRequest/invalid/no-ref.json +1 -0
  80. package/fixtures/openapi/BuildTriggerRequest/valid/no-environment.json +1 -0
  81. package/fixtures/openapi/BuildpacksConfiguration/invalid/mutable-builder.json +5 -0
  82. package/fixtures/openapi/BuildpacksConfiguration/invalid/privilege-override.json +6 -0
  83. package/fixtures/openapi/BuildpacksConfiguration/invalid/unsupported-platform.json +5 -0
  84. package/fixtures/openapi/BuildpacksConfiguration/valid/pinned.json +5 -0
  85. package/fixtures/openapi/CapabilityAssessment/invalid/contract.json +16 -0
  86. package/fixtures/openapi/CapabilityAssessment/valid/contract.json +16 -0
  87. package/fixtures/openapi/CapabilityPlan/invalid/contract.json +16 -0
  88. package/fixtures/openapi/CapabilityPlan/valid/contract.json +19 -0
  89. package/fixtures/openapi/CapabilityPlanValidation/invalid/contract.json +5 -0
  90. package/fixtures/openapi/CapabilityPlanValidation/valid/contract.json +11 -0
  91. package/fixtures/openapi/CapabilityTask/invalid/contract.json +33 -0
  92. package/fixtures/openapi/CapabilityTask/valid/contract.json +34 -0
  93. package/fixtures/openapi/CapabilityTaskInput/invalid/contract.json +22 -0
  94. package/fixtures/openapi/CapabilityTaskInput/valid/contract.json +22 -0
  95. package/fixtures/openapi/CapabilityValueSemantic/invalid/contract.json +4 -0
  96. package/fixtures/openapi/CapabilityValueSemantic/valid/contract.json +7 -0
  97. package/fixtures/openapi/DeliveryAccessResult/invalid/release-verification.json +8 -0
  98. package/fixtures/openapi/DeliveryAccessResult/valid/release-verification.json +8 -0
  99. package/fixtures/openapi/DeliveryBatchAssessment/invalid/release-verification.json +11 -0
  100. package/fixtures/openapi/DeliveryBatchAssessment/valid/release-verification.json +11 -0
  101. package/fixtures/openapi/DeliveryBatchAssessmentInput/invalid/release-verification.json +9 -0
  102. package/fixtures/openapi/DeliveryBatchAssessmentInput/valid/release-verification.json +8 -0
  103. package/fixtures/openapi/DeliveryBatchInput/invalid/ambiguous-source.json +19 -0
  104. package/fixtures/openapi/DeliveryBatchInput/invalid/missing-idempotency.json +16 -0
  105. package/fixtures/openapi/DeliveryBatchInput/valid/quick-deploy.json +17 -0
  106. package/fixtures/openapi/DeliveryBatchInput/valid/saved-workflow.json +5 -0
  107. package/fixtures/openapi/DeliveryBlueprintInput/invalid/unversioned-preset.json +31 -0
  108. package/fixtures/openapi/DeliveryBlueprintInput/valid/service-preset.json +32 -0
  109. package/fixtures/openapi/DeliveryDocument/invalid/unknown-field.json +24 -0
  110. package/fixtures/openapi/DeliveryDocument/invalid/unknown-kind.json +23 -0
  111. package/fixtures/openapi/DeliveryDocument/invalid/unknown-version.json +23 -0
  112. package/fixtures/openapi/DeliveryDocument/valid/build-template.json +26 -0
  113. package/fixtures/openapi/DeliveryDocument/valid/deployment-template.json +26 -0
  114. package/fixtures/openapi/DeliveryDocument/valid/legacy-dag.json +39 -0
  115. package/fixtures/openapi/DeliveryDocument/valid/workflow-template.json +23 -0
  116. package/fixtures/openapi/DeliveryDocument/valid/workflow.json +20 -0
  117. package/fixtures/openapi/DeliveryDraftInput/invalid/short-idempotency-key.json +30 -0
  118. package/fixtures/openapi/DeliveryDraftInput/valid/idempotent-create.json +30 -0
  119. package/fixtures/openapi/DeliveryDraftService/invalid/zero-version.json +7 -0
  120. package/fixtures/openapi/DeliveryDraftService/valid/versioned-template.json +9 -0
  121. package/fixtures/openapi/DeliveryExecutionHistoryPage/invalid-kind.json +9 -0
  122. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-build.json +16 -0
  123. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-empty.json +3 -0
  124. package/fixtures/openapi/DeliveryPlanApprovalInput/invalid/confirm.json +1 -0
  125. package/fixtures/openapi/DeliveryPlanApprovalInput/valid/approve.json +1 -0
  126. package/fixtures/openapi/DeliveryPlanInput/invalid/draft-manifest-revision.json +1 -0
  127. package/fixtures/openapi/DeliveryPlanInput/valid/manifest-revision.json +1 -0
  128. package/fixtures/openapi/DeliveryTargetInput/invalid/deploy-without-environment.json +7 -0
  129. package/fixtures/openapi/DeliveryTargetInput/valid/build-without-environment.json +6 -0
  130. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/duplicate-kind.json +13 -0
  131. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/embedded-credentials.json +16 -0
  132. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/empty-kinds.json +10 -0
  133. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/negative-generation.json +15 -0
  134. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/unknown-kind.json +12 -0
  135. package/fixtures/openapi/DeliveryTemplateSourceInput/valid/manual-source.json +15 -0
  136. package/fixtures/openapi/DeliveryWorkflowDefinition/invalid/unversioned-template.json +15 -0
  137. package/fixtures/openapi/DeliveryWorkflowDefinition/valid/cross-app.json +24 -0
  138. package/fixtures/openapi/DeliveryWorkflowInput/invalid/short-key.json +1 -0
  139. package/fixtures/openapi/DeliveryWorkflowInput/valid/idempotent-create.json +1 -0
  140. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-target-override.json +7 -0
  141. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-zero-version.json +6 -0
  142. package/fixtures/openapi/DeploymentTemplatePreviewInput/valid/valid.json +6 -0
  143. package/fixtures/openapi/DockerDeliveryConfiguration/invalid/empty-mappings.json +5 -0
  144. package/fixtures/openapi/DockerDeliveryConfiguration/valid/compose.json +7 -0
  145. package/fixtures/openapi/DockerDeliverySnapshot/invalid/missing-required-field.json +19 -0
  146. package/fixtures/openapi/DockerDeliverySnapshot/valid/frozen-project.json +20 -0
  147. package/fixtures/openapi/DockerOperationCallbackRequest/invalid/cancellation-acknowledged-not-boolean.json +13 -0
  148. package/fixtures/openapi/DockerOperationCallbackRequest/valid/cancellation-acknowledged.json +13 -0
  149. package/fixtures/openapi/DockerProjectInput/invalid/empty-host.json +4 -0
  150. package/fixtures/openapi/DockerProjectInput/valid/inline-idempotent.json +7 -0
  151. package/fixtures/openapi/DockerQuickCreateHostInput/invalid/capacity-type.json +1 -0
  152. package/fixtures/openapi/DockerQuickCreateHostInput/valid/capacity.json +1 -0
  153. package/fixtures/openapi/EndpointDevice/invalid/missing-owner.json +8 -0
  154. package/fixtures/openapi/EndpointDevice/valid/active.json +35 -0
  155. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/client-owned-status.json +5 -0
  156. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/oversized-network-inventory.json +18 -0
  157. package/fixtures/openapi/EndpointDeviceRegistrationInput/valid/macos.json +28 -0
  158. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/plain-credential.json +21 -0
  159. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/release-name.json +20 -0
  160. package/fixtures/openapi/HelmDeliveryConfiguration/valid/private-oci.json +20 -0
  161. package/fixtures/openapi/HelmDeliverySnapshot/invalid/secret-plan.json +31 -0
  162. package/fixtures/openapi/HelmDeliverySnapshot/invalid/unknown-operation.json +28 -0
  163. package/fixtures/openapi/HelmDeliverySnapshot/valid/first-install.json +28 -0
  164. package/fixtures/openapi/IdentityApplication/invalid/outpost-status.json +1 -0
  165. package/fixtures/openapi/IdentityApplicationOnboardingInput/invalid/missing-access.json +12 -0
  166. package/fixtures/openapi/IdentityApplicationOnboardingInput/valid/link.json +13 -0
  167. package/fixtures/openapi/IdentityOutpost/invalid/missing-runtime-status.json +9 -0
  168. package/fixtures/openapi/IdentityOutpost/valid/healthy.json +17 -0
  169. package/fixtures/openapi/IdentityOutpost/valid/unregistered.json +12 -0
  170. package/fixtures/openapi/IdentityOutpostDeployment/invalid/missing-protocol.json +3 -0
  171. package/fixtures/openapi/IdentityOutpostDeployment/valid/remote.json +6 -0
  172. package/fixtures/openapi/IdentityOutpostInput/invalid/forged-observation.json +7 -0
  173. package/fixtures/openapi/IdentityOutpostInput/valid/forward-auth.json +6 -0
  174. package/fixtures/openapi/IdentityOutpostInput/valid/legacy.json +8 -0
  175. package/fixtures/openapi/IdentityProviderSetup/invalid/fake-verification.json +1 -0
  176. package/fixtures/openapi/IdentityProviderSetup/valid/incomplete.json +1 -0
  177. package/fixtures/openapi/IdentityProviderSetup/valid/remote.json +1 -0
  178. package/fixtures/openapi/IdentityProviderUserMetadata/invalid/attributes.json +8 -0
  179. package/fixtures/openapi/IdentityProviderUserMetadata/valid/attributes.json +13 -0
  180. package/fixtures/openapi/KubernetesCustomResource/invalid/negative-generation.json +1 -0
  181. package/fixtures/openapi/KubernetesCustomResource/valid/deleting-operator.json +1 -0
  182. package/fixtures/openapi/KubernetesResourceCreationReceipt/invalid/manifest-content.json +20 -0
  183. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/confirmed.json +20 -0
  184. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/unknown.json +19 -0
  185. package/fixtures/openapi/LLMUpstreamTestResultEnvelope/invalid/models-not-array.json +9 -0
  186. package/fixtures/openapi/LLMUpstreamTestResultEnvelope/valid/discovered-models.json +11 -0
  187. package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/negative-resource-observation.json +57 -0
  188. package/fixtures/openapi/ManifestDeploymentEnvelope/valid/operator-deleting.json +57 -0
  189. package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/gitops-empty.json +7 -0
  190. package/fixtures/openapi/ManifestExecutionTaskPayload/valid/gitops-frozen-children.json +18 -0
  191. package/fixtures/openapi/ManifestKustomizeOptions/invalid/escaping-entry.json +1 -0
  192. package/fixtures/openapi/ManifestKustomizeOptions/invalid/mutable-image.json +1 -0
  193. package/fixtures/openapi/ManifestKustomizeOptions/valid/overlay-images.json +7 -0
  194. package/fixtures/openapi/ManifestRevisionInput/invalid/missing-version.json +1 -0
  195. package/fixtures/openapi/ManifestRevisionInput/valid/current-draft.json +1 -0
  196. package/fixtures/openapi/ManifestServiceTemplateInputs/invalid/mutable-image.json +8 -0
  197. package/fixtures/openapi/ManifestServiceTemplateInputs/valid/frozen-service-image.json +9 -0
  198. package/fixtures/openapi/NetworkAccessGrant/invalid/token-digest.json +15 -0
  199. package/fixtures/openapi/NetworkAccessGrant/valid/issued.json +14 -0
  200. package/fixtures/openapi/NetworkAccessGrantInput/invalid/no-resources.json +7 -0
  201. package/fixtures/openapi/NetworkAccessGrantInput/valid/direct-ztna.json +8 -0
  202. package/fixtures/openapi/NetworkAccessGrantSecret/invalid/short-token.json +17 -0
  203. package/fixtures/openapi/NetworkAccessGrantSecret/valid/issued.json +17 -0
  204. package/fixtures/openapi/NetworkAccessPolicy/invalid/missing-version.json +20 -0
  205. package/fixtures/openapi/NetworkAccessPolicy/valid/published-draft.json +21 -0
  206. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/duplicate-team.json +17 -0
  207. package/fixtures/openapi/NetworkAccessPolicyInput/valid/team-and-tag-allow.json +17 -0
  208. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/ipv6-range.json +9 -0
  209. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/runtime-ranges.json +15 -0
  210. package/fixtures/openapi/NetworkConnectionOption/invalid/secret.json +10 -0
  211. package/fixtures/openapi/NetworkConnectionOption/valid/wifi.json +9 -0
  212. package/fixtures/openapi/NetworkConnectionOption/valid/wired.json +8 -0
  213. package/fixtures/openapi/NetworkGateway/invalid/missing-site.json +7 -0
  214. package/fixtures/openapi/NetworkGateway/valid/online.json +23 -0
  215. package/fixtures/openapi/NetworkGatewayInput/invalid/default-route-overlay.json +13 -0
  216. package/fixtures/openapi/NetworkGatewayInput/valid/routed.json +14 -0
  217. package/fixtures/openapi/NetworkLease/invalid/empty-cidrs.json +12 -0
  218. package/fixtures/openapi/NetworkLease/valid/active.json +12 -0
  219. package/fixtures/openapi/NetworkMihomoProfile/invalid/subscription-leak.json +20 -0
  220. package/fixtures/openapi/NetworkMihomoProfile/valid/managed.json +21 -0
  221. package/fixtures/openapi/NetworkMihomoProfileInput/invalid/app-subscription-secret.json +14 -0
  222. package/fixtures/openapi/NetworkMihomoProfileInput/invalid/manual-node-with-subscription.json +20 -0
  223. package/fixtures/openapi/NetworkMihomoProfileInput/valid/app-subscription.json +14 -0
  224. package/fixtures/openapi/NetworkMihomoProfileInput/valid/managed.json +16 -0
  225. package/fixtures/openapi/NetworkMihomoProfileInput/valid/manual-node.json +21 -0
  226. package/fixtures/openapi/NetworkNASBinding/invalid/secret.json +13 -0
  227. package/fixtures/openapi/NetworkNASBinding/valid/default.json +16 -0
  228. package/fixtures/openapi/NetworkNASBindingInput/invalid/secret.json +10 -0
  229. package/fixtures/openapi/NetworkNASBindingInput/valid/default.json +13 -0
  230. package/fixtures/openapi/NetworkPolicyPreviewRequest/invalid/unknown-mode.json +6 -0
  231. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/external-direct-ztna.json +6 -0
  232. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/external-vpn-ztna.json +6 -0
  233. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/external-vpn.json +6 -0
  234. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/internal-direct.json +7 -0
  235. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/internal-ztna.json +7 -0
  236. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/bypass-path.json +9 -0
  237. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/onboarding.json +10 -0
  238. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/protected-broad-path-denied.json +9 -0
  239. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/protected-resource.json +9 -0
  240. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/quarantine.json +10 -0
  241. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/restricted.json +10 -0
  242. package/fixtures/openapi/NetworkPolicySnapshot/invalid/zero-version.json +7 -0
  243. package/fixtures/openapi/NetworkPolicySnapshot/valid/published.json +7 -0
  244. package/fixtures/openapi/NetworkResourceInput/invalid/invalid-port.json +10 -0
  245. package/fixtures/openapi/NetworkResourceInput/valid/protected-cidr.json +10 -0
  246. package/fixtures/openapi/NetworkRuntimeEnrollmentInput/invalid/excessive-ttl.json +7 -0
  247. package/fixtures/openapi/NetworkRuntimeEnrollmentInput/valid/endpoint.json +7 -0
  248. package/fixtures/openapi/NetworkRuntimeEnrollmentSecret/invalid/short-token.json +15 -0
  249. package/fixtures/openapi/NetworkRuntimeEnrollmentSecret/valid/pending.json +15 -0
  250. package/fixtures/openapi/NetworkSession/invalid/unknown-status.json +14 -0
  251. package/fixtures/openapi/NetworkSession/valid/restricted-vpn-ztna.json +16 -0
  252. package/fixtures/openapi/NetworkSessionActionExecuteInput/invalid/short-hash.json +6 -0
  253. package/fixtures/openapi/NetworkSessionActionExecuteInput/valid/restrict.json +6 -0
  254. package/fixtures/openapi/NetworkSessionActionInput/invalid/unknown-action.json +5 -0
  255. package/fixtures/openapi/NetworkSessionActionInput/valid/restrict.json +5 -0
  256. package/fixtures/openapi/NetworkSessionActionPlan/invalid/missing-hash.json +12 -0
  257. package/fixtures/openapi/NetworkSessionActionPlan/valid/fallback-disconnect.json +13 -0
  258. package/fixtures/openapi/NetworkSessionCommand/invalid/unknown-status.json +14 -0
  259. package/fixtures/openapi/NetworkSessionCommand/valid/pending.json +14 -0
  260. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-status.json +4 -0
  261. package/fixtures/openapi/NetworkSiteInput/valid/shanghai.json +5 -0
  262. package/fixtures/openapi/NetworkSiteProfileBinding/invalid/vlan.json +9 -0
  263. package/fixtures/openapi/NetworkSiteProfileBinding/valid/default.json +10 -0
  264. package/fixtures/openapi/NetworkSiteProfileBindingInput/invalid/timeout.json +7 -0
  265. package/fixtures/openapi/NetworkSiteProfileBindingInput/valid/default.json +7 -0
  266. package/fixtures/openapi/NetworkSpaceInput/invalid/empty-cidrs.json +6 -0
  267. package/fixtures/openapi/NetworkSpaceInput/invalid/malformed-cidr.json +6 -0
  268. package/fixtures/openapi/NetworkSpaceInput/valid/corp.json +6 -0
  269. package/fixtures/openapi/NetworkTelemetrySummary/invalid/raw-destination.json +29 -0
  270. package/fixtures/openapi/NetworkTelemetrySummary/valid/proxy-flow.json +36 -0
  271. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/auto-with-gateway.json +6 -0
  272. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/missing-manual-gateway.json +5 -0
  273. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/mode-override.json +7 -0
  274. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/auto-intent.json +5 -0
  275. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/manual-intent.json +6 -0
  276. package/fixtures/openapi/NetworkVPNProfileInput/invalid/unknown-private-key.json +25 -0
  277. package/fixtures/openapi/NetworkVPNProfileInput/valid/assigned-profile.json +24 -0
  278. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/invalid/unbounded-attempts.json +21 -0
  279. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/valid/provider-policy.json +21 -0
  280. package/fixtures/openapi/ObservabilityMetricAssessmentInput/invalid/arbitrary-expression.json +10 -0
  281. package/fixtures/openapi/ObservabilityMetricAssessmentInput/valid/metric-window.json +10 -0
  282. package/fixtures/openapi/PluginManifest/valid/hermes-api.json +53 -0
  283. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/missing-identity.json +5 -0
  284. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/skip-analysis.json +5 -0
  285. package/fixtures/openapi/ProgressiveRolloutControlInput/valid/pause.json +5 -0
  286. package/fixtures/openapi/ReleaseTargetInput/invalid/missing-required-field.json +15 -0
  287. package/fixtures/openapi/ReleaseTargetInput/valid/docker-compose.json +16 -0
  288. package/fixtures/openapi/RepositoryAnalysis/invalid/moving-ref.json +11 -0
  289. package/fixtures/openapi/RepositoryAnalysis/valid/go.json +29 -0
  290. package/fixtures/openapi/RepositoryAnalysisInput/invalid/arbitrary-source-url.json +6 -0
  291. package/fixtures/openapi/RepositoryAnalysisInput/invalid/missing-ref.json +4 -0
  292. package/fixtures/openapi/RepositoryAnalysisInput/valid/nested-project.json +6 -0
  293. package/fixtures/openapi/ResourceLease/invalid/missing-session.json +10 -0
  294. package/fixtures/openapi/ResourceLease/valid/active.json +11 -0
  295. package/fixtures/openapi/ServiceDeploymentTemplateBinding/invalid/invalid-unpinned.json +4 -0
  296. package/fixtures/openapi/ServiceDeploymentTemplateBinding/valid/valid.json +7 -0
  297. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-executable-schema.json +25 -0
  298. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-missing-schema.json +21 -0
  299. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-mixed-renderers.json +40 -0
  300. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-nested-parameters.json +41 -0
  301. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-yaml.json +34 -0
  302. package/fixtures/openapi/TemplatePublishInput/invalid/missing-revision.json +1 -0
  303. package/fixtures/openapi/TemplatePublishInput/invalid/unknown-field.json +4 -0
  304. package/fixtures/openapi/TemplatePublishInput/invalid/zero-revision.json +3 -0
  305. package/fixtures/openapi/TemplatePublishInput/valid/revision.json +3 -0
  306. package/fixtures/openapi/ToolCapability/invalid/bad-idempotency.json +35 -0
  307. package/fixtures/openapi/ToolCapability/invalid/unversioned-check.json +47 -0
  308. package/fixtures/openapi/ToolCapability/valid/checks.json +48 -0
  309. package/fixtures/openapi/ToolCapability/valid/versioned-task.json +35 -0
  310. package/fixtures/openapi/ToolInvocationRequest/invalid/empty-version.json +8 -0
  311. package/fixtures/openapi/ToolInvocationRequest/valid/versioned.json +8 -0
  312. package/fixtures/openapi/ToolInvocationResult/invalid/missing-status-call.json +13 -0
  313. package/fixtures/openapi/ToolInvocationResult/valid/task.json +20 -0
  314. package/fixtures/openapi/VirtualMachineCreateInput/invalid/capacity-type.json +1 -0
  315. package/fixtures/openapi/VirtualMachineCreateInput/valid/capacity.json +1 -0
  316. package/fixtures/openapi/VirtualizationCapacityInput/invalid/missing-disk.json +1 -0
  317. package/fixtures/openapi/VirtualizationCapacityInput/valid/running-vm.json +1 -0
  318. package/fixtures/openapi/VirtualizationCapacityResult/invalid/invented-status.json +1 -0
  319. package/fixtures/openapi/VirtualizationCapacityResult/valid/available.json +1 -0
  320. package/fixtures/openapi/VirtualizationWorkerCreateInput/invalid/secret-input.json +5 -0
  321. package/fixtures/openapi/VirtualizationWorkerCreateInput/valid/one-worker.json +4 -0
  322. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/external-owner.json +27 -0
  323. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/zero-budget.json +27 -0
  324. package/fixtures/openapi/VirtualizationWorkerPoolInput/valid/registered.json +27 -0
  325. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/invalid/nested-data.json +5 -0
  326. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/valid/empty.json +3 -0
  327. package/fixtures/openapi/WorkbenchContextSelection/invalid/missing-file-content.json +8 -0
  328. package/fixtures/openapi/WorkbenchContextSelection/invalid/unknown-reference-kind.json +8 -0
  329. package/fixtures/openapi/WorkbenchContextSelection/valid/text-and-resource.json +17 -0
  330. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/trigger-without-plan.json +10 -0
  331. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/unknown-trigger.json +28 -0
  332. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/zero-interval.json +28 -0
  333. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/alert-capability.json +30 -0
  334. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/scheduled-capability.json +28 -0
  335. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/missing-rule.json +3 -0
  336. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/schedule-alert-selector.json +4 -0
  337. package/fixtures/openapi/WorkbenchInspectionTrigger/valid/schedule.json +3 -0
  338. package/fixtures/openapi/WorkbenchModelPreferences/invalid/unknown-effort.json +1 -0
  339. package/fixtures/openapi/WorkbenchModelPreferences/valid/selected.json +1 -0
  340. package/fixtures/openapi/WorkflowCatalogPage/invalid/unsupported-kind.json +6 -0
  341. package/fixtures/openapi/WorkflowCatalogPage/valid/build-without-environment.json +6 -0
  342. package/fixtures/openapi/WorkflowTemplateInput/invalid/zero-revision.json +6 -0
  343. package/fixtures/openapi/WorkflowTemplateInput/valid/draft.json +10 -0
  344. package/gen/go/sohaapi/client.go +1 -1
  345. package/gen/go/sohaapi/types.go +15610 -6415
  346. package/gen/go/sohaapi/types_test.go +100 -1
  347. package/gen/go/sohaapi/vpn_models_test.go +21 -0
  348. package/gen/ts/sohaapi/index.ts +27859 -17518
  349. package/go.mod +107 -5
  350. package/go.sum +379 -16
  351. package/helmrelease/delivery.go +283 -0
  352. package/helmrelease/delivery_test.go +102 -0
  353. package/helmrelease/frozen_chart.go +91 -0
  354. package/helmrelease/result.go +27 -0
  355. package/helmrelease/runtime/delivery.go +508 -0
  356. package/helmrelease/runtime/delivery_test.go +236 -0
  357. package/helmrelease/runtime/resources_test.go +60 -0
  358. package/helmrelease/values.go +99 -0
  359. package/knowledge/context-envelope.schema.json +1 -0
  360. package/mcp/gateway-manifest.schema.json +387 -72
  361. package/network/README.md +156 -0
  362. package/network/network-ingest-event.schema.json +415 -0
  363. package/network/network-radius-accounting.schema.json +41 -0
  364. package/network/network-runtime-protocol.schema.json +1208 -0
  365. package/network/schema.go +26 -0
  366. package/network/schema_test.go +22 -0
  367. package/openapi/soha-api.json +59260 -35960
  368. package/openapi/soha-api.yaml +36064 -26232
  369. package/openapi/spec_generated.go +3 -3
  370. package/package.json +7 -1
  371. package/resource/manifest_health.go +155 -0
  372. package/resource/manifest_health_test.go +37 -0
  373. package/resource/models.go +14 -0
  374. package/resource/runtime/argocd.go +166 -0
  375. package/resource/runtime/argocd_integration_test.go +142 -0
  376. package/resource/runtime/argocd_observation.go +143 -0
  377. package/resource/runtime/argocd_operation.go +253 -0
  378. package/resource/runtime/argocd_resources.go +115 -0
  379. package/resource/runtime/argocd_resources_test.go +107 -0
  380. package/resource/runtime/argocd_test.go +386 -0
  381. package/resource/runtime/health.go +115 -0
  382. package/resource/runtime/health_test.go +78 -0
  383. package/resource/runtime/operator_integration_test.go +174 -0
  384. package/resource/runtime/ownership.go +178 -0
  385. package/resource/runtime/ownership_test.go +178 -0
  386. package/resource/runtime/rollout_integration_test.go +287 -0
  387. package/resource/runtime/rollout_observation.go +252 -0
  388. package/resource/runtime/rollout_operation.go +398 -0
  389. package/resource/runtime/rollout_resources.go +385 -0
  390. package/resource/runtime/rollout_task.go +165 -0
  391. package/resource/runtime/rollout_test.go +450 -0
@@ -0,0 +1,143 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "reflect"
8
+ "time"
9
+
10
+ contractresource "github.com/opensoha/soha-contracts/resource"
11
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
12
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14
+ "k8s.io/client-go/dynamic"
15
+ )
16
+
17
+ // ArgoObservation retains live child inventory while the operation progresses.
18
+ // Cached controller health cannot override missing, unowned or drifted children.
19
+ type ArgoObservation struct {
20
+ OperationID string
21
+ Phase string
22
+ Revision string
23
+ Health string
24
+ Drifted bool
25
+ Resources []*unstructured.Unstructured
26
+ }
27
+
28
+ func ObserveArgoApplication(ctx context.Context, client dynamic.Interface, desired, object *unstructured.Unstructured, children []*unstructured.Unstructured, owner string) (ArgoObservation, error) {
29
+ observation := ArgoObservation{Health: "unknown"}
30
+ if err := ValidateArgoResources(desired, children); err != nil {
31
+ return observation, err
32
+ }
33
+ if err := validateArgoOwner(object, owner); err != nil {
34
+ return observation, err
35
+ }
36
+ observation.Health = "progressing"
37
+ observation.OperationID = object.GetAnnotations()[ArgoOperationAnnotation]
38
+ observation.Phase = argoString(object, "status", "operationState", "phase")
39
+ observation.Revision = argoString(object, "status", "sync", "revision")
40
+ observation.Drifted = !argoMatchesDesired(desired.Object, object.Object)
41
+ expected := map[string]bool{}
42
+ for _, child := range children {
43
+ expected[argoResourceKey(child)] = true
44
+ }
45
+ reported, _, err := unstructured.NestedSlice(object.Object, "status", "resources")
46
+ if err != nil || len(reported) > 49 {
47
+ return observation, fmt.Errorf("invalid GitOps resource observations")
48
+ }
49
+ seen := map[string]bool{}
50
+ for _, raw := range reported {
51
+ item, ok := raw.(map[string]any)
52
+ if !ok {
53
+ return observation, fmt.Errorf("invalid GitOps resource observation")
54
+ }
55
+ group, _ := item["group"].(string)
56
+ kind, _ := item["kind"].(string)
57
+ name, _ := item["name"].(string)
58
+ key := group + "/" + kind + "/" + name
59
+ if !expected[key] || seen[key] || item["namespace"] != desired.GetNamespace() {
60
+ return observation, fmt.Errorf("GitOps observed a resource outside its frozen inventory")
61
+ }
62
+ seen[key] = true
63
+ }
64
+ queryCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
65
+ defer cancel()
66
+ healthy, degraded := len(seen) == len(expected), false
67
+ for _, child := range children {
68
+ gvk := child.GroupVersionKind()
69
+ live, err := client.Resource(argoResourceTypes[gvk.Group+"/"+gvk.Kind]).Namespace(child.GetNamespace()).Get(queryCtx, child.GetName(), metav1.GetOptions{})
70
+ if apierrors.IsNotFound(err) {
71
+ healthy = false
72
+ continue
73
+ }
74
+ if err != nil {
75
+ return observation, err
76
+ }
77
+ if live.GetUID() == "" || !argoTracksResource(desired, live) {
78
+ return observation, fmt.Errorf("GitOps child ownership does not match its Application")
79
+ }
80
+ observation.Resources = append(observation.Resources, live)
81
+ health := contractresource.ManifestHealth(live.Object)
82
+ healthy = healthy && health == "healthy"
83
+ degraded = degraded || health == "degraded"
84
+ observation.Drifted = observation.Drifted || !argoMatchesDesired(child.Object, live.Object)
85
+ for _, field := range []string{"containers", "initContainers"} {
86
+ wanted, _, _ := unstructured.NestedSlice(child.Object, "spec", "template", "spec", field)
87
+ actual, _, _ := unstructured.NestedSlice(live.Object, "spec", "template", "spec", field)
88
+ observation.Drifted = observation.Drifted || len(wanted) != len(actual)
89
+ }
90
+ }
91
+ current := observation.OperationID != "" && argoOperationID(object, "status", "operationState", "operation") == observation.OperationID
92
+ if observation.Drifted || degraded || current && (observation.Phase == "Failed" || observation.Phase == "Error") {
93
+ observation.Health = "degraded"
94
+ return observation, nil
95
+ }
96
+ source, _, _ := unstructured.NestedMap(desired.Object, "spec", "source")
97
+ compared, _, _ := unstructured.NestedMap(object.Object, "status", "sync", "comparedTo", "source")
98
+ if healthy && current && observation.Phase == "Succeeded" && argoString(object, "status", "operationState", "finishedAt") != "" && object.Object["operation"] == nil && reflect.DeepEqual(source, compared) && observation.Revision == argoString(desired, "spec", "source", "targetRevision") && argoString(object, "status", "operationState", "syncResult", "revision") == observation.Revision && argoString(object, "status", "sync", "status") == "Synced" && argoString(object, "status", "health", "status") == "Healthy" {
99
+ observation.Health = "healthy"
100
+ }
101
+ return observation, nil
102
+ }
103
+
104
+ func argoResourceKey(object *unstructured.Unstructured) string {
105
+ gvk := object.GroupVersionKind()
106
+ return gvk.Group + "/" + gvk.Kind + "/" + object.GetName()
107
+ }
108
+
109
+ // API defaults may add object fields, but lists must retain their frozen shape.
110
+ func argoMatchesDesired(desired, observed any) bool {
111
+ switch value := desired.(type) {
112
+ case map[string]any:
113
+ live, ok := observed.(map[string]any)
114
+ if !ok {
115
+ return false
116
+ }
117
+ for key, child := range value {
118
+ if !argoMatchesDesired(child, live[key]) {
119
+ return false
120
+ }
121
+ }
122
+ return true
123
+ case []any:
124
+ // Kubernetes omits empty optional lists such as container env/args.
125
+ if len(value) == 0 && observed == nil {
126
+ return true
127
+ }
128
+ live, ok := observed.([]any)
129
+ if !ok || len(value) != len(live) {
130
+ return false
131
+ }
132
+ for i, child := range value {
133
+ if !argoMatchesDesired(child, live[i]) {
134
+ return false
135
+ }
136
+ }
137
+ return true
138
+ default:
139
+ wanted, _ := json.Marshal(desired)
140
+ actual, _ := json.Marshal(observed)
141
+ return string(wanted) == string(actual)
142
+ }
143
+ }
@@ -0,0 +1,253 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "reflect"
8
+ "slices"
9
+ "strings"
10
+ "time"
11
+
12
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
13
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
15
+ "k8s.io/apimachinery/pkg/runtime/schema"
16
+ "k8s.io/client-go/dynamic"
17
+ "k8s.io/client-go/util/retry"
18
+ )
19
+
20
+ var ErrArgoStopUnconfirmed = errors.New("argo CD operation stop is not confirmed")
21
+
22
+ // ApplyArgoApplication writes only the Application root. A persisted operation
23
+ // key makes an ambiguous response or worker restart resume the same sync.
24
+ func ApplyArgoApplication(ctx context.Context, client dynamic.Interface, desired *unstructured.Unstructured, children []*unstructured.Unstructured, options metav1.PatchOptions, operationID string) (*unstructured.Unstructured, error) {
25
+ if err := ValidateArgoResources(desired, children); err != nil {
26
+ return nil, err
27
+ }
28
+ if operationID == "" || options.FieldManager == "" || options.Force != nil && *options.Force {
29
+ return nil, fmt.Errorf("GitOps requires a stable operation key and a non-forced owner")
30
+ }
31
+ if desired.Object["operation"] != nil {
32
+ return nil, fmt.Errorf("GitOps operation must be generated by the executor")
33
+ }
34
+ if err := validateArgoProject(ctx, client, desired); err != nil {
35
+ return nil, err
36
+ }
37
+ resources := client.Resource(argoApplications).Namespace(desired.GetNamespace())
38
+ var applied, attempted *unstructured.Unstructured
39
+ err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
40
+ if err := CheckArgoResourceOwners(ctx, client, desired, children); err != nil {
41
+ return err
42
+ }
43
+ live, err := resources.Get(ctx, desired.GetName(), metav1.GetOptions{})
44
+ if apierrors.IsNotFound(err) {
45
+ write := desired.DeepCopy()
46
+ setArgoOperation(write, options.FieldManager, operationID)
47
+ attempted = write.DeepCopy()
48
+ applied, err = resources.Create(ctx, write, metav1.CreateOptions{FieldManager: options.FieldManager, DryRun: options.DryRun})
49
+ return err
50
+ }
51
+ if err != nil {
52
+ return err
53
+ }
54
+ if err := validateArgoOwner(live, options.FieldManager); err != nil {
55
+ return err
56
+ }
57
+ if live.GetAnnotations()[ArgoOperationAnnotation] == operationID {
58
+ if !reflect.DeepEqual(live.Object["spec"], desired.Object["spec"]) {
59
+ return fmt.Errorf("GitOps operation key refers to different desired inputs")
60
+ }
61
+ applied = live
62
+ return nil
63
+ }
64
+ if argoOperationActive(live) {
65
+ return fmt.Errorf("GitOps Application has an unfinished operation")
66
+ }
67
+ write := live.DeepCopy()
68
+ write.Object["spec"] = desired.DeepCopy().Object["spec"]
69
+ write.SetLabels(desired.GetLabels())
70
+ write.SetAnnotations(desired.GetAnnotations())
71
+ setArgoOperation(write, options.FieldManager, operationID)
72
+ attempted = write.DeepCopy()
73
+ applied, err = resources.Update(ctx, write, metav1.UpdateOptions{FieldManager: options.FieldManager, DryRun: options.DryRun})
74
+ return err
75
+ })
76
+ if err != nil && len(options.DryRun) == 0 && attempted != nil && !apierrors.IsInvalid(err) && !apierrors.IsForbidden(err) && !apierrors.IsUnauthorized(err) && !apierrors.IsBadRequest(err) && !apierrors.IsConflict(err) && !apierrors.IsAlreadyExists(err) {
77
+ recoveryCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
78
+ live, readErr := resources.Get(recoveryCtx, desired.GetName(), metav1.GetOptions{})
79
+ cancel()
80
+ if readErr != nil || validateArgoOwner(live, options.FieldManager) != nil || live.GetAnnotations()[ArgoOperationAnnotation] != operationID || attempted.GetUID() != "" && attempted.GetUID() != live.GetUID() {
81
+ return live, fmt.Errorf("%w: sync write outcome could not be confirmed", ErrArgoStopUnconfirmed)
82
+ }
83
+ return stopArgoAfterError(ctx, resources, live, operationID, err)
84
+ }
85
+ if err != nil || len(options.DryRun) != 0 {
86
+ return applied, err
87
+ }
88
+ return waitArgoOperation(ctx, resources, applied, operationID)
89
+ }
90
+
91
+ func setArgoOperation(object *unstructured.Unstructured, owner, operationID string) {
92
+ annotations := object.GetAnnotations()
93
+ if annotations == nil {
94
+ annotations = map[string]string{}
95
+ }
96
+ annotations[ArgoOwnerAnnotation], annotations[ArgoOperationAnnotation] = owner, operationID
97
+ object.SetAnnotations(annotations)
98
+ object.Object["operation"] = map[string]any{
99
+ "initiatedBy": map[string]any{"username": "soha", "automated": false},
100
+ "info": []any{map[string]any{"name": ArgoOperationAnnotation, "value": operationID}},
101
+ "sync": map[string]any{"revision": argoString(object, "spec", "source", "targetRevision"), "prune": false, "syncOptions": []any{"FailOnSharedResource=true"}},
102
+ }
103
+ }
104
+
105
+ func validateArgoOwner(object *unstructured.Unstructured, owner string) error {
106
+ if err := ValidateArgoApplication(object); err != nil {
107
+ return err
108
+ }
109
+ if owner == "" || object.GetAnnotations()[ArgoOwnerAnnotation] != owner || object.GetUID() == "" {
110
+ return fmt.Errorf("GitOps Application belongs to another writer; implicit adoption is disabled")
111
+ }
112
+ if object.GetLabels()["argocd.argoproj.io/instance"] != "" || object.GetLabels()["app.kubernetes.io/managed-by"] == "Helm" {
113
+ return fmt.Errorf("GitOps Application is externally managed")
114
+ }
115
+ return nil
116
+ }
117
+
118
+ func argoOperationActive(object *unstructured.Unstructured) bool {
119
+ phase := argoString(object, "status", "operationState", "phase")
120
+ return object.Object["operation"] != nil || phase == "Running" || phase == "Terminating"
121
+ }
122
+
123
+ func waitArgoOperation(ctx context.Context, client dynamic.ResourceInterface, initial *unstructured.Unstructured, operationID string) (*unstructured.Unstructured, error) {
124
+ ticker := time.NewTicker(time.Second)
125
+ defer ticker.Stop()
126
+ for {
127
+ live, err := client.Get(ctx, initial.GetName(), metav1.GetOptions{})
128
+ if ctx.Err() != nil {
129
+ return stopArgoAfterError(ctx, client, initial, operationID, ctx.Err())
130
+ }
131
+ if err != nil {
132
+ return stopArgoAfterError(ctx, client, initial, operationID, err)
133
+ }
134
+ if live.GetUID() != initial.GetUID() || live.GetAnnotations()[ArgoOperationAnnotation] != operationID {
135
+ return live, fmt.Errorf("GitOps operation ownership changed while observing")
136
+ }
137
+ if !argoOperationActive(live) && argoOperationID(live, "status", "operationState", "operation") == operationID {
138
+ if argoString(live, "status", "operationState", "phase") == "Succeeded" {
139
+ return live, nil
140
+ }
141
+ return live, fmt.Errorf("argo CD sync did not succeed")
142
+ }
143
+ select {
144
+ case <-ctx.Done():
145
+ case <-ticker.C:
146
+ }
147
+ }
148
+ }
149
+
150
+ func stopArgoAfterError(ctx context.Context, client dynamic.ResourceInterface, initial *unstructured.Unstructured, operationID string, cause error) (*unstructured.Unstructured, error) {
151
+ stopCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
152
+ defer cancel()
153
+ stopped, err := terminateArgoOperation(stopCtx, client, initial, operationID)
154
+ if err != nil {
155
+ return stopped, fmt.Errorf("%w: %v", ErrArgoStopUnconfirmed, err)
156
+ }
157
+ if ctx.Err() != nil {
158
+ cause = ctx.Err()
159
+ }
160
+ return stopped, cause
161
+ }
162
+
163
+ // The native Argo CD TerminateOperation implementation updates the root status
164
+ // phase to Terminating. UID/RV and operation identity fence every request here;
165
+ // only a controller-written terminal observation confirms the stop.
166
+ func terminateArgoOperation(ctx context.Context, client dynamic.ResourceInterface, initial *unstructured.Unstructured, operationID string) (*unstructured.Unstructured, error) {
167
+ ticker := time.NewTicker(time.Second)
168
+ defer ticker.Stop()
169
+ for {
170
+ live, err := client.Get(ctx, initial.GetName(), metav1.GetOptions{})
171
+ if err != nil {
172
+ return live, err
173
+ }
174
+ if live.GetUID() != initial.GetUID() || live.GetAnnotations()[ArgoOperationAnnotation] != operationID {
175
+ return live, fmt.Errorf("GitOps operation identity changed before stop confirmation")
176
+ }
177
+ stateID := argoOperationID(live, "status", "operationState", "operation")
178
+ phase := argoString(live, "status", "operationState", "phase")
179
+ if !argoOperationActive(live) && stateID == operationID {
180
+ if phase == "Succeeded" || phase == "Failed" {
181
+ return live, nil
182
+ }
183
+ return live, fmt.Errorf("argo CD termination reported an error")
184
+ }
185
+ if live.Object["operation"] != nil && argoOperationID(live, "operation") != operationID {
186
+ return live, fmt.Errorf("another GitOps operation is active")
187
+ }
188
+ if stateID == operationID && phase == "Running" {
189
+ _ = unstructured.SetNestedField(live.Object, "Terminating", "status", "operationState", "phase")
190
+ _, err = client.Update(ctx, live, metav1.UpdateOptions{})
191
+ if err != nil && !apierrors.IsConflict(err) {
192
+ return live, err
193
+ }
194
+ }
195
+ select {
196
+ case <-ctx.Done():
197
+ return live, ctx.Err()
198
+ case <-ticker.C:
199
+ }
200
+ }
201
+ }
202
+
203
+ func validateArgoProject(ctx context.Context, client dynamic.Interface, application *unstructured.Unstructured) error {
204
+ namespace := application.GetAnnotations()["delivery.soha.io/argocd-namespace"]
205
+ if namespace == "" {
206
+ namespace = application.GetNamespace()
207
+ }
208
+ project, err := client.Resource(schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "appprojects"}).Namespace(namespace).Get(ctx, argoString(application, "spec", "project"), metav1.GetOptions{})
209
+ if err != nil {
210
+ return fmt.Errorf("read restricted Argo CD project: %w", err)
211
+ }
212
+ destinations, _, err := unstructured.NestedSlice(project.Object, "spec", "destinations")
213
+ if err != nil || len(destinations) != 1 {
214
+ return fmt.Errorf("GitOps project must authorize only the selected namespace and cluster")
215
+ }
216
+ destination, _ := destinations[0].(map[string]any)
217
+ if destination["namespace"] != application.GetNamespace() || destination["server"] != "https://kubernetes.default.svc" {
218
+ return fmt.Errorf("GitOps project destination is outside the delivery target")
219
+ }
220
+ clusterKinds, _, err := unstructured.NestedSlice(project.Object, "spec", "clusterResourceWhitelist")
221
+ if err != nil || len(clusterKinds) != 0 {
222
+ return fmt.Errorf("GitOps project must deny cluster-scoped resources")
223
+ }
224
+ allowed, _, err := unstructured.NestedSlice(project.Object, "spec", "namespaceResourceWhitelist")
225
+ if err != nil || len(allowed) == 0 {
226
+ return fmt.Errorf("GitOps project requires an explicit supported resource allowlist")
227
+ }
228
+ for _, raw := range allowed {
229
+ item, _ := raw.(map[string]any)
230
+ group, _ := item["group"].(string)
231
+ kind, _ := item["kind"].(string)
232
+ if _, ok := argoResourceTypes[group+"/"+kind]; !ok {
233
+ return fmt.Errorf("GitOps project permits unsupported resource kinds")
234
+ }
235
+ }
236
+ repositories, _, err := unstructured.NestedStringSlice(project.Object, "spec", "sourceRepos")
237
+ if err != nil || !slices.Contains(repositories, argoString(application, "spec", "source", "repoURL")) {
238
+ return fmt.Errorf("GitOps repository is not explicitly authorized by its project")
239
+ }
240
+ if namespace != application.GetNamespace() {
241
+ sources, _, err := unstructured.NestedStringSlice(project.Object, "spec", "sourceNamespaces")
242
+ if err != nil || !slices.Contains(sources, application.GetNamespace()) {
243
+ return fmt.Errorf("GitOps project does not authorize this Application namespace")
244
+ }
245
+ }
246
+ // Wildcard source access is not a sufficient substitute for an explicit grant.
247
+ for _, repository := range repositories {
248
+ if strings.ContainsAny(repository, "*?") {
249
+ return fmt.Errorf("GitOps project repository grants must be explicit")
250
+ }
251
+ }
252
+ return nil
253
+ }
@@ -0,0 +1,115 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strings"
7
+
8
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
9
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11
+ "k8s.io/client-go/dynamic"
12
+ )
13
+
14
+ // ValidateArgoResources bounds the controller's writes to the resources rendered
15
+ // from the same immutable source used by the delivery plan.
16
+ func ValidateArgoResources(application *unstructured.Unstructured, resources []*unstructured.Unstructured) error {
17
+ if err := ValidateArgoApplication(application); err != nil {
18
+ return err
19
+ }
20
+ if len(resources) == 0 || len(resources) > 49 {
21
+ return fmt.Errorf("GitOps requires 1 to 49 frozen child resources")
22
+ }
23
+ images, _, _ := unstructured.NestedStringSlice(application.Object, "spec", "source", "kustomize", "images")
24
+ expectedImages := map[string]bool{}
25
+ for _, image := range images {
26
+ _, image, _ = ParseArgoImageOverride(image)
27
+ expectedImages[image] = false
28
+ }
29
+ seen := map[string]bool{}
30
+ for _, resource := range resources {
31
+ if resource == nil {
32
+ return fmt.Errorf("GitOps child resource is missing")
33
+ }
34
+ gvk := resource.GroupVersionKind()
35
+ gvr, supported := argoResourceTypes[gvk.Group+"/"+gvk.Kind]
36
+ key := gvk.Group + "/" + gvk.Kind + "/" + resource.GetName()
37
+ if !supported || gvk.Version != gvr.Version || resource.GetNamespace() != application.GetNamespace() || resource.GetName() == "" || seen[key] {
38
+ return fmt.Errorf("GitOps child resource is unsupported, repeated or outside the target namespace")
39
+ }
40
+ seen[key] = true
41
+ if resource.GetUID() != "" || resource.GetResourceVersion() != "" || resource.GetGenerateName() != "" || resource.GetDeletionTimestamp() != nil || len(resource.GetOwnerReferences()) != 0 || len(resource.GetFinalizers()) != 0 || resource.Object["status"] != nil {
42
+ return fmt.Errorf("GitOps child resources must be independent desired objects")
43
+ }
44
+ if err := ValidateDirectManifestOwner(resource); err != nil {
45
+ return err
46
+ }
47
+ for key := range resource.GetAnnotations() {
48
+ if strings.HasPrefix(key, "argocd.argoproj.io/") && key != "argocd.argoproj.io/sync-wave" {
49
+ return fmt.Errorf("GitOps child hooks and controller overrides are unsupported")
50
+ }
51
+ }
52
+ if resource.GetKind() == "Secret" && (resource.Object["data"] != nil || resource.Object["stringData"] != nil) {
53
+ return fmt.Errorf("GitOps secret values must use administrator-managed references")
54
+ }
55
+ for _, field := range []string{"containers", "initContainers"} {
56
+ containers, _, err := unstructured.NestedSlice(resource.Object, "spec", "template", "spec", field)
57
+ if err != nil {
58
+ return err
59
+ }
60
+ for _, raw := range containers {
61
+ container, _ := raw.(map[string]any)
62
+ image, _ := container["image"].(string)
63
+ if _, ok := expectedImages[image]; !ok {
64
+ return fmt.Errorf("GitOps workload image is outside the frozen digest set")
65
+ }
66
+ expectedImages[image] = true
67
+ }
68
+ }
69
+ }
70
+ for _, used := range expectedImages {
71
+ if !used {
72
+ return fmt.Errorf("GitOps image override is not used by a workload")
73
+ }
74
+ }
75
+ return nil
76
+ }
77
+
78
+ // CheckArgoResourceOwners refuses implicit adoption, including unlabelled live
79
+ // objects. It is read-only and runs again immediately before controller sync.
80
+ func CheckArgoResourceOwners(ctx context.Context, client dynamic.Interface, application *unstructured.Unstructured, resources []*unstructured.Unstructured) error {
81
+ if err := ValidateArgoResources(application, resources); err != nil {
82
+ return err
83
+ }
84
+ for _, resource := range resources {
85
+ gvk := resource.GroupVersionKind()
86
+ gvr := argoResourceTypes[gvk.Group+"/"+gvk.Kind]
87
+ live, err := client.Resource(gvr).Namespace(resource.GetNamespace()).Get(ctx, resource.GetName(), metav1.GetOptions{})
88
+ if apierrors.IsNotFound(err) {
89
+ continue
90
+ }
91
+ if err != nil {
92
+ return err
93
+ }
94
+ if live.GetUID() == "" || live.GetDeletionTimestamp() != nil || !argoTracksResource(application, live) || metav1.GetControllerOf(live) != nil {
95
+ return fmt.Errorf("GitOps child %s/%s belongs to another writer or is being deleted", resource.GetKind(), resource.GetName())
96
+ }
97
+ labels, annotations := live.GetLabels(), live.GetAnnotations()
98
+ if labels["app.kubernetes.io/managed-by"] == "Helm" || annotations["meta.helm.sh/release-name"] != "" || labels["kustomize.toolkit.fluxcd.io/name"] != "" || labels["helm.toolkit.fluxcd.io/name"] != "" {
99
+ return fmt.Errorf("GitOps child has another delivery owner")
100
+ }
101
+ for _, field := range live.GetManagedFields() {
102
+ if strings.HasPrefix(field.Manager, "opensoha-") || field.Manager == "helm" || field.Manager == "kustomize-controller" || field.Manager == "helm-controller" {
103
+ return fmt.Errorf("GitOps child has fields owned by another delivery writer")
104
+ }
105
+ }
106
+ }
107
+ return nil
108
+ }
109
+
110
+ func argoTracksResource(application, resource *unstructured.Unstructured) bool {
111
+ gvk := resource.GroupVersionKind()
112
+ suffix := ":" + gvk.Group + "/" + gvk.Kind + ":" + resource.GetNamespace() + "/" + resource.GetName()
113
+ tracking := resource.GetAnnotations()["argocd.argoproj.io/tracking-id"]
114
+ return tracking == application.GetName()+suffix || tracking == application.GetNamespace()+"_"+application.GetName()+suffix
115
+ }
@@ -0,0 +1,107 @@
1
+ package runtime
2
+
3
+ import (
4
+ "testing"
5
+
6
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8
+ "k8s.io/apimachinery/pkg/runtime"
9
+ "k8s.io/client-go/dynamic/fake"
10
+ )
11
+
12
+ func TestArgoFrozenResourcesRejectUnsafeWrites(t *testing.T) {
13
+ for _, mode := range []string{"valid", "outside", "duplicate", "hook", "image", "init image", "external", "status", "missing image"} {
14
+ t.Run(mode, func(t *testing.T) {
15
+ app, live := argoFixture(t)
16
+ desired := live.DeepCopy()
17
+ desired.SetUID("")
18
+ desired.SetAnnotations(nil)
19
+ desired.SetGeneration(0)
20
+ delete(desired.Object, "status")
21
+ resources := []*unstructured.Unstructured{desired}
22
+ switch mode {
23
+ case "outside":
24
+ desired.SetNamespace("other")
25
+ case "duplicate":
26
+ resources = append(resources, desired.DeepCopy())
27
+ case "hook":
28
+ desired.SetAnnotations(map[string]string{"argocd.argoproj.io/hook": "PreSync"})
29
+ case "image":
30
+ _ = unstructured.SetNestedSlice(desired.Object, []any{map[string]any{"name": "app", "image": "mutable:latest"}}, "spec", "template", "spec", "containers")
31
+ case "init image":
32
+ _ = unstructured.SetNestedSlice(desired.Object, []any{map[string]any{"name": "init", "image": "mutable:latest"}}, "spec", "template", "spec", "initContainers")
33
+ case "external":
34
+ desired.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "Helm"})
35
+ case "status":
36
+ desired.Object["status"] = map[string]any{"observedGeneration": int64(1)}
37
+ case "missing image":
38
+ unstructured.RemoveNestedField(desired.Object, "spec", "template", "spec", "containers")
39
+ }
40
+ if err := ValidateArgoResources(app, resources); (err == nil) != (mode == "valid") {
41
+ t.Fatalf("%s: %v", mode, err)
42
+ }
43
+ })
44
+ }
45
+ }
46
+
47
+ func TestArgoResourceOwnerCheckIsReadOnlyAndRejectsAdoption(t *testing.T) {
48
+ for _, mode := range []string{"owned", "absent", "unowned", "other app", "direct", "helm"} {
49
+ t.Run(mode, func(t *testing.T) {
50
+ app, live := argoFixture(t)
51
+ desired := live.DeepCopy()
52
+ desired.SetUID("")
53
+ desired.SetAnnotations(nil)
54
+ desired.SetGeneration(0)
55
+ delete(desired.Object, "status")
56
+ switch mode {
57
+ case "unowned":
58
+ live.SetAnnotations(nil)
59
+ case "other app":
60
+ live.SetAnnotations(map[string]string{"argocd.argoproj.io/tracking-id": "other:apps/Deployment:demo/web"})
61
+ case "direct":
62
+ live.SetManagedFields([]metav1.ManagedFieldsEntry{{Manager: "opensoha-manifest/other"}})
63
+ case "helm":
64
+ live.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "Helm"})
65
+ }
66
+ objects := []runtime.Object{live}
67
+ if mode == "absent" {
68
+ objects = nil
69
+ }
70
+ client := fake.NewSimpleDynamicClient(runtime.NewScheme(), objects...)
71
+ err := CheckArgoResourceOwners(t.Context(), client, app, []*unstructured.Unstructured{desired})
72
+ if (err == nil) != (mode == "owned" || mode == "absent") {
73
+ t.Fatalf("%s: %v", mode, err)
74
+ }
75
+ for _, action := range client.Actions() {
76
+ if action.GetVerb() != "get" {
77
+ t.Fatal("ownership check mutated a resource")
78
+ }
79
+ }
80
+ })
81
+ }
82
+ }
83
+
84
+ func TestArgoApplicationRejectsDirectWriters(t *testing.T) {
85
+ app, _ := argoFixture(t)
86
+ for _, action := range []string{"apply", "update", "delete"} {
87
+ client := fake.NewSimpleDynamicClient(runtime.NewScheme(), app.DeepCopy())
88
+ resource := client.Resource(argoApplications).Namespace(app.GetNamespace())
89
+ var err error
90
+ switch action {
91
+ case "apply":
92
+ _, err = ApplyManifest(t.Context(), resource, argoDesiredFixture(app), metav1.PatchOptions{FieldManager: "direct"})
93
+ case "update":
94
+ _, err = UpdateManifest(t.Context(), resource, app.DeepCopy())
95
+ case "delete":
96
+ err = DeleteManifest(t.Context(), resource, app.GetName(), string(app.GetUID()))
97
+ }
98
+ if err == nil {
99
+ t.Fatalf("direct %s accepted GitOps root", action)
100
+ }
101
+ for _, call := range client.Actions() {
102
+ if call.GetVerb() != "get" {
103
+ t.Fatal("direct writer mutated GitOps root")
104
+ }
105
+ }
106
+ }
107
+ }