Files
orbit/scripts/orbit-local.py

174 lines
8.3 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""部署持久化的本地联调环境;私密配置只保存在 .local/compose-test。"""
import datetime
import json
import os
from pathlib import Path
import secrets
import subprocess
import sys
import time
import urllib.error
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
PRIVATE = ROOT / '.local' / 'compose-test'
ENV_FILE = PRIVATE / 'compose.env'
CREDENTIALS = PRIVATE / 'sdk.credentials.json'
BASE_URL = 'http://127.0.0.1:18080'
def private_write(path, value):
temporary = path.with_name(path.name + '.tmp')
with temporary.open('w', encoding='utf-8') as file:
os.chmod(temporary, 0o600)
file.write(value)
temporary.replace(path)
def settings():
return dict(line.split('=', 1) for line in ENV_FILE.read_text().splitlines()
if line and not line.startswith('#'))
def save_settings(values):
private_write(ENV_FILE, ''.join(f'{key}={value}\n' for key, value in values.items()))
def compose(*args, **kwargs):
# 不继承其他联调项目的 ORBIT_/COMPOSE_,防止配置串到旧库或旧端口。
env = {k: v for k, v in os.environ.items()
if not k.startswith(('ORBIT_', 'COMPOSE_'))}
env.update(settings())
env.update(ORBIT_LOCAL_DIRECTORY=str(PRIVATE),
ORBIT_LOCAL_USER=f'{os.getuid()}:{os.getgid()}')
# 本地环境使用 Docker 自带构建器,不占用用户选中的跨平台构建器。
env.update(BUILDX_BUILDER=subprocess.check_output(
['docker', 'context', 'show'], text=True).strip(), COMPOSE_BAKE='false')
command = ['docker', 'compose', '--project-name', 'orbit-test',
'--env-file', str(ENV_FILE), '-f', str(ROOT / 'deploy/docker-compose.yml'),
'-f', str(ROOT / 'deploy/local/compose.yml'), *args]
return subprocess.run(command, cwd=ROOT, env=env, check=True, **kwargs)
def wait_ready():
for _ in range(60):
try:
with urllib.request.urlopen(BASE_URL + '/health/ready', timeout=2) as response:
if response.status == 200 and json.load(response)['status'] == 'ready':
return
except (urllib.error.URLError, TimeoutError, OSError):
pass
time.sleep(1)
raise RuntimeError('Orbit readiness 未通过;使用 scripts/orbit-local.py logs orbit 排查。')
def setup():
os.umask(0o077)
PRIVATE.mkdir(parents=True, exist_ok=True, mode=0o700)
if not ENV_FILE.exists():
# 配置缺失而卷仍存在时不能换密码,先恢复旧配置。
volumes = subprocess.check_output(
['docker', 'volume', 'ls', '--filter', 'label=com.docker.compose.project=orbit-test', '-q'],
text=True).strip()
if volumes or any(PRIVATE.iterdir()):
raise RuntimeError('发现既有卷或初始化文件;请恢复 compose.env,不会生成替代密码。')
revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
save_settings({
'ORBIT_IMAGE': 'orbit:local-test', 'ORBIT_DB_NAME': 'orbit',
'ORBIT_DB_USER': 'orbit_runtime', 'ORBIT_DB_PASSWORD': secrets.token_hex(24),
'ORBIT_DB_ROOT_PASSWORD': secrets.token_hex(24),
'ORBIT_HTTP_BIND': '127.0.0.1', 'ORBIT_HTTP_PORT': '18080',
'ORBIT_TEST_BACKEND_SUBNET': '10.253.240.0/24',
'ORBIT_TEST_SERVICES_SUBNET': '10.253.241.0/24',
'ORBIT_CONSOLE_QUEUE_ID': '', 'ORBIT_ARCHIVE_STORAGE_ID': secrets.token_hex(16),
'ORBIT_BUILD_VERSION': 'v0.1.1-local', 'ORBIT_BUILD_REVISION': revision,
'ORBIT_BUILD_TIME': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')})
private_write(PRIVATE / 'admin.password', secrets.token_urlsafe(24) + '\n')
values = settings()
values['ORBIT_BUILD_REVISION'] = subprocess.check_output(
['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
save_settings(values)
compose('config', '--quiet')
compose('build', 'orbit')
compose('up', '-d', 'mysql', 'verify', 'archive-init')
# 显式再核验一次,失败不初始化身份、不启动流量。
compose('run', '--rm', '-T', '--no-deps', 'verify')
if not CREDENTIALS.exists():
marker = PRIVATE / 'bootstrap.started'
if marker.exists():
raise RuntimeError('上次初始化未确认;保留现场核查,不能重新创建管理员/凭据。')
password = (PRIVATE / 'admin.password').read_text()
private_write(marker, 'bootstrap started; retain credentials on uncertain commit\n')
compose('run', '--rm', '-T', '--no-deps', 'bootstrap', 'bootstrap',
'--application', 'local-testing', '--namespace', 'default', '--environment', 'test',
'--queue', 'integration', '--worker-access', '--schedule-access', '--event-access',
'--recovery-access', '--resource-access', '--workflow-access', '--workflow-run-access',
'--workflow-control-access', '--workflow-signal-access', '--workflow-recovery-access',
'--workflow-audit-access', '--console-username', 'admin',
'--console-display-name', 'Orbit 测试管理员', '--console-password-stdin',
'--output', '/run/local/sdk.credentials.json', input=password, text=True)
credential = json.loads(CREDENTIALS.read_text())
# 私密回执可能在 COMMIT 确认前写出;重启先核验库中同一身份,不重新 bootstrap。
status = compose('run', '--rm', '-T', '--no-deps', 'bootstrap', 'credential', 'status',
'--namespace-id', credential['namespace_id'], '--credential-id', credential['credential_id'],
stdout=subprocess.PIPE, text=True)
status = json.loads(status.stdout)
expired = status.get('expires_at') and datetime.datetime.fromisoformat(
status['expires_at'].replace('Z', '+00:00')) <= datetime.datetime.now(datetime.timezone.utc)
if status.get('revoked_at') or expired or not status['namespace_enabled'] or not status['application_enabled']:
raise RuntimeError('原凭据已停用或过期;请按凭据轮换手册处理,数据与原身份保持不变。')
values = settings()
values['ORBIT_CONSOLE_QUEUE_ID'] = credential['queue_id']
save_settings(values)
for name, url in [('host.env', BASE_URL), ('docker.env', 'http://orbit-test:8080')]:
private_write(PRIVATE / name, '\n'.join([
'ORBIT_URL=' + url, 'ORBIT_TOKEN=' + credential['token'],
'ORBIT_NAMESPACE_ID=' + credential['namespace_id'],
'ORBIT_QUEUE_ID=' + credential['queue_id'], '']) )
compose('up', '-d', 'orbit')
wait_ready()
request = urllib.request.Request(BASE_URL + '/v1/queues/' + credential['queue_id'],
headers={'Authorization': 'Bearer ' + credential['token']})
with urllib.request.urlopen(request, timeout=5) as response:
if response.status != 200:
raise RuntimeError('已保存的 SDK 凭据未通过队列访问校验。')
print('Orbit ready: ' + BASE_URL + '/console/')
print('Docker network: orbit-test-services; API: http://orbit-test:8080')
print('Admin: admin; password file: ' + str(PRIVATE / 'admin.password'))
print('SDK settings: ' + str(PRIVATE / 'host.env') + ' / docker.env')
print('Credential expiry: ' + (status.get('expires_at') or 'permanent'))
def main():
action = sys.argv[1] if len(sys.argv) > 1 else 'status'
if action == 'up':
if len(sys.argv) > 2:
raise RuntimeError('用法:scripts/orbit-local.py up')
setup()
return
if not ENV_FILE.exists():
raise RuntimeError('请先运行 scripts/orbit-local.py up。')
if action in ('status', 'ps'):
compose('ps', '--all')
elif action == 'logs':
compose('logs', '--tail', '100', *(sys.argv[2:] or ['orbit']))
elif action == 'stop':
compose('stop', 'orbit', 'mysql')
elif action == 'start':
compose('start', 'mysql', 'orbit')
wait_ready()
elif action == 'restart':
compose('restart', 'orbit')
wait_ready()
else:
raise RuntimeError('用法:scripts/orbit-local.py up|status|logs [service]|stop|start|restart')
if __name__ == '__main__':
try:
main()
except (RuntimeError, subprocess.CalledProcessError) as error:
print(str(error), file=sys.stderr)
sys.exit(1)