From 0df12e992916c366f4d2bcd3f308b4407c21ec9d Mon Sep 17 00:00:00 2001 From: Endial Fang Date: Fri, 22 Sep 2023 08:45:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E4=BA=8E=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=9B=B4=E6=96=B0=E8=84=9A=E6=9C=AC=E5=8F=8A?= =?UTF-8?q?Dockerfile=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 112 +-- customer/usr/local/bin/common.sh | 1145 +++++++++++-------------- customer/usr/local/bin/entry.sh | 36 +- customer/usr/local/bin/environment.sh | 152 ++-- customer/usr/local/bin/init.sh | 23 +- customer/usr/local/bin/run.sh | 27 +- customer/usr/local/bin/setup.sh | 40 +- 7 files changed, 656 insertions(+), 879 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7d88732..d209834 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ -# Ver: 1.9 by Endial Fang (endial@126.com) +# Ver: 1.12 by Endial Fang (endial@126.com) # -# 默认变量 ======================================================================== +# 系统默认变量 ==================================================================== # 该部分变量为系统根据编译命令默认设置 - # `TARGETPLATFORM`:构建后的目标平台信息。如 `linux/amd64`,`linux/arm/v7`,`windows/amd64` # `TARGETOS`:目标平台信息(TARGETPLATFORM)中的操作系统部分,如:`linux`、`windows` # `TARGETARCH`:目标平台信息(TARGETPLATFORM)中的平台架构部分,如:`amd64`、`arm` @@ -16,18 +15,11 @@ # 可变参数 ======================================================================== # 该部分变量,在编译命令中通过 `--build-arg` 传入;如果未设置,则使用下面对应的默认值 -# 设置当前应用名称及版本 -ARG APP_NAME=postgresql -ARG APP_VER=13.12 - -# 设置默认仓库地址,默认为本地仓库;定义时需要包含末尾的`/` -ARG REGISTRY_URL="docker.colovu.com/" - -# 设置 apt-get 源:default / ustc / aliyun -ARG APT_SOURCE=aliyun - -# 编译镜像时指定用于加速的本地软件包存储服务器地址 -ARG LOCAL_URL="http://local.colovu.com/dist" +ARG APP_NAME=postgresql # 设置当前应用名称 +ARG APP_VER=13.12 # 设置当前应用版本 +ARG REGISTRY_URL="docker.colovu.com/" # 设置默认仓库地址,默认为本地仓库;定义时需要包含末尾的`/` +ARG APT_SOURCE=aliyun # 设置 apt-get 源:default / ustc / aliyun +ARG LOCAL_URL="http://local.colovu.com/dist" # 编译镜像时指定用于加速的本地软件包存储服务器地址 # 0. 预处理 ====================================================================== FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/dbuilder:12 as builder @@ -38,21 +30,18 @@ ARG APP_VER ARG APT_SOURCE ARG LOCAL_URL -# 选择软件包源(Optional),以加速后续软件包安装 +# 选择软件包源,加速后续软件包安装 RUN select_source ${APT_SOURCE}; -# 安装依赖的软件包及库(Optional) +# 安装依赖的软件包及库 RUN install_pkg bison flex libedit-dev libxml2-dev libxslt-dev zlib1g-dev libreadline-dev uuid-dev \ libperl-dev libicu-dev libxslt1-dev libssl-dev libldap2-dev libkrb5-dev libpam0g-dev libselinux1-dev; -# 设置工作目录 -WORKDIR /tmp - # 下载并解压软件包 RUN set -eux; \ appName="${APP_NAME}-${APP_VER}.tar.gz"; \ sha256="3aef84ff557087bd01df4365a7a85e11678faa55ab6fb6c4283d57e41997a80c"; \ - [ -n ${LOCAL_URL} ] && localURL=${LOCAL_URL}/${APP_NAME}/v${APP_VER}; \ + [ -n ${LOCAL_URL} ] && localURL=${LOCAL_URL}/${APP_NAME}; \ appUrls="${localURL:-} \ https://ftp.postgresql.org/pub/source/v${APP_VER} \ "; \ @@ -62,6 +51,7 @@ RUN set -eux; \ RUN set -eux; \ APP_ARCH=`arch` \ APP_SRC="/tmp/${APP_NAME}-${APP_VER}"; \ + dpkgArch="$(dpkg --print-architecture)"; \ cd ${APP_SRC}; \ \ # update "DEFAULT_PGSOCKET_DIR" to "/var/run/postgresql" (matching Debian) @@ -107,8 +97,7 @@ RUN set -eux; \ # --with-python \ # --with-pam \ ; \ - make -j "$(nproc)" world; \ - make install-world; \ + make -j "$(nproc)" world && make install-world; \ make -C contrib install; # 删除编译生成的多余文件 @@ -119,12 +108,9 @@ RUN set -eux; \ # 检测并生成依赖文件记录 RUN set -eux; \ find /usr/local/${APP_NAME} -type f -executable -exec ldd '{}' ';' | \ - awk '/=>/ { print $(NF-1) }' | \ - sort -u | \ - xargs -r readlink -f | \ - xargs -r dpkg-query --search 2>/dev/null | \ - cut -d: -f1 | \ - sort -u >>/usr/local/${APP_NAME}/runDeps; + awk '/=>/ { print $(NF-1) }' | xargs -r basename -a | sort -u | \ + xargs -r dpkg-query --search 2>/dev/null | cut -d: -f1 | sort -u \ + >>/usr/local/${APP_NAME}/runDeps; # 1. 生成镜像 ===================================================================== FROM --platform=${TARGETPLATFORM:-linux/amd64} ${REGISTRY_URL}colovu/debian:12 @@ -138,12 +124,10 @@ ARG APT_SOURCE ENV APP_NAME=${APP_NAME} \ APP_VER=${APP_VER} \ APP_EXEC=postgres \ - APP_HOME_DIR=/usr/local/${APP_NAME} \ - APP_DEF_DIR=/etc/${APP_NAME} - -# 增加应用可执行文件及库文件搜索路径 -ENV PATH="${APP_HOME_DIR}/sbin:${APP_HOME_DIR}/bin:${PATH}" \ - LD_LIBRARY_PATH="${APP_HOME_DIR}/lib" + APP_USER=postgres \ + \ + LD_LIBRARY_PATH="/usr/local/${APP_NAME}/lib" \ + PATH="${PATH}:/usr/local/${APP_NAME}/bin" LABEL \ "Version"="v${APP_VER}" \ @@ -151,54 +135,46 @@ LABEL \ "Github"="https://github.com/colovu/docker-${APP_NAME}" \ "Vendor"="Endial Fang (endial@126.com)" -# 从预处理过程中拷贝软件包(Optional),可以使用阶段编号或阶段命名定义来源 +# 拷贝多阶段构建结果输出及客制化脚本 COPY --from=builder /usr/local/${APP_NAME} /usr/local/${APP_NAME} - -# 拷贝应用使用的客制化脚本 COPY customer / RUN set -eux; \ \ # 创建对应的用户及数据存储目录 - prepare_env; \ + useradd -U -u 996 -d /srv/${APP_NAME} -s /usr/sbin/nologin -r ${APP_USER}; \ + mkdir -p /var/log/${APP_NAME} /var/run/${APP_NAME} /var/cache/${APP_NAME}; \ + mkdir -p /srv/${APP_NAME}/conf /srv/${APP_NAME}/data /srv/${APP_NAME}/cert /srv/${APP_NAME}/log; \ + chown -R ${APP_USER}:${APP_USER} /var/log/${APP_NAME} /var/run/${APP_NAME} /var/cache/${APP_NAME}; \ + chown -R ${APP_USER}:${APP_USER} /usr/local/${APP_NAME} /srv/${APP_NAME}; \ \ - # 选择软件包源(Optional),以加速后续软件包安装 + /bin/bash -c "ln -sf /usr/local/${APP_NAME}/etc/${APP_NAME} /etc/"; \ + \ + # 选择软件包源,以加速后续软件包安装 select_source ${APT_SOURCE}; \ \ - # 安装依赖的软件包及库(Optional) + # 安装应用依赖的软件包及库 install_pkg `cat /usr/local/${APP_NAME}/runDeps`; \ \ - # 执行后处理脚本,并验证安装的应用 - override_file="/usr/local/overrides/overrides-${APP_VER}.sh"; \ - [ -e "${override_file}" ] && /bin/bash "${override_file}"; \ - postgres --version ; + # 执行后处理脚本 + overrideShell="/usr/local/overrides/overrides-${APP_VER}.sh"; \ + [ -e "${overrideShell}" ] && /bin/bash "${overrideShell}"; \ + \ + # 验证安装的应用 + ${APP_EXEC} --version ; -# 默认提供的数据卷 -VOLUME ["/srv/conf", "/srv/data", "/srv/datalog", "/srv/cert", "/var/log"] - -# 默认使用gosu切换为新建用户启动,必须保证端口在1024之上 +# 配置容器的数据卷、工作目录及服务端口(必须保证端口在1024之上) +VOLUME ["/srv/${APP_NAME}/conf", "/srv/${APP_NAME}/data", "/srv/${APP_NAME}/cert", "/srv/${APP_NAME}/log"] +WORKDIR /srv/${APP_NAME}/data EXPOSE 5432 +STOPSIGNAL SIGINT -# 关闭基础镜像的健康检查 #HEALTHCHECK NONE - -# 应用健康状态检查(需要使用 EXPOSE 定义的端口) -#HEALTHCHECK --interval=30s --timeout=30s --retries=3 \ -# CMD curl -fs http://localhost:8080/ || exit 1 -#HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ -# CMD netstat -ltun | grep 8080 +#HEALTHCHECK --interval=30s --timeout=30s --retries=3 CMD curl -fs http://localhost:8080/ || exit 1 +#HEALTHCHECK --interval=10s --timeout=10s --retries=3 CMD netstat -ltun | grep 8080 HEALTHCHECK CMD PGPASSWORD="${PG_POSTGRES_PASSWORD:-${PG_PASSWORD}}" psql -h 127.0.0.1 -d postgres -U postgres -At -c "select version();" || exit 1 -# 使用 non-root 用户运行后续的命令 -USER 1001 - -# 设置工作目录 -WORKDIR /srv/data - -# 容器入口命令脚本,'/usr/local/bin/entry.sh' -ENTRYPOINT ["entry.sh"] - -# 应用程序的启动命令,可为应用程序可执行命令或脚本 -# 必须使用非守护进程方式运行,'/usr/local/bin/run.sh' -CMD ["run.sh"] +# 使用 dumb-init 启动入口 Shell,确保容器可以接收控制信号;并使用前台方式启动应用程序 +ENTRYPOINT ["dumb-init", "entry.sh"] +CMD ["run.sh"] diff --git a/customer/usr/local/bin/common.sh b/customer/usr/local/bin/common.sh index e871859..a6cb8d3 100644 --- a/customer/usr/local/bin/common.sh +++ b/customer/usr/local/bin/common.sh @@ -1,11 +1,9 @@ #!/bin/bash -# Ver: 1.2 by Endial Fang (endial@126.com) +# Ver: 1.5 by Endial Fang (endial@126.com) # # 应用通用业务处理函数 -# 加载依赖脚本 . /colovu/lib/libcommon.sh # 通用函数库 - . /colovu/lib/libfile.sh . /colovu/lib/libfs.sh . /colovu/lib/liblog.sh @@ -13,27 +11,57 @@ . /colovu/lib/libservice.sh . /colovu/lib/libvalidations.sh -# 函数列表 +# 检测应用相应的配置文件是否存在,如果不存在,则从默认配置文件目录拷贝一份 +# 默认配置文件路径:/etc/${APP_NAME} +# 目标配置文件路径:/srv/conf/${APP_NAME} +# 参数: +# $1 - 目标路径 +# $2 - 源路径 +# $* - 基础路径下的文件及目录列表,以" "分割 +# 例子: +# ensure_config_file_exist /etc/${APP_NAME} conf.d server.conf +app_ensure_config_file_exist() { + local -r dist_path="${1:?dist paths is missing}" + local -r base_path="${2:?source paths is missing}" + local f="" -# 配置 libnss_wrapper 以使得 PostgreSQL 命令可以以任意用户身份执行 -postgresql_enable_nss_wrapper() { - if ! getent passwd "$(id -u)" &> /dev/null && [ -e /usr/lib/libnss_wrapper.so ]; then - LOG_D "Configuring libnss_wrapper..." - export LD_PRELOAD='/usr/lib/libnss_wrapper.so' - export NSS_WRAPPER_PASSWD="$(mktemp)" - export NSS_WRAPPER_GROUP="$(mktemp)" - echo "postgres:x:$(id -u):$(id -g):PostgreSQL:${PG_DATA_DIR}:/bin/false" > "${NSS_WRAPPER_PASSWD}" - echo "postgres:x:$(id -g):" > "${NSS_WRAPPER_GROUP}" - fi + shift 2 + LOG_D "List to check in ${base_path}: $@" + while [ "$#" -gt 0 ]; do + f="${1}" + LOG_D " Process \"${f}\"" + if [ -d "${base_path}/${f}" ]; then + [[ ! -d "${dist_path}/${f}" ]] && LOG_D " Create directory: ${dist_path}/${f}" && mkdir -p "${dist_path}/${f}" + [[ ! -z $(ls -A "${base_path}/${f}") ]] && app_ensure_config_file_exist "${dist_path}/${f}" "${base_path}/${f}" $(ls -A "${base_path}/${f}") + else + [[ ! -e "${dist_path}/${f}" ]] && LOG_D " Copy: ${base_path}/${f} to ${dist_path}" && cp "${base_path}/${f}" "${dist_path}" + fi + shift + done } -# 禁用 libnss_wrapper -postgresql_disable_nss_wrapper() { - # unset/cleanup "nss_wrapper" bits - if [ "${LD_PRELOAD:-}" = '/usr/lib/libnss_wrapper.so' ]; then - rm -f "${NSS_WRAPPER_PASSWD}" "${NSS_WRAPPER_GROUP}" - unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP - fi +# 检测以 "" 开头的环境变量,并更新指定配置文件中对应配置项的值 +# 如果需要全部转换为小写,可使用命令: tr '[:upper:]' '[:lower:]' +# 环境变量与配置项替换规则 : 环境变量中下划线 ==> 配置参数中特殊字符 +# - "_" ==> "_"(下划线) +# - "__" ==> "."(半角点) +# - "___" ==> "-"(中划线) +# +# 变量: +# $1 - 配置文件 +# $2 - 前缀(不含结束的"_") +app_configure_from_environment() { + local confFile="${1:?missing file}" + local envPrefix="${2:-APP_CFG}" + + LOG_D "Configuration File: ${confFile}" + + for var in $(eval echo \${!${envPrefix}_@}); do + key="$(echo "$var" | sed -e 's/^'${envPrefix}'_//g' -e 's/___/-/g' -e 's/__/./g' | tr '[:upper:]' '[:lower:]')" + value="${!var}" + LOG_D " ${key}: ${value}" + app_common_conf_set "${PG_CONF_FILE}" "$key" "$value" + done } # 将变量配置更新至配置文件 @@ -41,52 +69,362 @@ postgresql_disable_nss_wrapper() { # $1 - 文件 # $2 - 变量 # $3 - 值(列表) -postgresql_common_conf_set() { +app_common_conf_set() { local file="${1:?missing file}" local key="${2:?missing key}" local value="${3:?missing value}" - if grep -q "^#*\s*${key}" "$file" >/dev/null; then - replace_in_file "$file" "^#*\s*${key}\s*=.*" "${key} = '${value}'" false + if grep -q "^#*${key}" "$file" >/dev/null; then + replace_in_file "$file" "^#*${key}\s*=.*$" "${key} = '${value}'" false else - echo "${key} = '${value}'" >>"$file" + echo "\n${key} = '${value}'" >>"$file" fi } -# 更新 postgresql.conf 配置文件中指定变量值 -# 变量: -# $1 - 变量 -# $2 - 值(列表) -postgresql_conf_set() { - postgresql_common_conf_set "${PG_CONF_FILE}" "$@" +# 使用环境变量中配置,更新配置文件 +app_update_conf() { + LOG_I "Update configure files from PG_CFG_*..." + + if (( PG_SYNCHRONOUS_REPLICAS_NUM > 0 )); then + app_common_conf_set "${PG_CONF_FILE}" "synchronous_standby_names" "${PG_SYNCHRONOUS_REPLICAS_METHOD} ${PG_SYNCHRONOUS_REPLICAS_NUM} (\"${PG_SYNCHRONOUS_REPLICAS_NAMES}\")" + fi + + [[ "${PG_REPLICATION_MODE}" == "standby" ]] && pg_configure_recovery + + app_configure_from_environment "${PG_CONF_FILE}" "PG_CFG" + + pg_update_hba_conf } -# 更新 pg_hba.conf 配置文件中指定变量值 -# 变量: -# $1 - 变量 -# $2 - 值(列表) -postgresql_hba_set() { - replace_in_file "${PG_HBA_FILE}" "${1}" "${2}" false +# 检测用户参数信息是否满足条件; 针对部分权限过于开放情况,打印提示信息 +app_verify_minimum_env() { + local error_code=0 + + LOG_D "Validating settings in ENV vars..." + + print_validation_error() { + LOG_E "$1" + error_code=1 + } + + empty_password_error() { + print_validation_error "The $1 environment variable is empty or not set. Set the environment variable ALLOW_ANONYMOUS=yes to allow the container to be started with blank passwords. This is recommended only for development." + } + + # 检测认证设置。如果不允许匿名登录,检测登录用户名及密码是否设置 + if is_boolean_yes "${ALLOW_ANONYMOUS}"; then + LOG_W "You have set the environment variable ALLOW_ANONYMOUS=${ALLOW_ANONYMOUS}. For safety reasons, do not use this flag in a production environment." + else + if [[ -z "${PG_PASSWORD}" ]]; then + empty_password_error "{PG_PASSWORD}" + fi + if (( ${#PG_PASSWORD} > 100 )); then + print_validation_error "The password cannot be longer than 100 characters. Set the environment variable PG_PASSWORD with a shorter value" + fi + if [[ -n "${PG_USERNAME}" ]] && [[ -z "${PG_PASSWORD}" ]]; then + empty_password_error "{PG_PASSWORD}" + fi + if [[ -n "${PG_USERNAME}" ]] && [[ "${PG_USERNAME}" != "postgres" ]] && [[ -n "${PG_PASSWORD}" ]] && [[ -z "${PG_DATABASE}" ]]; then + print_validation_error "In order to use a custom PostgreSQL user you need to set the environment variable PG_DATABASE as well" + fi + fi + + if [[ "${PG_REPLICATION_MODE}" = "primary" ]]; then + if (( PG_SYNCHRONOUS_REPLICAS_NUM < 0 )); then + print_validation_error "The number of synchronous replicas cannot be less than 0. Set the environment variable PG_SYNCHRONOUS_REPLICAS_NUM" + fi + elif [[ "${PG_REPLICATION_MODE}" = "standby" ]]; then + if [[ -z "${PG_REPLICATION_HOST}" ]]; then + print_validation_error "Slave replication mode chosen without setting the environment variable PG_REPLICATION_HOST. Use it to indicate where the Master node is running" + fi + if [[ -z "${PG_REPLICATION_USER}" ]]; then + print_validation_error "Slave replication mode chosen without setting the environment variable PG_REPLICATION_USER. Make sure that the primary also has this parameter set" + fi + if [[ -n "${PG_REPLICATION_USER}" ]] && [[ -z "${PG_REPLICATION_PASSWORD}" ]]; then + empty_password_error "{PG_REPLICATION_PASSWORD}" + fi + else + print_validation_error "Invalid replication mode. Available options are 'primary/standby'" + fi + + if is_boolean_yes "${PG_ENABLE_LDAP}" && [[ -n "${PG_LDAP_URL}" ]] && [[ -n "${PG_LDAP_SERVER}" ]]; then + empty_password_error "You can not set PG_LDAP_URL and PG_LDAP_SERVER at the same time. Check your LDAP configuration." + fi + + if [[ "${PG_CFG_SSL:-off}" == "on" ]]; then + if [[ -z "${PG_CFG_ssl_cert_file:-}" ]]; then + print_validation_error "You must provide a X.509 certificate in order to use TLS" + elif [[ ! -f "${PG_CFG_ssl_cert_file}" ]]; then + print_validation_error "The X.509 certificate file in the specified path ${PG_CFG_ssl_cert_file} does not exist" + fi + if [[ -z "${PG_CFG_ssl_key_file:-}" ]]; then + print_validation_error "You must provide a private key in order to use TLS" + elif [[ ! -f "${PG_CFG_ssl_key_file}" ]]; then + print_validation_error "The private key file in the specified path ${PG_CFG_ssl_key_file} does not exist" + fi + if [[ -z "${PG_CFG_ssl_ca_file:-}" ]]; then + LOG_W "A CA X.509 certificate was not provided. Client verification will not be performed in TLS connections" + elif [[ ! -f "${PG_CFG_ssl_ca_file}" ]]; then + print_validation_error "The CA X.509 certificate file in the specified path ${PG_CFG_ssl_ca_file} does not exist" + fi + if [[ -n "${PG_CFG_ssl_crl_file:-}" ]] && [[ ! -f "${PG_CFG_ssl_crl_file}" ]]; then + print_validation_error "The CRL file in the specified path ${PG_CFG_ssl_crl_file} does not exist" + fi + if ! is_yes_no_value "${PG_CFG_ssl_prefer_server_ciphers:-}"; then + print_validation_error "The values allowed for prefer_server_ciphers are: on or off" + fi + fi + + [[ "$error_code" -eq 0 ]] || exit "$error_code" } -# 更新 pg_ident.conf 配置文件中指定变量值 -# 变量: -# $1 - 变量 -# $2 - 值(列表) -postgresql_ident_set() { - postgresql_common_conf_set "${PG_IDENT_FILE}" "$@" +# 更改默认监听地址为 "*" 或 "0.0.0.0",以对容器外提供服务 +app_enable_remote_connections() { + LOG_I "Modify default config to ENABLE external IP access" + + app_common_conf_set "${PG_CONF_FILE}" "listen_addresses" "0.0.0.0" } -# 更新 recover.conf 配置文件中指定变量值 -# 变量: -# $1 - 变量 -# $2 - 值(列表) -postgresql_recover_set() { - postgresql_common_conf_set "${PG_RECOVERY_FILE}" "$@" +# 更改默认监听地址为 "localhost" 或 "127.0.0.1",以禁止对容器外提供服务 +app_disable_remote_connections() { + LOG_I "Modify default config to DISABLE external IP access" + + app_common_conf_set "${PG_CONF_FILE}" "listen_addresses" "127.0.0.1" } -# 初始化 pg_hba.conf 文件,增加 LDAP 配置;同时保留本地认证 -postgresql_hba_ldap_auth() { +# 以后台方式启动应用服务,并等待启动就绪 +app_start_server_bg() { + LOG_I "Starting ${APP_NAME} in background..." + + # 使用 pg_ctl 命令,以服务方式启动 PostgreSQL + local pg_ctl_cmd=$(command -v pg_ctl) + local pg_ctl_flags=("-D" "${PGDATA}") + pg_ctl_flags+=("-l" "${PG_CFG_LOG_DIRECTORY}/postgres_init.log") + #pg_ctl_flags+=("-o" "'-c listen_addresses=127.0.0.1 -p 5432'") + pg_ctl_flags+=("-w" "-t" "30") + + LOG_D " extra arguments: ${pg_ctl_flags[@]}" + debug_execute "${pg_ctl_cmd}" "start" "${pg_ctl_flags[@]}" +} + +# 停止应用服务 +app_stop_server() { + LOG_I "Stopping ${APP_NAME} background service..." + + # 使用 pg_ctl 命令关闭服务 + local pg_ctl_cmd=$(command -v pg_ctl) + local pg_ctl_flags=("-D" "${PGDATA}") + pg_ctl_flags+=("-m" "fast") + pg_ctl_flags+=("-w" "-t" "30") + + LOG_D " extra arguments: ${pg_ctl_flags[@]}" + debug_execute "${pg_ctl_cmd}" "stop" "${pg_ctl_flags[@]}" +} + +# 检测应用服务是否在后台运行中 +app_is_server_running() { + local pid + pid="$(get_pid_from_file ${PG_CFG_EXTERNAL_PID_FILE})" + LOG_D "Checking ${APP_NAME} running status with PID: ${pid}" + + if [[ -n "${pid}" ]]; then + is_service_running "${pid}" + else + false + fi +} + +app_is_server_not_running() { + if [[ app_is_server_running == false ]]; then + true + else + false + fi +} + +# 清理初始化应用时生成的临时文件 +app_clean_tmp_file() { + LOG_D "Clean ${APP_NAME} tmp files ..." + + local -r -a files=( + "${PGDATA}/postmaster.pid" + "${PG_CFG_EXTERNAL_PID_FILE}" + ) + + for file in ${files[@]}; do + if [[ -f "$file" ]]; then + LOG_D " Remove $file" + rm -rf "$file" + fi + done +} + +# 用户自定义的前置初始化操作,依次执行目录 preinit.d 中的初始化脚本 +# 执行完毕后,生成以当前时间命名的'.tar'的文件,包含执行记录、已执行的脚本,同时删除已执行的脚本 +app_custom_preinit() { + LOG_I "Process pre-init for ${APP_NAME}..." + + # 检测用户配置文件目录是否存在 preinit.d 文件夹,如果存在,尝试执行目录中的初始化脚本 + if [[ -d "${APP_CONF_DIR}/preinit.d" ]] && [[ -n $(find "${APP_CONF_DIR}/preinit.d/" -type f -regex ".*\.\(sh\)") ]]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') : Init success." > "${APP_CONF_DIR}/preinit.d/preinit_info" + local tarFile="preinit-$(date '+%Y-%m-%d-%H%M%S').tar" + tar --remove-files -cf "${tarFile}" "${APP_CONF_DIR}/preinit.d/preinit_info" + + for f in $(find "${APP_CONF_DIR}/preinit.d/" -type f -regex ".*\.\(sh\)" | sort); do + LOG_D " Process: ${f}" + process_init_files "${f}" + tar --remove-files -f "${tarFile}" -r "${f}" + done + fi +} + +# 应用默认初始化操作 +app_default_init() { + LOG_I "Process default init for ${APP_NAME}..." + app_clean_tmp_file + + if is_dir_empty "${PGDATA}"; then + LOG_I "Deploying ${APP_NAME} from scratch..." + + if [[ "${PG_REPLICATION_MODE}" == "primary" ]]; then + # 使用 initdb 工具初始化集群仓库(包含配置文件的初始化) + pg_primary_init_db + + app_disable_remote_connections + app_start_server_bg + + [[ "${PG_DATABASE}" != "postgres" ]] && pg_create_custom_database + + # 为数据库授权;默认用户不为 postgres 时,需要创建管理员账户 + LOG_D "Set password for postgres user" + if [[ "${PG_USERNAME}" == "postgres" ]]; then + [[ -n "${PG_PASSWORD}" ]] && pg_alter_postgres_user "${PG_PASSWORD}" + else + [[ -n "${PG_POSTGRES_PASSWORD}" ]] && pg_alter_postgres_user "${PG_POSTGRES_PASSWORD}" + pg_create_admin_user + fi + [[ -n "${PG_REPLICATION_USER:-}" ]] && pg_create_replication_user + + app_stop_server + app_enable_remote_connections + else + pg_standby_init_db + fi + elif [[ -f "${PGDATA}/PG_VERSION" ]]; then + local -r db_version="$(< "${PGDATA}/PG_VERSION")" + local -r curr_version="$(pg_get_major_version)" + + if [[ "$db_version" -ne "$curr_version" ]]; then + LOG_E "PGDATA directory is not empty with INCORRECT version, application version is $curr_version, while DB version is $db_version" + exit 1 + fi + + LOG_I "Deploying ${APP_NAME} with persisted data..." + else + LOG_E "${PGDATA} not empty" + exit 1 + fi + + # 根据环境变量配置,更新 postgresql.conf / pg_hba.conf / pg_ident.conf 等 + # 注意:如果在数据库客户端内使用 ALTER 更新配置,则存储在 postgresql.auto.conf 文件中,优先级高于 postgresql.conf + app_update_conf +} + +# 用户自定义的应用初始化操作,依次执行目录 initdb.d 中的初始化脚本 +# 执行完毕后,生成以当前时间命名的'.tar'的文件,包含执行记录、已执行的脚本,同时删除已执行的脚本 +app_custom_init() { + LOG_I "Process DB-init for ${APP_NAME}..." + + # 检测用户配置文件目录是否存在 preinit.d 文件夹,如果存在,尝试执行目录中的初始化脚本 + if [[ -d "${APP_CONF_DIR}/initdb.d" ]] && [[ -n $(find "${APP_CONF_DIR}/initdb.d/" -type f -regex ".*\.\(sh\|sql\|sql.gz\)") ]]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') : Init success." > "${APP_CONF_DIR}/initdb.d/dbinit_info" + local tarFile="initdb-$(date '+%Y-%m-%d-%H%M%S').tar" + tar --remove-files -cf "${tarFile}" "${APP_CONF_DIR}/initdb.d/initdb_info" + + # 禁用远程访问,并启动后台服务 + app_disable_remote_connections + app_start_server_bg + + for f in $(find "${APP_CONF_DIR}/initdb.d/" -type f -regex ".*\.\(sh\|sql\|sql.gz\)" | sort); do + case "$f" in + *.sh) + LOG_D " Process Shell: ${f}" + process_init_files "$f" + ;; + *.sql) + LOG_D " Process SQL: ${f}"; + pg_execute "${PG_DATABASE}" "${PG_INITSCRIPTS_USERNAME}" "${PG_INITSCRIPTS_PASSWORD}" < "$f" + ;; + *.sql.gz) + LOG_D " Process SQLs in ${f}"; + gunzip -c "$f" | pg_execute "${PG_DATABASE}" "${PG_INITSCRIPTS_USERNAME}" "${PG_INITSCRIPTS_PASSWORD}" + ;; + *) + LOG_D " Ignoring $f" + ;; + esac + + tar --remove-files -f "${tarFile}" -r "${f}" + done + + # 停止后台服务,并启用远程访问 + app_stop_server + app_clean_tmp_file + app_enable_remote_connections + fi +} + +# 获取软件主版本号 +pg_get_major_version() { + psql --version | grep -oE "[0-9]+\.[0-9]+" | grep -oE "^[0-9]+" +} + +# 根据环境变量,更新 HBA 配置文件 +pg_update_hba_conf() { + LOG_I "Update pg_hba.conf with environment values..." + + # 默认生成的配置中: + # - 默认数据库,local / host 访问策略为 trust,host IP range 为 127.0.0.1/32 及 ::1/128 + # - replication 数据库,,local / host 访问策略为 trust,host IP range 为 127.0.0.1/32 及 ::1/128 + + pg_hba_update_host_auth + pg_hba_update_replication_auth + + if is_boolean_yes "${PG_ENABLE_LDAP}"; then + pg_hba_allow_ldap_auth + fi + + if [[ "${PG_CFG_SSL:-off}" == "on" ]]; then + pg_hba_allow_tls_connection + fi +} + +# 更新 pg_hba.conf 文件,所有数据库仅允许基于密码认证的访问 +pg_hba_update_host_auth() { + LOG_D " Update default host auth configs..." + + local host_auth="trust" + if [[ -n "${PG_PASSWORD}" ]]; then + host_auth="md5" + fi + sed -i -E "s@(host)\s+(all)\s+(all)\s+([0-9\.\/]+)\s+.*@\1\t\2\t\3\t${PG_USER_IP4_RANGE}\t${host_auth}@g" "${PG_CFG_HBA_FILE}" + sed -i -E "s@(host)\s+(all)\s+(all)\s+([a-z0-9\/\:]+)\s+.*@\1\t\2\t\3\t${PG_USER_IP6_RANGE}\t${host_auth}@g" "${PG_CFG_HBA_FILE}" +} + +# 更新 pg_hba.conf 文件,replication 仅允许基于密码认证的访问 +pg_hba_update_replication_auth() { + LOG_D " Update replication host auth configs..." + + local replication_auth="trust" + if [[ -n "${PG_REPLICATION_PASSWORD:-}" ]]; then + replication_auth="md5" + fi + sed -i -E "s@(host)\s+(replication)\s+(all)\s+([0-9\.\/]+)\s+.*@\1\t\2\t\3\t${PG_REPLICATION_IP4_RANGE}\t${replication_auth}@g" "${PG_CFG_HBA_FILE}" + sed -i -E "s@(host)\s+(replication)\s+(all)\s+([a-z0-9\/\:]+)\s+.*@\1\t\2\t\3\t${PG_REPLICATION_IP6_RANGE}\t${replication_auth}@g" "${PG_CFG_HBA_FILE}" +} + +# 更新 pg_hba.conf 文件,增加 LDAP 配置;同时增加 postgres 用户的本地认证 +pg_hba_allow_ldap_auth() { LOG_I "Enabling LDAP authentication" local ldap_configuration="" @@ -107,57 +445,88 @@ postgresql_hba_ldap_auth() { [[ -n "${PG_LDAP_SCHEME}" ]] && ldap_configuration+=" ldapscheme=${PG_LDAP_SCHEME}" fi - cat <"${PG_HBA_FILE}" -host all postgres 0.0.0.0/0 trust -host all postgres ::/0 trust -host all all 0.0.0.0/0 ldap ${ldap_configuration} -host all all ::/0 ldap ${ldap_configuration} + sed -i -E "s@(host)\s+(all)\s+(all)\s+([0-9\.\/]+)\s+.*@\1\t\2\t\3\t${PG_REPLICATION_IP4_RANGE}\tldap ${ldap_configuration}@g" "${PG_CFG_HBA_FILE}" + sed -i -E "s@(host)\s+(all)\s+(all)\s+([a-z0-9\/\:]+)\s+.*@\1\t\2\t\3\t${PG_REPLICATION_IP6_RANGE}\tldap ${ldap_configuration}@g" "${PG_CFG_HBA_FILE}" + + LOG_D " Add new postgres authentication when auth with LDAP" + local postgres_auth="trust" + if [[ -n "${PG_POSTGRES_PASSWORD}" ]]; then + postgres_auth="md5" + fi + if ! $(grep -iE "(host)\s+(all)\s+(postgres).*" "${PG_CFG_HBA_FILE}"); then + cat <"${PG_CFG_HBA_FILE}" + + host all postgres ${PG_REPLICATION_IP4_RANGE} ${postgres_auth} + host all postgres ${PG_REPLICATION_IP6_RANGE} ${postgres_auth} EOF + fi } # 设置 pg_hba.conf 文件,增加 TLS 配置 -postgresql_hba_allow_tls_connection() { +pg_hba_allow_tls_connection() { LOG_I "Enabling TLS client authentication" - cat <>"${PG_HBA_FILE}" -hostssl all all 0.0.0.0/0 cert -hostssl all all ::/0 cert + cat <<"EOF" >>"${PG_CFG_HBA_FILE}" + + hostssl all all ${PG_USER_IP4_RANGE} cert + hostssl all all ${PG_USER_IP6_RANGE} cert EOF } -# 设置 pg_hba.conf 文件,允许 replication 访问 -postgresql_hba_allow_replication_connection() { - LOG_I "Enabling replication client authentication" +# 为 Slava 模式工作的节点创建 recovery.conf 文件 +pg_configure_recovery() { + LOG_I "Setting up streaming replication standby..." - local replication_auth="trust" - if [[ -n "${PG_REPLICATION_PASSWORD}" ]]; then - replication_auth="md5" + # Recover 配置信息在不同版本保存位置不一样: + # 版本为12及以上时, Slave 节点配置保存在 postgresql.conf 文件中 + # 版本低于12时, Slave 节点配置保存在 recover.conf 文件中 + app_common_conf_set "${PG_CONF_FILE}" "primary_conninfo" "host=${PG_REPLICATION_HOST} port=${PG_REPLICATION_PORT} user=${PG_REPLICATION_USER} password=${PG_REPLICATION_PASSWORD} application_name=${PG_REPLICATION_APP_NAME} connect_timeout=${PG_REPLICATION_CONNECT_TIMEOUT}" + app_common_conf_set "${PG_CONF_FILE}" "promote_trigger_file" "/tmp/postgresql.trigger.${PG_REPLICATION_PORT}" + touch "${PGDATA}/standby.signal" +} + +# 为默认的数据库用户 postgres 设置密码 +# 参数: +# $1 - 用户密码 +pg_alter_postgres_user() { + local -r escaped_password="${1//\'/\'\'}" + LOG_I "Changing password of postgres" + + echo "ALTER ROLE postgres WITH PASSWORD '$escaped_password';" | pg_execute + if [[ -n "${PG_DB_CONNECTION_LIMIT}" ]]; then + echo "ALTER ROLE postgres WITH CONNECTION LIMIT ${PG_DB_CONNECTION_LIMIT};" | pg_execute fi - cat <>"${PG_HBA_FILE}" -host replication all 0.0.0.0/0 ${replication_auth} -host replication all ::/0 ${replication_auth} -EOF } -# 设置 pg_hba.conf,允许本地访问 -postgresql_hba_allow_local_connection() { - LOG_I "Enabling local client authentication" +# 为数据库 $PG_DATABASE 创建管理员账户 +pg_create_admin_user() { + local -r escaped_password="${PG_PASSWORD//\'/\'\'}" - cat <>"${PG_HBA_FILE}" -local all all trust -host all all 127.0.0.1/0 trust -host all all ::1/128 trust -EOF + local connlimit_string="" + if [[ -n "${PG_USER_CONNECTION_LIMIT}" ]]; then + connlimit_string="CONNECTION LIMIT ${PG_USER_CONNECTION_LIMIT}" + fi + + LOG_I "Creating user ${PG_USERNAME}" + echo "CREATE ROLE \"${PG_USERNAME}\" WITH LOGIN ${connlimit_string} CREATEDB PASSWORD '${escaped_password}';" | pg_execute + + LOG_I "Granting access to \"${PG_USERNAME}\" to the database \"${PG_DATABASE}\"" + echo "GRANT ALL PRIVILEGES ON DATABASE \"${PG_DATABASE}\" TO \"${PG_USERNAME}\"\;" | pg_execute "" "postgres" "${PG_POSTGRES_PASSWORD}" } -# 初始化 pg_hba.conf 文件 -postgresql_hba_password_auth() { - LOG_I "Enabling password client authentication" +# 为 primary-standby 复制模式创建用户 +pg_create_replication_user() { + local -r escaped_password="${PG_REPLICATION_PASSWORD//\'/\'\'}" + LOG_I "Creating replication user ${PG_REPLICATION_USER}" - cat <"${PG_HBA_FILE}" -host all all 0.0.0.0/0 trust -host all all ::/0 trust -EOF + echo "CREATE ROLE \"${PG_REPLICATION_USER}\" WITH REPLICATION LOGIN ENCRYPTED PASSWORD '$escaped_password'" | pg_execute +} + +# 创建用户自定义数据库 $PG_DATABASE +pg_create_custom_database() { + LOG_I "Creating custom database ${PG_DATABASE}" + + echo "CREATE DATABASE \"${PG_DATABASE}\"" | pg_execute "" "postgres" "" "localhost" } # 使用运行中的 PostgreSQL 服务执行 SQL 操作 @@ -168,7 +537,7 @@ EOF # $4 - 主机 # $5 - 端口 # $6 - 扩展参数 (如: -tA) -postgresql_execute() { +pg_execute() { local -r db="${1:-}" local -r user="${2:-postgres}" local -r pass="${3:-}" @@ -176,591 +545,42 @@ postgresql_execute() { local -r port="${5:-${PG_PORT_NUMBER}}" local -r opts="${6:-}" + local cmd="$(command -v psql)" local args=("-h" "$host" "-p" "$port" "-U" "$user") - local cmd=("${APP_HOME_DIR}/bin/psql") [[ -n "$db" ]] && args+=("-d" "$db") [[ -n "$opts" ]] && args+=("$opts") + LOG_D "Execute args: ${args[@]}" - if is_boolean_yes "${ENV_DEBUG}"; then - PGPASSWORD=$pass "${cmd[@]}" "${args[@]}" - else - PGPASSWORD=$pass "${cmd[@]}" "${args[@]}" >/dev/null 2>&1 - fi -} - -# 使用环境变量中的配置值更新配置文件 -postgresql_configure_from_environment_variables() { - LOG_D "Modify postgresql.conf with PG_CFG_* values..." - for var in "${!PG_CFG_@}"; do - key="$(echo "$var" | sed -e 's/^PG_CFG_//g' | sed -e 's/___/-/g' | sed -e 's/__/./g' | tr '[:upper:]' '[:lower:]')" - value="${!var}" - postgresql_conf_set "$key" "$value" - done -} - -# 生成初始 postgres.conf 配置 -postgresql_default_postgresql_config() { - LOG_I "Modify postgresql.conf with default values..." - - [ ! -e "${PG_CONF_FILE}" ] && cp -rf "${APP_HOME_DIR}/share/postgresql.conf.sample" "${PG_CONF_FILE}" - - postgresql_conf_set "logging_collector" "on" - postgresql_conf_set "wal_level" "hot_standby" - postgresql_conf_set "max_wal_size" "400MB" - postgresql_conf_set "max_wal_senders" "16" - postgresql_conf_set "wal_keep_segments" "12" - postgresql_conf_set "wal_log_hints" "on" - postgresql_conf_set "hot_standby" "on" - if (( PG_NUM_SYNCHRONOUS_REPLICAS > 0 )); then - postgresql_conf_set "synchronous_commit" "${PG_SYNCHRONOUS_COMMIT_MODE}" - postgresql_conf_set "synchronous_standby_names" "${PG_NUM_SYNCHRONOUS_REPLICAS} (\"${PG_CLUSTER_APP_NAME}\")" - fi - postgresql_conf_set "fsync" "${PG_FSYNC}" - - [[ -n "${PG_SHARED_PRELOAD_LIBRARIES}" ]] && postgresql_conf_set "shared_preload_libraries" "${PG_SHARED_PRELOAD_LIBRARIES}" - - postgresql_conf_set "include_dir" "conf.d" - mkdir -p "${APP_CONF_DIR}/conf.d" - - postgresql_configure_from_environment_variables -} - -# 生成初始 pg_hba.conf 配置 -postgresql_default_hba_config() { - LOG_I "Modify pg_hba.conf with default values..." - - if is_boolean_yes "${PG_ENABLE_LDAP}"; then - postgresql_hba_ldap_auth - else - postgresql_hba_password_auth - fi -} - -# 更新 pg_hba.conf 文件,仅允许基于密码认证的访问 -postgresql_restrict_hba_config() { - LOG_I "Check pg_hba.conf for restrict configs..." - - if [[ -n "${PG_PASSWORD}" ]]; then - LOG_D " Configuring md5 encrypt" - postgresql_hba_set "trust" "md5" - fi -} - -# 获取软件主版本号 -postgresql_get_major_version() { - psql --version | grep -oE "[0-9]+\.[0-9]+" | grep -oE "^[0-9]+" -} - -# 为 Slava 模式工作的节点创建 recovery.conf 文件 -postgresql_configure_recovery() { - LOG_I "Setting up streaming replication standby..." - - # Recover 配置信息在不同版本保存位置不一样: - # 版本为12及以上时, Slave 节点配置保存在 postgresql.conf 文件中 - # 版本低于12时, Slave 节点配置保存在 recover.conf 文件中 - local -r psql_major_version="$(postgresql_get_major_version)" - if (( psql_major_version >= 12 )); then - postgresql_conf_set "primary_conninfo" "host=${PG_PRIMARY_HOST} port=${PG_PRIMARY_PORT} user=${PG_REPLICATION_USER} password=${PG_REPLICATION_PASSWORD} application_name=${PG_CLUSTER_APP_NAME}" - postgresql_conf_set "promote_trigger_file" "/tmp/postgresql.trigger.${PG_PRIMARY_PORT}" - touch "${PG_DATA_DIR}/standby.signal" - else - [ ! -e "${PG_RECOVERY_FILE}" ] && cp -f "${APP_HOME_DIR}/share/recovery.conf.sample" "${PG_RECOVERY_FILE}" - chmod 600 "${PG_RECOVERY_FILE}" - postgresql_recover_set "standby_mode" "on" - postgresql_recover_set "primary_conninfo" "host=${PG_PRIMARY_HOST} port=${PG_PRIMARY_PORT} user=${PG_REPLICATION_USER} password=${PG_REPLICATION_PASSWORD} application_name=${PG_CLUSTER_APP_NAME}" - postgresql_recover_set "trigger_file" "/tmp/postgresql.trigger.${PG_PRIMARY_PORT}" - fi -} - -# 配置应用日志参数 -postgresql_configure_logging() { - LOG_I "Update logging configuration..." - - [[ -n "${PG_PGAUDIT_LOG}" ]] && postgresql_conf_set "pgaudit.log" "${PG_PGAUDIT_LOG}" - [[ -n "${PG_PGAUDIT_LOG_CATALOG}" ]] && postgresql_conf_set "pgaudit.log_catalog" "${PG_PGAUDIT_LOG_CATALOG}" - [[ -n "${PG_LOG_CONNECTIONS}" ]] && postgresql_conf_set "log_connections" "${PG_LOG_CONNECTIONS}" - [[ -n "${PG_LOG_DISCONNECTIONS}" ]] && postgresql_conf_set "log_disconnections" "${PG_LOG_DISCONNECTIONS}" - [[ -n "${PG_LOG_HOSTNAME}" ]] && postgresql_conf_set "log_hostname" "${PG_LOG_HOSTNAME}" - [[ -n "${PG_CLIENT_MIN_MESSAGES}" ]] && postgresql_conf_set "client_min_messages" "${PG_CLIENT_MIN_MESSAGES}" - [[ -n "${PG_LOG_LINE_PREFIX}" ]] && postgresql_conf_set "log_line_prefix" "${PG_LOG_LINE_PREFIX}" - ([[ -n "${PG_LOG_TIMEZONE}" ]] && postgresql_conf_set "log_timezone" "${PG_LOG_TIMEZONE}") || true -} - -# 配置应用连接控制参数 -postgresql_configure_connections() { - LOG_I "Update TCP connection configuration..." - - [[ -n "${PG_MAX_CONNECTIONS}" ]] && postgresql_conf_set "max_connections" "${PG_MAX_CONNECTIONS}" - [[ -n "${PG_TCP_KEEPALIVES_IDLE}" ]] && postgresql_conf_set "tcp_keepalives_idle" "${PG_TCP_KEEPALIVES_IDLE}" - [[ -n "${PG_TCP_KEEPALIVES_INTERVAL}" ]] && postgresql_conf_set "tcp_keepalives_interval" "${PG_TCP_KEEPALIVES_INTERVAL}" - [[ -n "${PG_TCP_KEEPALIVES_COUNT}" ]] && postgresql_conf_set "tcp_keepalives_count" "${PG_TCP_KEEPALIVES_COUNT}" - ([[ -n "${PG_STATEMENT_TIMEOUT}" ]] && postgresql_conf_set "statement_timeout" "${PG_STATEMENT_TIMEOUT}") || true -} - -# 配置应用 TLS 参数 -postgresql_configure_tls() { - LOG_I "Update TLS configuration..." - - chmod 600 "${PG_TLS_KEY_FILE}" || LOG_W "Could not set compulsory permissions (600) on file ${PG_TLS_KEY_FILE}" - postgresql_conf_set "ssl" "on" - ! is_boolean_yes "${PG_TLS_PREFER_SERVER_CIPHERS}" && postgresql_conf_set "ssl_prefer_server_ciphers" "off" - [[ -n "${PG_TLS_CA_FILE}" ]] && postgresql_conf_set "ssl_ca_file" "${PG_TLS_CA_FILE}" - [[ -n "${PG_TLS_CRL_FILE}" ]] && postgresql_conf_set "ssl_crl_file" "${PG_TLS_CRL_FILE}" - postgresql_conf_set "ssl_cert_file" "${PG_TLS_CERT_FILE}" - postgresql_conf_set "ssl_key_file" "${PG_TLS_KEY_FILE}" -} - -# 为默认的数据库用户 postgres 设置密码 -# 参数: -# $1 - 用户密码 -postgresql_alter_postgres_user() { - local -r escaped_password="${1//\'/\'\'}" - LOG_I "Changing password of postgres" - - echo "ALTER ROLE postgres WITH PASSWORD '$escaped_password';" | postgresql_execute - if [[ -n "${PG_POSTGRES_CONNECTION_LIMIT}" ]]; then - echo "ALTER ROLE postgres WITH CONNECTION LIMIT ${PG_POSTGRES_CONNECTION_LIMIT};" | postgresql_execute - fi -} - -# 为数据库 $PG_DATABASE 创建管理员账户 -postgresql_create_admin_user() { - local -r escaped_password="${PG_PASSWORD//\'/\'\'}" - - local connlimit_string="" - if [[ -n "${PG_USERNAME_CONNECTION_LIMIT}" ]]; then - connlimit_string="CONNECTION LIMIT ${PG_USERNAME_CONNECTION_LIMIT}" - fi - - LOG_I "Creating user ${PG_USERNAME}" - echo "CREATE ROLE \"${PG_USERNAME}\" WITH LOGIN ${connlimit_string} CREATEDB PASSWORD '${escaped_password}';" | postgresql_execute - - LOG_I "Granting access to \"${PG_USERNAME}\" to the database \"${PG_DATABASE}\"" - echo "GRANT ALL PRIVILEGES ON DATABASE \"${PG_DATABASE}\" TO \"${PG_USERNAME}\"\;" | postgresql_execute "" "postgres" "${PG_POSTGRES_PASSWORD}" -} - -# 为 primary-standby 复制模式创建用户 -postgresql_create_replication_user() { - local -r escaped_password="${PG_REPLICATION_PASSWORD//\'/\'\'}" - LOG_I "Creating replication user ${PG_REPLICATION_USER}" - - echo "CREATE ROLE \"${PG_REPLICATION_USER}\" WITH REPLICATION LOGIN ENCRYPTED PASSWORD '$escaped_password'" | postgresql_execute -} - -# 创建用户自定义数据库 $PG_DATABASE -postgresql_create_custom_database() { - LOG_I "Creating custom database ${PG_DATABASE}" - - echo "CREATE DATABASE \"${PG_DATABASE}\"" | postgresql_execute "" "postgres" "" "localhost" -} - -# 检测用户参数信息是否满足条件; 针对部分权限过于开放情况,打印提示信息 -postgresql_verify_minimum_env() { - local error_code=0 - LOG_D "Validating settings in PG_* env vars..." - - print_validation_error() { - LOG_E "$1" - error_code=1 - } - - # 检测认证设置。如果不允许匿名登录,检测登录用户名及密码是否设置 - empty_password_warn() { - LOG_W "You set the environment variable ALLOW_ANONYMOUS_LOGIN=${ALLOW_ANONYMOUS_LOGIN}. For safety reasons, do not use this flag in a production environment." - } - empty_password_error() { - print_validation_error "The $1 environment variable is empty or not set. Set the environment variable ALLOW_ANONYMOUS_LOGIN=yes to allow the container to be started with blank passwords. This is recommended only for development." - } - - if is_boolean_yes "${ALLOW_ANONYMOUS_LOGIN}"; then - empty_password_warn - else - if [[ -z "${PG_PASSWORD}" ]]; then - empty_password_error "{PG_PASSWORD}" - fi - if (( ${#PG_PASSWORD} > 100 )); then - print_validation_error "The password cannot be longer than 100 characters. Set the environment variable PG_PASSWORD with a shorter value" - fi - if [[ -n "${PG_USERNAME}" ]] && [[ -z "${PG_PASSWORD}" ]]; then - empty_password_error "{PG_PASSWORD}" - fi - if [[ -n "${PG_USERNAME}" ]] && [[ "${PG_USERNAME}" != "postgres" ]] && [[ -n "${PG_PASSWORD}" ]] && [[ -z "${PG_DATABASE}" ]]; then - print_validation_error "In order to use a custom PostgreSQL user you need to set the environment variable PG_DATABASE as well" - fi - fi - - if [[ -n "${PG_REPLICATION_MODE}" ]]; then - if [[ "${PG_REPLICATION_MODE}" = "primary" ]]; then - if (( PG_NUM_SYNCHRONOUS_REPLICAS < 0 )); then - print_validation_error "The number of synchronous replicas cannot be less than 0. Set the environment variable PG_NUM_SYNCHRONOUS_REPLICAS" - fi - elif [[ "${PG_REPLICATION_MODE}" = "standby" ]]; then - if [[ -z "${PG_PRIMARY_HOST}" ]]; then - print_validation_error "Slave replication mode chosen without setting the environment variable PG_PRIMARY_HOST. Use it to indicate where the Master node is running" - fi - if [[ -z "${PG_REPLICATION_USER}" ]]; then - print_validation_error "Slave replication mode chosen without setting the environment variable PG_REPLICATION_USER. Make sure that the primary also has this parameter set" - fi - else - print_validation_error "Invalid replication mode. Available options are 'primary/standby'" - fi - # Common replication checks - if [[ -n "${PG_REPLICATION_USER}" ]] && [[ -z "${PG_REPLICATION_PASSWORD}" ]]; then - empty_password_error "{PG_REPLICATION_PASSWORD}" - fi - else - if is_boolean_yes "${ALLOW_ANONYMOUS_LOGIN}"; then - empty_password_warn - else - if [[ -z "${PG_PASSWORD}" ]]; then - empty_password_error "{PG_PASSWORD}" - fi - if [[ -n "${PG_USERNAME}" ]] && [[ -z "${PG_PASSWORD}" ]]; then - empty_password_error "{PG_PASSWORD}" - fi - fi - fi - - if ! is_yes_no_value "${PG_ENABLE_LDAP}"; then - empty_password_error "The values allowed for PG_ENABLE_LDAP are: yes or no" - fi - - if is_boolean_yes "${PG_ENABLE_LDAP}" && [[ -n "${PG_LDAP_URL}" ]] && [[ -n "${PG_LDAP_SERVER}" ]]; then - empty_password_error "You can not set PG_LDAP_URL and PG_LDAP_SERVER at the same time. Check your LDAP configuration." - fi - - if ! is_yes_no_value "${PG_ENABLE_TLS}"; then - print_validation_error "The values allowed for PG_ENABLE_TLS are: yes or no" - elif is_boolean_yes "${PG_ENABLE_TLS}"; then - if [[ -z "${PG_TLS_CERT_FILE}" ]]; then - print_validation_error "You must provide a X.509 certificate in order to use TLS" - elif [[ ! -f "${PG_TLS_CERT_FILE}" ]]; then - print_validation_error "The X.509 certificate file in the specified path ${PG_TLS_CERT_FILE} does not exist" - fi - if [[ -z "${PG_TLS_KEY_FILE}" ]]; then - print_validation_error "You must provide a private key in order to use TLS" - elif [[ ! -f "${PG_TLS_KEY_FILE}" ]]; then - print_validation_error "The private key file in the specified path ${PG_TLS_KEY_FILE} does not exist" - fi - if [[ -z "${PG_TLS_CA_FILE}" ]]; then - warn "A CA X.509 certificate was not provided. Client verification will not be performed in TLS connections" - elif [[ ! -f "${PG_TLS_CA_FILE}" ]]; then - print_validation_error "The CA X.509 certificate file in the specified path ${PG_TLS_CA_FILE} does not exist" - fi - if [[ -n "${PG_TLS_CRL_FILE}" ]] && [[ ! -f "${PG_TLS_CRL_FILE}" ]]; then - print_validation_error "The CRL file in the specified path ${PG_TLS_CRL_FILE} does not exist" - fi - if ! is_yes_no_value "${PG_TLS_PREFER_SERVER_CIPHERS}"; then - print_validation_error "The values allowed for PG_TLS_PREFER_SERVER_CIPHERS are: yes or no" - fi - fi - - [[ "$error_code" -eq 0 ]] || exit "$error_code" -} - -# 更改默认监听地址为 "*" 或 "0.0.0.0",以对容器外提供服务;默认配置文件应当为仅监听 localhost(127.0.0.1) -postgresql_enable_remote_connections() { - LOG_I "Modify default config to enable all IP access" - - postgresql_conf_set "listen_addresses" "*" -} - -# 以后台方式启动应用服务,并等待启动就绪 -postgresql_start_server_bg() { - postgresql_is_server_running && return - LOG_I "Starting ${APP_NAME} in background..." - - # -w wait until operation completes (default) - # -W don't wait until operation completes - # -D location of the database storage area - # -l write (or append) server log to FILENAME - # -o command line options to pass to postgres or initdb - # --config-file 指定配置文件 - # --external_pid_file 指定 PID 文件,在配置文件中已定义 - # --hba_file 指定 HBA 文件,在配置文件中已定义 - local -r pg_ctl_flags=("-w" "-D" "${PG_DATA_DIR}" "-l" "${PG_LOG_FILE}" "-o" "--config-file=${PG_CONF_FILE} --external_pid_file=${PG_EXT_PID_FILE} --hba_file=${PG_HBA_FILE}") - local pg_ctl_cmd=("${APP_HOME_DIR}/bin/pg_ctl") - if is_boolean_yes "${ENV_DEBUG}"; then - "${pg_ctl_cmd[@]}" "start" "${pg_ctl_flags[@]}" - else - "${pg_ctl_cmd[@]}" "start" "${pg_ctl_flags[@]}" "-s" >/dev/null 2>&1 - fi - - local -r check_args=("-U" "postgres") - local check_cmd=("${APP_HOME_DIR}/bin/pg_isready") - local counter=${PG_INIT_MAX_TIMEOUT} - # 通过命令或特定端口检测应用是否就绪 - LOG_I "Checking ${APP_NAME} ready status..." - while ! "${check_cmd[@]}" "${check_args[@]}" "-q" >/dev/null 2>&1; do - sleep 1 - counter=$(( counter - 1 )) - if (( counter <= 0 )); then - LOG_E "PostgreSQL is not ready after ${PG_INIT_MAX_TIMEOUT} seconds" - exit 1 - fi - LOG_D "PostgreSQL is not ready now: ${counter}" - done - LOG_D "${APP_NAME} is ready for service" -} - -# 停止应用服务 -postgresql_stop_server() { - if postgresql_is_server_running ; then - LOG_I "Stopping background ${APP_NAME}..." - - if is_boolean_yes "${ENV_DEBUG}"; then - PGUSER="postgres" pg_ctl -D "${PG_DATA_DIR}" -m fast -w stop "-s" - else - PGUSER="postgres" pg_ctl -D "${PG_DATA_DIR}" -m fast -w stop "-s" >/dev/null 2>&1 - fi - fi - - # 使用 PID 文件 kill 进程 - #stop_service_using_pid "${PG_EXT_PID_FILE}" -} - -# 检测应用服务是否在后台运行中 -postgresql_is_server_running() { - LOG_D "Check if ${APP_NAME} is running..." - - local pid - pid="$(get_pid_from_file "${PG_EXT_PID_FILE}")" - - if [[ -z "${pid}" ]]; then - false - else - is_service_running "${pid}" - fi -} - -# 清理数据文件 -postgresql_clean_data() { - LOG_D "Clean ${APP_NAME} data files..." - - rm -rf "${PG_DATA_DIR}/*" "${APP_DATA_DIR}/.data_init_flag" -} - -# 在重新启动容器时,删除标志文件及必须删除的临时文件 (容器重新启动) -postgresql_clean_from_restart() { - LOG_D "Clean ${APP_NAME} tmp files for restart..." - - local -r -a files=( - "${PG_DATA_DIR}/postmaster.pid" - "${PG_DATA_DIR}/standby.signal" - "${PG_DATA_DIR}/recovery.signal" - "${PG_EXT_PID_FILE}" - ) - - for file in "${files[@]}"; do - if [[ -f "$file" ]]; then - LOG_I "Remove file: $file" - rm -rf "$file" - fi - done -} - -# 清空数据库及配置文件 -postgresql_reset() { - LOG_I "Clean all configuration and database files..." - rm -rf "${PG_DATA_DIR}/*" - rm -rf "${APP_DATA_DIR}/.data_init_flag" "${APP_DATA_DIR}/.custom_preinit_flag" "${APP_DATA_DIR}/.custom_init_flag" - rm -rf "${APP_CONF_DIR}/*" - rm -rf "${APP_CONF_DIR}/.app_init_flag" -} - -# 应用默认初始化操作 -# 执行完毕后,生成文件 ${APP_CONF_DIR}/.app_init_flag 及 ${APP_DATA_DIR}/.data_init_flag 文件 -postgresql_default_init() { - LOG_D "Check default init status of ${APP_NAME}..." - postgresql_clean_from_restart - - if is_dir_empty "${PG_DATA_DIR}"; then - LOG_I "Deploying ${APP_NAME} from scratch..." - [ ! -e "${PG_HBA_FILE}" ] && postgresql_default_hba_config && postgresql_hba_allow_local_connection - [ ! -e "${PG_CONF_FILE}" ] && postgresql_default_postgresql_config - - if [[ "${PG_REPLICATION_MODE}" = "primary" ]]; then - postgresql_primary_init_db - - postgresql_start_server_bg - [[ "${PG_DATABASE}" != "postgres" ]] && postgresql_create_custom_database - - # 为数据库授权;默认用户不为 postgres 时,需要创建管理员账户 - LOG_D "Set password for postgres user" - if [[ "${PG_USERNAME}" = "postgres" ]]; then - [[ -n "${PG_PASSWORD}" ]] && postgresql_alter_postgres_user "${PG_PASSWORD}" - else - [[ -n "${PG_POSTGRES_PASSWORD}" ]] && postgresql_alter_postgres_user "${PG_POSTGRES_PASSWORD}" - postgresql_create_admin_user - fi - [[ -n "${PG_REPLICATION_USER}" ]] && postgresql_create_replication_user - else - postgresql_standby_init_db - fi - else - LOG_I "Deploying ${APP_NAME} with persisted data..." - export PG_FIRST_BOOT="no" - fi - - # 检测配置文件是否存在 - if [[ ! -f "${APP_CONF_DIR}/.app_init_flag" ]]; then - LOG_I "Deploying postgresql with new configuration" - postgresql_default_postgresql_config - postgresql_default_hba_config - postgresql_hba_allow_local_connection - - if [[ "${PG_REPLICATION_MODE}" = "primary" ]]; then - [[ -n "${PG_REPLICATION_USER}" ]] && postgresql_hba_allow_replication_connection - else - postgresql_configure_recovery - fi - - if is_boolean_yes "${PG_ENABLE_TLS}" ; then - postgresql_configure_tls - [[ -n "${PG_TLS_CA_FILE}" ]] && postgresql_hba_allow_tls_connection - fi - - postgresql_configure_logging - postgresql_configure_connections - - touch "${APP_CONF_DIR}/.app_init_flag" - echo "$(date '+%Y-%m-%d %H:%M:%S') : Init success." >> "${APP_CONF_DIR}/.app_init_flag" - else - LOG_I "Deploying postgresql with persisted configuration" - fi - - if [[ ! -f "${APP_DATA_DIR}/.data_init_flag" ]]; then - touch ${APP_DATA_DIR}/.data_init_flag - echo "$(date '+%Y-%m-%d %H:%M:%S') : Init success." >> ${APP_DATA_DIR}/.data_init_flag - fi - - postgresql_restrict_hba_config - - # 删除第一次运行时生成的默认配置文件 - rm -f "${PG_DATA_DIR}"/postgresql.conf "${PG_DATA_DIR}"/pg_hba.conf -} - -# 用户自定义的前置初始化操作,依次执行目录 preinitdb.d 中的初始化脚本 -# 执行完毕后,生成文件 ${APP_DATA_DIR}/.custom_preinit_flag -postgresql_custom_preinit() { - LOG_I "Check custom pre-init status of ${APP_NAME}..." - - # 检测用户配置文件目录是否存在 preinitdb.d 文件夹,如果存在,尝试执行目录中的初始化脚本 - if [ -d "/srv/conf/${APP_NAME}/preinitdb.d" ]; then - # 检测数据存储目录是否存在已初始化标志文件;如果不存在,检索可执行脚本文件并进行初始化操作 - if [[ -n $(find "/srv/conf/${APP_NAME}/preinitdb.d/" -type f -regex ".*\.\(sh\)") ]] && \ - [[ ! -f "${APP_DATA_DIR}/.custom_preinit_flag" ]]; then - LOG_I "Process custom pre-init scripts from /srv/conf/${APP_NAME}/preinitdb.d..." - - # 检索所有可执行脚本,排序后执行 - find "/srv/conf/${APP_NAME}/preinitdb.d/" -type f -regex ".*\.\(sh\)" | sort | process_init_files - - touch "${APP_DATA_DIR}/.custom_preinit_flag" - echo "$(date '+%Y-%m-%d %H:%M:%S') : Init success." >> "${APP_DATA_DIR}/.custom_preinit_flag" - LOG_I "Custom preinit for ${APP_NAME} complete." - else - LOG_I "Custom preinit for ${APP_NAME} already done before, skipping initialization." - fi - fi - - # 检测依赖的服务是否就绪 - #for i in ${SERVICE_PRECONDITION[@]}; do - # app_wait_service "${i}" - #done -} - -# 用户自定义的应用初始化操作,依次执行目录initdb.d中的初始化脚本 -# 执行完毕后,生成文件 ${APP_DATA_DIR}/.custom_init_flag -postgresql_custom_init() { - LOG_I "Check custom initdb status of ${APP_NAME}..." - - # 检测用户配置文件目录是否存在 initdb.d 文件夹,如果存在,尝试执行目录中的初始化脚本 - if [ -d "/srv/conf/${APP_NAME}/initdb.d" ]; then - # 检测数据存储目录是否存在已初始化标志文件;如果不存在,检索可执行脚本文件并进行初始化操作 - if [[ -n $(find "/srv/conf/${APP_NAME}/initdb.d/" -type f -regex ".*\.\(sh\|sql\|sql.gz\)") ]] && \ - [[ ! -f "${APP_DATA_DIR}/.custom_init_flag" ]]; then - LOG_I "Process custom init scripts from /srv/conf/${APP_NAME}/initdb.d..." - - # 启动后台服务 - postgresql_start_server_bg - - # 检索所有可执行脚本,排序后执行 - find "/srv/conf/${APP_NAME}/initdb.d/" -type f -regex ".*\.\(sh\|sql\|sql.gz\)" | sort | while read -r f; do - case "$f" in - *.sh) - if [[ -x "$f" ]]; then - LOG_D "Executing $f"; "$f" - else - LOG_D "Sourcing $f"; . "$f" - fi - ;; - *.sql) - LOG_D "Executing $f"; - postgresql_execute "${PG_DATABASE}" "${PG_INITSCRIPTS_USERNAME}" "${PG_INITSCRIPTS_PASSWORD}" < "$f" - ;; - *.sql.gz) - LOG_D "Executing $f"; - gunzip -c "$f" | postgresql_execute "${PG_DATABASE}" "${PG_INITSCRIPTS_USERNAME}" "${PG_INITSCRIPTS_PASSWORD}" - ;; - *) - LOG_D "Ignoring $f" ;; - esac - done - - touch "${APP_DATA_DIR}/.custom_init_flag" - echo "$(date '+%Y-%m-%d %H:%M:%S') : Init success." >> "${APP_DATA_DIR}/.custom_init_flag" - LOG_I "Custom init for ${APP_NAME} complete." - else - LOG_I "Custom init for ${APP_NAME} already done before, skipping initialization." - fi - fi - - # 绑定所有 IP 及 指定端口 ,启用远程访问 - postgresql_enable_remote_connections - postgresql_conf_set "port" "${PG_PORT_NUMBER}" - + debug_execute PGPASSWORD=$pass "${cmd}" "${args[@]}" } # 初始化 Master 节点数据库 -postgresql_primary_init_db() { +pg_primary_init_db() { LOG_I "Initializing PostgreSQL database" - postgresql_enable_nss_wrapper - + local initdb_cmd="$(command -v initdb)" local envExtraFlags=() local initdb_args=() if [[ -n "${PG_INITDB_ARGS}" ]]; then read -r -a envExtraFlags <<< "${PG_INITDB_ARGS}" initdb_args+=("${envExtraFlags[@]}") fi - #initdb+=("-o" "--config-file=${PG_CONF_FILE} --external_pid_file=${PG_EXT_PID_FILE} --hba_file=${PG_HBA_FILE}") - if [[ -n "${PG_INITDB_WAL_DIR:-}" ]]; then - initdb_args+=("--waldir=${PG_INITDB_WAL_DIR}") - fi + [[ "${PG_INITDB_WAL_DIR}" != "${PGDATA}/pg_wal" ]] && initdb_args+=("--waldir=${PG_INITDB_WAL_DIR}") + # 配置 local / host 访问策略都为 trust + initdb_args+=("--auth=trust") - local initdb_cmd=("${APP_HOME_DIR}/bin/initdb") - - if [[ -n "${initdb_args[*]}" ]]; then - LOG_I "extra initdb arguments: ${initdb_args[*]}" - fi - - if is_boolean_yes "${ENV_DEBUG}"; then - "${initdb_cmd[@]}" -E UTF8 -D "${PG_DATA_DIR}" -U "postgres" "${initdb_args[@]}" - else - "${initdb_cmd[@]}" -E UTF8 -D "${PG_DATA_DIR}" -U "postgres" "${initdb_args[@]}" >/dev/null 2>&1 - fi - - postgresql_disable_nss_wrapper + LOG_D " extra initdb arguments: ${initdb_args[*]}" + debug_execute "${initdb_cmd}" -E UTF8 -D "${PGDATA}" -U "postgres" "${initdb_args[@]}" } # 初始化 Slave 节点数据库 -postgresql_standby_init_db() { - LOG_I "Waiting for replication primary to accept connections (${PG_INIT_MAX_TIMEOUT} seconds)..." - local -r check_args=("-U" "${PG_REPLICATION_USER}" "-h" "${PG_PRIMARY_HOST}" "-p" "${PG_PRIMARY_PORT}" "-d" "${PG_DATABASE}") - local check_cmd=("${APP_HOME_DIR}/bin/pg_isready") - local ready_counter=${PG_INIT_MAX_TIMEOUT} +pg_standby_init_db() { + LOG_I "Waiting for replication primary to accept connections..." + local check_cmd="$(command -v pg_isready)" + local check_args=("-h" "${PG_REPLICATION_HOST}" "-p" "${PG_REPLICATION_PORT}" "-U" "${PG_REPLICATION_USER}" "-d" "${PG_DATABASE}" "-t" "1") - while ! PGPASSWORD=${PG_REPLICATION_PASSWORD} "${check_cmd[@]}" "${check_args[@]}" >/dev/null 2>&1;do + local ready_counter=${PG_INIT_MAX_TIMEOUT} + while ! PGPASSWORD=${PG_REPLICATION_PASSWORD} "${check_cmd}" "${check_args[@]}" >/dev/null 2>&1;do sleep 1 ready_counter=$(( ready_counter - 1 )) if (( ready_counter <= 0 )); then @@ -770,19 +590,18 @@ postgresql_standby_init_db() { done LOG_I "Replicating the database from node primary..." - #local -r backup_args=("-D" "$PG_DATA_DIR" -d "hostaddr=$PG_PRIMARY_HOST port=$PG_PRIMARY_PORT user=$PG_REPLICATION_USER password=$PG_REPLICATION_PASSWORD" -v -Fp -Xs - local -r backup_args=("-D" "${PG_DATA_DIR}" "-U" "${PG_REPLICATION_USER}" "-h" "${PG_PRIMARY_HOST}" "-p" "${PG_PRIMARY_PORT}" "-X" "stream" "-w" "-v" "-P") - local backup_cmd=("${APP_HOME_DIR}/bin/pg_basebackup") - local replication_counter=${PG_INIT_MAX_TIMEOUT} + local backup_cmd="$(command -v pg_basebackup)" + local backup_args=("-D" "${PGDATA}") + backup_args+=("-h" "${PG_REPLICATION_HOST}" "-p" "${PG_REPLICATION_PORT}" "-U" "${PG_REPLICATION_USER}" "-W") + backup_args+=("-C" "-S" "${PG_REPLICATION_APP_NAME}") + [[ "${PG_INITDB_WAL_DIR}" != "${PGDATA}/pg_wal" ]] && backup_args+=("--waldir=${PG_INITDB_WAL_DIR}") + backup_args+=("-X" "stream" "-v" "-R" "-F" "plain") - while ! PGPASSWORD=${PG_REPLICATION_PASSWORD} "${backup_cmd[@]}" "${backup_args[@]}";do - LOG_D "Backup command failed. Sleeping and trying again" - sleep 1 - replication_counter=$(( replication_counter - 1 )) - if (( replication_counter <= 0 )); then - LOG_E "Slave replication failed after trying for ${PG_INIT_MAX_TIMEOUT} seconds" - exit 1 - fi + local replication_counter=0 + while ! PGPASSWORD=${PG_REPLICATION_PASSWORD} "${backup_cmd}" "${backup_args[@]}";do + replication_counter=$(( replication_counter + 1 )) + LOG_D "Backup command failed ${replication_counter} times. Sleeping and trying again later[]" + sleep 5 done } diff --git a/customer/usr/local/bin/entry.sh b/customer/usr/local/bin/entry.sh index ad57948..a1a112c 100755 --- a/customer/usr/local/bin/entry.sh +++ b/customer/usr/local/bin/entry.sh @@ -1,29 +1,37 @@ -#!/bin/bash -# Ver: 1.3 by Endial Fang (endial@126.com) +#!/usr/bin/dumb-init /bin/bash +# Ver: 1.6 by Endial Fang (endial@126.com) # -# 容器入口脚本 +# 容器入口脚本;当前脚本执行完毕时,使用默认用户执行镜像 CMD 定义的命令(默认为'/usr/local/bin/run.sh') # 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用: # -e: 命令执行错误则报错(errexit); -u: 变量未定义则报错(nounset); -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错 -set -eu -set -o pipefail +set -euo pipefail -. /colovu/lib/libcommon.sh # 加载通用函数库 +. /colovu/lib/libcommon.sh # 加载通用函数库 + +. /usr/local/bin/environment.sh # 设置环境变量 LOG_I "** Processing entry.sh **" -if [[ "$*" = "/usr/local/bin/run.sh" ]]; then - print_image_welcome +# 优先处理'-'开始的版本信息、帮助信息显示命令,如果是该类命令,处理后退出容器 +[[ "${1:0:1}" == '-' ]] && set -- "${APP_EXEC:-/bin/bash}" "$@" && print_command_help "$@" - LOG_I "** Starting ${APP_NAME} setup **" +# 处理 root 用户**且**使用默认启动脚本时的初始化 +if [[ "$(id -u)" == '0' ]] && [[ "$1" == "run.sh" ]]; then + print_welcome_info /usr/local/bin/setup.sh - /usr/local/bin/init.sh - LOG_I "** ${APP_NAME} setup finished! **" + gosu "${APP_USER}" /usr/local/bin/init.sh + + # 执行应用启动脚本并替换当前进程 + exec gosu "${APP_USER}" "$@" fi -# 检测是否仅打印帮助信息 -[ "${1:0:1}" = '-' ] && set -- "${APP_EXEC:-/bin/bash}" "$@" -print_command_help "$@" +# 处理 root 用户**且**使用init.sh脚本时的初始化 +if [[ "$(id -u)" == '0' ]] && [[ "$1" == "init.sh" ]]; then + /usr/local/bin/setup.sh + exec gosu "${APP_USER}" /usr/local/bin/init.sh +fi +# 处理非以上情形的自定义命令 LOG_I "Start container with command: $@" exec "$@" diff --git a/customer/usr/local/bin/environment.sh b/customer/usr/local/bin/environment.sh index 930a903..9525c3a 100644 --- a/customer/usr/local/bin/environment.sh +++ b/customer/usr/local/bin/environment.sh @@ -1,19 +1,17 @@ #!/bin/bash -# Ver: 1.1 by Endial Fang (endial@126.com) +# Ver: 1.3 by Endial Fang (endial@126.com) # # 应用环境变量定义及初始化 -# 通用设置 export ENV_DEBUG=${ENV_DEBUG:-false} -export ALLOW_ANONYMOUS_LOGIN="${ALLOW_ANONYMOUS_LOGIN:-no}" +export ALLOW_ANONYMOUS="${ALLOW_ANONYMOUS:-no}" -# 通过读取变量名对应的 *_FILE 文件,获取变量值;如果对应文件存在,则通过传入参数设置的变量值会被文件中对应的值覆盖 +# 通过读取变量名对应的`*_FILE`文件,获取变量值 # 变量优先级: *_FILE > 传入变量 > 默认值 app_env_file_lists=( PG_POSTGRES_PASSWORD PG_PASSWORD PG_REPLICATION_PASSWORD - PG_LDAP_BIND_PASSWORD ) for env_var in "${app_env_file_lists[@]}"; do file_env_var="${env_var}_FILE" @@ -24,96 +22,98 @@ for env_var in "${app_env_file_lists[@]}"; do done unset app_env_file_lists -# 应用路径参数 -export APP_HOME_DIR="/usr/local/${APP_NAME}" -export APP_DEF_DIR="/etc/${APP_NAME}" -export APP_CONF_DIR="/srv/conf/${APP_NAME}" -export APP_DATA_DIR="/srv/data/${APP_NAME}" -export APP_DATA_LOG_DIR="/srv/datalog/${APP_NAME}" +# 应用路径参数(Dockerfile 已定义:APP_NAME、APP_VER,可能定义 APP_USER、APP_EXEC) +export APP_EXEC="${APP_EXEC:-${APP_NAME}}" +export APP_USER="${APP_USER:-${APP_NAME}}" +export APP_GROUP="${APP_GROUP:-${APP_USER}}" +export APP_HOME="${APP_HOME:-/srv/${APP_NAME}}" +export APP_BASE="${APP_BASE:-/usr/local/${APP_NAME}}" + +export APP_DEF_DIR="${APP_BASE}/etc/${APP_NAME}" +export APP_CONF_DIR="/srv/${APP_NAME}/conf" +export APP_DATA_DIR="/srv/${APP_NAME}/data" +export APP_CERT_DIR="/srv/${APP_NAME}/cert" +export APP_LOG_DIR="/srv/${APP_NAME}/log" export APP_CACHE_DIR="/var/cache/${APP_NAME}" export APP_RUN_DIR="/var/run/${APP_NAME}" -export APP_LOG_DIR="/var/log/${APP_NAME}" -export APP_CERT_DIR="/srv/cert/${APP_NAME}" -export PG_DATA_DIR="${APP_DATA_DIR}/data" +export PGDATA="${PGDATA:-${APP_DATA_DIR}}" +export TZ=${TZ:-"Asia/Shanghai"} -export PG_CONF_FILE="${APP_CONF_DIR}/postgresql.conf" -export PG_HBA_FILE="${APP_CONF_DIR}/pg_hba.conf" -export PG_RECOVERY_FILE="${PG_DATA_DIR}/recovery.conf" -export PG_IDENT_FILE="${PG_DATA_DIR}/pg_ident.conf" -export PG_EXT_PID_FILE="${APP_RUN_DIR}/postgresql.pid" -export PG_LOG_FILE="${APP_LOG_DIR}/postgresql.log" +export PG_CONF_FILE="${PG_CONF_FILE:-${PGDATA}/postgresql.conf}" +export PG_PID_FILE="${PG_PID_FILE:-${PGDATA}/postmaster.pid}" +export PG_INIT_MAX_TIMEOUT=${PG_INIT_MAX_TIMEOUT:-60} +export PG_REPLICATION_MODE="${PG_REPLICATION_MODE:-primary}" + +if [[ "${PG_REPLICATION_MODE}" == "primary" ]]; then + export PG_SYNCHRONOUS_REPLICAS_NUM="${PG_SYNCHRONOUS_REPLICAS_NUM:-0}" + export PG_SYNCHRONOUS_REPLICAS_METHOD="${PG_SYNCHRONOUS_REPLICAS_METHOD:-}" + export PG_SYNCHRONOUS_REPLICAS_NAMES="${PG_SYNCHRONOUS_REPLICAS_NAMES:-\*}" + + export PG_CFG_MAX_WAL_SENDERS="${PG_CFG_MAX_WAL_SENDERS:-10}" + export PG_CFG_WAL_LEVEL="${PG_CFG_WAL_LEVEL:-logical}" + export PG_CFG_WAL_LOG_HINTS="${PG_CFG_WAL_LOG_HINTS:-on}" + +elif [[ "${PG_REPLICATION_MODE}" == "standby" ]]; then + export PG_REPLICATION_APP_NAME=${PG_REPLICATION_APP_NAME:-cvcluster} + export PG_REPLICATION_HOST="${PG_REPLICATION_HOST:-}" + export PG_REPLICATION_PORT="${PG_REPLICATION_PORT:-5432}" + export PG_REPLICATION_USER="${PG_REPLICATION_USER:-}" + export PG_REPLICATION_PASSWORD="${PG_REPLICATION_PASSWORD:-}" + export PG_REPLICATION_CONNECT_TIMEOUT="${PG_REPLICATION_CONNECT_TIMEOUT:-10}" + + # 配置允许从备机备份,参考:http://www.postgres.cn/docs/9.4/app-pgbasebackup.html + export PG_CFG_FULL_PAGE_WRITES="${PG_CFG_FULL_PAGE_WRITES:-on}" + export PG_CFG_HOT_STANDBY="${PG_CFG_HOT_STANDBY:-on}" + export PG_CFG_MAX_WAL_SENDERS="${PG_CFG_MAX_WAL_SENDERS:-16}" +fi # 应用配置参数 -export PG_CLUSTER_APP_NAME=${PG_CLUSTER_APP_NAME:-cvcluster} -export PG_REPLICATION_MODE="${PG_REPLICATION_MODE:-primary}" -export PG_PRIMARY_HOST="${PG_PRIMARY_HOST:-}" -export PG_PRIMARY_PORT="${PG_PRIMARY_PORT:-5432}" -export PG_NUM_SYNCHRONOUS_REPLICAS="${PG_NUM_SYNCHRONOUS_REPLICAS:-0}" -export PG_REPLICATION_USER="${PG_REPLICATION_USER:-}" -export PG_REPLICATION_PASSWORD="${PG_REPLICATION_PASSWORD:-}" -export PG_SYNCHRONOUS_COMMIT_MODE="${PG_SYNCHRONOUS_COMMIT_MODE:-on}" -export PG_FSYNC="${PG_FSYNC:-on}" -export PG_INIT_MAX_TIMEOUT=${PG_INIT_MAX_TIMEOUT:-60} -export PG_INITDB_ARGS="${PG_INITDB_ARGS:-}" -export PG_INITDB_WAL_DIR="${PG_INITDB_WAL_DIR:-}" -export PG_PORT_NUMBER="${PG_PORT_NUMBER:-5432}" -export PG_SHARED_PRELOAD_LIBRARIES="${PG_SHARED_PRELOAD_LIBRARIES:-}" -export PG_USERNAME_CONNECTION_LIMIT="${PG_USERNAME_CONNECTION_LIMIT:-}" -export PG_POSTGRES_CONNECTION_LIMIT="${PG_POSTGRES_CONNECTION_LIMIT:-}" export PG_ENABLE_LDAP="${PG_ENABLE_LDAP:-no}" -export PG_LDAP_URL="${PG_LDAP_URL:-}" -export PG_LDAP_PREFIX="${PG_LDAP_PREFIX:-}" -export PG_LDAP_SUFFIX="${PG_LDAP_SUFFIX:-}" -export PG_LDAP_SERVER="${PG_LDAP_SERVER:-}" -export PG_LDAP_PORT="${PG_LDAP_PORT:-}" -export PG_LDAP_SCHEME="${PG_LDAP_SCHEME:-}" -export PG_LDAP_TLS="${PG_LDAP_TLS:-}" -export PG_LDAP_BASE_DN="${PG_LDAP_BASE_DN:-}" -export PG_LDAP_BIND_DN="${PG_LDAP_BIND_DN:-}" -export PG_LDAP_BIND_PASSWORD="${PG_LDAP_BIND_PASSWORD:-}" -export PG_LDAP_SEARCH_ATTR="${PG_LDAP_SEARCH_ATTR:-}" -export PG_LDAP_SEARCH_FILTER="${PG_LDAP_SEARCH_FILTER:-}" +if [[ ${PG_ENABLE_LDAP} == yes ]]; then + export PG_LDAP_URL="${PG_LDAP_URL:-}" -export PG_ENABLE_TLS="${PG_ENABLE_TLS:-no}" -export PG_TLS_CERT_FILE="${PG_TLS_CERT_FILE:-}" -export PG_TLS_KEY_FILE="${PG_TLS_KEY_FILE:-}" -export PG_TLS_CA_FILE="${PG_TLS_CA_FILE:-}" -export PG_TLS_CRL_FILE="${PG_TLS_CRL_FILE:-}" -export PG_TLS_PREFER_SERVER_CIPHERS="${PG_TLS_PREFER_SERVER_CIPHERS:-yes}" + export PG_LDAP_PREFIX="${PG_LDAP_PREFIX:-}" + export PG_LDAP_SUFFIX="${PG_LDAP_SUFFIX:-}" + export PG_LDAP_SERVER="${PG_LDAP_SERVER:-}" + export PG_LDAP_PORT="${PG_LDAP_PORT:-}" + export PG_LDAP_SCHEME="${PG_LDAP_SCHEME:-}" + export PG_LDAP_TLS="${PG_LDAP_TLS:-}" + export PG_LDAP_BASE_DN="${PG_LDAP_BASE_DN:-}" + export PG_LDAP_BIND_DN="${PG_LDAP_BIND_DN:-}" + export PG_LDAP_BIND_PASSWORD="${PG_LDAP_BIND_PASSWORD:-}" + export PG_LDAP_SEARCH_ATTR="${PG_LDAP_SEARCH_ATTR:-}" + export PG_LDAP_SEARCH_FILTER="${PG_LDAP_SEARCH_FILTER:-}" +fi -export PG_PGAUDIT_LOG="${PG_PGAUDIT_LOG:-}" -export PG_PGAUDIT_LOG_CATALOG="${PG_PGAUDIT_LOG_CATALOG:-}" -export PG_LOG_CONNECTIONS="${PG_LOG_CONNECTIONS:-}" -export PG_LOG_DISCONNECTIONS="${PG_LOG_DISCONNECTIONS:-}" -export PG_LOG_HOSTNAME="${PG_LOG_HOSTNAME:-}" -export PG_CLIENT_MIN_MESSAGES="${PG_CLIENT_MIN_MESSAGES:-error}" -export PG_LOG_LINE_PREFIX="${PG_LOG_LINE_PREFIX:-}" -export PG_LOG_TIMEZONE="${PG_LOG_TIMEZONE:-}" - -export PG_MAX_CONNECTIONS="${PG_MAX_CONNECTIONS:-}" -export PG_TCP_KEEPALIVES_IDLE="${PG_TCP_KEEPALIVES_IDLE:-}" -export PG_TCP_KEEPALIVES_INTERVAL="${PG_TCP_KEEPALIVES_INTERVAL:-}" -export PG_TCP_KEEPALIVES_COUNT="${PG_TCP_KEEPALIVES_COUNT:-}" -export PG_STATEMENT_TIMEOUT="${PG_STATEMENT_TIMEOUT:-}" +export PG_INITDB_ARGS="${PG_INITDB_ARGS:-}" +export PG_INITDB_WAL_DIR="${PG_INITDB_WAL_DIR:-${PGDATA}/pg_wal}" +export PG_USER_CONNECTION_LIMIT="${PG_USER_CONNECTION_LIMIT:-}" +export PG_DB_CONNECTION_LIMIT="${PG_DB_CONNECTION_LIMIT:-}" +export PG_USER_IP4_RANGE="${PG_IP4_RANGE:-0.0.0.0/0}" +export PG_USER_IP6_RANGE="${PG_IP6_RANGE:-::0/0}" +export PG_REPLICATION_IP4_RANGE="${PG_REPLICATION_IP4_RANGE:-${PG_USER_IP4_RANGE}}" +export PG_REPLICATION_IP6_RANGE="${PG_REPLICATION_IP6_RANGE:-${PG_USER_IP6_RANGE}}" export PG_USERNAME="${PG_USERNAME:-postgres}" export PG_PASSWORD="${PG_PASSWORD:-}" export PG_DATABASE="${PG_DATABASE:-postgres}" -# 使用自定义用户名(非"postgres")时的管理员密码 -[[ "${PG_USERNAME}" = "postgres" ]] && PG_POSTGRES_PASSWORD="${PG_PASSWORD}" export PG_POSTGRES_PASSWORD="${PG_POSTGRES_PASSWORD:-}" + +# 使用自定义用户名(非"postgres")时的管理员密码 +[[ "${PG_USERNAME}" == "postgres" ]] && export PG_POSTGRES_PASSWORD="${PG_PASSWORD}" + +# 用来执行初始化数据库命令的用户名及密码 export PG_INITSCRIPTS_USERNAME="${PG_INITSCRIPTS_USERNAME:-${PG_USERNAME}}" export PG_INITSCRIPTS_PASSWORD="${PG_INITSCRIPTS_PASSWORD:-${PG_PASSWORD}}" -export PGCONNECT_TIMEOUT="${PGCONNECT_TIMEOUT:-10}" - # 内部变量 -export PG_FIRST_BOOT="yes" - -export APP_DAEMON_USER="${APP_NAME}" -export APP_DAEMON_GROUP="${APP_NAME}" # 个性化变量 +export PG_CFG_DATA_DIRECTORY="${PGDATA}" +export PG_CFG_HBA_FILE="${PGDATA}/pg_hba.conf" +export PG_CFG_IDENT_FILE="${PGDATA}/pg_ident.conf" +export PG_CFG_EXTERNAL_PID_FILE="${APP_RUN_DIR}/postgresql.pid" +export PG_CFG_LOG_DIRECTORY="${APP_LOG_DIR}" diff --git a/customer/usr/local/bin/init.sh b/customer/usr/local/bin/init.sh index 24cd936..439f3d9 100755 --- a/customer/usr/local/bin/init.sh +++ b/customer/usr/local/bin/init.sh @@ -1,29 +1,26 @@ #!/bin/bash -# Ver: 1.2 by Endial Fang (endial@126.com) +# Ver: 1.4 by Endial Fang (endial@126.com) # -# 应用初始化脚本 +# 应用初始化脚本;当前脚本使用‘gosu ${APP_USER}’方式切换至用户空间执行 # 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用: # -e: 命令执行错误则报错; -u: 变量未定义则报错; -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错 -set -eu -set -o pipefail +set -euo pipefail -. /usr/local/bin/common.sh # 应用专用函数库 . /usr/local/bin/environment.sh # 设置环境变量 +. /usr/local/bin/common.sh # 应用专用函数库 LOG_I "** Processing init.sh **" +#trap "app_stop_server" EXIT -trap "${APP_NAME}_stop_server" EXIT - -${APP_NAME}_verify_minimum_env +# 检测最小环境变量配置 +app_verify_minimum_env # 执行应用预初始化操作 -${APP_NAME}_custom_preinit +app_custom_preinit # 执行应用初始化操作 -${APP_NAME}_default_init +app_default_init # 执行用户自定义初始化脚本 -${APP_NAME}_custom_init - -LOG_I "** Processing init.sh finished! **" +app_custom_init diff --git a/customer/usr/local/bin/run.sh b/customer/usr/local/bin/run.sh index 3b7475a..6a76a86 100755 --- a/customer/usr/local/bin/run.sh +++ b/customer/usr/local/bin/run.sh @@ -1,28 +1,25 @@ #!/bin/bash -# Ver: 1.5 by Endial Fang (endial@126.com) +# Ver: 1.7 by Endial Fang (endial@126.com) # -# 应用启动脚本 +# 应用启动脚本;组合默认的配置参数及容器启动时传入的 CMD 参数,启动应用 # 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用: # -e: 命令执行错误则报错(errexit); -u: 变量未定义则报错(nounset); -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错 -set -eu -set -o pipefail +set -euo pipefail + +. /colovu/lib/liblog.sh # 日志输出函数库 -. /usr/local/bin/common.sh # 应用专用函数库 . /usr/local/bin/environment.sh # 设置环境变量 LOG_I "** Processing run.sh **" - -readonly START_COMMAND="$(command -v ${APP_EXEC})" +readonly START_COMMAND="$(command -v ${APP_EXEC:-${APP_NAME}})" # 配置默认启动参数(应用配置文件、前台方式启动) -flags=("--config-file=${PG_CONF_FILE}" "--hba_file=${PG_HBA_FILE}") -[[ -z "${APP_EXTRA_FLAGS:-}" ]] || flags+=("${APP_EXTRA_FLAGS[@]}") -# 增加 "@" 以使用用户在命令行添加的扩展标识 +flags=() +[[ -n "${PGDATA:-}" ]] && flags+=("-D" "${PGDATA}") +[[ -n "${PG_CONF_FILE:-}" ]] && flags+=("--config-file=${PG_CONF_FILE}") +[[ -n "${APP_EXTRA_FLAGS:-}" ]] && flags+=("${APP_EXTRA_FLAGS[@]}") flags+=("$@") -LOG_I "** Starting ${APP_NAME} **" -#is_root && flags=("-u" "$APP_DAEMON_USER" "${flags[@]}") - -LOG_I "Command: ${START_COMMAND[@]} ${flags[@]}" -exec "${START_COMMAND[@]}" "${flags[@]}" +LOG_I "Start ${APP_NAME} with command: ${START_COMMAND} ${flags[@]}" +exec "${START_COMMAND}" "${flags[@]}" diff --git a/customer/usr/local/bin/setup.sh b/customer/usr/local/bin/setup.sh index 42bce69..879c614 100755 --- a/customer/usr/local/bin/setup.sh +++ b/customer/usr/local/bin/setup.sh @@ -1,46 +1,26 @@ #!/bin/bash -# Ver: 1.2 by Endial Fang (endial@126.com) +# Ver: 1.4 by Endial Fang (endial@126.com) # -# 应用环境及依赖文件设置脚本 +# 应用环境及依赖文件设置脚本;当前脚本以‘root’用户执行 # 设置 shell 执行参数,可使用'-'(打开)'+'(关闭)控制。常用: # -e: 命令执行错误则报错(errexit); -u: 变量未定义则报错(nounset); -x: 打印实际待执行的命令行; -o pipefail: 设置管道中命令遇到失败则报错 -set -eu -set -o pipefail +set -euo pipefail . /colovu/lib/libcommon.sh # 加载通用函数库 . /colovu/lib/libfs.sh # 加载文件操作函数库 . /colovu/lib/libos.sh # 加载系统管理函数库 . /usr/local/bin/environment.sh # 设置环境变量 +. /usr/local/bin/common.sh # 应用专用函数库 LOG_I "** Processing setup.sh **" -APP_DIRS="${APP_CONF_DIR:-} ${APP_DATA_DIR:-} ${APP_LOG_DIR:-} ${APP_CERT_DIR:-} ${APP_DATA_LOG_DIR:-}" -APP_DIRS="${APP_DIRS} ${PG_DATA_DIR:-} ${PG_INITDB_WAL_DIR:-}" +APP_DIRS=(/var/log/${APP_NAME} /var/run/${APP_NAME} /var/cache/${APP_NAME} ${APP_HOME}) +APP_DIRS+=(${APP_HOME}/conf ${APP_HOME}/data ${APP_HOME}/cert ${APP_HOME}/log) +APP_DIRS+=(${PGDATA}) -LOG_I "Ensure directory exists: ${APP_DIRS}" -for dir in ${APP_DIRS}; do - ensure_dir_exists ${dir} +LOG_I "Ensure directory exists: ${APP_DIRS[@]}" +for dir in ${APP_DIRS[@]}; do + ensure_dir_exists ${dir} ${APP_USER} done - -# 检测指定文件是否在配置文件存储目录存在,如果不存在则拷贝(新挂载数据卷、手动删除都会导致不存在) -# PG 将使用默认模板生成配置文件,并放置在PGDATA目录 -#LOG_I "Check config files in: ${APP_CONF_DIR}" -#if [[ ! -z "$(ls -A "${APP_DEF_DIR}")" ]]; then -# ensure_config_file_exist "${APP_DEF_DIR}" $(ls -A "${APP_DEF_DIR}") -# : -#fi - -#LOG_I "Ensure directory ownership: ${APP_USER}" -#for dir in ${APP_DIRS}; do -# configure_permissions_ownership "$dir" -u "${APP_USER}" -g "${APP_USER}" -#done - -# 解决 PostgreSQL 目录权限过于开放,无法初始化问题:FATAL: data directory "/srv/data/postgresql" has group or world access -LOG_D "Lack of permissions on data directory: ${PG_DATA_DIR}" -chmod 0700 ${PG_DATA_DIR} - -is_root && ensure_user_exists "$APP_DAEMON_USER" -g "$APP_DAEMON_GROUP" - -LOG_I "** Processing setup.sh finished! **"