-
Notifications
You must be signed in to change notification settings - Fork 994
Expand file tree
/
Copy pathepctl-docker-test.sh
More file actions
executable file
·449 lines (400 loc) · 16.8 KB
/
Copy pathepctl-docker-test.sh
File metadata and controls
executable file
·449 lines (400 loc) · 16.8 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONTAINER_NAME="epctl-systemd-test-$$"
readonly BASE_IMAGE="${EPCTL_DOCKER_IMAGE:-ubuntu:24.04}"
ACTIVE_LANG=""
POSITIONAL_ARGS=()
detect_default_language() {
local raw normalized
raw="${EPCTL_LANG:-}"
if [[ -z "${raw}" ]]; then
printf 'zh\n'
return
fi
normalized="${raw,,}"
case "${normalized}" in
zh|zh-*|zh_*|cn|chs|cht|中文)
printf 'zh\n'
;;
en|en-*|en_*|english)
printf 'en\n'
;;
*)
printf '%s\n' "${raw}"
;;
esac
}
set_language() {
local requested normalized fallback_lang
requested="${1:-$(detect_default_language)}"
normalized="${requested,,}"
fallback_lang="${ACTIVE_LANG:-$(detect_default_language)}"
case "${normalized}" in
zh|zh-*|zh_*|cn|chs|cht|中文)
ACTIVE_LANG="zh"
;;
en|en-*|en_*|english)
ACTIVE_LANG="en"
;;
*)
ACTIVE_LANG="${fallback_lang}"
die "$(trf unsupported_language "${requested}")"
;;
esac
export EPCTL_LANG="${ACTIVE_LANG}"
}
trf() {
local key="$1"
shift || true
local template
case "${ACTIVE_LANG}:${key}" in
en:usage) template='Usage:' ;;
zh:usage) template='用法:' ;;
en:example) template='Example:' ;;
zh:example) template='示例:' ;;
en:missing_dependency) template='missing dependency: %s' ;;
zh:missing_dependency) template='缺少依赖:%s' ;;
en:arg_requires_value) template='%s requires a value' ;;
zh:arg_requires_value) template='%s 需要一个参数值' ;;
en:unsupported_language) template='unsupported language: %s' ;;
zh:unsupported_language) template='不支持的语言:%s' ;;
en:unsupported_arch) template='unsupported architecture: %s' ;;
zh:unsupported_arch) template='不支持的架构:%s' ;;
en:test_failed_collecting) template='test failed; collecting container diagnostics' ;;
zh:test_failed_collecting) template='测试失败,正在收集容器诊断信息' ;;
en:install_tag_invalid) template='install tag must be a real GitHub release tag such as v1.0.6' ;;
zh:install_tag_invalid) template='安装 tag 必须是真实的 GitHub release tag,例如 v1.0.6' ;;
en:upgrade_tag_invalid) template='upgrade tag must be a real GitHub release tag such as v1.0.6' ;;
zh:upgrade_tag_invalid) template='升级 tag 必须是真实的 GitHub release tag,例如 v1.0.6' ;;
en:base_image) template='base image: %s' ;;
zh:base_image) template='基础镜像:%s' ;;
en:starting_container) template='starting the Ubuntu systemd test container directly from %s' ;;
zh:starting_container) template='正在直接从 %s 启动 Ubuntu systemd 测试容器' ;;
en:systemd_not_ready) template='systemd did not become ready inside the container' ;;
zh:systemd_not_ready) template='容器内的 systemd 未能及时就绪' ;;
en:self_installing) template='installing epctl into PATH' ;;
zh:self_installing) template='正在将 epctl 安装到 PATH' ;;
en:downloading_release) template='downloading release %s' ;;
zh:downloading_release) template='正在下载 release %s' ;;
en:installing_release) template='installing epusdt from release %s' ;;
zh:installing_release) template='正在从 release %s 安装 epusdt' ;;
en:waiting_assets) template='waiting for epusdt to start and release www assets' ;;
zh:waiting_assets) template='正在等待 epusdt 启动并释放 www 静态文件' ;;
en:upgrading_release) template='upgrading epusdt from %s to %s' ;;
zh:upgrading_release) template='正在将 epusdt 从 %s 升级到 %s' ;;
en:verifying_upgrade_skip_restart) template='verifying that `upgrade --no-restart` skips the restart and keeps the current process running' ;;
zh:verifying_upgrade_skip_restart) template='正在验证 `upgrade --no-restart` 会跳过重启,并保持当前进程继续运行' ;;
en:verifying_upgrade_default_restart) template='verifying that a non-interactive upgrade restarts the service by default' ;;
zh:verifying_upgrade_default_restart) template='正在验证非交互升级默认会重启服务' ;;
en:verifying_upgrade_prompt_skip) template='verifying that `upgrade --prompt-restart` can skip the restart when `n` is entered' ;;
zh:verifying_upgrade_prompt_skip) template='正在验证 `upgrade --prompt-restart` 在输入 `n` 时会跳过重启' ;;
en:verifying_upgrade_prompt_restart) template='verifying that `upgrade --prompt-restart` restarts the service by default when Enter is pressed' ;;
zh:verifying_upgrade_prompt_restart) template='正在验证 `upgrade --prompt-restart` 在直接回车时默认会重启服务' ;;
en:checking_init_password) template='checking init-password behavior' ;;
zh:checking_init_password) template='正在检查 init-password 行为' ;;
en:expected_second_failure) template='expected init-password to fail after the password was changed' ;;
zh:expected_second_failure) template='密码修改后,init-password 本应失败,但它却成功了' ;;
en:verification_succeeded) template='docker verification succeeded for install=%s%s' ;;
zh:verification_succeeded) template='Docker 验证通过,install=%s%s' ;;
*)
template="missing translation: ${key}"
;;
esac
printf -- "${template}" "$@"
}
log() {
printf '[epctl-docker-test] %s\n' "$*" >&2
}
die() {
printf '[epctl-docker-test] error: %s\n' "$*" >&2
exit 1
}
usage() {
if [[ "${ACTIVE_LANG}" == "zh" ]]; then
cat <<'EOF'
用法:
./epctl-docker-test.sh [--lang zh|en] <install-tag> [upgrade-tag]
示例:
./epctl-docker-test.sh v1.0.6
./epctl-docker-test.sh --lang zh v1.0.6 v1.0.8
EOF
else
cat <<'EOF'
Usage:
./epctl-docker-test.sh [--lang zh|en] <install-tag> [upgrade-tag]
Example:
./epctl-docker-test.sh v1.0.6
./epctl-docker-test.sh --lang en v1.0.6 v1.0.8
EOF
fi
}
parse_global_options() {
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--lang)
[[ $# -ge 2 ]] || die "$(trf arg_requires_value "--lang")"
set_language "$2"
shift 2
;;
--lang=*)
set_language "${1#*=}"
shift
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "$(trf missing_dependency "$1")"
}
map_arch() {
case "$(uname -m)" in
x86_64|amd64)
printf 'amd64\n'
;;
aarch64|arm64)
printf 'arm64\n'
;;
*)
die "$(trf unsupported_arch "$(uname -m)")"
;;
esac
}
docker_bash() {
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c "$1"
}
cleanup() {
local exit_code="$?"
if [[ "${exit_code}" -ne 0 ]]; then
log "$(trf test_failed_collecting)"
docker logs "${CONTAINER_NAME}" >/dev/null 2>&1 && docker logs "${CONTAINER_NAME}" >&2 || true
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'systemctl status epusdt --no-pager || true' || true
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'journalctl -u epusdt --no-pager -n 100 || true' || true
fi
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
}
wait_for_systemd() {
local attempt
for attempt in $(seq 1 120); do
if docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'systemctl show --property=Version --value >/dev/null 2>&1'; then
return 0
fi
sleep 1
done
return 1
}
wait_for_service_assets() {
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c '
for _ in $(seq 1 30); do
if systemctl is-active --quiet epusdt && [[ -f /opt/epusdt/www/index.html ]]; then
exit 0
fi
sleep 1
done
systemctl status epusdt --no-pager || true
journalctl -u epusdt --no-pager -n 100 || true
exit 1
'
}
service_main_pid() {
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'systemctl show --property=MainPID --value epusdt' | tr -d '\r\n'
}
set_language ""
parse_global_options "$@"
INSTALL_TAG="${POSITIONAL_ARGS[0]:-}"
UPGRADE_TAG="${POSITIONAL_ARGS[1]:-}"
[[ -n "${INSTALL_TAG}" ]] || {
usage
exit 1
}
[[ "${INSTALL_TAG}" == v* ]] || die "$(trf install_tag_invalid)"
if [[ -n "${UPGRADE_TAG}" && "${UPGRADE_TAG}" != v* ]]; then
die "$(trf upgrade_tag_invalid)"
fi
require_command docker
require_command curl
ASSET_ARCH="$(map_arch)"
ASSET_URL="https://github.com/GMWalletApp/epusdt/releases/download/${INSTALL_TAG}/epusdt-${INSTALL_TAG#v}-linux-${ASSET_ARCH}.tar.gz"
SUMS_URL="https://github.com/GMWalletApp/epusdt/releases/download/${INSTALL_TAG}/SHA256SUMS"
curl --fail --silent --show-error --head "${ASSET_URL}" >/dev/null
curl --fail --silent --show-error --head "${SUMS_URL}" >/dev/null
if [[ -n "${UPGRADE_TAG}" ]]; then
curl --fail --silent --show-error --head "https://github.com/GMWalletApp/epusdt/releases/download/${UPGRADE_TAG}/epusdt-${UPGRADE_TAG#v}-linux-${ASSET_ARCH}.tar.gz" >/dev/null
curl --fail --silent --show-error --head "https://github.com/GMWalletApp/epusdt/releases/download/${UPGRADE_TAG}/SHA256SUMS" >/dev/null
fi
trap cleanup EXIT
log "$(trf base_image "${BASE_IMAGE}")"
log "$(trf starting_container "${BASE_IMAGE}")"
docker run -d \
--name "${CONTAINER_NAME}" \
--privileged \
--cgroupns=host \
-e container=docker \
--tmpfs /run \
--tmpfs /run/lock \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
-v "${SCRIPT_DIR}:/work" \
-w /work \
"${BASE_IMAGE}" \
bash -euo pipefail -c '
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y \
ca-certificates \
curl \
expect \
jq \
sudo \
systemd \
systemd-sysv \
tar
apt-get clean
rm -rf /var/lib/apt/lists/*
if ! id -u tester >/dev/null 2>&1; then
useradd -m -s /bin/bash tester
fi
mkdir -p /etc/sudoers.d
echo "tester ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/tester
chmod 0440 /etc/sudoers.d/tester
exec /sbin/init
' >/dev/null
wait_for_systemd || die "$(trf systemd_not_ready)"
log "$(trf self_installing)"
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c "su - tester -c 'cd /work && ./epctl self-install'"
docker_bash '[[ -x /usr/local/bin/epctl ]]'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'su - tester -c "EPCTL_NO_COLOR=1 epctl help | grep -q \"^用法:\""'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'su - tester -c "EPCTL_NO_COLOR=1 epctl --lang zh help | grep -q \"^用法:\""'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'su - tester -c "EPCTL_NO_COLOR=1 epctl --lang en help | grep -q \"^Usage:\""'
log "$(trf downloading_release "${INSTALL_TAG}")"
docker exec "${CONTAINER_NAME}" env TAG="${INSTALL_TAG}" bash -euo pipefail -c 'su - tester -c "epctl download --tag ${TAG}"'
log "$(trf installing_release "${INSTALL_TAG}")"
docker exec "${CONTAINER_NAME}" env TAG="${INSTALL_TAG}" bash -euo pipefail -c 'su - tester -c "epctl install --tag ${TAG} --app-uri http://127.0.0.1:8000"'
log "$(trf waiting_assets)"
wait_for_service_assets
docker_bash '[[ -x /opt/epusdt/epusdt ]]'
docker_bash '[[ -f /opt/epusdt/.env ]]'
docker_bash 'systemctl is-active --quiet epusdt'
docker_bash '[[ -f /opt/epusdt/www/index.html ]]'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'su - tester -c "epctl show-config | grep -q \"^install=false$\""'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'su - tester -c "EPCTL_NO_COLOR=1 epctl --lang zh show-config | grep -q \"^install=false$\""'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c '
su - tester -c "epctl status >/tmp/epctl-status.out 2>/tmp/epctl-status.err"
! grep -q "systemd-journal" /tmp/epctl-status.err
'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c '
su - tester -c "epctl logs --lines 50 >/tmp/epctl-logs.out 2>/tmp/epctl-logs.err"
! grep -q "systemd-journal" /tmp/epctl-logs.err
'
if [[ -n "${UPGRADE_TAG}" ]]; then
log "$(trf upgrading_release "${INSTALL_TAG}" "${UPGRADE_TAG}")"
env_checksum_before="$(docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'sha256sum /opt/epusdt/.env | cut -d" " -f1' | tr -d '\r\n')"
pid_before_skip_restart="$(service_main_pid)"
log "$(trf verifying_upgrade_skip_restart)"
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c '
output_file="$(mktemp)"
su - tester -c "EPCTL_NO_COLOR=1 epctl --lang en upgrade --tag ${TAG} --no-restart" >"${output_file}" 2>&1
grep -q "systemctl restart epusdt" "${output_file}"
! grep -q "Restart .*\[Y/n\]" "${output_file}"
rm -f "${output_file}"
'
pid_after_skip_restart="$(service_main_pid)"
[[ "${pid_before_skip_restart}" == "${pid_after_skip_restart}" ]]
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c '/opt/epusdt/epusdt version | grep -q "^version: ${TAG}$"'
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c 'cmp -s /opt/epusdt/.env.example "/tmp/epusdt/${TAG}/extract/.env.example"'
env_checksum_after_skip="$(docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'sha256sum /opt/epusdt/.env | cut -d" " -f1' | tr -d '\r\n')"
[[ "${env_checksum_before}" == "${env_checksum_after_skip}" ]]
log "$(trf verifying_upgrade_default_restart)"
pid_before_default_restart="$(service_main_pid)"
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c '
output_file="$(mktemp)"
su - tester -c "EPCTL_NO_COLOR=1 epctl --lang en upgrade --tag ${TAG}" >"${output_file}" 2>&1
! grep -q "Restart .*\[Y/n\]" "${output_file}"
rm -f "${output_file}"
'
wait_for_service_assets
pid_after_default_restart="$(service_main_pid)"
[[ "${pid_before_default_restart}" != "${pid_after_default_restart}" ]]
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c '
output_file="$(mktemp)"
if su - tester -c "EPCTL_NO_COLOR=1 epctl --lang en upgrade --tag ${TAG} --prompt-restart" </dev/null >"${output_file}" 2>&1; then
cat "${output_file}" >&2
rm -f "${output_file}"
exit 1
fi
grep -q "interactive terminal" "${output_file}"
rm -f "${output_file}"
'
log "$(trf verifying_upgrade_prompt_skip)"
pid_before_prompt_skip="$(service_main_pid)"
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c '
output_file="$(mktemp)"
OUTPUT_FILE="${output_file}" expect <<'"'"'EOF'"'"'
set timeout 120
log_file -noappend $env(OUTPUT_FILE)
spawn su - tester -c "EPCTL_NO_COLOR=1 epctl --lang en upgrade --tag $env(TAG) --prompt-restart"
expect {
-re {Restart .*\[Y/n\]} { send "n\r" }
timeout { exit 1 }
}
expect eof
EOF
grep -q "systemctl restart epusdt" "${output_file}"
rm -f "${output_file}"
'
pid_after_prompt_skip="$(service_main_pid)"
[[ "${pid_before_prompt_skip}" == "${pid_after_prompt_skip}" ]]
log "$(trf verifying_upgrade_prompt_restart)"
pid_before_prompt_restart="$(service_main_pid)"
docker exec "${CONTAINER_NAME}" env TAG="${UPGRADE_TAG}" bash -euo pipefail -c '
expect <<'"'"'EOF'"'"'
set timeout 120
spawn su - tester -c "EPCTL_NO_COLOR=1 epctl --lang en upgrade --tag $env(TAG) --prompt-restart"
expect {
-re {Restart .*\[Y/n\]} { send "\r" }
timeout { exit 1 }
}
expect eof
EOF
'
wait_for_service_assets
pid_after_prompt_restart="$(service_main_pid)"
[[ "${pid_before_prompt_restart}" != "${pid_after_prompt_restart}" ]]
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c 'su - tester -c "epctl show-config | grep -q \"^install=false$\""'
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c '
su - tester -c "epctl status >/tmp/epctl-status-upgrade.out 2>/tmp/epctl-status-upgrade.err"
! grep -q "systemd-journal" /tmp/epctl-status-upgrade.err
'
fi
log "$(trf checking_init_password)"
docker exec "${CONTAINER_NAME}" bash -euo pipefail -c '
response="$(su - tester -c "epctl init-password")"
password="$(printf "%s\n" "${response}" | jq -r ".data.password")"
printf "%s\n" "${response}" | jq -e ".status_code == 200 and (.data.password | type == \"string\" and length > 0)" >/dev/null
login_payload="$(jq -nc --arg username admin --arg password "${password}" "{username:\$username,password:\$password}")"
token="$(curl --fail --silent --show-error \
-H "Content-Type: application/json" \
-d "${login_payload}" \
http://127.0.0.1:8000/admin/api/v1/auth/login | jq -r ".data.token")"
[[ -n "${token}" && "${token}" != "null" ]]
change_payload="$(jq -nc --arg old_password "${password}" --arg new_password "new-pass-789" "{old_password:\$old_password,new_password:\$new_password}")"
curl --fail --silent --show-error \
-H "Authorization: Bearer ${token}" \
-H "Content-Type: application/json" \
-d "${change_payload}" \
http://127.0.0.1:8000/admin/api/v1/auth/password | jq -e ".status_code == 200" >/dev/null
failure_output="$(mktemp)"
if su - tester -c "epctl init-password" >"${failure_output}" 2>&1; then
cat "${failure_output}" >&2
rm -f "${failure_output}"
echo "'"$(trf expected_second_failure)"'" >&2
exit 1
fi
grep -Eq "\"status_code\"[[:space:]]*:[[:space:]]*10040" "${failure_output}"
rm -f "${failure_output}"
'
log "$(trf verification_succeeded "${INSTALL_TAG}" "${UPGRADE_TAG:+ upgrade=${UPGRADE_TAG}}")"