Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
330ec8c503 | ||
|
|
b1fe86e9d5 | ||
|
|
ccf4d01a34 | ||
|
|
fd055f0cf9 |
@@ -39,7 +39,8 @@ This project uses `just` (see `justfile`) and Nix directly.
|
|||||||
and disables `boot.loader.grub` purely to satisfy NixOS's built-in assertions when `nix flake check` evaluates
|
and disables `boot.loader.grub` purely to satisfy NixOS's built-in assertions when `nix flake check` evaluates
|
||||||
`config.system.build.toplevel` directly — the qemu-vm module overrides both with a tmpfs root and direct kernel
|
`config.system.build.toplevel` directly — the qemu-vm module overrides both with a tmpfs root and direct kernel
|
||||||
boot when actually building `config.system.build.vm` (what `just build-vm` uses), so these values are otherwise
|
boot when actually building `config.system.build.vm` (what `just build-vm` uses), so these values are otherwise
|
||||||
unused.
|
unused. Also raises `virtualisation.cores`/`memorySize` to 2/2560 (qemu-vm's defaults, 1 core/1024MB, are too
|
||||||
|
tight for real interactive use — see the module architecture notes below on PDF generation's memory cost).
|
||||||
- `nixos-test.nix` — the automated test (`pkgs.testers.nixosTest`, exposed as `checks.<system>.invoiceninja` in
|
- `nixos-test.nix` — the automated test (`pkgs.testers.nixosTest`, exposed as `checks.<system>.invoiceninja` in
|
||||||
`flake.nix`). Builds its own `pkgs` (with `allowUnfree` baked in — `nixosTest` forbids setting `nixpkgs.config`
|
`flake.nix`). Builds its own `pkgs` (with `allowUnfree` baked in — `nixosTest` forbids setting `nixpkgs.config`
|
||||||
on a node once `pkgs` is externally supplied) and boots two nodes from `common.nix` — one with
|
on a node once `pkgs` is externally supplied) and boots two nodes from `common.nix` — one with
|
||||||
@@ -48,9 +49,12 @@ This project uses `just` (see `justfile`) and Nix directly.
|
|||||||
against each node: waits for mysql/redis/phpfpm/queue-worker/proxy to be active, checks the
|
against each node: waits for mysql/redis/phpfpm/queue-worker/proxy to be active, checks the
|
||||||
`invoiceninja-data-setup` marker files (`.initial-migration`, `.db-seeded`, `.admin-created`) to confirm the
|
`invoiceninja-data-setup` marker files (`.initial-migration`, `.db-seeded`, `.admin-created`) to confirm the
|
||||||
one-time setup actually completed (not just that the oneshot unit exited), curls `/` for the app shell, exercises
|
one-time setup actually completed (not just that the oneshot unit exited), curls `/` for the app shell, exercises
|
||||||
the `invoiceninja-manage` wrapper, and logs in via `/api/v1/login` using the test admin credentials as an
|
the `invoiceninja-manage` wrapper, logs in via `/api/v1/login` using the test admin credentials as an
|
||||||
end-to-end check that migrations/seeding/account creation all worked. This is the only automated test in the
|
end-to-end check that migrations/seeding/account creation all worked, checks the full language list is
|
||||||
repo — there is no separate unit-test suite.
|
available (see the `cache:forget languages` note above), and exercises PDF generation end-to-end (POSTs to
|
||||||
|
`/api/v1/preview` with no entity, which falls back to a self-contained mock PDF built via the same
|
||||||
|
chromium/snappdf pipeline real invoices use, and checks for the `%PDF` magic bytes — no client/invoice setup
|
||||||
|
needed). This is the only automated test in the repo — there is no separate unit-test suite.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -123,7 +127,11 @@ expect Nix to report the correct hash on a failed build (or use `nix-prefetch`/`
|
|||||||
`127.0.0.1:80`/`[::1]:80` too — otherwise visiting the VM via the IP literal instead of `localhost` matches no
|
`127.0.0.1:80`/`[::1]:80` too — otherwise visiting the VM via the IP literal instead of `localhost` matches no
|
||||||
vhost and Caddy silently returns an empty `200 OK` (a blank page with no error anywhere).
|
vhost and Caddy silently returns an empty `200 OK` (a blank page with no error anywhere).
|
||||||
- Chromium is required at runtime for PDF generation (`PDF_GENERATOR=snappdf`, `SNAPPDF_CHROMIUM_PATH`) and is
|
- Chromium is required at runtime for PDF generation (`PDF_GENERATOR=snappdf`, `SNAPPDF_CHROMIUM_PATH`) and is
|
||||||
added to the PHP-FPM and data-setup services' `path`.
|
added to the PHP-FPM and data-setup services' `path`. Each PDF request launches a full chromium instance (not
|
||||||
|
a lightweight renderer), and PHP-FPM's default `pm.max_children = 8` means up to 8 of these can run
|
||||||
|
concurrently — on a memory-constrained deployment this can exhaust RAM (confirmed: even a couple of
|
||||||
|
concurrent PDF requests OOM-killed a 1024MB VM). Size deployments accordingly, or lower
|
||||||
|
`phpfpm.settings."pm.max_children"`.
|
||||||
|
|
||||||
### `flake.nix`
|
### `flake.nix`
|
||||||
|
|
||||||
|
|||||||
@@ -486,7 +486,6 @@ in
|
|||||||
|
|
||||||
recommendedTlsSettings = (if (cfg.hostname == "localhost") then false else true);
|
recommendedTlsSettings = (if (cfg.hostname == "localhost") then false else true);
|
||||||
recommendedGzipSettings = true;
|
recommendedGzipSettings = true;
|
||||||
recommendedProxySettings = true;
|
|
||||||
recommendedOptimisation = true;
|
recommendedOptimisation = true;
|
||||||
|
|
||||||
clientMaxBodySize = cfg.maxUploadSize;
|
clientMaxBodySize = cfg.maxUploadSize;
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
(modulesPath + "/profiles/qemu-guest.nix")
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
|
# Not imported by default for a plain nixosSystem in this nixpkgs (only nixosTest nodes get it
|
||||||
|
# automatically) - needed explicitly so virtualisation.cores/memorySize below are real options.
|
||||||
|
(modulesPath + "/virtualisation/qemu-vm.nix")
|
||||||
(import ./common.nix { inherit proxyServer; })
|
(import ./common.nix { inherit proxyServer; })
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -26,4 +29,27 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
services.resolved.enable = true;
|
services.resolved.enable = true;
|
||||||
|
|
||||||
|
# `just boot-vm` forwards the guest's port 80 to host port 8080 (QEMU_NET_OPTS in the justfile),
|
||||||
|
# but the backend builds absolute URLs (e.g. the stored company logo path) from APP_URL, which
|
||||||
|
# defaults to plain "http://localhost" with no port. The SPA's own relative URLs are fine (they
|
||||||
|
# inherit whatever port the browser is already on), but any backend-generated absolute URL then
|
||||||
|
# points at host port 80 - where nothing is listening - instead of the forwarded 8080, giving a
|
||||||
|
# silent net::ERR_CONNECTION_REFUSED for things like the logo image. Override to match the actual
|
||||||
|
# forwarded port.
|
||||||
|
services.invoiceninja.settings.APP_URL = "http://localhost:8080";
|
||||||
|
|
||||||
|
# The qemu-vm module's defaults (1 core, 1024MB) are too tight for real interactive use: MySQL +
|
||||||
|
# Redis + PHP-FPM + chromium (PDF generation, spawned per request - each is a full multi-process
|
||||||
|
# browser, not a lightweight renderer) running concurrently can exhaust 1GB fast, and this VM
|
||||||
|
# boots with panic_on_oom - so it's not a graceful slowdown, the whole VM goes down. Confirmed by
|
||||||
|
# reproducing a "logo upload hangs" report this way: at the 1024MB default, even a couple of
|
||||||
|
# concurrent PDF-generation requests triggered `redis-server invoked oom-killer` and a kernel
|
||||||
|
# panic. 2 cores / 2560MB comfortably handles 2 concurrent PDF generations alongside normal
|
||||||
|
# traffic (verified: ~1.4GB still free afterward); a deliberate flood of many concurrent PDF
|
||||||
|
# requests can still exhaust even this, since each is a full chromium instance - if that's a
|
||||||
|
# concern for your deployment, consider capping `phpfpm.settings."pm.max_children"` below its
|
||||||
|
# default of 8, or provisioning more RAM.
|
||||||
|
virtualisation.cores = 2;
|
||||||
|
virtualisation.memorySize = 2560;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ pkgs.testers.nixosTest {
|
|||||||
language_count = len(json.loads(statics)["languages"])
|
language_count = len(json.loads(statics)["languages"])
|
||||||
assert language_count > 40, f"expected ~45 languages, got {language_count}"
|
assert language_count > 40, f"expected ~45 languages, got {language_count}"
|
||||||
|
|
||||||
|
with subtest("PDF generation works (chromium/snappdf)"):
|
||||||
|
# /api/v1/preview with no entity falls back to a self-contained mock PDF (no invoice/client
|
||||||
|
# setup needed), exercising the same chromium/snappdf pipeline real invoice PDFs use.
|
||||||
|
machine.succeed(
|
||||||
|
"curl -sf -X POST http://localhost/api/v1/preview "
|
||||||
|
+ f"-H 'X-API-TOKEN: {token}' -H 'Content-Type: application/json' "
|
||||||
|
+ "-d '{}' -o /tmp/preview.pdf"
|
||||||
|
)
|
||||||
|
magic_bytes = machine.succeed("head -c 4 /tmp/preview.pdf")
|
||||||
|
assert magic_bytes == "%PDF", f"expected a PDF, got: {magic_bytes!r}"
|
||||||
|
|
||||||
|
|
||||||
nginx.start()
|
nginx.start()
|
||||||
caddy.start()
|
caddy.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user