max / alloy
5 files changed,
+72 insertions,
-11 deletions
| @@ -830,13 +830,15 @@ | |||
| 830 | 830 | ||
| 831 | 831 | # DB is the database the image carries, and it exists because the version | |
| 832 | 832 | # matters more than the presence. `postgres16` installs postgresql16 and | |
| 833 | - | # postgresql16-server, both 16.14-1.fc43 out of `updates`: three packages, | |
| 834 | - | # 9 MiB of download and 38 MiB installed, measured with | |
| 835 | - | # `dnf install --assumeno` on 2026-08-21. | |
| 833 | + | # postgresql16-server out of `updates`, whatever 16.x they are on the day: | |
| 834 | + | # three packages, 9 MiB of download and 38 MiB installed, measured with | |
| 835 | + | # `dnf install --assumeno` against 16.14-1.fc43 on 2026-08-21. The install arm | |
| 836 | + | # greps for the major and not the patch, because the patch moves under us and | |
| 837 | + | # already has: 16.15-1.fc43 by 2026-08-25. | |
| 836 | 838 | # | |
| 837 | 839 | # Fedora's own `postgresql-server` is 18.4 and is deliberately not what this | |
| 838 | 840 | # installs. Sando's scratch cluster exists to test MNW against what production | |
| 839 | - | # runs, and prod `alpha-west-1` is PostgreSQL 16.14; a scratch cluster two | |
| 841 | + | # runs, and prod `alpha-west-1` is PostgreSQL 16; a scratch cluster two | |
| 840 | 842 | # majors ahead tests a database nobody deploys. | |
| 841 | 843 | # | |
| 842 | 844 | # The default is `none` and the image carries neither binary today | |
| @@ -1959,7 +1961,8 @@ | |||
| 1959 | 1961 | RUN set -eu; \ | |
| 1960 | 1962 | case "$DB" in \ | |
| 1961 | 1963 | postgres16) \ | |
| 1962 | - | dnf install -y postgresql16 postgresql16-server; \ | |
| 1964 | + | dnf install -y --setopt=install_weak_deps=False \ | |
| 1965 | + | postgresql16 postgresql16-server; \ | |
| 1963 | 1966 | dnf clean all; \ | |
| 1964 | 1967 | command -v psql >/dev/null \ | |
| 1965 | 1968 | || { echo "DB=postgres16 was asked for and psql is not in the image" >&2; exit 1; }; \ | |
| @@ -1974,10 +1977,10 @@ | |||
| 1974 | 1977 | ! command -v postgres >/dev/null \ | |
| 1975 | 1978 | || { echo "DB=none and postgres is in the image anyway" >&2; exit 1; } \ | |
| 1976 | 1979 | ;; \ | |
| 1980 | + | *) echo "unknown DB '$DB' reached the install arm; the validator did not run" >&2; exit 1 ;; \ | |
| 1977 | 1981 | esac; \ | |
| 1978 | 1982 | echo "db: $DB" | |
| 1979 | 1983 | ||
| 1980 | - | ||
| 1981 | 1984 | # ===================================================================== | |
| 1982 | 1985 | # Printing — driverless only, and deliberately in its own layer. | |
| 1983 | 1986 | # | |
| @@ -3742,6 +3745,7 @@ | |||
| 3742 | 3745 | case "$PROFILE" in \ | |
| 3743 | 3746 | client) variant="Client" ;; \ | |
| 3744 | 3747 | server) variant="Server" ;; \ | |
| 3748 | + | *) echo "unknown PROFILE '$PROFILE' reached the os-release stamp" >&2; exit 1 ;; \ | |
| 3745 | 3749 | esac; \ | |
| 3746 | 3750 | sed -i "s/^VARIANT=.*/VARIANT=\"$variant\"/; \ | |
| 3747 | 3751 | s/^VARIANT_ID=.*/VARIANT_ID=$PROFILE/" /usr/lib/os-release; \ |
| @@ -90,8 +90,8 @@ | |||
| 90 | 90 | the finished system unless you replace it. | |
| 91 | 91 | ||
| 92 | 92 | `alloy image` is the same thing with a form around it: it asks for a profile, a | |
| 93 | - | browser, languages, a hostname and a path to a public key, and runs the script | |
| 94 | - | above with what you chose. | |
| 93 | + | browser, languages, a database, what to trim, a hostname and a path to a public | |
| 94 | + | key, and runs the script above with what you chose. | |
| 95 | 95 | ||
| 96 | 96 | Chapter 3 covers the install itself, and what the SSH posture means once the | |
| 97 | 97 | machine is up. | |
| @@ -111,6 +111,24 @@ | |||
| 111 | 111 | `crates/alloy/credits.toml`, which is what the installer's credits page | |
| 112 | 112 | reads. It is hand-curated on purpose, so nothing adds itself. | |
| 113 | 113 | ||
| 114 | + | ## Carrying a database | |
| 115 | + | ||
| 116 | + | `DB` decides whether the image ships PostgreSQL, and it defaults to `none`. A | |
| 117 | + | stock mint carries no database and nothing about it changes by this dial | |
| 118 | + | existing. | |
| 119 | + | ||
| 120 | + | podman build --build-arg DB=postgres16 ... | |
| 121 | + | ||
| 122 | + | `postgres16` installs `postgresql16` and `postgresql16-server` from `updates`: | |
| 123 | + | three packages, 9 MiB of download and 38 MiB installed. It is deliberately not | |
| 124 | + | Fedora's own `postgresql-server`, which is 18.4. The build host runs Sando's | |
| 125 | + | scratch cluster to test MNW against what production runs, and production is on | |
| 126 | + | 16, so a cluster two majors ahead would test a database nobody deploys. | |
| 127 | + | ||
| 128 | + | The arm puts the binaries on `PATH` and stops. No `systemctl enable`, no | |
| 129 | + | `initdb`: a cluster is state belonging to the machine that runs it rather than | |
| 130 | + | to the image. | |
| 131 | + | ||
| 114 | 132 | ## When the build cannot reach the fonts | |
| 115 | 133 | ||
| 116 | 134 | Alloy cuts its two faces during the build from a pinned upstream base, and the |
| @@ -6,7 +6,7 @@ | |||
| 6 | 6 | ID=alloy | |
| 7 | 7 | ID_LIKE=fedora | |
| 8 | 8 | VARIANT="<profile>" | |
| 9 | - | VARIANT_ID=<profile> | |
| 9 | + | VARIANT_ID="<profile>" | |
| 10 | 10 | ANSI_COLOR="0;38;2;138;69;48" | |
| 11 | 11 | LOGO=alloy | |
| 12 | 12 | DEFAULT_HOSTNAME=alloy |
| @@ -226,7 +226,7 @@ | |||
| 226 | 226 | #[test] | |
| 227 | 227 | fn the_committed_os_release_carries_the_profile_placeholder() { | |
| 228 | 228 | let os_release = read("usr/lib/os-release"); | |
| 229 | - | for line in ["VARIANT=\"<profile>\"", "VARIANT_ID=<profile>"] { | |
| 229 | + | for line in ["VARIANT=\"<profile>\"", "VARIANT_ID=\"<profile>\""] { | |
| 230 | 230 | assert!( | |
| 231 | 231 | os_release.lines().any(|candidate| candidate == line), | |
| 232 | 232 | "os-release has no `{line}` for the profile step to rewrite", | |
| @@ -244,6 +244,36 @@ | |||
| 244 | 244 | } | |
| 245 | 245 | } | |
| 246 | 246 | ||
| 247 | + | // os-release is sourced by shell (`. /etc/os-release`) far more often than it | |
| 248 | + | // is parsed. An unquoted value carrying `<` is a redirect, not an assignment, | |
| 249 | + | // so a placeholder written bare turns the whole file into a syntax error and | |
| 250 | + | // takes every reader down with it rather than only the field it belongs to. | |
| 251 | + | #[test] | |
| 252 | + | fn every_committed_os_release_value_is_shell_safe() { | |
| 253 | + | for line in read("usr/lib/os-release").lines() { | |
| 254 | + | let line = line.trim(); | |
| 255 | + | if line.is_empty() || line.starts_with('#') { | |
| 256 | + | continue; | |
| 257 | + | } | |
| 258 | + | let (key, value) = line | |
| 259 | + | .split_once('=') | |
| 260 | + | .unwrap_or_else(|| panic!("os-release line is not an assignment: `{line}`")); | |
| 261 | + | assert!( | |
| 262 | + | key.chars().all(|c| c.is_ascii_uppercase() || c == '_'), | |
| 263 | + | "os-release key `{key}` is not a plain shell name", | |
| 264 | + | ); | |
| 265 | + | let quoted = value.len() >= 2 && value.starts_with('"') && value.ends_with('"'); | |
| 266 | + | let bare_safe = !value.is_empty() | |
| 267 | + | && value | |
| 268 | + | .chars() | |
| 269 | + | .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':' | '/')); | |
| 270 | + | assert!( | |
| 271 | + | quoted || bare_safe, | |
| 272 | + | "os-release value for {key} must be quoted to stay sourceable: `{value}`", | |
| 273 | + | ); | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | ||
| 247 | 277 | #[test] | |
| 248 | 278 | fn the_committed_os_release_carries_what_the_build_stamp_rewrites() { | |
| 249 | 279 | let os_release = read("usr/lib/os-release"); |
| @@ -294,10 +294,19 @@ | |||
| 294 | 294 | #[test] | |
| 295 | 295 | fn the_db_dial_is_validated_and_defaults_the_same_in_both_files() { | |
| 296 | 296 | let text = containerfile(); | |
| 297 | + | // The validator arm specifically, not any `case "$DB" in`. The install arm | |
| 298 | + | // is a second one, so a bare `contains` stays green with the validator | |
| 299 | + | // deleted, and an unknown value then reaches an install `case` that matches | |
| 300 | + | // nothing. | |
| 297 | 301 | assert!( | |
| 298 | - | text.contains(r#"case "$DB" in"#), | |
| 302 | + | text.contains(r#"unknown DB '$DB'; expected 'none' or 'postgres16'"#), | |
| 299 | 303 | "DB is not validated against its set; a typo would build the wrong image silently" | |
| 300 | 304 | ); | |
| 305 | + | assert_eq!( | |
| 306 | + | text.matches(r#"case "$DB" in"#).count(), | |
| 307 | + | 2, | |
| 308 | + | "expected exactly two `case \"$DB\" in`: the validator and the install arm" | |
| 309 | + | ); | |
| 301 | 310 | ||
| 302 | 311 | let arg = text | |
| 303 | 312 | .lines() |