forked from wangbo/easyai
fix(deploy): 按服务读取镜像定义避免依赖镜像误判
This commit is contained in:
@@ -52,10 +52,17 @@ function Test-ApplicationVersionImages {
|
||||
param([object[]]$Plan, [string[]]$ComposeCommand)
|
||||
foreach ($item in $Plan) {
|
||||
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]
|
||||
$actual = & $executable @args
|
||||
if ($LASTEXITCODE -ne 0 -or "$actual".Trim() -ne "$($item.Image):$($item.Version)") {
|
||||
$configuration = & $executable @args
|
||||
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) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,22 @@ apply_application_versions() {
|
||||
}
|
||||
|
||||
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
|
||||
application_selected "$service" || continue
|
||||
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
|
||||
echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2
|
||||
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(1)
|
||||
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')}
|
||||
values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#'))
|
||||
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])
|
||||
sys.exit(0)
|
||||
''')
|
||||
|
||||
Reference in New Issue
Block a user