#!/bin/bash

# Creates a local PuppetDB sandbox directory
# Also creates a PostgreSQL sandbox inside of new PuppetDB sandbox in directory "pg"
# These sandboxes contain all PuppetDB and PostgreSQL configuration and data storage
# All flags are required
# Sandbox directory must not already exist

set -ueo pipefail

usage()
{
    echo "Usage: $(basename $0) <-s|--sandbox> DIR --pgbin DIR --pgport PORT --bind-addr ADDR1 [--bind-addr ADDR2 ...]"
}

misuse()
{
    usage 1>&2
    exit 2
}

# Validate arguments
pdbbox=''
pgbin=''
pgport=''
bind_addr=()
while test $# -gt 0; do
    case "$1" in
        -s|--sandbox)
            shift
            test $# -gt 0 || misuse
            pdbbox="$1"
            shift
            ;;
        --pgbin)
            shift
            test $# -gt 0 || misuse
            pgbin="$1"
            shift
            ;;
        --pgport)
            shift
            test $# -gt 0 || misuse
            pgport="$1"
            shift
            ;;
        --bind-addr)
            shift
            test $# -gt 0 || misuse
            bind_addr+=("$1")
            shift
            ;;
        *)
            misuse
    esac
done
test "$pdbbox" || misuse
test "$pgbin" || misuse
test "$pgport" || misuse
test "$bind_addr" || misuse

bind_args=()
for addr in "${bind_addr[@]}"; do
    bind_args+=(--bind-addr "$addr")
done

export PGBOX="$pdbbox/pg"
export PGUSER=postgres

mkdir "$pdbbox"

# Create PostgreSQL sandbox inside PuppetDB sandbox with pgbox
# Relies on the PGBOX environment variable to know where sandbox should go
pgbox init --pgbin "$pgbin" --port "$pgport" "${bind_args[@]}" -- -E UTF8 --locale=C

mkdir "$pdbbox/var"

tmp_dir="$(mktemp -d ./tmp-pdbbox-XXXXXX)"

trap "$(printf 'pgbox env pg_ctl stop; rm -rf %q' "$tmp_dir")" EXIT

# Start PostgreSQL server using new PostgreSQL sandbox
pgbox env pg_ctl start -w

# Create random PostgreSQL passwords
pg_passwd="$(cd "$PGBOX" && cat pass-admin)"
admin_passwd="$(dd if=/dev/urandom bs=1 count=32 | base64)"
passwd="$(dd if=/dev/urandom bs=1 count=32 | base64)"
migrator_passwd="$(dd if=/dev/urandom bs=1 count=32 | base64)"
read_passwd="$(dd if=/dev/urandom bs=1 count=32 | base64)"

# This code relies on echo being a function in bash, not a subprocess
# and "here documents" to prevent passwords from being visible in
# "ps", etc.  We also assume that mktemp perms are always go-wrx,
# across all the relevant platforms.

# Save password to a file securely
save-passwd() {
    test $# -eq 3
    local passwd="$1" tmp_dir="$2" dest="$3"

    # This code relies on echo being a function in bash, not a
    # subprocess and "here documents" to prevent passwords from being
    # visible in "ps", etc.  We also assume that mktemp perms are
    # always go-wrx, across all the relevant platforms.
    tmp_pass="$(mktemp "$tmp_dir/tmp-save-pass-XXXXXX")"
    chmod 0600 "$tmp_pass" # Not entirely safe, but expected to be redundant
    echo -n "$passwd" > "$tmp_pass"
    mv "$tmp_pass" "$dest"
}

# Set password for PostgreSQL user/role
set-passwd() {
    test $# -eq 3
    local role="$1" pwfile="$2" tmp_dir="$3" tmp_cmds
    tmp_cmds="$(mktemp "$tmp_dir/set-pass-XXXXXX")"
    chmod 0600 "$tmp_cmds" # Not entirely safe, but expected to be redundant
    echo -n "alter role $role with password '" > "$tmp_cmds"
    cat "$pwfile" >> "$tmp_cmds"
    echo "';" >> "$tmp_cmds"
    pgbox env psql -f "$tmp_cmds"
}

# Save newly created passwords to filesystem inside PuppetDB sandbox
save-passwd "$passwd" "$tmp_dir" "$pdbbox/test-pass"
save-passwd "$migrator_passwd" "$tmp_dir" "$pdbbox/test-pass-migrator"
save-passwd "$admin_passwd" "$tmp_dir" "$pdbbox/test-pass-admin"
save-passwd "$read_passwd" "$tmp_dir" "$pdbbox/test-pass-read"

# Create PostgreSQL password file
# https://www.postgresql.org/docs/14/libpq-pgpass.html
tmp_pass="$(mktemp "$tmp_dir/tmp-pass-XXXXXX")"
chmod 0600 "$tmp_pass" # Not entirely safe, but expected to be redundant
cat > "$tmp_pass" <<-EOF
	# hostname:port:database:username:password
	*:*:*:pdb_test:$passwd
	*:*:*:pdb_test_migrator:$migrator_passwd
	*:*:*:pdb_test_read:$read_passwd
	*:*:*:pdb_test_admin:$admin_passwd
	*:*:*:puppetdb:$passwd
	*:*:*:puppetdb_read:$read_passwd
	*:*:*:puppetdb_migrator:$migrator_passwd
	*:*:*:postgres:$pg_passwd
	EOF
mv "$tmp_pass" "$PGBOX/pgpass"

setup-roles()
{
  local read_role="$1" write_role="$2" migrator_role="$3"
  # The migrator needs the write role for migrations, so it can
  # terminate any existing read and write role connections
  # (transitively for the read role,via the write role's read role
  # membership) after locking out new connections (by revoking the
  # connect access that the migrator granted to the read and write
  # roles).
  pgbox env psql -c "grant $write_role to $migrator_role"
  # The write role needs the pdb_test_read role for the partition gc
  # bulldozer, so it can terminate any queries blocking the drop.
  pgbox env psql -c "grant $read_role to $write_role"
}

# Create Postgres testing roles
pgbox env createuser -dERs pdb_test_admin
pgbox env createuser -DERS pdb_test
pgbox env createuser -DERS pdb_test_migrator
pgbox env createuser -DERS pdb_test_read

tmp_cmds="$(mktemp "$tmp_dir/tmp-cmds-XXXXXX")"

# Set passwords for Postgres testing roles
set-passwd pdb_test "$pdbbox/test-pass" "$tmp_dir"
set-passwd pdb_test_admin "$pdbbox/test-pass-admin" "$tmp_dir"
set-passwd pdb_test_migrator "$pdbbox/test-pass-migrator" "$tmp_dir"
set-passwd pdb_test_read "$pdbbox/test-pass-read" "$tmp_dir"

setup-roles pdb_test_read pdb_test pdb_test_migrator

# Creates a PuppetDB database. Loads necessary extensions, sets permissions
setup-pdb()
{
  local dbname="$1"
  pgbox env createdb -E UTF8 -O postgres "$dbname"
  pgbox env psql "$dbname" <<ADMIN
    create extension pg_trgm;

    revoke create on schema public from public;
    grant create on schema public to puppetdb;

    -- Grant read to all future tables in public schema to the read-only user
    alter default privileges for user puppetdb in schema public grant select on tables to puppetdb_read;

    -- configure the migrator user
    revoke connect on database $dbname from public;
    grant connect on database $dbname to puppetdb_migrator with grant option;
ADMIN

  pgbox env psql "$dbname" -U puppetdb_migrator <<MIGRATOR
    grant connect on database $dbname to puppetdb;
    grant connect on database $dbname to puppetdb_read;
MIGRATOR
}

# Create main PuppetDB postgres roles
pgbox env createuser -DRS puppetdb
pgbox env createuser -DRS puppetdb_read
pgbox env createuser -DRS puppetdb_migrator

setup-roles puppetdb_read puppetdb puppetdb_migrator

# Set passwords for main PuppetDB postgres roles
set-passwd puppetdb "$pdbbox/test-pass" "$tmp_dir"
set-passwd puppetdb_read "$pdbbox/test-pass-read" "$tmp_dir"
set-passwd puppetdb_migrator "$pdbbox/test-pass-migrator" "$tmp_dir"

setup-pdb puppetdb
setup-pdb puppetdb2

mkdir "$pdbbox/ssl"
(cd "$pdbbox/ssl"
 openssl genrsa -out ca.key.pem 2048
 openssl req -x509 -new -nodes -days 1000 -key ca.key.pem -out ca.crt.pem \
         -subj "/C=US/ST=confusion/L=unknown/O=none/CN=pdbca.localhost"

  openssl genrsa -out pdb.key.pem 2048
  openssl req -new -key pdb.key.pem -out pdb.csr.pem \
         -subj "/C=US/ST=confusion/L=unknown/O=none/CN=pdb.localhost"

  openssl x509 -req -in pdb.csr.pem -days 1000 \
          -CA ca.crt.pem -CAkey ca.key.pem -CAcreateserial \
          -out pdb.crt.pem)

abs_pdbbox="$(cd "$pdbbox" && pwd)"

# Create logging config file
cat > "$pdbbox/logback.xml" <<EOF
<configuration scan="true">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %-5p [%thread] [%c{2}] %m%n</pattern>
        </encoder>
    </appender>

    <!-- Silence particularly noisy packages -->
    <logger name="org.apache.activemq" level="warn"/>
    <logger name="org.apache.activemq.store.kahadb.MessageDatabase"
        level="info"/>
    <logger name="org.springframework.jms.connection" level="warn"/>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>
EOF

host=${bind_addr[${#bind_addr[@]}-1]}

# Create PuppetDB main config file
confdir="$pdbbox/conf.d"
mkdir "$confdir"
cat > "$confdir/pdb.ini" <<EOF

[global]
vardir = $abs_pdbbox/var
logging-config = $abs_pdbbox/logback.xml

[database]
subname = //$host:$pgport/puppetdb
username = puppetdb
password = $passwd
migrator-username = puppetdb_migrator
migrator-password = $migrator_passwd

[read-database]
subname = //$host:$pgport/puppetdb
username = puppetdb_read
password = $read_passwd

[nrepl]
enabled = false

[jetty]
port = 8080
ssl-port = 8081
ssl-ca-cert = test-resources/ca.pem
ssl-cert = test-resources/localhost.pem
ssl-key = test-resources/localhost.key
client-auth = want

[puppetdb]
disable-update-checking = true
EOF

# Create PuppetDB auth config file
cat > "$confdir/auth.conf" <<EOF
authorization: {
    version: 1
    rules: [
        {
            # Allow unauthenticated access to the status service endpoint
            match-request: {
                path: "/status/v1/services"
                type: path
                method: get
            }
            allow-unauthenticated: true
            sort-order: 500
            name: "puppetlabs status service - full"
        },
        {
            match-request: {
                path: "/status/v1/simple"
                type: path
                method: get
            }
            allow-unauthenticated: true
            sort-order: 500
            name: "puppetlabs status service - simple"
        },
        {
            # Allow nodes to access the metrics service
            # for puppetdb, the metrics service is the only
            # service using the authentication service
            match-request: {
                path: "/metrics"
                type: path
                method: [get, post]
            }
            allow: "*"
            sort-order: 500
            name: "puppetlabs puppetdb metrics"
        },
        {
            # Deny everything else. This ACL is not strictly
            # necessary, but illustrates the default policy
            match-request: {
                path: "/"
                type: path
            }
            deny: "*"
            sort-order: 999
            name: "puppetlabs puppetdb deny all"
        }
    ]
}
EOF

# do some tuning on PostgreSQL

# generally accepted configuration parameters:
# Set the shared_buffers to be 1/4 of the amount of RAM

case "$OSTYPE" in
    darwin*)
        total_ram_bytes=$(sysctl hw.memsize | awk '{ print $2 }')
        total_ram_kb=$(($total_ram_bytes / 1024)) ;;
    *)
        total_ram_kb=$(grep MemTotal: /proc/meminfo | awk '{ print $2 }') ;;
esac

total_ram_mb=$(($total_ram_kb / 1024))
# these values come from pgtune and the postgresql tuning wiki

# shared buffers should max out at 1 GB of RAM
shared_buffers=$(($total_ram_mb / 4))
if (( $shared_buffers > 1024 )); then
    shared_buffers=1024
fi

# do not let this be less than the default value
maintenance_work_mem=$(($total_ram_mb / 15))
if (( $maintenance_work_mem < 64 )); then
    maintenance_work_mem=64
fi

# Append to PostgreSQL config
cat >> "$pdbbox/pg/data/postgresql.conf" <<EOF

# tuning parameters
shared_buffers = ${shared_buffers}MB

default_statistics_target = 50

maintenance_work_mem = ${maintenance_work_mem}MB

# aim to finish by the time 90% of the next checkpoint is here
# this will spread out writes
checkpoint_completion_target = 0.9

work_mem = 96MB

# Increasing wal_buffers from its tiny default of a small number of kilobytes
# is helpful for write-heavy systems, 16MB is the effective upper bound
wal_buffers = 8MB
EOF

# Make empty pdbbox file to signify that this directory is a PuppetDB sandbox
touch "$pdbbox/pdbbox"
