Skip to content

feat: decouple Fournos from FORGE — multi-engine support - #1

Closed
ashtarkb wants to merge 75 commits into
mainfrom
multi-engine-support
Closed

ashtarkb wants to merge 75 commits into
mainfrom
multi-engine-support

Conversation

@ashtarkb

Copy link
Copy Markdown
Owner

Summary

Make Fournos engine-agnostic so any execution engine (not just FORGE) can run on the platform. Introduces two modes: a built-in generic pipeline (zero-code path) and a custom engine contract (/opt/fournos/entrypoint).

This is the Fournos-side counterpart to the FORGE entrypoint contract work (ashtarkb/forge#2).

Motivation

Previously, Fournos was tightly coupled to FORGE:

  • The resolve Job template (config/forge/resolve_job.yaml) contained ~90 lines of inline FORGE-specific bash
  • The deploy Makefile target applied FORGE workflows
  • Settings, docs, and code all referenced FORGE as the only engine

This meant only FORGE jobs could run on Fournos. Teams with simpler workloads (e.g. a Python benchmark script in a container) had to build a full FORGE project just to use Fournos scheduling.

Architecture

After this change, Fournos supports two modes:

Mode 1: Generic Pipeline (zero Fournos code in your project)

Users provide only a container image + FournosJob YAML:

spec:
  pipeline: fournos-generic
  executionEngine:
    generic:
      image: quay.io/myteam/my-job:latest
      command: ["python", "run.py"]
      env: { MODEL: gpt-4, RATE: "10" }

The built-in runner creates a child K8s Job with the user's container. No SDK, no /opt/fournos/entrypoint, no Tekton Pipeline to write.

Mode 2: Custom Engine (advanced, e.g. FORGE)

Engines provide their own Tekton Pipeline/Task and a container image with /opt/fournos/entrypoint. Full lifecycle control. spec.executionEngine.<name> is opaque to Fournos.


Changes

Core: Engine-agnostic resolve template

  • Deleted config/forge/resolve_job.yaml (90 lines of inline FORGE bash)
  • New config/resolve/resolve_job.yaml — engine-agnostic template that just calls command: ["/opt/fournos/entrypoint"]. The image is selected per-Pipeline via fournos.dev/resolve-image annotation.
  • Modified fournos/settings.py — default path: config/forge/ → config/resolve/
  • Modified fournos/core/resolve.py — docstring updated to reflect engine-agnostic semantics
  • Modified fournos/core/tekton.py — set serviceAccountName: fournos on PipelineRun taskRunTemplate

Built-in generic pipeline (config/generic/)

File Purpose
pipeline.yaml fournos-generic Tekton Pipeline — references fournos-generic-step Task
task.yaml fournos-generic-step Tekton Task — runs the runner image
runner.py Generic runner: fetches FournosJob spec, reads spec.executionEngine.generic.*, creates child K8s Job, polls for completion, streams logs, cleans up
Containerfile Runner image: UBI9 + oc CLI + PyYAML + runner.py → /opt/fournos/entrypoint
rbac.yaml ClusterRole: read FournosJobs, create/manage child Jobs, stream pod logs, common workload resources
requirements.txt PyYAML dependency

Optional Engine SDK (fournos/sdk/engine.py)

  • FournosEngine class with @on_resolve / @on_run decorators
  • Handles all plumbing: env var reading, FournosJob fetch, config extraction from spec.executionEngine.<engine_name>, resolve/run routing, error handling
  • EngineContext dataclass with fjob_name, namespace, step, artifact_dir, fjob_spec
  • For advanced engines that want Python integration — most users will never need this

Contract documentation (docs/execution-engine-contract.md)

  • Formalizes Mode 1 (generic) and Mode 2 (custom engine)
  • Documents /opt/fournos/entrypoint requirements, environment variables, resolve phase behavior
  • References the optional SDK

Dev/test infrastructure

  • dev/mock-resolve/Dockerfile — now creates /opt/fournos/entrypoint symlink (matches production contract)
  • dev/mock-resolve/resolve_job.yaml — explicit command: ["/opt/fournos/entrypoint"] + missing env vars
  • dev/mock-generic/ — new directory: mock runner image, mock user image, sample FournosJob for local kind testing
  • dev/setup.sh — +110 lines: builds/loads mock generic images, applies generic Pipeline/Task/RBAC to kind
  • Makefile — deploy target now applies generic assets instead of FORGE workflows; new dev-test-generic target

Documentation updates

  • README.md — updated quick-start, deployment instructions, settings table, added generic pipeline docs
  • Fournos_Design_Document.md — rewritten Sections 7-8 for multi-engine architecture

File inventory

File Status
config/forge/resolve_job.yaml 🔴 Deleted
config/resolve/resolve_job.yaml 🟢 New
config/generic/pipeline.yaml 🟢 New
config/generic/task.yaml 🟢 New
config/generic/runner.py 🟢 New
config/generic/Containerfile 🟢 New
config/generic/rbac.yaml 🟢 New
config/generic/requirements.txt 🟢 New
fournos/sdk/__init__.py 🟢 New
fournos/sdk/engine.py 🟢 New
docs/execution-engine-contract.md 🟢 New
dev/mock-generic/* (5 files) 🟢 New
fournos/settings.py 🟡 Modified
fournos/core/resolve.py 🟡 Modified
fournos/core/tekton.py 🟡 Modified
dev/mock-resolve/Dockerfile 🟡 Modified
dev/mock-resolve/resolve_job.yaml 🟡 Modified
dev/setup.sh 🟡 Modified
Makefile 🟡 Modified
Fournos_Design_Document.md 🟡 Modified
README.md 🟡 Modified

Key metrics

Metric Value
Lines of FORGE bash removed from Fournos ~90
Lines of FORGE code remaining in Fournos 0
Engine modes supported 2 (generic + custom)
New files 16
Modified files 9
Deleted files 1

kpouget and others added 30 commits June 25, 2026 11:28
Replaced 'avasilevskii' with 'ashtarkb' in the OWNERS file.
Web dashboard for managing Fournos performance testing jobs.
Provides job submission, live monitoring, scheduling, and
historical results -- built with FastAPI, HTMX, and PostgreSQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Switch ClusterRoleBinding to namespace-scoped RoleBinding for least-privilege access
- Add securityContext to init and dashboard containers (drop capabilities, read-only root)
- Wrap blocking K8s API calls with asyncio.to_thread to avoid event-loop starvation
- Add configurable K8s request timeout (K8S_REQUEST_TIMEOUT env var)
- Pin dependency versions in requirements.txt
- Use kustomize configMapGenerator for projects ConfigMap instead of static manifest
- Add OOB HTMX swaps for live header status, completion banner, and MLflow links
- Fix htmx-sse.js exponential backoff (Math.pow instead of bitwise XOR)
- Deduplicate watcher archive events when phase/message unchanged
- Add resilience to malformed project entries in forge_discovery
- Reduce Dockerfile workers to 1 and disable Jinja2 cache_size=0 in favour of auto_reload=False
- Update README with corrected deploy instructions and project structure

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add optional PULL_PULL_SHA field to submit form, injected into
  spec.env so the resolve job checks out Forge code from a specific PR
- Show the MCP Gateway version field only when mcp_gateway project is
  selected; hide and clear it for all other projects

Co-authored-by: Cursor <cursoragent@cursor.com>
- Stop the background log reader thread on client disconnect via
  asyncio.Event; catch QueueFull to avoid dropping the executor thread
- Collect cluster filter options from the full filtered job set before
  paginating so the dropdown includes all clusters, not just the
  current page
- Rename dashboard-clusterrolebinding.yaml to dashboard-rolebinding.yaml
  to match the actual RoleBinding kind inside

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the manual SHA input with a searchable dropdown that lists
open PRs from the Forge repo (public API, no token needed). Users
can filter by PR number, title or author and the HEAD SHA is filled
automatically. Also sort pods by creation timestamp so they appear
in execution order.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Alberto Perdomo <aperdomo@redhat.com>
…conn-error-sync

fix: Catch SSL/conn errors while synching vault
Co-authored-by: alberto <aperdomo@redhat.com>
fournos: set a 24h timeout for the task completion
config/forge/resolve_job.yaml: sync with Forge Tasks
config/forge/resolve_job.yaml: adapt after Forge Task sync
ashtarkb and others added 27 commits August 24, 2026 09:24
This code path is unreachable in practice (lockUntil is already
validated at creation), and only concerns the fjob creator rather
than cluster admins, so warning-level logging was too noisy per
review feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
Secure the dashboard with an OAuth proxy sidecar that authenticates
users via OpenShift's built-in OAuth server. Add cert-manager
integration for automatic Let's Encrypt certificate issuance.

- Add kustomize overlay patches for OAuth proxy sidecar, service TLS,
  and ServiceAccount OAuth redirect annotation
- Add Route, Certificate CR, and ClusterIssuer manifests
- Update README with full deployment instructions and troubleshooting
- Gitignore deployment-specific files (secrets, cluster config)
- Replace params.env.example with oauth-cookie-secret.yaml.example

Co-authored-by: Cursor <cursoragent@cursor.com>
Add a TTL field to FournosJob that defines the delay after job
termination before the CR is automatically deleted. When not set,
jobs are never auto-pruned (preserving current behavior).

- Add spec.ttl to CRD schema (Go duration format: "12h", "7d", etc.)
- Add duration parser utility (fournos/core/duration.py)
- Add _gc_expired_jobs() to the operator GC loop
- Add unit tests for TTL logic

Co-authored-by: Cursor <cursoragent@cursor.com>
Add cluster unlock functionality to the cluster lock only mechanism
Co-authored-by: Kevin Pouget <kpouget@redhat.com>
- Add status.completionTime to CRD schema, set atomically on all
  terminal transitions via new set_terminal_phase() helper
- Validate spec.ttl in on_create; fail immediately if unparseable
- Refactor _get_completion_time to read status.completionTime with
  fallback to metadata.creationTimestamp for creation-time failures
- Downgrade GC log for invalid/missing TTL from warning to debug
- Fix indentation from accepted early-continue suggestion
- Update tests to match completionTime-based logic

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…gitops

fournos-ui: add OpenShift OAuth proxy and Let's Encrypt TLS
Drop the creationTimestamp fallback, require a terminal phase before
reading completionTime, and stamp completionTime on create-time failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep main's ValueError-based timestamp parsing and route those
failures through set_terminal_phase so completionTime is still set.

Co-authored-by: Cursor <cursoragent@cursor.com>
Kopf drops the status patch if the handler raises. Coerce a non-terminal
phase to Failed, reject overflowing TTL values, and skip GC deletes that
lose a resourceVersion race.

Co-authored-by: Cursor <cursoragent@cursor.com>
feat: add spec.ttl for automatic job cleanup after completion
…bac-fix

fix: allow TTL garbage collection to delete jobs
…ion-sample-project

Fix connectivity smoke-test sample
hacks: sync_vault_secrets: remove from fournos, move to forge
Make Fournos engine-agnostic so non-FORGE jobs can run on the platform.

Core changes:
- Introduce /opt/fournos/entrypoint contract: every engine image provides
  an executable at this fixed path. The resolve Job template and Tekton
  Tasks invoke it uniformly — no engine-specific logic in Fournos YAML.
- Move resolve Job template from config/forge/ to config/resolve/ (now
  engine-agnostic). Delete the old FORGE-specific template.
- Update settings.py default: config/forge/ → config/resolve/.
- Update resolve.py docstring to reflect engine-agnostic semantics.

Built-in generic pipeline (config/generic/):
- New fournos-generic Pipeline + Task + runner image + RBAC.
- The runner reads spec.executionEngine.generic.{image, command, args, env}
  from the FournosJob and launches the user's container as a child K8s Job.
- Users provide only a container image + FournosJob YAML. Zero Fournos
  awareness needed in their project.

Optional Engine SDK (fournos/sdk/engine.py):
- FournosEngine class with @on_resolve / @on_run decorators.
- Handles all plumbing: env vars, FournosJob fetch, config extraction,
  resolve/run routing, error handling.
- For advanced engines that want Python integration without raw bash.

Execution engine contract docs (docs/execution-engine-contract.md):
- Formalizes Mode 1 (generic) and Mode 2 (custom engine).
- Documents env vars, resolve phase, entrypoint requirements.

Dev/test infrastructure:
- dev/mock-resolve: updated to follow /opt/fournos/entrypoint contract.
- dev/mock-generic: new mock images + sample FournosJob for local testing.
- dev/setup.sh: builds and loads generic mock images into kind cluster.
- Makefile: deploy target now applies generic assets; new dev-test-generic.
- tekton.py: set serviceAccountName on PipelineRun taskRunTemplate.

Documentation:
- Updated README.md, Fournos_Design_Document.md to reflect multi-engine
  architecture, generic pipeline, and decoupled deployment model.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants