cf3d98eb2b
R-022: orca job run now deploys to remote nodes via scheduler -> emitter -> SSH-push. Local exec fallback only when no remote nodes registered. jobspec parser (REQ-152): - schedule: and timeout: now parsed (were silently dropped) - DaemonSet Count no longer defaults to 1 (was breaking DaemonSet) - restart: policy translated to systemd Restart=/StartLimitBurst - job lint: advisory warnings for cron/health/update/affinity (honest) scheduler wiring (REQ-151, C-44): - new internal/cli/job_dispatch.go: dispatchDecision + deployRemote - scheduler.Schedule evaluates constraints/capacity/affinity - --target overrides scheduler (manual pinning) - local fallback only when len(ready non-localhost nodes)==0 - C-44: SSH-push failure returns error (no silent local fallback) - systemd-analyze verify on rendered unit before deploy Tests: 22 new test functions covering scheduler, parser, C-44, local fallback, target override, systemd-analyze skip, restart directives. ---ci--- project: orca phase: 3 milestone: v0.13 status: complete requirements: covered: [151, 152] ---/ci---
137 lines
3.9 KiB
Go
137 lines
3.9 KiB
Go
package jobspec
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestREQ152_ScheduleTimeoutDaemonSet(t *testing.T) {
|
|
input := "---\n" +
|
|
"kind: DaemonSet\n" +
|
|
"name: logs\n" +
|
|
"schedule:\n" +
|
|
" mode: every-node\n" +
|
|
" cron: \"*/5 * * * *\"\n" +
|
|
"timeout: 30s\n" +
|
|
"restart:\n" +
|
|
" mode: service\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /bin/true\n" +
|
|
"---\nbody\n"
|
|
spec, err := ParseMarkdown([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("ParseMarkdown: %v", err)
|
|
}
|
|
if spec.Count != 0 {
|
|
t.Errorf("DaemonSet Count = %d, want 0 (no default)", spec.Count)
|
|
}
|
|
if spec.Schedule == nil {
|
|
t.Fatal("Schedule is nil")
|
|
}
|
|
if spec.Schedule.Mode != "every-node" {
|
|
t.Errorf("Schedule.Mode = %q, want every-node", spec.Schedule.Mode)
|
|
}
|
|
if spec.Schedule.Cron != "*/5 * * * *" {
|
|
t.Errorf("Schedule.Cron = %q, want */5 * * * *", spec.Schedule.Cron)
|
|
}
|
|
if spec.Timeout != "30s" {
|
|
t.Errorf("Timeout = %q, want 30s", spec.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestREQ152_JobScheduleTimeout(t *testing.T) {
|
|
input := "---\n" +
|
|
"kind: Job\n" +
|
|
"name: nightly\n" +
|
|
"schedule:\n" +
|
|
" cron: \"0 2 * * *\"\n" +
|
|
"timeout: 1h\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /bin/true\n" +
|
|
"---\nbody\n"
|
|
spec, err := ParseMarkdown([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("ParseMarkdown: %v", err)
|
|
}
|
|
if spec.Count != 1 {
|
|
t.Errorf("Job Count = %d, want 1 (default)", spec.Count)
|
|
}
|
|
if spec.Schedule == nil || spec.Schedule.Cron != "0 2 * * *" {
|
|
t.Errorf("Schedule.Cron = %+v, want 0 2 * * *", spec.Schedule)
|
|
}
|
|
if spec.Timeout != "1h" {
|
|
t.Errorf("Timeout = %q, want 1h", spec.Timeout)
|
|
}
|
|
}
|
|
|
|
// TestREQ152_DaemonSetPassesLint verifies a DaemonSet spec with a
|
|
// schedule block, restart, and runtime parses AND validates cleanly
|
|
// under the schema (T11). DaemonSet must NOT default Count to 1.
|
|
func TestREQ152_DaemonSetPassesLint(t *testing.T) {
|
|
input := "---\n" +
|
|
"kind: DaemonSet\n" +
|
|
"name: log-shipper\n" +
|
|
"schedule:\n" +
|
|
" mode: every-node\n" +
|
|
"restart:\n" +
|
|
" mode: service\n" +
|
|
" max_retries: 5\n" +
|
|
" delay: 5s\n" +
|
|
"runtime:\n" +
|
|
" one_of: process\n" +
|
|
" command: /usr/local/bin/log-shipper\n" +
|
|
"---\n# Log shipper\n\nRuns on every node.\n"
|
|
spec, err := ParseMarkdown([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("ParseMarkdown: %v", err)
|
|
}
|
|
if spec.Count != 0 {
|
|
t.Errorf("DaemonSet Count = %d, want 0", spec.Count)
|
|
}
|
|
if spec.Schedule == nil || spec.Schedule.Mode != "every-node" {
|
|
t.Errorf("Schedule.Mode = %+v, want every-node", spec.Schedule)
|
|
}
|
|
if spec.Restart == nil || spec.Restart.Mode != "service" {
|
|
t.Errorf("Restart.Mode = %+v, want service", spec.Restart)
|
|
}
|
|
}
|
|
|
|
// TestREQ152_TimeoutEnforced verifies the timeout field is parsed and
|
|
// stored on the WorkloadSpec (T12).
|
|
func TestREQ152_TimeoutEnforced(t *testing.T) {
|
|
cases := []struct {
|
|
timeout string
|
|
want string
|
|
}{
|
|
{"30s", "30s"},
|
|
{"5m", "5m"},
|
|
{"1h30m", "1h30m"},
|
|
{"900s", "900s"},
|
|
}
|
|
for _, c := range cases {
|
|
input := "---\nkind: Job\nname: t\ntimeout: " + c.timeout + "\nruntime:\n one_of: process\n command: /bin/true\n---\nbody\n"
|
|
spec, err := ParseMarkdown([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("ParseMarkdown(%q): %v", c.timeout, err)
|
|
}
|
|
if spec.Timeout != c.want {
|
|
t.Errorf("Timeout = %q, want %q", spec.Timeout, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestREQ152_DaemonSetExplicitCountRejected verifies that an explicit
|
|
// count on a DaemonSet is preserved (parser does not override it) so
|
|
// the validator can reject it.
|
|
func TestREQ152_DaemonSetExplicitCountPreserved(t *testing.T) {
|
|
input := "---\nkind: DaemonSet\nname: d\ncount: 3\nschedule:\n mode: every-node\nrestart:\n mode: service\nruntime:\n one_of: process\n command: /bin/true\n---\nbody\n"
|
|
spec, err := ParseMarkdown([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("ParseMarkdown: %v", err)
|
|
}
|
|
if spec.Count != 3 {
|
|
t.Errorf("DaemonSet explicit Count = %d, want 3 (preserved, not defaulted)", spec.Count)
|
|
}
|
|
}
|