mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-08-14 07:50:34 +02:00
feat: add optional MongoDB replica deployment (#765)
This commit is contained in:
@@ -3,6 +3,7 @@ build
|
|||||||
/repositories
|
/repositories
|
||||||
repo/
|
repo/
|
||||||
db_backups
|
db_backups
|
||||||
|
/secrets/mongo-replica-keyfile
|
||||||
message.txt
|
message.txt
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ docker-compose up -d
|
|||||||
|
|
||||||
**4. Open** <http://localhost:5000>. The port can be changed in `docker-compose.yml`; putting Anonymous GitHub behind nginx is recommended for HTTPS.
|
**4. Open** <http://localhost:5000>. The port can be changed in `docker-compose.yml`; putting Anonymous GitHub behind nginx is recommended for HTTPS.
|
||||||
|
|
||||||
|
For an optional remote, hidden, delayed MongoDB replica and backup source, see
|
||||||
|
the [MongoDB replication guide](docs/mongodb-replication.md).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Scope of anonymization
|
## Scope of anonymization
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
entrypoint:
|
||||||
|
- /bin/bash
|
||||||
|
- /opt/anonymous-github/mongodb-replica-entrypoint.sh
|
||||||
|
command:
|
||||||
|
- --quiet
|
||||||
|
- --replSet
|
||||||
|
- rs0
|
||||||
|
- --keyFile
|
||||||
|
- /tmp/mongo-replica-keyfile
|
||||||
|
- --bind_ip_all
|
||||||
|
environment:
|
||||||
|
MONGO_REPLICA_KEYFILE_PATH: /run/secrets/mongo-replica-keyfile
|
||||||
|
volumes:
|
||||||
|
- ${MONGO_REPLICA_KEYFILE:-./secrets/mongo-replica-keyfile}:/run/secrets/mongo-replica-keyfile:ro
|
||||||
|
- ./scripts/mongodb-replica-entrypoint.sh:/opt/anonymous-github/mongodb-replica-entrypoint.sh:ro
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: anonymous-github-mongo-secondary
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongodb-secondary:
|
||||||
|
image: ${MONGO_IMAGE:-mongo:latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
entrypoint:
|
||||||
|
- /bin/bash
|
||||||
|
- /opt/anonymous-github/mongodb-replica-entrypoint.sh
|
||||||
|
command:
|
||||||
|
- --quiet
|
||||||
|
- --replSet
|
||||||
|
- rs0
|
||||||
|
- --keyFile
|
||||||
|
- /tmp/mongo-replica-keyfile
|
||||||
|
- --bind_ip_all
|
||||||
|
environment:
|
||||||
|
MONGO_REPLICA_KEYFILE_PATH: /run/secrets/mongo-replica-keyfile
|
||||||
|
volumes:
|
||||||
|
- mongodb_secondary_data:/data/db
|
||||||
|
- ${MONGO_REPLICA_KEYFILE:-./secrets/mongo-replica-keyfile}:/run/secrets/mongo-replica-keyfile:ro
|
||||||
|
- ./scripts/mongodb-replica-entrypoint.sh:/opt/anonymous-github/mongodb-replica-entrypoint.sh:ro
|
||||||
|
ports:
|
||||||
|
- ${MONGO_BIND_ADDRESS:-127.0.0.1}:27017:27017
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mongosh
|
||||||
|
- --quiet
|
||||||
|
- --eval
|
||||||
|
- "db.adminCommand('ping')"
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 12
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_secondary_data:
|
||||||
+2
-2
@@ -79,7 +79,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: ${MONGO_IMAGE:-mongo:latest}
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
MONGO_INITDB_ROOT_USERNAME: $DB_USERNAME
|
MONGO_INITDB_ROOT_USERNAME: $DB_USERNAME
|
||||||
@@ -87,7 +87,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mongodb_data_container:/data/db
|
- mongodb_data_container:/data/db
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:27017:27017
|
- ${MONGO_BIND_ADDRESS:-127.0.0.1}:27017:27017
|
||||||
command: --quiet
|
command: --quiet
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# Optional remote MongoDB replica
|
||||||
|
|
||||||
|
This deployment option converts the existing standalone MongoDB container into
|
||||||
|
a replica set named `rs0` and adds a remote, hidden, non-voting secondary.
|
||||||
|
Normal single-node deployments continue to use `docker-compose.yml` alone.
|
||||||
|
|
||||||
|
The remote member is designed as a recovery and backup source:
|
||||||
|
|
||||||
|
- It cannot become primary.
|
||||||
|
- Its availability does not affect production majority writes.
|
||||||
|
- It is hidden from application traffic.
|
||||||
|
- It is delayed by one hour by default, providing a short recovery window for
|
||||||
|
accidental deletes or a bad migration.
|
||||||
|
|
||||||
|
Replication is not a backup. Destructive changes eventually reach every
|
||||||
|
member. Continue making encrypted, off-host `mongodump` archives from the
|
||||||
|
secondary.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. Take and verify a `mongodump` backup before converting production.
|
||||||
|
2. Install this repository on both servers.
|
||||||
|
3. Connect the servers through a trusted private network such as WireGuard,
|
||||||
|
Tailscale, a private VLAN, or a cloud private network.
|
||||||
|
4. Create stable DNS names that resolve from both servers and from the
|
||||||
|
application container, for example:
|
||||||
|
|
||||||
|
- `mongo-primary.internal`
|
||||||
|
- `mongo-secondary.internal`
|
||||||
|
|
||||||
|
5. Permit TCP port `27017` only between the required private-network hosts.
|
||||||
|
Do not expose MongoDB to the public internet.
|
||||||
|
6. Run the exact same MongoDB version on both servers. Find the current
|
||||||
|
production version with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec mongodb mongod --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the matching image in both servers' `.env` files:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MONGO_IMAGE=mongo:<exact-version>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Generate the shared member key
|
||||||
|
|
||||||
|
On the primary server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh generate-key
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `secrets/mongo-replica-keyfile` without overwriting an existing
|
||||||
|
key. Copy that exact file securely to the same repository-relative path on the
|
||||||
|
secondary server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp secrets/mongo-replica-keyfile \
|
||||||
|
secondary-server:/path/to/anonymous_github/secrets/mongo-replica-keyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep this key outside source control and backups that are accessible to
|
||||||
|
untrusted users. Every replica-set member must share the same key.
|
||||||
|
|
||||||
|
## 2. Configure production
|
||||||
|
|
||||||
|
Add these values to the primary server's `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Use the exact version already running in production.
|
||||||
|
MONGO_IMAGE=mongo:<exact-version>
|
||||||
|
|
||||||
|
# The primary server's private/VPN interface.
|
||||||
|
MONGO_BIND_ADDRESS=<primary-private-ip>
|
||||||
|
MONGO_REPLICA_KEYFILE=./secrets/mongo-replica-keyfile
|
||||||
|
|
||||||
|
# Make the replica overlay the default for future Compose commands.
|
||||||
|
COMPOSE_FILE=docker-compose.yml:docker-compose.replica-primary.yml
|
||||||
|
|
||||||
|
# URL-encode special characters in the username and password.
|
||||||
|
MONGODB_URI=mongodb://<user>:<password>@mongo-primary.internal:27017/production?authSource=admin&replicaSet=rs0&retryWrites=true&w=majority
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop application writers, leaving MongoDB available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop anonymous_github streamer
|
||||||
|
```
|
||||||
|
|
||||||
|
Start MongoDB with replica-set support and initialize the existing database as
|
||||||
|
the primary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh primary-up \
|
||||||
|
mongo-primary.internal:27017
|
||||||
|
```
|
||||||
|
|
||||||
|
`primary-up` is idempotent. If `rs0` is already initialized, it reports that
|
||||||
|
state instead of replacing the replica-set configuration.
|
||||||
|
|
||||||
|
## 3. Start the remote secondary
|
||||||
|
|
||||||
|
On the secondary server, create a minimal `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MONGO_IMAGE=mongo:<same-exact-version-as-primary>
|
||||||
|
MONGO_BIND_ADDRESS=<secondary-private-ip>
|
||||||
|
MONGO_REPLICA_KEYFILE=./secrets/mongo-replica-keyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Start its empty MongoDB data volume:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh secondary-up
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not copy the primary's live WiredTiger volume. MongoDB performs the initial
|
||||||
|
sync after the member is added.
|
||||||
|
|
||||||
|
## 4. Add and verify the secondary
|
||||||
|
|
||||||
|
Back on the primary server, add the remote member with the default one-hour
|
||||||
|
delay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh add-secondary \
|
||||||
|
mongo-secondary.internal:27017
|
||||||
|
```
|
||||||
|
|
||||||
|
To keep the remote copy current instead, explicitly set a zero-second delay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh add-secondary \
|
||||||
|
mongo-secondary.internal:27017 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Check initial-sync and replication state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait until the remote member reports `SECONDARY`, then restart the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d anonymous_github streamer redis
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
Use the primary overlay for every future operation on the production stack.
|
||||||
|
Keeping `COMPOSE_FILE` in `.env` makes ordinary commands such as
|
||||||
|
`docker compose up -d` and `docker compose logs mongodb` use it automatically.
|
||||||
|
|
||||||
|
Check replica status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mongodb-replica.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
If a destructive production operation occurs and the secondary is delayed,
|
||||||
|
stop the secondary container immediately before the delay window elapses:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.replica-secondary.yml stop mongodb-secondary
|
||||||
|
```
|
||||||
|
|
||||||
|
Then take a copy or logical dump of the delayed data before attempting
|
||||||
|
recovery.
|
||||||
|
|
||||||
|
For automatic failover, use three voting data-bearing members instead of
|
||||||
|
turning this two-server recovery topology into a two-voter replica set. Two
|
||||||
|
voters require both servers to acknowledge a majority and can make production
|
||||||
|
unwritable during a network outage.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- All members must use the same replica-set name (`rs0`), MongoDB version, and
|
||||||
|
shared keyfile.
|
||||||
|
- The hostnames stored in `rs.conf()` must resolve from every member.
|
||||||
|
- The application container must also resolve `mongo-primary.internal`. Use
|
||||||
|
private DNS or a Compose `extra_hosts` entry if host DNS is not propagated
|
||||||
|
into Docker.
|
||||||
|
- Check container logs with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs mongodb
|
||||||
|
docker compose -f docker-compose.replica-secondary.yml logs mongodb-secondary
|
||||||
|
```
|
||||||
|
|
||||||
|
- Re-run `primary-up`, `add-secondary`, or `status` safely; the management
|
||||||
|
operations do not replace existing replica-set members.
|
||||||
|
|
||||||
|
## MongoDB references
|
||||||
|
|
||||||
|
- [Convert a standalone server to a replica set](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/)
|
||||||
|
- [Deploy a replica set with member authentication](https://www.mongodb.com/docs/v8.0/tutorial/deploy-replica-set-with-keyfile-access-control/)
|
||||||
|
- [Configure a delayed member](https://www.mongodb.com/docs/manual/tutorial/configure-a-delayed-replica-set-member/)
|
||||||
|
- [Hidden replica-set members](https://www.mongodb.com/docs/manual/core/replica-set-hidden-member/)
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
source_keyfile="${MONGO_REPLICA_KEYFILE_PATH:-/run/secrets/mongo-replica-keyfile}"
|
||||||
|
runtime_keyfile="/tmp/mongo-replica-keyfile"
|
||||||
|
|
||||||
|
if [[ ! -s "$source_keyfile" ]]; then
|
||||||
|
echo "MongoDB replica keyfile is missing or empty: $source_keyfile" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bind-mounted secrets commonly have host ownership or permissive modes that
|
||||||
|
# mongod rejects. Copy to an ephemeral location with the required ownership
|
||||||
|
# and permissions before the official image entrypoint drops privileges.
|
||||||
|
install -m 400 -o mongodb -g mongodb "$source_keyfile" "$runtime_keyfile"
|
||||||
|
|
||||||
|
exec /usr/local/bin/docker-entrypoint.sh "$@"
|
||||||
Executable
+176
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
primary_files=(
|
||||||
|
-f "$repository_root/docker-compose.yml"
|
||||||
|
-f "$repository_root/docker-compose.replica-primary.yml"
|
||||||
|
)
|
||||||
|
secondary_files=(
|
||||||
|
-f "$repository_root/docker-compose.replica-secondary.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
compose_prefix=(docker compose)
|
||||||
|
if [[ -n "${COMPOSE_ENV_FILE:-}" ]]; then
|
||||||
|
compose_prefix+=(--env-file "$COMPOSE_ENV_FILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
keyfile="${MONGO_REPLICA_KEYFILE:-$repository_root/secrets/mongo-replica-keyfile}"
|
||||||
|
if [[ "$keyfile" != /* ]]; then
|
||||||
|
keyfile="$repository_root/${keyfile#./}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
./scripts/mongodb-replica.sh generate-key
|
||||||
|
./scripts/mongodb-replica.sh primary-up <primary-hostname:port>
|
||||||
|
./scripts/mongodb-replica.sh secondary-up
|
||||||
|
./scripts/mongodb-replica.sh add-secondary <secondary-hostname:port> [delay-seconds]
|
||||||
|
./scripts/mongodb-replica.sh status
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
COMPOSE_ENV_FILE Optional Compose environment file.
|
||||||
|
MONGO_REPLICA_KEYFILE Shared keyfile path (default: ./secrets/mongo-replica-keyfile).
|
||||||
|
|
||||||
|
The primary and secondary hostnames must resolve from both MongoDB servers.
|
||||||
|
The remote member is added hidden, priority 0, and non-voting so a WAN outage
|
||||||
|
cannot block production majority writes. Set delay-seconds to 0 for no delay.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_host() {
|
||||||
|
if [[ ! "$1" =~ ^[A-Za-z0-9.-]+:[0-9]{1,5}$ ]]; then
|
||||||
|
echo "Expected a resolvable hostname and port, for example mongo-primary.internal:27017" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_delay() {
|
||||||
|
if [[ ! "$1" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Delay must be a non-negative number of seconds" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_keyfile() {
|
||||||
|
if [[ ! -s "$keyfile" ]]; then
|
||||||
|
echo "Replica keyfile not found: $keyfile" >&2
|
||||||
|
echo "Run ./scripts/mongodb-replica.sh generate-key first." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
primary_compose() {
|
||||||
|
"${compose_prefix[@]}" "${primary_files[@]}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondary_compose() {
|
||||||
|
"${compose_prefix[@]}" "${secondary_files[@]}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
mongo_eval() {
|
||||||
|
local javascript="$1"
|
||||||
|
primary_compose exec -T -e "MONGO_RS_EVAL=$javascript" mongodb sh -eu -c \
|
||||||
|
'mongosh --quiet \
|
||||||
|
--username="$MONGO_INITDB_ROOT_USERNAME" \
|
||||||
|
--password="$MONGO_INITDB_ROOT_PASSWORD" \
|
||||||
|
--authenticationDatabase=admin \
|
||||||
|
--eval "$MONGO_RS_EVAL"'
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_primary_container() {
|
||||||
|
local attempt
|
||||||
|
for attempt in $(seq 1 60); do
|
||||||
|
if primary_compose exec -T mongodb sh -eu -c \
|
||||||
|
'mongosh --quiet \
|
||||||
|
--username="$MONGO_INITDB_ROOT_USERNAME" \
|
||||||
|
--password="$MONGO_INITDB_ROOT_PASSWORD" \
|
||||||
|
--authenticationDatabase=admin \
|
||||||
|
--eval "db.adminCommand({ ping: 1 }).ok"' >/dev/null 2>&1; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "MongoDB did not become ready within 120 seconds" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command="${1:-}"
|
||||||
|
case "$command" in
|
||||||
|
generate-key)
|
||||||
|
if [[ -e "$keyfile" ]]; then
|
||||||
|
echo "Refusing to overwrite existing keyfile: $keyfile" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$keyfile")"
|
||||||
|
umask 077
|
||||||
|
openssl rand -base64 756 > "$keyfile"
|
||||||
|
chmod 600 "$keyfile"
|
||||||
|
echo "Created $keyfile"
|
||||||
|
echo "Copy this exact file securely to the secondary server."
|
||||||
|
;;
|
||||||
|
|
||||||
|
primary-up)
|
||||||
|
primary_host="${2:-}"
|
||||||
|
validate_host "$primary_host"
|
||||||
|
require_keyfile
|
||||||
|
primary_compose up -d mongodb
|
||||||
|
wait_for_primary_container
|
||||||
|
mongo_eval "
|
||||||
|
try {
|
||||||
|
const status = rs.status();
|
||||||
|
print('Replica set already initialized as ' + status.set);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 94 && error.codeName !== 'NotYetInitialized') throw error;
|
||||||
|
printjson(rs.initiate({
|
||||||
|
_id: 'rs0',
|
||||||
|
members: [{ _id: 0, host: '$primary_host', priority: 1, votes: 1 }]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
secondary-up)
|
||||||
|
require_keyfile
|
||||||
|
secondary_compose up -d mongodb-secondary
|
||||||
|
;;
|
||||||
|
|
||||||
|
add-secondary)
|
||||||
|
secondary_host="${2:-}"
|
||||||
|
delay="${3:-3600}"
|
||||||
|
validate_host "$secondary_host"
|
||||||
|
validate_delay "$delay"
|
||||||
|
mongo_eval "
|
||||||
|
const host = '$secondary_host';
|
||||||
|
const existing = rs.conf().members.find((member) => member.host === host);
|
||||||
|
if (existing) {
|
||||||
|
print('Secondary already configured: ' + host);
|
||||||
|
} else {
|
||||||
|
printjson(rs.add({
|
||||||
|
host,
|
||||||
|
priority: 0,
|
||||||
|
votes: 0,
|
||||||
|
hidden: true,
|
||||||
|
secondaryDelaySecs: $delay
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
mongo_eval "
|
||||||
|
printjson(rs.status().members.map((member) => ({
|
||||||
|
name: member.name,
|
||||||
|
state: member.stateStr,
|
||||||
|
health: member.health,
|
||||||
|
replicationTime: member.optimeDate
|
||||||
|
})));
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
[[ -n "$command" ]] && exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
+10
-1
@@ -26,6 +26,12 @@ interface Config {
|
|||||||
DB_USERNAME: string;
|
DB_USERNAME: string;
|
||||||
DB_PASSWORD: string;
|
DB_PASSWORD: string;
|
||||||
DB_HOSTNAME: string;
|
DB_HOSTNAME: string;
|
||||||
|
/**
|
||||||
|
* Complete MongoDB connection string. When set, this takes precedence over
|
||||||
|
* DB_USERNAME, DB_PASSWORD, and DB_HOSTNAME and supports replica-set seed
|
||||||
|
* lists and options.
|
||||||
|
*/
|
||||||
|
MONGODB_URI: string;
|
||||||
FOLDER: string;
|
FOLDER: string;
|
||||||
additionalExtensions: string[];
|
additionalExtensions: string[];
|
||||||
S3_BUCKET: string | null;
|
S3_BUCKET: string | null;
|
||||||
@@ -73,6 +79,7 @@ const config: Config = {
|
|||||||
DB_USERNAME: "admin",
|
DB_USERNAME: "admin",
|
||||||
DB_PASSWORD: "password",
|
DB_PASSWORD: "password",
|
||||||
DB_HOSTNAME: "mongodb",
|
DB_HOSTNAME: "mongodb",
|
||||||
|
MONGODB_URI: "",
|
||||||
REDIS_HOSTNAME: "redis",
|
REDIS_HOSTNAME: "redis",
|
||||||
REDIS_PORT: 6379,
|
REDIS_PORT: 6379,
|
||||||
FOLDER: resolve(__dirname, "..", "repositories"),
|
FOLDER: resolve(__dirname, "..", "repositories"),
|
||||||
@@ -142,8 +149,10 @@ if (isProduction) {
|
|||||||
const insecureDefaults: [string, string][] = [
|
const insecureDefaults: [string, string][] = [
|
||||||
["CLIENT_ID", "CLIENT_ID"],
|
["CLIENT_ID", "CLIENT_ID"],
|
||||||
["CLIENT_SECRET", "CLIENT_SECRET"],
|
["CLIENT_SECRET", "CLIENT_SECRET"],
|
||||||
["DB_PASSWORD", "password"],
|
|
||||||
];
|
];
|
||||||
|
if (!config.MONGODB_URI) {
|
||||||
|
insecureDefaults.push(["DB_PASSWORD", "password"]);
|
||||||
|
}
|
||||||
for (const [key, badValue] of insecureDefaults) {
|
for (const [key, badValue] of insecureDefaults) {
|
||||||
if ((config as unknown as Record<string, unknown>)[key] === badValue) {
|
if ((config as unknown as Record<string, unknown>)[key] === badValue) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
+10
-4
@@ -8,7 +8,12 @@ import PullRequest from "../core/PullRequest";
|
|||||||
import AnonymizedGistModel from "../core/model/anonymizedGists/anonymizedGists.model";
|
import AnonymizedGistModel from "../core/model/anonymizedGists/anonymizedGists.model";
|
||||||
import Gist from "../core/Gist";
|
import Gist from "../core/Gist";
|
||||||
|
|
||||||
const MONGO_URL = `mongodb://${config.DB_USERNAME}:${config.DB_PASSWORD}@${config.DB_HOSTNAME}:27017/`;
|
export function getMongoUrl(): string {
|
||||||
|
return (
|
||||||
|
config.MONGODB_URI ||
|
||||||
|
`mongodb://${config.DB_USERNAME}:${config.DB_PASSWORD}@${config.DB_HOSTNAME}:27017/production`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const database = mongoose.connection;
|
export const database = mongoose.connection;
|
||||||
|
|
||||||
@@ -16,11 +21,12 @@ export let isConnected = false;
|
|||||||
|
|
||||||
export async function connect() {
|
export async function connect() {
|
||||||
mongoose.set("strictQuery", false);
|
mongoose.set("strictQuery", false);
|
||||||
await mongoose.connect(MONGO_URL + "production", {
|
const options: ConnectOptions = {
|
||||||
authSource: "admin",
|
|
||||||
appName: "Anonymous GitHub Server",
|
appName: "Anonymous GitHub Server",
|
||||||
compressors: "zstd",
|
compressors: "zstd",
|
||||||
} as ConnectOptions);
|
};
|
||||||
|
if (!config.MONGODB_URI) options.authSource = "admin";
|
||||||
|
await mongoose.connect(getMongoUrl(), options);
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
|
|
||||||
return database;
|
return database;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
require("ts-node/register/transpile-only");
|
||||||
|
const { expect } = require("chai");
|
||||||
|
|
||||||
|
const config = require("../src/config").default;
|
||||||
|
const { getMongoUrl } = require("../src/server/database");
|
||||||
|
|
||||||
|
describe("MongoDB connection configuration", function () {
|
||||||
|
const original = {
|
||||||
|
MONGODB_URI: config.MONGODB_URI,
|
||||||
|
DB_USERNAME: config.DB_USERNAME,
|
||||||
|
DB_PASSWORD: config.DB_PASSWORD,
|
||||||
|
DB_HOSTNAME: config.DB_HOSTNAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Object.assign(config, original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a complete replica-set URI when configured", function () {
|
||||||
|
const uri =
|
||||||
|
"mongodb://user:password@mongo-primary.internal:27017/production" +
|
||||||
|
"?authSource=admin&replicaSet=rs0&w=majority";
|
||||||
|
config.MONGODB_URI = uri;
|
||||||
|
|
||||||
|
expect(getMongoUrl()).to.equal(uri);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains the existing single-node connection fallback", function () {
|
||||||
|
config.MONGODB_URI = "";
|
||||||
|
config.DB_USERNAME = "user";
|
||||||
|
config.DB_PASSWORD = "password";
|
||||||
|
config.DB_HOSTNAME = "mongodb";
|
||||||
|
|
||||||
|
expect(getMongoUrl()).to.equal(
|
||||||
|
"mongodb://user:password@mongodb:27017/production"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user