-
Notifications
You must be signed in to change notification settings - Fork 0
Development and contributing
For whoever works on the code: build, tests, architecture, and how to add a step.
cargo build # build
cargo test # tests WITHOUT root: the system is mocked
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --checkThe tests never touch the real system: privileged operations go through a boundary (SystemOps) that
is mocked in tests. The CI (.github/workflows/test.yml) runs the same checks on every push/PR.
Mock tests prove the logic, not the integration with a real package manager, PostgreSQL and
systemd. .github/workflows/integration.yml installs Odoo on ephemeral runners and containers and
checks that invok rollback leaves the system clean. It runs on demand and on the main branches.
The scenarios are chosen to cover what mocks cannot see:
| Scenario | What it checks |
|---|---|
| Ubuntu 22.04 / 24.04 | full life cycle, second installation refused, system clean after the rollback |
| Debian 11 / 12 (container) | portability of the apt names and of the wkhtmltopdf pin. Not the service start: in a container systemd is not PID 1 |
| Fedora 41 (container, systemd as PID 1) | the whole rpm family: dnf names, wkhtmltopdf .rpm, PostgreSQL cluster init
|
| Fedora 41 with Nginx |
firewalld and SELinux — the only place they really run |
| Fedora 44 | the other branch of the interpreter choice: system Python 3.14, venv on python3.13
|
With Nginx, ufw active |
the six Nginx steps, in a matrix over the two natures of the default site (symlink and regular file) |
Pre-existing odoo user |
the home handed over to a user who is already there, and the refusal when /opt/odoo pre-exists owned by root |
| Real Ctrl-C | a SIGINT sent mid-installation: the installer must roll back on its own |
MODE=full bash scripts/ci/integration-test.sh # runnable by hand — DESTRUCTIVE: throwaway VMs onlyNearly every real defect in this project was found by a real machine, not by re-reading the code. When you add an area of behaviour, ask yourself whether any real execution crosses it: if the answer is no, that is where defects survive.
-
trait Step— every step exposessnapshot(detect the pre-existing state),run(mutate) andundo. Detail: 0. Engine. -
Installer(the engine) — runs the steps in sequence; if one fails it calls the previous ones'undoin reverse order. It knows nothing about the individual steps. -
PreState—Untracked/Preexisting/CreatedByUs: the source of truth for the undo. Only what isCreatedByUsis ever undone. -
SystemOps— the boundary over system commands (systemctl, useradd, psql, files…), which also yieldspackages()anddistro(), the two multi-distro boundaries (Multi-distribution support): real in production, mocked in tests. It is what makes everything testable without root. -
Context— the resolved config the steps read; the UI (inquire/indicatif) lives outside the steps (see 2. UI + dry-run).
invok/
├── src/
│ ├── main.rs entry point: install (parse → prompt → checks → lock → execute) | rollback
│ ├── lib.rs the library the tests use: main.rs is only the shell
│ ├── cli.rs CLI arguments (clap) + the `rollback`/`uninstall` subcommand
│ ├── config.rs CLI/.env/default cascade + declarative .env parser + validators
│ ├── context.rs the resolved config the steps read
│ ├── engine.rs the engine: execute + rollback (reverse order) + dry-run plan
│ ├── step.rs the Step trait (snapshot/run/undo)
│ ├── state.rs PreState + state persistence
│ ├── rollback.rs rollback from persisted state (step rehydration + leftovers report)
│ ├── system_ops.rs boundary over system commands (mockable in tests)
│ ├── checks.rs non-mutating preflight checks
│ ├── secret.rs redacted password (never in the logs)
│ ├── error.rs domain errors (thiserror), per step
│ ├── logging.rs tracing to TTY + file
│ ├── lockfile.rs concurrency lock (RAII)
│ ├── interrupt.rs Ctrl-C/SIGTERM: raises a flag, the engine watches it
│ ├── progress.rs ProgressReporter (indicatif/log/noop)
│ ├── prompt.rs interactive input (inquire)
│ ├── packaging/ FIRST BOUNDARY: which commands install, and what it is called here
│ │ mod.rs (PackageManager + per-family alternative groups) · apt.rs · dnf.rs
│ ├── distro/ SECOND BOUNDARY: where files live, who governs the firewall
│ │ mod.rs (Distro trait + OsFamily) · debian.rs · fedora.rs
│ │ ufw.rs (deb-family firewall) · firewalld.rs (rpm-family firewall)
│ └── steps/ the real steps, one per file
├── templates/ odoo.conf.tpl · odoo.service.tpl · nginx.conf.tpl (embedded in the binary)
├── configs/ the CI presets (ci.env, ci-nginx.env) — you write your own .env
├── debian/ · rpm/ postinst/postrm and post/postun: they create and remove the `vok` alias
├── scripts/ci/ integration-test.sh · journal.sh (reads the journal from the log)
│ selftest-journal.sh (checks that journal.sh can read it)
├── .github/workflows/ test.yml (fast, mock) · integration.yml (real) · release.yml
└── tests/ per-step tests + coordination + end-to-end rollback
The two boundaries (packaging/ and distro/) are why none of the 25 steps contains a match on the
distribution. Adding a family means adding a backend to each of them, not touching the steps — and the
Step trait does not change.
- Create
src/steps/<name>.rsimplementingStep:-
snapshot→ determine thePreState(what already existed?); -
run→ mutate only when needed; honourctx.dry_run; -
undo→ act only onCreatedByUs, best-effort and idempotent; -
snapshot_value/rehydrate→ serialise and deserialise the snapshot (persistence). They must be exact inverses: that is what makes rollback-from-disk trustworthy, andtests/rehydrate.rschecks it step by step.
-
- Use
SystemOpsfor every system operation (no directstd::process::Commandinside a step, or it stops being mockable). - Register the step in both functions of
src/steps/mod.rs—build_steps(the canonical order; order matters, because the rollback is its reverse) andstep_by_name(rebuilding by name, for rollback from disk). A parity test demands they cover the same set. - Write the tests in
tests/<name>.rsagainst the mock, including therun → undoround trip. -
Do not modify the
Steptrait or the engine to fit a new step: if it does not fit, that is a signal to discuss, not to widen the trait.
Every step also has its own detail page (1.1–1.16) in this wiki.
- No
.unwrap()/.expect()in production code: every failure is aResult. -
cargo fmtandclippy -D warningsclean before committing. - The password is never logged: use the
Secrettype.
See History: the installer was Bash up to the v1.x tags, then a complete rewrite in Rust.
Start here
Key concepts
References
For developers
Technical detail — how it works inside
Steps:
- 1.1 PrepareOptRoot
- 1.2 CreateOdooUser
- 1.3 SetupLogDir
- 1.3b SetupCacheDir
- 1.4 AptPackages (delta)
- 1.5 InstallWkhtmltopdf
- 1.6 SetupPostgres
- 1.7 CreateDbRole
- 1.8 CreateDatabase
- 1.9 CloneOdooRepo
- 1.10 CreateVirtualenv
- 1.11 InstallPythonRequirements
- 1.12 GenerateConfig
- 1.12b SetupDataDir
- 1.13 InitializeOdooDatabase
- 1.14 SetupSystemd
- 1.15 Nginx (6 sub-steps)
- 1.16 WriteControlScript + PatchBashrc
Cross-cutting: