fix(apiserver): reject submit options the job never gets; add exec_home (bug-1936)

Cause: CreateJob accepted in_file, out_file, err_file, out_dir,
asked_hosts, begin_time, term_time and transfer_files and returned
success, but neither the controller's submit data/start info nor the
worker carries any of them (the worker never stages transferred files),
so they were silently dropped. Exec home had no submit field at all, and
a relative cwd would have been resolved against the worker unit's
directory.

Fix: new preCheckSubmitOptions() runs right after binding, before file
uploads and ID allocation, and answers 400 naming every unsupported
option that is set; cwd and the new exec_home field must be absolute and
fit the worker's job_assignment_t buffers (1023 bytes). exec_home goes
to JobInfo.ExecHome and from there (submitStartInfo) to the start info's
exechome. num_files already reaches exdata as nofile_limit; a handler
test now pins it. The array %J/%I submit-time substitution test (9.4)
checks cwd instead of out_file/err_file, which are now refused. Swagger
regenerated with `swag init -g cmd/main.go -o docs`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 11:48:45 +00:00
parent 725b1d23fd
commit edad8c7385
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
6 changed files with 200 additions and 12 deletions

View file

@ -694,15 +694,17 @@ func TestJobHandler_SubmitOrdinaryUnchanged(t *testing.T) {
}
// TestJobHandler_SubmitArrayFileNameSubstitution: at submission a new
// array's file names get the first element's values, %I = 1 and
// %J = <array-id>[1] (9.4).
// array's path names get the first element's values, %I = 1 and
// %J = <array-id>[1] (9.4). Checked on cwd: out_file/err_file are refused
// since bug-1936 (the worker never writes them), cwd takes the same
// substitution.
func TestJobHandler_SubmitArrayFileNameSubstitution(t *testing.T) {
f := newHandlerFixture(t)
f.seedQueue("normal")
submitData(t, f.submit(map[string]interface{}{
"job_name": "arr[1-3]", "command": "sleep 1", "queue_name": "normal",
"out_file": "out.%J.%I", "err_file": "err.%J.%I"}))
"cwd": "/w/out.%J.%I"}))
subs := f.submissions()
if len(subs) != 1 {
@ -713,9 +715,8 @@ func TestJobHandler_SubmitArrayFileNameSubstitution(t *testing.T) {
if got := backend.FormatJobID(int64(backend.MakeElementID(handlerTestArrayBase, 1))); got != elem {
t.Fatalf("first element formats as %q, want %q", got, elem)
}
wantOut, wantErr := "out."+elem+".1", "err."+elem+".1"
if subs[0].Job.OutFile != wantOut || subs[0].Job.ErrFile != wantErr {
t.Errorf("files = {%q, %q}, want {%q, %q}", subs[0].Job.OutFile, subs[0].Job.ErrFile, wantOut, wantErr)
if want := "/w/out." + elem + ".1"; subs[0].Job.CWD != want {
t.Errorf("cwd = %q, want %q", subs[0].Job.CWD, want)
}
}
@ -1603,3 +1604,104 @@ func TestJobHandler_SubmitQueueRejectedByController(t *testing.T) {
}
}
}
// bug-1936: a submit option the job never gets must be refused with a 400
// naming it, not accepted and dropped. Neither the controller's submit data
// nor the worker carries stdin/stdout/stderr paths, an output directory,
// asked hosts or a begin/termination time, and the worker never stages
// transferred files in or out.
func TestJobHandler_SubmitRejectsUnsupportedOptions(t *testing.T) {
for _, tc := range []struct {
field string
value interface{}
}{
{"in_file", "/tmp/in.txt"},
{"out_file", "/tmp/out.%J"},
{"err_file", "/tmp/err.%J"},
{"out_dir", "/tmp/outdir"},
{"asked_hosts", "host1"},
{"begin_time", "2030-01-02 03:04"},
{"term_time", "2030-01-02 03:04"},
{"transfer_files", []string{"/tmp/a.txt > a.txt"}},
} {
f := newHandlerFixture(t)
f.seedQueue("normal")
w := f.submit(map[string]interface{}{
"command": "sleep 1", "queue_name": "normal", tc.field: tc.value,
})
assert400(t, tc.field, w, tc.field)
assert400(t, tc.field, w, "not supported")
f.assertNothingCreated()
}
}
// bug-1936: the job runs in cwd and with HOME set to exec_home on the
// worker, so both must be absolute paths that fit the worker's
// job_assignment_t buffers (PATH_LENGTH, 1024 bytes with the NUL).
func TestJobHandler_SubmitRejectsBadCwdAndExecHome(t *testing.T) {
long := "/" + strings.Repeat("d", 1023)
for _, tc := range []struct {
field, value, want string
}{
{"cwd", "relative/dir", "absolute"},
{"cwd", long, "too long"},
{"exec_home", "home/u", "absolute"},
{"exec_home", long, "too long"},
} {
f := newHandlerFixture(t)
f.seedQueue("normal")
w := f.submit(map[string]interface{}{
"command": "sleep 1", "queue_name": "normal", tc.field: tc.value,
})
assert400(t, tc.field+"="+tc.value[:6], w, tc.want)
f.assertNothingCreated()
}
}
// bug-1936: exec_home is a submit field and reaches the submitted job (and
// from there submitStartInfo's exechome), next to cwd.
func TestJobHandler_SubmitCarriesCwdAndExecHome(t *testing.T) {
f := newHandlerFixture(t)
f.seedQueue("normal")
submitData(t, f.submit(map[string]interface{}{
"command": "sleep 1", "queue_name": "normal",
"cwd": "/data/run/%J", "exec_home": "/home/alice",
}))
subs := f.submissions()
if len(subs) != 1 {
t.Fatalf("submitJob calls = %d, want 1", len(subs))
}
wantCwd := "/data/run/" + backend.FormatJobID(int64(handlerTestOrdinaryBase))
if subs[0].Job.CWD != wantCwd || subs[0].Job.ExecHome != "/home/alice" {
t.Errorf("cwd, exec_home = %q, %q; want %q, %q",
subs[0].Job.CWD, subs[0].Job.ExecHome, wantCwd, "/home/alice")
}
}
// bug-1936: num_files (and shell_limit.num_files) is the job's open-file
// limit; it lands in the exdata limits as nofile_limit, the key the
// worker's parse_limits() reads.
func TestJobHandler_SubmitNofileLimitInExdata(t *testing.T) {
for _, body := range []map[string]interface{}{
{"command": "sleep 1", "queue_name": "normal", "num_files": 2048},
{"command": "sleep 1", "queue_name": "normal", "shell_limit": map[string]interface{}{"num_files": 2048}},
} {
f := newHandlerFixture(t)
f.seedQueue("normal")
submitData(t, f.submit(body))
subs := f.submissions()
if len(subs) != 1 {
t.Fatalf("submitJob calls = %d, want 1", len(subs))
}
var ex struct {
Limits map[string]float64 `json:"limits"`
}
if err := json.Unmarshal([]byte(subs[0].Job.ExtendData.String), &ex); err != nil {
t.Fatalf("exdata %q: %v", subs[0].Job.ExtendData.String, err)
}
if ex.Limits["nofile_limit"] != 2048 {
t.Errorf("exdata %q: nofile_limit = %v, want 2048", subs[0].Job.ExtendData.String, ex.Limits["nofile_limit"])
}
}
}