2026-10-03 05:25:30 +08:00
|
|
|
"""Bounded remote TEST actions; injected by the JS executor, never invoked at import.
|
|
|
|
|
|
|
|
|
|
Only Order is recreated. Every Docker mutation is journaled before sending; an
|
|
|
|
|
uncertain result blocks further mutations until explicit read-only reconciliation.
|
|
|
|
|
"""
|
|
|
|
|
import ast
|
|
|
|
|
import base64
|
|
|
|
|
import copy
|
|
|
|
|
import hashlib
|
|
|
|
|
import http.client
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import secrets
|
|
|
|
|
import signal
|
|
|
|
|
import socket
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
import urllib.parse
|
|
|
|
|
import urllib.request
|
2026-10-03 06:21:57 +08:00
|
|
|
import urllib.error
|
2026-10-03 05:25:30 +08:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
ORDER = 'firstleg-test-order'
|
|
|
|
|
ENTRYPOINT = ['sh', '-c', 'exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher']
|
2026-10-04 00:04:36 +08:00
|
|
|
PROPERTIES = {'IAM': 'baiyue.clients.iam.url', 'FINANCE': 'baiyue.clients.finance.url',
|
|
|
|
|
'WAREHOUSE': 'baiyue.clients.warehouse.url'}
|
|
|
|
|
UPSTREAM_CONTAINERS = {'IAM': 'firstleg-test-iam', 'FINANCE': 'firstleg-test-finance',
|
|
|
|
|
'WAREHOUSE': 'firstleg-test-warehouse'}
|
2026-10-03 05:25:30 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
|
|
|
def redirect_request(self, *_args, **_kwargs): return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def http_open(request, timeout):
|
|
|
|
|
return urllib.request.build_opener(NoRedirect()).open(request, timeout=timeout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def common_tools(source):
|
|
|
|
|
"""Reuse only Docker read/config reconstruction primitives, never event/SQL functions."""
|
|
|
|
|
tree = ast.parse(source)
|
|
|
|
|
allowed = {'DockerConnection', 'docker', 'inspect', 'env_of', 'require_test', 'recreation', 'fingerprint'}
|
|
|
|
|
nodes = [node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))
|
|
|
|
|
or isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in allowed]
|
|
|
|
|
namespace = {'__name__': 'order_dependency_primitives'}
|
|
|
|
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), '<reviewed-docker-primitives>', 'exec'), namespace)
|
|
|
|
|
return {name: namespace[name] for name in allowed}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def digest(value):
|
|
|
|
|
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def private_write(path, value):
|
|
|
|
|
temporary = path.with_name(path.name + '.next')
|
|
|
|
|
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
|
|
|
with os.fdopen(fd, 'w') as stream:
|
|
|
|
|
json.dump(value, stream); stream.flush(); os.fsync(stream.fileno())
|
|
|
|
|
os.replace(temporary, path)
|
|
|
|
|
parent = os.open(path.parent, os.O_RDONLY)
|
|
|
|
|
try: os.fsync(parent)
|
|
|
|
|
finally: os.close(parent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_scope(scope):
|
2026-10-04 00:04:36 +08:00
|
|
|
if set(scope) != {'marker', 'customerId', 'receivingWarehouseId', 'orderIds', 'planSha256'} or not re.fullmatch(r'[A-Za-z0-9_-]{1,80}', scope['marker']):
|
2026-10-03 05:25:30 +08:00
|
|
|
raise RuntimeError('INVALID_SCOPE')
|
|
|
|
|
if not re.fullmatch(r'[1-9][0-9]{0,19}', scope['customerId']) or set(scope['orderIds']) != {'merchant', 'platform'}:
|
|
|
|
|
raise RuntimeError('INVALID_SCOPE')
|
2026-10-04 00:04:36 +08:00
|
|
|
if not re.fullmatch(r'[1-9][0-9]{0,19}', scope['receivingWarehouseId']):
|
|
|
|
|
raise RuntimeError('INVALID_RECEIVING_WAREHOUSE')
|
2026-10-03 05:25:30 +08:00
|
|
|
if any(not re.fullmatch(r'[1-9][0-9]{0,19}', value) for value in scope['orderIds'].values()) or len(set(scope['orderIds'].values())) != 2:
|
|
|
|
|
raise RuntimeError('TWO_DISTINCT_ORDERS_REQUIRED')
|
|
|
|
|
if not re.fullmatch(r'[0-9a-f]{64}', scope['planSha256']): raise RuntimeError('INVALID_PLAN_HASH')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Runtime:
|
|
|
|
|
def __init__(self, scope, tools, base=Path('/tmp')):
|
|
|
|
|
validate_scope(scope)
|
|
|
|
|
self.scope, self.tools = scope, tools
|
|
|
|
|
self.directory = base / ('chongchong-order-dependency-' + scope['marker'])
|
|
|
|
|
self.path = self.directory / 'state.json'
|
|
|
|
|
self.lock = base / 'chongchong-order-dependency.lock'
|
|
|
|
|
|
|
|
|
|
def save(self, state): private_write(self.path, state)
|
|
|
|
|
|
|
|
|
|
def load(self):
|
|
|
|
|
if self.directory.is_symlink() or self.directory.stat().st_uid != os.getuid() or self.directory.stat().st_mode & 0o077:
|
|
|
|
|
raise RuntimeError('UNSAFE_STATE_DIRECTORY')
|
|
|
|
|
if self.path.is_symlink() or self.path.stat().st_mode & 0o077: raise RuntimeError('UNSAFE_STATE_FILE')
|
|
|
|
|
state = json.loads(self.path.read_text())
|
|
|
|
|
if state['scope'] != self.scope: raise RuntimeError('SCOPE_CHANGED')
|
|
|
|
|
if (state.get('mutation') or {}).get('step', '').endswith('_INTENT'):
|
|
|
|
|
state['uncertain'] = True # Includes process death between HTTP completion and journal commit.
|
|
|
|
|
return state
|
|
|
|
|
|
|
|
|
|
def container(self):
|
|
|
|
|
value = self.tools['inspect'](ORDER)
|
|
|
|
|
self.tools['require_test'](value, ORDER)
|
|
|
|
|
if self.tools['env_of'](value).get('DEPLOY_ENV') != 'test': raise RuntimeError('NOT_TEST_ENV')
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
def control(self, state, mode=None, ttl=0):
|
|
|
|
|
pid = state.get('proxyPid')
|
|
|
|
|
if not isinstance(pid, int) or pid <= 1: raise RuntimeError('PROXY_PID_NOT_READY')
|
|
|
|
|
process = Path('/proc') / str(pid)
|
|
|
|
|
if str(self.directory / 'proxy.py').encode() not in (process / 'cmdline').read_bytes().split(b'\0'):
|
|
|
|
|
raise RuntimeError('PROXY_PROCESS_OWNERSHIP_MISMATCH')
|
|
|
|
|
socket_inodes = set()
|
|
|
|
|
for descriptor in (process / 'fd').iterdir():
|
|
|
|
|
try: target = os.readlink(descriptor)
|
|
|
|
|
except FileNotFoundError: continue
|
|
|
|
|
match = re.fullmatch(r'socket:\[(\d+)\]', target)
|
|
|
|
|
if match: socket_inodes.add(match.group(1))
|
|
|
|
|
owned_ports = set()
|
|
|
|
|
for line in Path('/proc/net/tcp').read_text().splitlines()[1:]:
|
|
|
|
|
values = line.split()
|
|
|
|
|
if len(values) > 9 and values[3] == '0A' and values[9] in socket_inodes:
|
|
|
|
|
owned_ports.add(int(values[1].split(':')[1], 16))
|
|
|
|
|
if not {state['proxyPort'], state['controlPort']}.issubset(owned_ports): raise RuntimeError('PROXY_LISTENERS_NOT_OWNED')
|
|
|
|
|
req = urllib.request.Request('http://127.0.0.1:' + str(state['controlPort']) + ('/mode' if mode else '/status'),
|
|
|
|
|
None if mode is None else json.dumps({'mode': mode, 'ttl': ttl}).encode(),
|
|
|
|
|
{'X-Order-Fault-Control': state['controlToken'], 'Content-Type': 'application/json'})
|
|
|
|
|
with http_open(req, timeout=5) as response: return json.load(response)
|
|
|
|
|
|
|
|
|
|
def probes(self, state):
|
|
|
|
|
"""Real authenticated reads, not manufactured business responses. No body/secret is returned."""
|
|
|
|
|
customer = self.scope['customerId']; results = {}
|
|
|
|
|
targets = {'IAM': '/internal/iam/tenants/' + customer + '/order-eligibility',
|
2026-10-04 00:04:36 +08:00
|
|
|
'FINANCE': '/internal/finance/v1/wallet/accounts/' + customer,
|
|
|
|
|
'WAREHOUSE': '/internal/warehouses/0/' + self.scope['receivingWarehouseId'] + '/active'}
|
2026-10-03 05:25:30 +08:00
|
|
|
for dependency, path in targets.items():
|
|
|
|
|
req = urllib.request.Request(state['upstreams'][dependency] + path,
|
|
|
|
|
headers={'X-Internal-Service-Token': state['internalToken']})
|
|
|
|
|
with http_open(req, timeout=8) as response:
|
|
|
|
|
value = json.load(response)
|
|
|
|
|
if response.status != 200 or str(value.get('code')) != '200': raise RuntimeError('UPSTREAM_PROBE_FAILED')
|
|
|
|
|
data = value.get('data') or {}
|
|
|
|
|
if dependency == 'IAM' and data.get('eligible') is not True: raise RuntimeError('CUSTOMER_NOT_ELIGIBLE')
|
|
|
|
|
if dependency == 'FINANCE' and (data.get('actualBalance') is None or float(data['actualBalance']) < 0):
|
|
|
|
|
raise RuntimeError('CUSTOMER_BALANCE_NOT_READY')
|
2026-10-04 00:04:36 +08:00
|
|
|
if dependency == 'WAREHOUSE':
|
|
|
|
|
warehouse = data.get('warehouse') or {}
|
|
|
|
|
if (str(warehouse.get('id')) != self.scope['receivingWarehouseId']
|
|
|
|
|
or str(warehouse.get('tenantId')) != '0' or warehouse.get('status') != 'ACTIVE'
|
|
|
|
|
or warehouse.get('warehouseType') != 'DOMESTIC' or not warehouse.get('warehouseName')):
|
|
|
|
|
raise RuntimeError('RECEIVING_WAREHOUSE_NOT_READY')
|
2026-10-03 05:25:30 +08:00
|
|
|
results[dependency] = True
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
def config_fingerprint(self, container, routing):
|
2026-10-03 06:21:57 +08:00
|
|
|
"""Read the two actual Nacos imports using the current v3 Admin endpoints.
|
2026-10-03 05:25:30 +08:00
|
|
|
No endpoint guessing/fallback and no Nacos write operation is supported.
|
|
|
|
|
"""
|
|
|
|
|
env = self.tools['env_of'](container); config = routing.get('nacos', {})
|
|
|
|
|
base = urllib.parse.urlsplit(config.get('baseUrl', ''))
|
|
|
|
|
configured = urllib.parse.urlsplit(env.get('NACOS_SERVER_ADDR', '') if '://' in env.get('NACOS_SERVER_ADDR', '')
|
|
|
|
|
else 'http://' + env.get('NACOS_SERVER_ADDR', ''))
|
|
|
|
|
allowed_hosts = {configured.hostname, '127.0.0.1', 'localhost'} | {
|
|
|
|
|
network.get('Gateway') for network in container['NetworkSettings']['Networks'].values()}
|
|
|
|
|
if (base.scheme not in ('http', 'https') or base.hostname not in allowed_hosts or base.port != configured.port
|
|
|
|
|
or base.path not in ('', '/') or base.username or base.password or base.query or base.fragment
|
2026-10-03 06:21:57 +08:00
|
|
|
or config.get('loginPath') != '/nacos/v3/auth/user/login'
|
|
|
|
|
or config.get('configPath') != '/nacos/v3/admin/cs/config'):
|
2026-10-03 05:25:30 +08:00
|
|
|
raise RuntimeError('EXPLICIT_NACOS_READ_ENDPOINTS_REQUIRED')
|
|
|
|
|
if any(not env.get(key) for key in ['NACOS_USERNAME', 'NACOS_PASSWORD', 'NACOS_GROUP', 'NACOS_NAMESPACE']):
|
|
|
|
|
raise RuntimeError('NACOS_READ_IDENTITY_UNAVAILABLE')
|
|
|
|
|
origin = config['baseUrl'].rstrip('/')
|
|
|
|
|
request = urllib.request.Request(origin + config['loginPath'], urllib.parse.urlencode({
|
|
|
|
|
'username': env['NACOS_USERNAME'], 'password': env['NACOS_PASSWORD']}).encode(),
|
|
|
|
|
{'Content-Type': 'application/x-www-form-urlencoded'})
|
|
|
|
|
with http_open(request, timeout=8) as response: token = json.load(response).get('accessToken')
|
|
|
|
|
if not isinstance(token, str) or not token: raise RuntimeError('NACOS_READ_LOGIN_FAILED')
|
|
|
|
|
fingerprints = {}
|
|
|
|
|
for data_id in ('baiyue-common.yml', 'order-service.yml'):
|
2026-10-03 06:21:57 +08:00
|
|
|
query = urllib.parse.urlencode({'dataId': data_id, 'groupName': env['NACOS_GROUP'], 'namespaceId': env['NACOS_NAMESPACE']})
|
|
|
|
|
request = urllib.request.Request(origin + config['configPath'] + '?' + query,
|
|
|
|
|
headers={'Authorization': 'Bearer ' + token})
|
|
|
|
|
with http_open(request, timeout=8) as response:
|
2026-10-03 05:25:30 +08:00
|
|
|
if response.status != 200: raise RuntimeError('NACOS_SOURCE_READ_FAILED')
|
2026-10-03 06:21:57 +08:00
|
|
|
value = json.load(response)
|
|
|
|
|
data = value.get('data')
|
|
|
|
|
if value.get('code') != 0 or not isinstance(data, dict): raise RuntimeError('NACOS_SOURCE_RESPONSE_INVALID')
|
|
|
|
|
if (data.get('dataId') != data_id or data.get('groupName') != env['NACOS_GROUP']
|
|
|
|
|
or data.get('namespaceId') != env['NACOS_NAMESPACE']): raise RuntimeError('NACOS_SOURCE_IDENTITY_MISMATCH')
|
|
|
|
|
content = data.get('content')
|
|
|
|
|
if not isinstance(content, str) or not content.strip() or content.lstrip().startswith(('{', '<')):
|
|
|
|
|
raise RuntimeError('NACOS_SOURCE_NOT_YAML')
|
|
|
|
|
content = content.encode('utf-8')
|
2026-10-03 05:25:30 +08:00
|
|
|
fingerprints[data_id] = hashlib.sha256(content).hexdigest()
|
|
|
|
|
return digest({'imports': fingerprints, 'namespace': env['NACOS_NAMESPACE'], 'group': env['NACOS_GROUP']})
|
|
|
|
|
|
|
|
|
|
def prepare(self, config):
|
|
|
|
|
if self.directory.exists(): raise RuntimeError('STATE_EXISTS_USE_STATUS_OR_RESTORE')
|
|
|
|
|
original = self.container()
|
|
|
|
|
if original['Image'] != config.get('expectedImageId') or not re.fullmatch(r'sha256:[0-9a-f]{64}', original['Image']):
|
|
|
|
|
raise RuntimeError('ORIGINAL_IMAGE_MISMATCH')
|
|
|
|
|
if original['State'].get('Health', {}).get('Status') != 'healthy': raise RuntimeError('ORDER_NOT_HEALTHY')
|
|
|
|
|
if original['Config']['Entrypoint'] != ENTRYPOINT or original['Config'].get('Cmd') not in (None, []):
|
|
|
|
|
raise RuntimeError('UNREVIEWED_JAVA_ENTRYPOINT')
|
|
|
|
|
if 'host.docker.internal:host-gateway' not in (original['HostConfig'].get('ExtraHosts') or []):
|
|
|
|
|
raise RuntimeError('REVIEWED_PROXY_HOST_ROUTE_REQUIRED')
|
|
|
|
|
env = self.tools['env_of'](original)
|
|
|
|
|
token = env.get('INTERNAL_SERVICE_TOKEN', '')
|
|
|
|
|
if len(token) < 16: raise RuntimeError('INTERNAL_TOKEN_UNAVAILABLE')
|
|
|
|
|
routing = config.get('routing', {})
|
|
|
|
|
if routing.get('configurationReviewed') is not True:
|
|
|
|
|
raise RuntimeError('UPSTREAM_CONFIGURATION_REVIEW_REQUIRED')
|
|
|
|
|
source_digest = self.config_fingerprint(original, routing)
|
|
|
|
|
if routing.get('sourceSha256') != source_digest: raise RuntimeError('REVIEWED_NACOS_SOURCE_DIGEST_CHANGED')
|
|
|
|
|
upstreams = routing.get('upstreams', {})
|
|
|
|
|
if set(upstreams) != set(PROPERTIES): raise RuntimeError('EXPLICIT_UPSTREAMS_REQUIRED')
|
|
|
|
|
for dependency, upstream in upstreams.items():
|
|
|
|
|
url = urllib.parse.urlsplit(upstream)
|
|
|
|
|
target = self.tools['inspect'](UPSTREAM_CONTAINERS[dependency]); self.tools['require_test'](target, UPSTREAM_CONTAINERS[dependency])
|
|
|
|
|
target_env = self.tools['env_of'](target)
|
|
|
|
|
ips = {network['IPAddress'] for name, network in target['NetworkSettings']['Networks'].items()
|
|
|
|
|
if name in original['NetworkSettings']['Networks']}
|
|
|
|
|
if (url.scheme != 'http' or url.username or url.password or url.path not in ('', '/') or url.query or url.fragment
|
|
|
|
|
or url.hostname not in ips or str(url.port) != target_env.get('SERVER_PORT')
|
|
|
|
|
or target_env.get('DEPLOY_ENV') != 'test' or target['State'].get('Health', {}).get('Status') != 'healthy'):
|
|
|
|
|
raise RuntimeError('UPSTREAM_NOT_VERIFIED_TEST_CONTAINER')
|
|
|
|
|
lock_fd = os.open(self.lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
|
|
|
with os.fdopen(lock_fd, 'w') as stream: stream.write(self.scope['marker']); stream.flush(); os.fsync(stream.fileno())
|
|
|
|
|
self.directory.mkdir(mode=0o700)
|
|
|
|
|
sockets = [socket.socket(), socket.socket()]
|
|
|
|
|
for sock in sockets: sock.bind(('127.0.0.1', 0))
|
|
|
|
|
proxy_port, control_port = [sock.getsockname()[1] for sock in sockets]
|
|
|
|
|
for sock in sockets: sock.close()
|
|
|
|
|
state = {'scope': self.scope, 'phase': 'PREPARED', 'originalId': original['Id'],
|
|
|
|
|
'originalImageId': original['Image'], 'originalFingerprint': self.tools['fingerprint'](original),
|
|
|
|
|
'originalBody': self.tools['recreation'](original, env, full=True), 'internalToken': token,
|
|
|
|
|
'proxyPort': proxy_port, 'controlPort': control_port, 'controlToken': secrets.token_urlsafe(32),
|
|
|
|
|
'upstreams': {k: v.rstrip('/') for k, v in upstreams.items()}, 'routingSourceSha256': source_digest, 'routing': routing,
|
|
|
|
|
'orderIps': [x['IPAddress'] for x in original['NetworkSettings']['Networks'].values()], 'mutation': None, 'proxyLaunchIntent': False}
|
|
|
|
|
self.save(state) # Recovery info exists before any process or container mutation.
|
|
|
|
|
self.probes(state)
|
|
|
|
|
proxy = self.directory / 'proxy.py'; proxy.write_bytes(base64.b64decode(config['proxySource'])); os.chmod(proxy, 0o600)
|
|
|
|
|
private_write(self.directory / 'proxy-config.json', {k: state[k] for k in
|
|
|
|
|
['internalToken', 'controlToken', 'proxyPort', 'controlPort', 'upstreams', 'orderIps']} | {'customerId': self.scope['customerId'],
|
2026-10-04 00:04:36 +08:00
|
|
|
'receivingWarehouseId': self.scope['receivingWarehouseId'],
|
2026-10-03 05:25:30 +08:00
|
|
|
'requestIds': ['dep-' + self.scope['marker'] + '-' + dependency + '-' + role
|
2026-10-04 00:04:36 +08:00
|
|
|
for dependency in PROPERTIES for role in ('merchant', 'platform')]})
|
2026-10-03 05:25:30 +08:00
|
|
|
state['phase'] = 'PROXY_START_INTENT'; state['proxyLaunchIntent'] = True; self.save(state)
|
|
|
|
|
with open(self.directory / 'proxy-output.log', 'ab') as output:
|
|
|
|
|
os.chmod(self.directory / 'proxy-output.log', 0o600)
|
|
|
|
|
process = subprocess.Popen(['python3', str(proxy), '--config', str(self.directory / 'proxy-config.json')],
|
|
|
|
|
stdout=output, stderr=output, start_new_session=True)
|
|
|
|
|
state['proxyPid'] = process.pid; state['phase'] = 'PROXY_STARTED'; self.save(state)
|
|
|
|
|
return self.summary(state)
|
|
|
|
|
|
|
|
|
|
def summary(self, state):
|
|
|
|
|
return {'scope': self.scope, 'phase': state['phase'], 'originalImageId': state['originalImageId'],
|
|
|
|
|
'originalFingerprint': state['originalFingerprint'], 'uncertain': bool(state.get('uncertain')),
|
|
|
|
|
'mutationStep': (state.get('mutation') or {}).get('step'), 'restored': state.get('restored', False)}
|
|
|
|
|
|
|
|
|
|
def mutation(self, state, step, method, path, body=None):
|
|
|
|
|
if state.get('uncertain'): raise RuntimeError('UNCERTAIN_MUTATION_REQUIRES_RECONCILE')
|
|
|
|
|
state['mutation'] = {'step': step, 'path': path, 'bodySha256': digest(body), 'intentAt': time.time()}; self.save(state)
|
|
|
|
|
try:
|
|
|
|
|
result = self.tools['docker'](method, path, body)
|
|
|
|
|
if step == 'CREATE_INTENT' and (not isinstance(result, dict) or not re.fullmatch(r'[A-Za-z0-9_-]{1,128}', result.get('Id', ''))):
|
|
|
|
|
raise RuntimeError('INVALID_CREATE_ACK')
|
|
|
|
|
except Exception:
|
|
|
|
|
state['uncertain'] = True; self.save(state)
|
|
|
|
|
raise RuntimeError('UNCERTAIN_DOCKER_MUTATION')
|
|
|
|
|
state['mutation']['step'] = step.replace('_INTENT', '_DONE')
|
|
|
|
|
if step == 'CREATE_INTENT': state['createdId'] = result['Id']
|
|
|
|
|
self.save(state)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def replace(self, state, current, desired, purpose):
|
|
|
|
|
if state.get('uncertain'): raise RuntimeError('UNCERTAIN_MUTATION_REQUIRES_RECONCILE')
|
|
|
|
|
if self.container()['Id'] != current['Id']: raise RuntimeError('CONCURRENT_ORDER_CHANGE')
|
|
|
|
|
image = self.tools['docker']('GET', '/images/' + urllib.parse.quote(desired['Image'], safe='') + '/json')
|
|
|
|
|
if image['Id'] != state['originalImageId']: raise RuntimeError('IMAGE_TAG_MOVED_NO_RECREATE')
|
|
|
|
|
state['purpose'] = purpose; state['desiredBody'] = desired; state['replacingId'] = current['Id']; self.save(state)
|
|
|
|
|
if current['State']['Running']:
|
|
|
|
|
self.mutation(state, 'STOP_INTENT', 'POST', '/containers/' + current['Id'] + '/stop?t=15')
|
|
|
|
|
self.mutation(state, 'DELETE_INTENT', 'DELETE', '/containers/' + current['Id'] + '?v=false')
|
|
|
|
|
created = self.mutation(state, 'CREATE_INTENT', 'POST', '/containers/create?name=' + ORDER, desired)
|
|
|
|
|
self.mutation(state, 'START_INTENT', 'POST', '/containers/' + created['Id'] + '/start')
|
|
|
|
|
state['currentId'] = created['Id']; state['phase'] = purpose; state['mutation'] = None; self.save(state)
|
|
|
|
|
|
|
|
|
|
def matches_injected(self, current, state):
|
|
|
|
|
expected = copy.deepcopy(state['originalBody']['Entrypoint'])
|
|
|
|
|
expected[2] += ''.join(' --' + prop + '=http://host.docker.internal:' + str(state['proxyPort']) + '/' + name.lower()
|
|
|
|
|
for name, prop in PROPERTIES.items())
|
|
|
|
|
restored_view = copy.deepcopy(current)
|
|
|
|
|
restored_view['Config']['Entrypoint'] = state['originalBody']['Entrypoint']
|
|
|
|
|
return (current['Image'] == state['originalImageId'] and current['Config']['Entrypoint'] == expected
|
|
|
|
|
and self.tools['fingerprint'](restored_view) == state['originalFingerprint'])
|
|
|
|
|
|
|
|
|
|
def activate(self, state):
|
|
|
|
|
if state['phase'] != 'PROXY_STARTED' or state.get('uncertain'): raise RuntimeError('ACTIVATE_NOT_RETRYABLE')
|
|
|
|
|
if self.control(state)['mode'] != 'PASS': raise RuntimeError('PROXY_NOT_PASS')
|
|
|
|
|
current = self.container()
|
|
|
|
|
if current['Id'] != state['originalId'] or self.tools['fingerprint'](current) != state['originalFingerprint']:
|
|
|
|
|
raise RuntimeError('ORIGINAL_CONFIGURATION_CHANGED')
|
|
|
|
|
desired = copy.deepcopy(state['originalBody'])
|
|
|
|
|
desired['Entrypoint'][2] += ''.join(' --' + prop + '=http://host.docker.internal:' + str(state['proxyPort']) + '/' + name.lower()
|
|
|
|
|
for name, prop in PROPERTIES.items())
|
|
|
|
|
self.replace(state, current, desired, 'INJECTED')
|
|
|
|
|
state['injectedFingerprint'] = self.tools['fingerprint'](self.container()); self.save(state)
|
|
|
|
|
return self.summary(state)
|
|
|
|
|
|
|
|
|
|
def restore(self, state):
|
|
|
|
|
# PASS is attempted even if Docker state is ambiguous. Never stop the proxy here.
|
|
|
|
|
try: self.control(state, 'PASS')
|
|
|
|
|
except Exception:
|
|
|
|
|
state['passUnconfirmed'] = True; self.save(state)
|
|
|
|
|
if state.get('uncertain'): raise RuntimeError('UNCERTAIN_MUTATION_REQUIRES_RECONCILE')
|
|
|
|
|
if state['phase'] == 'ABSENT_CONFIRMED':
|
|
|
|
|
try: self.tools['inspect'](ORDER)
|
|
|
|
|
except RuntimeError as error:
|
|
|
|
|
if str(error) != 'DOCKER_404': raise
|
|
|
|
|
else: raise RuntimeError('ORDER_REAPPEARED_NO_RECREATE')
|
|
|
|
|
body = copy.deepcopy(state['originalBody'])
|
|
|
|
|
image = self.tools['docker']('GET', '/images/' + urllib.parse.quote(body['Image'], safe='') + '/json')
|
|
|
|
|
if image['Id'] != state['originalImageId']: raise RuntimeError('IMAGE_TAG_MOVED_NO_RECREATE')
|
|
|
|
|
state['purpose'] = 'ORIGINAL_RESTORED'; state['desiredBody'] = body; self.save(state)
|
|
|
|
|
created = self.mutation(state, 'CREATE_INTENT', 'POST', '/containers/create?name=' + ORDER, body)
|
|
|
|
|
self.mutation(state, 'START_INTENT', 'POST', '/containers/' + created['Id'] + '/start')
|
|
|
|
|
state['currentId'] = created['Id']; state['phase'] = 'ORIGINAL_RESTORED'; state['mutation'] = None; self.save(state)
|
|
|
|
|
return self.summary(state)
|
|
|
|
|
current = self.container()
|
|
|
|
|
if self.tools['fingerprint'](current) == state['originalFingerprint'] and current['Image'] == state['originalImageId']:
|
|
|
|
|
if not current['State']['Running']:
|
|
|
|
|
state['purpose'] = 'ORIGINAL_RESTORED'; state['createdId'] = current['Id']; self.save(state)
|
|
|
|
|
self.mutation(state, 'START_INTENT', 'POST', '/containers/' + current['Id'] + '/start')
|
|
|
|
|
state['phase'] = 'ORIGINAL_RESTORED'; self.save(state); return self.summary(state)
|
|
|
|
|
if current['Id'] != state.get('currentId') or not self.matches_injected(current, state):
|
|
|
|
|
raise RuntimeError('UNRECOGNIZED_ORDER_CONFIGURATION')
|
|
|
|
|
self.replace(state, current, copy.deepcopy(state['originalBody']), 'ORIGINAL_RESTORED')
|
|
|
|
|
return self.summary(state)
|
|
|
|
|
|
|
|
|
|
def reconcile(self, state):
|
|
|
|
|
# This action is intentionally read-only with respect to Docker. It never resumes deletion/creation.
|
|
|
|
|
if not state.get('uncertain'): return self.summary(state)
|
|
|
|
|
try: current = self.container()
|
|
|
|
|
except RuntimeError as error:
|
|
|
|
|
if str(error) != 'DOCKER_404' or (state.get('mutation') or {}).get('step') != 'DELETE_INTENT':
|
|
|
|
|
raise RuntimeError('MANUAL_DOCKER_RECOVERY_REQUIRED_NO_BLIND_RECREATE')
|
|
|
|
|
try: self.tools['inspect'](state['replacingId'])
|
|
|
|
|
except RuntimeError as by_id:
|
|
|
|
|
if str(by_id) != 'DOCKER_404': raise
|
|
|
|
|
else: raise RuntimeError('ORIGINAL_ID_STILL_PRESENT')
|
|
|
|
|
state['uncertain'] = False; state['phase'] = 'ABSENT_CONFIRMED'; state['mutation'] = None; self.save(state)
|
|
|
|
|
return self.summary(state)
|
|
|
|
|
step = (state.get('mutation') or {}).get('step')
|
|
|
|
|
original = current['Image'] == state['originalImageId'] and self.tools['fingerprint'](current) == state['originalFingerprint']
|
|
|
|
|
injected = self.matches_injected(current, state)
|
|
|
|
|
desired_matches = injected if state.get('purpose') == 'INJECTED' else original
|
|
|
|
|
if step == 'STOP_INTENT' and current['Id'] == state.get('replacingId') and not current['State']['Running'] and (original or injected):
|
|
|
|
|
phase = 'ORIGINAL_RESTORED' if original else 'INJECTED'
|
|
|
|
|
elif step == 'CREATE_INTENT' and current['Id'] != state.get('replacingId') and desired_matches:
|
|
|
|
|
phase = state['purpose']
|
|
|
|
|
elif step == 'START_INTENT' and current['Id'] == state.get('createdId') and current['State']['Running'] and desired_matches:
|
|
|
|
|
phase = state['purpose']
|
|
|
|
|
else:
|
|
|
|
|
raise RuntimeError('MANUAL_DOCKER_RECOVERY_REQUIRED_NO_BLIND_RECREATE')
|
|
|
|
|
state['uncertain'] = False; state['currentId'] = current['Id']; state['phase'] = phase; state['mutation'] = None
|
|
|
|
|
if phase == 'INJECTED': state['injectedFingerprint'] = self.tools['fingerprint'](current)
|
|
|
|
|
self.save(state); return self.summary(state)
|
|
|
|
|
|
|
|
|
|
def verify_restored(self, state):
|
|
|
|
|
current = self.container()
|
|
|
|
|
if state.get('uncertain') or current['Image'] != state['originalImageId'] or self.tools['fingerprint'](current) != state['originalFingerprint']:
|
|
|
|
|
raise RuntimeError('RESTORED_CONFIG_OR_IMAGE_MISMATCH')
|
|
|
|
|
if current['State'].get('Health', {}).get('Status') != 'healthy': raise RuntimeError('RESTORED_NOT_HEALTHY')
|
|
|
|
|
if self.config_fingerprint(current, state['routing']) != state['routingSourceSha256']:
|
|
|
|
|
raise RuntimeError('NACOS_ROUTE_CONFIGURATION_CHANGED')
|
|
|
|
|
checks = self.probes(state)
|
2026-10-04 00:04:36 +08:00
|
|
|
if checks != {dependency: True for dependency in PROPERTIES}: raise RuntimeError('RESTORED_ROUTE_PROBE_INVALID')
|
2026-10-03 05:25:30 +08:00
|
|
|
state['routeReadsVerified'] = checks; self.save(state)
|
|
|
|
|
return {'healthy': True, 'originalFingerprint': state['originalFingerprint'], 'currentFingerprint': self.tools['fingerprint'](current),
|
|
|
|
|
'originalImageId': state['originalImageId'], 'currentImageId': current['Image'], 'upstreamReads': checks,
|
|
|
|
|
'nacosSourcesUnchanged': True}
|
|
|
|
|
|
|
|
|
|
def finish(self, state, business):
|
|
|
|
|
self.verify_restored(state)
|
|
|
|
|
if self.lock.is_symlink() or self.lock.read_text() != self.scope['marker']: raise RuntimeError('RUNTIME_LOCK_OWNER_CHANGED')
|
|
|
|
|
if set(business or {}) != {'merchant', 'platform'} or any(
|
|
|
|
|
business[role].get('orderId') != order_id or business[role].get('status') != 'SUBMITTED'
|
|
|
|
|
or business[role].get('sameKeyReplayVerified') is not True for role, order_id in self.scope['orderIds'].items()):
|
|
|
|
|
raise RuntimeError('RESTORED_ORDER_ROUTES_NOT_PROVED')
|
|
|
|
|
pid = state.get('proxyPid')
|
|
|
|
|
if not pid and state.get('proxyLaunchIntent'):
|
|
|
|
|
pid_file = self.directory / 'proxy.pid'
|
|
|
|
|
if pid_file.is_symlink() or not pid_file.exists() or not pid_file.read_text().isdigit(): raise RuntimeError('PROXY_PID_UNCERTAIN')
|
|
|
|
|
pid = int(pid_file.read_text())
|
|
|
|
|
command = Path('/proc/' + str(pid) + '/cmdline') if pid else None
|
|
|
|
|
if command and command.exists():
|
|
|
|
|
if str(self.directory / 'proxy.py').encode() not in command.read_bytes().split(b'\0'):
|
|
|
|
|
raise RuntimeError('PROXY_PROCESS_OWNERSHIP_MISMATCH')
|
|
|
|
|
os.kill(pid, signal.SIGTERM)
|
|
|
|
|
for _ in range(40):
|
|
|
|
|
if not command.exists() or not command.read_bytes(): break
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
else: raise RuntimeError('PROXY_NOT_STOPPED')
|
|
|
|
|
state['restored'] = True; state['phase'] = 'FINISHED'
|
|
|
|
|
# Recovery snapshots remain protected until completion; erase secrets only after verified restoration.
|
|
|
|
|
for field in ['originalBody', 'desiredBody', 'internalToken', 'controlToken']: state.pop(field, None)
|
|
|
|
|
self.save(state)
|
|
|
|
|
config = self.directory / 'proxy-config.json'
|
|
|
|
|
if config.exists(): config.unlink()
|
|
|
|
|
self.lock.unlink()
|
|
|
|
|
return {'restored': True, 'healthy': True, 'proxyStopped': True, 'originalImageId': state['originalImageId'],
|
|
|
|
|
'originalFingerprint': state['originalFingerprint'], 'orderRoutesVerified': True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def execute(config):
|
|
|
|
|
runtime = Runtime(config['scope'], common_tools(base64.b64decode(config['commonSource']).decode()))
|
|
|
|
|
action = config['action']
|
|
|
|
|
if action == 'preflight':
|
|
|
|
|
current = runtime.container()
|
|
|
|
|
return {'originalImageId': current['Image'], 'originalFingerprint': runtime.tools['fingerprint'](current),
|
|
|
|
|
'sourceSha256': runtime.config_fingerprint(current, config['routing']), 'runtimeChanged': False}
|
|
|
|
|
if action == 'prepare': return runtime.prepare(config)
|
|
|
|
|
state = runtime.load()
|
|
|
|
|
if action == 'status':
|
|
|
|
|
result = runtime.summary(state)
|
|
|
|
|
if not state.get('restored'):
|
|
|
|
|
try: result['proxy'] = runtime.control(state)
|
|
|
|
|
except Exception: result['proxy'] = {'unavailable': True}
|
|
|
|
|
return result
|
|
|
|
|
if state.get('restored'): raise RuntimeError('ALREADY_FINISHED_USE_STATUS')
|
|
|
|
|
if action not in ('records', 'health') and (runtime.lock.is_symlink() or runtime.lock.read_text() != runtime.scope['marker']):
|
|
|
|
|
raise RuntimeError('RUNTIME_LOCK_OWNER_CHANGED')
|
|
|
|
|
if action == 'records':
|
|
|
|
|
path = runtime.directory / 'proxy-evidence.jsonl'
|
|
|
|
|
return {'records': [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []}
|
|
|
|
|
if action == 'activate': return runtime.activate(state)
|
|
|
|
|
if action == 'mode':
|
|
|
|
|
if state['phase'] != 'INJECTED' or state.get('uncertain'): raise RuntimeError('FAULT_WINDOW_NOT_READY')
|
|
|
|
|
return runtime.control(state, config['mode'], config.get('ttl', 0))
|
|
|
|
|
if action == 'health': return {'healthy': runtime.container()['State'].get('Health', {}).get('Status') == 'healthy'}
|
|
|
|
|
if action == 'restore': return runtime.restore(state)
|
|
|
|
|
if action == 'reconcile': return runtime.reconcile(state)
|
|
|
|
|
if action == 'verify-restored': return runtime.verify_restored(state)
|
|
|
|
|
if action == 'finish': return runtime.finish(state, config.get('businessRecovery'))
|
|
|
|
|
raise RuntimeError('UNKNOWN_ACTION')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(config):
|
|
|
|
|
# A lost Jenkins response must not allow a second process to race the first Docker mutation.
|
|
|
|
|
import fcntl
|
|
|
|
|
validate_scope(config['scope'])
|
|
|
|
|
path = Path('/tmp') / ('chongchong-order-dependency-' + config['scope']['marker'] + '.action.lock')
|
|
|
|
|
if path.is_symlink(): raise RuntimeError('UNSAFE_ACTION_LOCK')
|
|
|
|
|
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
|
|
|
|
|
try:
|
|
|
|
|
try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
|
|
|
except BlockingIOError: raise RuntimeError('RUNTIME_ACTION_IN_PROGRESS_USE_STATUS_LATER')
|
|
|
|
|
return execute(config)
|
|
|
|
|
finally: os.close(fd)
|
|
|
|
|
|
|
|
|
|
|
2026-10-03 06:21:57 +08:00
|
|
|
def safe_error_code(error):
|
|
|
|
|
message = str(error)
|
|
|
|
|
if isinstance(error, urllib.error.HTTPError):
|
|
|
|
|
path = urllib.parse.urlsplit(error.geturl()).path
|
|
|
|
|
stage = 'NACOS_CONFIG' if path == '/nacos/v3/admin/cs/config' else (
|
|
|
|
|
'NACOS_LOGIN' if path == '/nacos/v3/auth/user/login' else 'DEPENDENCY_HTTP')
|
|
|
|
|
return stage + '_' + str(error.code)
|
|
|
|
|
if isinstance(error, urllib.error.URLError):
|
|
|
|
|
return 'DEPENDENCY_TRANSPORT_FAILED'
|
|
|
|
|
return message if re.fullmatch(r'[A-Z_0-9]{1,90}', message) else 'ORDER_DEPENDENCY_ACTION_FAILED'
|
|
|
|
|
|
|
|
|
|
|
2026-10-03 05:25:30 +08:00
|
|
|
if __name__ == '__main__':
|
|
|
|
|
try:
|
|
|
|
|
result = main(json.loads(base64.b64decode(sys.argv[1])))
|
|
|
|
|
print('ORDER_DEPENDENCY_JSON=' + json.dumps({'ok': True, 'data': result}))
|
|
|
|
|
except Exception as error:
|
2026-10-03 06:21:57 +08:00
|
|
|
print('ORDER_DEPENDENCY_JSON=' + json.dumps({'ok': False, 'error': safe_error_code(error)}))
|