2026-10-05 03:48:29 +08:00
|
|
|
// Explicit one-container TEST runtime actions. Imports and default plan are offline.
|
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
import { resolve } from 'node:path';
|
|
|
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
|
const need = (ok, message) => { if (!ok) throw Error(message); };
|
|
|
|
|
const encode = value => Buffer.from(value).toString('base64');
|
|
|
|
|
const source = name => readFileSync(new URL(name, import.meta.url));
|
|
|
|
|
const runtime = source('./inbound-plan-resource-fault-runtime.py');
|
|
|
|
|
const baseSource = encode(source('./order-submit-dependency-fault-runtime.py'));
|
|
|
|
|
const commonSource = encode(source('./test-event-fault-runtime.py'));
|
|
|
|
|
const proxySource = encode(source('./inbound-plan-resource-fault-proxy.py'));
|
|
|
|
|
const httpProxySource = encode(source('./order-submit-dependency-fault-proxy.py'));
|
|
|
|
|
export const actions = ['plan','preflight','prepare','activate','status','health','mode','records','restore','reconcile','verify-restored','finish'];
|
|
|
|
|
export const batchPrefixHash = booking => createHash('sha256').update(JSON.stringify(booking)).digest('hex');
|
|
|
|
|
export function scopeFor(plan) {
|
|
|
|
|
need(plan?.environment === 'TEST' && plan.runtimeChangesExecuted === false, 'EXPLICIT_UNEXECUTED_TEST_PLAN_REQUIRED');
|
|
|
|
|
need(/^[A-Za-z0-9_-]{1,80}$/.test(plan.marker || ''), 'INVALID_MARKER');
|
|
|
|
|
for(const key of ['containerBookingId','warehouseId'])need(/^[1-9]\d{0,19}$/.test(plan[key] || ''), 'SINGLE_CONTAINER_WAREHOUSE_REQUIRED');
|
|
|
|
|
need(plan.batchPrefixHash === batchPrefixHash(plan.containerBookingId), 'JSON_STRING_BATCH_HASH_REQUIRED');
|
|
|
|
|
need(Object.keys(plan).every(key=>['environment','marker','containerBookingId','warehouseId','batchPrefixHash','runtimeChangesExecuted'].includes(key)), 'PLAN_SCHEMA_INVALID');
|
|
|
|
|
return {environment:'TEST',marker:plan.marker,containerBookingId:plan.containerBookingId,warehouseId:plan.warehouseId,
|
|
|
|
|
batchPrefixHash:plan.batchPrefixHash,planSha256:createHash('sha256').update(JSON.stringify(plan)).digest('hex')};
|
|
|
|
|
}
|
|
|
|
|
function routingFor(action, extra) {
|
|
|
|
|
if(!['preflight','prepare'].includes(action))return;
|
|
|
|
|
const r=extra.routing;
|
|
|
|
|
need(r?.configurationReviewed===true && Object.keys(r).every(k=>['configurationReviewed','sourceSha256','upstreams','nacos'].includes(k))
|
|
|
|
|
&& r.upstreams && Object.keys(r.upstreams).join(',')==='WAREHOUSE' && r.nacos
|
|
|
|
|
&& Object.keys(r.nacos).sort().join(',')==='baseUrl,configPath,loginPath'
|
|
|
|
|
&& r.nacos.loginPath==='/nacos/v3/auth/user/login' && r.nacos.configPath==='/nacos/v3/admin/cs/config','REVIEWED_ROUTING_REQUIRED');
|
|
|
|
|
for(const value of [r.nacos.baseUrl,r.upstreams.WAREHOUSE]) {
|
|
|
|
|
const url=new URL(value);need(['http:','https:'].includes(url.protocol)&&!url.username&&!url.password&&!url.search&&!url.hash&&['','/'].includes(url.pathname),'ORIGIN_ONLY_NO_CREDENTIALS');
|
|
|
|
|
}
|
|
|
|
|
if(action==='prepare')need(/^sha256:[a-f0-9]{64}$/.test(extra.expectedImageId||'')&&/^[a-f0-9]{64}$/.test(r.sourceSha256||''),'FROZEN_IMAGE_AND_NACOS_HASH_REQUIRED');
|
|
|
|
|
}
|
|
|
|
|
export function renderInboundResourceAction(action, scope, agent, extra={}) {
|
|
|
|
|
need(actions.includes(action)&&action!=='plan','UNKNOWN_RUNTIME_ACTION');
|
|
|
|
|
need(/^[A-Za-z0-9_.-]{1,128}$/.test(agent||''),'INVALID_AGENT');
|
|
|
|
|
need(scope.environment==='TEST'&&/^[A-Za-z0-9_-]{1,80}$/.test(scope.marker||'')&&scope.batchPrefixHash===batchPrefixHash(scope.containerBookingId)
|
|
|
|
|
&& /^[1-9]\d{0,19}$/.test(scope.containerBookingId||'')&&/^[1-9]\d{0,19}$/.test(scope.warehouseId||'')&&/^[a-f0-9]{64}$/.test(scope.planSha256||'')
|
|
|
|
|
&& Object.keys(scope).sort().join(',')==='batchPrefixHash,containerBookingId,environment,marker,planSha256,warehouseId','INVALID_SCOPE');
|
|
|
|
|
need(Object.keys(extra).every(k=>['routing','expectedImageId','mode','ttl'].includes(k)), 'RUNTIME_ARGUMENT_SCHEMA_INVALID');
|
|
|
|
|
routingFor(action,extra);
|
|
|
|
|
if(action==='mode')need(['PASS','CONFIRM_ONCE'].includes(extra.mode)&&(extra.mode==='PASS'||Number.isInteger(extra.ttl)&&extra.ttl>=1&&extra.ttl<=60),'INVALID_MODE_OR_TTL');
|
|
|
|
|
const config={...extra,action,scope,baseSource,commonSource,...(action==='prepare'?{proxySource,httpProxySource}:{})};
|
2026-10-05 04:52:38 +08:00
|
|
|
const configJson=JSON.stringify(config);
|
|
|
|
|
need(Buffer.byteLength(configJson,'utf8')<=1024*1024,'RUNTIME_STDIN_CONFIG_TOO_LARGE');
|
|
|
|
|
need(runtime.length<65536,'RUNTIME_CODE_ARG_TOO_LARGE');
|
2026-10-05 03:48:29 +08:00
|
|
|
const literal=text=>'['+encode(text).match(/.{1,4000}/g).map(part=>`'${part}'`).join(',')+"].join('')";
|
2026-10-05 04:52:38 +08:00
|
|
|
const safeResult=(code,stage)=>`print('INBOUND_RESOURCE_JSON='+groovy.json.JsonOutput.toJson([ok:false,error:'${code}',diagnostic:[stage:'${stage}',errorType:'IOException']]))`;
|
|
|
|
|
const groovy=`def p;try{p=new ProcessBuilder(['python3','-c',new String(java.util.Base64.decoder.decode(${literal(runtime)}),'UTF-8')]).redirectErrorStream(true).start()}catch(java.io.IOException e){${safeResult('REMOTE_PROCESS_START_FAILED','REMOTE_PROCESS_START')};return};try{p.outputStream.withCloseable{s->s.write(java.util.Base64.decoder.decode(${literal(configJson)}));s.flush()}}catch(java.io.IOException e){${safeResult('RUNTIME_STDIN_OUTCOME_UNKNOWN_USE_STATUS','REMOTE_PROCESS_STDIN')};return};def out=p.inputStream.getText('UTF-8');p.waitFor();def rows=out.readLines().findAll{it.startsWith('INBOUND_RESOURCE_JSON=')};if(rows.size()==1){print(rows[0])}else{print('INBOUND_RESOURCE_JSON='+groovy.json.JsonOutput.toJson([ok:false,error:'AMBIGUOUS_RESPONSE_NO_AUTO_RETRY']))}`;
|
2026-10-05 03:48:29 +08:00
|
|
|
return `def c=jenkins.model.Jenkins.instance.getComputer('${agent}');if(c==null || c.channel==null)throw new RuntimeException('TEST agent unavailable');print(hudson.util.RemotingDiagnostics.executeGroovy(new String(java.util.Base64.decoder.decode(${literal(groovy)}),'UTF-8'),c.channel))`;
|
|
|
|
|
}
|
|
|
|
|
export function createInboundResourceRuntime(env, scope, fetcher=globalThis.fetch) {
|
|
|
|
|
need(env.CHONGCHONG_ENV==='test','EXPLICIT_TEST_ENV_REQUIRED');
|
|
|
|
|
need(env.JENKINS_URL&&env.JENKINS_USER&&env.JENKINS_API_TOKEN&&env.FBA_CORE_JENKINS_AGENT,'EXPLICIT_TEST_JENKINS_REQUIRED');
|
|
|
|
|
return async(action,extra={})=>{
|
|
|
|
|
const script=renderInboundResourceAction(action,scope,env.FBA_CORE_JENKINS_AGENT,extra);
|
|
|
|
|
let response;
|
|
|
|
|
try {response=await fetcher(new URL('scriptText',env.JENKINS_URL.replace(/\/?$/,'/')),{method:'POST',redirect:'error',signal:AbortSignal.timeout(55000),
|
|
|
|
|
headers:{Authorization:'Basic '+encode(env.JENKINS_USER+':'+env.JENKINS_API_TOKEN),'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({script})});}
|
|
|
|
|
catch {throw Error('RUNTIME_'+action.toUpperCase().replaceAll('-','_')+'_OUTCOME_UNKNOWN_USE_STATUS');}
|
|
|
|
|
need(response.ok,'RUNTIME_HTTP_'+response.status+'_NO_AUTO_RETRY');
|
|
|
|
|
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');
|
|
|
|
|
let result;try {result=JSON.parse(rows[0].slice('INBOUND_RESOURCE_JSON='.length));}catch{throw Error('INVALID_RESPONSE_NO_AUTO_RETRY');}
|
2026-10-05 04:11:33 +08:00
|
|
|
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;
|
2026-10-05 03:48:29 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
async function main(argv) {
|
|
|
|
|
const [action='plan',...rest]=argv,args={};
|
|
|
|
|
for(let i=0;i<rest.length;i+=2){need(rest[i]?.startsWith('--')&&rest[i+1]&&!rest[i+1].startsWith('--'),'EXPLICIT_ARGUMENT_PAIRS_REQUIRED');args[rest[i].slice(2)]=rest[i+1];}
|
|
|
|
|
need(actions.includes(action)&&args.plan,'ACTION_AND_PLAN_REQUIRED');const scope=scopeFor(JSON.parse(readFileSync(args.plan,'utf8')));
|
|
|
|
|
if(action==='plan'){console.log(JSON.stringify({scope,target:'firstleg-test-lastleg',property:'baiyue.warehouse.base-url',actions,networkCalls:0,businessWritesSupported:false,requiresExclusiveLastlegWindow:true},null,2));return;}
|
|
|
|
|
need(args['confirm-test-runtime']==='yes','EXPLICIT_RUNTIME_CONFIRMATION_REQUIRED');
|
|
|
|
|
const extra={};if(args.routing)extra.routing=JSON.parse(readFileSync(args.routing,'utf8'));if(args['expected-image'])extra.expectedImageId=args['expected-image'];
|
|
|
|
|
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));
|
|
|
|
|
}
|
2026-10-05 04:11:33 +08:00
|
|
|
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;});
|