Foreman is a Rust based job scheduler and executor agent.
- π¬ Language agnostic: Jobs are processed in containerised environments.
- π Secure by default: Self-hostable behind a NAT gateway, without the need to be exposed publically over the internet.
- π Fast, efficient and lightweight: Compiles to a single binary executable
Install the Rust toolchain via rustup.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shCompile release build.
cargo build --releaseThe foreman binary expects to find a configuration file named foreman.toml in one of the following locations:
- At a path specified by the
FOREMAN_CONFIGenvironment variable. e.g.FOREMAN_CONFIG=/path/to/foreman.toml - The current working directory
/etc/foreman/foreman.toml$HOME/.foreman/foreman.toml
Refer to example.foreman.toml for an explanation of the various configuration options and their defaults.
Alternatively, config values can be specified via environment variables.
Each environment variable should be prefixed with FOREMAN_.
For example ...
[core]
port = 8080... can be specified via the environment variable FOREMAN_CORE_PORT=8080.
Values set via environment variables will override any values specified in foreman.toml.
Labels are optional key/value pairs which you can define in the [core.labels] section of foreman.toml.
These labels will be sent to your control server in the header x-foreman-labels when polling for jobs.
Your control server MAY discriminate requests based on these labels and only deliver matching jobs.
For example, you may choose to identify features of the forman agent host by defining labels such as cpu = 4, gpu = true etc.
Similarly, your control server may maintain multiple job queues.
Labels allow you to assign agents to specific queues e.g. queue = high-priority, queue = low-priority etc.
Labels are formatted as a comma-separated list of key/value pairs e.g cpu=4,gpu=true,queue=high-priority.
Both keys and values will be URL-encoded so you are free to use '=' and ',' in your labels.
The trade-off here is you need to remember to URL-decode before usage on your control server.
The order of the key/values is NOT guaranteed.
Foreman (this project) is a Rust-based job scheduling agent which retrieves jobs from a control server and executes them inside a containerised environment. It is designed to be run in private subnets behind a NAT gateway, without the need to be exposed to the internet.
Foreman is similar in spirit to a CI/CD agent but more generic.
At a high level, a control server is a responsible for the following:
- Responds to requests from foreman agents returning zero-or-more jobs in response to a request
- Retrieves job execution statuses from foreman agents
The implementation of a control server is not within the scope of this project, though a reference implementation is included for development purposes. See the Development section below for more information.
A job defines a single task that needs to be executed. It can be anything from running a script to deploying an application.
An executor is responsible for executing jobs on behalf of a foreman agent.
Foreman manages Docker as it's job executor.
A custom bridge network is created on start-up which all containers created by foreman are added to.
By default the network is named foreman.
This can be changed via the core.network_name configuration option.
The following sequence diagram illustrates the flow of a job execution request between foreman, a control server and an executor.
sequenceDiagram
participant CS as Control Server
participant F as Foreman
participant E as Executor (Docker)
F->>CS: GET /job
CS-->>F: JSON
F->>E: Start container
E->>F: GET /job
F-->>E: JSON
E->>E: Execute job
E->>F: PUT /job/<job-id>
F->>CS: PUT /job/<job-id>
CS-->>F: OK
F->>E: Stop container
F->>F: Wait
F->>E: Remove container
A job returned by a control server is expected to conform to the following schema (denoted here as a Typescript interface):
interface Job {
/**
* Unique identifier for the job
*/
id: string;
/**
* Docker image to use for the job
*/
image: string;
/**
* Command to run in the container
*/
command?: string[];
/**
* Body of the job, which can be any type
*/
body: any;
/**
* Environment variables for the job
*/
env?: { [key: string]: string };
/**
* Callback URL for the job
*/
callbackUrl: string;
/**
* Whether to always pull the Docker image before creating a container
*/
alwaysPull?: boolean;
}Some things to note:
- The
idis used to uniquely identify each job and should be unique within your control server. Using a UUID is recommended. - The
callbackUrldoes not need to be the same server as your control server (though you will likely still need to signal back to your control server when the job completes). - Avoid setting
alwaysPull: trueas it will slow down the creation of job containers. You should only need this if your image tags are mutable which is generally considered bad practice. - The job schema is also available in JSON schema format in job.schema.json.
Foreman will create a container based on the image field defined in a job, pulling the image if necessary.
The foreman agent exposes a simple REST API which job containers are expected to communicate with when dealing with their associated job.
When a container is ready it MUST perform a GET request to the URL contained in the FOREMAN_GET_JOB_ENDPOINT environment variable.
This endpoint returns a JSON object containing the job id and body fields from the original job received from the control server.
Likewise the container MUST perform a PUT request to the URL contained in the FOREMAN_PUT_JOB_ENDPOINT environment variable with updates to the job's status.
When sending requests to this endpoint the only requirement is the following headers must be set in the request.
| name | required | description |
|---|---|---|
| x-foreman-job-status | YES | MUST be either 'running' or 'completed' |
| x-foreman-job-progress | NO | A floating point number representing the progress of the job. Defaults to 0.0 if undefined. |
Requests sent to this endpoint are forwarded to the job's callbackUrl as-is.
The completed status is a terminal state and can be set at-most once per job.
It is invalid to send a PUT request with x-foreman-job-status set to running on a completed job.
A container becomes eligible for removal once it's status changes to completed.
Build the test job image so it is available on your local machine. This image is used by jobs produced by the reference control server (see next step).
cd examples/test_job_image
docker build -t foreman-test-job-image:latest .
A reference control server, using Typescript and Deno, is defined in examples/control_server.
To run the server, cd into the examples/control_server directory and run:
deno run -A index.tsThe server runs on port 8888 and listens for GET requests on the /job endpoint.
Update your foreman.toml file to contain the following configuration.
This allows code running inside the test image to reach the foreman process running on your host machine.
[core]
url = 'http://localhost:8888/job'
token = 'MY-SUPER-SECRET-TOKEN'
hostname = "host.docker.internal"
extra_hosts = ["host.docker.internal:host-gateway"]In a separate terminal, start foreman.
cargo run