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

Merged
marcos-mendez merged 6 commits from feat/lamp-on-apache-php into master 2026-09-27 21:01:22 -03:00
marcos-mendez commented 2026-09-27 20:06:09 -03:00 (Migrated from github.com)

Closes #1.

LAMP is now a child of the apache-php layer carrying the mariadb unit, and
the recipe produces two artefacts split by content rather than by topology.

The two artefacts

Layer Parent Units Packages
lamp apache-php mariadb@1.0.0 520
lamp-client apache-php none 507

They are built from the same commit of this repository in two product
directories, and the only difference between those directories is whether
unit.d/mariadb was cloned into it. lamp-client is a strict subset:
13 packages fewer and none of its own, all 13 the server closure
(default-mysql-server, mariadb-server, mariadb-server-compat,
mariadb-server-core, galera-4, webmin-mysql, python3-pymysql, and
gawk, lsof, liblsof0, libmpfr6, libsigsegv2, socat).

Nothing in the recipe asks which artefact is being built by name. conf.d/main
reads what dpkg says the image contains, because a makefile variable says what
was asked for and dpkg says what happened.

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 lamp-mono expressing the same
stack as one recipe on core, built twice so the comparison has a noise floor
to be read against. All builds at SOURCE_DATE_EPOCH=1700000000, common
6803422, fab 1.1.1+keel2, parent core 7acf2c53 and apache-php b9dd64db.

packages regular files symlinks
control, first run 517 38,989 3,565
control, second run 519 39,031 3,594
composed, with the unit 520 39,034 3,594

The noise floor first. Two runs of the identical control differ by 2
packages (php-json, php8.4-phpdbg), 198 files, 42 paths and 29 symlinks.
The package set of this stack is not deterministic run to run: both packages
arrive through composer's dependency closure and apt does not resolve it the
same way twice. That is the instrument decision 0012 exists to fix, and it is
why the third build is the point of the method.

Against the second control run, which is the one the composed build should
be read against:

pair packages differing files differing paths only on one side symlinks
control vs control 2 198 42 29
control, second run, vs composed 1 182 3 0

The one package is turnkey-apache-php-19.0, the parent layer's own release
record, and the 3 paths are that package's changelog, .list and .md5sums.
A chain records every layer it is made of and a monolith records one; that is
correct and cannot be otherwise. Every version of every shared package is
identical, all 519.

Differences attributable to the unit form, that is the control-vs-composed
set minus the control-vs-control set: 2 paths, /etc/hostname and
/etc/hosts, and both are the product's own name, because the control has to
be a differently named product to exist beside the real one (lamp-mono
against lamp, measured in both trees). Net attributable to the unit form:
zero.

The 182 that do differ, all of them: 167 under /var/lib/mysql/**, the data
directory the database writes when the conf script starts it, same sizes with
a few bytes of creation stamp each; /etc/hostname and /etc/hosts, the two
above; /root/.wget-hsts, a clock written when the component downloads
mysqltuner; and 12 that are install-time state of the kind the M0 allow-list
already freezes: /var/lib/dpkg/status, status-old, triggers/File,
/var/lib/apt/extended_states, /var/cache/ldconfig/aux-cache,
/var/log/alternatives.log, /var/log/webmin/webmin.log,
/var/webmin/module.infos.cache, /etc/webmin/apache/config,
/etc/webmin/apache/httpd-info, /etc/webmin/mysql/config and
/etc/webmin/time/config.

One correction to the method, because it changed the numbers. The first run of
the comparison read sha256sum output with join on whitespace, so the one
path in the tree whose name contains a space,
setuptools/_vendor/jaraco/text/Lorem ipsum.txt, was split and counted as a
difference in every pair. It is byte identical in all three trees. The
comparison is keyed on a tab-separated path now, and the numbers above are the
corrected ones: 198 and 182 rather than 199 and 183.

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

Both artefacts, on the build host, over the container's global IPv6 address.

lamp:

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 'MariaDB 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-mysql is installed
boot-test: webmin answered 200 on port 12321
boot-test: the database listens on [::1]:3306
boot-test: the database answered on [::1]:3306 as 'adminer@localhost' with the declared secret
boot-test: the database listens on [127.0.0.1]:3306
boot-test: the database answered on [127.0.0.1]:3306 as 'adminer@localhost' with the declared secret
keel diff: no drift
boot-test: lamp boot test passed

lamp-client, with the 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 'MariaDB server you configure elsewhere'
boot-test: webmin-apache is installed
boot-test: mariadb-server is not installed
boot-test: there is no /var/lib/mysql
boot-test: nothing listens on port 3306
boot-test: no mariadbd is running
keel diff: no drift
boot-test: lamp-client boot test passed

The absence is asked four ways because each can be true while the others are
false, and an idle server in that artefact would be the cheapest failure to
ship and the most expensive to find.

A bug the boot test found, and the test that now holds it

The first lamp-client run exited after the PHP verdict with no message.
bt_has_local_database returns 1 for the artefact with no server, which is an
answer and not an error, and bt_has_local_database "$x"; rc=$? is not exempt
from errexit, so the verdict died at that line before it could print anything.
All 66 tests passed on that library, because bats run turns errexit off.

The idiom is || rc=$? now, and the 67th test runs the helpers in a script
with set -euo pipefail, which is the only way a unit test sees this.

Coverage

67 bats tests, 100 percent (284/284) of tests/lib/boot-test-lib.sh under
kcov, the threshold the workflow enforces.

What upstream ships and this does not

libapache2-mod-python (absent from Debian 13), php-xdebug, php-pear, and
conf-available/remove-upgrade-header.conf. Each is in the changelog with its
reason. The last is a web server setting with no database in it and is
proposed for apache-php, where both stacks would get it. Keeping any of them
would make this recipe differ from LAPP in something other than the database.

What I would change outside this repository, and did not

  • unit-mariadb should set its own bind addresses. unit-postgresql sets
    listen_addresses inside its conf script, because the addresses a server
    answers on belong to whoever installs the server. unit-mariadb does not,
    so conf.d/main here writes 99-keel-bind.cnf and keel-mariadb carries
    the same file in its overlay. One commit in the component, two deletions in
    the consumers. Left alone because keel-mariadb has that change in flight
    (its PR #11 is open and the published mariadb layer still records
    units none).
  • common/conf/adminer-mysql should be split into a driver half and an
    account half. It starts the database to create an account, so it can only
    run in the artefact that has a server, while conf/adminer-pgsql is two
    sed lines and runs anywhere. Splitting it would remove the last structural
    difference between this recipe and LAPP's. common is shared and another
    change landed there tonight, so this is proposed and not made.
  • keel-apache-php's workflow passes roles: primary replica to
    test-appliance.yml, which boots two containers for a web layer with no
    replication. Its tests/coverage.sh also still carries the PostgreSQL
    include pattern. Both look like copies from the database appliance.
  • Neither unit has a v1.0.0 tag, though keel-mariadb,
    keel-postgresql and now these two recipes all document
    git clone --branch v1.0.0. The pin resolves through the version file, so
    the manifest is right either way, but the documented command does not work.

What argues against the composition

Nothing found. The package set is identical, every version is identical, the
symlinks are identical, the files attributable to the unit form are zero, both
artefacts boot and answer, and lamp-client is a strict subset of lamp. The
two things the measurement did surface are about other repositories: the
package closure of this stack is not reproducible run to run, which pinning
addresses, and a child inherits its parent's closure where a monolith
re-resolves it, which is a property of layering and is visible in the
manifest.

Closes #1. LAMP is now a child of the `apache-php` layer carrying the `mariadb` unit, and the recipe produces two artefacts split by content rather than by topology. ## The two artefacts | Layer | Parent | Units | Packages | | --- | --- | --- | --- | | `lamp` | `apache-php` | `mariadb@1.0.0` | 520 | | `lamp-client` | `apache-php` | none | 507 | They are built from the same commit of this repository in two product directories, and the only difference between those directories is whether `unit.d/mariadb` was cloned into it. `lamp-client` is a strict subset: 13 packages fewer and none of its own, all 13 the server closure (`default-mysql-server`, `mariadb-server`, `mariadb-server-compat`, `mariadb-server-core`, `galera-4`, `webmin-mysql`, `python3-pymysql`, and `gawk`, `lsof`, `liblsof0`, `libmpfr6`, `libsigsegv2`, `socat`). Nothing in the recipe asks which artefact is being built by name. `conf.d/main` reads what dpkg says the image contains, because a makefile variable says what was asked for and dpkg says what happened. ## 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 `lamp-mono` expressing the same stack as one recipe on `core`, built twice so the comparison has a noise floor to be read against. All builds at `SOURCE_DATE_EPOCH=1700000000`, `common` 6803422, fab 1.1.1+keel2, parent `core` 7acf2c53 and `apache-php` b9dd64db. | | packages | regular files | symlinks | | --- | --- | --- | --- | | control, first run | 517 | 38,989 | 3,565 | | control, second run | 519 | 39,031 | 3,594 | | composed, with the unit | 520 | 39,034 | 3,594 | **The noise floor first.** Two runs of the identical control differ by 2 packages (`php-json`, `php8.4-phpdbg`), 198 files, 42 paths and 29 symlinks. The package set of this stack is not deterministic run to run: both packages arrive through composer's dependency closure and apt does not resolve it the same way twice. That is the instrument decision 0012 exists to fix, and it is why the third build is the point of the method. **Against the second control run**, which is the one the composed build should be read against: | pair | packages differing | files differing | paths only on one side | symlinks | | --- | --- | --- | --- | --- | | control vs control | 2 | 198 | 42 | 29 | | control, second run, vs composed | 1 | 182 | 3 | 0 | The one package is `turnkey-apache-php-19.0`, the parent layer's own release record, and the 3 paths are that package's `changelog`, `.list` and `.md5sums`. A chain records every layer it is made of and a monolith records one; that is correct and cannot be otherwise. Every version of every shared package is identical, all 519. **Differences attributable to the unit form**, that is the control-vs-composed set minus the control-vs-control set: **2 paths**, `/etc/hostname` and `/etc/hosts`, and both are the product's own name, because the control has to be a differently named product to exist beside the real one (`lamp-mono` against `lamp`, measured in both trees). Net attributable to the unit form: **zero**. The 182 that do differ, all of them: 167 under `/var/lib/mysql/**`, the data directory the database writes when the conf script starts it, same sizes with a few bytes of creation stamp each; `/etc/hostname` and `/etc/hosts`, the two above; `/root/.wget-hsts`, a clock written when the component downloads mysqltuner; and 12 that are install-time state of the kind the M0 allow-list already freezes: `/var/lib/dpkg/status`, `status-old`, `triggers/File`, `/var/lib/apt/extended_states`, `/var/cache/ldconfig/aux-cache`, `/var/log/alternatives.log`, `/var/log/webmin/webmin.log`, `/var/webmin/module.infos.cache`, `/etc/webmin/apache/config`, `/etc/webmin/apache/httpd-info`, `/etc/webmin/mysql/config` and `/etc/webmin/time/config`. One correction to the method, because it changed the numbers. The first run of the comparison read `sha256sum` output with `join` on whitespace, so the one path in the tree whose name contains a space, `setuptools/_vendor/jaraco/text/Lorem ipsum.txt`, was split and counted as a difference in every pair. It is byte identical in all three trees. The comparison is keyed on a tab-separated path now, and the numbers above are the corrected ones: 198 and 182 rather than 199 and 183. ## The boot test, which proves the stack rather than the ports Both artefacts, on the build host, over the container's global IPv6 address. `lamp`: ``` 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 'MariaDB 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-mysql is installed boot-test: webmin answered 200 on port 12321 boot-test: the database listens on [::1]:3306 boot-test: the database answered on [::1]:3306 as 'adminer@localhost' with the declared secret boot-test: the database listens on [127.0.0.1]:3306 boot-test: the database answered on [127.0.0.1]:3306 as 'adminer@localhost' with the declared secret keel diff: no drift boot-test: lamp boot test passed ``` `lamp-client`, with the 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 'MariaDB server you configure elsewhere' boot-test: webmin-apache is installed boot-test: mariadb-server is not installed boot-test: there is no /var/lib/mysql boot-test: nothing listens on port 3306 boot-test: no mariadbd is running keel diff: no drift boot-test: lamp-client boot test passed ``` The absence is asked four ways because each can be true while the others are false, and an idle server in that artefact would be the cheapest failure to ship and the most expensive to find. ## A bug the boot test found, and the test that now holds it The first `lamp-client` run exited after the PHP verdict with no message. `bt_has_local_database` returns 1 for the artefact with no server, which is an answer and not an error, and `bt_has_local_database "$x"; rc=$?` is not exempt from errexit, so the verdict died at that line before it could print anything. All 66 tests passed on that library, because `bats run` turns errexit off. The idiom is `|| rc=$?` now, and the 67th test runs the helpers in a script with `set -euo pipefail`, which is the only way a unit test sees this. ## Coverage 67 bats tests, **100 percent (284/284)** of `tests/lib/boot-test-lib.sh` under kcov, the threshold the workflow enforces. ## What upstream ships and this does not `libapache2-mod-python` (absent from Debian 13), `php-xdebug`, `php-pear`, and `conf-available/remove-upgrade-header.conf`. Each is in the changelog with its reason. The last is a web server setting with no database in it and is proposed for `apache-php`, where both stacks would get it. Keeping any of them would make this recipe differ from LAPP in something other than the database. ## What I would change outside this repository, and did not - **`unit-mariadb` should set its own bind addresses.** `unit-postgresql` sets `listen_addresses` inside its conf script, because the addresses a server answers on belong to whoever installs the server. `unit-mariadb` does not, so `conf.d/main` here writes `99-keel-bind.cnf` and `keel-mariadb` carries the same file in its overlay. One commit in the component, two deletions in the consumers. Left alone because `keel-mariadb` has that change in flight (its PR #11 is open and the published `mariadb` layer still records `units none`). - **`common/conf/adminer-mysql` should be split** into a driver half and an account half. It starts the database to create an account, so it can only run in the artefact that has a server, while `conf/adminer-pgsql` is two `sed` lines and runs anywhere. Splitting it would remove the last structural difference between this recipe and LAPP's. `common` is shared and another change landed there tonight, so this is proposed and not made. - **`keel-apache-php`'s workflow passes `roles: primary replica`** to `test-appliance.yml`, which boots two containers for a web layer with no replication. Its `tests/coverage.sh` also still carries the PostgreSQL include pattern. Both look like copies from the database appliance. - **Neither unit has a `v1.0.0` tag**, though `keel-mariadb`, `keel-postgresql` and now these two recipes all document `git clone --branch v1.0.0`. The pin resolves through the `version` file, so the manifest is right either way, but the documented command does not work. ## What argues against the composition Nothing found. The package set is identical, every version is identical, the symlinks are identical, the files attributable to the unit form are zero, both artefacts boot and answer, and `lamp-client` is a strict subset of `lamp`. The two things the measurement did surface are about other repositories: the package closure of this stack is not reproducible run to run, which pinning addresses, and a child inherits its parent's closure where a monolith re-resolves it, which is a property of layering and is visible in the manifest.
Commenting is not possible because the repository is archived.
No description provided.