// 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}:{})}; 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'); const literal=text=>'['+encode(text).match(/.{1,4000}/g).map(part=>`'${part}'`).join(',')+"].join('')"; 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']))}`; 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');} 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) { const [action='plan',...rest]=argv,args={}; for(let i=0;i{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;});