Commit Graph
38 Commits
Author SHA1 Message Date
Claude 994c2d67ab feat: add generate_job_file MCP tool (Stage 2 Task 22)
Exposes a single new MCP tool: generate_job_file(code) -> {script_path}.

Wiring follows the Stage 1 conventions (Pydantic body model for the
route, loguru DEBUG/INFO logging, summary+description for the startup
log, MCP arg-pattern via the body model so tools/call roundtrips long
code strings without 422-ing on FastAPI query length limits).

Flow:
  1. LLM calls generate_job_file(code=...)          -> {script_path}
  2. LLM calls prepare_submit_job(connection=...,
                                  script_path=...) -> {pending_id}
  3. User reviews the file + pending record
  4. LLM calls confirm_submit_job(pending_id=...)  -> spark-submit runs

The output directory is SPARK_EXECUTOR_JOBS_DIR (default ./data/jobs/,
gitignored, persists across container restarts via the existing
./data volume mount in docker-compose.yml).

3 new tests:
  - Unit: env-var override, default fallback in tmp cwd
  - Integration: end-to-end body call with a > FastAPI-query-limit code
    string (the canary test that would have caught the Stage 1
    query-params-422 bug)

Live MCP smoke verified: tools/list shows 14 tools (13 from Stage 1 +
the new one), tools/call generate_job_file returns the absolute path
under ./data/jobs/.
2026-06-25 10:19:59 +08:00
Claude 9340061b30 feat(job_writer): write PySpark code to disk with env-var override
spark_executor/core/job_writer.py provides write_job_file(code, jobs_dir=None)
that resolves the output directory in this order:
  1. explicit jobs_dir argument (for tests + programmatic override)
  2. SPARK_EXECUTOR_JOBS_DIR environment variable (operator override)
  3. DEFAULT_JOBS_DIR = './data/jobs' (gitignored, persists via volume mount)

Filenames are job_<UTC-stamp>_<random-hex>.py — same-second writes still
get distinct names via the random suffix, so concurrent agents won't
clobber each other.

8 tests cover: arg>env>default precedence, dir auto-creation, env-var
override, default fallback in a tmp cwd, and uniqueness under concurrent
calls.
2026-06-25 10:17:12 +08:00
Claude 516541a4df feat: Connection.master defaults to 'yarn'
YARN is the dominant use case, and the value of master for YARN is the
literal string 'yarn' (not a URL) — there is no useful variation. Making
it a default saves every YARN user from typing the same value every time:

    save_connection(name='prod')           # master defaults to 'yarn'
    save_connection(name='dev', master='spark://10.0.0.5:7077')  # override

Added a field_validator on master that catches common typos
('yarn-cluster', 'http://...', empty string) at save time with a clear
error message instead of letting them reach spark-submit.

Backward compatible: existing Connection records without an explicit
master field default to 'yarn' on load (Pydantic applies defaults on
deserialization), so already-saved data still works.

3 new tests cover the default, the validator, and that other cluster
managers (spark://, k8s://, local[N]) still pass.
2026-06-25 10:05:27 +08:00
Claude 504447048d feat(docker-compose.yml): remove healthcheck 2026-06-24 18:23:15 +08:00
Claude ce8dc1e2ba feat: add docker-compose.yml + .env.example
docker-compose.yml:
- Builds the Dockerfile, exposes :8000
- Mounts ./data (persistent) and ./hadoop-conf (ro) for cluster configs
- Forwards YARN_RESOURCE_MANAGER_URL via env var (fallback for REST client)
- Healthcheck via /openapi.json (cheap liveness probe; MCP initialize
  handshake would be more accurate but requires session-id plumbing)
- restart: unless-stopped for production
- Resource limit section commented out (server itself is lightweight)

.env.example:
- Documents the env vars compose expects
- .env is gitignored
2026-06-24 18:20:16 +08:00
Claude 565661371b fix(Dockerfile): openjdk 17 install error 2026-06-24 17:53:38 +08:00
Claude ff9dea0ba7 feat: log MCP tool list + descriptions on startup
main.py lifespan now enumerates every MCP tool with its description at
startup so operators can see at a glance what the server exposes.

The auto-generated '### Responses:' suffix that fastapi-mcp appends to
each tool description is truncated before logging to keep the startup
log scannable.

Docstrings added to all 12 route handlers in server.py so the MCP tool
descriptions are meaningful (without them, fastapi-mcp would emit empty
descriptions). Each route now carries a summary + description used by
the auto-generated OpenAPI schema.
2026-06-24 17:50:30 +08:00
Claude 093e51a9c2 feat: update Dockerfile mirror 2026-06-24 17:49:12 +08:00
Claude 33294af477 fix(Dockerfile): install openjdk-17-jre-headless for spark-submit
Spark 4.1.2 requires Java 17 at runtime (it boots a JVM via the
bin/spark-submit shell script). The previous Dockerfile only installed
curl, ca-certificates, and tar — leaving spark-submit unable to find
the 'java' binary.

A JRE is sufficient (not the full JDK) because spark-submit does not
compile Java at runtime — it loads pre-built JARs from $SPARK_HOME/jars/.
JRE-only keeps the image ~160 MB smaller than JDK-headless.

Sets JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 and prepends it to
PATH so spark-submit (which checks JAVA_HOME) finds the right JVM.

Combined with the prior yarn-REST rewrite, the runtime image now needs
exactly three things in addition to python:3.12-slim:
  - uv (from ghcr.io/astral-sh/uv)
  - openjdk-17-jre-headless
  - Spark 4.1.2 from Tsinghua mirror
2026-06-24 17:37:44 +08:00
Claude bfdf4fd1ff feat: update Dockerfile 2026-06-24 17:36:14 +08:00
Claude b3564737f7 refactor: replace yarn CLI shell-out with YARN REST API
yarn_client.py no longer invokes the 'yarn' binary via subprocess; it uses
httpx against /ws/v1/cluster/apps/* endpoints. This means the runtime image
no longer needs the Hadoop client installation — the only YARN-side
dependency left in the container is the config dir consumed by
spark-submit itself.

New model field:
  - Job.yarn_rm_url: str | None
  - PendingSubmission.yarn_rm_url: str | None (snapshotted at prepare)

prepare_submit_job snapshots Connection.yarn_rm_url into the pending
record (consistent with the existing master/deploy_mode/spark_conf
snapshot pattern); confirm_submit_job copies it onto the Job so
status/logs/kill can use it without re-looking-up the connection.

Resolution order for the RM URL at runtime:
  1. Job.yarn_rm_url (preferred — survives connection edits/deletes)
  2. Connection.yarn_rm_url fallback (if a future tool is added that
     doesn't go through a Job)
  3. YARN_RESOURCE_MANAGER_URL env var

Errors:
  - YarnConfigError (HTTP 4xx semantics) when URL is missing/malformed
  - YarnError for HTTP 4xx/5xx from the RM, network failures, missing
    state field, or unparseable log responses

10 new tests in test_yarn_client.py cover the REST surface:
success, 404, 5xx, missing state field, env-var fallback, malformed
URL, log 404 with log-aggregation hint, kill PUT body shape, and
httpx connection-error wrapping.
2026-06-24 17:33:36 +08:00
Claude 0d7b1d1ec3 feat: add Dockerfile 2026-06-24 17:07:23 +08:00
Claude 80df978206 feat: add requirements.txt 2026-06-24 16:53:47 +08:00
Claude 475c64cb69 feat: update spark executor title 2026-06-24 15:42:50 +08:00
Claude ce2eeb4bac feat: update memory 2026-06-24 15:20:29 +08:00
Claude b3eabe1fb6 feat: logging to file 2026-06-24 15:09:59 +08:00
Claude 5c7dcf57cc feat: structured DEBUG/INFO logging via loguru
- common/logging.py: improve format to timestamp|LEVEL|module:func:line - message
- core/ layer: DEBUG log every subprocess invocation (cmd, rc, byte counts),
  JSON load/dump events, parsed application_id. ERROR log on failures.
- tools/ layer: DEBUG log every public tool entry with key parameters,
  INFO log on business outcomes (saved/submitted/killed/...).
- New tests/unit/test_logging.py: capture loguru output via in-memory sink
  and assert DEBUG + INFO messages are emitted for representative flows.
2026-06-24 15:02:54 +08:00
Claude fad296591c fix: map KeyError->404 and ValueError->400 for MCP clients
Tool functions raise KeyError for unknown ids and ValueError for invalid
state transitions. Without handlers, FastAPI returned a bare 500
"Internal Server Error" to MCP clients — useless for an agent that needs
to know whether the id was wrong vs the cluster was down.

KeyError -> 404 with the message ("Unknown job_id: foo").
ValueError -> 400 with the message (e.g. "pending_id p_xyz is in status
'CANCELLED', not PENDING").
2026-06-24 14:59:00 +08:00
Claude 6b9f278294 fix: switch FastAPI routes to Pydantic body models for MCP tools/call
The plan's route signatures used query parameters, which worked for direct
HTTP callers and unit tests, but fastapi-mcp's HTTP transport passes
tools/call arguments as a JSON body. dict-typed parameters like
spark_conf arrived as a string and the route returned 422.

Refactor each route to take a single Pydantic body model (saved in
spark_executor/tools/requests.py). Underlying tool functions unchanged.

Integration tests in tests/integration/test_mcp_routes.py now exercise
the full body-based roundtrip (save_connection with spark_conf, prepare
→ list → get, list_connections with empty body).
2026-06-24 14:56:31 +08:00
Claude cc7eafaa16 feat: register 12 MCP tools on FastAPI app 2026-06-24 14:50:51 +08:00
Claude 0d60f9b246 feat: add kill_job tool 2026-06-24 14:44:10 +08:00
Claude 1cd3128522 feat: add get_job_logs tool with tail 2026-06-24 14:43:56 +08:00
Claude e443f96756 feat: add get_job_status tool 2026-06-24 14:43:41 +08:00
Claude 63dcae8d75 test: cover cancel_pending_job 2026-06-24 14:43:26 +08:00
Claude c734823e79 feat: add list_pending_jobs and get_pending_job 2026-06-24 14:40:54 +08:00
Claude 2f57c98aa6 feat: add confirm_submit_job (two-step submit flow) 2026-06-24 14:40:33 +08:00
Claude 7f08f0590a feat: add prepare_submit_job (no spark-submit invocation) 2026-06-24 14:40:01 +08:00
Claude 0aa3c1eaf5 feat: add JSON-backed PendingStore 2026-06-24 14:39:13 +08:00
Claude 82dafd357f test: cover delete_connection 2026-06-24 14:38:06 +08:00
Claude 3a08702c06 test: cover get_connection 2026-06-24 14:38:06 +08:00
Claude 1450ad1779 test: cover list_connections 2026-06-24 14:38:06 +08:00
Claude 00592f5cf8 feat: add save_connection tool 2026-06-24 14:37:58 +08:00
Claude 8dd39367ed feat: add JSON-backed ConnectionStore 2026-06-24 14:37:09 +08:00
Claude 31d28baace feat: add in-memory JobStore 2026-06-24 14:36:05 +08:00
Claude a9f64d9942 feat: add yarn client (status, logs, kill) 2026-06-24 14:35:30 +08:00
Claude b4b2ad534a feat: add spark-submit command builder and runner 2026-06-24 14:35:07 +08:00
Claude e6902b00a0 feat: add spark-submit output parser 2026-06-24 14:34:40 +08:00
Claude 864093f1fb chore: add pytest dev-deps and pydantic models (Job, Connection, PendingSubmission) 2026-06-24 14:34:09 +08:00