4c2e59cf3f
Root cause: orca job run <example>.md failed with fork/exec: no such
file or directory on every example. Two compounding problems:
1. workloadToTaskSpecs (internal/cli/job.go:340) passed the entire
runtime.command string (e.g. "/usr/bin/httpd -f /etc/orca/web-app/
httpd.conf") as a single binary path to exec.Command, which then
looked for a file literally named "/usr/bin/httpd -f ..." and
failed. The v0.9 markdown parser stores command: as a raw string;
the legacy HCL path had separate command+args fields. Fix: add
splitCommand helper that splits on strings.Fields into binary+args,
with /bin/true fallback for empty commands.
2. The example commands referenced binaries that don't exist on a bare
Linux machine (/usr/bin/httpd, postgres, api-server, fluent-bit).
Fix: rewrite the 5 example runtime.command values to use /bin/sleep
3600 (long-running services) or /bin/echo (one-shot job) so they
run out-of-the-box. Each file has a Production substitution note
showing the real binary to use in deployment.
Verified: orca job run examples/full-stack/worker.md now succeeds
(exit 0). All 4 services (web-app, api, log-shipper, postgres) start
correctly (task started, pid assigned). 12 new unit tests pass
(splitCommand: 7 cases, workloadToTaskSpecs: 5 cases). All 5 example
jobspecs still parse + validate (gate C-20). make lint clean.
---ci---
project: orca
phase: 6
milestone: v0.10
status: execute
decisions:
- id: D-195
decision: split command string via strings.Fields in workloadToTaskSpecs
rationale: exec.Command expects binary path + args as separate elements;
the v0.9 markdown parser stores command: as a single string with no
args field (unlike legacy HCL). strings.Fields is dep-free and handles
multiple spaces/tabs. Shell quoting (single/double quotes inside the
command) is not handled — examples avoid sh -c with quoted strings.
confidence: 0.95
alternatives: [shellquote.Split from mvdan/sh (adds dependency)]
lessons:
- The v0.9 markdown jobspec path needs the same command+args split that
the legacy HCL path had via separate command/args fields. The parser
stores command: as a raw string; the CLI must split it before passing
to exec.Command.
- Example jobspecs should use /bin/sleep and /bin/echo (binaries that
exist on every Linux machine) so they run out-of-the-box. Descriptive
production commands belong in a comment block, not in runtime.command.
---/ci---
142 lines
3.6 KiB
Go
142 lines
3.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
|
)
|
|
|
|
func TestSplitCommand(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantBin string
|
|
wantArgs []string
|
|
}{
|
|
{
|
|
name: "single binary",
|
|
input: "/bin/true",
|
|
wantBin: "/bin/true",
|
|
wantArgs: nil,
|
|
},
|
|
{
|
|
name: "binary with one arg",
|
|
input: "/bin/echo hello",
|
|
wantBin: "/bin/echo",
|
|
wantArgs: []string{"hello"},
|
|
},
|
|
{
|
|
name: "binary with multiple args",
|
|
input: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
|
|
wantBin: "/usr/bin/httpd",
|
|
wantArgs: []string{"-f", "/etc/orca/web-app/httpd.conf"},
|
|
},
|
|
{
|
|
name: "binary with sh -c and quoted string",
|
|
input: "/bin/sh -c 'echo hello world'",
|
|
wantBin: "/bin/sh",
|
|
wantArgs: []string{"-c", "'echo", "hello", "world'"},
|
|
},
|
|
{
|
|
name: "empty command falls back to /bin/true",
|
|
input: "",
|
|
wantBin: "/bin/true",
|
|
wantArgs: nil,
|
|
},
|
|
{
|
|
name: "all-whitespace command falls back to /bin/true",
|
|
input: " \t ",
|
|
wantBin: "/bin/true",
|
|
wantArgs: nil,
|
|
},
|
|
{
|
|
name: "multiple spaces between args",
|
|
input: "/bin/echo hello world",
|
|
wantBin: "/bin/echo",
|
|
wantArgs: []string{"hello", "world"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotBin, gotArgs := splitCommand(tt.input)
|
|
if gotBin != tt.wantBin {
|
|
t.Errorf("splitCommand(%q) bin = %q, want %q", tt.input, gotBin, tt.wantBin)
|
|
}
|
|
if len(gotArgs) != len(tt.wantArgs) {
|
|
t.Errorf("splitCommand(%q) args len = %d, want %d (got %v, want %v)",
|
|
tt.input, len(gotArgs), len(tt.wantArgs), gotArgs, tt.wantArgs)
|
|
return
|
|
}
|
|
for i, a := range gotArgs {
|
|
if a != tt.wantArgs[i] {
|
|
t.Errorf("splitCommand(%q) args[%d] = %q, want %q",
|
|
tt.input, i, a, tt.wantArgs[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkloadToTaskSpecs_SplitsCommand(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Name: "web-app",
|
|
Runtime: &jobspec.RuntimeBlock{
|
|
OneOf: "process",
|
|
Command: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
|
|
},
|
|
}
|
|
tasks := workloadToTaskSpecs(spec)
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
|
}
|
|
if tasks[0].Command != "/usr/bin/httpd" {
|
|
t.Errorf("expected Command=/usr/bin/httpd, got %q", tasks[0].Command)
|
|
}
|
|
if len(tasks[0].Args) != 2 {
|
|
t.Fatalf("expected 2 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
|
|
}
|
|
if tasks[0].Args[0] != "-f" || tasks[0].Args[1] != "/etc/orca/web-app/httpd.conf" {
|
|
t.Errorf("expected args [-f /etc/orca/web-app/httpd.conf], got %v", tasks[0].Args)
|
|
}
|
|
}
|
|
|
|
func TestWorkloadToTaskSpecs_NilRuntimeUsesBinTrue(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Name: "noop",
|
|
}
|
|
tasks := workloadToTaskSpecs(spec)
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
|
}
|
|
if tasks[0].Command != "/bin/true" {
|
|
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
|
|
}
|
|
if len(tasks[0].Args) != 0 {
|
|
t.Errorf("expected 0 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
|
|
}
|
|
}
|
|
|
|
func TestWorkloadToTaskSpecs_EmptyCommandUsesBinTrue(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Name: "empty",
|
|
Runtime: &jobspec.RuntimeBlock{
|
|
OneOf: "process",
|
|
Command: "",
|
|
},
|
|
}
|
|
tasks := workloadToTaskSpecs(spec)
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
|
}
|
|
if tasks[0].Command != "/bin/true" {
|
|
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
|
|
}
|
|
}
|
|
|
|
func TestWorkloadToTaskSpecs_NilSpecReturnsNil(t *testing.T) {
|
|
tasks := workloadToTaskSpecs(nil)
|
|
if tasks != nil {
|
|
t.Errorf("expected nil, got %v", tasks)
|
|
}
|
|
}
|