fix(release): 按服务核验 Compose 固定镜像并保留线上修复
This commit is contained in:
@@ -52,10 +52,17 @@ function Test-ApplicationVersionImages {
|
|||||||
param([object[]]$Plan, [string[]]$ComposeCommand)
|
param([object[]]$Plan, [string[]]$ComposeCommand)
|
||||||
foreach ($item in $Plan) {
|
foreach ($item in $Plan) {
|
||||||
if (-not $item.Service) { continue }
|
if (-not $item.Service) { continue }
|
||||||
$args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--images',$item.Service)
|
$args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--format','json',$item.Service)
|
||||||
$executable = $ComposeCommand[0]
|
$executable = $ComposeCommand[0]
|
||||||
$actual = & $executable @args
|
$configuration = & $executable @args
|
||||||
if ($LASTEXITCODE -ne 0 -or "$actual".Trim() -ne "$($item.Image):$($item.Version)") {
|
if ($LASTEXITCODE -ne 0) { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' }
|
||||||
|
try {
|
||||||
|
# Compose includes dependencies even when a service is selected.
|
||||||
|
# Keep the configuration in memory; do not log its environment.
|
||||||
|
$resolved = ($configuration -join "`n") | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
$actual = $resolved.services.PSObject.Properties[$item.Service].Value.image
|
||||||
|
} catch { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' }
|
||||||
|
if ("$actual".Trim() -ne "$($item.Image):$($item.Version)") {
|
||||||
throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务"
|
throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,22 @@ apply_application_versions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
verify_application_version_images() {
|
verify_application_version_images() {
|
||||||
local file="$1" compose="$2" service key option image version actual
|
local file="$1" compose="$2" service key option image version actual configuration
|
||||||
while read -r service key option image; do
|
while read -r service key option image; do
|
||||||
application_selected "$service" || continue
|
application_selected "$service" || continue
|
||||||
version="$(read_env_value "$file" "$key")"
|
version="$(read_env_value "$file" "$key")"
|
||||||
actual="$(sudo $compose config --images "$service")" || return 1
|
# --images SERVICE includes dependency images on Compose 5. Read the
|
||||||
|
# selected service from normalized config instead of comparing the list.
|
||||||
|
configuration="$(sudo $compose config --format json "$service")" || return 1
|
||||||
|
actual="$(printf '%s' "$configuration" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
image = json.load(sys.stdin)["services"][sys.argv[1]]["image"]
|
||||||
|
if not isinstance(image, str): raise ValueError()
|
||||||
|
print(image)
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
raise SystemExit("无法读取应用的有效镜像定义;尚未拉取或重建服务")
|
||||||
|
' "$service")" || return 1
|
||||||
if [ "$actual" != "$image:$version" ]; then
|
if [ "$actual" != "$image:$version" ]; then
|
||||||
echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2
|
echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Exercise Bash and PowerShell image verification with real Compose config.
|
||||||
|
|
||||||
|
No images are pulled and no containers are created. Fixture settings are fake.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parent.parent)
|
||||||
|
args = parser.parse_args()
|
||||||
|
source = args.source.resolve()
|
||||||
|
started = time.monotonic()
|
||||||
|
version = '1.0.0.20261014'
|
||||||
|
image = 'registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server'
|
||||||
|
secret = 'synthetic-config-value-must-not-be-printed'
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix='easyai-image-config-') as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
(root / '.env').write_text(f'SERVER_VERSION={version}\nFIXTURE_SECRET={secret}\n')
|
||||||
|
binaries = root / 'bin'
|
||||||
|
binaries.mkdir()
|
||||||
|
sudo = binaries / 'sudo'
|
||||||
|
sudo.write_text('#!/bin/sh\nexec "$@"\n')
|
||||||
|
sudo.chmod(0o755)
|
||||||
|
env = {**os.environ, 'PATH': str(binaries) + os.pathsep + os.environ['PATH'], 'UPDATE_SERVICES': 'easyai-server'}
|
||||||
|
reports = []
|
||||||
|
for correct in (True, False):
|
||||||
|
selected = f'{image}:${{SERVER_VERSION}}' if correct else f'{image}:latest'
|
||||||
|
# On the negative case a dependency deliberately has the expected image;
|
||||||
|
# checking membership in --images output would wrongly approve it.
|
||||||
|
dependency = 'redis:7-alpine' if correct else f'{image}:${{SERVER_VERSION}}'
|
||||||
|
(root / 'compose.yml').write_text(f'services:\n dependency:\n image: {dependency}\n easyai-server:\n image: {selected}\n depends_on:\n - dependency\n environment:\n FIXTURE_SECRET: ${{FIXTURE_SECRET}}\n')
|
||||||
|
config = subprocess.run(['docker','compose','config','--format','json','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True)
|
||||||
|
assert json.loads(config.stdout)['services']['easyai-server']['image'] == (f'{image}:{version}' if correct else f'{image}:latest')
|
||||||
|
images = subprocess.run(['docker','compose','config','--images','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True)
|
||||||
|
assert len(images.stdout.splitlines()) > 1, 'Compose fixture must include dependency images'
|
||||||
|
bash = ['bash','-c', '. "$1/scripts/init-security-env.sh"; . "$1/scripts/init-application-version-env.sh"; verify_application_version_images .env "docker compose"', 'fixture', str(source)]
|
||||||
|
powershell = ['pwsh','-NoProfile','-NonInteractive','-Command', f". '{source}/scripts/Initialize-ApplicationVersions.ps1'; Test-ApplicationVersionImages @([pscustomobject]@{{Service='easyai-server';Image='{image}';Version='{version}'}}) @('docker','compose')"]
|
||||||
|
for name, command in [('bash', bash), ('powershell', powershell)]:
|
||||||
|
result = subprocess.run(command, cwd=root, env=env, text=True, errors='backslashreplace', capture_output=True, timeout=30)
|
||||||
|
assert (result.returncode == 0) == correct, f'{name} incorrect verification: {result.stdout} {result.stderr}'
|
||||||
|
assert secret not in result.stdout + result.stderr, 'configuration leaked'
|
||||||
|
reports.append({'shell':name, 'correctImage':correct, 'expectedExitObserved':True})
|
||||||
|
|
||||||
|
print(json.dumps({'cases':reports,'seconds':round(time.monotonic()-started,3)}))
|
||||||
@@ -57,11 +57,12 @@ if args[:1] == ['inspect']:
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis')
|
if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis')
|
||||||
elif args[:3] == ['compose', 'config', '--images']:
|
elif args[:4] == ['compose', 'config', '--format', 'json']:
|
||||||
names = {'easyai-server':('SERVER_VERSION','comfy-ai/comfy-server'), 'easyai-web':('WEB_VERSION','comfy-ai/one-ai'), 'ws-gateway':('WS_GATEWAY_VERSION','easyaigc/wsgateway'), 'easyai-updater':('UPDATER_VERSION','easyaigc/easyai-updater'), 'model-conversion-worker':('MODEL_CONVERSION_WORKER_VERSION','easyaigc/model-conversion-worker')}
|
names = {'easyai-server':('SERVER_VERSION','comfy-ai/comfy-server'), 'easyai-web':('WEB_VERSION','comfy-ai/one-ai'), 'ws-gateway':('WS_GATEWAY_VERSION','easyaigc/wsgateway'), 'easyai-updater':('UPDATER_VERSION','easyaigc/easyai-updater'), 'model-conversion-worker':('MODEL_CONVERSION_WORKER_VERSION','easyaigc/model-conversion-worker')}
|
||||||
values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#'))
|
values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#'))
|
||||||
key,image = names[args[-1]]
|
key,image = names[args[-1]]
|
||||||
print('registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key]))
|
actual = 'registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key])
|
||||||
|
print(json.dumps({'services': {args[-1]: {'image':actual}, 'dependency': {'image':'redis:7-alpine'}}}))
|
||||||
elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1])
|
elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
''')
|
''')
|
||||||
|
|||||||
Reference in New Issue
Block a user