Skip to content
Draft
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
8 changes: 7 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,14 @@ Steps 2–6 each correspond to a `--steps` / `--except` slug, shown in **bold**.
- **`odoo`**: create if not present a virtual environment using `odoo-venv`, if
there is `--recreate` option then re-create the venv:
```bash
odoo-venv create --project-dir ~/<instance_name> --preset project
odoo-venv create --project-dir ~/<instance_name> --preset project [<venv options>]
```
The `tools.odoo-venv` mapping in `deploy.yml` is rendered as `--<key> <value>` options
and merged over the two defaults above, so each option is passed exactly once: a
`project-dir` or `preset` key overrides the default, a `true` value renders a bare
`--<key>` flag, a `false` or null value drops the option, and a list value repeats it.
The `tools.odoo-config` mapping does the same for the `odoo-config create` invocation
of the `generate-config` step.
- **`python` with `requirements`** (package mode): create a venv and install the listed
packages directly:
```bash
Expand Down
55 changes: 55 additions & 0 deletions site-docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ odoo-myproject-production:
workers: 4
limit_time_cpu: 600

# Command-line options for the tools `configure` runs. Each key under a tool is
# rendered as `--key value` and merged over the options deploy passes by default,
# so every option is passed exactly once. A matching key overrides the default,
# `true` renders a bare flag, `false` / null drops the option, and a list repeats it.
# Note the difference from `config:` above: that sets values *inside* odoo.conf,
# while `tools.odoo-config` controls *how odoo-config is invoked*.
tools:
odoo-venv: # → odoo-venv create
odoo-dir: /opt/odoo/code/odoo/odoo/20.0/
addons-path: /opt/odoo/code/odoo/odoo/20.0/addons,/opt/odoo/code/odoo/enterprise/20.0
odoo-config: # → odoo-config create
enterprise: true
output-format: all
from:
- /opt/odoo/shared/base.conf


# Environment variables — written to config/server.env by `configure`.
# Merged over the built-in thread-limit defaults (the value here wins).
env:
Expand Down Expand Up @@ -86,9 +103,47 @@ odoo-myproject-production:
| `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). |
| `tools` | mapping | `configure` | Command-line options for the tools the steps run, keyed by tool (Odoo only) — see [Tool options](#tool-options). |
| `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). |

## Tool options

The `configure` steps shell out to `odoo-venv` and `odoo-config`. The `tools` section sets
command-line options for them, keyed by tool:

| Tool key | Command | Options deploy passes by default |
|----------|---------|----------------------------------|
| `odoo-venv` | `odoo-venv create` | `--project-dir`, `--preset project` |
| `odoo-config` | `odoo-config create` | `--version`, `--preset`, `--instance-dir`, `--config` |

Your keys are merged over those defaults, so each option is passed exactly once:

- a key matching a default **overrides** it, keeping its position
- `true` renders a bare flag — `enterprise: true` → `--enterprise`
- `false` or null **drops** the option, including a default
- a list **repeats** the option — `from: [a, b]` → `--from a --from b`
- values are shell-quoted

```yaml
odoo-myproject-staging:
tools:
odoo-venv:
odoo-dir: /opt/odoo/code/odoo/odoo/20.0/
addons-path: /opt/odoo/code/odoo/odoo/20.0/addons,/opt/odoo/code/odoo/enterprise/20.0
odoo-config:
enterprise: true
version: 20.0
```

`tools.odoo-config.version` also short-circuits version detection, so `configure` will not
probe the codebase or prompt for it. It takes precedence over the top-level `version` and
`preset` keys, which keep working.

Do not confuse `tools.odoo-config` with `config`: the former controls **how odoo-config is
invoked**, the latter sets **values written into odoo.conf**. `odoo-config` treats any option
it does not recognise as an odoo.conf value, so a CLI flag placed under `config` will not work.

## Multiple instances

A single `deploy.yml` can hold configuration for any number of instances:
Expand Down
206 changes: 206 additions & 0 deletions tests/test_configure_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,209 @@ def test_dry_run_step_set_up_instance_dir_marks_mkdir(runner):
calls = _run_calls(mock_exec)
mkdir_call = next(c for c in calls if "mkdir -p" in c[0])
assert mkdir_call[1].get("dry_run") is True


def _odoo_venv_create_cmd(mock_exec) -> str:
return next(cmd for cmd in _run_commands(mock_exec) if "odoo-venv create" in cmd)


def test_venv_step_without_extra_args(runner):
result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "venv"],
cfg={},
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
assert _odoo_venv_create_cmd(mock_exec) == (
"odoo-venv create --project-dir /home/deploy/odoo-myapp-staging --preset project"
)


def test_venv_step_appends_extra_args_from_config(runner):
cfg = {
"tools": {
"odoo-venv": {
"odoo-dir": "/opt/odoo/code/odoo/odoo/20.0/",
"addons-path": "/opt/odoo/code/odoo/odoo/20.0/addons,/opt/odoo/code/odoo/enterprise/20.0",
"no-cache": True,
"backup": False,
"ignored": None,
}
}
}

result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "venv"],
cfg=cfg,
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
assert _odoo_venv_create_cmd(mock_exec) == (
"odoo-venv create --project-dir /home/deploy/odoo-myapp-staging --preset project "
"--odoo-dir /opt/odoo/code/odoo/odoo/20.0/ "
"--addons-path /opt/odoo/code/odoo/odoo/20.0/addons,/opt/odoo/code/odoo/enterprise/20.0 "
"--no-cache"
)


def test_venv_step_quotes_extra_arg_values(runner):
cfg = {"tools": {"odoo-venv": {"odoo-dir": "/opt/odoo/my code/20.0"}}}

result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "venv"],
cfg=cfg,
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
assert _odoo_venv_create_cmd(mock_exec).endswith("--odoo-dir '/opt/odoo/my code/20.0'")


def test_venv_step_overrides_builtin_option(runner):
cfg = {"tools": {"odoo-venv": {"preset": "local"}}}

result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "venv"],
cfg=cfg,
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
# --preset appears once, with the overriding value.
assert _odoo_venv_create_cmd(mock_exec) == (
"odoo-venv create --project-dir /home/deploy/odoo-myapp-staging --preset local"
)


def test_venv_step_drops_builtin_option_when_false(runner):
cfg = {"tools": {"odoo-venv": {"preset": False}}}

result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "venv"],
cfg=cfg,
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
assert _odoo_venv_create_cmd(mock_exec) == ("odoo-venv create --project-dir /home/deploy/odoo-myapp-staging")


def _odoo_config_create_cmd(mock_exec) -> str:
return next(cmd for cmd in _run_commands(mock_exec) if "odoo-config create" in cmd)


def _invoke_config_step(runner, cfg):
return _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "config"],
cfg={"version": "17.0", **cfg},
executor_factory=_executor_mock_fresh_dir,
)


def test_config_step_without_odoo_config_key(runner):
result, mock_exec = _invoke_config_step(runner, {})

assert result.exit_code == 0
cmd = _odoo_config_create_cmd(mock_exec)
assert cmd.startswith(
"odoo-config create --version 17.0 --preset staging "
"--instance-dir /home/deploy/odoo-myapp-staging "
"--config /home/deploy/odoo-myapp-staging/config/odoo.conf "
)
# odoo.conf value overrides still trail the CLI options.
assert "--db_user=odoo-myapp-staging" in cmd


def test_config_step_appends_odoo_config_flags(runner):
result, mock_exec = _invoke_config_step(
runner, {"tools": {"odoo-config": {"enterprise": True, "output-format": "all"}}}
)

assert result.exit_code == 0
cmd = _odoo_config_create_cmd(mock_exec)
assert "--enterprise --output-format all" in cmd
assert "--enterprise=True" not in cmd


def test_config_step_overrides_builtin_cli_option(runner):
result, mock_exec = _invoke_config_step(runner, {"tools": {"odoo-config": {"preset": "production"}}})

assert result.exit_code == 0
cmd = _odoo_config_create_cmd(mock_exec)
assert "--preset production" in cmd
assert "--preset staging" not in cmd


def test_config_step_drops_builtin_cli_option_when_false(runner):
result, mock_exec = _invoke_config_step(runner, {"tools": {"odoo-config": {"preset": False}}})

assert result.exit_code == 0
assert "--preset" not in _odoo_config_create_cmd(mock_exec)


def test_config_step_repeats_list_valued_option(runner):
result, mock_exec = _invoke_config_step(
runner, {"tools": {"odoo-config": {"from": ["/opt/base.conf", "/opt/extra.conf"]}}}
)

assert result.exit_code == 0
assert "--from /opt/base.conf --from /opt/extra.conf" in _odoo_config_create_cmd(mock_exec)


def test_config_step_version_in_odoo_config_skips_detection(runner):
"""A version in tools.odoo-config must not trigger detection, which prompts when it finds nothing."""
result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "config"],
cfg={"tools": {"odoo-config": {"version": "20.0"}}},
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
commands = _run_commands(mock_exec)
assert not any("odoo-addons-path" in cmd for cmd in commands)
assert not mock_exec.capture.call_args_list or all(
"odoo-addons-path" not in call.args[0] for call in mock_exec.capture.call_args_list
)
cmd = _odoo_config_create_cmd(mock_exec)
assert cmd.startswith("odoo-config create --version 20.0 ")
assert cmd.count("--version") == 1


def test_tools_section_with_other_tool_only(runner):
"""A tools section naming only one tool leaves the other step's defaults untouched."""
result, mock_exec = _invoke(
runner,
"odoo-myapp-staging",
"odoo",
["--steps", "venv"],
cfg={"tools": {"odoo-config": {"enterprise": True}}},
executor_factory=_executor_mock_fresh_dir,
)

assert result.exit_code == 0
assert _odoo_venv_create_cmd(mock_exec) == (
"odoo-venv create --project-dir /home/deploy/odoo-myapp-staging --preset project"
)
42 changes: 35 additions & 7 deletions trobz_deploy/command/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@

import typer

from trobz_deploy.utils.config import DeployType, load_config, parse_step_option, resolve_options, validate_step_slugs
from trobz_deploy.utils.config import (
DeployType,
load_config,
parse_step_option,
render_cli_args,
resolve_options,
tool_args,
validate_step_slugs,
)
from trobz_deploy.utils.executor import Executor, ExecutorError
from trobz_deploy.utils.render import render_unit
from trobz_deploy.utils.venv import setup_odoo_venv, setup_package_venv, setup_python_venv
Expand Down Expand Up @@ -298,7 +306,13 @@ def _run_step(slug: str) -> bool:
typer.secho(f"\nSetting up {eff_type} environment…", fg="green")
try:
if eff_type == "odoo":
setup_odoo_venv(executor, instance_path, recreate=recreate, dry_run=dry_run)
setup_odoo_venv(
executor,
instance_path,
recreate=recreate,
dry_run=dry_run,
extra_args=tool_args(opts, "odoo-venv"),
)
elif eff_type == "python":
if eff_requirements:
setup_package_venv(executor, instance_path, eff_requirements, dry_run=dry_run)
Expand All @@ -325,7 +339,14 @@ def _run_step(slug: str) -> bool:

if not conf_exists or recreate:
typer.secho(f"\n{CONFIGURE_STEPS['config']}…", fg="green")
version = opts.get("version") or _detect_version(executor, service_path, dry_run=dry_run)

# A `version` under tools.odoo-config short-circuits detection: it would
# override the flag anyway, and detection prompts when it comes up empty.
cli_overrides: dict[str, Any] = tool_args(opts, "odoo-config")
if "version" in cli_overrides:
version = cli_overrides["version"]
else:
version = opts.get("version") or _detect_version(executor, service_path, dry_run=dry_run)

overrides: dict[str, Any] = {
"db_user": instance_name,
Expand All @@ -337,16 +358,23 @@ def _run_step(slug: str) -> bool:
override_args = " ".join(f"--{key}={shlex.quote(str(value))}" for key, value in overrides.items())

preset = opts.get("preset") or _detect_preset(instance_name)
preset_arg = f" --preset {shlex.quote(preset)}" if preset else ""

# odoo-config's own CLI options, as opposed to the odoo.conf value overrides
# above. The deploy.yml `tools.odoo-config` mapping is merged over these
# defaults, so each option is passed once.
cli_args: dict[str, Any] = {"version": version}
if preset:
cli_args["preset"] = preset
cli_args["instance-dir"] = instance_path
cli_args["config"] = conf_path
cli_args.update(cli_overrides)

try:
executor.run(f"mkdir -p {conf_dir}", dry_run=dry_run)
if conf_exists:
executor.run(f"mv {conf_path} {conf_path}.bak", dry_run=dry_run)
executor.run(
f"odoo-config create --version {shlex.quote(str(version))}{preset_arg} "
f"--instance-dir={shlex.quote(instance_path)} "
f"-c {conf_path} {override_args}",
f"odoo-config create {render_cli_args(cli_args)} {override_args}",
dry_run=dry_run,
)
except ExecutorError as exc:
Expand Down
Loading
Loading