Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ touch config.json && echo '<json-config-content>' > config.json
sudo mv config.json /etc/n8n-task-runners.json
```

Alternatively, use this environment variable to specify the config file path:

```sh
export N8N_RUNNERS_CONFIG_PATH=/path/to/your/config.json
```

3. Make your changes.

4. Build launcher:
Expand Down
51 changes: 6 additions & 45 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,49 +44,10 @@ sequenceDiagram

## Config file

Example config file at `/etc/n8n-task-runners.json`:

```json
{
"task-runners": [
{
"runner-type": "javascript",
"workdir": "/usr/local/bin",
"command": "/usr/local/bin/node",
"args": [
"--disallow-code-generation-from-strings",
"--disable-proto=delete",
"/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"
],
"health-check-server-port": "5681",
"allowed-env": ["PATH", "GENERIC_TIMEZONE"],
"env-overrides": {
"N8N_RUNNERS_TASK_TIMEOUT": "80",
"N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT": "120",
"N8N_RUNNERS_MAX_CONCURRENCY": "3",
"NODE_FUNCTION_ALLOW_BUILTIN": "crypto",
"NODE_FUNCTION_ALLOW_EXTERNAL": "moment",
"NODE_OPTIONS": "--max-old-space-size=4096"
}
},
{
"runner-type": "python",
"workdir": "/usr/local/bin",
"command": "/usr/local/bin/python",
"args": [
"/usr/local/lib/python3.13/site-packages/n8n/task-runner-python/main.py"
],
"health-check-server-port": "5682",
"allowed-env": ["PATH", "GENERIC_TIMEZONE"],
"env-overrides": {
"N8N_RUNNERS_TASK_TIMEOUT": "30",
"N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT": "30",
"N8N_RUNNERS_MAX_CONCURRENCY": "2"
}
}
]
}
```
The launcher reads its config file from `/etc/n8n-task-runners.json` by default, or from the file path specified by the `N8N_RUNNERS_CONFIG_PATH` environment variable.

For an example, refer to the [config file](https://github.com/n8n-io/n8n/blob/master/docker/images/runners/n8n-task-runners.json) used in the [`n8nio/runners`](https://hub.docker.com/r/n8nio/runners) Docker image.


| Property | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -95,8 +56,8 @@ Example config file at `/etc/n8n-task-runners.json`:
| `command` | Command to start the task runner. |
| `args` | Args and flags to use with `command`. |
| `health-check-server-port` | Port for the runner's health check server. When a single runner is configured, this is optional and defaults to `5681`. When multiple runners are configured, this is required and must be unique per runner.
| `allowed-env` | Env vars that the launcher will pass through from its own environment to the runner. See [environment](environment.md). |
| `env-overrides` | Env vars that the launcher will set directly on the runner. See [environment](environment.md). |
| `allowed-env` | Env vars that the launcher will pass through from its own environment to the runner. See [environment variables](#environment-variables).
| `env-overrides` | Env vars that the launcher will set directly on the runner. See [environment variables](#environment-variables).

## Environment variables

Expand Down
16 changes: 9 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
"github.com/sethvargo/go-envconfig"
)

var configPath = "/etc/n8n-task-runners.json"

const (
// EnvVarHealthCheckPort is the env var for the port for the launcher's health check server.
EnvVarHealthCheckPort = "N8N_RUNNERS_LAUNCHER_HEALTH_CHECK_PORT"
Expand Down Expand Up @@ -52,6 +50,9 @@ type BaseConfig struct {
// RunnerHealthCheckServerHost is the host for all runners' health check servers.
RunnerHealthCheckServerHost string `env:"N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST, default=127.0.0.1"`

// ConfigPath is the path to the runners config file. Default: `/etc/n8n-task-runners.json`.
ConfigPath string `env:"N8N_RUNNERS_CONFIG_PATH, default=/etc/n8n-task-runners.json"`

// Sentry is the Sentry config for the launcher, a subset of what is defined in:
// https://docs.sentry.io/platforms/go/configuration/options/
Sentry *SentryConfig
Expand Down Expand Up @@ -92,7 +93,7 @@ type RunnerConfig struct {
}

// LoadLauncherConfig loads the launcher's base config from the launcher's environment and
// loads runner configs from the config file at `/etc/n8n-task-runners.json`.
// loads runner configs from the config file specified by N8N_RUNNERS_CONFIG_PATH.
func LoadLauncherConfig(runnerTypes []string, lookuper envconfig.Lookuper) (*LauncherConfig, error) {
ctx := context.Background()

Expand Down Expand Up @@ -131,7 +132,7 @@ func LoadLauncherConfig(runnerTypes []string, lookuper envconfig.Lookuper) (*Lau

// runners

runnerConfigs, err := readLauncherConfigFile(runnerTypes)
runnerConfigs, err := readLauncherConfigFile(baseConfig.ConfigPath, runnerTypes)
if err != nil {
cfgErrs = append(cfgErrs, err)
}
Expand All @@ -146,9 +147,10 @@ func LoadLauncherConfig(runnerTypes []string, lookuper envconfig.Lookuper) (*Lau
}, nil
}

// readLauncherConfigFile reads the config file at `/etc/n8n-task-runners.json` and
// readLauncherConfigFile reads the config file at the specified path and
// returns the runner config(s) for the requested runner type(s).
func readLauncherConfigFile(runnerTypes []string) (map[string]*RunnerConfig, error) {
func readLauncherConfigFile(configPath string, runnerTypes []string) (map[string]*RunnerConfig, error) {
// #nosec G304 -- configPath is controlled by system administrator via environment variable
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to open config file at %s: %v", configPath, err)
Expand All @@ -164,7 +166,7 @@ func readLauncherConfigFile(runnerTypes []string) (map[string]*RunnerConfig, err
taskRunnersNum := len(fileConfig.TaskRunners)

if taskRunnersNum == 0 {
return nil, errs.ErrMissingRunnerConfig
return nil, fmt.Errorf("config file at %s contains no task runners", configPath)
}

runnerConfigs := make(map[string]*RunnerConfig)
Expand Down
21 changes: 11 additions & 10 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestLoadConfig(t *testing.T) {
envVars: map[string]string{
"N8N_RUNNERS_AUTH_TOKEN": "test-token",
"N8N_RUNNERS_TASK_BROKER_URI": "http://localhost:5679",
"N8N_RUNNERS_CONFIG_PATH": testConfigPath,
"SENTRY_DSN": "https://[email protected]/123",
},
runnerType: "javascript",
Expand All @@ -48,6 +49,7 @@ func TestLoadConfig(t *testing.T) {
envVars: map[string]string{
"N8N_RUNNERS_AUTH_TOKEN": "test-token",
"N8N_RUNNERS_TASK_BROKER_URI": "http://127.0.0.1:5679",
"N8N_RUNNERS_CONFIG_PATH": testConfigPath,
"SENTRY_DSN": "https://[email protected]/123",
},
runnerType: "javascript",
Expand All @@ -57,9 +59,7 @@ func TestLoadConfig(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configPath = testConfigPath

err := os.WriteFile(configPath, []byte(tt.configContent), 0600)
err := os.WriteFile(testConfigPath, []byte(tt.configContent), 0600)
require.NoError(t, err, "Failed to write test config file")

lookuper := envconfig.MapLookuper(tt.envVars)
Expand Down Expand Up @@ -93,17 +93,19 @@ func TestConfigFileErrors(t *testing.T) {
envVars: map[string]string{
"N8N_RUNNERS_AUTH_TOKEN": "test-token",
"N8N_RUNNERS_TASK_BROKER_URI": "http://localhost:5679",
"N8N_RUNNERS_CONFIG_PATH": testConfigPath,
},
},
{
name: "empty task runners array",
configContent: `{
"task-runners": []
}`,
expectedError: "found no task runner configs",
expectedError: "contains no task runners",
envVars: map[string]string{
"N8N_RUNNERS_AUTH_TOKEN": "test-token",
"N8N_RUNNERS_TASK_BROKER_URI": "http://localhost:5679",
"N8N_RUNNERS_CONFIG_PATH": testConfigPath,
},
},
{
Expand All @@ -121,16 +123,15 @@ func TestConfigFileErrors(t *testing.T) {
envVars: map[string]string{
"N8N_RUNNERS_AUTH_TOKEN": "test-token",
"N8N_RUNNERS_TASK_BROKER_URI": "http://localhost:5679",
"N8N_RUNNERS_CONFIG_PATH": testConfigPath,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configPath = testConfigPath

if tt.configContent != "" {
err := os.WriteFile(configPath, []byte(tt.configContent), 0600)
err := os.WriteFile(testConfigPath, []byte(tt.configContent), 0600)
require.NoError(t, err, "Failed to write test config file")
}

Expand Down Expand Up @@ -250,11 +251,11 @@ func TestBackwardsCompatibilityPortDefaults(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configPath = filepath.Join(t.TempDir(), "test-config.json")
err := os.WriteFile(configPath, []byte(tt.configContent), 0600)
testConfigPath := filepath.Join(t.TempDir(), "test-config.json")
err := os.WriteFile(testConfigPath, []byte(tt.configContent), 0600)
require.NoError(t, err)

configs, err := readLauncherConfigFile(tt.runnerTypes)
configs, err := readLauncherConfigFile(testConfigPath, tt.runnerTypes)

if tt.expectError {
assert.Error(t, err)
Expand Down
3 changes: 0 additions & 3 deletions internal/errs/errs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,4 @@ var (

// ErrNegativeAutoShutdownTimeout is returned when the auto shutdown timeout is a negative integer.
ErrNegativeAutoShutdownTimeout = errors.New("negative auto-shutdown timeout - N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT must be >= 0")

// ErrMissingRunnerConfig is returned when the config file does not contain any runner configs.
ErrMissingRunnerConfig = errors.New("found no task runner configs at /etc/n8n-task-runners.json")
)
Loading