Commit 12aa067
authored
SEP-1943: Report an unlaunchable executor; drop a redundant sudo (#1445)
An execution whose interpreter the target Nomad node cannot launch used
to land in `FAILED`, indistinguishable from a script that ran and exited
non-zero on its own terms — the launcher's own error arrived as ordinary
script output. This does two things.
**1. Reports it as its own outcome.** A `check-launchable` prestart step
(`app/tasks/db/seed.py`) is added to the three seeded specs that
interpolate a launch command from meta — `run-command`, `exec-artifact`,
`exec-python-artifact`. `run-python` is excluded: it is payload-driven
and declares no launch-command meta. The step resolves the launch
command chain on the node and aborts with sentinel exit `78`, which the
Nomad executor maps to a new terminal
`TaskHistoryStatusEnum.UNLAUNCHABLE` — the same shape as the existing
`check-staleness` / exit `75` / `STALE` mechanism. It logs
`SEP_UNLAUNCHABLE: command=<cmd> node=<node>`, naming which command
could not be launched and where.
**2. Stops causing the failure on a root node.** Where a node's
`raw_exec` tasks already run as uid 0 and no `sudo` binary exists, the
`sudo ` prefix `build_execution_meta` prepends
(`app/sep/apps/framework/script_helpers.py`, unchanged here) is the
*sole* cause of the failure — the script would have run without it. The
check strips a redundant bare `sudo` prefix in exactly that case and
writes the effective interpreter to
`${NOMAD_ALLOC_DIR}/sep_interpreter`; both artifact specs' `run-script`
steps now launch from that file. `exec-artifact` therefore changes from
a direct `xargs` exec to `sh -c`, which passes identical argv to the
payload.
The strip is deliberately narrow. Only a **bare** `sudo` first token
followed by a plain word is dropped, and only when the token the
invocation actually names does not resolve on the node. `sudo -u
postgres bash` is never stripped: `sudo -u` *lowers* privilege, so
dropping it would run the payload as root instead of as `postgres`. An
operator-supplied `/opt/x/sudo` that exists is kept for the same reason
— a binary named by path may be a wrapper that changes the target user.
**The check recognises a small grammar and declines everything else.**
It tokenizes with `sh` word-splitting; the launcher tokenizes with `env
-S`. These are different grammars, so a form only one of them
understands is passed through unchanged rather than resolved — quotes,
`$`, backticks, backslashes, an `env` first token, a first token
beginning with `-` (`env -S` parses leading options itself, so `-u FOO
bash` unsets `FOO` and runs `bash`), a leading `NAME=VALUE` (which `env
-S` applies *before* locating the command, so it can decide where the
command resolves), any non-absolute path (this step pins no `work_dir`
and `run-script` pins one, so the check cannot tell where the launcher
would resolve it), and any `sudo` option outside the two tables the
walker knows — it enumerates the options that take a value and the
options that take none, and declines every other `-*` rather than
assuming it takes none and resolving the word after it. `set -f` stops
it globbing where `env -S` does not. Those are deliberate
false-negatives: behaviour for them is exactly what it is today, and the
alternative error — aborting a working execution — is the one that
hurts.
**Resolution tests the exec bit, not just the name.** For a token
holding a slash the check uses `[ -x ] && [ ! -d ]` rather than `command
-v`. Under dash and busybox ash — the two most common node shells —
`command -v` reports a bare-existing path as found whatever its mode, so
a non-executable interpreter, or a directory, would have passed the
check and landed in `FAILED`: the outcome this step exists to separate
out. Only bash checks the mode, which is also why the suite, running
under a bash `/bin/sh`, could not have surfaced it. Bare names keep
`command -v`, which does search `PATH` for an executable.
`exec-python-artifact`'s check resolves `python3` rather than the
interpreter meta, because that spec's `run-script` always execs the venv
python and reads the meta only for a `"sudo "` prefix test. Resolving
the meta's own token there would abort a runnable execution for any
operator who maps `.py` to something else through `INTERPRETERS`.
**Supporting changes.** An Alembic revision widens `taskhistory.status`
from `VARCHAR(7)` to `VARCHAR(12)`: the column stores enum member
*names*, and there is no CHECK constraint on it, so the length is the
only DB-side gate. `_TERMINAL_STATUS_EVENT_MAP` gains an entry (it is
indexed, not `.get()`-ed, in `stop_task`). `alert_for_status` gains an
arm with a `:unlaunchable` dedup-key suffix plus its paired resolve on
the `SUCCESS` arm, so the incident can clear. The ATW support bundle
selects the check step's log for this status. Frontend status badges and
the `FINISHED_TASK_STATUSES` set gain the new member.
`is_finished()` gaining a member changes chain dispatch through
`is_terminal()`: a parent carrying `_chain_on_failure` now dispatches
its successor on `UNLAUNCHABLE`, consistent with `FAILED` / `STOPPED` /
`LOST` / `STALE`. That is intended. The two chain tests now **derive**
the non-success terminal set from the enum rather than spelling it out,
which is why no test turned red when the previous terminal status
landed.
## Verified against PostgreSQL
The suite runs on SQLite, which ignores `VARCHAR` length, so a green
suite proves nothing about the migration. Checked directly on PostgreSQL
16: the column is `character varying(7)` with no CHECK constraints
before and `character varying(12)` after; a row with `status =
'UNLAUNCHABLE'` writes successfully; and `downgrade()` remaps such rows
to `FAILED` before re-narrowing, which is what keeps it runnable once
the feature has been used.1 parent 4a44eb5 commit 12aa067
41 files changed
Lines changed: 1867 additions & 102 deletions
File tree
- app
- sep/apps/atw
- tasks
- db
- execution
- executors/nomad
- migrations/versions
- changelog.d
- frontend/packages
- api
- specs
- src/generated
- apps/atw/src
- framework/src
- components
- ScheduledTasksPanel
- TaskHistoryTable
- TaskLogViewer
- __tests__
- hooks
- tests/app
- sep
- apps/atw
- snapshots/openapi
- tasks
- db
- execution
- executors/nomad
- migrations
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
85 | 89 | | |
86 | 90 | | |
87 | 91 | | |
| |||
424 | 428 | | |
425 | 429 | | |
426 | 430 | | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
427 | 443 | | |
428 | | - | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
429 | 448 | | |
430 | | - | |
| 449 | + | |
431 | 450 | | |
432 | | - | |
433 | | - | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
434 | 455 | | |
435 | 456 | | |
436 | 457 | | |
| |||
441 | 462 | | |
442 | 463 | | |
443 | 464 | | |
| 465 | + | |
444 | 466 | | |
445 | 467 | | |
446 | 468 | | |
| |||
456 | 478 | | |
457 | 479 | | |
458 | 480 | | |
459 | | - | |
| 481 | + | |
460 | 482 | | |
461 | 483 | | |
462 | 484 | | |
| |||
540 | 562 | | |
541 | 563 | | |
542 | 564 | | |
543 | | - | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
544 | 568 | | |
545 | 569 | | |
546 | 570 | | |
| |||
0 commit comments