@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.
- package/COMPATIBILITY.md +72 -0
- package/README.md +38 -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 +7 -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/ConnectionCheckResult/invalid/missing-health.json +1 -0
- package/fixtures/openapi/ConnectionCheckResult/invalid/task-id.json +1 -0
- package/fixtures/openapi/ConnectionCheckResult/valid/failure.json +1 -0
- package/fixtures/openapi/ConnectionCheckResult/valid/healthy.json +1 -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/UpdateBrandingSettingsRequest/invalid/dark-logo-type.json +9 -0
- package/fixtures/openapi/UpdateBrandingSettingsRequest/valid/dark-logos.json +10 -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 +11 -3
- package/gen/go/sohaapi/client_test.go +26 -3
- package/gen/go/sohaapi/types.go +8103 -863
- 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 +20390 -13160
- 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 +54874 -38722
- package/openapi/soha-api.yaml +12199 -4982
- 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
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
package helmrelease
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/tar"
|
|
5
|
+
"bytes"
|
|
6
|
+
"compress/gzip"
|
|
7
|
+
"crypto/sha256"
|
|
8
|
+
"encoding/hex"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"errors"
|
|
11
|
+
"io"
|
|
12
|
+
"net/url"
|
|
13
|
+
"path"
|
|
14
|
+
"regexp"
|
|
15
|
+
"strings"
|
|
16
|
+
|
|
17
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const (
|
|
21
|
+
MaxChartArchiveBytes = 5 << 20
|
|
22
|
+
MaxChartExpandedBytes = 20 << 20
|
|
23
|
+
MaxChartFiles = 1000
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
var (
|
|
27
|
+
helmName = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`)
|
|
28
|
+
helmVersion = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`)
|
|
29
|
+
helmSHA256 = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
|
|
30
|
+
helmSecretReference = regexp.MustCompile(`^soha://secrets/[A-Za-z0-9._-]+(?:/versions/[1-9][0-9]*)?$`)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// ValidateSource rejects executable/local sources and ambiguous, mutable chart references.
|
|
34
|
+
// Credential authorization and resolution belong to the consuming application.
|
|
35
|
+
func ValidateSource(source sohaapi.DeploymentTemplateHelmSource) error {
|
|
36
|
+
u, err := url.Parse(source.RepositoryURL)
|
|
37
|
+
if err != nil || u.Hostname() == "" || (u.Scheme != "https" && u.Scheme != "oci") || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Opaque != "" {
|
|
38
|
+
return errors.New("helm repository must be an HTTPS or OCI URL without embedded credentials, query or fragment")
|
|
39
|
+
}
|
|
40
|
+
if strings.Contains(u.Path, "\\") || path.Clean("/"+u.Path) != strings.TrimSuffix("/"+strings.TrimPrefix(u.Path, "/"), "/") && u.Path != "" && u.Path != "/" {
|
|
41
|
+
return errors.New("helm repository path must be canonical")
|
|
42
|
+
}
|
|
43
|
+
if !helmName.MatchString(source.Chart) || len(source.Chart) > 253 || !helmVersion.MatchString(source.Version) {
|
|
44
|
+
return errors.New("helm chart requires a chart name and an exact semantic version")
|
|
45
|
+
}
|
|
46
|
+
if source.Digest != "" && !helmSHA256.MatchString(source.Digest) {
|
|
47
|
+
return errors.New("helm chart digest must be a SHA-256 digest")
|
|
48
|
+
}
|
|
49
|
+
if source.ConnectionID != "" {
|
|
50
|
+
return errors.New("helm chart connections are unavailable; use scoped secret references")
|
|
51
|
+
}
|
|
52
|
+
if source.SecretRefs != nil {
|
|
53
|
+
for alias, reference := range *source.SecretRefs {
|
|
54
|
+
if alias != "CHART_USERNAME" && alias != "CHART_PASSWORD" && alias != "CHART_CA_CERT" {
|
|
55
|
+
return errors.New("unsupported Helm chart credential reference")
|
|
56
|
+
}
|
|
57
|
+
if !helmSecretReference.MatchString(reference) {
|
|
58
|
+
return errors.New("helm chart credentials require Soha secret references")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func ValidateConfiguration(config sohaapi.HelmDeliveryConfiguration) error {
|
|
66
|
+
if err := ValidateSource(config.Source); err != nil {
|
|
67
|
+
return err
|
|
68
|
+
}
|
|
69
|
+
if !helmName.MatchString(config.ReleaseName) || len(config.ReleaseName) > 53 {
|
|
70
|
+
return errors.New("helm release name must be a DNS label of at most 53 characters")
|
|
71
|
+
}
|
|
72
|
+
if config.TimeoutSeconds < 0 || config.TimeoutSeconds > 3600 {
|
|
73
|
+
return errors.New("helm timeout must be between 1 and 3600 seconds")
|
|
74
|
+
}
|
|
75
|
+
seen := map[string]bool{}
|
|
76
|
+
for _, mapping := range config.ImageMappings {
|
|
77
|
+
if mapping.ContainerName == "" || !mapping.Value.Valid() {
|
|
78
|
+
return errors.New("helm image mapping requires a container and an immutable image field")
|
|
79
|
+
}
|
|
80
|
+
if _, err := pointerParts(mapping.Path); err != nil {
|
|
81
|
+
return err
|
|
82
|
+
}
|
|
83
|
+
if seen[mapping.Path] {
|
|
84
|
+
return errors.New("helm image mapping paths must be unique")
|
|
85
|
+
}
|
|
86
|
+
seen[mapping.Path] = true
|
|
87
|
+
}
|
|
88
|
+
return nil
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func SHA256(content []byte) string {
|
|
92
|
+
sum := sha256.Sum256(content)
|
|
93
|
+
return "sha256:" + hex.EncodeToString(sum[:])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ValidateChartArchive bounds both compressed and expanded input before an SDK loads it.
|
|
97
|
+
// Symlinks, duplicate entries and dependency downloads are never followed here.
|
|
98
|
+
func ValidateChartArchive(content []byte) error {
|
|
99
|
+
budget, files := int64(MaxChartExpandedBytes), 0
|
|
100
|
+
return validateChartArchive(content, &budget, &files, 0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func validateChartArchive(content []byte, budget *int64, files *int, depth int) error {
|
|
104
|
+
if len(content) == 0 || len(content) > MaxChartArchiveBytes {
|
|
105
|
+
return errors.New("helm chart archive exceeds the 5 MiB limit or is empty")
|
|
106
|
+
}
|
|
107
|
+
if depth > 8 {
|
|
108
|
+
return errors.New("helm chart dependencies exceed the nesting limit")
|
|
109
|
+
}
|
|
110
|
+
zr, err := gzip.NewReader(bytes.NewReader(content))
|
|
111
|
+
if err != nil {
|
|
112
|
+
return errors.New("helm chart must be a gzip archive")
|
|
113
|
+
}
|
|
114
|
+
defer func() { _ = zr.Close() }()
|
|
115
|
+
bounded := chartBudgetReader{reader: zr, remaining: budget}
|
|
116
|
+
reader := tar.NewReader(bounded)
|
|
117
|
+
seen := map[string]bool{}
|
|
118
|
+
root := ""
|
|
119
|
+
for {
|
|
120
|
+
header, err := reader.Next()
|
|
121
|
+
if errors.Is(err, io.EOF) {
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
if err != nil || *budget <= 0 || *files >= MaxChartFiles {
|
|
125
|
+
return errors.New("helm chart archive is invalid or exceeds its file/expanded size limit")
|
|
126
|
+
}
|
|
127
|
+
name := strings.TrimSuffix(header.Name, "/")
|
|
128
|
+
if err := validateChartEntry(header, name, seen); err != nil {
|
|
129
|
+
return err
|
|
130
|
+
}
|
|
131
|
+
folder, _, _ := strings.Cut(name, "/")
|
|
132
|
+
if root != "" && root != folder {
|
|
133
|
+
return errors.New("helm chart archive must contain one root directory")
|
|
134
|
+
}
|
|
135
|
+
root, seen[name] = folder, true
|
|
136
|
+
*files++
|
|
137
|
+
if strings.Contains(name, "/crds/") {
|
|
138
|
+
return errors.New("application Helm charts cannot install cluster-scoped CRDs; install them through platform management first")
|
|
139
|
+
}
|
|
140
|
+
if header.Typeflag == tar.TypeReg && path.Base(name) == "values.schema.json" {
|
|
141
|
+
content, err := io.ReadAll(reader)
|
|
142
|
+
if err != nil {
|
|
143
|
+
return errors.New("read Helm values schema")
|
|
144
|
+
}
|
|
145
|
+
if err := validateValuesSchema(content); err != nil {
|
|
146
|
+
return err
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if header.Typeflag == tar.TypeReg && strings.Contains(name, "/charts/") && strings.HasSuffix(name, ".tgz") {
|
|
150
|
+
child, err := io.ReadAll(io.LimitReader(reader, MaxChartArchiveBytes+1))
|
|
151
|
+
if err != nil {
|
|
152
|
+
return errors.New("read Helm chart dependency")
|
|
153
|
+
}
|
|
154
|
+
if err := validateChartArchive(child, budget, files, depth+1); err != nil {
|
|
155
|
+
return err
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if _, err := io.Copy(io.Discard, bounded); err != nil || *budget <= 0 || !seen[root+"/Chart.yaml"] {
|
|
160
|
+
return errors.New("helm chart archive is invalid, oversized or missing Chart.yaml")
|
|
161
|
+
}
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func validateValuesSchema(content []byte) error {
|
|
166
|
+
var schema any
|
|
167
|
+
if err := json.Unmarshal(content, &schema); err != nil {
|
|
168
|
+
return errors.New("helm values schema must be valid JSON")
|
|
169
|
+
}
|
|
170
|
+
return validateSchemaReferences(schema)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func validateSchemaReferences(value any) error {
|
|
174
|
+
switch item := value.(type) {
|
|
175
|
+
case map[string]any:
|
|
176
|
+
for key, value := range item {
|
|
177
|
+
if reference, ok := value.(string); ok {
|
|
178
|
+
switch key {
|
|
179
|
+
case "$ref", "$dynamicRef", "$recursiveRef":
|
|
180
|
+
if reference != "" && !strings.HasPrefix(reference, "#") {
|
|
181
|
+
return errors.New("helm values schemas require references within the same document")
|
|
182
|
+
}
|
|
183
|
+
case "$schema":
|
|
184
|
+
if !knownSchemaDialect(reference) {
|
|
185
|
+
return errors.New("helm values schema uses an unsupported schema dialect")
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if err := validateSchemaReferences(value); err != nil {
|
|
190
|
+
return err
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
case []any:
|
|
194
|
+
for _, value := range item {
|
|
195
|
+
if err := validateSchemaReferences(value); err != nil {
|
|
196
|
+
return err
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func knownSchemaDialect(value string) bool {
|
|
204
|
+
value = strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(value, "https://"), "http://"), "#")
|
|
205
|
+
switch value {
|
|
206
|
+
case "json-schema.org/draft-04/schema", "json-schema.org/draft-06/schema", "json-schema.org/draft-07/schema", "json-schema.org/draft/2019-09/schema", "json-schema.org/draft/2020-12/schema":
|
|
207
|
+
return true
|
|
208
|
+
default:
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func validateChartEntry(header *tar.Header, name string, seen map[string]bool) error {
|
|
214
|
+
if name == "" || strings.Contains(name, "\\") || strings.HasPrefix(name, "/") || path.Clean(name) != name || name == ".." || strings.HasPrefix(name, "../") || seen[name] {
|
|
215
|
+
return errors.New("helm chart archive contains an unsafe or duplicate path")
|
|
216
|
+
}
|
|
217
|
+
if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeDir || header.Size < 0 || header.Size > MaxChartExpandedBytes {
|
|
218
|
+
return errors.New("helm chart archive contains an unsupported file type or size")
|
|
219
|
+
}
|
|
220
|
+
return nil
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type chartBudgetReader struct {
|
|
224
|
+
reader io.Reader
|
|
225
|
+
remaining *int64
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func (r chartBudgetReader) Read(p []byte) (int, error) {
|
|
229
|
+
if *r.remaining <= 0 {
|
|
230
|
+
return 0, errors.New("helm chart exceeds the expanded size limit")
|
|
231
|
+
}
|
|
232
|
+
if int64(len(p)) > *r.remaining {
|
|
233
|
+
p = p[:*r.remaining]
|
|
234
|
+
}
|
|
235
|
+
n, err := r.reader.Read(p)
|
|
236
|
+
*r.remaining -= int64(n)
|
|
237
|
+
return n, err
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ValidateTaskIdentity bounds the confidential RPC/claim input before native SDK work.
|
|
241
|
+
// Prepare accepts an unfinished render; Execute additionally verifies its digests.
|
|
242
|
+
func ValidateTaskIdentity(input sohaapi.HelmExecutionTaskPayload) error {
|
|
243
|
+
s := input.Snapshot
|
|
244
|
+
for _, value := range []string{s.DeliveryPlanID, s.TargetID, s.ApplicationID, s.ApplicationEnvironmentID, s.ServiceID, s.ClusterID} {
|
|
245
|
+
if strings.TrimSpace(value) == "" || len(value) > 512 {
|
|
246
|
+
return errors.New("helm task identity is incomplete")
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if !input.Action.Valid() || !helmName.MatchString(s.Namespace) || len(s.Namespace) > 63 || !helmName.MatchString(s.ReleaseName) || len(s.ReleaseName) > 53 || s.ExpectedRevision < 0 || s.RollbackRevision < 0 || s.TimeoutSeconds < 0 || s.TimeoutSeconds > 3600 {
|
|
250
|
+
return errors.New("helm task action, namespace, revision or timeout is invalid")
|
|
251
|
+
}
|
|
252
|
+
if len(s.Resources) > 300 {
|
|
253
|
+
return errors.New("helm task exceeds 300 resources")
|
|
254
|
+
}
|
|
255
|
+
if input.Prepared != nil {
|
|
256
|
+
prepared := input.Prepared
|
|
257
|
+
if len(prepared.ChartArchive) > 6990508 || len(prepared.Manifest) > 2<<20 || len(prepared.Hooks) > 50 {
|
|
258
|
+
return errors.New("helm preparation exceeds its content limits")
|
|
259
|
+
}
|
|
260
|
+
total := len(prepared.Manifest)
|
|
261
|
+
for _, hook := range prepared.Hooks {
|
|
262
|
+
total += len(hook)
|
|
263
|
+
}
|
|
264
|
+
encoded, err := json.Marshal(prepared.Values)
|
|
265
|
+
if err != nil || total > 2<<20 || len(encoded) > 1<<20 {
|
|
266
|
+
return errors.New("helm rendered content or values exceeds its limit")
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
encoded, err := json.Marshal(input)
|
|
270
|
+
if err != nil || len(encoded) > 8<<20 {
|
|
271
|
+
return errors.New("helm task exceeds the RPC limit")
|
|
272
|
+
}
|
|
273
|
+
return nil
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// LegacyAllowedActions restricts delivery-owned releases to inspection through
|
|
277
|
+
// the legacy resource API. Nil leaves unmanaged releases to normal authorization.
|
|
278
|
+
func LegacyAllowedActions(labels map[string]string) []string {
|
|
279
|
+
if labels["soha-delivery-owner"] != "" {
|
|
280
|
+
return []string{"view"}
|
|
281
|
+
}
|
|
282
|
+
return nil
|
|
283
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package helmrelease
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/tar"
|
|
5
|
+
"bytes"
|
|
6
|
+
"compress/gzip"
|
|
7
|
+
"reflect"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestHelmDeliveryBoundaries(t *testing.T) {
|
|
15
|
+
for _, repository := range []string{"https://charts.example.test", "https://charts.example.test/private/", "oci://registry.example.test/team"} {
|
|
16
|
+
if err := ValidateSource(sohaapi.DeploymentTemplateHelmSource{RepositoryURL: repository, Chart: "app", Version: "1.2.3-beta.1+build"}); err != nil {
|
|
17
|
+
t.Fatal(err)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for _, repository := range []string{"file:///tmp/chart", "http://example.test", "https://user:pass@example.test", "https://example.test?token=secret", "https://example.test/a/../b", "https://example.test/%2e%2e/b"} {
|
|
21
|
+
if err := ValidateSource(sohaapi.DeploymentTemplateHelmSource{RepositoryURL: repository, Chart: "app", Version: "1.2.3"}); err == nil {
|
|
22
|
+
t.Fatalf("accepted unsafe source %s", repository)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
image := "registry.example.test/app@sha256:" + strings.Repeat("a", 64)
|
|
26
|
+
values := map[string]any{"workers": []any{map[string]any{"image": "old"}}}
|
|
27
|
+
mappings := []sohaapi.HelmImageMapping{{ContainerName: "main", Path: "/workers/0/image", Value: sohaapi.HelmImageMappingValueImage}}
|
|
28
|
+
result, err := MapImages(values, mappings, map[string]string{"main": image})
|
|
29
|
+
if err != nil || !reflect.DeepEqual(result, map[string]any{"workers": []any{map[string]any{"image": image}}}) || !reflect.DeepEqual(values, map[string]any{"workers": []any{map[string]any{"image": "old"}}}) {
|
|
30
|
+
t.Fatalf("image mapping did not preserve input: %v", err)
|
|
31
|
+
}
|
|
32
|
+
for _, invalid := range []string{"/workers/2/image", "/workers/01/image", "/workers/-/image", "/workers/0/image/child", "/bad~2/path"} {
|
|
33
|
+
mappings[0].Path = invalid
|
|
34
|
+
if _, err := MapImages(values, mappings, map[string]string{"main": image}); err == nil {
|
|
35
|
+
t.Fatalf("accepted invalid image path %s", invalid)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
mappings[0].Path = "/image"
|
|
39
|
+
if _, err := MapImages(nil, mappings, map[string]string{"main": image}); err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
if _, err := MapImages(nil, mappings, map[string]string{"main": "registry/app:latest"}); err == nil {
|
|
43
|
+
t.Fatal("accepted mutable image")
|
|
44
|
+
}
|
|
45
|
+
valid := testChartArchive(t, []tar.Header{{Name: "app/Chart.yaml", Mode: 0600, Size: 1}})
|
|
46
|
+
if err := ValidateChartArchive(valid); err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
for _, headers := range [][]tar.Header{
|
|
50
|
+
{{Name: "../Chart.yaml", Size: 1}},
|
|
51
|
+
{{Name: "app/Chart.yaml", Size: 1}, {Name: "app/Chart.yaml", Size: 1}},
|
|
52
|
+
{{Name: "app/Chart.yaml", Typeflag: tar.TypeSymlink, Linkname: "/etc/passwd"}},
|
|
53
|
+
{{Name: "app/Chart.yaml", Size: 1}, {Name: "app/large", Size: MaxChartExpandedBytes}},
|
|
54
|
+
{{Name: "app/Chart.yaml", Size: 1}, {Name: "app/crds/example.yaml", Size: 1}},
|
|
55
|
+
{{Name: "app/Chart.yaml", Size: 1}, {Name: "app/charts/child/crds/example.yaml", Size: 1}},
|
|
56
|
+
} {
|
|
57
|
+
if err := ValidateChartArchive(testChartArchive(t, headers)); err == nil {
|
|
58
|
+
t.Fatal("accepted unsafe archive")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
budget, files := int64(100), 0
|
|
62
|
+
if err := validateChartArchive(valid, &budget, &files, 0); err == nil {
|
|
63
|
+
t.Fatal("expanded archive budget was not enforced")
|
|
64
|
+
}
|
|
65
|
+
for _, schema := range []string{
|
|
66
|
+
`{"$ref":"file:///etc/config.json"}`,
|
|
67
|
+
`{"properties":{"value":{"$ref":"https://example.test/schema"}}}`,
|
|
68
|
+
`{"allOf":[{"$dynamicRef":"../other.json#/$defs/value"}]}`,
|
|
69
|
+
`{"$schema":"https://example.test/dialect"}`,
|
|
70
|
+
} {
|
|
71
|
+
if err := validateValuesSchema([]byte(schema)); err == nil {
|
|
72
|
+
t.Fatalf("accepted external schema reference: %s", schema)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if err := validateValuesSchema([]byte(`{"$schema":"http://json-schema.org/draft-07/schema#","$defs":{"value":{"type":"string"}},"properties":{"value":{"$ref":"#/$defs/value"}}}`)); err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func testChartArchive(t *testing.T, headers []tar.Header) []byte {
|
|
81
|
+
t.Helper()
|
|
82
|
+
var buffer bytes.Buffer
|
|
83
|
+
zip := gzip.NewWriter(&buffer)
|
|
84
|
+
writer := tar.NewWriter(zip)
|
|
85
|
+
for _, header := range headers {
|
|
86
|
+
if err := writer.WriteHeader(&header); err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
if header.Size > 0 {
|
|
90
|
+
if _, err := writer.Write(bytes.Repeat([]byte("x"), int(header.Size))); err != nil {
|
|
91
|
+
t.Fatal(err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if err := writer.Close(); err != nil {
|
|
96
|
+
t.Fatal(err)
|
|
97
|
+
}
|
|
98
|
+
if err := zip.Close(); err != nil {
|
|
99
|
+
t.Fatal(err)
|
|
100
|
+
}
|
|
101
|
+
return buffer.Bytes()
|
|
102
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package helmrelease
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"archive/tar"
|
|
5
|
+
"bytes"
|
|
6
|
+
"compress/gzip"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
|
|
13
|
+
"sigs.k8s.io/yaml"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// FrozenChartArchive transports already rendered resources through native Helm
|
|
17
|
+
// install/upgrade/rollback, including hooks. It never evaluates source templates
|
|
18
|
+
// again. The original source digest remains part of the delivery snapshot.
|
|
19
|
+
func FrozenChartArchive(original []byte, manifest string, hooks []string) ([]byte, error) {
|
|
20
|
+
if len(manifest) > 2<<20 || len(hooks) > 50 {
|
|
21
|
+
return nil, errors.New("helm rendered resources exceed the snapshot limit")
|
|
22
|
+
}
|
|
23
|
+
total := len(manifest)
|
|
24
|
+
for _, hook := range hooks {
|
|
25
|
+
total += len(hook)
|
|
26
|
+
}
|
|
27
|
+
if total > 4<<20 {
|
|
28
|
+
return nil, errors.New("helm rendered hooks exceed the snapshot limit")
|
|
29
|
+
}
|
|
30
|
+
if err := ValidateChartArchive(original); err != nil {
|
|
31
|
+
return nil, err
|
|
32
|
+
}
|
|
33
|
+
metadata, err := chartMetadata(original)
|
|
34
|
+
if err != nil {
|
|
35
|
+
return nil, err
|
|
36
|
+
}
|
|
37
|
+
delete(metadata, "dependencies")
|
|
38
|
+
metadataBytes, err := yaml.Marshal(metadata)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
files := []struct{ name, content string }{{"Chart.yaml", string(metadataBytes)}, {"values.yaml", "{}\n"}, {"templates/resources.yaml", "{{ print " + strconv.Quote(manifest) + " }}"}}
|
|
43
|
+
for index, hook := range hooks {
|
|
44
|
+
files = append(files, struct{ name, content string }{fmt.Sprintf("templates/hook-%d.yaml", index), "{{ print " + strconv.Quote(hook) + " }}"})
|
|
45
|
+
}
|
|
46
|
+
var buffer bytes.Buffer
|
|
47
|
+
zip := gzip.NewWriter(&buffer)
|
|
48
|
+
writer := tar.NewWriter(zip)
|
|
49
|
+
for _, file := range files {
|
|
50
|
+
if err := writer.WriteHeader(&tar.Header{Name: "frozen/" + file.name, Mode: 0600, Size: int64(len(file.content))}); err != nil {
|
|
51
|
+
return nil, err
|
|
52
|
+
}
|
|
53
|
+
if _, err := io.WriteString(writer, file.content); err != nil {
|
|
54
|
+
return nil, err
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if err := writer.Close(); err != nil {
|
|
58
|
+
return nil, err
|
|
59
|
+
}
|
|
60
|
+
if err := zip.Close(); err != nil {
|
|
61
|
+
return nil, err
|
|
62
|
+
}
|
|
63
|
+
return buffer.Bytes(), nil
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func chartMetadata(content []byte) (map[string]any, error) {
|
|
67
|
+
zip, err := gzip.NewReader(bytes.NewReader(content))
|
|
68
|
+
if err != nil {
|
|
69
|
+
return nil, err
|
|
70
|
+
}
|
|
71
|
+
defer func() { _ = zip.Close() }()
|
|
72
|
+
reader := tar.NewReader(zip)
|
|
73
|
+
for {
|
|
74
|
+
header, err := reader.Next()
|
|
75
|
+
if err != nil {
|
|
76
|
+
return nil, errors.New("helm chart metadata is missing")
|
|
77
|
+
}
|
|
78
|
+
if strings.Count(header.Name, "/") != 1 || !strings.HasSuffix(header.Name, "/Chart.yaml") {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
content, err := io.ReadAll(reader)
|
|
82
|
+
if err != nil {
|
|
83
|
+
return nil, err
|
|
84
|
+
}
|
|
85
|
+
var metadata map[string]any
|
|
86
|
+
if err := yaml.Unmarshal(content, &metadata); err != nil {
|
|
87
|
+
return nil, errors.New("helm chart metadata is invalid")
|
|
88
|
+
}
|
|
89
|
+
return metadata, nil
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package helmrelease
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
|
|
6
|
+
"github.com/opensoha/soha-contracts/gen/go/sohaapi"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ValidateCompletedResult binds completion to the frozen plan and native revision.
|
|
10
|
+
func ValidateCompletedResult(payload sohaapi.HelmExecutionTaskPayload, result sohaapi.HelmExecutionTaskResult) error {
|
|
11
|
+
if !result.Stopped || result.RenderedDigest != payload.Snapshot.RenderedDigest || !helmSHA256.MatchString(result.RenderedDigest) {
|
|
12
|
+
return errors.New("helm completion must confirm executor stop and the approved render")
|
|
13
|
+
}
|
|
14
|
+
if payload.Action == sohaapi.Preflight {
|
|
15
|
+
if !result.Ready || result.Status != "preflighted" || result.Revision != payload.Snapshot.ExpectedRevision {
|
|
16
|
+
return errors.New("helm preflight must match the expected native revision")
|
|
17
|
+
}
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
20
|
+
if payload.Action != sohaapi.Apply && payload.Action != sohaapi.Observe || result.Status != "deployed" || result.Revision != payload.Snapshot.ExpectedRevision+1 {
|
|
21
|
+
return errors.New("helm completion must match the deployed native revision")
|
|
22
|
+
}
|
|
23
|
+
if payload.Action == sohaapi.Observe && !result.Ready {
|
|
24
|
+
return errors.New("helm health observation is not ready")
|
|
25
|
+
}
|
|
26
|
+
return nil
|
|
27
|
+
}
|