mirror of
https://github.com/This-weiyuqing/mi-fitness-health.git
synced 2026-10-18 03:03:49 +08:00
qa: validate server queries and prepare offline backend web update
This commit is contained in:
1 parent
6292755ff9
commit
c076bbc0d6
11 files changed
+640
-1
No files matched your search
@@ -0,0 +1 @@
|
||||
* text eol=lf
|
||||
@@ -0,0 +1,6 @@
|
||||
# 离线增量镜像;发布清单另固定基础镜像digest,不联网安装依赖。
|
||||
FROM local/mi-fitness-nas:20261017-audit3
|
||||
USER root
|
||||
COPY src/mi_fitness_mcp/ /src/src/mi_fitness_mcp/
|
||||
RUN python -c "import shutil,sysconfig; shutil.copytree('/src/src/mi_fitness_mcp',sysconfig.get_paths()['purelib']+'/mi_fitness_mcp',dirs_exist_ok=True)"
|
||||
USER 1000:1001
|
||||
@@ -0,0 +1,30 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Package,
|
||||
[string]$SshHost = 'fn',
|
||||
[switch]$Deploy
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$release = (Resolve-Path -LiteralPath $Package).Path
|
||||
$manifest = Join-Path $release 'manifest.sha256'
|
||||
foreach ($line in Get-Content -LiteralPath $manifest -Encoding utf8) {
|
||||
if ($line -notmatch '^([a-f0-9]{64}) ([A-Za-z0-9._-]+)$') { throw '发布清单格式不符' }
|
||||
if ((Get-FileHash -LiteralPath (Join-Path $release $Matches[2]) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $Matches[1]) { throw '本地发布文件校验失败' }
|
||||
}
|
||||
if (-not $Deploy) {
|
||||
Write-Output '本地发布包校验通过。仅更新backend/web,worker、端口、密码和数据挂载沿用。'
|
||||
Write-Output '得到明确部署确认后,在交互PowerShell使用同一命令加 -Deploy;sudo密码只在终端输入。'
|
||||
return
|
||||
}
|
||||
if ($SshHost -notmatch '^[A-Za-z0-9._-]+$') { throw 'SSH别名无效' }
|
||||
$remote = '/tmp/mi-fitness-summary8-' + (Get-Date -Format 'yyyyMMdd-HHmmss')
|
||||
& ssh $SshHost "umask 077; mkdir '$remote'"
|
||||
if ($LASTEXITCODE -ne 0) { throw '创建远程发布目录失败' }
|
||||
foreach ($line in Get-Content -LiteralPath $manifest -Encoding utf8) {
|
||||
$name = ($line -split ' ',2)[1]
|
||||
& scp (Join-Path $release $name) "${SshHost}:$remote/$name"
|
||||
if ($LASTEXITCODE -ne 0) { throw '发布文件传输失败' }
|
||||
}
|
||||
& scp $manifest "${SshHost}:$remote/manifest.sha256"
|
||||
if ($LASTEXITCODE -ne 0) { throw '发布清单传输失败' }
|
||||
& ssh -t $SshHost "sudo sh '$remote/update-health.sh'"
|
||||
if ($LASTEXITCODE -ne 0) { throw '部署未通过,请保留终端输出和回退备份。' }
|
||||
@@ -0,0 +1,40 @@
|
||||
"""部署后最小就绪核对;不查询健康记录,不输出凭据。"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
IMAGE = 'local/mi-fitness-nas:20261017-summary8'
|
||||
items = json.loads(subprocess.check_output(['docker', 'inspect', 'mi-fitness-health-backend-1', 'mi-fitness-health-web-1', 'mi-fitness-health-worker-1']))
|
||||
backend, web, worker = items
|
||||
assert backend['Config']['Image'] == web['Config']['Image'] == IMAGE
|
||||
assert worker['Config']['Image'] == 'local/mi-fitness-nas:20261017-audit3'
|
||||
if len(sys.argv) > 1:
|
||||
assert backend['Image'] == web['Image'] == sys.argv[1]
|
||||
assert backend['HostConfig']['ReadonlyRootfs'] and web['HostConfig']['ReadonlyRootfs']
|
||||
assert any(m['Destination'] == '/state' for m in backend['Mounts'])
|
||||
assert any(m['Destination'] == '/run/secrets/keyring_master' and not m['RW'] for m in backend['Mounts'])
|
||||
assert any(m['Destination'] == '/calendar' for m in web['Mounts'])
|
||||
assert set(web['HostConfig']['PortBindings']) == {'8322/tcp'}
|
||||
assert {(p['HostIp'], p['HostPort']) for p in web['HostConfig']['PortBindings']['8322/tcp']} == {('127.0.0.1', '18322'), ('192.168.31.201', '18322')}
|
||||
# 密码只在容器内部使用,不读取到控制端、不打印、不放在命令行。
|
||||
probe = r'''
|
||||
import base64,json,os,urllib.error,urllib.request
|
||||
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
token=base64.b64encode(('admin:'+os.environ['MI_FITNESS_WEB_PASSWORD']).encode()).decode()
|
||||
def get(path,auth=True):
|
||||
req=urllib.request.Request('http://127.0.0.1:8322'+path,headers={'Authorization':'Basic '+token} if auth else {})
|
||||
try:
|
||||
with opener.open(req,timeout=10) as r:return r.status,r.read(300000).decode()
|
||||
except urllib.error.HTTPError as e:return e.code,''
|
||||
assert get('/',False)[0]==401
|
||||
assert '20261017-summary8' in get('/')[1]
|
||||
for path,marker in [('/static/js/health-views.js','fromSummary'),('/static/js/health-ui.js','/api/views/'),('/static/js/app.js','/api/overview')]:
|
||||
status,body=get(path);assert status==200 and marker in body
|
||||
assert json.loads(get('/api/proxy_status')[1])['backend_reachable']
|
||||
for path in ['/proxy/api/views/stress','/proxy/api/views/stress/records','/proxy/api/views/stress/export','/proxy/api/overview']:
|
||||
assert get(path+'?start_date=invalid&end_date=invalid')[0]==422
|
||||
assert os.environ.get('MI_FITNESS_CALENDAR_DIR')=='/calendar'
|
||||
assert json.loads(get('/api/calendar?start=2026&end=2026')[1])['years']['2026']['days']
|
||||
print('backend/web资源、代理、匿名拒绝和新路由就绪;未查询健康记录。')
|
||||
'''
|
||||
subprocess.run(['docker', 'exec', '-i', 'mi-fitness-health-web-1', 'python', '-'], input=probe.encode('utf-8'), check=True)
|
||||
@@ -0,0 +1,17 @@
|
||||
# 沿用当前NAS的端口、密码来源和日历挂载,只升级backend/web。
|
||||
services:
|
||||
backend:
|
||||
image: local/mi-fitness-nas:20261017-summary8
|
||||
web:
|
||||
image: local/mi-fitness-nas:20261017-summary8
|
||||
environment:
|
||||
MI_FITNESS_CALENDAR_DIR: /calendar
|
||||
MI_FITNESS_ALLOWED_HOSTS: localhost,127.0.0.1,192.168.31.201
|
||||
MI_FITNESS_WEB_PASSWORD: ${MI_FITNESS_WEB_PASSWORD:?尚未初始化网页访问密码}
|
||||
volumes:
|
||||
- ./calendar:/calendar
|
||||
ports: !override
|
||||
- '127.0.0.1:18322:8322'
|
||||
- '192.168.31.201:18322:8322'
|
||||
worker:
|
||||
image: local/mi-fitness-nas:20261017-audit3
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 077
|
||||
[ "$(id -u)" = 0 ] || { echo '请通过sudo在交互终端执行。'; exit 1; }
|
||||
package=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
target=/vol2/1000/Soft/mi-fitness-health
|
||||
cd "$package"
|
||||
sha256sum -c manifest.sha256
|
||||
python3 - "$target/compose.override.yml" "$package/expected-current.sha256" <<'PY'
|
||||
import hashlib,sys
|
||||
from pathlib import Path
|
||||
assert hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest()==Path(sys.argv[2]).read_text().strip(), '现有覆盖配置已变化,停止避免覆盖'
|
||||
PY
|
||||
[ -d "$target/calendar" ] && [ ! -L "$target/calendar" ] || { echo '日历挂载目录缺失或异常,停止。'; exit 1; }
|
||||
cd "$target"
|
||||
compose() { docker compose --project-name mi-fitness-health --env-file .env -f compose.yml -f compose.override.yml "$@"; }
|
||||
snapshot() { docker ps -aq --filter label=com.docker.compose.project=mi-fitness-health | sort | while read -r id; do docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} {{.Id}} {{.Image}}' "$id"; done | grep -v -E '^(backend|web) ' | sort; }
|
||||
before=$(snapshot)
|
||||
[ "$(docker inspect --format '{{.Config.Image}}' mi-fitness-health-worker-1)" = local/mi-fitness-nas:20261017-audit3 ] || { echo 'worker版本已变化,停止。'; exit 1; }
|
||||
backup="$target/compose.override.pre-summary8-$(date +%Y%m%d-%H%M%S).yml"
|
||||
cp compose.override.yml "$backup"
|
||||
docker inspect --format '{{.Config.Image}} {{.Image}}' mi-fitness-health-backend-1 mi-fitness-health-web-1 > "$backup.images"
|
||||
# 不清理旧镜像;覆盖配置和镜像ID均保留以便回退。
|
||||
docker load -i "$package/health-summary8.tar"
|
||||
expected=$(cat "$package/image-id.txt")
|
||||
[ "$(docker image inspect --format '{{.Id}}' local/mi-fitness-nas:20261017-summary8)" = "$expected" ] || { echo '导入镜像ID不符。'; exit 1; }
|
||||
docker compose --project-name mi-fitness-health --env-file .env -f compose.yml -f "$package/compose.override.yml" config --quiet
|
||||
complete=0
|
||||
recover() {
|
||||
if [ "$complete" = 0 ]; then
|
||||
cp "$backup" "$target/compose.override.yml"
|
||||
chown 1000:1001 "$target/compose.override.yml"
|
||||
chmod 600 "$target/compose.override.yml"
|
||||
compose up -d --no-deps --no-build --pull never backend web || true
|
||||
echo "更新未通过,已恢复原配置并尝试回退backend/web。备份:$backup"
|
||||
fi
|
||||
}
|
||||
trap recover EXIT
|
||||
cp "$package/compose.override.yml" compose.override.yml
|
||||
chown 1000:1001 compose.override.yml
|
||||
chmod 600 compose.override.yml
|
||||
compose up -d --no-deps --no-build --pull never --wait --wait-timeout 90 backend web
|
||||
python3 "$package/check-ready.py" "$expected"
|
||||
[ "$before" = "$(snapshot)" ] || { echo 'worker或其他健康服务发生变化,停止验收。'; exit 1; }
|
||||
complete=1
|
||||
trap - EXIT
|
||||
printf '\nbackend/web更新完成;worker及其他健康服务未重建,原覆盖配置和旧镜像已保留。\n'
|
||||
Reference in new issue
Block a user