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
26 changes: 18 additions & 8 deletions schedune-agent/src/fixtures_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ mod tests {
fs::create_dir_all(&path).unwrap();
path.push(name);

let json = serde_json::to_string_pretty(envelope).unwrap();
let json_temp = serde_json::to_string(envelope).unwrap();
let mut cloned: SchedulerEnvelope = serde_json::from_str(&json_temp).unwrap();

let hash = name.bytes().fold(0u64, |acc, b| acc.wrapping_add(b as u64));
cloned.collection_id = format!("11111111-2222-3333-4444-{:012x}", hash);
if cloned.timestamp_sec > 1700000000 {
cloned.timestamp_sec = 1777086729;
}

let mut json = serde_json::to_string_pretty(&cloned).unwrap();
json.push('\n');
fs::write(path, json).unwrap();
}

Expand Down Expand Up @@ -45,7 +55,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Supported,
provenance: Provenance::Observed,
reason_code: Some("KVM_OPENABLE".to_string()),
reason_code: Some("CAP_KVM_OPENABLE".to_string()),
version: None,
observed_at_sec: 1776978000,
stale_after_sec: Some(1776978300),
Expand Down Expand Up @@ -139,7 +149,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Unsupported,
provenance: Provenance::Observed,
reason_code: Some("KVM_MISSING".to_string()),
reason_code: Some("CAP_KVM_MISSING".to_string()),
version: None,
observed_at_sec: 1776978000,
stale_after_sec: Some(1776978300),
Expand Down Expand Up @@ -224,7 +234,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Supported,
provenance: Provenance::Observed,
reason_code: Some("KVM_OPENABLE".to_string()),
reason_code: Some("CAP_KVM_OPENABLE".to_string()),
version: None,
observed_at_sec: 1000000000, // Very old
stale_after_sec: Some(1000000300),
Expand Down Expand Up @@ -301,7 +311,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Supported,
provenance: Provenance::Observed,
reason_code: Some("KVM_OPENABLE".to_string()),
reason_code: Some("CAP_KVM_OPENABLE".to_string()),
version: None,
observed_at_sec: 1776978000,
stale_after_sec: Some(1776978300),
Expand Down Expand Up @@ -373,7 +383,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Unavailable,
provenance: Provenance::Observed,
reason_code: Some("KVM_NOT_OPENABLE_PERMS".to_string()),
reason_code: Some("CAP_KVM_NOT_OPENABLE_PERMS".to_string()),
version: None,
observed_at_sec: 1776978000,
stale_after_sec: Some(1776978300),
Expand Down Expand Up @@ -453,7 +463,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Supported,
provenance: Provenance::Observed,
reason_code: Some("KVM_OPENABLE".to_string()),
reason_code: Some("CAP_KVM_OPENABLE".to_string()),
version: None,
observed_at_sec: 1776978000,
stale_after_sec: Some(1776978300),
Expand Down Expand Up @@ -528,7 +538,7 @@ mod tests {
feature: "kvm_vm_launch".to_string(),
state: SupportState::Unsupported,
provenance: Provenance::Observed,
reason_code: Some("KVM_MISSING".to_string()),
reason_code: Some("CAP_KVM_MISSING".to_string()),
version: None,
observed_at_sec: 1776978000,
stale_after_sec: Some(1776978300),
Expand Down
8 changes: 4 additions & 4 deletions schedune-control-plane/internal/domain/projector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func TestProjectEnvelope_HealthyArmProduction(t *testing.T) {
if cap.State != "Supported" {
t.Errorf("expected 'Supported', got %s", cap.State)
}
if cap.ReasonCode != "KVM_OPENABLE" {
t.Errorf("expected 'KVM_OPENABLE', got %s", cap.ReasonCode)
if cap.ReasonCode != "CAP_KVM_OPENABLE" {
t.Errorf("expected 'CAP_KVM_OPENABLE', got %s", cap.ReasonCode)
}
if cap.Version != "" {
t.Errorf("expected empty Version for kvm_vm_launch, got %s", cap.Version)
Expand Down Expand Up @@ -114,8 +114,8 @@ func TestProjectEnvelope_KvmExistsNotOpenable(t *testing.T) {
}

cap := record.Capabilities["kvm_vm_launch"]
if cap.State != "Unavailable" || cap.ReasonCode != "KVM_NOT_OPENABLE_PERMS" {
t.Errorf("expected Unavailable/KVM_NOT_OPENABLE_PERMS, got %s/%s", cap.State, cap.ReasonCode)
if cap.State != "Unavailable" || cap.ReasonCode != "CAP_KVM_NOT_OPENABLE_PERMS" {
t.Errorf("expected Unavailable/CAP_KVM_NOT_OPENABLE_PERMS, got %s/%s", cap.State, cap.ReasonCode)
}
}

Expand Down
79 changes: 75 additions & 4 deletions schedune-control-plane/internal/domain/validate_launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"
"time"

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

Expand Down Expand Up @@ -165,7 +166,7 @@ func TestValidateLaunch_MissingKvmX86(t *testing.T) {
t.Errorf("expected ERR_LAUNCH_BACKEND_NOT_SUPPORTED blocker, got %v", result.BlockingReasonCodes)
}

if result.RejectedBackends["kvm_qemu"] != "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU (KVM_MISSING)" {
if result.RejectedBackends["kvm_qemu"] != "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU (CAP_KVM_MISSING)" {
t.Errorf("expected rejected backend kvm_qemu with CAP_KVM_MISSING, got %v", result.RejectedBackends)
}
}
Expand Down Expand Up @@ -298,7 +299,7 @@ func TestValidateLaunch_KvmExistsNotOpenable(t *testing.T) {
t.Errorf("expected ERR_LAUNCH_BACKEND_NOT_SUPPORTED blocker, got %v", result.BlockingReasonCodes)
}

if result.RejectedBackends["kvm_qemu"] != "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU (KVM_NOT_OPENABLE_PERMS)" {
if result.RejectedBackends["kvm_qemu"] != "ERR_LAUNCH_MISSING_CAPABILITY_KVM_QEMU (CAP_KVM_NOT_OPENABLE_PERMS)" {
t.Errorf("expected rejected backend kvm_qemu, got %v", result.RejectedBackends)
}

Expand All @@ -307,8 +308,8 @@ func TestValidateLaunch_KvmExistsNotOpenable(t *testing.T) {
for _, tr := range result.ValidationTrace {
traceStr += tr + " "
}
if !strings.Contains(traceStr, "KVM_NOT_OPENABLE_PERMS") {
t.Errorf("expected trace to contain KVM_NOT_OPENABLE_PERMS, got %s", traceStr)
if !strings.Contains(traceStr, "CAP_KVM_NOT_OPENABLE_PERMS") {
t.Errorf("expected trace to contain CAP_KVM_NOT_OPENABLE_PERMS, got %s", traceStr)
}
}

Expand Down Expand Up @@ -606,3 +607,73 @@ func TestValidateLaunch_SecurityContextRequiresNamespaces(t *testing.T) {
t.Errorf("expected validation to pass even when namespaces capability is missing, because dropping capabilities does not require full namespace support, got false: %v", result.BlockingReasonCodes)
}
}

func TestValidateLaunch_ReasonCodeRegistryHygiene(t *testing.T) {
// Pick a few representative fixtures that test different validation failure modes
fixtures := []string{
"cloudhypervisor_binary_missing.json",
"firecracker_partial_fail.json",
"missing_kvm_x86.json",
"missing_qemu_binary.json",
"healthy_unsupported_compatibility.json",
"healthy_x86_kvm_openable.json",
"stale_telemetry.json",
}

for _, fname := range fixtures {
env := readFixture(t, fname)
node := ProjectEnvelope(env)

spec := launch.LaunchSpec{
SchemaVersion: "v1alpha1",
WorkloadID: "wl-test",
TenantID: "tenant-1",
NodeID: node.ID,
RuntimeClass: "VirtualMachine",
Architecture: "x86_64",
LaunchMode: "DryRun",
Vcpu: 2,
MemoryMB: 1024,
}

res := ValidateLaunch(spec, node)

for _, code := range res.BlockingReasonCodes {
if !schema.IsKnownReasonCode(code) {
t.Errorf("fixture %s produced unregistered blocking reason code: %q", fname, code)
}
}

for _, code := range res.Warnings {
if !schema.IsKnownReasonCode(code) {
t.Errorf("fixture %s produced unregistered warning reason code: %q", fname, code)
}
}

for _, ev := range res.BackendRejectionEvidence {
if !schema.IsKnownReasonCode(ev.ReasonCode) {
t.Errorf("fixture %s produced unregistered structured reason code: %q", fname, ev.ReasonCode)
}
if ev.CapabilityReasonCode != nil && *ev.CapabilityReasonCode != "" {
if !schema.IsKnownReasonCode(*ev.CapabilityReasonCode) {
t.Errorf("fixture %s produced unregistered structured capability reason code: %q", fname, *ev.CapabilityReasonCode)
}
}
}

for _, reason := range res.RejectedBackends {
if reason == "" {
t.Errorf("fixture %s produced an empty rejected backend reason", fname)
}
words := strings.Split(reason, " ")
for _, w := range words {
w = strings.Trim(w, "()[]")
if strings.HasPrefix(w, "ERR_") || strings.HasPrefix(w, "CAP_") {
if !schema.IsKnownReasonCode(w) {
t.Errorf("fixture %s produced unregistered backend rejection code in message %q: %q", fname, reason, w)
}
}
}
}
}
}
16 changes: 12 additions & 4 deletions schedune-control-plane/pkg/schema/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ const (
ReasonErrTermSignalFailed = "ERR_TERM_SIGNAL_FAILED"

// Readiness / Reconciliation
ReasonErrReadyProbeFailed = "ERR_READY_PROBE_FAILED"
ReasonErrReadyTimeout = "ERR_READY_TIMEOUT"
ReasonErrReconcileProcessMissing = "ERR_RECONCILE_PROCESS_MISSING"
ReasonErrReconcileStatusUnreadable = "ERR_RECONCILE_STATUS_UNREADABLE"
ReasonErrReadyProbeFailed = "ERR_READY_PROBE_FAILED"
ReasonErrReadyTimeout = "ERR_READY_TIMEOUT"
ReasonErrReadyQemuSocketTimeout = "ERR_READY_QEMU_SOCKET_TIMEOUT"
ReasonErrReadyCloudHypervisorSocketTimeout = "ERR_READY_CLOUDHYPERVISOR_SOCKET_TIMEOUT"
ReasonErrReadyBackendUnsupported = "ERR_READY_BACKEND_UNSUPPORTED"
ReasonErrReconcileProcessMissing = "ERR_RECONCILE_PROCESS_MISSING"
ReasonErrReconcileStatusUnreadable = "ERR_RECONCILE_STATUS_UNREADABLE"

// Recovery
ReasonErrRecoveryExecutionMissing = "ERR_RECOVERY_EXECUTION_MISSING"
Expand All @@ -124,4 +127,9 @@ const (
ReasonErrRecoveryStaleHandle = "ERR_RECOVERY_STALE_HANDLE"
ReasonRecoveryConfirmed = "RECOVERY_CONFIRMED"
ReasonRecoveryTerminatedMissing = "RECOVERY_TERMINATED_MISSING"

// Orphans
ReasonErrOrphanPossibleScheduneProcess = "ERR_ORPHAN_POSSIBLE_SCHEDUNE_PROCESS"
ReasonErrOrphanStaleArtifactState = "ERR_ORPHAN_STALE_ARTIFACT_STATE"
ReasonErrOrphanUnmanagedBackendProcess = "ERR_ORPHAN_UNMANAGED_BACKEND_PROCESS"
)
83 changes: 83 additions & 0 deletions schedune-control-plane/pkg/schema/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package schema

var knownReasonCodes = map[string]struct{}{
ReasonCapKvmOpenable: {},
ReasonCapKvmMissing: {},
ReasonCapKvmNotOpenablePerms: {},
ReasonCapQemuBinaryPresent: {},
ReasonCapQemuBinaryMissing: {},
ReasonCapQemuUnsupportedArch: {},
ReasonCapCloudHypervisorBinaryPresent: {},
ReasonCapCloudHypervisorBinaryMissing: {},
ReasonCapCloudHypervisorReady: {},
ReasonCapCloudHypervisorPrereqsMissing: {},
ReasonCapFirecrackerBinaryPresent: {},
ReasonCapFirecrackerBinaryMissing: {},
ReasonCapFirecrackerTunReady: {},
ReasonCapFirecrackerTunMissing: {},
ReasonCapFirecrackerCgroupsReady: {},
ReasonCapFirecrackerCgroupsMissing: {},
ReasonCapFirecrackerReady: {},
ReasonCapFirecrackerPrereqsMissing: {},
ReasonRejectArchitectureMismatch: {},
ReasonRejectCompatibilityClassMismatch: {},
ReasonRejectMissingKVM: {},
ReasonRejectMissingTPM: {},
ReasonRejectNodeUnhealthy: {},
ReasonRejectTelemetryStale: {},
ReasonRejectForbiddenConstraintPrefix: {},
ReasonErrLaunchArchMismatch: {},
ReasonErrLaunchBackendNotSupported: {},
ReasonErrLaunchMissingArtifact: {},
ReasonErrLaunchInvalidStorageFormat: {},
ReasonErrLaunchInvalidFirecrackerArtifactModel: {},
ReasonErrLaunchMissingCapabilityCloudHypervisor: {},
ReasonErrLaunchMissingCapabilityChBinary: {},
ReasonErrLaunchMissingCapabilityFcBinary: {},
ReasonErrLaunchMissingCapabilityFcTun: {},
ReasonErrLaunchMissingCapabilityFcCgroups: {},
ReasonErrLaunchMissingCapabilityKvmQemu: {},
ReasonErrLaunchMissingCapabilityQemuBinary: {},
ReasonErrLaunchMissingCapabilitySeccomp: {},
ReasonErrLaunchMissingCapabilityNamespaces: {},
ReasonWarnDeprecatedImageReference: {},
ReasonWarnDeprecatedNetworkAttachments: {},
ReasonErrPreparationFailed: {},
ReasonErrNodeNotFound: {},
ReasonErrValidationFailed: {},
ReasonErrExecRuntimeSpawnFailed: {},
ReasonErrExecRuntimeCrashed: {},
ReasonErrExecRuntimeExitedEarly: {},
ReasonErrTermSignalFailed: {},
ReasonErrReadyProbeFailed: {},
ReasonErrReadyTimeout: {},
ReasonErrReadyQemuSocketTimeout: {},
ReasonErrReadyCloudHypervisorSocketTimeout: {},
ReasonErrReadyBackendUnsupported: {},
ReasonErrReconcileProcessMissing: {},
ReasonErrReconcileStatusUnreadable: {},
ReasonErrRecoveryExecutionMissing: {},
ReasonErrRecoveryReassociationAmbiguous: {},
ReasonErrRecoveryRehydrateFailed: {},
ReasonErrRecoveryStaleHandle: {},
ReasonRecoveryConfirmed: {},
ReasonRecoveryTerminatedMissing: {},
ReasonErrOrphanPossibleScheduneProcess: {},
ReasonErrOrphanStaleArtifactState: {},
ReasonErrOrphanUnmanagedBackendProcess: {},
}

// KnownReasonCodes returns a map of all registered reason codes.
func KnownReasonCodes() map[string]struct{} {
copied := make(map[string]struct{}, len(knownReasonCodes))
for k, v := range knownReasonCodes {
copied[k] = v
}
return copied
}

// IsKnownReasonCode checks if a given reason code is registered.
func IsKnownReasonCode(code string) bool {
_, exists := knownReasonCodes[code]
return exists
}
70 changes: 70 additions & 0 deletions schedune-control-plane/pkg/schema/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package schema_test

import (
"strings"
"testing"

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

func TestReasonCodeRegistry_Constraints(t *testing.T) {
codes := schema.KnownReasonCodes()

allowedPrefixes := []string{"CAP_", "REJECT_", "ERR_", "WARN_", "RECOVERY_"}

for code := range codes {
if code == "" {
t.Errorf("found empty reason code")
}

hasPrefix := false
for _, prefix := range allowedPrefixes {
if strings.HasPrefix(code, prefix) {
hasPrefix = true
break
}
}

if !hasPrefix {
t.Errorf("reason code %q does not start with an allowed prefix: %v", code, allowedPrefixes)
}
}
}

func TestReasonCodeRegistry_RepresentativeCodes(t *testing.T) {
// Verify representative lifecycle/readiness/recovery codes
expectedCodes := []string{
schema.ReasonErrLaunchArchMismatch,
schema.ReasonErrLaunchMissingCapabilitySeccomp,
schema.ReasonRejectArchitectureMismatch,
schema.ReasonRecoveryConfirmed,
schema.ReasonErrReadyProbeFailed,
schema.ReasonErrReconcileProcessMissing,
schema.ReasonErrExecRuntimeCrashed,
}

for _, expected := range expectedCodes {
if !schema.IsKnownReasonCode(expected) {
t.Errorf("expected representative code %q to be registered", expected)
}
}
}

func TestReasonCodeRegistry_Immutability(t *testing.T) {
codes := schema.KnownReasonCodes()

codes["DUMMY_MUTATION_TEST"] = struct{}{}

if schema.IsKnownReasonCode("DUMMY_MUTATION_TEST") {
t.Errorf("registry was mutated by caller")
}
}

func TestReasonCodeRegistry_Uniqueness(t *testing.T) {
// The registry is implemented as a map literal, which guarantees uniqueness
// of keys at compile-time/runtime. We simply verify the map isn't empty.
codes := schema.KnownReasonCodes()
if len(codes) == 0 {
t.Errorf("expected non-empty registry")
}
}
Loading
Loading