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
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func TestDryRunLaunch_PreparationFails(t *testing.T) {
t.Errorf("expected preparation error, got %v", res.PreparationError)
}

if res.PreparationReasonCode == nil || *res.PreparationReasonCode != "ERR_PREPARATION_FAILED" {
if res.PreparationReasonCode == nil || *res.PreparationReasonCode != schema.ReasonErrPreparationFailed {
t.Errorf("expected preparation reason code, got %v", res.PreparationReasonCode)
}
}
13 changes: 7 additions & 6 deletions schedune-control-plane/internal/domain/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/TechnologyTailors/Schedune/schedune-control-plane/internal/domain/lifecycle"
"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"
"github.com/google/uuid"
)
Expand Down Expand Up @@ -78,15 +79,15 @@ func (o *LaunchOrchestrator) validateAndRecord(spec launch.LaunchSpec, rec *laun
node, err := o.nodeStore.GetNode(spec.NodeID)
if err != nil {
lifecycle.TransitionTo(rec, launch.StatePreparing, "", "Initializing preparation")
lifecycle.TransitionTo(rec, launch.StateFailed, "ERR_NODE_NOT_FOUND", "Target node not found in store")
lifecycle.TransitionTo(rec, launch.StateFailed, schema.ReasonErrNodeNotFound, "Target node not found in store")
o.save(rec)
return false
}

valRes := ValidateLaunch(spec, node)
if !valRes.IsValid {
lifecycle.TransitionTo(rec, launch.StatePreparing, "", "Initializing preparation")
lifecycle.TransitionTo(rec, launch.StateFailed, "ERR_VALIDATION_FAILED", fmt.Sprintf("Node missing prerequisites: %v", valRes.BlockingReasonCodes))
lifecycle.TransitionTo(rec, launch.StateFailed, schema.ReasonErrValidationFailed, fmt.Sprintf("Node missing prerequisites: %v", valRes.BlockingReasonCodes))
o.save(rec)
return false
}
Expand All @@ -106,14 +107,14 @@ func (o *LaunchOrchestrator) prepareAndRecord(spec launch.LaunchSpec, rec *launc

exec, err := o.resolver.Resolve(selectedBackend)
if err != nil {
lifecycle.TransitionTo(rec, launch.StateFailed, "ERR_PREPARATION_FAILED", fmt.Sprintf("Failed to resolve executor for backend %s: %v", selectedBackend, err))
lifecycle.TransitionTo(rec, launch.StateFailed, schema.ReasonErrPreparationFailed, fmt.Sprintf("Failed to resolve executor for backend %s: %v", selectedBackend, err))
o.save(rec)
return nil, false
}

prep, err := exec.Prepare(spec)
if err != nil {
lifecycle.TransitionTo(rec, launch.StateFailed, "ERR_PREPARATION_FAILED", err.Error())
lifecycle.TransitionTo(rec, launch.StateFailed, schema.ReasonErrPreparationFailed, err.Error())
o.save(rec)
return nil, false
}
Expand All @@ -137,7 +138,7 @@ func (o *LaunchOrchestrator) spawnAndRecord(spec launch.LaunchSpec, rec *launch.

pid, err := exec.Execute(*rec.PreparedState)
if err != nil {
lifecycle.TransitionTo(rec, launch.StateFailed, "ERR_EXEC_RUNTIME_SPAWN_FAILED", err.Error())
lifecycle.TransitionTo(rec, launch.StateFailed, schema.ReasonErrExecRuntimeSpawnFailed, err.Error())
o.save(rec)
return
}
Expand Down Expand Up @@ -175,7 +176,7 @@ func (o *LaunchOrchestrator) TerminateLaunch(executionID string) (launch.LaunchE
}
if err != nil {
// Do not jump to Failed immediately; let reconcile loop handle stubborn processes
lifecycle.AppendTrace(&rec, "Termination", "Failed", "ERR_TERM_SIGNAL_FAILED", err.Error())
lifecycle.AppendTrace(&rec, "Termination", "Failed", schema.ReasonErrTermSignalFailed, err.Error())
o.save(&rec)
return rec, err
}
Expand Down
3 changes: 2 additions & 1 deletion schedune-control-plane/internal/domain/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"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"
"testing"
"time"
Expand Down Expand Up @@ -145,7 +146,7 @@ func TestLaunchOrchestrator_ValidationFails(t *testing.T) {

hasValFailed := false
for _, tr := range rec.Trace {
if tr.Stage == "StateTransition" && tr.ReasonCode == "ERR_VALIDATION_FAILED" {
if tr.Stage == "StateTransition" && tr.ReasonCode == schema.ReasonErrValidationFailed {
hasValFailed = true
}
}
Expand Down
27 changes: 14 additions & 13 deletions schedune-control-plane/internal/domain/runtime_select.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package domain

import (
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema"
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema/launch"
)

Expand Down Expand Up @@ -77,7 +78,7 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if kvmExists {
capPtr = &kvmCap
}
reject("firecracker", "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU", &capName, capPtr)
reject("firecracker", schema.ReasonErrLaunchMissingCapabilityKvmQemu, &capName, capPtr)
return "", evidence, rejected
}

Expand All @@ -88,7 +89,7 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if binExists {
capPtr = &binCap
}
reject("firecracker", "ERR_LAUNCH_MISSING_CAPABILITY_FC_BINARY", &capName, capPtr)
reject("firecracker", schema.ReasonErrLaunchMissingCapabilityFcBinary, &capName, capPtr)
return "", evidence, rejected
}

Expand All @@ -99,7 +100,7 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if tunExists {
capPtr = &tunCap
}
reject("firecracker", "ERR_LAUNCH_MISSING_CAPABILITY_FC_TUN", &capName, capPtr)
reject("firecracker", schema.ReasonErrLaunchMissingCapabilityFcTun, &capName, capPtr)
return "", evidence, rejected
}

Expand All @@ -110,18 +111,18 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if cgExists {
capPtr = &cgCap
}
reject("firecracker", "ERR_LAUNCH_MISSING_CAPABILITY_FC_CGROUPS", &capName, capPtr)
reject("firecracker", schema.ReasonErrLaunchMissingCapabilityFcCgroups, &capName, capPtr)
return "", evidence, rejected
}

// Artifact validation
if len(storage) == 0 {
reject("firecracker", "ERR_LAUNCH_INVALID_FIRECRACKER_ARTIFACT_MODEL", nil, nil)
reject("firecracker", schema.ReasonErrLaunchInvalidFirecrackerArtifactModel, nil, nil)
return "", evidence, rejected
}
for _, s := range storage {
if s.Format == "qcow2" {
reject("firecracker", "ERR_LAUNCH_INVALID_STORAGE_FORMAT", nil, nil)
reject("firecracker", schema.ReasonErrLaunchInvalidStorageFormat, nil, nil)
return "", evidence, rejected
}
}
Expand All @@ -144,7 +145,7 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
}
}
} else {
reject(spec.RuntimeBackendPreference, "ERR_LAUNCH_BACKEND_NOT_SUPPORTED", nil, nil)
reject(spec.RuntimeBackendPreference, schema.ReasonErrLaunchBackendNotSupported, nil, nil)
return "", evidence, rejected
}
}
Expand All @@ -158,7 +159,7 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if kvmExists {
capPtr = &kvmCap
}
reject("cloud_hypervisor", "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU", &capName, capPtr)
reject("cloud_hypervisor", schema.ReasonErrLaunchMissingCapabilityKvmQemu, &capName, capPtr)
continue
}

Expand All @@ -169,12 +170,12 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if binExists {
capPtr = &binCap
}
reject("cloud_hypervisor", "ERR_LAUNCH_MISSING_CAPABILITY_CH_BINARY", &capName, capPtr)
reject("cloud_hypervisor", schema.ReasonErrLaunchMissingCapabilityChBinary, &capName, capPtr)
continue
}

if len(storage) == 0 {
reject("cloud_hypervisor", "ERR_LAUNCH_MISSING_ARTIFACT", nil, nil)
reject("cloud_hypervisor", schema.ReasonErrLaunchMissingArtifact, nil, nil)
continue
}
return "cloud_hypervisor", evidence, rejected
Expand All @@ -188,7 +189,7 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if exists {
capPtr = &cap
}
reject("kvm_qemu", "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU", &capName, capPtr)
reject("kvm_qemu", schema.ReasonErrLaunchMissingCapabilityKvmQemu, &capName, capPtr)
continue
}

Expand All @@ -199,12 +200,12 @@ func SelectBackend(spec launch.LaunchSpec, node NodeRecord) (string, []launch.Ba
if binExists {
capPtr = &binCap
}
reject("kvm_qemu", "ERR_LAUNCH_MISSING_CAPABILITY_QEMU_BINARY", &capName, capPtr)
reject("kvm_qemu", schema.ReasonErrLaunchMissingCapabilityQemuBinary, &capName, capPtr)
continue
}

if len(storage) == 0 {
reject("kvm_qemu", "ERR_LAUNCH_MISSING_ARTIFACT", nil, nil)
reject("kvm_qemu", schema.ReasonErrLaunchMissingArtifact, nil, nil)
continue
}
return "kvm_qemu", evidence, rejected
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package domain

import (
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema"
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema/launch"
"testing"
"time"
Expand Down Expand Up @@ -34,7 +35,7 @@ func TestSelectBackend_MicroVM(t *testing.T) {
if backend != "" {
t.Errorf("expected rejection due to missing artifact, got %s", backend)
}
if reason := rejected["firecracker"]; reason != "ERR_LAUNCH_INVALID_FIRECRACKER_ARTIFACT_MODEL" {
if reason := rejected["firecracker"]; reason != schema.ReasonErrLaunchInvalidFirecrackerArtifactModel {
t.Errorf("expected ERR_LAUNCH_INVALID_FIRECRACKER_ARTIFACT_MODEL, got %s", reason)
}
}
Expand Down
47 changes: 24 additions & 23 deletions schedune-control-plane/internal/domain/validate_launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema"
"github.com/TechnologyTailors/Schedune/schedune-control-plane/pkg/schema/launch"
)

Expand All @@ -23,12 +24,12 @@ func ValidateLaunch(spec launch.LaunchSpec, node NodeRecord) launch.LaunchValida
// 1. Layer 1: Generic launch checks
if spec.Architecture != node.Identity.Architecture {
result.IsValid = false
result.BlockingReasonCodes = append(result.BlockingReasonCodes, "ERR_LAUNCH_ARCH_MISMATCH")
result.BlockingReasonCodes = append(result.BlockingReasonCodes, schema.ReasonErrLaunchArchMismatch)
result.ValidationTrace = append(result.ValidationTrace, "Failed: Architecture mismatch. Node is "+node.Identity.Architecture)
}

if spec.ImageReference != "" || spec.KernelImagePath != "" {
result.Warnings = append(result.Warnings, "WARN_DEPRECATED_IMAGE_REFERENCE")
result.Warnings = append(result.Warnings, schema.ReasonWarnDeprecatedImageReference)
if len(spec.Storage) > 0 {
result.ValidationTrace = append(result.ValidationTrace, "Warning: Both typed Storage and legacy ImageReference provided. Typed Storage takes precedence.")
} else {
Expand All @@ -37,17 +38,17 @@ func ValidateLaunch(spec launch.LaunchSpec, node NodeRecord) launch.LaunchValida
}

if len(spec.NetworkAttachments) > 0 {
result.Warnings = append(result.Warnings, "WARN_DEPRECATED_NETWORK_ATTACHMENTS")
result.Warnings = append(result.Warnings, schema.ReasonWarnDeprecatedNetworkAttachments)
result.ValidationTrace = append(result.ValidationTrace, "Warning: Using deprecated NetworkAttachments field. Please migrate to typed Networks array.")
}

// Security Context Validation
if spec.Security != nil {
if spec.Security.SeccompProfile != "" {
cap, exists := node.Capabilities["kernel_seccomp_supported"]
if !exists || cap.State != "Supported" || cap.IsStale {
if !exists || cap.State != schema.CapabilityStateSupported || cap.IsStale {
result.IsValid = false
result.BlockingReasonCodes = append(result.BlockingReasonCodes, "ERR_LAUNCH_MISSING_CAPABILITY_SECCOMP")
result.BlockingReasonCodes = append(result.BlockingReasonCodes, schema.ReasonErrLaunchMissingCapabilitySeccomp)
result.ValidationTrace = append(result.ValidationTrace, "Failed: Security Context requires seccomp profile, but kernel seccomp is not supported or missing.")
} else {
result.ValidationTrace = append(result.ValidationTrace, "Passed: Node supports kernel seccomp for requested profile.")
Expand All @@ -69,7 +70,7 @@ func ValidateLaunch(spec launch.LaunchSpec, node NodeRecord) launch.LaunchValida

if selectedBackend == "" {
result.IsValid = false
result.BlockingReasonCodes = append(result.BlockingReasonCodes, "ERR_LAUNCH_BACKEND_NOT_SUPPORTED")
result.BlockingReasonCodes = append(result.BlockingReasonCodes, schema.ReasonErrLaunchBackendNotSupported)
result.ValidationTrace = append(result.ValidationTrace, "Failed: No supported runtime backend found for spec.")
for backend, reason := range rejectedBackends {
result.ValidationTrace = append(result.ValidationTrace, fmt.Sprintf("Backend %s rejected: %s", backend, reason))
Expand All @@ -84,11 +85,11 @@ func ValidateLaunch(spec launch.LaunchSpec, node NodeRecord) launch.LaunchValida

result.ExplainabilityText = "Node is fully capable of executing this launch spec."

if selectedBackend == "kvm_qemu" {
if selectedBackend == schema.BackendKvmQemu {
result.RecommendedRuntime = "qemu-system-" + spec.Architecture
} else if selectedBackend == "cloud_hypervisor" {
} else if selectedBackend == schema.BackendCloudHypervisor {
result.RecommendedRuntime = "cloud-hypervisor"
} else if selectedBackend == "firecracker" {
} else if selectedBackend == schema.BackendFirecracker {
result.RecommendedRuntime = "firecracker"
}

Expand All @@ -104,49 +105,49 @@ func generateRemediationHints(result launch.LaunchValidationResult) map[string]s
hints := make(map[string]string)

for _, code := range result.BlockingReasonCodes {
if code == "ERR_LAUNCH_ARCH_MISMATCH" {
if code == schema.ReasonErrLaunchArchMismatch {
hints["architecture"] = "Ensure the requested architecture matches the node architecture."
}
if code == "ERR_LAUNCH_BACKEND_NOT_SUPPORTED" {
if code == schema.ReasonErrLaunchBackendNotSupported {
hints["backend"] = "Check the RejectedBackends map for specific missing capabilities."
}
if code == "ERR_LAUNCH_MISSING_CAPABILITY_SECCOMP" {
if code == schema.ReasonErrLaunchMissingCapabilitySeccomp {
hints["kernel_seccomp"] = "Ensure the host kernel is compiled with CONFIG_SECCOMP and actions_avail is readable."
}
}

for backend, reason := range result.RejectedBackends {
if backend == "kvm_qemu" && strings.Contains(reason, "CAP_QEMU_BINARY_MISSING") {
if backend == schema.BackendKvmQemu && strings.Contains(reason, schema.ReasonCapQemuBinaryMissing) {
hints["kvm_qemu_binary"] = "Install qemu-system-x86_64 or qemu-system-aarch64 on the host."
}
if backend == "kvm_qemu" && strings.Contains(reason, "CAP_KVM_MISSING") {
if backend == schema.BackendKvmQemu && strings.Contains(reason, schema.ReasonCapKvmMissing) {
hints["kvm_qemu_kvm"] = "Enable KVM in BIOS or load kvm kernel modules."
}
if backend == "kvm_qemu" && strings.Contains(reason, "CAP_KVM_NOT_OPENABLE_PERMS") {
if backend == schema.BackendKvmQemu && strings.Contains(reason, schema.ReasonCapKvmNotOpenablePerms) {
hints["kvm_qemu_perms"] = "Ensure the Schedune agent has rw permissions to /dev/kvm."
}
if backend == "cloud_hypervisor" && strings.Contains(reason, "CAP_CLOUDHYPERVISOR_BINARY_MISSING") {
if backend == schema.BackendCloudHypervisor && strings.Contains(reason, schema.ReasonCapCloudHypervisorBinaryMissing) {
hints["cloud_hypervisor_binary"] = "Install cloud-hypervisor binary on the host."
}
if backend == "cloud_hypervisor" && strings.Contains(reason, "CAP_KVM_MISSING") {
if backend == schema.BackendCloudHypervisor && strings.Contains(reason, schema.ReasonCapKvmMissing) {
hints["cloud_hypervisor_kvm"] = "Enable KVM in BIOS or load kvm kernel modules."
}
if backend == "firecracker" && strings.Contains(reason, "CAP_FIRECRACKER_BINARY_MISSING") {
if backend == schema.BackendFirecracker && strings.Contains(reason, schema.ReasonCapFirecrackerBinaryMissing) {
hints["firecracker_binary"] = "Install firecracker binary on the host."
}
if backend == "firecracker" && strings.Contains(reason, "CAP_FIRECRACKER_TUN_MISSING") {
if backend == schema.BackendFirecracker && strings.Contains(reason, schema.ReasonCapFirecrackerTunMissing) {
hints["firecracker_tun"] = "Ensure the tun kernel module is loaded (/dev/net/tun)."
}
if backend == "firecracker" && strings.Contains(reason, "CAP_FIRECRACKER_CGROUPS_MISSING") {
if backend == schema.BackendFirecracker && strings.Contains(reason, schema.ReasonCapFirecrackerCgroupsMissing) {
hints["firecracker_cgroups"] = "Ensure cgroups v2 are mounted on the host."
}
if backend == "firecracker" && strings.Contains(reason, "CAP_KVM_MISSING") {
if backend == schema.BackendFirecracker && strings.Contains(reason, schema.ReasonCapKvmMissing) {
hints["firecracker_kvm"] = "Enable KVM in BIOS or load kvm kernel modules."
}
if strings.Contains(reason, "ERR_LAUNCH_MISSING_ARTIFACT") {
if strings.Contains(reason, schema.ReasonErrLaunchMissingArtifact) {
hints["artifact_missing"] = "Ensure ImageReference is provided for the workload."
}
if strings.Contains(reason, "ERR_LAUNCH_INVALID_FIRECRACKER_ARTIFACT_MODEL") {
if strings.Contains(reason, schema.ReasonErrLaunchInvalidFirecrackerArtifactModel) {
hints["firecracker_artifact"] = "Ensure KernelImagePath and RootfsPath are provided for MicroVMs."
}
}
Expand Down
Loading
Loading