From d911cc2e98388b7fd91a7249da27f0ab903cbb5d Mon Sep 17 00:00:00 2001 From: InderdeepBajwa Date: Sat, 25 Apr 2026 20:57:44 -0400 Subject: [PATCH] feat(data-plane): add Firecracker Execute validation gate This commit completes the Firecracker Execute validation gate for Data Plane V0. It correctly sets the launch as invalid with ERR_LAUNCH_BACKEND_EXECUTION_UNSUPPORTED if ValidateLaunch selects firecracker and LaunchMode is Execute. It also provides a stable reason code, validation trace, and remediation hints, while Firecracker Validate and DryRun remain valid. Tests are updated to assert these conditions. --- .../internal/domain/orchestrator_test.go | 50 ++++++++++++ .../internal/domain/validate_launch.go | 12 +++ .../internal/domain/validate_launch_test.go | 81 +++++++++++++++++++ .../pkg/schema/constants.go | 1 + 4 files changed, 144 insertions(+) diff --git a/schedune-control-plane/internal/domain/orchestrator_test.go b/schedune-control-plane/internal/domain/orchestrator_test.go index 22c5d4b..26c7673 100644 --- a/schedune-control-plane/internal/domain/orchestrator_test.go +++ b/schedune-control-plane/internal/domain/orchestrator_test.go @@ -3,6 +3,7 @@ package domain import ( "context" "errors" + "strings" "github.com/TechnologyTailors/Schedune/schedune-control-plane/internal/runtime" "github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema" "github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema/launch" @@ -154,3 +155,52 @@ func TestLaunchOrchestrator_ValidationFails(t *testing.T) { t.Errorf("expected trace to have HostPreflight Failed, got %v", rec.Trace) } } + +func TestLaunchOrchestrator_FirecrackerExecuteFailsValidation(t *testing.T) { + env := readFixture(t, "firecracker_host_ready.json") + now := time.Now().Unix() + for i := range env.Capabilities { + env.Capabilities[i].ObservedAtSec = now + staleAfter := now + 300 + env.Capabilities[i].StaleAfterSec = &staleAfter + } + node := ProjectEnvelope(env) + + store := &MockStore{ + node: node, + exec: make(map[string]launch.LaunchExecutionRecord), + } + exec := &MockExecutor{} + + orch := NewLaunchOrchestrator(store, store, exec) + + spec := launch.LaunchSpec{ + SchemaVersion: "v1alpha1", + WorkloadID: "wl-test-fc-exec", + TenantID: "tenant-test", + NodeID: node.ID, + RuntimeClass: "MicroVM", + Architecture: "x86_64", + Vcpu: 2, + MemoryMB: 1024, + LaunchMode: "Execute", + KernelImagePath: "/tmp/kernel.bin", + RootfsPath: "/tmp/rootfs.ext4", + } + + rec := orch.StartLaunch(spec) + + if rec.State != launch.StateFailed { + t.Errorf("expected state %s, got %s", launch.StateFailed, rec.State) + } + + hasBackendExecutionUnsupported := false + for _, tr := range rec.Trace { + if tr.Stage == "StateTransition" && tr.ReasonCode == schema.ReasonErrValidationFailed && strings.Contains(tr.Message, schema.ReasonErrLaunchBackendExecutionUnsupported) { + hasBackendExecutionUnsupported = true + } + } + if !hasBackendExecutionUnsupported { + t.Errorf("expected trace to have %s in validation failure message, got %v", schema.ReasonErrLaunchBackendExecutionUnsupported, rec.Trace) + } +} diff --git a/schedune-control-plane/internal/domain/validate_launch.go b/schedune-control-plane/internal/domain/validate_launch.go index 6d6892e..c749ed0 100644 --- a/schedune-control-plane/internal/domain/validate_launch.go +++ b/schedune-control-plane/internal/domain/validate_launch.go @@ -80,6 +80,15 @@ func ValidateLaunch(spec launch.LaunchSpec, node NodeRecord) launch.LaunchValida return result } + if selectedBackend == schema.BackendFirecracker && spec.LaunchMode == "Execute" { + result.IsValid = false + result.BlockingReasonCodes = append(result.BlockingReasonCodes, schema.ReasonErrLaunchBackendExecutionUnsupported) + result.ValidationTrace = append(result.ValidationTrace, "Failed: Firecracker execution is not implemented in Data Plane V0. Only Validate and DryRun are supported.") + result.ExplainabilityText = "Node cannot launch workload due to Data Plane limitations." + result.RemediationHints = generateRemediationHints(result) + return result + } + // 3. Layer 4: Setup context for preparation phase validation result.ValidationTrace = append(result.ValidationTrace, "Passed: Selected backend "+selectedBackend) if spec.RuntimeVersion != nil && (spec.RuntimeVersion.MinimumVersion != "" || spec.RuntimeVersion.ExactVersion != "") { @@ -114,6 +123,9 @@ func generateRemediationHints(result launch.LaunchValidationResult) map[string]s if code == schema.ReasonErrLaunchBackendNotSupported { hints["backend"] = "Check the RejectedBackends map for specific missing capabilities." } + if code == schema.ReasonErrLaunchBackendExecutionUnsupported { + hints["launch_mode"] = "Firecracker execution is not supported in Data Plane V0. Use Validate or DryRun, or select a different backend." + } if code == schema.ReasonErrLaunchMissingCapabilitySeccomp { hints["kernel_seccomp"] = "Ensure the host kernel is compiled with CONFIG_SECCOMP and actions_avail is readable." } diff --git a/schedune-control-plane/internal/domain/validate_launch_test.go b/schedune-control-plane/internal/domain/validate_launch_test.go index 636decc..0d992b9 100644 --- a/schedune-control-plane/internal/domain/validate_launch_test.go +++ b/schedune-control-plane/internal/domain/validate_launch_test.go @@ -731,3 +731,84 @@ func TestValidateLaunch_RuntimeVersionMismatch(t *testing.T) { t.Errorf("expected structured evidence for runtime version mismatch, got %+v", result.BackendRejectionEvidence) } } + +func TestValidateLaunch_FirecrackerExecuteUnsupported(t *testing.T) { + env := readFixture(t, "firecracker_host_ready.json") + now := time.Now().Unix() + env.TimestampSec = now + for i := range env.Capabilities { + env.Capabilities[i].ObservedAtSec = now + staleAfter := now + 300 + env.Capabilities[i].StaleAfterSec = &staleAfter + } + node := ProjectEnvelope(env) + + spec := launch.LaunchSpec{ + SchemaVersion: "v1alpha1", + WorkloadID: "wl-launch-fc-execute", + TenantID: "tenant-1", + NodeID: node.ID, + RuntimeClass: "MicroVM", + Architecture: "x86_64", + Vcpu: 2, + MemoryMB: 1024, + LaunchMode: "Execute", + KernelImagePath: "/tmp/kernel.bin", + RootfsPath: "/tmp/rootfs.ext4", + } + + result := ValidateLaunch(spec, node) + if result.IsValid { + t.Errorf("expected launch to be invalid due to Execute mode for Firecracker") + } + + hasBlocker := false + for _, code := range result.BlockingReasonCodes { + if code == schema.ReasonErrLaunchBackendExecutionUnsupported { + hasBlocker = true + } + } + + if !hasBlocker { + t.Errorf("expected ERR_LAUNCH_BACKEND_EXECUTION_UNSUPPORTED blocker, got %v", result.BlockingReasonCodes) + } + + if result.SelectedBackend != schema.BackendFirecracker { + t.Errorf("expected SelectedBackend to be firecracker, got %s", result.SelectedBackend) + } +} + +func TestValidateLaunch_FirecrackerDryRunSupported(t *testing.T) { + env := readFixture(t, "firecracker_host_ready.json") + now := time.Now().Unix() + env.TimestampSec = now + for i := range env.Capabilities { + env.Capabilities[i].ObservedAtSec = now + staleAfter := now + 300 + env.Capabilities[i].StaleAfterSec = &staleAfter + } + node := ProjectEnvelope(env) + + spec := launch.LaunchSpec{ + SchemaVersion: "v1alpha1", + WorkloadID: "wl-launch-fc-dryrun", + TenantID: "tenant-1", + NodeID: node.ID, + RuntimeClass: "MicroVM", + Architecture: "x86_64", + Vcpu: 2, + MemoryMB: 1024, + LaunchMode: "DryRun", + KernelImagePath: "/tmp/kernel.bin", + RootfsPath: "/tmp/rootfs.ext4", + } + + result := ValidateLaunch(spec, node) + if !result.IsValid { + t.Errorf("expected launch to be valid for Firecracker DryRun, got blockers: %v", result.BlockingReasonCodes) + } + + if result.SelectedBackend != schema.BackendFirecracker { + t.Errorf("expected SelectedBackend to be firecracker, got %s", result.SelectedBackend) + } +} diff --git a/schedune-control-plane/pkg/schema/constants.go b/schedune-control-plane/pkg/schema/constants.go index e729a1b..3b6717a 100644 --- a/schedune-control-plane/pkg/schema/constants.go +++ b/schedune-control-plane/pkg/schema/constants.go @@ -86,6 +86,7 @@ const ( // Launch Validation ReasonErrLaunchArchMismatch = "ERR_LAUNCH_ARCH_MISMATCH" ReasonErrLaunchBackendNotSupported = "ERR_LAUNCH_BACKEND_NOT_SUPPORTED" + ReasonErrLaunchBackendExecutionUnsupported = "ERR_LAUNCH_BACKEND_EXECUTION_UNSUPPORTED" ReasonErrLaunchMissingArtifact = "ERR_LAUNCH_MISSING_ARTIFACT" ReasonErrLaunchInvalidStorageFormat = "ERR_LAUNCH_INVALID_STORAGE_FORMAT" ReasonErrLaunchInvalidFirecrackerArtifactModel = "ERR_LAUNCH_INVALID_FIRECRACKER_ARTIFACT_MODEL"