@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,174 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/url"
|
|
8
|
+
"os"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/google/uuid"
|
|
14
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
15
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
16
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
17
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
18
|
+
"k8s.io/apimachinery/pkg/types"
|
|
19
|
+
"k8s.io/apimachinery/pkg/util/wait"
|
|
20
|
+
"k8s.io/client-go/dynamic"
|
|
21
|
+
"k8s.io/client-go/tools/clientcmd"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
func TestWorkloadCronJobWithKubernetes(t *testing.T) {
|
|
25
|
+
path, namespace, image := os.Getenv("SOHA_OPERATOR_TEST_KUBECONFIG"), os.Getenv("SOHA_OPERATOR_TEST_NAMESPACE"), os.Getenv("SOHA_OPERATOR_TEST_IMAGE")
|
|
26
|
+
if path == "" {
|
|
27
|
+
t.Skip("set SOHA_OPERATOR_TEST_KUBECONFIG, namespace and digest-pinned image for the isolated controller fixture")
|
|
28
|
+
}
|
|
29
|
+
if !strings.HasPrefix(namespace, "soha-workflow-r6-") || !strings.Contains(image, "@sha256:") {
|
|
30
|
+
t.Fatal("isolated namespace and pinned image are required")
|
|
31
|
+
}
|
|
32
|
+
config, err := clientcmd.BuildConfigFromFlags("", path)
|
|
33
|
+
operatorRequire(t, err)
|
|
34
|
+
endpoint, err := url.Parse(config.Host)
|
|
35
|
+
if err != nil || endpoint.Hostname() != "127.0.0.1" {
|
|
36
|
+
t.Fatal("only the loopback integration cluster is supported")
|
|
37
|
+
}
|
|
38
|
+
config.Timeout = 10 * time.Second
|
|
39
|
+
client, err := dynamic.NewForConfig(config)
|
|
40
|
+
operatorRequire(t, err)
|
|
41
|
+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
42
|
+
defer cancel()
|
|
43
|
+
name := "periodic-" + uuid.NewString()[:8]
|
|
44
|
+
deployments := client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace(namespace)
|
|
45
|
+
roots := client.Resource(schema.GroupVersionResource{Group: "workloads.soha.io", Version: "v1alpha1", Resource: "workloadcronjobs"}).Namespace(namespace)
|
|
46
|
+
targets := client.Resource(schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"}).Namespace(namespace)
|
|
47
|
+
source, root := operatorIntegrationObjects(t, namespace, name, image)
|
|
48
|
+
options := metav1.PatchOptions{FieldManager: "soha-operator-integration"}
|
|
49
|
+
t.Cleanup(func() {
|
|
50
|
+
cleanup, stop := context.WithTimeout(context.Background(), 10*time.Second)
|
|
51
|
+
defer stop()
|
|
52
|
+
_ = deployments.Delete(cleanup, name, metav1.DeleteOptions{})
|
|
53
|
+
})
|
|
54
|
+
t.Cleanup(func() {
|
|
55
|
+
cleanup, stop := context.WithTimeout(context.Background(), 10*time.Second)
|
|
56
|
+
defer stop()
|
|
57
|
+
_ = roots.Delete(cleanup, name, metav1.DeleteOptions{})
|
|
58
|
+
})
|
|
59
|
+
_, err = ApplyManifest(ctx, deployments, source, options)
|
|
60
|
+
operatorRequire(t, err)
|
|
61
|
+
_, err = ApplyManifest(ctx, roots, root, options)
|
|
62
|
+
operatorRequire(t, err)
|
|
63
|
+
live := operatorWaitHealth(t, ctx, client, roots, name, "healthy")
|
|
64
|
+
target, err := targets.Get(ctx, name, metav1.GetOptions{})
|
|
65
|
+
operatorRequire(t, err)
|
|
66
|
+
if metav1.GetControllerOf(target) == nil || metav1.GetControllerOf(target).UID != live.GetUID() {
|
|
67
|
+
t.Fatal("target ownership missing")
|
|
68
|
+
}
|
|
69
|
+
// This task is deliberately suspended; synchronization does not imply a Job ran.
|
|
70
|
+
jobs, err := client.Resource(schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}).Namespace(namespace).List(ctx, metav1.ListOptions{})
|
|
71
|
+
operatorRequire(t, err)
|
|
72
|
+
for _, job := range jobs.Items {
|
|
73
|
+
if owner := metav1.GetControllerOf(&job); owner != nil && owner.UID == target.GetUID() {
|
|
74
|
+
t.Fatal("suspended fixture unexpectedly ran a Job")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for _, schedule := range []string{"0 3 * * *", "0 2 * * *"} {
|
|
78
|
+
operatorRequire(t, unstructured.SetNestedField(root.Object, schedule, "spec", "cronJobSpec", "schedule"))
|
|
79
|
+
_, err = ApplyManifest(ctx, roots, root, options)
|
|
80
|
+
operatorRequire(t, err)
|
|
81
|
+
operatorWaitHealth(t, ctx, client, roots, name, "healthy")
|
|
82
|
+
target, err = targets.Get(ctx, name, metav1.GetOptions{})
|
|
83
|
+
operatorRequire(t, err)
|
|
84
|
+
actual, _, _ := unstructured.NestedString(target.Object, "spec", "schedule")
|
|
85
|
+
if actual != schedule {
|
|
86
|
+
t.Fatalf("schedule = %s, want %s", actual, schedule)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Even force must not write the Operator-owned child directly.
|
|
90
|
+
yes := true
|
|
91
|
+
if _, err := ApplyManifest(ctx, targets, target, metav1.PatchOptions{FieldManager: "other-delivery", Force: &yes, DryRun: []string{metav1.DryRunAll}}); err == nil {
|
|
92
|
+
t.Fatal("second writer was accepted")
|
|
93
|
+
}
|
|
94
|
+
// A missing source must degrade, and recreating it must recover with a new UID.
|
|
95
|
+
operatorRequire(t, deployments.Delete(ctx, name, metav1.DeleteOptions{}))
|
|
96
|
+
operatorWaitHealth(t, ctx, client, roots, name, "degraded")
|
|
97
|
+
_, err = ApplyManifest(ctx, deployments, source, options)
|
|
98
|
+
operatorRequire(t, err)
|
|
99
|
+
operatorWaitHealth(t, ctx, client, roots, name, "healthy")
|
|
100
|
+
operatorVerifyDeletion(t, ctx, client, roots, targets, name)
|
|
101
|
+
t.Log("real Operator: owned CronJob, generation/version observation, update/rollback, second-writer rejection, source-loss recovery and finalizer/foreground deletion passed")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func operatorWaitHealth(t *testing.T, ctx context.Context, client dynamic.Interface, roots dynamic.ResourceInterface, name, want string) *unstructured.Unstructured {
|
|
105
|
+
t.Helper()
|
|
106
|
+
var live *unstructured.Unstructured
|
|
107
|
+
var health string
|
|
108
|
+
err := wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) {
|
|
109
|
+
var err error
|
|
110
|
+
live, err = roots.Get(ctx, name, metav1.GetOptions{})
|
|
111
|
+
if err != nil {
|
|
112
|
+
return false, err
|
|
113
|
+
}
|
|
114
|
+
health, err = ManifestHealth(ctx, client, live)
|
|
115
|
+
return health == want, err
|
|
116
|
+
})
|
|
117
|
+
if err != nil {
|
|
118
|
+
t.Fatalf("Operator health = %s, want %s: %v", health, want, err)
|
|
119
|
+
}
|
|
120
|
+
return live
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func operatorVerifyDeletion(t *testing.T, ctx context.Context, client dynamic.Interface, roots, targets dynamic.ResourceInterface, name string) {
|
|
124
|
+
t.Helper()
|
|
125
|
+
_, err := roots.Patch(ctx, name, types.MergePatchType, []byte(`{"metadata":{"finalizers":["integration.soha.io/hold"]}}`), metav1.PatchOptions{})
|
|
126
|
+
operatorRequire(t, err)
|
|
127
|
+
if err := DeleteManifest(ctx, roots, name, "old-root-uid"); !apierrors.IsConflict(err) {
|
|
128
|
+
t.Fatalf("replacement fence: %v", err)
|
|
129
|
+
}
|
|
130
|
+
operatorRequire(t, DeleteManifest(ctx, roots, name, ""))
|
|
131
|
+
live, err := roots.Get(ctx, name, metav1.GetOptions{})
|
|
132
|
+
operatorRequire(t, err)
|
|
133
|
+
if live.GetDeletionTimestamp() == nil || len(live.GetFinalizers()) == 0 {
|
|
134
|
+
t.Fatal("finalizer did not preserve deleting state")
|
|
135
|
+
}
|
|
136
|
+
health, err := ManifestHealth(ctx, client, live)
|
|
137
|
+
operatorRequire(t, err)
|
|
138
|
+
if health != "degraded" {
|
|
139
|
+
t.Fatal("deleting object was healthy")
|
|
140
|
+
}
|
|
141
|
+
if _, err := ApplyManifest(ctx, roots, live, metav1.PatchOptions{FieldManager: "soha-operator-integration"}); err == nil {
|
|
142
|
+
t.Fatal("deleting root accepted a new write")
|
|
143
|
+
}
|
|
144
|
+
// Only the fixture removes its own hold finalizer. Production never does this.
|
|
145
|
+
operatorRequire(t, wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) {
|
|
146
|
+
_, err := targets.Get(ctx, name, metav1.GetOptions{})
|
|
147
|
+
return apierrors.IsNotFound(err), nil
|
|
148
|
+
}))
|
|
149
|
+
_, err = roots.Patch(ctx, name, types.MergePatchType, []byte(`{"metadata":{"finalizers":[]}}`), metav1.PatchOptions{})
|
|
150
|
+
operatorRequire(t, err)
|
|
151
|
+
operatorRequire(t, wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) {
|
|
152
|
+
_, err := roots.Get(ctx, name, metav1.GetOptions{})
|
|
153
|
+
return apierrors.IsNotFound(err), nil
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func operatorIntegrationObjects(t *testing.T, namespace, name, image string) (*unstructured.Unstructured, *unstructured.Unstructured) {
|
|
158
|
+
t.Helper()
|
|
159
|
+
decode := func(raw string) *unstructured.Unstructured {
|
|
160
|
+
var object map[string]any
|
|
161
|
+
operatorRequire(t, json.Unmarshal([]byte(raw), &object))
|
|
162
|
+
return &unstructured.Unstructured{Object: object}
|
|
163
|
+
}
|
|
164
|
+
source := decode(fmt.Sprintf(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":%q,"namespace":%q},"spec":{"replicas":1,"selector":{"matchLabels":{"app":%q}},"template":{"metadata":{"labels":{"app":%q}},"spec":{"containers":[{"name":"app","image":%q,"command":["sleep","3600"]}]}}}}`, name, namespace, name, name, image))
|
|
165
|
+
root := decode(fmt.Sprintf(`{"apiVersion":"workloads.soha.io/v1alpha1","kind":"WorkloadCronJob","metadata":{"name":%q,"namespace":%q},"spec":{"sourceRef":{"kind":"Deployment","name":%q,"container":"app"},"targetContainer":"task","cronJobSpec":{"schedule":"0 2 * * *","suspend":true,"jobTemplate":{"spec":{"template":{"spec":{"restartPolicy":"Never","containers":[{"name":"task","image":%q,"command":["true"]}]}}}}}}}`, name, namespace, name, image))
|
|
166
|
+
return source, root
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func operatorRequire(t *testing.T, err error) {
|
|
170
|
+
t.Helper()
|
|
171
|
+
if err != nil {
|
|
172
|
+
t.Fatal(err)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"strings"
|
|
9
|
+
|
|
10
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
11
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
12
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
13
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
14
|
+
"k8s.io/apimachinery/pkg/types"
|
|
15
|
+
"k8s.io/apimachinery/pkg/util/sets"
|
|
16
|
+
"k8s.io/client-go/dynamic"
|
|
17
|
+
"k8s.io/client-go/util/csaupgrade"
|
|
18
|
+
"k8s.io/client-go/util/retry"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// ManifestAgentProviderPrefix prevents legacy runners from claiming operations
|
|
22
|
+
// that require frozen controller resources and ownership-fenced writes.
|
|
23
|
+
const ManifestAgentProviderPrefix = "manifest_agent_v3."
|
|
24
|
+
const ManifestAgentCapability = "manifest.execution.v3"
|
|
25
|
+
|
|
26
|
+
var ErrResourceOwnership = errors.New("resource mutation is blocked by ownership")
|
|
27
|
+
|
|
28
|
+
// ApplyManifest serializes the ownership check with the write. Creation uses
|
|
29
|
+
// create semantics so a concurrent external owner cannot be silently adopted.
|
|
30
|
+
func ApplyManifest(ctx context.Context, client dynamic.ResourceInterface, desired *unstructured.Unstructured, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
|
|
31
|
+
if IsArgoApplication(desired) {
|
|
32
|
+
return nil, fmt.Errorf("%w: Argo CD Applications require frozen GitOps execution", ErrResourceOwnership)
|
|
33
|
+
}
|
|
34
|
+
if IsArgoRollout(desired) || desired.GetAnnotations()[RolloutOwnerAnnotation] != "" {
|
|
35
|
+
return nil, fmt.Errorf("%w: progressive resources require frozen rollout execution", ErrResourceOwnership)
|
|
36
|
+
}
|
|
37
|
+
live, err := client.Get(ctx, desired.GetName(), metav1.GetOptions{})
|
|
38
|
+
if apierrors.IsNotFound(err) {
|
|
39
|
+
created, createErr := client.Create(ctx, desired, metav1.CreateOptions{FieldManager: options.FieldManager, DryRun: options.DryRun, FieldValidation: options.FieldValidation})
|
|
40
|
+
if createErr != nil || len(options.DryRun) != 0 {
|
|
41
|
+
return created, createErr
|
|
42
|
+
}
|
|
43
|
+
live, err = created, nil
|
|
44
|
+
}
|
|
45
|
+
if err != nil {
|
|
46
|
+
return nil, err
|
|
47
|
+
}
|
|
48
|
+
expectedUID := live.GetUID()
|
|
49
|
+
var applied *unstructured.Unstructured
|
|
50
|
+
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
|
51
|
+
current, readErr := client.Get(ctx, desired.GetName(), metav1.GetOptions{})
|
|
52
|
+
if readErr != nil {
|
|
53
|
+
return readErr
|
|
54
|
+
}
|
|
55
|
+
if current.GetUID() != expectedUID {
|
|
56
|
+
return fmt.Errorf("resource was replaced during apply")
|
|
57
|
+
}
|
|
58
|
+
if ownerErr := ValidateDirectManifestOwner(current); ownerErr != nil {
|
|
59
|
+
return ownerErr
|
|
60
|
+
}
|
|
61
|
+
applied, readErr = applyObservedManifest(ctx, client, desired, current, options)
|
|
62
|
+
return readErr
|
|
63
|
+
})
|
|
64
|
+
return applied, err
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func applyObservedManifest(ctx context.Context, client dynamic.ResourceInterface, desired, live *unstructured.Unstructured, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
|
|
68
|
+
if len(options.DryRun) == 0 {
|
|
69
|
+
// Native migration moves only this manager's Create/Update ownership to
|
|
70
|
+
// Apply and guards resourceVersion; other field owners stay intact.
|
|
71
|
+
upgrade, err := csaupgrade.UpgradeManagedFieldsPatch(live, sets.New(options.FieldManager), options.FieldManager)
|
|
72
|
+
if err != nil {
|
|
73
|
+
return nil, err
|
|
74
|
+
}
|
|
75
|
+
if len(upgrade) != 0 {
|
|
76
|
+
live, err = client.Patch(ctx, desired.GetName(), types.JSONPatchType, upgrade, metav1.PatchOptions{FieldManager: options.FieldManager})
|
|
77
|
+
if err != nil {
|
|
78
|
+
return nil, err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return patchOwnedManifest(ctx, client, desired, live, options)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func patchOwnedManifest(ctx context.Context, client dynamic.ResourceInterface, desired, live *unstructured.Unstructured, options metav1.PatchOptions) (*unstructured.Unstructured, error) {
|
|
86
|
+
write := desired.DeepCopy()
|
|
87
|
+
write.SetResourceVersion(live.GetResourceVersion())
|
|
88
|
+
write.SetUID(live.GetUID())
|
|
89
|
+
body, err := json.Marshal(write.Object)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return nil, err
|
|
92
|
+
}
|
|
93
|
+
return client.Patch(ctx, desired.GetName(), types.ApplyPatchType, body, options)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ValidateDirectManifestOwner keeps direct SSA from becoming a second writer
|
|
97
|
+
// for a controller's child, a Helm release or a registered GitOps resource.
|
|
98
|
+
func ValidateDirectManifestOwner(object metav1.Object) error {
|
|
99
|
+
if object == nil {
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
if object.GetDeletionTimestamp() != nil {
|
|
103
|
+
return fmt.Errorf("%w: resource is being deleted; wait for finalizers to finish", ErrResourceOwnership)
|
|
104
|
+
}
|
|
105
|
+
return validateExternalManifestOwner(object)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func validateExternalManifestOwner(object metav1.Object) error {
|
|
109
|
+
if resource, ok := object.(*unstructured.Unstructured); ok && IsArgoRollout(resource) {
|
|
110
|
+
return fmt.Errorf("%w: Argo Rollouts require frozen progressive execution", ErrResourceOwnership)
|
|
111
|
+
}
|
|
112
|
+
if object.GetAnnotations()[RolloutOwnerAnnotation] != "" {
|
|
113
|
+
return fmt.Errorf("%w: resource belongs to a progressive rollout", ErrResourceOwnership)
|
|
114
|
+
}
|
|
115
|
+
if resource, ok := object.(*unstructured.Unstructured); ok && IsArgoApplication(resource) {
|
|
116
|
+
return fmt.Errorf("%w: Argo CD Applications require frozen GitOps execution", ErrResourceOwnership)
|
|
117
|
+
}
|
|
118
|
+
if metav1.GetControllerOf(object) != nil {
|
|
119
|
+
return fmt.Errorf("%w: resource is managed by a controller; update its owning resource", ErrResourceOwnership)
|
|
120
|
+
}
|
|
121
|
+
labels, annotations := object.GetLabels(), object.GetAnnotations()
|
|
122
|
+
if labels["app.kubernetes.io/managed-by"] == "Helm" || annotations["meta.helm.sh/release-name"] != "" ||
|
|
123
|
+
labels["argocd.argoproj.io/instance"] != "" || annotations["argocd.argoproj.io/tracking-id"] != "" ||
|
|
124
|
+
labels["kustomize.toolkit.fluxcd.io/name"] != "" || labels["helm.toolkit.fluxcd.io/name"] != "" {
|
|
125
|
+
return fmt.Errorf("%w: resource has an external delivery owner", ErrResourceOwnership)
|
|
126
|
+
}
|
|
127
|
+
for _, field := range object.GetManagedFields() {
|
|
128
|
+
if strings.HasPrefix(field.Manager, "argocd") || field.Manager == "kustomize-controller" || field.Manager == "helm-controller" {
|
|
129
|
+
return fmt.Errorf("%w: resource is managed by a GitOps controller", ErrResourceOwnership)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return nil
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// UpdateManifest preserves the user's optimistic lock and refuses updates to
|
|
136
|
+
// externally managed or deleting roots, including attempts to remove finalizers.
|
|
137
|
+
func UpdateManifest(ctx context.Context, client dynamic.ResourceInterface, desired *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
|
138
|
+
live, err := client.Get(ctx, desired.GetName(), metav1.GetOptions{})
|
|
139
|
+
if err != nil {
|
|
140
|
+
return nil, err
|
|
141
|
+
}
|
|
142
|
+
if err := ValidateDirectManifestOwner(live); err != nil {
|
|
143
|
+
return nil, err
|
|
144
|
+
}
|
|
145
|
+
write := desired.DeepCopy()
|
|
146
|
+
if write.GetUID() == "" {
|
|
147
|
+
write.SetUID(live.GetUID())
|
|
148
|
+
}
|
|
149
|
+
if write.GetResourceVersion() == "" {
|
|
150
|
+
write.SetResourceVersion(live.GetResourceVersion())
|
|
151
|
+
}
|
|
152
|
+
return client.Update(ctx, write, metav1.UpdateOptions{})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// DeleteManifest requests deletion of the observed root. A nil error confirms
|
|
156
|
+
// acceptance only; finalizers are never removed and completion must be observed.
|
|
157
|
+
func DeleteManifest(ctx context.Context, client dynamic.ResourceInterface, name, expectedUID string) error {
|
|
158
|
+
live, err := client.Get(ctx, name, metav1.GetOptions{})
|
|
159
|
+
if err != nil {
|
|
160
|
+
return err
|
|
161
|
+
}
|
|
162
|
+
uid, version := live.GetUID(), live.GetResourceVersion()
|
|
163
|
+
if uid == "" || version == "" || expectedUID != "" && string(uid) != expectedUID {
|
|
164
|
+
return apierrors.NewConflict(schema.GroupResource{Resource: live.GetKind()}, name, fmt.Errorf("resource identity changed; refresh before deleting"))
|
|
165
|
+
}
|
|
166
|
+
if err := validateExternalManifestOwner(live); err != nil {
|
|
167
|
+
return err
|
|
168
|
+
}
|
|
169
|
+
if live.GetDeletionTimestamp() != nil {
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
options := metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid, ResourceVersion: &version}}
|
|
173
|
+
if live.GetAPIVersion() == "workloads.soha.io/v1alpha1" && live.GetKind() == "WorkloadCronJob" {
|
|
174
|
+
foreground := metav1.DeletePropagationForeground
|
|
175
|
+
options.PropagationPolicy = &foreground
|
|
176
|
+
}
|
|
177
|
+
return client.Delete(ctx, name, options)
|
|
178
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
package runtime
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
9
|
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
10
|
+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
11
|
+
"k8s.io/apimachinery/pkg/runtime"
|
|
12
|
+
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
13
|
+
"k8s.io/client-go/dynamic/fake"
|
|
14
|
+
ktesting "k8s.io/client-go/testing"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func TestManifestDeleteFencesUIDAndWaitsForFinalizers(t *testing.T) {
|
|
18
|
+
for _, mode := range []string{"accepted", "replaced", "deleting", "external"} {
|
|
19
|
+
t.Run(mode, func(t *testing.T) {
|
|
20
|
+
live := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "workloads.soha.io/v1alpha1", "kind": "WorkloadCronJob", "metadata": map[string]any{"name": "periodic", "namespace": "demo", "uid": "root-uid", "resourceVersion": "12"}}}
|
|
21
|
+
live.SetFinalizers([]string{"operator.example/hold"})
|
|
22
|
+
if mode == "deleting" {
|
|
23
|
+
now := metav1.Now()
|
|
24
|
+
live.SetDeletionTimestamp(&now)
|
|
25
|
+
}
|
|
26
|
+
if mode == "external" {
|
|
27
|
+
live.SetLabels(map[string]string{"argocd.argoproj.io/instance": "external"})
|
|
28
|
+
}
|
|
29
|
+
client := fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
|
|
30
|
+
writes := 0
|
|
31
|
+
client.PrependReactor("delete", "workloadcronjobs", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
32
|
+
writes++
|
|
33
|
+
deleteAction, ok := action.(ktesting.DeleteAction)
|
|
34
|
+
if !ok {
|
|
35
|
+
t.Fatal("unexpected delete action")
|
|
36
|
+
}
|
|
37
|
+
options := deleteAction.GetDeleteOptions()
|
|
38
|
+
if options.Preconditions == nil || *options.Preconditions.UID != live.GetUID() || *options.Preconditions.ResourceVersion != "12" || options.PropagationPolicy == nil || *options.PropagationPolicy != metav1.DeletePropagationForeground {
|
|
39
|
+
t.Fatalf("missing delete fences: %+v", options)
|
|
40
|
+
}
|
|
41
|
+
return true, nil, nil
|
|
42
|
+
})
|
|
43
|
+
uid := "root-uid"
|
|
44
|
+
if mode == "replaced" {
|
|
45
|
+
uid = "previous-uid"
|
|
46
|
+
}
|
|
47
|
+
resources := client.Resource(schema.GroupVersionResource{Group: "workloads.soha.io", Version: "v1alpha1", Resource: "workloadcronjobs"}).Namespace("demo")
|
|
48
|
+
err := DeleteManifest(context.Background(), resources, "periodic", uid)
|
|
49
|
+
if mode == "replaced" && !apierrors.IsConflict(err) || mode == "external" && err == nil || (mode == "accepted" || mode == "deleting") && err != nil {
|
|
50
|
+
t.Fatalf("delete: %v", err)
|
|
51
|
+
}
|
|
52
|
+
if (mode == "accepted" && writes != 1) || (mode != "accepted" && writes != 0) {
|
|
53
|
+
t.Fatalf("delete calls: %d", writes)
|
|
54
|
+
}
|
|
55
|
+
for _, action := range client.Actions() {
|
|
56
|
+
if action.GetVerb() != "get" && action.GetVerb() != "delete" {
|
|
57
|
+
t.Fatalf("unexpected finalizer write: %s", action.GetVerb())
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func TestManifestUpdatePreservesCallerVersion(t *testing.T) {
|
|
65
|
+
live := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "settings", "namespace": "demo", "uid": "current", "resourceVersion": "12"}}}
|
|
66
|
+
client := fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
|
|
67
|
+
desired := live.DeepCopy()
|
|
68
|
+
desired.SetUID("previous")
|
|
69
|
+
desired.SetResourceVersion("11")
|
|
70
|
+
client.PrependReactor("update", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
71
|
+
update, ok := action.(ktesting.UpdateAction)
|
|
72
|
+
if !ok {
|
|
73
|
+
t.Fatal("unexpected update action")
|
|
74
|
+
}
|
|
75
|
+
write, ok := update.GetObject().(*unstructured.Unstructured)
|
|
76
|
+
if !ok {
|
|
77
|
+
t.Fatal("unexpected update object")
|
|
78
|
+
}
|
|
79
|
+
if write.GetUID() != "previous" || write.GetResourceVersion() != "11" {
|
|
80
|
+
t.Fatal("caller version was overwritten")
|
|
81
|
+
}
|
|
82
|
+
return true, nil, apierrors.NewConflict(schema.GroupResource{Resource: "configmaps"}, "settings", nil)
|
|
83
|
+
})
|
|
84
|
+
_, err := UpdateManifest(context.Background(), client.Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}).Namespace("demo"), desired)
|
|
85
|
+
if !apierrors.IsConflict(err) {
|
|
86
|
+
t.Fatalf("update: %v", err)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestDirectManifestRequiresOneWriter(t *testing.T) {
|
|
91
|
+
for _, test := range []struct {
|
|
92
|
+
name string
|
|
93
|
+
change func(*unstructured.Unstructured)
|
|
94
|
+
}{
|
|
95
|
+
{"controller", func(object *unstructured.Unstructured) {
|
|
96
|
+
yes := true
|
|
97
|
+
object.SetOwnerReferences([]metav1.OwnerReference{{Controller: &yes, UID: "owner"}})
|
|
98
|
+
}},
|
|
99
|
+
{"deleting", func(object *unstructured.Unstructured) { now := metav1.Now(); object.SetDeletionTimestamp(&now) }},
|
|
100
|
+
{"Helm", func(object *unstructured.Unstructured) {
|
|
101
|
+
object.SetAnnotations(map[string]string{"meta.helm.sh/release-name": "external"})
|
|
102
|
+
}},
|
|
103
|
+
{"Argo CD", func(object *unstructured.Unstructured) {
|
|
104
|
+
object.SetAnnotations(map[string]string{"argocd.argoproj.io/tracking-id": "external"})
|
|
105
|
+
}},
|
|
106
|
+
{"Flux", func(object *unstructured.Unstructured) {
|
|
107
|
+
object.SetLabels(map[string]string{"kustomize.toolkit.fluxcd.io/name": "external"})
|
|
108
|
+
}},
|
|
109
|
+
{"GitOps manager", func(object *unstructured.Unstructured) {
|
|
110
|
+
object.SetManagedFields([]metav1.ManagedFieldsEntry{{Manager: "argocd-controller"}})
|
|
111
|
+
}},
|
|
112
|
+
} {
|
|
113
|
+
t.Run(test.name, func(t *testing.T) {
|
|
114
|
+
object := &unstructured.Unstructured{Object: map[string]any{}}
|
|
115
|
+
if err := ValidateDirectManifestOwner(object); err != nil {
|
|
116
|
+
t.Fatal(err)
|
|
117
|
+
}
|
|
118
|
+
test.change(object)
|
|
119
|
+
if err := ValidateDirectManifestOwner(object); err == nil {
|
|
120
|
+
t.Fatal("external ownership was accepted")
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func TestManifestWritePreservesDryRunAndIdentity(t *testing.T) {
|
|
127
|
+
for _, exists := range []bool{false, true} {
|
|
128
|
+
t.Run(map[bool]string{false: "create", true: "apply"}[exists], func(t *testing.T) {
|
|
129
|
+
desired := &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{"name": "settings", "namespace": "demo"}}}
|
|
130
|
+
client := fake.NewSimpleDynamicClient(runtime.NewScheme())
|
|
131
|
+
verb := "create"
|
|
132
|
+
if exists {
|
|
133
|
+
live := desired.DeepCopy()
|
|
134
|
+
live.SetUID("settings-uid")
|
|
135
|
+
live.SetResourceVersion("12")
|
|
136
|
+
client = fake.NewSimpleDynamicClient(runtime.NewScheme(), live)
|
|
137
|
+
verb = "patch"
|
|
138
|
+
}
|
|
139
|
+
writes := 0
|
|
140
|
+
client.PrependReactor(verb, "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) {
|
|
141
|
+
writes++
|
|
142
|
+
if exists {
|
|
143
|
+
patch, ok := action.(ktesting.PatchAction)
|
|
144
|
+
if !ok {
|
|
145
|
+
t.Fatal("unexpected patch action")
|
|
146
|
+
}
|
|
147
|
+
var body unstructured.Unstructured
|
|
148
|
+
if err := json.Unmarshal(patch.GetPatch(), &body); err != nil {
|
|
149
|
+
t.Fatal(err)
|
|
150
|
+
}
|
|
151
|
+
if body.GetUID() != "settings-uid" || body.GetResourceVersion() != "12" {
|
|
152
|
+
t.Fatal("ownership read was not fenced")
|
|
153
|
+
}
|
|
154
|
+
patchOptions, ok := action.(interface{ GetPatchOptions() metav1.PatchOptions })
|
|
155
|
+
if !ok {
|
|
156
|
+
t.Fatal("unexpected patch options")
|
|
157
|
+
}
|
|
158
|
+
if len(patchOptions.GetPatchOptions().DryRun) != 1 {
|
|
159
|
+
t.Fatal("patch lost dry-run")
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
createOptions, ok := action.(interface{ GetCreateOptions() metav1.CreateOptions })
|
|
163
|
+
if !ok {
|
|
164
|
+
t.Fatal("unexpected create options")
|
|
165
|
+
}
|
|
166
|
+
if len(createOptions.GetCreateOptions().DryRun) != 1 {
|
|
167
|
+
t.Fatal("create lost dry-run")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return true, desired.DeepCopy(), nil
|
|
171
|
+
})
|
|
172
|
+
_, err := ApplyManifest(context.Background(), client.Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}).Namespace("demo"), desired, metav1.PatchOptions{FieldManager: "soha-test", DryRun: []string{metav1.DryRunAll}})
|
|
173
|
+
if err != nil || writes != 1 || desired.GetUID() != "" || desired.GetResourceVersion() != "" {
|
|
174
|
+
t.Fatalf("write = %d, %v; desired = %#v", writes, err, desired)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|