fix(test): retain safe inbound resource fault diagnostics

This commit is contained in:
2026-10-05 04:11:33 +08:00
parent 6fc32574c7
commit 5ea1c277bc
4 changed files with 74 additions and 8 deletions
@@ -61,7 +61,16 @@ export function createInboundResourceRuntime(env, scope, fetcher=globalThis.fetc
const text=await response.text();need(!text.includes('MultipleCompilationErrorsException'),'RENDERER_COMPILATION_FAILED_NO_ACTION'); const text=await response.text();need(!text.includes('MultipleCompilationErrorsException'),'RENDERER_COMPILATION_FAILED_NO_ACTION');
const rows=text.split(/\r?\n/).filter(row=>row.startsWith('INBOUND_RESOURCE_JSON='));need(rows.length===1,'AMBIGUOUS_RESPONSE_NO_AUTO_RETRY'); const rows=text.split(/\r?\n/).filter(row=>row.startsWith('INBOUND_RESOURCE_JSON='));need(rows.length===1,'AMBIGUOUS_RESPONSE_NO_AUTO_RETRY');
let result;try {result=JSON.parse(rows[0].slice('INBOUND_RESOURCE_JSON='.length));}catch{throw Error('INVALID_RESPONSE_NO_AUTO_RETRY');} let result;try {result=JSON.parse(rows[0].slice('INBOUND_RESOURCE_JSON='.length));}catch{throw Error('INVALID_RESPONSE_NO_AUTO_RETRY');}
need(result?.ok===true,/^[A-Z_0-9]{1,90}$/.test(result?.error||'')?result.error:'RUNTIME_ACTION_FAILED');return result.data; if(result?.ok!==true) {
const error=Error(/^[A-Z_0-9]{1,90}$/.test(result?.error||'')?result.error:'RUNTIME_ACTION_FAILED');
const d=result?.diagnostic;
if(d&&/^[A-Z_0-9]{1,90}$/.test(d.stage||'')&&/^[A-Za-z_]{1,80}$/.test(d.errorType||''))
error.runtimeDiagnostic={stage:d.stage,errorType:d.errorType,...(Number.isInteger(d.httpStatus)&&d.httpStatus>=100&&d.httpStatus<=599?{httpStatus:d.httpStatus}:{}),
...(/^[A-Za-z_]{1,80}$/.test(d.transportErrorType||'')?{transportErrorType:d.transportErrorType}:{}),
...(Number.isInteger(d.errno)&&d.errno>=-10000&&d.errno<=10000?{errno:d.errno}:{})};
throw error;
}
return result.data;
}; };
} }
async function main(argv) { async function main(argv) {
@@ -74,4 +83,4 @@ async function main(argv) {
if(args.mode)extra.mode=args.mode;if(args.ttl)extra.ttl=Number(args.ttl); if(args.mode)extra.mode=args.mode;if(args.ttl)extra.ttl=Number(args.ttl);
console.log(JSON.stringify(await createInboundResourceRuntime(process.env,scope)(action,extra),null,2)); console.log(JSON.stringify(await createInboundResourceRuntime(process.env,scope)(action,extra),null,2));
} }
if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href)main(process.argv.slice(2)).catch(error=>{console.error(/^[A-Z_0-9]{1,90}$/.test(error.message)?error.message:'INBOUND_RESOURCE_ARGUMENT_ERROR');process.exitCode=1;}); if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href)main(process.argv.slice(2)).catch(error=>{console.error(JSON.stringify({error:/^[A-Z_0-9]{1,90}$/.test(error.message)?error.message:'INBOUND_RESOURCE_ARGUMENT_ERROR',diagnostic:error.runtimeDiagnostic}));process.exitCode=1;});
@@ -46,6 +46,18 @@ test('transport unknown, duplicate result and malformed result each fail after e
} }
assert.throws(()=>createInboundResourceRuntime({...env,CHONGCHONG_ENV:'prod'},scope)); assert.throws(()=>createInboundResourceRuntime({...env,CHONGCHONG_ENV:'prod'},scope));
}); });
test('remote error preserves safe stage/type/status and strips untrusted diagnostic fields',async()=>{
const runtime=createInboundResourceRuntime(env,scope,async()=>({ok:true,text:async()=> 'INBOUND_RESOURCE_JSON='+JSON.stringify({ok:false,error:'NACOS_CONFIG_READ_HTTP_404',diagnostic:{stage:'NACOS_CONFIG_READ',errorType:'HTTPError',httpStatus:404,token:'must-never-escape',body:'secret',url:'http://private.invalid'}})}));
await assert.rejects(runtime('preflight',{routing}),error=>{
assert.equal(error.message,'NACOS_CONFIG_READ_HTTP_404');assert.deepEqual(error.runtimeDiagnostic,{stage:'NACOS_CONFIG_READ',errorType:'HTTPError',httpStatus:404});return true;
});
});
test('DNS diagnostics preserve only bounded errno and transport class, no private resolver message',async()=>{
const runtime=createInboundResourceRuntime(env,scope,async()=>({ok:true,text:async()=> 'INBOUND_RESOURCE_JSON='+JSON.stringify({ok:false,error:'NACOS_SOURCE_READ_TRANSPORT_FAILED',diagnostic:{stage:'NACOS_SOURCE_READ',errorType:'URLError',transportErrorType:'gaierror',errno:-2,message:'private resolver secret'}})}));
await assert.rejects(runtime('preflight',{routing}),error=>{
assert.deepEqual(error.runtimeDiagnostic,{stage:'NACOS_SOURCE_READ',errorType:'URLError',transportErrorType:'gaierror',errno:-2});return true;
});
});
test('default plan CLI has zero HTTP and exposes no business conversion action',()=>{ test('default plan CLI has zero HTTP and exposes no business conversion action',()=>{
const dir=mkdtempSync(join(tmpdir(),'inbound-fault-plan-'));const file=join(dir,'plan.json');writeFileSync(file,JSON.stringify(plan)); const dir=mkdtempSync(join(tmpdir(),'inbound-fault-plan-'));const file=join(dir,'plan.json');writeFileSync(file,JSON.stringify(plan));
const run=spawnSync(process.execPath,[fileURLToPath(new URL('./inbound-plan-resource-fault-executor.mjs',import.meta.url)),'plan','--plan',file],{encoding:'utf8',env:{...process.env,CHONGCHONG_ENV:'prod',JENKINS_URL:'http://127.0.0.1:1'}}); const run=spawnSync(process.execPath,[fileURLToPath(new URL('./inbound-plan-resource-fault-executor.mjs',import.meta.url)),'plan','--plan',file],{encoding:'utf8',env:{...process.env,CHONGCHONG_ENV:'prod',JENKINS_URL:'http://127.0.0.1:1'}});
+40 -5
View File
@@ -18,11 +18,37 @@ import sys
import time import time
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import urllib.error
from pathlib import Path from pathlib import Path
TARGET = 'firstleg-test-lastleg' TARGET = 'firstleg-test-lastleg'
UPSTREAM = 'firstleg-test-warehouse-overseas' UPSTREAM = 'firstleg-test-warehouse-overseas'
PROPERTY = 'baiyue.warehouse.base-url' PROPERTY = 'baiyue.warehouse.base-url'
STAGE = 'INITIALIZE'
def safe_diagnostic(error):
"""Never return exception messages, URLs, bodies, environment or identities."""
stage, status = STAGE, None
if isinstance(error, urllib.error.HTTPError):
endpoint = urllib.parse.urlsplit(error.geturl()).path
stage = {'/nacos/v3/auth/user/login': 'NACOS_LOGIN',
'/nacos/v3/admin/cs/config': 'NACOS_CONFIG_READ'}.get(endpoint, stage)
status = error.code if type(error.code) is int and 100 <= error.code <= 599 else None
kind = type(error).__name__
diagnostic = {'stage': stage, 'errorType': kind if re.fullmatch(r'[A-Za-z_]{1,80}', kind) else 'Exception'}
if isinstance(error, urllib.error.URLError) and not isinstance(error, urllib.error.HTTPError):
reason = error.reason
reason_type = type(reason).__name__
if re.fullmatch(r'[A-Za-z_]{1,80}', reason_type): diagnostic['transportErrorType'] = reason_type
number = getattr(reason, 'errno', None)
if type(number) is int and -10000 <= number <= 10000: diagnostic['errno'] = number
if type(status) is int and 100 <= status <= 599: diagnostic['httpStatus'] = status
message = str(error)
code = message if re.fullmatch(r'[A-Z_0-9]{1,90}', message) else 'INBOUND_RESOURCE_ACTION_FAILED'
if status is not None: code = stage + '_HTTP_' + str(status)
elif isinstance(error, urllib.error.URLError): code = stage + '_TRANSPORT_FAILED'
return {'ok': False, 'error': code, 'diagnostic': diagnostic}
def validate_scope(scope): def validate_scope(scope):
@@ -183,15 +209,25 @@ def runtime_class(base_source):
def execute(config): def execute(config):
global STAGE
STAGE = 'LOAD_REVIEWED_PRIMITIVES'
Runtime, namespace = runtime_class(base64.b64decode(config['baseSource']).decode()) Runtime, namespace = runtime_class(base64.b64decode(config['baseSource']).decode())
runtime = Runtime(config['scope'], namespace['common_tools'](base64.b64decode(config['commonSource']).decode())) runtime = Runtime(config['scope'], namespace['common_tools'](base64.b64decode(config['commonSource']).decode()))
action = config['action'] action = config['action']
if action == 'preflight': if action == 'preflight':
STAGE = 'LASTLEG_DOCKER_READ'
current = runtime.container() current = runtime.container()
STAGE = 'WAREHOUSE_UPSTREAM_IDENTITY'
target = runtime.verified_upstream(current, config['routing'], runtime.tools['env_of'](current).get('INTERNAL_SERVICE_TOKEN', '')) target = runtime.verified_upstream(current, config['routing'], runtime.tools['env_of'](current).get('INTERNAL_SERVICE_TOKEN', ''))
return {'originalImageId': current['Image'], 'originalFingerprint': runtime.tools['fingerprint'](current), STAGE = 'LASTLEG_FULL_FINGERPRINT'
'sourceSha256': runtime.config_fingerprint(current, config['routing']), 'runtimeChanged': False, original_fingerprint = runtime.tools['fingerprint'](current)
'upstreamImageId': target['Image'], 'upstreamFingerprint': runtime.tools['fingerprint'](target)} STAGE = 'NACOS_SOURCE_READ'
source_hash = runtime.config_fingerprint(current, config['routing'])
STAGE = 'WAREHOUSE_FULL_FINGERPRINT'
upstream_fingerprint = runtime.tools['fingerprint'](target)
return {'originalImageId': current['Image'], 'originalFingerprint': original_fingerprint,
'sourceSha256': source_hash, 'runtimeChanged': False,
'upstreamImageId': target['Image'], 'upstreamFingerprint': upstream_fingerprint}
if action == 'prepare': return runtime.prepare(config) if action == 'prepare': return runtime.prepare(config)
state = runtime.load() state = runtime.load()
if action == 'status': if action == 'status':
@@ -234,5 +270,4 @@ def main(config):
if __name__ == '__main__': if __name__ == '__main__':
try: print('INBOUND_RESOURCE_JSON=' + json.dumps({'ok': True, 'data': main(json.loads(base64.b64decode(sys.argv[1])))})) try: print('INBOUND_RESOURCE_JSON=' + json.dumps({'ok': True, 'data': main(json.loads(base64.b64decode(sys.argv[1])))}))
except Exception as error: except Exception as error:
message = str(error) print('INBOUND_RESOURCE_JSON=' + json.dumps(safe_diagnostic(error)))
print('INBOUND_RESOURCE_JSON=' + json.dumps({'ok': False, 'error': message if re.fullmatch(r'[A-Z_0-9]{1,90}', message) else 'INBOUND_RESOURCE_ACTION_FAILED'}))
@@ -3,9 +3,19 @@ import assert from 'node:assert/strict';
import {spawnSync} from 'node:child_process'; import {spawnSync} from 'node:child_process';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
const script=String.raw` const script=String.raw`
import importlib.util,sys,json,hashlib,copy import importlib.util,sys,json,hashlib,copy,urllib.error
from pathlib import Path from pathlib import Path
spec=importlib.util.spec_from_file_location('resource_runtime',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) spec=importlib.util.spec_from_file_location('resource_runtime',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
m.STAGE='NACOS_SOURCE_READ'
for endpoint,stage in [('/nacos/v3/auth/user/login','NACOS_LOGIN'),('/nacos/v3/admin/cs/config','NACOS_CONFIG_READ')]:
d=m.safe_diagnostic(urllib.error.HTTPError('http://user:password@private.invalid'+endpoint+'?token=secret',404,'secret body',{},None))
assert d=={'ok':False,'error':stage+'_HTTP_404','diagnostic':{'stage':stage,'errorType':'HTTPError','httpStatus':404}}
assert 'secret' not in json.dumps(d) and 'private' not in json.dumps(d)
assert m.safe_diagnostic(TypeError('private-secret-response'))=={'ok':False,'error':'INBOUND_RESOURCE_ACTION_FAILED','diagnostic':{'stage':'NACOS_SOURCE_READ','errorType':'TypeError'}}
import socket
dns=m.safe_diagnostic(urllib.error.URLError(socket.gaierror(-2,'private DNS hostname secret')))
assert dns=={'ok':False,'error':'NACOS_SOURCE_READ_TRANSPORT_FAILED','diagnostic':{'stage':'NACOS_SOURCE_READ','errorType':'URLError','transportErrorType':'gaierror','errno':-2}}
assert 'private' not in json.dumps(dns) and 'secret' not in json.dumps(dns)
base=Path(sys.argv[1]).with_name('order-submit-dependency-fault-runtime.py').read_text() base=Path(sys.argv[1]).with_name('order-submit-dependency-fault-runtime.py').read_text()
C,ns=m.runtime_class(base) C,ns=m.runtime_class(base)
scope={'environment':'TEST','marker':'OFFLINE','containerBookingId':'2106714159440728066','warehouseId':'193000000000200003','batchPrefixHash':hashlib.sha256(json.dumps('2106714159440728066').encode()).hexdigest(),'planSha256':'a'*64} scope={'environment':'TEST','marker':'OFFLINE','containerBookingId':'2106714159440728066','warehouseId':'193000000000200003','batchPrefixHash':hashlib.sha256(json.dumps('2106714159440728066').encode()).hexdigest(),'planSha256':'a'*64}