Compare commits

...

3 Commits

  1. 8
      Makefile
  2. 13
      README.md
  3. 6
      nginx/Makefile
  4. 60
      nginx/README.md
  5. 37
      postgresql/Makefile
  6. 104
      postgresql/README.md
  7. 2
      postgresql/config/config.env
  8. 6
      postgresql/config/init.sh
  9. 149
      postgresql/config/upgrade.sh
  10. 2
      postgresql/postgresql-init.container
  11. 9
      postgresql/postgresql-upgrade.container

8
Makefile

@ -67,6 +67,14 @@ uninstall: pre-requisites
rm -f $(TARGET_QUADLETS_FILES) $(TARGET_SYSTEMD_FILES) $(TARGET_CONFIG_FILES)
systemctl daemon-reload
tail-logs: pre-requisites
@run() { echo $$*; "$$@"; }; \
declare -a journalctl_args=( -f ); \
for unit in $(SYSTEMD_MAIN_UNIT_NAMES) $(QUADLET_UNIT_NAMES); do \
journalctl_args+=( -u "$$unit" ); \
done; \
run journalctl "$${journalctl_args[@]}"
clean: pre-requisites
@run() { echo $$*; "$$@"; }; \
read -p "This will remove all data of '$(PROJECT_NAME)'. Are you sure? (only 'yes' is accepted) " ans; \

13
README.md

@ -0,0 +1,13 @@
# Podman Quadlet Cookbook
[Podman Quadlets](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) are awesome, but vastly under-utilized in the Open Source communities.
This repository gathers all the recipes (hence the name "Cookbook") to deploy Open Source technologies using Podman Quadlets.
## Current cookbooks
- [nginx](nginx/): starts Nginx, content is initialized / updated from a GIT repository
- [postgresql](postgresql/): starts a PostgreSQL server, handles automated major upgrades, periodic backup and initialization of the database from the last backup.
## License
MIT

6
nginx/Makefile

@ -1,2 +1,8 @@
PARENT_DIR := ..
include $(PARENT_DIR)/Makefile
.PHONY: test
test: uninstall clean install
@echo "Running Nginx tests..."
curl -sSfL -I http://localhost/

60
nginx/README.md

@ -0,0 +1,60 @@
# Podman Quadlet: Nginx
## Overview
Nginx is started as a Podman Quadlet and before that, the content to serve is initialized (`git clone`) or updated (`git pull`) from a GIT repository.
## Usage
In a separate terminal, follow the logs.
```sh
sudo make tail-logs
```
Install the Podman Quadlets and start Nginx.
```sh
sudo make clean install
```
You should see the **nginx-init.service** cloning this git repository to fetch the content to serve.
Then, the **nginx-server.service** should start up.
You can check that the content is indeed served on port 80.
```
$ curl http://localhost/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
```
Then restart the **nginx.target** unit.
```sh
sudo systemctl restart nginx.target
```
In the logs, you should see the **nginx-update.service** starting up and executing a `git pull` to update the content to serve.
Then, the **nginx-server.service** should start up.
Finally, remove the quadlets, their configuration and their data.
```sh
sudo make uninstall clean
```
## Integration tests
```sh
sudo make test
```

37
postgresql/Makefile

@ -1,2 +1,39 @@
PARENT_DIR := ..
include $(PARENT_DIR)/Makefile
.PHONY: test test-set-pgmajor
PG_MAJOR_START ?= 14
PG_MAJOR_LAST ?= 18
test-set-pgmajor:
sed -i 's/^PG_MAJOR=.*/PG_MAJOR=$(PG_MAJOR_START)/' config/config.env
test: uninstall clean test-set-pgmajor install
@echo "Running PostgreSQL integration tests..."; \
set -Eeuo pipefail; \
sleep 2; \
echo "Creating a test database and a witness table..."; \
podman exec postgresql-server su postgres -c "createdb test"; \
podman exec postgresql-server su postgres -c "psql -U postgres -d test -c \"CREATE TABLE witness (id SERIAL PRIMARY KEY, version VARCHAR); INSERT INTO witness (version) SELECT version();\""; \
podman exec postgresql-server su postgres -c "psql -U postgres -d test -c \"SELECT * FROM witness;\""; \
for (( ver=$(PG_MAJOR_START); ver<$(PG_MAJOR_LAST); ver++ )); do \
echo "Running a backup..."; \
nextver=$$(($$ver + 1)); \
systemctl start postgresql-backup.service; \
systemctl stop postgresql.target; \
sleep 1; \
rm -rf /var/lib/quadlets/postgresql/{$$ver,$$nextver,data,latest,.initialized}; \
echo "Restoring the backup to PostgreSQL $$ver..."; \
systemctl start postgresql.target; \
sleep 2; \
podman exec postgresql-server su postgres -c "psql -U postgres -d test -c \"SELECT * FROM witness;\""; \
echo "Testing upgrade from PostgreSQL $$ver to $$nextver..."; \
systemctl stop postgresql.target; \
sed -i "s/^PG_MAJOR=.*/PG_MAJOR=$$nextver/" /etc/quadlets/postgresql/config.env; \
systemctl start postgresql.target; \
sleep 2; \
echo "Inserting line into the witness table..."; \
podman exec postgresql-server su postgres -c "psql -U postgres -d test -c \"INSERT INTO witness (version) SELECT version();\""; \
done; \
podman exec postgresql-server su postgres -c "psql -U postgres -d test -c \"SELECT * FROM witness;\""; \
echo "PostgreSQL upgrade tests completed."

104
postgresql/README.md

@ -0,0 +1,104 @@
# Podman Quadlet: PostgreSQL
## Overview
PostgreSQL is started as a Podman Quadlet and before that, the database is initialized:
- either from a previous backup,
- or from scratch using SQL statements or scripts.
The upgrade process between major versions is handled by a one-off job before the database server startup.
Finally, a Podman Quadlet is provided to perform a backup of the database, including a simple retention policy.
## Usage
In a separate terminal, follow the logs.
```sh
sudo make tail-logs
```
Install the Podman Quadlets and start PostgreSQL.
```sh
sudo make clean install
```
You should see the **postgresql-set-major.service** starting up to set the symlink pointing to the PGDATA directory of the desired major version.
Then, the **postgresql-init.service** should start up and initialize the database from scratch.
Finally, the **postgresql-server.service** is started.
Restart the **postgresql.target** unit.
```sh
sudo systemctl restart postgresql.target
```
You should see in the logs that the **postgresql-init.service** is skipped (because the database is already initialized) and the **postgresql-server.service** unit is started.
Increment the PostgreSQL major version number.
```sh
awk -i inplace -F= '/PG_MAJOR=/ { $2=$2+1; print $1"="$2; next } 1' /etc/quadlets/postgresql/config.env
```
Restart the **postgresql.target** unit.
```sh
sudo systemctl restart postgresql.target
```
In the logs, you should see that the **postgresql-upgrade.service** converts the database files to the new major version.
Make backups of the database.
```sh
for i in $(seq 1 10); do
sudo systemctl start postgresql-backup.service
sleep 1
done
```
In the logs, you should see ten runs of the **postgresql-backup.service** unit.
And in the three last runs, the retention policy should be kicked in to clean up old backup files.
Now, stop the database server.
```sh
sudo systemctl stop postgresql.target
```
Remove all the PostgreSQL files (except the backups).
```sh
sudo find /var/lib/quadlets/postgresql/ -maxdepth 1 -mindepth 1 \! -name backup -exec rm -rf '{}' \;
```
Confirm there is no more data in `/var/lib/quadlets/postgresql`.
```
$ sudo ls -l /var/lib/quadlets/postgresql
total 0
drwx------. 1 avahi avahi 38 1 déc. 21:04 backup
```
Start the PostgreSQL database server.
```sh
sudo systemctl start postgresql.target
```
In the logs, you should see the **postgresql-init.service** unit restoring the database from the last backup.
Finally, remove the quadlets, their configuration and their data.
```sh
sudo make uninstall clean
```
## Integration tests
```sh
sudo make test
```

2
postgresql/config/config.env

@ -4,5 +4,5 @@ POSTGRES_DB=postgres
POSTGRES_HOST_AUTH_METHOD=scram-sha-256
POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256
PGPORT=5432
PG_MAJOR=17
PG_MAJOR=14
POSTGRES_BACKUP_RETENTION=7

6
postgresql/config/init.sh

@ -2,6 +2,12 @@
set -Eeuo pipefail
echo "PostgreSQL binaries version:"
postgres --version
echo
echo "PostgreSQL data directory: $PGDATA"
echo
last_backup=""
for f in /var/lib/postgresql/backup/*/backup_manifest; do
# If there are no backups, the glob pattern above won't match any files

149
postgresql/config/upgrade.sh

@ -0,0 +1,149 @@
#!/bin/bash
set -Eeuo pipefail
# Find the latest PostgreSQL data directory
SOURCE_PGDATA=""
last_version=""
for version_file in /var/lib/postgresql/*/docker/PG_VERSION; do
if [ -f "$version_file" ]; then
version_dir=$(dirname "$version_file")
version_major=$(cat "$version_file")
if [ -z "$last_version" ] || [ "$version_major" -gt "$last_version" ]; then
last_version="$version_major"
SOURCE_PGDATA="$version_dir"
fi
fi
done
if [ -z "$SOURCE_PGDATA" ] || [ ! -d "$SOURCE_PGDATA" ]; then
echo "No PostgreSQL data directory found."
exit 1
fi
echo "Using PostgreSQL data directory: $SOURCE_PGDATA"
# Upgrade destination
TARGET_MAJOR_VERSION=${PGTARGET%%.*}
TARGET_PATH="/usr/local/bin/"
TARGET_PGDATA="/var/lib/postgresql/${TARGET_MAJOR_VERSION}/docker"
# Upgrade source
SOURCE_MAJOR_VERSION=$(cat "${SOURCE_PGDATA}/PG_VERSION")
SOURCE_PATH="/usr/local-pg${SOURCE_MAJOR_VERSION}/bin"
# Reuse functions from the official entrypoint script
source /usr/local/bin/postgres-docker-entrypoint.sh
# Because they may have been over by the sourced script, reset all flags
set -Eeuo pipefail
# if first arg looks like a flag, assume we want to run postgres server
if [ "$#" -eq 0 ] || [ "${1:0:1}" = '-' ]; then
set -- postgres "$@"
fi
# Setup environment variables
docker_setup_env
##
## Sanity checks
##
# No need to upgrade if same major version
if [ "${SOURCE_MAJOR_VERSION}" == "${TARGET_MAJOR_VERSION}" ]; then
echo "PostgreSQL data files version matches target version. No upgrade required."
exit 0
fi
# No automatic upgrade support for PostgreSQL versions less than 14
if [ "${SOURCE_MAJOR_VERSION}" -lt 14 ]; then
echo "PosgreSQL <14 is no longer supported for automatic upgrade. Please perform a manual upgrade."
exit 1
fi
# No downgrade support
if [ "${SOURCE_MAJOR_VERSION}" -gt "${TARGET_MAJOR_VERSION}" ]; then
echo "Downgrades are not supported. Aborting."
exit 1
fi
# Check for concurrent upgrade processes
if [ -f "${SOURCE_PGDATA}/upgrade_in_progress.lock" ]; then
echo "Another upgrade process seems to be running (upgrade_in_progress.lock file found). Aborting."
exit 2
fi
# On PG v18, we have to check that data checksums, be it positive or negative, is set on the initdb args
# even when the user already provided initdb args, because otherwise Postgres v18 assumes you want checksums
# we now do this on every version to avoid one more conditional
# it also adds support for people who used Postgres with checksums before v18
if [[ -z "${POSTGRES_INITDB_ARGS:-}" || "${POSTGRES_INITDB_ARGS:-}" != *"data-checksums"* ]]; then
DATA_CHECKSUMS_ENABLED=$(echo 'SHOW DATA_CHECKSUMS' | "${SOURCE_PATH}/postgres" --single "${@:2}" -D "${SOURCE_PGDATA}" "${POSTGRES_DB}" | grep 'data_checksums = "' | cut -d '"' -f 2)
if [ "$DATA_CHECKSUMS_ENABLED" == "on" ]; then
DATA_CHECKSUMS_PARAMETER="--data-checksums"
elif [ "$TARGET_MAJOR_VERSION" -eq 18 ]; then
# Postgres v18 enables data checksums by default and is the only version with this opt-out parameter
DATA_CHECKSUMS_PARAMETER="--no-data-checksums"
fi
POSTGRES_INITDB_ARGS="${POSTGRES_INITDB_ARGS:-} ${DATA_CHECKSUMS_PARAMETER:-}"
fi
# Flags the data directory as being in the middle of an upgrade
mkdir -p "${TARGET_PGDATA}"
touch "${SOURCE_PGDATA}/upgrade_in_progress.lock"
# Now PGDATA points to the target data directory
export PGDATA="${TARGET_PGDATA}"
# Initialize target data directory
docker_verify_minimum_env
docker_init_database_dir
# Change into the PostgreSQL database directory, to avoid a pg_upgrade error about write permissions
cd "${PGDATA}"
# Perform the upgrade
echo "Upgrading PostgreSQL from version ${SOURCE_MAJOR_VERSION} to ${TARGET_MAJOR_VERSION}..."
"${TARGET_PATH}/pg_upgrade" --username="${POSTGRES_USER}" --link \
--old-datadir "${SOURCE_PGDATA}" --new-datadir "${TARGET_PGDATA}" \
--old-bindir "${SOURCE_PATH}" --new-bindir "${TARGET_PATH}" \
--socketdir="/var/run/postgresql" \
--old-options "${run_options[*]}" --new-options "${run_options[*]}"
# Re-use the pg_hba.conf and pg_ident.conf from the old data directory
cp -f "${SOURCE_PGDATA}/pg_hba.conf" "${SOURCE_PGDATA}/pg_ident.conf" "${TARGET_PGDATA}"
# Set PGPASSWORD in case password authentication is used
if [ -z "${PGPASSWORD:-}" ] && [ -n "${POSTGRES_PASSWORD:-}" ]; then
export PGPASSWORD="${POSTGRES_PASSWORD}"
fi
# Start a temporary PostgreSQL server
docker_temp_server_start "$@"
if [ -n "${POSTGRES_UPDATE_SCRIPT:-}" ] && [ -f "${POSTGRES_UPDATE_SCRIPT}" ]; then
echo "Running update script: ${POSTGRES_UPDATE_SCRIPT}"
psql --username="${POSTGRES_USER}" -f "${POSTGRES_UPDATE_SCRIPT}"
fi
echo "Updating query planner stats"
declare -a database_list=( $(echo 'SELECT datname FROM pg_catalog.pg_database WHERE datistemplate IS FALSE' | psql --username="${POSTGRES_USER}" -1t --csv "${POSTGRES_DB}") )
for database in "${database_list[@]}"; do
echo "VACUUM (ANALYZE, VERBOSE, INDEX_CLEANUP FALSE)" | psql --username="${POSTGRES_USER}" -t --csv "${database}"
done
if [[ "${PGAUTO_REINDEX:-}" != "no" ]]; then
echo "Reindexing the databases"
if [[ "$TARGET_MAJOR_VERSION" -le 15 ]]; then
reindexdb --all --username="${POSTGRES_USER}"
else
reindexdb --all --concurrently --username="${POSTGRES_USER}"
fi
echo "End of reindexing the databases"
fi
# Stop the temporary PostgreSQL server
unset PGPASSWORD
docker_temp_server_stop
# Clean up lock files
rm -f "${SOURCE_PGDATA}/upgrade_in_progress.lock"
echo "PostgreSQL upgrade from version ${SOURCE_MAJOR_VERSION} to ${TARGET_MAJOR_VERSION} completed successfully."

2
postgresql/postgresql-init.container

@ -26,7 +26,7 @@ Environment=PGDATA=/var/lib/postgresql/${PG_MAJOR}/docker
# Those environment variables will be injected by podman into the container
EnvironmentFile=/etc/quadlets/postgresql/config.env
# Use the official entrypoint script to initialize the database
# Use our entrypoint script to initialize the database
Entrypoint=/usr/local/bin/init.sh
# Volume mounts

9
postgresql/postgresql-upgrade.container

@ -22,15 +22,16 @@ Image=docker.io/pgautoupgrade/pgautoupgrade:${PG_MAJOR}-alpine
# Network configuration
Network=host
# PostgreSQL storage is specific to major version
Environment=PGDATA=/var/lib/postgresql/${PG_MAJOR}/docker
Environment=PGAUTO_ONESHOT=yes
# Those environment variables will be injected by podman into the container
EnvironmentFile=/etc/quadlets/postgresql/config.env
# Use our entrypoint script to upgrade the database
Entrypoint=/usr/local/bin/upgrade.sh
User=postgres
# Volume mounts
Volume=/var/lib/quadlets/postgresql:/var/lib/postgresql:z
Volume=/etc/quadlets/postgresql/upgrade.sh:/usr/local/bin/upgrade.sh:z,ro
[Service]
Restart=no

Loading…
Cancel
Save