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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ schedune-agent/target/
schedune-control-plane/intake
schedune-control-plane/schedune-control-plane
bin/
var/
.schedune.pid
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ smoke-test: build ## Run headless E2E automated API smoke test
demo: doctor build dev-db-reset ## Run the automated end-to-end evaluator demo
@bash scripts/demo.sh

demo-fixture: build-control-plane dev-db-reset ## Run the fixture-backed evaluator demo (macOS/non-Linux friendly)
@bash scripts/demo-fixture.sh

demo-fixture-once: build-control-plane dev-db-reset ## Run the fixture-backed evaluator demo and exit cleanly
@bash scripts/demo-fixture.sh --once

example-nodes: ## List normalized node summaries
@curl -s http://127.0.0.1:9090/api/v1alpha1/nodes

example-node-explain: ## Explain scheduling eligibility for a specific node (Requires NODE_ID)
@if [ -z "$(NODE_ID)" ]; then \
echo "Usage: make example-node-explain NODE_ID=<id>"; \
else \
curl -s http://127.0.0.1:9090/api/v1alpha1/nodes/$(NODE_ID)/explain; \
fi

example-intake: ## Ingest the local node capability payload
@bash examples/curls/intake.sh

Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,36 @@ Get a single-node Schedune control plane and agent running in under 5 minutes.

### 1. Preflight Check

Check if your local Linux host is ready for the evaluator:
Check if your local host is ready for the evaluator:

```bash
make dev-preflight
```

### 2. Evaluator Demo
### 2. Evaluator Demo (Linux)

Run the end-to-end evaluator journey. This builds the components, starts the control plane, inspects your local node, ingests the truth, and evaluates a sample workload intent.
If you are on a Linux host with KVM, run the end-to-end evaluator journey. This builds the components, starts the control plane, inspects your local node, ingests the truth, and evaluates a sample workload intent.

```bash
make demo
```

### 3. Evaluate from a MacBook / non-Linux host

On a MacBook M2 Air (or other non-Linux hosts), you can still test control-plane intake, scheduling explainability, launch validation against fixture truth, node APIs, and orphan API shape. Actual VM/microVM execution requires Linux with KVM and runtime binaries.

Run the fixture-backed evaluator demo to quickly verify the pipeline:

```bash
make demo-fixture-once
```

For an interactive session where you can explore the API manually afterwards, run:

```bash
make demo-fixture
```

### 3. Step-by-Step Examples

If you want to run it manually using the provided targets:
Expand Down
20 changes: 18 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,32 @@ Get a single-node Schedune control plane and agent running in under 5 minutes.

You can evaluate Schedune using our automated demo mode or by walking through the manual flow.

### Automated Demo Mode
### Automated Demo Mode (Linux)

The easiest way to see Schedune in action:
The easiest way to see Schedune in action on Linux:

```bash
make demo
```

This will run the preflight checks, build the binaries, reset the database, start the control plane, ingest your local node's capabilities, and run a workload scheduling explanation.

### Evaluate from a MacBook / non-Linux host

On a MacBook M2 Air (or other non-Linux hosts), you can still test control-plane intake, scheduling explainability, launch validation against fixture truth, node APIs, and orphan API shape. Actual VM/microVM execution requires Linux with KVM and runtime binaries.

Run the fixture-backed evaluator demo to quickly verify the pipeline:

```bash
make demo-fixture-once
```

For an interactive session where you can explore the API manually afterwards, run:

```bash
make demo-fixture
```

### Manual Step-by-Step Flow

#### 1. Preflight Check
Expand Down
20 changes: 15 additions & 5 deletions schedune-control-plane/cmd/schedune/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,14 @@ func runDoctor() {
fmt.Printf("%s Architecture: %s\n", PASS, runtime.GOARCH)
fmt.Printf("%s Kernel: %s\n", PASS, getKernelVersion())
} else {
fmt.Printf("%s OS: %s (Schedune requires Linux)\n", FAIL, runtime.GOOS)
controlPlaneReady = false
fmt.Printf("%s OS: %s (Control plane: evaluator mode. Agent/Runtime: Linux required)\n", WARN, runtime.GOOS)
agentInspectReady = false
}

if syscall.Access("/proc", 4) == nil {
fmt.Printf("%s /proc: readable\n", PASS)
} else {
fmt.Printf("%s /proc: not readable\n", FAIL)
fmt.Printf("%s /proc: not readable\n", WARN)
orphanSweepReady = false
}
fmt.Println()
Expand Down Expand Up @@ -277,7 +276,9 @@ func runDoctor() {
fmt.Println()

fmt.Println("Recommended next step:")
if controlPlaneReady && agentInspectReady {
if runtime.GOOS != "linux" && controlPlaneReady {
fmt.Println(" make demo-fixture (evaluator mode)")
} else if controlPlaneReady && agentInspectReady {
fmt.Println(" make demo")
} else {
fmt.Println(" Resolve [FAIL] items before continuing.")
Expand All @@ -302,7 +303,7 @@ func runServer() {
}

// Phase 7B.5: Real Orphan Sweep
enumerator := &cp_runtime.LinuxProcEnumerator{}
enumerator := cp_runtime.NewEnumerator()
orphanSweeper := recovery.NewOrphanSweepService(enumerator, sqliteStore, sqliteStore, sqliteStore, "local-node")

go func() {
Expand All @@ -318,6 +319,8 @@ func runServer() {
memStore := store.NewInMemoryStore()

// Initialize HTTP Handlers
systemHandler := api.NewSystemHandler()
nodeHandler := api.NewNodeHandler(memStore)
intakeHandler := api.NewIntakeHandler(memStore)
schedulerHandler := api.NewSchedulerHandler(memStore)
launchHandler := api.NewLaunchHandler(memStore, sqliteStore)
Expand All @@ -330,6 +333,13 @@ func runServer() {
// API Group
v1 := r.Group("/api/v1alpha1")
{
// System Health
v1.GET("/healthz", systemHandler.Healthz)

// Operator -> Control Plane (Nodes)
v1.GET("/nodes", nodeHandler.ListNodes)
v1.GET("/nodes/:id", nodeHandler.GetNode)

// Data Plane -> Control Plane
v1.POST("/intake/envelope", intakeHandler.Ingest)

Expand Down
70 changes: 70 additions & 0 deletions schedune-control-plane/internal/api/system_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package api

import (
"net/http"
"runtime"

"github.com/TechnologyTailors/Schedune/schedune-control-plane/internal/store"
"github.com/gin-gonic/gin"
)

type SystemHandler struct{}

func NewSystemHandler() *SystemHandler {
return &SystemHandler{}
}

func (h *SystemHandler) Healthz(c *gin.Context) {
processEnumerationEnabled := runtime.GOOS == "linux"

c.JSON(http.StatusOK, gin.H{
"status": "ok",
"api_version": "v1alpha1",
"storage": "sqlite",
"process_enumeration_enabled": processEnumerationEnabled,
})
}

type NodeSummary struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
Architecture string `json:"architecture"`
Health string `json:"health"`
Class string `json:"class"`
}

type NodeHandler struct {
store *store.InMemoryStore
}

func NewNodeHandler(s *store.InMemoryStore) *NodeHandler {
return &NodeHandler{store: s}
}

func (h *NodeHandler) ListNodes(c *gin.Context) {
nodes := h.store.ListAllNodes()
var summaries []NodeSummary

for _, n := range nodes {
summaries = append(summaries, NodeSummary{
ID: n.ID,
Hostname: n.Identity.Hostname,
Architecture: n.Identity.Architecture,
Health: n.Health.State,
Class: n.Compatibility.Class,
})
}

c.JSON(http.StatusOK, gin.H{"nodes": summaries})
}

func (h *NodeHandler) GetNode(c *gin.Context) {
nodeID := c.Param("id")
record, err := h.store.GetNode(nodeID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"})
return
}

c.JSON(http.StatusOK, record)
}
80 changes: 80 additions & 0 deletions schedune-control-plane/internal/api/system_handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package api

import (
"encoding/json"
"net/http"
"net/http/httptest"
"runtime"
"testing"

"github.com/TechnologyTailors/Schedune/schedune-control-plane/internal/store"
"github.com/gin-gonic/gin"
)

func TestSystemHandler_Healthz(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

handler := NewSystemHandler()
handler.Healthz(c)

if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}

var response map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("Failed to parse response JSON: %v", err)
}

if response["status"] != "ok" {
t.Errorf("Expected status 'ok', got '%v'", response["status"])
}
if response["api_version"] != "v1alpha1" {
t.Errorf("Expected api_version 'v1alpha1', got '%v'", response["api_version"])
}
if response["storage"] != "sqlite" {
t.Errorf("Expected storage 'sqlite', got '%v'", response["storage"])
}

expectedEnumEnabled := runtime.GOOS == "linux"
if response["process_enumeration_enabled"] != expectedEnumEnabled {
t.Errorf("Expected process_enumeration_enabled %v, got %v", expectedEnumEnabled, response["process_enumeration_enabled"])
}
}

func TestNodeHandler_ListAndGet(t *testing.T) {
gin.SetMode(gin.TestMode)
memStore := store.NewInMemoryStore()
handler := NewNodeHandler(memStore)

// Add a dummy node directly to the underlying map for testing, or we can use domain.NodeRecord.
// Since InMemoryStore has SaveNodeState, we'd need an envelope. To mock, let's look at InMemoryStore.
// Actually, there is a ListAllNodes that reads from a map. If empty, returns [].

// Test ListNodes (empty)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
handler.ListNodes(c)

if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}

// For a fully fleshed out test, let's just make sure it returns 200 and a 'nodes' array
var listResp map[string][]NodeSummary
if err := json.Unmarshal(w.Body.Bytes(), &listResp); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}

// Test GetNode (not found)
w2 := httptest.NewRecorder()
c2, _ := gin.CreateTestContext(w2)
c2.Params = []gin.Param{{Key: "id", Value: "non-existent"}}
handler.GetNode(c2)

if w2.Code != http.StatusNotFound {
t.Errorf("Expected status 404, got %d", w2.Code)
}
}
15 changes: 15 additions & 0 deletions schedune-control-plane/internal/runtime/enumerate.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package runtime

import "runtime"

type EnumeratedProcess struct {
PID int
PPID *int
Expand All @@ -16,3 +18,16 @@ type EnumeratedProcess struct {
type Enumerator interface {
Enumerate() ([]EnumeratedProcess, error)
}

func NewEnumerator() Enumerator {
if runtime.GOOS == "linux" {
return &LinuxProcEnumerator{}
}
return &NoopEnumerator{}
}

type NoopEnumerator struct{}

func (e *NoopEnumerator) Enumerate() ([]EnumeratedProcess, error) {
return nil, nil
}
Loading
Loading