Skip to content

Commit 091db5b

Browse files
feat(data-plane): add Firecracker Execute validation gate (#28)
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.
1 parent 4e572ef commit 091db5b

4 files changed

Lines changed: 144 additions & 0 deletions

File tree

schedune-control-plane/internal/domain/orchestrator_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package domain
33
import (
44
"context"
55
"errors"
6+
"strings"
67
"github.com/TechnologyTailors/Schedune/schedune-control-plane/internal/runtime"
78
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema"
89
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema/launch"
@@ -154,3 +155,52 @@ func TestLaunchOrchestrator_ValidationFails(t *testing.T) {
154155
t.Errorf("expected trace to have HostPreflight Failed, got %v", rec.Trace)
155156
}
156157
}
158+
159+
func TestLaunchOrchestrator_FirecrackerExecuteFailsValidation(t *testing.T) {
160+
env := readFixture(t, "firecracker_host_ready.json")
161+
now := time.Now().Unix()
162+
for i := range env.Capabilities {
163+
env.Capabilities[i].ObservedAtSec = now
164+
staleAfter := now + 300
165+
env.Capabilities[i].StaleAfterSec = &staleAfter
166+
}
167+
node := ProjectEnvelope(env)
168+
169+
store := &MockStore{
170+
node: node,
171+
exec: make(map[string]launch.LaunchExecutionRecord),
172+
}
173+
exec := &MockExecutor{}
174+
175+
orch := NewLaunchOrchestrator(store, store, exec)
176+
177+
spec := launch.LaunchSpec{
178+
SchemaVersion: "v1alpha1",
179+
WorkloadID: "wl-test-fc-exec",
180+
TenantID: "tenant-test",
181+
NodeID: node.ID,
182+
RuntimeClass: "MicroVM",
183+
Architecture: "x86_64",
184+
Vcpu: 2,
185+
MemoryMB: 1024,
186+
LaunchMode: "Execute",
187+
KernelImagePath: "/tmp/kernel.bin",
188+
RootfsPath: "/tmp/rootfs.ext4",
189+
}
190+
191+
rec := orch.StartLaunch(spec)
192+
193+
if rec.State != launch.StateFailed {
194+
t.Errorf("expected state %s, got %s", launch.StateFailed, rec.State)
195+
}
196+
197+
hasBackendExecutionUnsupported := false
198+
for _, tr := range rec.Trace {
199+
if tr.Stage == "StateTransition" && tr.ReasonCode == schema.ReasonErrValidationFailed && strings.Contains(tr.Message, schema.ReasonErrLaunchBackendExecutionUnsupported) {
200+
hasBackendExecutionUnsupported = true
201+
}
202+
}
203+
if !hasBackendExecutionUnsupported {
204+
t.Errorf("expected trace to have %s in validation failure message, got %v", schema.ReasonErrLaunchBackendExecutionUnsupported, rec.Trace)
205+
}
206+
}

schedune-control-plane/internal/domain/validate_launch.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ func ValidateLaunch(spec launch.LaunchSpec, node NodeRecord) launch.LaunchValida
8080
return result
8181
}
8282

83+
if selectedBackend == schema.BackendFirecracker && spec.LaunchMode == "Execute" {
84+
result.IsValid = false
85+
result.BlockingReasonCodes = append(result.BlockingReasonCodes, schema.ReasonErrLaunchBackendExecutionUnsupported)
86+
result.ValidationTrace = append(result.ValidationTrace, "Failed: Firecracker execution is not implemented in Data Plane V0. Only Validate and DryRun are supported.")
87+
result.ExplainabilityText = "Node cannot launch workload due to Data Plane limitations."
88+
result.RemediationHints = generateRemediationHints(result)
89+
return result
90+
}
91+
8392
// 3. Layer 4: Setup context for preparation phase validation
8493
result.ValidationTrace = append(result.ValidationTrace, "Passed: Selected backend "+selectedBackend)
8594
if spec.RuntimeVersion != nil && (spec.RuntimeVersion.MinimumVersion != "" || spec.RuntimeVersion.ExactVersion != "") {
@@ -114,6 +123,9 @@ func generateRemediationHints(result launch.LaunchValidationResult) map[string]s
114123
if code == schema.ReasonErrLaunchBackendNotSupported {
115124
hints["backend"] = "Check the RejectedBackends map for specific missing capabilities."
116125
}
126+
if code == schema.ReasonErrLaunchBackendExecutionUnsupported {
127+
hints["launch_mode"] = "Firecracker execution is not supported in Data Plane V0. Use Validate or DryRun, or select a different backend."
128+
}
117129
if code == schema.ReasonErrLaunchMissingCapabilitySeccomp {
118130
hints["kernel_seccomp"] = "Ensure the host kernel is compiled with CONFIG_SECCOMP and actions_avail is readable."
119131
}

schedune-control-plane/internal/domain/validate_launch_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,84 @@ func TestValidateLaunch_RuntimeVersionMismatch(t *testing.T) {
731731
t.Errorf("expected structured evidence for runtime version mismatch, got %+v", result.BackendRejectionEvidence)
732732
}
733733
}
734+
735+
func TestValidateLaunch_FirecrackerExecuteUnsupported(t *testing.T) {
736+
env := readFixture(t, "firecracker_host_ready.json")
737+
now := time.Now().Unix()
738+
env.TimestampSec = now
739+
for i := range env.Capabilities {
740+
env.Capabilities[i].ObservedAtSec = now
741+
staleAfter := now + 300
742+
env.Capabilities[i].StaleAfterSec = &staleAfter
743+
}
744+
node := ProjectEnvelope(env)
745+
746+
spec := launch.LaunchSpec{
747+
SchemaVersion: "v1alpha1",
748+
WorkloadID: "wl-launch-fc-execute",
749+
TenantID: "tenant-1",
750+
NodeID: node.ID,
751+
RuntimeClass: "MicroVM",
752+
Architecture: "x86_64",
753+
Vcpu: 2,
754+
MemoryMB: 1024,
755+
LaunchMode: "Execute",
756+
KernelImagePath: "/tmp/kernel.bin",
757+
RootfsPath: "/tmp/rootfs.ext4",
758+
}
759+
760+
result := ValidateLaunch(spec, node)
761+
if result.IsValid {
762+
t.Errorf("expected launch to be invalid due to Execute mode for Firecracker")
763+
}
764+
765+
hasBlocker := false
766+
for _, code := range result.BlockingReasonCodes {
767+
if code == schema.ReasonErrLaunchBackendExecutionUnsupported {
768+
hasBlocker = true
769+
}
770+
}
771+
772+
if !hasBlocker {
773+
t.Errorf("expected ERR_LAUNCH_BACKEND_EXECUTION_UNSUPPORTED blocker, got %v", result.BlockingReasonCodes)
774+
}
775+
776+
if result.SelectedBackend != schema.BackendFirecracker {
777+
t.Errorf("expected SelectedBackend to be firecracker, got %s", result.SelectedBackend)
778+
}
779+
}
780+
781+
func TestValidateLaunch_FirecrackerDryRunSupported(t *testing.T) {
782+
env := readFixture(t, "firecracker_host_ready.json")
783+
now := time.Now().Unix()
784+
env.TimestampSec = now
785+
for i := range env.Capabilities {
786+
env.Capabilities[i].ObservedAtSec = now
787+
staleAfter := now + 300
788+
env.Capabilities[i].StaleAfterSec = &staleAfter
789+
}
790+
node := ProjectEnvelope(env)
791+
792+
spec := launch.LaunchSpec{
793+
SchemaVersion: "v1alpha1",
794+
WorkloadID: "wl-launch-fc-dryrun",
795+
TenantID: "tenant-1",
796+
NodeID: node.ID,
797+
RuntimeClass: "MicroVM",
798+
Architecture: "x86_64",
799+
Vcpu: 2,
800+
MemoryMB: 1024,
801+
LaunchMode: "DryRun",
802+
KernelImagePath: "/tmp/kernel.bin",
803+
RootfsPath: "/tmp/rootfs.ext4",
804+
}
805+
806+
result := ValidateLaunch(spec, node)
807+
if !result.IsValid {
808+
t.Errorf("expected launch to be valid for Firecracker DryRun, got blockers: %v", result.BlockingReasonCodes)
809+
}
810+
811+
if result.SelectedBackend != schema.BackendFirecracker {
812+
t.Errorf("expected SelectedBackend to be firecracker, got %s", result.SelectedBackend)
813+
}
814+
}

schedune-control-plane/pkg/schema/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ const (
8686
// Launch Validation
8787
ReasonErrLaunchArchMismatch = "ERR_LAUNCH_ARCH_MISMATCH"
8888
ReasonErrLaunchBackendNotSupported = "ERR_LAUNCH_BACKEND_NOT_SUPPORTED"
89+
ReasonErrLaunchBackendExecutionUnsupported = "ERR_LAUNCH_BACKEND_EXECUTION_UNSUPPORTED"
8990
ReasonErrLaunchMissingArtifact = "ERR_LAUNCH_MISSING_ARTIFACT"
9091
ReasonErrLaunchInvalidStorageFormat = "ERR_LAUNCH_INVALID_STORAGE_FORMAT"
9192
ReasonErrLaunchInvalidFirecrackerArtifactModel = "ERR_LAUNCH_INVALID_FIRECRACKER_ARTIFACT_MODEL"

0 commit comments

Comments
 (0)