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
4 changes: 2 additions & 2 deletions site-docs/docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ $ deploy configure [OPTIONS] INSTANCE_NAME [SSH_HOST] [REPO_URL]
* `--repo-branch TEXT`: Git branch to clone and track (defaults to the repository's default branch).
* `--recreate / --no-recreate`: Re-create existing artifacts (venv, odoo-config, systemd unit) instead of skipping them. \[default: no-recreate\]
* `--watch`: Stream service logs with journalctl after a successful configure. Also merge with odoo and click-odoo-update logs if applicable.
* `--steps TEXT`: Comma-separated steps to run, or 'all'. Available: dir, pg, gitaggregate, venv, config, unit. \[default: all\]
* `--except TEXT`: Comma-separated steps to skip. Available: dir, pg, gitaggregate, venv, config, unit.
* `--steps TEXT`: Comma-separated steps to run, or 'all'. Available: dir, pg, gitaggregate, venv, config, env, unit. \[default: all\]
* `--except TEXT`: Comma-separated steps to skip. Available: dir, pg, gitaggregate, venv, config, env, unit.
* `--dry-run`: Go through all steps without running any writing/destructive commands.
* `--help`: Show this message and exit.

Expand Down
13 changes: 13 additions & 0 deletions site-docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ odoo-myproject-production:
- myproject_staging
- myproject_integration

# Odoo config overrides — written to config/odoo.conf by `configure`
config:
workers: 4
limit_time_cpu: 600

# Environment variables — written to config/server.env by `configure`.
# Merged over the built-in thread-limit defaults (the value here wins).
env:
ODOO_SESSION_REDIS: 1
ODOO_SESSION_REDIS_HOST: localhost

# python / service only
exec_start: myapp.main:app # module path for python; verbatim for service
build: npm ci && npm run build # service only
Expand Down Expand Up @@ -74,6 +85,8 @@ odoo-myproject-production:
| `db` | string or list of string | `update` | Target database name (Odoo only). Can be a list for multiple names. |
| `exec_start` | string | `configure` | Entry point for python/service systemd unit. |
| `build` | string | `configure`, `update` | Build command for `service` type. |
| `config` | mapping | `configure` | Odoo config overrides written to `config/odoo.conf` (Odoo only). |
| `env` | mapping | `configure` | Environment variables written to `config/server.env`, merged over the built-in thread-limit defaults (Odoo only). |
| `hooks` | mapping | `update` | Lifecycle hooks — see [Hooks](hooks.md). |

## Multiple instances
Expand Down
69 changes: 69 additions & 0 deletions tests/test_configure_server_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from trobz_deploy.cli import app
from trobz_deploy.command.configure import _render_server_env
from trobz_deploy.utils.executor import ExecutorError


@pytest.fixture
def runner():
return CliRunner()


def test_render_server_env_emits_key_value_lines():
rendered = _render_server_env({"A": 1, "B": "x"})
assert rendered == "A=1\nB=x\n"


def _executor_mock(*, env_exists: bool):
"""Executor mock where config/server.env may or may not already exist."""
mock = MagicMock()
mock.capture.side_effect = lambda cmd, cwd=None, dry_run=False: "/home/deploy" if cmd == "echo $HOME" else ""

def run_side_effect(cmd, cwd=None, check=True, dry_run=False):
if not env_exists and cmd.startswith("test -f") and cmd.endswith("server.env"):
msg = "not found"
raise ExecutorError(msg)
return ""

mock.run.side_effect = run_side_effect
return mock


def _invoke(runner, cfg, *, env_exists):
with (
patch("trobz_deploy.command.configure.Executor") as MockExecutor,
patch("trobz_deploy.command.configure.load_config", return_value=cfg),
):
mock_exec = _executor_mock(env_exists=env_exists)
MockExecutor.return_value = mock_exec
result = runner.invoke(
app,
["configure", "odoo-myapp-staging", "--type", "odoo", "--steps", "env"],
)
return result, mock_exec


def test_server_env_writes_defaults_plus_overrides(runner):
cfg = {"version": "17.0", "env": {"ODOO_SESSION_REDIS": 1, "OMP_NUM_THREADS": 4}}
result, mock_exec = _invoke(runner, cfg, env_exists=False)

assert result.exit_code == 0
written = mock_exec.write_file.call_args.args[0]
assert "OPENBLAS_NUM_THREADS=1\n" in written # default kept
assert "ODOO_SESSION_REDIS=1\n" in written # extra env added
assert "OMP_NUM_THREADS=4\n" in written # user value wins over default
assert "OMP_NUM_THREADS=1\n" not in written


def test_server_env_skips_when_exists_without_recreate(runner):
result, mock_exec = _invoke(runner, {"version": "17.0"}, env_exists=True)

assert result.exit_code == 0
assert "server.env already exists" in result.output
mock_exec.write_file.assert_not_called()
5 changes: 4 additions & 1 deletion tests/test_configure_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ def test_step_all_runs_every_step_for_odoo(runner):
assert any("gitaggregate" in cmd for cmd in commands)
assert any("odoo-venv create" in cmd for cmd in commands)
assert any("odoo-config create --version 17.0" in cmd for cmd in commands)
mock_exec.write_file.assert_called_once()
# Two file writes: config/server.env and the systemd unit.
written = [call.args[0] for call in mock_exec.write_file.call_args_list]
assert mock_exec.write_file.call_count == 2
assert any("OMP_NUM_THREADS=1" in content for content in written)
assert any("systemctl --user enable --now" in cmd for cmd in commands)


Expand Down
46 changes: 45 additions & 1 deletion trobz_deploy/command/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,27 @@ def _ensure_postgres_user(executor: Executor, instance_name: str, dry_run: bool
"gitaggregate": "Run gitaggregate if needed",
"venv": "Creating the venv",
"config": "Generating Odoo config",
"env": "Generating server.env",
"unit": "Installing systemd unit",
}

ODOO_CONFIG_FILENAME = "odoo.conf"

# Default env vars written to config/server.env for every odoo instance.
# Single-thread the BLAS/OpenMP backends so workers don't oversubscribe cores.
DEFAULT_SERVER_ENV: dict[str, Any] = {
"OMP_NUM_THREADS": 1,
"OPENBLAS_NUM_THREADS": 1,
"NUMEXPR_NUM_THREADS": 1,
"MKL_NUM_THREADS": 1,
}


def _render_server_env(env: dict[str, Any]) -> str:
"""Render an ``env`` dict as ``KEY=value`` lines for config/server.env."""
return "".join(f"{key}={value}\n" for key, value in env.items())


# Odoo instance types that map to an odoo-config --preset (see odoo-config overlay.toml).
# The other types (demo, hotfix, test, training) carry no preset.
INSTANCE_PRESETS = ("production", "staging", "integration")
Expand Down Expand Up @@ -317,7 +333,35 @@ def _run_step(slug: str) -> bool:
fg="yellow",
)

# Step 6: Install systemd unit
# Step 6: Generate config/server.env — default thread limits, enriched by deploy.yml `env`.
if eff_type == "odoo" and _run_step("env"):
conf_dir = f"{instance_path}/config"
env_path = f"{conf_dir}/server.env"
env_exists = _file_exists(executor, env_path)

if not env_exists or recreate:
typer.secho(f"\n{CONFIGURE_STEPS['env']}…", fg="green")
server_env = {**DEFAULT_SERVER_ENV, **(opts.get("env") or {})}

try:
executor.run(f"mkdir -p {conf_dir}", dry_run=dry_run)

if env_exists:
executor.run(f"mv {env_path} {env_path}.bak", dry_run=dry_run)

executor.write_file(_render_server_env(server_env), env_path, dry_run=dry_run)

except ExecutorError as exc:
typer.echo(typer.style(str(exc), fg="red"), err=True)
raise typer.Exit(code=1) from exc

else:
typer.secho(
"\nserver.env already exists. Use --recreate to regenerate.",
fg="yellow",
)

# Step 7: Install systemd unit
if _run_step("unit"):
unit_dir = "$HOME/.config/systemd/user"
unit_path = f"{unit_dir}/{instance_name}.service"
Expand Down