From 40ccd24dde2adaedf06b4d0b634c4b32485e1289 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:31:28 +0100 Subject: [PATCH 01/41] fix: wrong regex for wal retention (#1026) (#1058) Co-authored-by: Rob Nickmans --- postgres-appliance/scripts/postgres_backup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/postgres_backup.sh b/postgres-appliance/scripts/postgres_backup.sh index 3216ae4ed..9b9a4723a 100755 --- a/postgres-appliance/scripts/postgres_backup.sh +++ b/postgres-appliance/scripts/postgres_backup.sh @@ -55,7 +55,7 @@ while read -r name last_modified rest; do # count how many backups will remain after we remove everything up to certain date ((LEFT=LEFT+1)) fi -done < <($WAL_E backup-list 2> /dev/null | sed '0,/^name\s*\(last_\)\?modified\s*/d') +done < <($WAL_E backup-list 2> /dev/null | sed '0,/^\(backup_\)\?name\s*\(last_\)\?modified\s*/d') # we want keep at least N backups even if the number of days exceeded if [ -n "$BEFORE" ] && [ $LEFT -ge $DAYS_TO_RETAIN ]; then From 786756464d45493a005f00182505a5ac38eba3ee Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 11 Dec 2024 13:34:12 +0100 Subject: [PATCH 02/41] Add pg17 and remove pg12 in trigger (#1059) --- delivery.yaml | 8 +- postgres-appliance/Dockerfile | 24 ++-- postgres-appliance/build_scripts/base.sh | 46 +++---- postgres-appliance/build_scripts/prepare.sh | 4 + .../major_upgrade/pg_upgrade.py | 2 +- postgres-appliance/scripts/post_init.sh | 88 ++++++++++++- postgres-appliance/scripts/spilo_commons.py | 10 +- postgres-appliance/tests/docker-compose.yml | 2 +- postgres-appliance/tests/test_spilo.sh | 120 +++++++++--------- 9 files changed, 194 insertions(+), 110 deletions(-) diff --git a/delivery.yaml b/delivery.yaml index 67366b64e..2435b0d0c 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -3,7 +3,7 @@ allow_concurrent_steps: true build_env: &BUILD_ENV BASE_IMAGE: container-registry.zalando.net/library/ubuntu-22.04 - PGVERSION: 16 + PGVERSION: 17 MULTI_ARCH_REGISTRY: container-registry-test.zalando.net/acid pipeline: @@ -33,6 +33,8 @@ pipeline: docker buildx build --platform "linux/amd64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ + --build-arg PGOLDVERSIONS="14 15 16" \ + --build-arg TIMESCALEDB="2.17.2" \ -t "$ECR_TEST_IMAGE" \ --push . @@ -61,6 +63,8 @@ pipeline: docker buildx build --platform "linux/amd64,linux/arm64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ + --build-arg PGOLDVERSIONS="14 15 16" \ + --build-arg TIMESCALEDB="2.17.2" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" @@ -91,6 +95,8 @@ pipeline: docker buildx build --platform "linux/amd64,linux/arm64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ + --build-arg PGOLDVERSIONS="14 15 16" \ + --build-arg TIMESCALEDB="2.17.2" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 272d70041..b31603610 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -1,6 +1,6 @@ ARG BASE_IMAGE=ubuntu:22.04 -ARG PGVERSION=16 -ARG TIMESCALEDB="2.11.2 2.14.2" +ARG PGVERSION=17 +ARG TIMESCALEDB="2.15.3 2.17.2" ARG DEMO=false ARG COMPRESS=false ARG ADDITIONAL_LOCALES= @@ -48,21 +48,21 @@ ARG TIMESCALEDB ARG TIMESCALEDB_APACHE_ONLY=true ARG TIMESCALEDB_TOOLKIT=true ARG COMPRESS -ARG PGOLDVERSIONS="12 13 14 15" +ARG PGOLDVERSIONS="13 14 15 16" ARG WITH_PERL=false ARG DEB_PG_SUPPORTED_VERSIONS="$PGOLDVERSIONS $PGVERSION" # Install PostgreSQL, extensions and contribs ENV POSTGIS_VERSION=3.5 \ - BG_MON_COMMIT=ef60961eff92672b1e21f5260dc1211367da6f1f \ - PG_AUTH_MON_COMMIT=3d010e5959285c32b155e8064c9c9b57869aeca7 \ - PG_MON_COMMIT=a6c5982368edd876edbee01e51b91e7387071e21 \ - SET_USER=REL4_0_1 \ - PLPROFILER=REL4_2_4 \ - PG_PROFILE=4.6 \ + BG_MON_COMMIT=7f5887218790b263fe3f42f85f4ddc9c8400b154 \ + PG_AUTH_MON_COMMIT=fe099eef7662cbc85b0b79191f47f52f1e96b779 \ + PG_MON_COMMIT=ead1de70794ed62ca1e34d4022f6165ff36e9a91 \ + SET_USER=REL4_1_0 \ + PLPROFILER=REL4_2_5 \ + PG_PROFILE=4.7 \ PAM_OAUTH2=v1.0.1 \ - PG_PERMISSIONS_COMMIT=314b9359e3d77c0b2ef7dbbde97fa4be80e31925 + PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 WORKDIR /builddeps RUN bash base.sh @@ -73,7 +73,7 @@ COPY --from=dependencies-builder /builddeps/wal-g /usr/local/bin/ COPY build_scripts/patroni_wale.sh build_scripts/compress_build.sh /builddeps/ # Install patroni and wal-e -ENV PATRONIVERSION=3.3.3 +ENV PATRONIVERSION=3.3.4 ENV WALE_VERSION=1.1.1 WORKDIR / @@ -92,7 +92,6 @@ FROM builder-${COMPRESS} LABEL maintainer="Team ACID @ Zalando " ARG PGVERSION -ARG TIMESCALEDB ARG DEMO ARG COMPRESS @@ -102,7 +101,6 @@ ENV LC_ALL=en_US.utf-8 \ PATH=$PATH:/usr/lib/postgresql/$PGVERSION/bin \ PGHOME=/home/postgres \ RW_DIR=/run \ - TIMESCALEDB=$TIMESCALEDB \ DEMO=$DEMO ENV WALE_ENV_DIR=$RW_DIR/etc/wal-e.d/env \ diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index e0d555d8a..6af88b12a 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -56,7 +56,6 @@ curl -sL "https://github.com/zalando-pg/pg_auth_mon/archive/$PG_AUTH_MON_COMMIT. curl -sL "https://github.com/cybertec-postgresql/pg_permissions/archive/$PG_PERMISSIONS_COMMIT.tar.gz" | tar xz curl -sL "https://github.com/zubkov-andrei/pg_profile/archive/$PG_PROFILE.tar.gz" | tar xz git clone -b "$SET_USER" https://github.com/pgaudit/set_user.git -git clone https://github.com/timescale/timescaledb.git apt-get install -y \ postgresql-common \ @@ -81,8 +80,8 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-first-last-agg" "postgresql-${version}-hll" "postgresql-${version}-hypopg" - "postgresql-${version}-plproxy" "postgresql-${version}-partman" + "postgresql-${version}-plproxy" "postgresql-${version}-pgaudit" "postgresql-${version}-pldebugger" "postgresql-${version}-pglogical" @@ -105,6 +104,12 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do fi + if [ "${TIMESCALEDB_APACHE_ONLY}" = "true" ]; then + EXTRAS+=("timescaledb-2-oss-postgresql-${version}") + else + EXTRAS+=("timescaledb-2-postgresql-${version}") + fi + # Install PostgreSQL binaries, contrib, plproxy and multiple pl's apt-get install --allow-downgrades -y \ "postgresql-${version}-cron" \ @@ -116,39 +121,28 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-pg-stat-kcache" \ "${EXTRAS[@]}" - # Install 3rd party stuff + # Clean up timescaledb versions except the highest compatible version + exclude_patterns=() + exclude_patterns_tsl=() + for ts_version in ${TIMESCALEDB}; do + exclude_patterns+=(! -name timescaledb-"${ts_version}".so) + exclude_patterns_tsl+=(! -name timescaledb-tsl-"${ts_version}".so) + done + find /usr/lib/postgresql/"${version}"/lib/ -name 'timescaledb-2.*.so' "${exclude_patterns[@]}" -delete; - # use subshell to avoid having to cd back (SC2103) - ( - cd timescaledb - for v in $TIMESCALEDB; do - git checkout "$v" - sed -i "s/VERSION 3.11/VERSION 3.10/" CMakeLists.txt - if BUILD_FORCE_REMOVE=true ./bootstrap -DREGRESS_CHECKS=OFF -DWARNINGS_AS_ERRORS=OFF \ - -DTAP_CHECKS=OFF -DPG_CONFIG="/usr/lib/postgresql/$version/bin/pg_config" \ - -DAPACHE_ONLY="$TIMESCALEDB_APACHE_ONLY" -DSEND_TELEMETRY_DEFAULT=NO; then - make -C build install - strip /usr/lib/postgresql/"$version"/lib/timescaledb*.so - fi - git reset --hard - git clean -f -d - done - ) + if [ "${TIMESCALEDB_APACHE_ONLY}" != "true" ]; then + find /usr/lib/postgresql/"${version}"/lib/ -name 'timescaledb-tsl-2.*.so' "${exclude_patterns_tsl[@]}" -delete; + fi - if [ "${TIMESCALEDB_APACHE_ONLY}" != "true" ] && [ "${TIMESCALEDB_TOOLKIT}" = "true" ]; then - __versionCodename=$(sed /usr/share/keyrings/timescale_E7391C94080429FF.gpg + # Install 3rd party stuff + if [ "${TIMESCALEDB_APACHE_ONLY}" != "true" ] && [ "${TIMESCALEDB_TOOLKIT}" = "true" ]; then apt-get update if [ "$(apt-cache search --names-only "^timescaledb-toolkit-postgresql-${version}$" | wc -l)" -eq 1 ]; then apt-get install "timescaledb-toolkit-postgresql-$version" else echo "Skipping timescaledb-toolkit-postgresql-$version as it's not found in the repository" fi - - rm /etc/apt/sources.list.d/timescaledb.list - rm /usr/share/keyrings/timescale_E7391C94080429FF.gpg fi EXTRA_EXTENSIONS=() diff --git a/postgres-appliance/build_scripts/prepare.sh b/postgres-appliance/build_scripts/prepare.sh index 50f32db86..66c2a2cb8 100644 --- a/postgres-appliance/build_scripts/prepare.sh +++ b/postgres-appliance/build_scripts/prepare.sh @@ -40,6 +40,10 @@ for t in deb deb-src; do done curl -s -o - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg +# add TimescaleDB repository +echo "deb [signed-by=/etc/apt/keyrings/timescale_timescaledb-archive-keyring.gpg] https://packagecloud.io/timescale/timescaledb/ubuntu/ ${DISTRIB_CODENAME} main" | tee /etc/apt/sources.list.d/timescaledb.list +curl -fsSL https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor | tee /etc/apt/keyrings/timescale_timescaledb-archive-keyring.gpg > /dev/null + # Clean up apt-get purge -y libcap2-bin apt-get autoremove -y diff --git a/postgres-appliance/major_upgrade/pg_upgrade.py b/postgres-appliance/major_upgrade/pg_upgrade.py index d9dafa4fc..ad1563e80 100644 --- a/postgres-appliance/major_upgrade/pg_upgrade.py +++ b/postgres-appliance/major_upgrade/pg_upgrade.py @@ -204,7 +204,7 @@ def pg_upgrade(self, check=False): def prepare_new_pgdata(self, version): from spilo_commons import append_extensions - locale = self.query('SHOW lc_collate')[0][0] + locale = self.query("SELECT datcollate FROM pg_database WHERE datname='template1';")[0][0] encoding = self.query('SHOW server_encoding')[0][0] initdb_config = [{'locale': locale}, {'encoding': encoding}] if self.query("SELECT current_setting('data_checksums')::bool")[0][0]: diff --git a/postgres-appliance/scripts/post_init.sh b/postgres-appliance/scripts/post_init.sh index 0950d04ef..c12e9c83b 100755 --- a/postgres-appliance/scripts/post_init.sh +++ b/postgres-appliance/scripts/post_init.sh @@ -5,7 +5,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 export PGOPTIONS="-c synchronous_commit=local -c search_path=pg_catalog" PGVER=$(psql -d "$2" -XtAc "SELECT pg_catalog.current_setting('server_version_num')::int/10000") -RESET_ARGS="oid, oid, bigint" +if [ "$PGVER" -lt 17 ]; then + RESET_ARGS="oid, oid, bigint" +else + RESET_ARGS="oid, oid, bigint, bool" +fi (echo "\set ON_ERROR_STOP on" echo "DO \$\$ @@ -213,6 +217,88 @@ while IFS= read -r db_name; do UPGRADE_TIMESCALEDB=$(echo -e "SELECT NULL;\nSELECT default_version != installed_version FROM pg_catalog.pg_available_extensions WHERE name = 'timescaledb'" | psql -tAX -d "${db_name}" 2> /dev/null | tail -n 1) if [ "$UPGRADE_TIMESCALEDB" = "t" ]; then echo "ALTER EXTENSION timescaledb UPDATE;" + IS_VERSION_BELOW_215=$(echo -e "SELECT (installed_version < '2.15')::bool FROM pg_catalog.pg_available_extensions WHERE name = 'timescaledb'" | psql -tAX -d "${db_name}" 2> /dev/null | tail -n 1) + if [ "$IS_VERSION_BELOW_215" = "t" ]; then + echo """ + -- Fix compressed hypertables with FOREIGN KEY constraints that were created with TimescaleDB versions before 2.15.0 + CREATE OR REPLACE FUNCTION pg_temp.constraint_columns(regclass, int2[]) RETURNS text[] AS + $$ + SELECT array_agg(attname) FROM unnest($2) un(attnum) LEFT JOIN pg_attribute att ON att.attrelid=$1 AND att.attnum = un.attnum; + $$ LANGUAGE SQL SET search_path TO pg_catalog, pg_temp; + DO $$ + DECLARE + ht_id int; + ht regclass; + chunk regclass; + con_oid oid; + con_frelid regclass; + con_name text; + con_columns text[]; + chunk_id int; + + BEGIN + + -- iterate over all hypertables that have foreign key constraints + FOR ht_id, ht in + SELECT + ht.id, + format('%I.%I',ht.schema_name,ht.table_name)::regclass + FROM _timescaledb_catalog.hypertable ht + WHERE + EXISTS ( + SELECT FROM pg_constraint con + WHERE + con.contype='f' AND + con.conrelid=format('%I.%I',ht.schema_name,ht.table_name)::regclass + ) + LOOP + RAISE NOTICE 'Hypertable % has foreign key constraint', ht; + + -- iterate over all foreign key constraints on the hypertable + -- and check that they are present on every chunk + FOR con_oid, con_frelid, con_name, con_columns IN + SELECT con.oid, con.confrelid, con.conname, pg_temp.constraint_columns(con.conrelid,con.conkey) + FROM pg_constraint con + WHERE + con.contype='f' AND + con.conrelid=ht + LOOP + RAISE NOTICE 'Checking constraint % %', con_name, con_columns; + -- check that the foreign key constraint is present on the chunk + + FOR chunk_id, chunk IN + SELECT + ch.id, + format('%I.%I',ch.schema_name,ch.table_name)::regclass + FROM _timescaledb_catalog.chunk ch + WHERE + ch.hypertable_id=ht_id + LOOP + RAISE NOTICE 'Checking chunk %', chunk; + IF NOT EXISTS ( + SELECT FROM pg_constraint con + WHERE + con.contype='f' AND + con.conrelid=chunk AND + con.confrelid=con_frelid AND + pg_temp.constraint_columns(con.conrelid,con.conkey) = con_columns + ) THEN + RAISE WARNING 'Restoring constraint % on chunk %', con_name, chunk; + PERFORM _timescaledb_functions.constraint_clone(con_oid, chunk); + INSERT INTO _timescaledb_catalog.chunk_constraint(chunk_id, dimension_slice_id, constraint_name, hypertable_constraint_name) VALUES (chunk_id, NULL, con_name, con_name); + END IF; + + END LOOP; + END LOOP; + + END LOOP; + + END + $$; + + DROP FUNCTION pg_temp.constraint_columns(regclass, int2[]); + """ + fi fi UPGRADE_TIMESCALEDB_TOOLKIT=$(echo -e "SELECT NULL;\nSELECT default_version != installed_version FROM pg_catalog.pg_available_extensions WHERE name = 'timescaledb_toolkit'" | psql -tAX -d "${db_name}" 2> /dev/null | tail -n 1) if [ "$UPGRADE_TIMESCALEDB_TOOLKIT" = "t" ]; then diff --git a/postgres-appliance/scripts/spilo_commons.py b/postgres-appliance/scripts/spilo_commons.py index d80a4dae9..0543bf771 100644 --- a/postgres-appliance/scripts/spilo_commons.py +++ b/postgres-appliance/scripts/spilo_commons.py @@ -12,13 +12,13 @@ # (min_version, max_version, shared_preload_libraries, extwlist.extensions) extensions = { - 'timescaledb': (9.6, 16, True, True), - 'pg_cron': (9.5, 16, True, False), - 'pg_stat_kcache': (9.4, 16, True, False), - 'pg_partman': (9.4, 16, False, True) + 'timescaledb': (9.6, 17, True, True), + 'pg_cron': (9.5, 17, True, False), + 'pg_stat_kcache': (9.4, 17, True, False), + 'pg_partman': (9.4, 17, False, True) } if os.environ.get('ENABLE_PG_MON') == 'true': - extensions['pg_mon'] = (11, 16, True, False) + extensions['pg_mon'] = (11, 17, True, False) def adjust_extensions(old, version, extwlist=False): diff --git a/postgres-appliance/tests/docker-compose.yml b/postgres-appliance/tests/docker-compose.yml index 31886dba2..f0399ecb2 100644 --- a/postgres-appliance/tests/docker-compose.yml +++ b/postgres-appliance/tests/docker-compose.yml @@ -51,7 +51,7 @@ services: postgresql: parameters: shared_buffers: 32MB - PGVERSION: '12' + PGVERSION: '13' # Just to test upgrade with clone. Without CLONE_SCOPE they don't work CLONE_WAL_S3_BUCKET: *bucket CLONE_AWS_ACCESS_KEY_ID: *access_key diff --git a/postgres-appliance/tests/test_spilo.sh b/postgres-appliance/tests/test_spilo.sh index d84bdc4a6..2570c0974 100755 --- a/postgres-appliance/tests/test_spilo.sh +++ b/postgres-appliance/tests/test_spilo.sh @@ -124,15 +124,15 @@ function drop_timescaledb() { } function test_inplace_upgrade_wrong_version() { - docker_exec "$1" "PGVERSION=12 $UPGRADE_SCRIPT 3" 2>&1 | grep 'Upgrade is not required' + docker_exec "$1" "PGVERSION=13 $UPGRADE_SCRIPT 3" 2>&1 | grep 'Upgrade is not required' } function test_inplace_upgrade_wrong_capacity() { - docker_exec "$1" "PGVERSION=13 $UPGRADE_SCRIPT 4" 2>&1 | grep 'number of replicas does not match' + docker_exec "$1" "PGVERSION=14 $UPGRADE_SCRIPT 4" 2>&1 | grep 'number of replicas does not match' } -function test_successful_inplace_upgrade_to_13() { - docker_exec "$1" "PGVERSION=13 $UPGRADE_SCRIPT 3" +function test_successful_inplace_upgrade_to_14() { + docker_exec "$1" "PGVERSION=14 $UPGRADE_SCRIPT 3" } function test_envdir_suffix() { @@ -147,15 +147,7 @@ function test_envdir_updated_to_x() { } function test_failed_inplace_upgrade_big_replication_lag() { - ! test_successful_inplace_upgrade_to_13 "$1" -} - -function test_successful_inplace_upgrade_to_13() { - docker_exec "$1" "PGVERSION=13 $UPGRADE_SCRIPT 3" -} - -function test_successful_inplace_upgrade_to_14() { - docker_exec "$1" "PGVERSION=14 $UPGRADE_SCRIPT 3" + ! test_successful_inplace_upgrade_to_14 "$1" } function test_successful_inplace_upgrade_to_15() { @@ -166,8 +158,12 @@ function test_successful_inplace_upgrade_to_16() { docker_exec "$1" "PGVERSION=16 $UPGRADE_SCRIPT 3" } -function test_pg_upgrade_to_16_check_failed() { - ! test_successful_inplace_upgrade_to_16 "$1" +function test_successful_inplace_upgrade_to_17() { + docker_exec "$1" "PGVERSION=17 $UPGRADE_SCRIPT 3" +} + +function test_pg_upgrade_to_17_check_failed() { + ! test_successful_inplace_upgrade_to_17 "$1" } function start_clone_with_wale_upgrade_container() { @@ -175,7 +171,7 @@ function start_clone_with_wale_upgrade_container() { docker-compose run \ -e SCOPE=upgrade \ - -e PGVERSION=13 \ + -e PGVERSION=14 \ -e CLONE_SCOPE=demo \ -e CLONE_METHOD=CLONE_WITH_WALE \ -e CLONE_TARGET_TIME="$(next_minute)" \ @@ -188,24 +184,24 @@ function start_clone_with_wale_upgrade_replica_container() { start_clone_with_wale_upgrade_container 2 } -function start_clone_with_wale_upgrade_to_16_container() { +function start_clone_with_wale_upgrade_to_17_container() { docker-compose run \ -e SCOPE=upgrade3 \ - -e PGVERSION=16 \ + -e PGVERSION=17 \ -e CLONE_SCOPE=demo \ - -e CLONE_PGVERSION=12 \ + -e CLONE_PGVERSION=13 \ -e CLONE_METHOD=CLONE_WITH_WALE \ -e CLONE_TARGET_TIME="$(next_minute)" \ --name "${PREFIX}upgrade4" \ -d "spilo3" } -function start_clone_with_wale_16_container() { +function start_clone_with_wale_17_container() { docker-compose run \ -e SCOPE=clone16 \ - -e PGVERSION=16 \ + -e PGVERSION=17 \ -e CLONE_SCOPE=upgrade3 \ - -e CLONE_PGVERSION=16 \ + -e CLONE_PGVERSION=17 \ -e CLONE_METHOD=CLONE_WITH_WALE \ -e CLONE_TARGET_TIME="$(next_hour)" \ --name "${PREFIX}clone16" \ @@ -216,7 +212,7 @@ function start_clone_with_basebackup_upgrade_container() { local container=$1 docker-compose run \ -e SCOPE=upgrade2 \ - -e PGVERSION=14 \ + -e PGVERSION=15 \ -e CLONE_SCOPE=upgrade \ -e CLONE_METHOD=CLONE_WITH_BASEBACKUP \ -e CLONE_HOST="$(docker_exec "$container" "hostname --ip-address")" \ @@ -230,10 +226,10 @@ function start_clone_with_basebackup_upgrade_container() { function start_clone_with_hourly_log_rotation() { docker-compose run \ -e SCOPE=hourlylogs \ - -e PGVERSION=16 \ + -e PGVERSION=17 \ -e LOG_SHIP_HOURLY="true" \ -e CLONE_SCOPE=upgrade2 \ - -e CLONE_PGVERSION=14 \ + -e CLONE_PGVERSION=15 \ -e CLONE_METHOD=CLONE_WITH_WALE \ -e CLONE_TARGET_TIME="$(next_minute)" \ --name "${PREFIX}hourlylogs" \ @@ -265,18 +261,18 @@ function verify_hourly_log_rotation() { [ "$log_rotation_age" = "1h" ] && [ "$log_filename" = "postgresql-%u-%H.log" ] && [ "$postgres_log_ftables" -eq 192 ] && [ "$postgres_log_views" -eq 8 ] && [ "$postgres_failed_auth_views" -eq 200 ] } -# TEST SUITE 1 - In-place major upgrade 12->13->...->16 -# TEST SUITE 2 - Major upgrade 12->16 after wal-e clone (with CLONE_PGVERSION set) -# TEST SUITE 3 - PITR (clone with wal-e) with unreachable target (13+) -# TEST SUITE 4 - Major upgrade 12->13 after wal-e clone (no CLONE_PGVERSION) +# TEST SUITE 1 - In-place major upgrade 13->14->...->17 +# TEST SUITE 2 - Major upgrade 13->17 after wal-e clone (with CLONE_PGVERSION set) +# TEST SUITE 3 - PITR (clone with wal-e) with unreachable target (14+) +# TEST SUITE 4 - Major upgrade 13->14 after wal-e clone (no CLONE_PGVERSION) # TEST SUITE 5 - Replica bootstrap with wal-e -# TEST SUITE 6 - Major upgrade 13->14 after clone with basebackup +# TEST SUITE 6 - Major upgrade 14->15 after clone with basebackup # TEST SUITE 7 - Hourly log rotation function test_spilo() { # TEST SUITE 1 local container=$1 - run_test test_envdir_suffix "$container" 12 + run_test test_envdir_suffix "$container" 13 log_info "[TS1] Testing wrong upgrade setups" run_test test_inplace_upgrade_wrong_version "$container" @@ -293,66 +289,66 @@ function test_spilo() { # TEST SUITE 2 local upgrade3_container - upgrade3_container=$(start_clone_with_wale_upgrade_to_16_container) # SCOPE=upgrade3 PGVERSION=16 CLONE: _SCOPE=demo _PGVERSION=12 _TARGET_TIME= - log_info "[TS2] Started $upgrade3_container for testing major upgrade 12->16 after clone with wal-e" + upgrade3_container=$(start_clone_with_wale_upgrade_to_17_container) # SCOPE=upgrade3 PGVERSION=17 CLONE: _SCOPE=demo _PGVERSION=13 _TARGET_TIME= + log_info "[TS2] Started $upgrade3_container for testing major upgrade 13->17 after clone with wal-e" # TEST SUITE 4 local upgrade_container - upgrade_container=$(start_clone_with_wale_upgrade_container) # SCOPE=upgrade PGVERSION=13 CLONE: _SCOPE=demo _TARGET_TIME= - log_info "[TS4] Started $upgrade_container for testing major upgrade 12->13 after clone with wal-e" + upgrade_container=$(start_clone_with_wale_upgrade_container) # SCOPE=upgrade PGVERSION=14 CLONE: _SCOPE=demo _TARGET_TIME= + log_info "[TS4] Started $upgrade_container for testing major upgrade 13->14 after clone with wal-e" # TEST SUITE 1 # wait clone to finish and prevent timescale installation gets cloned find_leader "$upgrade3_container" find_leader "$upgrade_container" - create_timescaledb "$container" # we don't install it at the beginning, as we do 12->16 in a clone + create_timescaledb "$container" # we don't install it at the beginning, as we do 13->17 in a clone - log_info "[TS1] Testing in-place major upgrade 12->13" + log_info "[TS1] Testing in-place major upgrade 13->14" wait_zero_lag "$container" - run_test test_successful_inplace_upgrade_to_13 "$container" + run_test test_successful_inplace_upgrade_to_14 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 13 + run_test test_envdir_updated_to_x 14 # TEST SUITE 2 - log_info "[TS2] Testing in-place major upgrade 12->16 after wal-e clone" - run_test verify_clone_upgrade "$upgrade3_container" "wal-e" 12 16 + log_info "[TS2] Testing in-place major upgrade 13->17 after wal-e clone" + run_test verify_clone_upgrade "$upgrade3_container" "wal-e" 13 17 run_test verify_archive_mode_is_on "$upgrade3_container" wait_backup "$upgrade3_container" # TEST SUITE 3 - local clone16_container - clone16_container=$(start_clone_with_wale_16_container) # SCOPE=clone16 CLONE: _SCOPE=upgrade3 _PGVERSION=16 _TARGET_TIME= - log_info "[TS3] Started $clone16_container for testing point-in-time recovery (clone with wal-e) with unreachable target on 13+" + local clone17_container + clone17_container=$(start_clone_with_wale_17_container) # SCOPE=clone17 CLONE: _SCOPE=upgrade3 _PGVERSION=17 _TARGET_TIME= + log_info "[TS3] Started $clone17_container for testing point-in-time recovery (clone with wal-e) with unreachable target on 14+" # TEST SUITE 1 - log_info "[TS1] Testing in-place major upgrade 13->14" - run_test test_successful_inplace_upgrade_to_14 "$container" + log_info "[TS1] Testing in-place major upgrade 14->15" + run_test test_successful_inplace_upgrade_to_15 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 14 + run_test test_envdir_updated_to_x 15 # TEST SUITE 3 - find_leader "$clone16_container" - run_test verify_archive_mode_is_on "$clone16_container" + find_leader "$clone17_container" + run_test verify_archive_mode_is_on "$clone17_container" # TEST SUITE 1 wait_backup "$container" - log_info "[TS1] Testing in-place major upgrade to 14->15" - run_test test_successful_inplace_upgrade_to_15 "$container" + log_info "[TS1] Testing in-place major upgrade to 15->16" + run_test test_successful_inplace_upgrade_to_16 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 15 + run_test test_envdir_updated_to_x 16 # TEST SUITE 4 - log_info "[TS4] Testing in-place major upgrade 12->13 after clone with wal-e" - run_test verify_clone_upgrade "$upgrade_container" "wal-e" 12 13 + log_info "[TS4] Testing in-place major upgrade 13->14 after clone with wal-e" + run_test verify_clone_upgrade "$upgrade_container" "wal-e" 13 14 run_test verify_archive_mode_is_on "$upgrade_container" wait_backup "$upgrade_container" @@ -366,20 +362,20 @@ function test_spilo() { # TEST SUITE 6 local basebackup_container - basebackup_container=$(start_clone_with_basebackup_upgrade_container "$upgrade_container") # SCOPE=upgrade2 PGVERSION=14 CLONE: _SCOPE=upgrade - log_info "[TS6] Started $basebackup_container for testing major upgrade 13->14 after clone with basebackup" + basebackup_container=$(start_clone_with_basebackup_upgrade_container "$upgrade_container") # SCOPE=upgrade2 PGVERSION=15 CLONE: _SCOPE=upgrade + log_info "[TS6] Started $basebackup_container for testing major upgrade 14->15 after clone with basebackup" wait_backup "$basebackup_container" # TEST SUITE 1 - # run_test test_pg_upgrade_to_16_check_failed "$container" # pg_upgrade --check complains about timescaledb + # run_test test_pg_upgrade_to_17_check_failed "$container" # pg_upgrade --check complains about timescaledb wait_backup "$container" # drop_timescaledb "$container" - log_info "[TS1] Testing in-place major upgrade 15->16" - run_test test_successful_inplace_upgrade_to_16 "$container" + log_info "[TS1] Testing in-place major upgrade 16->17" + run_test test_successful_inplace_upgrade_to_17 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 16 + run_test test_envdir_updated_to_x 17 # TEST SUITE 5 @@ -392,8 +388,8 @@ function test_spilo() { log_info "[TS7] Started $hourlylogs_container for testing hourly log rotation" # TEST SUITE 6 - log_info "[TS6] Testing in-place major upgrade 13->14 after clone with basebackup" - run_test verify_clone_upgrade "$basebackup_container" "basebackup" 13 14 + log_info "[TS6] Testing in-place major upgrade 14->15 after clone with basebackup" + run_test verify_clone_upgrade "$basebackup_container" "basebackup" 14 15 run_test verify_archive_mode_is_on "$basebackup_container" # TEST SUITE 7 From c423b86a578315061be4e34020be1f31788a786e Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 8 Jan 2025 13:45:06 +0100 Subject: [PATCH 03/41] Update Patroni to version 3.3.5 (#1068) --- postgres-appliance/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index b31603610..6e4dc2759 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -73,7 +73,7 @@ COPY --from=dependencies-builder /builddeps/wal-g /usr/local/bin/ COPY build_scripts/patroni_wale.sh build_scripts/compress_build.sh /builddeps/ # Install patroni and wal-e -ENV PATRONIVERSION=3.3.4 +ENV PATRONIVERSION=3.3.5 ENV WALE_VERSION=1.1.1 WORKDIR / From a45f038a19e2706b3ba74f6e0b4c727a4a2bdc91 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 8 Jan 2025 15:04:57 +0100 Subject: [PATCH 04/41] Pass TIMESCALEDB argument as environment variable (#1069) --- postgres-appliance/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 6e4dc2759..08320a10f 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -62,7 +62,8 @@ ENV POSTGIS_VERSION=3.5 \ PLPROFILER=REL4_2_5 \ PG_PROFILE=4.7 \ PAM_OAUTH2=v1.0.1 \ - PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 + PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 \ + TIMESCALEDB=$TIMESCALEDB WORKDIR /builddeps RUN bash base.sh From db65123b1c80ed92316257a141727be5ae78b4ea Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 31 Jan 2025 19:34:00 +0300 Subject: [PATCH 05/41] sync trigger branch (#1076) * Patroni 4.0 * timescaledb 2.18.0 --- .github/workflows/publish-ghcr-container.yaml | 24 +++++++++---------- ENVIRONMENT.rst | 4 +++- kubernetes/spilo_kubernetes.yaml | 4 ++++ postgres-appliance/Dockerfile | 4 ++-- postgres-appliance/runit/pgqd/run | 2 +- postgres-appliance/scripts/callback_aws.py | 2 +- postgres-appliance/scripts/callback_role.py | 3 ++- postgres-appliance/scripts/configure_spilo.py | 6 +++-- postgres-appliance/scripts/on_role_change.sh | 2 +- postgres-appliance/scripts/patroni_wait.sh | 4 ++-- postgres-appliance/scripts/wale_restore.sh | 2 +- 11 files changed, 33 insertions(+), 24 deletions(-) diff --git a/.github/workflows/publish-ghcr-container.yaml b/.github/workflows/publish-ghcr-container.yaml index 0e2228321..c7f2e0203 100644 --- a/.github/workflows/publish-ghcr-container.yaml +++ b/.github/workflows/publish-ghcr-container.yaml @@ -11,7 +11,7 @@ env: jobs: publish: name: Build and push Spilo multiarch images - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 permissions: contents: 'read' packages: 'write' @@ -20,15 +20,15 @@ jobs: shell: bash steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up packages + run: sudo apt-get install -y docker-compose - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.7 - - - name: Install flake8 and docker-compose - run: python -m pip install flake8 docker-compose==1.17.1 + python-version: '3.10' - name: Derive spilo image name id: image @@ -39,20 +39,20 @@ jobs: echo "NAME=$IMAGE" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to GHCR - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and export to local docker for testing - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: "postgres-appliance/" load: true @@ -65,7 +65,7 @@ jobs: bash postgres-appliance/tests/test_spilo.sh - name: Build arm64 additionaly and push multiarch image to ghcr - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: "postgres-appliance/" push: true diff --git a/ENVIRONMENT.rst b/ENVIRONMENT.rst index f791a75de..ea2e3df81 100644 --- a/ENVIRONMENT.rst +++ b/ENVIRONMENT.rst @@ -101,7 +101,9 @@ Environment Configuration Settings - **LOG_GROUP_BY_DATE**: (optional) enable grouping log by date. Default is False - group the log files based on the instance ID. - **DCS_ENABLE_KUBERNETES_API**: a non-empty value forces Patroni to use Kubernetes as a DCS. Default is empty. - **KUBERNETES_USE_CONFIGMAPS**: a non-empty value makes Patroni store its metadata in ConfigMaps instead of Endpoints when running on Kubernetes. Default is empty. -- **KUBERNETES_ROLE_LABEL**: name of the label containing Postgres role when running on Kubernetens. Default is 'spilo-role'. +- **KUBERNETES_ROLE_LABEL**: name of the label containing Postgres role when running on Kubernetes. Default is 'spilo-role'. +- **KUBERNETES_LEADER_LABEL_VALUE**: value of the pod label if Postgres role is primary when running on Kubernetes. Default is 'master'. +- **KUBERNETES_STANDBY_LEADER_LABEL_VALUE**: value of the pod label if Postgres role is standby_leader when running on Kubernetes. Default is 'master'. - **KUBERNETES_SCOPE_LABEL**: name of the label containing cluster name. Default is 'version'. - **KUBERNETES_LABELS**: a JSON describing names and values of other labels used by Patroni on Kubernetes to locate its metadata. Default is '{"application": "spilo"}'. - **INITDB_LOCALE**: database cluster's default UTF-8 locale (en_US by default) diff --git a/kubernetes/spilo_kubernetes.yaml b/kubernetes/spilo_kubernetes.yaml index 2eab92e64..b01da08fe 100644 --- a/kubernetes/spilo_kubernetes.yaml +++ b/kubernetes/spilo_kubernetes.yaml @@ -79,6 +79,10 @@ spec: value: spilo-cluster - name: KUBERNETES_ROLE_LABEL value: role + - name: KUBERNETES_LEADER_LABEL_VALUE + value: master + - name: KUBERNETES_STANDBY_LEADER_LABEL_VALUE + value: master - name: SPILO_CONFIGURATION value: | ## https://github.com/zalando/patroni#yaml-configuration bootstrap: diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 08320a10f..80815940a 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -1,6 +1,6 @@ ARG BASE_IMAGE=ubuntu:22.04 ARG PGVERSION=17 -ARG TIMESCALEDB="2.15.3 2.17.2" +ARG TIMESCALEDB="2.15.3 2.18.0" ARG DEMO=false ARG COMPRESS=false ARG ADDITIONAL_LOCALES= @@ -74,7 +74,7 @@ COPY --from=dependencies-builder /builddeps/wal-g /usr/local/bin/ COPY build_scripts/patroni_wale.sh build_scripts/compress_build.sh /builddeps/ # Install patroni and wal-e -ENV PATRONIVERSION=3.3.5 +ENV PATRONIVERSION=4.0.4 ENV WALE_VERSION=1.1.1 WORKDIR / diff --git a/postgres-appliance/runit/pgqd/run b/postgres-appliance/runit/pgqd/run index 27721aa18..93c2d00e0 100755 --- a/postgres-appliance/runit/pgqd/run +++ b/postgres-appliance/runit/pgqd/run @@ -6,4 +6,4 @@ if ! $CHPST true 2> /dev/null; then fi exec 2>&1 -exec $CHPST env -i PGAPPNAME="pgq ticker" /scripts/patroni_wait.sh --role master -- /usr/bin/pgqd /home/postgres/pgq_ticker.ini +exec $CHPST env -i PGAPPNAME="pgq ticker" /scripts/patroni_wait.sh --role primary -- /usr/bin/pgqd /home/postgres/pgq_ticker.ini diff --git a/postgres-appliance/scripts/callback_aws.py b/postgres-appliance/scripts/callback_aws.py index 971ac7684..6032fbaa9 100755 --- a/postgres-appliance/scripts/callback_aws.py +++ b/postgres-appliance/scripts/callback_aws.py @@ -65,7 +65,7 @@ def main(): ec2 = boto.ec2.connect_to_region(metadata['region']) - if argc == 5 and role in ('master', 'standby_leader') and action in ('on_start', 'on_role_change'): + if argc == 5 and role in ('primary', 'standby_leader') and action in ('on_start', 'on_role_change'): associate_address(ec2, sys.argv[1], instance_id) instance = get_instance(ec2, instance_id) diff --git a/postgres-appliance/scripts/callback_role.py b/postgres-appliance/scripts/callback_role.py index 393b069ce..b0d482834 100755 --- a/postgres-appliance/scripts/callback_role.py +++ b/postgres-appliance/scripts/callback_role.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) LABEL = os.environ.get("KUBERNETES_ROLE_LABEL", 'spilo-role') +LEADER_LABEL_VALUE = os.environ.get("KUBERNETES_LEADER_LABEL_VALUE", 'master') def read_first_line(filename): @@ -78,7 +79,7 @@ def record_role_change(action, new_role, cluster): new_role = None if action == 'on_stop' else new_role logger.debug("Changing the pod's role to %s", new_role) pod_namespace = os.environ.get('POD_NAMESPACE', read_first_line(KUBE_NAMESPACE_FILENAME)) or 'default' - if new_role == 'master': + if new_role == LEADER_LABEL_VALUE: change_endpoints(pod_namespace, cluster) change_pod_role_label(pod_namespace, new_role) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 549070759..b7a301202 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -379,7 +379,7 @@ def deep_update(a, b): threshold_megabytes: {{WALE_BACKUP_THRESHOLD_MEGABYTES}} threshold_backup_size_percentage: {{WALE_BACKUP_THRESHOLD_PERCENTAGE}} retries: 2 - no_master: 1 + no_leader: 1 {{/USE_WALE}} basebackup_fast_xlog: command: /scripts/basebackup.sh @@ -390,7 +390,7 @@ def deep_update(a, b): threshold_megabytes: {{WALE_BACKUP_THRESHOLD_MEGABYTES}} threshold_backup_size_percentage: {{WALE_BACKUP_THRESHOLD_PERCENTAGE}} retries: 2 - no_master: 1 + no_leader: 1 {{/STANDBY_WITH_WALE}} ''' @@ -578,6 +578,8 @@ def get_placeholders(provider): placeholders.setdefault('CALLBACK_SCRIPT', '') placeholders.setdefault('DCS_ENABLE_KUBERNETES_API', '') placeholders.setdefault('KUBERNETES_ROLE_LABEL', 'spilo-role') + placeholders.setdefault('KUBERNETES_LEADER_LABEL_VALUE', 'master') + placeholders.setdefault('KUBERNETES_STANDBY_LEADER_LABEL_VALUE', 'master') placeholders.setdefault('KUBERNETES_SCOPE_LABEL', 'version') placeholders.setdefault('KUBERNETES_LABELS', KUBERNETES_DEFAULT_LABELS) placeholders.setdefault('KUBERNETES_USE_CONFIGMAPS', '') diff --git a/postgres-appliance/scripts/on_role_change.sh b/postgres-appliance/scripts/on_role_change.sh index ad54b0cb5..270cc1b1e 100755 --- a/postgres-appliance/scripts/on_role_change.sh +++ b/postgres-appliance/scripts/on_role_change.sh @@ -7,7 +7,7 @@ shift readonly dbname=postgres -if [[ "${*: -3:1}" == "on_role_change" && "${*: -2:1}" == "master" ]]; then +if [[ "${*: -3:1}" == "on_role_change" && "${*: -2:1}" == "primary" ]]; then num=30 # wait 30 seconds for end of recovery while [[ $((num--)) -gt 0 ]]; do if [[ "$(psql -d $dbname -tAc 'SELECT pg_catalog.pg_is_in_recovery()')" == "f" ]]; then diff --git a/postgres-appliance/scripts/patroni_wait.sh b/postgres-appliance/scripts/patroni_wait.sh index 79a0be650..6edb95234 100755 --- a/postgres-appliance/scripts/patroni_wait.sh +++ b/postgres-appliance/scripts/patroni_wait.sh @@ -1,6 +1,6 @@ #!/bin/bash -ROLE=master +ROLE=primary INTERVAL=60 TIMEOUT="" @@ -17,7 +17,7 @@ Options: -t, --timeout Fail after TIMEOUT seconds (default: no timeout) -Waits for ROLE (master or replica). It will check every INTERVAL seconds ($INTERVAL). +Waits for ROLE (primary or replica). It will check every INTERVAL seconds ($INTERVAL). If TIMEOUT is specified, it will stop trying after TIMEOUT seconds. Executes COMMAND after ROLE has become available. (Default: exit 0) diff --git a/postgres-appliance/scripts/wale_restore.sh b/postgres-appliance/scripts/wale_restore.sh index 497afe30f..4fbcedd01 100755 --- a/postgres-appliance/scripts/wale_restore.sh +++ b/postgres-appliance/scripts/wale_restore.sh @@ -24,7 +24,7 @@ while getopts ":-:" optchar; do threshold_megabytes=*|threshold-megabytes=* ) THRESHOLD_MEGABYTES=${OPTARG#*=} ;; - no_master=*|no-master=* ) + no_leader=*|no-master=* ) NO_MASTER=${OPTARG#*=} ;; esac From f5a0ffc280411f8fa2aa2b7fd4a82091f60cdc92 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:20:41 +0300 Subject: [PATCH 06/41] Adjust timescaledb to 2.18.0 in delivery (#1078) --- delivery.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/delivery.yaml b/delivery.yaml index 2435b0d0c..cd4309b75 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -34,7 +34,7 @@ pipeline: --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ - --build-arg TIMESCALEDB="2.17.2" \ + --build-arg TIMESCALEDB="2.18.0" \ -t "$ECR_TEST_IMAGE" \ --push . @@ -64,7 +64,7 @@ pipeline: --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ - --build-arg TIMESCALEDB="2.17.2" \ + --build-arg TIMESCALEDB="2.18.0" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" @@ -96,7 +96,7 @@ pipeline: --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ - --build-arg TIMESCALEDB="2.17.2" \ + --build-arg TIMESCALEDB="2.18.0" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" From 771493d2787a810a8be4751c877e2ed0adb1a0cb Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Thu, 6 Mar 2025 09:34:39 +0100 Subject: [PATCH 07/41] Sync trigger 03-05 (#1080) - Fix timescaledb installation logic - Make leader tag value for native AWS configurable - Retain multiple minor versions of timescaledb --------- Co-authored-by: idanovinda --- README.rst | 3 +++ delivery.yaml | 3 --- postgres-appliance/Dockerfile | 5 +---- postgres-appliance/build_scripts/base.sh | 19 +++++++++---------- postgres-appliance/scripts/callback_aws.py | 4 +++- postgres-appliance/scripts/configure_spilo.py | 1 + 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index b5992ace2..102c46c80 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,9 @@ Spilo is currently evolving: Its creators are working on a Postgres operator tha How to Use This Docker Image ============================ +.. important:: + We encourage users to build the Docker images themselves from source code using the latest tags to benefit from ongoing improvements and fixes. The team continues to maintain the project and address issues, but does not make regular releases nor publishes latest Docker images. + Spilo's setup assumes that you've correctly configured a load balancer (HAProxy, ELB, Google load balancer) that directs client connections to the master. There are two ways to achieve this: A) if the load balancer relies on the status code to distinguish between the healthy and failed nodes (like ELB), then one needs to configure it to poll the API URL; otherwise, B) you can use callback scripts to change the load balancer configuration dynamically. **Available container registry and image architectures** diff --git a/delivery.yaml b/delivery.yaml index cd4309b75..f592600c3 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -34,7 +34,6 @@ pipeline: --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ - --build-arg TIMESCALEDB="2.18.0" \ -t "$ECR_TEST_IMAGE" \ --push . @@ -64,7 +63,6 @@ pipeline: --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ - --build-arg TIMESCALEDB="2.18.0" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" @@ -96,7 +94,6 @@ pipeline: --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ - --build-arg TIMESCALEDB="2.18.0" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 80815940a..9844872ea 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -1,6 +1,5 @@ ARG BASE_IMAGE=ubuntu:22.04 ARG PGVERSION=17 -ARG TIMESCALEDB="2.15.3 2.18.0" ARG DEMO=false ARG COMPRESS=false ARG ADDITIONAL_LOCALES= @@ -44,7 +43,6 @@ COPY build_scripts/base.sh /builddeps/ COPY --from=dependencies-builder /builddeps/*.deb /builddeps/ ARG PGVERSION -ARG TIMESCALEDB ARG TIMESCALEDB_APACHE_ONLY=true ARG TIMESCALEDB_TOOLKIT=true ARG COMPRESS @@ -62,8 +60,7 @@ ENV POSTGIS_VERSION=3.5 \ PLPROFILER=REL4_2_5 \ PG_PROFILE=4.7 \ PAM_OAUTH2=v1.0.1 \ - PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 \ - TIMESCALEDB=$TIMESCALEDB + PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 WORKDIR /builddeps RUN bash base.sh diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index 6af88b12a..425507ebe 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -121,18 +121,17 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-pg-stat-kcache" \ "${EXTRAS[@]}" - # Clean up timescaledb versions except the highest compatible version + # Clean up timescaledb versions except the last 5 minor versions exclude_patterns=() - exclude_patterns_tsl=() - for ts_version in ${TIMESCALEDB}; do - exclude_patterns+=(! -name timescaledb-"${ts_version}".so) - exclude_patterns_tsl+=(! -name timescaledb-tsl-"${ts_version}".so) + versions=$(find "/usr/lib/postgresql/$version/lib/" -name 'timescaledb-2.*.so' | sed -rn 's/.*timescaledb-([1-9]+\.[0-9]+\.[0-9]+)\.so$/\1/p' | sort -rV) + latest_minor_versions=$(echo "$versions" | awk -F. '{print $1"."$2}' | uniq | head -n 5) + for minor in $latest_minor_versions; do + for full_version in $(echo "$versions" | grep "^$minor"); do + exclude_patterns+=(! -name timescaledb-"${full_version}".so) + exclude_patterns+=(! -name timescaledb-tsl-"${full_version}".so) + done done - find /usr/lib/postgresql/"${version}"/lib/ -name 'timescaledb-2.*.so' "${exclude_patterns[@]}" -delete; - - if [ "${TIMESCALEDB_APACHE_ONLY}" != "true" ]; then - find /usr/lib/postgresql/"${version}"/lib/ -name 'timescaledb-tsl-2.*.so' "${exclude_patterns_tsl[@]}" -delete; - fi + find "/usr/lib/postgresql/$version/lib/" \( -name 'timescaledb-2.*.so' -o -name 'timescaledb-tsl-2.*.so' \) "${exclude_patterns[@]}" -delete # Install 3rd party stuff diff --git a/postgres-appliance/scripts/callback_aws.py b/postgres-appliance/scripts/callback_aws.py index 6032fbaa9..7b46c618d 100755 --- a/postgres-appliance/scripts/callback_aws.py +++ b/postgres-appliance/scripts/callback_aws.py @@ -3,10 +3,12 @@ import boto.ec2 import boto.utils import logging +import os import sys import time logger = logging.getLogger(__name__) +LEADER_TAG_VALUE = os.environ.get('AWS_LEADER_TAG_VALUE', 'master') def retry(func): @@ -70,7 +72,7 @@ def main(): instance = get_instance(ec2, instance_id) - tags = {'Role': role} + tags = {'Role': LEADER_TAG_VALUE if role == 'primary' else role} tag_resource(ec2, instance_id, tags) tags.update({'Instance': instance_id}) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index b7a301202..851af037c 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -577,6 +577,7 @@ def get_placeholders(provider): placeholders.setdefault('PAM_OAUTH2', '') placeholders.setdefault('CALLBACK_SCRIPT', '') placeholders.setdefault('DCS_ENABLE_KUBERNETES_API', '') + placeholders.setdefault('AWS_LEADER_TAG_VALUE', 'master') placeholders.setdefault('KUBERNETES_ROLE_LABEL', 'spilo-role') placeholders.setdefault('KUBERNETES_LEADER_LABEL_VALUE', 'master') placeholders.setdefault('KUBERNETES_STANDBY_LEADER_LABEL_VALUE', 'master') From bb1a86f9ff6c048d2235199ea2e704816439c5a3 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:52:52 +0100 Subject: [PATCH 08/41] Properly handle Patroni bootstrap_labels config (#1093) Additionally fix LOG_S3_TAGS parsing logic --- ENVIRONMENT.rst | 1 + postgres-appliance/scripts/configure_spilo.py | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ENVIRONMENT.rst b/ENVIRONMENT.rst index ea2e3df81..4fbb947a9 100644 --- a/ENVIRONMENT.rst +++ b/ENVIRONMENT.rst @@ -106,6 +106,7 @@ Environment Configuration Settings - **KUBERNETES_STANDBY_LEADER_LABEL_VALUE**: value of the pod label if Postgres role is standby_leader when running on Kubernetes. Default is 'master'. - **KUBERNETES_SCOPE_LABEL**: name of the label containing cluster name. Default is 'version'. - **KUBERNETES_LABELS**: a JSON describing names and values of other labels used by Patroni on Kubernetes to locate its metadata. Default is '{"application": "spilo"}'. +- **KUBERNETES_BOOTSTRAP_LABELS**: a JSON describing names and values of labels used by Patroni as ``kubernetes.bootstrap_labels``. Default is empty. - **INITDB_LOCALE**: database cluster's default UTF-8 locale (en_US by default) - **ENABLE_WAL_PATH_COMPAT**: old Spilo images were generating wal path in the backup store using the following template ``/spilo/{WAL_BUCKET_SCOPE_PREFIX}{SCOPE}{WAL_BUCKET_SCOPE_SUFFIX}/wal/``, while new images adding one additional directory (``{PGVERSION}``) to the end. In order to avoid (unlikely) issues with restoring WALs (from S3/GC/and so on) when switching to ``spilo-13`` please set the ``ENABLE_WAL_PATH_COMPAT=true`` when deploying old cluster with ``spilo-13`` for the first time. After that the environment variable could be removed. Change of the WAL path also mean that backups stored in the old location will not be cleaned up automatically. - **WALE_DISABLE_S3_SSE**, **WALG_DISABLE_S3_SSE**: by default wal-e/wal-g are configured to encrypt files uploaded to S3. In order to disable it you can set this environment variable to ``true``. diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 851af037c..298109338 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -585,6 +585,7 @@ def get_placeholders(provider): placeholders.setdefault('KUBERNETES_LABELS', KUBERNETES_DEFAULT_LABELS) placeholders.setdefault('KUBERNETES_USE_CONFIGMAPS', '') placeholders.setdefault('KUBERNETES_BYPASS_API_SERVICE', 'true') + placeholders.setdefault('KUBERNETES_BOOTSTRAP_LABELS', '') placeholders.setdefault('USE_PAUSE_AT_RECOVERY_TARGET', False) placeholders.setdefault('CLONE_METHOD', '') placeholders.setdefault('CLONE_WITH_WALE', '') @@ -741,13 +742,15 @@ def get_dcs_config(config, placeholders): if USE_KUBERNETES and placeholders.get('DCS_ENABLE_KUBERNETES_API'): config = {'kubernetes': dcs_configs['kubernetes']} - try: - kubernetes_labels = json.loads(config['kubernetes'].get('labels')) - except (TypeError, ValueError) as e: - logging.warning("could not parse kubernetes labels as a JSON: %r, " - "reverting to the default: %s", e, KUBERNETES_DEFAULT_LABELS) - kubernetes_labels = json.loads(KUBERNETES_DEFAULT_LABELS) - config['kubernetes']['labels'] = kubernetes_labels + + for param, default_val in (('labels', KUBERNETES_DEFAULT_LABELS), ('bootstrap_labels', '{}')): + try: + kubernetes_labels = json.loads(config['kubernetes'].get(param)) + except (TypeError, ValueError) as e: + logging.warning("could not parse kubernetes %s as a JSON: %r, " + "reverting to the default: %s", param, e, default_val) + kubernetes_labels = json.loads(default_val) + config['kubernetes'][param] = kubernetes_labels if not config['kubernetes'].pop('use_configmaps'): config['kubernetes'].update({'use_endpoints': True, @@ -792,7 +795,12 @@ def write_log_environment(placeholders): if not os.path.exists(log_env['LOG_ENV_DIR']): os.makedirs(log_env['LOG_ENV_DIR']) - tags = json.loads(os.getenv('LOG_S3_TAGS')) + try: + tags = json.loads(os.getenv('LOG_S3_TAGS')) + except (TypeError, ValueError) as e: + logging.warning("could not parse LOG_S3_TAGS as a JSON: %r, reverting to the default empty dict", e) + tags = {} + log_env['LOG_S3_TAGS'] = "&".join(f"{key}={os.getenv(value)}" for key, value in tags.items()) for var in ('LOG_TMPDIR', From 3f59bd28cef10e987b6ecc39e5f6ae32e0b61201 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:50:17 +0100 Subject: [PATCH 09/41] Set proper default KUBERNETES_BOOTSTRAP_LABELS (#1097) --- postgres-appliance/scripts/configure_spilo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 298109338..dd45b0be0 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -585,7 +585,7 @@ def get_placeholders(provider): placeholders.setdefault('KUBERNETES_LABELS', KUBERNETES_DEFAULT_LABELS) placeholders.setdefault('KUBERNETES_USE_CONFIGMAPS', '') placeholders.setdefault('KUBERNETES_BYPASS_API_SERVICE', 'true') - placeholders.setdefault('KUBERNETES_BOOTSTRAP_LABELS', '') + placeholders.setdefault('KUBERNETES_BOOTSTRAP_LABELS', '{}') placeholders.setdefault('USE_PAUSE_AT_RECOVERY_TARGET', False) placeholders.setdefault('CLONE_METHOD', '') placeholders.setdefault('CLONE_WITH_WALE', '') From b75fb61fd3978ece363fdd86b3a2cc64d8b547c3 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:44:52 +0200 Subject: [PATCH 10/41] Add pg_roaringbitmap and pgfaceting (#1101) --- postgres-appliance/build_scripts/base.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index 425507ebe..ff885c5e9 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -96,7 +96,12 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-wal2json" "postgresql-${version}-decoderbufs" "postgresql-${version}-pllua" - "postgresql-${version}-pgvector") + "postgresql-${version}-pgvector" + "postgresql-${version}-roaringbitmap") + + if [ "$version" -ge 14 ]; then + EXTRAS+=("postgresql-${version}-pgfaceting") + fi if [ "$WITH_PERL" = "true" ]; then EXTRAS+=("postgresql-plperl-${version}") From 786ab8f67d7c0261a737fc8a2f9f23478f440c4c Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:53:45 +0700 Subject: [PATCH 11/41] Add roaringbitmap to extwlist (#1107) --- postgres-appliance/scripts/configure_spilo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index dd45b0be0..60932fc0f 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -320,7 +320,7 @@ def deep_update(a, b): bg_mon.history_buckets: 120 pg_stat_statements.track_utility: 'off' extwlist.extensions: 'btree_gin,btree_gist,citext,extra_window_functions,first_last_agg,hll,\ -hstore,hypopg,intarray,ltree,pgcrypto,pgq,pgq_node,pg_trgm,postgres_fdw,tablefunc,uuid-ossp' +hstore,hypopg,intarray,ltree,pgcrypto,pgq,pgq_node,pg_trgm,postgres_fdw,roaringbitmap,tablefunc,uuid-ossp' extwlist.custom_path: /scripts pg_hba: - local all all trust From 042af714bbdeca6cd9689e2377183464f3d8092e Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:49:30 +0200 Subject: [PATCH 12/41] Correctly format URLs when address is IPv6 (#1108) Signed-off-by: Mikkel Oscar Lyderik Larsen --- postgres-appliance/scripts/configure_spilo.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 60932fc0f..5ee27a381 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -268,7 +268,7 @@ def deep_update(a, b): scope: &scope '{{SCOPE}}' restapi: listen: ':{{APIPORT}}' - connect_address: {{RESTAPI_CONNECT_ADDRESS}}:{{APIPORT}} + connect_address: '{{RESTAPI_CONNECT_ADDRESS}}' {{#SSL_RESTAPI_CA_FILE}} cafile: {{SSL_RESTAPI_CA_FILE}} {{/SSL_RESTAPI_CA_FILE}} @@ -284,7 +284,7 @@ def deep_update(a, b): use_unix_socket_repl: true name: '{{instance_data.id}}' listen: '*:{{PGPORT}}' - connect_address: {{instance_data.ip}}:{{PGPORT}} + connect_address: '{{PG_CONNECT_ADDRESS}}' data_dir: {{PGDATA}} parameters: archive_command: {{{postgresql.parameters.archive_command}}} @@ -697,7 +697,11 @@ def get_placeholders(provider): placeholders['postgresql']['parameters']['max_connections'] = min(max(100, int(os_memory_mb/30)), 1000) placeholders['instance_data'] = get_instance_metadata(provider) - placeholders.setdefault('RESTAPI_CONNECT_ADDRESS', placeholders['instance_data']['ip']) + restapi_connect_address = format_url(placeholders['instance_data']['ip'], placeholders.get("APIPORT")) + placeholders.setdefault('RESTAPI_CONNECT_ADDRESS', restapi_connect_address) + + connect_address = format_url(placeholders['instance_data']['ip'], placeholders.get("PGPORT")) + placeholders.setdefault("PG_CONNECT_ADDRESS", connect_address) placeholders['BGMON_LISTEN_IP'] = get_listen_ip() @@ -719,6 +723,12 @@ def get_placeholders(provider): return placeholders +def format_url(host, port): + if ":" in host: + return "[" + host + "]" + ":" + port + return host + ":" + port + + def pystache_render(*args, **kwargs): render = pystache.Renderer(missing_tags='strict') return render.render(*args, **kwargs) From ce8bb9ebdf76be159a0ddc97d69beb701947705c Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:39:15 +0200 Subject: [PATCH 13/41] Enable ipv6 on AWS (#1109) - keep AWS_EC2_METADATA_SERVICE* envs to enable wal-g - use custom wal-e --- postgres-appliance/build_scripts/patroni_wale.sh | 2 +- postgres-appliance/runit/patroni/run | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/build_scripts/patroni_wale.sh b/postgres-appliance/build_scripts/patroni_wale.sh index a476954ec..04c792314 100644 --- a/postgres-appliance/build_scripts/patroni_wale.sh +++ b/postgres-appliance/build_scripts/patroni_wale.sh @@ -40,7 +40,7 @@ if [ "$DEMO" != "true" ]; then find /usr/share/python-babel-localedata/locale-data -type f ! -name 'en_US*.dat' -delete pip3 install filechunkio protobuf \ - 'git+https://github.com/zalando-pg/wal-e.git#egg=wal-e[aws,google,swift]' \ + 'git+https://github.com/zalando-pg/wal-e.git@ipv6-imds#egg=wal-e[aws,google,swift]' \ 'git+https://github.com/zalando/pg_view.git@master#egg=pg-view' # https://github.com/wal-e/wal-e/issues/318 diff --git a/postgres-appliance/runit/patroni/run b/postgres-appliance/runit/patroni/run index c92fe9708..4cecaba06 100755 --- a/postgres-appliance/runit/patroni/run +++ b/postgres-appliance/runit/patroni/run @@ -24,7 +24,7 @@ then fi # Only small subset of environment variables is allowed. We don't want accidentally disclose sensitive information -for E in $(printenv -0 | tr '\n' ' ' | sed 's/\x00/\n/g' | grep -vE '^(KUBERNETES_(SERVICE|PORT|ROLE)[_=]|((POD_(IP|NAMESPACE))|HOSTNAME|PATH|PGHOME|LC_ALL|ENABLE_PG_MON)=)' | sed 's/=.*//g'); do +for E in $(printenv -0 | tr '\n' ' ' | sed 's/\x00/\n/g' | grep -vE '^((AWS_EC2_METADATA_SERVICE_ENDPOINT|KUBERNETES_(SERVICE|PORT|ROLE))[_=]|((POD_(IP|NAMESPACE))|HOSTNAME|PATH|PGHOME|LC_ALL|ENABLE_PG_MON)=)' | sed 's/=.*//g'); do unset $E done From befe3cf8f6cebe117fac66217e26d8a80fd73661 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:03:52 +0200 Subject: [PATCH 14/41] Move AWS_EC2_METADATA_SERVICE_ENDPOINT* env vars to env files (#1112) --- postgres-appliance/runit/patroni/run | 2 +- postgres-appliance/scripts/configure_spilo.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/postgres-appliance/runit/patroni/run b/postgres-appliance/runit/patroni/run index 4cecaba06..c92fe9708 100755 --- a/postgres-appliance/runit/patroni/run +++ b/postgres-appliance/runit/patroni/run @@ -24,7 +24,7 @@ then fi # Only small subset of environment variables is allowed. We don't want accidentally disclose sensitive information -for E in $(printenv -0 | tr '\n' ' ' | sed 's/\x00/\n/g' | grep -vE '^((AWS_EC2_METADATA_SERVICE_ENDPOINT|KUBERNETES_(SERVICE|PORT|ROLE))[_=]|((POD_(IP|NAMESPACE))|HOSTNAME|PATH|PGHOME|LC_ALL|ENABLE_PG_MON)=)' | sed 's/=.*//g'); do +for E in $(printenv -0 | tr '\n' ' ' | sed 's/\x00/\n/g' | grep -vE '^(KUBERNETES_(SERVICE|PORT|ROLE)[_=]|((POD_(IP|NAMESPACE))|HOSTNAME|PATH|PGHOME|LC_ALL|ENABLE_PG_MON)=)' | sed 's/=.*//g'); do unset $E done diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 5ee27a381..522342889 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -820,7 +820,9 @@ def write_log_environment(placeholders): 'LOG_S3_KEY', 'LOG_S3_BUCKET', 'LOG_S3_TAGS', - 'PGLOG'): + 'PGLOG', + 'AWS_EC2_METADATA_SERVICE_ENDPOINT', + 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE',): write_file(log_env[var], os.path.join(log_env['LOG_ENV_DIR'], var), True) @@ -828,7 +830,8 @@ def write_wale_environment(placeholders, prefix, overwrite): s3_names = ['WALE_S3_PREFIX', 'WALG_S3_PREFIX', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'WALE_S3_ENDPOINT', 'AWS_ENDPOINT', 'AWS_REGION', 'AWS_INSTANCE_PROFILE', 'WALE_DISABLE_S3_SSE', 'WALG_S3_SSE_KMS_ID', 'WALG_S3_SSE', 'WALG_DISABLE_S3_SSE', 'AWS_S3_FORCE_PATH_STYLE', 'AWS_ROLE_ARN', - 'AWS_WEB_IDENTITY_TOKEN_FILE', 'AWS_STS_REGIONAL_ENDPOINTS'] + 'AWS_WEB_IDENTITY_TOKEN_FILE', 'AWS_STS_REGIONAL_ENDPOINTS', 'AWS_EC2_METADATA_SERVICE_ENDPOINT', + 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] azure_names = ['WALG_AZ_PREFIX', 'AZURE_STORAGE_ACCOUNT', 'WALG_AZURE_BUFFER_SIZE', 'WALG_AZURE_MAX_BUFFERS', 'AZURE_ENVIRONMENT_NAME'] azure_auth_names = ['AZURE_STORAGE_ACCESS_KEY', 'AZURE_STORAGE_SAS_TOKEN', 'AZURE_CLIENT_ID', From 4c72b9a64a5da02e00e1d49b4ee70565e0d264a2 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:25:42 +0200 Subject: [PATCH 15/41] Write imds env files for all prefixes (#1113) --- postgres-appliance/scripts/configure_spilo.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 522342889..933c5f4ee 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -830,8 +830,7 @@ def write_wale_environment(placeholders, prefix, overwrite): s3_names = ['WALE_S3_PREFIX', 'WALG_S3_PREFIX', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'WALE_S3_ENDPOINT', 'AWS_ENDPOINT', 'AWS_REGION', 'AWS_INSTANCE_PROFILE', 'WALE_DISABLE_S3_SSE', 'WALG_S3_SSE_KMS_ID', 'WALG_S3_SSE', 'WALG_DISABLE_S3_SSE', 'AWS_S3_FORCE_PATH_STYLE', 'AWS_ROLE_ARN', - 'AWS_WEB_IDENTITY_TOKEN_FILE', 'AWS_STS_REGIONAL_ENDPOINTS', 'AWS_EC2_METADATA_SERVICE_ENDPOINT', - 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] + 'AWS_WEB_IDENTITY_TOKEN_FILE', 'AWS_STS_REGIONAL_ENDPOINTS'] azure_names = ['WALG_AZ_PREFIX', 'AZURE_STORAGE_ACCOUNT', 'WALG_AZURE_BUFFER_SIZE', 'WALG_AZURE_MAX_BUFFERS', 'AZURE_ENVIRONMENT_NAME'] azure_auth_names = ['AZURE_STORAGE_ACCESS_KEY', 'AZURE_STORAGE_SAS_TOKEN', 'AZURE_CLIENT_ID', @@ -850,6 +849,7 @@ def write_wale_environment(placeholders, prefix, overwrite): 'WALG_LIBSODIUM_KEY', 'WALG_LIBSODIUM_KEY_PATH', 'WALG_LIBSODIUM_KEY_TRANSFORM', 'WALG_PGP_KEY', 'WALG_PGP_KEY_PATH', 'WALG_PGP_KEY_PASSPHRASE', 'no_proxy', 'http_proxy', 'https_proxy'] + aws_imds_names = ['AWS_EC2_METADATA_SERVICE_ENDPOINT', 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] wale = defaultdict(lambda: '') for name in ['PGVERSION', 'PGPORT', 'WALE_ENV_DIR', 'SCOPE', 'WAL_BUCKET_SCOPE_PREFIX', 'WAL_BUCKET_SCOPE_SUFFIX', @@ -904,7 +904,13 @@ def write_wale_environment(placeholders, prefix, overwrite): if wale.get('USE_WALG_BACKUP') and wale.get('WALG_DISABLE_S3_SSE') != 'true' and not wale.get('WALG_S3_SSE'): wale['WALG_S3_SSE'] = 'AES256' - write_envdir_names = s3_names + walg_names + + # write IMDS env vars for any prefix if defined + for name in aws_imds_names: + if placeholders.get(name): + wale[name] = placeholders.get(name) + + write_envdir_names = s3_names + walg_names + aws_imds_names elif wale.get('WAL_GCS_BUCKET') or wale.get('WAL_GS_BUCKET') or\ wale.get('WALE_GCS_PREFIX') or wale.get('WALE_GS_PREFIX') or wale.get('WALG_GS_PREFIX'): if wale.get('WALE_GCS_PREFIX'): From 62e4cddba7d6a250855f4738cb9f6a37463393e2 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Tue, 6 May 2025 08:58:11 +0200 Subject: [PATCH 16/41] Add trailing slash to AWS_EC2_METADATA_SERVICE_ENDPOINT (#1117) To support older boto3 versions --- postgres-appliance/scripts/configure_spilo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 933c5f4ee..3d347f0fa 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -812,6 +812,9 @@ def write_log_environment(placeholders): tags = {} log_env['LOG_S3_TAGS'] = "&".join(f"{key}={os.getenv(value)}" for key, value in tags.items()) + # support for older boto3 versions: https://github.com/boto/botocore/pull/2600 + if not log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'].endswith('/'): + log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'] += '/' for var in ('LOG_TMPDIR', 'LOG_SHIP_HOURLY', From 8fbe53597f848a33738d9b74ca6d339a8d0a7efd Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Wed, 21 May 2025 19:58:37 +0200 Subject: [PATCH 17/41] Fix AWS_EC2_METADATA_SERVICE_ENDPOINT* env handling (#1123) * Write AWS_EC2_METADATA_SERVICE_ENDPOINT* env files only when actually needed --- postgres-appliance/scripts/configure_spilo.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 3d347f0fa..b44a14dc5 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -812,9 +812,15 @@ def write_log_environment(placeholders): tags = {} log_env['LOG_S3_TAGS'] = "&".join(f"{key}={os.getenv(value)}" for key, value in tags.items()) - # support for older boto3 versions: https://github.com/boto/botocore/pull/2600 - if not log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'].endswith('/'): - log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'] += '/' + if log_env.get('AWS_EC2_METADATA_SERVICE_ENDPOINT'): + # support for older boto3 versions: https://github.com/boto/botocore/pull/2600 + if not log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'].endswith('/'): + log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'] += '/' + write_file(log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT'], + os.path.join(log_env['LOG_ENV_DIR'], 'AWS_EC2_METADATA_SERVICE_ENDPOINT'), True) + if log_env.get('AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'): + write_file(log_env['AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'], + os.path.join(log_env['LOG_ENV_DIR'], 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'), True) for var in ('LOG_TMPDIR', 'LOG_SHIP_HOURLY', @@ -823,9 +829,7 @@ def write_log_environment(placeholders): 'LOG_S3_KEY', 'LOG_S3_BUCKET', 'LOG_S3_TAGS', - 'PGLOG', - 'AWS_EC2_METADATA_SERVICE_ENDPOINT', - 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE',): + 'PGLOG',): write_file(log_env[var], os.path.join(log_env['LOG_ENV_DIR'], var), True) From dd4839f58796da93e035c8cac9c2cf6ed5f97ee0 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Tue, 27 May 2025 09:06:46 +0200 Subject: [PATCH 18/41] Add pgvector to extension whitelist to allow non-superuser creation (#1127) --- postgres-appliance/scripts/configure_spilo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index b44a14dc5..afb2f2929 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -320,7 +320,7 @@ def deep_update(a, b): bg_mon.history_buckets: 120 pg_stat_statements.track_utility: 'off' extwlist.extensions: 'btree_gin,btree_gist,citext,extra_window_functions,first_last_agg,hll,\ -hstore,hypopg,intarray,ltree,pgcrypto,pgq,pgq_node,pg_trgm,postgres_fdw,roaringbitmap,tablefunc,uuid-ossp' +hstore,hypopg,intarray,ltree,pgcrypto,pgq,pgq_node,pg_trgm,postgres_fdw,roaringbitmap,tablefunc,uuid-ossp,vector' extwlist.custom_path: /scripts pg_hba: - local all all trust From 5c1470ae5e14cc2f81b9443eb323c1f32a5bc255 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 18 Jun 2025 09:54:11 +0200 Subject: [PATCH 19/41] Updating wal-g version v3.0.5->v3.0.7 (#1134) --- postgres-appliance/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 9844872ea..dee5c7ee7 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -18,7 +18,7 @@ FROM $BASE_IMAGE as dependencies-builder ARG DEMO -ENV WALG_VERSION=v3.0.3 +ENV WALG_VERSION=v3.0.7 COPY build_scripts/dependencies.sh /builddeps/ From aff02129c778cf76466b1a4c0c403ce603a3449a Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 18 Jun 2025 10:38:00 +0200 Subject: [PATCH 20/41] fix download url and use ubuntu 22.04 for wal-g (#1135) --- postgres-appliance/build_scripts/dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/build_scripts/dependencies.sh b/postgres-appliance/build_scripts/dependencies.sh index 07385379c..a8eaf1604 100644 --- a/postgres-appliance/build_scripts/dependencies.sh +++ b/postgres-appliance/build_scripts/dependencies.sh @@ -29,9 +29,9 @@ apt-get install -y curl ca-certificates mkdir /builddeps/wal-g if [ "$ARCH" = "amd64" ]; then - PKG_NAME='wal-g-pg-ubuntu-20.04-amd64' + PKG_NAME='wal-g-pg-ubuntu-22.04-amd64' else - PKG_NAME='wal-g-pg-ubuntu20.04-aarch64' + PKG_NAME='wal-g-pg-ubuntu-22.04-aarch64' fi curl -sL "https://github.com/wal-g/wal-g/releases/download/$WALG_VERSION/$PKG_NAME.tar.gz" \ From 6c936ae081c270327a6ab16acc808208a9a7a93b Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Tue, 1 Jul 2025 11:49:09 +0200 Subject: [PATCH 21/41] Support deleting old backups regardless of the current tool (#1133) --- postgres-appliance/Dockerfile | 2 +- postgres-appliance/scripts/postgres_backup.sh | 60 ++++++++++--------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index dee5c7ee7..7d15482d1 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -18,7 +18,7 @@ FROM $BASE_IMAGE as dependencies-builder ARG DEMO -ENV WALG_VERSION=v3.0.7 +ENV WALG_VERSION=v3.0.5 COPY build_scripts/dependencies.sh /builddeps/ diff --git a/postgres-appliance/scripts/postgres_backup.sh b/postgres-appliance/scripts/postgres_backup.sh index 9b9a4723a..36b50955e 100755 --- a/postgres-appliance/scripts/postgres_backup.sh +++ b/postgres-appliance/scripts/postgres_backup.sh @@ -23,9 +23,6 @@ else log "ERROR: Recovery state unknown: $IN_RECOVERY" && exit 1 fi -# leave at least 2 days base backups before creating a new one -[[ "$DAYS_TO_RETAIN" -lt 2 ]] && DAYS_TO_RETAIN=2 - if [[ "$USE_WALG_BACKUP" == "true" ]]; then readonly WAL_E="wal-g" [[ -z $WALG_BACKUP_COMPRESSION_METHOD ]] || export WALG_COMPRESSION_METHOD=$WALG_BACKUP_COMPRESSION_METHOD @@ -39,34 +36,43 @@ else POOL_SIZE=(--pool-size "$POOL_SIZE") fi -BEFORE="" -LEFT=0 +# push a new base backup +log "producing a new backup" +# We reduce the priority of the backup for CPU consumption +nice -n 5 $WAL_E backup-push "$PGDATA" "${POOL_SIZE[@]}" + +# Collect all backups and sort them by modification time +mapfile -t backup_records < <(wal-g backup-list 2>/dev/null | + sed '0,/^\(backup_\)\?name\s*\(last_\)\?modified\s*/d' | + sort -k2r | + awk '{ print $1, $2 }' + ) + +# leave at least 2 days base backups and/or 2 backups +[[ "$BACKUP_NUM_TO_RETAIN" -lt 2 ]] && BACKUP_NUM_TO_RETAIN=2 +[[ "$DAYS_TO_RETAIN" -lt 2 ]] && DAYS_TO_RETAIN=2 +# Compute total after collection +TOTAL=${#backup_records[@]} +BEFORE="" NOW=$(date +%s -u) readonly NOW -while read -r name last_modified rest; do - last_modified=$(date +%s -ud "$last_modified") - if [ $(((NOW-last_modified)/86400)) -ge $DAYS_TO_RETAIN ]; then - if [ -z "$BEFORE" ] || [ "$last_modified" -gt "$BEFORE_TIME" ]; then - BEFORE_TIME=$last_modified - BEFORE=$name - fi - else - # count how many backups will remain after we remove everything up to certain date - ((LEFT=LEFT+1)) - fi -done < <($WAL_E backup-list 2> /dev/null | sed '0,/^\(backup_\)\?name\s*\(last_\)\?modified\s*/d') -# we want keep at least N backups even if the number of days exceeded -if [ -n "$BEFORE" ] && [ $LEFT -ge $DAYS_TO_RETAIN ]; then - if [[ "$USE_WALG_BACKUP" == "true" ]]; then - $WAL_E delete before FIND_FULL "$BEFORE" --confirm - else - $WAL_E delete --confirm before "$BEFORE" +for ((index=BACKUP_NUM_TO_RETAIN-1; index= DAYS_TO_RETAIN )); then + BEFORE="${backup_records[$index]%% *}" + break fi +done + +if [[ -z $BEFORE ]]; then + log "No backups older than $DAYS_TO_RETAIN days found, not deleting any" + exit 0 fi -# push a new base backup -log "producing a new backup" -# We reduce the priority of the backup for CPU consumption -exec nice -n 5 $WAL_E backup-push "$PGDATA" "${POOL_SIZE[@]}" +if [[ $TOTAL -gt $BACKUP_NUM_TO_RETAIN ]]; then + wal-g delete before FIND_FULL "$BEFORE" --confirm +else + log "There are only $TOTAL backups, not deleting any" +fi From 991748f593bd26b02919b65a465cbc01a4f482d8 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Tue, 1 Jul 2025 12:53:40 +0200 Subject: [PATCH 22/41] fix wal-g download url and use ubuntu 20.04 (#1141) --- postgres-appliance/build_scripts/dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/build_scripts/dependencies.sh b/postgres-appliance/build_scripts/dependencies.sh index a8eaf1604..65aa28055 100644 --- a/postgres-appliance/build_scripts/dependencies.sh +++ b/postgres-appliance/build_scripts/dependencies.sh @@ -29,9 +29,9 @@ apt-get install -y curl ca-certificates mkdir /builddeps/wal-g if [ "$ARCH" = "amd64" ]; then - PKG_NAME='wal-g-pg-ubuntu-22.04-amd64' + PKG_NAME='wal-g-pg-ubuntu-20.04-amd64' else - PKG_NAME='wal-g-pg-ubuntu-22.04-aarch64' + PKG_NAME='wal-g-pg-ubuntu-20.04-aarch64' fi curl -sL "https://github.com/wal-g/wal-g/releases/download/$WALG_VERSION/$PKG_NAME.tar.gz" \ From 6fe8d428d2f13b3972869e849a4c12a03a8477f1 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Tue, 15 Jul 2025 10:17:17 +0200 Subject: [PATCH 23/41] Enable build image for arm64 in pull request (#1144) --- delivery.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/delivery.yaml b/delivery.yaml index f592600c3..d05d19a21 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -29,8 +29,7 @@ pipeline: # create a Buildkit builder with CDP specific configuration docker buildx create --config /etc/cdp-buildkitd.toml --driver-opt network=host --bootstrap --use - # single platform build for PR images! - docker buildx build --platform "linux/amd64" \ + docker buildx build --platform "linux/amd64,linux/arm64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ --build-arg PGOLDVERSIONS="14 15 16" \ From a4c037e84646799d558e05cd13753d3f4bc543b2 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Tue, 29 Jul 2025 15:59:42 +0200 Subject: [PATCH 24/41] remove spilo_cmd directory (#1147) --- spilo_cmd/.gitignore | 9 - spilo_cmd/README.md | 2 - spilo_cmd/requirements.txt | 5 - spilo_cmd/setup.py | 222 ---------- spilo_cmd/spilo/__init__.py | 1 - spilo_cmd/spilo/spilo.py | 721 -------------------------------- spilo_cmd/tests/pg_service.conf | 4 - spilo_cmd/tests/test_cli.py | 51 --- 8 files changed, 1015 deletions(-) delete mode 100644 spilo_cmd/.gitignore delete mode 100644 spilo_cmd/README.md delete mode 100644 spilo_cmd/requirements.txt delete mode 100644 spilo_cmd/setup.py delete mode 100644 spilo_cmd/spilo/__init__.py delete mode 100755 spilo_cmd/spilo/spilo.py delete mode 100644 spilo_cmd/tests/pg_service.conf delete mode 100644 spilo_cmd/tests/test_cli.py diff --git a/spilo_cmd/.gitignore b/spilo_cmd/.gitignore deleted file mode 100644 index 33c29d317..000000000 --- a/spilo_cmd/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -build/ -dist/ -*.egg-info/ -.coverage -.eggs/ -coverage.xml -junit.xml -spilo.yaml -*/__pycache__ diff --git a/spilo_cmd/README.md b/spilo_cmd/README.md deleted file mode 100644 index c516b3317..000000000 --- a/spilo_cmd/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Introduction -============ diff --git a/spilo_cmd/requirements.txt b/spilo_cmd/requirements.txt deleted file mode 100644 index 692f47120..000000000 --- a/spilo_cmd/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -clickclick>=0.9 -boto>=2.37.0 -PyYAML -stups>=0.6 -prettytable diff --git a/spilo_cmd/setup.py b/spilo_cmd/setup.py deleted file mode 100644 index 59cd9a2a5..000000000 --- a/spilo_cmd/setup.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - -""" - -import sys -import os -import inspect -from distutils.cmd import Command - -import setuptools -from setuptools.command.test import test as TestCommand -from setuptools import setup - -if sys.version_info < (3, 4, 0): - sys.stderr.write('FATAL: STUPS Senza needs to be run with Python 3.4+\n') - sys.exit(1) - -__location__ = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe()))) - - -def read_version(package): - data = {} - with open(os.path.join(package, '__init__.py'), 'r') as fd: - exec(fd.read(), data) - return data['__version__'] - -NAME = 'spilo' -MAIN_PACKAGE = 'spilo' -VERSION = read_version(MAIN_PACKAGE) -DESCRIPTION = 'Spilo command line client' -LICENSE = 'Apache License 2.0' -URL = 'https://github.com/zalando/spilo' -AUTHOR = 'Feike Steenbergen' -EMAIL = 'feike.steenbergen@zalando.de' -KEYWORDS = 'aws spilo PostgreSQL cluster tunnel connect' - -COVERAGE_XML = True -COVERAGE_HTML = False -JUNIT_XML = True - -# Add here all kinds of additional classifiers as defined under -# https://pypi.python.org/pypi?%3Aaction=list_classifiers -CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: Implementation :: CPython', -] - -CONSOLE_SCRIPTS = ['spilo = spilo.spilo:cli'] - - -class PyTest(TestCommand): - - user_options = [('cov=', None, 'Run coverage'), ('cov-xml=', None, 'Generate junit xml report'), ('cov-html=', - None, 'Generate junit html report'), ('junitxml=', None, 'Generate xml of test results')] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.cov = None - self.cov_xml = False - self.cov_html = False - self.junitxml = None - - def finalize_options(self): - TestCommand.finalize_options(self) - if self.cov is not None: - self.cov = ['--cov', self.cov, '--cov-report', 'term-missing'] - if self.cov_xml: - self.cov.extend(['--cov-report', 'xml']) - if self.cov_html: - self.cov.extend(['--cov-report', 'html']) - if self.junitxml is not None: - self.junitxml = ['--junitxml', self.junitxml] - - def run_tests(self): - try: - import pytest - except: - raise RuntimeError('py.test is not installed, run: pip install pytest') - params = {'args': self.test_args} - if self.cov: - params['args'] += self.cov - params['plugins'] = ['cov'] - if self.junitxml: - params['args'] += self.junitxml - params['args'] += ['--doctest-modules', MAIN_PACKAGE, '-s', '-vv'] - errno = pytest.main(**params) - sys.exit(errno) - - -def sphinx_builder(): - try: - from sphinx.setup_command import BuildDoc - except ImportError: - - class NoSphinx(Command): - - user_options = [] - - def initialize_options(self): - raise RuntimeError('Sphinx documentation is not installed, run: pip install sphinx') - - return NoSphinx - - class BuildSphinxDocs(BuildDoc): - - def run(self): - if self.builder == 'doctest': - import sphinx.ext.doctest as doctest - # Capture the DocTestBuilder class in order to return the total - # number of failures when exiting - ref = capture_objs(doctest.DocTestBuilder) - BuildDoc.run(self) - errno = ref[-1].total_failures - sys.exit(errno) - else: - BuildDoc.run(self) - - return BuildSphinxDocs - - -class ObjKeeper(type): - - instances = {} - - def __init__(cls, name, bases, dct): - cls.instances[cls] = [] - - def __call__(cls, *args, **kwargs): - cls.instances[cls].append(super(ObjKeeper, cls).__call__(*args, **kwargs)) - return cls.instances[cls][-1] - - -def capture_objs(cls): - from six import add_metaclass - module = inspect.getmodule(cls) - name = cls.__name__ - keeper_class = add_metaclass(ObjKeeper)(cls) - setattr(module, name, keeper_class) - cls = getattr(module, name) - return keeper_class.instances[cls] - - -def get_install_requirements(path): - content = open(os.path.join(__location__, path)).read() - return [req for req in content.split('\\n') if req != ''] - - -def read(fname): - return open(os.path.join(__location__, fname)).read() - - -def setup_package(): - # Assemble additional setup commands - cmdclass = {} - cmdclass['docs'] = sphinx_builder() - cmdclass['doctest'] = sphinx_builder() - cmdclass['test'] = PyTest - - # Some helper variables - version = os.getenv('GO_PIPELINE_LABEL', VERSION) - - docs_path = os.path.join(__location__, 'docs') - docs_build_path = os.path.join(docs_path, '_build') - install_reqs = get_install_requirements('requirements.txt') - - command_options = {'docs': { - 'project': ('setup.py', MAIN_PACKAGE), - 'version': ('setup.py', version.split('-', 1)[0]), - 'release': ('setup.py', version), - 'build_dir': ('setup.py', docs_build_path), - 'config_dir': ('setup.py', docs_path), - 'source_dir': ('setup.py', docs_path), - }, 'doctest': { - 'project': ('setup.py', MAIN_PACKAGE), - 'version': ('setup.py', version.split('-', 1)[0]), - 'release': ('setup.py', version), - 'build_dir': ('setup.py', docs_build_path), - 'config_dir': ('setup.py', docs_path), - 'source_dir': ('setup.py', docs_path), - 'builder': ('setup.py', 'doctest'), - }, 'test': {'test_suite': ('setup.py', 'tests'), 'cov': ('setup.py', MAIN_PACKAGE)}} - if JUNIT_XML: - command_options['test']['junitxml'] = 'setup.py', 'junit.xml' - if COVERAGE_XML: - command_options['test']['cov_xml'] = 'setup.py', True - if COVERAGE_HTML: - command_options['test']['cov_html'] = 'setup.py', True - - setup( - name=NAME, - version=version, - url=URL, - description=DESCRIPTION, - author=AUTHOR, - author_email=EMAIL, - license=LICENSE, - keywords=KEYWORDS, - long_description=read('README.md'), - classifiers=CLASSIFIERS, - test_suite='tests', - packages=setuptools.find_packages(exclude=['tests', 'tests.*']), - install_requires=install_reqs, - setup_requires=['flake8'], - cmdclass=cmdclass, - tests_require=['pytest-cov', 'pytest'], - command_options=command_options, - entry_points={'console_scripts': CONSOLE_SCRIPTS}, - ) - - -if __name__ == '__main__': - setup_package() diff --git a/spilo_cmd/spilo/__init__.py b/spilo_cmd/spilo/__init__.py deleted file mode 100644 index 11d27f8c7..000000000 --- a/spilo_cmd/spilo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.1' diff --git a/spilo_cmd/spilo/spilo.py b/spilo_cmd/spilo/spilo.py deleted file mode 100755 index 9992e8763..000000000 --- a/spilo_cmd/spilo/spilo.py +++ /dev/null @@ -1,721 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import click -import atexit -import getpass -import collections -import logging -import re -import sys -import boto -import boto.cloudformation -import boto.ec2 -import boto.ec2.elb -import boto.route53 -import os -import prettytable -import json -import signal -import dateutil -import time -import socket -import datetime -import subprocess -import yaml -import configparser - -from clickclick import AliasedGroup, OutputFormat -from clickclick.console import print_table, format_time -import senza.cli -from senza.cli import get_region, check_credentials, get_stacks, resources, handle_exceptions, get_instance_health, \ - parse_time, watching -from dateutil import parser as dateutil_parser - -STYLES = senza.cli.STYLES -TITLES = senza.cli.TITLES - -STYLES['MASTER'] = {'fg': 'green'} -STYLES['REPLICA'] = {'fg': 'yellow'} - -ec2 = None -tunnels = {'patroni':None, 'postgres':None} -managed_processes = dict() - -processed = False -PIUCONFIG = '~/.config/piu/piu.yaml' -if sys.platform == 'darwin': - PIUCONFIG = '~/Library/Application Support/piu/piu.yaml' -PIUCONFIG = os.path.expanduser(PIUCONFIG) - -option_port = click.option('-p', '--port', type=click.INT, help='The PostgreSQL port', envvar='PGPORT', default=5432) -option_log_level = click.option('--log-level', '--loglevel', help='Set the log level.', default='WARNING') -option_odd_config_file = click.option('--odd-config-file', help='Alternative odd config file', - type=click.Path(exists=False), default=os.path.expanduser(PIUCONFIG)) -option_pg_service_file = click.option('--pg_service-file', help='The PostgreSQL service file', envvar='PGSERVICEFILE', - type=click.Path(exists=True)) -option_region = click.option('--region', envvar='AWS_DEFAULT_REGION', metavar='AWS_REGION_ID', - help='AWS region ID (e.g. eu-west-1)') -option_reuse = click.option('--reuse/--no-reuse', default=True, help='Reuse an already exisiting tunnel') - -cluster_argument = click.argument('cluster') - - -class Spilo(collections.namedtuple('Spilo', 'stack_name, version, dns, elb, instances, vpc_id, stack')): - pass - - -@click.group(cls=AliasedGroup) -def cli(): - """ - Spilo can connect to your Spilo cluster running inside a vpc. It does this using the stups infrastructure. - """ - - # # Ensure all are spawned processes will be cleaned in the end - atexit.register(cleanup) - - -# logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=options['loglevel']) - -def process_options(opts): - global options - global odd_config - global pg_service_name - global pg_service - global odd_config - global processed - - if processed or opts is None: - return - - options = opts - - logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=options.get('loglevel', 'WARNING')) - pg_service_name, pg_service = get_pg_service() - - odd_config = load_odd_config() - - processed = True - -def cleanup(): - for name, process in managed_processes.items(): - if process.returncode is None: - logging.info('Terminating process {} (pid={})'.format(name, process.pid)) - process.kill() - os.system('stty sane') - - -def libpq_parameters(): - parameters = dict() - parameters['host'] = 'localhost' - parameters['port'] = tunnels['postgres'] - - if pg_service_name is not None: - parameters['service'] = pg_service_name - - return parameters, ' '.join(['{}={}'.format(k, v) for (k, v) in parameters.items()]) - - -@cli.command('connect', short_help='Connect using psql') -@cluster_argument -@option_port -@option_pg_service_file -@option_odd_config_file -@option_region -@option_reuse -@option_log_level -@click.argument('psql_arguments', nargs=-1, metavar='[-- [psql OPTIONS]]') -def connect(**options): - """Connects to the the cluster specified using psql""" - - process_options(options) - - tunnel_pid = get_tunnel(options['cluster'], options['reuse']) - - psql_cmd = ['psql', libpq_parameters()[1]] - psql_cmd.extend(options['psql_arguments']) - - logging.debug('psql command: {}'.format(psql_cmd)) - - psql = subprocess.Popen(psql_cmd) - managed_processes['psql'] = psql - psql.wait() - - -@cli.command('healthcheck', short_help='Healthcheck') -@click.option('--watch', help='Keep watching every WATCH seconds') -@option_port -@option_pg_service_file -@cluster_argument -@click.argument('libpq_parameters', nargs=-1) -def healthcheck(**options): - """Does a healthcheck on the given cluster""" - - pass - - -@cli.command('list', short_help='List available spilos') -@option_log_level -@option_region -@click.option('--tunnel', help='List only the established tunnels', is_flag=True, default=False) -@click.option('--details', help='Show more details', is_flag=True, default=False) -@click.option('--watch', help='Auto update the screen every X seconds', type=click.IntRange(1, 300), metavar='SECS') -@click.argument('clusters', nargs=-1) -def list_spilos(**options): - process_options(options) - - if options['tunnel']: - spilos = list() - else: - spilos = get_spilos(region=options['region'], clusters=options['clusters'], details=options['details']) - - processes = get_my_processes() - - for _ in watching(w=False, watch=options['watch']): - if options['details']: - spilos = update_spilo_info(spilos) - print_spilos(spilos) - - -def print_spilos(spilos): - if len(spilos) == 0: - return - - columns = [ - 'cluster', - 'dns', - 'instance_id', - 'private_ip', - 'role', - 'launch_time', - ] - if spilos[0].instances is None: - columns = ['cluster', 'dns'] - - pretty_rows = list() - - for s in spilos: - pretty_row = {'cluster': s.version} - pretty_row['dns'] = ', '.join(s.dns or list()) - - if s.instances is not None: - for i in s.instances: - pretty_row.update(i) - pretty_rows.append(pretty_row.copy()) - - # # Do not repeat general cluster information - pretty_row = {'cluster': '', 'dns': ''} - else: - pretty_rows.append(pretty_row) - - print_table(columns, pretty_rows, styles=STYLES, titles=TITLES) - - -def re_search(needles=None, haystacks=None): - """Searches a list of values for a list of regexp""" - - if needles is None or haystacks is None: - return None - - if isinstance(needles, str): - needles = [needles] - - if isinstance(haystacks, str): - haystacks = [haystacks] - - for n in needles: - rp = re.compile(n) - for h in haystacks: - if rp.search(h): - return n, h - - return None - - -def get_spilo_resources(stack, cloud_formation_connection): - status = stack.stack['StackStatus'] - if 'COMPLETE' in status and 'DELETE' not in status and 'ROLLBACK' not in status: - resources = cloud_formation_connection.describe_stack_resources(stack.stack['StackName']) - - # # We know it is a Spilo if it has a PostgresLoadBalancer - for resource in resources: - if resource.logical_resource_id == 'PostgresLoadBalancer': - return resources - - logging.debug('Stack {} is not a spilo appliance'.format(stack.stack_name)) - return None - - -def update_spilo_info(spilos): - global ec2 - global elb_conn - - new_spilos = list() - - for old_spilo in spilos: - new_spilos.append(Spilo(old_spilo.stack_name, old_spilo.version, old_spilo.dns, old_spilo.elb, - get_stack_instance_details(old_spilo.stack), old_spilo.vpc_id, old_spilo.stack)) - return new_spilos - - -def get_spilos(region, clusters=None, details=False): - global ec2 - global elb_conn - - if len(clusters) == 0: - clusters = None - if isinstance(clusters, str): - clusters = [clusters] - - region = get_region(region) - check_credentials(region) - if ec2 is None: - ec2 = boto.ec2.connect_to_region(region) - cf = boto.cloudformation.connect_to_region(region) - elb_conn = boto.ec2.elb.connect_to_region(region) - route53 = boto.route53.connect_to_region(region) - - zones = route53.get_zones() - - # # Getting a "DNSServerError: 400 Bad Request" when adding type='CNAME' to the below function call - route53_records = route53.get_all_rrsets(hosted_zone_id=zones[0].id) - - cname_records = list() - - for rr in route53_records: - if rr.type == 'CNAME': - cname_records.append({'name': rr.name, 'resource_records': rr.resource_records}) - - spilos = list() - - # # How to recognize a Spilo: There are a few things we can use to determine which stack is a spilo - # # The name itself is very volatile, therefore not a good candidate. - # # Stacks containing a PostgresLoadBalancer are deemed to be a spilo, q:x - - # # We try to do as little work as possible. Therefore we try to filter out non-matching stacks asap - stacks = list() - for stack in get_stacks(stack_refs=None, region=region, all=True): - res = get_spilo_resources(stack, cf) - if res is not None: - stacks.append((stack, res)) - - for stack, resources in stacks: - stack_name = stack.name - version = stack.version - elb = None - instances = None - vpc_id = None - dns = list() - - for resource in resources: - if resource.logical_resource_id == 'PostgresLoadBalancer': - info = elb_conn.get_all_load_balancers(load_balancer_names=[resource.physical_resource_id])[0] - elb = {'name': info.name, 'dns_name': info.dns_name} - vpc_id = info.vpc_id - - dns = [info.dns_name] - - for record in cname_records: - for rr in record['resource_records']: - if rr == info.dns_name: - dns.append(record['name'][:-1]) - - if clusters is None or re_search(clusters, dns) or re_search(clusters, stack.version): - if len(dns) > 1: - dns.pop(0) - spilos.append(Spilo(stack_name=resource.stack_name, version=stack.version, elb=elb, - instances=instances, dns=dns, vpc_id=vpc_id, stack=stack)) - - return spilos - - -def get_stack_instance_details(stack): - global ec2 - global elb_conn - - instances_info = \ - ec2.get_only_instances(filters={'tag:aws:cloudformation:stack-id': stack.stack_id}) - instances_health = elb_conn.describe_instance_health(stack.stack_name) - - instances = list() - for ii in instances_info: - for ih in instances_health: - if ih.instance_id == ii.id: - instance = {'instance_id': ii.id, 'private_ip': ii.private_ip_address, - 'launch_time': parse_time(ii.launch_time)} - - if ih.state == 'InService': - instance['role'] = 'MASTER' - else: - instance['role'] = 'REPLICA' - - instances.append(instance) - - instances.sort(key=lambda k: (k['role'], k['instance_id'])) - - return instances - - -def parse_time(s: str) -> float: - ''' - >>> parse_time('2015-04-14T19:09:01.000Z') > 0 - True - ''' - try: - utc = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ') - ts = time.time() - utc_offset = datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp(ts) - local = utc + utc_offset - return local.timestamp() - except: - return None - - -def list_tunnels(cluster): - processes = get_my_processes() - processes.sort(key=lambda k: k['cluster']) - - columns = [ - 'pid', - 'host', - 'service', - 'dsn', - ] - - rows = list() - if cluster is not None: - for p in processes: - if re.search(cluster, p['host']) or re.search(cluster, p.get('service', '')): - rows.append(p) - else: - rows = processes - - print_table(columns, rows, styles=STYLES, titles=TITLES) - - -def get_my_processes(): - # # We do not use psutil for processes, as environment variables of processes is not - # # available from it. We will just use good old ps for the task - - ps_cmd = [ - 'ps', - 'e', - '-eww', - '-U', - getpass.getuser(), - '-A', - '-o', - 'pid,command', - ] - - ps_output = subprocess.check_output(ps_cmd, shell=False, stderr=subprocess.DEVNULL, - env={'LANG': 'C'}).splitlines() - ps_output.reverse() - - processes = list() - - process_re = re.compile('^\s*(\d+)\s+([^\s]+).*SPILOCLUSTER=([^\s]*)') - pgport_re = re.compile('SPILOPGPORT=(\d+)') - patroniport_re = re.compile('SPILOPATRONIPORT=(\d+)') - service_re = re.compile('SPILOSERVICE=(\w*)') - host_re = re.compile('SPILOHOST=([^\s]*)') - vpc_re = re.compile('SPILOVPCID=([^\s]*)') - - # # We cannot disable the header on every ps (Mac OS X for example), the first line is a header - line = ps_output.pop() - while len(ps_output) > 0: - line = ps_output.pop().decode('utf-8') - - match = process_re.search(line) - if match: - logging.debug('Matched line: {}'.format(line[0:120])) - process = dict() - process['pid'] = match.group(1) - process['process'] = match.group(2) - process['cluster'] = match.group(3) - - match = process_re.match(line) - - process['pid'] = match.group(1) - process['process'] = match.group(2) - - match = pgport_re.search(line) - if match: - process['pgport'] = match.group(1) - - match = patroniport_re.search(line) - if match: - process['patroniport'] = match.group(1) - - match = host_re.search(line) - if match: - process['host'] = match.group(1) - - match = vpc_re.search(line) - if match: - process['vpc_id'] = match.group(1) - - match = service_re.search(line) - if match: - process['service'] = match.group(1) - - logging.debug('Process: {}'.format(process)) - - service_dsn = process.get('service') - if service_dsn is None: - service_dsn = '' - else: - service_dsn = ' service={}'.format(service_dsn) - - process['dsn'] = '"host=localhost port={}{}"'.format(process['pgport'], service_dsn) - processes.append(process) - else: - - # logging.debug("Disregarding line: {}".format(line)) - pass - - logging.debug('Processes : {}'.format(pretty(processes))) - return processes - - -@cli.command('tunnel', short_help='Create a tunnel') -@click.option('--background/--no-background', default=True, help='Push the tunnel in the background') -@click.option('--kill', help='Kill the tunnel for the specified cluster', is_flag=True) -@click.option('--list', help='List all the tunnels that are available', is_flag=True) -@option_reuse -@option_port -@option_pg_service_file -@option_odd_config_file -@option_region -@option_log_level -@cluster_argument -def tunnel(**options): - """Sets up a tunnel to use for connecting to Spilo""" - - global tunnels - - process_options(options) - - if options['list']: - list_tunnels(options['cluster']) - sys.exit(0) - - if options['kill']: - pid = get_tunnel(options['cluster'], reuse=True, create=False) - if pid is None: - logging.warning("There was no tunnel to kill") - else: - print("Terminating process with pid={}".format(pid)) - os.kill(int(pid), signal.SIGKILL) - sys.exit(0) - - tunnel_pid = get_tunnel(options['cluster'], options['reuse']) - - if tunnel_pid is None: - raise Exception('Tunnel was requested but no pid was returned') - - if pg_service_name is None: - pg_service_env = '' - else: - pg_service_env = 'export PGSERVICE={}'.format(pg_service_name) - - print(""" -The ssh tunnel is running as a process with pid {pid}. - -You can now connect to {cluster} using the following information: - -"{dsn}" - -Examples: - - psql "{dsn}" - pg_dump "{dsn}" - pg_basebackup -d "{dsn}" --pgdata=- --format=tar | gzip -4 > "{cluster}-backup.tar.gz" - -Or you can set the environment so you connect using your chosen tool: - -export PGHOST=localhost -export PGPORT={port} -{pgservice} - -""".format(pid=tunnel_pid, - cluster=options['cluster'], dsn=libpq_parameters()[1], port=tunnels['postgres'], pgservice=pg_service_env)) - - sys.exit(0) - - -def pretty(something): - return json.dumps(something, sort_keys=True, indent=4) - - -def get_pg_service(): - """Reads all the services from all the pg service files it can find""" - - # # http://www.postgresql.org/docs/current/static/libpq-pgservice.html - # # - # # There are some precedence rules which we want to honour. - - if options.get('cluster') is None: - return None, dict() - - filenames = list() - - if options.get('pg_service_file') is not None: - filenames.append(options['pg_service_file']) - else: - filenames.append('~/.pg_service.conf') - filenames.append('~/pg_service.conf') - if filenames.append(os.environ.get('PGSYSCONFDIR')) is not None: - filenames.append(os.environ.get('PGSYSCONFDIR') + '/pg_service.conf') - filenames.append('/etc/pg_service.conf') - - filenames = [os.path.expanduser(f) for f in filenames if f is not None] - - logging.debug(pretty(options)) - - defaults = dict() - defaults['port'] = options.get('port', 5432) - defaults['host'] = options['cluster'] - - parser = configparser.ConfigParser(defaults=defaults) - - services = [options['cluster'], 'spilo'] - parsed = parser.read(filenames) - logging.debug('Read pg_service definitions from the following files: {}'.format(parsed)) - - pg_service = dict() - - for service in services: - if parser.has_section(service): - logging.debug('Using service definition [{}]'.format(service)) - return service, dict(parser.items(service, raw=True)) - - return None, dict(parser.items('DEFAULT', raw=True)) - - -def load_odd_config(): - odd_config = {'user_name':None, 'odd_host':None} - - if options.get('odd_config_file') is not None and os.path.isfile(options['odd_config_file']): - with open(options['odd_config_file'], 'r') as f: - odd_config = yaml.safe_load(f) - logging.debug('Loaded odd configuration from {}:\n{}'.format(options['odd_config_file'], pretty(odd_config))) - - return odd_config - - -def get_tunnel(service_name=None, reuse=True, create=True): - if service_name is None: - return - - processes = get_my_processes() - - processes = [p for p in processes if service_name in p['host']] - - if reuse and len(processes) > 0: - logging.info('Found a tunnel which is available: {}'.format(pretty(processes))) - tunnels['postgres'] = processes[0]['pgport'] - tunnels['patroni'] = processes[0]['patroniport'] - return processes[0]['pid'] - - if not create: - return None - - if service_name == pg_service_name: - host = pg_service.get('hostaddr') or pg_service.get('host') or pg_service_name - spilo = Spilo(stack_name=None, version=None, dns=[host], elb=None, instances=None, vpc_id=None, stack=None) - else: - spilos = get_spilos(options['region'], [service_name]) - if len(spilos) == 0: - raise Exception('Could not find a spilo cluster beginning with {}'.format(options['cluster'])) - - if len(spilos) > 1: - logging.error('Multiple candidates starting with {}:\n'.format(options['cluster'])) - print_spilos(spilos) - sys.exit(1) - spilo = spilos[0] - - # # We open 2 sockets and let the OS pick a free port for us - # # later on we will use these ports for portforwarding - pg_socket = socket.socket() - pg_socket.bind(('', 0)) - tunnels['postgres'] = int(pg_socket.getsockname()[1]) - logging.debug('Postgres tunnel port: {}'.format(tunnels['postgres'])) - - patroni_socket = socket.socket() - patroni_socket.bind(('', 0)) - tunnels['patroni'] = int(patroni_socket.getsockname()[1]) - logging.debug('tunnel port: {}'.format(tunnels['postgres'])) - - ssh_cmd = ['ssh'] - if odd_config.get('user_name') is not None: - ssh_cmd += ['{}@{}'.format(odd_config['user_name'], odd_config['odd_host'])] - else: - ssh_cmd += [odd_config.get('odd_host') or ''] - - env = os.environ.copy() - env['SPILOCLUSTER'] = spilo.version or '' - env['SPILOHOST'] = spilo.dns[0] - env['SPILOSERVICE'] = pg_service_name or '' - env['SPILOVPCID'] = spilo.vpc_id or '' - - logging.debug('Testing ssh access using cmd:{}'.format(ssh_cmd)) - test = subprocess.check_output(ssh_cmd + ['printf t3st'], shell=False, stderr=subprocess.DEVNULL) - if test != b't3st': - logging.error('Could not setup a working tunnel. You may need to request access using piu') - raise Exception(str(test)) - - - # We will close the opened socket as late as possible, to prevent other processes from occupying this port - patroni_socket.close() - pg_socket.close() - - ssh_cmd.append('-L') - logging.debug(pg_service) - - env['SPILOPGPORT'] = str(tunnels['postgres']) - port = str(pg_service['port'] or options['port']) - ssh_cmd.append('{}:{}:{}'.format(tunnels['postgres'], spilo.dns[0], str(port))) - - ssh_cmd.append('-L') - env['SPILOPATRONIPORT'] = str(tunnels['patroni']) - port = 8008 - ssh_cmd.append('{}:{}:{}'.format(tunnels['patroni'], spilo.dns[0], str(port))) - - ssh_cmd.append('-N') - - logging.info('Setting up tunnel command: {}, env={}'.format(ssh_cmd, pretty(env))) - - tunnel = subprocess.Popen(ssh_cmd, shell=False, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, env=env) - tunnel.poll() - if tunnel.returncode is not None: - raise Exception('Tunnel not running anymore, exitcode tunnel: {}'.format(tunnel.returncode)) - - # # Wait for the tunnel to be available - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - conn_test = '127.0.0.1', tunnels['postgres'] - result = sock.connect_ex(conn_test) - - timeout = 5 - epoch_time = time.time() - threshold_time = time.time() + timeout - - # # Loop until connection is established - while result != 0: - time.sleep(0.1) - result = sock.connect_ex(conn_test) - if time.time() > threshold_time: - raise Exception('Tunnel was not established within timeout of {} seconds'.format(timeout)) - break - - sock.close() - - logging.debug('Established connectivity on tunnel after {} seconds'.format(time.time() - epoch_time)) - - if not options.get('background', False): - managed_processes['tunnel'] = tunnel - - return tunnel.pid - - -if __name__ == '__main__': - handle_exceptions(cli)() diff --git a/spilo_cmd/tests/pg_service.conf b/spilo_cmd/tests/pg_service.conf deleted file mode 100644 index 8c67868f1..000000000 --- a/spilo_cmd/tests/pg_service.conf +++ /dev/null @@ -1,4 +0,0 @@ -[mock] -host=nowhere -port=5433 -user=johnny diff --git a/spilo_cmd/tests/test_cli.py b/spilo_cmd/tests/test_cli.py deleted file mode 100644 index 8cbc2d747..000000000 --- a/spilo_cmd/tests/test_cli.py +++ /dev/null @@ -1,51 +0,0 @@ -import collections - -from click.testing import CliRunner - -from spilo.spilo import cli, process_options, print_spilos, tunnel - -Spilo = collections.namedtuple('Spilo', 'stack_name, version, dns, elb, instances, vpc_id, stack') - - -def test_cli(): - cli - - -def test_tunnel(): - arguments = [] - runner = CliRunner() - result = runner.invoke(tunnel, arguments) - assert 'Usage: tunnel [OPTIONS] CLUSTER' in result.output - - arguments = ['--list', True, 'abc'] - result = runner.invoke(tunnel, arguments) - assert result.output == '' - - options = ['--pg_service-file', 'tests/pg_service.conf', 'mock'] - result = runner.invoke(tunnel, options) - assert result.exit_code == -1 - assert 't3st' in str(result.exception) - - arguments = ['abc'] - result = runner.invoke(tunnel, arguments) - assert result.exit_code != 0 - - -def test_list(): - pass - - -def test_option_processing(): - process_options(opts=None) - process_options(opts={'loglevel': 'DEBUG', 'cluster': 'feike', 'odd_config_file': '~/.config/piu/piu.yaml'}) - - -def test_print_spilos(): - spilos = list() - print_spilos(spilos) - - spilos.append(Spilo(None, None, None, None, None, None, None)) - print_spilos(spilos) - - spilos.append(Spilo(None, None, None, None, None, None, None)) - print_spilos(spilos) From 8426d2a184df01f0c9e7bb31746badfc03ef7630 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 6 Aug 2025 17:22:40 +0200 Subject: [PATCH 25/41] fix sorting (#1150) --- postgres-appliance/scripts/postgres_backup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/scripts/postgres_backup.sh b/postgres-appliance/scripts/postgres_backup.sh index 36b50955e..37ce37bc0 100755 --- a/postgres-appliance/scripts/postgres_backup.sh +++ b/postgres-appliance/scripts/postgres_backup.sh @@ -44,8 +44,8 @@ nice -n 5 $WAL_E backup-push "$PGDATA" "${POOL_SIZE[@]}" # Collect all backups and sort them by modification time mapfile -t backup_records < <(wal-g backup-list 2>/dev/null | sed '0,/^\(backup_\)\?name\s*\(last_\)\?modified\s*/d' | - sort -k2r | - awk '{ print $1, $2 }' + awk '{ print $1, $2 }' | + sort -k2r ) # leave at least 2 days base backups and/or 2 backups From e382c904916696de53cb4c66c717fc8e0c4dcc3b Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 15 Aug 2025 14:55:44 +0200 Subject: [PATCH 26/41] pg_profile 4.10 and Patroni 4.0.6 (#1151) --- postgres-appliance/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 7d15482d1..06ab2874b 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -58,7 +58,7 @@ ENV POSTGIS_VERSION=3.5 \ PG_MON_COMMIT=ead1de70794ed62ca1e34d4022f6165ff36e9a91 \ SET_USER=REL4_1_0 \ PLPROFILER=REL4_2_5 \ - PG_PROFILE=4.7 \ + PG_PROFILE=4.10 \ PAM_OAUTH2=v1.0.1 \ PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 @@ -71,7 +71,7 @@ COPY --from=dependencies-builder /builddeps/wal-g /usr/local/bin/ COPY build_scripts/patroni_wale.sh build_scripts/compress_build.sh /builddeps/ # Install patroni and wal-e -ENV PATRONIVERSION=4.0.4 +ENV PATRONIVERSION=4.0.6 ENV WALE_VERSION=1.1.1 WORKDIR / From c944b47d92272eb5b5b3b0f0661d06db64240683 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Wed, 3 Sep 2025 18:09:08 +0200 Subject: [PATCH 27/41] Update postgis to version 3.6 (#1153) --- postgres-appliance/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 06ab2874b..c88d0bdfa 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -52,7 +52,7 @@ ARG WITH_PERL=false ARG DEB_PG_SUPPORTED_VERSIONS="$PGOLDVERSIONS $PGVERSION" # Install PostgreSQL, extensions and contribs -ENV POSTGIS_VERSION=3.5 \ +ENV POSTGIS_VERSION=3.6 \ BG_MON_COMMIT=7f5887218790b263fe3f42f85f4ddc9c8400b154 \ PG_AUTH_MON_COMMIT=fe099eef7662cbc85b0b79191f47f52f1e96b779 \ PG_MON_COMMIT=ead1de70794ed62ca1e34d4022f6165ff36e9a91 \ From b4ffda5aec0753307396447098d472f9159bc0d5 Mon Sep 17 00:00:00 2001 From: Ida Novindasari Date: Tue, 9 Sep 2025 11:35:08 +0200 Subject: [PATCH 28/41] Remove WAL-E and make WAL-G the default backup tool (#1143) --- ENVIRONMENT.rst | 39 ++- postgres-appliance/Dockerfile | 9 +- ...{clone_with_wale.py => clone_with_walg.py} | 23 +- .../{patroni_wale.sh => patroni.sh} | 18 +- postgres-appliance/build_scripts/prepare.sh | 2 - postgres-appliance/launch.sh | 3 +- .../major_upgrade/inplace_upgrade.py | 10 +- postgres-appliance/scripts/callback_aws.py | 72 ++-- postgres-appliance/scripts/configure_spilo.py | 312 +++++++++--------- postgres-appliance/scripts/postgres_backup.sh | 16 +- postgres-appliance/scripts/restore_command.sh | 28 +- postgres-appliance/scripts/wal-e-wal-fetch.sh | 225 ------------- .../{wale_restore.sh => walg_restore.sh} | 9 +- postgres-appliance/tests/docker-compose.yml | 5 +- postgres-appliance/tests/test_spilo.sh | 55 ++- 15 files changed, 269 insertions(+), 557 deletions(-) rename postgres-appliance/bootstrap/{clone_with_wale.py => clone_with_walg.py} (91%) rename postgres-appliance/build_scripts/{patroni_wale.sh => patroni.sh} (77%) delete mode 100755 postgres-appliance/scripts/wal-e-wal-fetch.sh rename postgres-appliance/scripts/{wale_restore.sh => walg_restore.sh} (94%) diff --git a/ENVIRONMENT.rst b/ENVIRONMENT.rst index 4fbb947a9..a9be0922d 100644 --- a/ENVIRONMENT.rst +++ b/ENVIRONMENT.rst @@ -11,11 +11,11 @@ Environment Configuration Settings - **ETCD_KEY**: Etcd client certificate key. Can be empty if the key is part of certificate. - **PGHOME**: filesystem path where to put PostgreSQL home directory (/home/postgres by default) - **APIPORT**: TCP port to Patroni API connections (8008 by default) -- **BACKUP_SCHEDULE**: cron schedule for doing backups via WAL-E (if WAL-E is enabled, '00 01 * * *' by default) +- **BACKUP_SCHEDULE**: cron schedule for doing backups via WAL-G ('00 01 * * *' by default) - **CLONE_TARGET_TIMELINE**: timeline id of the backup for restore, 'latest' by default. - **CRONTAB**: anything that you want to run periodically as a cron job (empty by default) - **PGROOT**: a directory where we put the pgdata (by default /home/postgres/pgroot). One may adjust it to point to the mount point of the persistent volume, such as EBS. -- **WALE_TMPDIR**: directory to store WAL-E temporary files. PGROOT/../tmp by default, make sure it has a few GBs of free space. +- **WALE_TMPDIR** or **WALG_TMPDIR**: directory to store WAL-G temporary files. PGROOT/../tmp by default, make sure it has a few GBs of free space. - **PGDATA**: location of PostgreSQL data directory, by default PGROOT/pgdata. - **PGUSER_STANDBY**: username for the replication user, 'standby' by default. - **PGPASSWORD_STANDBY**: a password for the replication user, 'standby' by default. @@ -47,22 +47,22 @@ Environment Configuration Settings - **SSL_RESTAPI_PRIVATE_KEY**: content of the REST Api SSL private key in the SSL_PRIVATE_KEY_FILE file (by default /run/certs/server.key). - **SSL_TEST_RELOAD**: whenever to test for certificate rotation and reloading (by default True if SSL_PRIVATE_KEY_FILE has been set). - **RESTAPI_CONNECT_ADDRESS**: when you configure Patroni RESTAPI in SSL mode some safe API (i.e. switchover) perform hostname validation. In this case could be convenient configure ````restapi.connect_address````as a hostname instead of IP. For example, you can configure it as "$(POD_NAME).". -- **WALE_BACKUP_THRESHOLD_MEGABYTES**: maximum size of the WAL segments accumulated after the base backup to consider WAL-E restore instead of pg_basebackup. -- **WALE_BACKUP_THRESHOLD_PERCENTAGE**: maximum ratio (in percents) of the accumulated WAL files to the base backup to consider WAL-E restore instead of pg_basebackup. -- **WALE_ENV_DIR**: directory where to store WAL-E environment variables +- **WALG_BACKUP_THRESHOLD_MEGABYTES** or **WALE_BACKUP_THRESHOLD_MEGABYTES**: maximum size of the WAL segments accumulated after the base backup to consider WAL-G restore instead of pg_basebackup. +- **WALG_BACKUP_THRESHOLD_PERCENTAGE** or **WALE_BACKUP_THRESHOLD_PERCENTAGE**: maximum ratio (in percents) of the accumulated WAL files to the base backup to consider WAL-G restore instead of pg_basebackup. +- **WALG_ENV_DIR** or **WALE_ENV_DIR**: directory where to store WAL-G environment variables - **WAL_RESTORE_TIMEOUT**: timeout (in seconds) for restoring a single WAL file (at most 16 MB) from the backup location, 0 by default. A duration of 0 disables the timeout. -- **WAL_S3_BUCKET**: (optional) name of the S3 bucket used for WAL-E base backups. +- **WAL_S3_BUCKET**: (optional) name of the S3 bucket used for WAL-G base backups. - **AWS_ACCESS_KEY_ID**: (optional) aws access key - **AWS_SECRET_ACCESS_KEY**: (optional) aws secret key - **AWS_REGION**: (optional) region of S3 bucket - **AWS_ENDPOINT**: (optional) in format 'https://s3.AWS_REGION.amazonaws.com:443', if not specified will be calculated from AWS_REGION -- **WALE_S3_ENDPOINT**: (optional) in format 'https+path://s3.AWS_REGION.amazonaws.com:443', if not specified will be calculated from AWS_ENDPOINT or AWS_REGION -- **WALE_S3_PREFIX**: (optional) the full path to the backup location on S3 in the format s3://bucket-name/very/long/path. If not specified Spilo will generate it from WAL_S3_BUCKET. -- **WAL_GS_BUCKET**: ditto for the Google Cloud Storage (WAL-E supports both S3 and GCS). -- **WALE_GS_PREFIX**: (optional) the full path to the backup location on the Google Cloud Storage in the format gs://bucket-name/very/long/path. If not specified Spilo will generate it from WAL_GS_BUCKET. -- **GOOGLE_APPLICATION_CREDENTIALS**: credentials for WAL-E when running in Google Cloud. +- **WALG_S3_ENDPOINT** or **WALE_S3_ENDPOINT**: (optional) in format 'https+path://s3.AWS_REGION.amazonaws.com:443', if not specified will be calculated from AWS_ENDPOINT or AWS_REGION +- **WALG_S3_PREFIX** or **WALE_S3_PREFIX**: (optional) the full path to the backup location on S3 in the format s3://bucket-name/very/long/path. If not specified Spilo will generate it from WAL_S3_BUCKET. +- **WAL_GS_BUCKET**: ditto for the Google Cloud Storage (WAL-G supports both S3 and GCS). +- **WALG_GS_PREFIX** or **WALE_GS_PREFIX**: (optional) the full path to the backup location on the Google Cloud Storage in the format gs://bucket-name/very/long/path. If not specified Spilo will generate it from WAL_GS_BUCKET. +- **GOOGLE_APPLICATION_CREDENTIALS**: credentials for WAL-G when running in Google Cloud. - **WAL_SWIFT_BUCKET**: ditto for the OpenStack Object Storage (Swift) -- **SWIFT_AUTHURL**: see wal-e documentation https://github.com/wal-e/wal-e#swift +- **SWIFT_AUTHURL**: see wal-g documentation https://wal-g.readthedocs.io/STORAGES/#swift - **SWIFT_TENANT**: - **SWIFT_TENANT_ID**: - **SWIFT_USER**: @@ -79,7 +79,7 @@ Environment Configuration Settings - **SWIFT_PROJECT_ID**: - **SWIFT_PROJECT_DOMAIN_NAME**: - **SWIFT_PROJECT_DOMAIN_ID**: -- **WALE_SWIFT_PREFIX**: (optional) the full path to the backup location on the Swift Storage in the format swift://bucket-name/very/long/path. If not specified Spilo will generate it from WAL_SWIFT_BUCKET. +- **WALG_SWIFT_PREFIX** or **WALE_SWIFT_PREFIX**: (optional) the full path to the backup location on the Swift Storage in the format swift://bucket-name/very/long/path. If not specified Spilo will generate it from WAL_SWIFT_BUCKET. - **SSH_USERNAME**: (optional) the username for WAL backups. - **SSH_PORT**: (optional) the ssh port for WAL backups. - **SSH_PRIVATE_KEY_PATH**: (optional) the path to the private key used for WAL backups. @@ -109,18 +109,17 @@ Environment Configuration Settings - **KUBERNETES_BOOTSTRAP_LABELS**: a JSON describing names and values of labels used by Patroni as ``kubernetes.bootstrap_labels``. Default is empty. - **INITDB_LOCALE**: database cluster's default UTF-8 locale (en_US by default) - **ENABLE_WAL_PATH_COMPAT**: old Spilo images were generating wal path in the backup store using the following template ``/spilo/{WAL_BUCKET_SCOPE_PREFIX}{SCOPE}{WAL_BUCKET_SCOPE_SUFFIX}/wal/``, while new images adding one additional directory (``{PGVERSION}``) to the end. In order to avoid (unlikely) issues with restoring WALs (from S3/GC/and so on) when switching to ``spilo-13`` please set the ``ENABLE_WAL_PATH_COMPAT=true`` when deploying old cluster with ``spilo-13`` for the first time. After that the environment variable could be removed. Change of the WAL path also mean that backups stored in the old location will not be cleaned up automatically. -- **WALE_DISABLE_S3_SSE**, **WALG_DISABLE_S3_SSE**: by default wal-e/wal-g are configured to encrypt files uploaded to S3. In order to disable it you can set this environment variable to ``true``. +- **WALG_DISABLE_S3_SSE** or **WALE_DISABLE_S3_SSE**: by default wal-g is configured to encrypt files uploaded to S3. In order to disable it you can set this environment variable to ``true``. - **USE_OLD_LOCALES**: whether to use old locales from Ubuntu 18.04 in the Ubuntu 22.04-based image. Default is false. wal-g ----- -`wal-g` is used by default for Azure and SSH backups and restore. -In case of S3, `wal-e` is used for backups and `wal-g` for restore. - -- **USE_WALG_BACKUP**: (optional) Enforce using `wal-g` instead of `wal-e` for backups (Boolean) -- **USE_WALG_RESTORE**: (optional) Enforce using `wal-g` instead of `wal-e` for restores (Boolean) - +wal-g is used everywhere in Spilo to perform backups and restore from them. **Support for wal-e has been removed**. +Backward compatibility is ensured for environment variables containing **WALE**, the env-dir layout, and bootstrap method names. +This allows existing configurations and clusters to continue working without requiring immediate changes. +Regardless of which variable is set, all backups and restores will be performed using wal-g. +However, if both **WALE** and **WALG** variables are present, the latter will take precedence. - **WALG_DELTA_MAX_STEPS**, **WALG_DELTA_ORIGIN**, **WALG_DOWNLOAD_CONCURRENCY**, **WALG_UPLOAD_CONCURRENCY**, **WALG_UPLOAD_DISK_CONCURRENCY**, **WALG_DISK_RATE_LIMIT**, **WALG_NETWORK_RATE_LIMIT**, **WALG_COMPRESSION_METHOD**, **WALG_BACKUP_COMPRESSION_METHOD**, **WALG_BACKUP_FROM_REPLICA**, **WALG_SENTINEL_USER_DATA**, **WALG_PREVENT_WAL_OVERWRITE**: (optional) configuration options for wal-g. - **WALG_S3_CA_CERT_FILE**: (optional) TLS CA certificate for wal-g (see [wal-g configuration](https://github.com/wal-g/wal-g#configuration)) - **WALG_SSH_PREFIX**: (optional) the ssh prefix to store WAL backups at in the format ssh://host.example.com/path/to/backups/ See `Wal-g `__ documentation for details. diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index c88d0bdfa..4f8410b5b 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -68,15 +68,14 @@ RUN bash base.sh # Install wal-g COPY --from=dependencies-builder /builddeps/wal-g /usr/local/bin/ -COPY build_scripts/patroni_wale.sh build_scripts/compress_build.sh /builddeps/ +COPY build_scripts/patroni.sh build_scripts/compress_build.sh /builddeps/ -# Install patroni and wal-e +# Install patroni ENV PATRONIVERSION=4.0.6 -ENV WALE_VERSION=1.1.1 WORKDIR / -RUN bash /builddeps/patroni_wale.sh +RUN bash /builddeps/patroni.sh RUN if [ "$COMPRESS" = "true" ]; then bash /builddeps/compress_build.sh; fi @@ -101,7 +100,7 @@ ENV LC_ALL=en_US.utf-8 \ RW_DIR=/run \ DEMO=$DEMO -ENV WALE_ENV_DIR=$RW_DIR/etc/wal-e.d/env \ +ENV WALG_ENV_DIR=$RW_DIR/etc/wal-e.d/env \ LOG_ENV_DIR=$RW_DIR/etc/log.d/env \ PGROOT=$PGHOME/pgdata/pgroot diff --git a/postgres-appliance/bootstrap/clone_with_wale.py b/postgres-appliance/bootstrap/clone_with_walg.py similarity index 91% rename from postgres-appliance/bootstrap/clone_with_wale.py rename to postgres-appliance/bootstrap/clone_with_walg.py index 9e0adc1c5..1d32823d0 100755 --- a/postgres-appliance/bootstrap/clone_with_wale.py +++ b/postgres-appliance/bootstrap/clone_with_walg.py @@ -25,7 +25,7 @@ def read_configuration(): parser.add_argument('--recovery-target-time', help='the timestamp up to which recovery will proceed (including time zone)', dest='recovery_target_time_string') - parser.add_argument('--dry-run', action='store_true', help='find a matching backup and build the wal-e ' + parser.add_argument('--dry-run', action='store_true', help='find a matching backup and build the wal-g.' 'command to fetch that backup without running it') args = parser.parse_args() @@ -40,8 +40,8 @@ def read_configuration(): return options(args.scope, args.datadir, recovery_target_time, args.dry_run) -def build_wale_command(command, datadir=None, backup=None): - cmd = ['wal-g' if os.getenv('USE_WALG_RESTORE') == 'true' else 'wal-e'] + [command] +def build_walg_command(command, datadir=None, backup=None): + cmd = ['wal-g', command] if command == 'backup-fetch': if datadir is None or backup is None: raise Exception("backup-fetch requires datadir and backup arguments") @@ -79,7 +79,7 @@ def choose_backup(backup_list, recovery_target_time): def list_backups(env): - backup_list_cmd = build_wale_command('backup-list') + backup_list_cmd = build_walg_command('backup-list') output = subprocess.check_output(backup_list_cmd, env=env) reader = csv.DictReader(fix_output(output), dialect='excel-tab') return list(reader) @@ -89,7 +89,7 @@ def get_clone_envdir(): from spilo_commons import get_patroni_config config = get_patroni_config() - restore_command = shlex.split(config['bootstrap']['clone_with_wale']['recovery_conf']['restore_command']) + restore_command = shlex.split(config['bootstrap']['clone_with_walg']['recovery_conf']['restore_command']) if len(restore_command) > 4 and restore_command[0] == 'envdir': return restore_command[1] raise Exception('Failed to find clone envdir') @@ -117,10 +117,9 @@ def get_possible_versions(): return [ver for _, ver in sorted(versions.items(), reverse=True)] -def get_wale_environments(env): - use_walg = env.get('USE_WALG_RESTORE') == 'true' - prefix = 'WALG_' if use_walg else 'WALE_' - # len('WALE__PREFIX') = 12 +def get_walg_environments(env): + prefix = 'WALG_' + # len('WALG_PREFIX') = 12 names = [name for name in env.keys() if name.endswith('_PREFIX') and name.startswith(prefix) and len(name) > 12] if len(names) != 1: raise Exception('Found find {0} {1}*_PREFIX environment variables, expected 1' @@ -141,7 +140,7 @@ def get_wale_environments(env): def find_backup(recovery_target_time, env): old_value = None - for name, value in get_wale_environments(env): + for name, value in get_walg_environments(env): logger.info('Trying %s for clone', value) if not old_value: old_value = env[name] @@ -164,12 +163,12 @@ def run_clone_from_s3(options): backup_name, update_envdir = find_backup(options.recovery_target_time, env) - backup_fetch_cmd = build_wale_command('backup-fetch', options.datadir, backup_name) + backup_fetch_cmd = build_walg_command('backup-fetch', options.datadir, backup_name) logger.info("cloning cluster %s using %s", options.name, ' '.join(backup_fetch_cmd)) if not options.dry_run: ret = subprocess.call(backup_fetch_cmd, env=env) if ret != 0: - raise Exception("wal-e backup-fetch exited with exit code {0}".format(ret)) + raise Exception("wal-g backup-fetch exited with exit code {0}".format(ret)) if update_envdir: # We need to update file in the clone envdir or restore_command will fail! envdir = get_clone_envdir() diff --git a/postgres-appliance/build_scripts/patroni_wale.sh b/postgres-appliance/build_scripts/patroni.sh similarity index 77% rename from postgres-appliance/build_scripts/patroni_wale.sh rename to postgres-appliance/build_scripts/patroni.sh index 04c792314..e9fe60a79 100644 --- a/postgres-appliance/build_scripts/patroni_wale.sh +++ b/postgres-appliance/build_scripts/patroni.sh @@ -1,8 +1,8 @@ #!/bin/bash -## ------------------------- -## Install patroni and wal-e -## ------------------------- +## ---------------- +## Install patroni +## ---------------- export DEBIAN_FRONTEND=noninteractive @@ -26,25 +26,17 @@ if [ "$DEMO" != "true" ]; then python3-etcd \ python3-consul \ python3-kazoo \ - python3-boto \ python3-boto3 \ python3-botocore \ python3-cachetools \ - python3-cffi \ - python3-gevent \ python3-pyasn1-modules \ python3-rsa \ - python3-s3transfer \ - python3-swiftclient + python3-s3transfer find /usr/share/python-babel-localedata/locale-data -type f ! -name 'en_US*.dat' -delete - pip3 install filechunkio protobuf \ - 'git+https://github.com/zalando-pg/wal-e.git@ipv6-imds#egg=wal-e[aws,google,swift]' \ + pip3 install protobuf \ 'git+https://github.com/zalando/pg_view.git@master#egg=pg-view' - - # https://github.com/wal-e/wal-e/issues/318 - sed -i 's/^\( for i in range(0,\) num_retries):.*/\1 100):/g' /usr/lib/python3/dist-packages/boto/utils.py else EXTRAS="" fi diff --git a/postgres-appliance/build_scripts/prepare.sh b/postgres-appliance/build_scripts/prepare.sh index 66c2a2cb8..8590e34c9 100644 --- a/postgres-appliance/build_scripts/prepare.sh +++ b/postgres-appliance/build_scripts/prepare.sh @@ -19,8 +19,6 @@ rm -fr /etc/cron.??* truncate --size 0 /etc/crontab if [ "$DEMO" != "true" ]; then - # Required for wal-e - apt-get install -y pv lzop # install etcdctl ETCDVERSION=3.3.27 curl -L https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-"$(dpkg --print-architecture)".tar.gz \ diff --git a/postgres-appliance/launch.sh b/postgres-appliance/launch.sh index 1e3e54f30..07f17474a 100755 --- a/postgres-appliance/launch.sh +++ b/postgres-appliance/launch.sh @@ -51,10 +51,11 @@ chmod -R go-w "$PGROOT" chmod 01777 "$RW_DIR/tmp" chmod 0700 "$PGDATA" +WALG_ENV_DIR="${WALG_ENV_DIR:-$WALE_ENV_DIR}" if [ "$DEMO" = "true" ]; then python3 /scripts/configure_spilo.py patroni pgqd certificate pam-oauth2 elif python3 /scripts/configure_spilo.py all; then - CMD="/scripts/patroni_wait.sh -t 3600 -- envdir $WALE_ENV_DIR /scripts/postgres_backup.sh $PGDATA" + CMD="/scripts/patroni_wait.sh -t 3600 -- envdir $WALG_ENV_DIR /scripts/postgres_backup.sh $PGDATA" if [ "$(id -u)" = "0" ]; then su postgres -c "PATH=$PATH $CMD" & else diff --git a/postgres-appliance/major_upgrade/inplace_upgrade.py b/postgres-appliance/major_upgrade/inplace_upgrade.py index 0390cfbd4..d8c722ffc 100644 --- a/postgres-appliance/major_upgrade/inplace_upgrade.py +++ b/postgres-appliance/major_upgrade/inplace_upgrade.py @@ -21,7 +21,7 @@ RSYNC_PORT = 5432 -def patch_wale_prefix(value, new_version): +def patch_walg_prefix(value, new_version): from spilo_commons import is_valid_pg_version if '/spilo/' in value and '/wal/' in value: # path crafted in the configure_spilo.py? @@ -51,20 +51,20 @@ def update_configs(new_version): write_patroni_config(config, True) - # update wal-e/wal-g envdir files + # update wal-g envdir files restore_command = shlex.split(config['postgresql'].get('recovery_conf', {}).get('restore_command', '')) if len(restore_command) > 6 and restore_command[0] == 'envdir': envdir = restore_command[1] try: for name in os.listdir(envdir): - # len('WALE__PREFIX') = 12 - if len(name) > 12 and name.endswith('_PREFIX') and name[:5] in ('WALE_', 'WALG_'): + # len('WALG__PREFIX') = 12 + if len(name) > 12 and name.endswith('_PREFIX') and name.startswith('WALG_'): name = os.path.join(envdir, name) try: with open(name) as f: value = f.read().strip() - new_value = patch_wale_prefix(value, new_version) + new_value = patch_walg_prefix(value, new_version) if new_value != value: write_file(new_value, name, True) except Exception as e: diff --git a/postgres-appliance/scripts/callback_aws.py b/postgres-appliance/scripts/callback_aws.py index 7b46c618d..89300f824 100755 --- a/postgres-appliance/scripts/callback_aws.py +++ b/postgres-appliance/scripts/callback_aws.py @@ -1,54 +1,43 @@ #!/usr/bin/env python -import boto.ec2 -import boto.utils +from botocore.config import Config +import boto3 import logging import os import sys -import time +import requests logger = logging.getLogger(__name__) LEADER_TAG_VALUE = os.environ.get('AWS_LEADER_TAG_VALUE', 'master') -def retry(func): - def wrapped(*args, **kwargs): - count = 0 - while True: - try: - return func(*args, **kwargs) - except boto.exception.BotoServerError as e: - if count >= 10 or str(e.error_code) not in ('Throttling', 'RequestLimitExceeded'): - raise - logger.info('Throttling AWS API requests...') - time.sleep(2 ** count * 0.5) - count += 1 - - return wrapped - - def get_instance_metadata(): - return boto.utils.get_instance_identity()['document'] + response = requests.put( + url='http://169.254.169.254/latest/api/token', # AWS EC2 metadata service endpoint to get a token + headers={'X-aws-ec2-metadata-token-ttl-seconds': '60'} + ) + token = response.text + instance_identity = requests.get( + url='http://169.254.169.254/latest/dynamic/instance-identity/document', + headers={'X-aws-ec2-metadata-token': token} + ) + return instance_identity.json() -@retry def associate_address(ec2, allocation_id, instance_id): - return ec2.associate_address(instance_id=instance_id, allocation_id=allocation_id, allow_reassociation=True) + return ec2.associate_address(InstanceId=instance_id, AllocationId=allocation_id, AllowReassociation=True) -@retry def tag_resource(ec2, resource_id, tags): - return ec2.create_tags([resource_id], tags) + return ec2.create_tags(Resources=[resource_id], Tags=tags) -@retry def list_volumes(ec2, instance_id): - return ec2.get_all_volumes(filters={'attachment.instance-id': instance_id}) + return ec2.describe_volumes(Filters=[{'Name': 'attachment.instance-id', 'Values': [instance_id]}]) -@retry def get_instance(ec2, instance_id): - return ec2.get_only_instances([instance_id])[0] + return ec2.describe_instances(InstanceIds=[instance_id])['Reservations'][0]['Instances'][0] def main(): @@ -65,30 +54,35 @@ def main(): instance_id = metadata['instanceId'] - ec2 = boto.ec2.connect_to_region(metadata['region']) + config = Config( + region_name=metadata['region'], + retries={ + 'max_attempts': 10, + 'mode': 'standard' + } + ) + ec2 = boto3.client('ec2', config=config) if argc == 5 and role in ('primary', 'standby_leader') and action in ('on_start', 'on_role_change'): associate_address(ec2, sys.argv[1], instance_id) instance = get_instance(ec2, instance_id) - tags = {'Role': LEADER_TAG_VALUE if role == 'primary' else role} + tags = [{'Key': 'Role', 'Value': LEADER_TAG_VALUE if role == 'primary' else role}] tag_resource(ec2, instance_id, tags) - tags.update({'Instance': instance_id}) + tags.append({'Key': 'Instance', 'Value': instance_id}) volumes = list_volumes(ec2, instance_id) - for v in volumes: - if 'Name' in v.tags: + for v in volumes['Volumes']: + if any(tag['Key'] == 'Name' for tag in v.get('Tags', [])): tags_to_update = tags else: - if v.attach_data.device == instance.root_device_name: - volume_device = 'root' - else: - volume_device = 'data' - tags_to_update = dict(tags, Name='spilo_{}_{}'.format(cluster, volume_device)) + for attachment in v['Attachments']: + volume_device = 'root' if attachment['Device'] == instance.get('RootDeviceName') else 'data' + tags_to_update = tags + [{'Key': 'Name', 'Value': 'spilo_{}_{}'.format(cluster, volume_device)}] - tag_resource(ec2, v.id, tags_to_update) + tag_resource(ec2, v.get('VolumeId'), tags_to_update) if __name__ == '__main__': diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index afb2f2929..783020f2c 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -34,12 +34,12 @@ USE_KUBERNETES = os.environ.get('KUBERNETES_SERVICE_HOST') is not None KUBERNETES_DEFAULT_LABELS = '{"application": "spilo"}' PATRONI_DCS = ('kubernetes', 'zookeeper', 'exhibitor', 'consul', 'etcd3', 'etcd') -AUTO_ENABLE_WALG_RESTORE = ('WAL_S3_BUCKET', 'WALE_S3_PREFIX', 'WALG_S3_PREFIX', 'WALG_AZ_PREFIX', 'WALG_SSH_PREFIX') +AUTO_ENABLE_WALG_RESTORE = ('WAL_S3_BUCKET', 'WALG_S3_PREFIX', 'WALG_AZ_PREFIX', 'WALG_SSH_PREFIX') WALG_SSH_NAMES = ['WALG_SSH_PREFIX', 'SSH_PRIVATE_KEY_PATH', 'SSH_USERNAME', 'SSH_PORT'] def parse_args(): - sections = ['all', 'patroni', 'pgqd', 'certificate', 'wal-e', 'crontab', + sections = ['all', 'patroni', 'pgqd', 'certificate', 'wal-g', 'crontab', 'pam-oauth2', 'pgbouncer', 'bootstrap', 'standby-cluster', 'log'] argp = argparse.ArgumentParser(description='Configures Spilo', epilog="Choose from the following sections:\n\t{}".format('\n\t'.join(sections)), @@ -174,14 +174,14 @@ def deep_update(a, b): {{#STANDBY_CLUSTER}} standby_cluster: create_replica_methods: - {{#STANDBY_WITH_WALE}} + {{#STANDBY_WITH_WALG}} - bootstrap_standby_with_wale - {{/STANDBY_WITH_WALE}} + {{/STANDBY_WITH_WALG}} - basebackup_fast_xlog - {{#STANDBY_WITH_WALE}} - restore_command: envdir "{{STANDBY_WALE_ENV_DIR}}" timeout "{{WAL_RESTORE_TIMEOUT}}" + {{#STANDBY_WITH_WALG}} + restore_command: envdir "{{STANDBY_WALG_ENV_DIR}}" timeout "{{WAL_RESTORE_TIMEOUT}}" /scripts/restore_command.sh "%f" "%p" - {{/STANDBY_WITH_WALE}} + {{/STANDBY_WITH_WALG}} {{#STANDBY_HOST}} host: {{STANDBY_HOST}} {{/STANDBY_HOST}} @@ -225,13 +225,13 @@ def deep_update(a, b): autovacuum_max_workers: 5 autovacuum_vacuum_scale_factor: 0.05 autovacuum_analyze_scale_factor: 0.02 - {{#CLONE_WITH_WALE}} - method: clone_with_wale - clone_with_wale: - command: envdir "{{CLONE_WALE_ENV_DIR}}" python3 /scripts/clone_with_wale.py + {{#CLONE_WITH_WALG}} + method: clone_with_walg + clone_with_walg: + command: envdir "{{CLONE_WALG_ENV_DIR}}" python3 /scripts/clone_with_walg.py --recovery-target-time="{{CLONE_TARGET_TIME}}" recovery_conf: - restore_command: envdir "{{CLONE_WALE_ENV_DIR}}" timeout "{{WAL_RESTORE_TIMEOUT}}" + restore_command: envdir "{{CLONE_WALG_ENV_DIR}}" timeout "{{WAL_RESTORE_TIMEOUT}}" /scripts/restore_command.sh "%f" "%p" recovery_target_timeline: "{{CLONE_TARGET_TIMELINE}}" {{#USE_PAUSE_AT_RECOVERY_TARGET}} @@ -246,7 +246,7 @@ def deep_update(a, b): {{^CLONE_TARGET_INCLUSIVE}} recovery_target_inclusive: false {{/CLONE_TARGET_INCLUSIVE}} - {{/CLONE_WITH_WALE}} + {{/CLONE_WITH_WALG}} {{#CLONE_WITH_BASEBACKUP}} method: clone_with_basebackup clone_with_basebackup: @@ -347,11 +347,11 @@ def deep_update(a, b): - hostssl all all all md5 {{/ALLOW_NOSSL}} - {{#USE_WALE}} + {{#USE_WALG}} recovery_conf: - restore_command: envdir "{{WALE_ENV_DIR}}" timeout "{{WAL_RESTORE_TIMEOUT}}" + restore_command: envdir "{{WALG_ENV_DIR}}" timeout "{{WAL_RESTORE_TIMEOUT}}" /scripts/restore_command.sh "%f" "%p" - {{/USE_WALE}} + {{/USE_WALG}} authentication: superuser: username: {{PGUSER_SUPERUSER}} @@ -369,29 +369,29 @@ def deep_update(a, b): on_role_change: '/scripts/on_role_change.sh {{HUMAN_ROLE}} true' {{/CALLBACK_SCRIPT}} create_replica_method: - {{#USE_WALE}} - - wal_e - {{/USE_WALE}} + {{#USE_WALG}} + - wal_g + {{/USE_WALG}} - basebackup_fast_xlog - {{#USE_WALE}} - wal_e: - command: envdir {{WALE_ENV_DIR}} bash /scripts/wale_restore.sh - threshold_megabytes: {{WALE_BACKUP_THRESHOLD_MEGABYTES}} - threshold_backup_size_percentage: {{WALE_BACKUP_THRESHOLD_PERCENTAGE}} + {{#USE_WALG}} + wal_g: + command: envdir {{WALG_ENV_DIR}} bash /scripts/walg_restore.sh + threshold_megabytes: {{WALG_BACKUP_THRESHOLD_MEGABYTES}} + threshold_backup_size_percentage: {{WALG_BACKUP_THRESHOLD_PERCENTAGE}} retries: 2 no_leader: 1 - {{/USE_WALE}} + {{/USE_WALG}} basebackup_fast_xlog: command: /scripts/basebackup.sh retries: 2 -{{#STANDBY_WITH_WALE}} +{{#STANDBY_WITH_WALG}} bootstrap_standby_with_wale: - command: envdir "{{STANDBY_WALE_ENV_DIR}}" bash /scripts/wale_restore.sh - threshold_megabytes: {{WALE_BACKUP_THRESHOLD_MEGABYTES}} - threshold_backup_size_percentage: {{WALE_BACKUP_THRESHOLD_PERCENTAGE}} + command: envdir "{{STANDBY_WALG_ENV_DIR}}" bash /scripts/walg_restore.sh + threshold_megabytes: {{WALG_BACKUP_THRESHOLD_MEGABYTES}} + threshold_backup_size_percentage: {{WALG_BACKUP_THRESHOLD_PERCENTAGE}} retries: 2 no_leader: 1 -{{/STANDBY_WITH_WALE}} +{{/STANDBY_WITH_WALG}} ''' @@ -409,7 +409,15 @@ def get_provider(): try: logging.info("Figuring out my environment (Google? AWS? Openstack? Local?)") - r = requests.get('http://169.254.169.254', timeout=2) + response = requests.put( + url='http://169.254.169.254/latest/api/token', + headers={'X-aws-ec2-metadata-token-ttl-seconds': '60'} + ) + token = response.text + r = requests.get( + url='http://169.254.169.254', + headers={'X-aws-ec2-metadata-token': token} + ) if r.headers.get('Metadata-Flavor', '') == 'Google': return PROVIDER_GOOGLE else: @@ -422,7 +430,10 @@ def get_provider(): return PROVIDER_OPENSTACK # is accessible from both AWS and Openstack, Possiblity of misidentification if previous try fails - r = requests.get('http://169.254.169.254/latest/meta-data/ami-id') + r = requests.get( + url='http://169.254.169.254/latest/meta-data/ami-id', + headers={'X-aws-ec2-metadata-token': token} + ) return PROVIDER_AWS if r.ok else PROVIDER_UNSUPPORTED except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): logging.info("Could not connect to 169.254.169.254, assuming local Docker setup") @@ -474,32 +485,21 @@ def get_instance_metadata(provider): return metadata -def set_extended_wale_placeholders(placeholders, prefix): - """ checks that enough parameters are provided to configure cloning or standby with WAL-E """ +def set_extended_walg_placeholders(placeholders, prefix): + """ checks that enough parameters are provided to configure cloning or standby with WAL-G """ for name in ('S3', 'GS', 'GCS', 'SWIFT', 'AZ'): - if placeholders.get('{0}WALE_{1}_PREFIX'.format(prefix, name)) or\ - name in ('S3', 'GS', 'AZ') and placeholders.get('{0}WALG_{1}_PREFIX'.format(prefix, name)) or\ + if placeholders.get('{0}WALG_{1}_PREFIX'.format(prefix, name)) or\ placeholders.get('{0}WAL_{1}_BUCKET'.format(prefix, name)) and placeholders.get(prefix + 'SCOPE'): break else: return False scope = placeholders.get(prefix + 'SCOPE') dirname = 'env-' + prefix[:-1].lower() + ('-' + scope if scope else '') - placeholders[prefix + 'WALE_ENV_DIR'] = os.path.join(placeholders['RW_DIR'], 'etc', 'wal-e.d', dirname) - placeholders[prefix + 'WITH_WALE'] = True + placeholders[prefix + 'WALG_ENV_DIR'] = os.path.join(placeholders['RW_DIR'], 'etc', 'wal-e.d', dirname) + placeholders[prefix + 'WITH_WALG'] = True return name -def set_walg_placeholders(placeholders, prefix=''): - walg_supported = any(placeholders.get(prefix + n) for n in AUTO_ENABLE_WALG_RESTORE + - ('WAL_GS_BUCKET', 'WALE_GS_PREFIX', 'WALG_GS_PREFIX')) - default = placeholders.get('USE_WALG', False) - placeholders.setdefault(prefix + 'USE_WALG', default) - for name in ('USE_WALG_BACKUP', 'USE_WALG_RESTORE'): - value = str(placeholders.get(prefix + name, placeholders[prefix + 'USE_WALG'])).lower() - placeholders[prefix + name] = 'true' if value == 'true' and walg_supported else None - - def get_listen_ip(): """ Get IP to listen on for things that don't natively support detecting IPv4/IPv6 dualstack """ def has_dual_stack(): @@ -524,7 +524,16 @@ def has_dual_stack(): def get_placeholders(provider): - placeholders = dict(os.environ) + placeholders = {} + for key, value in os.environ.items(): + if "WALE" in key: + new_key = key.replace("WALE", "WALG") # backward compatibility + if new_key in os.environ: + # skip, because a real WALG env already exists + continue + placeholders[new_key] = value + else: + placeholders[key] = value placeholders.setdefault('PGHOME', os.path.expanduser('~')) placeholders.setdefault('APIPORT', '8008') @@ -532,7 +541,7 @@ def get_placeholders(provider): placeholders.setdefault('BACKUP_NUM_TO_RETAIN', '5') placeholders.setdefault('CRONTAB', '[]') placeholders.setdefault('PGROOT', os.path.join(placeholders['PGHOME'], 'pgroot')) - placeholders.setdefault('WALE_TMPDIR', os.path.abspath(os.path.join(placeholders['PGROOT'], '../tmp'))) + placeholders.setdefault('WALG_TMPDIR', os.path.abspath(os.path.join(placeholders['PGROOT'], '../tmp'))) placeholders.setdefault('PGDATA', os.path.join(placeholders['PGROOT'], 'pgdata')) placeholders.setdefault('HUMAN_ROLE', 'zalandos') placeholders.setdefault('PGUSER_STANDBY', 'standby') @@ -555,8 +564,8 @@ def get_placeholders(provider): placeholders.setdefault('SSL_RESTAPI_CA_FILE', '') placeholders.setdefault('SSL_RESTAPI_CERTIFICATE_FILE', '') placeholders.setdefault('SSL_RESTAPI_PRIVATE_KEY_FILE', '') - placeholders.setdefault('WALE_BACKUP_THRESHOLD_MEGABYTES', 102400) - placeholders.setdefault('WALE_BACKUP_THRESHOLD_PERCENTAGE', 30) + placeholders.setdefault('WALG_BACKUP_THRESHOLD_MEGABYTES', 102400) + placeholders.setdefault('WALG_BACKUP_THRESHOLD_PERCENTAGE', 30) placeholders.setdefault('INITDB_LOCALE', 'en_US') placeholders.setdefault('CLONE_TARGET_TIMELINE', 'latest') # if Kubernetes is defined as a DCS, derive the namespace from the POD_NAMESPACE, if not set explicitely. @@ -569,8 +578,9 @@ def get_placeholders(provider): if placeholders['NAMESPACE'] not in ('default', '') else '') placeholders.setdefault('WAL_BUCKET_SCOPE_SUFFIX', '') placeholders.setdefault('WAL_RESTORE_TIMEOUT', '0') - placeholders.setdefault('WALE_ENV_DIR', os.path.join(placeholders['RW_DIR'], 'etc', 'wal-e.d', 'env')) - placeholders.setdefault('USE_WALE', False) + # the env dir path is still called "wal-e.d" for backwards compatibility: many existing deployments, scripts, + # or manifests expect this path, even though wal-e itself is not used (wal-g reads env vars from here too) + placeholders.setdefault('WALG_ENV_DIR', os.path.join(placeholders['RW_DIR'], 'etc', 'wal-e.d', 'env')) cpu_count = str(min(psutil.cpu_count(), 10)) placeholders.setdefault('WALG_DOWNLOAD_CONCURRENCY', cpu_count) placeholders.setdefault('WALG_UPLOAD_CONCURRENCY', cpu_count) @@ -588,7 +598,7 @@ def get_placeholders(provider): placeholders.setdefault('KUBERNETES_BOOTSTRAP_LABELS', '{}') placeholders.setdefault('USE_PAUSE_AT_RECOVERY_TARGET', False) placeholders.setdefault('CLONE_METHOD', '') - placeholders.setdefault('CLONE_WITH_WALE', '') + placeholders.setdefault('CLONE_WITH_WALG', '') placeholders.setdefault('CLONE_WITH_BASEBACKUP', '') placeholders.setdefault('CLONE_TARGET_TIME', '') placeholders.setdefault('CLONE_TARGET_INCLUSIVE', True) @@ -608,18 +618,17 @@ def get_placeholders(provider): else: placeholders['LOG_SHIP_HOURLY'] = '' - # see comment for wal-e bucket prefix + # use namespaces to set WAL bucket prefix scope naming the folder namespace-clustername for non-default namespace placeholders.setdefault('LOG_BUCKET_SCOPE_PREFIX', '{0}-'.format(placeholders['NAMESPACE']) if placeholders['NAMESPACE'] not in ('default', '') else '') - if placeholders['CLONE_METHOD'] == 'CLONE_WITH_WALE': + placeholders['CLONE_METHOD'] = placeholders['CLONE_METHOD'].replace('WALE', 'WALG') # backwards compatibility + if placeholders['CLONE_METHOD'] == 'CLONE_WITH_WALG': # modify placeholders and take care of error cases - name = set_extended_wale_placeholders(placeholders, 'CLONE_') + name = set_extended_walg_placeholders(placeholders, 'CLONE_') if name is False: - logging.warning('Cloning with WAL-E is only possible when CLONE_WALE_*_PREFIX ' - 'or CLONE_WALG_*_PREFIX or CLONE_WAL_*_BUCKET and CLONE_SCOPE are set.') - elif name == 'S3': - placeholders.setdefault('CLONE_USE_WALG', 'true') + logging.warning('Cloning with WAL-G is only possible when CLONE_WALG_*_PREFIX ' + 'or CLONE_WAL_*_BUCKET and CLONE_SCOPE are set.') elif placeholders['CLONE_METHOD'] == 'CLONE_WITH_BASEBACKUP': clone_scope = placeholders.get('CLONE_SCOPE') if clone_scope and placeholders.get('CLONE_HOST') \ @@ -632,14 +641,13 @@ def get_placeholders(provider): logging.warning("Clone method is set to basebackup, but no 'CLONE_SCOPE' " "or 'CLONE_HOST' or 'CLONE_USER' or 'CLONE_PASSWORD' specified") else: - if set_extended_wale_placeholders(placeholders, 'STANDBY_') == 'S3': - placeholders.setdefault('STANDBY_USE_WALG', 'true') + set_extended_walg_placeholders(placeholders, 'STANDBY_') - placeholders.setdefault('STANDBY_WITH_WALE', '') + placeholders.setdefault('STANDBY_WITH_WALG', '') placeholders.setdefault('STANDBY_HOST', '') placeholders.setdefault('STANDBY_PORT', '') placeholders.setdefault('STANDBY_PRIMARY_SLOT_NAME', '') - placeholders.setdefault('STANDBY_CLUSTER', placeholders['STANDBY_WITH_WALE'] or placeholders['STANDBY_HOST']) + placeholders.setdefault('STANDBY_CLUSTER', placeholders['STANDBY_WITH_WALG'] or placeholders['STANDBY_HOST']) if provider == PROVIDER_AWS and not USE_KUBERNETES: # AWS specific callback to tag the instances with roles @@ -647,17 +655,9 @@ def get_placeholders(provider): if placeholders.get('EIP_ALLOCATION'): placeholders['CALLBACK_SCRIPT'] += ' ' + placeholders['EIP_ALLOCATION'] - if any(placeholders.get(n) for n in AUTO_ENABLE_WALG_RESTORE): - placeholders.setdefault('USE_WALG_RESTORE', 'true') - if placeholders.get('WALG_AZ_PREFIX'): - placeholders.setdefault('USE_WALG_BACKUP', 'true') - if all(placeholders.get(n) for n in WALG_SSH_NAMES): - placeholders.setdefault('USE_WALG_BACKUP', 'true') - set_walg_placeholders(placeholders) - - placeholders['USE_WALE'] = any(placeholders.get(n) for n in AUTO_ENABLE_WALG_RESTORE + - ('WAL_SWIFT_BUCKET', 'WALE_SWIFT_PREFIX', 'WAL_GCS_BUCKET', - 'WAL_GS_BUCKET', 'WALE_GS_PREFIX', 'WALG_GS_PREFIX')) + # check if we have enough parameters to enable WAL-G + placeholders['USE_WALG'] = any(placeholders.get(n) for n in AUTO_ENABLE_WALG_RESTORE + + ('WAL_SWIFT_BUCKET', 'WAL_GS_BUCKET', 'WALG_GS_PREFIX')) if placeholders.get('WALG_BACKUP_FROM_REPLICA'): placeholders['WALG_BACKUP_FROM_REPLICA'] = str(placeholders['WALG_BACKUP_FROM_REPLICA']).lower() @@ -669,10 +669,9 @@ def get_placeholders(provider): placeholders.setdefault('postgresql', {}) placeholders['postgresql'].setdefault('parameters', {}) - placeholders['WALE_BINARY'] = 'wal-g' if placeholders.get('USE_WALG_BACKUP') == 'true' else 'wal-e' placeholders['postgresql']['parameters']['archive_command'] = \ - 'envdir "{WALE_ENV_DIR}" {WALE_BINARY} wal-push "%p"'.format(**placeholders) \ - if placeholders['USE_WALE'] else '/bin/true' + 'envdir "{WALG_ENV_DIR}" wal-g wal-push "%p"'.format(**placeholders) \ + if placeholders['USE_WALG'] else '/bin/true' cgroup_memory_limit_path = '/sys/fs/cgroup/memory/memory.limit_in_bytes' cgroup_v2_memory_limit_path = '/sys/fs/cgroup/memory.max' @@ -833,114 +832,104 @@ def write_log_environment(placeholders): write_file(log_env[var], os.path.join(log_env['LOG_ENV_DIR'], var), True) -def write_wale_environment(placeholders, prefix, overwrite): - s3_names = ['WALE_S3_PREFIX', 'WALG_S3_PREFIX', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', - 'WALE_S3_ENDPOINT', 'AWS_ENDPOINT', 'AWS_REGION', 'AWS_INSTANCE_PROFILE', 'WALE_DISABLE_S3_SSE', +def write_walg_environment(placeholders, prefix, overwrite): + s3_names = ['WALG_S3_PREFIX', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', + 'WALG_S3_ENDPOINT', 'AWS_ENDPOINT', 'AWS_REGION', 'AWS_INSTANCE_PROFILE', 'WALG_S3_SSE_KMS_ID', 'WALG_S3_SSE', 'WALG_DISABLE_S3_SSE', 'AWS_S3_FORCE_PATH_STYLE', 'AWS_ROLE_ARN', 'AWS_WEB_IDENTITY_TOKEN_FILE', 'AWS_STS_REGIONAL_ENDPOINTS'] azure_names = ['WALG_AZ_PREFIX', 'AZURE_STORAGE_ACCOUNT', 'WALG_AZURE_BUFFER_SIZE', 'WALG_AZURE_MAX_BUFFERS', 'AZURE_ENVIRONMENT_NAME'] azure_auth_names = ['AZURE_STORAGE_ACCESS_KEY', 'AZURE_STORAGE_SAS_TOKEN', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', 'AZURE_TENANT_ID'] - gs_names = ['WALE_GS_PREFIX', 'WALG_GS_PREFIX', 'GOOGLE_APPLICATION_CREDENTIALS'] - swift_names = ['WALE_SWIFT_PREFIX', 'SWIFT_AUTHURL', 'SWIFT_TENANT', 'SWIFT_TENANT_ID', 'SWIFT_USER', + gs_names = ['WALG_GS_PREFIX', 'GOOGLE_APPLICATION_CREDENTIALS'] + swift_names = ['WALG_SWIFT_PREFIX', 'SWIFT_AUTHURL', 'SWIFT_TENANT', 'SWIFT_TENANT_ID', 'SWIFT_USER', 'SWIFT_USER_ID', 'SWIFT_USER_DOMAIN_NAME', 'SWIFT_USER_DOMAIN_ID', 'SWIFT_PASSWORD', 'SWIFT_AUTH_VERSION', 'SWIFT_ENDPOINT_TYPE', 'SWIFT_REGION', 'SWIFT_DOMAIN_NAME', 'SWIFT_DOMAIN_ID', 'SWIFT_PROJECT_NAME', 'SWIFT_PROJECT_ID', 'SWIFT_PROJECT_DOMAIN_NAME', 'SWIFT_PROJECT_DOMAIN_ID'] ssh_names = WALG_SSH_NAMES walg_names = ['WALG_DELTA_MAX_STEPS', 'WALG_DELTA_ORIGIN', 'WALG_DOWNLOAD_CONCURRENCY', 'WALG_UPLOAD_CONCURRENCY', 'WALG_UPLOAD_DISK_CONCURRENCY', 'WALG_DISK_RATE_LIMIT', - 'WALG_NETWORK_RATE_LIMIT', 'WALG_COMPRESSION_METHOD', 'USE_WALG_BACKUP', - 'USE_WALG_RESTORE', 'WALG_BACKUP_COMPRESSION_METHOD', 'WALG_BACKUP_FROM_REPLICA', + 'WALG_NETWORK_RATE_LIMIT', 'WALG_COMPRESSION_METHOD', + 'WALG_BACKUP_COMPRESSION_METHOD', 'WALG_BACKUP_FROM_REPLICA', 'WALG_SENTINEL_USER_DATA', 'WALG_PREVENT_WAL_OVERWRITE', 'WALG_S3_CA_CERT_FILE', 'WALG_LIBSODIUM_KEY', 'WALG_LIBSODIUM_KEY_PATH', 'WALG_LIBSODIUM_KEY_TRANSFORM', 'WALG_PGP_KEY', 'WALG_PGP_KEY_PATH', 'WALG_PGP_KEY_PASSPHRASE', 'no_proxy', 'http_proxy', 'https_proxy'] aws_imds_names = ['AWS_EC2_METADATA_SERVICE_ENDPOINT', 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE'] - wale = defaultdict(lambda: '') - for name in ['PGVERSION', 'PGPORT', 'WALE_ENV_DIR', 'SCOPE', 'WAL_BUCKET_SCOPE_PREFIX', 'WAL_BUCKET_SCOPE_SUFFIX', - 'WAL_S3_BUCKET', 'WAL_GCS_BUCKET', 'WAL_GS_BUCKET', 'WAL_SWIFT_BUCKET', 'BACKUP_NUM_TO_RETAIN', + walg = defaultdict(lambda: '') + for name in ['PGVERSION', 'PGPORT', 'WALG_ENV_DIR', 'SCOPE', 'WAL_BUCKET_SCOPE_PREFIX', 'WAL_BUCKET_SCOPE_SUFFIX', + 'WAL_S3_BUCKET', 'WAL_GS_BUCKET', 'WAL_SWIFT_BUCKET', 'BACKUP_NUM_TO_RETAIN', 'ENABLE_WAL_PATH_COMPAT'] + s3_names + swift_names + gs_names + walg_names + azure_names + \ azure_auth_names + ssh_names: - wale[name] = placeholders.get(prefix + name, '') + walg[name] = placeholders.get(prefix + name, '') - if wale.get('WAL_S3_BUCKET') or wale.get('WALE_S3_PREFIX') or wale.get('WALG_S3_PREFIX'): - wale_endpoint = wale.pop('WALE_S3_ENDPOINT', None) - aws_endpoint = wale.pop('AWS_ENDPOINT', None) - aws_region = wale.pop('AWS_REGION', None) + if walg.get('WAL_S3_BUCKET') or walg.get('WALG_S3_PREFIX'): + walg_endpoint = walg.pop('WALG_S3_ENDPOINT', None) + aws_endpoint = walg.pop('AWS_ENDPOINT', None) + aws_region = walg.pop('AWS_REGION', None) - # for S3-compatible storage we want to specify WALE_S3_ENDPOINT and AWS_ENDPOINT, but not AWS_REGION - if aws_endpoint or wale_endpoint: + # for S3-compatible storage we want to specify WALG_S3_ENDPOINT and AWS_ENDPOINT, but not AWS_REGION + if aws_endpoint or walg_endpoint: convention = 'path' - if not wale_endpoint: - wale_endpoint = aws_endpoint.replace('://', '+path://') + if not walg_endpoint: + walg_endpoint = aws_endpoint.replace('://', '+path://') else: - match = re.match(r'^(\w+)\+(\w+)(://.+)$', wale_endpoint) + match = re.match(r'^(\w+)\+(\w+)(://.+)$', walg_endpoint) if match: convention = match.group(2) else: - logging.warning('Invalid WALE_S3_ENDPOINT, the format is protocol+convention://hostname:port, ' - 'but got %s', wale_endpoint) + logging.warning('Invalid WALG_S3_ENDPOINT, the format is protocol+convention://hostname:port, ' + 'but got %s', walg_endpoint) if not aws_endpoint: - aws_endpoint = match.expand(r'\1\3') if match else wale_endpoint - wale.update(WALE_S3_ENDPOINT=wale_endpoint, AWS_ENDPOINT=aws_endpoint) - for name in ('WALE_DISABLE_S3_SSE', 'WALG_DISABLE_S3_SSE'): - if not wale.get(name): - wale[name] = 'true' - wale['AWS_S3_FORCE_PATH_STYLE'] = 'true' if convention == 'path' else 'false' - if aws_region and wale.get('USE_WALG_BACKUP') == 'true': - wale['AWS_REGION'] = aws_region + aws_endpoint = match.expand(r'\1\3') if match else walg_endpoint + walg.update(WALG_S3_ENDPOINT=walg_endpoint, AWS_ENDPOINT=aws_endpoint) + walg.setdefault('WALG_DISABLE_S3_SSE', 'true') + walg['AWS_S3_FORCE_PATH_STYLE'] = 'true' if convention == 'path' else 'false' + if aws_region: + walg['AWS_REGION'] = aws_region elif not aws_region: # try to determine region from the endpoint or bucket name - name = wale.get('WAL_S3_BUCKET') or wale.get('WALE_S3_PREFIX') + name = walg.get('WAL_S3_BUCKET') or walg.get('WALG_S3_PREFIX') match = re.search(r'.*(\w{2}-\w+-\d)-.*', name) if match: aws_region = match.group(1) else: aws_region = placeholders['instance_data']['zone'][:-1] - wale['AWS_REGION'] = aws_region + walg['AWS_REGION'] = aws_region else: - wale['AWS_REGION'] = aws_region - - if not (wale.get('AWS_SECRET_ACCESS_KEY') and wale.get('AWS_ACCESS_KEY_ID')): - wale['AWS_INSTANCE_PROFILE'] = 'true' + walg['AWS_REGION'] = aws_region - if wale.get('WALE_DISABLE_S3_SSE') and not wale.get('WALG_DISABLE_S3_SSE'): - wale['WALG_DISABLE_S3_SSE'] = wale['WALE_DISABLE_S3_SSE'] + if not (walg.get('AWS_SECRET_ACCESS_KEY') and walg.get('AWS_ACCESS_KEY_ID')): + walg['AWS_INSTANCE_PROFILE'] = 'true' - if wale.get('USE_WALG_BACKUP') and wale.get('WALG_DISABLE_S3_SSE') != 'true' and not wale.get('WALG_S3_SSE'): - wale['WALG_S3_SSE'] = 'AES256' + if walg.get('WALG_DISABLE_S3_SSE') != 'true' and not walg.get('WALG_S3_SSE'): + walg['WALG_S3_SSE'] = 'AES256' # write IMDS env vars for any prefix if defined for name in aws_imds_names: if placeholders.get(name): - wale[name] = placeholders.get(name) + walg[name] = placeholders.get(name) write_envdir_names = s3_names + walg_names + aws_imds_names - elif wale.get('WAL_GCS_BUCKET') or wale.get('WAL_GS_BUCKET') or\ - wale.get('WALE_GCS_PREFIX') or wale.get('WALE_GS_PREFIX') or wale.get('WALG_GS_PREFIX'): - if wale.get('WALE_GCS_PREFIX'): - wale['WALE_GS_PREFIX'] = wale['WALE_GCS_PREFIX'] - elif wale.get('WAL_GCS_BUCKET'): - wale['WAL_GS_BUCKET'] = wale['WAL_GCS_BUCKET'] + elif walg.get('WAL_GS_BUCKET') or walg.get('WALG_GS_PREFIX'): write_envdir_names = gs_names + walg_names - elif wale.get('WAL_SWIFT_BUCKET') or wale.get('WALE_SWIFT_PREFIX'): + elif walg.get('WAL_SWIFT_BUCKET') or walg.get('WALG_SWIFT_BUCKET'): write_envdir_names = swift_names - elif wale.get("WALG_AZ_PREFIX"): + elif walg.get("WALG_AZ_PREFIX"): azure_auth = [] auth_opts = 0 - if wale.get('AZURE_STORAGE_ACCESS_KEY'): + if walg.get('AZURE_STORAGE_ACCESS_KEY'): azure_auth.append('AZURE_STORAGE_ACCESS_KEY') auth_opts += 1 - if wale.get('AZURE_STORAGE_SAS_TOKEN'): + if walg.get('AZURE_STORAGE_SAS_TOKEN'): if auth_opts == 0: azure_auth.append('AZURE_STORAGE_SAS_TOKEN') auth_opts += 1 - if wale.get('AZURE_CLIENT_ID') and wale.get('AZURE_CLIENT_SECRET') and wale.get('AZURE_TENANT_ID'): + if walg.get('AZURE_CLIENT_ID') and walg.get('AZURE_CLIENT_SECRET') and walg.get('AZURE_TENANT_ID'): if auth_opts == 0: azure_auth.extend(['AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', 'AZURE_TENANT_ID']) auth_opts += 1 @@ -953,41 +942,40 @@ def write_wale_environment(placeholders, prefix, overwrite): write_envdir_names = azure_names + azure_auth + walg_names - elif wale.get("WALG_SSH_PREFIX"): + elif walg.get("WALG_SSH_PREFIX"): write_envdir_names = ssh_names + walg_names else: return prefix_env_name = write_envdir_names[0] store_type = prefix_env_name[5:].split('_')[0] - if not wale.get(prefix_env_name): # WALE_*_PREFIX is not defined in the environment - bucket_path = '/spilo/{WAL_BUCKET_SCOPE_PREFIX}{SCOPE}{WAL_BUCKET_SCOPE_SUFFIX}/wal/{PGVERSION}'.format(**wale) + if not walg.get(prefix_env_name): # WALG_*_PREFIX is not defined in the environment + bucket_path = '/spilo/{WAL_BUCKET_SCOPE_PREFIX}{SCOPE}{WAL_BUCKET_SCOPE_SUFFIX}/wal/{PGVERSION}'.format(**walg) prefix_template = '{0}://{{WAL_{1}_BUCKET}}{2}'.format(store_type.lower(), store_type, bucket_path) - wale[prefix_env_name] = prefix_template.format(**wale) + walg[prefix_env_name] = prefix_template.format(**walg) # Set WALG_*_PREFIX for future compatibility - if store_type in ('S3', 'GS') and not wale.get(write_envdir_names[1]): - wale[write_envdir_names[1]] = wale[prefix_env_name] + if store_type in ('S3', 'GS') and not walg.get(write_envdir_names[1]): + walg[write_envdir_names[1]] = walg[prefix_env_name] - if not os.path.exists(wale['WALE_ENV_DIR']): - os.makedirs(wale['WALE_ENV_DIR']) + if not os.path.exists(walg['WALG_ENV_DIR']): + os.makedirs(walg['WALG_ENV_DIR']) - wale['WALE_LOG_DESTINATION'] = 'stderr' - for name in write_envdir_names + ['WALE_LOG_DESTINATION', 'PGPORT'] + ([] if prefix else ['BACKUP_NUM_TO_RETAIN']): - if wale.get(name): - path = os.path.join(wale['WALE_ENV_DIR'], name) - write_file(wale[name], path, overwrite) + walg['WALG_LOG_DESTINATION'] = 'stderr' + for name in write_envdir_names + ['WALG_LOG_DESTINATION', 'PGPORT'] + ([] if prefix else ['BACKUP_NUM_TO_RETAIN']): + if walg.get(name): + path = os.path.join(walg['WALG_ENV_DIR'], name) + write_file(walg[name], path, overwrite) adjust_owner(path, gid=-1) - if not os.path.exists(placeholders['WALE_TMPDIR']): - os.makedirs(placeholders['WALE_TMPDIR']) - os.chmod(placeholders['WALE_TMPDIR'], 0o1777) + if not os.path.exists(placeholders['WALG_TMPDIR']): + os.makedirs(placeholders['WALG_TMPDIR']) + os.chmod(placeholders['WALG_TMPDIR'], 0o1777) - write_file(placeholders['WALE_TMPDIR'], os.path.join(wale['WALE_ENV_DIR'], 'TMPDIR'), True) + write_file(placeholders['WALG_TMPDIR'], os.path.join(walg['WALG_ENV_DIR'], 'TMPDIR'), True) -def update_and_write_wale_configuration(placeholders, prefix, overwrite): - set_walg_placeholders(placeholders, prefix) - write_wale_environment(placeholders, prefix, overwrite) +def update_and_write_walg_configuration(placeholders, prefix, overwrite): + write_walg_environment(placeholders, prefix, overwrite) def write_clone_pgpass(placeholders, overwrite): @@ -1061,8 +1049,8 @@ def write_crontab(placeholders, overwrite): hash_dir = os.path.join(placeholders['RW_DIR'], 'tmp') lines += ['*/5 * * * * {0} /scripts/test_reload_ssl.sh {1}'.format(env, hash_dir)] - if bool(placeholders.get('USE_WALE')): - lines += [('{BACKUP_SCHEDULE} envdir "{WALE_ENV_DIR}" /scripts/postgres_backup.sh' + + if bool(placeholders.get('USE_WALG')): + lines += [('{BACKUP_SCHEDULE} envdir "{WALG_ENV_DIR}" /scripts/postgres_backup.sh' + ' "{PGDATA}"').format(**placeholders)] if bool(placeholders.get('LOG_S3_BUCKET')): @@ -1192,9 +1180,9 @@ def main(): elif section == 'log': if bool(placeholders.get('LOG_S3_BUCKET')): write_log_environment(placeholders) - elif section == 'wal-e': - if placeholders['USE_WALE']: - write_wale_environment(placeholders, '', args['force']) + elif section == 'wal-g': + if placeholders['USE_WALG']: + write_walg_environment(placeholders, '', args['force']) elif section == 'certificate': write_certificates(placeholders, args['force']) write_restapi_certificates(placeholders, args['force']) @@ -1205,18 +1193,18 @@ def main(): elif section == 'pgbouncer': write_pgbouncer_configuration(placeholders, args['force']) elif section == 'bootstrap': - if placeholders['CLONE_WITH_WALE']: - update_and_write_wale_configuration(placeholders, 'CLONE_', args['force']) + if placeholders['CLONE_WITH_WALG']: + update_and_write_walg_configuration(placeholders, 'CLONE_', args['force']) if placeholders['CLONE_WITH_BASEBACKUP']: write_clone_pgpass(placeholders, args['force']) elif section == 'standby-cluster': - if placeholders['STANDBY_WITH_WALE']: - update_and_write_wale_configuration(placeholders, 'STANDBY_', args['force']) + if placeholders['STANDBY_WITH_WALG']: + update_and_write_walg_configuration(placeholders, 'STANDBY_', args['force']) else: raise Exception('Unknown section: {}'.format(section)) # We will abuse non zero exit code as an indicator for the launch.sh that it should not even try to create a backup - sys.exit(int(not placeholders['USE_WALE'])) + sys.exit(int(not placeholders['USE_WALG'])) def escape_pgpass_value(val): diff --git a/postgres-appliance/scripts/postgres_backup.sh b/postgres-appliance/scripts/postgres_backup.sh index 37ce37bc0..ce5850d3a 100755 --- a/postgres-appliance/scripts/postgres_backup.sh +++ b/postgres-appliance/scripts/postgres_backup.sh @@ -23,23 +23,13 @@ else log "ERROR: Recovery state unknown: $IN_RECOVERY" && exit 1 fi -if [[ "$USE_WALG_BACKUP" == "true" ]]; then - readonly WAL_E="wal-g" - [[ -z $WALG_BACKUP_COMPRESSION_METHOD ]] || export WALG_COMPRESSION_METHOD=$WALG_BACKUP_COMPRESSION_METHOD - export PGHOST=/var/run/postgresql -else - readonly WAL_E="wal-e" - - # Ensure we don't have more workes than CPU's - POOL_SIZE=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) - [ "$POOL_SIZE" -gt 4 ] && POOL_SIZE=4 - POOL_SIZE=(--pool-size "$POOL_SIZE") -fi +export WALG_COMPRESSION_METHOD="${WALG_BACKUP_COMPRESSION_METHOD:-$WALE_BACKUP_COMPRESSION_METHOD}" +export PGHOST=/var/run/postgresql # push a new base backup log "producing a new backup" # We reduce the priority of the backup for CPU consumption -nice -n 5 $WAL_E backup-push "$PGDATA" "${POOL_SIZE[@]}" +nice -n 5 wal-g backup-push "$PGDATA" # Collect all backups and sort them by modification time mapfile -t backup_records < <(wal-g backup-list 2>/dev/null | diff --git a/postgres-appliance/scripts/restore_command.sh b/postgres-appliance/scripts/restore_command.sh index a4bb88939..b861c6bdf 100755 --- a/postgres-appliance/scripts/restore_command.sh +++ b/postgres-appliance/scripts/restore_command.sh @@ -5,14 +5,14 @@ if [[ "$ENABLE_WAL_PATH_COMPAT" = "true" ]]; then bash "$(readlink -f "${BASH_SOURCE[0]}")" "$@" exitcode=$? [[ $exitcode = 0 ]] && exit 0 - for wale_env in $(printenv -0 | tr '\n' ' ' | sed 's/\x00/\n/g' | sed -n 's/^\(WAL[EG]_[^=][^=]*_PREFIX\)=.*$/\1/p'); do - suffix=$(basename "${!wale_env}") + for walg_env in $(printenv -0 | tr '\n' ' ' | sed 's/\x00/\n/g' | sed -n 's/^\(WALG_[^=][^=]*_PREFIX\)=.*$/\1/p'); do + suffix=$(basename "${!walg_env}") if [[ -x "/usr/lib/postgresql/$suffix/bin/postgres" ]]; then - prefix=$(dirname "${!wale_env}") + prefix=$(dirname "${!walg_env}") if [[ $prefix =~ /spilo/ ]] && [[ $prefix =~ /wal$ ]]; then - printf -v "$wale_env" "%s" "$prefix" + printf -v "$walg_env" "%s" "$prefix" # shellcheck disable=SC2163 - export "$wale_env" + export "$walg_env" changed_env=true fi fi @@ -34,22 +34,6 @@ readonly wal_fast_source if [[ "$wal_destination" =~ /$wal_filename$ ]]; then # Patroni fetching missing files for pg_rewind export WALG_DOWNLOAD_CONCURRENCY=1 - POOL_SIZE=0 -else - POOL_SIZE=$WALG_DOWNLOAD_CONCURRENCY fi -[[ "$USE_WALG_RESTORE" == "true" ]] && exec wal-g wal-fetch "${wal_filename}" "${wal_destination}" - -[[ $POOL_SIZE -gt 8 ]] && POOL_SIZE=8 - -if [[ -z $WALE_S3_PREFIX ]]; then # non AWS environment? - readonly wale_prefetch_source=${wal_dir}/.wal-e/prefetch/${wal_filename} - if [[ -f $wale_prefetch_source ]]; then - exec mv "${wale_prefetch_source}" "${wal_destination}" - else - exec wal-e wal-fetch -p $POOL_SIZE "${wal_filename}" "${wal_destination}" - fi -else - exec bash /scripts/wal-e-wal-fetch.sh wal-fetch -p $POOL_SIZE "${wal_filename}" "${wal_destination}" -fi +exec wal-g wal-fetch "${wal_filename}" "${wal_destination}" diff --git a/postgres-appliance/scripts/wal-e-wal-fetch.sh b/postgres-appliance/scripts/wal-e-wal-fetch.sh deleted file mode 100755 index c5302aae7..000000000 --- a/postgres-appliance/scripts/wal-e-wal-fetch.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/bash -set -e - -date - -prefetch=8 - -function load_aws_instance_profile() { - local CREDENTIALS_URL=http://169.254.169.254/latest/meta-data/iam/security-credentials/ - local INSTANCE_PROFILE - INSTANCE_PROFILE=$(curl -s "$CREDENTIALS_URL") - # shellcheck source=/dev/null - source <(curl -s "$CREDENTIALS_URL$INSTANCE_PROFILE" | jq -r '"AWS_SECURITY_TOKEN=\"" + .Token + "\"\nAWS_SECRET_ACCESS_KEY=\"" + .SecretAccessKey + "\"\nAWS_ACCESS_KEY_ID=\"" + .AccessKeyId + "\""') -} - -function load_region_from_aws_instance_profile() { - local AZ - AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone) - AWS_REGION=${AZ:0:-1} -} - -function usage() { - echo "Usage: $0 wal-fetch [--prefetch PREFETCH] WAL_SEGMENT WAL_DESTINATION" - exit 1 -} - -while [[ $# -gt 0 ]]; do - case $1 in - --s3-prefix ) - WALE_S3_PREFIX=$2 - shift - ;; - -k|--aws-access-key-id ) - AWS_ACCESS_KEY_ID=$2 - shift - ;; - --aws-instance-profile ) - AWS_INSTANCE_PROFILE=true - ;; - wal-fetch ) - ;; - -p|--prefetch ) - prefetch=$2 - shift - ;; - * ) - PARAMS+=("$1") - ;; - esac - shift -done - -[[ ${#PARAMS[@]} == 2 ]] || usage - -[[ "$AWS_INSTANCE_PROFILE" == "true" ]] && load_aws_instance_profile - -if [[ -z $AWS_SECRET_ACCESS_KEY || -z $AWS_ACCESS_KEY_ID || -z $WALE_S3_PREFIX ]]; then - echo bad environment - exit 1 -fi - -readonly SEGMENT=${PARAMS[-2]} -readonly DESTINATION=${PARAMS[-1]} - -if [[ $WALE_S3_PREFIX =~ ^s3://([^\/]+)(.+) ]]; then - readonly BUCKET=${BASH_REMATCH[1]} - BUCKET_PATH=${BASH_REMATCH[2]} - readonly BUCKET_PATH=${BUCKET_PATH%/} -else - echo bad WALE_S3_PREFIX - exit 1 -fi - -if [[ -n $WALE_S3_ENDPOINT && $WALE_S3_ENDPOINT =~ ^([a-z\+]{2,10}://)?([^:\/?]+) ]]; then - S3_HOST=${BASH_REMATCH[2]} -fi - -if [[ -z $AWS_REGION ]]; then - if [[ -n $WALE_S3_ENDPOINT && $WALE_S3_ENDPOINT =~ ^([a-z\+]{2,10}://)?s3-([^\.]+) ]]; then - AWS_REGION=${BASH_REMATCH[2]} - elif [[ "$AWS_INSTANCE_PROFILE" == "true" ]]; then - load_region_from_aws_instance_profile - fi -fi - -if [[ -z $AWS_REGION ]]; then - echo AWS_REGION is unknown - exit 1 -fi - -if [[ -z $S3_HOST ]]; then - S3_HOST=s3.$AWS_REGION.amazonaws.com -fi - -readonly SERVICE=s3 -readonly REQUEST=aws4_request -readonly HOST=$BUCKET.$S3_HOST -TIME=$(date +%Y%m%dT%H%M%SZ) -readonly TIME -readonly DATE=${TIME%T*} -readonly DRSR="$DATE/$AWS_REGION/$SERVICE/$REQUEST" -readonly EMPTYHASH=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 - -function hmac_sha256() { - echo -en "$2" | openssl dgst -sha256 -mac HMAC -macopt "$1" | sed 's/^.* //' -} - -# Four-step signing key calculation -DATE_KEY=$(hmac_sha256 key:"AWS4$AWS_SECRET_ACCESS_KEY" "$DATE") -readonly DATE_KEY -DATE_REGION_KEY=$(hmac_sha256 "hexkey:$DATE_KEY" "$AWS_REGION") -readonly DATE_REGION_KEY -DATE_REGION_SERVICE_KEY=$(hmac_sha256 "hexkey:$DATE_REGION_KEY" "$SERVICE") -readonly DATE_REGION_SERVICE_KEY -SIGNING_KEY=$(hmac_sha256 "hexkey:$DATE_REGION_SERVICE_KEY" "$REQUEST") -readonly SIGNING_KEY - -if [[ -z $AWS_INSTANCE_PROFILE ]]; then - readonly SIGNED_HEADERS="host;x-amz-content-sha256;x-amz-date" - readonly REQUEST_TOKEN="" - readonly TOKEN_HEADER=() -else - readonly SIGNED_HEADERS="host;x-amz-content-sha256;x-amz-date;x-amz-security-token" - readonly REQUEST_TOKEN="x-amz-security-token:$AWS_SECURITY_TOKEN\n" - readonly TOKEN_HEADER=(-H "x-amz-security-token: $AWS_SECURITY_TOKEN") -fi - -function s3_get() { - local segment=$1 - local destination=$2 - local FILE=$BUCKET_PATH/wal_005/$segment.lzo - local CANONICAL_REQUEST="GET\n$FILE\n\nhost:$HOST\nx-amz-content-sha256:$EMPTYHASH\nx-amz-date:$TIME\n$REQUEST_TOKEN\n$SIGNED_HEADERS\n$EMPTYHASH" - local CANONICAL_REQUEST_HASH - CANONICAL_REQUEST_HASH=$(echo -en "$CANONICAL_REQUEST" | openssl dgst -sha256 | sed 's/^.* //') - local STRING_TO_SIGN="AWS4-HMAC-SHA256\n$TIME\n$DRSR\n$CANONICAL_REQUEST_HASH" - local SIGNATURE - SIGNATURE=$(hmac_sha256 "hexkey:$SIGNING_KEY" "$STRING_TO_SIGN") - - if curl -s "https://$HOST$FILE" "${TOKEN_HEADER[@]}" -H "x-amz-content-sha256: $EMPTYHASH" -H "x-amz-date: $TIME" \ - -H "Authorization: AWS4-HMAC-SHA256 Credential=$AWS_ACCESS_KEY_ID/$DRSR, SignedHeaders=$SIGNED_HEADERS, Signature=$SIGNATURE" \ - | lzop -dc > "$destination" 2> /dev/null && [[ ${PIPESTATUS[0]} == 0 ]]; then - [[ -s $destination ]] && echo "$$ success $FILE" && return 0 - fi - rm -f "$destination" - echo "$$ failed $FILE" - return 1 -} - -function generate_next_segments() { - local num=$1 - - local timeline=${SEGMENT:0:8} - local log=$((16#${SEGMENT:8:8})) - local seg=$((16#${SEGMENT:16:8})) - - while [[ $((num--)) -gt 0 ]]; do - seg=$((seg+1)) - printf "%s%08X%08X\n" "$timeline" $((log+seg/256)) $((seg%256)) - done -} - -function clear_except() { - set +e - for dir in "$PREFETCHDIR"/running/0*; do - item=$(basename "$dir") - if [[ $item =~ ^[0-9A-F]{24}$ ]]; then - [[ " ${PREFETCHES[*]} " =~ \ $item\ ]] || rm -fr "$dir" - fi - done - - for file in "$PREFETCHDIR"/0*; do - item=$(basename "$file") - if [[ $item =~ ^[0-9A-F]{24}$ ]]; then - [[ " ${PREFETCHES[*]} " =~ \ $item\ ]] || rm -f "$file" - fi - done - set -e - return 0 -} - -function try_to_promote_prefetched() { - local prefetched=$PREFETCHDIR/$SEGMENT - [[ -f $prefetched ]] || return 1 - echo "$$ promoting $prefetched" - mv "$prefetched" "$DESTINATION" && clear_except && exit 0 -} - -echo "$$ $SEGMENT" - -PREFETCHDIR=$(dirname "$DESTINATION")/.wal-e/prefetch -readonly PREFETCHDIR -if [[ $prefetch -gt 0 && $SEGMENT =~ ^[0-9A-F]{24}$ ]]; then - mapfile -t PREFETCHES < <(generate_next_segments "$prefetch") - readonly PREFETCHES - for segment in "${PREFETCHES[@]}"; do - running="$PREFETCHDIR/running/$segment" - [[ -d $running || -f $PREFETCHDIR/$segment ]] && continue - - mkdir -p "$running" - ( - trap 'rm -fr $running' QUIT TERM EXIT - TMPFILE=$(mktemp -p "$running") - echo "$$ prefetching $segment" - s3_get "$segment" "$TMPFILE" && mv "$TMPFILE" "$PREFETCHDIR/$segment" - ) & - done - - last_size=0 - while ! try_to_promote_prefetched; do - size=$(du -bs "$PREFETCHDIR/running/$SEGMENT" 2> /dev/null | cut -f1) - if [[ -z $size ]]; then - try_to_promote_prefetched || break - elif [[ $size > $last_size ]]; then - echo "($size > $last_size), sleeping 1" - last_size=$size - sleep 1 - else - echo "size=$size, last_size=$last_size" - break - fi - done - clear_except -fi - -s3_get "$SEGMENT" "$DESTINATION" diff --git a/postgres-appliance/scripts/wale_restore.sh b/postgres-appliance/scripts/walg_restore.sh similarity index 94% rename from postgres-appliance/scripts/wale_restore.sh rename to postgres-appliance/scripts/walg_restore.sh index 4fbcedd01..70768188f 100755 --- a/postgres-appliance/scripts/wale_restore.sh +++ b/postgres-appliance/scripts/walg_restore.sh @@ -33,16 +33,11 @@ done [[ -z $DATA_DIR ]] && exit 1 [[ -z $NO_MASTER && -z "$CONNSTR" ]] && exit 1 -if [[ "$USE_WALG_RESTORE" == "true" ]]; then - readonly WAL_E="wal-g" -else - readonly WAL_E="wal-e" -fi ATTEMPT=0 server_version="-1" while true; do - [[ -z $wal_segment_backup_start ]] && wal_segment_backup_start=$($WAL_E backup-list 2> /dev/null \ + [[ -z $wal_segment_backup_start ]] && wal_segment_backup_start=$(wal-g backup-list 2> /dev/null \ | sed '0,/^\(backup_\)\?name\s*\(last_\)\?modified\s*/d' | sort -bk2 | tail -n1 | awk '{print $3;}' | sed 's/_.*$//') [[ -n "$CONNSTR" && $server_version == "-1" ]] && server_version=$(psql -d "$CONNSTR" -tAc 'show server_version_num' 2> /dev/null || echo "-1") @@ -84,7 +79,7 @@ fi ATTEMPT=0 while true; do - if $WAL_E backup-fetch "$DATA_DIR" LATEST; then + if wal-g backup-fetch "$DATA_DIR" LATEST; then version=$(<"$DATA_DIR/PG_VERSION") [[ "$version" =~ \. ]] && wal_name=xlog || wal_name=wal readonly wal_dir=$DATA_DIR/pg_$wal_name diff --git a/postgres-appliance/tests/docker-compose.yml b/postgres-appliance/tests/docker-compose.yml index f0399ecb2..e8e7952e9 100644 --- a/postgres-appliance/tests/docker-compose.yml +++ b/postgres-appliance/tests/docker-compose.yml @@ -33,8 +33,7 @@ services: AWS_ENDPOINT: &aws_endpoint 'http://minio:9000' AWS_S3_FORCE_PATH_STYLE: &aws_s3_force_path_style 'true' WAL_S3_BUCKET: &bucket testbucket -# USE_WALG: 'true' # wal-e is used and tested by default, wal-g is used automatically for restore in case of S3 - WALE_DISABLE_S3_SSE: &wale_disable_s3_sse 'true' + WALG_DISABLE_S3_SSE: &walg_disable_s3_sse 'true' ETCDCTL_ENDPOINTS: http://etcd:2379 ETCD3_HOST: "etcd:2379" SCOPE: demo @@ -58,7 +57,7 @@ services: CLONE_AWS_SECRET_ACCESS_KEY: *secret_key CLONE_AWS_ENDPOINT: *aws_endpoint CLONE_AWS_S3_FORCE_PATH_STYLE: *aws_s3_force_path_style - CLONE_WALE_DISABLE_S3_SSE: *wale_disable_s3_sse + CLONE_WALG_DISABLE_S3_SSE: *walg_disable_s3_sse hostname: spilo1 container_name: demo-spilo1 diff --git a/postgres-appliance/tests/test_spilo.sh b/postgres-appliance/tests/test_spilo.sh index 2570c0974..66100cbe9 100755 --- a/postgres-appliance/tests/test_spilo.sh +++ b/postgres-appliance/tests/test_spilo.sh @@ -136,8 +136,7 @@ function test_successful_inplace_upgrade_to_14() { } function test_envdir_suffix() { - docker_exec "$1" "cat /run/etc/wal-e.d/env/WALG_S3_PREFIX" | grep -q "$2$" \ - && docker_exec "$1" "cat /run/etc/wal-e.d/env/WALE_S3_PREFIX" | grep -q "$2$" + docker_exec "$1" "cat /run/etc/wal-e.d/env/WALG_S3_PREFIX" | grep -q "$2$" } function test_envdir_updated_to_x() { @@ -166,43 +165,43 @@ function test_pg_upgrade_to_17_check_failed() { ! test_successful_inplace_upgrade_to_17 "$1" } -function start_clone_with_wale_upgrade_container() { +function start_clone_with_walg_upgrade_container() { local ID=${1:-1} docker-compose run \ -e SCOPE=upgrade \ -e PGVERSION=14 \ -e CLONE_SCOPE=demo \ - -e CLONE_METHOD=CLONE_WITH_WALE \ + -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_minute)" \ - -e WALE_BACKUP_THRESHOLD_PERCENTAGE=80 \ + -e WALG_BACKUP_THRESHOLD_PERCENTAGE=80 \ --name "${PREFIX}upgrade$ID" \ -d "spilo$ID" } -function start_clone_with_wale_upgrade_replica_container() { - start_clone_with_wale_upgrade_container 2 +function start_clone_with_walg_upgrade_replica_container() { + start_clone_with_walg_upgrade_container 2 } -function start_clone_with_wale_upgrade_to_17_container() { +function start_clone_with_walg_upgrade_to_17_container() { docker-compose run \ -e SCOPE=upgrade3 \ -e PGVERSION=17 \ -e CLONE_SCOPE=demo \ -e CLONE_PGVERSION=13 \ - -e CLONE_METHOD=CLONE_WITH_WALE \ + -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_minute)" \ --name "${PREFIX}upgrade4" \ -d "spilo3" } -function start_clone_with_wale_17_container() { +function start_clone_with_walg_17_container() { docker-compose run \ -e SCOPE=clone16 \ -e PGVERSION=17 \ -e CLONE_SCOPE=upgrade3 \ -e CLONE_PGVERSION=17 \ - -e CLONE_METHOD=CLONE_WITH_WALE \ + -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_hour)" \ --name "${PREFIX}clone16" \ -d "spilo3" @@ -230,7 +229,7 @@ function start_clone_with_hourly_log_rotation() { -e LOG_SHIP_HOURLY="true" \ -e CLONE_SCOPE=upgrade2 \ -e CLONE_PGVERSION=15 \ - -e CLONE_METHOD=CLONE_WITH_WALE \ + -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_minute)" \ --name "${PREFIX}hourlylogs" \ -d "spilo3" @@ -262,10 +261,10 @@ function verify_hourly_log_rotation() { } # TEST SUITE 1 - In-place major upgrade 13->14->...->17 -# TEST SUITE 2 - Major upgrade 13->17 after wal-e clone (with CLONE_PGVERSION set) -# TEST SUITE 3 - PITR (clone with wal-e) with unreachable target (14+) -# TEST SUITE 4 - Major upgrade 13->14 after wal-e clone (no CLONE_PGVERSION) -# TEST SUITE 5 - Replica bootstrap with wal-e +# TEST SUITE 2 - Major upgrade 13->17 after wal-g clone (with CLONE_PGVERSION set) +# TEST SUITE 3 - PITR (clone with wal-g) with unreachable target (14+) +# TEST SUITE 4 - Major upgrade 13->14 after wal-g clone (no CLONE_PGVERSION) +# TEST SUITE 5 - Replica bootstrap with wal-g # TEST SUITE 6 - Major upgrade 14->15 after clone with basebackup # TEST SUITE 7 - Hourly log rotation function test_spilo() { @@ -289,14 +288,14 @@ function test_spilo() { # TEST SUITE 2 local upgrade3_container - upgrade3_container=$(start_clone_with_wale_upgrade_to_17_container) # SCOPE=upgrade3 PGVERSION=17 CLONE: _SCOPE=demo _PGVERSION=13 _TARGET_TIME= - log_info "[TS2] Started $upgrade3_container for testing major upgrade 13->17 after clone with wal-e" + upgrade3_container=$(start_clone_with_walg_upgrade_to_17_container) # SCOPE=upgrade3 PGVERSION=17 CLONE: _SCOPE=demo _PGVERSION=13 _TARGET_TIME= + log_info "[TS2] Started $upgrade3_container for testing major upgrade 13->17 after clone with wal-g" # TEST SUITE 4 local upgrade_container - upgrade_container=$(start_clone_with_wale_upgrade_container) # SCOPE=upgrade PGVERSION=14 CLONE: _SCOPE=demo _TARGET_TIME= - log_info "[TS4] Started $upgrade_container for testing major upgrade 13->14 after clone with wal-e" + upgrade_container=$(start_clone_with_walg_upgrade_container) # SCOPE=upgrade PGVERSION=14 CLONE: _SCOPE=demo _TARGET_TIME= + log_info "[TS4] Started $upgrade_container for testing major upgrade 13->14 after clone with wal-g" # TEST SUITE 1 @@ -312,8 +311,8 @@ function test_spilo() { run_test test_envdir_updated_to_x 14 # TEST SUITE 2 - log_info "[TS2] Testing in-place major upgrade 13->17 after wal-e clone" - run_test verify_clone_upgrade "$upgrade3_container" "wal-e" 13 17 + log_info "[TS2] Testing in-place major upgrade 13->17 after wal-g clone" + run_test verify_clone_upgrade "$upgrade3_container" "wal-g" 13 17 run_test verify_archive_mode_is_on "$upgrade3_container" wait_backup "$upgrade3_container" @@ -321,8 +320,8 @@ function test_spilo() { # TEST SUITE 3 local clone17_container - clone17_container=$(start_clone_with_wale_17_container) # SCOPE=clone17 CLONE: _SCOPE=upgrade3 _PGVERSION=17 _TARGET_TIME= - log_info "[TS3] Started $clone17_container for testing point-in-time recovery (clone with wal-e) with unreachable target on 14+" + clone17_container=$(start_clone_with_walg_17_container) # SCOPE=clone17 CLONE: _SCOPE=upgrade3 _PGVERSION=17 _TARGET_TIME= + log_info "[TS3] Started $clone17_container for testing point-in-time recovery (clone with wal-g) with unreachable target on 14+" # TEST SUITE 1 @@ -347,8 +346,8 @@ function test_spilo() { # TEST SUITE 4 - log_info "[TS4] Testing in-place major upgrade 13->14 after clone with wal-e" - run_test verify_clone_upgrade "$upgrade_container" "wal-e" 13 14 + log_info "[TS4] Testing in-place major upgrade 13->14 after clone with wal-g" + run_test verify_clone_upgrade "$upgrade_container" "wal-g" 13 14 run_test verify_archive_mode_is_on "$upgrade_container" wait_backup "$upgrade_container" @@ -356,8 +355,8 @@ function test_spilo() { # TEST SUITE 5 local upgrade_replica_container - upgrade_replica_container=$(start_clone_with_wale_upgrade_replica_container) # SCOPE=upgrade - log_info "[TS5] Started $upgrade_replica_container for testing replica bootstrap with wal-e" + upgrade_replica_container=$(start_clone_with_walg_upgrade_replica_container) # SCOPE=upgrade + log_info "[TS5] Started $upgrade_replica_container for testing replica bootstrap with wal-g" # TEST SUITE 6 From 3817421e685395e8bedd0126220f738ee025728a Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:32:08 +0200 Subject: [PATCH 29/41] pg_basebackup server compression (#1155) --- postgres-appliance/scripts/basebackup.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/postgres-appliance/scripts/basebackup.sh b/postgres-appliance/scripts/basebackup.sh index 7c8fc68dc..6e1622eea 100755 --- a/postgres-appliance/scripts/basebackup.sh +++ b/postgres-appliance/scripts/basebackup.sh @@ -95,6 +95,11 @@ else receivewal_pid=$(cat "$WAL_FAST/receivewal.pid") fi +PGVER=$(psql -d "$CONNSTR" -tAc "SELECT pg_catalog.current_setting('server_version_num')::int/10000" || echo 0) +if [[ $PGVER -ge 15 ]]; then + PG_BASEBACKUP_OPTS+=("--compress=server-lz4") +fi + ATTEMPT=0 while [[ $((ATTEMPT++)) -le $RETRIES ]]; do pg_basebackup --pgdata="${DATA_DIR}" "${PG_BASEBACKUP_OPTS[@]}" --dbname="${CONNSTR}" & From c8004eb7300ed948047c307cb9937cc4722226d6 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:52:25 +0100 Subject: [PATCH 30/41] Allow admin selecting from hypopg views (#1162) --- postgres-appliance/scripts/hypopg/after-create.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 postgres-appliance/scripts/hypopg/after-create.sql diff --git a/postgres-appliance/scripts/hypopg/after-create.sql b/postgres-appliance/scripts/hypopg/after-create.sql new file mode 100644 index 000000000..32d5d47cd --- /dev/null +++ b/postgres-appliance/scripts/hypopg/after-create.sql @@ -0,0 +1,2 @@ +GRANT SELECT ON hypopg_hidden_indexes TO admin; +GRANT SELECT ON hypopg_list_indexes TO admin; From a4005762d73375837864a95af96a481a65018a92 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:54:52 +0100 Subject: [PATCH 31/41] Change logic for keeping ts minor versions (#1173) keep at least 5 minor versions, but ensure compatibility with the lowest/oldest PG version (where possible) --- postgres-appliance/build_scripts/base.sh | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index ff885c5e9..dbc384649 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -126,10 +126,32 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-pg-stat-kcache" \ "${EXTRAS[@]}" - # Clean up timescaledb versions except the last 5 minor versions + # Clean up timescaledb versions - keep at least 5 minor versions, but ensure compatibility with the lowest/oldest PG version (where possible) + exclude_patterns=() versions=$(find "/usr/lib/postgresql/$version/lib/" -name 'timescaledb-2.*.so' | sed -rn 's/.*timescaledb-([1-9]+\.[0-9]+\.[0-9]+)\.so$/\1/p' | sort -rV) - latest_minor_versions=$(echo "$versions" | awk -F. '{print $1"."$2}' | uniq | head -n 5) + + # Calculate the number of versions dynamically based on the lowest PG version's latest minor + num_versions=5 + if [ -n "$first_latest_minor" ]; then + minor_versions=$(echo "$versions" | awk -F. '{print $1"."$2}' | uniq) + position=0 + found=0 + while IFS= read -r minor; do + position=$((position + 1)) + if [ "$minor" = "$first_latest_minor" ]; then + found=1 + break + fi + done <<< "$minor_versions" + + # if found, keep max(5, position) versions (so all versions have at least 1 version in common with lowest PG version) + if [ $found -eq 1 ] && [ $position -gt $num_versions ]; then + num_versions=$position + fi + fi + + latest_minor_versions=$(echo "$versions" | awk -F. '{print $1"."$2}' | uniq | head -n "$num_versions") for minor in $latest_minor_versions; do for full_version in $(echo "$versions" | grep "^$minor"); do exclude_patterns+=(! -name timescaledb-"${full_version}".so) @@ -138,6 +160,11 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do done find "/usr/lib/postgresql/$version/lib/" \( -name 'timescaledb-2.*.so' -o -name 'timescaledb-tsl-2.*.so' \) "${exclude_patterns[@]}" -delete + # Save the latest minor version from the first PG version + if [ -z "$first_latest_minor" ]; then + first_latest_minor=$(echo "$latest_minor_versions" | head -n 1) + fi + # Install 3rd party stuff if [ "${TIMESCALEDB_APACHE_ONLY}" != "true" ] && [ "${TIMESCALEDB_TOOLKIT}" = "true" ]; then From fd9eb121e804837ff0163656b9426fd567a93e71 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:22:57 +0100 Subject: [PATCH 32/41] Set SPILO_PROVIDER to local by default (#1172) Co-authored-by: Ida Novindasari --- postgres-appliance/scripts/configure_spilo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index 783020f2c..f260c1304 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -396,7 +396,7 @@ def deep_update(a, b): def get_provider(): - provider = os.environ.get('SPILO_PROVIDER') + provider = os.environ.get('SPILO_PROVIDER', PROVIDER_LOCAL) if provider: if provider in {PROVIDER_AWS, PROVIDER_GOOGLE, PROVIDER_OPENSTACK, PROVIDER_LOCAL}: return provider From 6f7d064d1fb4bd74d4a80c7139da309f9b780bdd Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:05:04 +0100 Subject: [PATCH 33/41] Remove default SPILO_PROVIDER (#1176) Instead, handle PUT IMDS request differently + add timeouts --- postgres-appliance/scripts/configure_spilo.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index f260c1304..f073b2f60 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -396,7 +396,7 @@ def deep_update(a, b): def get_provider(): - provider = os.environ.get('SPILO_PROVIDER', PROVIDER_LOCAL) + provider = os.environ.get('SPILO_PROVIDER') if provider: if provider in {PROVIDER_AWS, PROVIDER_GOOGLE, PROVIDER_OPENSTACK, PROVIDER_LOCAL}: return provider @@ -411,18 +411,23 @@ def get_provider(): logging.info("Figuring out my environment (Google? AWS? Openstack? Local?)") response = requests.put( url='http://169.254.169.254/latest/api/token', - headers={'X-aws-ec2-metadata-token-ttl-seconds': '60'} + headers={'X-aws-ec2-metadata-token-ttl-seconds': '60'}, + timeout=2 ) + if not response.ok: + logging.info("Failed to get IMDS token (status %s), assuming local Docker setup", response.status_code) + return PROVIDER_LOCAL token = response.text r = requests.get( url='http://169.254.169.254', - headers={'X-aws-ec2-metadata-token': token} + headers={'X-aws-ec2-metadata-token': token}, + timeout=2 ) if r.headers.get('Metadata-Flavor', '') == 'Google': return PROVIDER_GOOGLE else: # accessible on Openstack, will fail on AWS - r = requests.get('http://169.254.169.254/openstack/latest/meta_data.json') + r = requests.get('http://169.254.169.254/openstack/latest/meta_data.json', timeout=2) if r.ok: # make sure the response is parsable - https://github.com/Azure/aad-pod-identity/issues/943 and # https://github.com/zalando/spilo/issues/542 @@ -432,7 +437,8 @@ def get_provider(): # is accessible from both AWS and Openstack, Possiblity of misidentification if previous try fails r = requests.get( url='http://169.254.169.254/latest/meta-data/ami-id', - headers={'X-aws-ec2-metadata-token': token} + headers={'X-aws-ec2-metadata-token': token}, + timeout=2 ) return PROVIDER_AWS if r.ok else PROVIDER_UNSUPPORTED except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): From 84ab3860ddbaca5b18dfe8e2b6c124c9d5273ca9 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:21:59 +0100 Subject: [PATCH 34/41] Properly setup clean env for ext build across major versions (#1174) --- postgres-appliance/build_scripts/base.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index dbc384649..19759b39b 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -187,7 +187,8 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do pg_permissions-${PG_PERMISSIONS_COMMIT} \ pg_profile-${PG_PROFILE} \ "${EXTRA_EXTENSIONS[@]}"; do - make -C "$n" USE_PGXS=1 clean install-strip + PATH="/usr/lib/postgresql/$version/bin:$PATH" make -C "$n" USE_PGXS=1 clean + PATH="/usr/lib/postgresql/$version/bin:$PATH" make -C "$n" USE_PGXS=1 install-strip done done From 32fe3cae1ac4bbd8152c44c1d9a997bba2785001 Mon Sep 17 00:00:00 2001 From: Mikkel Oscar Lyderik Larsen Date: Fri, 20 Feb 2026 13:20:49 +0100 Subject: [PATCH 35/41] Update to wal-g v3.0.8 (#1181) https://github.com/wal-g/wal-g/releases/tag/v3.0.8 Signed-off-by: Mikkel Oscar Lyderik Larsen --- postgres-appliance/Dockerfile | 2 +- postgres-appliance/build_scripts/dependencies.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 4f8410b5b..731158472 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -18,7 +18,7 @@ FROM $BASE_IMAGE as dependencies-builder ARG DEMO -ENV WALG_VERSION=v3.0.5 +ENV WALG_VERSION=v3.0.8 COPY build_scripts/dependencies.sh /builddeps/ diff --git a/postgres-appliance/build_scripts/dependencies.sh b/postgres-appliance/build_scripts/dependencies.sh index 65aa28055..2f1ef68cb 100644 --- a/postgres-appliance/build_scripts/dependencies.sh +++ b/postgres-appliance/build_scripts/dependencies.sh @@ -29,9 +29,9 @@ apt-get install -y curl ca-certificates mkdir /builddeps/wal-g if [ "$ARCH" = "amd64" ]; then - PKG_NAME='wal-g-pg-ubuntu-20.04-amd64' + PKG_NAME='wal-g-pg-22.04-amd64' else - PKG_NAME='wal-g-pg-ubuntu-20.04-aarch64' + PKG_NAME='wal-g-pg-22.04-aarch64' fi curl -sL "https://github.com/wal-g/wal-g/releases/download/$WALG_VERSION/$PKG_NAME.tar.gz" \ From f6d8d006234463b8ac111a9b1fdf4aa494070dec Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:44:03 +0100 Subject: [PATCH 36/41] Fix server version request in basebackup.sh (#1184) --- postgres-appliance/scripts/basebackup.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/basebackup.sh b/postgres-appliance/scripts/basebackup.sh index 6e1622eea..d47cd2923 100755 --- a/postgres-appliance/scripts/basebackup.sh +++ b/postgres-appliance/scripts/basebackup.sh @@ -19,6 +19,10 @@ done [[ -z $DATA_DIR || -z "$CONNSTR" || ! $RETRIES =~ ^[1-9]$ ]] && exit 1 +if [[ ! $CONNSTR =~ dbname= ]]; then + CONNSTR="${CONNSTR} dbname=postgres" +fi + if which pg_receivewal &> /dev/null; then PG_RECEIVEWAL=pg_receivewal PG_BASEBACKUP_OPTS=(-X none) @@ -95,7 +99,7 @@ else receivewal_pid=$(cat "$WAL_FAST/receivewal.pid") fi -PGVER=$(psql -d "$CONNSTR" -tAc "SELECT pg_catalog.current_setting('server_version_num')::int/10000" || echo 0) +PGVER=$(psql "$CONNSTR" -tAc "SELECT pg_catalog.current_setting('server_version_num')::int/10000" || echo 0) if [[ $PGVER -ge 15 ]]; then PG_BASEBACKUP_OPTS+=("--compress=server-lz4") fi From 4c2d4b88c27bb290783816d950f8831b393c698b Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:34:53 +0100 Subject: [PATCH 37/41] Update bg_mon, pg_mon, pg_profile, pg_permissions refs (#1185) * Update bg_mon, pg_mon, pg_profile, pg_permissions refs * Use pgdg for set_user and pg_permissions --- postgres-appliance/Dockerfile | 10 ++++------ postgres-appliance/build_scripts/base.sh | 6 ++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 731158472..94add7ffd 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -53,14 +53,12 @@ ARG DEB_PG_SUPPORTED_VERSIONS="$PGOLDVERSIONS $PGVERSION" # Install PostgreSQL, extensions and contribs ENV POSTGIS_VERSION=3.6 \ - BG_MON_COMMIT=7f5887218790b263fe3f42f85f4ddc9c8400b154 \ + BG_MON_COMMIT=a73c6bcd10dfdf9feaf5eabab7eb6b12d167680d \ PG_AUTH_MON_COMMIT=fe099eef7662cbc85b0b79191f47f52f1e96b779 \ - PG_MON_COMMIT=ead1de70794ed62ca1e34d4022f6165ff36e9a91 \ - SET_USER=REL4_1_0 \ + PG_MON_COMMIT=88ac7b58348aa061c814982defc170644f368f39 \ PLPROFILER=REL4_2_5 \ - PG_PROFILE=4.10 \ - PAM_OAUTH2=v1.0.1 \ - PG_PERMISSIONS_COMMIT=f4b7c18676fa64236a1c8e28d34a35764e4a70e2 + PG_PROFILE=4.11 \ + PAM_OAUTH2=v1.0.1 WORKDIR /builddeps RUN bash base.sh diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index 19759b39b..fd54a3e39 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -53,9 +53,7 @@ fi curl -sL "https://github.com/zalando-pg/bg_mon/archive/$BG_MON_COMMIT.tar.gz" | tar xz curl -sL "https://github.com/zalando-pg/pg_auth_mon/archive/$PG_AUTH_MON_COMMIT.tar.gz" | tar xz -curl -sL "https://github.com/cybertec-postgresql/pg_permissions/archive/$PG_PERMISSIONS_COMMIT.tar.gz" | tar xz curl -sL "https://github.com/zubkov-andrei/pg_profile/archive/$PG_PROFILE.tar.gz" | tar xz -git clone -b "$SET_USER" https://github.com/pgaudit/set_user.git apt-get install -y \ postgresql-common \ @@ -124,6 +122,8 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-server-dev-${version}" \ "postgresql-${version}-pgq3" \ "postgresql-${version}-pg-stat-kcache" \ + "postgresql-${version}-pg-permissions" \ + "postgresql-${version}-set-user" \ "${EXTRAS[@]}" # Clean up timescaledb versions - keep at least 5 minor versions, but ensure compatibility with the lowest/oldest PG version (where possible) @@ -183,8 +183,6 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do for n in bg_mon-${BG_MON_COMMIT} \ pg_auth_mon-${PG_AUTH_MON_COMMIT} \ - set_user \ - pg_permissions-${PG_PERMISSIONS_COMMIT} \ pg_profile-${PG_PROFILE} \ "${EXTRA_EXTENSIONS[@]}"; do PATH="/usr/lib/postgresql/$version/bin:$PATH" make -C "$n" USE_PGXS=1 clean From 28a2327831c8ddd990f403a1d23f0bb571093e5a Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:07:29 +0100 Subject: [PATCH 38/41] Properly format ipv6 address for rsync (#1187) --- postgres-appliance/major_upgrade/inplace_upgrade.py | 1 + 1 file changed, 1 insertion(+) diff --git a/postgres-appliance/major_upgrade/inplace_upgrade.py b/postgres-appliance/major_upgrade/inplace_upgrade.py index d8c722ffc..cad014d95 100644 --- a/postgres-appliance/major_upgrade/inplace_upgrade.py +++ b/postgres-appliance/major_upgrade/inplace_upgrade.py @@ -711,6 +711,7 @@ def rsync_replica(config, desired_version, primary_ip, pid): env = os.environ.copy() env['RSYNC_PASSWORD'] = postgresql.config.replication['password'] + primary_ip = f'[{primary_ip}]' if ':' in primary_ip else primary_ip if subprocess.call(['rsync', '--archive', '--delete', '--hard-links', '--size-only', '--omit-dir-times', '--no-inc-recursive', '--include=/data/***', '--include=/data_old/***', '--exclude=/data/pg_xlog/*', '--exclude=/data_old/pg_xlog/*', From 4be7a748f9be548138ccaf9165fa702be1e03c38 Mon Sep 17 00:00:00 2001 From: Mikkel Oscar Lyderik Larsen Date: Mon, 2 Mar 2026 11:15:32 +0100 Subject: [PATCH 39/41] Ensure CLONE_HOST is IPv6 compatible (#1190) Signed-off-by: Mikkel Oscar Lyderik Larsen --- postgres-appliance/scripts/configure_spilo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/postgres-appliance/scripts/configure_spilo.py b/postgres-appliance/scripts/configure_spilo.py index f073b2f60..61f00acfa 100755 --- a/postgres-appliance/scripts/configure_spilo.py +++ b/postgres-appliance/scripts/configure_spilo.py @@ -987,7 +987,12 @@ def update_and_write_walg_configuration(placeholders, prefix, overwrite): def write_clone_pgpass(placeholders, overwrite): pgpassfile = placeholders['CLONE_PGPASS'] # pgpass is host:port:database:user:password - r = {'host': escape_pgpass_value(placeholders['CLONE_HOST']), + clone_host = escape_pgpass_value(placeholders['CLONE_HOST']) + # IPv6 addresses contain colons which conflict with the pgpass delimiter; + # wrap them in brackets so libpq can parse the host field correctly. + if ':' in str(clone_host): + clone_host = f'[{clone_host}]' + r = {'host': clone_host, 'port': placeholders['CLONE_PORT'], 'database': '*', 'user': escape_pgpass_value(placeholders['CLONE_USER']), From f332b91be5f9a71ccfb5b0ef11ced96e0a401d24 Mon Sep 17 00:00:00 2001 From: Polina Bungina <27892524+hughcapet@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:38:07 +0100 Subject: [PATCH 40/41] Move to spilo-18/spilo-cdp-18 (#1191) - Support PG18 - Patroni 4.1.0 - bg_mon from the original repo Not included for PG18: - pglogical-ticker - pgl-ddl-deploy --------- Co-authored-by: Ida Novindasari --- delivery.yaml | 9 +- postgres-appliance/Dockerfile | 6 +- .../bootstrap/maybe_pg_upgrade.py | 4 +- postgres-appliance/build_scripts/base.sh | 12 +- .../major_upgrade/inplace_upgrade.py | 12 +- .../major_upgrade/pg_upgrade.py | 14 +- postgres-appliance/scripts/post_init.sh | 10 +- postgres-appliance/scripts/spilo_commons.py | 10 +- postgres-appliance/tests/docker-compose.yml | 2 +- postgres-appliance/tests/test_spilo.sh | 122 +++++++++--------- 10 files changed, 104 insertions(+), 97 deletions(-) diff --git a/delivery.yaml b/delivery.yaml index d05d19a21..12ea76a70 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -3,7 +3,8 @@ allow_concurrent_steps: true build_env: &BUILD_ENV BASE_IMAGE: container-registry.zalando.net/library/ubuntu-22.04 - PGVERSION: 17 + PGVERSION: 18 + PGOLDVERSIONS: "16 17" MULTI_ARCH_REGISTRY: container-registry-test.zalando.net/acid pipeline: @@ -32,7 +33,7 @@ pipeline: docker buildx build --platform "linux/amd64,linux/arm64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ - --build-arg PGOLDVERSIONS="14 15 16" \ + --build-arg PGOLDVERSIONS="$PGOLDVERSIONS" \ -t "$ECR_TEST_IMAGE" \ --push . @@ -61,7 +62,7 @@ pipeline: docker buildx build --platform "linux/amd64,linux/arm64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ - --build-arg PGOLDVERSIONS="14 15 16" \ + --build-arg PGOLDVERSIONS="$PGOLDVERSIONS" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" @@ -92,7 +93,7 @@ pipeline: docker buildx build --platform "linux/amd64,linux/arm64" \ --build-arg PGVERSION="$PGVERSION" \ --build-arg BASE_IMAGE="$BASE_IMAGE" \ - --build-arg PGOLDVERSIONS="14 15 16" \ + --build-arg PGOLDVERSIONS="$PGOLDVERSIONS" \ -t "$ECR_TEST_IMAGE" \ --push . cdp-promote-image "$ECR_TEST_IMAGE" diff --git a/postgres-appliance/Dockerfile b/postgres-appliance/Dockerfile index 94add7ffd..6608483ab 100644 --- a/postgres-appliance/Dockerfile +++ b/postgres-appliance/Dockerfile @@ -1,5 +1,5 @@ ARG BASE_IMAGE=ubuntu:22.04 -ARG PGVERSION=17 +ARG PGVERSION=18 ARG DEMO=false ARG COMPRESS=false ARG ADDITIONAL_LOCALES= @@ -46,7 +46,7 @@ ARG PGVERSION ARG TIMESCALEDB_APACHE_ONLY=true ARG TIMESCALEDB_TOOLKIT=true ARG COMPRESS -ARG PGOLDVERSIONS="13 14 15 16" +ARG PGOLDVERSIONS="14 15 16 17" ARG WITH_PERL=false ARG DEB_PG_SUPPORTED_VERSIONS="$PGOLDVERSIONS $PGVERSION" @@ -69,7 +69,7 @@ COPY --from=dependencies-builder /builddeps/wal-g /usr/local/bin/ COPY build_scripts/patroni.sh build_scripts/compress_build.sh /builddeps/ # Install patroni -ENV PATRONIVERSION=4.0.6 +ENV PATRONIVERSION=4.1.0 WORKDIR / diff --git a/postgres-appliance/bootstrap/maybe_pg_upgrade.py b/postgres-appliance/bootstrap/maybe_pg_upgrade.py index 4f36e6953..9252d7899 100644 --- a/postgres-appliance/bootstrap/maybe_pg_upgrade.py +++ b/postgres-appliance/bootstrap/maybe_pg_upgrade.py @@ -40,7 +40,7 @@ def perform_pitr(postgresql, cluster_version, bin_version, config): except Exception: logs = tail_postgres_logs() # Spilo has no other locales except en_EN.UTF-8, therefore we are safe here. - if int(cluster_version) >= 13 and 'recovery ended before configured recovery target was reached' in logs: + if 'recovery ended before configured recovery target was reached' in logs: # Starting from version 13 Postgres stopped promoting when recovery target wasn't reached. # In order to improve the user experience we reset all possible recovery targets and retry. recovery_conf = config[config['method']].get('recovery_conf', {}) @@ -103,7 +103,7 @@ def main(): except Exception as e: logger.error('Failed to update extensions: %r', e) - upgrade.analyze() + upgrade.analyze(bin_version) def call_maybe_pg_upgrade(): diff --git a/postgres-appliance/build_scripts/base.sh b/postgres-appliance/build_scripts/base.sh index fd54a3e39..bc4633d82 100644 --- a/postgres-appliance/build_scripts/base.sh +++ b/postgres-appliance/build_scripts/base.sh @@ -51,7 +51,7 @@ if [ "$WITH_PERL" != "true" ]; then equivs-build perl fi -curl -sL "https://github.com/zalando-pg/bg_mon/archive/$BG_MON_COMMIT.tar.gz" | tar xz +curl -sL "https://github.com/CyberDem0n/bg_mon/archive/$BG_MON_COMMIT.tar.gz" | tar xz curl -sL "https://github.com/zalando-pg/pg_auth_mon/archive/$PG_AUTH_MON_COMMIT.tar.gz" | tar xz curl -sL "https://github.com/zubkov-andrei/pg_profile/archive/$PG_PROFILE.tar.gz" | tar xz @@ -83,10 +83,8 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-pgaudit" "postgresql-${version}-pldebugger" "postgresql-${version}-pglogical" - "postgresql-${version}-pglogical-ticker" "postgresql-${version}-plpgsql-check" "postgresql-${version}-pg-checksums" - "postgresql-${version}-pgl-ddl-deploy" "postgresql-${version}-pgq-node" "postgresql-${version}-postgis-${POSTGIS_VERSION%.*}" "postgresql-${version}-postgis-${POSTGIS_VERSION%.*}-scripts" @@ -95,10 +93,12 @@ for version in $DEB_PG_SUPPORTED_VERSIONS; do "postgresql-${version}-decoderbufs" "postgresql-${version}-pllua" "postgresql-${version}-pgvector" - "postgresql-${version}-roaringbitmap") + "postgresql-${version}-roaringbitmap" + "postgresql-${version}-pgfaceting") - if [ "$version" -ge 14 ]; then - EXTRAS+=("postgresql-${version}-pgfaceting") + if [ "$version" != "18" ]; then + EXTRAS+=("postgresql-${version}-pgl-ddl-deploy" + "postgresql-${version}-pglogical-ticker") fi if [ "$WITH_PERL" = "true" ]; then diff --git a/postgres-appliance/major_upgrade/inplace_upgrade.py b/postgres-appliance/major_upgrade/inplace_upgrade.py index cad014d95..51c193bb6 100644 --- a/postgres-appliance/major_upgrade/inplace_upgrade.py +++ b/postgres-appliance/major_upgrade/inplace_upgrade.py @@ -451,7 +451,7 @@ def restore_custom_statistics_target(self): except Exception: logger.error("Failed to execute '%s'", query) - def reanalyze(self): + def custom_stats_target_reanalyze(self): from patroni.postgresql.connection import get_connection_cursor if not self._statistics: @@ -470,12 +470,15 @@ def reanalyze(self): except Exception: logger.error("Failed to execute '%s'", query) + def full_reanalyze(self): + self.postgresql.analyze(self.desired_version) + def analyze(self): try: self.reset_custom_statistics_target() except Exception as e: logger.error('Failed to reset custom statistics targets: %r', e) - self.postgresql.analyze(True) + self.postgresql.analyze(self.desired_version, in_stages=True) try: self.restore_custom_statistics_target() except Exception as e: @@ -634,7 +637,10 @@ def do_upgrade(self): analyze_thread.join() - self.reanalyze() + if int(self.desired_version) < 18: + self.custom_stats_target_reanalyze() + else: + self.full_reanalyze() logger.info('Total upgrade time (with analyze): %s', time.time() - downtime_start) self.postgresql.bootstrap.call_post_bootstrap(self.config['bootstrap']) diff --git a/postgres-appliance/major_upgrade/pg_upgrade.py b/postgres-appliance/major_upgrade/pg_upgrade.py index ad1563e80..f3f05ea6b 100644 --- a/postgres-appliance/major_upgrade/pg_upgrade.py +++ b/postgres-appliance/major_upgrade/pg_upgrade.py @@ -207,8 +207,9 @@ def prepare_new_pgdata(self, version): locale = self.query("SELECT datcollate FROM pg_database WHERE datname='template1';")[0][0] encoding = self.query('SHOW server_encoding')[0][0] initdb_config = [{'locale': locale}, {'encoding': encoding}] - if self.query("SELECT current_setting('data_checksums')::bool")[0][0]: - initdb_config.append('data-checksums') + checksums_enabled = self.query("SELECT current_setting('data_checksums')::bool")[0][0] + if checksums_enabled == (int(version) < 18): + initdb_config.append('data-checksums' if checksums_enabled else 'no-data-checksums') logger.info('initdb config: %s', initdb_config) @@ -268,9 +269,12 @@ def do_upgrade(self): return self.pg_upgrade() and self.restore_shared_preload_libraries()\ and self.switch_pgdata() and self.cleanup_old_pgdata() - def analyze(self, in_stages=False): - vacuumdb_args = ['--analyze-in-stages'] if in_stages else [] - logger.info('Rebuilding statistics (vacuumdb%s)', (' ' + vacuumdb_args[0] if in_stages else '')) + def analyze(self, version, in_stages=False): + vacuumdb_args = [] + if in_stages: + vacuumdb_args = ['--analyze-in-stages'] if int(version) < 18 else ['--analyze-in-stages', + '--missing-stats-only'] + logger.info('Rebuilding statistics (vacuumdb%s)', (' ' + ' '.join(vacuumdb_args) if in_stages else '')) if 'username' in self.config.superuser: vacuumdb_args += ['-U', self.config.superuser['username']] vacuumdb_args += ['-Z', '-j'] diff --git a/postgres-appliance/scripts/post_init.sh b/postgres-appliance/scripts/post_init.sh index c12e9c83b..b1f8f325a 100755 --- a/postgres-appliance/scripts/post_init.sh +++ b/postgres-appliance/scripts/post_init.sh @@ -139,16 +139,12 @@ CREATE TABLE IF NOT EXISTS public.postgres_log ( query_pos integer, location text, application_name text, + backend_type text, + leader_pid integer, + query_id bigint, CONSTRAINT postgres_log_check CHECK (false) NO INHERIT ); GRANT SELECT ON public.postgres_log TO admin;" -if [ "$PGVER" -ge 13 ]; then - echo "ALTER TABLE public.postgres_log ADD COLUMN IF NOT EXISTS backend_type text;" -fi -if [ "$PGVER" -ge 14 ]; then - echo "ALTER TABLE public.postgres_log ADD COLUMN IF NOT EXISTS leader_pid integer;" - echo "ALTER TABLE public.postgres_log ADD COLUMN IF NOT EXISTS query_id bigint;" -fi # Sunday could be 0 or 7 depending on the format, we just create both LOG_SHIP_HOURLY=$(echo "SELECT text(current_setting('log_rotation_age') = '1h')" | psql -tAX -d postgres 2> /dev/null | tail -n 1) diff --git a/postgres-appliance/scripts/spilo_commons.py b/postgres-appliance/scripts/spilo_commons.py index 0543bf771..981c9caa8 100644 --- a/postgres-appliance/scripts/spilo_commons.py +++ b/postgres-appliance/scripts/spilo_commons.py @@ -12,13 +12,13 @@ # (min_version, max_version, shared_preload_libraries, extwlist.extensions) extensions = { - 'timescaledb': (9.6, 17, True, True), - 'pg_cron': (9.5, 17, True, False), - 'pg_stat_kcache': (9.4, 17, True, False), - 'pg_partman': (9.4, 17, False, True) + 'timescaledb': (9.6, 18, True, True), + 'pg_cron': (9.5, 18, True, False), + 'pg_stat_kcache': (9.4, 18, True, False), + 'pg_partman': (9.4, 18, False, True) } if os.environ.get('ENABLE_PG_MON') == 'true': - extensions['pg_mon'] = (11, 17, True, False) + extensions['pg_mon'] = (11, 18, True, False) def adjust_extensions(old, version, extwlist=False): diff --git a/postgres-appliance/tests/docker-compose.yml b/postgres-appliance/tests/docker-compose.yml index e8e7952e9..3358f61a9 100644 --- a/postgres-appliance/tests/docker-compose.yml +++ b/postgres-appliance/tests/docker-compose.yml @@ -50,7 +50,7 @@ services: postgresql: parameters: shared_buffers: 32MB - PGVERSION: '13' + PGVERSION: '14' # Just to test upgrade with clone. Without CLONE_SCOPE they don't work CLONE_WAL_S3_BUCKET: *bucket CLONE_AWS_ACCESS_KEY_ID: *access_key diff --git a/postgres-appliance/tests/test_spilo.sh b/postgres-appliance/tests/test_spilo.sh index 66100cbe9..a68c86a7f 100755 --- a/postgres-appliance/tests/test_spilo.sh +++ b/postgres-appliance/tests/test_spilo.sh @@ -124,15 +124,15 @@ function drop_timescaledb() { } function test_inplace_upgrade_wrong_version() { - docker_exec "$1" "PGVERSION=13 $UPGRADE_SCRIPT 3" 2>&1 | grep 'Upgrade is not required' + docker_exec "$1" "PGVERSION=14 $UPGRADE_SCRIPT 3" 2>&1 | grep 'Upgrade is not required' } function test_inplace_upgrade_wrong_capacity() { - docker_exec "$1" "PGVERSION=14 $UPGRADE_SCRIPT 4" 2>&1 | grep 'number of replicas does not match' + docker_exec "$1" "PGVERSION=15 $UPGRADE_SCRIPT 4" 2>&1 | grep 'number of replicas does not match' } -function test_successful_inplace_upgrade_to_14() { - docker_exec "$1" "PGVERSION=14 $UPGRADE_SCRIPT 3" +function test_successful_inplace_upgrade_to_15() { + docker_exec "$1" "PGVERSION=15 $UPGRADE_SCRIPT 3" } function test_envdir_suffix() { @@ -146,11 +146,7 @@ function test_envdir_updated_to_x() { } function test_failed_inplace_upgrade_big_replication_lag() { - ! test_successful_inplace_upgrade_to_14 "$1" -} - -function test_successful_inplace_upgrade_to_15() { - docker_exec "$1" "PGVERSION=15 $UPGRADE_SCRIPT 3" + ! test_successful_inplace_upgrade_to_15 "$1" } function test_successful_inplace_upgrade_to_16() { @@ -161,8 +157,12 @@ function test_successful_inplace_upgrade_to_17() { docker_exec "$1" "PGVERSION=17 $UPGRADE_SCRIPT 3" } -function test_pg_upgrade_to_17_check_failed() { - ! test_successful_inplace_upgrade_to_17 "$1" +function test_successful_inplace_upgrade_to_18() { + docker_exec "$1" "PGVERSION=18 $UPGRADE_SCRIPT 3" +} + +function test_pg_upgrade_to_18_check_failed() { + ! test_successful_inplace_upgrade_to_18 "$1" } function start_clone_with_walg_upgrade_container() { @@ -170,7 +170,7 @@ function start_clone_with_walg_upgrade_container() { docker-compose run \ -e SCOPE=upgrade \ - -e PGVERSION=14 \ + -e PGVERSION=15 \ -e CLONE_SCOPE=demo \ -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_minute)" \ @@ -183,27 +183,27 @@ function start_clone_with_walg_upgrade_replica_container() { start_clone_with_walg_upgrade_container 2 } -function start_clone_with_walg_upgrade_to_17_container() { +function start_clone_with_walg_upgrade_to_18_container() { docker-compose run \ -e SCOPE=upgrade3 \ - -e PGVERSION=17 \ + -e PGVERSION=18 \ -e CLONE_SCOPE=demo \ - -e CLONE_PGVERSION=13 \ + -e CLONE_PGVERSION=14 \ -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_minute)" \ --name "${PREFIX}upgrade4" \ -d "spilo3" } -function start_clone_with_walg_17_container() { +function start_clone_with_walg_18_container() { docker-compose run \ - -e SCOPE=clone16 \ - -e PGVERSION=17 \ + -e SCOPE=clone17 \ + -e PGVERSION=18 \ -e CLONE_SCOPE=upgrade3 \ - -e CLONE_PGVERSION=17 \ + -e CLONE_PGVERSION=18 \ -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_hour)" \ - --name "${PREFIX}clone16" \ + --name "${PREFIX}clone17" \ -d "spilo3" } @@ -211,7 +211,7 @@ function start_clone_with_basebackup_upgrade_container() { local container=$1 docker-compose run \ -e SCOPE=upgrade2 \ - -e PGVERSION=15 \ + -e PGVERSION=16 \ -e CLONE_SCOPE=upgrade \ -e CLONE_METHOD=CLONE_WITH_BASEBACKUP \ -e CLONE_HOST="$(docker_exec "$container" "hostname --ip-address")" \ @@ -225,10 +225,10 @@ function start_clone_with_basebackup_upgrade_container() { function start_clone_with_hourly_log_rotation() { docker-compose run \ -e SCOPE=hourlylogs \ - -e PGVERSION=17 \ + -e PGVERSION=18 \ -e LOG_SHIP_HOURLY="true" \ -e CLONE_SCOPE=upgrade2 \ - -e CLONE_PGVERSION=15 \ + -e CLONE_PGVERSION=16 \ -e CLONE_METHOD=CLONE_WITH_WALG \ -e CLONE_TARGET_TIME="$(next_minute)" \ --name "${PREFIX}hourlylogs" \ @@ -260,18 +260,18 @@ function verify_hourly_log_rotation() { [ "$log_rotation_age" = "1h" ] && [ "$log_filename" = "postgresql-%u-%H.log" ] && [ "$postgres_log_ftables" -eq 192 ] && [ "$postgres_log_views" -eq 8 ] && [ "$postgres_failed_auth_views" -eq 200 ] } -# TEST SUITE 1 - In-place major upgrade 13->14->...->17 -# TEST SUITE 2 - Major upgrade 13->17 after wal-g clone (with CLONE_PGVERSION set) -# TEST SUITE 3 - PITR (clone with wal-g) with unreachable target (14+) -# TEST SUITE 4 - Major upgrade 13->14 after wal-g clone (no CLONE_PGVERSION) +# TEST SUITE 1 - In-place major upgrade 14->15->...->18 +# TEST SUITE 2 - Major upgrade 14->18 after wal-g clone (with CLONE_PGVERSION set) +# TEST SUITE 3 - PITR (clone with wal-g) with unreachable target (15+) +# TEST SUITE 4 - Major upgrade 14->15 after wal-g clone (no CLONE_PGVERSION) # TEST SUITE 5 - Replica bootstrap with wal-g -# TEST SUITE 6 - Major upgrade 14->15 after clone with basebackup +# TEST SUITE 6 - Major upgrade 15->16 after clone with basebackup # TEST SUITE 7 - Hourly log rotation function test_spilo() { # TEST SUITE 1 local container=$1 - run_test test_envdir_suffix "$container" 13 + run_test test_envdir_suffix "$container" 14 log_info "[TS1] Testing wrong upgrade setups" run_test test_inplace_upgrade_wrong_version "$container" @@ -288,66 +288,66 @@ function test_spilo() { # TEST SUITE 2 local upgrade3_container - upgrade3_container=$(start_clone_with_walg_upgrade_to_17_container) # SCOPE=upgrade3 PGVERSION=17 CLONE: _SCOPE=demo _PGVERSION=13 _TARGET_TIME= - log_info "[TS2] Started $upgrade3_container for testing major upgrade 13->17 after clone with wal-g" + upgrade3_container=$(start_clone_with_walg_upgrade_to_18_container) # SCOPE=upgrade3 PGVERSION=18 CLONE: _SCOPE=demo _PGVERSION=14 _TARGET_TIME= + log_info "[TS2] Started $upgrade3_container for testing major upgrade 14->18 after clone with wal-g" # TEST SUITE 4 local upgrade_container - upgrade_container=$(start_clone_with_walg_upgrade_container) # SCOPE=upgrade PGVERSION=14 CLONE: _SCOPE=demo _TARGET_TIME= - log_info "[TS4] Started $upgrade_container for testing major upgrade 13->14 after clone with wal-g" + upgrade_container=$(start_clone_with_walg_upgrade_container) # SCOPE=upgrade PGVERSION=15 CLONE: _SCOPE=demo _TARGET_TIME= + log_info "[TS4] Started $upgrade_container for testing major upgrade 14->15 after clone with wal-g" # TEST SUITE 1 # wait clone to finish and prevent timescale installation gets cloned find_leader "$upgrade3_container" find_leader "$upgrade_container" - create_timescaledb "$container" # we don't install it at the beginning, as we do 13->17 in a clone + create_timescaledb "$container" # we don't install it at the beginning, as we do 14->18 in a clone - log_info "[TS1] Testing in-place major upgrade 13->14" + log_info "[TS1] Testing in-place major upgrade 14->15" wait_zero_lag "$container" - run_test test_successful_inplace_upgrade_to_14 "$container" + run_test test_successful_inplace_upgrade_to_15 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 14 + run_test test_envdir_updated_to_x 15 # TEST SUITE 2 - log_info "[TS2] Testing in-place major upgrade 13->17 after wal-g clone" - run_test verify_clone_upgrade "$upgrade3_container" "wal-g" 13 17 + log_info "[TS2] Testing in-place major upgrade 14->18 after wal-g clone" + run_test verify_clone_upgrade "$upgrade3_container" "wal-g" 14 18 run_test verify_archive_mode_is_on "$upgrade3_container" wait_backup "$upgrade3_container" # TEST SUITE 3 - local clone17_container - clone17_container=$(start_clone_with_walg_17_container) # SCOPE=clone17 CLONE: _SCOPE=upgrade3 _PGVERSION=17 _TARGET_TIME= - log_info "[TS3] Started $clone17_container for testing point-in-time recovery (clone with wal-g) with unreachable target on 14+" + local clone18_container + clone18_container=$(start_clone_with_walg_18_container) # SCOPE=clone18 CLONE: _SCOPE=upgrade3 _PGVERSION=18 _TARGET_TIME= + log_info "[TS3] Started $clone18_container for testing point-in-time recovery (clone with wal-g) with unreachable target on 15+" # TEST SUITE 1 - log_info "[TS1] Testing in-place major upgrade 14->15" - run_test test_successful_inplace_upgrade_to_15 "$container" + log_info "[TS1] Testing in-place major upgrade 15->16" + run_test test_successful_inplace_upgrade_to_16 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 15 + run_test test_envdir_updated_to_x 16 # TEST SUITE 3 - find_leader "$clone17_container" - run_test verify_archive_mode_is_on "$clone17_container" + find_leader "$clone18_container" + run_test verify_archive_mode_is_on "$clone18_container" # TEST SUITE 1 wait_backup "$container" - log_info "[TS1] Testing in-place major upgrade to 15->16" - run_test test_successful_inplace_upgrade_to_16 "$container" + log_info "[TS1] Testing in-place major upgrade to 16->17" + run_test test_successful_inplace_upgrade_to_17 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 16 + run_test test_envdir_updated_to_x 17 # TEST SUITE 4 - log_info "[TS4] Testing in-place major upgrade 13->14 after clone with wal-g" - run_test verify_clone_upgrade "$upgrade_container" "wal-g" 13 14 + log_info "[TS4] Testing in-place major upgrade 14->15 after clone with wal-g" + run_test verify_clone_upgrade "$upgrade_container" "wal-g" 14 15 run_test verify_archive_mode_is_on "$upgrade_container" wait_backup "$upgrade_container" @@ -361,20 +361,20 @@ function test_spilo() { # TEST SUITE 6 local basebackup_container - basebackup_container=$(start_clone_with_basebackup_upgrade_container "$upgrade_container") # SCOPE=upgrade2 PGVERSION=15 CLONE: _SCOPE=upgrade - log_info "[TS6] Started $basebackup_container for testing major upgrade 14->15 after clone with basebackup" + basebackup_container=$(start_clone_with_basebackup_upgrade_container "$upgrade_container") # SCOPE=upgrade2 PGVERSION=16 CLONE: _SCOPE=upgrade + log_info "[TS6] Started $basebackup_container for testing major upgrade 15->16 after clone with basebackup" wait_backup "$basebackup_container" # TEST SUITE 1 - # run_test test_pg_upgrade_to_17_check_failed "$container" # pg_upgrade --check complains about timescaledb + # run_test test_pg_upgrade_to_18_check_failed "$container" # pg_upgrade --check complains about timescaledb wait_backup "$container" - # drop_timescaledb "$container" - log_info "[TS1] Testing in-place major upgrade 16->17" - run_test test_successful_inplace_upgrade_to_17 "$container" + drop_timescaledb "$container" + log_info "[TS1] Testing in-place major upgrade 17->18" + run_test test_successful_inplace_upgrade_to_18 "$container" wait_all_streaming "$container" - run_test test_envdir_updated_to_x 17 + run_test test_envdir_updated_to_x 18 # TEST SUITE 5 @@ -387,8 +387,8 @@ function test_spilo() { log_info "[TS7] Started $hourlylogs_container for testing hourly log rotation" # TEST SUITE 6 - log_info "[TS6] Testing in-place major upgrade 14->15 after clone with basebackup" - run_test verify_clone_upgrade "$basebackup_container" "basebackup" 14 15 + log_info "[TS6] Testing in-place major upgrade 15->16 after clone with basebackup" + run_test verify_clone_upgrade "$basebackup_container" "basebackup" 15 16 run_test verify_archive_mode_is_on "$basebackup_container" # TEST SUITE 7 From 0b78ed2d164a2a7dc0444745ef802a44ceba3a0a Mon Sep 17 00:00:00 2001 From: "m.doulabi" Date: Sat, 4 Apr 2026 19:18:09 +0330 Subject: [PATCH 41/41] fix(callback_role): support custom Kubernetes cluster domains Replace hardcoded cluster.local with KUBERNETES_SERVICE_HOST env var. Users can now set custom cluster domains like: - https://kubernetes.default.svc.cluster.bk1 - http://192.168.1.1:8443 Fully backwards compatible - defaults to previous behavior. Fixes: patroni could not patch pod labels on clusters with custom cluster domains. --- postgres-appliance/scripts/callback_role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres-appliance/scripts/callback_role.py b/postgres-appliance/scripts/callback_role.py index b0d482834..369064ae6 100755 --- a/postgres-appliance/scripts/callback_role.py +++ b/postgres-appliance/scripts/callback_role.py @@ -14,7 +14,7 @@ KUBE_TOKEN_FILENAME = KUBE_SERVICE_DIR + 'token' KUBE_CA_CERT = KUBE_SERVICE_DIR + 'ca.crt' -KUBE_API_URL = 'https://kubernetes.default.svc.cluster.local/api/v1/namespaces' +KUBE_API_URL = os.environ.get('KUBERNETES_SERVICE_HOST', 'https://kubernetes.default.svc.cluster.local') + '/api/v1/namespaces' logger = logging.getLogger(__name__) @@ -94,4 +94,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file