diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..87a14f0 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,64 @@ +name: Docker Build and Push + +on: + schedule: + - cron: "43 0 * * *" + push: + branches: ["main"] + tags: ["v*.*.*"] + pull_request: + branches: ["main"] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + pgbouncer: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata for pgbouncer + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-pgbouncer + + - name: Build and push Docker image for pgbouncer + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: docker/pgbouncer + file: docker/pgbouncer/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=pgbouncer + cache-to: type=gha,scope=pgbouncer,mode=max + + - name: Sign the published Docker image for PgBouncer + if: ${{ github.event_name != 'pull_request' }} + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes "{}@${DIGEST}" diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index 5d55c53..879f1a3 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,8 +1,18 @@ import path from 'path'; import process from 'process'; -const buildEslintCommand = (filenames) => - `eslint ${filenames.map((f) => path.relative(process.cwd(), f)).join(' ')}`; +const buildEslintCommand = (filenames) => { + const srcDir = path.resolve(process.cwd(), 'src'); + const srcFiles = filenames.filter((f) => { + const absolute = path.resolve(process.cwd(), f); + const relativeToSrc = path.relative(srcDir, absolute); + return !relativeToSrc.startsWith('..'); + }); + if (srcFiles.length === 0) return []; + return `eslint ${srcFiles + .map((f) => path.relative(process.cwd(), f)) + .join(' ')}`; +}; const prettierCommand = 'prettier --write'; diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index b7cb6c4..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,62 +0,0 @@ -services: - postgres: - image: postgres:17-alpine - container_name: postgres - restart: always - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} - volumes: - - ./certs/psql-server.crt:/var/lib/postgresql/server.crt:ro - - ./certs/psql-server.key:/var/lib/postgresql/server.key:ro - - postgres_data:/var/lib/postgresql/data - ports: - - '5432:5432' - command: > - postgres - -c ssl=on - -c ssl_cert_file=/var/lib/postgresql/server.crt - -c ssl_key_file=/var/lib/postgresql/server.key - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}'] - interval: 10s - timeout: 5s - retries: 5 - networks: - - backend - - valkey: - image: valkey/valkey:8-alpine - container_name: valkey - restart: always - ports: - - '6379:6379' - volumes: - - ./certs/cache-server.crt:/certs/server.crt:ro - - ./certs/cache-server.key:/certs/server.key:ro - - ./certs/cache-ca.crt:/certs/ca.crt:ro - - valkey_data:/data - command: > - valkey-server - --requirepass ${VALKEY_PASSWORD} - --tls-port 6379 - --port 0 - --tls-cert-file /certs/server.crt - --tls-key-file /certs/server.key - --tls-ca-cert-file /certs/ca.crt - healthcheck: - test: ['CMD', 'valkey-cli', '-a', '${VALKEY_PASSWORD}', 'ping'] - interval: 10s - timeout: 5s - retries: 5 - networks: - - backend - -volumes: - postgres_data: - valkey_data: - -networks: - backend: - driver: bridge diff --git a/.env.example b/docker/.env.example similarity index 73% rename from .env.example rename to docker/.env.example index 558a922..5298d68 100644 --- a/.env.example +++ b/docker/.env.example @@ -1,4 +1,3 @@ POSTGRES_USER=your_postgres_user POSTGRES_PASSWORD=your_postgres_password POSTGRES_DB=your_database_name -VALKEY_PASSWORD=your_valkey_password diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..885ce32 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,97 @@ +services: + postgres: + image: postgres:17-alpine + container_name: postgres + restart: unless-stopped + volumes: + - ../certs/psql-cert.pem:/var/lib/postgresql/cert.pem:ro + - ../certs/psql-key.pem:/var/lib/postgresql/key.pem:ro + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + command: > + postgres + -c ssl=on + -c ssl_cert_file=/var/lib/postgresql/cert.pem + -c ssl_key_file=/var/lib/postgresql/key.pem + healthcheck: + test: + [ + 'CMD-SHELL', + 'PGPASSWORD=${POSTGRES_PASSWORD} pg_isready -U ${POSTGRES_USER} -h localhost -p 5432 --db=${POSTGRES_DB}', + ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - services + + redis: + image: redis:8-alpine + container_name: redis + restart: unless-stopped + ports: + - '6379:6379' + volumes: + - ../certs/cache-cert.pem:/usr/local/etc/redis/cert.pem:ro + - ../certs/cache-key.pem:/usr/local/etc/redis/key.pem:ro + - ../certs/rootCA.pem:/usr/local/etc/redis/ca.pem:ro + - ./redis.conf:/usr/local/etc/redis/redis.conf:ro + command: > + redis-server /usr/local/etc/redis/redis.conf + healthcheck: + test: + [ + 'CMD-SHELL', + 'redis-cli --tls --cacert /usr/local/etc/redis/ca.pem ping | grep PONG', + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - services + + pgbouncer: + image: ghcr.io/ahmadk953/poixpixel-discord-bot-pgbouncer + container_name: pgbouncer + restart: unless-stopped + depends_on: + - postgres + ports: + - '5432:5432' + volumes: + - ../certs/pgbouncer-cert.pem:/certs/cert.pem:ro + - ../certs/pgbouncer-key.pem:/certs/key.pem:ro + - ../certs/rootCA.pem:/certs/ca.pem:ro + environment: + DB_USER: ${POSTGRES_USER} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_HOST: postgres + AUTH_USER: ${POSTGRES_USER} + AUTH_TYPE: scram-sha-256 + POOL_MODE: transaction + ADMIN_USERS: ${POSTGRES_USER} + CLIENT_TLS_SSLMODE: require + CLIENT_TLS_CERT_FILE: /certs/cert.pem + CLIENT_TLS_KEY_FILE: /certs/key.pem + CLIENT_TLS_CA_FILE: /certs/ca.pem + SERVER_TLS_SSLMODE: require + SERVER_TLS_CA_FILE: /certs/ca.pem + healthcheck: + test: + [ + 'CMD-SHELL', + 'PGPASSWORD=${POSTGRES_PASSWORD} pg_isready -U ${POSTGRES_USER} -h localhost -p 5432 --db=${POSTGRES_DB}', + ] + networks: + - services + +volumes: + postgres_data: + +networks: + services: + driver: bridge diff --git a/docker/pgbouncer/Dockerfile b/docker/pgbouncer/Dockerfile new file mode 100644 index 0000000..99b0502 --- /dev/null +++ b/docker/pgbouncer/Dockerfile @@ -0,0 +1,39 @@ +# Based on https://raw.githubusercontent.com/edoburu/docker-pgbouncer/master/Dockerfile + +FROM alpine:3.22 AS build + +LABEL org.opencontainers.image.source=https://github.com/ahmadk953/poixpixel-discord-bot/tree/main/docker/pgbouncer +LABEL org.opencontainers.image.description="Docker image for pgbouncer with c-ares support" +LABEL org.opencontainers.image.licenses=Apache-2.0 + +ARG PGBOUNCER_VERSION=1.24.1 +ARG C_ARES_VERSION=1.34.5 + +RUN apk add --no-cache autoconf autoconf-doc automake curl gcc git libc-dev libevent-dev libtool make openssl-dev pandoc pkgconfig + +RUN set -eux; \ + curl -Lo /c-ares.tar.gz https://github.com/c-ares/c-ares/releases/download/v${C_ARES_VERSION}/c-ares-${C_ARES_VERSION}.tar.gz && \ + tar -xzf /c-ares.tar.gz && mv /c-ares-${C_ARES_VERSION} /c-ares && \ + cd /c-ares && ./configure && make && make install && \ + curl -Lo /pgbouncer.tar.gz https://pgbouncer.github.io/downloads/files/${PGBOUNCER_VERSION}/pgbouncer-${PGBOUNCER_VERSION}.tar.gz && \ + tar -xzf /pgbouncer.tar.gz -C / && mv /pgbouncer-${PGBOUNCER_VERSION} /pgbouncer && \ + cd /pgbouncer && ./configure --with-cares && make && make install + +FROM alpine:3.22 + +RUN apk add --no-cache busybox libevent postgresql-client libssl3 \ + && mkdir -p /etc/pgbouncer /var/log/pgbouncer /var/run/pgbouncer \ + && touch /etc/pgbouncer/userlist.txt \ + && addgroup -S -g 1100 pgbouncer \ + && adduser -S -u 1100 -G pgbouncer pgbouncer \ + && chown -R pgbouncer:pgbouncer /etc/pgbouncer /var/log/pgbouncer /var/run/pgbouncer + +COPY --chmod=+x entrypoint.sh /entrypoint.sh +COPY --from=build /usr/local/bin /usr/local/bin +COPY --from=build /usr/local/lib /usr/local/lib +COPY --from=build /pgbouncer/etc/pgbouncer.ini /etc/pgbouncer/pgbouncer.ini.example +COPY --from=build /pgbouncer/etc/userlist.txt /etc/pgbouncer/userlist.txt.example +EXPOSE 5432 +USER pgbouncer +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/local/bin/pgbouncer", "/etc/pgbouncer/pgbouncer.ini"] \ No newline at end of file diff --git a/docker/pgbouncer/README.md b/docker/pgbouncer/README.md new file mode 100644 index 0000000..5e6ab87 --- /dev/null +++ b/docker/pgbouncer/README.md @@ -0,0 +1,76 @@ +# PgBouncer + +PgBouncer is a lightweight connection pooler for PostgreSQL that helps optimize database connections by reusing established sessions. + +## Overview + +This directory contains all the necessary files to build and run PgBouncer as part of the Poixpixel Discord Bot project. It is based on Alpine Linux and includes support for c-ares. + +## Contents + +- **Dockerfile**: Builds the PgBouncer image with c-ares support. +- **entrypoint.sh**: Generates and configures the PgBouncer configuration file at container startup. + +## Building the Docker Image + +To build the PgBouncer Docker image, run: + +```sh +docker build -t my-pgbouncer ./docker/pgbouncer +``` + +## Running the Container + +Run the container with your desired environment variables. For example: + +```sh +docker run --rm \ + -e DATABASE_URL="postgres://user:pass@postgres-host/database" \ + -p 5432:5432 \ + my-pgbouncer +``` + +Or, if you would like to use separate environment variables: + +```sh +docker run --rm \ + -e DB_USER=user \ + -e DB_PASSWORD=pass \ + -e DB_HOST=postgres-host \ + -e DB_NAME=database \ + -p 5432:5432 \ + my-pgbouncer +``` + +You can also use the prebuilt image. For example: + +```sh +docker run --rm \ + -e DB_USER=user \ + -e DB_PASSWORD=pass \ + -e DB_HOST=postgres-host \ + -e DB_NAME=database \ + -p 5432:5432 \ + ghcr.io/ahmadk953/poixpixel-discord-bot-pgbouncer +``` + +## Customizing Your Setup + +- **Dockerfile**: Modify build arguments or dependencies as needed. +- **entrypoint.sh**: Adjust how the configuration file is generated and updated. +- **Environment Variables**: Almost all settings found in the `pgbouncer.ini` file can be set as environment variables, except for a few system-specific configuration options. For an example, check out [the example Docker compose file](../../docker-compose.yml). For all configuration options, check the [PgBouncer configuration documentation](https://www.pgbouncer.org/config.html). +- **Configuration File**: You can specify your own `pgbouncer.ini` file by mounting it as a volume like so: +```sh +docker run --rm \ + -e DB_USER=user \ + -e DB_PASSWORD=pass \ + -e DB_HOST=postgres-host \ + -e DB_NAME=database \ + -v PgBouncer.ini:/etc/PgBouncer/PgBouncer.ini:ro \ + -p 5432:5432 \ + ghcr.io/ahmadk953/poixpixel-discord-bot-pgbouncer +``` + +## License + +See the [LICENSE](../../LICENSE) file in the root of the project for licensing details. diff --git a/docker/pgbouncer/entrypoint.sh b/docker/pgbouncer/entrypoint.sh new file mode 100644 index 0000000..0f243f9 --- /dev/null +++ b/docker/pgbouncer/entrypoint.sh @@ -0,0 +1,354 @@ +#!/bin/sh +# Based on https://raw.githubusercontent.com/brainsam/pgbouncer/master/entrypoint.sh +# and https://raw.githubusercontent.com/edoburu/docker-pgbouncer/master/entrypoint.sh + +set -e + +# Here are some parameters. See all on +# https://pgbouncer.github.io/config.html + +PG_CONFIG_DIR=/etc/pgbouncer +PG_CONFIG_FILE="${PG_CONFIG_DIR}/pgbouncer.ini" +_AUTH_FILE="${AUTH_FILE:-$PG_CONFIG_DIR/userlist.txt}" + +# Workaround userlist.txt missing issue +# https://github.com/edoburu/docker-pgbouncer/issues/33 +if [ ! -e "${_AUTH_FILE}" ]; then + touch "${_AUTH_FILE}" +fi + +# Extract all info from a given URL. Sets variables because shell functions can't return multiple values. +# +# Parameters: +# - The url we should parse +# Returns (sets variables): DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME +parse_url() { + # Thanks to https://stackoverflow.com/a/17287984/146289 + + # Allow to pass values like dj-database-url / django-environ accept + proto="$(echo $1 | grep :// | sed -e's,^\(.*://\).*,\1,g')" + url="$(echo $1 | sed -e s,$proto,,g)" + + # extract the user and password (if any) + userpass="$(echo $url | grep @ | sed -r 's/^(.*)@([^@]*)$/\1/')" + DB_PASSWORD="$(echo $userpass | grep : | cut -d: -f2)" + if [ -n "${DB_PASSWORD}" ]; then + DB_USER="$(echo $userpass | grep : | cut -d: -f1)" + else + DB_USER="${userpass}" + fi + + # extract the host -- updated + hostport=$(echo $url | sed -e s,$userpass@,,g | cut -d/ -f1) + port=$(echo $hostport | grep : | cut -d: -f2) + if [ -n "$port" ]; then + DB_HOST=$(echo $hostport | grep : | cut -d: -f1) + DB_PORT="${port}" + else + DB_HOST="${hostport}" + fi + + DB_NAME="$(echo $url | grep / | cut -d/ -f2-)" +} + +# Grabs variables set by `parse_url` and adds them to the userlist if not already set in there. +generate_userlist_if_needed() { + if [ -n "${DB_USER}" ] && [ -n "${DB_PASSWORD}" ] && [ -e "${_AUTH_FILE}" ] && ! grep -q "^\"${DB_USER}\"" "${_AUTH_FILE}"; then + if [ "${AUTH_TYPE}" = "plain" ] || [ "${AUTH_TYPE}" = "scram-sha-256" ]; then + pass="${DB_PASSWORD}" + else + pass="md5$(printf '%s' "${DB_PASSWORD}${DB_USER}" | md5sum | cut -f 1 -d ' ')" + fi + echo "\"${DB_USER}\" \"${pass}\"" >>"${_AUTH_FILE}" + echo "Wrote authentication credentials for '${DB_USER}' to ${_AUTH_FILE}" + fi +} + +# Grabs variables set by `parse_url` and adds them to the PG config file as a database entry. +generate_config_db_entry() { + # Prepare values + dbname=${DB_NAME:-*} + host=${DB_HOST:?"Setup pgbouncer config error! You must set DB_HOST env"} + port=${DB_PORT:-5432} + auth_user=${DB_USER:-postgres} + + # Print main entry + printf '%s = host=%s port=%s auth_user=%s\n' \ + "$dbname" "$host" "$port" "$auth_user" \ + >>"$PG_CONFIG_FILE" + + # Optional client_encoding + if [ -n "$CLIENT_ENCODING" ]; then + printf 'client_encoding = %s\n' "$CLIENT_ENCODING" \ + >>"$PG_CONFIG_FILE" + fi +} + +# Write the password with MD5 encryption, to avoid printing it during startup. +# Notice that `docker inspect` will show unencrypted env variables. +if [ -n "${DATABASE_URLS}" ]; then + echo "${DATABASE_URLS}" | tr ',' '\n' | while IFS= read -r url; do + parse_url "$url" + generate_userlist_if_needed + done +else + if [ -n "${DATABASE_URL}" ]; then + parse_url "${DATABASE_URL}" + fi + generate_userlist_if_needed +fi + +if [ ! -f "$PG_CONFIG_FILE" ]; then + echo "Creating pgbouncer config in ${PG_CONFIG_DIR}" + + # Config file is in "ini" format. Section names are between "[" and "]". + # Lines starting with ";" or "#" are taken as comments and ignored. + # The characters ";" and "#" are not recognized when they appear later in the line. + # write static header + printf '%s\n%s\n' \ + '################## Auto generated ##################' \ + '[databases]' \ + >"$PG_CONFIG_FILE" + + if [ -n "$DATABASE_URLS" ]; then + echo "$DATABASE_URLS" | tr , '\n' | while read -r url; do + parse_url "$url" + generate_config_db_entry + done + else + if [ -n "$DATABASE_URL" ]; then + parse_url "$DATABASE_URL" + fi + generate_config_db_entry + fi + + # write [pgbouncer] section with a constant format string + { + printf '%s\n' '[pgbouncer]' + printf 'listen_addr = %s\n' "${LISTEN_ADDR:-0.0.0.0}" + printf 'listen_port = %s\n' "${LISTEN_PORT:-5432}" + printf 'unix_socket_dir = %s\n' "${UNIX_SOCKET_DIR}" + printf 'user = %s\n' "pgbouncer" + printf 'auth_file = %s\n' "${_AUTH_FILE}" + } >>"$PG_CONFIG_FILE" + + # now handle each optional setting in its own if-block: + if [ -n "${AUTH_HBA_FILE}" ]; then + printf 'auth_hba_file = %s\n' "${AUTH_HBA_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${AUTH_TYPE}" ]; then + printf 'auth_type = %s\n' "${AUTH_TYPE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${AUTH_USER}" ]; then + printf 'auth_user = %s\n' "${AUTH_USER}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${AUTH_QUERY}" ]; then + printf 'auth_query = %s\n' "${AUTH_QUERY}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${AUTH_DBNAME}" ]; then + printf 'auth_dbname = %s\n' "${AUTH_DBNAME}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${POOL_MODE}" ]; then + printf 'pool_mode = %s\n' "${POOL_MODE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${MAX_CLIENT_CONN}" ]; then + printf 'max_client_conn = %s\n' "${MAX_CLIENT_CONN}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${POOL_SIZE}" ]; then + printf 'pool_size = %s\n' "${POOL_SIZE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${DEFAULT_POOL_SIZE}" ]; then + printf 'default_pool_size = %s\n' "${DEFAULT_POOL_SIZE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${MIN_POOL_SIZE}" ]; then + printf 'min_pool_size = %s\n' "${MIN_POOL_SIZE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${RESERVE_POOL_SIZE}" ]; then + printf 'reserve_pool_size = %s\n' "${RESERVE_POOL_SIZE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${RESERVE_POOL_TIMEOUT}" ]; then + printf 'reserve_pool_timeout = %s\n' "${RESERVE_POOL_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${MAX_DB_CONNECTIONS}" ]; then + printf 'max_db_connections = %s\n' "${MAX_DB_CONNECTIONS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${MAX_USER_CONNECTIONS}" ]; then + printf 'max_user_connections = %s\n' "${MAX_USER_CONNECTIONS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_ROUND_ROBIN}" ]; then + printf 'server_round_robin = %s\n' "${SERVER_ROUND_ROBIN}" >>"$PG_CONFIG_FILE" + fi + printf 'ignore_startup_parameters = %s\n' "${IGNORE_STARTUP_PARAMETERS:-extra_float_digits}" >>"$PG_CONFIG_FILE" + if [ -n "${DISABLE_PQEXEC}" ]; then + printf 'disable_pqexec = %s\n' "${DISABLE_PQEXEC}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${APPLICATION_NAME_ADD_HOST}" ]; then + printf 'application_name_add_host = %s\n' "${APPLICATION_NAME_ADD_HOST}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TIMEZONE}" ]; then + printf 'timezone = %s\n' "${TIMEZONE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${MAX_PREPARED_STATEMENTS}" ]; then + printf 'max_prepared_statements = %s\n' "${MAX_PREPARED_STATEMENTS}" >>"$PG_CONFIG_FILE" + fi + + # Log settings + if [ -n "${LOG_CONNECTIONS}" ]; then + printf 'log_connections = %s\n' "${LOG_CONNECTIONS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${LOG_DISCONNECTIONS}" ]; then + printf 'log_disconnections = %s\n' "${LOG_DISCONNECTIONS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${LOG_POOLER_ERRORS}" ]; then + printf 'log_pooler_errors = %s\n' "${LOG_POOLER_ERRORS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${LOG_STATS}" ]; then + printf 'log_stats = %s\n' "${LOG_STATS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${STATS_PERIOD}" ]; then + printf 'stats_period = %s\n' "${STATS_PERIOD}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${VERBOSE}" ]; then + printf 'verbose = %s\n' "${VERBOSE}" >>"$PG_CONFIG_FILE" + fi + printf 'admin_users = %s\n' "${ADMIN_USERS:-postgres}" >>"$PG_CONFIG_FILE" + if [ -n "${STATS_USERS}" ]; then + printf 'stats_users = %s\n' "${STATS_USERS}" >>"$PG_CONFIG_FILE" + fi + + # Connection sanity checks, timeouts + if [ -n "${SERVER_RESET_QUERY}" ]; then + printf 'server_reset_query = %s\n' "${SERVER_RESET_QUERY}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_RESET_QUERY_ALWAYS}" ]; then + printf 'server_reset_query_always = %s\n' "${SERVER_RESET_QUERY_ALWAYS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_CHECK_DELAY}" ]; then + printf 'server_check_delay = %s\n' "${SERVER_CHECK_DELAY}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_CHECK_QUERY}" ]; then + printf 'server_check_query = %s\n' "${SERVER_CHECK_QUERY}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_LIFETIME}" ]; then + printf 'server_lifetime = %s\n' "${SERVER_LIFETIME}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_IDLE_TIMEOUT}" ]; then + printf 'server_idle_timeout = %s\n' "${SERVER_IDLE_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_CONNECT_TIMEOUT}" ]; then + printf 'server_connect_timeout = %s\n' "${SERVER_CONNECT_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_LOGIN_RETRY}" ]; then + printf 'server_login_retry = %s\n' "${SERVER_LOGIN_RETRY}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_LOGIN_TIMEOUT}" ]; then + printf 'client_login_timeout = %s\n' "${CLIENT_LOGIN_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${AUTODB_IDLE_TIMEOUT}" ]; then + printf 'autodb_idle_timeout = %s\n' "${AUTODB_IDLE_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${DNS_MAX_TTL}" ]; then + printf 'dns_max_ttl = %s\n' "${DNS_MAX_TTL}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${DNS_NXDOMAIN_TTL}" ]; then + printf 'dns_nxdomain_ttl = %s\n' "${DNS_NXDOMAIN_TTL}" >>"$PG_CONFIG_FILE" + fi + + # TLS settings + if [ -n "${CLIENT_TLS_SSLMODE}" ]; then + printf 'client_tls_sslmode = %s\n' "${CLIENT_TLS_SSLMODE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_KEY_FILE}" ]; then + printf 'client_tls_key_file = %s\n' "${CLIENT_TLS_KEY_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_CERT_FILE}" ]; then + printf 'client_tls_cert_file = %s\n' "${CLIENT_TLS_CERT_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_CA_FILE}" ]; then + printf 'client_tls_ca_file = %s\n' "${CLIENT_TLS_CA_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_PROTOCOLS}" ]; then + printf 'client_tls_protocols = %s\n' "${CLIENT_TLS_PROTOCOLS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_CIPHERS}" ]; then + printf 'client_tls_ciphers = %s\n' "${CLIENT_TLS_CIPHERS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_ECDHCURVE}" ]; then + printf 'client_tls_ecdhcurve = %s\n' "${CLIENT_TLS_ECDHCURVE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_TLS_DHEPARAMS}" ]; then + printf 'client_tls_dheparams = %s\n' "${CLIENT_TLS_DHEPARAMS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_TLS_SSLMODE}" ]; then + printf 'server_tls_sslmode = %s\n' "${SERVER_TLS_SSLMODE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_TLS_CA_FILE}" ]; then + printf 'server_tls_ca_file = %s\n' "${SERVER_TLS_CA_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_TLS_KEY_FILE}" ]; then + printf 'server_tls_key_file = %s\n' "${SERVER_TLS_KEY_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_TLS_CERT_FILE}" ]; then + printf 'server_tls_cert_file = %s\n' "${SERVER_TLS_CERT_FILE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_TLS_PROTOCOLS}" ]; then + printf 'server_tls_protocols = %s\n' "${SERVER_TLS_PROTOCOLS}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SERVER_TLS_CIPHERS}" ]; then + printf 'server_tls_ciphers = %s\n' "${SERVER_TLS_CIPHERS}" >>"$PG_CONFIG_FILE" + fi + + # Dangerous timeouts + if [ -n "${QUERY_TIMEOUT}" ]; then + printf 'query_timeout = %s\n' "${QUERY_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${QUERY_WAIT_TIMEOUT}" ]; then + printf 'query_wait_timeout = %s\n' "${QUERY_WAIT_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${CLIENT_IDLE_TIMEOUT}" ]; then + printf 'client_idle_timeout = %s\n' "${CLIENT_IDLE_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${IDLE_TRANSACTION_TIMEOUT}" ]; then + printf 'idle_transaction_timeout = %s\n' "${IDLE_TRANSACTION_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${PKT_BUF}" ]; then + printf 'pkt_buf = %s\n' "${PKT_BUF}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${MAX_PACKET_SIZE}" ]; then + printf 'max_packet_size = %s\n' "${MAX_PACKET_SIZE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${LISTEN_BACKLOG}" ]; then + printf 'listen_backlog = %s\n' "${LISTEN_BACKLOG}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SBUF_LOOPCNT}" ]; then + printf 'sbuf_loopcnt = %s\n' "${SBUF_LOOPCNT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${SUSPEND_TIMEOUT}" ]; then + printf 'suspend_timeout = %s\n' "${SUSPEND_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TCP_DEFER_ACCEPT}" ]; then + printf 'tcp_defer_accept = %s\n' "${TCP_DEFER_ACCEPT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TCP_KEEPALIVE}" ]; then + printf 'tcp_keepalive = %s\n' "${TCP_KEEPALIVE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TCP_KEEPCNT}" ]; then + printf 'tcp_keepcnt = %s\n' "${TCP_KEEPCNT}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TCP_KEEPIDLE}" ]; then + printf 'tcp_keepidle = %s\n' "${TCP_KEEPIDLE}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TCP_KEEPINTVL}" ]; then + printf 'tcp_keepintvl = %s\n' "${TCP_KEEPINTVL}" >>"$PG_CONFIG_FILE" + fi + if [ -n "${TCP_USER_TIMEOUT}" ]; then + printf 'tcp_user_timeout = %s\n' "${TCP_USER_TIMEOUT}" >>"$PG_CONFIG_FILE" + fi + printf '\n################## end file ##################\n' >>"$PG_CONFIG_FILE" + if [ "${DEBUG}" = "true" ]; then + cat "${PG_CONFIG_FILE}" + fi +fi + +echo "Starting $*..." +exec "$@" diff --git a/docker/redis.conf b/docker/redis.conf new file mode 100644 index 0000000..98416d0 --- /dev/null +++ b/docker/redis.conf @@ -0,0 +1,7 @@ +# redis.conf +port 0 +tls-port 6379 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no diff --git a/drizzle.config.ts b/drizzle.config.ts index eb5083a..3baff27 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -14,9 +14,7 @@ export default defineConfig({ ssl: (() => { try { return { - ca: fs.readFileSync(path.resolve('./certs/psql-ca.crt')), - key: fs.readFileSync(path.resolve('./certs/psql-client.key')), - cert: fs.readFileSync(path.resolve('./certs/psql-server.crt')), + ca: fs.readFileSync(path.resolve('./certs/rootCA.pem')), }; } catch (error) { console.warn( diff --git a/generate-certs.sh b/generate-certs.sh index e025695..7f3f28e 100755 --- a/generate-certs.sh +++ b/generate-certs.sh @@ -1,43 +1,37 @@ #!/bin/bash -# Get the Effective User ID -_uid="$(id -u)" +# Get the Group ID +_gid="$(id -g)" -# Create the certificates directory -mkdir -p certs +# Remove everything in the certs directory except for rootCA.pem and rootCA-key.pem +if [ -d certs ]; then + find certs -mindepth 1 ! -name 'rootCA.pem' ! -name 'rootCA-key.pem' -exec rm -rf {} + +else + mkdir certs +fi -# Generate PostgreSQL Certificates -openssl req -new -x509 -days 365 -nodes \ - -out certs/psql-server.crt \ - -keyout certs/psql-server.key \ - -subj "/CN=localhost" +# Set CAROOT Environment Variable +CAROOT="$(pwd)/certs" +export CAROOT -# Generate Valkey Certificates -openssl req -new -x509 -days 365 -nodes \ - -out certs/cache-server.crt \ - -keyout certs/cache-server.key \ - -subj "/CN=localhost" +# Generate postgres Certificates +mkcert -key-file certs/psql-key.pem -cert-file certs/psql-cert.pem localhost 127.0.0.1 ::1 -# Get CA Certificates -cp certs/psql-server.crt certs/psql-ca.crt -cp certs/cache-server.crt certs/cache-ca.crt +# Generate Cache Certificates +mkcert -key-file certs/cache-key.pem -cert-file certs/cache-cert.pem localhost 127.0.0.1 ::1 + +# Generate PgBouncer Certificates +mkcert -key-file certs/pgbouncer-key.pem -cert-file certs/pgbouncer-cert.pem localhost 127.0.0.1 ::1 + +# Install the Root CA +mkcert -install # Setup Permissions -chmod 0600 certs/psql-server.key -chmod 0600 certs/cache-server.key +chmod 0600 certs/psql-key.pem +chmod 0640 certs/pgbouncer-key.pem +chmod 0640 certs/cache-key.pem # Assign Ownership -sudo chown 70:70 certs/psql-*.* -sudo chown 999:1000 certs/cache-*.* - -# Get Client Keys -sudo cp certs/psql-server.key certs/psql-client.key -sudo cp certs/cache-server.key certs/cache-client.key - -# Change Client Key Ownership -sudo chown $_uid:$_uid certs/psql-client.key -sudo chown $_uid:$_uid certs/cache-client.key - -# Change Client Key Permissions -sudo chmod +r certs/psql-client.key -sudo chmod +r certs/cache-client.key +sudo chown 70:70 certs/psql-key.pem +sudo chown 1100:"${_gid}" certs/pgbouncer-key.pem +sudo chown 999:"${_gid}" certs/cache-key.pem diff --git a/package.json b/package.json index 11e287e..b5ed2dd 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "discord.js": "^14.20.0", "drizzle-orm": "^0.44.2", "ioredis": "^5.6.1", - "pg": "^8.16.0" + "pg": "^8.16.2" }, "devDependencies": { "@commitlint/cli": "^19.8.1", diff --git a/src/db/db.ts b/src/db/db.ts index bc5005f..498662e 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -103,9 +103,7 @@ export async function initializeDatabaseConnection(): Promise { ssl: (() => { try { return { - ca: fs.readFileSync(path.resolve('./certs/psql-ca.crt')), - key: fs.readFileSync(path.resolve('./certs/psql-client.key')), - cert: fs.readFileSync(path.resolve('./certs/psql-server.crt')), + ca: fs.readFileSync(path.resolve('./certs/rootCA.pem')), }; } catch (error) { console.warn( diff --git a/src/db/redis.ts b/src/db/redis.ts index 8348190..23fab78 100644 --- a/src/db/redis.ts +++ b/src/db/redis.ts @@ -96,9 +96,9 @@ async function initializeRedisConnection() { tls: (() => { try { return { - ca: fs.readFileSync(path.resolve('./certs/cache-ca.crt')), - key: fs.readFileSync(path.resolve('./certs/cache-client.key')), - cert: fs.readFileSync(path.resolve('./certs/cache-server.crt')), + ca: fs.readFileSync(path.resolve('./certs/rootCA.pem')), + key: fs.readFileSync(path.resolve('./certs/cache-key.pem')), + cert: fs.readFileSync(path.resolve('./certs/cache-cert.pem')), }; } catch (error) { console.warn( diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 1301414..5b7b2e7 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -19,7 +19,7 @@ import { moderationTable } from '@/db/schema.js'; import { db, getMember, handleDbError, updateMember } from '@/db/db.js'; import logAction from './logging/logAction.js'; -const __dirname = path.resolve(); +const PROJECT_ROOT = path.resolve(); /** * Turns a duration string into milliseconds @@ -67,7 +67,12 @@ export async function generateMemberBanner({ width, height, }: generateMemberBannerTypes): Promise { - const welcomeBackground = path.join(__dirname, 'assets', 'images', 'welcome-bg.png'); + const welcomeBackground = path.join( + PROJECT_ROOT, + 'assets', + 'images', + 'welcome-bg.png', + ); const canvas = Canvas.createCanvas(width, height); const context = canvas.getContext('2d'); const background = await Canvas.loadImage(welcomeBackground); diff --git a/yarn.lock b/yarn.lock index 8b41cdc..fad579b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4026,17 +4026,17 @@ __metadata: languageName: node linkType: hard -"pg-cloudflare@npm:^1.2.5": - version: 1.2.5 - resolution: "pg-cloudflare@npm:1.2.5" - checksum: 10c0/48b9105ef027c7b3f57ef88ceaec3634cd82120059bd68273cce06989a1ec547e0b0fbb5d1afdd0711824f409c8b410f9bdec2f6c8034728992d3658c0b36f86 +"pg-cloudflare@npm:^1.2.6": + version: 1.2.6 + resolution: "pg-cloudflare@npm:1.2.6" + checksum: 10c0/db339518ed763982c45a94c96cedba1b25819fe32e9d3a4df6f82cd647c716ff9b5009f3c8b90f2940b0a1889387710ef764c9e3e7ddd8397d217309d663a2c8 languageName: node linkType: hard -"pg-connection-string@npm:^2.9.0": - version: 2.9.0 - resolution: "pg-connection-string@npm:2.9.0" - checksum: 10c0/7145d00688200685a9d9931a7fc8d61c75f348608626aef88080ece956ceb4ff1cbdee29c3284e41b7a3345bab0e4f50f9edc256e270bfa3a563af4ea78bb490 +"pg-connection-string@npm:^2.9.1": + version: 2.9.1 + resolution: "pg-connection-string@npm:2.9.1" + checksum: 10c0/9a646529bbc0843806fc5de98ce93735a4612b571f11867178a85665d11989a827e6fd157388ca0e34ec948098564fce836c178cfd499b9f0e8cd9972b8e2e5c languageName: node linkType: hard @@ -4047,12 +4047,12 @@ __metadata: languageName: node linkType: hard -"pg-pool@npm:^3.10.0": - version: 3.10.0 - resolution: "pg-pool@npm:3.10.0" +"pg-pool@npm:^3.10.1": + version: 3.10.1 + resolution: "pg-pool@npm:3.10.1" peerDependencies: pg: ">=8.0" - checksum: 10c0/b36162dc98c0ad88cd26f3d65f3e3932c3f870abe7a88905f16fc98282e8131692903e482720ebc9698cb08851c9b19242ff16a50af7f9434c8bb0b5d33a9a9a + checksum: 10c0/a00916b7df64226cc597fe769e3a757ff9b11562dc87ce5b0a54101a18c1fe282daaa2accaf27221e81e1e4cdf4da6a33dab09614734d32904d6c4e11c44a079 languageName: node linkType: hard @@ -4063,10 +4063,10 @@ __metadata: languageName: node linkType: hard -"pg-protocol@npm:^1.10.0": - version: 1.10.0 - resolution: "pg-protocol@npm:1.10.0" - checksum: 10c0/7d0d64fe9df50262d907fd476454e1e36f41f5f66044c3ba6aa773fb8add1d350a9c162306e5c33e99bdfbdcc1140dd4ca74f66eda41d0aaceb5853244dcdb65 +"pg-protocol@npm:^1.10.2": + version: 1.10.2 + resolution: "pg-protocol@npm:1.10.2" + checksum: 10c0/3f9b5aba3f356190738ea25ecded3cd033cd2218789acf9c67b75788932c4b594eeb7043481822b69eaae4d84401e00142a2ef156297a8347987a78a52afd50e languageName: node linkType: hard @@ -4083,14 +4083,14 @@ __metadata: languageName: node linkType: hard -"pg@npm:^8.16.0": - version: 8.16.0 - resolution: "pg@npm:8.16.0" +"pg@npm:^8.16.2": + version: 8.16.2 + resolution: "pg@npm:8.16.2" dependencies: - pg-cloudflare: "npm:^1.2.5" - pg-connection-string: "npm:^2.9.0" - pg-pool: "npm:^3.10.0" - pg-protocol: "npm:^1.10.0" + pg-cloudflare: "npm:^1.2.6" + pg-connection-string: "npm:^2.9.1" + pg-pool: "npm:^3.10.1" + pg-protocol: "npm:^1.10.2" pg-types: "npm:2.2.0" pgpass: "npm:1.0.5" peerDependencies: @@ -4101,7 +4101,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/24542229c7e5cbf69d654de32e8cdc8302c73f1338e56728543cb16364fb319d5689e03fa704b69a208105c7065c867cfccb9dbccccea2020bb5c64ead785713 + checksum: 10c0/e444103fda2fa236bb7951e534bbce52d81df8f0ca43b36cdd32da2e558946ae011fa6a0fcfea2e48d935addba821868e76d3f4f670b313f639a3d24fcd420af languageName: node linkType: hard @@ -4161,7 +4161,7 @@ __metadata: husky: "npm:^9.1.7" ioredis: "npm:^5.6.1" lint-staged: "npm:^16.1.2" - pg: "npm:^8.16.0" + pg: "npm:^8.16.2" prettier: "npm:3.5.3" ts-node: "npm:^10.9.2" ts-patch: "npm:^3.3.0"