fix(test): retain safe inbound resource fault diagnostics
This commit is contained in:
@@ -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'}});
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user