Add PostgreSQL deployment tooling and release validation
This commit is contained in:
+14
@@ -21,3 +21,17 @@ __pycache__/
|
|||||||
|
|
||||||
# Development candidate files
|
# Development candidate files
|
||||||
*.new
|
*.new
|
||||||
|
|
||||||
|
# Node-local PostgreSQL configuration / secrets
|
||||||
|
quadlet/postgres.env
|
||||||
|
**/secrets/
|
||||||
|
*.secret
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Pacemaker live/runtime snapshots
|
||||||
|
pacemaker/cib.xml
|
||||||
|
pacemaker/current-cluster-state.txt
|
||||||
|
pacemaker/constraints.txt
|
||||||
|
pacemaker/pcs-config.txt
|
||||||
|
pacemaker/postgresql-ha.txt
|
||||||
|
pacemaker/postgresql-ha-clone.txt
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
0675ddf1cadf497393404864370519c861ba4619787b086cf369e5b0113d6a44 ocf/loxthelion/ipvs-sync
|
||||||
|
5aa02fa065d1dba55486838fa4f0cd8eba982edaaf73cdc7d818223a09cbadc6 ocf/loxthelion/managed-container
|
||||||
|
10932919c4339b553c7c9dbf30b45a917c51c0a08f362e12678003d58b12fa82 ocf/loxthelion/managed-container.d/applications/postgresql/auth.sh
|
||||||
|
61d64b4a3a15c354f101ef34c5dc0425723f4204207d359b5377d2b9f04f6b23 ocf/loxthelion/managed-container.d/applications/postgresql/demote.sh
|
||||||
|
38323b217204132e572a9b1cfec8072773e502e49d6f0f96dfa2e565d332bbd1 ocf/loxthelion/managed-container.d/applications/postgresql/native_peers.sh
|
||||||
|
625e3b4fc6775164d351c98d1ef0e2c54cb2042c8a86015aa68649cb9c339260 ocf/loxthelion/managed-container.d/applications/postgresql/notify.sh
|
||||||
|
957a5a72ed277d0216e070abb80733483be3d421b9fa488e27fdd75bb2919548 ocf/loxthelion/managed-container.d/applications/postgresql/peer_config.sh
|
||||||
|
5091bc554915d1450fe939f94c0b0a370fecdb2ae5db9c4f0f55c340c0145519 ocf/loxthelion/managed-container.d/applications/postgresql/plugin.sh
|
||||||
|
c1d878dabf22ad3928d66a5542bda3c150e5e9b039926629e7c5fd3401d29be4 ocf/loxthelion/managed-container.d/applications/postgresql/postgres.sh
|
||||||
|
3a6c92e8fe7d0df2573b19be22dd707b89b1eaa8147bdc07ef84dcded51aaa31 ocf/loxthelion/managed-container.d/applications/postgresql/postgres.sh.pre-score-rejoin-reconcile-20260917-103742
|
||||||
|
d7547d66e4960a6aadfdd5cb1270431009453c8b769d0fa21f0b06d7fe7df686 ocf/loxthelion/managed-container.d/applications/postgresql/postgres.sh.pre-streaming-rejoin-marker-fix-20260917-103610
|
||||||
|
f482853a0c7992981e59b4ad78d1ee69851d5d60fdec1f695673228017dd8244 ocf/loxthelion/managed-container.d/applications/postgresql/promote.sh
|
||||||
|
5166fc3370f49ab14cd975ded4b03a74b9bd7dbff5762b41a15552c42f934ea7 ocf/loxthelion/managed-container.d/applications/postgresql/promotion_state.py
|
||||||
|
b0ebad749ed736f08ec748a693f12e8dc2541430e0d3db75bbb6b2b7e517f84b ocf/loxthelion/managed-container.d/applications/postgresql/rejoin.sh
|
||||||
|
0d3017fdca92b2dc5dba406065bc269622ce5140e3fce4a959ca95ce4a78e755 ocf/loxthelion/managed-container.d/applications/postgresql/replication.sh
|
||||||
|
d7d2fd7ab651d1f2f026bdb11d62e2c0c6ce6ee3128c9814d09550cf4304b3f2 ocf/loxthelion/managed-container.d/common.sh
|
||||||
|
2c897c3a21d924a1a865494cb5c76a3405e2de7fee55e41ccd7bee2511726927 ocf/loxthelion/managed-container.d/common.sh.pre-named-proxy-rc-20260917-100736
|
||||||
|
3a27ac9c63c61310da8b8711fa2c4400b37ea8eae2ee2a79e28bc63180b06c7c ocf/loxthelion/managed-container.d/container.sh
|
||||||
|
70431e0d390a3612339c7b5887286c1be4d6b305cbe8898e7895a211c26b6087 ocf/loxthelion/managed-container.d/engine.sh
|
||||||
|
6c92c095ce10ac95d843fdc1c49e31c19491ca22fdac960affd8649606d76d82 ocf/loxthelion/managed-container.d/lifecycles/systemd.sh
|
||||||
|
259e8cafa5b6d3fb54896ab37ff3917942c62c51eaa6e9bfa8ccf3afc6a7327b ocf/loxthelion/managed-container.d/proxy.sh
|
||||||
|
d5ca2aceddd5c8c5467e0997b099118279b9f54b96b8275e3a33182e1b992093 ocf/loxthelion/managed-container.d/proxy.sh.pre-named-proxy-rc-20260917-100736
|
||||||
|
f82e2571d309bce8e4d7e6aee6b61b68feb7d99dc85bb8b688b6123c13ccf6a7 ocf/loxthelion/managed-container.d/runtimes/podman.sh
|
||||||
|
8679baffebc77614fd80888d344ca55e11a13f84de6f096a2399c22a49c8cc6a ocf/loxthelion/managed-container.pre-entrypoint-selftest-20260917-105341
|
||||||
|
01eb9b161c8e3db289f09bf63953be779921e15bdf8b1a4d8ce7e60032b826e4 ocf/loxthelion/managed-container.pre-named-proxy-rc-20260917-100736
|
||||||
|
62ec256e108f40f91d626ea71e8945862aedb347e576a00e6bd1e15f9f363e70 ocf/loxthelion/managed-container.pre-proxy-load-fix-20260917-101231
|
||||||
|
4a99efea3d4a185e0630db89cfacc28aa8777bad9c747794ae69f70d87cd0327 pacemaker/README.md
|
||||||
|
a42bb59a058ffeb59c1baa96e403856910bbbaea98f9104ff93daf4b5970c780 postgres-host/postgres-ha-bootstrap
|
||||||
|
957a5a72ed277d0216e070abb80733483be3d421b9fa488e27fdd75bb2919548 postgres-host/postgres-ha-peer-config.sh
|
||||||
|
deb09e3ae40629f43b262ba4f9a7ebf59a1dca3da9d8c94478f670aea28bd0e1 quadlet/postgres.container
|
||||||
|
04a125068677bbee6e71d313ca9d95d35faa32851b79de781cd7b9f466561445 quadlet/postgres.env.example
|
||||||
|
761e4d5166e26507f34b3276258bd012ddbd7c667020a9797542e08b8976891a tests/run-all.sh
|
||||||
|
5e5d4fdd2b6c7e0b8b146de7aad06c1f96b221a10fec525b00d40e0d85709fb0 tests/test-entrypoint-structure.sh
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Pacemaker integration
|
||||||
|
|
||||||
|
The files generated by `pcs config`, `crm_mon`, and `cibadmin --query`
|
||||||
|
are intentionally not tracked here.
|
||||||
|
|
||||||
|
Those exports contain point-in-time cluster state, transient node
|
||||||
|
attributes, operation history, and temporary constraints, so they are
|
||||||
|
diagnostic snapshots rather than canonical configuration.
|
||||||
|
|
||||||
|
The `managed-container` OCF resource agent itself is under:
|
||||||
|
|
||||||
|
ocf/loxthelion/
|
||||||
|
|
||||||
|
Current PostgreSQL production design:
|
||||||
|
|
||||||
|
- promotable clone
|
||||||
|
- clone-max=3
|
||||||
|
- clone-node-max=1
|
||||||
|
- promoted-max=1
|
||||||
|
- promoted-node-max=1
|
||||||
|
- notify=true
|
||||||
|
- target-role=Promoted
|
||||||
|
- PostgreSQL VIP colocated with the Promoted instance
|
||||||
|
- promotion score controlled by the resource agent
|
||||||
Executable
+572
@@ -0,0 +1,572 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[postgres-ha-bootstrap] %s\n' "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
log "ERROR: $*"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
local name="$1"
|
||||||
|
[[ -n "${!name:-}" ]] || die "Required environment variable $name is not set"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
[[ -r "$path" ]] || die "Required file is not readable: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
as_postgres() {
|
||||||
|
runuser -u postgres -- "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL replication slot names are intentionally restricted here
|
||||||
|
# so values can safely be used in SQL and configuration.
|
||||||
|
validate_slot_name() {
|
||||||
|
[[ "$1" =~ ^[a-z0-9_]+$ ]] ||
|
||||||
|
die "Invalid replication slot name: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_secret_for_postgres() {
|
||||||
|
local source="$1"
|
||||||
|
local destination="$2"
|
||||||
|
|
||||||
|
require_file "$source"
|
||||||
|
|
||||||
|
install \
|
||||||
|
-o postgres \
|
||||||
|
-g postgres \
|
||||||
|
-m 0600 \
|
||||||
|
"$source" \
|
||||||
|
"$destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
pgdata_is_initialized() {
|
||||||
|
[[ -s "$PGDATA/PG_VERSION" ]] &&
|
||||||
|
[[ -s "$PGDATA/global/pg_control" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
pgdata_is_empty() {
|
||||||
|
[[ ! -d "$PGDATA" ]] && return 0
|
||||||
|
|
||||||
|
local first
|
||||||
|
first="$(find "$PGDATA" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null || true)"
|
||||||
|
[[ -z "$first" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
marker_path() {
|
||||||
|
printf '%s/%s\n' \
|
||||||
|
"$PGDATA" \
|
||||||
|
"${POSTGRES_BOOTSTRAP_MARKER:-.loxthelion-ha-initialized}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_managed_config() {
|
||||||
|
local config="$PGDATA/postgres-ha.conf"
|
||||||
|
|
||||||
|
cat > "$config" <<EOF_CONFIG
|
||||||
|
# Managed by postgres-ha-bootstrap.
|
||||||
|
# Do not place node-role-specific settings in this file.
|
||||||
|
|
||||||
|
listen_addresses = '*'
|
||||||
|
wal_level = '${POSTGRES_WAL_LEVEL:-replica}'
|
||||||
|
max_wal_senders = ${POSTGRES_MAX_WAL_SENDERS:-10}
|
||||||
|
max_replication_slots = ${POSTGRES_MAX_REPLICATION_SLOTS:-10}
|
||||||
|
hot_standby = ${POSTGRES_HOT_STANDBY:-on}
|
||||||
|
password_encryption = 'scram-sha-256'
|
||||||
|
EOF_CONFIG
|
||||||
|
|
||||||
|
chown postgres:postgres "$config"
|
||||||
|
chmod 0600 "$config"
|
||||||
|
|
||||||
|
if ! grep -Eq \
|
||||||
|
"^[[:space:]]*include_if_exists[[:space:]]*=[[:space:]]*'postgres-ha\.conf'" \
|
||||||
|
"$PGDATA/postgresql.conf"; then
|
||||||
|
|
||||||
|
cat >> "$PGDATA/postgresql.conf" <<'EOF_CONFIG'
|
||||||
|
|
||||||
|
# Loxthelion PostgreSQL HA configuration
|
||||||
|
include_if_exists = 'postgres-ha.conf'
|
||||||
|
EOF_CONFIG
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_managed_hba() {
|
||||||
|
local hba="$PGDATA/pg_hba.conf"
|
||||||
|
local tmp
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
|
||||||
|
# Remove a previous block generated by this script.
|
||||||
|
awk '
|
||||||
|
/^# BEGIN LOXTHELION POSTGRES HA$/ { skip=1; next }
|
||||||
|
/^# END LOXTHELION POSTGRES HA$/ { skip=0; next }
|
||||||
|
!skip { print }
|
||||||
|
' "$hba" > "$tmp"
|
||||||
|
|
||||||
|
cat >> "$tmp" <<EOF_HBA
|
||||||
|
|
||||||
|
# BEGIN LOXTHELION POSTGRES HA
|
||||||
|
host replication ${POSTGRES_REPLICATION_USER} ${POSTGRES_HA_CIDR} scram-sha-256
|
||||||
|
host postgres ${POSTGRES_REPLICATION_USER} ${POSTGRES_HA_CIDR} scram-sha-256
|
||||||
|
# END LOXTHELION POSTGRES HA
|
||||||
|
EOF_HBA
|
||||||
|
|
||||||
|
cat "$tmp" > "$hba"
|
||||||
|
rm -f "$tmp"
|
||||||
|
|
||||||
|
chown postgres:postgres "$hba"
|
||||||
|
chmod 0600 "$hba"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_persistent_pgpass() {
|
||||||
|
require_var POSTGRES_PRIMARY_HOST
|
||||||
|
require_var POSTGRES_PRIMARY_PORT
|
||||||
|
require_var POSTGRES_REPLICATION_USER
|
||||||
|
require_var POSTGRES_REPLICATION_PASSWORD_FILE
|
||||||
|
|
||||||
|
require_file "$POSTGRES_REPLICATION_PASSWORD_FILE"
|
||||||
|
|
||||||
|
local password
|
||||||
|
password="$(cat "$POSTGRES_REPLICATION_PASSWORD_FILE")"
|
||||||
|
|
||||||
|
mkdir -p /var/lib/postgresql
|
||||||
|
chown postgres:postgres /var/lib/postgresql
|
||||||
|
|
||||||
|
if [[ -n "${POSTGRES_REPLICATION_PEERS:-}" ]]; then
|
||||||
|
local endpoints hosts ports host port
|
||||||
|
endpoints=$(mc_peer_endpoints "$POSTGRES_REPLICATION_PEERS" "$POSTGRES_NODE_NAME") || die "Invalid replication peer map"
|
||||||
|
hosts=${endpoints%|*}; ports=${endpoints#*|}
|
||||||
|
[[ "$password" != *$'\n'* && "$password" != *$'\r'* ]] || die "Invalid multiline replication password"
|
||||||
|
password=${password//\\/\\\\}; password=${password//:/\\:}
|
||||||
|
(umask 077; : > /var/lib/postgresql/.pgpass)
|
||||||
|
while [[ -n "$hosts" ]]; do
|
||||||
|
host=${hosts%%,*}; port=${ports%%,*}
|
||||||
|
printf '%s:%s:*:%s:%s\n' "$host" "$port" "$POSTGRES_REPLICATION_USER" "$password" >> /var/lib/postgresql/.pgpass
|
||||||
|
[[ "$hosts" == *,* ]] || break
|
||||||
|
hosts=${hosts#*,}; ports=${ports#*,}
|
||||||
|
done
|
||||||
|
else
|
||||||
|
printf '%s:%s:*:%s:%s\n' \
|
||||||
|
"$POSTGRES_PRIMARY_HOST" \
|
||||||
|
"$POSTGRES_PRIMARY_PORT" \
|
||||||
|
"$POSTGRES_REPLICATION_USER" \
|
||||||
|
"$password" \
|
||||||
|
> /var/lib/postgresql/.pgpass
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown postgres:postgres /var/lib/postgresql/.pgpass
|
||||||
|
chmod 0600 /var/lib/postgresql/.pgpass
|
||||||
|
}
|
||||||
|
|
||||||
|
grant_rewind_privileges() {
|
||||||
|
require_var POSTGRES_USER
|
||||||
|
require_var POSTGRES_REPLICATION_USER
|
||||||
|
|
||||||
|
log "Ensuring pg_rewind privileges for $POSTGRES_REPLICATION_USER"
|
||||||
|
|
||||||
|
as_postgres psql \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--set=repl_user="$POSTGRES_REPLICATION_USER" <<'SQL'
|
||||||
|
GRANT EXECUTE
|
||||||
|
ON FUNCTION pg_catalog.pg_ls_dir(text, boolean, boolean)
|
||||||
|
TO :"repl_user";
|
||||||
|
|
||||||
|
GRANT EXECUTE
|
||||||
|
ON FUNCTION pg_catalog.pg_stat_file(text, boolean)
|
||||||
|
TO :"repl_user";
|
||||||
|
|
||||||
|
GRANT EXECUTE
|
||||||
|
ON FUNCTION pg_catalog.pg_read_binary_file(text)
|
||||||
|
TO :"repl_user";
|
||||||
|
|
||||||
|
GRANT EXECUTE
|
||||||
|
ON FUNCTION pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean)
|
||||||
|
TO :"repl_user";
|
||||||
|
SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ensure_local_ha_slots() {
|
||||||
|
[[ -n "${POSTGRES_HA_SLOTS:-}" ]] || {
|
||||||
|
log "POSTGRES_HA_SLOTS is empty; no local HA slots requested"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
local raw_slot slot slot_type
|
||||||
|
|
||||||
|
while IFS= read -r raw_slot; do
|
||||||
|
slot="${raw_slot//[[:space:]]/}"
|
||||||
|
|
||||||
|
[[ -n "$slot" ]] || continue
|
||||||
|
|
||||||
|
validate_slot_name "$slot"
|
||||||
|
|
||||||
|
slot_type="$(
|
||||||
|
as_postgres psql \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
--tuples-only \
|
||||||
|
--no-align \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--set=slot="$slot" <<'SQL'
|
||||||
|
SELECT slot_type
|
||||||
|
FROM pg_replication_slots
|
||||||
|
WHERE slot_name = :'slot';
|
||||||
|
SQL
|
||||||
|
)"
|
||||||
|
|
||||||
|
slot_type="${slot_type//[[:space:]]/}"
|
||||||
|
|
||||||
|
case "$slot_type" in
|
||||||
|
physical)
|
||||||
|
log "Physical HA slot already exists: $slot"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"")
|
||||||
|
log "Creating physical HA slot: $slot"
|
||||||
|
|
||||||
|
as_postgres psql \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--set=slot="$slot" <<'SQL' >/dev/null
|
||||||
|
SELECT pg_create_physical_replication_slot(:'slot');
|
||||||
|
SQL
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
die "HA slot $slot exists but is type $slot_type"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < <(
|
||||||
|
printf '%s\n' "$POSTGRES_HA_SLOTS" |
|
||||||
|
tr ',' '\n'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bootstrap_primary() {
|
||||||
|
require_var POSTGRES_DB
|
||||||
|
require_var POSTGRES_USER
|
||||||
|
require_var POSTGRES_REPLICATION_USER
|
||||||
|
require_var POSTGRES_HA_CIDR
|
||||||
|
require_var POSTGRES_PASSWORD_FILE
|
||||||
|
require_var POSTGRES_REPLICATION_PASSWORD_FILE
|
||||||
|
|
||||||
|
require_file "$POSTGRES_PASSWORD_FILE"
|
||||||
|
require_file "$POSTGRES_REPLICATION_PASSWORD_FILE"
|
||||||
|
|
||||||
|
pgdata_is_empty ||
|
||||||
|
die "primary bootstrap requires an empty PGDATA"
|
||||||
|
|
||||||
|
log "Initializing fresh PostgreSQL primary"
|
||||||
|
|
||||||
|
mkdir -p "$PGDATA"
|
||||||
|
chown -R postgres:postgres /var/lib/postgresql
|
||||||
|
chmod 0700 "$PGDATA"
|
||||||
|
|
||||||
|
local pwfile
|
||||||
|
pwfile="/tmp/postgres-bootstrap-password"
|
||||||
|
|
||||||
|
install_secret_for_postgres \
|
||||||
|
"$POSTGRES_PASSWORD_FILE" \
|
||||||
|
"$pwfile"
|
||||||
|
|
||||||
|
as_postgres initdb \
|
||||||
|
-D "$PGDATA" \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--pwfile="$pwfile" \
|
||||||
|
--auth-host=scram-sha-256 \
|
||||||
|
--auth-local=trust \
|
||||||
|
--data-checksums
|
||||||
|
|
||||||
|
rm -f "$pwfile"
|
||||||
|
|
||||||
|
write_managed_config
|
||||||
|
write_managed_hba
|
||||||
|
|
||||||
|
log "Starting PostgreSQL temporarily for initial role/database setup"
|
||||||
|
|
||||||
|
as_postgres pg_ctl \
|
||||||
|
-D "$PGDATA" \
|
||||||
|
-w \
|
||||||
|
start
|
||||||
|
|
||||||
|
trap 'as_postgres pg_ctl -D "$PGDATA" -m fast -w stop >/dev/null 2>&1 || true' RETURN
|
||||||
|
|
||||||
|
local repl_password
|
||||||
|
repl_password="$(cat "$POSTGRES_REPLICATION_PASSWORD_FILE")"
|
||||||
|
|
||||||
|
# psql variables keep the password out of SQL construction itself.
|
||||||
|
as_postgres psql \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
-v ON_ERROR_STOP=1 \
|
||||||
|
--set=repl_user="$POSTGRES_REPLICATION_USER" \
|
||||||
|
--set=repl_password="$repl_password" <<'SQL'
|
||||||
|
SELECT format(
|
||||||
|
'CREATE ROLE %I WITH LOGIN REPLICATION PASSWORD %L',
|
||||||
|
:'repl_user',
|
||||||
|
:'repl_password'
|
||||||
|
)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_roles
|
||||||
|
WHERE rolname = :'repl_user'
|
||||||
|
)
|
||||||
|
\gexec
|
||||||
|
SQL
|
||||||
|
|
||||||
|
grant_rewind_privileges
|
||||||
|
ensure_local_ha_slots
|
||||||
|
|
||||||
|
if ! as_postgres psql \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
--tuples-only \
|
||||||
|
--no-align \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--set=db="$POSTGRES_DB" <<'SQL' | grep -qx 1
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_database
|
||||||
|
WHERE datname = :'db';
|
||||||
|
SQL
|
||||||
|
then
|
||||||
|
as_postgres createdb \
|
||||||
|
--username="$POSTGRES_USER" \
|
||||||
|
--owner="$POSTGRES_USER" \
|
||||||
|
"$POSTGRES_DB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
as_postgres pg_ctl \
|
||||||
|
-D "$PGDATA" \
|
||||||
|
-m fast \
|
||||||
|
-w \
|
||||||
|
stop
|
||||||
|
|
||||||
|
trap - RETURN
|
||||||
|
|
||||||
|
touch "$(marker_path)"
|
||||||
|
chown postgres:postgres "$(marker_path)"
|
||||||
|
chmod 0600 "$(marker_path)"
|
||||||
|
|
||||||
|
log "Fresh primary bootstrap complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_remote_replication_slot() {
|
||||||
|
validate_slot_name "$POSTGRES_REPLICATION_SLOT"
|
||||||
|
|
||||||
|
local slot_type
|
||||||
|
|
||||||
|
slot_type="$(
|
||||||
|
as_postgres env \
|
||||||
|
PGPASSFILE=/var/lib/postgresql/.pgpass \
|
||||||
|
psql \
|
||||||
|
--host="$POSTGRES_PRIMARY_HOST" \
|
||||||
|
--port="$POSTGRES_PRIMARY_PORT" \
|
||||||
|
--username="$POSTGRES_REPLICATION_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
--tuples-only \
|
||||||
|
--no-align \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--command="
|
||||||
|
SELECT slot_type
|
||||||
|
FROM pg_replication_slots
|
||||||
|
WHERE slot_name = '$POSTGRES_REPLICATION_SLOT';
|
||||||
|
" |
|
||||||
|
tr -d '[:space:]'
|
||||||
|
)"
|
||||||
|
|
||||||
|
case "$slot_type" in
|
||||||
|
physical)
|
||||||
|
log "Replication slot already exists: $POSTGRES_REPLICATION_SLOT"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"")
|
||||||
|
log "Creating physical replication slot: $POSTGRES_REPLICATION_SLOT"
|
||||||
|
|
||||||
|
as_postgres env \
|
||||||
|
PGPASSFILE=/var/lib/postgresql/.pgpass \
|
||||||
|
psql \
|
||||||
|
--host="$POSTGRES_PRIMARY_HOST" \
|
||||||
|
--port="$POSTGRES_PRIMARY_PORT" \
|
||||||
|
--username="$POSTGRES_REPLICATION_USER" \
|
||||||
|
--dbname=postgres \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--command="
|
||||||
|
SELECT pg_create_physical_replication_slot(
|
||||||
|
'$POSTGRES_REPLICATION_SLOT'
|
||||||
|
);
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
die "Slot $POSTGRES_REPLICATION_SLOT exists but is not physical"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap_standby() {
|
||||||
|
require_var POSTGRES_PRIMARY_HOST
|
||||||
|
require_var POSTGRES_PRIMARY_PORT
|
||||||
|
require_var POSTGRES_REPLICATION_USER
|
||||||
|
require_var POSTGRES_REPLICATION_SLOT
|
||||||
|
require_var POSTGRES_APPLICATION_NAME
|
||||||
|
require_var POSTGRES_REPLICATION_PASSWORD_FILE
|
||||||
|
|
||||||
|
pgdata_is_empty ||
|
||||||
|
die "standby bootstrap requires an empty PGDATA"
|
||||||
|
|
||||||
|
validate_slot_name "$POSTGRES_REPLICATION_SLOT"
|
||||||
|
|
||||||
|
log "Preparing fresh standby from $POSTGRES_PRIMARY_HOST"
|
||||||
|
|
||||||
|
mkdir -p "$PGDATA"
|
||||||
|
chmod 0700 "$PGDATA"
|
||||||
|
chown -R postgres:postgres /var/lib/postgresql
|
||||||
|
|
||||||
|
create_persistent_pgpass
|
||||||
|
ensure_remote_replication_slot
|
||||||
|
|
||||||
|
log "Running pg_basebackup"
|
||||||
|
|
||||||
|
as_postgres env \
|
||||||
|
PGPASSFILE=/var/lib/postgresql/.pgpass \
|
||||||
|
pg_basebackup \
|
||||||
|
--dbname="host=$POSTGRES_PRIMARY_HOST port=$POSTGRES_PRIMARY_PORT user=$POSTGRES_REPLICATION_USER application_name=$POSTGRES_APPLICATION_NAME passfile=/var/lib/postgresql/.pgpass" \
|
||||||
|
--pgdata="$PGDATA" \
|
||||||
|
--format=plain \
|
||||||
|
--wal-method=stream \
|
||||||
|
--slot="$POSTGRES_REPLICATION_SLOT" \
|
||||||
|
--write-recovery-conf \
|
||||||
|
--progress
|
||||||
|
|
||||||
|
[[ -e "$PGDATA/standby.signal" ]] ||
|
||||||
|
die "pg_basebackup completed without standby.signal"
|
||||||
|
|
||||||
|
pgdata_is_initialized ||
|
||||||
|
die "pg_basebackup did not produce a valid PostgreSQL data directory"
|
||||||
|
|
||||||
|
touch "$(marker_path)"
|
||||||
|
chown postgres:postgres "$(marker_path)"
|
||||||
|
chmod 0600 "$(marker_path)"
|
||||||
|
|
||||||
|
log "Fresh standby bootstrap complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
adopt_existing() {
|
||||||
|
pgdata_is_initialized ||
|
||||||
|
die "POSTGRES_HA_MODE=existing requires a valid existing PostgreSQL cluster"
|
||||||
|
|
||||||
|
log "Existing PostgreSQL cluster detected"
|
||||||
|
|
||||||
|
write_managed_config
|
||||||
|
|
||||||
|
# HBA management requires these only when supplied.
|
||||||
|
if [[ -n "${POSTGRES_REPLICATION_USER:-}" &&
|
||||||
|
-n "${POSTGRES_HA_CIDR:-}" ]]; then
|
||||||
|
write_managed_hba
|
||||||
|
fi
|
||||||
|
|
||||||
|
touch "$(marker_path)"
|
||||||
|
chown postgres:postgres "$(marker_path)"
|
||||||
|
chmod 0600 "$(marker_path)"
|
||||||
|
|
||||||
|
log "Existing cluster adopted; no initdb or basebackup performed"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_bootstrapped_cluster() {
|
||||||
|
pgdata_is_initialized ||
|
||||||
|
die "Bootstrap marker exists but PGDATA is not a valid PostgreSQL cluster"
|
||||||
|
|
||||||
|
local version
|
||||||
|
version="$(cat "$PGDATA/PG_VERSION")"
|
||||||
|
|
||||||
|
[[ "$version" == "18" ]] ||
|
||||||
|
die "Expected PostgreSQL major version 18, found $version"
|
||||||
|
|
||||||
|
as_postgres pg_controldata "$PGDATA" >/dev/null ||
|
||||||
|
die "pg_controldata validation failed"
|
||||||
|
|
||||||
|
local pgdata_mode
|
||||||
|
pgdata_mode="$(stat -c '%a' "$PGDATA")"
|
||||||
|
|
||||||
|
[[ "$pgdata_mode" == "700" || "$pgdata_mode" == "750" ]] ||
|
||||||
|
die "PGDATA has invalid permissions: $pgdata_mode (expected 700 or 750)"
|
||||||
|
|
||||||
|
[[ "$(stat -c '%U' "$PGDATA")" == "postgres" ]] ||
|
||||||
|
die "PGDATA is not owned by postgres"
|
||||||
|
|
||||||
|
log "Existing bootstrapped PostgreSQL 18 cluster validated"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
require_var PGDATA
|
||||||
|
if [[ -n "${POSTGRES_REPLICATION_PEERS:-}" ]]; then
|
||||||
|
require_var POSTGRES_NODE_NAME
|
||||||
|
require_var POSTGRES_APPLICATION_NAME
|
||||||
|
require_var POSTGRES_REPLICATION_USER
|
||||||
|
require_var POSTGRES_REPLICATION_SLOT
|
||||||
|
require_file /usr/local/libexec/postgres-ha-peer-config.sh
|
||||||
|
. /usr/local/libexec/postgres-ha-peer-config.sh
|
||||||
|
mc_peer_conninfo "$POSTGRES_REPLICATION_PEERS" "$POSTGRES_NODE_NAME" "$POSTGRES_REPLICATION_USER" "$POSTGRES_APPLICATION_NAME" >/dev/null || die "Invalid native peer configuration"
|
||||||
|
validate_slot_name "$POSTGRES_REPLICATION_SLOT"
|
||||||
|
# Basebackup slot creation still uses one explicitly selected stable source.
|
||||||
|
if [[ "${POSTGRES_HA_MODE:-}" == standby && ! -e "$(marker_path)" ]]; then
|
||||||
|
[[ ",$POSTGRES_REPLICATION_PEERS," == *"=$POSTGRES_PRIMARY_HOST:$POSTGRES_PRIMARY_PORT,"* ]] || die "Bootstrap source must be a mapped stable peer"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local marker
|
||||||
|
marker="$(marker_path)"
|
||||||
|
|
||||||
|
log "Node: ${POSTGRES_NODE_NAME:-unknown}"
|
||||||
|
log "Mode: ${POSTGRES_HA_MODE:-unset}"
|
||||||
|
log "PGDATA: $PGDATA"
|
||||||
|
|
||||||
|
if [[ -e "$marker" ]]; then
|
||||||
|
validate_bootstrapped_cluster
|
||||||
|
else
|
||||||
|
case "${POSTGRES_HA_MODE:-}" in
|
||||||
|
primary)
|
||||||
|
bootstrap_primary
|
||||||
|
;;
|
||||||
|
|
||||||
|
standby)
|
||||||
|
bootstrap_standby
|
||||||
|
;;
|
||||||
|
|
||||||
|
existing)
|
||||||
|
adopt_existing
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
die "POSTGRES_HA_MODE must be primary, standby, or existing"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$#" -gt 0 ]] ||
|
||||||
|
die "No long-running container command supplied"
|
||||||
|
|
||||||
|
log "Bootstrap gate passed; executing: $*"
|
||||||
|
if [[ -n "${POSTGRES_REPLICATION_PEERS:-}" ]]; then
|
||||||
|
create_persistent_pgpass
|
||||||
|
if [[ -e "$PGDATA/standby.signal" ]]; then
|
||||||
|
[[ ! -e "$PGDATA/postmaster.pid" ]] || die "Refusing native peer rewrite with a postmaster PID file"
|
||||||
|
mc_peer_write_recovery "$PGDATA" "$POSTGRES_REPLICATION_PEERS" "$POSTGRES_NODE_NAME" "$POSTGRES_REPLICATION_USER" "$POSTGRES_APPLICATION_NAME" "$POSTGRES_REPLICATION_SLOT" || die "Native peer recovery configuration failed"
|
||||||
|
chown postgres:postgres "$PGDATA/postgresql.auto.conf" "$PGDATA/standby.signal"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# POSIX-compatible shared normalizer. Also mounted beside the bootstrap script.
|
||||||
|
# Inputs are data, never evaluated shell. IPv4/DNS endpoints only in API 1.
|
||||||
|
mc_peer_endpoints()
|
||||||
|
{
|
||||||
|
LC_ALL=C awk -v peers="$1" -v self="$2" -v select_node="${3:-}" '
|
||||||
|
BEGIN {
|
||||||
|
n=split(peers, entries, ","); self=tolower(self); select_node=tolower(select_node);
|
||||||
|
if(n<2 || n>32 || self=="") exit 1;
|
||||||
|
for(i=1;i<=n;i++) {
|
||||||
|
if(split(entries[i],pair,"=")!=2 || pair[1]!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/) exit 1;
|
||||||
|
node=tolower(pair[1]); if(seen[node]++) exit 1;
|
||||||
|
if(split(pair[2],addr,":")!=2 || addr[1]!~/^[A-Za-z0-9][A-Za-z0-9.-]*$/ || addr[2]!~/^[0-9]+$/ || addr[2]+0<1 || addr[2]+0>65535) exit 1;
|
||||||
|
endpoint=tolower(addr[1]) ":" (addr[2]+0); if(addresses[endpoint]++) exit 1;
|
||||||
|
if(node==self) { own++; continue; }
|
||||||
|
if(select_node!="" && node!=select_node) continue;
|
||||||
|
hosts=hosts (count?",":"") addr[1]; ports=ports (count?",":"") (addr[2]+0); count++;
|
||||||
|
}
|
||||||
|
if(own!=1 || count<1 || (select_node!="" && count!=1)) exit 1;
|
||||||
|
print hosts "|" ports;
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
mc_peer_conninfo()
|
||||||
|
{
|
||||||
|
# peers, self, replication user, application name
|
||||||
|
case "$3" in ''|*[!a-zA-Z0-9_.-]*) return 1;; esac
|
||||||
|
case "$4" in ''|*[!a-zA-Z0-9_.-]*) return 1;; esac
|
||||||
|
mc_peer_pair=$(mc_peer_endpoints "$1" "$2") || return 1
|
||||||
|
printf 'host=%s port=%s user=%s application_name=%s target_session_attrs=primary connect_timeout=2\n' \
|
||||||
|
"${mc_peer_pair%|*}" "${mc_peer_pair#*|}" "$3" "$4"
|
||||||
|
}
|
||||||
|
|
||||||
|
mc_peer_write_recovery()
|
||||||
|
(
|
||||||
|
# PGDATA, peers, self, user, application, slot. No credentials in output.
|
||||||
|
set -eu
|
||||||
|
data=$1
|
||||||
|
conn=$(mc_peer_conninfo "$2" "$3" "$4" "$5") || exit 1
|
||||||
|
case "$6" in ''|*[!a-z0-9_]*) exit 1;; esac
|
||||||
|
[ ${#6} -le 63 ] || exit 1
|
||||||
|
[ -d "$data" ] && [ -f "$data/postgresql.auto.conf" ] && [ ! -L "$data/postgresql.auto.conf" ] || exit 1
|
||||||
|
[ ! -L "$data/standby.signal" ] || exit 1
|
||||||
|
umask 077
|
||||||
|
tmp=$(mktemp "$data/.peer-recovery.XXXXXXXX")
|
||||||
|
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
||||||
|
sed -e '/^[[:space:]]*primary_conninfo[[:space:]]*=/d' \
|
||||||
|
-e '/^[[:space:]]*primary_slot_name[[:space:]]*=/d' "$data/postgresql.auto.conf" > "$tmp"
|
||||||
|
printf "primary_conninfo = '%s'\nprimary_slot_name = '%s'\n" "$conn" "$6" >> "$tmp"
|
||||||
|
chmod 600 "$tmp"
|
||||||
|
mv -f "$tmp" "$data/postgresql.auto.conf"
|
||||||
|
[ ! -L "$data/standby.signal" ] || exit 1
|
||||||
|
touch "$data/standby.signal"
|
||||||
|
chmod 600 "$data/standby.signal"
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=PostgreSQL HA Maintenance Container
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Volume=/home/david/.local/libexec/postgres-ha-peer-config.sh:/usr/local/libexec/postgres-ha-peer-config.sh:ro,Z
|
||||||
|
ContainerName=postgres
|
||||||
|
Image=docker.io/library/postgres@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280
|
||||||
|
|
||||||
|
Entrypoint=/usr/local/sbin/postgres-ha-bootstrap
|
||||||
|
Exec=sleep infinity
|
||||||
|
|
||||||
|
RunInit=true
|
||||||
|
StopSignal=SIGTERM
|
||||||
|
StopTimeout=5
|
||||||
|
|
||||||
|
Network=postgres.network
|
||||||
|
PublishPort=5432:5432
|
||||||
|
|
||||||
|
Volume=postgres-data:/var/lib/postgresql:Z
|
||||||
|
Volume=/home/david/.local/libexec/postgres-ha-bootstrap:/usr/local/sbin/postgres-ha-bootstrap:ro,Z
|
||||||
|
Volume=/home/david/.config/postgres-ha/secrets:/run/secrets/postgres-ha:ro,Z
|
||||||
|
|
||||||
|
EnvironmentFile=/home/david/.config/postgres-ha/postgres.env
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
SuccessExitStatus=143
|
||||||
|
Restart=on-abnormal
|
||||||
|
TimeoutStartSec=0
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
PGDATA=/var/lib/postgresql/18/docker
|
||||||
|
|
||||||
|
# Initial bootstrap role. Pacemaker/RA controls promotion afterward.
|
||||||
|
POSTGRES_HA_MODE=standby
|
||||||
|
|
||||||
|
POSTGRES_REPLICATION_USER=replicator
|
||||||
|
|
||||||
|
# Node-specific values:
|
||||||
|
POSTGRES_NODE_NAME=CHANGE_ME
|
||||||
|
POSTGRES_APPLICATION_NAME=CHANGE_ME
|
||||||
|
POSTGRES_REPLICATION_SLOT=ha_pg_CHANGE_ME
|
||||||
|
|
||||||
|
# Stable cluster peer map:
|
||||||
|
POSTGRES_REPLICATION_PEERS=david-prodesk=192.168.1.4:5432,david-macbookpro=192.168.1.5:5432,david-eve=192.168.1.3:5432
|
||||||
|
|
||||||
|
# Initial/bootstrap upstream. The RA later determines the elected primary.
|
||||||
|
POSTGRES_PRIMARY_HOST=192.168.1.4
|
||||||
|
POSTGRES_PRIMARY_PORT=5432
|
||||||
|
|
||||||
|
POSTGRES_HA_SLOTS=ha_pg_david_macbookpro,ha_pg_david_eve,ha_pg_david_prodesk
|
||||||
|
|
||||||
|
# Secret contents are never stored here.
|
||||||
|
POSTGRES_REPLICATION_PASSWORD_FILE=/run/secrets/postgres-ha/replication_password
|
||||||
|
|
||||||
|
POSTGRES_HA_REPLICATION_NETWORKS=192.168.1.3/32,192.168.1.4/32,192.168.1.5/32,10.89.0.0/16
|
||||||
Executable
+146
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/.." &&
|
||||||
|
pwd
|
||||||
|
)"
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " managed-container release test suite"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">>> Entrypoint / shell structure"
|
||||||
|
./tests/test-entrypoint-structure.sh
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">>> Python syntax"
|
||||||
|
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
python3 -m py_compile "$file"
|
||||||
|
done < <(
|
||||||
|
find ocf \
|
||||||
|
-type f \
|
||||||
|
-name '*.py' \
|
||||||
|
-print0
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "PYTHON_SYNTAX_OK=1"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">>> Host helper shell syntax"
|
||||||
|
|
||||||
|
for file in postgres-host/*; do
|
||||||
|
[[ -f "$file" ]] || continue
|
||||||
|
|
||||||
|
case "$file" in
|
||||||
|
*.sh|*/postgres-ha-bootstrap)
|
||||||
|
bash -n "$file"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "HOST_HELPER_SYNTAX_OK=1"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">>> Secret-content guard"
|
||||||
|
|
||||||
|
python3 - <<'PY_SECRET_GUARD'
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
roots = [Path("quadlet"), Path("postgres-host")]
|
||||||
|
|
||||||
|
credential_key = re.compile(
|
||||||
|
r'^[A-Z0-9_]*(?:PASSWORD|PASSWD|SECRET|TOKEN)[A-Z0-9_]*$'
|
||||||
|
)
|
||||||
|
|
||||||
|
private_key = re.compile(
|
||||||
|
r'-----BEGIN (?:OPENSSH|RSA|EC|DSA) PRIVATE KEY-----'
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for root in roots:
|
||||||
|
if not root.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = path.read_text(errors="replace")
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append(f"{path}: unable to read: {exc}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if private_key.search(text):
|
||||||
|
errors.append(f"{path}: contains private-key material")
|
||||||
|
|
||||||
|
for lineno, raw in enumerate(text.splitlines(), 1):
|
||||||
|
line = raw.strip()
|
||||||
|
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only inspect configuration-style assignments whose KEY itself
|
||||||
|
# is uppercase and explicitly credential-related.
|
||||||
|
m = re.match(
|
||||||
|
r'^([A-Z][A-Z0-9_]*)\s*=\s*(.*)$',
|
||||||
|
line
|
||||||
|
)
|
||||||
|
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key, value = m.groups()
|
||||||
|
|
||||||
|
if not credential_key.match(key):
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
# Empty values are not embedded credentials.
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# References, secret-file paths, templates and placeholders
|
||||||
|
# are acceptable.
|
||||||
|
safe_prefixes = (
|
||||||
|
"$",
|
||||||
|
"${",
|
||||||
|
"/run/secrets/",
|
||||||
|
"/etc/",
|
||||||
|
"<",
|
||||||
|
"CHANGE_ME",
|
||||||
|
)
|
||||||
|
|
||||||
|
unquoted = value.strip("'\"")
|
||||||
|
|
||||||
|
if value.startswith(safe_prefixes) or unquoted.startswith(safe_prefixes):
|
||||||
|
continue
|
||||||
|
|
||||||
|
errors.append(
|
||||||
|
f"{path}:{lineno}: possible literal credential assignment: {key}=..."
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"ERROR: possible embedded credential material found",
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("SECRET_GUARD_OK=1")
|
||||||
|
PY_SECRET_GUARD
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ALL_TESTS_PASS=1"
|
||||||
Reference in New Issue
Block a user