@opensoha/contracts 0.1.17 → 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.
- package/COMPATIBILITY.md +45 -0
- package/README.md +37 -0
- package/auth/catalog_generated.go +2 -2
- package/auth/outpost_headers.go +17 -0
- package/auth/outpost_headers_test.go +16 -0
- package/auth/permission-catalog.json +383 -1
- package/compat/consumer-revisions.json +3 -0
- package/compat/json-schema-ecosystem-0.1.0.json +1 -1
- package/compat/openapi-loose-boundaries-0.1.0.json +78 -46
- package/delivery/canonical.go +136 -0
- package/delivery/document.go +203 -0
- package/delivery/document.schema.json +1527 -0
- package/delivery/document_test.go +191 -0
- package/delivery/parser.go +155 -0
- package/evaluation/evaluation-run.schema.json +64 -10
- package/fixtures/json-schema/gateway-manifest/invalid/bad-execution-mode.json +73 -0
- package/fixtures/json-schema/gateway-manifest/invalid/unversioned-check.json +85 -0
- package/fixtures/json-schema/gateway-manifest/valid/checks.json +86 -0
- package/fixtures/json-schema/gateway-manifest/valid/versioned-task.json +73 -0
- package/fixtures/json-schema/network-ingest-event/invalid/vpn-probe-negative-rtt.json +34 -0
- package/fixtures/json-schema/network-ingest-event/invalid/vpn-stats-secret.json +27 -0
- package/fixtures/json-schema/network-ingest-event/valid/vpn-probe-batch.json +45 -0
- package/fixtures/json-schema/network-ingest-event/valid/vpn-tunnel-stats.json +26 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-bad-token.json +15 -0
- package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-mode-override.json +16 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/managed-vpn-macos-enrollment.json +27 -0
- package/fixtures/json-schema/network-runtime-protocol/valid/vpn-managed-connect.json +15 -0
- package/fixtures/openapi/AgentChatInput/invalid/empty-question.json +4 -0
- package/fixtures/openapi/AgentChatInput/invalid/system-role.json +9 -0
- package/fixtures/openapi/AgentChatInput/valid/authorized-background.json +5 -0
- package/fixtures/openapi/AgentChatInput/valid/conversation.json +15 -0
- package/fixtures/openapi/AgentRunnerToolCallRequest/invalid/model-identity.json +1 -0
- package/fixtures/openapi/AgentRunnerToolCallRequest/valid/search.json +1 -0
- package/fixtures/openapi/ApplicationInput/invalid/negative-version.json +6 -0
- package/fixtures/openapi/ApplicationInput/invalid/zero-version.json +6 -0
- package/fixtures/openapi/ApplicationInput/valid/versioned.json +6 -0
- package/fixtures/openapi/ApprovalRequest/invalid/resolved-scopes.json +20 -0
- package/fixtures/openapi/ApprovalRequest/valid/resolved-scopes.json +21 -0
- package/fixtures/openapi/BuildTemplateInput/invalid/negative-revision.json +6 -0
- package/fixtures/openapi/BuildTemplateInput/valid/draft.json +11 -0
- package/fixtures/openapi/BuildTriggerRequest/invalid/no-ref.json +1 -0
- package/fixtures/openapi/BuildTriggerRequest/valid/no-environment.json +1 -0
- package/fixtures/openapi/BuildpacksConfiguration/invalid/mutable-builder.json +5 -0
- package/fixtures/openapi/BuildpacksConfiguration/invalid/privilege-override.json +6 -0
- package/fixtures/openapi/BuildpacksConfiguration/invalid/unsupported-platform.json +5 -0
- package/fixtures/openapi/BuildpacksConfiguration/valid/pinned.json +5 -0
- package/fixtures/openapi/CapabilityAssessment/invalid/contract.json +16 -0
- package/fixtures/openapi/CapabilityAssessment/valid/contract.json +16 -0
- package/fixtures/openapi/CapabilityPlan/invalid/contract.json +16 -0
- package/fixtures/openapi/CapabilityPlan/valid/contract.json +19 -0
- package/fixtures/openapi/CapabilityPlanValidation/invalid/contract.json +5 -0
- package/fixtures/openapi/CapabilityPlanValidation/valid/contract.json +11 -0
- package/fixtures/openapi/CapabilityTask/invalid/contract.json +33 -0
- package/fixtures/openapi/CapabilityTask/valid/contract.json +34 -0
- package/fixtures/openapi/CapabilityTaskInput/invalid/contract.json +22 -0
- package/fixtures/openapi/CapabilityTaskInput/valid/contract.json +22 -0
- package/fixtures/openapi/CapabilityValueSemantic/invalid/contract.json +4 -0
- package/fixtures/openapi/CapabilityValueSemantic/valid/contract.json +7 -0
- package/fixtures/openapi/DeliveryAccessResult/invalid/release-verification.json +8 -0
- package/fixtures/openapi/DeliveryAccessResult/valid/release-verification.json +8 -0
- package/fixtures/openapi/DeliveryBatchAssessment/invalid/release-verification.json +11 -0
- package/fixtures/openapi/DeliveryBatchAssessment/valid/release-verification.json +11 -0
- package/fixtures/openapi/DeliveryBatchAssessmentInput/invalid/release-verification.json +9 -0
- package/fixtures/openapi/DeliveryBatchAssessmentInput/valid/release-verification.json +8 -0
- package/fixtures/openapi/DeliveryBatchInput/invalid/ambiguous-source.json +19 -0
- package/fixtures/openapi/DeliveryBatchInput/invalid/missing-idempotency.json +16 -0
- package/fixtures/openapi/DeliveryBatchInput/valid/quick-deploy.json +17 -0
- package/fixtures/openapi/DeliveryBatchInput/valid/saved-workflow.json +5 -0
- package/fixtures/openapi/DeliveryBlueprintInput/invalid/unversioned-preset.json +31 -0
- package/fixtures/openapi/DeliveryBlueprintInput/valid/service-preset.json +32 -0
- package/fixtures/openapi/DeliveryDocument/invalid/unknown-field.json +24 -0
- package/fixtures/openapi/DeliveryDocument/invalid/unknown-kind.json +23 -0
- package/fixtures/openapi/DeliveryDocument/invalid/unknown-version.json +23 -0
- package/fixtures/openapi/DeliveryDocument/valid/build-template.json +26 -0
- package/fixtures/openapi/DeliveryDocument/valid/deployment-template.json +26 -0
- package/fixtures/openapi/DeliveryDocument/valid/legacy-dag.json +39 -0
- package/fixtures/openapi/DeliveryDocument/valid/workflow-template.json +23 -0
- package/fixtures/openapi/DeliveryDocument/valid/workflow.json +20 -0
- package/fixtures/openapi/DeliveryDraftInput/invalid/short-idempotency-key.json +30 -0
- package/fixtures/openapi/DeliveryDraftInput/valid/idempotent-create.json +30 -0
- package/fixtures/openapi/DeliveryDraftService/invalid/zero-version.json +7 -0
- package/fixtures/openapi/DeliveryDraftService/valid/versioned-template.json +9 -0
- package/fixtures/openapi/DeliveryExecutionHistoryPage/invalid-kind.json +9 -0
- package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-build.json +16 -0
- package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-empty.json +3 -0
- package/fixtures/openapi/DeliveryPlanApprovalInput/invalid/confirm.json +1 -0
- package/fixtures/openapi/DeliveryPlanApprovalInput/valid/approve.json +1 -0
- package/fixtures/openapi/DeliveryPlanInput/invalid/draft-manifest-revision.json +1 -0
- package/fixtures/openapi/DeliveryPlanInput/valid/manifest-revision.json +1 -0
- package/fixtures/openapi/DeliveryTargetInput/invalid/deploy-without-environment.json +7 -0
- package/fixtures/openapi/DeliveryTargetInput/valid/build-without-environment.json +6 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/duplicate-kind.json +13 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/embedded-credentials.json +16 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/empty-kinds.json +10 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/negative-generation.json +15 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/unknown-kind.json +12 -0
- package/fixtures/openapi/DeliveryTemplateSourceInput/valid/manual-source.json +15 -0
- package/fixtures/openapi/DeliveryWorkflowDefinition/invalid/unversioned-template.json +15 -0
- package/fixtures/openapi/DeliveryWorkflowDefinition/valid/cross-app.json +24 -0
- package/fixtures/openapi/DeliveryWorkflowInput/invalid/short-key.json +1 -0
- package/fixtures/openapi/DeliveryWorkflowInput/valid/idempotent-create.json +1 -0
- package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-target-override.json +7 -0
- package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-zero-version.json +6 -0
- package/fixtures/openapi/DeploymentTemplatePreviewInput/valid/valid.json +6 -0
- package/fixtures/openapi/DockerDeliveryConfiguration/invalid/empty-mappings.json +5 -0
- package/fixtures/openapi/DockerDeliveryConfiguration/valid/compose.json +7 -0
- package/fixtures/openapi/DockerDeliverySnapshot/invalid/missing-required-field.json +19 -0
- package/fixtures/openapi/DockerDeliverySnapshot/valid/frozen-project.json +20 -0
- package/fixtures/openapi/DockerOperationCallbackRequest/invalid/cancellation-acknowledged-not-boolean.json +13 -0
- package/fixtures/openapi/DockerOperationCallbackRequest/valid/cancellation-acknowledged.json +13 -0
- package/fixtures/openapi/DockerProjectInput/invalid/empty-host.json +4 -0
- package/fixtures/openapi/DockerProjectInput/valid/inline-idempotent.json +7 -0
- package/fixtures/openapi/DockerQuickCreateHostInput/invalid/capacity-type.json +1 -0
- package/fixtures/openapi/DockerQuickCreateHostInput/valid/capacity.json +1 -0
- package/fixtures/openapi/HelmDeliveryConfiguration/invalid/plain-credential.json +21 -0
- package/fixtures/openapi/HelmDeliveryConfiguration/invalid/release-name.json +20 -0
- package/fixtures/openapi/HelmDeliveryConfiguration/valid/private-oci.json +20 -0
- package/fixtures/openapi/HelmDeliverySnapshot/invalid/secret-plan.json +31 -0
- package/fixtures/openapi/HelmDeliverySnapshot/invalid/unknown-operation.json +28 -0
- package/fixtures/openapi/HelmDeliverySnapshot/valid/first-install.json +28 -0
- package/fixtures/openapi/IdentityApplication/invalid/outpost-status.json +1 -0
- package/fixtures/openapi/IdentityApplicationOnboardingInput/invalid/missing-access.json +12 -0
- package/fixtures/openapi/IdentityApplicationOnboardingInput/valid/link.json +13 -0
- package/fixtures/openapi/IdentityOutpost/invalid/missing-runtime-status.json +9 -0
- package/fixtures/openapi/IdentityOutpost/valid/healthy.json +17 -0
- package/fixtures/openapi/IdentityOutpost/valid/unregistered.json +12 -0
- package/fixtures/openapi/IdentityOutpostDeployment/invalid/missing-protocol.json +3 -0
- package/fixtures/openapi/IdentityOutpostDeployment/valid/remote.json +6 -0
- package/fixtures/openapi/IdentityOutpostInput/invalid/forged-observation.json +7 -0
- package/fixtures/openapi/IdentityOutpostInput/valid/forward-auth.json +6 -0
- package/fixtures/openapi/IdentityOutpostInput/valid/legacy.json +8 -0
- package/fixtures/openapi/IdentityProviderSetup/invalid/fake-verification.json +1 -0
- package/fixtures/openapi/IdentityProviderSetup/valid/incomplete.json +1 -0
- package/fixtures/openapi/IdentityProviderSetup/valid/remote.json +1 -0
- package/fixtures/openapi/IdentityProviderUserMetadata/invalid/attributes.json +8 -0
- package/fixtures/openapi/IdentityProviderUserMetadata/valid/attributes.json +13 -0
- package/fixtures/openapi/KubernetesCustomResource/invalid/negative-generation.json +1 -0
- package/fixtures/openapi/KubernetesCustomResource/valid/deleting-operator.json +1 -0
- package/fixtures/openapi/KubernetesResourceCreationReceipt/invalid/manifest-content.json +20 -0
- package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/confirmed.json +20 -0
- package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/unknown.json +19 -0
- package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/negative-resource-observation.json +57 -0
- package/fixtures/openapi/ManifestDeploymentEnvelope/valid/operator-deleting.json +57 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/gitops-empty.json +7 -0
- package/fixtures/openapi/ManifestExecutionTaskPayload/valid/gitops-frozen-children.json +18 -0
- package/fixtures/openapi/ManifestKustomizeOptions/invalid/escaping-entry.json +1 -0
- package/fixtures/openapi/ManifestKustomizeOptions/invalid/mutable-image.json +1 -0
- package/fixtures/openapi/ManifestKustomizeOptions/valid/overlay-images.json +7 -0
- package/fixtures/openapi/ManifestRevisionInput/invalid/missing-version.json +1 -0
- package/fixtures/openapi/ManifestRevisionInput/valid/current-draft.json +1 -0
- package/fixtures/openapi/ManifestServiceTemplateInputs/invalid/mutable-image.json +8 -0
- package/fixtures/openapi/ManifestServiceTemplateInputs/valid/frozen-service-image.json +9 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/auto-with-gateway.json +6 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/missing-manual-gateway.json +5 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/mode-override.json +7 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/auto-intent.json +5 -0
- package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/manual-intent.json +6 -0
- package/fixtures/openapi/NetworkVPNProfileInput/invalid/unknown-private-key.json +25 -0
- package/fixtures/openapi/NetworkVPNProfileInput/valid/assigned-profile.json +24 -0
- package/fixtures/openapi/NetworkVPNSelectionPolicyInput/invalid/unbounded-attempts.json +21 -0
- package/fixtures/openapi/NetworkVPNSelectionPolicyInput/valid/provider-policy.json +21 -0
- package/fixtures/openapi/ObservabilityMetricAssessmentInput/invalid/arbitrary-expression.json +10 -0
- package/fixtures/openapi/ObservabilityMetricAssessmentInput/valid/metric-window.json +10 -0
- package/fixtures/openapi/PluginManifest/valid/hermes-api.json +53 -0
- package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/missing-identity.json +5 -0
- package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/skip-analysis.json +5 -0
- package/fixtures/openapi/ProgressiveRolloutControlInput/valid/pause.json +5 -0
- package/fixtures/openapi/ReleaseTargetInput/invalid/missing-required-field.json +15 -0
- package/fixtures/openapi/ReleaseTargetInput/valid/docker-compose.json +16 -0
- package/fixtures/openapi/RepositoryAnalysis/invalid/moving-ref.json +11 -0
- package/fixtures/openapi/RepositoryAnalysis/valid/go.json +29 -0
- package/fixtures/openapi/RepositoryAnalysisInput/invalid/arbitrary-source-url.json +6 -0
- package/fixtures/openapi/RepositoryAnalysisInput/invalid/missing-ref.json +4 -0
- package/fixtures/openapi/RepositoryAnalysisInput/valid/nested-project.json +6 -0
- package/fixtures/openapi/ServiceDeploymentTemplateBinding/invalid/invalid-unpinned.json +4 -0
- package/fixtures/openapi/ServiceDeploymentTemplateBinding/valid/valid.json +7 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-executable-schema.json +25 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-missing-schema.json +21 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-mixed-renderers.json +40 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-nested-parameters.json +41 -0
- package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-yaml.json +34 -0
- package/fixtures/openapi/TemplatePublishInput/invalid/missing-revision.json +1 -0
- package/fixtures/openapi/TemplatePublishInput/invalid/unknown-field.json +4 -0
- package/fixtures/openapi/TemplatePublishInput/invalid/zero-revision.json +3 -0
- package/fixtures/openapi/TemplatePublishInput/valid/revision.json +3 -0
- package/fixtures/openapi/ToolCapability/invalid/bad-idempotency.json +35 -0
- package/fixtures/openapi/ToolCapability/invalid/unversioned-check.json +47 -0
- package/fixtures/openapi/ToolCapability/valid/checks.json +48 -0
- package/fixtures/openapi/ToolCapability/valid/versioned-task.json +35 -0
- package/fixtures/openapi/ToolInvocationRequest/invalid/empty-version.json +8 -0
- package/fixtures/openapi/ToolInvocationRequest/valid/versioned.json +8 -0
- package/fixtures/openapi/ToolInvocationResult/invalid/missing-status-call.json +13 -0
- package/fixtures/openapi/ToolInvocationResult/valid/task.json +20 -0
- package/fixtures/openapi/VirtualMachineCreateInput/invalid/capacity-type.json +1 -0
- package/fixtures/openapi/VirtualMachineCreateInput/valid/capacity.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityInput/invalid/missing-disk.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityInput/valid/running-vm.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityResult/invalid/invented-status.json +1 -0
- package/fixtures/openapi/VirtualizationCapacityResult/valid/available.json +1 -0
- package/fixtures/openapi/VirtualizationWorkerCreateInput/invalid/secret-input.json +5 -0
- package/fixtures/openapi/VirtualizationWorkerCreateInput/valid/one-worker.json +4 -0
- package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/external-owner.json +27 -0
- package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/zero-budget.json +27 -0
- package/fixtures/openapi/VirtualizationWorkerPoolInput/valid/registered.json +27 -0
- package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/invalid/nested-data.json +5 -0
- package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/valid/empty.json +3 -0
- package/fixtures/openapi/WorkbenchContextSelection/invalid/missing-file-content.json +8 -0
- package/fixtures/openapi/WorkbenchContextSelection/invalid/unknown-reference-kind.json +8 -0
- package/fixtures/openapi/WorkbenchContextSelection/valid/text-and-resource.json +17 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/trigger-without-plan.json +10 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/unknown-trigger.json +28 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/zero-interval.json +28 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/alert-capability.json +30 -0
- package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/scheduled-capability.json +28 -0
- package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/missing-rule.json +3 -0
- package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/schedule-alert-selector.json +4 -0
- package/fixtures/openapi/WorkbenchInspectionTrigger/valid/schedule.json +3 -0
- package/fixtures/openapi/WorkbenchModelPreferences/invalid/unknown-effort.json +1 -0
- package/fixtures/openapi/WorkbenchModelPreferences/valid/selected.json +1 -0
- package/fixtures/openapi/WorkflowCatalogPage/invalid/unsupported-kind.json +6 -0
- package/fixtures/openapi/WorkflowCatalogPage/valid/build-without-environment.json +6 -0
- package/fixtures/openapi/WorkflowTemplateInput/invalid/zero-revision.json +6 -0
- package/fixtures/openapi/WorkflowTemplateInput/valid/draft.json +10 -0
- package/gen/go/sohaapi/client.go +1 -1
- package/gen/go/sohaapi/types.go +8070 -859
- package/gen/go/sohaapi/types_test.go +100 -1
- package/gen/go/sohaapi/vpn_models_test.go +21 -0
- package/gen/ts/sohaapi/index.ts +20343 -13162
- package/go.mod +107 -5
- package/go.sum +379 -16
- package/helmrelease/delivery.go +283 -0
- package/helmrelease/delivery_test.go +102 -0
- package/helmrelease/frozen_chart.go +91 -0
- package/helmrelease/result.go +27 -0
- package/helmrelease/runtime/delivery.go +508 -0
- package/helmrelease/runtime/delivery_test.go +236 -0
- package/helmrelease/runtime/resources_test.go +60 -0
- package/helmrelease/values.go +99 -0
- package/knowledge/context-envelope.schema.json +1 -0
- package/mcp/gateway-manifest.schema.json +387 -72
- package/network/README.md +40 -0
- package/network/network-ingest-event.schema.json +228 -0
- package/network/network-runtime-protocol.schema.json +457 -1
- package/openapi/soha-api.json +54958 -38899
- package/openapi/soha-api.yaml +12143 -4984
- package/openapi/spec_generated.go +3 -3
- package/package.json +3 -1
- package/resource/manifest_health.go +155 -0
- package/resource/manifest_health_test.go +37 -0
- package/resource/models.go +14 -0
- package/resource/runtime/argocd.go +166 -0
- package/resource/runtime/argocd_integration_test.go +142 -0
- package/resource/runtime/argocd_observation.go +143 -0
- package/resource/runtime/argocd_operation.go +253 -0
- package/resource/runtime/argocd_resources.go +115 -0
- package/resource/runtime/argocd_resources_test.go +107 -0
- package/resource/runtime/argocd_test.go +386 -0
- package/resource/runtime/health.go +115 -0
- package/resource/runtime/health_test.go +78 -0
- package/resource/runtime/operator_integration_test.go +174 -0
- package/resource/runtime/ownership.go +178 -0
- package/resource/runtime/ownership_test.go +178 -0
- package/resource/runtime/rollout_integration_test.go +287 -0
- package/resource/runtime/rollout_observation.go +252 -0
- package/resource/runtime/rollout_operation.go +398 -0
- package/resource/runtime/rollout_resources.go +385 -0
- package/resource/runtime/rollout_task.go +165 -0
- package/resource/runtime/rollout_test.go +450 -0
|
@@ -5,9 +5,9 @@ package openapi
|
|
|
5
5
|
import _ "embed"
|
|
6
6
|
|
|
7
7
|
const (
|
|
8
|
-
Version = "0.1.
|
|
9
|
-
YAMLSHA256 = "
|
|
10
|
-
JSONSHA256 = "
|
|
8
|
+
Version = "0.1.18"
|
|
9
|
+
YAMLSHA256 = "d614ce858924d94da353c7a9380971a3e80964f3e6c865e8b27108e859500cf4"
|
|
10
|
+
JSONSHA256 = "a98ea9ad99b6f044ce14e42f37b90b42e28347d53e37c727a04210b076aba960"
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
//go:embed soha-api.yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opensoha/contracts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared OpenSoha API, agent, MCP, skill, event, and auth contracts.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test": "npm run check:version-sync && npm run check:generated && npm run validate && npm run lint:openapi && npm run check:openapi-breaking && npm run check:openapi-loose-boundaries && npm run check:json-schema-compat && npm run typecheck && go test ./..."
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
|
+
"./delivery/document.schema.json": "./delivery/document.schema.json",
|
|
31
32
|
"./gen/ts/sohaapi": "./gen/ts/sohaapi/index.ts",
|
|
32
33
|
"./openapi/soha-api.yaml": "./openapi/soha-api.yaml",
|
|
33
34
|
"./openapi/soha-api.json": "./openapi/soha-api.json",
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
"./memory/memory-policy.schema.json": "./memory/memory-policy.schema.json"
|
|
91
92
|
},
|
|
92
93
|
"files": [
|
|
94
|
+
"delivery",
|
|
93
95
|
"COMPATIBILITY.md",
|
|
94
96
|
"go.mod",
|
|
95
97
|
"go.sum",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
package resource
|
|
2
|
+
|
|
3
|
+
import "encoding/json"
|
|
4
|
+
|
|
5
|
+
// ManifestHealth gives Direct and Agent execution the same conservative health
|
|
6
|
+
// interpretation. Successful persistence alone is health evidence only for
|
|
7
|
+
// built-in configuration objects, never for an unknown custom resource.
|
|
8
|
+
func ManifestHealth(object map[string]any) string {
|
|
9
|
+
if object == nil {
|
|
10
|
+
return "unknown"
|
|
11
|
+
}
|
|
12
|
+
encoded, err := json.Marshal(object)
|
|
13
|
+
if err != nil {
|
|
14
|
+
return "unknown"
|
|
15
|
+
}
|
|
16
|
+
var item manifestHealthObject
|
|
17
|
+
if err := json.Unmarshal(encoded, &item); err != nil {
|
|
18
|
+
return "unknown"
|
|
19
|
+
}
|
|
20
|
+
if item.Metadata.DeletionTimestamp != "" {
|
|
21
|
+
return "degraded"
|
|
22
|
+
}
|
|
23
|
+
kind := item.APIVersion + "/" + item.Kind
|
|
24
|
+
switch kind {
|
|
25
|
+
case "workloads.soha.io/v1alpha1/WorkloadCronJob":
|
|
26
|
+
// Ready describes configuration synchronization. Its source and owned
|
|
27
|
+
// CronJob must also be observed by the runtime before declaring health.
|
|
28
|
+
if item.conditionHealth(true) == "degraded" {
|
|
29
|
+
return "degraded"
|
|
30
|
+
}
|
|
31
|
+
return "unknown"
|
|
32
|
+
case "v1/ConfigMap", "v1/Secret", "v1/ServiceAccount", "rbac.authorization.k8s.io/v1/Role", "rbac.authorization.k8s.io/v1/RoleBinding", "networking.k8s.io/v1/NetworkPolicy":
|
|
33
|
+
return "healthy"
|
|
34
|
+
case "apps/v1/Deployment", "apps/v1/StatefulSet", "apps/v1/DaemonSet":
|
|
35
|
+
return item.workloadHealth()
|
|
36
|
+
case "v1/PersistentVolumeClaim":
|
|
37
|
+
if item.Status.Phase == "Bound" {
|
|
38
|
+
return "healthy"
|
|
39
|
+
}
|
|
40
|
+
if item.Status.Phase == "Lost" {
|
|
41
|
+
return "degraded"
|
|
42
|
+
}
|
|
43
|
+
return "progressing"
|
|
44
|
+
case "v1/Service":
|
|
45
|
+
if item.Spec.Type != "LoadBalancer" || len(item.Status.LoadBalancer.Ingress) > 0 {
|
|
46
|
+
return "healthy"
|
|
47
|
+
}
|
|
48
|
+
return "progressing"
|
|
49
|
+
case "v1/Pod", "batch/v1/Job":
|
|
50
|
+
if item.Status.Phase == "Succeeded" {
|
|
51
|
+
return "healthy"
|
|
52
|
+
}
|
|
53
|
+
if item.Status.Phase == "Failed" {
|
|
54
|
+
return "degraded"
|
|
55
|
+
}
|
|
56
|
+
return item.conditionHealth(false)
|
|
57
|
+
default:
|
|
58
|
+
return item.conditionHealth(true)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type manifestHealthObject struct {
|
|
63
|
+
APIVersion string `json:"apiVersion"`
|
|
64
|
+
Kind string `json:"kind"`
|
|
65
|
+
Metadata struct {
|
|
66
|
+
Generation int64 `json:"generation"`
|
|
67
|
+
DeletionTimestamp string `json:"deletionTimestamp"`
|
|
68
|
+
} `json:"metadata"`
|
|
69
|
+
Spec struct {
|
|
70
|
+
Replicas *int64 `json:"replicas"`
|
|
71
|
+
Type string `json:"type"`
|
|
72
|
+
UpdateStrategy struct {
|
|
73
|
+
RollingUpdate struct {
|
|
74
|
+
Partition int64 `json:"partition"`
|
|
75
|
+
} `json:"rollingUpdate"`
|
|
76
|
+
} `json:"updateStrategy"`
|
|
77
|
+
} `json:"spec"`
|
|
78
|
+
Status struct {
|
|
79
|
+
ObservedGeneration int64 `json:"observedGeneration"`
|
|
80
|
+
Replicas int64 `json:"replicas"`
|
|
81
|
+
ReadyReplicas int64 `json:"readyReplicas"`
|
|
82
|
+
UpdatedReplicas int64 `json:"updatedReplicas"`
|
|
83
|
+
AvailableReplicas int64 `json:"availableReplicas"`
|
|
84
|
+
DesiredNumberScheduled int64 `json:"desiredNumberScheduled"`
|
|
85
|
+
UpdatedNumberScheduled int64 `json:"updatedNumberScheduled"`
|
|
86
|
+
NumberAvailable int64 `json:"numberAvailable"`
|
|
87
|
+
Phase string `json:"phase"`
|
|
88
|
+
LoadBalancer struct {
|
|
89
|
+
Ingress []json.RawMessage `json:"ingress"`
|
|
90
|
+
} `json:"loadBalancer"`
|
|
91
|
+
Conditions []struct {
|
|
92
|
+
Type string `json:"type"`
|
|
93
|
+
Status string `json:"status"`
|
|
94
|
+
Reason string `json:"reason"`
|
|
95
|
+
ObservedGeneration int64 `json:"observedGeneration"`
|
|
96
|
+
} `json:"conditions"`
|
|
97
|
+
} `json:"status"`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func (item manifestHealthObject) workloadHealth() string {
|
|
101
|
+
if item.Metadata.Generation < 1 || item.Status.ObservedGeneration < item.Metadata.Generation {
|
|
102
|
+
return "progressing"
|
|
103
|
+
}
|
|
104
|
+
if item.conditionHealth(false) == "degraded" {
|
|
105
|
+
return "degraded"
|
|
106
|
+
}
|
|
107
|
+
desired := int64(1)
|
|
108
|
+
if item.Spec.Replicas != nil {
|
|
109
|
+
desired = *item.Spec.Replicas
|
|
110
|
+
}
|
|
111
|
+
healthy := false
|
|
112
|
+
switch item.Kind {
|
|
113
|
+
case "Deployment":
|
|
114
|
+
healthy = item.Status.UpdatedReplicas == desired && item.Status.Replicas == desired && item.Status.AvailableReplicas >= desired
|
|
115
|
+
case "StatefulSet":
|
|
116
|
+
updated := max(int64(0), desired-item.Spec.UpdateStrategy.RollingUpdate.Partition)
|
|
117
|
+
healthy = item.Status.ReadyReplicas >= desired && item.Status.UpdatedReplicas >= updated
|
|
118
|
+
case "DaemonSet":
|
|
119
|
+
healthy = item.Status.UpdatedNumberScheduled == item.Status.DesiredNumberScheduled && item.Status.NumberAvailable >= item.Status.DesiredNumberScheduled
|
|
120
|
+
}
|
|
121
|
+
if healthy {
|
|
122
|
+
return "healthy"
|
|
123
|
+
}
|
|
124
|
+
return "progressing"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func (item manifestHealthObject) conditionHealth(requireCurrentGeneration bool) string {
|
|
128
|
+
result := "progressing"
|
|
129
|
+
notReady := false
|
|
130
|
+
if requireCurrentGeneration {
|
|
131
|
+
result = "unknown"
|
|
132
|
+
}
|
|
133
|
+
for _, condition := range item.Status.Conditions {
|
|
134
|
+
if requireCurrentGeneration && (item.Metadata.Generation < 1 || max(condition.ObservedGeneration, item.Status.ObservedGeneration) < item.Metadata.Generation) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if condition.Status == "True" && (condition.Type == "Failed" || condition.Type == "Degraded" || condition.Type == "ReplicaFailure") {
|
|
138
|
+
return "degraded"
|
|
139
|
+
}
|
|
140
|
+
if condition.Type == "Progressing" && condition.Status == "False" && condition.Reason == "ProgressDeadlineExceeded" {
|
|
141
|
+
return "degraded"
|
|
142
|
+
}
|
|
143
|
+
if condition.Type == "Ready" || condition.Type == "Available" || condition.Type == "Complete" || condition.Type == "Established" {
|
|
144
|
+
if condition.Status == "True" {
|
|
145
|
+
result = "healthy"
|
|
146
|
+
} else {
|
|
147
|
+
notReady = true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if notReady {
|
|
152
|
+
return "progressing"
|
|
153
|
+
}
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package resource
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestManifestHealth(t *testing.T) {
|
|
9
|
+
for _, test := range []struct{ name, object, want string }{
|
|
10
|
+
{"configuration", `{"apiVersion":"v1","kind":"ConfigMap"}`, "healthy"},
|
|
11
|
+
{"unknown CR", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2}}`, "unknown"},
|
|
12
|
+
{"custom kind cannot impersonate built-in", `{"apiVersion":"example.com/v1","kind":"ConfigMap"}`, "unknown"},
|
|
13
|
+
{"unobserved generation", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":1,"replicas":1,"updatedReplicas":1,"availableReplicas":1,"conditions":[{"type":"Available","status":"True"}]}}`, "progressing"},
|
|
14
|
+
{"old replicas still available", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":2,"replicas":2,"updatedReplicas":1,"availableReplicas":2}}`, "progressing"},
|
|
15
|
+
{"available but not updated", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":2,"replicas":1,"updatedReplicas":0,"availableReplicas":1}}`, "progressing"},
|
|
16
|
+
{"deployment converged", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"spec":{"replicas":1},"status":{"observedGeneration":2,"replicas":1,"updatedReplicas":1,"availableReplicas":1}}`, "healthy"},
|
|
17
|
+
{"deployment failed after available", `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"generation":2},"status":{"observedGeneration":2,"replicas":1,"updatedReplicas":1,"availableReplicas":1,"conditions":[{"type":"Available","status":"True"},{"type":"Progressing","status":"False","reason":"ProgressDeadlineExceeded"}]}}`, "degraded"},
|
|
18
|
+
{"stale custom ready", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"conditions":[{"type":"Ready","status":"True","observedGeneration":1}]}}`, "unknown"},
|
|
19
|
+
{"current custom ready", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"conditions":[{"type":"Ready","status":"True","observedGeneration":2}]}}`, "healthy"},
|
|
20
|
+
{"conflicting current ready conditions", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"observedGeneration":2,"conditions":[{"type":"Available","status":"True"},{"type":"Ready","status":"False"}]}}`, "progressing"},
|
|
21
|
+
{"current custom failure", `{"apiVersion":"example.com/v1","kind":"Database","metadata":{"generation":2},"status":{"observedGeneration":2,"conditions":[{"type":"Ready","status":"True"},{"type":"Failed","status":"True"}]}}`, "degraded"},
|
|
22
|
+
{"deleting object", `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"deletionTimestamp":"2026-09-12T00:00:00Z"}}`, "degraded"},
|
|
23
|
+
{"pod not ready", `{"apiVersion":"v1","kind":"Pod","status":{"phase":"Running","conditions":[{"type":"Ready","status":"False"}]}}`, "progressing"},
|
|
24
|
+
{"statefulset partition", `{"apiVersion":"apps/v1","kind":"StatefulSet","metadata":{"generation":2},"spec":{"replicas":3,"updateStrategy":{"rollingUpdate":{"partition":2}}},"status":{"observedGeneration":2,"readyReplicas":3,"updatedReplicas":1}}`, "healthy"},
|
|
25
|
+
{"daemonset rolling", `{"apiVersion":"apps/v1","kind":"DaemonSet","metadata":{"generation":2},"status":{"observedGeneration":2,"desiredNumberScheduled":3,"updatedNumberScheduled":2,"numberAvailable":3}}`, "progressing"},
|
|
26
|
+
} {
|
|
27
|
+
t.Run(test.name, func(t *testing.T) {
|
|
28
|
+
var object map[string]any
|
|
29
|
+
if err := json.Unmarshal([]byte(test.object), &object); err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
if got := ManifestHealth(object); got != test.want {
|
|
33
|
+
t.Fatalf("health = %s, want %s", got, test.want)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
package/resource/models.go
CHANGED
|
@@ -1097,6 +1097,10 @@ type CRDView struct {
|
|
|
1097
1097
|
}
|
|
1098
1098
|
|
|
1099
1099
|
type CustomResourceView struct {
|
|
1100
|
+
UID string `json:"uid,omitempty"`
|
|
1101
|
+
Generation int64 `json:"generation,omitempty"`
|
|
1102
|
+
DeletingAt string `json:"deletingAt,omitempty"`
|
|
1103
|
+
Finalizers []string `json:"finalizers,omitempty"`
|
|
1100
1104
|
APIVersion string `json:"apiVersion,omitempty"`
|
|
1101
1105
|
Kind string `json:"kind"`
|
|
1102
1106
|
Name string `json:"name"`
|
|
@@ -1107,6 +1111,16 @@ type CustomResourceView struct {
|
|
|
1107
1111
|
AllowedActions []string `json:"allowedActions,omitempty"`
|
|
1108
1112
|
}
|
|
1109
1113
|
|
|
1114
|
+
// CustomResourceYAMLRequest is the Core/Agent custom-resource transport.
|
|
1115
|
+
// ExpectedUID fences deletion against replacement after a user viewed the object.
|
|
1116
|
+
type CustomResourceYAMLRequest struct {
|
|
1117
|
+
Definition CRDResourceDefinition `json:"definition"`
|
|
1118
|
+
Namespace string `json:"namespace"`
|
|
1119
|
+
Name string `json:"name,omitempty"`
|
|
1120
|
+
Content string `json:"content,omitempty"`
|
|
1121
|
+
ExpectedUID string `json:"expectedUid,omitempty"`
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1110
1124
|
type HelmReleaseView struct {
|
|
1111
1125
|
Name string `json:"name"`
|
|
1112
1126
|
Namespace string `json:"namespace"`
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/hex"
|
|
5
|
+
"fmt"
|
|
6
|
+
"net/url"
|
|
7
|
+
"path"
|
|
8
|
+
"reflect"
|
|
9
|
+
"regexp"
|
|
10
|
+
"slices"
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
14
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const (
|
|
18
|
+
ArgoOwnerAnnotation = "delivery.soha.io/argocd-owner"
|
|
19
|
+
ArgoOperationAnnotation = "delivery.soha.io/argocd-operation"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
var argoApplications = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
|
|
23
|
+
var argoImageOverride = regexp.MustCompile(`^(?:([A-Za-z0-9][A-Za-z0-9._:/-]*)=)?([A-Za-z0-9][A-Za-z0-9._:/-]*)@sha256:[a-f0-9]{64}$`)
|
|
24
|
+
|
|
25
|
+
// ParseArgoImageOverride accepts Kustomize's image@digest and name=image@digest forms.
|
|
26
|
+
func ParseArgoImageOverride(value string) (name, image string, err error) {
|
|
27
|
+
parts := argoImageOverride.FindStringSubmatch(value)
|
|
28
|
+
if len(parts) != 3 || strings.LastIndex(parts[2], ":") > strings.LastIndex(parts[2], "/") {
|
|
29
|
+
return "", "", fmt.Errorf("GitOps image overrides require an immutable image digest without a tag")
|
|
30
|
+
}
|
|
31
|
+
name = parts[1]
|
|
32
|
+
if name == "" {
|
|
33
|
+
name = parts[2]
|
|
34
|
+
}
|
|
35
|
+
_, image, found := strings.Cut(value, "=")
|
|
36
|
+
if !found {
|
|
37
|
+
image = value
|
|
38
|
+
}
|
|
39
|
+
return name, image, nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func IsArgoApplication(object *unstructured.Unstructured) bool {
|
|
43
|
+
return object != nil && object.GetAPIVersion() == "argoproj.io/v1alpha1" && object.GetKind() == "Application"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ValidateArgoApplication defines the supported, single-namespace GitOps path.
|
|
47
|
+
// AppProject and repository credentials remain administrator-managed in Argo CD.
|
|
48
|
+
func ValidateArgoApplication(object *unstructured.Unstructured) error {
|
|
49
|
+
if !IsArgoApplication(object) || object.GetNamespace() == "" || object.GetName() == "" {
|
|
50
|
+
return fmt.Errorf("GitOps requires a namespaced Argo CD Application")
|
|
51
|
+
}
|
|
52
|
+
if object.GetDeletionTimestamp() != nil || len(object.GetOwnerReferences()) != 0 || len(object.GetFinalizers()) != 0 {
|
|
53
|
+
return fmt.Errorf("GitOps Application must be an independent, non-deleting root without cascading deletion")
|
|
54
|
+
}
|
|
55
|
+
for key := range object.GetAnnotations() {
|
|
56
|
+
if strings.HasPrefix(key, "argocd.argoproj.io/") {
|
|
57
|
+
return fmt.Errorf("GitOps Application must not override controller behavior with annotations")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
spec, _, err := unstructured.NestedMap(object.Object, "spec")
|
|
61
|
+
if err != nil {
|
|
62
|
+
return err
|
|
63
|
+
}
|
|
64
|
+
if err := argoAllowedFields(spec, "project", "source", "destination", "syncPolicy"); err != nil {
|
|
65
|
+
return err
|
|
66
|
+
}
|
|
67
|
+
if argoString(object, "spec", "project") == "" || argoString(object, "spec", "project") == "default" {
|
|
68
|
+
return fmt.Errorf("GitOps requires a dedicated restricted AppProject")
|
|
69
|
+
}
|
|
70
|
+
destination, _, err := unstructured.NestedStringMap(object.Object, "spec", "destination")
|
|
71
|
+
if err != nil || len(destination) != 2 || destination["namespace"] != object.GetNamespace() || destination["server"] != "https://kubernetes.default.svc" {
|
|
72
|
+
return fmt.Errorf("GitOps destination must be this cluster and the Application namespace")
|
|
73
|
+
}
|
|
74
|
+
source, _, err := unstructured.NestedMap(object.Object, "spec", "source")
|
|
75
|
+
if err != nil {
|
|
76
|
+
return err
|
|
77
|
+
}
|
|
78
|
+
if err := argoAllowedFields(source, "repoURL", "targetRevision", "path", "kustomize"); err != nil {
|
|
79
|
+
return err
|
|
80
|
+
}
|
|
81
|
+
revision := argoString(object, "spec", "source", "targetRevision")
|
|
82
|
+
if decoded, err := hex.DecodeString(revision); err != nil || len(decoded) != 20 && len(decoded) != 32 {
|
|
83
|
+
return fmt.Errorf("GitOps targetRevision must be a full immutable Git commit")
|
|
84
|
+
}
|
|
85
|
+
repository, err := url.Parse(argoString(object, "spec", "source", "repoURL"))
|
|
86
|
+
if err != nil || repository.Host == "" || repository.User != nil || repository.RawQuery != "" || repository.Fragment != "" || (repository.Scheme != "https" && repository.Scheme != "http" && repository.Scheme != "ssh") {
|
|
87
|
+
return fmt.Errorf("GitOps repository must be an HTTP(S) or SSH URL without embedded credentials")
|
|
88
|
+
}
|
|
89
|
+
directory := argoString(object, "spec", "source", "path")
|
|
90
|
+
if directory == "" || path.IsAbs(directory) || path.Clean(directory) != directory || directory == ".." || strings.HasPrefix(directory, "../") || strings.Contains(directory, "\\") {
|
|
91
|
+
return fmt.Errorf("GitOps path must stay within the pinned repository")
|
|
92
|
+
}
|
|
93
|
+
kustomize, _, err := unstructured.NestedMap(object.Object, "spec", "source", "kustomize")
|
|
94
|
+
if err != nil {
|
|
95
|
+
return err
|
|
96
|
+
}
|
|
97
|
+
if err := argoAllowedFields(kustomize, "images", "namespace"); err != nil {
|
|
98
|
+
return err
|
|
99
|
+
}
|
|
100
|
+
if argoString(object, "spec", "source", "kustomize", "namespace") != object.GetNamespace() {
|
|
101
|
+
return fmt.Errorf("GitOps Kustomize namespace must be explicitly fixed to the delivery namespace")
|
|
102
|
+
}
|
|
103
|
+
images, _, err := unstructured.NestedStringSlice(object.Object, "spec", "source", "kustomize", "images")
|
|
104
|
+
if err != nil || len(images) == 0 || len(images) > 16 {
|
|
105
|
+
return fmt.Errorf("GitOps requires 1 to 16 fixed Kustomize image overrides")
|
|
106
|
+
}
|
|
107
|
+
imageNames := map[string]bool{}
|
|
108
|
+
for _, image := range images {
|
|
109
|
+
name, _, err := ParseArgoImageOverride(image)
|
|
110
|
+
if err != nil {
|
|
111
|
+
return err
|
|
112
|
+
}
|
|
113
|
+
if imageNames[name] {
|
|
114
|
+
return fmt.Errorf("GitOps image overrides must use unique image names")
|
|
115
|
+
}
|
|
116
|
+
imageNames[name] = true
|
|
117
|
+
}
|
|
118
|
+
policy, _, err := unstructured.NestedMap(object.Object, "spec", "syncPolicy")
|
|
119
|
+
if err != nil {
|
|
120
|
+
return err
|
|
121
|
+
}
|
|
122
|
+
if err := argoAllowedFields(policy, "syncOptions"); err != nil {
|
|
123
|
+
return err
|
|
124
|
+
}
|
|
125
|
+
options, _, err := unstructured.NestedStringSlice(object.Object, "spec", "syncPolicy", "syncOptions")
|
|
126
|
+
if err != nil || !reflect.DeepEqual(options, []string{"FailOnSharedResource=true"}) {
|
|
127
|
+
return fmt.Errorf("GitOps requires manual sync and FailOnSharedResource=true")
|
|
128
|
+
}
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func argoAllowedFields(object map[string]any, allowed ...string) error {
|
|
133
|
+
for key := range object {
|
|
134
|
+
if !slices.Contains(allowed, key) {
|
|
135
|
+
return fmt.Errorf("unsupported GitOps field %q", key)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func argoString(object *unstructured.Unstructured, fields ...string) string {
|
|
142
|
+
value, _, _ := unstructured.NestedString(object.Object, fields...)
|
|
143
|
+
return value
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func argoOperationID(object *unstructured.Unstructured, fields ...string) string {
|
|
147
|
+
items, _, _ := unstructured.NestedSlice(object.Object, append(fields, "info")...)
|
|
148
|
+
for _, raw := range items {
|
|
149
|
+
item, _ := raw.(map[string]any)
|
|
150
|
+
if item["name"] == ArgoOperationAnnotation {
|
|
151
|
+
value, _ := item["value"].(string)
|
|
152
|
+
return value
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return ""
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var argoResourceTypes = map[string]schema.GroupVersionResource{
|
|
159
|
+
"apps/Deployment": {Group: "apps", Version: "v1", Resource: "deployments"},
|
|
160
|
+
"apps/StatefulSet": {Group: "apps", Version: "v1", Resource: "statefulsets"},
|
|
161
|
+
"/Service": {Version: "v1", Resource: "services"},
|
|
162
|
+
"/ConfigMap": {Version: "v1", Resource: "configmaps"},
|
|
163
|
+
"/Secret": {Version: "v1", Resource: "secrets"},
|
|
164
|
+
"/ServiceAccount": {Version: "v1", Resource: "serviceaccounts"},
|
|
165
|
+
"/PersistentVolumeClaim": {Version: "v1", Resource: "persistentvolumeclaims"},
|
|
166
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"net/url"
|
|
8
|
+
"os"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/google/uuid"
|
|
14
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
15
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
16
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
17
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
18
|
+
"k8s.io/apimachinery/pkg/util/wait"
|
|
19
|
+
"k8s.io/client-go/dynamic"
|
|
20
|
+
"k8s.io/client-go/kubernetes"
|
|
21
|
+
"k8s.io/client-go/tools/clientcmd"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
func TestArgoCDWithKubernetes(t *testing.T) {
|
|
25
|
+
kubeconfig := os.Getenv("SOHA_ARGOCD_TEST_KUBECONFIG")
|
|
26
|
+
if kubeconfig == "" {
|
|
27
|
+
t.Skip("set explicit loopback kubeconfig and SOHA_ARGOCD_TEST_STATE for the private GitOps fixture")
|
|
28
|
+
}
|
|
29
|
+
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
|
30
|
+
operatorRequire(t, err)
|
|
31
|
+
endpoint, err := url.Parse(config.Host)
|
|
32
|
+
if err != nil || endpoint.Hostname() != "127.0.0.1" {
|
|
33
|
+
t.Fatal("only the loopback fixture cluster is supported")
|
|
34
|
+
}
|
|
35
|
+
config.Timeout = 15 * time.Second
|
|
36
|
+
client, err := dynamic.NewForConfig(config)
|
|
37
|
+
operatorRequire(t, err)
|
|
38
|
+
kube, err := kubernetes.NewForConfig(config)
|
|
39
|
+
operatorRequire(t, err)
|
|
40
|
+
data, err := os.ReadFile(os.Getenv("SOHA_ARGOCD_TEST_STATE"))
|
|
41
|
+
operatorRequire(t, err)
|
|
42
|
+
var state map[string]json.RawMessage
|
|
43
|
+
operatorRequire(t, json.Unmarshal(data, &state))
|
|
44
|
+
value := func(key string) string {
|
|
45
|
+
var value string
|
|
46
|
+
operatorRequire(t, json.Unmarshal(state[key], &value))
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
namespace, name := "soha-workflow-r6-gitops", "r6-gitops-runtime"
|
|
50
|
+
app := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "argoproj.io/v1alpha1", "kind": "Application", "metadata": map[string]any{"name": name, "namespace": namespace}, "spec": map[string]any{"project": "r6-gitops", "source": map[string]any{"repoURL": value("repositoryURL"), "path": ".", "targetRevision": value("v1"), "kustomize": map[string]any{"namespace": namespace, "images": []any{"app=" + value("image")}}}, "destination": map[string]any{"server": "https://kubernetes.default.svc", "namespace": namespace}, "syncPolicy": map[string]any{"syncOptions": []any{"FailOnSharedResource=true"}}}}}
|
|
51
|
+
children := func(version string) []*unstructured.Unstructured {
|
|
52
|
+
var documents map[string][]*unstructured.Unstructured
|
|
53
|
+
operatorRequire(t, json.Unmarshal(state["documents"], &documents))
|
|
54
|
+
if len(documents[version]) != 2 {
|
|
55
|
+
t.Fatal("frozen fixture requires two source-derived child documents")
|
|
56
|
+
}
|
|
57
|
+
return documents[version]
|
|
58
|
+
}
|
|
59
|
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
60
|
+
defer cancel()
|
|
61
|
+
resources := client.Resource(argoApplications).Namespace(namespace)
|
|
62
|
+
options := metav1.PatchOptions{FieldManager: "soha-r6-gitops-integration"}
|
|
63
|
+
id := uuid.NewString()
|
|
64
|
+
preflight := options
|
|
65
|
+
preflight.DryRun = []string{metav1.DryRunAll}
|
|
66
|
+
_, err = ApplyArgoApplication(ctx, client, app, children("v1"), preflight, id+":preflight")
|
|
67
|
+
operatorRequire(t, err)
|
|
68
|
+
if _, err = resources.Get(ctx, name, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
|
|
69
|
+
t.Fatal("preflight created an Application or fixture already exists")
|
|
70
|
+
}
|
|
71
|
+
t.Cleanup(func() {
|
|
72
|
+
if t.Failed() {
|
|
73
|
+
t.Log("failed fixture retained for diagnosis in its isolated namespace")
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
cleanup, stop := context.WithTimeout(context.Background(), 15*time.Second)
|
|
77
|
+
defer stop()
|
|
78
|
+
_ = resources.Delete(cleanup, name, metav1.DeleteOptions{})
|
|
79
|
+
for _, gvr := range []schema.GroupVersionResource{{Group: "apps", Version: "v1", Resource: "deployments"}, {Version: "v1", Resource: "services"}} {
|
|
80
|
+
_ = client.Resource(gvr).Namespace(namespace).Delete(cleanup, "r6-gitops-http", metav1.DeleteOptions{})
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
apply := func(version, operation string) {
|
|
84
|
+
operatorRequire(t, unstructured.SetNestedField(app.Object, value(version), "spec", "source", "targetRevision"))
|
|
85
|
+
live, err := ApplyArgoApplication(ctx, client, app, children(version), options, id+operation)
|
|
86
|
+
if err != nil {
|
|
87
|
+
var status any
|
|
88
|
+
if live != nil {
|
|
89
|
+
status = live.Object["status"]
|
|
90
|
+
}
|
|
91
|
+
t.Fatalf("sync %s: %v; status=%v", version, err, status)
|
|
92
|
+
}
|
|
93
|
+
err = wait.PollUntilContextTimeout(ctx, time.Second, 45*time.Second, true, func(ctx context.Context) (bool, error) {
|
|
94
|
+
live, err := resources.Get(ctx, name, metav1.GetOptions{})
|
|
95
|
+
if err != nil {
|
|
96
|
+
return false, err
|
|
97
|
+
}
|
|
98
|
+
observed, err := ObserveArgoApplication(ctx, client, app, live, children(version), options.FieldManager)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return false, err
|
|
101
|
+
}
|
|
102
|
+
return observed.Health == "healthy" && observed.OperationID == id+operation && observed.Revision == value(version), nil
|
|
103
|
+
})
|
|
104
|
+
operatorRequire(t, err)
|
|
105
|
+
body, err := kube.CoreV1().RESTClient().Get().Namespace(namespace).Resource("services").Name("r6-gitops-http:8080").SubResource("proxy").DoRaw(ctx)
|
|
106
|
+
if err != nil || strings.TrimSpace(string(body)) != "gitops-"+version {
|
|
107
|
+
t.Fatalf("actual HTTP %s: %q %v", version, body, err)
|
|
108
|
+
}
|
|
109
|
+
t.Logf("Argo operation %s applied commit %s with owned, healthy resources and actual HTTP %s", operation, value(version), version)
|
|
110
|
+
}
|
|
111
|
+
apply("v1", ":deploy")
|
|
112
|
+
apply("v2", ":upgrade")
|
|
113
|
+
apply("v1", ":rollback")
|
|
114
|
+
operatorRequire(t, unstructured.SetNestedField(app.Object, value("hold"), "spec", "source", "targetRevision"))
|
|
115
|
+
stopping, stop := context.WithCancel(ctx)
|
|
116
|
+
observedHold := make(chan error, 1)
|
|
117
|
+
go func() {
|
|
118
|
+
err := wait.PollUntilContextTimeout(ctx, time.Second, 45*time.Second, true, func(ctx context.Context) (bool, error) {
|
|
119
|
+
live, err := resources.Get(ctx, name, metav1.GetOptions{})
|
|
120
|
+
if err != nil {
|
|
121
|
+
return false, err
|
|
122
|
+
}
|
|
123
|
+
return argoOperationID(live, "status", "operationState", "operation") == id+":cancel" && argoString(live, "status", "operationState", "phase") == "Running", nil
|
|
124
|
+
})
|
|
125
|
+
observedHold <- err
|
|
126
|
+
stop()
|
|
127
|
+
}()
|
|
128
|
+
live, err := ApplyArgoApplication(stopping, client, app, children("hold"), options, id+":cancel")
|
|
129
|
+
operatorRequire(t, <-observedHold)
|
|
130
|
+
if !errors.Is(err, context.Canceled) || live == nil || argoOperationActive(live) {
|
|
131
|
+
t.Fatalf("cancel was not confirmed: %v", err)
|
|
132
|
+
}
|
|
133
|
+
t.Log("controller confirmed cancellation; restoring through a new operation")
|
|
134
|
+
apply("v1", ":restore")
|
|
135
|
+
// The direct writer cannot adopt a resource managed by Argo CD, even with force.
|
|
136
|
+
deployment, err := client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace(namespace).Get(ctx, "r6-gitops-http", metav1.GetOptions{})
|
|
137
|
+
operatorRequire(t, err)
|
|
138
|
+
if ValidateDirectManifestOwner(deployment) == nil {
|
|
139
|
+
t.Fatal("direct writer accepted Argo CD-owned resource")
|
|
140
|
+
}
|
|
141
|
+
t.Logf("real Argo CD GitOps lifecycle passed for %s/%s", namespace, name)
|
|
142
|
+
}
|