mirror of
https://github.com/This-weiyuqing/mi-fitness-health.git
synced 2026-10-17 18:55:37 +08:00
deploy: normalize release metadata and check route metadata only
This commit is contained in:
1 parent
1c05825b6a
commit
ecbd485700
6 files changed
+87
-7
No files matched your search
@@ -0,0 +1,4 @@
|
||||
*.sh text eol=lf
|
||||
*.sha256 text eol=lf
|
||||
deploy/query-summary/*.txt text eol=lf
|
||||
artifacts/query-summary/release/*.txt text eol=lf
|
||||
+3
-1
@@ -30,7 +30,9 @@
|
||||
|
||||
`deploy/query-summary/`包含离线增量Dockerfile、仅替换backend/web镜像的覆盖配置、哈希防覆盖和回退脚本。基础镜像audit3必须在本机已有,构建不安装依赖;worker继续audit3。发布包为本机 `artifacts/query-summary/release/`,不提交Git。
|
||||
|
||||
正式更新前须明确确认仅替换健康项目backend和web。确认后由用户在交互PowerShell运行 `deploy/query-summary/Update-Nas.ps1 -Package artifacts/query-summary/release -Deploy` 并在终端输入sudo密码;不加 `-Deploy` 只校验本地发布包。脚本沿用原端口、密码来源和数据/日历挂载,不处理其他NAS服务。就绪检查只检查资源、代理、鉴权和无效日期的新路由,不运行合成测试或读取健康记录。
|
||||
已有镜像tar时,`python deploy/query-summary/prepare-release.py`仅重建发布脚本、UTF-8无BOM/LF元数据及manifest,不构建或修改镜像tar。shell读取镜像身份时兼容旧CRLF,并严格校验`sha256:<64位小写十六进制>`。
|
||||
|
||||
正式更新前须明确确认仅替换健康项目backend和web。确认后由用户在交互PowerShell运行 `deploy/query-summary/Update-Nas.ps1 -Package artifacts/query-summary/release -Deploy` 并在终端输入sudo密码;不加 `-Deploy` 只校验本地发布包。脚本沿用原端口、密码来源和数据/日历挂载,不处理其他NAS服务。就绪检查只检查资源、代理、鉴权、日历和OpenAPI路由元数据,不发健康查询/导出请求,不运行合成或负向参数测试。
|
||||
|
||||
## 后续维护
|
||||
|
||||
|
||||
@@ -32,10 +32,13 @@ 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
|
||||
status,schema=get('/proxy/openapi.json')
|
||||
assert status==200
|
||||
paths=json.loads(schema)['paths']
|
||||
for path in ['/api/views/{kind}','/api/views/{kind}/records','/api/views/{kind}/export','/api/overview']:
|
||||
assert 'get' in paths.get(path,{})
|
||||
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资源、代理、匿名拒绝和新路由就绪;未查询健康记录。')
|
||||
print('backend/web资源、代理、匿名拒绝和OpenAPI路由元数据就绪;未请求健康查询或导出。')
|
||||
'''
|
||||
subprocess.run(['docker', 'exec', '-i', 'mi-fitness-health-web-1', 'python', '-'], input=probe.encode('utf-8'), check=True)
|
||||
@@ -0,0 +1,59 @@
|
||||
"""为现有summary8离线tar生成LF发布元数据;不构建镜像或连接NAS。"""
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
RELEASE = ROOT / 'artifacts/query-summary/release'
|
||||
TAG = 'local/mi-fitness-nas:20261017-summary8'
|
||||
BASE_IMAGE_ID = 'sha256:f4cbe187a98e3d3223e28e79b6edd115446ba62a78931f54f3bb0a6991a9395d'
|
||||
# 部署前只读快照;实际更新脚本仍会再次比对,漂移则停止。
|
||||
EXPECTED_CURRENT = '3cdbdc882b169f5785d6d1c3c46ceb159e209c567d6e28ceb31b15d4280fcdd8'
|
||||
|
||||
|
||||
def sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def write_lf(name, value):
|
||||
assert '\r' not in value and not value.startswith('\ufeff')
|
||||
(RELEASE / name).write_bytes((value.rstrip('\n') + '\n').encode('utf-8'))
|
||||
|
||||
|
||||
def main():
|
||||
tar = RELEASE / 'health-summary8.tar'
|
||||
# 仅读取索引/配置元数据,不展开镜像层,也不修改tar。
|
||||
with tarfile.open(tar) as archive:
|
||||
manifest = json.load(archive.extractfile('manifest.json'))
|
||||
assert len(manifest) == 1 and manifest[0]['RepoTags'] == [TAG]
|
||||
index = json.load(archive.extractfile('index.json'))
|
||||
assert len(index['manifests']) == 1
|
||||
image_id = index['manifests'][0]['digest']
|
||||
assert re.fullmatch(r'sha256:[0-9a-f]{64}', image_id)
|
||||
index_blob = archive.extractfile('blobs/sha256/' + image_id.split(':')[1]).read()
|
||||
assert hashlib.sha256(index_blob).hexdigest() == image_id.split(':')[1]
|
||||
config = archive.extractfile(manifest[0]['Config']).read()
|
||||
config_id = 'sha256:' + hashlib.sha256(config).hexdigest()
|
||||
assert manifest[0]['Config'] == 'blobs/sha256/' + config_id.split(':')[1]
|
||||
for name in ['compose.override.yml', 'update-health.sh', 'check-ready.py', 'Update-Nas.ps1']:
|
||||
content = (Path(__file__).resolve().parent / name).read_bytes()
|
||||
assert b'\r' not in content and not content.startswith(b'\xef\xbb\xbf'), name
|
||||
(RELEASE / name).write_bytes(content)
|
||||
for name, value in [('image-id.txt', image_id), ('image-config-id.txt', config_id),
|
||||
('base-image-id.txt', BASE_IMAGE_ID), ('expected-current.sha256', EXPECTED_CURRENT)]:
|
||||
write_lf(name, value)
|
||||
files = sorted(path for path in RELEASE.iterdir() if path.is_file() and path.name != 'manifest.sha256')
|
||||
assert len(files) == 9, '发布目录包含非预期文件,停止生成清单'
|
||||
write_lf('manifest.sha256', ''.join(sha256(path) + ' ' + path.name + '\n' for path in files))
|
||||
print(json.dumps(dict(image_id=image_id, config_id=config_id, tar_sha256=sha256(tar),
|
||||
manifest_sha256=sha256(RELEASE / 'manifest.sha256')), ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -22,8 +22,18 @@ 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")
|
||||
config_id=$(cat "$package/image-config-id.txt")
|
||||
read_image_id() {
|
||||
python3 - "$1" <<'PY'
|
||||
import re,sys
|
||||
from pathlib import Path
|
||||
value=Path(sys.argv[1]).read_bytes().rstrip(b'\r\n')
|
||||
if re.fullmatch(rb'sha256:[0-9a-f]{64}',value) is None:
|
||||
raise SystemExit('镜像身份文件格式无效')
|
||||
sys.stdout.write(value.decode('ascii'))
|
||||
PY
|
||||
}
|
||||
expected=$(read_image_id "$package/image-id.txt")
|
||||
config_id=$(read_image_id "$package/image-config-id.txt")
|
||||
actual=$(docker image inspect --format '{{.Id}}' local/mi-fitness-nas:20261017-summary8)
|
||||
[ "$actual" = "$expected" ] || [ "$actual" = "$config_id" ] || { echo '导入镜像ID不符。'; exit 1; }
|
||||
docker compose --project-name mi-fitness-health --env-file .env -f compose.yml -f "$package/compose.override.yml" config --quiet
|
||||
|
||||
@@ -59,4 +59,6 @@ node --test qa/*.test.cjs
|
||||
|
||||
本机离线发布目录为 `artifacts/query-summary/release/`:镜像tar、image-id及同包配置digest、基础镜像id、覆盖配置、就绪/更新脚本、预期当前配置哈希及manifest.sha256。Docker引擎身份核对只接受本包确切的索引/配置digest,先校验整个tar;NAS引擎尚未实际导入验证。摘要和镜像包不提交Git。`Update-Nas.ps1`不带Deploy仅校验;正式执行必须先取得仅更新backend/web的明确确认,由用户交互输入sudo密码。更新脚本核对manifest和现有覆盖配置SHA256、保留旧覆盖与镜像、不变更.env,使用 `up -d --no-deps --no-build --pull never backend web`,比较其余健康服务容器id/镜像。失败恢复旧覆盖并尝试回退这两个服务。
|
||||
|
||||
当前NAS覆盖配置防覆盖哈希由controller在部署前只读取得:`3cdbdc882b169f5785d6d1c3c46ceb159e209c567d6e28ceb31b15d4280fcdd8`。实际正式更新时脚本再次验证;若有漂移即停止。此记录不是部署确认。NAS就绪脚本只访问资源/代理状态、匿名认证、日历和无效日期路由,不进行健康导出、合成/压力测试。
|
||||
`python deploy/query-summary/prepare-release.py`只读现有tar的OCI索引/配置元数据,以`write_bytes`生成UTF-8无BOM/LF的所有txt/sha256文件、同步当前执行脚本并重建manifest,不改镜像或tar。根.gitattributes固定shell及发布元数据为LF;shell身份读取器只去掉末尾CR/LF,随后严格校验`sha256:<64位小写十六进制>`,兼容历史CRLF而不接受其他空白/无效digest。审查修复后,旧CRLF/新LF分别对索引/配置两种身份的实际Linux shell模型全部通过,非法尾空格被拒;原tar SHA256保持不变。
|
||||
|
||||
当前NAS覆盖配置防覆盖哈希由controller在部署前只读取得:`3cdbdc882b169f5785d6d1c3c46ceb159e209c567d6e28ceb31b15d4280fcdd8`。实际正式更新时脚本再次验证;若有漂移即停止。此记录不是部署确认。NAS就绪脚本只访问资源/代理状态、匿名认证、日历及`/proxy/openapi.json`的paths元数据,确认四个GET路径存在;不发任何健康查询/导出请求,不运行无效参数、合成或压力测试。以上OpenAPI逻辑只在本机内存模型验证,未访问NAS。
|
||||
Reference in new issue
Block a user