@opensoha/contracts 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/COMPATIBILITY.md +72 -0
  2. package/README.md +38 -0
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/outpost_headers.go +17 -0
  5. package/auth/outpost_headers_test.go +16 -0
  6. package/auth/permission-catalog.json +383 -1
  7. package/compat/consumer-revisions.json +7 -0
  8. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  9. package/compat/openapi-loose-boundaries-0.1.0.json +78 -46
  10. package/delivery/canonical.go +136 -0
  11. package/delivery/document.go +203 -0
  12. package/delivery/document.schema.json +1527 -0
  13. package/delivery/document_test.go +191 -0
  14. package/delivery/parser.go +155 -0
  15. package/evaluation/evaluation-run.schema.json +64 -10
  16. package/fixtures/json-schema/gateway-manifest/invalid/bad-execution-mode.json +73 -0
  17. package/fixtures/json-schema/gateway-manifest/invalid/unversioned-check.json +85 -0
  18. package/fixtures/json-schema/gateway-manifest/valid/checks.json +86 -0
  19. package/fixtures/json-schema/gateway-manifest/valid/versioned-task.json +73 -0
  20. package/fixtures/json-schema/network-ingest-event/invalid/vpn-probe-negative-rtt.json +34 -0
  21. package/fixtures/json-schema/network-ingest-event/invalid/vpn-stats-secret.json +27 -0
  22. package/fixtures/json-schema/network-ingest-event/valid/vpn-probe-batch.json +45 -0
  23. package/fixtures/json-schema/network-ingest-event/valid/vpn-tunnel-stats.json +26 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-bad-token.json +15 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-mode-override.json +16 -0
  26. package/fixtures/json-schema/network-runtime-protocol/valid/managed-vpn-macos-enrollment.json +27 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-managed-connect.json +15 -0
  28. package/fixtures/openapi/AgentChatInput/invalid/empty-question.json +4 -0
  29. package/fixtures/openapi/AgentChatInput/invalid/system-role.json +9 -0
  30. package/fixtures/openapi/AgentChatInput/valid/authorized-background.json +5 -0
  31. package/fixtures/openapi/AgentChatInput/valid/conversation.json +15 -0
  32. package/fixtures/openapi/AgentRunnerToolCallRequest/invalid/model-identity.json +1 -0
  33. package/fixtures/openapi/AgentRunnerToolCallRequest/valid/search.json +1 -0
  34. package/fixtures/openapi/ApplicationInput/invalid/negative-version.json +6 -0
  35. package/fixtures/openapi/ApplicationInput/invalid/zero-version.json +6 -0
  36. package/fixtures/openapi/ApplicationInput/valid/versioned.json +6 -0
  37. package/fixtures/openapi/ApprovalRequest/invalid/resolved-scopes.json +20 -0
  38. package/fixtures/openapi/ApprovalRequest/valid/resolved-scopes.json +21 -0
  39. package/fixtures/openapi/BuildTemplateInput/invalid/negative-revision.json +6 -0
  40. package/fixtures/openapi/BuildTemplateInput/valid/draft.json +11 -0
  41. package/fixtures/openapi/BuildTriggerRequest/invalid/no-ref.json +1 -0
  42. package/fixtures/openapi/BuildTriggerRequest/valid/no-environment.json +1 -0
  43. package/fixtures/openapi/BuildpacksConfiguration/invalid/mutable-builder.json +5 -0
  44. package/fixtures/openapi/BuildpacksConfiguration/invalid/privilege-override.json +6 -0
  45. package/fixtures/openapi/BuildpacksConfiguration/invalid/unsupported-platform.json +5 -0
  46. package/fixtures/openapi/BuildpacksConfiguration/valid/pinned.json +5 -0
  47. package/fixtures/openapi/CapabilityAssessment/invalid/contract.json +16 -0
  48. package/fixtures/openapi/CapabilityAssessment/valid/contract.json +16 -0
  49. package/fixtures/openapi/CapabilityPlan/invalid/contract.json +16 -0
  50. package/fixtures/openapi/CapabilityPlan/valid/contract.json +19 -0
  51. package/fixtures/openapi/CapabilityPlanValidation/invalid/contract.json +5 -0
  52. package/fixtures/openapi/CapabilityPlanValidation/valid/contract.json +11 -0
  53. package/fixtures/openapi/CapabilityTask/invalid/contract.json +33 -0
  54. package/fixtures/openapi/CapabilityTask/valid/contract.json +34 -0
  55. package/fixtures/openapi/CapabilityTaskInput/invalid/contract.json +22 -0
  56. package/fixtures/openapi/CapabilityTaskInput/valid/contract.json +22 -0
  57. package/fixtures/openapi/CapabilityValueSemantic/invalid/contract.json +4 -0
  58. package/fixtures/openapi/CapabilityValueSemantic/valid/contract.json +7 -0
  59. package/fixtures/openapi/ConnectionCheckResult/invalid/missing-health.json +1 -0
  60. package/fixtures/openapi/ConnectionCheckResult/invalid/task-id.json +1 -0
  61. package/fixtures/openapi/ConnectionCheckResult/valid/failure.json +1 -0
  62. package/fixtures/openapi/ConnectionCheckResult/valid/healthy.json +1 -0
  63. package/fixtures/openapi/DeliveryAccessResult/invalid/release-verification.json +8 -0
  64. package/fixtures/openapi/DeliveryAccessResult/valid/release-verification.json +8 -0
  65. package/fixtures/openapi/DeliveryBatchAssessment/invalid/release-verification.json +11 -0
  66. package/fixtures/openapi/DeliveryBatchAssessment/valid/release-verification.json +11 -0
  67. package/fixtures/openapi/DeliveryBatchAssessmentInput/invalid/release-verification.json +9 -0
  68. package/fixtures/openapi/DeliveryBatchAssessmentInput/valid/release-verification.json +8 -0
  69. package/fixtures/openapi/DeliveryBatchInput/invalid/ambiguous-source.json +19 -0
  70. package/fixtures/openapi/DeliveryBatchInput/invalid/missing-idempotency.json +16 -0
  71. package/fixtures/openapi/DeliveryBatchInput/valid/quick-deploy.json +17 -0
  72. package/fixtures/openapi/DeliveryBatchInput/valid/saved-workflow.json +5 -0
  73. package/fixtures/openapi/DeliveryBlueprintInput/invalid/unversioned-preset.json +31 -0
  74. package/fixtures/openapi/DeliveryBlueprintInput/valid/service-preset.json +32 -0
  75. package/fixtures/openapi/DeliveryDocument/invalid/unknown-field.json +24 -0
  76. package/fixtures/openapi/DeliveryDocument/invalid/unknown-kind.json +23 -0
  77. package/fixtures/openapi/DeliveryDocument/invalid/unknown-version.json +23 -0
  78. package/fixtures/openapi/DeliveryDocument/valid/build-template.json +26 -0
  79. package/fixtures/openapi/DeliveryDocument/valid/deployment-template.json +26 -0
  80. package/fixtures/openapi/DeliveryDocument/valid/legacy-dag.json +39 -0
  81. package/fixtures/openapi/DeliveryDocument/valid/workflow-template.json +23 -0
  82. package/fixtures/openapi/DeliveryDocument/valid/workflow.json +20 -0
  83. package/fixtures/openapi/DeliveryDraftInput/invalid/short-idempotency-key.json +30 -0
  84. package/fixtures/openapi/DeliveryDraftInput/valid/idempotent-create.json +30 -0
  85. package/fixtures/openapi/DeliveryDraftService/invalid/zero-version.json +7 -0
  86. package/fixtures/openapi/DeliveryDraftService/valid/versioned-template.json +9 -0
  87. package/fixtures/openapi/DeliveryExecutionHistoryPage/invalid-kind.json +9 -0
  88. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-build.json +16 -0
  89. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-empty.json +3 -0
  90. package/fixtures/openapi/DeliveryPlanApprovalInput/invalid/confirm.json +1 -0
  91. package/fixtures/openapi/DeliveryPlanApprovalInput/valid/approve.json +1 -0
  92. package/fixtures/openapi/DeliveryPlanInput/invalid/draft-manifest-revision.json +1 -0
  93. package/fixtures/openapi/DeliveryPlanInput/valid/manifest-revision.json +1 -0
  94. package/fixtures/openapi/DeliveryTargetInput/invalid/deploy-without-environment.json +7 -0
  95. package/fixtures/openapi/DeliveryTargetInput/valid/build-without-environment.json +6 -0
  96. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/duplicate-kind.json +13 -0
  97. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/embedded-credentials.json +16 -0
  98. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/empty-kinds.json +10 -0
  99. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/negative-generation.json +15 -0
  100. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/unknown-kind.json +12 -0
  101. package/fixtures/openapi/DeliveryTemplateSourceInput/valid/manual-source.json +15 -0
  102. package/fixtures/openapi/DeliveryWorkflowDefinition/invalid/unversioned-template.json +15 -0
  103. package/fixtures/openapi/DeliveryWorkflowDefinition/valid/cross-app.json +24 -0
  104. package/fixtures/openapi/DeliveryWorkflowInput/invalid/short-key.json +1 -0
  105. package/fixtures/openapi/DeliveryWorkflowInput/valid/idempotent-create.json +1 -0
  106. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-target-override.json +7 -0
  107. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-zero-version.json +6 -0
  108. package/fixtures/openapi/DeploymentTemplatePreviewInput/valid/valid.json +6 -0
  109. package/fixtures/openapi/DockerDeliveryConfiguration/invalid/empty-mappings.json +5 -0
  110. package/fixtures/openapi/DockerDeliveryConfiguration/valid/compose.json +7 -0
  111. package/fixtures/openapi/DockerDeliverySnapshot/invalid/missing-required-field.json +19 -0
  112. package/fixtures/openapi/DockerDeliverySnapshot/valid/frozen-project.json +20 -0
  113. package/fixtures/openapi/DockerOperationCallbackRequest/invalid/cancellation-acknowledged-not-boolean.json +13 -0
  114. package/fixtures/openapi/DockerOperationCallbackRequest/valid/cancellation-acknowledged.json +13 -0
  115. package/fixtures/openapi/DockerProjectInput/invalid/empty-host.json +4 -0
  116. package/fixtures/openapi/DockerProjectInput/valid/inline-idempotent.json +7 -0
  117. package/fixtures/openapi/DockerQuickCreateHostInput/invalid/capacity-type.json +1 -0
  118. package/fixtures/openapi/DockerQuickCreateHostInput/valid/capacity.json +1 -0
  119. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/plain-credential.json +21 -0
  120. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/release-name.json +20 -0
  121. package/fixtures/openapi/HelmDeliveryConfiguration/valid/private-oci.json +20 -0
  122. package/fixtures/openapi/HelmDeliverySnapshot/invalid/secret-plan.json +31 -0
  123. package/fixtures/openapi/HelmDeliverySnapshot/invalid/unknown-operation.json +28 -0
  124. package/fixtures/openapi/HelmDeliverySnapshot/valid/first-install.json +28 -0
  125. package/fixtures/openapi/IdentityApplication/invalid/outpost-status.json +1 -0
  126. package/fixtures/openapi/IdentityApplicationOnboardingInput/invalid/missing-access.json +12 -0
  127. package/fixtures/openapi/IdentityApplicationOnboardingInput/valid/link.json +13 -0
  128. package/fixtures/openapi/IdentityOutpost/invalid/missing-runtime-status.json +9 -0
  129. package/fixtures/openapi/IdentityOutpost/valid/healthy.json +17 -0
  130. package/fixtures/openapi/IdentityOutpost/valid/unregistered.json +12 -0
  131. package/fixtures/openapi/IdentityOutpostDeployment/invalid/missing-protocol.json +3 -0
  132. package/fixtures/openapi/IdentityOutpostDeployment/valid/remote.json +6 -0
  133. package/fixtures/openapi/IdentityOutpostInput/invalid/forged-observation.json +7 -0
  134. package/fixtures/openapi/IdentityOutpostInput/valid/forward-auth.json +6 -0
  135. package/fixtures/openapi/IdentityOutpostInput/valid/legacy.json +8 -0
  136. package/fixtures/openapi/IdentityProviderSetup/invalid/fake-verification.json +1 -0
  137. package/fixtures/openapi/IdentityProviderSetup/valid/incomplete.json +1 -0
  138. package/fixtures/openapi/IdentityProviderSetup/valid/remote.json +1 -0
  139. package/fixtures/openapi/IdentityProviderUserMetadata/invalid/attributes.json +8 -0
  140. package/fixtures/openapi/IdentityProviderUserMetadata/valid/attributes.json +13 -0
  141. package/fixtures/openapi/KubernetesCustomResource/invalid/negative-generation.json +1 -0
  142. package/fixtures/openapi/KubernetesCustomResource/valid/deleting-operator.json +1 -0
  143. package/fixtures/openapi/KubernetesResourceCreationReceipt/invalid/manifest-content.json +20 -0
  144. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/confirmed.json +20 -0
  145. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/unknown.json +19 -0
  146. package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/negative-resource-observation.json +57 -0
  147. package/fixtures/openapi/ManifestDeploymentEnvelope/valid/operator-deleting.json +57 -0
  148. package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/gitops-empty.json +7 -0
  149. package/fixtures/openapi/ManifestExecutionTaskPayload/valid/gitops-frozen-children.json +18 -0
  150. package/fixtures/openapi/ManifestKustomizeOptions/invalid/escaping-entry.json +1 -0
  151. package/fixtures/openapi/ManifestKustomizeOptions/invalid/mutable-image.json +1 -0
  152. package/fixtures/openapi/ManifestKustomizeOptions/valid/overlay-images.json +7 -0
  153. package/fixtures/openapi/ManifestRevisionInput/invalid/missing-version.json +1 -0
  154. package/fixtures/openapi/ManifestRevisionInput/valid/current-draft.json +1 -0
  155. package/fixtures/openapi/ManifestServiceTemplateInputs/invalid/mutable-image.json +8 -0
  156. package/fixtures/openapi/ManifestServiceTemplateInputs/valid/frozen-service-image.json +9 -0
  157. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/auto-with-gateway.json +6 -0
  158. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/missing-manual-gateway.json +5 -0
  159. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/mode-override.json +7 -0
  160. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/auto-intent.json +5 -0
  161. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/manual-intent.json +6 -0
  162. package/fixtures/openapi/NetworkVPNProfileInput/invalid/unknown-private-key.json +25 -0
  163. package/fixtures/openapi/NetworkVPNProfileInput/valid/assigned-profile.json +24 -0
  164. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/invalid/unbounded-attempts.json +21 -0
  165. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/valid/provider-policy.json +21 -0
  166. package/fixtures/openapi/ObservabilityMetricAssessmentInput/invalid/arbitrary-expression.json +10 -0
  167. package/fixtures/openapi/ObservabilityMetricAssessmentInput/valid/metric-window.json +10 -0
  168. package/fixtures/openapi/PluginManifest/valid/hermes-api.json +53 -0
  169. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/missing-identity.json +5 -0
  170. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/skip-analysis.json +5 -0
  171. package/fixtures/openapi/ProgressiveRolloutControlInput/valid/pause.json +5 -0
  172. package/fixtures/openapi/ReleaseTargetInput/invalid/missing-required-field.json +15 -0
  173. package/fixtures/openapi/ReleaseTargetInput/valid/docker-compose.json +16 -0
  174. package/fixtures/openapi/RepositoryAnalysis/invalid/moving-ref.json +11 -0
  175. package/fixtures/openapi/RepositoryAnalysis/valid/go.json +29 -0
  176. package/fixtures/openapi/RepositoryAnalysisInput/invalid/arbitrary-source-url.json +6 -0
  177. package/fixtures/openapi/RepositoryAnalysisInput/invalid/missing-ref.json +4 -0
  178. package/fixtures/openapi/RepositoryAnalysisInput/valid/nested-project.json +6 -0
  179. package/fixtures/openapi/ServiceDeploymentTemplateBinding/invalid/invalid-unpinned.json +4 -0
  180. package/fixtures/openapi/ServiceDeploymentTemplateBinding/valid/valid.json +7 -0
  181. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-executable-schema.json +25 -0
  182. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-missing-schema.json +21 -0
  183. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-mixed-renderers.json +40 -0
  184. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-nested-parameters.json +41 -0
  185. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-yaml.json +34 -0
  186. package/fixtures/openapi/TemplatePublishInput/invalid/missing-revision.json +1 -0
  187. package/fixtures/openapi/TemplatePublishInput/invalid/unknown-field.json +4 -0
  188. package/fixtures/openapi/TemplatePublishInput/invalid/zero-revision.json +3 -0
  189. package/fixtures/openapi/TemplatePublishInput/valid/revision.json +3 -0
  190. package/fixtures/openapi/ToolCapability/invalid/bad-idempotency.json +35 -0
  191. package/fixtures/openapi/ToolCapability/invalid/unversioned-check.json +47 -0
  192. package/fixtures/openapi/ToolCapability/valid/checks.json +48 -0
  193. package/fixtures/openapi/ToolCapability/valid/versioned-task.json +35 -0
  194. package/fixtures/openapi/ToolInvocationRequest/invalid/empty-version.json +8 -0
  195. package/fixtures/openapi/ToolInvocationRequest/valid/versioned.json +8 -0
  196. package/fixtures/openapi/ToolInvocationResult/invalid/missing-status-call.json +13 -0
  197. package/fixtures/openapi/ToolInvocationResult/valid/task.json +20 -0
  198. package/fixtures/openapi/UpdateBrandingSettingsRequest/invalid/dark-logo-type.json +9 -0
  199. package/fixtures/openapi/UpdateBrandingSettingsRequest/valid/dark-logos.json +10 -0
  200. package/fixtures/openapi/VirtualMachineCreateInput/invalid/capacity-type.json +1 -0
  201. package/fixtures/openapi/VirtualMachineCreateInput/valid/capacity.json +1 -0
  202. package/fixtures/openapi/VirtualizationCapacityInput/invalid/missing-disk.json +1 -0
  203. package/fixtures/openapi/VirtualizationCapacityInput/valid/running-vm.json +1 -0
  204. package/fixtures/openapi/VirtualizationCapacityResult/invalid/invented-status.json +1 -0
  205. package/fixtures/openapi/VirtualizationCapacityResult/valid/available.json +1 -0
  206. package/fixtures/openapi/VirtualizationWorkerCreateInput/invalid/secret-input.json +5 -0
  207. package/fixtures/openapi/VirtualizationWorkerCreateInput/valid/one-worker.json +4 -0
  208. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/external-owner.json +27 -0
  209. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/zero-budget.json +27 -0
  210. package/fixtures/openapi/VirtualizationWorkerPoolInput/valid/registered.json +27 -0
  211. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/invalid/nested-data.json +5 -0
  212. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/valid/empty.json +3 -0
  213. package/fixtures/openapi/WorkbenchContextSelection/invalid/missing-file-content.json +8 -0
  214. package/fixtures/openapi/WorkbenchContextSelection/invalid/unknown-reference-kind.json +8 -0
  215. package/fixtures/openapi/WorkbenchContextSelection/valid/text-and-resource.json +17 -0
  216. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/trigger-without-plan.json +10 -0
  217. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/unknown-trigger.json +28 -0
  218. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/zero-interval.json +28 -0
  219. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/alert-capability.json +30 -0
  220. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/scheduled-capability.json +28 -0
  221. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/missing-rule.json +3 -0
  222. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/schedule-alert-selector.json +4 -0
  223. package/fixtures/openapi/WorkbenchInspectionTrigger/valid/schedule.json +3 -0
  224. package/fixtures/openapi/WorkbenchModelPreferences/invalid/unknown-effort.json +1 -0
  225. package/fixtures/openapi/WorkbenchModelPreferences/valid/selected.json +1 -0
  226. package/fixtures/openapi/WorkflowCatalogPage/invalid/unsupported-kind.json +6 -0
  227. package/fixtures/openapi/WorkflowCatalogPage/valid/build-without-environment.json +6 -0
  228. package/fixtures/openapi/WorkflowTemplateInput/invalid/zero-revision.json +6 -0
  229. package/fixtures/openapi/WorkflowTemplateInput/valid/draft.json +10 -0
  230. package/gen/go/sohaapi/client.go +11 -3
  231. package/gen/go/sohaapi/client_test.go +26 -3
  232. package/gen/go/sohaapi/types.go +8103 -863
  233. package/gen/go/sohaapi/types_test.go +100 -1
  234. package/gen/go/sohaapi/vpn_models_test.go +21 -0
  235. package/gen/ts/sohaapi/index.ts +20390 -13160
  236. package/go.mod +107 -5
  237. package/go.sum +379 -16
  238. package/helmrelease/delivery.go +283 -0
  239. package/helmrelease/delivery_test.go +102 -0
  240. package/helmrelease/frozen_chart.go +91 -0
  241. package/helmrelease/result.go +27 -0
  242. package/helmrelease/runtime/delivery.go +508 -0
  243. package/helmrelease/runtime/delivery_test.go +236 -0
  244. package/helmrelease/runtime/resources_test.go +60 -0
  245. package/helmrelease/values.go +99 -0
  246. package/knowledge/context-envelope.schema.json +1 -0
  247. package/mcp/gateway-manifest.schema.json +387 -72
  248. package/network/README.md +40 -0
  249. package/network/network-ingest-event.schema.json +228 -0
  250. package/network/network-runtime-protocol.schema.json +457 -1
  251. package/openapi/soha-api.json +54874 -38722
  252. package/openapi/soha-api.yaml +12199 -4982
  253. package/openapi/spec_generated.go +3 -3
  254. package/package.json +3 -1
  255. package/resource/manifest_health.go +155 -0
  256. package/resource/manifest_health_test.go +37 -0
  257. package/resource/models.go +14 -0
  258. package/resource/runtime/argocd.go +166 -0
  259. package/resource/runtime/argocd_integration_test.go +142 -0
  260. package/resource/runtime/argocd_observation.go +143 -0
  261. package/resource/runtime/argocd_operation.go +253 -0
  262. package/resource/runtime/argocd_resources.go +115 -0
  263. package/resource/runtime/argocd_resources_test.go +107 -0
  264. package/resource/runtime/argocd_test.go +386 -0
  265. package/resource/runtime/health.go +115 -0
  266. package/resource/runtime/health_test.go +78 -0
  267. package/resource/runtime/operator_integration_test.go +174 -0
  268. package/resource/runtime/ownership.go +178 -0
  269. package/resource/runtime/ownership_test.go +178 -0
  270. package/resource/runtime/rollout_integration_test.go +287 -0
  271. package/resource/runtime/rollout_observation.go +252 -0
  272. package/resource/runtime/rollout_operation.go +398 -0
  273. package/resource/runtime/rollout_resources.go +385 -0
  274. package/resource/runtime/rollout_task.go +165 -0
  275. package/resource/runtime/rollout_test.go +450 -0
@@ -0,0 +1,174 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
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/types"
19
+ "k8s.io/apimachinery/pkg/util/wait"
20
+ "k8s.io/client-go/dynamic"
21
+ "k8s.io/client-go/tools/clientcmd"
22
+ )
23
+
24
+ func TestWorkloadCronJobWithKubernetes(t *testing.T) {
25
+ path, namespace, image := os.Getenv("SOHA_OPERATOR_TEST_KUBECONFIG"), os.Getenv("SOHA_OPERATOR_TEST_NAMESPACE"), os.Getenv("SOHA_OPERATOR_TEST_IMAGE")
26
+ if path == "" {
27
+ t.Skip("set SOHA_OPERATOR_TEST_KUBECONFIG, namespace and digest-pinned image for the isolated controller fixture")
28
+ }
29
+ if !strings.HasPrefix(namespace, "soha-workflow-r6-") || !strings.Contains(image, "@sha256:") {
30
+ t.Fatal("isolated namespace and pinned image are required")
31
+ }
32
+ config, err := clientcmd.BuildConfigFromFlags("", path)
33
+ operatorRequire(t, err)
34
+ endpoint, err := url.Parse(config.Host)
35
+ if err != nil || endpoint.Hostname() != "127.0.0.1" {
36
+ t.Fatal("only the loopback integration cluster is supported")
37
+ }
38
+ config.Timeout = 10 * time.Second
39
+ client, err := dynamic.NewForConfig(config)
40
+ operatorRequire(t, err)
41
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
42
+ defer cancel()
43
+ name := "periodic-" + uuid.NewString()[:8]
44
+ deployments := client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace(namespace)
45
+ roots := client.Resource(schema.GroupVersionResource{Group: "workloads.soha.io", Version: "v1alpha1", Resource: "workloadcronjobs"}).Namespace(namespace)
46
+ targets := client.Resource(schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}).Namespace(namespace)
47
+ source, root := operatorIntegrationObjects(t, namespace, name, image)
48
+ options := metav1.PatchOptions{FieldManager: "soha-operator-integration"}
49
+ t.Cleanup(func() {
50
+ cleanup, stop := context.WithTimeout(context.Background(), 10*time.Second)
51
+ defer stop()
52
+ _ = deployments.Delete(cleanup, name, metav1.DeleteOptions{})
53
+ })
54
+ t.Cleanup(func() {
55
+ cleanup, stop := context.WithTimeout(context.Background(), 10*time.Second)
56
+ defer stop()
57
+ _ = roots.Delete(cleanup, name, metav1.DeleteOptions{})
58
+ })
59
+ _, err = ApplyManifest(ctx, deployments, source, options)
60
+ operatorRequire(t, err)
61
+ _, err = ApplyManifest(ctx, roots, root, options)
62
+ operatorRequire(t, err)
63
+ live := operatorWaitHealth(t, ctx, client, roots, name, "healthy")
64
+ target, err := targets.Get(ctx, name, metav1.GetOptions{})
65
+ operatorRequire(t, err)
66
+ if metav1.GetControllerOf(target) == nil || metav1.GetControllerOf(target).UID != live.GetUID() {
67
+ t.Fatal("target ownership missing")
68
+ }
69
+ // This task is deliberately suspended; synchronization does not imply a Job ran.
70
+ jobs, err := client.Resource(schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}).Namespace(namespace).List(ctx, metav1.ListOptions{})
71
+ operatorRequire(t, err)
72
+ for _, job := range jobs.Items {
73
+ if owner := metav1.GetControllerOf(&job); owner != nil && owner.UID == target.GetUID() {
74
+ t.Fatal("suspended fixture unexpectedly ran a Job")
75
+ }
76
+ }
77
+ for _, schedule := range []string{"0 3 * * *", "0 2 * * *"} {
78
+ operatorRequire(t, unstructured.SetNestedField(root.Object, schedule, "spec", "cronJobSpec", "schedule"))
79
+ _, err = ApplyManifest(ctx, roots, root, options)
80
+ operatorRequire(t, err)
81
+ operatorWaitHealth(t, ctx, client, roots, name, "healthy")
82
+ target, err = targets.Get(ctx, name, metav1.GetOptions{})
83
+ operatorRequire(t, err)
84
+ actual, _, _ := unstructured.NestedString(target.Object, "spec", "schedule")
85
+ if actual != schedule {
86
+ t.Fatalf("schedule = %s, want %s", actual, schedule)
87
+ }
88
+ }
89
+ // Even force must not write the Operator-owned child directly.
90
+ yes := true
91
+ if _, err := ApplyManifest(ctx, targets, target, metav1.PatchOptions{FieldManager: "other-delivery", Force: &yes, DryRun: []string{metav1.DryRunAll}}); err == nil {
92
+ t.Fatal("second writer was accepted")
93
+ }
94
+ // A missing source must degrade, and recreating it must recover with a new UID.
95
+ operatorRequire(t, deployments.Delete(ctx, name, metav1.DeleteOptions{}))
96
+ operatorWaitHealth(t, ctx, client, roots, name, "degraded")
97
+ _, err = ApplyManifest(ctx, deployments, source, options)
98
+ operatorRequire(t, err)
99
+ operatorWaitHealth(t, ctx, client, roots, name, "healthy")
100
+ operatorVerifyDeletion(t, ctx, client, roots, targets, name)
101
+ t.Log("real Operator: owned CronJob, generation/version observation, update/rollback, second-writer rejection, source-loss recovery and finalizer/foreground deletion passed")
102
+ }
103
+
104
+ func operatorWaitHealth(t *testing.T, ctx context.Context, client dynamic.Interface, roots dynamic.ResourceInterface, name, want string) *unstructured.Unstructured {
105
+ t.Helper()
106
+ var live *unstructured.Unstructured
107
+ var health string
108
+ err := wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) {
109
+ var err error
110
+ live, err = roots.Get(ctx, name, metav1.GetOptions{})
111
+ if err != nil {
112
+ return false, err
113
+ }
114
+ health, err = ManifestHealth(ctx, client, live)
115
+ return health == want, err
116
+ })
117
+ if err != nil {
118
+ t.Fatalf("Operator health = %s, want %s: %v", health, want, err)
119
+ }
120
+ return live
121
+ }
122
+
123
+ func operatorVerifyDeletion(t *testing.T, ctx context.Context, client dynamic.Interface, roots, targets dynamic.ResourceInterface, name string) {
124
+ t.Helper()
125
+ _, err := roots.Patch(ctx, name, types.MergePatchType, []byte(`{"metadata":{"finalizers":["integration.soha.io/hold"]}}`), metav1.PatchOptions{})
126
+ operatorRequire(t, err)
127
+ if err := DeleteManifest(ctx, roots, name, "old-root-uid"); !apierrors.IsConflict(err) {
128
+ t.Fatalf("replacement fence: %v", err)
129
+ }
130
+ operatorRequire(t, DeleteManifest(ctx, roots, name, ""))
131
+ live, err := roots.Get(ctx, name, metav1.GetOptions{})
132
+ operatorRequire(t, err)
133
+ if live.GetDeletionTimestamp() == nil || len(live.GetFinalizers()) == 0 {
134
+ t.Fatal("finalizer did not preserve deleting state")
135
+ }
136
+ health, err := ManifestHealth(ctx, client, live)
137
+ operatorRequire(t, err)
138
+ if health != "degraded" {
139
+ t.Fatal("deleting object was healthy")
140
+ }
141
+ if _, err := ApplyManifest(ctx, roots, live, metav1.PatchOptions{FieldManager: "soha-operator-integration"}); err == nil {
142
+ t.Fatal("deleting root accepted a new write")
143
+ }
144
+ // Only the fixture removes its own hold finalizer. Production never does this.
145
+ operatorRequire(t, wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) {
146
+ _, err := targets.Get(ctx, name, metav1.GetOptions{})
147
+ return apierrors.IsNotFound(err), nil
148
+ }))
149
+ _, err = roots.Patch(ctx, name, types.MergePatchType, []byte(`{"metadata":{"finalizers":[]}}`), metav1.PatchOptions{})
150
+ operatorRequire(t, err)
151
+ operatorRequire(t, wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) {
152
+ _, err := roots.Get(ctx, name, metav1.GetOptions{})
153
+ return apierrors.IsNotFound(err), nil
154
+ }))
155
+ }
156
+
157
+ func operatorIntegrationObjects(t *testing.T, namespace, name, image string) (*unstructured.Unstructured, *unstructured.Unstructured) {
158
+ t.Helper()
159
+ decode := func(raw string) *unstructured.Unstructured {
160
+ var object map[string]any
161
+ operatorRequire(t, json.Unmarshal([]byte(raw), &object))
162
+ return &unstructured.Unstructured{Object: object}
163
+ }
164
+ source := decode(fmt.Sprintf(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":%q,"namespace":%q},"spec":{"replicas":1,"selector":{"matchLabels":{"app":%q}},"template":{"metadata":{"labels":{"app":%q}},"spec":{"containers":[{"name":"app","image":%q,"command":["sleep","3600"]}]}}}}`, name, namespace, name, name, image))
165
+ root := decode(fmt.Sprintf(`{"apiVersion":"workloads.soha.io/v1alpha1","kind":"WorkloadCronJob","metadata":{"name":%q,"namespace":%q},"spec":{"sourceRef":{"kind":"Deployment","name":%q,"container":"app"},"targetContainer":"task","cronJobSpec":{"schedule":"0 2 * * *","suspend":true,"jobTemplate":{"spec":{"template":{"spec":{"restartPolicy":"Never","containers":[{"name":"task","image":%q,"command":["true"]}]}}}}}}}`, name, namespace, name, image))
166
+ return source, root
167
+ }
168
+
169
+ func operatorRequire(t *testing.T, err error) {
170
+ t.Helper()
171
+ if err != nil {
172
+ t.Fatal(err)
173
+ }
174
+ }
@@ -0,0 +1,178 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "strings"
9
+
10
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
11
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13
+ "k8s.io/apimachinery/pkg/runtime/schema"
14
+ "k8s.io/apimachinery/pkg/types"
15
+ "k8s.io/apimachinery/pkg/util/sets"
16
+ "k8s.io/client-go/dynamic"
17
+ "k8s.io/client-go/util/csaupgrade"
18
+ "k8s.io/client-go/util/retry"
19
+ )
20
+
21
+ // ManifestAgentProviderPrefix prevents legacy runners from claiming operations
22
+ // that require frozen controller resources and ownership-fenced writes.
23
+ const ManifestAgentProviderPrefix = "manifest_agent_v3."
24
+ const ManifestAgentCapability = "manifest.execution.v3"
25
+
26
+ var ErrResourceOwnership = errors.New("resource mutation is blocked by ownership")
27
+
28
+ // ApplyManifest serializes the ownership check with the write. Creation uses
29
+ // create semantics so a concurrent external owner cannot be silently adopted.
30
+ func ApplyManifest(ctx context.Context, client dynamic.ResourceInterface, desired *unstructured.Unstructured, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
31
+ if IsArgoApplication(desired) {
32
+ return nil, fmt.Errorf("%w: Argo CD Applications require frozen GitOps execution", ErrResourceOwnership)
33
+ }
34
+ if IsArgoRollout(desired) || desired.GetAnnotations()[RolloutOwnerAnnotation] != "" {
35
+ return nil, fmt.Errorf("%w: progressive resources require frozen rollout execution", ErrResourceOwnership)
36
+ }
37
+ live, err := client.Get(ctx, desired.GetName(), metav1.GetOptions{})
38
+ if apierrors.IsNotFound(err) {
39
+ created, createErr := client.Create(ctx, desired, metav1.CreateOptions{FieldManager: options.FieldManager, DryRun: options.DryRun, FieldValidation: options.FieldValidation})
40
+ if createErr != nil || len(options.DryRun) != 0 {
41
+ return created, createErr
42
+ }
43
+ live, err = created, nil
44
+ }
45
+ if err != nil {
46
+ return nil, err
47
+ }
48
+ expectedUID := live.GetUID()
49
+ var applied *unstructured.Unstructured
50
+ err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
51
+ current, readErr := client.Get(ctx, desired.GetName(), metav1.GetOptions{})
52
+ if readErr != nil {
53
+ return readErr
54
+ }
55
+ if current.GetUID() != expectedUID {
56
+ return fmt.Errorf("resource was replaced during apply")
57
+ }
58
+ if ownerErr := ValidateDirectManifestOwner(current); ownerErr != nil {
59
+ return ownerErr
60
+ }
61
+ applied, readErr = applyObservedManifest(ctx, client, desired, current, options)
62
+ return readErr
63
+ })
64
+ return applied, err
65
+ }
66
+
67
+ func applyObservedManifest(ctx context.Context, client dynamic.ResourceInterface, desired, live *unstructured.Unstructured, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
68
+ if len(options.DryRun) == 0 {
69
+ // Native migration moves only this manager's Create/Update ownership to
70
+ // Apply and guards resourceVersion; other field owners stay intact.
71
+ upgrade, err := csaupgrade.UpgradeManagedFieldsPatch(live, sets.New(options.FieldManager), options.FieldManager)
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+ if len(upgrade) != 0 {
76
+ live, err = client.Patch(ctx, desired.GetName(), types.JSONPatchType, upgrade, metav1.PatchOptions{FieldManager: options.FieldManager})
77
+ if err != nil {
78
+ return nil, err
79
+ }
80
+ }
81
+ }
82
+ return patchOwnedManifest(ctx, client, desired, live, options)
83
+ }
84
+
85
+ func patchOwnedManifest(ctx context.Context, client dynamic.ResourceInterface, desired, live *unstructured.Unstructured, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
86
+ write := desired.DeepCopy()
87
+ write.SetResourceVersion(live.GetResourceVersion())
88
+ write.SetUID(live.GetUID())
89
+ body, err := json.Marshal(write.Object)
90
+ if err != nil {
91
+ return nil, err
92
+ }
93
+ return client.Patch(ctx, desired.GetName(), types.ApplyPatchType, body, options)
94
+ }
95
+
96
+ // ValidateDirectManifestOwner keeps direct SSA from becoming a second writer
97
+ // for a controller's child, a Helm release or a registered GitOps resource.
98
+ func ValidateDirectManifestOwner(object metav1.Object) error {
99
+ if object == nil {
100
+ return nil
101
+ }
102
+ if object.GetDeletionTimestamp() != nil {
103
+ return fmt.Errorf("%w: resource is being deleted; wait for finalizers to finish", ErrResourceOwnership)
104
+ }
105
+ return validateExternalManifestOwner(object)
106
+ }
107
+
108
+ func validateExternalManifestOwner(object metav1.Object) error {
109
+ if resource, ok := object.(*unstructured.Unstructured); ok && IsArgoRollout(resource) {
110
+ return fmt.Errorf("%w: Argo Rollouts require frozen progressive execution", ErrResourceOwnership)
111
+ }
112
+ if object.GetAnnotations()[RolloutOwnerAnnotation] != "" {
113
+ return fmt.Errorf("%w: resource belongs to a progressive rollout", ErrResourceOwnership)
114
+ }
115
+ if resource, ok := object.(*unstructured.Unstructured); ok && IsArgoApplication(resource) {
116
+ return fmt.Errorf("%w: Argo CD Applications require frozen GitOps execution", ErrResourceOwnership)
117
+ }
118
+ if metav1.GetControllerOf(object) != nil {
119
+ return fmt.Errorf("%w: resource is managed by a controller; update its owning resource", ErrResourceOwnership)
120
+ }
121
+ labels, annotations := object.GetLabels(), object.GetAnnotations()
122
+ if labels["app.kubernetes.io/managed-by"] == "Helm" || annotations["meta.helm.sh/release-name"] != "" ||
123
+ labels["argocd.argoproj.io/instance"] != "" || annotations["argocd.argoproj.io/tracking-id"] != "" ||
124
+ labels["kustomize.toolkit.fluxcd.io/name"] != "" || labels["helm.toolkit.fluxcd.io/name"] != "" {
125
+ return fmt.Errorf("%w: resource has an external delivery owner", ErrResourceOwnership)
126
+ }
127
+ for _, field := range object.GetManagedFields() {
128
+ if strings.HasPrefix(field.Manager, "argocd") || field.Manager == "kustomize-controller" || field.Manager == "helm-controller" {
129
+ return fmt.Errorf("%w: resource is managed by a GitOps controller", ErrResourceOwnership)
130
+ }
131
+ }
132
+ return nil
133
+ }
134
+
135
+ // UpdateManifest preserves the user's optimistic lock and refuses updates to
136
+ // externally managed or deleting roots, including attempts to remove finalizers.
137
+ func UpdateManifest(ctx context.Context, client dynamic.ResourceInterface, desired *unstructured.Unstructured) (*unstructured.Unstructured, error) {
138
+ live, err := client.Get(ctx, desired.GetName(), metav1.GetOptions{})
139
+ if err != nil {
140
+ return nil, err
141
+ }
142
+ if err := ValidateDirectManifestOwner(live); err != nil {
143
+ return nil, err
144
+ }
145
+ write := desired.DeepCopy()
146
+ if write.GetUID() == "" {
147
+ write.SetUID(live.GetUID())
148
+ }
149
+ if write.GetResourceVersion() == "" {
150
+ write.SetResourceVersion(live.GetResourceVersion())
151
+ }
152
+ return client.Update(ctx, write, metav1.UpdateOptions{})
153
+ }
154
+
155
+ // DeleteManifest requests deletion of the observed root. A nil error confirms
156
+ // acceptance only; finalizers are never removed and completion must be observed.
157
+ func DeleteManifest(ctx context.Context, client dynamic.ResourceInterface, name, expectedUID string) error {
158
+ live, err := client.Get(ctx, name, metav1.GetOptions{})
159
+ if err != nil {
160
+ return err
161
+ }
162
+ uid, version := live.GetUID(), live.GetResourceVersion()
163
+ if uid == "" || version == "" || expectedUID != "" && string(uid) != expectedUID {
164
+ return apierrors.NewConflict(schema.GroupResource{Resource: live.GetKind()}, name, fmt.Errorf("resource identity changed; refresh before deleting"))
165
+ }
166
+ if err := validateExternalManifestOwner(live); err != nil {
167
+ return err
168
+ }
169
+ if live.GetDeletionTimestamp() != nil {
170
+ return nil
171
+ }
172
+ options := metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid, ResourceVersion: &version}}
173
+ if live.GetAPIVersion() == "workloads.soha.io/v1alpha1" && live.GetKind() == "WorkloadCronJob" {
174
+ foreground := metav1.DeletePropagationForeground
175
+ options.PropagationPolicy = &foreground
176
+ }
177
+ return client.Delete(ctx, name, options)
178
+ }
@@ -0,0 +1,178 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "testing"
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/apimachinery/pkg/runtime"
12
+ "k8s.io/apimachinery/pkg/runtime/schema"
13
+ "k8s.io/client-go/dynamic/fake"
14
+ ktesting "k8s.io/client-go/testing"
15
+ )
16
+
17
+ func TestManifestDeleteFencesUIDAndWaitsForFinalizers(t *testing.T) {
18
+ for _, mode := range []string{"accepted", "replaced", "deleting", "external"} {
19
+ t.Run(mode, func(t *testing.T) {
20
+ live := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "workloads.soha.io/v1alpha1", "kind": "WorkloadCronJob", "metadata": map[string]any{"name": "periodic", "namespace": "demo", "uid": "root-uid", "resourceVersion": "12"}}}
21
+ live.SetFinalizers([]string{"operator.example/hold"})
22
+ if mode == "deleting" {
23
+ now := metav1.Now()
24
+ live.SetDeletionTimestamp(&now)
25
+ }
26
+ if mode == "external" {
27
+ live.SetLabels(map[string]string{"argocd.argoproj.io/instance": "external"})
28
+ }
29
+ client := fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
30
+ writes := 0
31
+ client.PrependReactor("delete", "workloadcronjobs", func(action ktesting.Action) (bool, runtime.Object, error) {
32
+ writes++
33
+ deleteAction, ok := action.(ktesting.DeleteAction)
34
+ if !ok {
35
+ t.Fatal("unexpected delete action")
36
+ }
37
+ options := deleteAction.GetDeleteOptions()
38
+ if options.Preconditions == nil || *options.Preconditions.UID != live.GetUID() || *options.Preconditions.ResourceVersion != "12" || options.PropagationPolicy == nil || *options.PropagationPolicy != metav1.DeletePropagationForeground {
39
+ t.Fatalf("missing delete fences: %+v", options)
40
+ }
41
+ return true, nil, nil
42
+ })
43
+ uid := "root-uid"
44
+ if mode == "replaced" {
45
+ uid = "previous-uid"
46
+ }
47
+ resources := client.Resource(schema.GroupVersionResource{Group: "workloads.soha.io", Version: "v1alpha1", Resource: "workloadcronjobs"}).Namespace("demo")
48
+ err := DeleteManifest(context.Background(), resources, "periodic", uid)
49
+ if mode == "replaced" && !apierrors.IsConflict(err) || mode == "external" && err == nil || (mode == "accepted" || mode == "deleting") && err != nil {
50
+ t.Fatalf("delete: %v", err)
51
+ }
52
+ if (mode == "accepted" && writes != 1) || (mode != "accepted" && writes != 0) {
53
+ t.Fatalf("delete calls: %d", writes)
54
+ }
55
+ for _, action := range client.Actions() {
56
+ if action.GetVerb() != "get" && action.GetVerb() != "delete" {
57
+ t.Fatalf("unexpected finalizer write: %s", action.GetVerb())
58
+ }
59
+ }
60
+ })
61
+ }
62
+ }
63
+
64
+ func TestManifestUpdatePreservesCallerVersion(t *testing.T) {
65
+ live := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "settings", "namespace": "demo", "uid": "current", "resourceVersion": "12"}}}
66
+ client := fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
67
+ desired := live.DeepCopy()
68
+ desired.SetUID("previous")
69
+ desired.SetResourceVersion("11")
70
+ client.PrependReactor("update", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) {
71
+ update, ok := action.(ktesting.UpdateAction)
72
+ if !ok {
73
+ t.Fatal("unexpected update action")
74
+ }
75
+ write, ok := update.GetObject().(*unstructured.Unstructured)
76
+ if !ok {
77
+ t.Fatal("unexpected update object")
78
+ }
79
+ if write.GetUID() != "previous" || write.GetResourceVersion() != "11" {
80
+ t.Fatal("caller version was overwritten")
81
+ }
82
+ return true, nil, apierrors.NewConflict(schema.GroupResource{Resource: "configmaps"}, "settings", nil)
83
+ })
84
+ _, err := UpdateManifest(context.Background(), client.Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}).Namespace("demo"), desired)
85
+ if !apierrors.IsConflict(err) {
86
+ t.Fatalf("update: %v", err)
87
+ }
88
+ }
89
+
90
+ func TestDirectManifestRequiresOneWriter(t *testing.T) {
91
+ for _, test := range []struct {
92
+ name string
93
+ change func(*unstructured.Unstructured)
94
+ }{
95
+ {"controller", func(object *unstructured.Unstructured) {
96
+ yes := true
97
+ object.SetOwnerReferences([]metav1.OwnerReference{{Controller: &yes, UID: "owner"}})
98
+ }},
99
+ {"deleting", func(object *unstructured.Unstructured) { now := metav1.Now(); object.SetDeletionTimestamp(&now) }},
100
+ {"Helm", func(object *unstructured.Unstructured) {
101
+ object.SetAnnotations(map[string]string{"meta.helm.sh/release-name": "external"})
102
+ }},
103
+ {"Argo CD", func(object *unstructured.Unstructured) {
104
+ object.SetAnnotations(map[string]string{"argocd.argoproj.io/tracking-id": "external"})
105
+ }},
106
+ {"Flux", func(object *unstructured.Unstructured) {
107
+ object.SetLabels(map[string]string{"kustomize.toolkit.fluxcd.io/name": "external"})
108
+ }},
109
+ {"GitOps manager", func(object *unstructured.Unstructured) {
110
+ object.SetManagedFields([]metav1.ManagedFieldsEntry{{Manager: "argocd-controller"}})
111
+ }},
112
+ } {
113
+ t.Run(test.name, func(t *testing.T) {
114
+ object := &unstructured.Unstructured{Object: map[string]any{}}
115
+ if err := ValidateDirectManifestOwner(object); err != nil {
116
+ t.Fatal(err)
117
+ }
118
+ test.change(object)
119
+ if err := ValidateDirectManifestOwner(object); err == nil {
120
+ t.Fatal("external ownership was accepted")
121
+ }
122
+ })
123
+ }
124
+ }
125
+
126
+ func TestManifestWritePreservesDryRunAndIdentity(t *testing.T) {
127
+ for _, exists := range []bool{false, true} {
128
+ t.Run(map[bool]string{false: "create", true: "apply"}[exists], func(t *testing.T) {
129
+ desired := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "settings", "namespace": "demo"}}}
130
+ client := fake.NewSimpleDynamicClient(runtime.NewScheme())
131
+ verb := "create"
132
+ if exists {
133
+ live := desired.DeepCopy()
134
+ live.SetUID("settings-uid")
135
+ live.SetResourceVersion("12")
136
+ client = fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
137
+ verb = "patch"
138
+ }
139
+ writes := 0
140
+ client.PrependReactor(verb, "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) {
141
+ writes++
142
+ if exists {
143
+ patch, ok := action.(ktesting.PatchAction)
144
+ if !ok {
145
+ t.Fatal("unexpected patch action")
146
+ }
147
+ var body unstructured.Unstructured
148
+ if err := json.Unmarshal(patch.GetPatch(), &body); err != nil {
149
+ t.Fatal(err)
150
+ }
151
+ if body.GetUID() != "settings-uid" || body.GetResourceVersion() != "12" {
152
+ t.Fatal("ownership read was not fenced")
153
+ }
154
+ patchOptions, ok := action.(interface{ GetPatchOptions() metav1.PatchOptions })
155
+ if !ok {
156
+ t.Fatal("unexpected patch options")
157
+ }
158
+ if len(patchOptions.GetPatchOptions().DryRun) != 1 {
159
+ t.Fatal("patch lost dry-run")
160
+ }
161
+ } else {
162
+ createOptions, ok := action.(interface{ GetCreateOptions() metav1.CreateOptions })
163
+ if !ok {
164
+ t.Fatal("unexpected create options")
165
+ }
166
+ if len(createOptions.GetCreateOptions().DryRun) != 1 {
167
+ t.Fatal("create lost dry-run")
168
+ }
169
+ }
170
+ return true, desired.DeepCopy(), nil
171
+ })
172
+ _, err := ApplyManifest(context.Background(), client.Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}).Namespace("demo"), desired, metav1.PatchOptions{FieldManager: "soha-test", DryRun: []string{metav1.DryRunAll}})
173
+ if err != nil || writes != 1 || desired.GetUID() != "" || desired.GetResourceVersion() != "" {
174
+ t.Fatalf("write = %d, %v; desired = %#v", writes, err, desired)
175
+ }
176
+ })
177
+ }
178
+ }