@deeep-network/riptide 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -5
- package/dist/index.d.ts +5 -72
- package/dist/index.js +5 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var z=Object.create;var E=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var Z=Object.getPrototypeOf,ee=Object.prototype.hasOwnProperty;var p=(o,e)=>E(o,"name",{value:e,configurable:!0});var te=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Q(e))!ee.call(o,s)&&s!==t&&E(o,s,{get:()=>e[s],enumerable:!(r=X(e,s))||r.enumerable});return o};var g=(o,e,t)=>(t=o!=null?z(Z(o)):{},te(e||!o||!o.__esModule?E(t,"default",{value:o,enumerable:!0}):t,o));var G=g(require("pino"));var W=require("crypto");var C=class o extends Error{static{p(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,o.prototype)}};var y=class o extends C{static{p(this,"PortOpenError")}exitCode=8;constructor(e){super(e),this.name="PortOpenError",Object.setPrototypeOf(this,o.prototype)}};function w(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}p(w,"isRiptideError");var k=g(require("pino"));function _(o){let{level:e="info",format:t="pretty",serviceName:r}=o,s={level:e,timestamp:k.default.stdTimeFunctions.isoTime,formatters:{log:p(i=>({service:r,...i}),"log"),bindings:p(i=>{let{pid:n,hostname:c,...a}=i;return a},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,k.default)({...s,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,k.default)(s)}p(_,"createLogger");var H=g(require("http")),D=g(require("fs"));function $(o){let e=[],t=/^NOMAD_PORT_(tcp|udp)_(\d+)$/i;for(let[r,s]of Object.entries(o)){let i=r.match(t);if(i&&s){let n=i[1].toLowerCase(),c=parseInt(i[2],10);c>=1&&c<=65535&&e.push({port:c,protocol:n})}}return e}p($,"parseNomadPorts");function N(){return process.env.PORT_MANAGER_SOCKET_PATH||"/usr/local/port-manager/port-manager.sock"}p(N,"getSocketPath");function A(o){try{return D.statSync(o).isSocket()}catch{return!1}}p(A,"isSocketAvailable");async function R(o,e,t,r){return new Promise((s,i)=>{let c=H.request({socketPath:o,path:t,method:e,headers:{"Content-Type":"application/json"}},a=>{let l="";a.on("data",f=>l+=f),a.on("end",()=>{if(a.statusCode&&a.statusCode>=200&&a.statusCode<300)try{s(l?JSON.parse(l):{})}catch{i(new Error(`Invalid JSON response: ${l}`))}else i(new Error(`HTTP ${a.statusCode}: ${l}`))})});c.on("error",a=>{i(new Error(`Socket request failed: ${a.message}`))}),c.setTimeout(3e4,()=>{c.destroy(),i(new Error("Request timeout (30s) - NAT operation took too long"))}),r&&c.write(JSON.stringify(r)),c.end()})}p(R,"unixSocketRequest");var b=class{static{p(this,"PortManager")}socketPath;mappings=[];logger;constructor(e,t){this.socketPath=e,this.logger=t}async openPorts(e,t,r){let s=[];for(let i of e){let n={port:i.port,protocol:i.protocol,client_id:t,alloc_id:r};this.logger.debug({port:i.port,protocol:i.protocol},"Opening port via port-manager");try{let c=await R(this.socketPath,"POST","/v1/ports",n),a={id:c.id,port:c.port,externalPort:c.external_port,externalIp:c.external_ip,protocol:c.protocol,natMethod:c.nat_method};s.push(a),this.mappings.push(a),this.logger.info({port:a.port,externalPort:a.externalPort,externalIp:a.externalIp,protocol:a.protocol,natMethod:a.natMethod},"Port opened successfully")}catch(c){let a=c instanceof Error?c.message:String(c);throw this.mappings.length>0&&(this.logger.warn({openedCount:this.mappings.length,failedPort:`${i.protocol}:${i.port}`},"Port opening failed, rolling back previously opened ports"),await this.closePorts()),new Error(`Failed to open port ${i.protocol}:${i.port}: ${a}`)}}return s}async closePorts(){for(let e of this.mappings)try{this.logger.debug({id:e.id,port:e.port},"Closing port via port-manager"),await R(this.socketPath,"DELETE",`/v1/ports/${e.id}`),this.logger.info({port:e.port,protocol:e.protocol},"Port closed successfully")}catch(t){let r=t instanceof Error?t.message:String(t);this.logger.warn({id:e.id,port:e.port,error:r},"Failed to close port (continuing cleanup)")}this.mappings=[]}getMappings(){return[...this.mappings]}async renewPorts(){for(let e of this.mappings)try{await R(this.socketPath,"POST",`/v1/ports/${e.id}/renew`,{}),this.logger.debug({id:e.id,port:e.port},"Port renewed")}catch(t){this.logger.warn({id:e.id,port:e.port,error:t instanceof Error?t.message:String(t)},"Failed to renew port")}}};var j=g(require("net")),L=g(require("dgram"));var O=class o{static{p(this,"ProbeListener")}port;protocol;logger;tcpServer;udpSocket;constructor(e,t,r){this.port=e,this.protocol=t,this.logger=r}static async start(e,t,r){let s=new o(e,t,r);return t==="tcp"?s.startTcp():s.startUdp()}startTcp(){return new Promise(e=>{let t=j.createServer(r=>{r.destroy()});t.on("error",r=>{r.code==="EADDRINUSE"?(this.logger.info({port:this.port},"Port already in use - existing listener will handle reachability check"),e({status:"port_in_use"})):(this.logger.warn({port:this.port,error:r.message,code:r.code},"Failed to bind TCP probe listener"),e({status:"bind_failed",error:r}))}),t.listen(this.port,"0.0.0.0",()=>{this.tcpServer=t,this.logger.debug({port:this.port,protocol:"tcp"},"TCP probe listener started"),e({status:"started",listener:this})})})}startUdp(){return new Promise(e=>{let t=L.createSocket("udp4");t.on("error",r=>{r.code==="EADDRINUSE"?(this.logger.info({port:this.port},"Port already in use - existing socket will handle reachability check"),e({status:"port_in_use"})):(this.logger.warn({port:this.port,error:r.message,code:r.code},"Failed to bind UDP probe socket"),e({status:"bind_failed",error:r}))}),t.on("message",(r,s)=>{t.send(r,0,r.length,s.port,s.address)}),t.bind(this.port,"0.0.0.0",()=>{this.udpSocket=t,this.logger.debug({port:this.port,protocol:"udp"},"UDP probe listener started"),e({status:"started",listener:this})})})}async stop(){if(this.tcpServer){try{await Promise.race([new Promise(t=>{this.tcpServer.close(()=>t())}),new Promise((t,r)=>setTimeout(()=>r(new Error("TCP server close timed out")),2e3))])}catch(t){this.logger.warn({port:this.port,error:t instanceof Error?t.message:String(t)},"TCP probe listener close did not complete cleanly")}this.logger.debug({port:this.port,protocol:"tcp"},"TCP probe listener stopped"),this.tcpServer=void 0}if(this.udpSocket){try{await Promise.race([new Promise(t=>{this.udpSocket.close(()=>t())}),new Promise((t,r)=>setTimeout(()=>r(new Error("UDP socket close timed out")),2e3))])}catch(t){this.logger.warn({port:this.port,error:t instanceof Error?t.message:String(t)},"UDP probe listener close did not complete cleanly")}this.logger.debug({port:this.port,protocol:"udp"},"UDP probe listener stopped"),this.udpSocket=void 0}}},x=class{static{p(this,"ProbeListenerManager")}probes=[];logger;constructor(e){this.logger=e}async startAll(e){for(let t of e){let r=await O.start(t.port,t.protocol,this.logger);switch(r.status){case"started":this.probes.push(r.listener);break;case"port_in_use":this.logger.info({port:t.port,protocol:t.protocol},"Port already in use, skipping probe");break;case"bind_failed":throw this.logger.error({port:t.port,protocol:t.protocol,error:r.error.message},"Probe listener bind failed, rolling back"),await this.stopAll(),r.error}}this.probes.length>0&&this.logger.info({count:this.probes.length,total:e.length},"Probe listeners started")}async stopAll(){let e=[...this.probes];this.probes=[];for(let t of e)try{await t.stop()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to stop probe listener (best effort)")}}};var F=require("child_process"),m=require("fs"),re=g(require("http")),oe=g(require("https")),I=g(require("path")),U=require("util");var ie=(0,U.promisify)(F.exec);function J(){return{sleep:p(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:p(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:s=2,maxDelay:i=3e4}=e,n,c=r;for(let a=1;a<=t;a++)try{return await o()}catch(l){if(n=l instanceof Error?l:new Error(String(l)),a===t)throw n;await new Promise(f=>setTimeout(f,Math.min(c,i))),c*=s}throw n},"retry"),execCommand:p(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:s=process.env}=e;try{let{stdout:i,stderr:n}=await ie(o,{timeout:t,cwd:r,env:{...process.env,...s}});return{stdout:i.trim(),stderr:n.trim(),exitCode:0}}catch(i){if(i.killed&&i.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,a=i.stderr?.trim()||"",l=a?`${a}
|
|
3
3
|
|
|
4
|
-
${
|
|
4
|
+
${c}`:c,f=[];i.stdout?.trim()&&f.push(`STDOUT: ${i.stdout.trim()}`),l&&f.push(`STDERR: ${l}`);let B=[`Command execution timed out: ${o}`,...f].join(`
|
|
5
5
|
|
|
6
|
-
`),I=new Error(V);throw I.name="CommandTimeoutError",I}return{stdout:i.stdout?.trim()||"",stderr:i.stderr?.trim()||i.message,exitCode:i.code||1}}},"execCommand"),writeFile:p(async(o,e,t={})=>{let{mode:r="0644",encoding:s="utf8"}=t;await m.promises.mkdir(R.dirname(o),{recursive:!0}),await m.promises.writeFile(o,e,{encoding:s}),await m.promises.chmod(o,r)},"writeFile"),readFile:p(async o=>await m.promises.readFile(o,"utf8"),"readFile"),fileExists:p(async o=>{try{return await m.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:p(async function o(e,t){return new Promise((r,s)=>{let i=R.dirname(t);m.promises.mkdir(i,{recursive:!0}).then(()=>{let c=(0,m.createWriteStream)(t);(e.startsWith("https:")?Z:Q).get(e,l=>{if(l.statusCode===200)l.pipe(c),c.on("finish",()=>{c.close(),r(t)});else if(l.statusCode===301||l.statusCode===302){let f=l.headers.location;f?r(o(f,t)):s(new Error(`Redirect without location header: ${l.statusCode}`))}else s(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{m.promises.unlink(t).catch(()=>{}),s(l)}),c.on("error",l=>{m.promises.unlink(t).catch(()=>{}),s(l)})}).catch(s)})},"downloadFile")}}p(j,"createUtilityContext");var F=require("http");var P=class{constructor(e,t,r,s,i){this.getStatus=r;this.executeHealthCheck=s;this.getMetrics=i;this.port=e,this.logger=t.child({component:"web-server"})}static{p(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,F.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),s=this.getStatus(),i={healthy:r,status:s.status,uptime:s.uptime,message:s.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(i))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var b=class{static{p(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;riptideStartTime;heartbeatInterval;updateInterval;echoInterval;portRenewalInterval;lastEchoHash=null;isEchoExecuting=!1;webServer;portManager;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.riptideStartTime=Date.now(),this.logger=H({serviceName:t.service.name,level:process.env.RIPTIDE_LOG_LEVEL||r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.openPorts(),this.startPortRenewal(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startEcho(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},w(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),await this.cleanupAndExit(e.exitCode)):await this.cleanupAndExit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async openPorts(){let e=_(process.env);if(e.length===0){this.logger.debug("No NOMAD_PORT_* variables found, skipping port manager");return}let t=$();if(this.logger.info({ports:e.map(i=>`${i.protocol}:${i.port}`),socketPath:t},"Found ports to open via port-manager"),!D(t))throw new y(`Port manager socket not found at ${t}. Ports ${e.map(i=>`${i.protocol}:${i.port}`).join(", ")} cannot be opened. Ensure port-manager daemon is running and socket is bind-mounted.`);this.portManager=new S(t,this.logger);let r=process.env.NOMAD_JOB_NAME,s=process.env.NOMAD_ALLOC_ID;try{let i=await this.portManager.openPorts(e,r,s);this.logger.info({count:i.length,mappings:i.map(c=>({port:c.port,externalPort:c.externalPort,externalIp:c.externalIp,protocol:c.protocol,natMethod:c.natMethod}))},"All ports opened successfully via port-manager")}catch(i){throw new y(`Failed to open ports: ${i instanceof Error?i.message:String(i)}`)}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.debug(`Executing ${e} hook`);let r=this.createHookContext(),s=Date.now();try{let i=await t(r),c=Date.now()-s;return this.logger.debug(`${e} hook completed (${c}ms)`),i}catch(i){let c=Date.now()-s;throw this.logger.error({hookName:e,duration:c,error:i instanceof Error?i.message:String(i),stack:i instanceof Error?i.stack:void 0},`${e} hook threw an exception, will not continue`),i}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){w(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),await this.cleanupAndExit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),await this.cleanupAndExit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:j()}}async cleanupAndExit(e){if(this.isShuttingDown&&process.exit(e),this.isShuttingDown=!0,this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager during error cleanup")}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Failed to close ports during error cleanup (best effort)")}process.exit(e)}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),w(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),this.cleanupAndExit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),w(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),this.cleanupAndExit(1))})}setupSignalHandlers(){let e=p(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{if(this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.webServer&&await this.webServer.stop(),this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager")}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to close ports (continuing shutdown)")}await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(s){this.logger.warn({error:s instanceof Error?s.message:String(s)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}startPortRenewal(){if(!this.portManager)return;let e=25*60*1e3,t=parseInt(process.env.PORT_RENEWAL_INTERVAL_MS||"",10)||e;this.logger.info({interval:t},"Starting port renewal"),this.portRenewalInterval=setInterval(async()=>{if(this.portManager)try{await this.portManager.renewPorts()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal cycle failed")}},t)}startEcho(){if(!this.hooks.echo){this.logger.info("No echo hook defined, skipping echo");return}if(this.config.echo?.enabled===!1){this.logger.info("Echo disabled in config, skipping...");return}let e=this.config.echo?.interval||3e3;this.logger.info({interval:e},"Starting echo"),this.echoInterval=setInterval(async()=>{if(this.isEchoExecuting){this.logger.debug("Echo still executing, skipping this interval");return}this.isEchoExecuting=!0;try{let t=await this.executeHookSafely("echo");if(!t)return;let r=JSON.stringify(t),s=(0,L.createHash)("sha256").update(r).digest("hex");s!==this.lastEchoHash&&(this.lastEchoHash=s,await this.sendEcho(t))}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Echo failed")}finally{this.isEchoExecuting=!1}},e)}async sendEcho(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,s=process.env.NOMAD_JOB_NAME;if(!t||!r||!s){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!s},"Sonar API configuration incomplete, skipping echo send");return}let i={...e,condition:e.condition??"normal"},c={entity_id:s,client_timestamp:Math.floor(Date.now()/1e3),data:i};try{this.logger.info({payload:c,sonarUrl:t},"Sending echo to Sonar API");let a=await fetch(`${t}/api/v1/echo`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(c)});if(!a.ok){let n=await a.text();this.logger.warn({status:a.status,error:n,entity_id:s},"Sonar API echo failed")}}catch(a){this.logger.warn({error:a instanceof Error?a.message:String(a)},"Failed to send echo to Sonar API, dropping")}}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,s=process.env.NOMAD_JOB_NAME;if(!t||!r||!s){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!s},"Sonar API configuration incomplete, skipping heartbeat send");return}let i=Math.floor((Date.now()-this.riptideStartTime)/1e3),c={...e},a={entity_id:s,client_timestamp:Math.floor(Date.now()/1e3),riptide_uptime:i,data:c};try{this.logger.info({payload:a,sonarUrl:t,nomadJobId:s},"Sending heartbeat to Sonar API");let n=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(a)});if(!n.ok){let l=await n.text();this.logger.warn({status:n.status,error:l,entity_id:s},"Sonar API heartbeat failed")}}catch(n){this.logger.error({error:n instanceof Error?n.message:String(n),errorType:n?.constructor?.name,errorCause:n instanceof Error&&"cause"in n?n.cause:void 0,stack:n instanceof Error?n.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:s},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new P(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>{if(this.portManager){try{let r=await this.portManager.checkPortHealth();if(!r.healthy)return this.logger.warn({ports:r.ports},"Port health check failed - ports unreachable"),!1}catch(r){return this.logger.error({error:r instanceof Error?r.message:String(r)},"Failed to check port health"),!1}this.portManager.renewPorts().catch(r=>{this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal during health check failed")})}return await this.executeHookSafely("health")===!0},async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=p(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var h=g(require("fs/promises")),d=g(require("path"));var O=class{constructor(e){this.logger=e}static{p(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:s="basic",description:i}=e,c=d.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${s}`),this.logger.info(`Location: ${c}`),await this.createDirectoryStructure(c),await this.createPackageJson(c,t,i),await this.createRiptideConfig(c,t,i),await this.createTsConfig(c),await this.createTsupConfig(c),await this.createHooks(c,s),await this.createDockerfile(c,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,c)}async createDirectoryStructure(e){await h.mkdir(d.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let s=await this.checkIfInWorkspace(e),i=s?"workspace:*":await this.getRiptideVersion(),c=s?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,a={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":c,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":i},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await h.writeFile(d.join(e,"package.json"),JSON.stringify(a,null,2))}async createRiptideConfig(e,t,r){let s={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await h.writeFile(d.join(e,"riptide.config.json"),JSON.stringify(s,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await h.writeFile(d.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await h.writeFile(d.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
|
|
6
|
+
`),M=new Error(B);throw M.name="CommandTimeoutError",M}return{stdout:i.stdout?.trim()||"",stderr:i.stderr?.trim()||i.message,exitCode:i.code||1}}},"execCommand"),writeFile:p(async(o,e,t={})=>{let{mode:r="0644",encoding:s="utf8"}=t;await m.promises.mkdir(I.dirname(o),{recursive:!0}),await m.promises.writeFile(o,e,{encoding:s}),await m.promises.chmod(o,r)},"writeFile"),readFile:p(async o=>await m.promises.readFile(o,"utf8"),"readFile"),fileExists:p(async o=>{try{return await m.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:p(async function o(e,t){return new Promise((r,s)=>{let i=I.dirname(t);m.promises.mkdir(i,{recursive:!0}).then(()=>{let n=(0,m.createWriteStream)(t);(e.startsWith("https:")?oe:re).get(e,l=>{if(l.statusCode===200)l.pipe(n),n.on("finish",()=>{n.close(),r(t)});else if(l.statusCode===301||l.statusCode===302){let f=l.headers.location;f?r(o(f,t)):s(new Error(`Redirect without location header: ${l.statusCode}`))}else s(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{m.promises.unlink(t).catch(()=>{}),s(l)}),n.on("error",l=>{m.promises.unlink(t).catch(()=>{}),s(l)})}).catch(s)})},"downloadFile")}}p(J,"createUtilityContext");var q=require("http");var S=class{constructor(e,t,r,s,i){this.getStatus=r;this.executeHealthCheck=s;this.getMetrics=i;this.port=e,this.logger=t.child({component:"web-server"})}static{p(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,q.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),s=this.getStatus(),i={healthy:r,status:s.status,uptime:s.uptime,message:s.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(i))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var P=class{static{p(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;riptideStartTime;heartbeatInterval;updateInterval;echoInterval;portRenewalInterval;lastEchoHash=null;isEchoExecuting=!1;webServer;portManager;probeListenerManager;externalIp;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.riptideStartTime=Date.now(),this.logger=_({serviceName:t.service.name,level:process.env.RIPTIDE_LOG_LEVEL||r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.openPorts(),this.startPortRenewal(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startEcho(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},w(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),await this.cleanupAndExit(e.exitCode)):await this.cleanupAndExit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async openPorts(){let e=$(process.env);if(e.length===0){this.logger.debug("No NOMAD_PORT_* variables found, skipping port manager");return}let t=N();if(this.logger.info({ports:e.map(i=>`${i.protocol}:${i.port}`),socketPath:t},"Found ports to open via port-manager"),!A(t))throw new y(`Port manager socket not found at ${t}. Ports ${e.map(i=>`${i.protocol}:${i.port}`).join(", ")} cannot be opened. Ensure port-manager daemon is running and socket is bind-mounted.`);this.probeListenerManager=new x(this.logger);try{await this.probeListenerManager.startAll(e)}catch(i){throw new y(`Failed to start probe listeners: ${i instanceof Error?i.message:String(i)}`)}this.portManager=new b(t,this.logger);let r=process.env.NOMAD_JOB_NAME,s=process.env.NOMAD_ALLOC_ID;try{let i=await this.portManager.openPorts(e,r,s);this.logger.info({count:i.length,mappings:i.map(n=>({port:n.port,externalPort:n.externalPort,externalIp:n.externalIp,protocol:n.protocol,natMethod:n.natMethod}))},"All ports opened successfully via port-manager"),this.externalIp=i.find(n=>n.externalIp)?.externalIp??void 0}catch(i){throw await this.probeListenerManager.stopAll(),new y(`Failed to open ports: ${i instanceof Error?i.message:String(i)}`)}await this.probeListenerManager.stopAll(),this.logger.debug("Probe listeners released, ports ready for service")}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.debug(`Executing ${e} hook`);let r=this.createHookContext(),s=Date.now();try{let i=await t(r),n=Date.now()-s;return this.logger.debug(`${e} hook completed (${n}ms)`),i}catch(i){let n=Date.now()-s;throw this.logger.error({hookName:e,duration:n,error:i instanceof Error?i.message:String(i),stack:i instanceof Error?i.stack:void 0},`${e} hook threw an exception, will not continue`),i}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){w(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),await this.cleanupAndExit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),await this.cleanupAndExit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:J(),externalIp:this.externalIp}}async cleanupAndExit(e){if(this.isShuttingDown&&process.exit(e),this.isShuttingDown=!0,this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.probeListenerManager)try{await this.probeListenerManager.stopAll()}catch{}if(this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager during error cleanup")}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Failed to close ports during error cleanup (best effort)")}process.exit(e)}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),w(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),this.cleanupAndExit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),w(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),this.cleanupAndExit(1))})}setupSignalHandlers(){let e=p(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{if(this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.webServer&&await this.webServer.stop(),this.probeListenerManager)try{await this.probeListenerManager.stopAll()}catch{}if(this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager")}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to close ports (continuing shutdown)")}await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(s){this.logger.warn({error:s instanceof Error?s.message:String(s)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}startPortRenewal(){if(!this.portManager)return;let e=25*60*1e3,t=parseInt(process.env.PORT_RENEWAL_INTERVAL_MS||"",10)||e;this.logger.info({interval:t},"Starting port renewal"),this.portRenewalInterval=setInterval(async()=>{if(this.portManager)try{await this.portManager.renewPorts()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal cycle failed")}},t)}startEcho(){if(!this.hooks.echo){this.logger.info("No echo hook defined, skipping echo");return}if(this.config.echo?.enabled===!1){this.logger.info("Echo disabled in config, skipping...");return}let e=this.config.echo?.interval||3e3;this.logger.info({interval:e},"Starting echo"),this.echoInterval=setInterval(async()=>{if(this.isEchoExecuting){this.logger.debug("Echo still executing, skipping this interval");return}this.isEchoExecuting=!0;try{let t=await this.executeHookSafely("echo");if(!t)return;let r=JSON.stringify(t),s=(0,W.createHash)("sha256").update(r).digest("hex");s!==this.lastEchoHash&&(this.lastEchoHash=s,await this.sendEcho(t))}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Echo failed")}finally{this.isEchoExecuting=!1}},e)}async sendEcho(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,s=process.env.NOMAD_JOB_NAME;if(!t||!r||!s){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!s},"Sonar API configuration incomplete, skipping echo send");return}let i={...e,condition:e.condition??"normal"},n={entity_id:s,client_timestamp:Math.floor(Date.now()/1e3),data:i};try{this.logger.info({payload:n,sonarUrl:t},"Sending echo to Sonar API");let c=await fetch(`${t}/api/v1/echo`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(n)});if(!c.ok){let a=await c.text();this.logger.warn({status:c.status,error:a,entity_id:s},"Sonar API echo failed")}}catch(c){this.logger.warn({error:c instanceof Error?c.message:String(c)},"Failed to send echo to Sonar API, dropping")}}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,s=process.env.NOMAD_JOB_NAME;if(!t||!r||!s){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!s},"Sonar API configuration incomplete, skipping heartbeat send");return}let i=Math.floor((Date.now()-this.riptideStartTime)/1e3),n={...e},c={entity_id:s,client_timestamp:Math.floor(Date.now()/1e3),riptide_uptime:i,data:n};try{this.logger.info({payload:c,sonarUrl:t,nomadJobId:s},"Sending heartbeat to Sonar API");let a=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(c)});if(!a.ok){let l=await a.text();this.logger.warn({status:a.status,error:l,entity_id:s},"Sonar API heartbeat failed")}}catch(a){this.logger.error({error:a instanceof Error?a.message:String(a),errorType:a?.constructor?.name,errorCause:a instanceof Error&&"cause"in a?a.cause:void 0,stack:a instanceof Error?a.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:s},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new S(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>(this.portManager&&this.portManager.renewPorts().catch(r=>{this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal during health check failed")}),await this.executeHookSafely("health")===!0),async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=p(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var h=g(require("fs/promises")),d=g(require("path"));var T=class{constructor(e){this.logger=e}static{p(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:s="basic",description:i}=e,n=d.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${s}`),this.logger.info(`Location: ${n}`),await this.createDirectoryStructure(n),await this.createPackageJson(n,t,i),await this.createRiptideConfig(n,t,i),await this.createTsConfig(n),await this.createTsupConfig(n),await this.createHooks(n,s),await this.createDockerfile(n,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,n)}async createDirectoryStructure(e){await h.mkdir(d.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let s=await this.checkIfInWorkspace(e),i=s?"workspace:*":await this.getRiptideVersion(),n=s?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":n,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":i},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await h.writeFile(d.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,r){let s={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await h.writeFile(d.join(e,"riptide.config.json"),JSON.stringify(s,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await h.writeFile(d.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await h.writeFile(d.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
9
|
entry: ['src/hooks.ts'],
|
|
@@ -396,7 +396,7 @@ Next steps:
|
|
|
396
396
|
|
|
397
397
|
NOTE: Riptide is a node application. If your base image does
|
|
398
398
|
not have node installed (v22+), install it in the Dockerfile.
|
|
399
|
-
`)}async checkIfInWorkspace(e){let t=d.resolve(e),r=d.parse(t).root;for(;t!==r;){try{return await h.access(d.join(t,"pnpm-workspace.yaml")),!0}catch{}t=d.dirname(t)}return!1}async getRiptideVersion(){try{let e=[d.join(__dirname,"..","package.json"),d.join(__dirname,"..","..","package.json"),d.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),d.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await h.readFile(t,"utf-8"),s=JSON.parse(r);if(s.name==="@deeep-network/riptide")return`^${s.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function
|
|
399
|
+
`)}async checkIfInWorkspace(e){let t=d.resolve(e),r=d.parse(t).root;for(;t!==r;){try{return await h.access(d.join(t,"pnpm-workspace.yaml")),!0}catch{}t=d.dirname(t)}return!1}async getRiptideVersion(){try{let e=[d.join(__dirname,"..","package.json"),d.join(__dirname,"..","..","package.json"),d.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),d.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await h.readFile(t,"utf-8"),s=JSON.parse(r);if(s.name==="@deeep-network/riptide")return`^${s.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function V(o,e,t={}){await new T(o).scaffold({serviceName:e,...t})}p(V,"initService");var u=(0,G.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function se(){try{let o=process.argv[2];if(o==="--help"||o==="-h"||o==="help"){Y();return}if(o==="--version"||o==="-v"||o==="version"){await ne();return}let e=v("--config")||v("-c")||"./riptide.config.json",t=v("--hooks")||v("-h")||"./hooks.js";switch(o){case"init":await le();break;case"start":await K(e,t);break;case"validate":await ae(e,t);break;case"health":await ce();break;case"status":await pe();break;case"verify":break;default:o?(u.error(`Unknown command: ${o}`),Y(),process.exit(1)):await K(e,t)}}catch(o){u.error({error:o},"CLI command failed"),process.exit(1)}}p(se,"main");function v(o){let e=process.argv.indexOf(o);if(e>=0&&e+1<process.argv.length)return process.argv[e+1]}p(v,"getArgValue");function Y(){console.log(`
|
|
400
400
|
Riptide - Self-contained service lifecycle management
|
|
401
401
|
|
|
402
402
|
USAGE:
|
|
@@ -427,4 +427,4 @@ EXAMPLES:
|
|
|
427
427
|
npx riptide start # Start service
|
|
428
428
|
npx riptide validate # Validate config and hooks
|
|
429
429
|
npx riptide health # Check service health
|
|
430
|
-
`)}p(
|
|
430
|
+
`)}p(Y,"showHelp");async function ne(){try{let o=await import("fs/promises"),e=await import("path"),t=[e.resolve(__dirname,"../package.json"),e.resolve(__dirname,"./package.json"),"/riptide-runtime/package.json",e.resolve(process.cwd(),"package.json")];for(let r of t)try{let s=await o.readFile(r,"utf-8"),i=JSON.parse(s);if(i.name==="@deeep-network/riptide"){console.log(`@deeep-network/riptide v${i.version}`);return}}catch{continue}console.log("@deeep-network/riptide (version unknown)")}catch{console.log("@deeep-network/riptide (version unknown)")}}p(ne,"showVersion");async function K(o,e){try{let t=await import("fs/promises"),r=await import("path"),s=await t.readFile(o,"utf-8"),i=JSON.parse(s),c=await import(r.resolve(process.cwd(),e)),a=c.default||c;await new P(a,i,{}).start()}catch(t){u.error({error:t},"Failed to start service"),process.exit(1)}}p(K,"startService");async function ae(o,e){u.info("Validating riptide configuration and hooks...");try{let t=await import("fs/promises"),r=await import("path"),s=await t.readFile(o,"utf-8"),i=JSON.parse(s);u.info({config:i},"Configuration loaded successfully");let c=await import(r.resolve(process.cwd(),e)),a=c.default||c;u.info({availableHooks:Object.keys(a)},"Hooks loaded successfully"),u.info("\u2705 Validation completed successfully"),process.exit(0)}catch(t){u.error({error:t},"\u274C Validation failed"),process.exit(1)}}p(ae,"validateService");async function ce(){try{let e=await(await fetch("http://localhost:3000/health")).json();console.log("Health Status:",JSON.stringify(e,null,2)),e.status==="healthy"?process.exit(0):process.exit(1)}catch(o){u.error({error:o},"Failed to check health"),process.exit(1)}}p(ce,"checkHealth");async function pe(){try{let e=await(await fetch("http://localhost:3000/status")).json();console.log("Service Status:",JSON.stringify(e,null,2))}catch(o){u.error({error:o},"Failed to get status"),process.exit(1)}}p(pe,"showStatus");async function le(){let o=process.argv[3];o||(u.error("Service name is required"),console.log("Usage: npx riptide init <service-name>"),process.exit(1)),/^[a-z0-9-]+$/.test(o)||(u.error("Service name must contain only lowercase letters, numbers, and hyphens"),process.exit(1));let e=v("--template")||"basic",t=v("--path")||".",r=v("--description"),s=["basic","with-secrets","with-process","with-metrics"];s.includes(e)||(u.error(`Invalid template: ${e}`),console.log(`Valid templates: ${s.join(", ")}`),process.exit(1));try{await V(u,o,{targetPath:t,template:e,description:r}),process.exit(0)}catch(i){u.error({error:i},"Failed to initialize service"),process.exit(1)}}p(le,"initServiceCommand");se();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ interface HookContext {
|
|
|
5
5
|
logger: Logger;
|
|
6
6
|
env: NodeJS.ProcessEnv;
|
|
7
7
|
utils: UtilityContext;
|
|
8
|
+
externalIp?: string;
|
|
8
9
|
[key: string]: any;
|
|
9
10
|
}
|
|
10
11
|
interface UtilityContext {
|
|
@@ -56,6 +57,7 @@ declare enum WorkloadCondition {
|
|
|
56
57
|
interface EchoResult {
|
|
57
58
|
condition?: WorkloadCondition;
|
|
58
59
|
message?: string;
|
|
60
|
+
swap_ready?: boolean;
|
|
59
61
|
[key: string]: unknown;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
@@ -141,6 +143,8 @@ declare class RiptideEntrypoint {
|
|
|
141
143
|
private isEchoExecuting;
|
|
142
144
|
private webServer?;
|
|
143
145
|
private portManager?;
|
|
146
|
+
private probeListenerManager?;
|
|
147
|
+
private externalIp?;
|
|
144
148
|
constructor(hooks: HookModule, config: ServiceConfig, options?: RiptideEntrypointOptions);
|
|
145
149
|
start(): Promise<void>;
|
|
146
150
|
private processSecrets;
|
|
@@ -196,24 +200,6 @@ interface NomadPort {
|
|
|
196
200
|
port: number;
|
|
197
201
|
protocol: 'tcp' | 'udp';
|
|
198
202
|
}
|
|
199
|
-
interface PortMapping {
|
|
200
|
-
id: string;
|
|
201
|
-
port: number;
|
|
202
|
-
externalPort: number;
|
|
203
|
-
externalIp: string | null;
|
|
204
|
-
protocol: 'tcp' | 'udp';
|
|
205
|
-
natMethod: string;
|
|
206
|
-
}
|
|
207
|
-
interface PortHealthResponse {
|
|
208
|
-
healthy: boolean;
|
|
209
|
-
external_ip: string | null;
|
|
210
|
-
checked_at: string;
|
|
211
|
-
ports: Record<string, {
|
|
212
|
-
reachable: boolean;
|
|
213
|
-
nat_method: string;
|
|
214
|
-
last_check: string;
|
|
215
|
-
}>;
|
|
216
|
-
}
|
|
217
203
|
/**
|
|
218
204
|
* Parse NOMAD_PORT_* environment variables to discover ports to open.
|
|
219
205
|
*
|
|
@@ -223,59 +209,6 @@ interface PortHealthResponse {
|
|
|
223
209
|
* NOMAD_PORT_udp_5000=5000 -> { port: 5000, protocol: 'udp' }
|
|
224
210
|
*/
|
|
225
211
|
declare function parseNomadPorts(env: NodeJS.ProcessEnv): NomadPort[];
|
|
226
|
-
/**
|
|
227
|
-
* Get the port-manager socket path from environment or default.
|
|
228
|
-
*/
|
|
229
|
-
declare function getSocketPath(): string;
|
|
230
|
-
/**
|
|
231
|
-
* Check if the port-manager socket exists.
|
|
232
|
-
*/
|
|
233
|
-
declare function isSocketAvailable(socketPath: string): boolean;
|
|
234
|
-
/**
|
|
235
|
-
* Manages port mappings through the port-manager daemon.
|
|
236
|
-
*/
|
|
237
|
-
declare class PortManager {
|
|
238
|
-
private socketPath;
|
|
239
|
-
private mappings;
|
|
240
|
-
private logger;
|
|
241
|
-
constructor(socketPath: string, logger: Logger);
|
|
242
|
-
/**
|
|
243
|
-
* Open all specified ports via port-manager.
|
|
244
|
-
*
|
|
245
|
-
* @throws Error if any port fails to open
|
|
246
|
-
*/
|
|
247
|
-
openPorts(ports: NomadPort[], clientId?: string, allocId?: string): Promise<PortMapping[]>;
|
|
248
|
-
/**
|
|
249
|
-
* Close all previously opened ports.
|
|
250
|
-
* Errors are logged but not thrown (best-effort cleanup).
|
|
251
|
-
*/
|
|
252
|
-
closePorts(): Promise<void>;
|
|
253
|
-
/**
|
|
254
|
-
* Get the list of currently opened mappings.
|
|
255
|
-
*/
|
|
256
|
-
getMappings(): PortMapping[];
|
|
257
|
-
/**
|
|
258
|
-
* Renew all open port mappings to prevent stale cleanup.
|
|
259
|
-
*
|
|
260
|
-
* Calls POST /v1/ports/{id}/renew for each mapping, which updates
|
|
261
|
-
* the last_activity timestamp in port-manager. Without periodic
|
|
262
|
-
* renewal, mappings are marked stale and removed after the
|
|
263
|
-
* SONAR_STALE_MAPPING_TIMEOUT_SECS window (default 60 minutes).
|
|
264
|
-
*
|
|
265
|
-
* Errors are logged but not thrown (best-effort renewal).
|
|
266
|
-
*/
|
|
267
|
-
renewPorts(): Promise<void>;
|
|
268
|
-
/**
|
|
269
|
-
* Check the health of all port mappings via port-manager.
|
|
270
|
-
*
|
|
271
|
-
* This calls the port-manager's /v1/health endpoint which
|
|
272
|
-
* verifies all ports are reachable from the internet via Sonar API.
|
|
273
|
-
*
|
|
274
|
-
* @returns PortHealthResponse with overall health and per-port status
|
|
275
|
-
* @throws Error if port-manager is unavailable or health check fails
|
|
276
|
-
*/
|
|
277
|
-
checkPortHealth(): Promise<PortHealthResponse>;
|
|
278
|
-
}
|
|
279
212
|
|
|
280
213
|
interface ScaffoldOptions {
|
|
281
214
|
serviceName: string;
|
|
@@ -378,4 +311,4 @@ declare function isAlreadyRunningError(error: unknown): error is AlreadyRunningE
|
|
|
378
311
|
declare function isUserConfigNeededError(error: unknown): error is UserConfigNeededError;
|
|
379
312
|
declare function isPortOpenError(error: unknown): error is PortOpenError;
|
|
380
313
|
|
|
381
|
-
export { AlreadyRunningError, DiagnoseRequiredError, type EchoResult, type ExecOptions, type ExecResult, type HeartbeatResult, type HookContext, type HookModule, InvalidSecretError, MissingSecretError, type NomadPort,
|
|
314
|
+
export { AlreadyRunningError, DiagnoseRequiredError, type EchoResult, type ExecOptions, type ExecResult, type HeartbeatResult, type HookContext, type HookModule, InvalidSecretError, MissingSecretError, type NomadPort, PortOpenError, type RetryOptions, RiptideEntrypoint, type RiptideEntrypointOptions, RiptideError, type ScaffoldOptions, type ServiceConfig, type ServiceMetrics, ServiceScaffolder, type ServiceStatus, UserConfigNeededError, type UtilityContext, WorkloadCondition, type WriteFileOptions, createChildLogger, createLogger, createUtilityContext, expandEnvironmentVariables, getDefaultConfig, initService, isAlreadyRunningError, isDiagnoseRequiredError, isInvalidSecretError, isMissingSecretError, isPortOpenError, isRiptideError, isUserConfigNeededError, loadConfig, loadHooks, mergeConfigs, parseEnvironmentVariables, parseNomadPorts, redactSecret, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"use strict";var de=Object.create;var k=Object.defineProperty;var ge=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var he=Object.getPrototypeOf,fe=Object.prototype.hasOwnProperty;var a=(o,e)=>k(o,"name",{value:e,configurable:!0});var me=(o,e)=>{for(var t in e)k(o,t,{get:e[t],enumerable:!0})},L=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ue(e))!fe.call(o,i)&&i!==t&&k(o,i,{get:()=>e[i],enumerable:!(r=ge(e,i))||r.enumerable});return o};var m=(o,e,t)=>(t=o!=null?de(he(o)):{},L(e||!o||!o.__esModule?k(t,"default",{value:o,enumerable:!0}):t,o)),ve=o=>L(k({},"__esModule",{value:!0}),o);var Ee={};me(Ee,{AlreadyRunningError:()=>b,DiagnoseRequiredError:()=>E,InvalidSecretError:()=>P,MissingSecretError:()=>S,PortManager:()=>w,PortOpenError:()=>v,RiptideEntrypoint:()=>A,RiptideError:()=>f,ServiceScaffolder:()=>x,UserConfigNeededError:()=>C,WorkloadCondition:()=>$,createChildLogger:()=>G,createLogger:()=>O,createUtilityContext:()=>_,expandEnvironmentVariables:()=>D,getDefaultConfig:()=>ae,getSocketPath:()=>M,initService:()=>pe,isAlreadyRunningError:()=>Y,isDiagnoseRequiredError:()=>J,isInvalidSecretError:()=>W,isMissingSecretError:()=>q,isPortOpenError:()=>K,isRiptideError:()=>y,isSocketAvailable:()=>T,isUserConfigNeededError:()=>V,loadConfig:()=>ne,loadHooks:()=>se,mergeConfigs:()=>ce,parseEnvironmentVariables:()=>ee,parseNomadPorts:()=>H,redactSecret:()=>Z,validateConfig:()=>j});module.exports=ve(Ee);var re=require("crypto");var f=class o extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,o.prototype)}},E=class o extends f{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,o.prototype)}},S=class o extends f{static{a(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,o.prototype)}},P=class o extends f{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,o.prototype)}},b=class o extends f{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,o.prototype)}},C=class o extends f{static{a(this,"UserConfigNeededError")}exitCode=7;constructor(e){super(e),this.name="UserConfigNeededError",Object.setPrototypeOf(this,o.prototype)}},v=class o extends f{static{a(this,"PortOpenError")}exitCode=8;constructor(e){super(e),this.name="PortOpenError",Object.setPrototypeOf(this,o.prototype)}};function y(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}a(y,"isRiptideError");function J(o){return o instanceof Error&&o.name==="DiagnoseRequiredError"&&o.exitCode===3}a(J,"isDiagnoseRequiredError");function q(o){return o instanceof Error&&o.name==="MissingSecretError"&&o.exitCode===4}a(q,"isMissingSecretError");function W(o){return o instanceof Error&&o.name==="InvalidSecretError"&&o.exitCode===5}a(W,"isInvalidSecretError");function Y(o){return o instanceof Error&&o.name==="AlreadyRunningError"&&o.exitCode===6}a(Y,"isAlreadyRunningError");function V(o){return o instanceof Error&&o.name==="UserConfigNeededError"&&o.exitCode===7}a(V,"isUserConfigNeededError");function K(o){return o instanceof Error&&o.name==="PortOpenError"&&o.exitCode===8}a(K,"isPortOpenError");var R=m(require("pino"));function O(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:R.default.stdTimeFunctions.isoTime,formatters:{log:a(n=>({service:r,...n}),"log"),bindings:a(n=>{let{pid:s,hostname:p,...c}=n;return c},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,R.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,R.default)(i)}a(O,"createLogger");function G(o,e){return o.child(e)}a(G,"createChildLogger");var B=m(require("http")),z=m(require("fs"));function H(o){let e=[],t=/^NOMAD_PORT_(tcp|udp)_(\d+)$/i;for(let[r,i]of Object.entries(o)){let n=r.match(t);if(n&&i){let s=n[1].toLowerCase(),p=parseInt(n[2],10);p>=1&&p<=65535&&e.push({port:p,protocol:s})}}return e}a(H,"parseNomadPorts");function M(){return process.env.PORT_MANAGER_SOCKET_PATH||"/usr/local/port-manager/port-manager.sock"}a(M,"getSocketPath");function T(o){try{return z.statSync(o).isSocket()}catch{return!1}}a(T,"isSocketAvailable");async function I(o,e,t,r){return new Promise((i,n)=>{let p=B.request({socketPath:o,path:t,method:e,headers:{"Content-Type":"application/json"}},c=>{let l="";c.on("data",u=>l+=u),c.on("end",()=>{if(c.statusCode&&c.statusCode>=200&&c.statusCode<300)try{i(l?JSON.parse(l):{})}catch{n(new Error(`Invalid JSON response: ${l}`))}else n(new Error(`HTTP ${c.statusCode}: ${l}`))})});p.on("error",c=>{n(new Error(`Socket request failed: ${c.message}`))}),p.setTimeout(3e4,()=>{p.destroy(),n(new Error("Request timeout (30s) - NAT operation took too long"))}),r&&p.write(JSON.stringify(r)),p.end()})}a(I,"unixSocketRequest");var w=class{static{a(this,"PortManager")}socketPath;mappings=[];logger;constructor(e,t){this.socketPath=e,this.logger=t}async openPorts(e,t,r){let i=[];for(let n of e){let s={port:n.port,protocol:n.protocol,client_id:t,alloc_id:r};this.logger.debug({port:n.port,protocol:n.protocol},"Opening port via port-manager");try{let p=await I(this.socketPath,"POST","/v1/ports",s),c={id:p.id,port:p.port,externalPort:p.external_port,externalIp:p.external_ip,protocol:p.protocol,natMethod:p.nat_method};i.push(c),this.mappings.push(c),this.logger.info({port:c.port,externalPort:c.externalPort,externalIp:c.externalIp,protocol:c.protocol,natMethod:c.natMethod},"Port opened successfully")}catch(p){let c=p instanceof Error?p.message:String(p);throw this.mappings.length>0&&(this.logger.warn({openedCount:this.mappings.length,failedPort:`${n.protocol}:${n.port}`},"Port opening failed, rolling back previously opened ports"),await this.closePorts()),new Error(`Failed to open port ${n.protocol}:${n.port}: ${c}`)}}return i}async closePorts(){for(let e of this.mappings)try{this.logger.debug({id:e.id,port:e.port},"Closing port via port-manager"),await I(this.socketPath,"DELETE",`/v1/ports/${e.id}`),this.logger.info({port:e.port,protocol:e.protocol},"Port closed successfully")}catch(t){let r=t instanceof Error?t.message:String(t);this.logger.warn({id:e.id,port:e.port,error:r},"Failed to close port (continuing cleanup)")}this.mappings=[]}getMappings(){return[...this.mappings]}async renewPorts(){for(let e of this.mappings)try{await I(this.socketPath,"POST",`/v1/ports/${e.id}/renew`,{}),this.logger.debug({id:e.id,port:e.port},"Port renewed")}catch(t){this.logger.warn({id:e.id,port:e.port,error:t instanceof Error?t.message:String(t)},"Failed to renew port")}}async checkPortHealth(){this.logger.debug("Checking port health via port-manager");let e=await I(this.socketPath,"GET","/v1/health");return this.logger.debug({healthy:e.healthy,portCount:Object.keys(e.ports).length},"Port health check completed"),e}};var $=(r=>(r.Normal="normal",r.Degraded="degraded",r.Unknown="unknown",r))($||{});var X=require("child_process"),h=require("fs"),ye=m(require("http")),we=m(require("https")),F=m(require("path")),Q=require("util");var ke=(0,Q.promisify)(X.exec);function _(){return{sleep:a(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:a(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:n=3e4}=e,s,p=r;for(let c=1;c<=t;c++)try{return await o()}catch(l){if(s=l instanceof Error?l:new Error(String(l)),c===t)throw s;await new Promise(u=>setTimeout(u,Math.min(p,n))),p*=i}throw s},"retry"),execCommand:a(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:n,stderr:s}=await ke(o,{timeout:t,cwd:r,env:{...process.env,...i}});return{stdout:n.trim(),stderr:s.trim(),exitCode:0}}catch(n){if(n.killed&&n.signal==="SIGTERM"){let p=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,c=n.stderr?.trim()||"",l=c?`${c}
|
|
1
|
+
"use strict";var fe=Object.create;var w=Object.defineProperty;var me=Object.getOwnPropertyDescriptor;var ve=Object.getOwnPropertyNames;var ye=Object.getPrototypeOf,we=Object.prototype.hasOwnProperty;var a=(o,e)=>w(o,"name",{value:e,configurable:!0});var xe=(o,e)=>{for(var t in e)w(o,t,{get:e[t],enumerable:!0})},F=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ve(e))!we.call(o,i)&&i!==t&&w(o,i,{get:()=>e[i],enumerable:!(r=me(e,i))||r.enumerable});return o};var h=(o,e,t)=>(t=o!=null?fe(ye(o)):{},F(e||!o||!o.__esModule?w(t,"default",{value:o,enumerable:!0}):t,o)),ke=o=>F(w({},"__esModule",{value:!0}),o);var Ce={};xe(Ce,{AlreadyRunningError:()=>E,DiagnoseRequiredError:()=>k,InvalidSecretError:()=>S,MissingSecretError:()=>b,PortOpenError:()=>v,RiptideEntrypoint:()=>D,RiptideError:()=>m,ServiceScaffolder:()=>x,UserConfigNeededError:()=>P,WorkloadCondition:()=>T,createChildLogger:()=>G,createLogger:()=>R,createUtilityContext:()=>_,expandEnvironmentVariables:()=>H,getDefaultConfig:()=>de,initService:()=>ue,isAlreadyRunningError:()=>Y,isDiagnoseRequiredError:()=>J,isInvalidSecretError:()=>W,isMissingSecretError:()=>q,isPortOpenError:()=>K,isRiptideError:()=>y,isUserConfigNeededError:()=>V,loadConfig:()=>pe,loadHooks:()=>le,mergeConfigs:()=>ge,parseEnvironmentVariables:()=>ie,parseNomadPorts:()=>I,redactSecret:()=>oe,validateConfig:()=>N});module.exports=ke(Ce);var ne=require("crypto");var m=class o extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,o.prototype)}},k=class o extends m{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,o.prototype)}},b=class o extends m{static{a(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,o.prototype)}},S=class o extends m{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,o.prototype)}},E=class o extends m{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,o.prototype)}},P=class o extends m{static{a(this,"UserConfigNeededError")}exitCode=7;constructor(e){super(e),this.name="UserConfigNeededError",Object.setPrototypeOf(this,o.prototype)}},v=class o extends m{static{a(this,"PortOpenError")}exitCode=8;constructor(e){super(e),this.name="PortOpenError",Object.setPrototypeOf(this,o.prototype)}};function y(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}a(y,"isRiptideError");function J(o){return o instanceof Error&&o.name==="DiagnoseRequiredError"&&o.exitCode===3}a(J,"isDiagnoseRequiredError");function q(o){return o instanceof Error&&o.name==="MissingSecretError"&&o.exitCode===4}a(q,"isMissingSecretError");function W(o){return o instanceof Error&&o.name==="InvalidSecretError"&&o.exitCode===5}a(W,"isInvalidSecretError");function Y(o){return o instanceof Error&&o.name==="AlreadyRunningError"&&o.exitCode===6}a(Y,"isAlreadyRunningError");function V(o){return o instanceof Error&&o.name==="UserConfigNeededError"&&o.exitCode===7}a(V,"isUserConfigNeededError");function K(o){return o instanceof Error&&o.name==="PortOpenError"&&o.exitCode===8}a(K,"isPortOpenError");var C=h(require("pino"));function R(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:C.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:r,...s}),"log"),bindings:a(s=>{let{pid:n,hostname:p,...c}=s;return c},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,C.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,C.default)(i)}a(R,"createLogger");function G(o,e){return o.child(e)}a(G,"createChildLogger");var B=h(require("http")),z=h(require("fs"));function I(o){let e=[],t=/^NOMAD_PORT_(tcp|udp)_(\d+)$/i;for(let[r,i]of Object.entries(o)){let s=r.match(t);if(s&&i){let n=s[1].toLowerCase(),p=parseInt(s[2],10);p>=1&&p<=65535&&e.push({port:p,protocol:n})}}return e}a(I,"parseNomadPorts");function X(){return process.env.PORT_MANAGER_SOCKET_PATH||"/usr/local/port-manager/port-manager.sock"}a(X,"getSocketPath");function Q(o){try{return z.statSync(o).isSocket()}catch{return!1}}a(Q,"isSocketAvailable");async function A(o,e,t,r){return new Promise((i,s)=>{let p=B.request({socketPath:o,path:t,method:e,headers:{"Content-Type":"application/json"}},c=>{let l="";c.on("data",u=>l+=u),c.on("end",()=>{if(c.statusCode&&c.statusCode>=200&&c.statusCode<300)try{i(l?JSON.parse(l):{})}catch{s(new Error(`Invalid JSON response: ${l}`))}else s(new Error(`HTTP ${c.statusCode}: ${l}`))})});p.on("error",c=>{s(new Error(`Socket request failed: ${c.message}`))}),p.setTimeout(3e4,()=>{p.destroy(),s(new Error("Request timeout (30s) - NAT operation took too long"))}),r&&p.write(JSON.stringify(r)),p.end()})}a(A,"unixSocketRequest");var O=class{static{a(this,"PortManager")}socketPath;mappings=[];logger;constructor(e,t){this.socketPath=e,this.logger=t}async openPorts(e,t,r){let i=[];for(let s of e){let n={port:s.port,protocol:s.protocol,client_id:t,alloc_id:r};this.logger.debug({port:s.port,protocol:s.protocol},"Opening port via port-manager");try{let p=await A(this.socketPath,"POST","/v1/ports",n),c={id:p.id,port:p.port,externalPort:p.external_port,externalIp:p.external_ip,protocol:p.protocol,natMethod:p.nat_method};i.push(c),this.mappings.push(c),this.logger.info({port:c.port,externalPort:c.externalPort,externalIp:c.externalIp,protocol:c.protocol,natMethod:c.natMethod},"Port opened successfully")}catch(p){let c=p instanceof Error?p.message:String(p);throw this.mappings.length>0&&(this.logger.warn({openedCount:this.mappings.length,failedPort:`${s.protocol}:${s.port}`},"Port opening failed, rolling back previously opened ports"),await this.closePorts()),new Error(`Failed to open port ${s.protocol}:${s.port}: ${c}`)}}return i}async closePorts(){for(let e of this.mappings)try{this.logger.debug({id:e.id,port:e.port},"Closing port via port-manager"),await A(this.socketPath,"DELETE",`/v1/ports/${e.id}`),this.logger.info({port:e.port,protocol:e.protocol},"Port closed successfully")}catch(t){let r=t instanceof Error?t.message:String(t);this.logger.warn({id:e.id,port:e.port,error:r},"Failed to close port (continuing cleanup)")}this.mappings=[]}getMappings(){return[...this.mappings]}async renewPorts(){for(let e of this.mappings)try{await A(this.socketPath,"POST",`/v1/ports/${e.id}/renew`,{}),this.logger.debug({id:e.id,port:e.port},"Port renewed")}catch(t){this.logger.warn({id:e.id,port:e.port,error:t instanceof Error?t.message:String(t)},"Failed to renew port")}}};var Z=h(require("net")),ee=h(require("dgram"));var L=class o{static{a(this,"ProbeListener")}port;protocol;logger;tcpServer;udpSocket;constructor(e,t,r){this.port=e,this.protocol=t,this.logger=r}static async start(e,t,r){let i=new o(e,t,r);return t==="tcp"?i.startTcp():i.startUdp()}startTcp(){return new Promise(e=>{let t=Z.createServer(r=>{r.destroy()});t.on("error",r=>{r.code==="EADDRINUSE"?(this.logger.info({port:this.port},"Port already in use - existing listener will handle reachability check"),e({status:"port_in_use"})):(this.logger.warn({port:this.port,error:r.message,code:r.code},"Failed to bind TCP probe listener"),e({status:"bind_failed",error:r}))}),t.listen(this.port,"0.0.0.0",()=>{this.tcpServer=t,this.logger.debug({port:this.port,protocol:"tcp"},"TCP probe listener started"),e({status:"started",listener:this})})})}startUdp(){return new Promise(e=>{let t=ee.createSocket("udp4");t.on("error",r=>{r.code==="EADDRINUSE"?(this.logger.info({port:this.port},"Port already in use - existing socket will handle reachability check"),e({status:"port_in_use"})):(this.logger.warn({port:this.port,error:r.message,code:r.code},"Failed to bind UDP probe socket"),e({status:"bind_failed",error:r}))}),t.on("message",(r,i)=>{t.send(r,0,r.length,i.port,i.address)}),t.bind(this.port,"0.0.0.0",()=>{this.udpSocket=t,this.logger.debug({port:this.port,protocol:"udp"},"UDP probe listener started"),e({status:"started",listener:this})})})}async stop(){if(this.tcpServer){try{await Promise.race([new Promise(t=>{this.tcpServer.close(()=>t())}),new Promise((t,r)=>setTimeout(()=>r(new Error("TCP server close timed out")),2e3))])}catch(t){this.logger.warn({port:this.port,error:t instanceof Error?t.message:String(t)},"TCP probe listener close did not complete cleanly")}this.logger.debug({port:this.port,protocol:"tcp"},"TCP probe listener stopped"),this.tcpServer=void 0}if(this.udpSocket){try{await Promise.race([new Promise(t=>{this.udpSocket.close(()=>t())}),new Promise((t,r)=>setTimeout(()=>r(new Error("UDP socket close timed out")),2e3))])}catch(t){this.logger.warn({port:this.port,error:t instanceof Error?t.message:String(t)},"UDP probe listener close did not complete cleanly")}this.logger.debug({port:this.port,protocol:"udp"},"UDP probe listener stopped"),this.udpSocket=void 0}}},M=class{static{a(this,"ProbeListenerManager")}probes=[];logger;constructor(e){this.logger=e}async startAll(e){for(let t of e){let r=await L.start(t.port,t.protocol,this.logger);switch(r.status){case"started":this.probes.push(r.listener);break;case"port_in_use":this.logger.info({port:t.port,protocol:t.protocol},"Port already in use, skipping probe");break;case"bind_failed":throw this.logger.error({port:t.port,protocol:t.protocol,error:r.error.message},"Probe listener bind failed, rolling back"),await this.stopAll(),r.error}}this.probes.length>0&&this.logger.info({count:this.probes.length,total:e.length},"Probe listeners started")}async stopAll(){let e=[...this.probes];this.probes=[];for(let t of e)try{await t.stop()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to stop probe listener (best effort)")}}};var T=(r=>(r.Normal="normal",r.Degraded="degraded",r.Unknown="unknown",r))(T||{});var te=require("child_process"),f=require("fs"),be=h(require("http")),Se=h(require("https")),j=h(require("path")),re=require("util");var Ee=(0,re.promisify)(te.exec);function _(){return{sleep:a(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:a(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,n,p=r;for(let c=1;c<=t;c++)try{return await o()}catch(l){if(n=l instanceof Error?l:new Error(String(l)),c===t)throw n;await new Promise(u=>setTimeout(u,Math.min(p,s))),p*=i}throw n},"retry"),execCommand:a(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:n}=await Ee(o,{timeout:t,cwd:r,env:{...process.env,...i}});return{stdout:s.trim(),stderr:n.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let p=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,c=s.stderr?.trim()||"",l=c?`${c}
|
|
2
2
|
|
|
3
|
-
${p}`:p,u=[];
|
|
3
|
+
${p}`:p,u=[];s.stdout?.trim()&&u.push(`STDOUT: ${s.stdout.trim()}`),l&&u.push(`STDERR: ${l}`);let he=[`Command execution timed out: ${o}`,...u].join(`
|
|
4
4
|
|
|
5
|
-
`),U=new Error(
|
|
6
|
-
`);for(let r of t){let i=r.trim();if(i&&!i.startsWith("#")){let[n,...s]=i.split("=");n&&s.length>0&&(e[n.trim()]=s.join("=").trim())}}return e}a(ee,"parseEnvironmentVariables");function D(o,e=process.env){return o.replace(/\$\{([^}]+)\}/g,(t,r)=>e[r]||t)}a(D,"expandEnvironmentVariables");var te=require("http");var N=class{constructor(e,t,r,i,n){this.getStatus=r;this.executeHealthCheck=i;this.getMetrics=n;this.port=e,this.logger=t.child({component:"web-server"})}static{a(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,te.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),i=this.getStatus(),n={healthy:r,status:i.status,uptime:i.uptime,message:i.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(n))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var A=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;riptideStartTime;heartbeatInterval;updateInterval;echoInterval;portRenewalInterval;lastEchoHash=null;isEchoExecuting=!1;webServer;portManager;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.riptideStartTime=Date.now(),this.logger=O({serviceName:t.service.name,level:process.env.RIPTIDE_LOG_LEVEL||r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.openPorts(),this.startPortRenewal(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startEcho(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),await this.cleanupAndExit(e.exitCode)):await this.cleanupAndExit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async openPorts(){let e=H(process.env);if(e.length===0){this.logger.debug("No NOMAD_PORT_* variables found, skipping port manager");return}let t=M();if(this.logger.info({ports:e.map(n=>`${n.protocol}:${n.port}`),socketPath:t},"Found ports to open via port-manager"),!T(t))throw new v(`Port manager socket not found at ${t}. Ports ${e.map(n=>`${n.protocol}:${n.port}`).join(", ")} cannot be opened. Ensure port-manager daemon is running and socket is bind-mounted.`);this.portManager=new w(t,this.logger);let r=process.env.NOMAD_JOB_NAME,i=process.env.NOMAD_ALLOC_ID;try{let n=await this.portManager.openPorts(e,r,i);this.logger.info({count:n.length,mappings:n.map(s=>({port:s.port,externalPort:s.externalPort,externalIp:s.externalIp,protocol:s.protocol,natMethod:s.natMethod}))},"All ports opened successfully via port-manager")}catch(n){throw new v(`Failed to open ports: ${n instanceof Error?n.message:String(n)}`)}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.debug(`Executing ${e} hook`);let r=this.createHookContext(),i=Date.now();try{let n=await t(r),s=Date.now()-i;return this.logger.debug(`${e} hook completed (${s}ms)`),n}catch(n){let s=Date.now()-i;throw this.logger.error({hookName:e,duration:s,error:n instanceof Error?n.message:String(n),stack:n instanceof Error?n.stack:void 0},`${e} hook threw an exception, will not continue`),n}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){y(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),await this.cleanupAndExit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),await this.cleanupAndExit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:_()}}async cleanupAndExit(e){if(this.isShuttingDown&&process.exit(e),this.isShuttingDown=!0,this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager during error cleanup")}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Failed to close ports during error cleanup (best effort)")}process.exit(e)}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),this.cleanupAndExit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),this.cleanupAndExit(1))})}setupSignalHandlers(){let e=a(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{if(this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.webServer&&await this.webServer.stop(),this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager")}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to close ports (continuing shutdown)")}await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(i){this.logger.warn({error:i instanceof Error?i.message:String(i)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}startPortRenewal(){if(!this.portManager)return;let e=25*60*1e3,t=parseInt(process.env.PORT_RENEWAL_INTERVAL_MS||"",10)||e;this.logger.info({interval:t},"Starting port renewal"),this.portRenewalInterval=setInterval(async()=>{if(this.portManager)try{await this.portManager.renewPorts()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal cycle failed")}},t)}startEcho(){if(!this.hooks.echo){this.logger.info("No echo hook defined, skipping echo");return}if(this.config.echo?.enabled===!1){this.logger.info("Echo disabled in config, skipping...");return}let e=this.config.echo?.interval||3e3;this.logger.info({interval:e},"Starting echo"),this.echoInterval=setInterval(async()=>{if(this.isEchoExecuting){this.logger.debug("Echo still executing, skipping this interval");return}this.isEchoExecuting=!0;try{let t=await this.executeHookSafely("echo");if(!t)return;let r=JSON.stringify(t),i=(0,re.createHash)("sha256").update(r).digest("hex");i!==this.lastEchoHash&&(this.lastEchoHash=i,await this.sendEcho(t))}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Echo failed")}finally{this.isEchoExecuting=!1}},e)}async sendEcho(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping echo send");return}let n={...e,condition:e.condition??"normal"},s={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),data:n};try{this.logger.info({payload:s,sonarUrl:t},"Sending echo to Sonar API");let p=await fetch(`${t}/api/v1/echo`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(s)});if(!p.ok){let c=await p.text();this.logger.warn({status:p.status,error:c,entity_id:i},"Sonar API echo failed")}}catch(p){this.logger.warn({error:p instanceof Error?p.message:String(p)},"Failed to send echo to Sonar API, dropping")}}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let n=Math.floor((Date.now()-this.riptideStartTime)/1e3),s={...e},p={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),riptide_uptime:n,data:s};try{this.logger.info({payload:p,sonarUrl:t,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(p)});if(!c.ok){let l=await c.text();this.logger.warn({status:c.status,error:l,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new N(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>{if(this.portManager){try{let r=await this.portManager.checkPortHealth();if(!r.healthy)return this.logger.warn({ports:r.ports},"Port health check failed - ports unreachable"),!1}catch(r){return this.logger.error({error:r instanceof Error?r.message:String(r)},"Failed to check port health"),!1}this.portManager.renewPorts().catch(r=>{this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal during health check failed")})}return await this.executeHookSafely("health")===!0},async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=a(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var oe=require("fs"),ie=m(require("path"));async function ne(o){try{let e=await oe.promises.readFile(o,"utf8"),t=JSON.parse(e),r=xe(t);return j(r),r}catch(e){throw new Error(`Failed to load config from ${o}: ${e instanceof Error?e.message:String(e)}`)}}a(ne,"loadConfig");async function se(o){try{let e=ie.resolve(o);delete require.cache[e];let t=require(e);if(typeof t.start!="function")throw new Error('Hooks module must export a "start" function');return t}catch(e){throw new Error(`Failed to load hooks from ${o}: ${e instanceof Error?e.message:String(e)}`)}}a(se,"loadHooks");function j(o){if(!o.service?.name)throw new Error("Config must have service.name");if(o.health,o.heartbeat&&o.heartbeat.interval&&o.heartbeat.interval<1e3)throw new Error("heartbeat.interval must be at least 1000ms");if(o.echo&&o.echo.interval&&o.echo.interval<1e3)throw new Error("echo.interval must be at least 1000ms")}a(j,"validateConfig");function xe(o,e=process.env){let t=JSON.parse(JSON.stringify(o));function r(i){if(typeof i=="string")return D(i,e);if(Array.isArray(i))return i.map(r);if(i&&typeof i=="object"){let n={};for(let[s,p]of Object.entries(i))n[s]=r(p);return n}return i}return a(r,"expandObject"),r(t)}a(xe,"expandConfigVariables");function ae(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},echo:{interval:3e3,enabled:!0},logging:{level:"info",format:"pretty"}}}a(ae,"getDefaultConfig");function ce(o,e){function t(i,n){if(n&&typeof n=="object"&&!Array.isArray(n))for(let s in n)n.hasOwnProperty(s)&&(i[s]&&typeof i[s]=="object"&&!Array.isArray(i[s])?i[s]=t(i[s],n[s]):i[s]=n[s]);return i}a(t,"deepMerge");let r=t({...o},e);return j(r),r}a(ce,"mergeConfigs");var g=m(require("fs/promises")),d=m(require("path"));var x=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:n}=e,s=d.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${s}`),await this.createDirectoryStructure(s),await this.createPackageJson(s,t,n),await this.createRiptideConfig(s,t,n),await this.createTsConfig(s),await this.createTsupConfig(s),await this.createHooks(s,i),await this.createDockerfile(s,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,s)}async createDirectoryStructure(e){await g.mkdir(d.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),n=i?"workspace:*":await this.getRiptideVersion(),s=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,p={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":s,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":n},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await g.writeFile(d.join(e,"package.json"),JSON.stringify(p,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await g.writeFile(d.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await g.writeFile(d.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await g.writeFile(d.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
|
|
5
|
+
`),U=new Error(he);throw U.name="CommandTimeoutError",U}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(o,e,t={})=>{let{mode:r="0644",encoding:i="utf8"}=t;await f.promises.mkdir(j.dirname(o),{recursive:!0}),await f.promises.writeFile(o,e,{encoding:i}),await f.promises.chmod(o,r)},"writeFile"),readFile:a(async o=>await f.promises.readFile(o,"utf8"),"readFile"),fileExists:a(async o=>{try{return await f.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:a(async function o(e,t){return new Promise((r,i)=>{let s=j.dirname(t);f.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,f.createWriteStream)(t);(e.startsWith("https:")?Se:be).get(e,l=>{if(l.statusCode===200)l.pipe(n),n.on("finish",()=>{n.close(),r(t)});else if(l.statusCode===301||l.statusCode===302){let u=l.headers.location;u?r(o(u,t)):i(new Error(`Redirect without location header: ${l.statusCode}`))}else i(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{f.promises.unlink(t).catch(()=>{}),i(l)}),n.on("error",l=>{f.promises.unlink(t).catch(()=>{}),i(l)})}).catch(i)})},"downloadFile")}}a(_,"createUtilityContext");function oe(o){return!o||o.length<=10?"[REDACTED]":`${o.slice(0,4)}...${o.slice(-4)}`}a(oe,"redactSecret");function ie(o){let e={},t=o.split(`
|
|
6
|
+
`);for(let r of t){let i=r.trim();if(i&&!i.startsWith("#")){let[s,...n]=i.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(ie,"parseEnvironmentVariables");function H(o,e=process.env){return o.replace(/\$\{([^}]+)\}/g,(t,r)=>e[r]||t)}a(H,"expandEnvironmentVariables");var se=require("http");var $=class{constructor(e,t,r,i,s){this.getStatus=r;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{a(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,se.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:r,status:i.status,uptime:i.uptime,message:i.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var D=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;riptideStartTime;heartbeatInterval;updateInterval;echoInterval;portRenewalInterval;lastEchoHash=null;isEchoExecuting=!1;webServer;portManager;probeListenerManager;externalIp;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.riptideStartTime=Date.now(),this.logger=R({serviceName:t.service.name,level:process.env.RIPTIDE_LOG_LEVEL||r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.openPorts(),this.startPortRenewal(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startEcho(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),await this.cleanupAndExit(e.exitCode)):await this.cleanupAndExit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async openPorts(){let e=I(process.env);if(e.length===0){this.logger.debug("No NOMAD_PORT_* variables found, skipping port manager");return}let t=X();if(this.logger.info({ports:e.map(s=>`${s.protocol}:${s.port}`),socketPath:t},"Found ports to open via port-manager"),!Q(t))throw new v(`Port manager socket not found at ${t}. Ports ${e.map(s=>`${s.protocol}:${s.port}`).join(", ")} cannot be opened. Ensure port-manager daemon is running and socket is bind-mounted.`);this.probeListenerManager=new M(this.logger);try{await this.probeListenerManager.startAll(e)}catch(s){throw new v(`Failed to start probe listeners: ${s instanceof Error?s.message:String(s)}`)}this.portManager=new O(t,this.logger);let r=process.env.NOMAD_JOB_NAME,i=process.env.NOMAD_ALLOC_ID;try{let s=await this.portManager.openPorts(e,r,i);this.logger.info({count:s.length,mappings:s.map(n=>({port:n.port,externalPort:n.externalPort,externalIp:n.externalIp,protocol:n.protocol,natMethod:n.natMethod}))},"All ports opened successfully via port-manager"),this.externalIp=s.find(n=>n.externalIp)?.externalIp??void 0}catch(s){throw await this.probeListenerManager.stopAll(),new v(`Failed to open ports: ${s instanceof Error?s.message:String(s)}`)}await this.probeListenerManager.stopAll(),this.logger.debug("Probe listeners released, ports ready for service")}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.debug(`Executing ${e} hook`);let r=this.createHookContext(),i=Date.now();try{let s=await t(r),n=Date.now()-i;return this.logger.debug(`${e} hook completed (${n}ms)`),s}catch(s){let n=Date.now()-i;throw this.logger.error({hookName:e,duration:n,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},`${e} hook threw an exception, will not continue`),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){y(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),await this.cleanupAndExit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),await this.cleanupAndExit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:_(),externalIp:this.externalIp}}async cleanupAndExit(e){if(this.isShuttingDown&&process.exit(e),this.isShuttingDown=!0,this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.probeListenerManager)try{await this.probeListenerManager.stopAll()}catch{}if(this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager during error cleanup")}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Failed to close ports during error cleanup (best effort)")}process.exit(e)}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),this.cleanupAndExit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),this.cleanupAndExit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),this.cleanupAndExit(1))})}setupSignalHandlers(){let e=a(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{if(this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.webServer&&await this.webServer.stop(),this.probeListenerManager)try{await this.probeListenerManager.stopAll()}catch{}if(this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager")}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to close ports (continuing shutdown)")}await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(i){this.logger.warn({error:i instanceof Error?i.message:String(i)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}startPortRenewal(){if(!this.portManager)return;let e=25*60*1e3,t=parseInt(process.env.PORT_RENEWAL_INTERVAL_MS||"",10)||e;this.logger.info({interval:t},"Starting port renewal"),this.portRenewalInterval=setInterval(async()=>{if(this.portManager)try{await this.portManager.renewPorts()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal cycle failed")}},t)}startEcho(){if(!this.hooks.echo){this.logger.info("No echo hook defined, skipping echo");return}if(this.config.echo?.enabled===!1){this.logger.info("Echo disabled in config, skipping...");return}let e=this.config.echo?.interval||3e3;this.logger.info({interval:e},"Starting echo"),this.echoInterval=setInterval(async()=>{if(this.isEchoExecuting){this.logger.debug("Echo still executing, skipping this interval");return}this.isEchoExecuting=!0;try{let t=await this.executeHookSafely("echo");if(!t)return;let r=JSON.stringify(t),i=(0,ne.createHash)("sha256").update(r).digest("hex");i!==this.lastEchoHash&&(this.lastEchoHash=i,await this.sendEcho(t))}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Echo failed")}finally{this.isEchoExecuting=!1}},e)}async sendEcho(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping echo send");return}let s={...e,condition:e.condition??"normal"},n={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),data:s};try{this.logger.info({payload:n,sonarUrl:t},"Sending echo to Sonar API");let p=await fetch(`${t}/api/v1/echo`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(n)});if(!p.ok){let c=await p.text();this.logger.warn({status:p.status,error:c,entity_id:i},"Sonar API echo failed")}}catch(p){this.logger.warn({error:p instanceof Error?p.message:String(p)},"Failed to send echo to Sonar API, dropping")}}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor((Date.now()-this.riptideStartTime)/1e3),n={...e},p={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),riptide_uptime:s,data:n};try{this.logger.info({payload:p,sonarUrl:t,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(p)});if(!c.ok){let l=await c.text();this.logger.warn({status:c.status,error:l,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new $(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>(this.portManager&&this.portManager.renewPorts().catch(r=>{this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal during health check failed")}),await this.executeHookSafely("health")===!0),async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=a(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var ae=require("fs"),ce=h(require("path"));async function pe(o){try{let e=await ae.promises.readFile(o,"utf8"),t=JSON.parse(e),r=Pe(t);return N(r),r}catch(e){throw new Error(`Failed to load config from ${o}: ${e instanceof Error?e.message:String(e)}`)}}a(pe,"loadConfig");async function le(o){try{let e=ce.resolve(o);delete require.cache[e];let t=require(e);if(typeof t.start!="function")throw new Error('Hooks module must export a "start" function');return t}catch(e){throw new Error(`Failed to load hooks from ${o}: ${e instanceof Error?e.message:String(e)}`)}}a(le,"loadHooks");function N(o){if(!o.service?.name)throw new Error("Config must have service.name");if(o.health,o.heartbeat&&o.heartbeat.interval&&o.heartbeat.interval<1e3)throw new Error("heartbeat.interval must be at least 1000ms");if(o.echo&&o.echo.interval&&o.echo.interval<1e3)throw new Error("echo.interval must be at least 1000ms")}a(N,"validateConfig");function Pe(o,e=process.env){let t=JSON.parse(JSON.stringify(o));function r(i){if(typeof i=="string")return H(i,e);if(Array.isArray(i))return i.map(r);if(i&&typeof i=="object"){let s={};for(let[n,p]of Object.entries(i))s[n]=r(p);return s}return i}return a(r,"expandObject"),r(t)}a(Pe,"expandConfigVariables");function de(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},echo:{interval:3e3,enabled:!0},logging:{level:"info",format:"pretty"}}}a(de,"getDefaultConfig");function ge(o,e){function t(i,s){if(s&&typeof s=="object"&&!Array.isArray(s))for(let n in s)s.hasOwnProperty(n)&&(i[n]&&typeof i[n]=="object"&&!Array.isArray(i[n])?i[n]=t(i[n],s[n]):i[n]=s[n]);return i}a(t,"deepMerge");let r=t({...o},e);return N(r),r}a(ge,"mergeConfigs");var g=h(require("fs/promises")),d=h(require("path"));var x=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:s}=e,n=d.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${n}`),await this.createDirectoryStructure(n),await this.createPackageJson(n,t,s),await this.createRiptideConfig(n,t,s),await this.createTsConfig(n),await this.createTsupConfig(n),await this.createHooks(n,i),await this.createDockerfile(n,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,n)}async createDirectoryStructure(e){await g.mkdir(d.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),n=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,p={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":n,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await g.writeFile(d.join(e,"package.json"),JSON.stringify(p,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await g.writeFile(d.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await g.writeFile(d.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await g.writeFile(d.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
9
|
entry: ['src/hooks.ts'],
|
|
@@ -396,4 +396,4 @@ Next steps:
|
|
|
396
396
|
|
|
397
397
|
NOTE: Riptide is a node application. If your base image does
|
|
398
398
|
not have node installed (v22+), install it in the Dockerfile.
|
|
399
|
-
`)}async checkIfInWorkspace(e){let t=d.resolve(e),r=d.parse(t).root;for(;t!==r;){try{return await g.access(d.join(t,"pnpm-workspace.yaml")),!0}catch{}t=d.dirname(t)}return!1}async getRiptideVersion(){try{let e=[d.join(__dirname,"..","package.json"),d.join(__dirname,"..","..","package.json"),d.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),d.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await g.readFile(t,"utf-8"),i=JSON.parse(r);if(i.name==="@deeep-network/riptide")return`^${i.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function
|
|
399
|
+
`)}async checkIfInWorkspace(e){let t=d.resolve(e),r=d.parse(t).root;for(;t!==r;){try{return await g.access(d.join(t,"pnpm-workspace.yaml")),!0}catch{}t=d.dirname(t)}return!1}async getRiptideVersion(){try{let e=[d.join(__dirname,"..","package.json"),d.join(__dirname,"..","..","package.json"),d.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),d.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await g.readFile(t,"utf-8"),i=JSON.parse(r);if(i.name==="@deeep-network/riptide")return`^${i.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function ue(o,e,t={}){await new x(o).scaffold({serviceName:e,...t})}a(ue,"initService");0&&(module.exports={AlreadyRunningError,DiagnoseRequiredError,InvalidSecretError,MissingSecretError,PortOpenError,RiptideEntrypoint,RiptideError,ServiceScaffolder,UserConfigNeededError,WorkloadCondition,createChildLogger,createLogger,createUtilityContext,expandEnvironmentVariables,getDefaultConfig,initService,isAlreadyRunningError,isDiagnoseRequiredError,isInvalidSecretError,isMissingSecretError,isPortOpenError,isRiptideError,isUserConfigNeededError,loadConfig,loadHooks,mergeConfigs,parseEnvironmentVariables,parseNomadPorts,redactSecret,validateConfig});
|