Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions schedune-control-plane/internal/domain/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
12 changes: 12 additions & 0 deletions schedune-control-plane/internal/domain/validate_launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "") {
Expand Down Expand Up @@ -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."
}
Expand Down
81 changes: 81 additions & 0 deletions schedune-control-plane/internal/domain/validate_launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions schedune-control-plane/pkg/schema/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading