-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathcompose
More file actions
executable file
·143 lines (126 loc) · 6.11 KB
/
Copy pathcompose
File metadata and controls
executable file
·143 lines (126 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/bin/bash
# Passed to `docker compose` natively so file contents are parsed as plain KEY=value pairs,
# never executed as shell (unlike `source`/`export $(... | xargs)`, both of which let a value
# containing $(...) or backticks run arbitrary commands).
#
# Merged into a single temp file rather than passed as two separate --env-file flags: compose
# only uses the FIRST --env-file for interpolating docker-compose.yml itself (e.g. the
# ${LOCAL_DATABASE_PORT} port mapping) - a second flag is silently ignored for that purpose,
# even though per-service `env_file:` lists do merge both files correctly. A single merged
# file sidesteps the ordering footgun entirely.
ENV_FILE_ARGS=()
if [ -f .env.public ] || [ -f .env.private ]
then
MERGED_ENV_FILE="$(mktemp)"
trap 'rm -f "$MERGED_ENV_FILE"' EXIT
[ -f .env.public ] && cat .env.public >> "$MERGED_ENV_FILE"
[ -f .env.private ] && cat .env.private >> "$MERGED_ENV_FILE"
ENV_FILE_ARGS+=(--env-file "$MERGED_ENV_FILE")
fi
# Grafana's Discord-login allowlist is derived from ADMINS so it stays a single source of
# truth. Extracted with grep/cut (no shell evaluation of the file's contents) and strictly
# validated as a comma-separated list of numeric Discord snowflakes before use.
admins_raw=''
if [ -f .env.private ]
then
admins_raw="$(grep -m1 '^ADMINS=' .env.private | cut -d= -f2-)"
fi
if [ -n "$admins_raw" ]
then
if ! [[ "$admins_raw" =~ ^[0-9]+(,[[:space:]]*[0-9]+)*$ ]]
then
echo "ADMINS in .env.private must be a comma-separated list of numeric Discord IDs, got: $admins_raw" >&2
exit 1
fi
IFS=', ' read -ra _admin_ids <<< "$admins_raw"
_quoted_ids=""
for _id in "${_admin_ids[@]}"
do
_quoted_ids="${_quoted_ids}${_quoted_ids:+,}'${_id}'"
done
export GRAFANA_OAUTH_ROLE_ATTRIBUTE_PATH="contains([${_quoted_ids}],id)&&'Admin'"
fi
# Replica counts for sharded bots are computed here rather than written down anywhere, so the only
# number a human maintains is <BOT>_SHARDS_PER_REPLICA. Discord's own /gateway/bot recommendation
# decides the shard count; each replica then works out which slice is its own against redis (see
# packages/private/bot-core/src/lib/replica.ts).
#
# Host-side on purpose: this box already has docker access, so nothing needs the docker socket
# mounted into a container that processes untrusted Discord input.
# Same precedence as the merged env file above (.env.private is concatenated last, so it wins), and the
# same grep/cut approach as the ADMINS block: read the value without ever letting the file's contents be
# evaluated as shell.
#
# Presence in .env.private wins, not just a non-empty value there. `KEY=` is a deliberate "not configured
# here" -- it is how .env.public spells the default -- and `${private:-$public}` would silently fall back
# to the public value instead. The container reads the merged file and would see the empty one, so this
# script must agree with it or it would size a service from a value the bots themselves never receive.
read_env() {
local key="$1" value=''
if [ -f .env.public ] && grep -q "^${key}=" .env.public
then
value="$(grep -m1 "^${key}=" .env.public | cut -d= -f2-)"
fi
if [ -f .env.private ] && grep -q "^${key}=" .env.private
then
value="$(grep -m1 "^${key}=" .env.private | cut -d= -f2-)"
fi
printf '%s' "$value"
}
SCALE_ARGS=()
plan_scale() {
local service="$1" token_key="$2" shards_per_replica_key="$3"
local shards_per_replica token response shards replicas running
shards_per_replica="$(read_env "$shards_per_replica_key")"
# Unset means this bot isn't sharded across replicas, which is the default -- leave compose alone.
[ -n "$shards_per_replica" ] || return 0
if ! [[ "$shards_per_replica" =~ ^[0-9]+$ ]] || [ "$shards_per_replica" -lt 1 ]
then
echo "${shards_per_replica_key} must be a positive integer, got: ${shards_per_replica}" >&2
exit 1
fi
token="$(read_env "$token_key")"
if [ -z "$token" ]
then
echo "warning: ${shards_per_replica_key} is set but ${token_key} is empty; leaving ${service} scale untouched" >&2
return 0
fi
# The token goes in via a curl config on stdin rather than as an argument: anything in argv is readable
# from the process list by any local user for as long as the request runs.
response="$(printf 'header = "Authorization: Bot %s"\nsilent\nshow-error\nmax-time = 10\nurl = "%s"\n' \
"$token" 'https://discord.com/api/v10/gateway/bot' | curl -K - 2>/dev/null)" || response=''
shards="$(printf '%s' "$response" | grep -o '"shards"[[:space:]]*:[[:space:]]*[0-9]\+' | grep -o '[0-9]\+$')"
if ! [[ "$shards" =~ ^[0-9]+$ ]] || [ "$shards" -lt 1 ]
then
# Deliberately keeps whatever is already running instead of falling back to 1: silently scaling a
# sharded bot down to a single replica because Discord was briefly unreachable would be a far worse
# outcome than deploying with a stale-but-correct replica count.
running="$(docker compose -f docker-compose.yml "${ENV_FILE_ARGS[@]}" ps -q "$service" 2>/dev/null | grep -c .)"
if [ "${running:-0}" -gt 0 ]
then
echo "warning: could not read Discord's shard count for ${service}; holding at ${running} replica(s)" >&2
SCALE_ARGS+=(--scale "${service}=${running}")
else
echo "warning: could not read Discord's shard count for ${service}; leaving its scale untouched" >&2
fi
return 0
fi
replicas=$(( (shards + shards_per_replica - 1) / shards_per_replica ))
echo "${service}: ${shards} shard(s) / ${shards_per_replica} per replica -> ${replicas} replica(s)"
SCALE_ARGS+=(--scale "${service}=${replicas}")
}
# Only for `up`. Every other subcommand (logs, stop, ps, exec, ...) must not silently restructure the
# deployment, and `--scale` isn't meaningful for them anyway.
if [ "$1" = 'up' ]
then
plan_scale ama-bot AMA_BOT_TOKEN AMA_SHARDS_PER_REPLICA
# Only the public deployment: a custom instance (#216) is single-guild by definition, so it is always
# one shard and one replica.
plan_scale modmail-bot MODMAIL_BOT_TOKEN MODMAIL_SHARDS_PER_REPLICA
plan_scale social-bot SOCIAL_BOT_TOKEN SOCIAL_SHARDS_PER_REPLICA
fi
docker compose \
-f docker-compose.yml \
"${ENV_FILE_ARGS[@]}" \
"$@" \
"${SCALE_ARGS[@]}"