LAPP as a child of apache-php, in two content variants #2

Merged
marcos-mendez merged 2 commits from feat/lapp-on-apache-php into master 2026-09-27 21:17:48 -03:00
marcos-mendez commented 2026-09-27 20:23:02 -03:00 (Migrated from github.com)

Closes #1.

LAPP is now a child of the apache-php layer carrying the postgresql unit,
with two artefacts split by content rather than by topology. This is
Keel-Linux/keel-lamp#2 with the database named differently, which is the
claim the issue asked to be tested, so most of this description is the
difference between the two recipes and the numbers behind it.

The two artefacts

Layer Parent Units Packages
lapp apache-php postgresql@1.0.0 515
lapp-client apache-php none 505

Both built from one commit of this repository, in two product directories whose
only difference is whether unit.d/postgresql was cloned into it.
lapp-client is a strict subset: 10 packages fewer and none of its own, all 10
the server closure (postgresql, postgresql-17, postgresql-common,
postgresql-common-dev, webmin-postgresql, ssl-cert, libipc-run-perl,
libjson-perl, libllvm19, libz3-4).

How much of this recipe is keel-lamp's

Makefile and plan/main are the same files once the four database packages
are named by their role. Diffed after substituting the engine names, every
substantive line is identical and only comments differ; the plans reduce to the
same seven entries, four of them the client, the PHP driver, the Perl DBI
driver and the Python driver.

conf.d/main is the same script with the same skeleton and exactly two
blocks that are not the same
. Both are asymmetries between the two
components, neither between the two stacks:

  1. The listen addresses. unit-postgresql writes them inside its own conf
    script, because the addresses a server answers on belong to whoever installs
    the server, so this recipe only checks the line is there. unit-mariadb
    does not, so keel-lamp has to write 99-keel-bind.cnf itself, and
    keel-mariadb already carries the same file in its overlay: the setting
    exists twice on the MariaDB side and zero times here.
  2. The build time credential. unit-postgresql's conf script opens with
    set ${PGSQL_PASS:=postgres}, so a build naming no PGSQL_PASS gives the
    superuser a known password and every consumer has to undo it, as
    keel-postgresql and this recipe both do. The shared tree's
    conf/adminer-mysql uses a value from mcookie that nobody records, so
    keel-lamp has nothing to undo.

A third difference belongs to the shared tree and not to the components:
conf/adminer-mysql starts the database to create an account, so keel-lamp
can only run it in the artefact that has a server, while conf/adminer-pgsql
is two sed lines and could run in both. It is kept to the server artefact
here anyway, so that the two Makefiles stay the same shape and the client
artefact gets the same result from conf.d/main. Splitting
conf/adminer-mysql into a driver half and an account half would remove the
last structural difference between the two recipes; common is shared, so that
is proposed and not done.

None of the three argues against the composition. All three say the same
thing: the two components were extracted to different depths and the recipes
are carrying the difference.

Equivalence against a monolithic build

Measured the way decision 0010 measured the unit extraction, on the assembled
root and not on the delta: a scratch product lapp-mono expressing the same
stack as one recipe on core, built twice for the noise floor. All builds at
SOURCE_DATE_EPOCH=1700000000, common 6803422, fab 1.1.1+keel2, core
7acf2c53, apache-php b9dd64db.

The control needed one line that the shared tree does not have:
unit-postgresql is not a verbatim extraction of common/conf/pgsql, it also
sets the cluster's listen addresses. Without that line the control would be a
different stack, listening on IPv4 only, and the comparison would measure that
improvement instead of the composition. The line is applied in the control and
nothing else about the shared tree is touched.

packages regular files symlinks
control, first run 510 41,219 3,985
control, second run 510 41,219 3,985
composed, with the unit 515 41,301 4,023
pair packages files differing paths on one side symlinks
control vs control 0 16 0 0
control, second run, vs composed 5 36 82 38

Every version of every shared package is identical, all 510.

The five extra packages, and none of them is the unit's:

  • turnkey-apache-php-19.0, the parent layer's own release record. A chain
    records every layer it is made of and a monolith records one.

  • php-json and php8.4-phpdbg, the parent's composer closure. In keel-lamp
    the control-vs-control run showed these two arriving and leaving between two
    runs of the identical recipe, so they are not deterministic; here the control
    happened to be stable and the composed build inherits them from the published
    parent, where they are installed.

  • php-mysql and php8.4-mysql, a MariaDB driver in a PostgreSQL stack.
    adminer depends on php-mysql | php-mysqli | php-pdo-mysql | php-sqlite3 | php-pgsql and apt takes the first alternative. The published apache-php
    layer installs Adminer before either stack's packages exist, so it resolves
    to MySQL's driver and every child inherits it. The monolithic LAPP already
    had php-pgsql in its plan, which satisfied the same alternative, so it
    installed no MySQL driver at all.

    This is the one finding of the measurement that is worth acting on, and it is
    apache-php's: putting Adminer in the shared layer forces a database driver
    into the shared layer, and the alternative decides which. apache-php should
    name the driver it wants.

Differences attributable to the unit form, the control-vs-composed set
minus the control-vs-control set: 20 paths, and every one of them records
the package set or the order it was installed in:

  • /etc/hostname and /etc/hosts, the product's own name; the control has to
    be a differently named product to exist beside the real one (lapp-mono
    against lapp, read out of both trees).
  • /var/lib/dpkg/status and status-old: the five extra stanzas above and
    nothing else, checked by diffing the Package: lines.
  • twelve /var/lib/ucf/{registry,hashfile}*: the php8.4-mysql and
    php8.4-phpdbg .ini lines and nothing else.
  • /var/lib/apt/extended_states, /var/lib/dpkg/triggers/File,
    /var/cache/man/index.db (identical size, a cache), and
    /etc/webmin/webmin.acl, which differs only in the order of the module list
    (apache postgresql phpini against apache phpini postgresql).

So the count attributable to the unit form is zero. The 16 the control
differs from itself by are the self-signed certificate and key, the webmin
caches and logs, alternatives.log, aux-cache and the dpkg and apt
registries: install-time state of the kind the M0 allow-list already freezes.

The boot test, which proves the stack rather than the ports

lapp:

boot-test: apache answered 200 on port 80 over IPv6
boot-test: apache over TLS answered 200 on port 443 over IPv6
boot-test: PHP executed ('PHP Version' in the body of /phpinfo.php)   [x2, 80 and 443]
boot-test: the landing page is the appliance's, carries the mark, and says 'PostgreSQL server on this machine'
boot-test: the mark is served at /keel-lockup.svg
boot-test: the CGI handler ran /cgi-bin/test.cgi
boot-test: adminer answered with its own page on port 12322
boot-test: webmin-postgresql is installed
boot-test: webmin answered 200 on port 12321
boot-test: the database listens on [::1]:5432
boot-test: the database answered on [::1]:5432 as 'postgres' with the declared secret
boot-test: the database listens on [127.0.0.1]:5432
boot-test: the database answered on [127.0.0.1]:5432 as 'postgres' with the declared secret
keel diff: no drift
boot-test: lapp boot test passed

lapp-client, same web verdicts, and the absence in place of the database:

boot-test: the landing page is the appliance's, carries the mark, and says 'PostgreSQL server you configure elsewhere'
boot-test: webmin-apache is installed
boot-test: postgresql is not installed
boot-test: there is no /var/lib/postgresql
boot-test: nothing listens on port 5432
boot-test: no postgres is running
keel diff: no drift
boot-test: lapp-client boot test passed

The absence is asked four ways because each can be true while the others are
false. The data directory verdict reads the directory's contents rather than
its existence, because postgresql-client creates /var/lib/postgresql as the
role's home and leaves it empty.

Coverage

67 bats tests, 100 percent (284/284) of tests/lib/boot-test-lib.sh under
kcov, the threshold the workflow enforces. The 67th runs the artefact helpers
in a script with set -euo pipefail, because bats run turns errexit off and
the first lamp-client boot test died silently on exactly that: a helper that
returns 1 to mean "this artefact has no server" killed its caller. See
Keel-Linux/keel-lamp#2.

Why package / changelog is red

require-changelog compares only the Debian revision inside the parentheses.
master's top entry is turnkey-lapp-18.1 (1) and the new one is
turnkey-lapp-19.0 (1), so it reports "1 is not greater than 1". The version
did increase, from 18.1 to 19.0; the checker cannot see it, and a fork's first
entry under a new upstream version can never pass. Fixing it is a change to
keel-linux/.github, which the 2026-09-26 audit called the highest blast
radius in the organization, so it is reported rather than made, and the entry
is left honest at revision 1.

appliance / build-and-boot and appliance-client / build-and-boot stay red
or skipped until the layers are on the mirror. Nothing has been published.

Closes #1. LAPP is now a child of the `apache-php` layer carrying the `postgresql` unit, with two artefacts split by content rather than by topology. This is `Keel-Linux/keel-lamp#2` with the database named differently, which is the claim the issue asked to be tested, so most of this description is the difference between the two recipes and the numbers behind it. ## The two artefacts | Layer | Parent | Units | Packages | | --- | --- | --- | --- | | `lapp` | `apache-php` | `postgresql@1.0.0` | 515 | | `lapp-client` | `apache-php` | none | 505 | Both built from one commit of this repository, in two product directories whose only difference is whether `unit.d/postgresql` was cloned into it. `lapp-client` is a strict subset: 10 packages fewer and none of its own, all 10 the server closure (`postgresql`, `postgresql-17`, `postgresql-common`, `postgresql-common-dev`, `webmin-postgresql`, `ssl-cert`, `libipc-run-perl`, `libjson-perl`, `libllvm19`, `libz3-4`). ## How much of this recipe is keel-lamp's `Makefile` and `plan/main` are the same files once the four database packages are named by their role. Diffed after substituting the engine names, every substantive line is identical and only comments differ; the plans reduce to the same seven entries, four of them the client, the PHP driver, the Perl DBI driver and the Python driver. `conf.d/main` is the same script with the same skeleton and **exactly two blocks that are not the same**. Both are asymmetries between the two components, neither between the two stacks: 1. **The listen addresses.** `unit-postgresql` writes them inside its own conf script, because the addresses a server answers on belong to whoever installs the server, so this recipe only checks the line is there. `unit-mariadb` does not, so `keel-lamp` has to write `99-keel-bind.cnf` itself, and `keel-mariadb` already carries the same file in its overlay: the setting exists twice on the MariaDB side and zero times here. 2. **The build time credential.** `unit-postgresql`'s conf script opens with `set ${PGSQL_PASS:=postgres}`, so a build naming no `PGSQL_PASS` gives the superuser a known password and every consumer has to undo it, as `keel-postgresql` and this recipe both do. The shared tree's `conf/adminer-mysql` uses a value from `mcookie` that nobody records, so `keel-lamp` has nothing to undo. A third difference belongs to the shared tree and not to the components: `conf/adminer-mysql` starts the database to create an account, so `keel-lamp` can only run it in the artefact that has a server, while `conf/adminer-pgsql` is two `sed` lines and could run in both. It is kept to the server artefact here anyway, so that the two Makefiles stay the same shape and the client artefact gets the same result from `conf.d/main`. Splitting `conf/adminer-mysql` into a driver half and an account half would remove the last structural difference between the two recipes; `common` is shared, so that is proposed and not done. None of the three argues against the composition. All three say the same thing: the two components were extracted to different depths and the recipes are carrying the difference. ## Equivalence against a monolithic build Measured the way decision 0010 measured the unit extraction, on the assembled root and not on the delta: a scratch product `lapp-mono` expressing the same stack as one recipe on `core`, built twice for the noise floor. All builds at `SOURCE_DATE_EPOCH=1700000000`, `common` 6803422, fab 1.1.1+keel2, `core` 7acf2c53, `apache-php` b9dd64db. The control needed one line that the shared tree does not have: `unit-postgresql` is not a verbatim extraction of `common/conf/pgsql`, it also sets the cluster's listen addresses. Without that line the control would be a different stack, listening on IPv4 only, and the comparison would measure that improvement instead of the composition. The line is applied in the control and nothing else about the shared tree is touched. | | packages | regular files | symlinks | | --- | --- | --- | --- | | control, first run | 510 | 41,219 | 3,985 | | control, second run | 510 | 41,219 | 3,985 | | composed, with the unit | 515 | 41,301 | 4,023 | | pair | packages | files differing | paths on one side | symlinks | | --- | --- | --- | --- | --- | | control vs control | 0 | 16 | 0 | 0 | | control, second run, vs composed | 5 | 36 | 82 | 38 | Every version of every shared package is identical, all 510. The five extra packages, and none of them is the unit's: - `turnkey-apache-php-19.0`, the parent layer's own release record. A chain records every layer it is made of and a monolith records one. - `php-json` and `php8.4-phpdbg`, the parent's composer closure. In `keel-lamp` the control-vs-control run showed these two arriving and leaving between two runs of the identical recipe, so they are not deterministic; here the control happened to be stable and the composed build inherits them from the published parent, where they are installed. - **`php-mysql` and `php8.4-mysql`, a MariaDB driver in a PostgreSQL stack.** `adminer` depends on `php-mysql | php-mysqli | php-pdo-mysql | php-sqlite3 | php-pgsql` and apt takes the first alternative. The published `apache-php` layer installs Adminer before either stack's packages exist, so it resolves to MySQL's driver and every child inherits it. The monolithic LAPP already had `php-pgsql` in its plan, which satisfied the same alternative, so it installed no MySQL driver at all. This is the one finding of the measurement that is worth acting on, and it is `apache-php`'s: putting Adminer in the shared layer forces a database driver into the shared layer, and the alternative decides which. `apache-php` should name the driver it wants. **Differences attributable to the unit form**, the control-vs-composed set minus the control-vs-control set: **20 paths**, and every one of them records the package set or the order it was installed in: - `/etc/hostname` and `/etc/hosts`, the product's own name; the control has to be a differently named product to exist beside the real one (`lapp-mono` against `lapp`, read out of both trees). - `/var/lib/dpkg/status` and `status-old`: the five extra stanzas above and nothing else, checked by diffing the `Package:` lines. - twelve `/var/lib/ucf/{registry,hashfile}*`: the `php8.4-mysql` and `php8.4-phpdbg` `.ini` lines and nothing else. - `/var/lib/apt/extended_states`, `/var/lib/dpkg/triggers/File`, `/var/cache/man/index.db` (identical size, a cache), and `/etc/webmin/webmin.acl`, which differs only in the order of the module list (`apache postgresql phpini` against `apache phpini postgresql`). So the count attributable to the unit form is **zero**. The 16 the control differs from itself by are the self-signed certificate and key, the webmin caches and logs, `alternatives.log`, `aux-cache` and the dpkg and apt registries: install-time state of the kind the M0 allow-list already freezes. ## The boot test, which proves the stack rather than the ports `lapp`: ``` boot-test: apache answered 200 on port 80 over IPv6 boot-test: apache over TLS answered 200 on port 443 over IPv6 boot-test: PHP executed ('PHP Version' in the body of /phpinfo.php) [x2, 80 and 443] boot-test: the landing page is the appliance's, carries the mark, and says 'PostgreSQL server on this machine' boot-test: the mark is served at /keel-lockup.svg boot-test: the CGI handler ran /cgi-bin/test.cgi boot-test: adminer answered with its own page on port 12322 boot-test: webmin-postgresql is installed boot-test: webmin answered 200 on port 12321 boot-test: the database listens on [::1]:5432 boot-test: the database answered on [::1]:5432 as 'postgres' with the declared secret boot-test: the database listens on [127.0.0.1]:5432 boot-test: the database answered on [127.0.0.1]:5432 as 'postgres' with the declared secret keel diff: no drift boot-test: lapp boot test passed ``` `lapp-client`, same web verdicts, and the absence in place of the database: ``` boot-test: the landing page is the appliance's, carries the mark, and says 'PostgreSQL server you configure elsewhere' boot-test: webmin-apache is installed boot-test: postgresql is not installed boot-test: there is no /var/lib/postgresql boot-test: nothing listens on port 5432 boot-test: no postgres is running keel diff: no drift boot-test: lapp-client boot test passed ``` The absence is asked four ways because each can be true while the others are false. The data directory verdict reads the directory's contents rather than its existence, because `postgresql-client` creates `/var/lib/postgresql` as the role's home and leaves it empty. ## Coverage 67 bats tests, **100 percent (284/284)** of `tests/lib/boot-test-lib.sh` under kcov, the threshold the workflow enforces. The 67th runs the artefact helpers in a script with `set -euo pipefail`, because `bats run` turns errexit off and the first `lamp-client` boot test died silently on exactly that: a helper that returns 1 to mean "this artefact has no server" killed its caller. See `Keel-Linux/keel-lamp#2`. ## Why `package / changelog` is red `require-changelog` compares only the Debian revision inside the parentheses. `master`'s top entry is `turnkey-lapp-18.1 (1)` and the new one is `turnkey-lapp-19.0 (1)`, so it reports "1 is not greater than 1". The version did increase, from 18.1 to 19.0; the checker cannot see it, and a fork's first entry under a new upstream version can never pass. Fixing it is a change to `keel-linux/.github`, which the 2026-09-26 audit called the highest blast radius in the organization, so it is reported rather than made, and the entry is left honest at revision 1. `appliance / build-and-boot` and `appliance-client / build-and-boot` stay red or skipped until the layers are on the mirror. Nothing has been published.
Commenting is not possible because the repository is archived.
No description provided.